From 63aa366992c5c63abf83a983806220d8004ed2c8 Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Sat, 27 Jul 2024 11:06:10 +0200 Subject: [PATCH 01/91] Improve server video tutorial (#7027) --- doc/_static/images/global-task-runner.png | Bin 0 -> 67846 bytes doc/_static/images/serverside-video.png | Bin 0 -> 436067 bytes doc/_static/images/session-task-runner.png | Bin 0 -> 47645 bytes doc/how_to/concurrency/manual_threading.md | 18 ++++++++++++++-- .../intermediate/build_server_video_stream.md | 20 ++++++++++++++---- 5 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 doc/_static/images/global-task-runner.png create mode 100644 doc/_static/images/serverside-video.png create mode 100644 doc/_static/images/session-task-runner.png diff --git a/doc/_static/images/global-task-runner.png b/doc/_static/images/global-task-runner.png new file mode 100644 index 0000000000000000000000000000000000000000..ee7ddd185ac2b848ffb2eadb235c3d1385a5bed7 GIT binary patch literal 67846 zcmV)DK*7I>P)v>)X0058pzRwsVgotI+vMk%S9mlbM$N9(V^?I#V zYcv|oW^*(eO(v7&a=Biwx7)2O%ZJ0^_~~>yU9VR@DvF}2supvN(H}{rQenihtYk9T z>2!i1D3{BvR!jSt3IOnKE|-f~6-6p`A_*2TBM8a>0Dq1|48t%@lQCvRUNbIpypSYm zzu%wFXI0l8{ZuQ06@&cycn-N&l5|kZQHzf!S3>EwUQ)Bgy!9Dr>ZI; zM1=b%;xeC*$z%v2A}jO1)oQid?eblvQmNf;hcN*FzlM75;m7c{m(Sr_;q^F`v(QAc%&Al=_x%K84DTxg06fj}JjR%&Y2s69k(FO3F3)B&J|2(X z*twt8)PX1r;C-OJPZwR3QYiEdT!|nEg0ChDUfl?;R5JddcXP3o%;!DLY05!IV#jzZ zx!;e2gg9m<6PtbwNvEsT>Q!7IE<5LLZ*LzTAA7ytj)JhL@I!h|a&j?3}yWxICFmoO2SCe`iXJ z%L;%#0K;D>L{vQhZgxaObT;Rl{+FS>=lnMR(9_e?!^1Bg5;;L{x2z%S1#aNRmV{=+DOG=jUfh%*|%A-EQy2W#?S!aar*;3?iyL#$_U+ z63plGU|jxn0h=T4@9*#K?rODKy@MiHMBL#AUU)xZ=1hV_Pg1 z%jJ@BnTY5_$T2Px5mhrTce`DU%d(IpiN@vidVLs|Q;Ex|&x&!Gi0BiH%g1F%o!s)u zPtK4!vZgA#SO8Mz?F=Hi3a^VFI8fLdfQX$!Rvs}sS=_K{U#6q4eG%bw2cVLA{dv=z#66dnvmJ52=HrfbdKLIuEH-_BpAeE%goElAS~N z@X;~j555gEAsNQ#DF0zx{_`=m?fcem)33NJFTt-agq{NO%9jcP2D}t>6H?}T`Pg#158ZWa~AvAM;gc`Nk6MrR%HXNhbn$aF$#fTVuZ4fK`OR0#LI| z>UbzPCXuLD^fyR=B}ggBp0U_0AQDMPx)WG{6~&`cwCTr51#G1SWE3gPC=2o){El4q zSk&+!Jef--)5D%MIq0+ zD|>)oY6&7UZI2Y0C~>bxp(iW})msH`NSO0U$O0;uCm!4ITqgP+S;fst;qv)FsM!T# zst!~|?3$XI7Dm#k>2gRADFs4JJo_yqbT&kgg%X#y5rtTMZn+{DF=>u$C4k)ssUSfj z==+DVlM-s8@|ziY6yz#oO(coeW-kZmy8mryX=!6)BZN@gN84w*yu7^JH)P&}oe8>SfM#M@2+c#!}*!(5>Y|e^)7^n8En6*^huy6l}!}t35fII=sP>6 z3;iIO?03rA>@ivUsxw$}@-}UEQ(>=@^PJ6722}5ADe^48_kG8*b**F*qlJb;3y~XN% znHnZx>?F#3vYIO@oW);=IwD&lv6|1652D#XWz8dmDn)@+!Odh$%ATmG1S`!^au%ZY znS2bilP(0!{sG2@Zz{-8a4N$kRMzV%5f(@{7J;|`HZ-U0XSbNv z62(LUD7|a9pJBHlQYgE@5L?W%`Y2LbCkUAVS6jaJLn0RPJ1XhUT)lEEHOdU*&PlXO z4uJK4w09|8?!^izASce?hr&Sw*j3V?qN+$j29*>MeGy}LBej%>s3MWtBad2p(dA64 zDGB$P6YiwT%p@3v6AELtWbt{Z9B{IicVFY^3d1(QGBH~kFe^vy=|}`BScp_R3M(Dk zMVA>JyS33pPnX%zWk-drCPdQb!A@Mr?!UFXS{`9$Wo%;9DpuS8~=juHhJcD8@dAgh z9P15JexiJg_MR;R9g5kG7v*#vtje_vG21|8#n! z)*c;i)}|Y^BZnJDZ>$wY`?M%{>sP<{VeQDtPn>=#I!yAXLS;2NgQzR3Z+`CMkGyy4 z)Q^7qr`RtESGeGJ_x!_ErPavhuYdORWc@u~c=8*!f+X5iC5XTL$@h=cA86D^n(fKa zT79He8)?*QjoL)L)^4_+`OP2Ks}o}>3ZiRs_3Gl^4%Z%T9R2jIu5?r_BVQ8P=&ZbP=9wS2 zTlLR>?K?MfIHQo(0YQ#y=L%)koi7Yfp?!9{=R& zFW-(*^y%!Dq}wl^`+ci1+G5=J&9(&@e z??CxNh4tC^ueoz)n#~6r?Wxw-@%E8N=dS(dt}zQnvdBA2*B9p|>K~pt{`gW?)411N!U2wg=;sXNC$+o2fd3G zcS4`BES`CKF0AP!)Ac;qf2P^ATxFenmpoy!8j}*`p5%%RlX*h@850+>xbQ}i#0F^z zx9S)(-1+zAm(D+b>B@DpT;8V+ch@ESm%Z-{v!Ynn{ddpV=eXTSny^CWxN@4D^AHrt zFvEZW6-h(Rpn?g3A?VgEA~}d6k`3gXha3imVRBwMhgr9}dMys_bQBg-a*TT{?IE!j<#quRjsDKqkZtjH)N( zJaOf6CqNky+RgKn8vyUZ?*>!;`Ac`(oFQLiViZreg1^q3yL|R+rQ7$!;za|lL7V79 zoxO1D+TA)B(S=PCg5lyWuXyg6vsb~nf&USa_#hZ}h*STmi+8TwZHNH(h?fT*$_~%H zdiLyfSTFdA^5>#+?{zjo&zgH|7d=Rj*w=E1plx9XTzEs6>|=zHKNGaz`k=kYID|HG{Ge zdTDds+j`h9eAf?-{90ZV4RJ()?32OogV<$b#RMhxpM6G;tA||&1b;*~UZm$XtyyFs zyV1CeVptr6e2@p8a?_%ku81#4U<1&9G^09FxC%6AW^$3T9K!NXirOEp4QGp$De?PZH+f_ zlMQjwL|pswBS+W@nrlA&5SGomJP5XsF((45MisMJwf$ zy6>2&%|5ZYUEh2<-#|Q1khF}_Vx&S&Y2~_t9lux(ZNWr-TN|TH}&QNsX zkqve;;o@KdcGg|*pO#7>Sr|9qp*`Fg8DYLh*Nz)lP%SRQRXD1ML-_pBsKr?lCcs5Ls%__FgT&@jwStY@~%(p0?**` zzghmIaM|bbc6o#lFPP&!g0~s6Z(g-nPsDN*NzzK3)*-CX0)a^|Vk^z-Km7>fO+`cB z2uAqED5nrL*iqX84_cZ>6z25C6)QG;7mC8;r=w-R0hgoahXjM<*|d5Q7?ndtzhxFd zUqE!!MS$twxMrcAdLE}F0*yms1cl=ig;A^=16Ida@zuBPe?#T@D`AI#blNQSm3>ll zYKlH~?xyqUpf{`(Sg<+cMkRA{HH)z=LQ!`Dl0=a6{@*|u z{{H{V4uZ%3H)0O|vkY3S?bjsq(W3ubBt{8=m{2a4hXozzD{%f{hW+kMq!F06$3|)wFDr+AA-Dwzw^TJeXNqC zR2;|iEU!6tBLBPQp_SWCMb)3_2GF~a;^pj`5=ZW&QkW&Lt+c9OrYib3}l8jcL za;);cJ;K$3F57l=-$sVxrI>;xl{#JOm4}^X_((AF+}>1mg&|0SP?5Cu<>9Y6AifTX z{$9v>c;7b^uZ+hP6s6JV)2>vvSRyWqHyr3_cyI*=(1GSLMmuo$Yu>OJ5fm|N`_X-y z8BQg|R4l31Y16Mfv~-G$FZAw&07Jl2b>*g#%j8t~PKc5HVi?ii+1z+9BP*TelmyL= z81;$^QvD!E{>$ENJV(nh0;gz_q&b{oFojfRRA>u2Z9sAA6R4BD+f_7=A_^&@P#7{V z-)nM#D?d}&{p<3_!)3qA19{VquCa;Xyk* zEG}UvLP?<*CYLC6 z5~ZGId7fqezHY5hi#GrjZ4u|r9l@GGVKfR-Eg0p8Q7Z?iIs}e)+M90o(Q6fQdiTj| z?V_m)Ml?y#xLX!Y9YhHKxYH~5-p+*D;j}>PYwrmGz}r>5{zRo@az1JpilVRFIwP` zXak}`F*lGoL`^d^94$u=Z6t|aD9#{QE~6P#>kL~Xfivg<1zq- zk0T1Qj)jv(^Qe64f;HeK9ZfZv3F=;|jEZl5?h3igkFFFaYJdq{vvF5v2=uh}SyLv& z5vj#PC;5V&E>C-(v@`~1zu37O)OEe`Bt=Mia0$Ck-+~Y3-1Ny@9olnnX_*sRw>i_N z$Q3DX&H6x4c6r)Iq-eGAac@pvY!e#D);BY)JDV|L_Hv*(ODA zI^okDKlwt|j;0&wdXK2Pd%&hSHp^8Cd zHG}?NHz{}oZ}cv@qM9~`x$aJ0avCYmRw@U#m?1#yisq|$-2yGLcLR<0!g;=^Y-*D~ z5@W2na#&Brp{!QInP`>;7__Lgyfq?$bFcqoo(AoKvb3D%6s(+Mi5Mo~yJOcp!i~93 z9oVGAyT>4?jM3mI!O-e{BPLiQv%lZjaJ49v#u)=*!pbU*0`VSx5CUzQd&}QzNm5pF(8I$_imV<6# z$W?u{dMbp=GXdrc2!PBU4w{#JJda`J7>N_SX2_WF7Jqc8+v|p;XyQrJ$LsbT_~{n~ zLlOw7(WGCx)#8W@HV+uXr}liS;uR#tF*pres*3fyECO%@#j#_>R4ww?g8zZbT~EPf z0B>7CoLYzn1zfYokKst zy~GasxL_J#GX~s)GV^#sJ8Z_Xws5%ZadlC$9#NTgp19)df&ksw);~Qlo;6H-Zxsx@ zc;-$Tt)b&4Pn&k});&Nxq9lp1+}}5B@kQXZbl-bv=Tvb!JlEfL7U$Nm^ z2;yN0*s89cVQ7Y+R4l0-G4xf3N3=wqriQy&2?;#P@7sMmD`$v|G;#WjHmfrlQ4;OE zYs(jl4BX!sE?!1(ax9Nk53YmkvipCM`s|CQjZ+Y@1Qjb)usN?zZ-&?RwBFA(0$Zcp z`r|DRWTe(Do!gs98oOW>IP1j09a@sqs!W$2G{SkP?eu{i2${hdUUE9xs_&fR5RT*0 zj@)_V6wYm&JE=5Yl`>}5Qa^aVuI3@xCJip#f9!OpFqH_}W-$mD1Vl&e?UyvXUV?vj z#tL( z5pw78YZgd>uU+v`H!?AA%v49vYi_+?oT^Z>==aAj2i;6|RJRGO3vck2B@ zuV_B8dy9<7Ks5t999ursNNKE3Cz`l zQ#J9damBVn{s462p|;Z%MX_D*Q)qa+PkQK6ku>+#Ra4c(UqK|NHYmc@r{xR24Ha{= z_;V;JMZxVVD2zm7DPE?~qm)8U@}F(_t}OuTiZTM;e^NFFJYv0`5KL=`D4<)NEtN$^ zg^FNyAG>ZAYYTYb%mKyAKJ$Cyha2!7aTJ9Zk}y`u&>TZEJjrTtB~KeGwjJn<+-g8n zvHBvEupoSK!wCysN({CE4Q|BQ5TD%x{~v9MfYa*>MI;PfVdawOQ^26?lk}8-UT7l9 z`vgSx#0SO0PG59(h-gb7S?=#VdvKeMj%862IP(hSux1OqhX}X^_w07n-z-YuX(}F3 zsB_C^G>C7)-SN)bW0a_D?96xDyTa{F4>M9tJybdGu0IHln;Wb8Br2Hr9)I7k(Eo#Cu zS-YjH>iRXBAmY@SyH4M52Y}+Pb!fU$he^iG{Sdq(pS`mn3y4B7K)N0+PS z_`a`sB$mNc@zP9H;>gF2kQK(m13ufF@>h6DGIrW58)&QgdufT(gk zc5d(HBr={M83d(hNi2z=y}7idn-005(sWzJG%fmEamkwv0NHhk>~awNM3n|&nur(J z@g`yEa8o{#d)1|yDhU8d;AXJEyL$O$8(#{hX1uNv~9GH@;M>GVkjg!*^ zhT#N58T-_l1%TpezFBBggYWM?b{t%7#_ahNqeXPtXMo5xWI0lS6PneV4!Q+4xq0#U!2||?EX^wP9IfCGIf+y8x||O;?R5!e)P7?3 zRxK()P!`2iyd2X|3UzYdN1YxpsyiF6WvT@CnI;oZHmx^ldVR`*Ct|>0#EyEtS~JhU z{q?Iami+wl&vJoX9@1bJ48149XSe_K-8zI)4lEhH^n=MPA5%~^u??}!3NW!Ss%9jWL z+T2l`k5-8%8=?g93@ywlt zX<0oKIqz@z*$wA3;kg4YF#iR8&g1*GD`^Zh=AC(H@d`2{TS-P@5=p)`ZLw82x3wT6 zwYwr`@s?u{6Z_Y$m~A4T+rDnafgcW`SP`KZ(%=od1!||Q{`NUW##3z8$tp9%AFZbk zY?ZL-nS-a9L1zp^6K~utrw)9BQY@-VyHMROcnMveV8{Ve&BJgKbcBPTt=^IFnev1| zKnHZ{&5{A#W?)D0b z&qo_q&y%p3IU^Q`*$4rz1;nq{u3VtRdMI#|MA%`&$2lUzia+eE8=9_*r*z9V>^X4o zpiar*I8Uq7&)#oy!#)YcspgaWzfq7V$FKy>5S*!E{Q+krbmrQ;W~zdEK5umSqfi7B z{#vWx1+0pI>3sGX6U$f*DVedbQIvv)!Dww-_xU>ngP;sU(iF-lzCC``9*!bcTl34= zi5!hlJdY~1%B1XOv#2HMu(w{%H>ni_zx%{(0r+~_=as)9p_1980Sz5yFoP^9AE_dG zyfS|gsLK@$3##dm9oUdTNy&O#TQa?(NnnH?7a7&@jPBbL*CPb4tL3Gfq&Q9LtM690 z1TNV=f6`bZgHD|`x$@=>5~V0oC1H)LHi2LfEZ(L?GvAa@nsGDUyIy%w#mX@p!&uFV zuXZ}04CkP&{^|*(G*(Hm6l>@=5(_bpvv8`QSho2&b3 z6gq_2a_G7pPUv5LJg+;MkX2G{^?TYH?-wQN6f)$XNJU#)`(&haqfDP{+ZPHu9nF<5 z>B%ID{`Tng4mewVvHZQ~ndG$6X@I9|X{^mlNuV+8!142fzh|i#ke%3*PFuF=7hzIu zzTaOB>gBf_x@m_k|K-OEx|4|+CDS{C0Pfnid^X33|A&f1D8eAa3R&au8dumZVC5EP z+x^UB1Ih6_kDi6JNZZ$|7k6h9%H}Np^}2dKA>KH7I)!QTM^Aq&D9t@5fB9a`k!p?Z z+|4TZ?wXG6-;7b|Kq_K$hM$$+QT{;fGC-fzH~Q#Q@qFgClMlr46BPxxBVyZN)Nc;f z07qZSCJXFxNYLqQ`($B)vMkuIw{viW&Ur=p2@;#c>)C5BpDC z0~hIXx%y`3OAr&K&8xSVgJFCBbX5`=ck0M7m?-6%H*;<`GCdOn0ih|}vT~XR`|rLb zuQ!JyV>`N7ZnOkMY;S$%%|SXs^47dnEwDqH8}IcsDN4^7y6lSXm!Zm3$f=S*b)Sc_#VNmre#QLTkfqKw#4n>F4XV2?NY#9*~3TlyDQLTwl!bx zlVDWKd0w3b5HN@bSp#s{il@|dppNxle)RCx5zq!l4(>(KY)(6@-Vt_0n%ZuCd?hDY zEmfwRzhArlAM-Ta^VvgRZxKMN{ZZvH6j$Tg!ZTH7p>}`6#eJJ7JV9X`*yI3{7%Y)I zWvjFHe2z(h(z&mVUkboz&7I2(qTp4T$L`eFg_qbc_w7=Y$)NOoEVlYB@<(*L@h;g7+o-GZUg8cA_XI|DS@{4rcsiT@7}(CW&Qm#VPC`1gZl}*kW>zPXbU@p z3%Ax>J5C{bTGj8=0}E`K)>Hep#K}#Beqf6iyq91=%rEJ@|8lmeH*Fd|ZK+wz(}@Cy zpa5b7A)vSZQeLvU7ne3_&MIg4nHb~uls_e0cDr02mow;WzklYtmon7xa#iuLsZGvk zasr^@Jb-n!_bKSNcEh*PEUwy3tLDYC=~*M^0pY$7EOfOFE=W+(u{0LPVwi?elX9g> zn|bDT4YWM9Lo;;oLUKYbMsgfU(ilb1x_DLYmY*((X8Pp*?M5<=MWrN)69^$el)d=8 zk2m}TT}{uXHPd+N`JB<^)i4R*ePE+6zn(w6V9yl1AvkVjYCl!WmT~#DWg@4E~~>1DlywyZ|0jcDuO$BvJwPK z7tStMP~h3}PTv)Ihc#m1scx&VrrU-(2bl{A;c={tNZHM z7~1&VxoWR?-IQ&fKBk8-m}Am#i>Y)K)_Qn7^zew71VbMVi0TX z?HrX#Bc}*VAtMX`F26D90|&GkPwAq)8<)*a7M%HI2lpMo&}?2kxX~(RrNJ)+S}z^j zgJJBjk*_RYx`5`0%;8fYTsC*yJ*AQ=I5y|h!w$${)t}zC4nV-%S7y3|PUH!A{Xq~q z^?C%?#s7Hg>i2OfkJh|sw>MYaJ&T|kT#Ag2$WJER?Wm`NJo>e3t5aXVoOb z;_CgsoYAG`p@foBn$A9~w+b!o2437ylhGih#?4yZCa@K){c;liMkRi@`Nxnb>~y=H zq^6$V|E&U*#cH#UR<{KNcII(On$jSNdwb>wcHs@Q_sL0lo=aP@?T9Nf5d>iXMBoHZ ze*d)wm&$a7r>g95{ie2ICWAprkDIjuTnqf!FDDVy`~H^09-#|*)~=WdXpv1{EZ@8P zM-(r_l>ermIc#FNh$`TF^pmXZID+stwl&4Zr-?jp_IGehyR zfP}7{RMV(Ll~sAaGi(JiQ@w=NzdCQZU0htoZ+(2{I!9<3CE(BM?P9+dTy|gG-3OeU zAW7W|bY51;tWKzj+qiOyhIp>H^o<4x-hVUo8Kz~}KAtp6hsBSXxwJ(T68ON-6_@}J ztUq*cmx86@2<|z3Udf_m-~pTNp4X!>N(^JPsdWHK`oj*Gy8vTZZLZdvMFzb}PRIzP zJ4N?YWqx<+o<9m-f7~}{|G72f9lqz+xyk_jo`EHnl9DIDOkBRHa z8MmIk5D<8Xpr zwRI18gfBtBlzuLM#JGB|z-rXoxFExHsHO<44&eaT^4DM7zXg@*80~<%{{h^B4K-I+ z<|k`NoIzE6uT%*uXHAu(wfF8GrdUM6s0v0; z5x4|I~s>&rh;>0NWF@oScV&1zN|)s>0+!@(6BWE7L7);X~mE#a|p7_jQ}naay_N& z2V4s?l%||L^erY&;`DTJ56X`~Sp zmFNqO-M4t2rTYDz@+XDM-~=xi%Z^Y}Wub|ckSW`Lz8CdoU13+7Ks@`rVh^@PXpzP( zE9b}ZNg1Q2wL@2U^NNLuR4k`8RX%7IzUOqmn3BTE$-nW)$C%>Dbf!9);pSPBdKbW>!$ zwqPls4If?mnZ;x(b;8A)4Zy;;Ha4WECIeVs@ztiTu*KGTtI()1VED14KQI4ubv(uq z>eO@ho19{Lx4-k<2}5=0^Ek}`#sno;hGr>(lE^R#cv3>Q?U$BB z^NDM?nd^oDxq>o%$byxkY~Vu>pqKWH*Cp>ff79Isf=yM0$t;P+=9f-v7e;UE!ighf zj5bb{`Qzz}!I0C@csVqeOJODB7LW_$DfqU0P(rdjj#H-eRNHxJ5BMb&95Htqo@ zTO01>>$Frn!5H%%fGO7rUM92yzsed6Zdw1?gBvHp-rA#kb`nS?ryf#ggY-O<5Ibuw z<|nX}CjIKYhIJJSl|;{+;S=j2ZQFSB7)xphRqlxgt!`+$F8r_w!P7APpa!?!Ev|az z^@-gnP%&gi75(q?eEWV}>%S%m^ z48@r3r|&jJ5__!S`YZj_atb97jAv1tV$}I#7d(RJLW>Czg0K0#NhL-K!4T%#llNNT z$3i;L!|N|;Scasf6pND>#;Xd(&Z`MW7L59O?drO_=fI44eE)X@mcwX=0UPcXj&#=C zJWb*%LX~soL8}Wg#Ao(yLeNY?J+i?H!sAgc-}-o9PGUT5d~NOu%dd@gAfoU0IvTEJ zm{hS$)+^IL^+ngoezsrzzbJn!yBrXLE#aEm1xiN7CN5pSKe}vh)57w>3==COq%uqf zqyp)qXSN7EscqBpMN&E^cLb2;xb5cZ0Xm&Np8fupTeb+`^Em1+XC<<|=;VDzZiX%G z!;%d#T*~`fe)5Ppw!uz&V^x|ynWPMR&Rz!K;`+6#N(|;0QmmF@2~5?U%1;|Mvt1Yp zt_>^8HN<}xmb?LtBkWkevHcE!%hr!4m+G)MAbxF$h%tOe_wUiNB#BE&LZ+l~lvVzR zCVSxAkDoyOB%&t9P(|en5x14E5GE;_OQS@ESOyK0-HQ=>L*^<_yAb0 zUp*s^Nhuya2?Vw_ubE3y3~**pwO&TzS_WZdaygR3Y4V*OTW9Noe3MB@D0lvR!6NGF zqRUP5%kXmAxaY*ZX!y^4W&idB zQW}e~v9yY15QZe>Og^nGaysj8Uq3)#y`{8j^YMEfVNlvn@81-MrBVrl+ub(5xB2B% zlQK^J#fBfO0!()l=jQZg@<&XrfESOX*29!-ki%NjCpY~CDs{~4iU2s|>i`}LFOmIy z{cdXjFf-=j%v=fAw`|I%fyk(VD=K?I*8_^omZI$3o=j%hoMiw|go3RvXP9`2^s^1S z%pqXUTJv)~f9RA0bvsHL+Jn7pL)h>~z@V73W`YKPzXbSwtqqMevX}Mj< zhHl!ja$c+=A#>E^PQQ>NU6Ktv&-jXU-wS3Lh5;Kc9Mhsw%v5mkAsA(?FDGlHii8o< zKC%TwUZK7A>QRo-Vyd(Yk7{9cKG20jfxx}nceI3@M9FS!ZhG0g&af~nH?E$d0)e{mCO0MDG!^K5T$1o5RG02qnIM-(EE#V`#RAHvhGEDncQ1-My1a zmcU7+&Q#Fe5r8YW2LNs}3zsxIT5j|)XcADAMS5c-DpMwH|K+j^lGZ}iA=|qX1{;v) zaE!ni4af5&NyrceVN5-^5&B;c={{__gWHo^VK@7;65pe0p~#xv*?% z(BE|W;07hGV%e-3t8ff}yt4;(_Qn%ldS$lT4>Ca>_DxFUF~!pLd)p(Kg@Iphmt{UqG9HAD$y0$;Qd6J@Zm^$y| zgI1w-f8*8tJ6JM@(GPoM^Fz}ou6gCPKfYIJP;dk(ODI1605}33z%OU(5_wYp`mFcC z9CZ8U2@Z`#Wtkk?$K|rzzIO^|5hZxmae6|6$f?|t@}@{X@`SzSAxSzRQXybwFgz5a=Bxzg+Tq200hqEC zu06efGl^xg+F`Xo_a_`_2XL9db-1$N%!78JZ3DFz4{m4hJXSNR0n#E6nRp+c-ERT` z1bxo&YA1Y(=q46H(2PDoYF&4)ILpYebi6A2+(RIGdRBf%`9sAR0l?l3PBRjU9rC5w1;z*XsEScOS#P3@+te79qq~?q$w{}o-`v;I<6A_L*_?Xaa|Xli0_E`7}#{kpEd&GPWKLlHc4V6%P2$4<`-NU@>E6E`wSn zZ_UlKB%_EWwWKNY=KY6ZYjgjcv~If0!3$TwtYa6n9rk`XMVy=_<0UlB#j?q}jsmaI z>;iCEr%}sjgw_okJCR})GJ@N2w9*j)=WXwo4_6UA$ISb%Uf>~I3ucT}3Crfvt9Kr2 z6xDbkN1(Q{FhTJGXWV@Ht_P+=gCO0&jHJIQ3x>@3(i;*S$PXuuQlfFM&R+$tf#b!Mt#mhAE!5lLx+IRSF4-jvn>$j&FAKVsna0%Kd@1h6g9K zlnlpt5bl5mz2)N1+Ymg3(e}4HI;-zr($X5GBK>?#2h`Ga9^18%#8YWi-!?n6NJ03R zqTvhF9Y3%WLDMN^kvm|%efy-E(eQlw`RaCfuI1==+b~%k!w>DWfjslTuU6ppHXh&m z9ZF;q$|9$?^ZvclYDUQ^QqI=2nBnst`F=f$C6MZVK!5FxoMa7$9-rI0g+ww* zSd?BggRrBV<>|E3Rc#jdL_hzqt`|t98uJc#sZfd00Vsz7$*G)FRk7|zYlN{5Mbs>z z4olOm+!QT|lZY~H@2NX35tBr*^0RG9{>}0ygv-JSZV+s7hU;$@8+o}Rb@AFC{30{w zuUq)mK!!w6ikAtiOz4V6Ot*T4fcxgvv*P%qoRV@kh%vR?EdawtPVPH>w+$c>(JgGc zS!7~+G5WnHD*<&q6eQF#Ngr+b#UB|6Ey3nnIeIOL(Fe|+10O!Reb&fG5uBqqoraW= z81g)qlQw!zC-ltMub!Y{{wsg<+fU%~)7w`4zu+!!i}fsImv2GK4?y1iL1~)q zs+1uMDj>TI;PP+{5nnpz!)9=C;X%ybteUPwo&)T1t8kIl1(Sxqz-vd&TLwuJV2vNH zTRo>2mytbeB4GNre?FU#OK8@BaXOskRV+>*I6@b&`WHJat@e(Gxq1zUayt%Jc0|{G z`lsEogRmu`WcINLmjS!nXoYKA1)DDH|C+)i{Rfv=gTm>5|73xRjOjn}%{Ca1tF5Y} zNfW8eKV8)VnV{P9`!`EvCeAdp$tg(0LZCXg!&m#TNT;C@5@!uRoD$T=E-w%ay3X(W zCW*yK2H<%e!|^n2kRZjJ>Ls_weD~f-ftKwfqXol!Ppr44S-k3Q~)k_ zx`oDje16{+d3*wI7}4w%xcs2dE@<+mDxSFr+$j%KU!O2LIDxq&nn@2#CzBVZE34FF{DakT9qC6nf zxV~aeJeQnRGT9RL_o()D^7;M;cF#Jzlj{@O}Gj3EVLjH=GCy>m#aDV9zgGjF9$ ztjpWhcUjO<-^<8?p^0dwVF>Ax5fZHY2M_wdy(1cLuPe=q1)x$R54yl{-45< z*CD(7zub9P~`c(+h+p(Ac-!%hIY@g#GUDW#@0Q%h8{w;Bp#(%MDJr z+@7at;fz;EG8QG|I7Toe$&&FH9xI_}RINRD@|XIS#~^EtMA(BD?z*9|+xpqZy_uxp zh!4?7?UZ=UC`u;hNU|4m^(Fe6QSnu)g*HdI7 zr|aKf4MegG{pN5Gz~!w3l0m43)!0Rz-eIf1S)kW3a^~QLJ5KmS>;CcK^U90?Ggfs( zlKdVu-8jj|GD7*Ba3adc^huzCm1<` z-hb?u>gMY71WrN9WRz~tPp87Z&S_J|_0Z&{j}hUrb^WS|BD?%sa5*F{$`4n6c8YL$ zrLc3u5u4FKr1gs5@Q5=cM zNrH`$rz#A6J8jJ^b=PwBY7XUgpSsf-h09SH*OT0a5M&YTVNdn>gWm#>$Et@mT1E6$ zw|Md}nxQadBJh_`jMXs`3h60ljdDfOfuHu=zkNl4Yk5BN3{2bv-#@*7BgUl{4xeEa z!K2sKWp3$bH0ThLVhp=~xd3-*g(WB~ByjG~w=_XBtakX2VcU1?iy?}Ohs|*LtoQGp zU@195skR@z3)e?#KfZS}N@d9SoK|c7{kx}>D5X_oT&{JyA=!L-*ET7hg6R6TgK&x1 z4WDe7;%_^*e|H>}K_?BcSevSEoYo+mNtJuCs#9E%_T;Y35;|3C=xYmtwCTvXKA80n zIN&q;x5v=wc=Agfwz>zmj;T-rV3M;B%`RxYPkpypPNm3o{U4jX(c~`YKfz_6@Wick zH;!=_$tqKUKv6i?Uw>xbW}MC_7&@yhAa0@J0Lbda)C3-(R&CrbxHk~fz`bqaTH@{8o?N~g5JJ$p8NGEXn?9D{_KVBR&4zkO?4VPg;t>vAGL)1jv8*?il{s^8u2q*_F$tG7;wSvP(_iy7E z=?j`PAbJC!@)TI~KpLN_G2Xm>AMT9e`f|-@abyl*=-26PYpT0kpi`+S0nDji(fBVa&-)5Ji(-Gu)K~lo^fD=_{M*tw{eQHH?J+8egwy3U35>M)#lox@ zD)Y@*s|BZ_;ZC0xCv>g`yK?zAxylot6RFIT9?p@37gfK(M|REWKxt#Y*3tw$KCh zV}0`ro+C3x&0YnFXIsBtZ~&w{?j!)8 ztsY$OgaNpK7id!6oiJL7#=bpsk)v5LJvpY_2ceK$Z3&*1X!E`LtA>~_1IAl?OBvWq`%))N>l z=MdcwaCmzyD!IdNbcO6rP-PUs9mr08ibO4C2j zXN;V`IwVFB1LAVa;&Qw#GbX==tFsD+z8MT#K{9laUa3XsNi!CM@jiF@>pT(YD!9nWWNJO0ouQV-7R z;w;_snzW${R(fH?Y4P}BYCNWF-tyY$egfaDo}vXJ!_x7Bb~ymx^2om^69>*;C0sM) z0xtKul?wp7Tv#?U=cDx^;Y;QeJzWojOJ*Zq7@9S;VD`}^a5}hD7-<2_n{LAbr{QvgI~;($+S_`opNZk+vL8;|fWV;P^JTNUQ^}*|e(s8_ zHrDyz>PZe&Gjzt4Dzg=QB<_Ih1E}5RuD@2C#E~>9SLpYgsuahX$M?*l|Bmv#nBZ zxk)5W;gniI@r;bZVmO^TySmls6RcSi*ro(S2^pi2G8{sZEQTprO&pc>{n6__A(mzB zmzfF>vy@QApbW>6I6-tr6N*Y^SfF9p_{ju4{+HrXu#lX%NaSz8WtdCqnE&=LK;OMK zYpFxrma6t%PI3l`@ffY5d7dLAleML3mr&>Tw);Qa+ISO zod&}gIg06WFI2aJFZ(*~=4w@p9IPsO?f)&th{EM?^NBs%FljQa9MA~a<79Hs&5UwHCMD2U=Dc4G?3ItaVGKCC>! z%es@YSb|fbtPUep2&N>MB!woU)9MO}apPd%8C?F?<&)2-QVnGa_CctGsn9MC(m(ET?dMlJ^XRb8} zw!mfWla-3g@#2x?RyP=Pon`3>7^CaXChj_N$_ucW*U~>XI}SG$4SC(=ZmYU|hQ$dK zQ|vx`)&(SGH%>CN^lwy3M(Ip5Ty0=|#S|_5LVnrrfXhvgKTz+^*t-Nh4Kg@6y?y06Xsy;*jpJtzlS3A|X}t+QLR#K+t@7 z>jvhUB)Fl^h^aQ85MTXj`FsT{`>Q5n==@b8TwXGMh#Cj%@`@;2c6_tCTub~{Vd>jq zs^7dxC4W(*4x0BVgv&s(_;~%wMZMXK{Ic2ppueO3Zb3>au%usZ`pzq4n%L&gm?Bde zC%m)RyoyWKB+&&u4vq@*8>#q-DPk&hMzdla->5*JppY=>QZ7w=J1G23S;DMrU^DQ-A_&AXk7OBKehz?Aj|r8MxvspB5lX%yDmVf_*xfDC{yDy)|Ay~3k5n` zi!;)DG8toLFAH_KEe$twjcOIEe0}B;0rT3deUnpBCZllZI8Vsc+Hx;9B^i~|ug`eb zDx~Mx`sWqI5}9w!tmq1P0oX1unG}TL^%;xIp|H>!v(hDWx}xaL?F_&*wNg_6n_e({GKjnTpXA7fYEJIi zh|AMy_0UK5E{KL*VQ1Zvaf7tdZf}*(2UB*_TYbxdmDaqN1D3v%)uQ9W}My@uf93iugiFA+ znU^26ID-BSYu;s%9z~-jJoG|D@@q68no$U$eG4ay(KGm%sqb0B;=a>?+t)7(c?T3i z;L;-p_8i)KNY12i#$vMt^m<>&X8xjL8BUNm$toBXjVX^Fy%7pQH@3MdHz|R^nM21f zd7x3Y)Za@pW?-0p?@?iu$3;_KHE_u5bKZ|=#YDkb!2V^$2PA>Wb&0!AoP!xGwi%P& z=2;E}NJne9e)Vi_L2kS@d-SYN;Inw=P8g%*=+|a_)Gj`k&$g*zhKhQj-+ zim`H9kMXL{H*SjPas58{ucOP{+&3*pjcHi9D#4J|Z1sA;QE`{oF0fhX4jGDLjE+K8 z3eI@${zK=JeIkR_s;j3-j_rxl5?+%(xYP!wZ#X2NNX&6;_fAwsu$+;j)Jm1{{M}md zT=f$+w{!J|6-aD&re2!6$JbpQ5v6d{y)mewj%rZZJlR+uRnjJR&z zu(#v%58tckOjcFg;f9rd@8lM~|+8e(!^w~#@Tt*W#ijz1E8k5BdCC#R*GVScBe< zmiv|clTC_vIV-^!gaBhRj?l$1CK;{y`}*yUNXW5n^Qw8Vd?Fy59bpLI%=KkCDV$t_ z;{?tq4rEd9?$7;hU93R!AOGC#wv5R{(K2;w$nB=Gf^v# zm*J$8W>^YEWOzJZoKrHtR@kQ0yP;x&j{HkO*%R$@@L$5^dKl8Nwbf-MByc#1A{Zi< zX&97c_?~Qj>C9Dv0xj6lH$`P6r5ME$Dh5nvHa^FLSeJL=}NIyp_X&IkKGa; z(^vQP%GoH&4j3`MQ*c?`@0FMEc(0LjKduubdvGt0pr!T3X)PX)V_@y}ccF~ivSj*L zGF}2uAByS-M4=^wwc`F;lQU$&YAxEWGsr{6mKACK91n>MnPUY3{onN8+j6!qcS;;Qmm9DB?K?UlXZzNcDMq-!u@^u zG?4KwlS@!qg)y9zR=+a+V{?S7bAm*sq9hehvvN5@qAHXy$_z!P?zM>9qikIo9=IH%c_Rs!L0s8y%v1|F8ritJ z`X8T6kLQiqBPTnaS`8P(J!&o<(vZN;69ml=oC0SRIHf0$Os%4i^BHjd9pw*)%OSzX z5nw04P7Yv_L~I=R_4Y_2T!5Q6!OjugRxYxqbqIKseh+kDMfn;;5rC+)Ml18dPa?;< zAl4PZLiqo(cdfBeR8jc1zl=ZBXx*;6vybj}Y18twNDvZ41)JbM!~!vCG#D^35_uV$ zpg+`zP2h*h!x$ewi~_+&{3FFi6N%L*Dg*;TUMro0Y%U;*J)n!-bT6~?R#B`qhB zDK$pomyeNPYlDE1bA9&oz*O1d-b374erEXZ- z$Fqn?`s~Y}{YjO`Mv1B*IRpz8pl%glxVh(>s#KIj0l!qOtXe+yg!T~zCe~^Du92mz zn;dVl)vLNGfU-!~7<9r157@tynyN0_NWl~-Lw#Ih9c@a6K5E19k~o?mYgOG+@&FhQ zAQ#N60kSl(8?s@z>$+6FC#3I7GdP}Pdah)a><_Z0sgonFzrp6#O}nHHe$mE*)9@Rh z$!CFX0CMz97W)tL0+koce?YU*4u2D2&=VFxCk(nEv?7W=HOHkg8oz*DaUr{qCbUp# zGI@8UR=#f4t(sS-+A}~Y32>1uBQx!hF`7@QoDO_Gl_#{ZrV!?zW``y01y-uo-N|^&O zI%h4#%^Tjt;iho{onXguJ&T`ww&HnMz6m}~?DyV!HIrydpjMRWU9x-~?)Q8yUpn)| zyqW1tZup&@UmWaZscfO|@QG5nrGNYGsiTkM8T?KnneTjm*Y4kbJ>H&On4I2! zWTIThH&;70trvN*@ao5Z{&7Z!lS?`Ij*dek z-+uMY!8@I~**X0uFPDUIA5r{Z__b^%+n&y$@`UHO??t`GsNa>!`6G*Zx(fK~i_za{ zlV7~@&$0Or%+Ka#eL4C)-oY=w@HD<6TeZ>fV(-<)5JH#VePc5!TKD8;>_0TdC-rVv zGuVY{PO7)N(06ofY{7zscTAi2)TTFusZ|?Vy8^wU4sLq=LhCh@`_cA|h1Bhjtr#rR ztI&<>R-<{t%9plzEV}i%J;Ot7+1ZZ{tS`%^L>U9cQ-6%+v&lpvlZ+QS3-iWnY_e6k zc=pJ`89lg7*gx{iq)7>n;0t>WbnDDexb(xch7;&&;*; zuUtDpXf@z6R4SkO^P#@k^aH1^WvkYV`&1E%ySHu0Iz~B6q%%p>O_TBal9{+OC!Jp~ zQT7xP@K5jAz9o~1qx+blUcsY6D;RpMicnV*P=6e^Nu2Q>z=2hd| z$lhJ)R3dIXMt5gA=l*n}gqV$mHwd!4JBSB?#}etgoZiLDpU21H*4S6!CS-K~E+-Xh z!%s4u>*+iESE+`%n$Gc%m&cDCPCK!fleyRF>Rv z7UqtfERhQ<8}Je)Mh<-9#AoHYmKFs&u;kNbE>*Fw=FE1SJbGwfHt^iVD}i%I8VifEPd`b`RL{bF=rUBLG@-|Db(@&nkRMN`A5P-rMVzIfBzL}n6we?!9 z#K>1fN6S#ZpK5WS!CG!`i|M1erBfr<0(-@OL2)i)qlPFZ4Ys?f-;h72bE>8Yz_o~| z44cLc2Ts92T#N+al%Tf-GqT3Sx<#Lefef(0)heze?sRnnEsFSp2p=d%r$;M=u?MXN z06RMXESCVM=F5<2II@-`5O`ZuUp;EnE)78)V5WA;1#^b8B4ZKuu@0`n=71d(iz>=J zq#UIg1|F*@Ul{@o=AlH+j8*EOBG|eB*RV6zJjg17766zU#i_IY)`R5#5^h>`*+sOz z@A=Z#T9VIXjKWMeO`C2Yu>vOQz>6MuDsWN_!pLCFH>?J}KPf6MuLVZBveKPM35t{! zl!4%8RJMS9@JaR#5Q-cdIat9BK61v77qA0G5wk-XD_TNNn(`n6$*+Sf5Qc_Um}Dh3 zQ4QsZitysfK>gqoP0c+JEOQnwL7*ybxL6?BDVTXn&p3&i5MeHsZ(yP#KqN*LW%``o zk}Z@)5PTMBk#*z-a-Lj?5sfM zqC6`k<7Y^A;CMB3nSONagzW{&C3gZ0=^AzaW$$is+6KY^3@NkGw>si|AsF$wF za8sz|m&b-Huledeg2T{W7L2$!y^BU6iyZ7abpNEYiY8DgUEAZji*MY}Q9 z*|qt+{}UKn-d_~lH9FC;Wohd@Wpl(5u71d7>b{1k|9%nQXFUo3U!o(?H@=ln5tO{T-9(0UMIb*4l5B%Pl7I#1j!-far%A{=ksS0MH_7E-xzNG5`!f zE(1UWX>-{a^ZfkmoLhWGE(1V!q&d#j0EihO!{YK*bglFI-vOyXQ_Y2Tod$z=f3v`a2K=kh%N z>&X}=Cnw?2Wxbnoj$8(S?$YP74*4=XxeQ?9cFAS$eVwQFGfmhm%j$le2gqdr=p}tF zZw_)90Cvgc<#K6^$@Bc><>g1<{QUgr=;-3&;#cKo6Tn2#d$YlVCBz`U%W12HA zY(O(Q%d-01<@)2~bX6ikE(1V!&wf_ z+uPgw`};7LUteF}-rlsBHJ9~UkjnrNFPAxB3;^Tja-Qe2ie4a>0bm$%82}-*+1Z|nmwX`|$_Rsa3y^0FxW%70jO$Yu5g01jC9TrQ=_<@GA+n|0LJ zT1tBNxmEtLl<611cekIp?3^o>#nP{QGfKZDmn9e3`(k@8H+|ep zZPkV5O6h}Kt}gm7#VqA-cFty1jUb4^50gb8f#i}+ASC2H1Yg9JuOg_3=t6u62Ny=% zT(XhbxN;*JXL`DRQgl;8o5|!4a&yV8k7L>0)z0$F0edE{BxqMjw^3@N4d=bZ{u49| z<8hJ3<$p)F+wF8ZyQid&J{1z~NMEdd&sz0}%s%ff3Hf)ZO+%f{*n=W!8eR5J)3&e&R}R6IGZJw-epldyj!Qb{Th6~xffxE#Au zP``;m6c(hGLBL7c73s7Qmj$YO@q(VHB7&xAR#jCGR;$%;IGju-)9F;|U@#bu$61zL zE|;<_nb{syF$|-fry7@wQXlmLKmwHNr4k_{a2Oy7e-xL~vvickWzydO-^S&z-U=d~ z&*wbP?*oU!K^&e9!!TNSn7CZKU=hR&>?o@88(hcOE0 ziprNED^n0^IM&ddS}-*ZV~`4oScK8<dFhPAJnvv8QL8PHMI_>9A_iyjMvUldsQ5;ts|1*+u4s@(mCq_abBm@$Y zI79>p<6sUM>>QY2KnUzWY*RTZDJQA848cbN1nh*w4j~wu02Y$41+gJmLe{ZbgoK2I z4z$wlYLA(nnV#wX=6R>sS~-yXrXQmIRM&2Ach}VP{_y%+uixv=_XR@$?5FYQbH!u> z3V@9MhaY}Oa@phY2q7pxrmwG$LNat+w?*^0bLXg+76vJoB@)TQnVxc4(X^DU!<^LI(p(DBk4@3l1JTp#re9x-+L| z=PnxeGjIk=1s7bkVvmIt1_E67>H-mf0RkGJ0%0TiCBOmAGDFe9!Z1O#2nY)>VW_as z-PAKbyC@J=1Q?(|1T_d1F#jXN@6%5|rO*qq{?tw;m3+x$Ps!zQ=FAy;la6dCwNR)A z=UmEViA3@+rl(wvsH&msrWS@3j08LjN|%4lG3)CI$(cEGMigNn$``%Ss>J{S&hcgJ zC=QDY;(nNg0H+2j_laA3IJMtu@5;-`^Z0!X{{wO!lUA;+t)({yK+?Ilx0l=^_D`A- zb)7wXmgX+ayF?<1F?DiTfZ*ED?PUdXTlStLgiB4zw z)_p(snE=GZP)~8e+_Z$013x`bkd+?{j4=`1{rt13S$$e&LK z00x{nSDThsGUV4F*!?_gE=0_Pf-@5W{|UFZ6k|aemIbC;CNnrpd%Q4g;oW|{vtagu z@lZeqH{-S^zwCwP+>XJZ*QPqXGx?JL7KL^W?N~edq}YsW*RIKPlOz&J98(mG!5MTw zMP&46d$%X0JI-Bd0Ow#(bc-LE z9XNjMw|7VSa|+8EKE4WNeE-g8w(r>=uu#2o*VF4Np^bHQG^geix4K6VoLF5l|LBQ7 zhfKWro7amTeUvG8>Kott{tG|x1Bz;XyK*%I;u#tmB8y4ZA5r3y|0o87q%)$>h8=mJ zX|S}U5{cv?OjEf`<4G>Fpl_ns@!ffF@uG6fLQ!t-k~_7#i#<$eT06k(NDg z!H6%&0n;+a?|I4=7H2q|Pp;pfs0x6a?QQ7}ha)9zP1WWJAY@tl_wIY`BC^+!TBeOH}LlGrl26y~uXJV>jrgP4pe*afYhI0Gt7z{+w zb)7a9>3_DfR$5wG5QQ@TNHJ5d?InRkB9Tm$Tox8AOGE&j>*F1+=Cll_GdbCjoPNIc zy2;Sf+@6;?^IphM0MB5sqIiiK9%^+rt*WXCFn|ytz~9k%WBJCdiU1H8Xt|PGSkdQ2 zP0ijv9zFz2SdsDa6;-XD59)VEDi;>s3o8b-4tpz$i1}y8jnJ*n4{%la(&5 z0dyLM5F)0P$x4#-r_c+M(d1^P7DOSrOo@PGc4aVzL?U@0(^f9)5e-HJp?fb?Z+XO- ze!B6R0T2oUh_Gkv;{1mCIt%#q!K0OHH$|g2Sow-I?VY{C#NpRoKk?Slv3o=L^XAvr ze@s)~UQ@H}g&&6m_U?Xu^O{xA#yUG%Qf8Jle?ElJ*s7wOHx3_H8IB$}F@NTKuYdf# zi|_5)U!#GmAw2f2O{2r(u!-vO{(*sa-hC(L7U7)7HW{KeWCW2a7? zq6I@y6e*V_63GMkzmdz^wr$(8Wy{Mizx>K8uf)q`+GiutN$DIUmnj-}QZRI6?1?=+W%j9()92|^!ohj#!76sUn zS|X81rcy2wE_ds$UC(mE0tdhVRPXs^ZQYIT_LisDt>C611T1FvgnGJfEZg{l5epVz znHKnj%YD7#$*27sfO6B_*L#Nb~jx5hk)NOf-B3bZnt`rJEP=L9`|_w1p0j| ziz__gfC>XpV5*j)uc=tmcl!?KJpNJ=(!ue;7}QIV%H&{(_b$-daN@)XTE@tpO1UhN zNFK=al*=?eqA1{;8JcCO77uVtaD)a%?#?Z)Y;z3{w6%TTDsuU#ZN39;OK0><(ysKerpVRcxA!NOSR_# zoc-WRZq5Q`_`KfUqD5sccOQU*hYsz2>BRxBx3IXRq2VKw>D%}0`QE;tdVwALo@=;F za@ogNc;&LEds;nE13%fb<=C;~%*5eWkIbE!?Fse&_SmnFzjYV_W)N#vRD~uK+bqW1 z2^4fev1>7lDTJW9t(EE1Zl`Mn zaiKoD`^C#wTKikupL(n^U|KpL7{CD$>bTjmc*8aiM-&q*(&_H0SXb?j08lz_G=6jT zvhFZy_P+GdrE@@tnUTkqkX-hPzVTHBMR&cyAcwzqbj`w2RT)!+8WJ!$gcnaPd!qMN zuWm68*tf=x4v+l(IvtJ?@B5(HkzA(#9}>IXsx_gl(%*`f-k zBR3-{eO6ZPXPv`30M3HForUw}ej~w=k&)-V*#{T`Vb|w3vYZq^k@)!fC;XNO3$(Yk zr6eSzq^4DFs2WnuAh7NE>JKk{AQ1dwldim~r)>z@gn!6AKff?BA@j&unyOaj9K&CU3YmPRI#Y2wy}``XU|;B&B|e#Z*ZhH zfANaXT>U0vzkluEo;}aq8w<@T_*Q-Wzn}&7J-=&H&Ck6+^>cgc>OKGzW@@XKZtlO~ zfokpFzU}S5oYaBihu)gw%o-mbc=e6n965ecFd&Ex%N`H<0*JfEDxxT7PM>zWIuQrw zJU&L5DkPvO7=tEd_sW~GB@)S$$z=!#zyb__#qKhtVgfvNzlex7*+aoO%xIGUEDp;A zEZ7JW76Z2cQ^ZDEFsT|H#0=1I0lP#W$vH&q2@nss>WQ-V=$aBr_@FEnM4_l*^T_suqlhcd)m3 zeqO?igv^<9Kkw+)IpZ)oJ6p0c)02|Y)^GTZZb1OnG>D`kjJ!4}Q=P|bkrK#XPWdBJk)AfFkNFz7khqM}@nJ<3491fokUVQobV96Xeu3+sW%l7cfgTVmV9z9aWah$>_ z?6MS9%gPB=c9~fPLcE66@}8wyv6}578aLHwM^MR4ObnI$SQ}+wWm18pI(5!iwQXC> zV8*12>+5UyX523QB!f8^P18umpX%rI5fSnA!h1yT>~agQ9F`%jBZY{6qLg!PjFLpT zB!5V)s;!_5NI9jXtQ&+W#=z!|wWN0e)`(DP6>$y+TL%G1&E~liNTNu`^w#$I>3sgp?Yr9+= zbJ4zC7G;Q3a(S0xg6+;f3^^c~PN&n^Y!+iIZLFEwWv6%8cS4_b-xvC8mx-tgq^cp? zWQau}Vf#&wv7tn$kU|n;Ea|$yf@+22rmP{5s*yCcs-#4UXlF*b48mw^NcRy1q3h6# ztE;PL84Ql#R&Sew;p57Vrr7C0B&$N|NUg&MaaZeHCPbu0 zwZIFu{*~|i>7iY2ma9bw9dlxl#9S=0v46dB))B~Y(Zem*s}(wePGMLC@3uXBs^Fgy z=dqE0!Oj7R$gkM5MKT9Qm!+OQmSXt)>Erv!{p<1V+c)=jqtQ5P%lLMyzktwq1lhj7 z%XiSP>KL*}ZU^PE<6J6cbeg6qzS*t7`O`bQj0+6Agb>Q(pX#6NY9xbLQV4|tZ?!>r zD`DM{W(mttB%-5CjN54EF_44!b@=}wg+mL;LBgxW_YdzT^KTFBddU<**KkUlxFYRp zpAt$?UFM>_V>!;{5<-Bdv6-4EtE#8k^XDgfXRw<#5QJgifOrr^dPdA>8utReswuW#$qOu3D!38WS%&=oS9*Kg6u|Ag@$pn*`Ort zp}V`~LS1GR{{HRjub)3uMY#;UDOe1{xCQlsjQeKQAqyPfGdCTtX8XN%n~qNPMSn@**S;r$&+hQ7Q{Z`PNeCw@sx;2)z%gzR&RM! zPtLMTjRt5`mqltueAqI%MA}EN{|_9Jt$&>HOhA$Q{eG*}>U27z(Wo+J)8&UDMO{WO zHs>4?9TP*5D|)QJRT1QgFMf%A)M7^oGz=; z0L|*M9tl>&?u}R!86^g04PNbbg-Xz2B84ugcNnTdUhGT3z=C?5x-)!PC}2 z*>u)OAGj~>A7YcoRzDmLvGbqSWq=A*qXC-MWk%F=x%3l1CB;u7Aq$j5!cK*hoEB8P z3eue-Q`8lTG339J+DAi;2ZM;PV?vJG?RIM)96zkrYt!XF?A_gO6xSKYajE}BFIosL zwt0zb0)!W1<2OP`TnNSyX^2szTB(yJfK6y@S0-S`QcIA`i({ZkiqwlYRFa@lxXWop>x^XXMxukCuZ*Lx!Rs#pHO z!9kPDkE!{~WsM%>vXDqR|N5%;pI7^mfCCVip$C!$prB>|NC}a8Y5@!eT&Or)%O%Vx zq`qeu-XVk_qW1RoqeqXnwzjslwY~d5TZ+DKa@iX^oy!`fMiEPbWR_6rQD9~wCeHjE zWQv(6lgqQCOfDPV!puSlz4xKx_^)l=JMi9p`@g zd$V2|*~a9uH+V9aHOyrvt2S>cSn|YE1*`8*t2mH;p2}SpB!+KXU9vpy`?<@PEibvz z6L4*SArezfhwAf}En1SF_lsY)#~H%pvSFB9Uhw84mnoCW77%wttgK@D7oT4Ng%e}` zvhuxw5ClVkks(~9OpS~^yLpRma7X~Q>i`n4z}AVAXZ9axG3%v~ZA>nEgBNodsbQok zI)D1i(F2W$M?dW9-q(CQ2B`EC}k7OMwB4gsWUAK*F*lfT_Slc}Yd@tvfbA3@8cB6{ot+ z9BOHm009b2fXQVe%b8qWu;wwBC6HtUCqwtjDr=`g09%v@R+ZO$-80Gr#R8Cu9h)pb zh68siHg3qx&A)KziUMpv0ZjHsT^$AaKYVe|YnC~?Y-Ask%iiFvT&8d2vQ*aebgZ=G z#eu#tz>1IgD~ev|yB-E0(knSqQpF6#Uwrz>?waa25M>b32(DeZb<5|M|0cm~a@jCU zE-!5JbQnVqTp%IUXkfx$T(LWtS~+ztzfkeg(9IAO07-BFav>zUU_Dm&-nDgW@;n0!J5Cb z%Pi8n%hR#Zjio#LZwEoK@V#5bYo5DxeLCIK1VMVp6^^Y)0Ty`TcU|w*z4P9jT!vC2 zI5NDYXyb_APh|dl*~lU$m%YK$xhx35iSXUB$~~bF02_AfxuV*^9t#RYAf$%?7s`SY zi`?~9mTkE3 z_4SRw4JHBu+qS-Xe+(e7asiN1AiyCoz!LUk>;C%V=l&D{ECr=p$%0w>dakW2cq$%^ z!7#Fk$z^ZwP%dl4gvnmtX>z6re={1K9v+s2!M@VS#(dM`6j(zxtFrc7G zkRA;F&sVy-I1x)QKxTK@Fib8lV2}2ilG!1U1PB@t$F1sTs`3^uU$tt}M2uX3l^F65 zRM*ypk_yrpy7}Myyv0xCEL!no!NtG+UAllKqVF`;<^1ReIm=e;dbxfwV$NY0-o(u5 zH~i`A+#oi&>>1w5WiZW73W3zQ&hD0dZ&NaQ@+aP8Ip5exX>^s%Of*FZ`nxS{daZ#8uMpS`nbZ7PVua8l^MP^1CVdy^>Qe-O9o zQgqivH{E9w11;I~2f1-4C`z#))=R8(p>9gCkZSWWALqOdGiJO}B1pUFot}piax*#E z<(@p840ALg=&~FnCnP`|=(A{Dbh)CON0+(OS|*SM&wC154|m_cdHrE(p!@=0jGB%C z7r+cO3W}}Zv4F9_Dar9v~P93ejoeg$)}Gy51%~!p&8T3=iQy_H=EZwcW$iSe*WU+ z%&qC!w=YGnyOA{-t?cfj#~a5Jghq>;yDX%HF(%wcn4`6h7oy8mJQiCUFcDxw zf^k9c_>>u-0Bx)ZZ{l~_Phbk8;Bz%WzAr+0IAzK6yk4)@N-QlcWmz^H4n>#KAYok& z#>~vIqO&y!kd6IkCKy8i6#JKK2^N61n#i~?)+&Nxkm$0I7RsP@Y;JCf zE?1EY(PaPxlyn(j5W(<#qZ%U2r|M+J17K<>s4)A7)e~jM05mZGVsFU)GmIeJ zTnf=;p{j&!b*`T0d0s0~_heaC6ou$=6}bpqcEqdr95Wa(Jj^gI04}9_*a8HSuCkCm z&Rw!Bt9jb(_Wu5U{O3+|IRP$%F0ZezudS^O27~SG?XoT(9v&VY9XVrmx;z?<{%@*Z z4F0?JGsj+|Q&@we=A;S_ZF<;#~ZUc7ky`t_SPZ#L<2`#*F-mxJJeDH$Vl z`D7V%d71zG>z|h&|5zR+9ZXVqa}eAsBGMkefB*jRVFPCy1H!ggH-O*J1$6jv@a=09Kl>G$t`_x=yRTwL691i{nh-Me?? zu~<%V8n&C^f^*;;b-DCbM9NiJd0KfwGiCsx%Rz9jm{~auOIf6xdQlcRS=Q+?jTnOW z74c1TK@glZXR6CNXEQ6mmQV3tUS7Vhluh1`l=VRn?B?y;x8>U#KYaL5PP*dC@d1^S z<&^O0GmK$zw^Glqmi9k-Pd@YPuGLTN$B$Vo_4!B^{6^= zr=W{{#0UqQxp!T50Ai9|fB@;Sk7Jd~6vGAWZWW3I~$B@g5^snfPI#lcTwyClP#-XU_X_!xURo{69-OTbuxn=0r& z3es{75DWPmebc((Ru&9F^F4qH$`kCj}hL>t^ zT4LT-VhMooBLa=>$kA;nH1+Z9+ASE7C`XDCvmuPaL~stey!1tKU%!4WuKexWx1!5e zS67oRU#E1Ge1s-a4NwrP?Y9PQvLmbEnV|x5Kj1uWf3||u+P6f{CrYLoI1S$+}-?3l=xn;3M&YkO`I#1QjiH6l|=SS0n} zZgd$+X4hr@xHi!1vL_r}N(iLQb$L8d_us2AXyPEDTY#6(J7Zj^&rXhJO_%!-0|>Z} z)*nL4V~_fDT=9z#TM-zOMMWp)R#1#B%zmWHSkmPo`Ob#N2vB$FGNuPJs*zZ(1hqaS zBGI$tkxb*12GC~fpjDUA24p_@C~=^b5aNm5EfhRWT|P)Yz?v?b0xNVG^V{<y>ss_A>-+FA*{sX0#X*->w(CJ+>2+L{{V>MT z&7APqsmqn2En0U(Sfk5{Rw8sc*s0477T))pblKN{4X-yZzxUN+d;0TUyDsmj4uX_! z4=~u!N2$2IgHBDeL?gWDwXc-YsREZMgu+JLcON+uf)2KxtZWygt=47fN2A%3*MnF16VL-a+fYEX`L?XvM#$&g7btZ zE^8%*BZ4msm&FrW13%PdGwC)Mk2ZI@EG57Us=+(FJW>X7QJ0;$_dA_?8&?2&22}MK zb;Mm5u*rajxufqV&~^;6+5I%vWo4f0@|1c9OGfB&@Hln(kkY|mlP+h{R_QY9qv>K* za!B#M<_>ik<_lfo$F{x0i#;&)MB&cmZL|OzO``!@HNW)73?s2%y#%oSGNA`Bj(9_u zwTm&YH->2@L#=H&#IiB_H16|GdsKhJ-14%h>$jMJFpRQ*KGbaZqc9N_kl=1~*^Drn z5Xyp0m0+0UbkjAyx+Q~0AiFRDSn)Xp7)+W5ZP8@_eqbb%h+(?k2BFkUPg9#PGqRx9 z*H`sdA(IULoNMRJsDK&(@@N!LL92neVylP&oNCx5)D$q%%lhMdL_kH9q$x;> zVEHOSy5yyVupLj9l*G{W#hA1Br96OzIg0Iv7!AL^IXop@H)YNDZ%RT`r|ggMwJ7}e z`an|aubPB&J5OyuN&rR*`R}Y`0ZPfpiHY5=1*T+)>vZ(2s-{UYNJe!zc)Yr-rYRqY zY?ehcNk9NyUtKm;GXv1F+#$)*M~O``0q|~Uc@KT9(fpUK1rit}$uUEvIbzp;1O3r6 zEl8{nAXIl_|ULz`|&taN1nIoHu0FsqL+5`wIjjiOY69wqmE zQ9~WNY^o}P)+LZWdN7m3O8y^vU;Y-wv9|q3yx(_nPT~U03^S~POWgN;jheV7YGPvC z5ZPDv#CX(bR8&w|1h*_IBCDc+;vP2;0R>UnXPA9@ukNm|x@vmFc=?{3e81(QuNGmZ zyXvXxsk`pF>v^6L1lV)*9Dju_%PL(KIS5$@D4DFhf%vE0AON~th72#b83-ns9uEA5 znsxV$R;64u9#9mBp#A5`3c&~<0!b|k5Tp}4Zeo9t@&JjEF-`(N6aWr0Dl`&3*bPNh z-k?PpRY)ms$_OlQPeKO|T@a#D#CMh$xWO{da+p#BY$+-a!8a=aM#k&s?i{uIsh9vn zghGPge*10l0|S)$f*K4*whL$o1;w^^*WDi8DFR#qGJq7Wa!!$kA`4X~bn+g;E-3wp zAa0Sz%~h*Y%LI}{U<{c@^8$zGUsU(I&kH|ZERqx3QGuveMRa;9@!Y2HT{~$ut`JD9 zgw1v-%Q9@R+AmT!-+Bo+pjwC^f}T*np5t#xml+c4CvZ?=30ShCTkKHB?|-1n?+a1x zei90fiuGJC{8iHIkwyN1F+f2mLdYbdlB;(7=}mqC!Lve_$t>ZLl%E73Jn}Lxd1@t< zLJ^%30(p*bOJ&s|Dm0R?TaV3{ZoWZpVw!vM@W3V!V=-}-)kEcl^ljsGgSV=V<&uqvVjt5-#?Si*GRb-`b zp2iJQnYSdOO8?{|0RowK+%4hB^i5hPs`8b{$`XTTCmug|)!p!|36ESVbeHb|HWSl# zbUW(-{+^8EJ;z_6%b2F%Q>38KlAW@x^k#LJh({V0?B1nBp7kyMi_jtEvuLrJBf~cf zEV?sBy|4KwMu@kJV_9$rl!Ua*iREU zBsKdS<0j*xTpxd*-T{8AW41BKcGNV{5*4vVtJMYg zhdilzPL{RRPipjje%?C6g0H@|K-opAmV}n7s!(mf;p{vMgiaZPZhAZFFzgNhpFMiw z?`!De;~kL{?|_06@*^jV3JBJFX+H1|&<_Y5)A-D!B5F4eX4FMMh>D8V_~>*xL)q2q zWc@v6+*toWzrJ3*42IwVq0{PX9V&El=p;K-iY<~UJ|CKd!E7Kv&n+J_-yD@b>N%r0=ECc}-Xn z)+9g&U?U<|1O{sQ`ugVGsHV_$JhLr+y^nYAzFPe!^OieUD7q&FYucf0-rgSt1O=8} zy^82^503U6e@nVdvmC_(9^n^oBD818VqO+7^Lvj&gX*=^U7SNr=6U`-QYAqvL1E#L)s?-a7!~ zM7-!`D5%OYKteBY1X^X$>E?){)ZJ!LRwGzY!8}EakaiPrMyW;) z6BXhSRuw+X@*<(x>N{A(kuYlqB6@ zw>ks`XCXtL6z^0{Tm&E&kQ*h*)8~Y~qDmz-dXiUYI|CUjl*RWt@pU&`r4cMt4~a$3 zLgqPMCKsSqI3?C5OT3KRMZak*k9w5DFJj&;NVFXlDBw^PK_)f}XySm92}-mqOFapi zdyfCFP(MtU(ZFapv42Bu*~8}!$dlnCsk`~zSEA}DlgS%U7L@Tt89SR-uS#GUybvd# zriKEmpGTK0yXMYXY&R(iM^xmuS*Op?Wb9zqQh@6dJ<7D)J^7qS#8UwfhzCNqW%_T0 z6^FCWsk`tLWLqo_HrU%ehbeu~)+SP_2z!!E1M%5&8E*r+`@r>qLTaIDL;Y{t}`o4((Fq6k&) zgx#bB885gc&i=Y-#+2!1iybv#-@*NRGqcfUyj%2iiUK`(%0i13-}H7g9Rk>=HPsmz zndsWb_p1km4zb%=mX}3RzR1dqFm2)zV<%M6+at3g+bvmX|IW@lg6@6&s%!kjx$QQV z0x&5)vHIa7PIT)KMA??HaG}QEagb@bUHtRmtW!oABD-t|7+D!_CvXa!axgA&%k|1T zBHoS=ct(L!55{rk9m&yE4X^B2_oc~;yd9EOG2GS-o2u%c**TWH&^<7SVz{{YM4shX zS{gTQ(Y3P22<$QreV3DyhPv|QtM+l@r&%0!UJ^Kw$DdNBrnYLu<~Q)|j2bc2-%so1)!X0S ze?b2swNGDQ>FSSs4{%&;HT#6K{xK)w>eyHe+iYN z;~#8s!FgZ*fDbkL5A{LO$(!(khn_uQOn}BmtJO!wCaH`WOIzLPGHXi=U-hAv<|9K$ zU%%jzD_0?)H^d+_ytGJX((2H8`_8>T&}I+V7v>iu?WwnqCL&=gE8zKEbj3%mL8?l~ zfT1Sqt$r%_ytT~y1Ln=_+7+BhWGXM@$m~Oxcs}aP@$+Ti7`5j z*4Hm&-sh{$4$nuxc6k35y}qx(Pk*)aQV(73IsP(TmNAn<9Jx`U%jLftJweS9W9w`h zF*ro0@$%N_4jnsA!p9)T%$_{K$6KQf2sm^i(caL!qk z^(ww`DwtVCVEc5&Qb#KT1#@Y^iEraJSx{lTGd*pWR^P|l-*11$X^|+Sf_=)Q$vVH# zkF+6&Gf!}Wwae1_byyP3Lnu0~75sWQE7t)bgSX|*7hOJ*b&5bu>i&&^z8`A6LVWe( zdT9qo#BUaOT4^0?oI7UNp{!r6=%W9yz*J1wvpco2^bWMU=1rMscbYkwep+)!>+4f| z>nW<27RjpGR#tlMr|euxX(~WE<>`!lm#Cft`bbk7?S8t(OHYROosI8~b-7+D^ije|fhL4zdEd6I7 z0KYaPyThK6^#2rHZ~!qy4mP-cR3AZ3s0FROkdt%C!&c<=~f8z|EcXCEX@M){z+fZA#A1P6!Eb5Us`G z5eNZ~AO{K}g9kEB$E}Elo+-8z{ztqLQ_lIv~lA*><-k54awivRyQ!52;St*<2-mPx|biO zyO)1GmVJtXcsd}B5mnzc%C$LSeeR_)n%p6Qx28)M!ugoT4X<3NAbg(yI8o$*ate+0 z4M}myjg3t_@hX;OwO(92D{k7j#lcZD6o!r&bM;aQ_c?%$^rZM;H^XjMGGAS$V;Bg~>;`E2UvbQ{82OMKKf zdDnlZAS^2_$T|8Wgq#BitKzoiUA_iI=e3gGvX5u8612;N4>?{p*M7McVH_&TcB|k_ zN>-s64H@Mbmk@ z^+Ivp4=0OF$YNw$cd-(nj(L@RFfq5h!o-T}LSTvOV^zl@!OGFFwC) z@su4uBwoDvzzX5|h^Sz++%^QyoxjZL6fit84Ck0IVR~lfukL9{GF{2b*7ybVHVpO) znowQUPSS@sjV)t`gogV2Wgg2^E$K3KH4GmxS+zxD&Kz5#u7BR_rUF7I_lfGt}CCJ+=F8#7Lc`KpI$M$bGX!;^ws-JSspxQ-|gN$?fn282$U@yOb zJ()*PwH=)=!@?67h5y>i@=s@-I%fwM6eMu*(xoHEb1*JRDF+gQ{6En2>K9;`;_Wvo zEN(0I*l-~}KWog0gXyPi5(NKS>>x>-#vjDq+0};XC{sNBq!@U zsStv;TZJc5GjhxT(zxqb>X;4n$;#SW{IO6O|-+*r}TjbaMg$iY_cbe%DV2Zj|4E4(sCSlS~|iZf>f( z7pVQP@Ww40xmS4>rVR;UzrQlJLjv^(*v*i#&7QW>WPGErh(KPOkeFX~8JTBqd$D$P z7|XkZ+VQ)fM_leX{u*5-!KY}zyga!t`F#03BOZTvS!RZ8^Q1AuH9mS@U9diA$on`e)m71TFWuk}qy zJA{ftu#m8R%|L&jzCIuI@zZ_i8(36Y4k2CpdwGC=@QQUwb}USyAC|5cE5bKX%2>4A z%s+kLc%c~&WP~#piVM=v+R?3E7zM^Sf7)_$v#PEFEI~>X0wr_>|NgIkCZY;pEOD{x z(lW9o0PRhUF)LSQT_{*X5|)NkMzqlG5~6jf^JXhPl;HT5P=kO`QHyz@$K;f9Dr z0z~kSbXnqFy-r*co_Ve$eA5;Li<$d36kNNl==$U-^Tv#nUD2ENI>^K)(ui)V&gno= zYb2KVbUeG?FrwcmyPZ=}#{v1yO;IaTPo1?QJFqe&0^9)kO{^!e^2gM)$bEE;6Ob@BKm(ma|63g&)p1Ap+=VY>8VDmXlkuCOHwxt;bT5^DA$=2&<+o zt0JQ>mfyfFmatgDINI7KkC{+a*Tj(01P-FgS>eEuqY*J-XiB?$A-!+N@Y@ZY5K`C6 zF07B;ND;q79Dj|=!fEn+3%o*woqn;_Fq(IB$pTUeTE&S$T-~ zGK!n7kXMp_Bs)`0R$-QS%ghOj9VRGn>M~nmA!D66X5``Y6N*~#o)C5Qpk&%QbLzsn z>dvla+nR6J*g3kmykNz$89(mYboTl^gbOroGZZ`HW4^w4wSq^o$fIW~7cPd*&WfV* zyR*+)&;=NP2=K5XG2~^=frR2Km0dKPHhFYTcKTQAwzo5GT$Ctr(cI5X4o-kXk8tvm zgfNQnL?IId6WHgE8kR3w$)oz+2nINp^RwBXI;|2e=#MEsT`k6l6|0aeo_8tlo5fRh zZHvEf^|lkj`0$8+K7Dk)-k&X9g6404Q+xh;&pMW8n zpfL~XIyj8oOncYF&_MxxdL2tm1rS>Tcw5`+;X}q=zI?+SB*S&ZuUew>(e}{=&s?^$ zjZ-EnOv`B~aq&stKYi8#un?9%rc=&Uo6T`J)VgNB8#pBtogHv3#oPpID7$7oh60#aR?^fN0-sv0AUFg z8ymhZHj*VTGM4FMhOR(90}6vRC9ePG{I%;=1#X;ahj-3hw#1>7=c;>f)8G^A>*XJ) z`9HzI-v6T?GJC})CnLLS;#Hl4ZM|K5CcqF<_pB8a{owK48DE5(MRx$rlvzt3KJ8GA zaf*T!K;X%*j(@l;Ej8nY4=TEHm$(DsA1;}<&zZgW*|Sc9st5|-PF^CQR?HYP^4sWb zPNgQx=BRH9uT{EG$z-ymUo}=$u87`Flv?sbO)ZDImqQKU0j9MMIA`LgW{V3mU=cqf zYkA?%D-yPJklTn+YWl&d!&khmO8fdV|Kb zldldWZiWw^KAFF876h%&t18weY@;ALXmdyXs>n@dj`TJlX#jg*wCDJH(Pa*bF33GQ zvL`mL;-1B=E@KQ~yCzH+tJnDjdi#a=4SLwniLj#XX|-P4XH{&p4XR;SqKP&(eX}ar z{G=<0Pa-r(k_FK7GhcYg9i3CwVesTWM1P2Cjm=$=>m$zO+WlMQ{VoVG zui_7+?1+qBC1SvwGU?OjRnH)2F1>gL(PcA&C60HQ+Q$wVxdmz$&HJp1-Bx@HgO7_1hVWXy_}20C?Eg+fdj|4nK{Xgz?8G2 zdGeSUbx&HlXXq#?x)vV3K@uUyyyL7VLdUC?=#{I_78I>dh=O!;=Aj+AS8h3=+xZ-d zj@0zjl?mJJq&)R&e1Rm1GJ`r3Bd1c*{i?QpgQprPj${jhJi3>Tj*3Xnxxh;}qY>bs zn(Y)zQFdj@#&L<;Q?pkjZDriXxoWHL&t32hVg;aEnvQO;iaJG-7#F%dE+*<|`b7>s zin~or&Z`-kAZKSfUydI>{9&EZ<&pU$ zMX29>G;3vS6cpR9=Vu%G54~H{f|`8gVqsX)&JNTdx6a-3%~UlW3(F^Cyg0TTb$087b6D`K5(+cO28V}3yTVlW}jr-G@a#H%lxU+UsN?B zV5JFUN-pSZn?GiF+KKbF_c>$890aE0Dc`S-igBs_B#`UOO5I*sUXGCC-ks+m{YG}W z6iQ!GaDM5+&nS+O)KE!*G#GSV&Oi0%x0_Gn^2Tx0X2-|ZWG+u>>ZHr_+;Y5(!w$hJDJ!-f)3R6{Ijc~5|wtj zt&!i}WJ2v~&pPx&abX3=!K(X>0|yLmby_J@FefTtNt@&1BGOJ>bRduxXdp79M^DYl zI;*DFaNw$B)Rf%(+Y1-11w08)M~KPDf;}c7_FVaWi_%?8M`z>Eq2sPxeIOBZ2QG+! zSQHphT9uG=Fy}PhCqoL&qa{n0I0cD+lTlDGcD4>4Ht9(ES^U96g&Q*tZe16rOw|;b ztz}1c>aI)IDrp(kJ+A$@-=H=tEkLMv7Dlcps=RA~_y8_s$m~%AORikAV`0or5)8SV z$ZDFCemr{YP^jU#21>uFIqoro` z>LeEn0f!Th$pS(nSvh$L10jM7NpQ}Yv`93;rdySv`rdb6zU+iBcFX4KCyy0`Q$xr- z8<0<))_>$Xa7}bFy4PlUu{wO4%Z}c(R^}exd-SZ6M0iVbK|xVw_6ZjMAwdg6ndVMK zA>AHcj7hRcL%{!UbXmqCw7{;3*?=Hdka1z0ea6V4E2B3%a4z@w@bKKq-!1s+qX+lR zS-6jVAJiHjp>(F3=t8KCp}#Z$V#AxQ3lqWR_1+q0KP zwTTFyX^yc@p1!oI)+%D`CSDi-@8psQYSf`LhY5QEY}{Icwb<@lENS`=s|U!8O%aRjy3hVTSlov2JBcl9l#Dne{kV z^A2g&wE0FS!@O~Dwv-p23)`^M>b8YZH76FsGf)6lk%)zx)%9VC8#!4}3>GYLk!wpU z9yo+g@FtR|cj)@CT-p&KE7?tM$oU zSPSud`(O`U?m7NEU6y5@$3WAXaUi+3UiW{s{k(L8zF71X6m6Z&)kB94(fJ0&MkdBY zCqJ%x1t8qNduym)FCWcEy>-6lF5jg|IRbZh_ilp^%D5JCH2X9znWQY200LgDM9LEN z*~~AU9Xyon#+I64vbAmy(YMSQF#SK zw*!0HPkV!WG{gG^W@ls_IbDeAExd5HzoD-|@7v2urwtfz@p74ce_m6!7lW}l^0XX9?0l|++ei}Ui3 zzp%GfvpzQVwaJQc!1~3U8IKy95m=L4sFFlGU4KeoEIP4UhaT{9<43ieU5x*f?p z=5!|?6&TJncI?dbW4{PM#;UwI?a+3;zuw!>Z}!5~6z)4qd&~HtgOCSJpr7C6@=7~) zyyHZxx$EVyVH1io5Y#MPMX+98`wW&bZt#Dz%G0BkFGCW zIIEN5+_9A+owaLINZ)^W=?otQ4f;n==#eu&3wGOr$&*loyO+-I(8*H{)Xg~iVcnz0 z0a^p<)ddT`Fmrf~M4neq`sJSE??sncmf-{ricZKjrS6I=sJz=Dsw9SM&-`giO4ex# zqO3cQhYc89g?7(9EKN2Ujg8Brw%8PiW04&1SyAA;BA2$pN9@JuEXb3OoUs%3qp^NY{l1ZKy@KzO5T3EP0^MXl)wA`I- zVGm(oceTD)y*kQ?7#!Ob8NNK_)OjafY!u|3S)cfCJ3wWSZ14D+t)M8nQ;^6w2Om9a z+_5HIJKt#d%h2^*HBCM~f9UL5Lb^tO(Y>5f`weY$V{YtE& zUw$$^wB$-T(NlohDVX{T`%(_4r>8^FY;LU|H~G`%P6~?lYgcollDAm}qRRDf<(}ie z5A{>O8-s=-<=G@r#YIUF1(kn!Hh|nn$qTYOSDdWI9Vjbbk!;VY>H#?=L6Len+AOv_)Cy}jr4{{5NoixHJ0a@lQQg)~GR`0++iB%%*C|t1WJ&*2W@Eh24 zfdA)7;k@`JmYPD{plax%DBzDMBXCagIh2W&2reZCmq1=5S0iH-132;|WdUC+aSSI3 zVzziWWuL!^xO`DxJZ4Nu0P=t|kQ6G;U=x}dc5GwOFx0|+^CV*Vs8 zn7o*4WgzRYw7nQPex{8@nC(o6U+LxB+bh7|FnCm}%>^ab(F5Q68@#l7Lm&Tvg%x)( z;Bl!bd-@sl|Bt<^d9AC6;{Sn55o*(k@`k&Z zd*_{xnYr)W-aKK=^iyZ`0nM6vYH zx9z1@-ZCJO|36>#Blq*vx0`#*>q_-|`?jwu-}mImb6^^f%^lb~RO+d0+Var!)f>z_ zbN#oyyB^$JtM~L&y2|yz9YZtoiK^gq;o_&C9651Tx2dd-7?KAG@(UlH8z`5$di!5^ z{f&l(V6*V}qx&!Y^sAmSqDX25iKJrlW25g>ySqw7sn`0qy*qvym|_0!L&L+Drhi#f zS*9q1oH&x0h1mmp_SGw!wr<@yb$L2jz69{tzJr66{tczddnZn|9`Gr<^uWUhKk#rl zXEXo)=-a)u-tOw)6Ng`%OAFOEu3y`|^Zr_?)KlH~(dVBv#0H~j{PFt(JBB7Fzg7ec z&Dr7oy9bKpqk*Hx&n_XGLSz2wYcF5C@I^HEH~*Yn{s;2t*eP*7y>{iBzVhAO<U86;{4@ykva<^o$`O+GBG(1 zV?4N#GskGSI0qnT=YoS=(}7b_mljYK4N&OR+D5jT-CFRR1vY7xkwcNNRxW`kF1L=S9b-64kr#sgJAl>a@^qC>FY zK=C&c68j@&(AIoISa@w@Y-6rz#l(RMI~0%8TenF>bL&Z}acb2 zs!KLB1u$)i4T(1ZD(gkS6AfAifv!=Mh}*UqrbumsDuD^KDWTJ-KhD7FokO(py0i?e zb?p4Ob>MQR+(~dbO z$p0XuUNt6{jR+Z%xXy zB-M!GT0WACRwo+B5@$LY5(ZrYZR{T6o6FkPNlc;GHUTjAIr6ojhHN8OG7*=Zaz3O; zt$4bCY%?JRvm6RKL||{;IirllIywID4WbTQ?v&ew%d3$UIu%m#WNk&lC~H9|NG)g= zE(fSa5ojayx!A+|Tux1db`??Z7Z&?y)#hIkU@eEJ3YN75sTH%4IOA=Bpa7d%Zd1F- z5N@LJAq>qi(S9^HE2?89aSUz~YC-8+O2EqhYEs7`>b>Y$BW#2Qim1QzZi&YNF_OI1 zK@y|U2ej(Ka8cZ6e;U7+LE&t*HPVYgHtJFA9<3rV>qOCi*gLo7w3Q%={{Oe0x06)B z()8ZBwAC7qg0KT5fY++7vXSN@;bLo5$ztI7bou8R*vD`4Pve^G_ImS5nHkGJ%|(P< z+wD0U+nLCH#>OUmqpJH{E-#hFH_6em|Esr6HPkS^WvyQ~*1CmFDkh?it(A>wwqkFY z5=Y*%9K$59M_SLcPdug8)+HA~~MX{fMTT{u*Y<)QCL1*F32 zlQDAS?)aBsqK0m%+V$?Y?liOE+Fdn8Qg5BBWps~!j#mx2tCoL@Y+IX4)9ds%L0>mD zCpl_TOICVCALFY1r8l@TT%gNZ%36Mjq184Tn})%zz#I#sqAK;F;oi1mg%jhz|rvqIE0C*?~WdMK+botu4Dcw!^N`)>1d{WS50Dx!W z5?#KYG3%2R06aD5G629cfi9!V0I+~AqsssQ=rXzt04G70(PaPtbQxU+fRmuh=rRBR zx{NLZz)8?$bQu8PpXsv7iqGkC4N$E$)W;1w^pW6rl)|wflya-}ntPfvi!CLP;YZ#d4 z{qV5Hle>_ji+}-%yp4bWl*=ON$b)y^w(Z&1CP64L%uoPNhB}6Z2&51zjWyG*TD#y4-9w`*b;~nW5^@J>bbyV-O4!vqC}DJsC-n;#_|}GGsaq zK!gMaeIBWx{laJnWxuuV8gp!LgAma29f~K2AcNkiAbv8X#B1tqQCic~w>IZT26 zv(P%dAdnpE7P9J4tTsox+I$-QQ0@`7Z1-F86KWJZ)Dqx zFXa$&1Hhnn6qmkERMnZzG!#e*6o>-~W}qj$)wt=}TksF60y7|UtE9A}xyFPw-$96@$tL+{k z_PcU46cTuXfC;#%0_tepKx=AdHZwt&7cE+}V8McQ>(*`9u;Gll96}Uv%~p8PtR-~d;gQqer;F3@B~Ik2yOVG z(WHwIGay0}GH{Ol*_c>y$Gus^!C_FB%a}iK*L@RT90UhVcme~!-56|;t)qV+anHW5 zjpE`X&<`&>V?M|v?CKg{*1Swd2r81#5{vo zwNab;_RIaB@7|A6>8vF~GLVqS9ROVO4k`)(j7pcQULVo?q1B@+7B4<9@mmw#*|lri z^G{@CO8(8)eEcej1D0}xDEQiVhPU^U(WE?)8w-e%6{a2fsnwiQ>(B;w5 z(dp+gHf`FpWy_W`PnWZhO$idoet!DV^*i^B9~C0F+n_FU>Vzo*idPqK0vvBn{&M@B zYuX9`QfXhAt`v@~7@a?N?$GR^v**rxa{F5y(C$dsFhCtKC}n+c{945OV&DeB2#Prg zIyyCZ!!@^d8lZq{SC=R6T)FhI7hcc*R+lO8D&<}MWGW1F@B>CXtX;KY@q&eUWGeE( zhdW>0{t7ts)$(FJ(C@=VnFIlH2*sovynfobre=n4W?gm&Q6LZ}UV3EB+q?ERASpP+ z&e7k0y6UpaE*`mX_63(d`rHl*vdQbNxcvXK&YLxS(bgB=IG)nlu_fmZ|8LHm`R5E> za@KkC9)Id-r_^QFUd$gJyZ`$y4+7D02Z6O?_uaGV?vwjYk}I#h>8WR4K?8($-+O!Z z$oc2aoqzMfC8`;E;=uUsIm1JGzw5Ij9vmjGh#s;`^q_u8cJ@QnQPHb4Zt0S;m5g8AV=!J*pl z;)-elJn1%ny?4KE>!#p9zlbT*El!Sr0?gF$=#R5z6<61C>fEGHPa%prL!t2dW47L? zs7(1f%?+Hdtja9N5|~pY&LxW%2L=Rdg2$JXmQlss*VFdR{(UjqVl`v5lO{~FIyeAu z5A2TF7@`^Lr_q+z-E|Yl0QF^>efW4)`Td14cv0jONFH#_`qcOf)i=#gCm~g!P&`e{ zQ4oNtxd=sFdUQ)1cO3HzS>hChm4haEmIykHrOHBmM>i8Gy_x>bqV0W>;TUJZdI3`Rn?K8 z2!nbM>Kci`Nl-7kkK{WT@Kk?AFn*6NYV+E4Ep1PD1%^^%L0&c>0%Sz6srvX_Dq98! zD4-q}#~>@urAVkxmwotn5ni4yQveVkKm6fH;>FtAmLXlHUKiv+LKIi6Z63&$YnRU+&AVU$I1rI>I^E9d%Ijd1b35N#`hIIe zo2SP-ck;BV+Up+5(z4St^%(%b;OcoYZ^n$vHFYjP!P9!5I)WuI0aQ%VMKw1xrR+Xv zvXbJe8hxG~yfuE?rlRueq5^7lu3o(0myY^wTkjiF7Sxp8fX8&|=)QD)z6fL-{O(5C zB}(ifl@~1i(BB!h2(Bd{g!z z-TCTUeSiQc-tlNNcg>zQQyUQGA22TMcs^AN7@~W{+w(Pkfx#i;Gjj?A;@K6mYLfP! z{6_`)1xy_kI4Lvt>~P?i{zjKmVxqd5n-xkGKmc3>+qXqWZQ201Mcz8^t+!8P=X*6$ zF&d3w<0g#_ium}G^;B>`b~OF`kJq*1G@7uLt3Ml%AON+-sZ=VBg-Cp^?8C>)=f!nd`i(9VP$wko@+FHkqcssh5g~yQ*B^G;f%@hD~ zbUR!O995siX6t>lWm6I_LS`fFkKbeWmx9ZC3@|I>|V`J;@H{bucIXC;v`I7PzdOcj$4;L-!aX|ne zh-VM&tE?@2WbIoMnXCwa&|Q1(@X?$CJ23an+2Q{G_Ujl8)rP$>=jPpZ01A_-uk`1` z$Nw?^Pjy+~1&Na&Il({BAJYB3`erX601OhC4T_zK3K1cSNZ7gm=Dk+JP-D1|myyaS z1O~eT2@F!FU~Y=plE85g7(`%1ml=CDjG^23<;37%jV34{BsBBnDF%R&J-gx?>u*(5 z8jtBu;aA+vcWYO#`Llz-O~COCaM^nsqql4pcm+UVY3-MZ-?)L|OC@hcOtkl#7{m62 z6k}BtV1?ZO{?d;u7Pts_3NSY?0O?6Z$+^^=^XjZ-jKsu555A(TIAp@Tv*d%O`|YOg z_StVvEU&I)z<1wgp3FI;_OU%(4;RjwT3B<_KBUXctIIM4=x`V#DQ0_1ePha=Z!8Y6 z#8{ej?0X2d!OJ}r6_qjpbqVh;e!Jz--41j6)R}WDjkORw>4y&-Jbl^>9Ql6#^@((LW(8nAUH0MQ#d&GjWd?Y!zqXw^oSkJ<9WmRk_r_biP>Q>|6)jpEio3hJ zyL)kpySux)Zrt5B?(Q7kcYS}qwaz(zW@Tn2nIw};R-Qb$uT;m9`vah_tgrZ1n>5}= z+fRC70DtxGq(>d(HY;CA2u!PHFDu3r46LR_5da6?TLsR-KzMEw5%U^}w|5>p%&4=- zKqEU_bp5U2S8@(+{CrX}p$n;_xGgB(%rVH;Iu&a^;HfVH>gQUxMz)FpBQ*6Ru0m+r zPtr&@KptKU-CkrRZ0vSd%GqzYY$MV!`4Ry=|DY)MHP0>Rt#nWRJOEtAkp_?zc?u{w znonToh^I4uOKLAi$u91-r9Ht^Bt`vd_iNsWDcb(k+rdpglvdxUyBuY zVl-&I(|kME8rcr=atXe3L(K!c!%%qcg!3SV@0)D>4_vr^km5T4+Hm5w1jNJX_0icM zw<-93{`{!BxV847V}FniTfgutV#P5 zZ8D+xxDj%43dPJJRl~ZKExVtK3{84gU|f ztdd#vLmGQ!^d6KNP-2vZg*lpun13JeRbmbw9*2tI8w_-xQdd9T7sR_rL_}m6Lojy4 zcj#r#%NQ99`UY+{zY$+LZk}b^GyS|4i?c*haR9oDlbrW+^b=(FOjDvTRA7kt?||M7 zI9&D@<=@EA$&(466R(}N`Fyk+Ki73Md6H|$4>@R+qe;$sZn_6i_9@j()~mZOchS%3 z=K4twzHa4(=M5V16;QrI#@>%bO-ZX(j(fE=D+1n?Vq@PL2Yz`^eH95Mrv3&5G~ES| z-Dqf}(kCdDN3MfmvJpn@dR{CR(txmp{t%UZ=b|?U<%}srz+PK}s{EZeUIx4vH@}zJ zI56S^c88C2BAxpBhJGFbx^^+Zyi?wji{zmlZwm-5p)b{YcWutNVR@YXw!nQip3%r_ zaa%}ND@bWOBI|kDZwDakU!Uc%*}v^w;iAZnIAoTCj~IKYW6t{D&Py?yMTwo%=}oUcRvHYhh@wqrHj6!r-mpd(Q34>EKdS_M?_hW6crOxpP%#ChTw9EnKbF?MrH_L z(J6m}`GGOOU{o!bf7{=X{w&qhl{^t0pziE^^P#1k7#R&wQ0FnpnLkhLNuK+JZJ0hD z>HnR-JsMnZhn2-$441*5;IU3z7M1+yn>B*&mTRW`h4A%rsecvccGdPQ0K<|YwTXtw zYHX?7>@zi&K*OitDk>v7d%JA&3>#v-+WwP?zUD}vjbBw{FsC|(X(ijYySw^&#UnR2 z5$iFpL?KrdOD?S`(2O+|&=rFc$;&MnJMg%4Mp4J|UBXo@ zN_wqDsVE@nmZT(gD`uK#m+#e)jszEd!Zk;Bw+IUllZ)C=Q0`6!ijuCA(X5@Vds3zh zk%mQ4K82YYP`hn!r+WFgI3yDt`6oZOta3-tbc=x1FAwIUK~4ZI(y573_xMzmk&f8& z)M6W(@3$}>kQU?RQ@D&&Jl8j8EZIw*0mKaWwEe9eDKMxg!~6EmZHspsveizKuXemJ zAm07ixG~*4nLRK%l<|8&0mZklkem6x_6ZIp;DAa_&ksG@^(|MHtDyZz2LRX_)-?lk_Oj5|A4+! z2bY!Y?RDs;oiC=IkM(JE>$m_8K-pM&1|(#b=*RpZ(Es%GShLYKWt~3EH;wYvXCols ztyMlQ?tH!TPf2;PJQAEVXmsF)D>b!r)Pik@;s&r)Ov%&aJ`U3c1rZtuOdnrswB4{w z8-VeJ=x;+vf*oK*L*ZDivs@OX8;|aBrA9c9sHxqS=9W>q_}5du#YGc7VTh5g@?8_g zxnD?&jt*wqgL~zGL4D=1x@^;Mz16G?)Q7ee_h|~K>!vi!yf91SGqAsE;FeZO$HI|b zE1~qwoh4&sYmhC)q$%6&;(FXX%~-X-Rl*(m2n!c$0BzL9pSFHaP|68+xfDl1|H|v? zCXZbrK8+D^FMtYVRWndsSbt;=eI=Ykx-V$@J%aLiA@_SiTy~{t>*}9<`-zKdy7>4E zs~2@jy0H)}OeB^Y?y>G@2Hc4U4L)}Wi;oVL*PXSo2mpx2hxgI;?Dm51^=h)^K*yc? z5aRx$U_Z8Rh=Kt?;Pbd)at8@PzV(a9pTz2hib`eVX98$~v;vP{2Qm~B3f9c#pi1@V zscEQV7zIZVg(!Gltv{{0!;G^0+_94rO3YJCzqQ0LFH(RJvqMf^Vugl7w~J(?(jbfw z*#8aXzE%;#q=;e&Ip7}xZdwqlKOD%o74DQ8g)#YiKI2G+08TwpO+Fk0`!5V73>d)o zrD3&BEdDtqT}mx;$b4#SK4N7QYGN2lf33fp$X~u7h%%%H{g!=26h<*nLr0x*I@W*| zG$Z&U-oo1g1lwbl2UQdGfQHz zCNgjQkT*8FIy@yStH~6`yc;g-dZ>z?=9jP3q&Dt?M_&KN11;>ScXx!EcPrwBsp9yOF`Qgtu)pCIstp7MZU#@F?F} zCZ;+yy&Wt+Qf9j#RLvUB-4DaA46^*WwspGJe01pIX%a{0TLz$M&U{of`l>oD7J$c| zH{crgT{ME~6Cb`OS2~qp%NuGLzNRufLNYRNs{Nh)vJh|%f`}C$CcG|)Swd!a{-d0l z|C|1gZxxV)0t6?_#}E`X28sX)jJkBFvm#C^c#c?5zTt4Qwgs7Z7_*m*TTSJBYpzIl z_;N~Bw6qkZWL!v6ztEkRZ`>B|JSmh=JPZSjU`KH=Z?1X7GA5epYBZIdLImoTgwVD` zD1;yiWWy3+E+SAmKP?`Ov5gO1r<(Z-5aW~InUWr7AKSsA8M_whclq*x1SJ?ZG>m$! zxV<%ao>rJghq>25cN|y)Wwdhk^78nVgxbIc5rE@~CP-BkkDiTs`FDG?$ykG8q zxS{IF#DAk(O9;ac#ujSzz)-ALCE8u>nB!V&4dJ6IAoU0(*pDD7yOD_645vr7X|r5k znaL>#yR)Xd=T%uLG(;$Xu3cRj+`L3A|Fd1%-)y_Re&yVHir{O>(U@xp_B6zHE=qNF zr|u``DEPP7pyX3{ilx`q`r&{Q;8C8v9HKtB2|wi_szzow_p`v9)e2C+(aVz>QWIfk z)D`iD)jGnmgXGaX%?weEu%@2YX}uK+c~F^i+D12tQ&|&~QDHJ>(8a6av)wy1NRba} za?*0wm{-65PTUdf@i#FR?w8@WHLJ+)h%;DhIMjk&#z91dJCxo1pt z5&nrz$u3}`pZ|l2OLjFiF@CUg%SeEZ3ZW>Odm%M-_;GJ5N^fS6FT0jRcH#CZ-RO*^ zg%%lE;fqLbr|L}*JONf#EPYdFz3PHys=EYM$)PGUb0df68`>aZ?c@(5Gu#J|>QP-0 zs9C-*xn1a|n~D?l!!fMaN2&BfkL(wN{rA)8Ahd%_5{a*a_R7YwbT|Gv@%Ij$$#wq7 zCy3P8nj+gv6tYhJt)rhdCRdmC4En|40D8XRO60ro^)$nyksD~muj9F^(v_;Nrr^n0 z@YmWcCVY8R%~S`_6Z&HN->W?FuFTYIjJ??a?m^J@MRG>(60V&0dD_G!;@#$ADRLN` z<1`rl2QMWriPrhqf`OW&#=4-gAbFgXn)9bia(QXB6&BgDY#oVQNU^cY!2ss>k&mpg zQRkr56v@oPTDDjk=r8FWSfQ5V$B2}_)M&xwPNGmioPG z+LLI%w||uv!&=RkKwH5indMq>KGRj_bs^YRToP$KDk{BHw&`6tp+Un0Ama5w7JZwb z)zc&O_i62;>&Kj$Fnl^;j(8T#@cF{((f%X1%v27fai34QYwI0G6#nfbD>7Go|+ymu!WjW0zmJuqS@Jris>5w#2?LPpTN)QpPM zoG^OvqkU@+miX;NrDfM6dE)ls#FS)H$)QcUG(~@4-v~Pu+OlS7f1$Zh1g@>OIh*2O z<7q*N=;v^FBAp*;8#R05OR_MtUv48!O4Eu5Np-9xnxKgbgRe`YThf(E>y?=SHbzF8 z&ip3yZ5H&a5{BHgOxhPCM&gqL#xB3=9OEc-;xrRUlgL~N(kYVsfCw3&!gb3Yk-~h) zTwFV|ij{Lg`*Pme>!VdVZnovYn8!Q2k-hoOx{8yP!R1(T8F4BKo%z0e>i5`p^qp(pAa3jYs)ihd9vGDkM z{B#h0G~+8@L{9R>nB>0k~rxT)cHxO%nsEgPx@~p;{T6GnMt7f|Kc<+-`)3X z-&OzTM%sO^)*KTT?_E<T|2N&R#79nZda-%jqueqTj}$!Edc?@7j?0>G3gn z-1zT6ZU(4q-n?$vvT60r+lK=;yd_#2MmnR{ES~Y?{yUqYq|@3lsG4nRXl&{MZO3d=#`Qfa?tUxKu}x zE7fOcr|dvAJCb)|PvP8rpkmRBkNNZY*}_+O_>AkYjQ7XV1IIkmE!lG6l0Z=`kxB_5 zVYz=sNA}BhxF%n~_5=2&%^wosq*B?mEs26o=_mZNsUc?*T}4m{8YO7m4k!^(9iuzn(2v4IPed}pwvXL`j$=twdIAn zRTBo==vSGh@g&GBIg}|_R)l6mjLqvPeq3aQXP$oTl8=yeI&wL5#9-Th_P{~f?wU~WJayW24eCsZ5duTDMw5-Lx{ z>;B3V6G7)VD6u&TMaIsr>ABEg1~uFC5F(+3OZn^IZG zKO+$f=iC!Z1YWJLKKN98=P#*YM9@bkI=q=kqEV^DM~qU`$O|lGNHui}Ow-U<)sP81 zks~|l{OI$_^myR$FjlljM;Eq_@>fG8rP@u7#qDQ3Phl~@{Eor|*k|26Kj|aP2D$HRmOZBUmj_^lxab^^gPcE5HjCk11_%Bnwe)~^f0{sc0&_o3 z@!Js47ll6%%iCLK>|Vy z0w_r6?QsW4hT|zuB(?L043I_Lx=`0u6M`U*f(uOrCQh(-&`WO^aCN|N3N2)O2=5f6pYLM+eD3MI|PQ* zR~cJG)W0JyL0%TQgxKrtyE-RKy>$L=pqy1W6eDkK{gt!=`Z$Cs>J}idI6Ms5z|+N+ zoSZIyAAUfUFp%7cBh^RTU{J;s22E|31Q?P16>>PA4<-}>0ygnCZA^V=4XJOYaN)!@ zWp+$xl4tZd%f8!F7Eu z-MaNqDE<+pqVLn&U}r(0!lUQJM0d~lVs5n*nr-QIKgwy^qS2RMPX}<*>~^K1OZ+fO z9498UlTW6E8yH`aS4BB|(F~_9F0d?qA|rKfL-0j$$i`xXfdT%(1i8y!O*YuQBtV!> zt`PDIDZkTwOXcTg?W7`9tx~idq-J3qVy4Yi)9ud@JA?~V_3w%&k1?WB??2SR=aHI} z{%Ovd(ltGjz!3#6`V4L#wVZ)Myh*vvwL7kg!&UZQpI-kfm>seYb~^o{n;p+7KSq;R%% zVWoIJ+@F1uga=KIt!a7{V8=nB(LkCDGCx|+?<0!JWNPagAL-Yxe}h6i)OB7tKhvnd z!z;z-+oycBHBQZIC_sGyS!7KCy5mM`h!|vK;Nge-(NvN+pNegtRy&(4(45WP2#HH+ zyw~j>=U4&oLT`ClR%aEK>51R9pRf)902`c z82M)s^%}LWYxAp5!a*)o+C8FR3r`3nyb)F%{dacmznfWeS9iw4n?4@8R%R!<;K%g` z;Z=tTYAM@jAkp9J2d8>CC#N;m4OYzUBQ@uP=go}M)Asfi_g268+n-&U8Pyiy0Y_BJ zqW~8i8V8)Bf{S2sR`SGAfG$GO=XlK_W3@^PoW zb{Y;@#RFclhzz9u{EP)C_{1Eq59q}v#0O}+bfbTfRaE@kTZZm-!XBv(DQgM>&lE^P zB-rE0&UTzl=IR@`oQgZJHaR;udu_5*LSE^FrRRWdBHXlj&7SrSnFY3>jQDBEagN9S ztOp$3a*U9f;M29dqXFPx6u+U|oozVsg?vBL`n^QE3+;D1j@qAmPWoJTf&GR5a&h@5 z3mzmcY;aG>U1leBq@krdDJ7vai_vJh2>Q2{)R;1{q$fhz11trKq10bbz5&R;5)4s9 z?fGr_;SH0FOv8U}Q1OSaTxUV-+(!XS|DeZh-(@nrGS?5k9$~#vc_KMMCF=2`!@x>w zYdA6JT3n;}dN+3Ze+eP=Rr*EuVPfkTj?ORo#mbj*YpT2^{hJl^PJR#Jnixu;kvN9w znqj=4K7(y2f2azI94%qINZ0(rslLizNcjI_ceqlW$_ zyv3v5n2O>GEu;kf`2A47lU=v_)=D4*2_IMFLJicSsw%1CfTl>dVwwWv0L+M)t1M1l zQGS2n;6X|WWZ(U_^v^jY_H!K4(rh>(P(`YuO)V_rA2wWz-$`C^lT0lCeACLINlaX1 zKXyHju=$EmR&Fr|Oy|nzB6BDC1ERskC&mB8_9eSd?~|XJU)Yp)qbBwmMa7Gz)R#xV zet>%pvlJWuBC17KTz}>5XXC+@kD!j#Xw)u>^eJz+lXS!Gy#T>TzLkM}>~qJ?-SnCU z#@#xA?_~FH%M6MbgTy*|!RiswR8H>VW`ltEib6(Uf!hP&r*v~TqjKIx{a zg_~B%0%qg3^Xp0~bsj*~YW(6M4k;5K)^%~VlghooZ$=lr#rTh-g`|v=k+A0)d8)Fc z$P1q4dMUxYpK=FLch}yEmcQT0Q_|Y$kLaTWbJPJd4D@TnOnfqxb7p{wbkg$*?O+kC z2G>TplvG+FV9HTz;I9Ao<{yak4p(0HS#$q<;Fi=?3eI9UB4h`R*7`(}_QC4q+CXt} zq!?diI;P`|&g=?}iPLCkjBo$xg^|A>RtyFvCmSy=#$$mpQhyAq;nR%6;(2kT+25*u z>t%?3A(_>`!_9d181nKDd=+kh@|=t`zPI?b6#PT;l(5K!xB2b^L`GWxdov3p~B`Kf0!IFBmeNLssy09_%uB{9h9T|V`Av1b#BGsgCZ}tF@Pa!(}I&Eo|iOEmV=huP~YAH*F>^d7W$1+$qR|U9-y4hBn9XPPN*>)DdW> zby^i?e=Eup)jhqBAH%Zyaldpg*Miusbs)An{?y)FA7kU+*h-Z2K?dTAl2iQ>d6@^`lLn?gPZuL+39^#s~aD33Mqi zvtM;zNb6MZpnm#TYa;~PT@&1XdlrS1B}ySOa3=08`;d;R#!DRh!bNLoHC8?zwRe7c z>WPQ@Fz;8lh_TvMsd`S<6AO`jX#ZYO2F+hWkDgiG&{SVtCo}mb68-l~+yMcG-&Dk- zcP#^NoLA>~=eboU2SQsST$+nhm9Dm)zV-fZGqHQiV}1jeSVN;|uin5DDWdaqhYE*= zMG_}u{YVrJ;!>^;`$D!wT^caIvSDarVP#-|k4I3I>kHAW=(ztlt*C?iGH|~iX18j` z6UyA>Vx;~4d~LU2WOo((kGJfj=5$clU!d%FF1E!6F!ie1C?z)Mr}jBr011b0e#`t& zk9cKK10X(rwyZDUXKT%MaQK;yPZ>a0AYFfSg*!8d^fW^daH^;^A4wX% z)Q~+x_6YkBfhglpO|GAoul1E*Ap$8arIUrV+5l3^$Fs)MC<_RnOehC&>vkx9DZvV< z)M9{~l52jI9ew93Edw5L23~Arc{SW3@%+O5zQtX>Und_$&Grn7$?AodXSMOAgc+Y( z(FxhwhA*8D>hial_{g0;l4@#!-?r}#v;2&NB)zRKj!tH$syldv$J5X=L&jH1To=x< z53S70Ysl$q(0dat9oL!s>DVLacYfBJaieMIluIEp|bDkS4`}vN%Z>@cw521 z-v`>3YiqCdDhK65$P#;)=!lvjx=igH`u+r-^ZincSKNAOV{7N5(I;cz@|Ih(+7Cu@vTxOe;&_^Y1h~R{7x%91O z!D}e4JMn=oz&8IkbAZ8P=i^+)>W}5=*vO5ZuaH}S(VSRH0u{BP+4u7Y`1F7xTfh`g zS##T&?R#}`z{n)jm7X{9d%Nf8wMsU~DD za=P}L%xDp)8pUL~M}i|5kdMdXhYuWo+cl40hJbXoEZ$9NsXMr-6b}wK8X4iw>h_!9 z%}#D#Yu9qqk&&-v^PKN)Q_kK>f<>O(Y^wq;Wo5`|s32uS|@f7z`mc!L=~M$@aTI^&4MI;!f+5av3&Wytro{dwsolgemUv z&Uqz}LUlr2XE)nDqvE0oM554bj-XtcC?9FgD{SCR_bMxget_ z0FU@mr4Fs5+)h7;1gQPib-!Qw6b_lE!-x$}f z^gY(p!v`E$>lkiM7Amih&qe?hy!AdB22V2GwRmT5FGyKRkcvw0*G8w*Q3Ls38!{G4 z!w2^h_YZW)$^}^mfrj|MRyXL*%)Mgk$s&mB;RxTh!X_rfogQwFzjtz-Jzgqq+7ndi zjM7jECM6~|vL6Hjv$MDIvx*|MStzqGFIBfSRBpnFKe`Z4PJbC>CUq6QvLDvUFJ!dS zd^P#U@jDo{+(+{+5vKc(qzWksmfFD)djWXcIlF9N;q`{W^7P$$*V4GfwI<%eLWU1s z)^UOZQOnfY>|t+2A-jlsMLT%lVmFM0&c@Z1d;hSh1Mq@`?{?RW^lzo#H-WChfTR0% zNpY+8I~#^EPE)^x-K;+kBRXU4`#oQ8*BGtRahj2 zZeRWEB+BFBk#TzoHZaKZ!)@1e{>2p?BE3}~4Z0Y_<6ttcr9bgYo0E9yd)0Sh?uJ3f zf$TL@7yNQr=t0Qk%(6wobLImzbecDL!llD(lp$f|zj!Lcj3}COh||E# z%6$wFZepaF0Hp2p&QV!GQ2YhAm*sjP93g;T#s=}0z}jPDdKTl`r`9weX2mAR zFgw%QLDhDWsR%)dDqn>ZhULyOj`;1Wb34#oH5;VCsxY-VlZf&?r%#d9Hxu=>9;)bT zzbwL^!gHAxubU_ij_(*5T@GL_?0>?vcV;*{COg4$^CDfBOhKu86vyFCyg3~iHk(fD zAvTinvw-@_wj(85{Xs(5B6_V(O?9DJcL4oQma=oZ0B0eL<9(Kj8Ek+M{Hd%){EL1X z6ZnLp_aak3oCrCvibBiifJ@bXMP*R8$hR3(ODbG&pl^zj^Us^2n_;=%J zY_;vZzMPsF?;lh^OlIu-;~^6^_Fv7N>2>xI2$a0%Nb3!;<;C@z(J`JixCwx()l)@a z`|mtJ$&ngj!gv1JZhNzml%GEl9oP?)<`#^=`4HS}OVg%cZ06n~GW>)OOpix&n`f#C z_8G9>f{D&xWe*%8FdJ>{A`n*<0}jJ1+W5?@{1p#i`t1=8HzT(3Y@-1pibx!(v(2&X zaxgmM%w~r@2q+m>KQd6zb_Q1Y5(|^z56O7R1LeA@*sek-FTT>M15II>wFgi=<3HgJ zA=8tmd$oO+I*;W6sP~)AvfR%vE)oasySh#Z@-s5rTqngv7l9x&3QtNuz=j_!L1!Oo zhEFY#DRl`cCwYSpFerhL zh)x|R=dNwk&J;+1h}_51*yOy;2#C|s2`>c|SodE{l(`);Q9!^rK;KuOYmwD6#X*Qj zo+c4Wgl%f$aloGM%g71))ceB3vW1kN{7kJmzEZ6&fBC!a@w-VvLiaS>^vldlWug{K z#3*scn~^!bNJHKz;)cLK<&}lmJ%R=;0ibMa+Yk7->nM<`i+^uit-iL60CbdblrcG9 z#p{nvj9+6n4YN@rD(>zw)D5h#!AyUeRfdzL8NSKtnwq;h*7?MURDe-*e}_kA`sEw9 zw;WXHNbRsfZ0$8k%GmHE^C89Pcs_|71!1*%zN{GOyV^I{EG0|l?W_w1>+k4u8lN_m zYA`K-#9X(`Ds`=I+HZ{&0}lP!lQkL6A}=Fz_H+*wsvb0`7Pr-X7kRwW4A_CZYqRga zIIJ0vwGn)^VOiU4j^UT%Xhr$$0s6}bFcBD(5J$RJQ_olDg{`G8C7iHsW3VgTJI!0B zu$W)a;aS@~W`Cfjl@q#+L4Sk9=1O}eHgKT`JjKn;4WE7)fgS>>q~0&P zd{y;HIYUv0tiN?_%Ix!VPwtyJxR8I2Upz=O*SSOr!Nk|Tz-Bp;p9o@7c#wen_q^i` zlYMC&RR)(D9aEbp_LJ|Ozp1V5g|PPaO|11gsCcD?4M?-Su&9tvykV1jufjrO9&?O& z=0k+tp!BiBEJwwX4BR%2U^3_4t-F?tV^ww)-jvksPP_i5mm^}6ZgMy2IH^(B z2=OCMw1A(vk@gP)rI}HgN#1rPXr5jTGNsmJcyB{+zB-4cML(nc3Vn?f;#u%{ECI>u zBr4XqpuD*I!KuRziGGWDStsuEsUxBdvZ_mwWvnenWYERxk_LTsEp>}`R>mEc@gqKa z@!}&a9h1wo>N$Jq0ydzakX()aQRjYP0Yf<|Yq^z_;4#%G%*08lQN#v```{+&oJbqk zw|kt0ds> zdKf#X=P0%h`GSG>hDLlos?g>}`eLS{7p7T?cv$t{;{QPMKjl5YwmtAo89_|B>UgaC zEUhH*`N+i0J>}kTJ4BfRXX+dX7I}97z<_nyS8EwX4&Cs^#Ka)Ux6CF)h>9n79ep!3 zEO^_7%gU^}E`9lhg=KxclV4NZ5b9^tAKg_*>534Fyw6S+mQcbtj2NEEB7ByvN;+;n zDxOeg$qV1=Elf{?aJnGaV%sccD^65^0!K$Q){^ZZcj{z>siJncwof(x&Wy;&;CdST zEjlOg`9ZR!tJ( z<8Xi7vO4GoR1tN1(WGjzAl%l!`t)noi1Ds1Yx~JaIv6j-ELa(|x83Qzp7E(!$MKdj zAP5WlVr2aH^at&$e%NZAACq2H9sbhxd*gFH!DtGDYA>Z%Z;g7f#a83oOTFDn2}_`9g}>J{gy(ww^iuR zN-SQl>#vyGrfj-^X*Vn^6?%-D0zeZ9=gC5Y4!?frm39&1WmPij0CtnFK!N}wBRN*o zrOO4j>wD$L+vuYr9(4B+Olp>}^?_HC3_6a?Ijx{>zoZ$z*xn6=jgttxdv=$U=%FyN z_&3Niyc)S0b7fyDk+n6B`Gp&LW-6m_A4R9eg%^tah%b(6&0?!uCn|$vC1wCwga)>9 zDG34y4mC|p_X6MDIhX)yO4Bz`tFyH#!5Lo8G%79ZST)1u97FE7OZYEp}EXM*2DB7GsV$@D@F$wR3;5taFVgoREyBE~gN? zKa#Fomq?)ZnIO_MU0eoKQelOH7ljo?q{_ufa`qH2;;BnLNE0(HvEr@+D|C?<512FD ze1Xwebm}mF+!nl=$2s6o?u$7_*l5)ZK!LnrX2|mQ+#A6x$5iY7h0sUt4cz?qlTHIf5W{cp`|U>95RjEYR?GT>m6US&r#S0#FdDLW^|j z>hrRD`A4qCqpspC(K0kVLH{5A=ZbQ7yU$$WmmqA~tEhV1U$BWYTVvPHMm*3@kb z85d+0hawPk6T5IfmLul4tcgsPQ+55Fy<@9_vxCR893(Pbs)I?Cx;mYzu93t0bCo-W zZ=b}Zmx0AwP(eoD#F&i}f{x9=r3Hh^4-c9m(38r7kTv#fnG+!d;7htX#o5)(RZWvi zi=u2Jk$wE&5-+WD>1BRKMM`wt*?QTcGu&MJmSE33O%MR?%5k}Qz6u>DLwy&Atwat7 zuu*?2GDMC_8|GVHQQD|sde+dtyD$zLwro-e#3bd%F9xc>GQi)|AS*bIC_e>JBXWGX z>};dpGDeF*TR-zDmssK*;P-H-`WuutOU(VlB$|n*_|w#{`n@bZ7&=H*jZ2MQb#3V~ zRfH9=zP@5;U>N{yOb=QUal(X|T3KmXTIn&4t=QcGGe;08wyw0Mk6YpWSfO=rjz6aS z-hJgyV7=K4^-qL*?%?d$Lhop2Z>LE1*tf5>O`~t;fm)zG*J*dHNe#sqSr40sh;^5S(9HD%S6x={tI#a#D(EPX% zJ?mdKIOv25V=VZa#ilFTB&Ro{iHYj#xm~;& zI;?DU_AL^khw2BEsj&5aX7&;fvw>O=^lDr#ru6*$tiHwWitJM&7L!mVT(Z0cCQ>o# zxQGj%)av5g2Qe*`Fx{zlCBp|N`CBo<*5Xh@&6g8#k@v>CpqAO|(xQv~DWk&LMsgNz z$e6;mI!@2B7z|d#?=Fo8ng4+4ygH8H(U#H>X5bQS_n9VI`eC$!G{cAd4gMmR*i$K#(nvcKiwM&b+Nzgl>(aK8-Tg9Me(AXXqr1ZVahT~)=z zOmz5=JqsPC(BG`8*vQUev4uRozIQ%FHdItR34*|qJQPJtP?yEeIT?{xzR;j?yDRiQ zTUr(-w$oj(wVsuhqCK6ZlcngvQoayA7JBK_sGShZQ%*cv2jV8^}mu6X}R6mi-94oYFwY9VH;kAH(T-7?(5Z_USfOsBG75%5*ca;?RIq4isTMXrWj4nhz6CE{9OGqVOkw{PH~{8YJ6tQ|=> z3T#CDQD@Wf>ysCfQqzHUvM`(h97_wq{JK-war&$lYm@Xkq?7f=^Vi3Rhh3Kq*OT`v zorJ3m89qxk2J1?mkDj)R??YRIKiE6loo2n@xi}v8?$1_dQn@|A_Ls@x)zgZxnf-hk z?*9f`Hj8~eoVOGY0xr!Oj* z0~fFbTPR^Hl^4AqfNxaJNIOYAPh9!$O-b#S^<}Bort8~QUYkoNa4`IySbL~iP{bX2 z<)wVh8F@;%22DtT=WdQUv^Nk;ujpCKB&$)D5HjWc1HDhkN&XA>4ap=bT)%z6KZCaV z9$nLcHoDEI;}Kzh(~2C<-Et5n?5MT$De5|#yRAA@*E*m3*&X}E+ba#Bok+MI?(Yb% zh1xjY?iv)73bnQ;Gw~+s_IoOPg#a8}0r4SLvQwlZYhe!CvM8{}x8Mlbf@EH>7M1O?d znmE*WS!_A^h$x1AmYD6ZyT%FcI6-?VRkaqyU(ha3`o&3gJf0F%{hGB&WeM05V~N~f ze=;vZx+p8Z>tz0Maa$w}mP+dFWA5WYIY?$dTV4mBXBJMZ*J>(g$0kFsR`V(=!0*3U zjKfrLpxwNA$4(N*TyQ7RDA9*VFu9nO8X7j8ge8YnYQeKB3eiVWt`?_mFa${0*zK-i z&pDPZ{`Hv9$zh3h!O&)~muLLvp2#a}&p{+pEbxi*lRqF}Bm1av5m`0s<41Y>_^_H) zU5NUs{#Q+7iQg3v)M7GGvr}&LsbwdEsopu`78?J7rAt4qZ4m5#+)tHLp5j1F>InDS zuct}#965-w-hv*(V3>B-2;`D%#Q%eV2{Yh`d4UPQcz!uE6;G*a+=7L!l2enQx!H4WtaT$uCUG=P3&+Knf<0a`*W^W%+O$O>v--Q9y z@Et45M9^W?&qnFW`$Hy7b*!S?Ep!_Xzal&cR>;bRlD$jkL}~RWg?hJ4HGGDc6G)fy z;=Jk1rdy-(vJ-m9TB3uvx)WYe2wE*5W5+wypplps(%JdA-pps&&^)W@Cy|fr=Q;nj zw#PA{o1d+qvgmhpmUuK@;j2O!{-`@|>2LvKR*e=t*5DdN+Eg9d(^PgQt)@Cx!Z$cX zUd3P=aI_bOgcdbqu+})YcQ|`=PYx;Ex1Mg^ip+tryESl8)tw!&JkiSukD2e%RMx?N zp0{-bt@;RfD=7cwU`Zv>P!1okY85g|CIz9$T&1+WJ}By_B*iaF)d`Fv@zd354lG-w zL|O)nL%^FmXM=v>b}v7B=YkY(PjyJ#HPOMwo*M{dqrCKWJ0Ew-rLeQ4RWJfcRpbyb zlp=Eb2TjuF-AfY)!0IEq0KO#HP#s$MsjX3tRwsjvYdNDO4qEHw2XkJkM{l$S>uwZ- zn}76OaOh)mVEzd7?f6PNytJ_XRX)sYq6;>T~>7ah8A6kvN<3riV zf9`jhO2TPZ!cM)b4$9heU9P5C>4PWdA3LOe7&B2vpD+hpGoxfO$>!<&ChEWOET{MR zGwYH{QTNnYB(H3_hjNge%_w>=%)U>qd@GsEH)BMTIO#_hQXOB}cXtQ9aF3a4ZD* zc=T6Bz-|oDiF-G5(Ft)N0N>uVv%`~FmrF%CO`V%P)>e|HH^iq?|2stznEBK3KXk8F z6D9`q-^@PU{|Vzj_E`M%kV2msLk=7HiqV6N04)e8BStL1PPPl?Yc zlNgdfM~>9txuvCxn%1d>C#@yPMVk1*0{?wU)Bj!WD{}N*NF$$T5?T-#L-4@YckVn{ zlSXO_PIUh%Q#bDaQWNe`@a~l>2d?#hjtwM%J}asF)1Kakt~&;o|MOgfGUdVb5X5Jl z{wI9!KjD3{|EZ(%KhTj%SEE&t^7)JRDA*47zwPFl_SyRXYlFS6Z*;V?)j$I1#ny!EBA0308I*PTMm0lhk-JGV$rv)z1syrV zM$`=BHpDc8vhHc*Hj_&Vp+C69AzrOQ*@B4dx&+mPnbH4MO=l#Cx>|w)B;smN9Wxl<=-S771 zRDjx%1Rhaax`G$XC!mjRK85-D+_;sw5~qUOlD)%qWV7c8lBNEw6?1^S7r^SKvxGV+ z_=sLJx~i(`9S%h&NufyC=9Qc5PH4OU0Cy9R4lXwHM2=6z-KPf5%-N|a|CLryEKakB=*L3usoXuQ3|6xmu@al4g8v(b+L6%CP55_5t?A7jxhhl*i+N#(Q|? zsk+3?TD*YPjQF)>@0U^jOVhn8pGTy}fx|{3ux{knDyLLDlhNroz>8a9n1yWbi`v^s zJbLKe&>NlQaMc|Ej0OKEh6GQ zw-t*cU%!5zk0FM4fB1_U*u1(lQ{i1JnFbo)1RDPiG*&O63>R8ovxpRn=2Q}wdP-eq z-FDU*55zXnY2RbrY#oIf#=p^ta!%%V8)YAg|iO8yprsu-Y8M+n|6N7HM!S9oS{X=+F20>6a zK!5b_~s}Kd9)SCGCc#4kFPCm*k z)qhCdFugL$PlaOvZIBsZ2Y{R!&BUCkp5J&Pn3mSy%jog9I!W4=5sp%vbD@f_`bLba z10s*o8@M(OXtHc!GO+FTek5?G=}yQhu^#}}%(vQ>W4O}&=7IO+YRDfWm~<&^Nywrb z`__}@6P-iB6go%Lf3BH_5~2Y2jLVX=PB^xif5g__qzf;xR1t+|m-k+>(c%L(R#ISi zOQ!BH(#ixl`v4T^P^jlPHj;EC6>l5!QJrIBVq%A^Di;w{Yl2KZSi&GMt|Au=%&iwv zjKip_zDSR*dztPX^`PIYq6~83e-oO(9~K_K=mJl&S{Pr)R@3Vy5s< zDbqIowbb$NueBTzECBFr!%O-E6lN%a!CCM82=Zgylf;bX`KWy-2Q@w84)^!BO)sQt z7!2Gvfn1E@T1JZN;@7T%+}cX8Fns|rF3_g%^{~gSTYti4_V;_y;ABt(gbiEHEXC%R z=(viiag?~wWf^h%%rfkh7tPMM@&~k_IBXxpneV`;q`DJsavB;~TL};YO}N6qBk96j z#DP3h3yW85*2Uv6>&uJ9N)EX00KOBmfJ}qmJD0o`l3((Db1k`poo!&Jya^1f{rS`7 IC;bxt0r5M15dZ)H literal 0 HcmV?d00001 diff --git a/doc/_static/images/serverside-video.png b/doc/_static/images/serverside-video.png new file mode 100644 index 0000000000000000000000000000000000000000..4a5052d4f93a79edf007c5b5722c486a1760da07 GIT binary patch literal 436067 zcmV)hK%>8jP)T>Agx(L58y53z$|9YfHeB&$P%uFTkKTQ+!cR{IU3LUyxC!q%|sO{IIuROCG z3ZEPS=W|(>&bh(>;66S+0FZRfRaJE| znzl?P^Xw4xk)s!cBy=c)ZXpY@D7|GzhqSw9ovkb4WI(#lG-Xb3f6!-3P1gPG8 z5MnrG1J#(3BY=@91c<;gHE@Fm0eT&D3Wb&Nrk1b1y{tG*iF{7ZUmc|`E!9qfOzbEC z_g4dFSof@*WQdK>MvM+6K}^@$FIxz5>IGx;gD z)}r(C^Rp<5s;bJe%nm%0d15qjsK#CfWMU&u+q>#GIbj?$UZ#frO>zGL8hwb^?G#Sx ztT^F0tf`tb>^>53qt6;l96{-Bm%`y5imZ5#V#ERpi{U&=Eet@7oR=^$b+=`?t4xg- zu`XPgoG5udJmnam%ArqEXX0RK!spn#eXQG2QF z5}ht4c5j6;R6y(tZHz?IK+{MU1uoGmszO_g{26hU=|2(yugy9L=(@v*L~}JnJ!PIyXSS!y!K3cW|H@r zqw4$at6TTf?YZ~Wx6Y}%%ODT7!%mSuVdg{a;P&@_1w1##TO3KVyE3k@+-_zkICnxM-nx+uP6X43lm<(b zAgI`6f>00z0t!@Gf+!UNlF>|qGB$w3jFSohu@#+cWI7vI5`aX+j@fW~lp+ckccY^i z8=|u<)y$e0E-1654TKcEO*_F-VNGTxorrPlz;1w4!FH1bW%!(S+k5~@v7K;dU}{rH zz`D-B&h=6E@n|8ynK|ks>8Yyp1Vd~L%FFPlkrUa7ghl|y3FaaS0Oz(~(b}SbSS|r- zMS&r6YZbm?9A!eR5oR|M2M}zLhbyuR*_SrbuB;HRYf2EHu@n0ddQ}on2VX8qkiybt zF9iaV1|SXycL;L`hhHB%4^j2r-@YFDY&J*1zHr}poLQDi#Q@}bFPH!}pR|U76B`=| ziMFK9As`_i?2e7}iHESvWzm6N8T{PNf_*ZpVhnzk0@uND5$Gzb;>n@I>+2E^QaPJc zjHCkM*z6a=AAIn^d_J$G!yevy@4dI)dh4Bc-eDKB+3dv^Uwq}2S6+DGg&&P*FP?s} zSlm04B#wY>I#+7{iYq=Y9-`^auYUQLSl$ku*-K-XEtk2R5)`FG)Kcc@xc$S$_kVEv z_U#X+)&P(rh#GaSQWdqV=kEkGJDuwk9V}ezXp=HCr`DQnI;9j85oPCnEr=2kv7uUJ z)5{InFQ5_4Y7)eT__4bD_terhpf1Hjfa&eqo~9`?H5f0mBosh8Gjq9I-gmeYol*jD zdTRHWGTk9Fr>SL{&h_ipZMW5~6%QyY4SilBKI-q-9UwZZwliBB$D;juZ2x0(lo62~ zb`cT%MI*D)(TV^g3qwkdq6p1qF|Tz#tMgK5EfE#j>Rx<<9xCL2Dk@knJgtt@e%5@I;f%=&UzD=FteCwKgrMaR+&H zmVsf*d81}5rR{BJK+lpC_b4BRZDH9V34}w0R-~QjOh9Q;y5w1d z{k}9Vs_8h*1x~M>PRmJ(zxmt0Q_Oja3?1ECtM@*32fcd?{f#%?xOMB+tFOMw9^QWY z?Kj_i^LKiM5#fo5U|{>Bh3N;!#t%;*c!sk#uejn_#-@pc<>K!5PvWjP3l;#(oF)?# zDnUIxQFA#ji&>dwwLt;~%amf)LS1DMp{NloFx) z3b=m&5t0Ph%{Fx8DA+nbKLcTb7`i>AoCN^4%oXNu-ro~m4*&uYyOVFY9{ zNIPx%#zh?tLCT<52Iq^Gi%A7q;!I2k+fIpkMIimWR4MlbM%z`4nH*vAn);Nl(-w+);$LcS~^Vv;?YRyJgmzR zj~7+eD7}B3yY~cb#2W8Y@-z!+Ny0B@IL9+QHY zOO^AWfCx*Xvqy>xWRAsnBB#WqkuU91D8T6i^UPqIqaZ0jHphvIs~adl3@iZef3n$r z#-zAFm#t?ki!HogiXXSXhqrIgp*bg#4)Odz*(h&4rf`@GoS)s6R7+LpSrCk;r>FOl z)9e0_IP>}Z-FM%8?X}m~&&``RUw{4ekN`-Z-TBI|{rdmD|GkDPEu6pgo4@g=fAYs= zRzY^L@EBEr5&;3=h76SiAPfnDV1np|y9Igw6TS1@|NiZ-e)U`b_OCZ?Tq{Tz47y4O zx#EhQXxFVE1c_SFRJ1HCq1ia4%(hqp_4G6#+nK4M22*RfTtdf6xj7mR6$;8+Mg$fS zX(zTE5h85_!T~ttEH;8PXkKeuF3N0Xz$+xl77j%~5nyAGA=5ybP#9U*2^fT=*c?Gh z^wPD)G_6YLl~zb1?xVYBlbw>l2(X=*)LKAx_TTc6IClr=MM(0B1V@C8y_Khwc8Q>~ zPX^nWD5t5ED5WYWL~FU-7b`?3qzgLJI}>F~2?pzHGG-!nD^D?WwiE&b-l(G$NDe8a zHZ7NpnW6?%+!|_;8A(Z%Ah3omb|=6XlNhiQ1knu$6G*B|krS;b`YuFj_z*Ru0N{ay ze;LvSv4I1iUse1VVE_qXaY<}jxD-W1=GreC#>bXyGXk6ffxXrc3+ zNE`=ulcb|cabs(1uR;4725}%h;FdqO6t}H?c~HmyejmSmkMc3Dj@gG9^@Gdd(Y)}U z_T8oL$@%#Lf_6@+U_KG{P1M&?vbA~-J{FQ}f3)-*I&P)5YwheW|LU)Aed!l|TFl)3 z@b`c3U;gwpf$=)jCWK5%au!0`dhHM)HAp8N+n`L_QUC^L;!HcG=>L!aBWJeE28IN;Hc54e zRVlV+b8UC6Wp9K~Y$UZz`dJ??pF)c0a$m9y220R0Li5humI;y~og;bP&=GWc9YQPE zOM=0htBoXWco<*>QnyX%2*v_oY|OmQsxWr{I_>5*(T@%jSWfH>H!WiukaL_AoJI&E zsV0$f$`jcvBVaphXuGY2hI3nrvm5!Kfr!&P@^6g}3EGXh8i72zjCnYMkUYA&aey8i zE*~FB4~>+FofsfOj2}E)FA*)Vb8|N!vx!0%CzvETjfheJJDs-u#9o-K;nLnjJnhiw zc>iEqd-BqaeT-5KGt~!XkkrY!m`Jglw(9EXxnBt4=V2kSZ%v$P(bqwJ_cg zLN?%W^retO634{05Br}V9Dcvp&uK_VAdZ6Q*y+oST0-J*qyQiKZir*NYrtc`fVYlE zXury}%^{?dLkPt|z4a(RE(Y)VQQeTjPS9a(4J1AIdwDr6q>yHm)8YQpL|8a@LKw(K zPBXVfBGCZF&wc81@4x>6&HwYC-}(GYFMaBBFTZ^A7r*(9Z?OFEyWjcttH1jPcPCif z{ilEU`k(#zUrg}r|M;(4pLyx$KYR0wU-;q=Zl6WWGxbMb``SyN{q)N(eg2!@`gZ`G z-1)!vzwo7B{`6;Fe&g@IKHWJ-T0(-65WutgcE2mTvZrOh#f(tVS!W|QkpdBNNJT2P z(k!QJCVjEZYAUW!T661CHCs-tO)bYs0>rS;%CLoH1Yx3=1r;PaqzJ)?Eem0h!+(be zQ9`wpkZ^((Vt8jTAypF~*oeRVKYQmMElXC{_wV<&tNYx0XNGwJBQwt6$l$Oh3^P3B z6&5NwBSZrMA}$C-A|z_!BV&9gQU8dKEL~YyF|0s{aV=EhQbbu0T%d_!Kpql=B_Pi= zyaMu6n7MP$IbHkrP518JtE#%X&*?g+``mNxtbbg+u*+?!| z_?qH?HY@BHzv?fba@gLJ(sR1M*CGNsj2O{L(kW~Tj9f(^u-(6p39ihdsl|=R&jB}z z@FLit2*I+Z8DlIBQkVG1-0L&QjWJs)EbF)BNT@qnMAxY~Zd9$Jzxq5bJOjsweBxLOz18+m63jY@89T`=hqjVft3>-;l!UEJFz$J8fF=^T57&|$ z2L!0Vp~yCqCPgub0D+xSzxg}=^XGr&XTS29d;j_O{NSs9|1~$<^w2+g;~RhL6~Fqe zkN-vY^2^($0-)&I_ug~g&b`2o-1UpEf6f27_Mz9j{tbWdyTAMD7yRSrz2qgoUi#Kw zzV}^!`Q8sa^SgfV_ka6U55M-x=YPiwU;O<)_T4Xh&Tszm%PucCPy|eXfZiV7({oTN zl~@2Lgh=-^y%dXp6xc8z7~}vcLXrz|C*cyJlOb3ESdwhJrebt+SB$I;B7$V31tTHI z0A(#V&+0_xeUeBdlkvc}cVbqCX$F_aY?f&{Q3o>sX2B^M6h#^nAdEmxmgN2>6nB~- zCr+c8k)RPUKr#b-!FI~Dn5Jf9OyaI?ZJ%nm6Jf+n#(n5g&1OY-O-jfm2$ z0GzXIOyfz+R=5N!Ewa5zyrTt;axJ$uR0%CdNVv40VMpi$g~LKrYNyME1*l~})t;V= z+PwX(ZE$d`iS|l9x!`KmTs5?Tgar+ULysPX)LrBx<&J%!p#$o8SE$QaOEtEegyekR z8E6y*vSmfzbGRm_1T)CSa0VuDU$ZJZ*jc;VLP@yL2{U#|AB4~qJE?OFGs&h9SFyp? zIc_!2ENg&RfWvXDAPI<2m-RxLaQRD(*5LtB^@STD?1K@2aBNtsRXp>;7k<~n9{!{U z-}JCsZn@>fFaBpw{)Q*q??KnT;eY?}O*cJcs|XLe_N#Vw?n42=Po24RV1Px7+g6+s z5K9Cz7{2vsPkrGFpXb19Uj6!KJ^eWdr*FOW);qrWTR#2C&y;_A?(99g&w9>t1U&aS z-*MF?XAFKqpKU!o$A_#zRz?K?MkaG;UYE-jj%t=mYMN~&$>9PSq&)Qtvr^|?_xdDY z;H-BxSReyofRS_7n-oq86bvkkffxh}Gb1^SGG;4ZVEvh!jU?G3!wO_llv$iT7Ny7< z_`IH<4rRmpm|$3f5_^nUO$XU_PIHfYHv){bs+@}p!lhVcQfdM~QqpuB3tOR@2x~*j zCACZ4@X1gTSY3=^EH#5`8<>Ga&Zl)+_WbZdBFRBh%Sf(7Hy6GPmxBa11`>xhhM6_9 z3P4NrIB7w+1)MrvoZ1?kJ~h1L(lbS2p|BzZKv)TNkd6r+G_r717$+Fb@^*ub!1NMi>K0J~!A5hRYO(K<`V0(VM$^EKT)4IDF!cC4gp-}Oe)Z~J@h z1+t|?I8GFsn5yw*FZ-{L|At3j2Al%S0XWWn@mGKKzy17wdHFr1TTwju@wbC96jbyW zfiSxC%vOwnz_0(tuRQx1cYNmTx!a%gcVF`ImtJ8*+_(KhFZ%v3Z+|{qa(dfNKkgB? zfBK3`&y8YH$ic#7!TM>_dwMpCq`IjsRzs^DHC%!~%)!- znDUez*-&rDQdfW_N?w-mNP{7Oa5)qK!ht{|MU$LlwzbY<#$^M^j1^0(B^RF_jYcBd zkYhgNlIkZi;qHw&r87oW${JC|Xfh)u$;H6-vSennL?lO(P^*6&8j^K)fE8De7UV)O zqfr83oF7js0#v>$IIT3VW;p|tr!OR>h$ha@Ia?JoYY&$YD zKsW}NWzYSiMXrS(X?>G+&5+i)Zr#@BIB)`<=aOS(1Q<tw-LHAgYlRSn-E!A=?=c6Q z?|uJ=0UH3pGzw6IG^1%;2{U7|pfBQ2(yX|q@)BSs|{vRLw_{ZM=fxr5|2j2C< z?|SC_zv_xpmk{L+g?qQRqZQ=|odu((=h&k}r7qU`IEfmn#vf97I$Gv9)yP8>MHovc zf;DZ?W>w=gna4vh7TTO)oT!F7W^<|x3ZxMe0SEz3sQ$#w)1TQs{@9tesoY33nIy;= zQpP2@C-bqS`h+NQ$rmW(o&4T5?RcCkJMU)ZiZ6G`+nI6AsAeXgfYzX9Ya7FJVB_+V zHmCM;h1QSJZ~<|lHq5z5=bXFqtN|@B6etP~2E|y0TN8%EVF-Z+04I57tn&bwHSXpZ z^3{hZy5wV|%A+WciUx9m6IdmFByjany9svZkXBaagNr}Xj+1QIioW52^vJC0@1joA zG#mxoDBUj>)&l`+Rc8)dTR#YmM)G*RQ8(hC9)b-qp>zoZ0#Lw1=`17u`^$c9$WyRO zXvO>f;!i&JsgGe){PyqtFAAUj0-o{>ck0VK1WpyhhuwVZz{1m>`Asi<#eV?r{p6=V zb^BxQc*C3CdFGnp2~T|3YkvE8Fv6=|_lD2hbDtMORm1S(M0$F94g}3A2W3>0Wk~?S zG<+%{4_D??B)Eua908(4IpKi{L_tyt#*m%2nVIbi1PZe-m0$$2WVLJ>O$cBPVRo%I z05L|RwFea27;J+Tr>7o37EkWToVWokjrC44si)9PD8X1 zVg=N|mlU(QBjk(`fC1*JTFA(8a#M+xc9^GqvK)>YS(6VyzyzJ#MJh>SVwI`8Ek)x= z9ca30yD8s_`6fvr$Sd0q2E!r@hJ&rsr%rEeogNfhfEj}v057oqmrXNMegL+0D1btn zMjxFKHF5|fuLmw)7=oKtMY^Vp4YQYG6T(=Ixr6jcC)XxXZ9Jlrvu)x6*hpg^%3VJ){=14C`tQA-L@R5Elk__sLEUIS3V`#u!&J%TR@u=fjECKa}qk*w4!uP z1=zF|ngEuh#P(_!-Jy}j4TAFr+jLizwe%2{1foOY`@~O(0FRls837j9lk2a){;h9) z>l2^&#AVm!O>cVBgCG3h(P%Un48VZmlGncF?U5nY91g-@h{53J{?o7g9OxMwc=>;P$*)aQ@>$KDx$K&UzWvYNjuh_& zFM7_XF%w_kmDtm>0PbNzXk3Cuf>wQO44BA)iRm#l!LXfPvNIN+fJ4JGlgc_-=cZ@MfY!Wuqud1X*hG0hIqmH~lJ#*YxmL$tE zhG1ps;~g4;8GxOg5os>$?Cweg*x?#&hI|LYm)y7KMH`n3XpX;!ND@lo1PK9}?FfWu zsG<6t$7UoEGETBoYu>uu}~Yq^V0S=Pa#?gies`5NGpj z_%2*pnF81?OIi{{t)avOR9`xj%Ap;TmbCCh5euP?*Ymk+iAh^MPefM;cxQVsKrTB@ zl2Uc^Kq>ZlMWFUo#h>wvXT1LPuYc`pUyDVU*$p?`@bsrYy(kI*qJaj$q(BNn%P-%W z26@(w4qDX~s+d+gy&0GJbFN%ou%qgFdJc&It**4YoU~vZ6hTlhnpG-$y@ykPkuC=b zh(Hkv1OO~TNYOPM4z{)iG?0Q6%!Hs=X5~W6s+SQY0U0qY24yUbAVk4vG;*%lr0H3f zdul~9y2Bu7@r$30Rf7nuqMgd!N$iPSZn8yhO(PwW+!z2^wf1-$0oBTFfToGj5QW+j z=r9Y>BQgS)4Z>~5A=JHLCB;Y(mi4Ha{x5gY=wgiSHrWQV!puPl5+G8510cyjMR%Ze z8JJ?Pgdh^4nki(oxP?u$&|aT5WXlvmK_t!KanDu@b0JRhRCG1dciTcCLYTWq5(E$g z1h_jBl7K9%F0LG-7)irgIf1tBt|$xV`X|}Se;y?P3dZbvl)E5<*;u6gpQxN*lB0$8 zP=W-mte=Q`sN9B5tVwmp>Y10YVTi@Ltnx4J&}dI>d>(et!|SRt4`16kk%dkkD^hvt zX;mGRkWR!jAWQGkE{zBQW<(cU>~;mT{FG*dn?^+HxiP8u@C30Q)exr5Xm4X;_I;MuZG$!B?Xhe!8_bwd)% z$m^K}1OPBV?vMx?2y?)}7#V@+URVHZDg+6o?LpuakOq?372*PM+rIb^Rzogl%=bT( zsHY%#eU^*td7yt{SmiOt9YlFl5m_Lxw$d0 z1lZX+YS<(+cGVrx1=6rdByI5e=K68hS{Cd-{M@T9t&k>NIBwX!m(a|XSFVjlBLE~% z+HjYio}N`2#p29A+?ZIlc3?10#gJF|mdDY~X#3pm&V5824#Sz#Tc?IYxkkIAot@pW z)HO*qcg$=sn51MhNu*f_NK|@wnEbCVjhpsothePMW!zl?z=TV6J5P$~F2xjWX@dEq z)gPWV5!8@-y?xv+lS^{ppm=M1JIOs(+;J)IxXWYodWS_hCdTN|Jsoi6rUpGKg+T&o z#PNb{Iob2UU6{DL5Da;w#QDRFOC@cp9t7ISn6`~1R$wp~pr@zj z!bMUj!f-e&iXxaT@4t6Ia1X>F*!?a!bLHii4FZWcENFBhmT|X?quB^(1d4J2DqmoiJz?)lDs>7Wrgq&4@8Arsw| z$T@71)5#IMUNe;}!X+$$2_vxtWspBkv+2?0%Ka4{)4!yjd-Dp?)hdE(g3n4yW~{wJ zj;&Fh=TtN&y>53LQ39FP{$3n^)$8aXX|_*R7tRsQj9e*B^C`Ub)oBmSY1I!QVBLF*d(+LMP{P!`s`Kl zo}QkiaDMD10g?frq_j_Dv}npN15AJ-;WM2ay5HrOU3ujd4|u@+ufO4|hr_|Sv-fOo z-?zKH9Y-T*8ZEEh2^O>dQ?t1xzLRcBvM7qdgrtWjg?#Fu9xSWjW@!_YI9OAlOd2Ce z>3|A=n$UYgfjfYjjwW#6V&p&dUaMvpSEPfHI z<(pu2TXE&TLN+w>=F-A-0B|8G`oWYs&VglO3&j{#m#$`XrVUpXfX2WMV%#uB>mKOU>p*QMgon43mF@ z!N5Y0lxNYakX%`+y*XuqjpIq1m zT^Hzxk&C1=YjXh3N>={i)OuH&u)_9uzNy-3d;45YN`Od0S*L5}mZXK{4)Y>gDvHfg zp!O6nRFZw*wKC{P7o+9@v2J3k3u#@o%C&V3h+HHB7au`O2oYISnS*_+$L#nl>C4q6BU(qMMq zxpVj4d-k5QXYaZ9-toR!amAHaKJbCpTyf&nZ)~H`^O)WS%Fql=bu+0n5dqdwnWH1uAAfO<9Y}D13;$mRkJ5M%M-p$V)03dG{aFn8jL^y9iV{OcT&<+ z5j6{A%mx5$vKQPq0XFehS9B-@6D<13NjXNNkpzHpNe)yRd)91C=?~O2U4|fcl9JZd zF)SC2D@^LHU;$9hs*wyNvZs1FN;3$oEYFqoc+C)?lvkmnQz9Hf5>bLf9!oDrBO~GR zINpzC!5BTAD1e7wcnJ{Jgjm~G?;(BukxSA=J}cYNKoU|X3m5@I4kRI@$9o`cl|)FQ z#RX}Cn}c17p_6~}lLUBZr+=->g`c#|-E{=G;dSXZU0%sigGQ_gvv7b*Mq+&sWaDmf z2eB@Ct9VaO&*4#$IoQBBcfy>Lp}X9*J6SV)bne{F7ryYtuiSI?%U_wu|NHdk?*8~E zKm3uqKlI^`{_XhlefO19o2ldyTUIcu&%G-cnG{YaeZ|ys+l(=~Br}`KYcLC;7(>>x z=cLWeDm3SRJ`1P8CL#j0$<+3H1?O!>GknpQOl!D=H9-@X+JQz|2xF#$A>(v=(?@?tk9=geI$JFJyx}$(ebc zW;Gv*K`{Y<5@h2twI>K@Urjb>3>vo!x@nkc3>tTbBoc%!)MB@_wY=@XMyC`qvV&ac z_vajD)>iUyAgl!v+;GKiRCTGV?eh-Pq?0Ysq?8^N`?5Q3L{TDY_hr5t5<<7RxBfEI{rNW=25nFu}dj-&Ex}j~;BI zsQgBJ!^wmDn-@hnv(y{B4}Rcl;9FWpcRAR)|FRY^?>_7;OZ-{ zzVeEzgB4_$$wWyX4h93Wz?ph(^)x9+k~>^-_c#%Gge&bwNwe=uI~2T(UY2mha&}Y> zfVoVq67Fk-&KNU$PBq+2+W^Zcu^C5Gk$iqwY^Gk@tQ}P@lH4IF_dXncc-Wi(P^SG3 zt}jSrBq1XTvjBwYPBMuP2g9NWWgJPRQAM!AXhf0;E}@28SOJp30<)RvL{F`D!CUqB zg{*d11AyecD&9>G&uE!+8{pb~BJp6td~&RT7OUG2ZLJQsI8TTWRur!%j zpKb{el6!TsSY>!36qqt8fS82}-q6NbdpH9;;P84vAo7>0QI%xXtdbcMi)_xB=02G| zMi{5t$6&p5^}Dzs%#iSNw8Q1BaNg$96Jnq(QfIr zk4HdxCw5ytCn2fb9=#A3P3`G94seM$pe35QC80nUqL5F?4?nmFkpL-}5NN^dvP&gJ!Qf0iX+|%ZVs4 z1f>vx5~!^3i*Udp2WjFg4y`elo8)^gx@!PjTKd`%DCGJQ^ASldr7Uxq_RDF_I3#!$ zfN(*oG(R;mz?7(^En9Pz8e#d&e8vI`(w^G5Kx&pLS>QxcvH=W4$ZtEN%u;-JKr|R3 zISrD08b~%Xqe}hOObliS#w;{~!9vKTtgn4U;}j*3nMFb^UC4+mM2-4LN1+m!EtEza zVj*1{Nm5f>ME*U}j<_kd*0GdE1~Pz#tjLj&;2tF>L?{?7Foh@F)dZO%AN#G&%5M`U zHfqIPRzCumL8X=mHF3G*(|r=NotiYKy(G*8f(F{w)|NJ-`obte2HTFbjfH_|$btt?E^B{5pYFZ$XA-Up`Gve~!^~>D8KVu5N%ekW|(Q?B%xZ9(~SU}gw@{JMG>HeqQbV9 ze`OM9^UeUo_4YymIstRW2!u}h0C{mHKwYB)4b0sl#$#>?R|i>{BT;pl*=qL^QfA;x zvu>P`<5ai=BU4l+ECd?sV|>;%1Ni}em0ETP%+&X3%vG{6**qsa^P8OzglnqDWy&7F z_4pLnBGuO@bVi$#gC*YqaROEn+rO#C3CDB>2E$$m^1R0-YY~U+a+dGHJ!|})d+vGn zyWjoBH@@+0Z+qLj6XTuNz3z4IeCIpA@|CZU?ETn#dXAoUm#s9@#yz@cJzRO<_+)=V zLl4hNuDK*)RF>tajAc}G05Q5}%d<;+jGk4-YikR;yV6i_0*Ws8I2y@g3XgC>ldu$A zjuLTmH=((t2N6>`xZ9COxx}-7h zFGY2{VKX-i{eatJ4!dDVR>yG6U`GU0a=beT&kHotu(|;1AlyimbBYl-F;;OwB@jzs zl&i#(q)qHvO<(xJ7vB8lH{X5t-8bEI(^H@N)MqEgJ2&2V2 zJL?xH<8C6BrAJqi*`Zj*I60d3eVM*zH0%dkahiUBuxk7tkzunWO=~|%n~SeZyLUhm zjRK-O($=FJ%}9Wx9ddXiNpwUmsWun7n*I!#9EMzA5R#k_4YZ|eKAqf^etLQi#*y9d zZMB@p5DCqhwP_tze#+-gsGi%0;#f~b#!g&((}R)5wPCH)Y~$-}w_LKZJRJc%;R_s^ zv6FPQa;LMfq8#jc*p*|Tqd``bxATEYLPcc~S2~_sc&_X#pIE3~rtr=!UHSaN*OHi-Tk%-Z!e)nARS&Jfc+IuD zn%T}VA$b+vfJrQGO(I}fj-u^r!KzA5)Jr5+J)cSd@bouDR+0+ighlHT6HWTQ+Mb?` z66*71v@oj_Ai0o;wdXT{y2lhN?6A@mW0yG&I)BHpQ|UXE;6y$}ndH23Xvgcr+~vlu zbX2W7l1}2vVw?S4*onKJD+(+@02HB7q4l`q`j@`qRl79A?U%jemmhk|LvOtK;XnD)zf?MCK%AAIymBbnAr=+ zV8KB5l2BnR%nHL;g0Uzp1O{WE2seY|+uP^vz4z?dd(V!4(H=|pQYr~WkATQL8+5jW zw;*W{B`8aba#u`xcTG{JkBMH!SeDUU(eonQ5^HYN0$`@bgx$n`NJ5ayNs0~> zhS4a7ftTe@QG{Rkg@6C&fAQ|W{?fgF@7d3P$~Qe}^rgRg+gtx^?1Fy!va5dNNB`Z2 zKK$w1Zn_qRDWCo9XGf#a_19kyfQiRF?r~-&>C#Itoz9?6@*xj-$VWf=(a(MEbCZ}o zJw4|GNg(pB)2Q0C*nu3CWkGEPjEE2b2z0?*B}vc>^l}u9@-jMHkYW)8I$ePPks=b& z0T>BMSrOB`3EBa;oM4~{h%R#{sl0eNK}ZliB}h@oY9Hp&G;VwDqV7fjfdbn-8CpJY zE))j9EZCyy$27yMb%YizARHx=$}(2hiy$hD=5nSWsj-+*{-;6S=>EU~cqKVk`Hm_K*Gg)RZJZV!l!*eUfyntHpVNce9X& zCSAMF2nZcu+k}@NGWll;vf)Duh@3kwNk-GiA)irw4_7f`k_WKLvQzz#3u|77HM(xX z!hACPw~3W85{E`dcxAd@Bi5Y?*E974!KL@JLs*S$Y1ky%ut$T@w3%CmTb_w*b$nybr_1OU2JX;i@&rGgv=TybXWOi}FamhMV%Rf;9)2%rUtr9+r0+bGRI z%FOaOLJ|xxl#z|q7v?@q^CP4WMhXN`x+gns>SY6X5dPtOLnF+U!FLRI^b+0yGOG2ZKQ|C;*HmH5h3m8Ot~-Mx%aQBhT`hs`4iF>^P_BHdiU!Rm94|g+ zUK07wKHd;aK}vh8s|2JSq~&=+CTUv;YlRW>kCTF83d|lZM1F*753QI+Nfv@xFd9UP zA^~fS&1oU!^o0$e#n(Oe9ClzMs-~Yn=X~oY)NCU*6+Fl{bw86Pq62bl4CU>L@Bm$D z9Wz68Ax2n%<3WKHc;#>W`eUB>xcdQez~qRa-S9PEgOLDI+T9)9{D{Xq`#I0Q?Y7&a z!cYC{pML&xpLc2q3=DbH=$y|v$+0i`c*^wKZ@>Mz>#hSZ8jS!*8t;(oweg;wql19d z)W-9EMj%8AfngXp+zO`$gWX~`dc*`qgb^44(p(OVKvM1i5n;%#*>XZNRUCjJI!L%H zg^M5!6thAK05kwD!97?Z(48P$@G=_BSe-Euh@iX6Nw_3WLQsjSYywy59XF#UAS1~v zOtVL`Mp8oBV~(h-?<vwY4?Ty|GJU5hnkl#jNeSkph^uV9rEd!dtVtlAxjp zWppGgm^_tQ3TDy0`MPX(+iOWHooG_IpeJS(4gk61psg^0lIHBckV#k>5EO+7iVT4y;~0;k(*$`SobWONQKC>tMg)MM172$`QVtPv zzzi_R0Ge5;T>vB`PdP1Nlw_olpc$D5kE6CsH%J@|my=C;>@e)aJk;|Asfu(~)6a^E+r7NZFV-F{dbWXF5Fa|pz`9u68=cgi4W z*6jJJ9XFc>h+v|tGr7?OKRylxB#9%!PN0K&hx+gETH%2ey-nXaF5gSJ6#*P(n2B82 z5Sy;Gr(Y+4*Ut}y((=UhqcF#*rQvoP3WPlm(}c!DS0ygQ^y>;FQsV`D{G@e#k-Rbt|;Zj|? zlB#Z()VJ%cIv@Xh|E=G>J#UrS-TUGF%`txF_A{CJ<(FSBq`0JE7`m=ogiHsj>g42P zHk$#97evJE+qZ#(4nJ_3u%mV*1Aa-6_PTe>$(~$Gs?@Cr&KYS z&Oo3x)XDM5!`B~Y)45BrUEhWnIkT@7k|hcw7~|6@Q!*JHP&JgTjofb`*#!ww$f%4p z?(I_-awHC&3i1$7pxyTO8l6&#;Cq+TR0^&KlE8q4yzNiSAo&rgAQUo4vIIymzA+>r zaSaIRQpKMn_JM77>`L-lJCYSYG7%wOJLQ&6ARL`b-tNylCt)BDhRK zvN$)wco!Wm39+4<5)S5qQd~exU^2V)%tYB4gxw;K+WhOMp6XtD=_jwg`dT;p!8?Bd zjX-k0`Rjw1pa1Fo+wcGF-5E~J|p|SSIFO- z>8`H^AWf!|F1rdvXfm11X0srwO7r>R{H>>MDf*mDrI?@01(J$$q0ny2^P?sQk78Jv zIqfEZokQPOmCWd{P#_m3j17kJ+WJCb6EDHo0FtUo`o0J6I&ff#LhO#DT_mV*d+6dal3hcYJog%ZandSF<#x)p0#TG<;_SbvLHB-u?SO=5&1#Npzj5 zoF1X>XFvbNFMsvxc~2z*68Np4qPFT0fm|Ot(z*40aUHd1|u;bEE+ZhON5|= zp`ZW}U8l#3^PM>_Sl{pjwGjTpm5&!LC-etJ$WifU2mdHVoNycF{N; zXUR*zwk4GdS;DS}j7UX`$fMa&E~R)x@5qR5m=Apoo9;i`BBlV^RUSyf2BZ)rAt@qP z6;MP#*w!#qtBkZufi^lHe z2M$~@5(K4E9``3fNdb-sR5AbxKmshuE>SG0@@Q%<9>O&UCuQzq9eq0F5U2q|4K`BZiqI$%9aI98J=o0{)@mFqLIS*#8n+9=HdH8t1zv(r z>jIXgkSsw}W5sAZPnE_Nfojb*V;2t1Pu8ycQbNp+=l#5&O{Se9t&W&Xk0K}iaZhRS z6yV3mIZ487Fb@8D>KqU4hX<4t5oE^Z3?wA9ELR7Z@dH5K>_N8Os-+5DJlU1TJ{6*2 zt_o>C>-Za~)Un4g-PVkJGj_W!i5ni9>Y_7FNqaKL6ad!vASB^Xq|jz81_CT$y!|E; zDnJln+X4v;HV`_ix6L*rgf1T9OVY)Kpuy~hBDhxgrtFIUe~^UsKh6T}b>$+2*+!Y2 zkhoODPT1_gK5z8`m-SH2Q*%A&cqE1Z+@p?Z$H}~(PA8eufVbZMV+w$vYRCvQGqkEM z;%GyNx1nX1!13|%LT11D=9`~>`su<_{gY2VS?F_M6)$~ZzW(6BgGIKp`Fy_G6|bs@ zL*j%32d)gE`1+f<+y^bgP+b>Lkz*d`h)hY85dqd_4iC}Fl6>}NCjbj`7E#t%wKqho z3?KwpLWqb?X#|1*;leF+0`b(-s^)?UYJQ9uY>SLYES17ag*+5x8(U=*G9a1=PB9P$ zuo+d5u!Vqffn0^8&HU#WY!i*OadvedWJuGzuR&w*BjCCAV&PV{PS z+2+&Zp}8F0#EX+{Xc^s518YWsO#xc+(2D*kvNj)&l3e2mARIvin;KXG3}kjPuHJS; zg29Ra8HEA_2C5Y)yQE-^LB^6;QnnsHhc?(?t#jmgZ{cCgC8MnsvmI~sc@!J?NsSb3 z+wcUsGw1?pTm~TsO008<1KvJWwLuDm0WfO;9~po#8v;Ib&RFc?YB3kZh#k2CVPq}~ z?RlKdIT2=W$N+oq+91xs^)?{i5dp#%jfyB_w&K0TIM9Yr*E&6rxF&RLN3D_B0S?PD zOeURe<3br?oGVEI*cd`lMgW!sWS1;CIyzdFuPyof^UwRfUu~Gb_~MIbyd516yZ3EqH9n^F^hmOt5Wc}z6!~@7#S*wu@mhH6aXra zpe+VXOmrq<2!NqoB*`K>)G+Dw?O(O@(RTN!aTsZn@NpjhICL8r9VT|VVNjJh$iUgo z={OWs04qyu)pYml6cwwx4~J1!NhnBnYprbDR7e*i(I^^|^m_KHrb)-f5H^W)8Kdox zg3_ohrwrQ>O$IS()AL{0I{onX!|OZW-@mWJE3D(uV(78z0JhWA#yrpjHs+dvdh*2{ zNxDx=Z&bxlMAwz9v$mSZ6sSsQdv;Uk%HmJ7pEgj1c5Anlq_U9qs4yU%R`VHSjBKC+ zCQ(jj7DPL!8U$Qt7!%HN125Z2 zz>5S50&5{yL|CdVfC6+yx5h*b-xgbt!lm$QoRkK2)u>DM)&r02)*}OV5bU%)%NiyW z!Ki$pVU`k{UKnc`xZq&+M|e1J4QgS228h9ZgC*I4{eLG{g9$!=L<+vp)`lmB{j6-9 zOR~&v6uXVUJwUPpQ0!A2=cB~joxF|B-N%bxKABmPF;+@EP`}R4&Z?*Fk3Dnb6a7~6 zTW(|DaKksTO(Jmh9Q$rg(5Y8{J$xVzBokbza3DpBP+4s-@9uJU=O~gUZr=-I6a*?} z#zY`-wNPggw=_^q+`7rZ7=n5d4ylAB3!Mq`#2!b#UU^_s$2cidvva7!oUNJN$2N(f zC<0XLadvlv82mDj zOCsr{NbDdn38RMX@#DwC=c3EgQk_nK(oR^rhc6Z>HcjC_1KNO?M54)hX$Xr>z{ zEf0TUi=(&v5KWmnq_SY~(k}4onT-xyKv(`hw(kd|lriKv!+$8S3N@xj$@1CWG zTo>-^M6T(LcD-K_BtCZxfERR!*YzrsNI2VaR*q5mSqd^Q5HM3a2iW9_N1U?=_q)fD zgp1X!EW(O}{13hr-}>e5@9*Efef#d+yN8E|Pd;rm`=cY&b8LSs`NMAx@EdN(Iw_Xi zesA8q`STz4-YwR)ZO!ZY{l3=6+WVX;K%h`5tJFi46jceqz^hLZB@($57eOJ455Dq$;-PtXEtbW$fovRa_q&I;VTaCS}J&*>-)RD>$_env6p4p@AqSjC%6cvEBM7H z{KAFP`=YzQ{`If_Fj~3@oKS$>v~<)Bu<6TDjlv& zn~spOl`7Pjwy%24NGEG(Wkh>7uxQpjdv3ZGVgeKqFimpMhi9#5qNoBTaRy+oP6LHb zfAuPpfU2Oib{C=nYfx*mQkBlsuo-#0Ugm&oE5z93$Z558{JQ~7|av3g=?94#dX=2kPYzz8v1 zV`iJaZ~rAsHW;ivBUIUff?HnU=OtfW1ZiKwcJPxyt8jjOAx-EKEy6FzufRMqR7?sd+;*ZBor ztH+mRNy7O#&M#foojkPZd!O$SN+8ZpX9=v<${UnXrK*@JmIbw{AQoj*n4jgre$rwh zlt?7UB3W4w4tDNqo$6$IPebC7s(=0oL!!$PmLhfJS+pjxZ2MK&p+m57HSKD?#mc8k zGF9sRmnMYNJu3pIuYlpbll0>F5pLjex7EOwpLtKqwFw&~ECj-$HR$O(j!B$e! zm&=Y-6%2YbdvETJfD_SqX6-3*OSBYCQ)?_;BTdrle%)VPm5|m_3pz19Cq?W;Iu2Cr zq_n#^qK^!z8ocFQELEVWXaTBPswj2j?3UzNt57byNIu3(sFaBoML{FsJ`&yBltr1AoKHlQ%+i2c3Dw56G8CtQxXOrkx|?-&2wYn zVY>UZUHa>M;7@V=_|Zpry>Njs#&xJd9>KSN`?vq>&;INbD$Y&j$mkfskhiN+a_b#E zg-hfO_N*8}fm4~;-}-ECU9YD~wTd|(kz<8SX^mROxVqYpH!DLSbSJ5SA}KjY@+vKX z$bn#8m)=fW3Lzb=a_*sXxh52ybOKN|uDT}4Ud_nb-D`IzgEU}=b>A)PnmubJ2{ndd z_QW7o6V$ooP6WwRGEs?yG~K0{G208)gzm=p(A(P6Ih1)CVjhaZdpLGtf{@T*Pm(O7 zz|3Uf(W`Y$uT@#b!W+_Af{GPtrlT{7(#i8rH1#$v*}p$7R9~n8WOff#Dp#@Mo-JSiud(CXb#H(7@eY)7241+nuCd=#k!DYAf zw76H2E@DzpG=M2G(lIfe21YIF+WV>!Oe7|A0-{geJW=6rno3BV2LW^aIqQ@OZy2S( ziWb%yXi9`qr7;+4Z9?ysUD7$^B45_nD^OYqA>-MbMENm}RxW&WsCNx(JGTo@XYuUY zWBsZM+X6YM&n#KDY``kB=^*#Yb3$vQ!FHa}Y(-Gr9{%m|h#Pk*Y@tROxlJDKgww1 zd%ov;cm&_}ZQlmGHfW!U^e+q__?lx0{H_=V!6r?;fyRKmFT0X5e zbw*RYuKONkuj{CSmei;vs$f;ZHIbBvl1gNXn5yPvH!Vq!^h4o)XZg{NO6p*xZ;IW{ zEGE&KqPV@dU%Qur3I?y)@7K@FzSm``eRmM-?q#rc(>-+LATw8LKxc(?s@uvU3t5vC zUFo7btK@hRN6UyI(Lh8%q<}scsGFlb4F)jWR8&Z>YmE`af>@Pwvtl&?5?I%d`!c9OQUt3FZZaMaB?fbm)7YHG2AhI3LWX|rheVUxj zVL`T5pK2E0bzRM>-g{0?RYg@VDT4aCuDpwo+_r(d0^!xMq0)TdUUI8k_}JL*_cO=> zKlWok_Pc-g@BZdDzxn6>+@Jf}*S_}Z)vLE8DpSAljc)+g)%bGoUPJafhU<6sA{}m! z$mBG2b0f#P_Z(MfwX^|Pm8+Jt>lMu#O1)__RgO_5%P5JuECaf3#hJ5A3Eq^?oiL4> zZZMt-;de`v}jg7W+M4{d8FaWoM+5&@oA3uad}~2qx<& zZLs$pt##j5>96i~X+&wWCu`Py5>;-HN~Tr2Jw_=^-^(1ou4t|~N|SEsb$JC+08NdQ zTG4J&qq`|PSw^ueQO8(`-Je~3wk8Q3KrQIgmwItS-;x2QCSM!H616nxB%K@D<4l`h z!nC)#?cVBwwyYQtBi>)tk_!D?=Pb^FGXL@2CYQSTWStjP$fM@7jUgBA& z1_Evw8YM~Okr?XpI%wpbm*8;#+w_?pi4C|dLA;!cfRejqIc&O|oi^cFv9N*dk@SM* zc&IG_FP=6RJ`#@Q@E`y2AOF3-_xJvdzwtNLb^Wcs^|$`XKlvxW_j|v0{`zIpfsk}! zy5|c;EBC+r!V!T^h^KEUz4wLbee0e+QaZN^#3-|JtIUT=r`86_spDl2QO2m7m2${1 zW$Ud3Z0e}8PLwxtjCvFRJjZ=Y&^viSBwCeja1=`3z#ClBVP|zZf#G!6uOv`(uF+b1 zU3To7{ob{S)iqMp`>*2?T+}7I%2j!Fwep7U74Vh~>RO4)G)RkK{^}vUwA{` zN)jk5R4B%XLZ`Z`2}u|WW1zL{YFVnRWmy9uxL?&t@P37m7!?kdWwR8GR(zyB&K0>uPBeIbil})YpnHA#? zAEl(18v_0mZosj+ii*$;XJYc;F;rq}GF zU_&ZvW$&6~zpm>{`kNdgrdOmS2ZjiT=u!ZgN`3IUtz|1F~ zM|L_wo{6LM!c*WJgjNG|WOT94ovgW=1%Z_d7d{S>sITSl*J|*;{@4F{Rqb}W?jB>j zR*(PS5B}hv`cr@E`@Zk{cm`|_bK+4g!EB#eP6B!4i(gEGGO>>Rw{O;SSDj-r%fkwu zI6#(2%4j}Yua>2&ZZ$H8n@UDGMs)yVg6i?kmIWRy*CbFwq0{NUZn~-jAsz782y>XC zA!Tx1Qe99afZej1W^JAQp_4YNeeH|hoeogx#DIPMy+T}*l*gvPO^0ZKg{G@adZec2 z-!*{O)F`!S6zisjrK8Gj*6wm>jj@;k>qli`ec>FarzMxGYJ+w938gBas!0$LV3vaq zX^|`u6$#P09T?1e#EEFtrW2iH^X{KfG5*nAa#|mpl=h@FIji%u2E6@;2Dfh_9ZuLd zH51&h56)$a{@){lRahe*Yi*4}Rb8`#lS?54B>2y5xGj@G$5A~V4;z(;BWlbzPWPMi~(HzgP;1_|H%*kr+@s<{JF1uh2Q?W{?&i( z*Z)iZ7&lr@ zpL4++1}-5pVA36z%}lAB(wMMHicEWcg{lZZ-T+BP4<(Q(ha1cQ5~M1fB;BlQW}Sjo z!H$8e?i5X^EEQeL!ZcWQg=Ka3z)C7sxf}I*u_#fsG7a-hO1U*V5GF~g#&zK(yvT(M zXCeu5JEDZOC4=VL4U8I9)rneF)85s&+;VBIQI8aaKG1Lya^TBo^Abh*8KAnw8$3zm z@mR_UQ^oSWT5(CTeSbG0h{$Htd*-y1P`ZAQVcQIU;m`hwKk)niqkrS~{=5JAfAvQ{ zTlr)c$p82k|L1@7SN__6?0f&|U*OOETfh4^{DmL=(SP&z{MOzreDrR#?yliWouQA;-&sLNw`zP_eUMd`ym z&)Y1atWp!w(1z&gU8F_&R?CT&s#PA@gKC1-{hc}gLAKpxT&IP^h4B&gTLaR{+|E!fBE13+F$+I z|MHLhND^td`sttge}3tA{!^>4|0ery{P2-OS0ArA39aVjHCX{-y+wH zW!S5rbmF_E3jwXUo=w^e-#1L=yJNHaR((LH0TIbPkxQKZMJCJ!D`_Qdph=Z_PgJBb z5Y>t*4V38N0vKbcCS%;Tawi3CnI2;S8VOYcNC-s2_0nDg4AT+N?se8`VwqBA&e5kQbwmg%$wSc^0_LT>QiCCe^9MV3Umw2dzw`|8rJX!!Cj0Q72stuV4vIO*(s2B|a+VY-9viNYWr3Tj_l?zk z|2s*Md$5X)&d<;mpm+_N5@4GV82Z{XdG@BM*)mayy9&%XZ2BJAO_UG0DM_x~z^`tX#_DXKHu#E#kCCee=V)0&VLHvQZs)5AWtCRejG+jm8;Yz+OOif5ZG|c{I1dbqqZ1s#V0vAu zV4waBE8)#jogNiO!CF;lfjW8>hF9ZNX#-bRd&rU+Ri)FF!%i#@dOGODnR{0W6P`&9s6uZ&t2cm-Maj3wN~i!^@W zG_N#SY$6(Y`$dEGzRz9Xw3>ASbMeLD;8f zJ6f`Dtu!?<%Yl?Br`mfmMyb}YJPI5g$x>O0GG3RIAdWPW&au1|gV>2Eq+@yesAQfv zJ8M~?nbvTor{ae){E}#ql(sdG|6x3s=dK2yJh-z#AXQeY zsMAa1LE>ydjY+iBc{uXU(|HGuo(c~tq}Oh-gRrJ{U;T;y^Z(uhT>W4F$4|WC zAHUk;g7IU_b8z8f-k)zXp4NG{ZBe>Q zk1V@b$;eo{p#fKqj%QS9ye*g+t*Se3lt`DoE6R)-5=BezpRJdaI2DN@#$!_gty{GZ zortqbO0o}@bnh+IQG#WgeWw$O0(RA8RXQXs84a*@m>f&vhr6aTLg}cgQDZFBNT-w1 z59W5e@MRDt>Z61cH?=M2q!hw-v5QKJ(q1lsH zF(@X_$nUwH3t;97nD2k!R3T3U~NRmd^f z^7c*ow4thnH*``plALISOn@TyxGh*Yo`;u6N>Y@{3Y2IxD&+X=luC~(gOWroi#J%R z1XX=t);>o?iI7fEQG*m6jX@Xk-(jRT=cN{_5Q^Ay8LXI0f%WC%};kN$P`rzCH)cvs))aG{EU(lx14Ee&X8 zZu)pr5v^Mrv*aEuzZ5O5D(_-@e=cE>Ug$_kYlDNs`84731*3lH_e(&I_z*9!rxnEo zEVyD{YbqmE6j$Td|MUOC5B>0O|CxXNU-`yYm;d6A|F{3yANjW%8tU>Sy6|z(-Boq{ zdH=7^ds<2lzCiWvr<>3lg!VvE${8u3lhfYjiBhI(y`{n<-$`nchx!k>V=|wWBvqAp z+{=pWyCocmuE#mj%WEht8G0Ity!!CP%4|pVa7mbS-&85ztV@DGkYrWO+*y;fdTcVb z_B#C_if%B9;WiZl{id?Ic3%=u1|~W2b&Obo(s}3fVxSWqK@bv@bK^$;WKSON z-N}$yBOOYC(vY{OKuglZD!pFJ1Ex}uFCKNHy8300?DMGOeClI-F=*d{;{Ar=;^%W> z@$rJX_{fX^+s~I48#sHc0k>bEkG_mQPy_X~89RFCtFL~#$SYv?3ts>CAODAc;J5t7 zfAK2IzyCvj=%4(~UqUFMJ|dHh3m-F81+44Z-N(V()^&aTwW>b*?6X&|Uh%P_p3To)UXjvkZVfL*& z62I+ou9qBHsH&wHI5^K&Rn0%nimHYXI#I!Oy{_CHVuwiFtVev$}<*QMT;*pu6~+0xUlvDao|-SOX1669Xa zFQ992mppJR1F4!h8BEkNJ}@5ibq3BPhdVEaY<+TW zbOcC}ts{Xdc{JVOi$}5{)YxY7aE^_9Os~0|Gj>?#gELS!0ZY>SGr#8hfAoL)(GC?5 zmQR1fZ}~0%`@j5KmW8V(pi}_#W4fj;e8hD35C8BF@Avzvx+7?fF@QhxhyD;BI+hy@ zoSx8jDS_xcK0@OA&KBqMOGsKtKhk*ZcFpxjp|MW++$4ga4~}xyHXxdiKuAwv#+ud2 zDm`q)0P0b-Kr9qM1B@!?3v^UjxueME4b{1>Qz8x2C;}M7%G_teHETkm5{Sovye_v+`-!f|fCcrCPV zkt4%4+<1_bO;pJ?&f31j)8G$zx0rq=Y5#Lgoov95$LOIx(7fUy0==fR2CS^1jb#fk zJY;>n#*b%W|H8-0)z#H*xBH#H^LPH>5B}g^{i}cV&Li^szVG|~#Gm*Rd_ZVH&YbNe z5VpJirNgZ>)!7ojBTw|u&z(_e(r>k9&*PN?xo@DaNREThm==yichjk(VPEWqJL7VR z3W-86tYM<=ZHYu{Z(9jBy~><*qI;AOOo>FPj+QYPTkGz-pqsgKT9tS*(o+>dY+ej1 zqFS=%o#R)DS|-{F3g&~6!?mn?=iKDLrfgVRK)NkeFiELXXfBl~0v%A%Qj}3;dDKq& z=Q-6}9p(8C0jD%BtT{fjo`w|^zgRikJ5SLhrMJ>p9^L-jrc?pNBV}ee*Hx{DLf5HB zeNud^2w`YZ?sq!y{p0LK49^F3tDqcw$L{-iCF_`Msg7(TVH=j`nZloy%liZFJ>>P# zc>+zi#b5Z5=bYMu4{owSB)Xiv}BZP8d`?{sTv@Y5(yezlY&m{ z5YqQ&y3$JWrMPsKvH{7;ZMyM+Gg|_7S^@|AhA(kcQTo&#m#AuKDcezRyH`vb>||7R zqSA4S&z+2NNNJ8#sa1~Q7~|%vCZv+C?lnoElhVwGO($#893_B+wHrJMiWa;js;cq@ zAee4t_8Nnm^orpQ>1LwlmKO1>x1U_|pggbEw!y67*^q=S8@hp(LWxiXHTc+ zPA0H+pp>+Xxm)RjD!7G`ta~6Bkd&0JFv>Bmr67|W9lU%Pj^YWAP&&oiTF!h}@6y{U zQ{0eII`hJg%%&0xqhL9z*0oit3k;SeC8HdrR42S)RMl`9Rb$M)RqxjX7@c%-C#&|p z`)XY~iPbGtNveHQ6ri(~b=&LHCsAE#J=)7RP!OdsSphZk^LEUnT$hw}d_GEpqMD91 z0QJmakhKbOdh*r#^pt9`0Yi9BgiQ#(E;pWJ4%w9!sPQry)sl`@TB}f`-P;cpt+Bntmn2JLplxnA05@8tVVcnEyVU>cq zRJklbNu^0wRY_GZqefYpG|)@mn>9fMYN(rc3J z*Vq3A0(rYeESw!xaA!;WNF^oPsx2r=N2vi`z__oBP^`~7Qa{8#?UU-_s1^q>C5 zH@@*0yYwUvquI=~>eMe$D7UiUr5x^}XL4p>%oXI*>k9_7C5wUcy`u}4^G!dS4uKo+BLR}flyg}J64;)hANeTsV zu|_duV99Q=$}t#K!-D9|$8H%V5-k-}?`|s@qrf;Nzl0K4Ls?Z+x$oXph0+Qp>F#fS z>szZ;E^Bp?wR>HAMqoIvUHUt6Uxg$gUQ-_bVaRDamSjFr>sB(ePQXX{jpX?yAxzFI z`j)bVCOie2WSUdl0?_7v<2di`YC?RLvDs-(;)$GGW6 zS(59Y=9MgXJOr9jw^d>_vt-t(%)7CuF>nN}O0#x1ezGiA>)JBf4%Jj6zv+T~aliM=y^OpWGUDOU2WhChA;&o8$Dw9qe<;F8LtD z9ng9y-T(0415X)2eezI>B&U*jcT_gy!rd}%doZi;yZ^kC)4@-+cn0g#GmA$fX-_j1l}FR@ zIWE(i^&}8|x0j({B9OKmWuRcdDAR#hP->24fRd6LEMwHDsw$AV@inl__c|m)&kDOG?$<7_Z7oM%8X{HbYYf5e zCIN?b5;n4Z8ZmP@jwh@tB&F@W(zrr2GrrwE(F&9%8y=5Z+qB2YliLeF=j_C#Je5!< z&p#H$S+&q|7E~rl_~O}qJd=KssVKKwz;88BfB*iddG;du!dv-{@A!_N{n?*=GoQSC z+qZq&MH;_w;oKzo`q#g{-|t_G;@|n5-^mw%p2R!zMTc{d`%&+e$vgRcmWyr}3&}=& z_jdA-O_$D}rJP~ot=p-C<5A}x(lgJNQj%Mv!#LOV_Ml(6sTl>a9O{h3re3!>st&=R zL{g^8WwA;~4V7g##!|ES%w}ijGZ)FrG3szet`dy79S114MgqMQKw!`PxTG z%{+;JG|zI#JX|FPE2SkRXp%BZ`IX%mGO9*dquz+(W2ur1vlL~)n+l^2{^ZjqoEy|S z_7a+>sIJyE8V1nGfK)m$NlIOlRAB|d1gY+oQKMM9uBGu&*EZ^ms=aHk>7!e>0|C#X zlAK4DfQEv>Rl`pq(CB{jF@ZFvaa;{wj(>OWH{?(GDhn?6-Y zzKmn!`|s;iFMM#S=?C{~zB)btZM+aL&$^Rm6ve;#)vx}tU-rxX_TT>7>$<)+m)-C8 z%d&7E1|M)ndJNBYR_VfpFOsXPt5-GE>=O;l$K&sLab0VH?%|os} zB41G+s`HM~iSzs0OcK5A!JZ>c(Q%(sPA&99x`O99H!Gdb6OO4gf#7kR zIq!M(Dncij<=#0y;;eS;jaEV4!O6+WeJ3kAh4Ui=-%_UJTq04GQmY_(XrW|`DyhN( zRzTV9cGO|dV%Bs_r-8gd5Ij;Tmlbm2FX#Jc0PdZWwyicw0+TCAn&li^ZTs_vFC zs$T7usV8TcQ39n)l?oYEHL9wFfU4D@J7^Lj8f@4tV-3A_<|z@Y+;=ZyF)2Z*>X0Tu zdZi2XTM*C)lMVHJmaP9>4?bp3_g_EMkhj`(`#C?N~bzOI;po_&ZWUl zCs>svp_5Lgw%i${Y0LSd|vCRyw!KzkmSC+=&3T0#CoPWrsW+!FUz-o`?r7p_kTaI z-|wsH;x~Tb!Us&Sst6KE(&vB2hb=^yDC9A&fe!_2>)XGkbe25WNa^a$SKnc-*0+Re zvh}4aa^%XM+i~C2J6mZIkNPSl304tu--xsH^i^Hnn=C5cEzgvqow8E_HG3v0kK42g zor~8+hZe@DF@#l_@1Pos0gX-tYP!>>rk8>zx`GqmRQa5^LBFzXtfz8MOcY&t2)-OExLMF*guilrDe0}51@V(6=7yc)}Hj8B$f7z&Kx7_}=#SyH|! znKNi(l%OtSjAh}-_d8J9RMH($FbYz#wpB?y3Pdzi6(W73FgMaOQ@mAnqmpS_;wS`R z?dncxCu?`<6qPv&AfWGmxk-*5F62JBR;Lc?QJQFHAyrg?Qb9{@H{zjr!TaK6h}r6< ztnvkrFF{ux0Jl~@BWJh6k$cEn55J%1G(O{4ai3QjKP6iC8s#N08c1abQ&4R7<}# z30dCLAnUfsol_h8WBbpfAMZ?6D%7FD+>&OU>jG3+H^rG|6kIb(zyeHq6wI7bRT>}| zwPwewgjnc2l9q*JTF7RMI+FRLSg0V{mlwnbf{;_~nckEyMTZVTg;6XEORIPk{T+Urm@Ce!PAGNSqYepHLNUrphAfLE zU-|UatKDu4OFw<}$tTNFu;B3eQ#em zT)R8&dtJ&-h6I?wCQ2g#F%860O1eW(ObBCb);DEIhQ@-1(dkjOcGhIyt#+Va6BQMc z=BZH?Rcq5DILX}-z6jJW@)ebjT%r*mc}d>Iw|IfWGdUq=H)XI2$g|fWe|RiUDTKew zEB?ZT3r~d>_okxNdq&~JZ9Zl}M2_&`+%*b8P`RHvmY9>M_jVv@^}0?eUo6%^ATP~b zt+?JBs{Qygb6t;CteZN;9B9?Ca6Z-Gs!~;CRc~9cM`dxOm2dm>)7@@?EMxq9 zqOvg-ef>FL$gZjsQd&|~%Q6h*uvAOaA+17G1nnF~q~++UBx@&OMjEfRJG!PNN+;V|x`$F#X#-|i#%|OnZ$4{S^64_hs8Nop zQ7q=w7>n$7%Tl!~2SSoaN(-)y)k^_Gs-cc;fI><-YfuD6xi%JQIz)uXQi^M5-(4E$ zWK^vo-31|P5;fu)%TAh%5}X~YV6a13`~51RMJNJmu5X!TU0YZ?&rlDU1pOiM~A_Y|RKYo)ViDX&Y=oBFCZB|1y4>rg6(Hs_a8 z5uz!@s8hhMn-$RlX0s_(;HVu+=GV-K_hntCiC-j9Ptsc73ME!bVmVThQDe6oua@Q2 zaFk}orq#-ik0pV z?LgcUs%B_2J1GrFN@k?Ch@FTbB~}>%-Cd=QPC5xz<}4M1WD2+#g-~NiFrCam`obhn zlmWUd_3??y(`hNGU%0Rt zg-KO)uS`@D&cK--D=HI@LabZIBQ|f_r{__zi#n&{QBSHSxnC?7w>yi69Ji^5%_zCO z(efx2*4$hHB2HO5vtundWeCme^Py&`HMQcDq%6siG#*6Jf>fb!HR|gah#dV zma?1O7|T+xmSs2U)ov{1ZdA!H_{t}*fy^lDwX0gHN+m_r6i6$MQEF+;D{YLC^vS#c zV25A_FqkChj2>476iv^(fotcW7#FOyo2p({Kv;V%D{pGbA(eaypcU-bB)c)XglLbr zCL^pS)V|}wmG1Xzci);$lxwsSU|XCIEeWKo!%h7zGMxvk>J9_7M)4(Z#=9bqh&mF^ zOU@wVd|^jfeVX>wdw!HWXQ|{|s4q87xR8zS*{9tvEpRT+h2@2w&J$J$yy9%!y6RlY+9K$wPv~AR>Q~gSP<&;80zia7f+6fHHAvmf+@y}P&K^e zh;FHB0$3wdC@5#I&HXRhB%Bdp5>JxHJTmk&2EhqH6~TLia6wWKMI#o3!Z{>8*?KxW z5A<$EKsZiEm-%ljmfcvY>XT)Z1Fmy}f4bYfTE=ep`s=GPUX?X!8AFAG&$!xYQ3((V2wBXks{M3473 z&Ofnmis&T*QXGD|Mzt8Is0tIybV@Nt^=GAno>GW%oEGXxmRD|yj6p2$Fv;+YkSDD5MdQc!QH5K?KMd!gg}P`t&`Xhn+7~SLW!k%dc7;9 ze93rr!6`&GaMrw?Uy{$Z_Y3FeM)G7&-496Wd3hKj@(A(~oth6uyd1~VbMNWgesy)V z+wFks&m@-!b{9S#)Mbn{A*}tv9LLoM3WdJ^IbIx*hpjZjq=uj|d1N;Vn4;ukiI~WN zu3Hh&J?UTHo;MJ5ejY_VwNxZZDP$^EmFU40yu2w@=N5A$ayymbrCOA7Adac0o%OK} zIg>1_fI5U*9};g_-CHWRg7=Q9wA@BjuPZ&<$?uOAxhASKHwlZUB8p1 zJ{b;fK(b!z5bM(WZ@sWswpNGI`agp-2*2l1gXo z?hr`mv{>5RrIAiX6@gxplmHCP+zm<3drUi#X}*ra+DYO}HlFL+YVNqrM^Z`1)cr|X zs|-+9hfYk^n=bn#!jVeu74C{7s%k!69J1#OJOht$Phut+j`%q3qmK^D2N6boGv|+> z&X?66z6i`C`(i$tFc8K4IG1aD_HNh(KGh|z0^j(?H~!Y&`ddH#hM)fFpZ@ySzs`jV zA3BzRx-5eaklZU{pDg#Fs&^`=iZ|d~AkGzt9&KdA}riiJhVan^2~ zC54lzQ4}b>^Cu-$R_XlPDo0sTbAgPiQi)TdaJF;2u8vV-RE=SIvrMXLjNxr>{HkIi zs8)?CP|GUSQcJzemOgWYQi)_}!OeO(%H_>6%&Xz+<YJBzQ zSC*eIf3;k%ypm7KSEF7Hcg0TMc$JUW%h1xLY8hixy=lD0sO8P~QKgiQs!>(eo3dKQ z_3tH^dDWF;Sa4l=Dd%4?h9#q%i@Pzdw^iQMmyni2mI2AFC5Lt9QX-KqDJU7`GAOef z?sj8Ytl_fMSYEF$HA-bWRbEq;rR76I!G{UNXQ36RtkSvoB#nAgVh1_t9FGg6+&*`%8c6Fa6|C{$vu~uq?~BzV)rIeeG+%=GXk1uYBbz*VQf~zY8BQ zYd70Tns;-{oV%*HLr}RFnVi^02BGw{`Yfzln&ZP-J<{JB(3Va^LxaogtGn z4UX%Hw5raL#99L8BtC+IC{M?H!uz4xufBs1#aDSNT5KW7S&v(lJeuyPNqA|$e^1=h`mfgBvtyb67Wf1_cuCA_y;VFlIL8;DOZ|=1;{_52$ zmHSzjU6cK1pXH4e8=FX?cK6Ty+|T{!kN#+PfB*M?|1baLzx)^e!e2PX_wev@`;PDUj+g!jyKv!N0-cls9g@6g^E1_VxD=iUy6+PIlP(Js zL?FlQoUETjJNCK}E#8U`8gr={`oU-?1@WLs*5LelB#<8&u9wcHOC(iQVmS*nVO|?m zmBdn+lPRg1ozvu4SC*rVYB9fhTmjG$Ax)QZgc@$BQQdNanqEV_;DJ3jju#S+gio`h@^cK%*!M zi6M{>xS_E~vz8#x6Q^@9lA&36G`a{{;pM@dMgT6*%%v&~bTlgjcNe;#%CbkG$(=wT z)7wn$iqSkN?k@HHo-}TX4Gjh*6349q_BHorJf=ya12X|TGuTd{jppv>jzk1tm;@w@ zG|MDC^luLxJb&S+mP7C1o_S`?8~@vhYk&Siv(cEOM^X6t z(Z;$rFI{%eJ>NzDaQAiV)~#5vqF%2rS+Zo|!i6ni&6+ijKKf`&IN*Q-mM&c?={w)~ z&f2wWF@qWGh!E(cNH72pL8GRPXBvA7Qub|5>`&ec0tU1VI1~0VcKA?Rr34EUOd>$Y zgaB?RY?sbKCM~r8?R=wID&=vi*VXHU*&bwd%rIlw6_#Ah(^X-tFu|&qOk)RZ1Cq>C zVd+Henk0nk%}ZY8>`==SrtSgC?!IOuIw5Um90ywoG)`EytR7w-bh705Mzgd)2pB*i zfO)Wbw81=^Rm)ptFpKpVG9nT!VS`#;x}!%9r8celdW-{ER?lJ{LuL#>0LozUh>itl zB!LtX!BB~_tA=ona5AXG9PC#FjdK-|e>q%-HWG z+vH`EQ?$U|GBIoiUoTA0j#di7v_X7{sL|%SCm(zK)YE_Gx|{!b-mCUCFFx?T_nWxv zl#^r31tS1C$O0xJ!XZyezNM`PI3XaQ0309?njnE72OydNlK_F{sgYH2Dd){*bJna`#~pXvuDkAf%rVE@a?35P=<&si7h?u9*bbqMv#^9H6@>)4 z%%I>ZXp|snGAHpQ2qLJhWSSrVvG)q7iBCp=lke}bqlaGdPf}`vz*o80(?9^(JxU`b z0h~(uX?DxKA|aKMC4#7Qiv-BXY92Bp0TSrU>P$*Z$Ez@?vwa;*jgtc|5@;1(^91X@ zhgP^`t~uSA}Nq&(8Lp&G)R(=zLwI~S~9&~)JN)$YD;a$W;{;v zRaN@`v}px{44`CC03uYgFc8N{(d&5}%(Kiin1z-wRI9aWjEcrQFbf+#4TuJya3D}- z1Q=MJHzflnl}3>EeXM&;ktH#K05eK9USH59BbyMw!V$?aK@g?!8R-(sZVbV!p@t;6 zBpBuC%@arggapvc1tSZI?zG^NC?j2fM3R83Y~KNro&=B#W?-ZhuEL#7C%gaxSh!qp z3C6;GLO*bJy&JWqa%HP?HIS+vXUill)AqSeHjjx;UDevC4UrJ04M=W9Gv!p@xYL@J zzb}Z}z{;y>lsk_EH1(H&*u6Zu-b05CZvu_WKmExw&%0o+17040VD^D? zF8Gb#Jo&^oov_oKIXB&O>-lH@=}>TG`PDbwI&a>7A2{obV-H_=&6QXEpWtYahcJsg7J7;cY7&-s!GjIRqoiD7%C0Ac}(vl;GpIh^mlb6jo;K+ab;lrzL zzwwF3|NpPN?X(fb5c4Z9yX;q&90$n3AdEcwv$y`%$^ZC`|MSojkNuCo_?zA4?!I#Q zZEm&C{nK3s?mu^^7&-r(Kf3w$))7V0pyA=CpM1lz-}~u$jTXhK`|qAJxcv=*_~Vc7vdb=mgM)`2dT7g2ebJ&t^?H3K-}nr+FKCiVmy;|�&~v6p$D*n!M>Q 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%0Aq%zzXKa2UqY@p$K_22Ac^__ zI%iL{3qEXP>A45%yrWlK(#yR+NAMlJ24b@DeEF0h`P5$b=lwCg8x>DJ@Ynv@UwiZB z&5o11`|jnf2{Rv_qHDPafi)?`-1&@WUuT?gGu(Z>UjN+B{amBsA2JX_3cwc{XoNW# zlp8t`r~$Lasu31Zr3^lgaN5ytsM60#VnP*ZjV~52mKmZ37-%%h$uS!l1k9W?(o(+L z14aX)ZinkPZ@0tlzwH1;#JL24+MwMc7y&1N2dRMwVj7C6**HlbKlv(|rHILE$<-y-ny|_Q9?+=oYc)NZS znfy_BT<`&ZbTg$JL8x)^&TVIXXC3X;y5H3de|^zk_1?2yt;&UafmVIr8CPb7oOpOX zmdi0o0g#c9CjCWHNakP$A;T&&fFga-ys3(=f#8Yc;YoBxfSmImDXB4)8mVNiwo{67 zLS_Ic)d)#RxtUZP#Y}rtx`#4Vgw@PJ8zKUVKtxnP83r;$7#*ox2q+{0a}GI4L1VX2 zg~U?VZCo0&yo5Oeox70;!3@zZvY8@}+;W5md1y&mJME6Hh>kL0T=+>ECV1vnX< zdf8m~qs24G6(`;e;=4W8d7ioP_W@}yMYe6i=bwN6SO4l?-KmK0h(X`?!!V?W{FI9G zNJK;*$Xia#h=u&q^RM!E#u>Lh|9boO?XJZAuF!qgb*t41PmT1C9zn1aTy&VAm}>Dz z%Bh&3h)Mwxcyo~zCZ11!9L(me6#bo%n@gx;GeW86RY{6aTPi1K>2r1x z&2&IX0)hNE%v|OHLKYxN5F~@PZTo6eoUGLf{moWG#q|J%$52aPi&|wckIJCcI&6mR zW(YXS#7YcZfk23uU{qEhhXtlEY;%kVfQy3XK(=u+KFT`Av)G}8o1mxmlHIfq@|X=6 zL5UC%B#d%nSJ-#9UiT}vi>_RB{%X}1CK}}2V@ko8h6M>H z1kPM1Lnk0&>Hⅆ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

o9k`hI{Pbj3l`7Y49*pma&dJN=XS4(*R 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 J@$zH3v_;GliHTR{ryxIq~a^=0o^a*XV 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

>0aGl*`e;9;quR}_$_QzuVJpWp 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�ux1iyqk)^fPSY+hV#GW% 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Μz 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 literal 0 HcmV?d00001 diff --git a/doc/_static/images/session-task-runner.png b/doc/_static/images/session-task-runner.png new file mode 100644 index 0000000000000000000000000000000000000000..c6f769597de9bede7c3d8a3a839e4a3613fac337 GIT binary patch literal 47645 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 literal 0 HcmV?d00001 diff --git a/doc/how_to/concurrency/manual_threading.md b/doc/how_to/concurrency/manual_threading.md index 25c85d5804..39dadc6fbc 100644 --- a/doc/how_to/concurrency/manual_threading.md +++ b/doc/how_to/concurrency/manual_threading.md @@ -87,7 +87,7 @@ class SessionTaskRunner(pn.viewable.Viewer): def __panel__(self): return pn.Column( - f"## TaskRunner {id(self)}", + f"## Session TaskRunner {id(self)}", pn.pane.Str(self.param.status), pn.pane.Str(pn.rx("Last Result: {value}").format(value=self.param.value)), ) @@ -118,6 +118,13 @@ button = pn.widgets.Button(name="Add Task", on_click=add_task, button_type="prim pn.Column(button, task_runner).servable() ``` +The application should look like: + + + Since processing occurs on a separate thread, the application remains responsive to further user interactions, such as queuing new tasks. :::{note} @@ -231,7 +238,7 @@ class GlobalTaskRunner(pn.viewable.Viewer): def __panel__(self): return pn.Column( - f"## TaskRunner {id(self)}", + f"## Global TaskRunner {id(self)}", self.param.seconds, pn.pane.Str(pn.rx("Last Result: {value}").format(value=self.param.value)), pn.pane.Str( @@ -263,6 +270,13 @@ pn.Column( ).servable() ``` +The application should look like: + + + :::{note} For efficient use of global threading: diff --git a/doc/tutorials/intermediate/build_server_video_stream.md b/doc/tutorials/intermediate/build_server_video_stream.md index 16037aacc1..daa9a5b9b4 100644 --- a/doc/tutorials/intermediate/build_server_video_stream.md +++ b/doc/tutorials/intermediate/build_server_video_stream.md @@ -2,9 +2,12 @@ Welcome to our tutorial on building a **server-side video camera application** using HoloViz Panel! In this fun and engaging guide, we'll walk you through the process of setting up a video stream from a camera connected to a server, not the user's machine. This approach uses Python's threading to handle real-time video processing without freezing the user interface. -Let's dive into the code and see how it all comes together. + -:::{drowdown} Code +:::{dropdown} Code `server_video_stream.py` @@ -127,6 +130,8 @@ server_video_stream.servable() ::: +Let's dive into the code and see how it all comes together. + ## Install the Dependencies To run the application, you'll need several packages: @@ -143,7 +148,7 @@ You can install these using conda or pip: :sync: conda ``` bash -conda install -y -c conda-forge opencv panel pillow +conda install -y -c conda-forge opencv panel pillow watchfiles ``` ::: @@ -152,7 +157,7 @@ conda install -y -c conda-forge opencv panel pillow :sync: pip ``` bash -pip install opencv panel pillow +pip install opencv-python panel pillow watchfiles ``` ::: @@ -342,6 +347,13 @@ Try serving the app with panel serve app.py ``` +It should look like: + + + ## References ### How-To Guides From 130ad417ab44eddafb4de23a4ce3f84479160d4f Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Sat, 27 Jul 2024 11:06:50 +0200 Subject: [PATCH 02/91] Fix empty Vega pane with sizing_mode exception (#7020) * add failing test * fix failing vega test --- panel/pane/vega.py | 2 +- panel/tests/pane/test_vega.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/panel/pane/vega.py b/panel/pane/vega.py index 854536eb43..081ec5661f 100644 --- a/panel/pane/vega.py +++ b/panel/pane/vega.py @@ -267,7 +267,7 @@ def _get_properties(self, doc, sources={}): data = props['data'] if data is not None: sources = self._get_sources(data, sources) - if self.sizing_mode: + if self.sizing_mode and data: if 'both' in self.sizing_mode: if 'width' in data: data['width'] = 'container' diff --git a/panel/tests/pane/test_vega.py b/panel/tests/pane/test_vega.py index 99f29620e4..436befab4c 100644 --- a/panel/tests/pane/test_vega.py +++ b/panel/tests/pane/test_vega.py @@ -322,3 +322,7 @@ def test_altair_pane(document, comm): pane._cleanup(model) assert pane._models == {} + +def test_vega_can_instantiate_empty_with_sizing_mode(document, comm): + pane = Vega(sizing_mode="stretch_width") + pane.get_root(document, comm=comm) From c6d0ff3efa12198e30685e5ff6c231d760eba51d Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Sat, 27 Jul 2024 11:07:33 +0200 Subject: [PATCH 03/91] document vega dataframe support (#7024) --- examples/reference/panes/Vega.ipynb | 77 ++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 11 deletions(-) diff --git a/examples/reference/panes/Vega.ipynb b/examples/reference/panes/Vega.ipynb index b008a74aef..8ef1c8a9c6 100644 --- a/examples/reference/panes/Vega.ipynb +++ b/examples/reference/panes/Vega.ipynb @@ -6,7 +6,9 @@ "metadata": {}, "outputs": [], "source": [ + "import pandas as pd\n", "import panel as pn\n", + "\n", "pn.extension('vega')" ] }, @@ -14,29 +16,33 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The ``Vega`` pane renders Vega-based plots (including those from Altair) inside a panel. It optimizes the plot rendering by using binary serialization for any array data found on the Vega/Altair object, providing huge speedups over the standard JSON serialization employed by Vega natively. Note that to use the ``Vega`` pane in the notebook the Panel extension has to be loaded with 'vega' as an argument to ensure that vega.js is initialized.\n", + "The ``Vega`` pane renders Vega-based plots (including those from Altair) inside a panel. It optimizes plot rendering by using binary serialization for any array data found in the Vega/Altair object, providing significant speedups over the standard JSON serialization employed by Vega natively. Note that to use the ``Vega`` pane in the notebook, the Panel extension must be loaded with 'vega' as an argument to ensure that vega.js is initialized.\n", "\n", "#### Parameters:\n", "\n", - "For details on other options for customizing the component see the [layout](../../how_to/layout/index.md) and [styling](../../how_to/styling/index.md) how-to guides.\n", + "For details on other options for customizing the component, see the [layout](../../how_to/layout/index.md) and [styling](../../how_to/styling/index.md) how-to guides.\n", "\n", "* **``debounce``** (int or dict): The debounce timeout to apply to selection events, either specified as a single integer value (in milliseconds) or a dictionary that declares a debounce value per event. Debouncing ensures that events are only dispatched N milliseconds after a user is done interacting with the plot.\n", - "* **``object``** (dict or altair Chart): Either a dictionary containing a Vega or Vega-Lite plot specification, or an Altair Chart\n", - "* **``theme``** (str): A theme to apply to the plot, must be one of 'excel', 'ggplot2', 'quartz', 'vox', 'fivethirtyeight', 'dark', 'latimes', 'urbaninstitute', 'googlecharts'.\n", - "* **``show_actions``** (boolean): Whether to show chart actions menu such as save, edit etc.\n", + "* **``object``** (dict or altair Chart): Either a dictionary containing a Vega or Vega-Lite plot specification, or an Altair Chart.\n", + "* **``show_actions``** (boolean): Whether to show the chart actions menu, such as save, edit, etc.\n", + "* **``theme``** (str): A theme to apply to the plot. Must be one of 'excel', 'ggplot2', 'quartz', 'vox', 'fivethirtyeight', 'dark', 'latimes', 'urbaninstitute', or 'googlecharts'.\n", "\n", "Readonly parameters:\n", "\n", - "* **``selection``** (Selection): The Selection object exposes parameters which reflect the selections declared on the plot into Python. \n", + "* **``selection``** (Selection): The Selection object exposes parameters that reflect the selections declared on the plot into Python.\n", + "\n", + "___\n", "\n", - "___" + "The ``Vega`` pane supports both [`vega`](https://vega.github.io/vega/docs/specification/) and [`vega-lite`](https://vega.github.io/vega-lite/docs/spec.html) specifications, which may be provided in raw form (i.e., a dictionary) or by defining an ``altair`` plot.\n", + "\n", + "---" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The ``Vega`` pane supports both ``vega`` and ``vega-lite`` specs which may be provided in a raw form (i.e. a dictionary) or by defining an ``altair`` plot.\n", + "### Vega and Vega-lite\n", "\n", "To display ``vega`` and ``vega-lite`` specification simply construct a ``Vega`` pane directly or pass it to ``pn.panel``:" ] @@ -91,7 +97,7 @@ "metadata": {}, "outputs": [], "source": [ - "vgl_pane.object = {\n", + "vega_disasters = {\n", " \"$schema\": \"https://vega.github.io/schema/vega-lite/v5.json\",\n", " \"data\": {\n", " \"url\": \"https://raw.githubusercontent.com/vega/vega/master/docs/data/disasters.csv\"\n", @@ -127,13 +133,32 @@ " },\n", " \"color\": {\"field\": \"Entity\", \"type\": \"nominal\", \"legend\": None}\n", " }\n", - "}" + "}\n", + "vgl_pane.object = vega_disasters" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Lets reset the plot back to the original:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "vgl_pane.object = vegalite" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ + "#### Responsive Sizing\n", + "\n", "The `vega-lite` specification can also be responsively sized by declaring the width or height to match the container:" ] }, @@ -143,7 +168,7 @@ "metadata": {}, "outputs": [], "source": [ - "responsive_spec = dict(vgl_pane.object, width='container')\n", + "responsive_spec = dict(vega_disasters, width='container', title=\"Responsive Plot\")\n", "\n", "vgl_responsive_pane = pn.pane.Vega(responsive_spec)\n", "vgl_responsive_pane" @@ -156,6 +181,36 @@ "Please note that the `vega` specification does not support setting `width` and `height` to `container`." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### DataFrame Data Values\n", + "\n", + "For convenience we support a Pandas DataFrame as `data` `values`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dataframe_spec = {\n", + " \"title\": \"A Simple Bar Chart from a Pandas DataFrame\",\n", + " 'config': {\n", + " 'mark': {'tooltip': None},\n", + " 'view': {'height': 200, 'width': 500}\n", + " },\n", + " 'data': {'values': pd.DataFrame({'x': ['A', 'B', 'C', 'D', 'E'], 'y': [5, 3, 6, 7, 2]})},\n", + " 'mark': 'bar',\n", + " 'encoding': {'x': {'type': 'ordinal', 'field': 'x'},\n", + " 'y': {'type': 'quantitative', 'field': 'y'}},\n", + " '$schema': 'https://vega.github.io/schema/vega-lite/v3.2.1.json'\n", + "}\n", + "pn.pane.Vega(dataframe_spec)" + ] + }, { "cell_type": "markdown", "metadata": {}, From 78982fb9d3d723f9e61df28075cf4b96f9470815 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 27 Jul 2024 15:00:14 +0200 Subject: [PATCH 04/91] Make autoreload robust to syntax errors and empty apps (#7028) --- panel/command/serve.py | 17 +++++++++---- panel/io/application.py | 2 +- panel/io/handlers.py | 43 +++++++++++++++++++++++++------- panel/tests/ui/io/test_reload.py | 27 ++++++++++++++++++++ 4 files changed, 74 insertions(+), 15 deletions(-) diff --git a/panel/command/serve.py b/panel/command/serve.py index fe21ecbec3..cbd19b7eb8 100644 --- a/panel/command/serve.py +++ b/panel/command/serve.py @@ -3,6 +3,7 @@ ways. """ +import argparse import ast import base64 import logging @@ -279,10 +280,14 @@ def customize_applications(self, args, applications): applications['/'] = applications[f'/{index}'] return super().customize_applications(args, applications) - def warm_applications(self, applications, reuse_sessions): + def warm_applications(self, applications, reuse_sessions, error=True): from ..io.session import generate_session for path, app in applications.items(): - session = generate_session(app) + try: + session = generate_session(app) + except Exception as e: + if error: + raise e with set_curdoc(session.document): if config.session_key_func: reuse_sessions = False @@ -345,7 +350,6 @@ def customize_kwargs(self, args, server_kwargs): elif args.rest_provider is not None: raise ValueError(f"rest-provider {args.rest_provider!r} not recognized.") - config.autoreload = args.autoreload config.global_loading_spinner = args.global_loading_spinner config.reuse_sessions = args.reuse_sessions @@ -371,7 +375,7 @@ def customize_kwargs(self, args, server_kwargs): if args.autoreload: with record_modules(list(applications.values())): self.warm_applications( - applications, args.reuse_sessions + applications, args.reuse_sessions, error=False ) else: self.warm_applications(applications, args.reuse_sessions) @@ -638,7 +642,10 @@ def customize_kwargs(self, args, server_kwargs): return kwargs - def invoke(self, args): + def invoke(self, args: argparse.Namespace): + # Autoreload must be enabled before the application(s) are executed + # to avoid erroring out + config.autoreload = args.autoreload # Empty layout are valid and the Bokeh warning is silenced as usually # not relevant to Panel users. silence(EMPTY_LAYOUT, True) diff --git a/panel/io/application.py b/panel/io/application.py index ee8906ad56..ae3062c98c 100644 --- a/panel/io/application.py +++ b/panel/io/application.py @@ -136,7 +136,7 @@ def build_single_handler_application(path, argv=None): else: raise ValueError(f"Path for Bokeh server application does not exist: {path}") - if handler.failed: + if handler.failed and not config.autoreload: raise RuntimeError(f"Error loading {path}:\n\n{handler.error}\n{handler.error_detail} ") application = Application(handler) diff --git a/panel/io/handlers.py b/panel/io/handlers.py index fec9ab2651..e30c144205 100644 --- a/panel/io/handlers.py +++ b/panel/io/handlers.py @@ -239,7 +239,7 @@ def autoreload_handle_exception(handler, module, e): alert_type='danger', margin=5, sizing_mode='stretch_width' ).servable() -def run_app(handler, module, doc, post_run=None): +def run_app(handler, module, doc, post_run=None, allow_empty=False): try: old_doc = curdoc() except RuntimeError: @@ -266,9 +266,25 @@ def post_check(): with patch_curdoc(doc): with profile_ctx(config.profiler) as sessions: with record_modules(handler=handler): - handler._runner.run(module, post_check) - if post_run: - post_run() + runner = handler._runner + if runner.error: + from ..pane import Alert + Alert( + f'{runner.error}\n

{runner.error_detail}
', + alert_type='danger', margin=5, sizing_mode='stretch_width' + ).servable() + else: + handler._runner.run(module, post_check) + if post_run: + post_run() + if not doc.roots and not allow_empty and config.autoreload: + from ..pane import Alert + Alert( + ('Application did not publish any contents\n\n' + 'Ensure you have marked items as servable or added models to ' + 'the bokeh document manually.'), + alert_type='danger', margin=5, sizing_mode='stretch_width' + ).servable() finally: if config.profiler: try: @@ -422,6 +438,13 @@ def __init__(self, *, source: str, filename: PathLike, argv: list[str] = [], pac for f in PanelCodeHandler._io_functions: self._loggers[f] = self._make_io_logger(f) + def url_path(self) -> str | None: + if self.failed and not config.autoreload: + return None + + # TODO should fix invalid URL characters + return '/' + os.path.splitext(os.path.basename(self._runner.path))[0] + def modify_document(self, doc: 'Document'): if config.autoreload: path = self._runner.path @@ -433,14 +456,15 @@ def modify_document(self, doc: 'Document'): # If no module was returned it means the code runner has some permanent # unfixable problem, e.g. the configured source code has a syntax error - if module is None: + if module is None and not config.autoreload: return # One reason modules are stored is to prevent the module from being gc'd # before the document is. A symptom of a gc'd module is that its globals # become None. Additionally stored modules are used to provide correct # paths to custom models resolver. - doc.modules.add(module) + if module is not None: + doc.modules.add(module) run_app(self, module, doc) @@ -649,14 +673,15 @@ def modify_document(self, doc: Document) -> None: # If no module was returned it means the code runner has some permanent # unfixable problem, e.g. the configured source code has a syntax error - if module is None: + if module is None and not config.autoreload: return # One reason modules are stored is to prevent the module from being gc'd # before the document is. A symptom of a gc'd module is that its globals # become None. Additionally stored modules are used to provide correct # paths to custom models resolver. - doc.modules.add(module) + if module is not None: + doc.modules.add(module) def post_run(): if not (doc.roots or doc in state._templates or self._runner.error): @@ -665,7 +690,7 @@ def post_run(): with _patch_ipython_display(): with set_env_vars(MPLBACKEND='agg'): - run_app(self, module, doc, post_run) + run_app(self, module, doc, post_run, allow_empty=True) def _update_position_metadata(self, event): """ diff --git a/panel/tests/ui/io/test_reload.py b/panel/tests/ui/io/test_reload.py index 4725e05a04..cc5b582375 100644 --- a/panel/tests/ui/io/test_reload.py +++ b/panel/tests/ui/io/test_reload.py @@ -53,6 +53,33 @@ def test_reload_app_with_error(page, autoreload, py_file): expect(page.locator('.alert')).to_have_count(1) +def test_reload_app_with_syntax_error(page, autoreload, py_file): + py_file.write("import panel as pn; pn.panel('foo').servable();") + py_file.close() + + path = pathlib.Path(py_file.name) + + autoreload(path) + serve_component(page, path) + + expect(page.locator('.markdown')).to_have_text('foo') + + with open(py_file.name, 'w') as f: + f.write("foo?bar") + os.fsync(f) + + expect(page.locator('.alert')).to_have_count(1) + +def test_load_app_with_no_content(page, autoreload, py_file): + py_file.write("import panel as pn; pn.panel('foo')") + py_file.close() + + path = pathlib.Path(py_file.name) + + serve_component(page, path) + + expect(page.locator('.alert')).to_have_count(1) + @pytest.mark.flaky(reruns=3, reason="Writing files can sometimes be unpredictable") def test_reload_app_on_local_module_change(page, autoreload, py_files): py_file, module = py_files From c452f7b972c1c061d4941f7ca129804a9aec118c Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Sat, 27 Jul 2024 15:30:35 +0200 Subject: [PATCH 05/91] Add back missing scrollbar (#7026) * add back missing scrollbar * remove not needed shared axes --- doc/tutorials/basic/build_crossfilter_dashboard.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/tutorials/basic/build_crossfilter_dashboard.md b/doc/tutorials/basic/build_crossfilter_dashboard.md index 0ac004688a..915bb0e4fe 100644 --- a/doc/tutorials/basic/build_crossfilter_dashboard.md +++ b/doc/tutorials/basic/build_crossfilter_dashboard.md @@ -19,7 +19,7 @@ holoviews panel :::::{dropdown} Code -```{pyodide} +```python import holoviews as hv import numpy as np import pandas as pd @@ -80,7 +80,7 @@ def get_plots(): active_tools=["box_select"], ) - return (plot_by_year + plot_by_manufacturer).opts(shared_axes=False).cols(1) + return (plot_by_year + plot_by_manufacturer).cols(1) crossfilter_plots = hv.link_selections(get_plots()).opts(shared_axes=False) @@ -149,6 +149,8 @@ def get_plots(): # Define shared dataset ds = hv.Dataset(data, ["t_manu", "p_year", "t_cap"], "t_cap") + + ... # continues in next section ``` The function starts by calling `get_data()` to retrieve the preprocessed dataset. It then uses HoloViews to define a dataset (`ds`) that encapsulates our data, specifying columns for manufacturers (`t_manu`), production year (`p_year`), and turbine capacity (`t_cap`). @@ -164,10 +166,9 @@ In order for [HoloViews linked brushing](https://holoviews.org/user_guide/Linked Next, we aggregate the data by year and manufacturer to create two separate plots. The plots are formatted to be responsive and use an accent color for consistency. ```python - @pn.cache def get_plots(): - ... + ... # continues from previous section # Create plots ds_by_year = ds.aggregate("p_year", function=np.sum).sort("p_year")[1995:] @@ -228,7 +229,7 @@ pn.template.FastListTemplate( main=[crossfilter_plots], main_layout=None, accent=ACCENT, -) +).servable() ``` The [`FastListTemplate`](https://panel.holoviz.org/reference/templates/FastListTemplate.html) is a pre-built Panel template that provides a clean and modern layout for our dashboard. It takes our crossfiltering plot and other configurations as input, creating a cohesive and interactive web application. From 54ee7680049a797bfd5df30fd56ed21c3cbc726c Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 28 Jul 2024 14:19:44 +0200 Subject: [PATCH 06/91] Ensure Bytes default is deserialized correctly (#7032) --- panel/io/datamodel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/panel/io/datamodel.py b/panel/io/datamodel.py index 7a486553e0..1489b012b3 100644 --- a/panel/io/datamodel.py +++ b/panel/io/datamodel.py @@ -84,8 +84,8 @@ def class_selector_to_model(p, kwargs): return bp.Any(**kwargs) def bytes_param(p, kwargs): - kwargs.pop('default') - return bp.Bytes(**kwargs) + kwargs['default'] = None + return bp.Nullable(bp.Bytes, **kwargs) PARAM_MAPPING = { pm.Array: lambda p, kwargs: bp.Array(bp.Any, **kwargs), From d0f025da2325a738a49d5531e35f871e3482df17 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 28 Jul 2024 22:46:45 +0200 Subject: [PATCH 07/91] Ensure Gauge is responsively sized (#7034) --- panel/widgets/indicators.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/panel/widgets/indicators.py b/panel/widgets/indicators.py index acd2a3ddfd..920e2546af 100644 --- a/panel/widgets/indicators.py +++ b/panel/widgets/indicators.py @@ -520,7 +520,7 @@ def _update_value_bounds(self): def _process_param_change(self, msg): msg = super()._process_param_change(msg) vmin, vmax = msg.pop('bounds', self.bounds) - msg['data'] = { + msg['data'] = data = { 'tooltip': { 'formatter': msg.pop('tooltip_format', self.tooltip_format) }, @@ -546,6 +546,13 @@ def _process_param_change(self, msg): } }] } + sm = self.sizing_mode + if 'stretch' in sm: + data['responsive'] = True + if 'width' in msg and ('both' in sm or 'width' in sm): + del msg['width'] + if 'height' in msg and ('both' in sm or 'height' in sm): + del msg['height'] colors = msg.pop('colors', self.colors) if colors: msg['data']['series'][0]['axisLine']['lineStyle']['color'] = colors From 9aeca5dd7c91c188666cdb3556fea198e24c14d4 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 29 Jul 2024 12:31:04 +0200 Subject: [PATCH 08/91] Improve font-size and alignment in Bootstrap Card header (#7037) * Reduce Card title font-size in Bootstrap * Further styling fixes --- panel/theme/css/bootstrap.css | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/panel/theme/css/bootstrap.css b/panel/theme/css/bootstrap.css index 1630fd3dda..6a7bdbac5c 100644 --- a/panel/theme/css/bootstrap.css +++ b/panel/theme/css/bootstrap.css @@ -149,6 +149,38 @@ button.accordion-header { border-radius: 0; } +.card-button { + line-height: 0.9em; +} + +.card-title { + margin-bottom: 5px; +} + +:host(.card-title) h1 { + font-size: 2em; +} + +:host(.card-title) h2 { + font-size: 1.5em; +} + +:host(.card-title) h3 { + font-size: 1.17em; +} + +:host(.card-title) h4 { + font-size: 1em; +} + +:host(.card-title) h5 { + font-size: 0.83em; +} + +:host(.card-title) h6 { + font-size: 0.67em; +} + :host(.card-title) h1, :host(.card-title) h2, :host(.card-title) h3, From a6f1503af341728f859b44c4683546e26223b619 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 29 Jul 2024 12:31:15 +0200 Subject: [PATCH 09/91] Ensure Tabulator sorters are correctly synced (#7036) * Ensure Tabulator sorters are correctly synced * Add test * Make other test more robust --- panel/models/tabulator.ts | 2 +- panel/tests/ui/widgets/test_tabulator.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index 3ca6601bbc..c7680ebff4 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -590,7 +590,7 @@ export class DataTabulatorView extends HTMLBoxView { } if (this.model.pagination !== "remote") { this._updating_sort = true - this.model.sorters = sorts + this.model.sorters = sorts.reverse() this._updating_sort = false } }) diff --git a/panel/tests/ui/widgets/test_tabulator.py b/panel/tests/ui/widgets/test_tabulator.py index 09b5b61c8e..8584d89928 100644 --- a/panel/tests/ui/widgets/test_tabulator.py +++ b/panel/tests/ui/widgets/test_tabulator.py @@ -2399,6 +2399,8 @@ def test_tabulator_sorters_set_after_init(page, df_mixed): serve_component(page, widget) + expect(page.locator('.pnx-tabulator.tabulator')).to_have_count(1) + widget.sorters = [{'field': 'int', 'dir': 'desc'}] sheader = page.locator('[aria-sort="descending"]:visible') @@ -3019,6 +3021,23 @@ def test_tabulator_selection_header_filter_changed(page): expected_selected = df.iloc[selection, :] assert widget.selected_dataframe.equals(expected_selected) +def test_tabulator_sorter_not_reversed_after_init(page): + df = pd.DataFrame({ + 'col1': [1, 2, 3, 4], + 'col2': [1, 4, 3, 2], + }) + + sorters = [ + {'field': 'col1', 'dir': 'desc'}, + {'field': 'col2', 'dir': 'asc'} + ] + table = Tabulator(df, sorters=sorters) + + serve_component(page, table) + + expect(page.locator('.pnx-tabulator.tabulator')).to_have_count(1) + page.wait_for_timeout(300) + assert table.sorters == sorters def test_tabulator_loading_no_horizontal_rescroll(page, df_mixed): widths = 100 From 310309ed624cb1b65fefbfd28b3521a421585304 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 29 Jul 2024 13:30:12 +0200 Subject: [PATCH 10/91] Do not sync StaticText.value with the frontend (#7038) --- panel/tests/widgets/test_input.py | 26 +++++++++++++++++++++++++- panel/widgets/input.py | 20 +++++++++++++++++--- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/panel/tests/widgets/test_input.py b/panel/tests/widgets/test_input.py index a45e486a0d..c36b0d03d3 100644 --- a/panel/tests/widgets/test_input.py +++ b/panel/tests/widgets/test_input.py @@ -228,7 +228,6 @@ def test_literal_input(document, comm): with pytest.raises(ValueError): literal.value = [] - def test_static_text(document, comm): text = StaticText(value='ABC', name='Text:') @@ -244,6 +243,31 @@ def test_static_text(document, comm): text.value = 'Text:: ABC' assert widget.text == 'Text:: ABC' +def test_static_text_no_sync(document, comm): + text = StaticText(value='ABC', name='Text:') + + widget = text.get_root(document, comm=comm) + + widget.text = 'CBA' + assert text.value == 'ABC' + +def test_static_text_empty(document, comm): + + text = StaticText(name='Text:') + + widget = text.get_root(document, comm=comm) + + assert widget.text == 'Text:: ' + +def test_static_text_repr(document, comm): + + text = StaticText(value=StaticText, name='Text:') + + widget = text.get_root(document, comm=comm) + + assert widget.text == 'Text:: <class 'panel.widgets.input.StaticText'>' + + def test_text_input(document, comm): diff --git a/panel/widgets/input.py b/panel/widgets/input.py index bed0f658b0..f8c9f1032e 100644 --- a/panel/widgets/input.py +++ b/panel/widgets/input.py @@ -33,7 +33,9 @@ DatetimePicker as _bkDatetimePicker, TextAreaInput as _bkTextAreaInput, TextInput as _BkTextInput, ) -from ..util import lazy_load, param_reprs, try_datetime64_to_datetime +from ..util import ( + escape, lazy_load, param_reprs, try_datetime64_to_datetime, +) from .base import CompositeWidget, Widget if TYPE_CHECKING: @@ -429,7 +431,7 @@ class StaticText(Widget): """ value = param.Parameter(default=None, doc=""" - The current value""") + The current value to be displayed.""") _format: ClassVar[str] = '{title}: {value}' @@ -445,10 +447,22 @@ class StaticText(Widget): _widget_type: ClassVar[type[Model]] = _BkDiv + @property + def _linked_properties(self) -> tuple[str]: + return () + + def _init_params(self) -> dict[str, Any]: + return { + k: v for k, v in self.param.values().items() + if k in self._synced_params and (v is not None or k == 'value') + } + def _process_param_change(self, msg): msg = super()._process_param_change(msg) if 'text' in msg: - text = str(msg.pop('text')) + text = msg.pop('text') + if not isinstance(text, str): + text = escape("" if text is None else str(text)) partial = self._format.replace('{value}', '').format(title=self.name) if self.name: text = self._format.format(title=self.name, value=text.replace(partial, '')) From cdc3f32bb6fc6f6aec685f6d23fe4a4e753ed694 Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Wed, 31 Jul 2024 10:57:16 -0700 Subject: [PATCH 11/91] Allow none (#7048) --- panel/chat/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panel/chat/message.py b/panel/chat/message.py index 4e9cdeeca3..26313d433b 100644 --- a/panel/chat/message.py +++ b/panel/chat/message.py @@ -179,7 +179,7 @@ class ChatMessage(Pane): header_objects = param.List(doc=""" A list of objects to display in the row of the header of the message.""") - max_width = param.Integer(default=1200, bounds=(0, None)) + max_width = param.Integer(default=1200, bounds=(0, None), allow_None=True) object = param.Parameter(allow_refs=False, doc=""" The message contents. Can be any Python object that panel can display.""") From 8a886893b5c3e555f70e06c02b5397449826de5e Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Wed, 31 Jul 2024 10:57:32 -0700 Subject: [PATCH 12/91] Propagate width to Card header in ChatStep (#7049) * Propagate width * rm width * add back width without stretch --- panel/chat/step.py | 6 +++++- panel/tests/chat/test_step.py | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/panel/chat/step.py b/panel/chat/step.py index 22c9eb021a..a7384f13c2 100644 --- a/panel/chat/step.py +++ b/panel/chat/step.py @@ -127,7 +127,11 @@ def __init__(self, *objects, **params): self._title_pane, stylesheets=self._stylesheets + self.param.stylesheets.rx(), css_classes=["step-header"], - margin=(5, 0) + margin=(5, 0), + width=self.width, + max_width=self.max_width, + min_width=self.min_width, + sizing_mode=self.sizing_mode, ) def __enter__(self): diff --git a/panel/tests/chat/test_step.py b/panel/tests/chat/test_step.py index 43e529905c..017353d85d 100644 --- a/panel/tests/chat/test_step.py +++ b/panel/tests/chat/test_step.py @@ -127,3 +127,13 @@ def test_stream_none(self): step.stream("abc") assert len(step) == 1 assert step[0].object == "abc" + + def test_header_inherits_width(self): + step = ChatStep(width=100) + assert step.header.width == 100 + + @pytest.mark.parametrize("width_key", ["max_width", "min_width"]) + def test_header_inherits_stretch_width(self, width_key): + step = ChatStep(**{width_key: 100}, sizing_mode="stretch_width") + assert getattr(step.header, width_key) == 100 + assert step.header.sizing_mode == "stretch_width" From 94df573f8f0f31c6948efc754b7b3d1ab2ec7ea4 Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Wed, 31 Jul 2024 10:57:42 -0700 Subject: [PATCH 13/91] Additional message params (#7047) --- panel/chat/interface.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/panel/chat/interface.py b/panel/chat/interface.py index 9c8df18e61..3ef7bfc2e3 100644 --- a/panel/chat/interface.py +++ b/panel/chat/interface.py @@ -644,7 +644,9 @@ def send( user: str | None = None, avatar: str | bytes | BytesIO | None = None, respond: bool = True, + **message_params ) -> ChatMessage | None: + """ Sends a value and creates a new message in the chat log. @@ -662,6 +664,8 @@ def send( Will default to the avatar parameter. respond : bool Whether to execute the callback. + message_params : dict + Additional parameters to pass to the ChatMessage. Returns ------- @@ -672,7 +676,7 @@ def send( user = self.user if avatar is None: avatar = self.avatar - return super().send(value, user=user, avatar=avatar, respond=respond) + return super().send(value, user=user, avatar=avatar, respond=respond, **message_params) def stream( self, @@ -706,6 +710,8 @@ def stream( The message to update. replace : bool Whether to replace the existing text when streaming a string or dict. + message_params : dict + Additional parameters to pass to the ChatMessage. Returns ------- From e46145a29e1bff7e95cf3a2e328a079846f240f0 Mon Sep 17 00:00:00 2001 From: Coderambling <159031875+Coderambling@users.noreply.github.com> Date: Thu, 1 Aug 2024 10:48:51 +0200 Subject: [PATCH 14/91] Update conf.py with updated hyperlink (#7046) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aa discussed with @Maximlt, In the below section, the url: https://panel.holoviz.org/about/releases.html#version-1-4-0 Was changed to: https://panel.holoviz.org/about/releases.html See below for comment and explanation: https://github.com/holoviz/panel/pull/6903#pullrequestreview-2108660028 announcement_text = f"Panel {current_release} has just been released! Check out the release notes and support Panel by giving it a 🌟 on Github." --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 2b3f2f4a09..b9e86a0a1c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -45,7 +45,7 @@ current_release = panel.__version__ # Current release version variable -announcement_text = f"Panel {current_release} has just been released! Check out the release notes and support Panel by giving it a 🌟 on Github." +announcement_text = f"Panel {current_release} has just been released! Check out the release notes and support Panel by giving it a 🌟 on Github." html_theme_options = { From 0d1391576ebbe0eeb668d088705e39411c7cfc61 Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Thu, 1 Aug 2024 13:54:51 +0200 Subject: [PATCH 15/91] Update Pyscript (#7016) --- doc/how_to/wasm/convert.md | 8 +++++--- doc/how_to/wasm/standalone.md | 38 +++++++++++++---------------------- panel/io/convert.py | 9 ++++----- panel/io/pyodide.py | 3 ++- 4 files changed, 25 insertions(+), 33 deletions(-) diff --git a/doc/how_to/wasm/convert.md b/doc/how_to/wasm/convert.md index f34beb3cc5..cbe1807cea 100644 --- a/doc/how_to/wasm/convert.md +++ b/doc/how_to/wasm/convert.md @@ -27,6 +27,7 @@ The ``panel convert`` command has the following options: This example will demonstrate how to *convert* and *serve* a basic data app locally. +- install the dependencies `pip install panel scikit-learn xgboost`. - Create a `script.py` file with the following content ```python @@ -88,7 +89,8 @@ Using the `--to` argument on the CLI you can control the format of the file that - **`pyodide`** (default): Run application using Pyodide running in the main thread. This option is less performant than pyodide-worker but produces completely standalone HTML files that do not have to be hosted on a static file server (e.g. Github Pages). - **`pyodide-worker`**: Generates an HTML file and a JS file containing a Web Worker that runs in a separate thread. This is the most performant option, but files have to be hosted on a static file server. -- **`pyscript`**: Generates an HTML leveraging PyScript. This produces standalone HTML files containing `` and `` tags containing the dependencies and the application code. This output is the most readable, and should have equivalent performance to the `pyodide` option. +- **`pyscript`**: Generates an HTML leveraging PyScript. This produces standalone HTML files containing ` @@ -44,7 +46,7 @@ To get started with Pyodide simply follow their [Getting started guide](https:// return f'Amplitude is: {new}' pn.Row(slider, pn.bind(callback, slider)).servable(target='simple_app'); - `); + `); } main(); @@ -64,44 +66,30 @@ const bk_whl = "https://cdn.holoviz.org/panel/{{PANEL_VERSION}}/dist/wheels/boke const pn_whl = "https://cdn.holoviz.org/panel/{{PANEL_VERSION}}/dist/wheels/panel-{{PANEL_VERSION}}-py3-none-any.whl" await micropip.install(bk_whl, pn_whl) ``` + ::: ### PyScript -PyScript makes it even easier to manage your dependencies, with a `` HTML tag. Simply include `panel` in the list of dependencies and PyScript will install it automatically: - -```html - -packages = [ - "panel", - ... -] - -``` - -Once installed you will be able to `import panel` in your `` tag. Again, make sure you also load Bokeh.js and Panel.js: +A basic, single file pyscript example looks like ```html + + - - + + - - packages = [ - "https://cdn.holoviz.org/panel/{{PANEL_VERSION}}/dist/wheels/bokeh-{{BOKEH_VERSION}}-py3-none-any.whl", - "https://cdn.holoviz.org/panel/{{PANEL_VERSION}}/dist/wheels/panel-{{PANEL_VERSION}}-py3-none-any.whl" - ] -
- + ``` -The app should look identical to the one above but show a loading spinner while Pyodide is initializing. +The app should look identical to the one above. + +The [PyScript](https://docs.pyscript.net) documentation recommends you put your configuration and python code into separate files. You can find such examples in the [PyScript Examples Gallery](https://pyscript.com/@examples?q=panel). ## Rendering Panel components in Pyodide or Pyscript diff --git a/panel/io/convert.py b/panel/io/convert.py index c5f1480e8d..aac95542d6 100644 --- a/panel/io/convert.py +++ b/panel/io/convert.py @@ -41,8 +41,8 @@ PANEL_ROOT = pathlib.Path(__file__).parent.parent BOKEH_VERSION = base_version(bokeh.__version__) PY_VERSION = base_version(__version__) -PYODIDE_VERSION = 'v0.25.0' -PYSCRIPT_VERSION = '2024.2.1' +PYODIDE_VERSION = 'v0.26.2' +PYSCRIPT_VERSION = '2024.8.1' WHL_PATH = DIST_DIR / 'wheels' PANEL_LOCAL_WHL = WHL_PATH / f'panel-{__version__.replace("-dirty", "")}-py3-none-any.whl' BOKEH_LOCAL_WHL = WHL_PATH / f'bokeh-{BOKEH_VERSION}-py3-none-any.whl' @@ -272,7 +272,6 @@ def script_to_html( reqs = base_reqs + [ req for req in requirements if req not in ('panel', 'bokeh') ] - print(reqs) for name, min_version in MINIMUM_VERSIONS.items(): if any(name in req for req in reqs): reqs = [f'{name}>={min_version}' if name in req else req for req in reqs] @@ -290,12 +289,12 @@ def script_to_html( css_resources = [PYSCRIPT_CSS, PYSCRIPT_CSS_OVERRIDES] elif not css_resources: css_resources = [] - pyconfig = json.dumps({'packages': reqs, 'plugins': ["!error"]}) + pyconfig = json.dumps({'packages': reqs}) if 'worker' in runtime: plot_script = f'' web_worker = code else: - plot_script = f'{code}' + plot_script = f'' else: if css_resources == 'auto': css_resources = [] diff --git a/panel/io/pyodide.py b/panel/io/pyodide.py index 58dab86054..4cbbd4c6ca 100644 --- a/panel/io/pyodide.py +++ b/panel/io/pyodide.py @@ -343,7 +343,8 @@ def pysync(event): return json_patch, buffer_map = _process_document_events(doc, [event]) json_patch = pyodide.ffi.to_js(json_patch, dict_converter=_dict_converter) - dispatch_fn(json_patch, pyodide.ffi.to_js(buffer_map), msg_id) + buffers = js.Map.new(pyodide.ffi.to_js(buffer_map)) + dispatch_fn(json_patch, buffers, msg_id) doc.on_change(pysync) doc.unhold() From b04669a74dc60e1fa8c518a655b86405b24e06e9 Mon Sep 17 00:00:00 2001 From: Nicky Sandhu Date: Thu, 1 Aug 2024 13:28:58 -0700 Subject: [PATCH 16/91] selection indices seem to be on _processed and not current view (#6685) * selection indices seem to be on _processed and not current view * Fix test --------- Co-authored-by: Philipp Rudiger --- panel/tests/ui/widgets/test_tabulator.py | 1 - panel/widgets/tables.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/panel/tests/ui/widgets/test_tabulator.py b/panel/tests/ui/widgets/test_tabulator.py index 8584d89928..1e2070c4d4 100644 --- a/panel/tests/ui/widgets/test_tabulator.py +++ b/panel/tests/ui/widgets/test_tabulator.py @@ -2952,7 +2952,6 @@ def test_tabulator_click_event_selection_integrations(page, sorter, python_filte assert widget.selected_dataframe.equals(expected_selected) -@pytest.mark.xfail(reason='See https://github.com/holoviz/panel/issues/3664') def test_tabulator_selection_sorters_on_init(page, df_mixed): widget = Tabulator(df_mixed, sorters=[{'field': 'int', 'dir': 'desc'}]) diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index dca0422c00..78749f4c5d 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -873,7 +873,7 @@ def selected_dataframe(self): """ if not self.selection: return self.current_view.iloc[:0] - return self.current_view.iloc[self.selection] + return self._processed.iloc[self.selection] class DataFrame(BaseTable): From e2afe414918df95a160a95bef9c7ae4eb84ff837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Thu, 1 Aug 2024 22:42:26 +0200 Subject: [PATCH 17/91] Update warnings to raise errors in test (#6889) --- panel/io/convert.py | 2 +- panel/io/ipywidget.py | 14 +++++++++---- panel/io/state.py | 2 +- panel/pane/vtk/vtk.py | 5 ++++- panel/reactive.py | 26 ++++++++---------------- panel/tests/chat/test_message.py | 2 +- panel/tests/conftest.py | 5 +---- panel/tests/io/test_reload.py | 5 +++-- panel/tests/pane/test_vtk.py | 2 +- panel/tests/test_models.py | 3 ++- panel/tests/test_server.py | 4 ++-- panel/tests/ui/io/test_convert.py | 12 ++++++----- panel/tests/ui/io/test_jupyterlite.py | 8 ++++++-- panel/tests/ui/widgets/test_tabulator.py | 6 +++--- panel/tests/util.py | 3 +-- panel/tests/widgets/test_tables.py | 22 ++++++++++++-------- panel/util/__init__.py | 4 ++-- panel/widgets/tables.py | 12 ++++++++--- pyproject.toml | 6 ++++++ 19 files changed, 81 insertions(+), 62 deletions(-) diff --git a/panel/io/convert.py b/panel/io/convert.py index aac95542d6..18732bc0cd 100644 --- a/panel/io/convert.py +++ b/panel/io/convert.py @@ -299,7 +299,7 @@ def script_to_html( if css_resources == 'auto': css_resources = [] env_spec = ', '.join([repr(req) for req in reqs]) - code = code.encode("unicode_escape").decode("utf-8").replace('`', '\`') + code = code.encode("unicode_escape").decode("utf-8").replace('`', r'\`') if runtime == 'pyodide-worker': if js_resources == 'auto': js_resources = [] diff --git a/panel/io/ipywidget.py b/panel/io/ipywidget.py index 715ae09400..24be606f25 100644 --- a/panel/io/ipywidget.py +++ b/panel/io/ipywidget.py @@ -10,7 +10,7 @@ from bokeh.document.events import MessageSentEvent from bokeh.document.json import Literal, MessageSent, TypedDict from bokeh.util.serialization import make_id -from ipykernel.comm import Comm, CommManager +from ipykernel.comm import CommManager from ipykernel.kernelbase import Kernel from ipywidgets import Widget from ipywidgets._version import __protocol_version__ @@ -30,6 +30,11 @@ from ..util import classproperty from .state import set_curdoc, state +try: + from ipykernel.comm.comm import BaseComm as _IPyComm +except Exception: + from ipykernel.comm.comm import Comm as _IPyComm + try: # Support for ipywidgets>=8.0.5 import comm @@ -81,12 +86,12 @@ def _on_widget_constructed(widget, doc=None): 'metadata': { 'version': __protocol_version__ }, - 'kernel': kernel } if widget._model_id is not None: args['comm_id'] = widget._model_id try: - widget.comm = Comm(**args) + widget.comm = _IPyComm(**args) + widget.comm.kernel = kernel except Exception as e: if 'PANEL_IPYWIDGET' not in os.environ: raise e @@ -132,8 +137,9 @@ def generate(self, references, buffers): class PanelSessionWebsocket(SessionWebsocket): def __init__(self, *args, **kwargs): - session.Session.__init__(self, *args, **kwargs) + self.parent = kwargs.pop('parent', None) self._document = kwargs.pop('document', None) + session.Session.__init__(self, **kwargs) self._queue = [] self._document.on_message("ipywidgets_bokeh", self.receive) diff --git a/panel/io/state.py b/panel/io/state.py index 680e9f6c2c..5869e3c0f1 100644 --- a/panel/io/state.py +++ b/panel/io/state.py @@ -868,7 +868,7 @@ def dgen(): yield new.timestamp() elif callable(at): while True: - new = at(dt.datetime.utcnow()) + new = at(dt.datetime.now(dt.timezone.utc).replace(tzinfo=None)) if new is None: raise StopIteration yield new.replace(tzinfo=dt.timezone.utc).astimezone().timestamp() diff --git a/panel/pane/vtk/vtk.py b/panel/pane/vtk/vtk.py index 1df5ed8081..99b22ec452 100644 --- a/panel/pane/vtk/vtk.py +++ b/panel/pane/vtk/vtk.py @@ -782,7 +782,10 @@ def _subsample_array(self, array): if any([d_f > 1 for d_f in dowsnscale_factor]): try: import scipy.ndimage as nd - sub_array = nd.interpolation.zoom(array, zoom=[1 / d_f for d_f in dowsnscale_factor], order=0, mode="nearest") + if hasattr(nd, "zoom"): + sub_array = nd.zoom(array, zoom=[1 / d_f for d_f in dowsnscale_factor], order=0, mode="nearest") + else: # Slated for removal in 2.0 + sub_array = nd.interpolation.zoom(array, zoom=[1 / d_f for d_f in dowsnscale_factor], order=0, mode="nearest") except ImportError: sub_array = array[::int(np.ceil(dowsnscale_factor[0])), ::int(np.ceil(dowsnscale_factor[1])), diff --git a/panel/reactive.py b/panel/reactive.py index e61c506cff..626c2e5835 100644 --- a/panel/reactive.py +++ b/panel/reactive.py @@ -1275,9 +1275,6 @@ class ReactiveData(SyncableData): __abstract = True - def __init__(self, **params): - super().__init__(**params) - def _update_selection(self, indices: list[int]) -> None: self.selection = indices @@ -1289,21 +1286,14 @@ def _convert_column( if dtype.kind == 'M': if values.dtype.kind in 'if': if getattr(dtype, 'tz', None): - # dtype has a timezone - if dtype.tz == dt.timezone.utc: - # Milliseconds to nanoseconds, to datetime64. - converted = (values * 1e6).astype('datetime64[ns]') - else: - import pandas as pd - - # Using pandas to convert from milliseconds - # timezone-aware, to UTC nanoseconds, to datetime64. - converted = ( - pd.Series(pd.to_datetime(values, unit="ms")) - .dt.tz_localize(dtype.tz) - .dt.tz_convert('utc') - .dt.tz_localize(None) - ) + import pandas as pd + + # Using pandas to convert from milliseconds + # timezone-aware, to UTC nanoseconds, to datetime64. + converted = ( + pd.Series(pd.to_datetime(values, unit="ms")) + .dt.tz_localize(dtype.tz) + ) else: # Timestamps converted from milliseconds to nanoseconds, # to datetime. diff --git a/panel/tests/chat/test_message.py b/panel/tests/chat/test_message.py index 1a0bbe41b0..d0d1e9b44e 100644 --- a/panel/tests/chat/test_message.py +++ b/panel/tests/chat/test_message.py @@ -179,7 +179,7 @@ def test_update_timestamp(self): columns = message._composite.objects timestamp_pane = columns[1][2][0] assert isinstance(timestamp_pane, HTML) - dt_str = datetime.datetime.utcnow().strftime("%H:%M") + dt_str = datetime.datetime.now(datetime.timezone.utc).strftime("%H:%M") assert timestamp_pane.object == dt_str message = ChatMessage(timestamp_tz="US/Pacific") diff --git a/panel/tests/conftest.py b/panel/tests/conftest.py index 8126d69d70..c839dac02d 100644 --- a/panel/tests/conftest.py +++ b/panel/tests/conftest.py @@ -12,7 +12,6 @@ import tempfile import time import unittest -import warnings from contextlib import contextmanager from subprocess import PIPE, Popen @@ -43,9 +42,7 @@ JUPYTER_PROCESS = None try: - with warnings.catch_warnings(): - warnings.filterwarnings("error", category=DeprecationWarning) - asyncio.get_event_loop() + asyncio.get_event_loop() except (RuntimeError, DeprecationWarning): asyncio.set_event_loop(asyncio.new_event_loop()) diff --git a/panel/tests/io/test_reload.py b/panel/tests/io/test_reload.py index b9638e92cd..902b72d894 100644 --- a/panel/tests/io/test_reload.py +++ b/panel/tests/io/test_reload.py @@ -12,9 +12,10 @@ def test_record_modules_not_stdlib(): + old_modules = _modules.copy() with record_modules(): - import audioop # noqa - assert ((_modules == set()) or (_modules == set(['audioop']))) + import dis # noqa + assert _modules == old_modules _modules.clear() def test_check_file(): diff --git a/panel/tests/pane/test_vtk.py b/panel/tests/pane/test_vtk.py index 188f22123b..13c5b6a8b2 100644 --- a/panel/tests/pane/test_vtk.py +++ b/panel/tests/pane/test_vtk.py @@ -407,7 +407,7 @@ def test_vtkvol_serialization_coherence(document, comm): vd_f = p_f._get_volume_data() vd_id = p_id._get_volume_data() data_decoded = np.frombuffer(base64.b64decode(vd_c["buffer"]), dtype=vd_c["dtype"]).reshape(vd_c["dims"], order="F") - assert (data_decoded==data_matrix).all() + assert np.all(data_decoded==data_matrix) assert vd_id == vd_c == vd_f p_c_ds = VTKVolume(data_matrix_c, origin=origin, spacing=spacing, max_data_size=0.1) diff --git a/panel/tests/test_models.py b/panel/tests/test_models.py index 252345fc14..c6af59eea4 100644 --- a/panel/tests/test_models.py +++ b/panel/tests/test_models.py @@ -7,4 +7,5 @@ def test_models_encoding(): model_dir = os.path.join(panel.__path__[0], 'models') for file in os.listdir(model_dir): if file.endswith('.ts'): - open(os.path.join(model_dir, file), 'r').read() + with open(os.path.join(model_dir, file), 'r') as f: + f.read() diff --git a/panel/tests/test_server.py b/panel/tests/test_server.py index c65629ff20..4099109786 100644 --- a/panel/tests/test_server.py +++ b/panel/tests/test_server.py @@ -382,8 +382,8 @@ def periodic_cb(): state.cache['at'].append(dt.datetime.now()) scheduled = [ - dt.datetime.utcnow() + dt.timedelta(seconds=1.57), - dt.datetime.utcnow() + dt.timedelta(seconds=1.86) + dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=1.57), + dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=1.86) ] siter = iter(scheduled) diff --git a/panel/tests/ui/io/test_convert.py b/panel/tests/ui/io/test_convert.py index 09546b5d60..41df409ed5 100644 --- a/panel/tests/ui/io/test_convert.py +++ b/panel/tests/ui/io/test_convert.py @@ -118,7 +118,8 @@ def http_serve(): temp_dir = tempfile.TemporaryDirectory() temp_path = pathlib.Path(temp_dir.name) - (temp_path / 'test.html').write_text('Test') + test_file = (temp_path / 'test.html') + test_file.write_text('Test') try: shutil.copy(PANEL_LOCAL_WHL, temp_path / PANEL_LOCAL_WHL.name) @@ -131,7 +132,6 @@ def http_serve(): httpd, _ = http_serve_directory(str(temp_path), port=HTTP_PORT) - time.sleep(1) def write(app): @@ -141,9 +141,11 @@ def write(app): f.write(app) return app_path - yield write - - httpd.shutdown() + try: + yield write + finally: + httpd.shutdown() + temp_dir.cleanup() def wait_for_app(http_serve, app, page, runtime, wait=True, **kwargs): diff --git a/panel/tests/ui/io/test_jupyterlite.py b/panel/tests/ui/io/test_jupyterlite.py index 4ccd789a8e..9dd269d780 100644 --- a/panel/tests/ui/io/test_jupyterlite.py +++ b/panel/tests/ui/io/test_jupyterlite.py @@ -1,3 +1,4 @@ +import sys import time from http.client import HTTPConnection @@ -15,7 +16,7 @@ @pytest.fixture() def launch_jupyterlite(): process = Popen( - ["python", "-m", "http.server", "8123", "--directory", 'lite/dist/'], stdout=PIPE + [sys.executable, "-m", "http.server", "8123", "--directory", 'lite/dist/'], stdout=PIPE ) retries = 5 while retries > 0: @@ -24,12 +25,15 @@ def launch_jupyterlite(): conn.request("HEAD", 'index.html') response = conn.getresponse() if response is not None: + conn.close() break except ConnectionRefusedError: time.sleep(1) retries -= 1 if not retries: + process.terminate() + process.wait() raise RuntimeError("Failed to start http server") try: yield @@ -38,7 +42,7 @@ def launch_jupyterlite(): process.wait() - +@pytest.mark.filterwarnings("ignore::ResourceWarning") def test_jupyterlite_execution(launch_jupyterlite, page): # INFO: Needs TS changes uploaded to CDN. Relevant when # testing a new version of Bokeh. diff --git a/panel/tests/ui/widgets/test_tabulator.py b/panel/tests/ui/widgets/test_tabulator.py index 1e2070c4d4..52804ef57d 100644 --- a/panel/tests/ui/widgets/test_tabulator.py +++ b/panel/tests/ui/widgets/test_tabulator.py @@ -2264,7 +2264,7 @@ def test_tabulator_styling_init(page, df_mixed): df_styled = ( df_mixed.style .apply(highlight_max, subset=['int']) - .applymap(color_false, subset=['bool']) + .map(color_false, subset=['bool']) ) widget = Tabulator(df_styled) @@ -2404,7 +2404,7 @@ def test_tabulator_sorters_set_after_init(page, df_mixed): widget.sorters = [{'field': 'int', 'dir': 'desc'}] sheader = page.locator('[aria-sort="descending"]:visible') - expect(sheader).to_have_count(1) + wait_until(lambda: expect(sheader).to_have_count(1), page) assert sheader.get_attribute('tabulator-field') == 'int' expected_df_sorted = df_mixed.sort_values('int', ascending=False) @@ -2875,7 +2875,7 @@ def test_tabulator_edit_event_integrations(page, sorter, python_filter, header_f expected_current_view = expected_current_view.query(f'{python_filter_col} == @python_filter_val') if header_filter == 'header_filter': expected_current_view = expected_current_view.query(f'{header_filter_col} == @header_filter_val') - assert widget.current_view.equals(expected_current_view) + pd.testing.assert_frame_equal(widget.current_view, expected_current_view) @pytest.mark.parametrize('sorter', ['sorter', 'no_sorter']) diff --git a/panel/tests/util.py b/panel/tests/util.py index 981943633e..66f7938258 100644 --- a/panel/tests/util.py +++ b/panel/tests/util.py @@ -442,8 +442,7 @@ def serve_forever(httpd): with httpd: httpd.serve_forever() - thread = Thread(target=serve_forever, args=(httpd, )) - thread.setDaemon(True) + thread = Thread(target=serve_forever, args=(httpd, ), daemon=True) thread.start() return httpd, address diff --git a/panel/tests/widgets/test_tables.py b/panel/tests/widgets/test_tables.py index a3d904d64f..59aa871a2e 100644 --- a/panel/tests/widgets/test_tables.py +++ b/panel/tests/widgets/test_tables.py @@ -791,7 +791,7 @@ def test_tabulator_styling(document, comm): def high_red(value): return 'color: red' if value > 2 else 'color: black' - table.style.applymap(high_red, subset=['A']) + table.style.map(high_red, subset=['A']) model = table.get_root(document, comm) @@ -819,25 +819,29 @@ def test_tabulator_empty_table(document, comm): def test_tabulator_sorters_unnamed_index(document, comm): df = pd.DataFrame(np.random.rand(10, 4)) + assert df.columns.dtype == np.int64 table = Tabulator(df) table.sorters = [{'field': 'index', 'sorter': 'number', 'dir': 'desc'}] + res = table.current_view + exp = df.sort_index(ascending=False) + exp.columns = exp.columns.astype(object) - pd.testing.assert_frame_equal( - table.current_view, - df.sort_index(ascending=False) - ) + pd.testing.assert_frame_equal(res, exp) + assert df.columns.dtype == np.int64 def test_tabulator_sorters_int_name_column(document, comm): df = pd.DataFrame(np.random.rand(10, 4)) + assert df.columns.dtype == np.int64 table = Tabulator(df) table.sorters = [{'field': '0', 'dir': 'desc'}] + res = table.current_view + exp = df.sort_values([0], ascending=False) + exp.columns = exp.columns.astype(object) - pd.testing.assert_frame_equal( - table.current_view, - df.sort_values([0], ascending=False) - ) + pd.testing.assert_frame_equal(res, exp) + assert df.columns.dtype == np.int64 def test_tabulator_stream_series(document, comm): diff --git a/panel/util/__init__.py b/panel/util/__init__.py index 3692ff2545..80cff9e211 100644 --- a/panel/util/__init__.py +++ b/panel/util/__init__.py @@ -180,13 +180,13 @@ def value_as_datetime(value): Retrieve the value tuple as a tuple of datetime objects. """ if isinstance(value, numbers.Number): - value = datetime.utcfromtimestamp(value / 1000) + value = datetime.fromtimestamp(value / 1000, tz=dt.timezone.utc).replace(tzinfo=None) return value def value_as_date(value): if isinstance(value, numbers.Number): - value = datetime.utcfromtimestamp(value / 1000).date() + value = datetime.fromtimestamp(value / 1000, tz=dt.timezone.utc).replace(tzinfo=None).date() elif isinstance(value, datetime): value = value.date() return value diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index 78749f4c5d..abd22bae55 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -369,6 +369,9 @@ def _sort_df(self, df: pd.DataFrame) -> pd.DataFrame: fields = [self._renamed_cols.get(s['field'], s['field']) for s in self.sorters] ascending = [s['dir'] == 'asc' for s in self.sorters] + # Making a copy of the DataFrame because it could be a view of the original + # dataframe. There could be a better place to do this. + df = df.copy() # Temporarily add _index_ column because Tabulator uses internal _index # as additional sorter to break ties df['_index_'] = np.arange(len(df)).astype(str) @@ -398,9 +401,7 @@ def tabulator_sorter(col): # Revert temporary changes to DataFrames if rename: - df.index.name = None df_sorted.index.name = None - df.drop(columns=['_index_'], inplace=True) df_sorted.drop(columns=['_index_'], inplace=True) return df_sorted @@ -712,7 +713,12 @@ def stream(self, stream_value, rollover=None, reset_index=True): if reset_index: stream_value = stream_value.reset_index(drop=True) stream_value.index += value_index_start - combined = pd.concat([self.value, stream_value]) + if self.value.empty: + combined = pd.DataFrame( + stream_value, columns=self.value.columns + ).astype(self.value.dtypes) + else: + combined = pd.concat([self.value, stream_value]) if rollover is not None: combined = combined.iloc[-rollover:] with param.discard_events(self): diff --git a/pyproject.toml b/pyproject.toml index b2f3f050cd..198bf321d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -197,7 +197,13 @@ xfail_strict = true minversion = "7" log_cli_level = "INFO" filterwarnings = [ + "error", # 2023-11: `pkg_resources` is deprecated "ignore:Deprecated call to `pkg_resources.+?'zope:DeprecationWarning", # https://github.com/zopefoundation/meta/issues/194 "ignore: pkg_resources is deprecated as an API:DeprecationWarning:streamz.plugins", # https://github.com/python-streamz/streamz/issues/460 + # 2024-06: Adding error to the filterwarnings + "ignore:Jupyter is migrating its paths to use standard platformdirs:DeprecationWarning", # OK + "ignore:distutils Version classes are deprecated:DeprecationWarning:ipywidgets_bokeh.kernel", # OK + "ignore:unclosed file <_io.TextIOWrapper name='(/dev/null|nul)' mode='w':ResourceWarning", # OK + "ignore:Deprecated in traitlets 4.1, use the instance .metadata dictionary directly", # OK (ipywidgets internal) ] From 7c90f7b54d9a40049253c74dca46630d1bb3cb38 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 1 Aug 2024 23:10:00 +0200 Subject: [PATCH 18/91] Add support for automatically determining optimal Tabulator page_size (#6978) * Add support for automatically determining optimal Tabulator page_size * Improvements, tests and docs * Fix lint * Apply suggestions from code review --- examples/reference/widgets/Tabulator.ipynb | 5 ++- panel/models/tabulator.py | 2 +- panel/models/tabulator.ts | 31 +++++++++++++-- panel/tests/ui/widgets/test_tabulator.py | 23 +++++++++++ panel/widgets/tables.py | 45 ++++++++++++---------- 5 files changed, 81 insertions(+), 25 deletions(-) diff --git a/examples/reference/widgets/Tabulator.ipynb b/examples/reference/widgets/Tabulator.ipynb index c061c224ef..403ba414fe 100644 --- a/examples/reference/widgets/Tabulator.ipynb +++ b/examples/reference/widgets/Tabulator.ipynb @@ -49,9 +49,10 @@ "* **`header_filters`** (`boolean`/`dict`): A boolean enabling filters in the column headers or a dictionary providing filter definitions for specific columns.\n", "* **`hidden_columns`** (`list`): List of columns to hide.\n", "* **`hierarchical`** (boolean, default=False): Whether to render multi-indexes as hierarchical index (note hierarchical must be enabled during instantiation and cannot be modified later)\n", + "* **`initial_page_size`** (`int`, `default=20`): If pagination is enabled and `page_size` this determines the initial size of each page before rendering.\n", "* **`layout`** (`str`, `default='fit_data_table'`): Describes the column layout mode with one of the following options `'fit_columns'`, `'fit_data'`, `'fit_data_stretch'`, `'fit_data_fill'`, `'fit_data_table'`. \n", "* **`page`** (`int`, `default=1`): Current page, if pagination is enabled.\n", - "* **`page_size`** (`int`, `default=20`): Number of rows on each page, if pagination is enabled.\n", + "* **`page_size`** (`int | None`, `default=None`): Number of rows on each page, if pagination is enabled. By default the number of rows is automatically determined based on the number of rows that fit on screen. If None the initial amount of data is determined by the `initial_page_size`. \n", "* **`pagination`** (`str`, `default=None`): Set to `'local` or `'remote'` to enable pagination; by default pagination is disabled with the value set to `None`.\n", "* **`row_content`** (`callable`): A function that receives the expanded row (`pandas.Series`) as input and should return a Panel object to render into the expanded region below the row.\n", "* **`selection`** (`list`): The currently selected rows as a list of integer indexes.\n", @@ -822,6 +823,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "Note that the default `page_size` is None, which means it will measure the height of the rows and try to fit the appropriate number of rows into the available space. To override the number of rows sent to the frontend before the measurement has taken place set the `initial_page_size`.\n", + "\n", "Contrary to the `'remote'` option, `'local'` pagination transfers all of the data but still allows to display it on multiple pages:" ] }, diff --git a/panel/models/tabulator.py b/panel/models/tabulator.py index 586c42895e..22eb3a7f7f 100644 --- a/panel/models/tabulator.py +++ b/panel/models/tabulator.py @@ -155,7 +155,7 @@ class DataTabulator(HTMLBox): page = Nullable(Int) - page_size = Int() + page_size = Nullable(Int) max_page = Int() diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index c7680ebff4..e996d231fe 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -603,6 +603,31 @@ export class DataTabulatorView extends HTMLBoxView { this.setStyles() if (this.model.pagination) { + if (this.model.page_size == null) { + const table = this.shadow_el.querySelector(".tabulator-table") + const holder = this.shadow_el.querySelector(".tabulator-tableholder") + if (table != null && holder != null) { + const table_height = holder.clientHeight + let height = 0 + let page_size = null + const heights = [] + for (let i = 0; i table_height) { + page_size = i + break + } + } + if (height < table_height) { + page_size = table.children.length + const remaining = table_height - height + page_size += Math.floor(remaining / Math.min(...heights)) + } + this.model.page_size = page_size + } + } this.setMaxPage() this.tabulator.setPage(this.model.page) } @@ -665,7 +690,7 @@ export class DataTabulatorView extends HTMLBoxView { layout: this.getLayout(), pagination: this.model.pagination != null, paginationMode: this.model.pagination, - paginationSize: this.model.page_size, + paginationSize: this.model.page_size || 20, paginationInitialPage: 1, groupBy: this.groupBy, rowFormatter: (row: any) => this._render_row(row), @@ -1351,7 +1376,7 @@ export namespace DataTabulator { layout: p.Property max_page: p.Property page: p.Property - page_size: p.Property + page_size: p.Property pagination: p.Property select_mode: p.Property selectable_rows: p.Property @@ -1397,7 +1422,7 @@ export class DataTabulator extends HTMLBox { max_page: [ Float, 0 ], pagination: [ Nullable(Str), null ], page: [ Float, 0 ], - page_size: [ Float, 0 ], + page_size: [ Nullable(Float), null ], select_mode: [ Any, true ], selectable_rows: [ Nullable(List(Float)), null ], source: [ Ref(ColumnDataSource) ], diff --git a/panel/tests/ui/widgets/test_tabulator.py b/panel/tests/ui/widgets/test_tabulator.py index 52804ef57d..664a7b8edc 100644 --- a/panel/tests/ui/widgets/test_tabulator.py +++ b/panel/tests/ui/widgets/test_tabulator.py @@ -3388,6 +3388,29 @@ def test_tabulator_update_hidden_columns(page): ), page) +def test_tabulator_remote_pagination_auto_page_size_grow(page, df_mixed): + nrows, ncols = df_mixed.shape + widget = Tabulator(df_mixed, pagination='remote', initial_page_size=1, height=200) + + serve_component(page, widget) + + expect(page.locator('.tabulator-table')).to_have_count(1) + + wait_until(lambda: widget.page_size == 4, page) + + +def test_tabulator_remote_pagination_auto_page_size_shrink(page, df_mixed): + nrows, ncols = df_mixed.shape + widget = Tabulator(df_mixed, pagination='remote', initial_page_size=10, height=150) + + serve_component(page, widget) + + expect(page.locator('.tabulator-table')).to_have_count(1) + + wait_until(lambda: widget.page_size == 3, page) + + + class Test_RemotePagination: @pytest.fixture(autouse=True) diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index abd22bae55..8e1a384d79 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -1090,13 +1090,16 @@ class Tabulator(BaseTable): 'fit_data', 'fit_data_fill', 'fit_data_stretch', 'fit_data_table', 'fit_columns']) + initial_page_size = param.Integer(default=20, bounds=(1, None), doc=""" + Initial page size if page_size is None and therefore automatically set.""") + pagination = param.ObjectSelector(default=None, allow_None=True, objects=['local', 'remote']) page = param.Integer(default=1, doc=""" Currently selected page (indexed starting at 1), if pagination is enabled.""") - page_size = param.Integer(default=20, bounds=(1, None), doc=""" + page_size = param.Integer(default=None, bounds=(1, None), doc=""" Number of rows to render per page, if pagination is enabled.""") row_content = param.Callable(doc=""" @@ -1168,7 +1171,7 @@ class Tabulator(BaseTable): 'selection': None, 'row_content': None, 'row_height': None, 'text_align': None, 'embed_content': None, 'header_align': None, 'header_filters': None, 'styles': 'cell_styles', - 'title_formatters': None, 'sortable': None + 'title_formatters': None, 'sortable': None, 'initial_page_size': None } # Determines the maximum size limits beyond which (local, remote) @@ -1261,7 +1264,7 @@ def _process_event(self, event) -> None: event_col = self._renamed_cols.get(event.column, event.column) if self.pagination == 'remote': - nrows = self.page_size + nrows = self.page_size or self.initial_page_size event.row = event.row+(self.page-1)*nrows idx = self._index_mapping.get(event.row, event.row) @@ -1349,7 +1352,7 @@ def _get_data(self): import pandas as pd df = self._filter_dataframe(self.value) df = self._sort_df(df) - nrows = self.page_size + nrows = self.page_size or self.initial_page_size start = (self.page-1)*nrows page_df = df.iloc[start: start+nrows] @@ -1389,8 +1392,9 @@ def _get_style_data(self, recompute=True): return {} offset = 1 + len(self.indexes) + int(self.selectable in ('checkbox', 'checkbox-single')) + int(bool(self.row_content)) if self.pagination == 'remote': - start = (self.page-1)*self.page_size - end = start + self.page_size + page_size = self.page_size or self.initial_page_size + start = (self.page - 1) * page_size + end = start + page_size # Map column indexes in the data to indexes after frozen_columns are applied column_mapper = {} @@ -1434,7 +1438,7 @@ def _get_selectable(self): return None df = self._processed if self.pagination == 'remote': - nrows = self.page_size + nrows = self.page_size or self.initial_page_size start = (self.page-1)*nrows df = df.iloc[start:(start+nrows)] return self.selectable_rows(df) @@ -1451,7 +1455,7 @@ def _get_children(self, old={}): from ..pane import panel df = self._processed if self.pagination == 'remote': - nrows = self.page_size + nrows = self.page_size or self.initial_page_size start = (self.page-1)*nrows df = df.iloc[start:(start+nrows)] children = {} @@ -1524,7 +1528,7 @@ def _update_children(self, *events): def _stream(self, stream, rollover=None, follow=True): if self.pagination == 'remote': length = self._length - nrows = self.page_size + nrows = self.page_size or self.initial_page_size max_page = max(length//nrows + bool(length%nrows), 1) if self.page != max_page: return @@ -1538,7 +1542,7 @@ def stream(self, stream_value, rollover=None, reset_index=True, follow=True): self._apply_update([], {'follow': follow}, model, ref) if follow and self.pagination: length = self._length - nrows = self.page_size + nrows = self.page_size or self.initial_page_size self.page = max(length//nrows + bool(length%nrows), 1) super().stream(stream_value, rollover, reset_index) if follow and self.pagination: @@ -1551,8 +1555,8 @@ def _patch(self, patch): self._update_cds() return if self.pagination == 'remote': - nrows = self.page_size - start = (self.page-1)*nrows + nrows = self.page_size or self.initial_page_size + start = (self.page - 1) * nrows end = start+nrows filtered = {} for c, values in patch.items(): @@ -1597,7 +1601,7 @@ def _update_selectable(self): def _update_max_page(self): length = self._length - nrows = self.page_size + nrows = self.page_size or self.initial_page_size max_page = max(length//nrows + bool(length%nrows), 1) self.param.page.bounds = (1, max_page) for ref, (model, _) in self._models.items(): @@ -1621,8 +1625,8 @@ def _update_selected(self, *events: param.parameterized.Event, indices=None): indices.append((ind, iloc)) except KeyError: continue - nrows = self.page_size - start = (self.page-1)*nrows + nrows = self.page_size or self.initial_page_size + start = (self.page - 1) * nrows end = start+nrows p_range = self._processed.index[start:end] kwargs['indices'] = [iloc - start for ind, iloc in indices @@ -1638,8 +1642,8 @@ def _update_column(self, column: str, array: np.ndarray): with pd.option_context('mode.chained_assignment', None): self._processed[column] = array return - nrows = self.page_size - start = (self.page-1)*nrows + nrows = self.page_size or self.initial_page_size + start = (self.page - 1) * nrows end = start+nrows index = self._processed.iloc[start:end].index.values self.value.loc[index, column] = array @@ -1659,7 +1663,7 @@ def _update_selection(self, indices: list[int] | SelectionEvent): ilocs = [] if indices.flush else self.selection.copy() indices = indices.indices - nrows = self.page_size + nrows = self.page_size or self.initial_page_size start = (self.page-1)*nrows index = self._processed.iloc[[start+ind for ind in indices]].index for v in index.values: @@ -1684,7 +1688,8 @@ def _get_properties(self, doc: Document) -> dict[str, Any]: properties['indexes'] = self.indexes if self.pagination: length = self._length - properties['max_page'] = max(length//self.page_size + bool(length%self.page_size), 1) + page_size = self.page_size or self.initial_page_size + properties['max_page'] = max(length//page_size + bool(length % page_size), 1) if isinstance(self.selectable, str) and self.selectable.startswith('checkbox'): properties['select_mode'] = 'checkbox' else: @@ -1726,7 +1731,7 @@ def _get_model( model.children = self._get_model_children( child_panels, doc, root, parent, comm ) - self._link_props(model, ['page', 'sorters', 'expanded', 'filters'], doc, root, comm) + self._link_props(model, ['page', 'sorters', 'expanded', 'filters', 'page_size'], doc, root, comm) self._register_events('cell-click', 'table-edit', 'selection-change', model=model, doc=doc, comm=comm) return model From fb70dc57521809e4a23d93119ed2f0a967e326f5 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 2 Aug 2024 00:09:55 +0200 Subject: [PATCH 19/91] Update CODE_OF_CONDUCT.md with CoC members (#7054) * Update CODE_OF_CONDUCT.md with CoC members * Add people page and add NumFOCUS CoC committee as fallback --- CODE_OF_CONDUCT.md | 12 +++++++++--- doc/about/index.md | 1 + doc/about/people.md | 23 +++++++++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 doc/about/people.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index bea506eb72..177b3b9167 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -66,9 +66,15 @@ representative at an online or offline event. ## 👩‍⚖️ Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -coc@holoviz.org. +Instances of abusive, harassing, or otherwise unacceptable behavior +may be reported to the community leaders responsible for enforcement +at coc@holoviz.org, which is monitored by the [CoC +subcommittee](https://panel.holoviz.org/about/people.html#CoC-Subcommittee) +or a report can be made using the NumFOCUS Code of Conduct report +form. If community leaders cannot come to a resolution about +enforcement, reports will be escalated to the NumFocus Code of Conduct +committee (conduct@numfocus.org). + All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the diff --git a/doc/about/index.md b/doc/about/index.md index 6666523fb1..ccbb10c9d5 100644 --- a/doc/about/index.md +++ b/doc/about/index.md @@ -15,5 +15,6 @@ If you like Panel and have built something you want to share, tweet a link or sc :maxdepth: 2 releases +people roadmap ``` diff --git a/doc/about/people.md b/doc/about/people.md new file mode 100644 index 0000000000..6a897fb9ee --- /dev/null +++ b/doc/about/people.md @@ -0,0 +1,23 @@ +# People + +## Project Lead + +Philipp Rudiger (@philippfr) + +## Maintainers + +- Philipp Rudiger (@philippfr) +- Simon Hansen (@Hoxbro) +- Maxime Liquet (@maximlt) +- Marc Skov Madsen (@MarcSkovMadsen) +- Andrew Huang (@ahuang11) + +## Steering Committee + +The Panel project is governed by the [HoloViz steering committee](https://github.com/holoviz/holoviz/blob/main/doc/governance/org-docs/STEERING-COMMITTEE.md). + +## CoC Subcommittee + +- James A. Bednar (@jbednar) +- Sophia Yang (@sophiamyang) +- Philipp Rudiger (@philippjfr) From b9e89e75fe4d48fd3cfe772a8d9ddb26d9fcb70d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 2 Aug 2024 12:20:17 +0200 Subject: [PATCH 20/91] Tabulator errors with selectable_rows and nonallowed selection (#3721) --- panel/tests/widgets/test_tables.py | 36 +++++++++++++++++++++++++----- panel/widgets/tables.py | 28 +++++++++++++++++++++++ 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/panel/tests/widgets/test_tables.py b/panel/tests/widgets/test_tables.py index 59aa871a2e..015a27791a 100644 --- a/panel/tests/widgets/test_tables.py +++ b/panel/tests/widgets/test_tables.py @@ -685,18 +685,42 @@ def test_tabulator_selectable_rows(document, comm): assert model.selectable_rows == [3, 4] -@pytest.mark.xfail(reason='See https://github.com/holoviz/panel/issues/3644') def test_tabulator_selectable_rows_nonallowed_selection_error(document, comm): df = makeMixedDataFrame() - table = Tabulator(df, selectable_rows=lambda df: [1]) + table = Tabulator(df, selectable_rows=lambda df: [0]) model = table.get_root(document, comm) + assert model.selectable_rows == [0] - assert model.selectable_rows == [1] + err_msg = ( + "Values in 'selection' must not have values " + "which are not available with 'selectable_rows'." + ) - # - with pytest.raises(ValueError): - table.selection = [0] + # This is available with selectable rows + table.selection = [] + assert table.selection == [] + table.selection = [0] + assert table.selection == [0] + + # This is not and should raise the error + with pytest.raises(ValueError, match=err_msg): + table.selection = [1] + assert table.selection == [0] + with pytest.raises(ValueError, match=err_msg): + table.selection = [0, 1] + assert table.selection == [0] + + # No selectable_rows everything should work + table = Tabulator(df) + table.selection = [] + assert table.selection == [] + table.selection = [0] + assert table.selection == [0] + table.selection = [1] + assert table.selection == [1] + table.selection = [0, 1] + assert table.selection == [0, 1] def test_tabulator_pagination(document, comm): diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index 8e1a384d79..480bd17ea0 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -1028,6 +1028,29 @@ def _update_aggregators(self, model): g.aggregators = self._get_aggregators(index) +class _ListValidateWithCallable(param.List): + + __slots__ = ['callable'] + + def __init__(self, **params): + self.callable = params.pop("callable", None) + super().__init__(**params) + + def _validate(self, val): + super()._validate(val) + self._validate_callable(val) + + def _validate_callable(self, val): + if self.callable is not None: + selectable = self.callable() + if selectable and val: + if set(val) - set(selectable): + raise ValueError( + "Values in 'selection' must not have values " + "which are not available with 'selectable_rows'." + ) + + class Tabulator(BaseTable): """ The `Tabulator` widget wraps the [Tabulator js](http://tabulator.info/) @@ -1109,6 +1132,10 @@ class Tabulator(BaseTable): row_height = param.Integer(default=30, doc=""" The height of each table row.""") + selection = _ListValidateWithCallable(default=[], doc=""" + The currently selected rows of the table. It validates + its values against 'selectable_rows' if used.""") + selectable = param.ClassSelector( default=True, class_=(bool, str, int), doc=""" Defines the selection mode of the Tabulator. @@ -1207,6 +1234,7 @@ def __init__(self, value=None, **params): self.on_edit(edit_handler) if style is not None: self.style._todo = style._todo + self.param.selection.callable = self._get_selectable @param.depends('value', watch=True, on_init=True) def _apply_max_size(self): From 071f51a540c318d889e7bbec03836073b40f5a33 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 2 Aug 2024 17:02:12 +0200 Subject: [PATCH 21/91] Do not propagate clicks on input elements in Card header (#7057) * Do not propagate clicks on input elements in Card header * Update card.ts * Apply suggestions from code review --- panel/models/card.ts | 10 +++++++--- panel/tests/ui/layout/test_card.py | 23 +++++++++++++++++++++-- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/panel/models/card.ts b/panel/models/card.ts index 9ba534efa8..01f4654912 100644 --- a/panel/models/card.ts +++ b/panel/models/card.ts @@ -86,8 +86,7 @@ export class CardView extends ColumnView { this.button_el.style.backgroundColor = header_background != null ? header_background : "" header.el.style.backgroundColor = header_background != null ? header_background : "" this.button_el.appendChild(header.el) - - this.button_el.onclick = () => this._toggle_button() + this.button_el.addEventListener("click", (e: MouseEvent) => this._toggle_button(e)) header_el = this.button_el } else { header_el = DOM.create_element((header_tag as any), {class: header_css_classes}) @@ -120,7 +119,12 @@ export class CardView extends ColumnView { this.invalidate_layout() } - _toggle_button(): void { + _toggle_button(e: MouseEvent): void { + for (const path of e.composedPath()) { + if (path instanceof HTMLInputElement) { + return + } + } this.model.collapsed = !this.model.collapsed } diff --git a/panel/tests/ui/layout/test_card.py b/panel/tests/ui/layout/test_card.py index d46e1cebd2..caf481e729 100644 --- a/panel/tests/ui/layout/test_card.py +++ b/panel/tests/ui/layout/test_card.py @@ -4,8 +4,8 @@ from playwright.sync_api import expect -from panel import Card -from panel.tests.util import serve_component +from panel import Card, Row +from panel.tests.util import serve_component, wait_until from panel.widgets import FloatSlider, TextInput pytestmark = pytest.mark.ui @@ -182,3 +182,22 @@ def test_card_scrollable(page): serve_component(page, card) expect(page.locator('.card')).to_have_class('bk-panel-models-layout-Card card scrollable-vertical') + + +def test_card_widget_not_collapsed(page, card_components): + # Fixes https://github.com/holoviz/panel/issues/7045 + w1, w2 = card_components + card = Card(w1, header=Row(w2)) + + serve_component(page, card) + + text_input = page.locator('.bk-input[type="text"]') + expect(text_input).to_have_count(1) + + text_input.click() + + text_input.press("F") + text_input.press("Enter") + + wait_until(lambda: w2.value == 'F', page) + assert not card.collapsed From 3fe630783ac25b529caf851a17d0e3047dfc66cf Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 2 Aug 2024 23:12:44 +0200 Subject: [PATCH 22/91] Ensure Tabulator.selection is consisting across pagination and filtering (#7058) --- panel/tests/conftest.py | 35 +++++++ panel/tests/ui/widgets/test_tabulator.py | 116 ++++++++++++++++++++--- panel/tests/widgets/test_tables.py | 45 ++++++++- panel/widgets/tables.py | 27 ++---- 4 files changed, 188 insertions(+), 35 deletions(-) diff --git a/panel/tests/conftest.py b/panel/tests/conftest.py index c839dac02d..08dee3e78a 100644 --- a/panel/tests/conftest.py +++ b/panel/tests/conftest.py @@ -3,6 +3,7 @@ """ import asyncio import atexit +import datetime as dt import os import pathlib import re @@ -493,3 +494,37 @@ def eh(exception): yield exceptions finally: config.exception_handler = old_eh + + +@pytest.fixture +def df_mixed(): + df = pd.DataFrame({ + 'int': [1, 2, 3, 4], + 'float': [3.14, 6.28, 9.42, -2.45], + 'str': ['A', 'B', 'C', 'D'], + 'bool': [True, True, True, False], + 'date': [dt.date(2019, 1, 1), dt.date(2020, 1, 1), dt.date(2020, 1, 10), dt.date(2019, 1, 10)], + 'datetime': [dt.datetime(2019, 1, 1, 10), dt.datetime(2020, 1, 1, 12), dt.datetime(2020, 1, 10, 13), dt.datetime(2020, 1, 15, 13)] + }, index=['idx0', 'idx1', 'idx2', 'idx3']) + return df + +@pytest.fixture +def df_strings(): + descr = [ + 'Under the Weather', + 'Top Drawer', + 'Happy as a Clam', + 'Cut To The Chase', + 'Knock Your Socks Off', + 'A Cold Day in Hell', + 'All Greek To Me', + 'A Cut Above', + 'Cut The Mustard', + 'Up In Arms', + 'Playing For Keeps', + 'Fit as a Fiddle', + ] + + code = [f'{i:02d}' for i in range(len(descr))] + + return pd.DataFrame(dict(code=code, descr=descr)) diff --git a/panel/tests/ui/widgets/test_tabulator.py b/panel/tests/ui/widgets/test_tabulator.py index 664a7b8edc..a358044a12 100644 --- a/panel/tests/ui/widgets/test_tabulator.py +++ b/panel/tests/ui/widgets/test_tabulator.py @@ -24,24 +24,11 @@ from panel.layout.base import Column from panel.models.tabulator import _TABULATOR_THEMES_MAPPING from panel.tests.util import get_ctrl_modifier, serve_component, wait_until -from panel.widgets import Select, Tabulator +from panel.widgets import Select, Tabulator, TextInput pytestmark = pytest.mark.ui -@pytest.fixture -def df_mixed(): - df = pd.DataFrame({ - 'int': [1, 2, 3, 4], - 'float': [3.14, 6.28, 9.42, -2.45], - 'str': ['A', 'B', 'C', 'D'], - 'bool': [True, True, True, False], - 'date': [dt.date(2019, 1, 1), dt.date(2020, 1, 1), dt.date(2020, 1, 10), dt.date(2019, 1, 10)], - 'datetime': [dt.datetime(2019, 1, 1, 10), dt.datetime(2020, 1, 1, 12), dt.datetime(2020, 1, 10, 13), dt.datetime(2020, 1, 15, 13)] - }, index=['idx0', 'idx1', 'idx2', 'idx3']) - return df - - @pytest.fixture(scope='session') def df_mixed_as_string(): return """index @@ -2967,7 +2954,6 @@ def test_tabulator_selection_sorters_on_init(page, df_mixed): assert widget.selected_dataframe.equals(expected_selected) -@pytest.mark.xfail(reason='https://github.com/holoviz/panel/issues/3664') def test_tabulator_selection_header_filter_unchanged(page): df = pd.DataFrame({ 'col1': list('XYYYYY'), @@ -3410,6 +3396,106 @@ def test_tabulator_remote_pagination_auto_page_size_shrink(page, df_mixed): wait_until(lambda: widget.page_size == 3, page) +@pytest.mark.parametrize('pagination', ['local', 'remote', None]) +def test_selection_indices_on_paginated_and_filtered_data(page, df_strings, pagination): + tbl = Tabulator( + df_strings, + disabled=True, + pagination=pagination, + page_size=6, + ) + + descr_filter = TextInput(name='descr', value='cut') + + def contains_filter(df, pattern=None): + if not pattern: + return df + return df[df.descr.str.contains(pattern, case=False)] + + filter_fn = param.bind(contains_filter, pattern=descr_filter) + tbl.add_filter(filter_fn) + + serve_component(page, tbl) + + expect(page.locator('.tabulator-table')).to_have_count(1) + + row = page.locator('.tabulator-row').nth(1) + row.click() + + wait_until(lambda: tbl.selection == [7], page) + + tbl.page_size = 2 + + page.locator('.tabulator-row').nth(0).click() + + wait_until(lambda: tbl.selection == [3], page) + + if pagination: + page.locator('.tabulator-pages > .tabulator-page').nth(1).click() + expect(page.locator('.tabulator-row')).to_have_count(1) + page.locator('.tabulator-row').nth(0).click() + else: + expect(page.locator('.tabulator-row')).to_have_count(3) + page.locator('.tabulator-row').nth(2).click() + + wait_until(lambda: tbl.selection == [8], page) + + descr_filter.value = '' + + wait_until(lambda: tbl.selection == [8], page) + + +@pytest.mark.parametrize('pagination', ['local', 'remote', None]) +def test_selection_indices_on_paginated_sorted_and_filtered_data(page, df_strings, pagination): + tbl = Tabulator( + df_strings, + disabled=True, + pagination=pagination, + page_size=6, + ) + + descr_filter = TextInput(name='descr', value='cut') + + def contains_filter(df, pattern=None): + if not pattern: + return df + return df[df.descr.str.contains(pattern, case=False)] + + filter_fn = param.bind(contains_filter, pattern=descr_filter) + tbl.add_filter(filter_fn) + + serve_component(page, tbl) + + expect(page.locator('.tabulator-table')).to_have_count(1) + + page.locator('.tabulator-col-title-holder').nth(3).click() + + row = page.locator('.tabulator-row').nth(1) + row.click() + + wait_until(lambda: tbl.selection == [8], page) + + tbl.page_size = 2 + + page.locator('.tabulator-col-title-holder').nth(3).click() + page.locator('.tabulator-row').nth(0).click() + + wait_until(lambda: tbl.selection == [3], page) + + if pagination: + page.locator('.tabulator-pages > .tabulator-page').nth(1).click() + expect(page.locator('.tabulator-row')).to_have_count(1) + page.locator('.tabulator-row').nth(0).click() + else: + expect(page.locator('.tabulator-row')).to_have_count(3) + page.locator('.tabulator-row').nth(2).click() + + wait_until(lambda: tbl.selection == [7], page) + + descr_filter.value = '' + + wait_until(lambda: tbl.selection == [7], page) + class Test_RemotePagination: diff --git a/panel/tests/widgets/test_tables.py b/panel/tests/widgets/test_tables.py index 015a27791a..c874a5b32d 100644 --- a/panel/tests/widgets/test_tables.py +++ b/panel/tests/widgets/test_tables.py @@ -5,6 +5,7 @@ import numpy as np import pandas as pd +import param import pytest from bokeh.models.widgets.tables import ( @@ -36,6 +37,7 @@ def makeMixedDataFrame(): return pd.DataFrame(data) + def test_dataframe_widget(dataframe, document, comm): table = DataFrame(dataframe) @@ -395,6 +397,8 @@ def test_tabulator_selected_and_filtered_dataframe(document, comm): table.add_filter('foo3', 'C') + assert table.selection == list(range(5)) + pd.testing.assert_frame_equal(table.selected_dataframe, df[df["C"] == "foo3"]) table.remove_filter('foo3') @@ -403,7 +407,46 @@ def test_tabulator_selected_and_filtered_dataframe(document, comm): table.add_filter('foo3', 'C') - assert table.selection == [0] + assert table.selection == [0, 1, 2] + + +@pytest.mark.parametrize('pagination', ['local', 'remote', None]) +def test_selection_indices_on_remote_paginated_and_filtered_data(document, comm, df_strings, pagination): + tbl = Tabulator( + df_strings, + pagination=pagination, + page_size=6, + show_index=False, + height=300, + width=400 + ) + + descr_filter = TextInput(name='descr') + + def contains_filter(df, pattern=None): + if not pattern: + return df + return df[df.descr.str.contains(pattern, case=False)] + + filter_fn = param.bind(contains_filter, pattern=descr_filter) + tbl.add_filter(filter_fn) + + model = tbl.get_root(document, comm) + + descr_filter.value = 'cut' + + pd.testing.assert_frame_equal( + tbl.current_view, df_strings[df_strings.descr.str.contains('cut', case=False)] + ) + + model.source.selected.indices = [0, 2] + + assert tbl.selection == [3, 8] + + model.page_size = 2 + model.source.selected.indices = [1] + + assert tbl.selection == [7] def test_tabulator_config_defaults(document, comm): diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index 480bd17ea0..92b1750621 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -295,21 +295,8 @@ def _update_index_mapping(self): @updating def _update_cds(self, *events: param.parameterized.Event): - old_processed = self._processed self._processed, data = self._get_data() self._update_index_mapping() - # If there is a selection we have to compute new index - if self.selection and old_processed is not None: - indexes = list(self._processed.index) - selection = [] - for sel in self.selection: - try: - iv = old_processed.index[sel] - idx = indexes.index(iv) - selection.append(idx) - except Exception: - continue - self.selection = selection self._data = {k: _convert_datetime_array_ignore_list(v) for k, v in data.items()} msg = {'data': self._data} for ref, (m, _) in self._models.items(): @@ -879,7 +866,8 @@ def selected_dataframe(self): """ if not self.selection: return self.current_view.iloc[:0] - return self._processed.iloc[self.selection] + df = self.value.iloc[self.selection] + return self._filter_dataframe(df) class DataFrame(BaseTable): @@ -1627,6 +1615,7 @@ def _update_selectable(self): for ref, (model, _) in self._models.items(): self._apply_update([], {'selectable_rows': selectable}, model, ref) + @param.depends('page_size', watch=True) def _update_max_page(self): length = self._length nrows = self.page_size or self.initial_page_size @@ -1680,9 +1669,6 @@ def _update_column(self, column: str, array: np.ndarray): self._processed.loc[index, column] = array def _update_selection(self, indices: list[int] | SelectionEvent): - if self.pagination != 'remote': - self.selection = indices - return if isinstance(indices, list): selected = True ilocs = [] @@ -1691,8 +1677,11 @@ def _update_selection(self, indices: list[int] | SelectionEvent): ilocs = [] if indices.flush else self.selection.copy() indices = indices.indices - nrows = self.page_size or self.initial_page_size - start = (self.page-1)*nrows + if self.pagination == 'remote': + nrows = self.page_size or self.initial_page_size + start = (self.page-1)*nrows + else: + start = 0 index = self._processed.iloc[[start+ind for ind in indices]].index for v in index.values: try: From a93b6091ef50cd8fe75822373260ed9957a6516f Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 3 Aug 2024 11:02:37 +0200 Subject: [PATCH 23/91] Ensure Tabulator range selection applies to current view (#7063) --- panel/models/tabulator.ts | 66 ++++++++++-------------- panel/tests/ui/widgets/test_tabulator.py | 37 +++++++++++++ panel/widgets/tables.py | 3 +- 3 files changed, 66 insertions(+), 40 deletions(-) diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index e996d231fe..6befdceed4 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -312,6 +312,7 @@ export class DataTabulatorView extends HTMLBoxView { _updating_page: boolean = false _updating_sort: boolean = false _selection_updating: boolean = false + _last_selected_row: any = null _initializing: boolean _lastVerticalScrollbarTopPosition: number = 0 _lastHorizontalScrollbarLeftPosition: number = 0 @@ -785,7 +786,7 @@ export class DataTabulatorView extends HTMLBoxView { _expand_render(cell: any): string { const index = cell._cell.row.data._index const icon = this.model.expanded.indexOf(index) < 0 ? "►" : "▼" - return `${icon}` + return icon } _update_expand(cell: any): void { @@ -1233,44 +1234,25 @@ export class DataTabulatorView extends HTMLBoxView { const selected = this.model.source.selected const index: number = row._row.data._index - if (this.model.pagination === "remote") { - const includes = this.model.source.selected.indices.indexOf(index) == -1 - const flush = !(e.ctrlKey || e.metaKey || e.shiftKey) - if (e.shiftKey && selected.indices.length) { - const start = selected.indices[selected.indices.length-1] - if (index>start) { - for (let i = start; i<=index; i++) { - indices.push(i) - } - } else { - for (let i = start; i>=index; i--) { - indices.push(i) - } - } - } else { - indices.push(index) - } - this._selection_updating = true - this.model.trigger_event(new SelectionEvent(indices, includes, flush)) - this._selection_updating = false - return - } - if (e.ctrlKey || e.metaKey) { - indices = [...this.model.source.selected.indices] - } else if (e.shiftKey && selected.indices.length) { - const start = selected.indices[selected.indices.length-1] - if (index>start) { - for (let i = start; iindex; i--) { - indices.push(i) - } + indices = [...selected.indices] + } else if (e.shiftKey && this._last_selected_row) { + const rows = row._row.parent.getDisplayRows() + const start_idx = rows.indexOf(this._last_selected_row) + if (start_idx !== -1) { + const end_idx = rows.indexOf(row._row) + const reverse = start_idx > end_idx + const [start, end] = reverse ? [end_idx+1, start_idx+1] : [start_idx, end_idx] + indices = rows.slice(start, end).map((r: any) => r.data._index) + if (reverse) { indices = indices.reverse() } } } - if (indices.indexOf(index) < 0) { + const flush = !(e.ctrlKey || e.metaKey || e.shiftKey) + const includes = indices.includes(index) + const remote = this.model.pagination === "remote" + + // Toggle the index on or off (if remote we let Python do the toggling) + if (!includes || remote) { indices.push(index) } else { indices.splice(indices.indexOf(index), 1) @@ -1282,10 +1264,16 @@ export class DataTabulatorView extends HTMLBoxView { } } const filtered = this._filter_selected(indices) - this.tabulator.deselectRow() - this.tabulator.selectRow(filtered) + if (!remote) { + this.tabulator.deselectRow() + this.tabulator.selectRow(filtered) + } + this._last_selected_row = row._row this._selection_updating = true - selected.indices = filtered + if (!remote) { + selected.indices = filtered + } + this.model.trigger_event(new SelectionEvent(indices, !includes, flush)) this._selection_updating = false } diff --git a/panel/tests/ui/widgets/test_tabulator.py b/panel/tests/ui/widgets/test_tabulator.py index a358044a12..fd9dc06cd0 100644 --- a/panel/tests/ui/widgets/test_tabulator.py +++ b/panel/tests/ui/widgets/test_tabulator.py @@ -3497,6 +3497,42 @@ def contains_filter(df, pattern=None): wait_until(lambda: tbl.selection == [7], page) +@pytest.mark.parametrize('pagination', ['remote', 'local', None]) +def test_range_selection_on_sorted_data_downward(page, pagination): + df = pd.DataFrame({'a': [1, 3, 2, 4, 5, 6, 7, 8, 9], 'b': [6, 5, 6, 7, 7, 7, 7, 7, 7]}) + table = Tabulator(df, disabled=True, pagination=pagination) + + serve_component(page, table) + + page.locator('.tabulator-col-title-holder').nth(2).click() + + page.locator('.tabulator-row').nth(0).click() + + page.keyboard.down('Shift') + + page.locator('.tabulator-row').nth(1).click() + + wait_until(lambda: table.selection == [0, 2], page) + + +@pytest.mark.parametrize('pagination', ['remote', 'local', None]) +def test_range_selection_on_sorted_data_upward(page, pagination): + df = pd.DataFrame({'a': [1, 3, 2, 4, 5, 6, 7, 8, 9], 'b': [6, 5, 6, 7, 7, 7, 7, 7, 7]}) + table = Tabulator(df, disabled=True, pagination=pagination, page_size=3) + + serve_component(page, table) + + page.locator('.tabulator-col-title-holder').nth(2).click() + + page.locator('.tabulator-row').nth(1).click() + + page.keyboard.down('Shift') + + page.locator('.tabulator-row').nth(0).click() + + wait_until(lambda: table.selection == [2, 0], page) + + class Test_RemotePagination: @pytest.fixture(autouse=True) @@ -3516,6 +3552,7 @@ def check_selected(self, page, expected, ui_count=None): ui_count = len(expected) expect(page.locator('.tabulator-selected')).to_have_count(ui_count) + wait_until(lambda: self.widget.selection == expected, page) @contextmanager diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index 92b1750621..153358c905 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -1275,7 +1275,8 @@ def _cleanup(self, root: Model | None = None) -> None: def _process_event(self, event) -> None: if event.event_name == 'selection-change': - self._update_selection(event) + if self.pagination == 'remote': + self._update_selection(event) return event_col = self._renamed_cols.get(event.column, event.column) From 7adcf3db8d35be69346a35b65917119852093fa9 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 3 Aug 2024 13:00:56 +0200 Subject: [PATCH 24/91] Ensure Tabulator selection is updated when indexes change (#7066) --- panel/tests/ui/widgets/test_tabulator.py | 5 ++- panel/tests/widgets/test_tables.py | 9 +++++ panel/widgets/tables.py | 45 ++++++++++++++++-------- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/panel/tests/ui/widgets/test_tabulator.py b/panel/tests/ui/widgets/test_tabulator.py index fd9dc06cd0..1aa40e69ac 100644 --- a/panel/tests/ui/widgets/test_tabulator.py +++ b/panel/tests/ui/widgets/test_tabulator.py @@ -1968,7 +1968,6 @@ def test_tabulator_header_filters_default(page, df_mixed, cols): ([0, 1], 'input[type="number"]'), (np.array([0, 1], dtype=np.uint64), 'input[type="number"]'), ([0.1, 1.1], 'input[type="number"]'), - # ([True, False], 'input[type="checkbox"]'), # Pandas cannot have boolean indexes apparently ), ) def test_tabulator_header_filters_default_index(page, index, expected_selector): @@ -3506,6 +3505,8 @@ def test_range_selection_on_sorted_data_downward(page, pagination): page.locator('.tabulator-col-title-holder').nth(2).click() + page.wait_for_timeout(100) + page.locator('.tabulator-row').nth(0).click() page.keyboard.down('Shift') @@ -3524,6 +3525,8 @@ def test_range_selection_on_sorted_data_upward(page, pagination): page.locator('.tabulator-col-title-holder').nth(2).click() + page.wait_for_timeout(100) + page.locator('.tabulator-row').nth(1).click() page.keyboard.down('Shift') diff --git a/panel/tests/widgets/test_tables.py b/panel/tests/widgets/test_tables.py index c874a5b32d..d7fdf19ccc 100644 --- a/panel/tests/widgets/test_tables.py +++ b/panel/tests/widgets/test_tables.py @@ -240,6 +240,15 @@ def test_none_table(document, comm): assert model.source.data == {} +def test_tabulator_selection_resets(): + df = makeMixedDataFrame() + table = Tabulator(df, selection=list(range(len(df)))) + + for i in reversed(range(len(df))): + table.value = df.iloc[:i] + assert table.selection == list(range(i)) + + def test_tabulator_selected_dataframe(): df = makeMixedDataFrame() table = Tabulator(df, selection=[0, 2]) diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index 153358c905..a69fda8419 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -116,7 +116,10 @@ def __init__(self, value=None, **params): self._index_mapping = {} self._edited_indexes = [] super().__init__(value=value, **params) - self.param.watch(self._setup_on_change, ['editors', 'formatters']) + self._internal_callbacks.extend([ + self.param.watch(self._setup_on_change, ['editors', 'formatters']), + self.param._watch(self._reset_selection, ['value'], precedence=-1) + ]) self.param.trigger('editors') self.param.trigger('formatters') @@ -129,6 +132,32 @@ def _compute_renamed_cols(self): str(col) if str(col) != col else col: col for col in self._get_fields() } + def _reset_selection(self, event): + if event.type == 'triggered' and self._updating: + return + if self._indexes_changed(event.old, event.new): + selection = [] + for sel in self.selection: + idx = event.old.index[sel] + try: + new = event.new.index.get_loc(idx) + selection.append(new) + except KeyError: + pass + self.selection = selection + + def _indexes_changed(self, old, new): + """ + Comparator that checks whether DataFrame indexes have changed. + + If indexes and length are unchanged we assume we do not + have to reset various settings including expanded rows, + scroll position, pagination etc. + """ + if type(old) != type(new) or isinstance(new, dict) or len(old) != len(new): + return True + return (old.index != new.index).any() + @property def _length(self): return len(self._processed) @@ -1495,20 +1524,6 @@ def _get_model_children(self, panels, doc, root, parent, comm=None): models[i] = model return models - def _indexes_changed(self, old, new): - """ - Comparator that checks whether DataFrame indexes have changed. - - If indexes and length are unchanged we assume we do not - have to reset various settings including expanded rows, - scroll position, pagination etc. - """ - if type(old) != type(new) or isinstance(new, dict): - return True - elif len(old) != len(new): - return False - return (old.index != new.index).any() - def _update_children(self, *events): cleanup, reuse = set(), set() page_events = ('page', 'page_size', 'pagination') From 2ecc982da9880efe9315ce80e56ec2f9fe96f48f Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 3 Aug 2024 13:56:29 +0200 Subject: [PATCH 25/91] Ensure Tabulator can be updated with None value (#7067) --- panel/tests/widgets/test_tables.py | 22 +++++++++++++++++++++- panel/widgets/tables.py | 2 +- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/panel/tests/widgets/test_tables.py b/panel/tests/widgets/test_tables.py index d7fdf19ccc..e8dbd73cd4 100644 --- a/panel/tests/widgets/test_tables.py +++ b/panel/tests/widgets/test_tables.py @@ -37,7 +37,6 @@ def makeMixedDataFrame(): return pd.DataFrame(data) - def test_dataframe_widget(dataframe, document, comm): table = DataFrame(dataframe) @@ -240,6 +239,27 @@ def test_none_table(document, comm): assert model.source.data == {} +def test_tabulator_none_value(document, comm): + table = Tabulator(value=None) + assert table.indexes == [] + + model = table.get_root(document, comm) + + assert model.source.data == {} + assert model.columns == [] + + +def test_tabulator_update_none_value(document, comm, df_mixed): + table = Tabulator(value=df_mixed) + + model = table.get_root(document, comm) + + table.value = None + + assert model.source.data == {} + assert model.columns == [] + + def test_tabulator_selection_resets(): df = makeMixedDataFrame() table = Tabulator(df, selection=list(range(len(df)))) diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index a69fda8419..66f17b3938 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -172,7 +172,7 @@ def _validate(self, *events: param.parameterized.Event): def _get_fields(self) -> list[str]: indexes = self.indexes - col_names = list(self.value.columns) + col_names = [] if self.value is None else list(self.value.columns) if not self.hierarchical or len(indexes) == 1: col_names = indexes + col_names else: From 3c1ce395c1666c531df01a3505769be74eb0bbe8 Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Sun, 4 Aug 2024 22:13:06 +0200 Subject: [PATCH 26/91] add missing docstring (#7072) --- panel/layout/float.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/panel/layout/float.py b/panel/layout/float.py index af945905b8..7740b4fe67 100644 --- a/panel/layout/float.py +++ b/panel/layout/float.py @@ -34,6 +34,14 @@ class FloatPanel(ListLike, ReactiveHTML): """ Float provides a floating panel layout. + + Reference: https://panel.holoviz.org/reference/layouts/FloatPanel.html + + :Example: + + >>> import panel as pn + >>> pn.extension("floatpanel") + >>> pn.layout.FloatPanel("**I can float**!", position="center", width=300).servable() """ config = param.Dict({}, doc=""" From d98bf1f6280ba45b3ad223ec2bbd36ac8a4548c2 Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Sun, 4 Aug 2024 22:13:24 +0200 Subject: [PATCH 27/91] fix anywidget file name (#7071) --- .../custom_components/{AnyWidget.md => AnyWidgetComponent.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/reference/custom_components/{AnyWidget.md => AnyWidgetComponent.md} (100%) diff --git a/examples/reference/custom_components/AnyWidget.md b/examples/reference/custom_components/AnyWidgetComponent.md similarity index 100% rename from examples/reference/custom_components/AnyWidget.md rename to examples/reference/custom_components/AnyWidgetComponent.md From 6f0b03ec8e3d05d289811acc37aada181c5264c7 Mon Sep 17 00:00:00 2001 From: Jordan Samuels Date: Mon, 5 Aug 2024 05:38:57 -0500 Subject: [PATCH 28/91] Resolves #6962. (#7059) * See comment by @MarcSkovMadsen [here](https://github.com/holoviz/panel/issues/6962#issuecomment-2227213039). --- panel/io/jupyter_server_extension.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panel/io/jupyter_server_extension.py b/panel/io/jupyter_server_extension.py index 6d1d01dc35..d11a74e1a7 100644 --- a/panel/io/jupyter_server_extension.py +++ b/panel/io/jupyter_server_extension.py @@ -114,7 +114,7 @@ def get_server_root_dir(settings): app = r'{{ path }}' os.chdir(str(pathlib.Path(app).parent)) -sys.path = [os.getcwd()] + sys.path[1:] +sys.path = [os.getcwd()] + sys.path from panel.io.jupyter_executor import PanelExecutor executor = PanelExecutor(app, '{{ token }}', '{{ root_url }}') From b10308bae22c77f94ef7653cd2681050f13ae3df Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 5 Aug 2024 12:47:34 +0200 Subject: [PATCH 29/91] Ensure Tabulator styling is correctly applied on multi-index (#7075) --- panel/tests/widgets/test_tables.py | 26 ++++++++++++++++++++++++++ panel/widgets/tables.py | 5 ++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/panel/tests/widgets/test_tables.py b/panel/tests/widgets/test_tables.py index e8dbd73cd4..6fd5f46773 100644 --- a/panel/tests/widgets/test_tables.py +++ b/panel/tests/widgets/test_tables.py @@ -2247,6 +2247,32 @@ def test_tabulator_styling_empty_dataframe(document, comm): } } +def test_tabulator_style_multi_index_dataframe(document, comm): + # See https://github.com/holoviz/panel/issues/6151 + arrays = [['A', 'A', 'B', 'B'], [1, 2, 1, 2]] + index = pd.MultiIndex.from_arrays(arrays, names=('Letters', 'Numbers')) + df = pd.DataFrame({ + 'Values': [1, 2, 3, 4], + 'X': [10, 20, 30, 40], + 'Y': [100, 200, 300, 400], + 'Z': [1000, 2000, 3000, 4000] + }, index=index) + + def color_func(vals): + return ["background-color: #ff0000;" for v in vals] + + tabulator = Tabulator(df, width=500, height=300) + tabulator.style.apply(color_func, subset = ['X']) + + model = tabulator.get_root(document, comm) + + assert model.cell_styles['data'] == { + 0: {4: [('background-color', '#ff0000')]}, + 1: {4: [('background-color', '#ff0000')]}, + 2: {4: [('background-color', '#ff0000')]}, + 3: {4: [('background-color', '#ff0000')]} + } + @mpl_available def test_tabulator_style_background_gradient_with_frozen_columns(document, comm): diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index 66f17b3938..d228fe946f 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -1436,7 +1436,10 @@ def _get_style_data(self, recompute=True): styler = self._computed_styler if styler is None: return {} - offset = 1 + len(self.indexes) + int(self.selectable in ('checkbox', 'checkbox-single')) + int(bool(self.row_content)) + + # Compute offsets (not that multi-indexes are reset so don't require an offset) + offset = 1 + int(len(self.indexes) == 1) + int(self.selectable in ('checkbox', 'checkbox-single')) + int(bool(self.row_content)) + if self.pagination == 'remote': page_size = self.page_size or self.initial_page_size start = (self.page - 1) * page_size From e7afa8a06ed6652e950f0184189fa808f0c74536 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 5 Aug 2024 15:57:29 +0200 Subject: [PATCH 30/91] Fix various scrolling related Tabulator issues (#7076) --- panel/models/tabulator.ts | 39 ++++++++++++++++++------ panel/tests/ui/widgets/test_tabulator.py | 11 ++----- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index 6befdceed4..84e86eed7a 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -319,7 +319,8 @@ export class DataTabulatorView extends HTMLBoxView { _applied_styles: boolean = false _building: boolean = false _debounced_redraw: any = null - _restore_scroll: boolean = false + _restore_scroll: boolean | "horizontal" | "vertical" = false + _updating_scroll: boolean = false override connect_signals(): void { super.connect_signals() @@ -396,18 +397,23 @@ export class DataTabulatorView extends HTMLBoxView { if (this.tabulator === undefined) { return } + this._restore_scroll = "horizontal" this._selection_updating = true + this._updating_scroll = true this.setData() + this._updating_scroll = false this._selection_updating = false this.postUpdate() }) this.connect(this.model.source.streaming, () => this.addData()) this.connect(this.model.source.patching, () => { const inds = this.model.source.selected.indices + this._updating_scroll = true this.updateOrAddData() - this.record_scroll() + this._updating_scroll = false // Restore indices since updating data may have reset checkbox column this.model.source.selected.indices = inds + this.restore_scroll() }) this.connect(this.model.source.selected.change, () => this.setSelection()) this.connect(this.model.source.selected.properties.indices.change, () => this.setSelection()) @@ -1028,7 +1034,6 @@ export class DataTabulatorView extends HTMLBoxView { } // Update table - setData(): void { if (this._initializing || this._building || !this.tabulator.initialized) { return @@ -1055,7 +1060,9 @@ export class DataTabulatorView extends HTMLBoxView { this.setSelection() this.setStyles() if (this._restore_scroll) { - this.restore_scroll() + const vertical = this._restore_scroll === "horizontal" ? false : true + const horizontal = this._restore_scroll === "vertical" ? false : true + this.restore_scroll(horizontal, vertical) this._restore_scroll = false } } @@ -1203,18 +1210,30 @@ export class DataTabulatorView extends HTMLBoxView { this._selection_updating = false } - restore_scroll(): void { - const opts = { - top: this._lastVerticalScrollbarTopPosition, - left: this._lastHorizontalScrollbarLeftPosition, - behavior: "instant", + restore_scroll(horizontal: boolean=true, vertical: boolean=true): void { + if (!(horizontal || vertical)) { + return } - setTimeout(() => this.tabulator.rowManager.element.scrollTo(opts), 0) + const opts: ScrollToOptions = {behavior: "instant"} + if (vertical) { + opts.top = this._lastVerticalScrollbarTopPosition + } + if (horizontal) { + opts.left = this._lastHorizontalScrollbarLeftPosition + } + setTimeout(() => { + this._updating_scroll = true + this.tabulator.rowManager.element.scrollTo(opts) + this._updating_scroll = false + }, 0) } // Update model record_scroll() { + if (this._updating_scroll) { + return + } this._lastVerticalScrollbarTopPosition = this.tabulator.rowManager.element.scrollTop this._lastHorizontalScrollbarLeftPosition = this.tabulator.rowManager.element.scrollLeft } diff --git a/panel/tests/ui/widgets/test_tabulator.py b/panel/tests/ui/widgets/test_tabulator.py index 1aa40e69ac..2ee5f6476e 100644 --- a/panel/tests/ui/widgets/test_tabulator.py +++ b/panel/tests/ui/widgets/test_tabulator.py @@ -1029,7 +1029,6 @@ def test_tabulator_frozen_rows(page): assert Y_bb == page.locator('text="Y"').bounding_box() -@pytest.mark.xfail(reason='See https://github.com/holoviz/panel/issues/3669') def test_tabulator_patch_no_horizontal_rescroll(page, df_mixed): widths = 100 width = int(((df_mixed.shape[1] + 1) * widths) / 2) @@ -1049,8 +1048,7 @@ def test_tabulator_patch_no_horizontal_rescroll(page, df_mixed): # Catch a potential rescroll page.wait_for_timeout(400) # The table should keep the same scroll position - # This fails - assert bb == page.locator('text="tomodify"').bounding_box() + wait_until(lambda: bb == page.locator('text="tomodify"').bounding_box(), page) @pytest.mark.xfail(reason='See https://github.com/holoviz/panel/issues/3249') @@ -1113,12 +1111,7 @@ def test_tabulator_patch_no_height_resize(page): @pytest.mark.parametrize( - 'pagination', - ( - pytest.param('local', marks=pytest.mark.xfail(reason='See https://github.com/holoviz/panel/issues/3553')), - pytest.param('remote', marks=pytest.mark.xfail(reason='See https://github.com/holoviz/panel/issues/3553')), - None, - ) + 'pagination', ('local', 'remote', None) ) def test_tabulator_header_filter_no_horizontal_rescroll(page, df_mixed, pagination): widths = 100 From a33d277a3710eece06ac1e551290cafd99fa8bff Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 6 Aug 2024 10:30:58 +0200 Subject: [PATCH 31/91] Ensure Tabulator data is updated after filters are changed (#7074) --- panel/tests/widgets/test_tables.py | 8 ++++++++ panel/widgets/tables.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/panel/tests/widgets/test_tables.py b/panel/tests/widgets/test_tables.py index 6fd5f46773..b716522f21 100644 --- a/panel/tests/widgets/test_tables.py +++ b/panel/tests/widgets/test_tables.py @@ -1312,6 +1312,14 @@ def test_tabulator_patch_with_filters(document, comm): table.value[col].values, expected_df[col] ) + table.filters = [] + + for col, values in model.source.data.items(): + expected = expected_df[col] + if col == 'D': + expected = expected.astype(np.int64) / 10e5 + np.testing.assert_array_equal(values, expected) + def test_tabulator_patch_with_sorters(document, comm): df = makeMixedDataFrame() table = Tabulator(df, sorters=[{'field': 'A', 'sorter': 'number', 'dir': 'desc'}]) diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index d228fe946f..9aa320b3e2 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -1609,7 +1609,7 @@ def _patch(self, patch): def _update_cds(self, *events): if any(event.name == 'filters' for event in events): self._edited_indexes = [] - page_events = ('page', 'page_size', 'sorters', 'filters') + page_events = ('page', 'page_size', 'sorters') if self._updating: return elif events and all(e.name in page_events[:-1] for e in events) and self.pagination == 'local': From 663a159c9a44e7f3ca327894773e8e38f4f80330 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 6 Aug 2024 11:59:01 +0200 Subject: [PATCH 32/91] Allow controlling DataFrame header and cell alignment (#7082) * Ensure DataFrame justify parameter controls header alignment * Allow controlling DataFrame cell alignment --- examples/reference/panes/DataFrame.ipynb | 1 + panel/dist/css/dataframe.css | 24 ++++++++++++++++++++++++ panel/pane/markup.py | 14 +++++++++++--- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/examples/reference/panes/DataFrame.ipynb b/examples/reference/panes/DataFrame.ipynb index 1747404f96..f97c565fdc 100644 --- a/examples/reference/panes/DataFrame.ipynb +++ b/examples/reference/panes/DataFrame.ipynb @@ -43,6 +43,7 @@ "* **``render_links``** (boolean, default=False): Convert URLs to HTML links.\n", "* **``show_dimensions``** (boolean, default=False): Display DataFrame dimensions (number of rows by number of columns).\n", "* **``sparsify``** (boolean, default=True): Set to False for a DataFrame with a hierarchical index to print every multi-index key at each row.\n", + "* **``text_align``** (str): How to justify the non-header cells ('left', 'right', 'center')\n", "\n", "___" ] diff --git a/panel/dist/css/dataframe.css b/panel/dist/css/dataframe.css index d3e09bfd7b..1d7a0c8121 100644 --- a/panel/dist/css/dataframe.css +++ b/panel/dist/css/dataframe.css @@ -48,3 +48,27 @@ table.panel-df { overflow: auto; padding-right: 1px; } + +th[halign='left'] { + text-align: left; +} + +th[halign='left'] { + text-align: right; +} + +th[halign='center'] { + text-align: center; +} + +.panel-df.start-align td { + text-align: left; +} + +.panel-df.center-align td { + text-align: center; +} + +.panel-df.end-align td { + text-align: right; +} diff --git a/panel/pane/markup.py b/panel/pane/markup.py index c98cd5d693..ba5cddb7f3 100644 --- a/panel/pane/markup.py +++ b/panel/pane/markup.py @@ -176,6 +176,10 @@ class DataFrame(HTML): Set to False for a DataFrame with a hierarchical index to print every multi-index key at each row.""") + text_align = param.Selector(default=None, objects=[ + 'start', 'end', 'center'], doc=""" + Alignment of non-header cells.""") + _object = param.Parameter(default=None, doc="""Hidden parameter.""") _dask_params: ClassVar[list[str]] = ['max_rows'] @@ -185,7 +189,7 @@ class DataFrame(HTML): 'col_space', 'decimal', 'escape', 'float_format', 'formatters', 'header', 'index', 'index_names', 'justify', 'max_rows', 'max_cols', 'na_rep', 'render_links', 'show_dimensions', - 'sparsify', 'sizing_mode' + 'sparsify', 'text_align', 'sizing_mode' ] _rename: ClassVar[Mapping[str, str | None]] = { @@ -244,14 +248,18 @@ def _transform_object(self, obj: Any) -> dict[str, Any]: module = getattr(obj, '__module__', '') if hasattr(obj, 'to_html'): + classes = list(self.classes) + if self.text_align: + classes.append(f'{self.text_align}-align') if 'dask' in module: html = obj.to_html(max_rows=self.max_rows).replace('border="1"', '') elif 'style' in module: - classes = ' '.join(self.classes) + classes = ' '.join(classes) html = obj.to_html(table_attributes=f'class="{classes}"') else: kwargs = {p: getattr(self, p) for p in self._rerender_params - if p not in HTMLBasePane.param and p != '_object'} + if p not in HTMLBasePane.param and p not in ('_object', 'text_align')} + kwargs['classes'] = classes html = obj.to_html(**kwargs) else: html = '' From ad94551cfa5fc15c637d459748aeb8da340974e2 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 6 Aug 2024 15:49:11 +0200 Subject: [PATCH 33/91] Highlight active page in Tabulator using Fast Design (#7085) --- .../tabulator-tables@6.2.1/dist/css/tabulator_fast.min.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panel/dist/bundled/datatabulator/tabulator-tables@6.2.1/dist/css/tabulator_fast.min.css b/panel/dist/bundled/datatabulator/tabulator-tables@6.2.1/dist/css/tabulator_fast.min.css index b24f7beb87..6ded229fe5 100644 --- a/panel/dist/bundled/datatabulator/tabulator-tables@6.2.1/dist/css/tabulator_fast.min.css +++ b/panel/dist/bundled/datatabulator/tabulator-tables@6.2.1/dist/css/tabulator_fast.min.css @@ -488,7 +488,7 @@ background: rgba(255, 255, 255, 0.2); } .tabulator .tabulator-footer .tabulator-page.active { - color: var(--neutral-foreground-active); + color: var(--accent-foreground-active); } .tabulator .tabulator-footer .tabulator-page:disabled { opacity: 0.5; From 9af550955f32f1d710918b9d3b8f19fa4475e5c4 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 7 Aug 2024 13:04:46 +0200 Subject: [PATCH 34/91] Ensure follow behavior works when streaming to paginated Tabulator (#7084) --- panel/models/tabulator.ts | 27 +++++++++++++++++------- panel/tests/ui/widgets/test_tabulator.py | 19 ++++++++++++++++- panel/widgets/tables.py | 10 +++++---- 3 files changed, 43 insertions(+), 13 deletions(-) diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index 84e86eed7a..563dbf4221 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -1034,15 +1034,15 @@ export class DataTabulatorView extends HTMLBoxView { } // Update table - setData(): void { + setData(): Promise { if (this._initializing || this._building || !this.tabulator.initialized) { - return + return Promise.resolve(undefined) } const data = this.getData() if (this.model.pagination != null) { - this.tabulator.rowManager.setData(data, true, false) + return this.tabulator.rowManager.setData(data, true, false) } else { - this.tabulator.setData(data) + return this.tabulator.setData(data) } } @@ -1050,9 +1050,20 @@ export class DataTabulatorView extends HTMLBoxView { const rows = this.tabulator.rowManager.getRows() const last_row = rows[rows.length-1] const start = ((last_row?.data._index) || 0) - this.setData() - if (this.model.follow && last_row) { - this.tabulator.scrollToRow(start, "top", false) + this._updating_page = true + const promise = this.setData() + if (this.model.follow) { + promise.then(() => { + if (this.model.pagination) { + this.tabulator.setPage(Math.ceil(this.tabulator.rowManager.getDataCount() / (this.model.page_size || 20))) + } + if (last_row) { + this.tabulator.scrollToRow(start, "top", false) + } + this._updating_page = false + }) + } else { + this._updating_page = true } } @@ -1099,7 +1110,7 @@ export class DataTabulatorView extends HTMLBoxView { } updatePage(pageno: number): void { - if (this.model.pagination === "local" && this.model.page !== pageno) { + if (this.model.pagination === "local" && this.model.page !== pageno && !this._updating_page) { this._updating_page = true this.model.page = pageno this._updating_page = false diff --git a/panel/tests/ui/widgets/test_tabulator.py b/panel/tests/ui/widgets/test_tabulator.py index 2ee5f6476e..7f5671a410 100644 --- a/panel/tests/ui/widgets/test_tabulator.py +++ b/panel/tests/ui/widgets/test_tabulator.py @@ -2116,7 +2116,6 @@ def test_tabulator_streaming_default(page): height_start = page.locator('.pnx-tabulator.tabulator').bounding_box()['height'] - def stream_data(): widget.stream(df) # follow is True by default @@ -2131,6 +2130,24 @@ def stream_data(): assert page.locator('.pnx-tabulator.tabulator').bounding_box()['height'] > height_start +@pytest.mark.parametrize('pagination', ['remote', 'local']) +def test_tabulator_streaming_follow_pagination(page, pagination): + df = pd.DataFrame(np.random.random((3, 2)), columns=['A', 'B']) + widget = Tabulator(df, pagination=pagination, page_size=3) + + serve_component(page, widget) + + expect(page.locator('.tabulator-row')).to_have_count(len(df)) + + widget.stream(df) + + expect(page.locator('.tabulator-page.active')).to_have_text('2') + + widget.stream(df) + + expect(page.locator('.tabulator-page.active')).to_have_text('3') + + def test_tabulator_streaming_no_follow(page): nrows1 = 10 arr = np.random.randint(10, 20, (nrows1, 2)) diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index 9aa320b3e2..d9f2ac63f9 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -1565,8 +1565,10 @@ def _stream(self, stream, rollover=None, follow=True): length = self._length nrows = self.page_size or self.initial_page_size max_page = max(length//nrows + bool(length%nrows), 1) - if self.page != max_page: + if self.page != max_page and not follow: return + self._processed, _ = self._get_data() + return super()._stream(stream, rollover) self._update_style() self._update_selectable() @@ -1575,13 +1577,13 @@ def _stream(self, stream, rollover=None, follow=True): def stream(self, stream_value, rollover=None, reset_index=True, follow=True): for ref, (model, _) in self._models.items(): self._apply_update([], {'follow': follow}, model, ref) + super().stream(stream_value, rollover, reset_index) + if follow and self.pagination: + self._update_max_page() if follow and self.pagination: length = self._length nrows = self.page_size or self.initial_page_size self.page = max(length//nrows + bool(length%nrows), 1) - super().stream(stream_value, rollover, reset_index) - if follow and self.pagination: - self._update_max_page() @updating def _patch(self, patch): From cdafab3227716ddcba9d548eb2bf5dc5d2ccf8af Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 7 Aug 2024 13:06:20 +0200 Subject: [PATCH 35/91] Bump panel.js version to 1.5.0-b.3 --- panel/package-lock.json | 12 ++++++------ panel/package.json | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/panel/package-lock.json b/panel/package-lock.json index 9eae014520..153f1ea44d 100644 --- a/panel/package-lock.json +++ b/panel/package-lock.json @@ -1,15 +1,15 @@ { "name": "@holoviz/panel", - "version": "1.5.0-b.2", + "version": "1.5.0-b.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@holoviz/panel", - "version": "1.5.0-b.2", + "version": "1.5.0-b.3", "license": "BSD-3-Clause", "dependencies": { - "@bokeh/bokehjs": "3.5.0", + "@bokeh/bokehjs": "3.5.1", "@types/debounce": "^1.2.0", "@types/gl-matrix": "^2.4.5", "ace-code": "^1.24.1", @@ -41,9 +41,9 @@ } }, "node_modules/@bokeh/bokehjs": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@bokeh/bokehjs/-/bokehjs-3.5.0.tgz", - "integrity": "sha512-9BdydcclXPmEIq8C12+d44HZZgxLooseugdHi/uo0Rd7Xgmvqs3SfjjtgpHKYCfzcI20v0XgjdMhjiwmq068hw==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@bokeh/bokehjs/-/bokehjs-3.5.1.tgz", + "integrity": "sha512-mxtmZxRppVUydble18UsFcff5HYUTsqhZMqGLVjE09eTak7cqRQXfF4v94EqwSaCwwkad6vg0kl0lF+rY9BiXw==", "workspaces": [ "./make", "./src/compiler", diff --git a/panel/package.json b/panel/package.json index bc7896f379..aa7d4efc98 100644 --- a/panel/package.json +++ b/panel/package.json @@ -1,6 +1,6 @@ { "name": "@holoviz/panel", - "version": "1.5.0-b.2", + "version": "1.5.0-b.3", "description": "The powerful data exploration & web app framework for Python.", "license": "BSD-3-Clause", "repository": { @@ -8,7 +8,7 @@ "url": "https://github.com/holoviz/panel.git" }, "dependencies": { - "@bokeh/bokehjs": "3.5.0", + "@bokeh/bokehjs": "3.5.1", "@types/debounce": "^1.2.0", "@types/gl-matrix": "^2.4.5", "ace-code": "^1.24.1", From b90b6523e13fda497e860fa35a4bd7f99cefddcb Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 7 Aug 2024 13:13:11 +0200 Subject: [PATCH 36/91] Add pytest-rerunfailures to conda test deps --- scripts/conda/recipe/meta.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/conda/recipe/meta.yaml b/scripts/conda/recipe/meta.yaml index a15d37e9b1..05098aacab 100644 --- a/scripts/conda/recipe/meta.yaml +++ b/scripts/conda/recipe/meta.yaml @@ -37,6 +37,7 @@ test: requires: - pip - pytest-asyncio + - pytest-rerunfailures - pytest-xdist commands: - pip check From 1083ed2ea2aac90cc8a4baaf8aed35a4512d29ac Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Wed, 7 Aug 2024 16:03:48 +0200 Subject: [PATCH 37/91] Enable using mypy manually (#7081) --- .github/workflows/test.yaml | 19 +++++++++++ pixi.toml | 18 +++++++++++ pyproject.toml | 64 +++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 5da38ff007..9364d0b90f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -257,3 +257,22 @@ jobs: - name: Test Unit run: | pixi run -e ${{ matrix.environment }} test-unit + + type_test_suite: + name: type:${{ matrix.environment }}:${{ matrix.os }} + needs: [pre_commit, setup, pixi_lock] + runs-on: ${{ matrix.os }} + if: needs.setup.outputs.code_change == 'true' + strategy: + fail-fast: false + matrix: + os: ["ubuntu-latest"] + environment: ["test-type"] + timeout-minutes: 120 + steps: + - uses: holoviz-dev/holoviz_tasks/pixi_install@pixi + with: + environments: ${{ matrix.environment }} + - name: Test Type + run: | + pixi run -e ${{ matrix.environment }} test-type || echo "Failed" diff --git a/pixi.toml b/pixi.toml index 005371371d..6cbdb658cb 100644 --- a/pixi.toml +++ b/pixi.toml @@ -16,6 +16,7 @@ test-311 = ["py311", "test-core", "test", "example", "test-example", "test-unit- test-312 = ["py312", "test-core", "test", "example", "test-example", "test-unit-task"] test-ui = ["py312", "test-core", "test", "test-ui"] test-core = ["py312", "test-core", "test-unit-task"] +test-type = ["py311", "type"] docs = ["py311", "example", "doc"] build = ["py311", "build"] lint = ["py311", "lint"] @@ -144,6 +145,23 @@ _install-ui = 'playwright install chromium' cmd = 'pytest panel/tests/ui --ui --browser chromium -n logical --dist loadgroup --reruns 3 --reruns-delay 10' depends_on = ["_install-ui"] +# ============================================= +# ================== TYPES ==================== +# ============================================= +[feature.type.dependencies] +mypy = "*" +pandas-stubs = "*" +types-bleach = "*" +types-croniter = "*" +types-Markdown = "*" +types-psutil = "*" +types-requests = "*" +types-tqdm = "*" +typing-extensions = "*" + +[feature.type.tasks] +test-type = 'mypy panel' + # ============================================= # =================== DOCS ==================== # ============================================= diff --git a/pyproject.toml b/pyproject.toml index 198bf321d8..a51b256a35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,6 +85,17 @@ tests = [ 'pytest-rerunfailures', 'pytest-xdist', ] +mypy = [ + "mypy", + "pandas-stubs", + "types-bleach", + "types-croniter", + "types-Markdown", + "types-psutil", + "types-requests", + "types-tqdm", + "typing-extensions", +] [project.scripts] panel = "panel.command:main" @@ -207,3 +218,56 @@ filterwarnings = [ "ignore:unclosed file <_io.TextIOWrapper name='(/dev/null|nul)' mode='w':ResourceWarning", # OK "ignore:Deprecated in traitlets 4.1, use the instance .metadata dictionary directly", # OK (ipywidgets internal) ] + +[tool.mypy] +namespace_packages = true +explicit_package_bases = true +mypy_path = "" +exclude = [] + +[[tool.mypy.overrides]] +module = [ + "altair.*", + "bokeh_django.*", + "bokeh.*", + "cachecontrol.*", + "cryptography.*", + "diskcache.*", + "flask.*", + "fsspec.*", + "holoviews.*", + "ipympl.*", + "ipywidgets_bokeh", + "ipywidgets.*", + "js.*", + "jupyter_bokeh.*", + "jupyter_server.*", + "langchain.*", + "lumen.*", + "magic.*", + "matplotlib.*", + "mdit_py_emoji.*", + "memray.*", + "myst_parser.*", + "nbconvert.*", + "nbformat.*", + "param.*", + "playwright.*", + "plotly.*", + "pydeck.*", + "pyecharts.*", + "pyinstrument.*", + "pyodide_http.*", + "pyodide.*", + "pyviz_comms.*", + "reacton.*", + "rpy2.*", + "scipy.*", + "setuptools_scm.*", + "snakeviz.*", + "streamz.*", + "textual.*", + "tranquilizer.*", + "vtk.*", +] +ignore_missing_imports = true From 1177917b070049c5f7d6b6c545eed0911919ebed Mon Sep 17 00:00:00 2001 From: Siddhartha Gandhi Date: Wed, 7 Aug 2024 10:36:06 -0400 Subject: [PATCH 38/91] Annotate pn.io.profile decorator (#7092) --- panel/io/profile.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/panel/io/profile.py b/panel/io/profile.py index e220b75f72..0d1a4f76fd 100644 --- a/panel/io/profile.py +++ b/panel/io/profile.py @@ -7,11 +7,19 @@ from contextlib import contextmanager from cProfile import Profile from functools import wraps +from typing import ( + Callable, Iterator, Literal, ParamSpec, TypeVar, +) from ..config import config from ..util import escape from .state import state +_P = ParamSpec("_P") +_R = TypeVar("_R") + +ProfilingEngine = Literal["pyinstrument", "snakeviz", "memray"] + def render_pyinstrument(sessions, timeline=False, show_all=False): from pyinstrument.renderers import HTMLRenderer @@ -183,7 +191,7 @@ def update_memray(*args): @contextmanager -def profile_ctx(engine='pyinstrument'): +def profile_ctx(engine: ProfilingEngine = 'pyinstrument') -> Iterator[list[Profile | bytes]]: """ A context manager which profiles the body of the with statement with the supplied profiling engine and returns the profiling object @@ -217,7 +225,7 @@ def profile_ctx(engine='pyinstrument'): tracker.__enter__() elif engine is None: pass - sessions = [] + sessions: list[Profile | bytes] = [] yield sessions if engine == 'pyinstrument': sessions.append(prof.stop()) @@ -230,7 +238,7 @@ def profile_ctx(engine='pyinstrument'): os.remove(tmp_file) -def profile(name, engine='pyinstrument'): +def profile(name: str, engine: ProfilingEngine = 'pyinstrument') -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: """ A decorator which may be added to any function to record profiling output. @@ -244,9 +252,9 @@ def profile(name, engine='pyinstrument'): """ if not isinstance(name, str): raise ValueError("Profiler must be given a name.") - def wrapper(func): + def wrapper(func: Callable[_P, _R]) -> Callable[_P, _R]: @wraps(func) - def wrapped(*args, **kwargs): + def wrapped(*args: _P.args, **kwargs: _P.kwargs) -> _R: if state.curdoc and state.curdoc in state._launching: return func(*args, **kwargs) with profile_ctx(engine) as sessions: From b9e661041237243494705619e20ebc6fd76513b8 Mon Sep 17 00:00:00 2001 From: Siddhartha Gandhi Date: Wed, 7 Aug 2024 11:49:51 -0400 Subject: [PATCH 39/91] Hide typing constructs behind TYPE_CHECKING guard (#7094) --- panel/io/profile.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/panel/io/profile.py b/panel/io/profile.py index 0d1a4f76fd..fb39f1d0c6 100644 --- a/panel/io/profile.py +++ b/panel/io/profile.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import io import os import re @@ -8,15 +10,16 @@ from cProfile import Profile from functools import wraps from typing import ( - Callable, Iterator, Literal, ParamSpec, TypeVar, + TYPE_CHECKING, Callable, Iterator, Literal, ParamSpec, TypeVar, ) from ..config import config from ..util import escape from .state import state -_P = ParamSpec("_P") -_R = TypeVar("_R") +if TYPE_CHECKING: + _P = ParamSpec("_P") + _R = TypeVar("_R") ProfilingEngine = Literal["pyinstrument", "snakeviz", "memray"] From 4c4925180b33b8ea3cf764e4b10e8d7b2d407a81 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 7 Aug 2024 21:10:34 +0200 Subject: [PATCH 40/91] Avoid events boomeranging from frontend (#7093) --- panel/reactive.py | 55 +++++++++++++++++++++++++++++------- panel/tests/test_param.py | 6 ++-- panel/tests/test_reactive.py | 14 +++++++++ 3 files changed, 62 insertions(+), 13 deletions(-) diff --git a/panel/reactive.py b/panel/reactive.py index 626c2e5835..7558cb3750 100644 --- a/panel/reactive.py +++ b/panel/reactive.py @@ -138,6 +138,9 @@ def __init__(self, **params): # A dictionary of bokeh property changes being processed self._changing = {} + # A dictionary of parameter changes being processed + self._in_process__events = {} + # Whether the component is watching the stylesheets self._watching_stylesheets = False @@ -304,21 +307,36 @@ def _update_manual(self, *events: param.parameterized.Event) -> None: else: cb() + def _scheduled_update_model( + self, events: dict[str, param.parameterized.Event], msg: dict[str, Any], + root: Model, model: Model, doc: Document, comm: Optional[Comm], + curdoc_events: dict[str, Any] + ) -> None: + # + self._in_process__events[doc] = curdoc_events + try: + self._update_model(events, msg, root, model, doc, comm) + finally: + del self._in_process__events[doc] + def _apply_update( self, events: dict[str, param.parameterized.Event], msg: dict[str, Any], model: Model, ref: str - ) -> None: + ) -> bool: if ref not in state._views or ref in state._fake_roots: - return + return True viewable, root, doc, comm = state._views[ref] if comm or not doc.session_context or state._unblocked(doc): with unlocked(): self._update_model(events, msg, root, model, doc, comm) if comm and 'embedded' not in root.tags: push(doc, comm) + return True else: - cb = partial(self._update_model, events, msg, root, model, doc, comm) + curdoc_events = self._in_process__events.pop(doc, {}) + cb = partial(self._scheduled_update_model, events, msg, root, model, doc, comm, curdoc_events) doc.add_next_tick_callback(cb) + return False def _update_model( self, events: dict[str, param.parameterized.Event], msg: dict[str, Any], @@ -326,7 +344,20 @@ def _update_model( ) -> None: ref = root.ref['id'] self._changing[ref] = attrs = [] - for attr, value in msg.items(): + curdoc_events = self._in_process__events.get(doc, {}) + for attr, value in msg.copy().items(): + if attr in curdoc_events and value is curdoc_events[attr]: + # Do not apply change that originated directly from + # the frontend since this may cause boomerang if a + # new property value is already in-flight + del msg[attr] + continue + elif attr in self._events: + # Do not override a property value that was just changed + # on the frontend + del self._events[attr] + continue + # Bokeh raises UnsetValueError if the value is Undefined. try: model_val = getattr(model, attr) @@ -336,10 +367,6 @@ def _update_model( if not model.lookup(attr).property.matches(model_val, value): attrs.append(attr) - # Do not apply model change that is in flight - if attr in self._events: - del self._events[attr] - try: model.update(**msg) finally: @@ -405,18 +432,26 @@ async def _watch_stylesheets(self): def _param_change(self, *events: param.parameterized.Event) -> None: named_events = {event.name: event for event in events} + applied = False for ref, (model, _) in self._models.copy().items(): properties = self._update_properties(*events, doc=model.document) if not properties: return - self._apply_update(named_events, properties, model, ref) + applied &= self._apply_update(named_events, properties, model, ref) + if ref not in state._views: + continue + doc = state._views[ref][2] + if applied and doc in self._in_process__events: + del self._in_process__events[doc] def _process_events(self, events: dict[str, Any]) -> None: self._log('received events %s', events) if any(e for e in events if e not in self._busy__ignore): with edit_readonly(state): state._busy_counter += 1 - params = self._process_property_change(dict(events)) + if events: + self._in_process__events[state.curdoc] = events + params = self._process_property_change(events) try: with edit_readonly(self): self_params = {k: v for k, v in params.items() if '.' not in k} diff --git a/panel/tests/test_param.py b/panel/tests/test_param.py index e3801ec98d..7ea286e55d 100644 --- a/panel/tests/test_param.py +++ b/panel/tests/test_param.py @@ -574,7 +574,7 @@ class Test(param.Parameterized): assert mb.value != 3 assert test.b == 3 - test_pane._widgets['b']._process_events({'value': 4}) + mb.value = 4 assert test.b == 3 assert mb.value == 4 @@ -616,7 +616,7 @@ class Test(param.Parameterized): assert mb.value != '3' assert test.b == '3' - test_pane._widgets['b']._process_events({'value': '4'}) + mb.value = '4' assert test.b == '3' assert mb.value == '4' @@ -873,7 +873,7 @@ class Test(param.Parameterized): assert number.value != 3 assert test.a == 3 - pane._widgets['a']._process_events({'value': 4}) + number.value = 4 assert test.a == 3 assert number.value == 4 diff --git a/panel/tests/test_reactive.py b/panel/tests/test_reactive.py index 232a86e491..9d1bc6f4f9 100644 --- a/panel/tests/test_reactive.py +++ b/panel/tests/test_reactive.py @@ -15,6 +15,7 @@ from bokeh.models import Div from panel.depends import bind, depends +from panel.io.state import set_curdoc from panel.layout import Tabs, WidgetBox from panel.pane import Markdown from panel.reactive import Reactive, ReactiveHTML @@ -370,6 +371,19 @@ def test_text_input_controls_explicit(): text_input.placeholder = "Test placeholder..." assert placeholder.value == "Test placeholder..." +def test_property_change_does_not_boomerang(document, comm): + text_input = TextInput(value='A') + + model = text_input.get_root(document, comm) + + assert model.value == 'A' + model.value = 'B' + with set_curdoc(document): + text_input._process_events({'value': 'C'}) + + assert model.value == 'B' + assert text_input.value == 'C' + def test_reactive_html_basic(): class Test(ReactiveHTML): From 23a1feea249d857bd581e2f638a49f6efa6d029f Mon Sep 17 00:00:00 2001 From: Coderambling <159031875+Coderambling@users.noreply.github.com> Date: Thu, 8 Aug 2024 10:02:32 +0200 Subject: [PATCH 41/91] Update Tabulator.ipynb to show correct version number of Tabulator (#7053) --- examples/reference/widgets/Tabulator.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/reference/widgets/Tabulator.ipynb b/examples/reference/widgets/Tabulator.ipynb index 403ba414fe..206f252fbe 100644 --- a/examples/reference/widgets/Tabulator.ipynb +++ b/examples/reference/widgets/Tabulator.ipynb @@ -19,7 +19,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The `Tabulator` widget allows displaying and editing a pandas DataFrame. The `Tabulator` is a largely backward compatible replacement for the [`DataFrame`](./DataFrame.ipynb) widget and will eventually replace it. It is built on the **version 5.5** of the [Tabulator](http://tabulator.info/) library, which provides for a wide range of features.\n", + "The `Tabulator` widget allows displaying and editing a pandas DataFrame. The `Tabulator` is a largely backward compatible replacement for the [`DataFrame`](./DataFrame.ipynb) widget and will eventually replace it. It is built on the **version {{TABULATOR_VERSION}}** of the [Tabulator](http://tabulator.info/) library, which provides for a wide range of features.\n", "\n", "Discover more on using widgets to add interactivity to your applications in the [how-to guides on interactivity](../how_to/interactivity/index.md). Alternatively, learn [how to set up callbacks and (JS-)links between parameters](../../how_to/links/index.md) or [how to use them as part of declarative UIs with Param](../../how_to/param/index.html).\n", "\n", From 86c7690f233b066512ad2feca96e71cd4bfbd19c Mon Sep 17 00:00:00 2001 From: Coderambling <159031875+Coderambling@users.noreply.github.com> Date: Thu, 8 Aug 2024 10:02:48 +0200 Subject: [PATCH 42/91] Update conf.py to retrieve tabulator version dynamically (#7062) --- doc/conf.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/conf.py b/doc/conf.py index b9e86a0a1c..ed41e65fe8 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -236,12 +236,15 @@ def _get_pyodide_version(): raise NotImplementedError(F"{PYODIDE_VERSION=} is not valid") def update_versions(app, docname, source): + from panel.models.tabulator import TABULATOR_VERSION + # Inspired by: https://stackoverflow.com/questions/8821511 version_replace = { "{{PANEL_VERSION}}" : PY_VERSION, "{{BOKEH_VERSION}}" : BOKEH_VERSION, "{{PYSCRIPT_VERSION}}" : PYSCRIPT_VERSION, "{{PYODIDE_VERSION}}" : _get_pyodide_version(), + "{{TABULATOR_VERSION}}" : TABULATOR_VERSION, } for old, new in version_replace.items(): From 811d79498e230bb498647066fe16ef42455e2655 Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Thu, 8 Aug 2024 01:03:48 -0700 Subject: [PATCH 43/91] Display value for player (#7060) --- .../reference/widgets/DiscretePlayer.ipynb | 5 +- examples/reference/widgets/Player.ipynb | 5 +- panel/dist/css/player.css | 3 + panel/models/discrete_player.ts | 46 +++++++++++ panel/models/index.ts | 1 + panel/models/player.ts | 78 ++++++++++++++++++- panel/models/widgets.py | 15 ++++ panel/tests/ui/widgets/test_player.py | 55 +++++++++++++ panel/widgets/player.py | 26 +++++-- 9 files changed, 223 insertions(+), 11 deletions(-) create mode 100644 panel/dist/css/player.css create mode 100644 panel/models/discrete_player.ts create mode 100644 panel/tests/ui/widgets/test_player.py diff --git a/examples/reference/widgets/DiscretePlayer.ipynb b/examples/reference/widgets/DiscretePlayer.ipynb index 9323014579..bcc189b78b 100644 --- a/examples/reference/widgets/DiscretePlayer.ipynb +++ b/examples/reference/widgets/DiscretePlayer.ipynb @@ -38,6 +38,8 @@ "* **``disabled``** (boolean): Whether the widget is editable\n", "* **``name``** (str): The title of the widget\n", "* **``show_loop_controls``** (boolean): Whether radio buttons allowing to switch between loop policies options are shown\n", + "* **``show_value``** (boolean): Whether to display the value of the player\n", + "* **``value_align``** (str): Where to display the value; must be one of 'start', 'center', 'end'\n", "\n", "___" ] @@ -55,7 +57,8 @@ "metadata": {}, "outputs": [], "source": [ - "discrete_player = pn.widgets.DiscretePlayer(name='Discrete Player', options=[2, 4, 8, 16, 32, 64, 128], value=8, loop_policy='loop')\n", + "discrete_player = pn.widgets.DiscretePlayer(name='Discrete Player', options=[2, 4, 8, 16, 32, 64, 128],\n", + " value=8, loop_policy='loop', show_value=True, value_align='start')\n", "\n", "discrete_player" ] diff --git a/examples/reference/widgets/Player.ipynb b/examples/reference/widgets/Player.ipynb index 238b852a96..d210a01038 100644 --- a/examples/reference/widgets/Player.ipynb +++ b/examples/reference/widgets/Player.ipynb @@ -40,6 +40,8 @@ "* **``disabled``** (boolean): Whether the widget is editable\n", "* **``name``** (str): The title of the widget\n", "* **``show_loop_controls``** (boolean): Whether radio buttons allowing to switch between loop policies options are shown\n", + "* **``show_value``** (boolean): Whether to display the value of the player\n", + "* **``value_align``** (str): Where to display the value; must be one of 'start', 'center', 'end'\n", "\n", "___" ] @@ -57,7 +59,8 @@ "metadata": {}, "outputs": [], "source": [ - "player = pn.widgets.Player(name='Player', start=0, end=100, value=32, loop_policy='loop')\n", + "player = pn.widgets.Player(name='Player', start=0, end=100, value=32, loop_policy='loop',\n", + " show_value=True, value_align='start')\n", "\n", "player" ] diff --git a/panel/dist/css/player.css b/panel/dist/css/player.css new file mode 100644 index 0000000000..1b0bb61ad5 --- /dev/null +++ b/panel/dist/css/player.css @@ -0,0 +1,3 @@ +.pn-player-value { + font-weight: bold; +} diff --git a/panel/models/discrete_player.ts b/panel/models/discrete_player.ts new file mode 100644 index 0000000000..97cb31b3d1 --- /dev/null +++ b/panel/models/discrete_player.ts @@ -0,0 +1,46 @@ +import type * as p from "@bokehjs/core/properties" +import {PlayerView, Player} from "./player" +import {span} from "@bokehjs/core/dom" +import {to_string} from "@bokehjs/core/util/pretty" + +export class DiscretePlayerView extends PlayerView { + declare model: DiscretePlayer + + override append_value_to_title_el(): void { + let label = this.model.options[this.model.value] + if (typeof label !== "string") { + label = to_string(label) + } + this.titleEl.appendChild(span({class: "pn-player-value"}, label)) + } +} + +export namespace DiscretePlayer { + export type Attrs = p.AttrsOf + export type Props = Player.Props & { + options: p.Property + } +} + +export interface DiscretePlayer extends DiscretePlayer.Attrs { } + +export class DiscretePlayer extends Player { + + declare properties: DiscretePlayer.Props + + constructor(attrs?: Partial) { + super(attrs) + } + + static override __module__ = "panel.models.widgets" + + static { + this.prototype.default_view = DiscretePlayerView + + this.define(({List, Any}) => ({ + options: [List(Any), []], + })) + + this.override({width: 400}) + } +} diff --git a/panel/models/index.ts b/panel/models/index.ts index f6e656899c..ba9cf6e1fd 100644 --- a/panel/models/index.ts +++ b/panel/models/index.ts @@ -15,6 +15,7 @@ export {CustomMultiSelect} from "./multiselect" export {DataTabulator} from "./tabulator" export {DatetimePicker} from "./datetime_picker" export {DeckGLPlot} from "./deckgl" +export {DiscretePlayer} from "./discrete_player" export {ECharts} from "./echarts" export {Feed} from "./feed" export {FileDownload} from "./file_download" diff --git a/panel/models/player.ts b/panel/models/player.ts index 94eb0435c6..71d876ce3a 100644 --- a/panel/models/player.ts +++ b/panel/models/player.ts @@ -1,7 +1,8 @@ import {Enum} from "@bokehjs/core/kinds" import type * as p from "@bokehjs/core/properties" -import {div} from "@bokehjs/core/dom" +import {div, empty, span} from "@bokehjs/core/dom" import {Widget, WidgetView} from "@bokehjs/models/widgets/widget" +import {to_string} from "@bokehjs/core/util/pretty" const SVG_STRINGS = { slower: ' this.update_title_and_value()) + this.on_change(value_align, () => this.set_value_align()) this.on_change(direction, () => this.set_direction()) this.on_change(value, () => this.render()) this.on_change(loop_policy, () => this.set_loop_state(this.model.loop_policy)) @@ -92,6 +96,8 @@ export class PlayerView extends WidgetView { this.groupEl.removeChild(this.loop_state) } }) + this.on_change(show_value, () => this.update_title_and_value()) + } toggle_disable() { @@ -127,7 +133,13 @@ export class PlayerView extends WidgetView { this.groupEl = div() this.groupEl.style.display = "flex" this.groupEl.style.flexDirection = "column" - this.groupEl.style.alignItems = "center" + + // Display Value + this.titleEl = div() + this.titleEl.classList.add("pn-player-title") + this.titleEl.style.padding = "0 5px 0 5px" + this.update_title_and_value() + this.set_value_align() // Slider this.sliderEl = document.createElement("input") @@ -263,6 +275,7 @@ export class PlayerView extends WidgetView { this.loop_state.appendChild(reflect) this.loop_state.appendChild(reflect_label) + this.groupEl.appendChild(this.titleEl) this.groupEl.appendChild(this.sliderEl) this.groupEl.appendChild(button_div) if (this.model.show_loop_controls) { @@ -275,6 +288,7 @@ export class PlayerView extends WidgetView { set_frame(frame: number, throttled: boolean = true): void { this.model.value = frame + this.update_title_and_value() if (throttled) { this.model.value_throttled = frame } @@ -294,6 +308,56 @@ export class PlayerView extends WidgetView { return "once" } + update_title_and_value(): void { + empty(this.titleEl) + + const hide_header = this.model.title == null || (this.model.title.length == 0 && !this.model.show_value) + this.titleEl.style.display = hide_header ? "none" : "" + + if (!hide_header) { + this.titleEl.style.visibility = "visible" + const {title} = this.model + if (title != null && title.length > 0) { + if (this.contains_tex_string(title)) { + this.titleEl.innerHTML = `${this.process_tex(title)}` + if (this.model.show_value) { + this.titleEl.innerHTML += ": " + } + } else { + this.titleEl.textContent = `${title}` + if (this.model.show_value) { + this.titleEl.textContent += ": " + } + } + } + + if (this.model.show_value) { + this.append_value_to_title_el() + } + } else { + this.titleEl.style.visibility = "hidden" + } + } + + append_value_to_title_el(): void { + this.titleEl.appendChild(span({class: "pn-player-value"}, to_string(this.model.value))) + } + + set_value_align(): void { + switch (this.model.value_align) { + case "start": + this.titleEl.style.textAlign = "left" + break + case "center": + this.titleEl.style.textAlign = "center" + break + case "end": + this.titleEl.style.textAlign = "right" + console.log(this.titleEl) + break + } + } + set_loop_state(state: string): void { const button_group = this.loop_state.state for (let i = 0; i < button_group.length; i++) { @@ -429,9 +493,12 @@ export namespace Player { end: p.Property step: p.Property loop_policy: p.Property + title: p.Property value: p.Property + value_align: p.Property value_throttled: p.Property show_loop_controls: p.Property + show_value: p.Property } } @@ -450,16 +517,19 @@ export class Player extends Widget { static { this.prototype.default_view = PlayerView - this.define(({Bool, Int}) => ({ + this.define(({Bool, Int, Str}) => ({ direction: [Int, 0], interval: [Int, 500], start: [Int, 0], end: [Int, 10], step: [Int, 1], loop_policy: [LoopPolicy, "once"], + title: [Str, ""], value: [Int, 0], + value_align: [Str, "start"], value_throttled: [Int, 0], show_loop_controls: [Bool, true], + show_value: [Bool, true], })) this.override({width: 400}) diff --git a/panel/models/widgets.py b/panel/models/widgets.py index d35d9c298f..8504fcddda 100644 --- a/panel/models/widgets.py +++ b/panel/models/widgets.py @@ -31,6 +31,9 @@ class Player(Widget): """ The Player widget provides controls to play through a number of frames. """ + title = Nullable(String, default="", help=""" + The slider's label (supports :ref:`math text `). + """) start = Int(0, help="Lower bound of the Player slider") @@ -40,6 +43,9 @@ class Player(Widget): value_throttled = Int(0, help="Current throttled value of the player app") + value_align = String("start", help="""Location to display + the value of the slider ("start" "center", "end")""") + step = Int(1, help="Number of steps to advance the player by.") interval = Int(500, help="Interval between updates") @@ -53,11 +59,20 @@ class Player(Widget): show_loop_controls = Bool(True, help="""Whether the loop controls radio buttons are shown""") + show_value = Bool(True, help=""" + Whether to show the widget value""") + width = Override(default=400) height = Override(default=250) +class DiscretePlayer(Player): + + options = List(Any, help=""" + List of discrete options.""") + + class SingleSelect(InputWidget): ''' Single-select widget. diff --git a/panel/tests/ui/widgets/test_player.py b/panel/tests/ui/widgets/test_player.py new file mode 100644 index 0000000000..fde255d6a8 --- /dev/null +++ b/panel/tests/ui/widgets/test_player.py @@ -0,0 +1,55 @@ +import pytest + +pytest.importorskip("playwright") + +from playwright.sync_api import expect + +from panel.tests.util import serve_component, wait_until +from panel.widgets import Player + +pytestmark = pytest.mark.ui + + +def test_init(page): + player = Player() + serve_component(page, player) + + assert not page.is_visible('pn-player-value') + assert page.query_selector('.pn-player-value') is None + +def test_show_value(page): + player = Player(show_value=True) + serve_component(page, player) + + wait_until(lambda: page.query_selector('.pn-player-value') is not None) + assert page.query_selector('.pn-player-value') is not None + + +def test_name(page): + player = Player(name='test') + serve_component(page, player) + + assert page.is_visible('label') + assert page.query_selector('.pn-player-value') is None + + name = page.locator('.pn-player-title:has-text("test")') + expect(name).to_have_count(1) + + +def test_value_align(page): + player = Player(name='test', value_align='end') + serve_component(page, player) + + name = page.locator('.pn-player-title:has-text("test")') + expect(name).to_have_css("text-align", "right") + + +def test_name_and_show_value(page): + player = Player(name='test', show_value=True) + serve_component(page, player) + + assert page.is_visible('label') + assert page.query_selector('.pn-player-value') is not None + + name = page.locator('.pn-player-title:has-text("test")') + expect(name).to_have_count(1) diff --git a/panel/widgets/player.py b/panel/widgets/player.py index 817ead241e..565d6489da 100644 --- a/panel/widgets/player.py +++ b/panel/widgets/player.py @@ -8,7 +8,10 @@ import param from ..config import config -from ..models.widgets import Player as _BkPlayer +from ..io.resources import CDN_DIST +from ..models.widgets import ( + DiscretePlayer as _BkDiscretePlayer, Player as _BkPlayer, +) from ..util import indexOf, isIn from .base import Widget from .select import SelectBase @@ -34,19 +37,29 @@ class PlayerBase(Widget): show_loop_controls = param.Boolean(default=True, doc=""" Whether the loop controls radio buttons are shown""") + show_value = param.Boolean(default=False, doc=""" + Whether to show the widget value""") + step = param.Integer(default=1, doc=""" Number of frames to step forward and back by on each event.""") height = param.Integer(default=80) + value_align = param.ObjectSelector( + objects=["start", "center", "end"], doc=""" + Location to display the value of the slider + ("start", "center", "end")""") + width = param.Integer(default=510, allow_None=True, doc=""" Width of this component. If sizing_mode is set to stretch or scale mode this will merely be used as a suggestion.""") - _rename: ClassVar[Mapping[str, str | None]] = {'name': None} + _rename: ClassVar[Mapping[str, str | None]] = {'name': 'title'} _widget_type: ClassVar[type[Model]] = _BkPlayer + _stylesheets: ClassVar[list[str]] = [f"{CDN_DIST}css/player.css"] + __abstract = True def __init__(self, **params): @@ -76,7 +89,7 @@ class Player(PlayerBase): :Example: - >>> Player(name='Player', start=0, end=100, value=32, loop_policy='loop') + >>> Player(name='Player', start=0, end=100, value=32, loop_policy='loop', value_align='top_center') """ start = param.Integer(default=0, doc="Lower bound on the slider value") @@ -130,7 +143,8 @@ class DiscretePlayer(PlayerBase, SelectBase): >>> DiscretePlayer( ... name='Discrete Player', ... options=[2, 4, 8, 16, 32, 64, 128], value=32, - ... loop_policy='loop' + ... loop_policy='loop', + ... value_align='start' ... ) """ @@ -140,10 +154,12 @@ class DiscretePlayer(PlayerBase, SelectBase): value_throttled = param.Parameter(constant=True, doc="Current player value") - _rename: ClassVar[Mapping[str, str | None]] = {'name': None, 'options': None} + _rename: ClassVar[Mapping[str, str | None]] = {'name': 'title'} _source_transforms: ClassVar[Mapping[str, str | None]] = {'value': None, 'value_throttled': None} + _widget_type: ClassVar[type[Model]] = _BkDiscretePlayer + def _process_param_change(self, msg): values = self.values if 'options' in msg: From 3a7f440bebbf10bf62cbd55c35cf20628f984b8a Mon Sep 17 00:00:00 2001 From: Siddhartha Gandhi Date: Thu, 8 Aug 2024 04:04:04 -0400 Subject: [PATCH 44/91] Annotate pn.io.cache decorator (#7079) --- panel/io/cache.py | 84 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 17 deletions(-) diff --git a/panel/io/cache.py b/panel/io/cache.py index c02556ec0d..54b3b6d3be 100644 --- a/panel/io/cache.py +++ b/panel/io/cache.py @@ -1,6 +1,8 @@ """ Implements memoization for functions with arbitrary arguments """ +from __future__ import annotations + import datetime as dt import functools import hashlib @@ -17,6 +19,10 @@ import weakref from contextlib import contextmanager +from typing import ( + TYPE_CHECKING, Any, Callable, Hashable, Literal, ParamSpec, Protocol, + TypeVar, overload, +) import param @@ -28,11 +34,22 @@ # Private API #--------------------------------------------------------------------- +if TYPE_CHECKING: + _P = ParamSpec("_P") + _R = TypeVar("_R") + _CallableT = TypeVar("_CallableT", bound=Callable) + + class _CachedFunc(Protocol[_CallableT]): + def clear(self, func_hashes: list[str | None]) -> None: + pass + + __call__: _CallableT + _CYCLE_PLACEHOLDER = b"panel-93KZ39Q-floatingdangeroushomechose-CYCLE" _FFI_TYPE_NAMES = ("_cffi_backend.FFI", "builtins.CompiledFFI",) -_HASH_MAP = {} +_HASH_MAP: dict[Hashable, str] = {} _HASH_STACKS = weakref.WeakKeyDictionary() @@ -305,11 +322,42 @@ def compute_hash(func, hash_funcs, args, kwargs): _HASH_MAP[key] = hash_value return hash_value +@overload +def cache( + func: Literal[None] = ..., + hash_funcs: dict[type[Any], Callable[[Any], bytes]] | None = ..., + max_items: int | None = ..., + policy: Literal['FIFO', 'LRU', 'LFU'] = ..., + ttl: float | None = ..., + to_disk: bool = ..., + cache_path: str | os.PathLike = ..., + per_session: bool = ..., +) -> Callable[[Callable[_P, _R]], _CachedFunc[Callable[_P, _R]]]: + ... + +@overload +def cache( + func: Callable[_P, _R], + hash_funcs: dict[type[Any], Callable[[Any], bytes]] | None = ..., + max_items: int | None = ..., + policy: Literal['FIFO', 'LRU', 'LFU'] = ..., + ttl: float | None = ..., + to_disk: bool = ..., + cache_path: str | os.PathLike = ..., + per_session: bool = ..., +) -> _CachedFunc[Callable[_P, _R]]: + ... def cache( - func=None, hash_funcs=None, max_items=None, policy='LRU', - ttl=None, to_disk=False, cache_path='./cache', per_session=False -): + func: Callable[_P, _R] | None = None, + hash_funcs: dict[type[Any], Callable[[Any], bytes]] | None = None, + max_items: int | None = None, + policy: Literal['FIFO', 'LRU', 'LFU'] = 'LRU', + ttl: float | None = None, + to_disk: bool = False, + cache_path: str | os.PathLike = './cache', + per_session: bool = False +) -> _CachedFunc[Callable[_P, _R]] | Callable[[Callable[_P, _R]], _CachedFunc[Callable[_P, _R]]]: """ Memoizes functions for a user session. Can be used as function annotation or just directly. @@ -336,7 +384,7 @@ def cache( the cache should not expire. The default is None. to_disk: bool Whether to cache to disk using diskcache. - cache_dir: str + cache_path: str Directory to cache to on disk. per_session: bool Whether to cache data only for the current session. @@ -348,15 +396,17 @@ def cache( hash_funcs = hash_funcs or {} if func is None: - return lambda f: cache( - func=f, - hash_funcs=hash_funcs, - max_items=max_items, - ttl=ttl, - to_disk=to_disk, - cache_path=cache_path, - per_session=per_session, - ) + def decorator(func: Callable[_P, _R]) -> _CachedFunc[Callable[_P, _R]]: + return cache( + func=func, + hash_funcs=hash_funcs, + max_items=max_items, + ttl=ttl, + to_disk=to_disk, + cache_path=cache_path, + per_session=per_session, + ) + return decorator func_hashes = [None] # noqa lock = threading.RLock() @@ -414,7 +464,7 @@ def hash_func(*args, **kwargs): if iscoroutinefunction(func): @functools.wraps(func) - async def wrapped_func(*args, **kwargs): + async def wrapped_func(*args: _P.args, **kwargs: _P.kwargs) -> _R: func_cache, hash_value, time = hash_func(*args, **kwargs) if hash_value in func_cache: with lock: @@ -427,7 +477,7 @@ async def wrapped_func(*args, **kwargs): return ret else: @functools.wraps(func) - def wrapped_func(*args, **kwargs): + def wrapped_func(*args: _P.args, **kwargs: _P.kwargs) -> _R: func_cache, hash_value, time = hash_func(*args, **kwargs) if hash_value in func_cache: with lock: @@ -452,7 +502,7 @@ def clear(func_hashes=func_hashes): cache = state._memoize_cache.get(func_hash, {}) cache.clear() - wrapped_func.clear = clear + wrapped_func.clear = clear # type: ignore[attr-defined] if per_session and state.curdoc and state.curdoc.session_context: def server_clear(session_context, clear=clear): From c255c6ddb65c26a4a1e4fd3d7c2151f910053f5b Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 8 Aug 2024 10:49:27 +0200 Subject: [PATCH 45/91] Optimize rendering and scrolling behavior of Feed (#7101) --- panel/models/feed.ts | 67 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/panel/models/feed.ts b/panel/models/feed.ts index 9767779c8e..51a5194b6d 100644 --- a/panel/models/feed.ts +++ b/panel/models/feed.ts @@ -30,6 +30,8 @@ export class FeedView extends ColumnView { _last_visible: UIElementView | null _rendered: boolean = false _sync: boolean + _reference: number | null = null + _reference_view: UIElementView | null = null override initialize(): void { super.initialize() @@ -87,17 +89,64 @@ export class FeedView extends ColumnView { override async update_children(): Promise { const last = this._last_visible const scroll_top = this.el.scrollTop - const before_offset = last?.el.offsetTop || 0 + this._reference_view = last + this._reference = last?.el.offsetTop || 0 this._sync = false - await super.update_children() - this._sync = true - requestAnimationFrame(() => { - const after_offset = last?.el.offsetTop || 0 - const offset = (after_offset-before_offset) - if (offset > 0) { - this.el.scrollTo({top: scroll_top + offset, behavior: "instant"}) + const created = await this.build_child_views() + const created_children = new Set(created) + const createdLength = created.length + const views_length = this.child_views.length + + // Check whether we simply have to prepend or append items + // instead of removing and reordering them + const is_prepended = created.every((view, index) => view === this.child_views[index]) + const is_appended = created.every((view, index) => view === this.child_views[views_length - createdLength + index]) + const reorder = !(is_prepended || is_appended) + if (reorder) { + // First remove and then either reattach existing elements or render and + // attach new elements, so that the order of children is consistent, while + // avoiding expensive re-rendering of existing views. + for (const child_view of this.child_views) { + child_view.el.remove() } - }) + } + const prepend: Element[] = [] + for (const child_view of this.child_views) { + const is_new = created_children.has(child_view) + const target = this.shadow_el + if (reorder) { + if (is_new) { + child_view.render_to(target) + } else { + target.append(child_view.el) + } + } else { + if (is_new) { + child_view.render() + if (is_appended) { + target.append(child_view.el) + } else if (is_prepended) { + prepend.push(child_view.el) + } + } + } + } + if (is_prepended) { + this.shadow_el.prepend(...prepend) + } + this.r_after_render() + this._update_children() + this.invalidate_layout() + this._sync = true + + // Ensure we adjust the scroll position in case we prepended items + if (is_prepended) { + requestAnimationFrame(() => { + const after_offset = this._reference_view?.el.offsetTop || 0 + const offset = (after_offset-(this._reference || 0)) + this.el.scrollTo({top: scroll_top + offset, behavior: "smooth"}) + }) + } } override async build_child_views(): Promise { From 01f0fba37014977b66b9f7b059872631397ad7be Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 8 Aug 2024 23:21:36 +0200 Subject: [PATCH 46/91] Correctly map Tabulator expanded indexes when paginated, filtered and sorted (#7103) --- panel/models/tabulator.ts | 21 +++-- panel/tests/widgets/test_tables.py | 94 +++++++++++++++++++++- panel/widgets/tables.py | 124 +++++++++++++++++------------ 3 files changed, 179 insertions(+), 60 deletions(-) diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index 563dbf4221..17736da80b 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -365,7 +365,7 @@ export class DataTabulatorView extends HTMLBoxView { for (const row of this.tabulator.rowManager.getRows()) { if (row.cells.length > 0) { const index = row.data._index - const icon = this.model.expanded.indexOf(index) < 0 ? "►" : "▼" + const icon = this.model.expanded.includes(index) ? "▼" : "►" row.cells[1].element.innerText = icon } } @@ -741,10 +741,17 @@ export class DataTabulatorView extends HTMLBoxView { const new_children = await this.build_child_views() resolve(new_children) }).then((new_children) => { - for (const r of this.model.expanded) { - const row = this.tabulator.getRow(r) + const rows = this.tabulator.getRows() + const lookup = new Map() + for (const row of rows) { const index = row._row?.data._index - if (this.model.children.get(index) == null) { + if (index != null) { + lookup.set(index, row) + } + } + for (const index of this.model.expanded) { + const row = lookup.get(index) + if (!this.model.children.has(index)) { continue } const model = this.model.children.get(index) @@ -798,10 +805,10 @@ export class DataTabulatorView extends HTMLBoxView { _update_expand(cell: any): void { const index = cell._cell.row.data._index const expanded = [...this.model.expanded] - const exp_index = expanded.indexOf(index) - if (exp_index < 0) { + if (!expanded.includes(index)) { expanded.push(index) } else { + const exp_index = expanded.indexOf(index) const removed = expanded.splice(exp_index, 1)[0] const model = this.model.children.get(removed) if (model != null) { @@ -812,7 +819,7 @@ export class DataTabulatorView extends HTMLBoxView { } } this.model.expanded = expanded - if (expanded.indexOf(index) < 0) { + if (!expanded.includes(index)) { return } let ready = true diff --git a/panel/tests/widgets/test_tables.py b/panel/tests/widgets/test_tables.py index b716522f21..b0d60f6933 100644 --- a/panel/tests/widgets/test_tables.py +++ b/panel/tests/widgets/test_tables.py @@ -339,6 +339,99 @@ def test_tabulator_expanded_content(document, comm): assert row2.text == "<pre>2.0</pre>" +def test_tabulator_remote_paginated_expanded_content(document, comm): + df = makeMixedDataFrame() + + table = Tabulator( + df, expanded=[0, 4], row_content=lambda r: r.A, pagination='remote', page_size=3 + ) + + model = table.get_root(document, comm) + + assert len(model.children) == 1 + assert 0 in model.children + row0 = model.children[0] + assert row0.text == "<pre>0.0</pre>" + + table.page = 2 + + assert len(model.children) == 1 + assert 1 in model.children + row1 = model.children[1] + assert row1.text == "<pre>4.0</pre>" + + +def test_tabulator_remote_sorted_paginated_expanded_content(document, comm): + df = makeMixedDataFrame() + + table = Tabulator( + df, expanded=[0, 1], row_content=lambda r: r.A, pagination='remote', page_size=2, + sorters = [{'field': 'A', 'sorter': 'number', 'dir': 'desc'}], page=3 + ) + + model = table.get_root(document, comm) + + assert len(model.children) == 1 + assert 0 in model.children + row0 = model.children[0] + assert row0.text == "<pre>0.0</pre>" + + table.page = 2 + + assert len(model.children) == 1 + assert 1 in model.children + row1 = model.children[1] + assert row1.text == "<pre>1.0</pre>" + + table.expanded = [0, 1, 2] + + assert len(model.children) == 2 + assert 0 in model.children + row0 = model.children[0] + assert row0.text == "<pre>2.0</pre>" + + +@pytest.mark.parametrize('pagination', ['local', 'remote', None]) +def test_tabulator_filtered_expanded_content(document, comm, pagination): + df = makeMixedDataFrame() + + table = Tabulator( + df, + expanded=[0, 1, 2, 3], + filters=[{'field': 'B', 'sorter': 'number', 'type': '=', 'value': '1.0'}], + pagination=pagination, + row_content=lambda r: r.A, + ) + + model = table.get_root(document, comm) + + assert len(model.children) == 2 + + assert 0 in model.children + row0 = model.children[0] + assert row0.text == "<pre>1.0</pre>" + + assert 1 in model.children + row1 = model.children[1] + assert row1.text == "<pre>3.0</pre>" + + model.expanded = [0] + assert table.expanded == [1] + + table.filters = [{'field': 'B', 'sorter': 'number', 'type': '=', 'value': '0'}] + + assert not model.expanded + assert table.expanded == [1] + + table.expanded = [0, 1] + + assert len(model.children) == 1 + + assert 0 in model.children + row0 = model.children[0] + assert row0.text == "<pre>0.0</pre>" + + def test_tabulator_index_column(document, comm): df = pd.DataFrame({ 'int': [1, 2, 3], @@ -912,7 +1005,6 @@ def test_tabulator_empty_table(document, comm): assert table.value.shape == value_df.shape - def test_tabulator_sorters_unnamed_index(document, comm): df = pd.DataFrame(np.random.rand(10, 4)) assert df.columns.dtype == np.int64 diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index d9f2ac63f9..554a7fe397 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -1237,6 +1237,7 @@ def __init__(self, value=None, **params): self.style = None self._computed_styler = None self._child_panels = {} + self._indexed_children = {} self._explicit_pagination = 'pagination' in params self._on_edit_callbacks = [] self._on_click_callbacks = {} @@ -1302,6 +1303,11 @@ def _cleanup(self, root: Model | None = None) -> None: p._cleanup(root) super()._cleanup(root) + def _process_events(self, events: dict[str, Any]) -> None: + if 'expanded' in events: + self._update_expanded(events.pop('expanded')) + return super()._process_events(events) + def _process_event(self, event) -> None: if event.event_name == 'selection-change': if self.pagination == 'remote': @@ -1498,27 +1504,50 @@ def _update_style(self, recompute=True): for ref, (m, _) in self._models.items(): self._apply_update([], msg, m, ref) - def _get_children(self, old={}): + def _get_children(self): if self.row_content is None or self.value is None: - return {} + return {}, [], [] from ..pane import panel df = self._processed if self.pagination == 'remote': nrows = self.page_size or self.initial_page_size start = (self.page-1)*nrows df = df.iloc[start:(start+nrows)] - children = {} - for i in (range(len(df)) if self.embed_content else self.expanded): - if i in old: - children[i] = old[i] - else: - children[i] = panel(self.row_content(df.iloc[i])) - return children - - def _get_model_children(self, panels, doc, root, parent, comm=None): + indexed_children, children = {}, {} + expanded = [] + if self.embed_content: + for i in range(len(df)): + expanded.append(i) + idx = df.index[i] + if idx in self._indexed_children: + child = self._indexed_children[idx] + else: + child = panel(self.row_content(df.iloc[i])) + indexed_children[idx] = children[i] = child + else: + for i in self.expanded: + idx = self.value.index[i] + if idx in self._indexed_children: + child = self._indexed_children[idx] + else: + child = panel(self.row_content(self.value.iloc[i])) + try: + loc = df.index.get_loc(idx) + except KeyError: + continue + expanded.append(loc) + indexed_children[idx] = children[loc] = child + removed = [ + child for idx, child in self._indexed_children.items() + if idx not in indexed_children + ] + self._indexed_children = indexed_children + return children, removed, expanded + + def _get_model_children(self, doc, root, parent, comm=None): ref = root.ref['id'] models = {} - for i, p in panels.items(): + for i, p in self._child_panels.items(): if ref in p._models: model = p._models[ref][0] else: @@ -1528,35 +1557,20 @@ def _get_model_children(self, panels, doc, root, parent, comm=None): return models def _update_children(self, *events): - cleanup, reuse = set(), set() - page_events = ('page', 'page_size', 'pagination') - old_panels = self._child_panels for event in events: - if event.name == 'expanded' and len(events) == 1: - if self.embed_content: - cleanup = set() - reuse = set(old_panels) - else: - cleanup = set(event.old) - set(event.new) - reuse = set(event.old) & set(event.new) - elif ( - (event.name == 'value' and self._indexes_changed(event.old, event.new)) or - (event.name in page_events and not self._updating) or - (self.pagination == 'remote' and event.name == 'sorters') - ): + if event.name == 'value' and self._indexes_changed(event.old, event.new): self.expanded = [] + self._indexed_children.clear() return - self._child_panels = child_panels = self._get_children( - {i: old_panels[i] for i in reuse} - ) + elif event.name == 'row_content': + self._indexed_children.clear() + self._child_panels, removed, expanded = self._get_children() for ref, (m, _) in self._models.items(): root, doc, comm = state._views[ref][1:] - for idx in cleanup: - old_panels[idx]._cleanup(root) - children = self._get_model_children( - child_panels, doc, root, m, comm - ) - msg = {'children': children} + for child_panel in removed: + child_panel._cleanup(root) + children = self._get_model_children(doc, root, m, comm) + msg = {'expanded': expanded, 'children': children} self._apply_update([], msg, m, ref) @updating @@ -1689,32 +1703,39 @@ def _update_column(self, column: str, array: np.ndarray): with pd.option_context('mode.chained_assignment', None): self._processed.loc[index, column] = array - def _update_selection(self, indices: list[int] | SelectionEvent): - if isinstance(indices, list): - selected = True - ilocs = [] - else: # SelectionEvent - selected = indices.selected - ilocs = [] if indices.flush else self.selection.copy() - indices = indices.indices - + def _map_indexes(self, indexes, existing=[], add=True): if self.pagination == 'remote': nrows = self.page_size or self.initial_page_size start = (self.page-1)*nrows else: start = 0 - index = self._processed.iloc[[start+ind for ind in indices]].index + ilocs = list(existing) + index = self._processed.iloc[[start+ind for ind in indexes]].index for v in index.values: try: iloc = self.value.index.get_loc(v) self._validate_iloc(v, iloc) except KeyError: continue - if selected: + if add: ilocs.append(iloc) elif iloc in ilocs: ilocs.remove(iloc) - ilocs = list(dict.fromkeys(ilocs)) + return list(dict.fromkeys(ilocs)) + + def _update_expanded(self, expanded): + self.expanded = self._map_indexes(expanded) + + def _update_selection(self, indices: list[int] | SelectionEvent): + if isinstance(indices, list): + selected = True + ilocs = [] + else: + selected = indices.selected + ilocs = [] if indices.flush else self.selection.copy() + indices = indices.indices + + ilocs = self._map_indexes(indices, ilocs, add=selected) if isinstance(self.selectable, int) and not isinstance(self.selectable, bool): ilocs = ilocs[len(ilocs) - self.selectable:] self.selection = ilocs @@ -1765,10 +1786,9 @@ def _get_model( ) model = super()._get_model(doc, root, parent, comm) root = root or model - self._child_panels = child_panels = self._get_children() - model.children = self._get_model_children( - child_panels, doc, root, parent, comm - ) + self._child_panels, removed, expanded = self._get_children() + model.expanded = expanded + model.children = self._get_model_children(doc, root, parent, comm) self._link_props(model, ['page', 'sorters', 'expanded', 'filters', 'page_size'], doc, root, comm) self._register_events('cell-click', 'table-edit', 'selection-change', model=model, doc=doc, comm=comm) return model From 07f9be38320b416995aed9187114637a94fef640 Mon Sep 17 00:00:00 2001 From: Steven DeMartini <1647130+sjdemartini@users.noreply.github.com> Date: Fri, 9 Aug 2024 01:21:01 -0700 Subject: [PATCH 47/91] [docs] Remove extraneous cell from Param Component Gallery example (#7105) The cell with `Select` and `RadioButtonGroup` seems to be there by mistake. Its variables etc aren't used elsewhere, and it doesn't relate to the rest of the Athlete/Power Curve example. --- examples/reference/panes/Param.ipynb | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/examples/reference/panes/Param.ipynb b/examples/reference/panes/Param.ipynb index e5a99fdf3b..a3f865bde4 100644 --- a/examples/reference/panes/Param.ipynb +++ b/examples/reference/panes/Param.ipynb @@ -290,26 +290,6 @@ "Let's put a plot of the PowerCurve in the mix." ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "sections = {'InputFiles': ['dirs', 'files'], 'Field': ['grps', 'varns']}\n", - "\n", - "def update(target, event):\n", - " target.param.update(options=sections[event.new], value=sections[event.new][0])\n", - "\n", - "sel = pn.widgets.Select(options=list(sections.keys()))\n", - "rad = pn.widgets.RadioButtonGroup(options=sections[sel.value])\n", - "sel.link(rad, callbacks={'value': update})\n", - "\n", - "pn.Column(sel, rad)" - ] - }, { "cell_type": "code", "execution_count": null, From 41f59e6a338a58f6b24ca75d4b04af4c097ae38d Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 9 Aug 2024 11:21:39 +0200 Subject: [PATCH 48/91] Implement support for multi-index columns in Tabulator (#7108) --- panel/models/tabulator.ts | 48 +++++++++++++++++------------- panel/tests/widgets/test_tables.py | 38 +++++++++++++++++++++++ panel/widgets/tables.py | 34 +++++++++++++++++---- 3 files changed, 94 insertions(+), 26 deletions(-) diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index 17736da80b..1a8def6e8d 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -302,6 +302,30 @@ const datetimeEditor = function(cell: any, onRendered: any, success: any, cancel return input } +function find_column(group: any, field: string): any { + if (group.columns != null) { + for (const col of group.columns) { + const found = find_column(col, field) + if (found) { + return found + } + } + } else { + return group.field === field ? group : null + } +} + +function clone_column(group: any): any { + if (group.columns == null) { + return {...group} + } + const group_columns = [] + for (const col of group.columns) { + group_columns.push(clone_column(col)) + } + return {...group, columns: group_columns} +} + export class DataTabulatorView extends HTMLBoxView { declare model: DataTabulator @@ -849,13 +873,8 @@ export class DataTabulatorView extends HTMLBoxView { columns.push({field: "_index", frozen: true, visible: false}) if (config_columns != null) { for (const column of config_columns) { - if (column.columns != null) { - const group_columns = [] - for (const col of column.columns) { - group_columns.push({...col}) - } - columns.push({...column, columns: group_columns}) - } else if (column.formatter === "expand") { + const new_column = clone_column(column) + if (column.formatter === "expand") { const expand = { hozAlign: "center", cellClick: (_: any, cell: any) => { @@ -869,7 +888,6 @@ export class DataTabulatorView extends HTMLBoxView { } columns.push(expand) } else { - const new_column = {...column} if (new_column.formatter === "rowSelection") { new_column.cellClick = (_: any, cell: any) => { cell.getRow().toggleSelect() @@ -883,18 +901,8 @@ export class DataTabulatorView extends HTMLBoxView { let tab_column: any = null if (config_columns != null) { for (const col of columns) { - if (col.columns != null) { - for (const c of col.columns) { - if (column.field === c.field) { - tab_column = c - break - } - } - if (tab_column != null) { - break - } - } else if (column.field === col.field) { - tab_column = col + tab_column = find_column(col, column.field) + if (tab_column != null) { break } } diff --git a/panel/tests/widgets/test_tables.py b/panel/tests/widgets/test_tables.py index b0d60f6933..d0e6f3ae6c 100644 --- a/panel/tests/widgets/test_tables.py +++ b/panel/tests/widgets/test_tables.py @@ -310,6 +310,44 @@ def test_tabulator_multi_index_remote_pagination(document, comm): assert np.array_equal(model.source.data['C'], np.array(['foo1', 'foo2', 'foo3'])) +def test_tabulator_multi_index_columns(document, comm): + level_1 = ['A', 'A', 'A', 'B', 'B', 'B'] + level_2 = ['one', 'one', 'two', 'two', 'three', 'three'] + level_3 = ['X', 'Y', 'X', 'Y', 'X', 'Y'] + + # Combine these into a MultiIndex + multi_index = pd.MultiIndex.from_arrays([level_1, level_2, level_3], names=['Level 1', 'Level 2', 'Level 3']) + + # Create a DataFrame with this MultiIndex as columns + df = pd.DataFrame(np.random.randn(4, 6), columns=multi_index) + + table = Tabulator(df) + + model = table.get_root(document, comm) + + assert model.configuration['columns'] == [ + {'field': 'index', 'sorter': 'number'}, + {'title': 'A', 'columns': [ + {'title': 'one', 'columns': [ + {'field': 'A_one_X', 'sorter': 'number'}, + {'field': 'A_one_Y', 'sorter': 'number'}, + ]}, + {'title': 'two', 'columns': [ + {'field': 'A_two_X', 'sorter': 'number'} + ]}, + ]}, + {'title': 'B', 'columns': [ + {'title': 'two', 'columns': [ + {'field': 'B_two_Y', 'sorter': 'number'}, + ]}, + {'title': 'three', 'columns': [ + {'field': 'B_three_X', 'sorter': 'number'}, + {'field': 'B_three_Y', 'sorter': 'number'} + ]}, + ]} + ] + + def test_tabulator_expanded_content(document, comm): df = makeMixedDataFrame() diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index 554a7fe397..50c2d715c8 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -129,7 +129,7 @@ def _compute_renamed_cols(self): self._renamed_cols.clear() return self._renamed_cols = { - str(col) if str(col) != col else col: col for col in self._get_fields() + ('_'.join(col) if isinstance(col, tuple) else str(col)) if str(col) != col else col: col for col in self._get_fields() } def _reset_selection(self, event): @@ -274,12 +274,14 @@ def _get_column_definitions(self, col_names: list[str], df: pd.DataFrame) -> lis else: col_kwargs['width'] = 0 - title = self.titles.get(col, str(col)) + col_name = '_'.join(col) if isinstance(col, tuple) else col + title = self.titles.get(col, str(col_name)) if col in indexes and len(indexes) > 1 and self.hierarchical: title = 'Index: {}'.format(' | '.join(indexes)) elif col in self.indexes and col.startswith('level_'): title = '' - column = TableColumn(field=str(col), title=title, + + column = TableColumn(field=str(col_name), title=title, editor=editor, formatter=formatter, **col_kwargs) columns.append(column) @@ -1903,6 +1905,7 @@ def _config_columns(self, column_objs: list[TableColumn]) -> list[dict[str, Any] } for i, column in enumerate(ordered_columns): field = column.field + index = self._renamed_cols[field] matching_groups = [ group for group, group_cols in grouping.items() if field in group_cols @@ -1934,14 +1937,13 @@ def _config_columns(self, column_objs: list[TableColumn]) -> list[dict[str, Any] title_formatter = dict(title_formatter) col_dict['titleFormatter'] = title_formatter.pop('type') col_dict['titleFormatterParams'] = title_formatter - col_name = self._renamed_cols[field] if field in self.indexes: if len(self.indexes) == 1: dtype = self.value.index.dtype else: dtype = self.value.index.get_level_values(self.indexes.index(field)).dtype else: - dtype = self.value.dtypes[col_name] + dtype = self.value.dtypes[index] if dtype.kind == 'M': col_dict['sorter'] = 'timestamp' elif dtype.kind in 'iuf': @@ -1971,7 +1973,27 @@ def _config_columns(self, column_objs: list[TableColumn]) -> list[dict[str, Any] if isinstance(self.widths, dict) and isinstance(self.widths.get(field), str): col_dict['width'] = self.widths[field] col_dict.update(self._get_filter_spec(column)) - if matching_groups: + + if isinstance(index, tuple): + if columns: + children = columns + last = children[-1] + for group in index[:-1]: + if 'title' in last and last['title'] == group: + new = False + children = last['columns'] + else: + new = True + children.append({ + 'columns': [], + 'title': group, + }) + last = children[-1] + if new: + children = last['columns'] + children.append(col_dict) + column.title = index[-1] + elif matching_groups: group = matching_groups[0] if group in groups: groups[group]['columns'].append(col_dict) From 64a8312de1a333f0b8f9bfefd61eb8b0acaa7dc6 Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Fri, 9 Aug 2024 02:24:24 -0700 Subject: [PATCH 49/91] Allow using Card as default layout for ChatStep (#7022) --- panel/chat/feed.py | 58 +++++++++++++++++++++++++++-------- panel/dist/css/chat_steps.css | 26 ++++++++++++++++ 2 files changed, 71 insertions(+), 13 deletions(-) create mode 100644 panel/dist/css/chat_steps.css diff --git a/panel/chat/feed.py b/panel/chat/feed.py index c37dabfe30..068fe03955 100644 --- a/panel/chat/feed.py +++ b/panel/chat/feed.py @@ -26,7 +26,7 @@ from ..layout.card import Card from ..layout.spacer import VSpacer from ..pane.image import SVG, ImageBase -from ..pane.markup import Markdown +from ..pane.markup import HTML, Markdown from ..util import to_async_gen from ..viewable import Children from .icon import ChatReactionIcons @@ -705,7 +705,9 @@ def add_step( append: bool = True, user: str | None = None, avatar: str | bytes | BytesIO | None = None, - steps_column: Column | None = None, + steps_layout: Column | Card | None = None, + default_layout: Literal["column", "card"] = "card", + layout_params: dict | None = None, **step_params ) -> ChatStep: """ @@ -724,9 +726,15 @@ def add_step( avatar : str | bytes | BytesIO | None The avatar to use; overrides the message's avatar if provided. Will default to the avatar parameter. Only applicable if steps is "new". - steps_column : Column | None - An existing Column of steps to stream to, if None is provided + steps_layout : Column | None + An existing layout of steps to stream to, if None is provided it will default to the last Column of steps or create a new one. + default_layout : str + The default layout to use if steps_layout is None. + 'column' will create a new Column layout. + 'card' will create a new Card layout. + layout_params : dict | None + Additional parameters to pass to the layout. step_params : dict Parameters to pass to the ChatStep. """ @@ -735,6 +743,8 @@ def add_step( step = [] elif not isinstance(step, list): step = [step] + if "margin" not in step_params: + step_params["margin"] = (5, 1) step_params["objects"] = [ ( Markdown(obj, css_classes=["step-message"]) @@ -752,17 +762,39 @@ def add_step( all(isinstance(o, ChatStep) for o in last.object) or last.object.css_classes == 'chat-steps' ) and (user is None or last.user == user): - steps_column = last.object - if steps_column is None: - steps_column = Column( - step, css_classes=["chat-steps"], styles={ - 'max-width': 'calc(100% - 30px)', - 'padding-block': '0px' - } + steps_layout = last.object + if steps_layout is None: + layout_params = layout_params or {} + input_layout_params = dict( + min_width=100, + styles={ + "margin-inline": "10px", + }, + css_classes=["chat-steps"], + stylesheets=[f"{CDN_DIST}css/chat_steps.css"] ) - self.stream(steps_column, user=user, avatar=avatar) + if default_layout == "column": + layout = Column + elif default_layout == "card": + layout = Card + input_layout_params["header_css_classes"] = ["card-header"] + title = layout_params.pop("title", None) + input_layout_params["header"] = HTML( + title or "🪜 Steps", + css_classes=["card-title"], + stylesheets=[f"{CDN_DIST}css/chat_steps.css"] + ) + else: + raise ValueError( + f"Invalid default_layout {default_layout!r}; " + f"expected 'column' or 'card'." + ) + if layout_params: + input_layout_params.update(layout_params) + steps_layout = layout(step, **input_layout_params) + self.stream(steps_layout, user=user, avatar=avatar) else: - steps_column.append(step) + steps_layout.append(step) self._chat_log.scroll_to_latest() return step diff --git a/panel/dist/css/chat_steps.css b/panel/dist/css/chat_steps.css new file mode 100644 index 0000000000..620eecbc74 --- /dev/null +++ b/panel/dist/css/chat_steps.css @@ -0,0 +1,26 @@ +.card-header { + border-bottom: 1px solid var(--panel-border-color, #e0e0e0); + padding-block: 15px; + margin-bottom: 5px; + box-shadow: + color-mix(in srgb, var(--panel-shadow-color) 30%, transparent) 0px 1px 2px + 0px, + color-mix(in srgb, var(--panel-shadow-color) 15%, transparent) 0px 1px 3px + 1px; + background-color: var(--panel-surface-color, #f1f1f1); +} + +.card-header:not(:hover) { + box-shadow: + color-mix(in srgb, var(--panel-shadow-color) 30%, transparent) 0px 1px 2px + 0px, + color-mix(in srgb, var(--panel-shadow-color) 15%, transparent) 0px 1px 3px + 1px; +} + +:host(.card-title) { + font-size: 1.25em; + padding-inline: 5px; + margin-left: 0px; + font-weight: 500; +} From aac41144dbc9b28310d279d851023143a2743709 Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Fri, 9 Aug 2024 02:31:07 -0700 Subject: [PATCH 50/91] Add placeholder while loading to ChatFeed (#7042) * Add placeholder while loading * Add test --- panel/chat/feed.py | 9 +++++++++ panel/tests/chat/test_feed.py | 17 +++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/panel/chat/feed.py b/panel/chat/feed.py index 068fe03955..222a9255fb 100644 --- a/panel/chat/feed.py +++ b/panel/chat/feed.py @@ -337,6 +337,13 @@ def _update_placeholder(self): **self.placeholder_params ) + @param.depends("loading", watch=True, on_init=True) + def _show_placeholder(self): + if self.loading: + self.append(self._placeholder) + else: + self._replace_placeholder(None) + def _replace_placeholder(self, message: ChatMessage | None = None) -> None: """ Replace the placeholder from the chat log with the message @@ -348,6 +355,8 @@ def _replace_placeholder(self, message: ChatMessage | None = None) -> None: self.append(message) try: + if self.loading: + return self.remove(self._placeholder) except ValueError: pass diff --git a/panel/tests/chat/test_feed.py b/panel/tests/chat/test_feed.py index 619c9fbafd..e0ed55e5f1 100644 --- a/panel/tests/chat/test_feed.py +++ b/panel/tests/chat/test_feed.py @@ -40,6 +40,12 @@ def test_init_with_help_text(self): assert message.object == "Instructions" assert message.user == "Help" + def test_init_with_loading(self): + chat_feed = ChatFeed(loading=True) + assert chat_feed._placeholder in chat_feed._chat_log + chat_feed.loading = False + assert chat_feed._placeholder not in chat_feed._chat_log + def test_update_header(self): chat_feed = ChatFeed(header="1") assert chat_feed._card.header == "1" @@ -1136,6 +1142,17 @@ def callback(cls, contents, user): wait_until(lambda: len(chat_feed.objects) == 2) assert chat_feed.objects[1].object == "User: Message" + def test_persist_placeholder_while_loading(self, chat_feed): + def callback(contents): + assert chat_feed._placeholder in chat_feed._chat_log + return "hey testing" + + chat_feed.loading = True + chat_feed.callback = callback + chat_feed.send("Message", respond=True) + assert chat_feed._placeholder in chat_feed._chat_log + + @pytest.mark.xdist_group("chat") class TestChatFeedSerializeForTransformers: From 9404b4348a80a190b3dcda7f270ba3e5b3c10210 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 9 Aug 2024 16:49:50 +0200 Subject: [PATCH 51/91] Optimize rendering of children in JSComponent (#7109) --- panel/models/reactive_esm.ts | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/panel/models/reactive_esm.ts b/panel/models/reactive_esm.ts index 9bcdf0d67e..5b7b6cd833 100644 --- a/panel/models/reactive_esm.ts +++ b/panel/models/reactive_esm.ts @@ -154,6 +154,7 @@ export class ReactiveESMView extends HTMLBoxView { ["after_resize", []], ["remove", []], ]) + _parent_map: Map = new Map() _rendered: boolean = false override initialize(): void { @@ -291,7 +292,8 @@ Promise.resolve(output).then((out) => { cb() } this.render_children() - this.model_proxy.on(this.accessed_children, () => this.render_esm()) + const rerender = this.accessed_children.filter((child) => !this._parent_map.has(child)) + this.model_proxy.on(rerender, () => this.render_esm()) this._rendered = true } @@ -300,6 +302,7 @@ Promise.resolve(output).then((out) => { return } this.accessed_properties = [] + this._parent_map.clear() for (const lf of this._lifecycle_handlers.keys()) { (this._lifecycle_handlers.get(lf) || []).splice(0) } @@ -316,17 +319,32 @@ Promise.resolve(output).then((out) => { for (const child of this.model.children) { const child_model = this.model.data[child] const children = isArray(child_model) ? child_model : [child_model] + const nodes = [] for (const subchild of children) { const view = this._child_views.get(subchild) if (!view) { continue } const parent = view.el.parentNode + nodes.push(parent) if (parent) { view.render() view.after_render() } } + if ((new Set(nodes)).size === 1 && nodes[0] != null) { + const parent = nodes[0] + let in_sequence = true + for (let i=0; i { const child = this._lookup_child(child_view) if (!child) { continue - } else if (new_views.has(child)) { + } + + if (this._parent_map.has(child)) { + const parent = this._parent_map.get(child) + child_view.render() + // @ts-ignore + parent.append(child_view.el) + } + + if (new_views.has(child)) { new_views.get(child).push(child_view) } else { new_views.set(child, [child_view]) @@ -388,7 +415,6 @@ Promise.resolve(output).then((out) => { callback(new_children) } } - this._update_children() this.invalidate_layout() } From f2643367e44763f63fea5de7e9f4fa1c19753ef5 Mon Sep 17 00:00:00 2001 From: Christoph Deil Date: Sat, 10 Aug 2024 21:15:02 +0100 Subject: [PATCH 52/91] Fix example in reactivity.md (#7123) --- doc/explanation/api/reactivity.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/explanation/api/reactivity.md b/doc/explanation/api/reactivity.md index de6fea9b13..a838a9545b 100644 --- a/doc/explanation/api/reactivity.md +++ b/doc/explanation/api/reactivity.md @@ -46,7 +46,7 @@ def submit_form(event): user.param.watch(update_preview, 'value') age.param.watch(update_preview, 'value') -submit.onclick(submit_form) +submit.on_click(submit_form) pn.Row(widgets, md) ``` From b2958079ed131bf54e6daeaa98e9ff71dec795e8 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 11 Aug 2024 10:02:00 +0200 Subject: [PATCH 53/91] Simplify and generalize JSComponent child re-rendering (#7124) --- panel/models/reactive_esm.ts | 45 ++++++++++---------------- panel/tests/ui/test_custom.py | 60 ++++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 30 deletions(-) diff --git a/panel/models/reactive_esm.ts b/panel/models/reactive_esm.ts index 5b7b6cd833..ca63ab3ca1 100644 --- a/panel/models/reactive_esm.ts +++ b/panel/models/reactive_esm.ts @@ -148,13 +148,13 @@ export class ReactiveESMView extends HTMLBoxView { model_proxy: any _changing: boolean = false _child_callbacks: Map void)[]> + _child_rendered: Map = new Map() _event_handlers: ((event: ESMEvent) => void)[] = [] _lifecycle_handlers: Map void)[]> = new Map([ ["after_render", []], ["after_resize", []], ["remove", []], ]) - _parent_map: Map = new Map() _rendered: boolean = false override initialize(): void { @@ -257,6 +257,7 @@ export class ReactiveESMView extends HTMLBoxView { this._apply_visible() this._child_callbacks = new Map() + this._child_rendered.clear() this._rendered = false set_size(this.el, this.model) @@ -292,8 +293,7 @@ Promise.resolve(output).then((out) => { cb() } this.render_children() - const rerender = this.accessed_children.filter((child) => !this._parent_map.has(child)) - this.model_proxy.on(rerender, () => this.render_esm()) + this.model_proxy.on(this.accessed_children, () => this.render_esm()) this._rendered = true } @@ -302,7 +302,6 @@ Promise.resolve(output).then((out) => { return } this.accessed_properties = [] - this._parent_map.clear() for (const lf of this._lifecycle_handlers.keys()) { (this._lifecycle_handlers.get(lf) || []).splice(0) } @@ -319,33 +318,19 @@ Promise.resolve(output).then((out) => { for (const child of this.model.children) { const child_model = this.model.data[child] const children = isArray(child_model) ? child_model : [child_model] - const nodes = [] for (const subchild of children) { const view = this._child_views.get(subchild) if (!view) { continue } const parent = view.el.parentNode - nodes.push(parent) - if (parent) { + if (parent && !this._child_rendered.has(view)) { view.render() - view.after_render() - } - } - if ((new Set(nodes)).size === 1 && nodes[0] != null) { - const parent = nodes[0] - let in_sequence = true - for (let i=0; i { for (const cb of (this._lifecycle_handlers.get("remove") || [])) { cb() } + this._child_callbacks.clear() + this._child_rendered.clear() } override after_resize(): void { @@ -380,7 +367,8 @@ Promise.resolve(output).then((out) => { override async update_children(): Promise { const created_children = new Set(await this.build_child_views()) - for (const child_view of this.child_views) { + const all_views = this.child_views + for (const child_view of all_views) { child_view.el.remove() } @@ -394,13 +382,6 @@ Promise.resolve(output).then((out) => { continue } - if (this._parent_map.has(child)) { - const parent = this._parent_map.get(child) - child_view.render() - // @ts-ignore - parent.append(child_view.el) - } - if (new_views.has(child)) { new_views.get(child).push(child_view) } else { @@ -408,6 +389,12 @@ Promise.resolve(output).then((out) => { } } + for (const view of this._child_rendered.keys()) { + if (!all_views.includes(view)) { + this._child_rendered.delete(view) + } + } + for (const child of this.model.children) { const callbacks = this._child_callbacks.get(child) || [] const new_children = new_views.get(child) || [] diff --git a/panel/tests/ui/test_custom.py b/panel/tests/ui/test_custom.py index d8c2ef406d..34507254b2 100644 --- a/panel/tests/ui/test_custom.py +++ b/panel/tests/ui/test_custom.py @@ -11,6 +11,7 @@ AnyWidgetComponent, Child, Children, JSComponent, ReactComponent, ) from panel.layout import Row +from panel.pane import Markdown from panel.tests.util import serve_component, wait_until pytestmark = pytest.mark.ui @@ -235,10 +236,13 @@ class JSChild(JSComponent): child = Child() + render_count = param.Integer(default=0) + _esm = """ export function render({ model }) { const button = document.createElement('button') button.appendChild(model.get_child('child')) + model.render_count += 1 return button }""" @@ -247,8 +251,11 @@ class ReactChild(ReactComponent): child = Child() + render_count = param.Integer(default=0) + _esm = """ export function render({ model }) { + model.render_count += 1 return }""" @@ -265,31 +272,51 @@ def test_child(page, component): expect(page.locator('button')).to_have_text('A different Markdown pane!') + wait_until(lambda: example.render_count == 1, page) + class JSChildren(JSComponent): children = Children() + render_count = param.Integer(default=0) + _esm = """ export function render({ model }) { const div = document.createElement('div') div.id = "container" div.append(...model.get_child('children')) + model.render_count += 1 return div }""" +class JSChildrenNoReturn(JSChildren): + + _esm = """ + export function render({ model, view }) { + const div = document.createElement('div') + div.id = "container" + div.append(...model.get_child('children')) + view.container.replaceChildren(div) + model.render_count += 1 + }""" + + class ReactChildren(ReactComponent): children = Children() + render_count = param.Integer(default=0) + _esm = """ export function render({ model }) { + model.render_count += 1 return
{model.get_child("children")}
}""" -@pytest.mark.parametrize('component', [JSChildren, ReactChildren]) +@pytest.mark.parametrize('component', [JSChildren, JSChildrenNoReturn, ReactChildren]) def test_children(page, component): example = component(children=['A Markdown pane!']) @@ -306,6 +333,37 @@ def test_children(page, component): expect(page.locator('.foo').nth(0)).to_have_text('1') expect(page.locator('.foo').nth(1)).to_have_text('2') + page.wait_for_timeout(400) + + assert example.render_count == (3 if issubclass(component, JSChildren) else 2) + + +@pytest.mark.parametrize('component', [JSChildren, JSChildrenNoReturn, ReactChildren]) +def test_children_append_without_rerender(page, component): + child = JSChild(child=Markdown( + 'A Markdown pane!', css_classes=['first'] + )) + example = component(children=[child]) + + serve_component(page, example) + + expect(page.locator('.first')).to_have_text('A Markdown pane!') + + wait_until(lambda: child.render_count == 1, page) + + example.children = example.children+[Markdown( + 'A different Markdown pane!', css_classes=['second'] + )] + + expect(page.locator('.second')).to_have_text('A different Markdown pane!') + + page.wait_for_timeout(400) + + assert child.render_count == 1 + assert example.render_count == 2 + + + JS_CODE_BEFORE = """ export function render() { const h1 = document.createElement('h1') From 752626d99ecebc4a755d7626e8e5d1969d294ccc Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 11 Aug 2024 18:26:28 +0200 Subject: [PATCH 54/91] Ensure events are dispatched sequentially (#7128) --- panel/io/document.py | 93 ++++++++++++++++++++++++++++---------------- pixi.toml | 2 +- 2 files changed, 60 insertions(+), 35 deletions(-) diff --git a/panel/io/document.py b/panel/io/document.py index 646152a7bb..744a805a53 100644 --- a/panel/io/document.py +++ b/panel/io/document.py @@ -23,7 +23,7 @@ from bokeh.document.document import Document from bokeh.document.events import ( ColumnDataChangedEvent, ColumnsPatchedEvent, ColumnsStreamedEvent, - DocumentChangedEvent, ModelChangedEvent, + DocumentChangedEvent, MessageSentEvent, ModelChangedEvent, ) from bokeh.model.util import visit_immediate_value_references from bokeh.models import CustomJS @@ -45,16 +45,12 @@ DISPATCH_EVENTS = ( ColumnDataChangedEvent, ColumnsPatchedEvent, ColumnsStreamedEvent, - ModelChangedEvent + ModelChangedEvent, MessageSentEvent ) GC_DEBOUNCE = 5 -_WRITE_LOCK = None - -def WRITE_LOCK(): - global _WRITE_LOCK - if _WRITE_LOCK is None: - _WRITE_LOCK = asyncio.Lock() - return _WRITE_LOCK +_WRITE_FUTURES = weakref.WeakKeyDictionary() +_WRITE_MSGS = weakref.WeakKeyDictionary() +_WRITE_BLOCK = weakref.WeakKeyDictionary() _panel_last_cleanup = None _write_tasks = [] @@ -140,19 +136,19 @@ def _cleanup_doc(doc, destroy=True): # Destroy document doc.destroy(None) -async def _run_write_futures(futures): +async def _run_write_futures(doc): """ Ensure that all write_message calls are awaited and handled. """ from tornado.websocket import WebSocketClosedError - async with WRITE_LOCK(): - for future in futures: - try: - await future - except WebSocketClosedError: - logger.warning("Failed sending message as connection was closed") - except Exception as e: - logger.warning(f"Failed sending message due to following error: {e}") + futures = _WRITE_FUTURES.pop(doc, []) + for future in futures: + try: + await future + except WebSocketClosedError: + logger.warning("Failed sending message as connection was closed") + except Exception as e: + logger.warning(f"Failed sending message due to following error: {e}") def _dispatch_write_task(doc, func, *args, **kwargs): """ @@ -165,29 +161,44 @@ def _dispatch_write_task(doc, func, *args, **kwargs): except RuntimeError: doc.add_next_tick_callback(partial(func, *args, **kwargs)) -async def _dispatch_msgs(doc, msgs): +async def _dispatch_msgs(doc): """ Writes messages to a socket, ensuring that the write_lock is not set, otherwise re-schedules the write task on the event loop. """ from tornado.websocket import WebSocketHandler remaining = {} - for conn, msg in msgs.items(): + futures = [] + conn_msgs = _WRITE_MSGS.pop(doc, {}) + for conn, msgs in conn_msgs.items(): socket = conn._socket if hasattr(socket, 'write_lock') and socket.write_lock._block._value == 0: - remaining[conn] = msg + remaining[conn] = msgs continue - if isinstance(conn._socket, WebSocketHandler): - futures = dispatch_tornado(conn, msg=msg) - elif (socket_type:= type(conn._socket)) in extra_socket_handlers: - futures = extra_socket_handlers[socket_type](conn, msg=msg) + for msg in msgs: + if isinstance(conn._socket, WebSocketHandler): + futures += dispatch_tornado(conn, msg=msg) + elif (socket_type:= type(conn._socket)) in extra_socket_handlers: + futures += extra_socket_handlers[socket_type](conn, msg=msg) + else: + futures += dispatch_django(conn, msg=msg) + if futures: + if doc in _WRITE_FUTURES: + _WRITE_FUTURES[doc] += futures else: - futures = dispatch_django(conn, msg=msg) - await _run_write_futures(futures) + _WRITE_FUTURES[doc] = futures + await _run_write_futures(doc) if not remaining: + if doc in _WRITE_BLOCK: + del _WRITE_BLOCK[doc] return + for conn, msgs in remaining.items(): + if doc in _WRITE_MSGS: + _WRITE_MSGS[doc][conn] = msgs + _WRITE_MSGS[doc].get(conn, []) + else: + _WRITE_MSGS[doc] = {conn: msgs} await asyncio.sleep(0.01) - _dispatch_write_task(doc, _dispatch_msgs, doc, remaining) + _dispatch_write_task(doc, _dispatch_msgs, doc) def _garbage_collect(): if (new_time:= time.monotonic()-_panel_last_cleanup) < GC_DEBOUNCE: @@ -387,7 +398,7 @@ def unlocked() -> Iterator: remaining_events, dispatch_events = [], [] try: yield - locked = False + locked = curdoc in _WRITE_MSGS or curdoc in _WRITE_BLOCK for conn in connections: socket = conn._socket if hasattr(socket, 'write_lock') and socket.write_lock._block._value == 0: @@ -414,11 +425,17 @@ def unlocked() -> Iterator: else: futures += dispatch_django(conn, dispatch_events) + if futures: + if curdoc in _WRITE_FUTURES: + _WRITE_FUTURES[curdoc] += futures + else: + _WRITE_FUTURES[curdoc] = futures + if state._unblocked(curdoc): - _dispatch_write_task(curdoc, _run_write_futures, futures) + _dispatch_write_task(curdoc, _run_write_futures, curdoc) else: - curdoc.add_next_tick_callback(partial(_run_write_futures, futures)) + curdoc.add_next_tick_callback(partial(_run_write_futures, curdoc)) except Exception as e: # If we error out during the yield, there won't be any events # captured so we end up simply calling curdoc.unhold() and @@ -440,14 +457,22 @@ def unlocked() -> Iterator: leftover_events = [e for e in remaining_events if not isinstance(e, Serializable)] remaining_events = [e for e in remaining_events if isinstance(e, Serializable)] + # Set up write locks + if remaining_events: + _WRITE_BLOCK[curdoc] = True + _WRITE_MSGS[curdoc] = msgs = _WRITE_MSGS.get(curdoc, {}) # Create messages for remaining events - msgs = {} for conn in connections: if not remaining_events: continue # Create a protocol message for any events that cannot be immediately dispatched - msgs[conn] = conn.protocol.create('PATCH-DOC', remaining_events) - _dispatch_write_task(curdoc, _dispatch_msgs, curdoc, msgs) + msg = conn.protocol.create('PATCH-DOC', remaining_events) + if conn in msgs: + msgs[conn].append(msg) + else: + msgs[conn] = [msg] + + _dispatch_write_task(curdoc, _dispatch_msgs, curdoc) curdoc.callbacks._held_events += leftover_events curdoc.unhold() diff --git a/pixi.toml b/pixi.toml index 6cbdb658cb..cdf3b56d5d 100644 --- a/pixi.toml +++ b/pixi.toml @@ -116,7 +116,7 @@ ipywidgets_bokeh = "*" numba = "*" reacton = "*" scipy = "*" -textual = "*" +textual = "<0.76" # Temporary fix [feature.test-unit-task.tasks] # So it is not showing up in the test-ui environment test-unit = 'pytest panel/tests -n logical --dist loadgroup' From 52335a1761d850f65f0a9174b496ebdb884a16c5 Mon Sep 17 00:00:00 2001 From: jeffrey-hicks <95231534+jeffrey-hicks@users.noreply.github.com> Date: Sun, 11 Aug 2024 12:08:01 -0500 Subject: [PATCH 55/91] Add `.copy()` to `self._models` to prevent dictionary changed size during iteration error. (#7112) --- panel/pane/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panel/pane/base.py b/panel/pane/base.py index 5a58256933..7a7c149633 100644 --- a/panel/pane/base.py +++ b/panel/pane/base.py @@ -420,7 +420,7 @@ def _update_object( view._preprocess(root, self) def _update_pane(self, *events) -> None: - for ref, (_, parent) in self._models.items(): + for ref, (_, parent) in self._models.copy().items(): if ref not in state._views or ref in state._fake_roots: continue viewable, root, doc, comm = state._views[ref] From 395dbce3044dac636607691ccf5785d4a9a16ee2 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 11 Aug 2024 20:08:01 +0200 Subject: [PATCH 56/91] Fix potential size changed during iteration errors (#7131) --- panel/custom.py | 2 +- panel/pane/echarts.py | 4 ++-- panel/pane/plotly.py | 2 +- panel/reactive.py | 14 +++++++------- panel/widgets/misc.py | 2 +- panel/widgets/tables.py | 14 +++++++------- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/panel/custom.py b/panel/custom.py index 08c761ec63..6b19d9f137 100644 --- a/panel/custom.py +++ b/panel/custom.py @@ -153,7 +153,7 @@ async def _watch_esm(self): def _update_esm(self): esm = self._render_esm() - for ref, (model, _) in self._models.items(): + for ref, (model, _) in self._models.copy().items(): if esm == model.esm: continue self._apply_update({}, {'esm': esm}, model, ref) diff --git a/panel/pane/echarts.py b/panel/pane/echarts.py index 8c9e50c3ae..7a2d4384d5 100644 --- a/panel/pane/echarts.py +++ b/panel/pane/echarts.py @@ -151,7 +151,7 @@ def on_event(self, event: str, callback: Callable, query: str | None = None): """ self._py_callbacks[event][query].append(callback) event_config = {event: list(queries) for event, queries in self._py_callbacks.items()} - for ref, (model, _) in self._models.items(): + for ref, (model, _) in self._models.copy().items(): self._apply_update({}, {'event_config': event_config}, model, ref) def js_on_event(self, event: str, callback: str | CustomJS, query: str | None = None, **args): @@ -176,7 +176,7 @@ def js_on_event(self, event: str, callback: str | CustomJS, query: str | None = of the object. """ self._js_callbacks[event].append((query, callback, args)) - for ref, (model, _) in self._models.items(): + for ref, (model, _) in self._models.copy().items(): js_events = self._get_js_events(ref) self._apply_update({}, {'js_events': js_events}, model, ref) diff --git a/panel/pane/plotly.py b/panel/pane/plotly.py index f7796667f0..a8a20f943b 100644 --- a/panel/pane/plotly.py +++ b/panel/pane/plotly.py @@ -206,7 +206,7 @@ def _send_update_msg( msg['relayout'] = relayout_data if restyle_data: msg['restyle'] = {'data': restyle_data, 'traces': trace_indexes} - for ref, (m, _) in self._models.items(): + for ref, (m, _) in self._models.copy().items(): self._apply_update([], msg, m, ref) def _update_from_figure(self, event, *args, **kwargs): diff --git a/panel/reactive.py b/panel/reactive.py index 7558cb3750..0ce19307f2 100644 --- a/panel/reactive.py +++ b/panel/reactive.py @@ -291,7 +291,7 @@ def _manual_update( """ def _update_manual(self, *events: param.parameterized.Event) -> None: - for ref, (model, parent) in self._models.items(): + for ref, (model, parent) in self._models.copy().items(): if ref not in state._views or ref in state._fake_roots: continue viewable, root, doc, comm = state._views[ref] @@ -927,7 +927,7 @@ def _send_event(self, Event: ModelEvent, **event_kwargs): Event(model=model, **event_kwargs) """ - for ref, (model, _) in self._models.items(): + for ref, (model, _) in self._models.copy().items(): if ref not in state._views or ref in state._fake_roots: continue event = Event(model=model, **event_kwargs) @@ -1029,7 +1029,7 @@ def _update_cds(self, *events: param.parameterized.Event) -> None: self._processed, self._data = self._get_data() msg = {'data': self._data} named_events = {event.name: event for event in events} - for ref, (m, _) in self._models.items(): + for ref, (m, _) in self._models.copy().items(): self._apply_update(named_events, msg, m.source, ref) @updating @@ -1039,7 +1039,7 @@ def _update_selected( indices = self.selection if indices is None else indices msg = {'indices': indices} named_events = {event.name: event for event in events} - for ref, (m, _) in self._models.items(): + for ref, (m, _) in self._models.copy().items(): self._apply_update(named_events, msg, m.source.selected, ref) def _apply_stream(self, ref: str, model: Model, stream: 'DataDict', rollover: Optional[int]) -> None: @@ -1052,7 +1052,7 @@ def _apply_stream(self, ref: str, model: Model, stream: 'DataDict', rollover: Op @updating def _stream(self, stream: 'DataDict', rollover: Optional[int] = None) -> None: self._processed, _ = self._get_data() - for ref, (m, _) in self._models.items(): + for ref, (m, _) in self._models.copy().items(): if ref not in state._views or ref in state._fake_roots: continue viewable, root, doc, comm = state._views[ref] @@ -1074,7 +1074,7 @@ def _apply_patch(self, ref: str, model: Model, patch: 'Patches') -> None: @updating def _patch(self, patch: 'Patches') -> None: - for ref, (m, _) in self._models.items(): + for ref, (m, _) in self._models.copy().items(): if ref not in state._views or ref in state._fake_roots: continue viewable, root, doc, comm = state._views[ref] @@ -2176,7 +2176,7 @@ def on_event(self, node: str, event: str, callback: Callable) -> None: f"nodes include: {self._parser.nodes}.") self._event_callbacks[node][event].append(callback) events = self._get_events() - for ref, (model, _) in self._models.items(): + for ref, (model, _) in self._models.copy().items(): self._apply_update({}, {'events': events}, model, ref) __all__ = ( diff --git a/panel/widgets/misc.py b/panel/widgets/misc.py index 61edb8e4d8..027ac2340e 100644 --- a/panel/widgets/misc.py +++ b/panel/widgets/misc.py @@ -61,7 +61,7 @@ def snapshot(self): Triggers a snapshot of the current VideoStream state to sync the widget value. """ - for ref, (m, _) in self._models.items(): + for ref, (m, _) in self._models.copy().items(): m.snapshot = not m.snapshot (self, root, doc, comm) = state._views[ref] if comm and 'embedded' not in root.tags: diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index 50c2d715c8..3fa632fbc1 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -330,7 +330,7 @@ def _update_cds(self, *events: param.parameterized.Event): self._update_index_mapping() self._data = {k: _convert_datetime_array_ignore_list(v) for k, v in data.items()} msg = {'data': self._data} - for ref, (m, _) in self._models.items(): + for ref, (m, _) in self._models.copy().items(): self._apply_update(events, msg, m.source, ref) def _process_param_change(self, params): @@ -1503,7 +1503,7 @@ def _get_selectable(self): def _update_style(self, recompute=True): styles = self._get_style_data(recompute) msg = {'cell_styles': styles} - for ref, (m, _) in self._models.items(): + for ref, (m, _) in self._models.copy().items(): self._apply_update([], msg, m, ref) def _get_children(self): @@ -1567,7 +1567,7 @@ def _update_children(self, *events): elif event.name == 'row_content': self._indexed_children.clear() self._child_panels, removed, expanded = self._get_children() - for ref, (m, _) in self._models.items(): + for ref, (m, _) in self._models.copy().items(): root, doc, comm = state._views[ref][1:] for child_panel in removed: child_panel._cleanup(root) @@ -1591,7 +1591,7 @@ def _stream(self, stream, rollover=None, follow=True): self._update_index_mapping() def stream(self, stream_value, rollover=None, reset_index=True, follow=True): - for ref, (model, _) in self._models.items(): + for ref, (model, _) in self._models.copy().items(): self._apply_update([], {'follow': follow}, model, ref) super().stream(stream_value, rollover, reset_index) if follow and self.pagination: @@ -1649,7 +1649,7 @@ def _update_cds(self, *events): def _update_selectable(self): selectable = self._get_selectable() - for ref, (model, _) in self._models.items(): + for ref, (model, _) in self._models.copy().items(): self._apply_update([], {'selectable_rows': selectable}, model, ref) @param.depends('page_size', watch=True) @@ -1658,7 +1658,7 @@ def _update_max_page(self): nrows = self.page_size or self.initial_page_size max_page = max(length//nrows + bool(length%nrows), 1) self.param.page.bounds = (1, max_page) - for ref, (model, _) in self._models.items(): + for ref, (model, _) in self._models.copy().items(): self._apply_update([], {'max_page': max_page}, model, ref) def _clear_selection_remote_pagination(self, event): @@ -2035,7 +2035,7 @@ def download(self, filename: str = 'table.csv'): filename: str The filename to save the table as. """ - for ref, (model, _) in self._models.items(): + for ref, (model, _) in self._models.copy().items(): self._apply_update({}, {'filename': filename}, model, ref) self._apply_update({}, {'download': not model.download}, model, ref) From 2a53e2a41709dc0d75398ad3b6ecd5de21006ede Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 11 Aug 2024 20:14:19 +0200 Subject: [PATCH 57/91] Add compatibility for latest Textual (#7130) --- panel/pane/_textual.py | 7 ++++++- pixi.toml | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/panel/pane/_textual.py b/panel/pane/_textual.py index a1af9781a7..33e34c210f 100644 --- a/panel/pane/_textual.py +++ b/panel/pane/_textual.py @@ -77,7 +77,12 @@ def disable_input(self): def start_application_mode(self): self._size_watcher = self._terminal.param.watch(self._resize, ['nrows', 'ncols']) - self._parser = XTermParser(lambda: False, self._debug) + try: + # Textual < 0.76 + self._parser = XTermParser(lambda: False, debug=self._debug) + except TypeError: + # Textual >= 0.76 + self._parser = XTermParser(debug=self._debug) self._input_watcher = self._terminal.param.watch(self._process_input, 'value') def stop_application_mode(self): diff --git a/pixi.toml b/pixi.toml index cdf3b56d5d..6cbdb658cb 100644 --- a/pixi.toml +++ b/pixi.toml @@ -116,7 +116,7 @@ ipywidgets_bokeh = "*" numba = "*" reacton = "*" scipy = "*" -textual = "<0.76" # Temporary fix +textual = "*" [feature.test-unit-task.tasks] # So it is not showing up in the test-ui environment test-unit = 'pytest panel/tests -n logical --dist loadgroup' From 93b43a647114510e534c0dd439c701d06ce82f5d Mon Sep 17 00:00:00 2001 From: Siddhartha Gandhi Date: Mon, 12 Aug 2024 05:39:10 -0400 Subject: [PATCH 58/91] Annotate Widget params as Any (#7132) --- panel/widgets/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panel/widgets/base.py b/panel/widgets/base.py index dd2b705f49..0ecd04102a 100644 --- a/panel/widgets/base.py +++ b/panel/widgets/base.py @@ -103,7 +103,7 @@ class Widget(Reactive, WidgetBase): __abstract = True - def __init__(self, **params): + def __init__(self, **params: Any): if 'name' not in params: params['name'] = '' if '_supports_embed' in params: From f7d7f58c52ffb96681b96877849efea897437d52 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 12 Aug 2024 12:07:35 +0200 Subject: [PATCH 59/91] Unpin jupyterlite-core version (#7129) --- lite/requirements.txt | 2 +- panel/tests/ui/io/test_jupyterlite.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lite/requirements.txt b/lite/requirements.txt index 47627c27e6..57303026b7 100644 --- a/lite/requirements.txt +++ b/lite/requirements.txt @@ -4,7 +4,7 @@ ipywidgets jupyter_bokeh jupyterlab jupyterlab-open-url-parameter -jupyterlite-core <0.3.0 +jupyterlite-core>=0.4.0 jupyterlite-pyodide-kernel nbformat pkginfo diff --git a/panel/tests/ui/io/test_jupyterlite.py b/panel/tests/ui/io/test_jupyterlite.py index 9dd269d780..656a3bbb71 100644 --- a/panel/tests/ui/io/test_jupyterlite.py +++ b/panel/tests/ui/io/test_jupyterlite.py @@ -56,7 +56,7 @@ def test_jupyterlite_execution(launch_jupyterlite, page): page.locator('.jp-Dialog-footer > button').nth(1).click() for _ in range(6): - page.locator('button[data-command="notebook:run-cell-and-select-next"]').click() + page.locator('jp-button[data-command="notebook:run-cell-and-select-next"]').click() page.wait_for_timeout(500) page.locator('.noUi-handle').click(timeout=120 * 1000) From 9b198c8ef8d48be275c6c78c205de4d0c5819ebc Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 12 Aug 2024 13:59:20 +0200 Subject: [PATCH 60/91] Allow streaming chunks to HTML panes (#7125) --- examples/reference/panes/HTML.ipynb | 1 + examples/reference/panes/Markdown.ipynb | 1 + panel/chat/icon.py | 40 ++++++++++++----- panel/chat/message.py | 2 +- panel/models/html.ts | 60 ++++++++++++++++++++++++- panel/models/markup.py | 16 +++++++ panel/pane/markup.py | 25 +++++++++-- panel/tests/ui/pane/test_markup.py | 22 +++++++-- panel/util/__init__.py | 18 ++++++++ 9 files changed, 166 insertions(+), 19 deletions(-) diff --git a/examples/reference/panes/HTML.ipynb b/examples/reference/panes/HTML.ipynb index 1bccb8372e..73862ca03f 100644 --- a/examples/reference/panes/HTML.ipynb +++ b/examples/reference/panes/HTML.ipynb @@ -22,6 +22,7 @@ "For details on other options for customizing the component see the [layout](../../how_to/layout/index.md) and [styling](../../how_to/styling/index.md) how-to guides.\n", "\n", "* **`disable_math`** (boolean, `default=True`): Whether to disable MathJax math rendering for strings escaped with `$$` delimiters.\n", + "* **`enable_streaming`** (boolean, `default=False`): Whether to enable streaming of text snippets. This will diff the `object` when it is updated and only send the trailing chunk that was added.\n", "* **`object`** (str or object): The string or object with ``_repr_html_`` method to display\n", "* **`sanitize_html`** (boolean, `default=False`): Whether to sanitize HTML sent to the frontend.\n", "* **`sanitize_hook`** (Callable, `default=bleach.clean`): Sanitization callback to apply if `sanitize_html=True`.\n", diff --git a/examples/reference/panes/Markdown.ipynb b/examples/reference/panes/Markdown.ipynb index 93eeeb7541..ef4f0055e1 100644 --- a/examples/reference/panes/Markdown.ipynb +++ b/examples/reference/panes/Markdown.ipynb @@ -25,6 +25,7 @@ "\n", "* **`dedent`** (bool): Whether to dedent common whitespace across all lines.\n", "* **`disable_math`** (boolean, `default=False`): Whether to disable MathJax math rendering for strings escaped with `$$` delimiters.\n", + "* **`enable_streaming`** (boolean, `default=False`): Whether to enable streaming of text snippets. This will diff the `object` when it is updated and only send the trailing chunk that was added.\n", "* **`extensions`** (list): A list of [Python-Markdown extensions](https://python-markdown.github.io/extensions/) to use (does not apply for 'markdown-it' and 'myst' renderers).\n", "* **`object`** (str or object): A string containing Markdown, or an object with a ``_repr_markdown_`` method.\n", "* **`plugins`** (function): A list of additional markdown-it-py plugins to apply.\n", diff --git a/panel/chat/icon.py b/panel/chat/icon.py index d7c2729323..0d0db1dadd 100644 --- a/panel/chat/icon.py +++ b/panel/chat/icon.py @@ -93,24 +93,33 @@ def _update_value(self, event): class ChatCopyIcon(ReactiveHTML): + """ + ChatCopyIcon copies the value to the clipboard when clicked. + To avoid sending the value to the frontend the value is only + synced after the icon is clicked. + """ + + css_classes = param.List(default=["copy-icon"], doc="The CSS classes of the widget.") fill = param.String(default="none", doc="The fill color of the icon.") - value = param.String(default=None, doc="The text to copy to the clipboard.") + value = param.String(default=None, doc="The text to copy to the clipboard.", precedence=-1) - css_classes = param.List(default=["copy-icon"], doc="The CSS classes of the widget.") + _synced = param.String(default=None, doc="The text to copy to the clipboard.") + + _request_sync = param.Integer(default=0) _template = """
- @@ -119,10 +128,21 @@ class ChatCopyIcon(ReactiveHTML):
""" - _scripts = {"copy_to_clipboard": """ - navigator.clipboard.writeText(`${data.value}`); - data.fill = "currentColor"; - setTimeout(() => data.fill = "none", 50); - """} + _scripts = { + "render": "copy_icon.setAttribute('fill', data.fill)", + "fill": "copy_icon.setAttribute('fill', data.fill)", + "request_value": """ + data._request_sync += 1; + data.fill = "currentColor"; + """, + "_synced": """ + navigator.clipboard.writeText(`${data._synced}`); + data.fill = "none"; + """ + } _stylesheets: ClassVar[list[str]] = [f"{CDN_DIST}css/chat_copy_icon.css"] + + @param.depends('_request_sync', watch=True) + def _sync(self): + self._synced = self.value diff --git a/panel/chat/message.py b/panel/chat/message.py index 26313d433b..11a8a8c9f9 100644 --- a/panel/chat/message.py +++ b/panel/chat/message.py @@ -487,7 +487,7 @@ def _create_panel(self, value, old=None): pass else: if isinstance(old, Markdown) and isinstance(value, str): - self._set_params(old, object=value) + self._set_params(old, enable_streaming=True, object=value) return old object_panel = _panel(value) diff --git a/panel/models/html.ts b/panel/models/html.ts index ec0d2b4800..fe549519e2 100644 --- a/panel/models/html.ts +++ b/panel/models/html.ts @@ -1,4 +1,4 @@ -import {ModelEvent} from "@bokehjs/core/bokeh_events" +import {ModelEvent, server_event} from "@bokehjs/core/bokeh_events" import type * as p from "@bokehjs/core/properties" import type {Attrs, Dict} from "@bokehjs/core/types" import {entries} from "@bokehjs/core/util/object" @@ -6,6 +6,25 @@ import {Markup} from "@bokehjs/models/widgets/markup" import {PanelMarkupView} from "./layout" import {serializeEvent} from "./event-to-object" +@server_event("html_stream") +export class HTMLStreamEvent extends ModelEvent { + constructor(readonly model: HTML, readonly patch: string, readonly start: number) { + super() + this.patch = patch + this.start = start + this.origin = model + } + + protected override get event_values(): Attrs { + return {model: this.origin, patch: this.patch, start: this.start} + } + + static override from_values(values: object) { + const {model, patch, start} = values as {model: HTML, patch: string, start: number} + return new HTMLStreamEvent(model, patch, start) + } +} + export class DOMEvent extends ModelEvent { constructor(readonly node: string, readonly data: unknown) { super() @@ -39,8 +58,33 @@ export function run_scripts(node: Element): void { } } +function throttle(func: Function, limit: number): any { + let lastFunc: number + let lastRan: number + + return function(...args: any) { + // @ts-ignore + const context = this + + if (!lastRan) { + func.apply(context, args) + lastRan = Date.now() + } else { + clearTimeout(lastFunc) + + lastFunc = setTimeout(function() { + if ((Date.now() - lastRan) >= limit) { + func.apply(context, args) + lastRan = Date.now() + } + }, limit - (Date.now() - lastRan)) + } + } +} + export class HTMLView extends PanelMarkupView { declare model: HTML + _buffer: string | null = null protected readonly _event_listeners: Map void>> = new Map() @@ -49,6 +93,7 @@ export class HTMLView extends PanelMarkupView { const {text, visible, events} = this.model.properties this.on_change(text, () => { + this._buffer = null const html = this.process_tex() this.set_html(html) }) @@ -61,6 +106,19 @@ export class HTMLView extends PanelMarkupView { this._remove_event_listeners() this._setup_event_listeners() }) + + const set_text = throttle(() => { + const text = this._buffer + this._buffer = null + this.model.setv({text}, {silent: true}) + const html = this.process_tex() + this.set_html(html) + }, 10) + this.model.on_event(HTMLStreamEvent, (event: HTMLStreamEvent) => { + const beginning = this._buffer == null ? this.model.text : this._buffer + this._buffer = beginning.slice(0, event.start)+event.patch + set_text() + }) } protected rerender() { diff --git a/panel/models/markup.py b/panel/models/markup.py index 69b0d40385..c0299c504c 100644 --- a/panel/models/markup.py +++ b/panel/models/markup.py @@ -1,12 +1,28 @@ """ Custom bokeh Markup models. """ +from typing import Any + from bokeh.core.properties import ( Bool, Dict, Either, Float, Int, List, Null, String, ) +from bokeh.events import ModelEvent from bokeh.models.widgets import Markup +class HTMLStreamEvent(ModelEvent): + + event_name = 'html_stream' + + def __init__(self, model, patch=None, start=None): + self.patch = patch + self.start = start + super().__init__(model=model) + + def event_values(self) -> dict[str, Any]: + return dict(super().event_values(), patch=self.patch, start=self.start) + + class HTML(Markup): """ A bokeh model to render HTML markup including embedded script tags. diff --git a/panel/pane/markup.py b/panel/pane/markup.py index ba5cddb7f3..583903a932 100644 --- a/panel/pane/markup.py +++ b/panel/pane/markup.py @@ -15,8 +15,8 @@ import param # type: ignore from ..io.resources import CDN_DIST -from ..models import HTML as _BkHTML, JSON as _BkJSON -from ..util import HTML_SANITIZER, escape +from ..models.markup import HTML as _BkHTML, JSON as _BkJSON, HTMLStreamEvent +from ..util import HTML_SANITIZER, escape, prefix_length from .base import ModelPane if TYPE_CHECKING: @@ -24,6 +24,7 @@ from bokeh.model import Model from pyviz_comms import Comm # type: ignore + class HTMLBasePane(ModelPane): """ Baseclass for Panes which render HTML inside a Bokeh Div. @@ -31,14 +32,30 @@ class HTMLBasePane(ModelPane): the supported options like style and sizing_mode. """ + enable_streaming = param.Boolean(default=False, doc=""" + Whether to enable streaming of text snippets. This is useful + when updating a string step by step, e.g. in a chat message.""") + _bokeh_model: ClassVar[Model] = _BkHTML - _rename: ClassVar[Mapping[str, str | None]] = {'object': 'text'} + _rename: ClassVar[Mapping[str, str | None]] = {'object': 'text', 'enable_streaming': None} _updates: ClassVar[bool] = True __abstract = True + def _update(self, ref: str, model: Model) -> None: + props = self._get_properties(model.document) + if self.enable_streaming and 'text' in props: + text = props['text'] + start = prefix_length(text, model.text) + model.run_scripts = False + patch = text[start:] + self._send_event(HTMLStreamEvent, patch=patch, start=start) + model._property_values['text'] = model.text[:start]+patch + del props['text'] + model.update(**props) + class HTML(HTMLBasePane): """ @@ -71,7 +88,7 @@ class HTML(HTMLBasePane): priority: ClassVar[float | bool | None] = None _rename: ClassVar[Mapping[str, str | None]] = { - 'sanitize_html': None, 'sanitize_hook': None + 'sanitize_html': None, 'sanitize_hook': None, 'stream': None } _rerender_params: ClassVar[list[str]] = [ diff --git a/panel/tests/ui/pane/test_markup.py b/panel/tests/ui/pane/test_markup.py index f2bae01304..6d246d76ee 100644 --- a/panel/tests/ui/pane/test_markup.py +++ b/panel/tests/ui/pane/test_markup.py @@ -82,12 +82,28 @@ def test_markdown_pane_visible_toggle(page): serve_component(page, md) - assert page.locator(".markdown").locator("div").text_content() == 'Initial\n' - assert not page.locator(".markdown").locator("div").is_visible() + expect(page.locator(".markdown").locator("div")).to_have_text('Initial\n') + expect(page.locator(".markdown").locator("div")).not_to_be_visible() md.visible = True - wait_until(lambda: page.locator(".markdown").locator("div").is_visible(), page) + expect(page.locator(".markdown").locator("div")).to_be_visible() + + +def test_markdown_pane_stream(page): + md = Markdown('Empty', enable_streaming=True) + + serve_component(page, md) + + expect(page.locator('.markdown')).to_have_text('Empty') + + md.object = '' + for i in range(1000): + md.object += str(i) + + assert md.object == ''.join(map(str, range(1000))) + expect(page.locator('.markdown')).to_have_text(md.object) + def test_html_model_no_stylesheet(page): # regression test for https://github.com/holoviz/holoviews/issues/5963 diff --git a/panel/util/__init__.py b/panel/util/__init__.py index 80cff9e211..0a074121cd 100644 --- a/panel/util/__init__.py +++ b/panel/util/__init__.py @@ -503,3 +503,21 @@ def safe_next(): if value is done: break yield value + + +def prefix_length(a: str, b: str) -> int: + """ + Searches for the length of overlap in the starting + characters of string b in a. Uses binary search + if b is not already a prefix of a. + """ + if a.startswith(b): + return len(b) + left, right = 0, min(len(a), len(b)) + while left < right: + mid = (left + right + 1) // 2 + if a.startswith(b[:mid]): + left = mid + else: + right = mid - 1 + return left From 8799cdac3196ced20766e3a544c97386d2e15d8c Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Mon, 12 Aug 2024 14:05:30 +0200 Subject: [PATCH 61/91] support custom vizzu backend (#7114) --- panel/pane/holoviews.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/panel/pane/holoviews.py b/panel/pane/holoviews.py index cc3ea591f2..f157072775 100644 --- a/panel/pane/holoviews.py +++ b/panel/pane/holoviews.py @@ -385,6 +385,9 @@ def _sync_sizing_mode(self, plot): 'width': None, 'height': None } + else: + params = {} + self._syncing_props = True try: self.param.update({k: v for k, v in params.items() if k not in self._overrides}) From 5ce22358a95a4e80b869bc1c8b1a4f78306c90b4 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 12 Aug 2024 19:13:16 +0200 Subject: [PATCH 62/91] Tweaks to make tests more robust (#7133) --- panel/models/tabulator.ts | 17 ++-- panel/tests/ui/chat/test_chat_interface_ui.py | 13 +-- panel/tests/ui/test_custom.py | 2 +- panel/tests/ui/widgets/test_tabulator.py | 88 +++++++++++++++---- panel/widgets/tables.py | 6 +- 5 files changed, 93 insertions(+), 33 deletions(-) diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index 1a8def6e8d..67f05e781f 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -600,8 +600,10 @@ export class DataTabulatorView extends HTMLBoxView { if (rows.length === 0) { this.tabulator.rowManager.renderEmptyScroll() } - // Ensure that after filtering the page is updated - this.updatePage(this.tabulator.getPage()) + if (this.model.pagination != null) { + // Ensure that after filtering the page is updated + this.updatePage(this.tabulator.getPage()) + } }) this.tabulator.on("pageLoaded", (pageno: number) => { this.updatePage(pageno) @@ -774,14 +776,15 @@ export class DataTabulatorView extends HTMLBoxView { } } for (const index of this.model.expanded) { - const row = lookup.get(index) if (!this.model.children.has(index)) { continue } + const row = lookup.get(index) const model = this.model.children.get(index) const view = model == null ? null : this._child_views.get(model) - if ((view != null) && (new_children as UIElementView[]).includes(view)) { - this._render_row(row, false) + if (view != null) { + const render = (new_children as UIElementView[]).includes(view) + this._render_row(row, false, render) } } this._update_children() @@ -792,7 +795,7 @@ export class DataTabulatorView extends HTMLBoxView { }) } - _render_row(row: any, resize: boolean = true): void { + _render_row(row: any, resize: boolean = true, render: boolean = true): void { const index = row._row?.data._index if (!this.model.expanded.includes(index) || this.model.children.get(index) == null) { return @@ -809,7 +812,7 @@ export class DataTabulatorView extends HTMLBoxView { const viewEl = div({style: {background_color: bg, margin_left: neg_margin, max_width: "100%", overflow_x: "hidden"}}) viewEl.appendChild(view.el) rowEl.appendChild(viewEl) - if (!view.has_finished()) { + if (!view.has_finished() && render) { view.render() view.after_render() } diff --git a/panel/tests/ui/chat/test_chat_interface_ui.py b/panel/tests/ui/chat/test_chat_interface_ui.py index 6f26698c97..4f764bd7bd 100644 --- a/panel/tests/ui/chat/test_chat_interface_ui.py +++ b/panel/tests/ui/chat/test_chat_interface_ui.py @@ -2,8 +2,10 @@ pytest.importorskip("playwright") +from playwright.sync_api import expect + from panel.chat import ChatInterface -from panel.tests.util import serve_component, wait_until +from panel.tests.util import serve_component pytestmark = pytest.mark.ui @@ -13,9 +15,8 @@ def test_chat_interface_help(page): help_text="This is a test help text" ) serve_component(page, chat_interface) - message = page.locator("p") - message_text = message.inner_text() - wait_until(lambda: message_text == "This is a test help text", page) + + expect(page.locator("p")).to_have_text("This is a test help text") def test_chat_interface_custom_js(page): @@ -38,7 +39,7 @@ def test_chat_interface_custom_js(page): page.locator("button", has_text="help").click() msg = msg_info.value - wait_until(lambda: msg.args[0].json_value() == "Typed: 'Hello'", page) + assert msg.args[0].json_value() == "Typed: 'Hello'" def test_chat_interface_custom_js_string(page): @@ -58,4 +59,4 @@ def test_chat_interface_custom_js_string(page): page.locator("button", has_text="help").click() msg = msg_info.value - wait_until(lambda: msg.args[0].json_value() == "Clicked", page) + assert msg.args[0].json_value() == "Clicked" diff --git a/panel/tests/ui/test_custom.py b/panel/tests/ui/test_custom.py index 34507254b2..dbb7b3aaa8 100644 --- a/panel/tests/ui/test_custom.py +++ b/panel/tests/ui/test_custom.py @@ -272,7 +272,7 @@ def test_child(page, component): expect(page.locator('button')).to_have_text('A different Markdown pane!') - wait_until(lambda: example.render_count == 1, page) + wait_until(lambda: example.render_count == (2 if component is JSChild else 1), page) class JSChildren(JSComponent): diff --git a/panel/tests/ui/widgets/test_tabulator.py b/panel/tests/ui/widgets/test_tabulator.py index 7f5671a410..4e24683a1c 100644 --- a/panel/tests/ui/widgets/test_tabulator.py +++ b/panel/tests/ui/widgets/test_tabulator.py @@ -164,13 +164,14 @@ def test_tabulator_value_changed(page, df_mixed): serve_component(page, widget) + expect(page.locator('.pnx-tabulator.tabulator')).to_have_count(1) + df_mixed.loc['idx0', 'str'] = 'AA' # Need to trigger the value as the dataframe was modified # in place which is not detected. widget.param.trigger('value') - wait_until(lambda: page.locator('text="AA"') is not None, page) - changed_cell = page.locator('text="AA"') - expect(changed_cell).to_have_count(1) + + expect(page.locator('text="AA"')).to_have_count(1) def test_tabulator_disabled(page, df_mixed): @@ -1096,9 +1097,11 @@ def test_tabulator_patch_no_height_resize(page): serve_component(page, app) + page.wait_for_timeout(100) + page.mouse.wheel(delta_x=0, delta_y=10000) at_bottom_script = """ - isAtBottom => (window.innerHeight + window.scrollY) >= document.body.scrollHeight; + () => Math.round(window.innerHeight + window.scrollY) === document.body.scrollHeight """ wait_until(lambda: page.evaluate(at_bottom_script), page) @@ -1107,7 +1110,7 @@ def test_tabulator_patch_no_height_resize(page): # Give it some time to potentially "re-scroll" page.wait_for_timeout(400) - wait_until(lambda: page.evaluate(at_bottom_script), page) + wait_until(lambda: page.locator('.pnx-tabulator').evaluate(at_bottom_script), page) @pytest.mark.parametrize( @@ -1128,9 +1131,14 @@ def test_tabulator_header_filter_no_horizontal_rescroll(page, df_mixed, paginati serve_component(page, widget) + page.wait_for_timeout(100) + header = page.locator(f'text="{col_name}"') # Scroll to the right header.scroll_into_view_if_needed() + + page.wait_for_timeout(100) + bb = header.bounding_box() header = page.locator('input[type="search"]') @@ -1139,10 +1147,10 @@ def test_tabulator_header_filter_no_horizontal_rescroll(page, df_mixed, paginati header.press('Enter') # Wait to catch a potential rescroll - page.wait_for_timeout(400) + page.wait_for_timeout(500) # The table should keep the same scroll position, this fails - assert page.locator(f'text="{col_name}"').bounding_box() == bb + wait_until(lambda: page.locator(f'text="{col_name}"').bounding_box() == bb, page) def test_tabulator_header_filter_always_visible(page, df_mixed): @@ -1567,6 +1575,34 @@ def test_tabulator_row_content_expand_from_python_after(page, df_mixed): expect(page.locator('text="►"')).to_have_count(len(df_mixed)) +def test_tabulator_row_content_expand_after_filtered(page, df_mixed): + table = Tabulator(df_mixed, row_content=lambda e: f"Hello {e.int}", header_filters=True) + + serve_component(page, table) + + idx_filter = page.locator('.tabulator-col').nth(2).locator('input[type="search"]') + idx_filter.click() + idx_filter.fill('idx1') + idx_filter.press('Enter') + + rows = page.locator('.tabulator-row') + + expect(rows).to_have_count(1) + + page.locator('.tabulator-row').nth(0).locator('.tabulator-cell').nth(1).click() + + expect(page.locator('.markdown')).to_have_text('Hello 2') + + idx_filter.click() + idx_filter.fill('') + idx_filter.press('Enter') + + expect(rows).to_have_count(4) + + expect(rows.nth(0).locator('.markdown')).to_have_count(0) + expect(rows.nth(1).locator('.markdown')).to_have_text('Hello 2') + + def test_tabulator_groups(page, df_mixed): widget = Tabulator( df_mixed, @@ -2112,6 +2148,8 @@ def test_tabulator_streaming_default(page): serve_component(page, widget) + page.wait_for_timeout(100) + expect(page.locator('.tabulator-row')).to_have_count(len(df)) height_start = page.locator('.pnx-tabulator.tabulator').bounding_box()['height'] @@ -2158,11 +2196,16 @@ def test_tabulator_streaming_no_follow(page): serve_component(page, widget) + page.wait_for_timeout(100) + expect(page.locator('.tabulator-row')).to_have_count(len(df)) - assert page.locator('text="-1"').count() == 2 + expect(page.locator('text="-1"')).to_have_count(2) height_start = page.locator('.pnx-tabulator.tabulator').bounding_box()['height'] + scroll_top = page.locator('.pnx-tabulator.tabulator').evaluate("(el) => el.scrollTop") + assert scroll_top == 0 + recs = [] nrows2 = 5 def stream_data(): @@ -2176,16 +2219,16 @@ def stream_data(): repetitions = 3 state.add_periodic_callback(stream_data, period=100, count=repetitions) - # Explicit wait to make sure the periodic callback has completed + # Wait until data is updated + wait_until(lambda: len(widget.value) == nrows1 + repetitions * nrows2, page) + + # Explicit wait to make sure the periodic callback has propagated page.wait_for_timeout(500) - expect(page.locator('text="-1"')).to_have_count(2) - # As we're not in follow mode the last row isn't visible - # and seems to be out of reach to the selector. How visibility - # is used here seems brittle though, may need to be revisited. - expect(page.locator(f'text="{val[0]}"')).to_have_count(0) + scroll_top = page.locator('.pnx-tabulator.tabulator').evaluate("(el) => el.scrollTop") + assert scroll_top == 0 - assert len(widget.value) == nrows1 + repetitions * nrows2 + # Assert the data matches what we expect assert widget.current_view.equals(widget.value) assert page.locator('.pnx-tabulator.tabulator').bounding_box()['height'] == height_start @@ -2550,19 +2593,23 @@ def test_tabulator_click_event_and_header_filters_and_streamed_data(page): str_header.press('Enter') wait_until(lambda: len(widget.filters) == 1, page) + page.wait_for_timeout(100) + # Stream data in ensuring that it does not mess up the index widget.stream(pd.DataFrame([('D', 'Y')], columns=['col1', 'col2'], index=[5])) + page.wait_for_timeout(100) + # Click on the last cell cell = page.locator('text="Z"') - cell.click() + cell.click(force=True) wait_until(lambda: len(values) == 1, page) # This cell was at index 4 in col2 of the original dataframe assert values[0] == ('col2', 4, 'Z') cell = page.locator('text="Y"') - cell.click() + cell.click(force=True) wait_until(lambda: len(values) == 2, page) # This cell was at index 5 in col2 of the original dataframe @@ -3479,6 +3526,9 @@ def contains_filter(df, pattern=None): page.locator('.tabulator-col-title-holder').nth(3).click() + # Wait for sorting + page.wait_for_timeout(100) + row = page.locator('.tabulator-row').nth(1) row.click() @@ -3487,6 +3537,10 @@ def contains_filter(df, pattern=None): tbl.page_size = 2 page.locator('.tabulator-col-title-holder').nth(3).click() + + # Wait for sorting + page.wait_for_timeout(100) + page.locator('.tabulator-row').nth(0).click() wait_until(lambda: tbl.selection == [3], page) diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index 3fa632fbc1..4dc66d7dc3 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -1211,7 +1211,7 @@ class Tabulator(BaseTable): _manual_params: ClassVar[list[str]] = BaseTable._manual_params + _config_params - _priority_changes: ClassVar[list[str]] = ['data'] + _priority_changes: ClassVar[list[str]] = ['data', 'filters'] _rename: ClassVar[Mapping[str, str | None]] = { 'selection': None, 'row_content': None, 'row_height': None, @@ -1559,6 +1559,8 @@ def _get_model_children(self, doc, root, parent, comm=None): return models def _update_children(self, *events): + if all(e.name in ('page', 'page_size', 'pagination', 'sorters') for e in events) and self.pagination != 'remote': + return for event in events: if event.name == 'value' and self._indexes_changed(event.old, event.new): self.expanded = [] @@ -1630,7 +1632,7 @@ def _update_cds(self, *events): page_events = ('page', 'page_size', 'sorters') if self._updating: return - elif events and all(e.name in page_events[:-1] for e in events) and self.pagination == 'local': + elif events and all(e.name in page_events for e in events) and self.pagination == 'local': return elif events and all(e.name in page_events for e in events) and not self.pagination: self._processed, _ = self._get_data() From f63a9dd916e286632b426aa80ca68f18b90ecb22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 13 Aug 2024 09:53:27 +0200 Subject: [PATCH 63/91] Add lite environment to pixi (#7134) --- .github/workflows/jupyterlite.yaml | 78 ++++++++++--------- .github/workflows/test.yaml | 7 +- lite/requirements.txt | 11 --- panel/.eslintrc.js | 2 +- pixi.toml | 16 ++++ pyproject.toml | 1 + scripts/jupyterlite/build.sh | 20 +++++ scripts/jupyterlite/extra_packages.json | 5 ++ .../jupyterlite}/files/Getting_Started.ipynb | 4 +- .../files/Reset_Jupyterlite.ipynb | 2 +- scripts/jupyterlite/jupyter-lite.json | 10 +++ scripts/jupyterlite/jupyter_lite_config.json | 6 ++ scripts/jupyterlite/package-lock.json | 44 +++++++++++ scripts/jupyterlite/package.json | 5 ++ scripts/jupyterlite/patch_lock.py | 47 +++++++++++ scripts/jupyterlite/update_lock.js | 20 +++++ scripts/panelite/generate_panelite_content.py | 2 +- 17 files changed, 223 insertions(+), 57 deletions(-) delete mode 100644 lite/requirements.txt create mode 100644 scripts/jupyterlite/build.sh create mode 100644 scripts/jupyterlite/extra_packages.json rename {lite => scripts/jupyterlite}/files/Getting_Started.ipynb (94%) rename {lite => scripts/jupyterlite}/files/Reset_Jupyterlite.ipynb (99%) create mode 100644 scripts/jupyterlite/jupyter-lite.json create mode 100644 scripts/jupyterlite/jupyter_lite_config.json create mode 100644 scripts/jupyterlite/package-lock.json create mode 100644 scripts/jupyterlite/package.json create mode 100644 scripts/jupyterlite/patch_lock.py create mode 100644 scripts/jupyterlite/update_lock.js diff --git a/.github/workflows/jupyterlite.yaml b/.github/workflows/jupyterlite.yaml index e9a1f148ea..40523eec7d 100644 --- a/.github/workflows/jupyterlite.yaml +++ b/.github/workflows/jupyterlite.yaml @@ -3,59 +3,67 @@ name: jupyterlite on: push: tags: - - 'v[0-9]+.[0-9]+.[0-9]+' - - 'v[0-9]+.[0-9]+.[0-9]+a[0-9]+' - - 'v[0-9]+.[0-9]+.[0-9]+b[0-9]+' - - 'v[0-9]+.[0-9]+.[0-9]+rc[0-9]+' + - "v[0-9]+.[0-9]+.[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+a[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+b[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+rc[0-9]+" workflow_dispatch: inputs: target: - description: 'Site to build and deploy' + description: "Site to build and deploy" type: choice options: - - dev - - main - - dryrun + - dev + - main + - dryrun required: true default: dryrun schedule: - - cron: '0 19 * * SUN' + - cron: "0 19 * * SUN" jobs: - deploy_jupyterlite: - name: JupyterLite + pixi_lock: + name: Pixi lock runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v3 + - uses: holoviz-dev/holoviz_tasks/pixi_lock@pixi + + lite_build: + name: Build Jupyterlite + needs: [pixi_lock] + runs-on: "ubuntu-latest" + steps: + - uses: holoviz-dev/holoviz_tasks/pixi_install@pixi + with: + environments: "lite" + install: false + download-data: false + - name: lite build + run: pixi run lite-build + - uses: actions/upload-artifact@v4 + if: always() with: - fetch-depth: 0 - - name: Setup Python - uses: actions/setup-python@v4 + name: jupyterlite + path: lite/dist/ + if-no-files-found: error + + lite_publish: + name: Publish Jupyterlite + runs-on: ubuntu-latest + needs: [lite_build] + steps: + - uses: actions/download-artifact@v4 with: - python-version: '3.10' - - name: Set and echo git ref + name: jupyterlite + path: lite/dist/ + - name: Set output id: vars - run: | - echo 'Deploying from ref ${GITHUB_REF#refs/*/}' - echo 'tag=${GITHUB_REF#refs/*/}' >> $GITHUB_OUTPUT - - name: Install the dependencies - run: | - python -m pip install -r ./lite/requirements.txt - - name: Build pyodide wheels for JupyterLite - run: | - python ./scripts/build_pyodide_wheels.py lite/pypi - - name: Convert content - run: | - python ./scripts/panelite/generate_panelite_content.py - - name: Build the JupyterLite site - run: | - jupyter lite build --lite-dir lite --output-dir lite/dist + run: echo "tag=${{ needs.docs_build.outputs.tag }}" >> $GITHUB_OUTPUT - name: Upload dev if: | (github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'dev') || (github.event_name == 'workflow_run' && (contains(steps.vars.outputs.tag, 'a') || contains(steps.vars.outputs.tag, 'b') || contains(steps.vars.outputs.tag, 'rc'))) - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: personal_token: ${{ secrets.ACCESS_TOKEN }} external_repository: holoviz-dev/panelite-dev @@ -65,7 +73,7 @@ jobs: if: | (github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'main') || (github.event_name == 'push' && !(contains(steps.vars.outputs.tag, 'a') || contains(steps.vars.outputs.tag, 'b') || contains(steps.vars.outputs.tag, 'rc'))) - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: personal_token: ${{ secrets.ACCESS_TOKEN }} external_repository: holoviz-dev/panelite diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9364d0b90f..f330f061b0 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -206,12 +206,7 @@ jobs: (jupyter lab --config panel/tests/ui/jupyter_server_test_config.py --port 8887 > /tmp/jupyterlab_server.log 2>&1) & - name: Build JupyterLite shell: pixi run -e test-ui bash -el {0} - run: | - # TODO: Make this a pixi feature/environment - python -m pip install -r ./lite/requirements.txt - python ./scripts/build_pyodide_wheels.py lite/pypi - python ./scripts/panelite/generate_panelite_content.py - jupyter lite build --lite-dir lite --output-dir lite/dist + run: pixi run -e lite lite-build - name: Wait for JupyterLab uses: ifaxity/wait-on-action@v1.2.1 with: diff --git a/lite/requirements.txt b/lite/requirements.txt deleted file mode 100644 index 57303026b7..0000000000 --- a/lite/requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -awscli -bokeh -ipywidgets -jupyter_bokeh -jupyterlab -jupyterlab-open-url-parameter -jupyterlite-core>=0.4.0 -jupyterlite-pyodide-kernel -nbformat -pkginfo -pyviz_comms diff --git a/panel/.eslintrc.js b/panel/.eslintrc.js index 66a06d4024..b31a84de68 100644 --- a/panel/.eslintrc.js +++ b/panel/.eslintrc.js @@ -9,7 +9,7 @@ module.exports = { }, "plugins": ["@typescript-eslint", "@stylistic/eslint-plugin"], "extends": [], - "ignorePatterns": ["*/dist", "*/theme/**/*.js", ".eslintrc.js", "*/_templates/*.js", "*/template/**/*.js", "examples/*"], + "ignorePatterns": ["*/dist", "*/theme/**/*.js", ".eslintrc.js", "*/_templates/*.js", "*/template/**/*.js", "examples/*", "scripts/*"], "rules": { "@typescript-eslint/ban-types": ["error", { "types": { diff --git a/pixi.toml b/pixi.toml index 6cbdb658cb..1bf6be8d20 100644 --- a/pixi.toml +++ b/pixi.toml @@ -20,6 +20,7 @@ test-type = ["py311", "type"] docs = ["py311", "example", "doc"] build = ["py311", "build"] lint = ["py311", "lint"] +lite = ["py311", "lite"] [dependencies] bleach = "*" @@ -213,3 +214,18 @@ pre-commit = "*" [feature.lint.tasks] lint = 'pre-commit run --all-files' lint-install = 'pre-commit install' + +# ============================================= +# =================== LITE ==================== +# ============================================= +[feature.lite.dependencies] +jupyterlab-myst = "*" +jupyterlite-core = "*" +jupyterlite-pyodide-kernel = "*" +python-build = "*" + +[feature.lite.tasks] +lite-build = "bash scripts/jupyterlite/build.sh" +# Service worker only work on 127.0.0.1 +# https://jupyterlite.readthedocs.io/en/latest/howto/configure/advanced/service-worker.html#limitations +lite-server = "python -m http.server --directory ./lite/dist --bind 127.0.0.1" diff --git a/pyproject.toml b/pyproject.toml index a51b256a35..86444af24c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -116,6 +116,7 @@ include = ["panel"] [tool.hatch.build.targets.sdist] include = ["panel", "scripts", "examples"] +exclude = ["scripts/jupyterlite"] [tool.hatch.build.targets.sdist.force-include] "panel/dist" = "panel/dist" diff --git a/scripts/jupyterlite/build.sh b/scripts/jupyterlite/build.sh new file mode 100644 index 0000000000..35d30bf114 --- /dev/null +++ b/scripts/jupyterlite/build.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +python ./scripts/build_pyodide_wheels.py dist +python ./scripts/panelite/generate_panelite_content.py + +# Update lockfiles +cd "$(dirname "${BASH_SOURCE[0]}")" +rm -rf node_modules +npm install . +node update_lock.js +python patch_lock.py +rm node_modules/pyodide/*.whl + +jupyter lite build + +cp -r node_modules/pyodide/ ../../lite/dist/pyodide +mv pyodide-lock.json ../../lite/dist/pyodide/pyodide-lock.json +mv ../../dist/* ../../lite/dist/pyodide diff --git a/scripts/jupyterlite/extra_packages.json b/scripts/jupyterlite/extra_packages.json new file mode 100644 index 0000000000..3d15b58add --- /dev/null +++ b/scripts/jupyterlite/extra_packages.json @@ -0,0 +1,5 @@ +[ + "panel", + "bokeh", + "pyodide-http" +] diff --git a/lite/files/Getting_Started.ipynb b/scripts/jupyterlite/files/Getting_Started.ipynb similarity index 94% rename from lite/files/Getting_Started.ipynb rename to scripts/jupyterlite/files/Getting_Started.ipynb index 04e11635e5..92a27bb37f 100644 --- a/lite/files/Getting_Started.ipynb +++ b/scripts/jupyterlite/files/Getting_Started.ipynb @@ -5,7 +5,7 @@ "id": "8c91012d-3445-4052-ab2f-129ca785a666", "metadata": {}, "source": [ - "Panel is not installed by default in the Pyodide distribution that JupyterLite is built on, therefore we must install it manually:" + "Panel is installed by default in the Pyodide distribution that JupyterLite is built on. Though, not all dependencies are therefore we must install it manually:" ] }, { @@ -16,7 +16,7 @@ "outputs": [], "source": [ "import piplite\n", - "await piplite.install(['panel', 'pyodide-http', 'altair'])" + "await piplite.install(['altair'])" ] }, { diff --git a/lite/files/Reset_Jupyterlite.ipynb b/scripts/jupyterlite/files/Reset_Jupyterlite.ipynb similarity index 99% rename from lite/files/Reset_Jupyterlite.ipynb rename to scripts/jupyterlite/files/Reset_Jupyterlite.ipynb index 7f70f88361..f88985ff64 100644 --- a/lite/files/Reset_Jupyterlite.ipynb +++ b/scripts/jupyterlite/files/Reset_Jupyterlite.ipynb @@ -57,5 +57,5 @@ } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/scripts/jupyterlite/jupyter-lite.json b/scripts/jupyterlite/jupyter-lite.json new file mode 100644 index 0000000000..b8f4613bf6 --- /dev/null +++ b/scripts/jupyterlite/jupyter-lite.json @@ -0,0 +1,10 @@ +{ + "jupyter-lite-schema-version": 0, + "jupyter-config-data": { + "litePluginSettings": { + "@jupyterlite/pyodide-kernel-extension:kernel": { + "pyodideUrl": "./pyodide/pyodide.js" + } + } + } + } diff --git a/scripts/jupyterlite/jupyter_lite_config.json b/scripts/jupyterlite/jupyter_lite_config.json new file mode 100644 index 0000000000..4385c1d261 --- /dev/null +++ b/scripts/jupyterlite/jupyter_lite_config.json @@ -0,0 +1,6 @@ +{ + "LiteBuildConfig": { + "contents": ["../../lite/files", "files"], + "output_dir": "../../lite/dist" + } +} diff --git a/scripts/jupyterlite/package-lock.json b/scripts/jupyterlite/package-lock.json new file mode 100644 index 0000000000..e0e1b1595f --- /dev/null +++ b/scripts/jupyterlite/package-lock.json @@ -0,0 +1,44 @@ +{ + "name": "jupyterlite", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "pyodide": "^0.26.2" + } + }, + "node_modules/pyodide": { + "version": "0.26.2", + "resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.26.2.tgz", + "integrity": "sha512-8VCRdFX83gBsWs6XP2rhG8HMaB+JaVyyav4q/EMzoV8fXH8HN6T5IISC92SNma6i1DRA3SVXA61S1rJcB8efgA==", + "license": "Apache-2.0", + "dependencies": { + "ws": "^8.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/ws": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/scripts/jupyterlite/package.json b/scripts/jupyterlite/package.json new file mode 100644 index 0000000000..b2fcd7fe76 --- /dev/null +++ b/scripts/jupyterlite/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "pyodide": "^0.26.2" + } +} diff --git a/scripts/jupyterlite/patch_lock.py b/scripts/jupyterlite/patch_lock.py new file mode 100644 index 0000000000..cb083c2873 --- /dev/null +++ b/scripts/jupyterlite/patch_lock.py @@ -0,0 +1,47 @@ +import hashlib +import json +import os.path + +from glob import glob + +from packaging.utils import parse_wheel_filename + + +def calculate_sha256(file_path): + sha256_hash = hashlib.sha256() + + with open(file_path, "rb") as file: + for byte_block in iter(lambda: file.read(4096), b""): + sha256_hash.update(byte_block) + + return sha256_hash.hexdigest() + + +with open("package.json") as f: + package = json.load(f) +pyodide_version = package["dependencies"]["pyodide"].removeprefix("^") + +path = "pyodide-lock.json" +url = f"https://cdn.jsdelivr.net/pyodide/v{pyodide_version}/full" + +with open(path) as f: + data = json.load(f) + +for p in data["packages"].values(): + if not p["file_name"].startswith("http"): + p["file_name"] = f'{url}/{p["file_name"]}' + + +whl_files = glob("../../dist/*.whl") +for whl_file in whl_files: + name, version, *_ = parse_wheel_filename(os.path.basename(whl_file)) + + package = data["packages"][name] + package["version"] = str(version) + package["file_name"] = os.path.basename(whl_file) + package["sha256"] = calculate_sha256(whl_file) + package["imports"] = [name] + + +with open(path, "w") as f: + data = json.dump(data, f) diff --git a/scripts/jupyterlite/update_lock.js b/scripts/jupyterlite/update_lock.js new file mode 100644 index 0000000000..c46e1fa0df --- /dev/null +++ b/scripts/jupyterlite/update_lock.js @@ -0,0 +1,20 @@ +const { loadPyodide } = require("pyodide"); +const fs = require("fs"); + +async function main() { + const extra = fs.readFileSync("extra_packages.json", "utf8"); + + let pyodide = await loadPyodide(); + await pyodide.loadPackage(["micropip"]); + + output = await pyodide.runPythonAsync(` +import json +import micropip +extra = json.loads("""${extra}""") +await micropip.install(extra) +micropip.freeze() +`); + fs.writeFileSync("pyodide-lock.json", output); +} + +main(); diff --git a/scripts/panelite/generate_panelite_content.py b/scripts/panelite/generate_panelite_content.py index d23f6532c4..1038017f07 100644 --- a/scripts/panelite/generate_panelite_content.py +++ b/scripts/panelite/generate_panelite_content.py @@ -17,7 +17,7 @@ EXAMPLES_DIR = PANEL_BASE / 'examples' LITE_FILES = PANEL_BASE / 'lite' / 'files' DOC_DIR = PANEL_BASE / 'doc' -BASE_DEPENDENCIES = ['panel', 'pyodide-http'] +BASE_DEPENDENCIES = [] MINIMUM_VERSIONS = {} INLINE_DIRECTIVE = re.compile('\{.*\}`.*`\s*') From 4cf754fad69a1bbc25b828e42b3c70ea3614d580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 13 Aug 2024 14:13:52 +0200 Subject: [PATCH 64/91] Don't install mkl on Windows (#7135) --- pixi.toml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pixi.toml b/pixi.toml index 1bf6be8d20..a216fa1a3e 100644 --- a/pixi.toml +++ b/pixi.toml @@ -23,18 +23,20 @@ lint = ["py311", "lint"] lite = ["py311", "lite"] [dependencies] +nodejs = ">=18" +nomkl = "*" +pip = "*" +# Required bleach = "*" bokeh = ">=3.5.0,<3.6.0" linkify-it-py = "*" markdown = "*" markdown-it-py = "*" mdit-py-plugins = "*" -nodejs = ">=18" -numpy = "<2.0" # Temporary pin until panel release with support for bokeh 3.5.0 is available +numpy = "*" packaging = "*" pandas = ">=1.2" param = ">=2.1.0,<3.0" -pip = "*" pyviz_comms = ">=2.0.0" requests = "*" tqdm = ">=4.48.0" From a9f8c76a6521756ab922f4b59b768782216e2a7c Mon Sep 17 00:00:00 2001 From: Christoph Deil Date: Tue, 13 Aug 2024 13:14:34 +0100 Subject: [PATCH 65/91] Fix some PyCharm warnings (#7120) Co-authored-by: Philipp Rudiger --- README.md | 2 +- binder/jupyter-panel-apps-server/jupyter_panel_apps_server.py | 2 +- codecov.yml | 2 +- examples/how_to/custom/react/material_ui.py | 2 -- panel/io/cache.py | 1 - scripts/panelite/generate_panelite_content.py | 1 - scripts/panelite/test/test_panelite.py | 4 ++-- 7 files changed, 5 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index c8b12f06e0..072d059313 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Enjoying Panel? Show your support with a [Github star](https://github.com/holovi Downloads - +PyPi Downloads Conda Downloads Build Status diff --git a/binder/jupyter-panel-apps-server/jupyter_panel_apps_server.py b/binder/jupyter-panel-apps-server/jupyter_panel_apps_server.py index d661d41dac..847c6c6cbb 100644 --- a/binder/jupyter-panel-apps-server/jupyter_panel_apps_server.py +++ b/binder/jupyter-panel-apps-server/jupyter_panel_apps_server.py @@ -20,7 +20,7 @@ def get_apps(): return [ app for app in glob("examples/gallery/**/*.ipynb", recursive=True) - if not app in DONT_SERVE + if app not in DONT_SERVE ] diff --git a/codecov.yml b/codecov.yml index b6ee7c18e8..bec5dc77f5 100644 --- a/codecov.yml +++ b/codecov.yml @@ -7,4 +7,4 @@ coverage: threshold: 0.5% comment: - require_changes: yes + require_changes: true diff --git a/examples/how_to/custom/react/material_ui.py b/examples/how_to/custom/react/material_ui.py index 262ac7ee29..a63ea4eef6 100644 --- a/examples/how_to/custom/react/material_ui.py +++ b/examples/how_to/custom/react/material_ui.py @@ -1,5 +1,3 @@ -import pathlib - import param import panel as pn diff --git a/panel/io/cache.py b/panel/io/cache.py index 54b3b6d3be..8a806b879c 100644 --- a/panel/io/cache.py +++ b/panel/io/cache.py @@ -14,7 +14,6 @@ import sys import threading import time -import unittest import unittest.mock import weakref diff --git a/scripts/panelite/generate_panelite_content.py b/scripts/panelite/generate_panelite_content.py index 1038017f07..a6251e8cc4 100644 --- a/scripts/panelite/generate_panelite_content.py +++ b/scripts/panelite/generate_panelite_content.py @@ -1,7 +1,6 @@ """ Helper script to convert and copy example notebooks into JupyterLite build. """ -import hashlib import json import os import pathlib diff --git a/scripts/panelite/test/test_panelite.py b/scripts/panelite/test/test_panelite.py index a79cd6cc32..e969737963 100644 --- a/scripts/panelite/test/test_panelite.py +++ b/scripts/panelite/test/test_panelite.py @@ -57,7 +57,7 @@ def get_panelite_nb_paths(): nbs = list(FILES.glob('*/*/*.ipynb')) + list(FILES.glob('*/*.*')) for nb in nbs: path = str(nb).replace("\\", "/").split("files/")[-1] - if path.endswith(".ipynb") and not ".ipynb_checkpoints" in path: + if path.endswith(".ipynb") and ".ipynb_checkpoints" not in path: yield path PATHS_WITH_NOTHING_TO_TEST = [ "gallery/demos/attractors.ipynb", @@ -66,7 +66,7 @@ def get_panelite_nb_paths(): "gallery/demos/nyc_taxi.ipynb", "gallery/demos/portfolio-optimizer.ipynb", ] -PATHS = list(path for path in get_panelite_nb_paths() if not path in PATHS_WITH_NOTHING_TO_TEST) +PATHS = list(path for path in get_panelite_nb_paths() if path not in PATHS_WITH_NOTHING_TO_TEST) PATHS_WITHOUT_ISSUES = list(path for path in PATHS if path not in NOTEBOOK_ISSUES) PATHS_WITH_ISSUES = list(path for path in PATHS if path in NOTEBOOK_ISSUES) From a090eb6ac3d258913b062d92afa642e40cf91798 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 13 Aug 2024 14:41:03 +0200 Subject: [PATCH 66/91] Ensure multiselect Tabulator header filter uses 'in' filter function (#7111) --- panel/tests/widgets/test_tables.py | 6 +++--- panel/widgets/tables.py | 7 +++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/panel/tests/widgets/test_tables.py b/panel/tests/widgets/test_tables.py index d0e6f3ae6c..38a781283f 100644 --- a/panel/tests/widgets/test_tables.py +++ b/panel/tests/widgets/test_tables.py @@ -663,7 +663,7 @@ def test_tabulator_header_filters_column_config_list(document, comm): {'field': 'index', 'sorter': 'number'}, {'field': 'A', 'sorter': 'number'}, {'field': 'B', 'sorter': 'number'}, - {'field': 'C', 'headerFilter': 'list', 'headerFilterParams': {'valuesLookup': True}}, + {'field': 'C', 'headerFilter': 'list', 'headerFilterParams': {'valuesLookup': True}, 'headerFilterFunc': 'in'}, {'field': 'D', 'sorter': 'timestamp'} ] assert model.configuration['selectable'] == True @@ -682,8 +682,8 @@ def test_tabulator_header_filters_column_config_select_autocomplete_backwards_co {'field': 'index', 'sorter': 'number'}, {'field': 'A', 'sorter': 'number'}, {'field': 'B', 'sorter': 'number'}, - {'field': 'C', 'headerFilter': 'list', 'headerFilterParams': {'valuesLookup': True}}, - {'field': 'D', 'headerFilter': 'list', 'headerFilterParams': {'valuesLookup': True}, 'sorter': 'timestamp'}, + {'field': 'C', 'headerFilter': 'list', 'headerFilterParams': {'valuesLookup': True}, 'headerFilterFunc': 'in'}, + {'field': 'D', 'headerFilter': 'list', 'headerFilterParams': {'valuesLookup': True}, 'sorter': 'timestamp', 'headerFilterFunc': 'in'}, ] assert model.configuration['selectable'] == True diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index 4dc66d7dc3..4b9a4794dc 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -1851,8 +1851,11 @@ def _get_filter_spec(self, column: TableColumn) -> dict[str, Any]: ) del filter_params['values'] filter_params['valuesLookup'] = True - if filter_type == 'list' and not filter_params: - filter_params = {'valuesLookup': True} + if filter_type == 'list': + if not filter_params: + filter_params = {'valuesLookup': True} + if filter_func is None: + filter_func = 'in' fspec['headerFilter'] = filter_type if filter_params: fspec['headerFilterParams'] = filter_params From 3308974c810fa79fa6002a50bff3da3c93857691 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 13 Aug 2024 14:41:46 +0200 Subject: [PATCH 67/91] Add ESM shimMode to docs (#7141) --- doc/conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index ed41e65fe8..0d814fefdc 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -169,8 +169,9 @@ def get_requirements(): nbsite_pyodide_conf = { 'PYODIDE_URL': f'https://cdn.jsdelivr.net/pyodide/{PYODIDE_VERSION}/full/pyodide.js', + 'preamble': '', 'requirements': [bokeh_req, panel_req, 'pyodide-http'], - 'requires': get_requirements() + 'requires': get_requirements(), } templates_path += [ From c620eb665d719d42fbf03dd1d09998010aff6a74 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 13 Aug 2024 14:43:06 +0200 Subject: [PATCH 68/91] Bump panel.js version to 1.5.0-b.4 --- panel/package-lock.json | 4 ++-- panel/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/panel/package-lock.json b/panel/package-lock.json index 153f1ea44d..9e774a6679 100644 --- a/panel/package-lock.json +++ b/panel/package-lock.json @@ -1,12 +1,12 @@ { "name": "@holoviz/panel", - "version": "1.5.0-b.3", + "version": "1.5.0-b.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@holoviz/panel", - "version": "1.5.0-b.3", + "version": "1.5.0-b.4", "license": "BSD-3-Clause", "dependencies": { "@bokeh/bokehjs": "3.5.1", diff --git a/panel/package.json b/panel/package.json index aa7d4efc98..c31a24c88d 100644 --- a/panel/package.json +++ b/panel/package.json @@ -1,6 +1,6 @@ { "name": "@holoviz/panel", - "version": "1.5.0-b.3", + "version": "1.5.0-b.4", "description": "The powerful data exploration & web app framework for Python.", "license": "BSD-3-Clause", "repository": { From 1ce03a26912b49f9a9b115c312e819391ab08f2c Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 13 Aug 2024 14:48:26 +0200 Subject: [PATCH 69/91] Avoid param warning when passing reference to Child param (#7140) --- panel/viewable.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/panel/viewable.py b/panel/viewable.py index ccb35b085e..c75f61d20b 100644 --- a/panel/viewable.py +++ b/panel/viewable.py @@ -1093,7 +1093,7 @@ def __init__( ): ... - def __init__(self, /, default=Undefined, class_=Viewable, **params): + def __init__(self, /, default=Undefined, class_=Viewable, allow_refs=False, **params): if isinstance(class_, type) and not issubclass(class_, Viewable): raise TypeError( f"Child.class_ must be an instance of Viewable, not {type(class_)}." @@ -1103,7 +1103,10 @@ def __init__(self, /, default=Undefined, class_=Viewable, **params): raise TypeError( f"Child.class_ must be an instance of Viewable, not {invalid}." ) - super().__init__(default=self._transform_value(default), class_=class_, **params) + super().__init__( + default=self._transform_value(default), class_=class_, + allow_refs=allow_refs, **params + ) def _transform_value(self, val): if not isinstance(val, Viewable) and val not in (None, Undefined): From a3959ec89c09a082fe43779ee7e56ce7d65c38cf Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Wed, 14 Aug 2024 08:05:57 -0700 Subject: [PATCH 70/91] Show interval value on click (#7064) --- panel/dist/css/player.css | 8 ++++++++ panel/models/player.ts | 29 +++++++++++++++++++++++++-- panel/models/widgets.py | 4 ++++ panel/tests/ui/widgets/test_player.py | 27 +++++++++++++++++++++++++ panel/widgets/player.py | 4 ++++ 5 files changed, 70 insertions(+), 2 deletions(-) diff --git a/panel/dist/css/player.css b/panel/dist/css/player.css index 1b0bb61ad5..6812c70dea 100644 --- a/panel/dist/css/player.css +++ b/panel/dist/css/player.css @@ -1,3 +1,11 @@ +.faster { + font-size: 11px; +} + +.slower { + font-size: 11px; +} + .pn-player-value { font-weight: bold; } diff --git a/panel/models/player.ts b/panel/models/player.ts index 71d876ce3a..48a5f3c9f8 100644 --- a/panel/models/player.ts +++ b/panel/models/player.ts @@ -78,6 +78,8 @@ export class PlayerView extends WidgetView { protected _toogle_pause: CallableFunction protected _toggle_play: CallableFunction protected _changing: boolean = false + protected slowerButton: HTMLButtonElement + protected fasterButton: HTMLButtonElement override connect_signals(): void { super.connect_signals() @@ -160,61 +162,72 @@ export class PlayerView extends WidgetView { this.buttonEl = button_div button_div.style.cssText = "margin: 0 auto; display: flex; padding: 5px; align-items: stretch; width: 100%;" - const button_style_small = "text-align: center; min-width: 20px; flex-grow: 1; margin: 2px" - const button_style = "text-align: center; min-width: 40px; flex-grow: 2; margin: 2px" + const button_style_small = "text-align: center; min-width: 50px; flex-grow: 1; margin: 2px" + const button_style = "text-align: center; min-width: 50px; flex-grow: 2; margin: 2px" const slower = document.createElement("button") + slower.classList.add("slower") slower.style.cssText = button_style_small slower.innerHTML = SVG_STRINGS.slower slower.onclick = () => this.slower() + this.slowerButton = slower button_div.appendChild(slower) const first = document.createElement("button") + first.classList.add("first") first.style.cssText = button_style first.innerHTML = SVG_STRINGS.first first.onclick = () => this.first_frame() button_div.appendChild(first) const previous = document.createElement("button") + previous.classList.add("previous") previous.style.cssText = button_style previous.innerHTML = SVG_STRINGS.previous previous.onclick = () => this.previous_frame() button_div.appendChild(previous) const reverse = document.createElement("button") + reverse.classList.add("reverse") reverse.style.cssText = button_style reverse.innerHTML = SVG_STRINGS.reverse reverse.onclick = () => this.reverse_animation() button_div.appendChild(reverse) const pause = document.createElement("button") + pause.classList.add("pause") pause.style.cssText = button_style pause.innerHTML = SVG_STRINGS.pause pause.onclick = () => this.pause_animation() button_div.appendChild(pause) const play = document.createElement("button") + play.classList.add("play") play.style.cssText = button_style play.innerHTML = SVG_STRINGS.play play.onclick = () => this.play_animation() button_div.appendChild(play) const next = document.createElement("button") + next.classList.add("next") next.style.cssText = button_style next.innerHTML = SVG_STRINGS.next next.onclick = () => this.next_frame() button_div.appendChild(next) const last = document.createElement("button") + last.classList.add("last") last.style.cssText = button_style last.innerHTML = SVG_STRINGS.last last.onclick = () => this.last_frame() button_div.appendChild(last) const faster = document.createElement("button") + faster.classList.add("faster") faster.style.cssText = button_style_small faster.innerHTML = SVG_STRINGS.faster faster.onclick = () => this.faster() + this.fasterButton = faster button_div.appendChild(faster) // toggle @@ -384,8 +397,17 @@ export class PlayerView extends WidgetView { this.set_frame(this.model.end) } + updateSpeedButton(button: HTMLButtonElement, interval: number, originalSVG: string): void { + const fps = 1000 / interval + button.innerHTML = `${fps.toFixed(1)}
fps` + setTimeout(() => { + button.innerHTML = originalSVG + }, this.model.preview_duration) // Show for 1.5 seconds + } + slower(): void { this.model.interval = Math.round(this.model.interval / 0.7) + this.updateSpeedButton(this.slowerButton, this.model.interval, SVG_STRINGS.slower) if (this.model.direction > 0) { this.play_animation() } else if (this.model.direction < 0) { @@ -395,6 +417,7 @@ export class PlayerView extends WidgetView { faster(): void { this.model.interval = Math.round(this.model.interval * 0.7) + this.updateSpeedButton(this.fasterButton, this.model.interval, SVG_STRINGS.faster) if (this.model.direction > 0) { this.play_animation() } else if (this.model.direction < 0) { @@ -497,6 +520,7 @@ export namespace Player { value: p.Property value_align: p.Property value_throttled: p.Property + preview_duration: p.Property show_loop_controls: p.Property show_value: p.Property } @@ -528,6 +552,7 @@ export class Player extends Widget { value: [Int, 0], value_align: [Str, "start"], value_throttled: [Int, 0], + preview_duration: [Int, 1500], show_loop_controls: [Bool, true], show_value: [Bool, true], })) diff --git a/panel/models/widgets.py b/panel/models/widgets.py index 8504fcddda..3f10ff51f7 100644 --- a/panel/models/widgets.py +++ b/panel/models/widgets.py @@ -59,6 +59,10 @@ class Player(Widget): show_loop_controls = Bool(True, help="""Whether the loop controls radio buttons are shown""") + preview_duration = Int(1500, help=""" + Duration (in milliseconds) for showing the current FPS when clicking + the slower/faster buttons, before reverting to the icon.""") + show_value = Bool(True, help=""" Whether to show the widget value""") diff --git a/panel/tests/ui/widgets/test_player.py b/panel/tests/ui/widgets/test_player.py index fde255d6a8..25666728ee 100644 --- a/panel/tests/ui/widgets/test_player.py +++ b/panel/tests/ui/widgets/test_player.py @@ -10,6 +10,32 @@ pytestmark = pytest.mark.ui +def test_player_faster_click_shows_ms(page): + player = Player() + serve_component(page, player) + + faster_element = page.locator(".faster") + faster_element.click() + + wait_until(lambda: player.interval == 350) + assert faster_element.inner_text() == "2.9\nfps" + + wait_until(lambda: faster_element.inner_text() == "") + + +def test_player_slower_click_shows_ms(page): + player = Player() + serve_component(page, player) + + slower_element = page.locator(".slower") + slower_element.click() + + wait_until(lambda: player.interval == 714) + assert slower_element.inner_text() == "1.4\nfps" + + wait_until(lambda: slower_element.inner_text() == "") + + def test_init(page): player = Player() serve_component(page, player) @@ -17,6 +43,7 @@ def test_init(page): assert not page.is_visible('pn-player-value') assert page.query_selector('.pn-player-value') is None + def test_show_value(page): player = Player(show_value=True) serve_component(page, player) diff --git a/panel/widgets/player.py b/panel/widgets/player.py index 565d6489da..e800f02925 100644 --- a/panel/widgets/player.py +++ b/panel/widgets/player.py @@ -34,6 +34,10 @@ class PlayerBase(Widget): default='once', objects=['once', 'loop', 'reflect'], doc=""" Policy used when player hits last frame""") + preview_duration = param.Integer(default=1500, bounds=(0, None), doc=""" + Duration (in milliseconds) for showing the current FPS when clicking + the slower/faster buttons, before reverting to the icon.""") + show_loop_controls = param.Boolean(default=True, doc=""" Whether the loop controls radio buttons are shown""") From 0b76e99f439620ebe27b31fd48a3a9f176113eb2 Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Wed, 14 Aug 2024 09:48:31 -0700 Subject: [PATCH 71/91] Minify Player/DiscretePlayer with scale_buttons/visible_buttons/visible_loop_options (#7065) * scale player button * add support for visible loop options and buttons * add and fix test * add docs * pre-commit * tweak ts * add ui test * Apply suggestions from code review Co-authored-by: Philipp Rudiger * fix merge --------- Co-authored-by: Philipp Rudiger --- .../reference/widgets/DiscretePlayer.ipynb | 20 ++ examples/reference/widgets/Player.ipynb | 20 ++ panel/models/player.ts | 288 ++++++++++-------- panel/models/widgets.py | 9 + panel/tests/ui/widgets/test_player.py | 42 +++ panel/tests/widgets/test_player.py | 12 + panel/widgets/player.py | 16 +- 7 files changed, 272 insertions(+), 135 deletions(-) diff --git a/examples/reference/widgets/DiscretePlayer.ipynb b/examples/reference/widgets/DiscretePlayer.ipynb index bcc189b78b..36599f913c 100644 --- a/examples/reference/widgets/DiscretePlayer.ipynb +++ b/examples/reference/widgets/DiscretePlayer.ipynb @@ -37,9 +37,12 @@ "\n", "* **``disabled``** (boolean): Whether the widget is editable\n", "* **``name``** (str): The title of the widget\n", + "* **``scale_buttons``** (float): The scaling factor to resize the buttons\n", "* **``show_loop_controls``** (boolean): Whether radio buttons allowing to switch between loop policies options are shown\n", "* **``show_value``** (boolean): Whether to display the value of the player\n", "* **``value_align``** (str): Where to display the value; must be one of 'start', 'center', 'end'\n", + "* **``visible_buttons``** (list[str]): The buttons to display on the player ('slower', 'first', 'previous', 'reverse', 'pause', 'play', 'next', 'last', 'faster')\n", + "* **``visible_loop_options``** (list[str]): The loop options to display on the player. ('once', 'loop', 'reflect')\n", "\n", "___" ] @@ -107,6 +110,23 @@ "discrete_player.pause()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `DiscretePlayer` can be slimmed down by setting `scale_buttons`, `show_loop_controls`, `visible_buttons`, and/or `visible_loop_options`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "discrete_player = pn.widgets.DiscretePlayer(name='Player', visible_buttons=[\"play\", \"pause\"], scale_buttons=0.9, show_loop_controls=False, width=150)\n", + "discrete_player" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/examples/reference/widgets/Player.ipynb b/examples/reference/widgets/Player.ipynb index d210a01038..212b882825 100644 --- a/examples/reference/widgets/Player.ipynb +++ b/examples/reference/widgets/Player.ipynb @@ -39,9 +39,12 @@ "\n", "* **``disabled``** (boolean): Whether the widget is editable\n", "* **``name``** (str): The title of the widget\n", + "* **``scale_buttons``** (float): The scaling factor to resize the buttons\n", "* **``show_loop_controls``** (boolean): Whether radio buttons allowing to switch between loop policies options are shown\n", "* **``show_value``** (boolean): Whether to display the value of the player\n", "* **``value_align``** (str): Where to display the value; must be one of 'start', 'center', 'end'\n", + "* **``visible_buttons``** (list[str]): The buttons to display on the player ('slower', 'first', 'previous', 'reverse', 'pause', 'play', 'next', 'last', 'faster')\n", + "* **``visible_loop_options``** (list[str]): The loop options to display on the player. ('once', 'loop', 'reflect')\n", "\n", "___" ] @@ -109,6 +112,23 @@ "player.pause()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `Player` can be slimmed down by setting `scale_buttons`, `show_loop_controls`, `visible_buttons`, and/or `visible_loop_options`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "player = pn.widgets.Player(name='Player', visible_buttons=[\"play\", \"pause\"], scale_buttons=0.9, show_loop_controls=False, width=150)\n", + "player" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/panel/models/player.ts b/panel/models/player.ts index 48a5f3c9f8..591da8537e 100644 --- a/panel/models/player.ts +++ b/panel/models/player.ts @@ -5,56 +5,15 @@ import {Widget, WidgetView} from "@bokehjs/models/widgets/widget" import {to_string} from "@bokehjs/core/util/pretty" const SVG_STRINGS = { - slower: '', - first: '\ - ', - previous: ' \ - ', - reverse: '', - pause: '', - play: '', - next: ' \ - ', - last: '', - faster: '', + slower: '', + first: '', + previous: '', + reverse: '', + pause: '', + play: '', + next: '', + last: '', + faster: '', } function press(btn_list: HTMLButtonElement[]): void { @@ -78,13 +37,21 @@ export class PlayerView extends WidgetView { protected _toogle_pause: CallableFunction protected _toggle_play: CallableFunction protected _changing: boolean = false - protected slowerButton: HTMLButtonElement - protected fasterButton: HTMLButtonElement + + protected slower: HTMLButtonElement + protected first: HTMLButtonElement + protected previous: HTMLButtonElement + protected reverse: HTMLButtonElement + protected pause: HTMLButtonElement + protected play: HTMLButtonElement + protected next: HTMLButtonElement + protected last: HTMLButtonElement + protected faster: HTMLButtonElement override connect_signals(): void { super.connect_signals() - const {title, value_align, direction, value, loop_policy, disabled, show_loop_controls, show_value} = this.model.properties + const {title, value_align, direction, value, loop_policy, disabled, show_loop_controls, show_value, scale_buttons, visible_buttons, visible_loop_options} = this.model.properties this.on_change(title, () => this.update_title_and_value()) this.on_change(value_align, () => this.set_value_align()) this.on_change(direction, () => this.set_direction()) @@ -99,7 +66,9 @@ export class PlayerView extends WidgetView { } }) this.on_change(show_value, () => this.update_title_and_value()) - + this.on_change(scale_buttons, () => this.update_css()) + this.on_change(visible_buttons, () => this.update_css()) + this.on_change(visible_loop_options, () => this.update_css()) } toggle_disable() { @@ -121,6 +90,56 @@ export class PlayerView extends WidgetView { return 250 } + update_css(): void { + const button_style_small = `text-align: center; flex-grow: 1; margin: 2px; transform: scale(${this.model.scale_buttons}); max-width: 50px;` + const button_style = `text-align: center; flex-grow: 2; margin: 2px; transform: scale(${this.model.scale_buttons}); max-width: 50px;` + + const buttons = { + slower: this.slower, + first: this.first, + previous: this.previous, + reverse: this.reverse, + pause: this.pause, + play: this.play, + next: this.next, + last: this.last, + faster: this.faster, + } + + for (const [name, button] of Object.entries(buttons)) { + if (button) { + if (this.model.visible_buttons.includes(name)) { + button.style.display = "" // Reset to default display + if (name === "slower" || name === "faster") { + button.style.cssText += button_style_small + } else { + button.style.cssText += button_style + } + } else { + button.style.display = "none" // Hide the button completely + } + } + } + + for (const el of this.loop_state.children) { + if (el.tagName.toLowerCase() == "input") { + const anyEl = el as any + if (this.model.visible_loop_options.includes(anyEl.value)) { + anyEl.style.display = "" + } else { + anyEl.style.display = "none" + } + } else if (el.tagName.toLowerCase() == "label") { + const anyEl = el as any + if (this.model.visible_loop_options.includes(anyEl.innerHTML.toLowerCase())) { + anyEl.style.display = "" + } else { + anyEl.style.display = "none" + } + } + } + } + override render(): void { if (this.sliderEl == null) { super.render() @@ -160,88 +179,74 @@ export class PlayerView extends WidgetView { // Buttons const button_div = div() as any this.buttonEl = button_div - button_div.style.cssText = "margin: 0 auto; display: flex; padding: 5px; align-items: stretch; width: 100%;" - - const button_style_small = "text-align: center; min-width: 50px; flex-grow: 1; margin: 2px" - const button_style = "text-align: center; min-width: 50px; flex-grow: 2; margin: 2px" - - const slower = document.createElement("button") - slower.classList.add("slower") - slower.style.cssText = button_style_small - slower.innerHTML = SVG_STRINGS.slower - slower.onclick = () => this.slower() - this.slowerButton = slower - button_div.appendChild(slower) - - const first = document.createElement("button") - first.classList.add("first") - first.style.cssText = button_style - first.innerHTML = SVG_STRINGS.first - first.onclick = () => this.first_frame() - button_div.appendChild(first) - - const previous = document.createElement("button") - previous.classList.add("previous") - previous.style.cssText = button_style - previous.innerHTML = SVG_STRINGS.previous - previous.onclick = () => this.previous_frame() - button_div.appendChild(previous) - - const reverse = document.createElement("button") - reverse.classList.add("reverse") - reverse.style.cssText = button_style - reverse.innerHTML = SVG_STRINGS.reverse - reverse.onclick = () => this.reverse_animation() - button_div.appendChild(reverse) - - const pause = document.createElement("button") - pause.classList.add("pause") - pause.style.cssText = button_style - pause.innerHTML = SVG_STRINGS.pause - pause.onclick = () => this.pause_animation() - button_div.appendChild(pause) - - const play = document.createElement("button") - play.classList.add("play") - play.style.cssText = button_style - play.innerHTML = SVG_STRINGS.play - play.onclick = () => this.play_animation() - button_div.appendChild(play) - - const next = document.createElement("button") - next.classList.add("next") - next.style.cssText = button_style - next.innerHTML = SVG_STRINGS.next - next.onclick = () => this.next_frame() - button_div.appendChild(next) - - const last = document.createElement("button") - last.classList.add("last") - last.style.cssText = button_style - last.innerHTML = SVG_STRINGS.last - last.onclick = () => this.last_frame() - button_div.appendChild(last) - - const faster = document.createElement("button") - faster.classList.add("faster") - faster.style.cssText = button_style_small - faster.innerHTML = SVG_STRINGS.faster - faster.onclick = () => this.faster() - this.fasterButton = faster - button_div.appendChild(faster) + button_div.style.cssText = "margin: 0 auto; display: flex; padding: 5px; align-items: stretch; justify-content: center; width: 100%;" + + this.slower = document.createElement("button") + this.slower.classList.add("slower") + this.slower.innerHTML = SVG_STRINGS.slower + this.slower.onclick = () => this.slower_speed() + button_div.appendChild(this.slower) + + this.first = document.createElement("button") + this.first.classList.add("first") + this.first.innerHTML = SVG_STRINGS.first + this.first.onclick = () => this.first_frame() + button_div.appendChild(this.first) + + this.previous = document.createElement("button") + this.previous.classList.add("previous") + this.previous.innerHTML = SVG_STRINGS.previous + this.previous.onclick = () => this.previous_frame() + button_div.appendChild(this.previous) + + this.reverse = document.createElement("button") + this.reverse.classList.add("reverse") + this.reverse.innerHTML = SVG_STRINGS.reverse + this.reverse.onclick = () => this.reverse_animation() + button_div.appendChild(this.reverse) + + this.pause = document.createElement("button") + this.pause.classList.add("pause") + this.pause.innerHTML = SVG_STRINGS.pause + this.pause.onclick = () => this.pause_animation() + button_div.appendChild(this.pause) + + this.play = document.createElement("button") + this.play.classList.add("play") + this.play.innerHTML = SVG_STRINGS.play + this.play.onclick = () => this.play_animation() + button_div.appendChild(this.play) + + this.next = document.createElement("button") + this.next.classList.add("next") + this.next.innerHTML = SVG_STRINGS.next + this.next.onclick = () => this.next_frame() + button_div.appendChild(this.next) + + this.last = document.createElement("button") + this.last.classList.add("last") + this.last.innerHTML = SVG_STRINGS.last + this.last.onclick = () => this.last_frame() + button_div.appendChild(this.last) + + this.faster = document.createElement("button") + this.faster.classList.add("faster") + this.faster.innerHTML = SVG_STRINGS.faster + this.faster.onclick = () => this.faster_speed() + button_div.appendChild(this.faster) // toggle this._toggle_reverse = () => { - unpress([pause, play]) - press([reverse]) + unpress([this.pause, this.play]) + press([this.reverse]) } this._toogle_pause = () => { - unpress([reverse, play]) - press([pause]) + unpress([this.reverse, this.play]) + press([this.pause]) } this._toggle_play = () => { - unpress([reverse, pause]) - press([play]) + unpress([this.reverse, this.pause]) + press([this.play]) } // Loop control @@ -249,26 +254,32 @@ export class PlayerView extends WidgetView { this.loop_state.style.cssText = "margin: 0 auto; display: table" const once = document.createElement("input") + once.classList.add("once") once.type = "radio" once.value = "once" once.name = "state" const once_label = document.createElement("label") once_label.innerHTML = "Once" + once_label.classList.add("once-label") once_label.style.cssText = "padding: 0 10px 0 5px; user-select:none;" const loop = document.createElement("input") + loop.classList.add("loop") loop.setAttribute("type", "radio") loop.setAttribute("value", "loop") loop.setAttribute("name", "state") const loop_label = document.createElement("label") + loop_label.classList.add("loop-label") loop_label.innerHTML = "Loop" loop_label.style.cssText = "padding: 0 10px 0 5px; user-select:none;" const reflect = document.createElement("input") + reflect.classList.add("reflect") reflect.setAttribute("type", "radio") reflect.setAttribute("value", "reflect") reflect.setAttribute("name", "state") const reflect_label = document.createElement("label") + loop_label.classList.add("reflect-label") reflect_label.innerHTML = "Reflect" reflect_label.style.cssText = "padding: 0 10px 0 5px; user-select:none;" @@ -296,6 +307,7 @@ export class PlayerView extends WidgetView { } this.toggle_disable() + this.update_css() this.shadow_el.appendChild(this.groupEl) } @@ -405,9 +417,9 @@ export class PlayerView extends WidgetView { }, this.model.preview_duration) // Show for 1.5 seconds } - slower(): void { + slower_speed(): void { this.model.interval = Math.round(this.model.interval / 0.7) - this.updateSpeedButton(this.slowerButton, this.model.interval, SVG_STRINGS.slower) + this.updateSpeedButton(this.slower, this.model.interval, SVG_STRINGS.slower) if (this.model.direction > 0) { this.play_animation() } else if (this.model.direction < 0) { @@ -415,9 +427,9 @@ export class PlayerView extends WidgetView { } } - faster(): void { + faster_speed(): void { this.model.interval = Math.round(this.model.interval * 0.7) - this.updateSpeedButton(this.fasterButton, this.model.interval, SVG_STRINGS.faster) + this.updateSpeedButton(this.faster, this.model.interval, SVG_STRINGS.faster) if (this.model.direction > 0) { this.play_animation() } else if (this.model.direction < 0) { @@ -523,6 +535,10 @@ export namespace Player { preview_duration: p.Property show_loop_controls: p.Property show_value: p.Property + button_scale: p.Property + scale_buttons: p.Property + visible_buttons: p.Property + visible_loop_options: p.Property } } @@ -541,7 +557,7 @@ export class Player extends Widget { static { this.prototype.default_view = PlayerView - this.define(({Bool, Int, Str}) => ({ + this.define(({Bool, Int, Float, List, Str}) => ({ direction: [Int, 0], interval: [Int, 500], start: [Int, 0], @@ -555,6 +571,10 @@ export class Player extends Widget { preview_duration: [Int, 1500], show_loop_controls: [Bool, true], show_value: [Bool, true], + button_scale: [Float, 1], + scale_buttons: [Float, 1], + visible_buttons: [List(Str), ["slower", "first", "previous", "reverse", "pause", "play", "next", "last", "faster"]], + visible_loop_options: [List(Str), ["once", "loop", "reflect"]], })) this.override({width: 400}) diff --git a/panel/models/widgets.py b/panel/models/widgets.py index 3f10ff51f7..615255f55a 100644 --- a/panel/models/widgets.py +++ b/panel/models/widgets.py @@ -70,6 +70,15 @@ class Player(Widget): height = Override(default=250) + scale_buttons = Float(1, help="Percentage to scale the size of the buttons by") + + visible_buttons = List(String, default=[ + 'slower', 'first', 'previous', 'reverse', 'pause', 'play', 'next', 'last', 'faster' + ], help="The buttons to display on the player.") + + visible_loop_options = List(String, default=[ + 'once', 'loop', 'reflect' + ], help="The loop options to display on the player.") class DiscretePlayer(Player): diff --git a/panel/tests/ui/widgets/test_player.py b/panel/tests/ui/widgets/test_player.py index 25666728ee..44616607e9 100644 --- a/panel/tests/ui/widgets/test_player.py +++ b/panel/tests/ui/widgets/test_player.py @@ -80,3 +80,45 @@ def test_name_and_show_value(page): name = page.locator('.pn-player-title:has-text("test")') expect(name).to_have_count(1) +def test_player_visible_buttons(page): + player = Player(visible_buttons=["play", "pause"]) + serve_component(page, player) + + assert page.is_visible(".play") + assert page.is_visible(".pause") + assert not page.is_visible(".reverse") + assert not page.is_visible(".first") + assert not page.is_visible(".previous") + assert not page.is_visible(".next") + assert not page.is_visible(".last") + assert not page.is_visible(".slower") + assert not page.is_visible(".faster") + + player.visible_buttons = ["first"] + expect(page.locator(".first")).to_be_visible() + assert not page.is_visible(".play") + assert not page.is_visible(".pause") + + +def test_player_visible_loop_options(page): + player = Player(visible_loop_options=["loop", "once"]) + serve_component(page, player) + + assert page.is_visible(".loop") + assert page.is_visible(".once") + assert not page.is_visible(".reflect") + + player.visible_loop_options = ["reflect"] + expect(page.locator(".reflect")).to_be_visible() + assert not page.is_visible(".loop") + assert not page.is_visible(".once") + + +def test_player_scale_buttons(page): + player = Player(scale_buttons=2) + serve_component(page, player) + + expect(page.locator(".play")).to_have_attribute( + "style", + "text-align: center; flex-grow: 2; margin: 2px; transform: scale(2); max-width: 50px;", + ) diff --git a/panel/tests/widgets/test_player.py b/panel/tests/widgets/test_player.py index d891508fb0..cf2ece523f 100644 --- a/panel/tests/widgets/test_player.py +++ b/panel/tests/widgets/test_player.py @@ -20,3 +20,15 @@ def test_discrete_player(document, comm): discrete_player.value = 100 assert widget.value == 3 + + +def test_player_loop_policy_not_in_loop_options(document, comm): + player = DiscretePlayer(name='Player', loop_policy='once', visible_loop_options=['loop', 'reflect']) + assert player.loop_policy == 'loop' + assert player.visible_loop_options == ['loop', 'reflect'] + + +def test_player_loop_policy_with_no_loop_options(document, comm): + player = DiscretePlayer(name='Player', loop_policy='loop', visible_loop_options=[]) + assert player.loop_policy == 'loop' + assert player.visible_loop_options == [] diff --git a/panel/widgets/player.py b/panel/widgets/player.py index e800f02925..86a60dc74f 100644 --- a/panel/widgets/player.py +++ b/panel/widgets/player.py @@ -58,7 +58,18 @@ class PlayerBase(Widget): Width of this component. If sizing_mode is set to stretch or scale mode this will merely be used as a suggestion.""") - _rename: ClassVar[Mapping[str, str | None]] = {'name': 'title'} + scale_buttons = param.Number(default=1, doc=""" + The scaling factor to resize the buttons.""") + + visible_buttons = param.List(default=[ + 'slower', 'first', 'previous', 'reverse', 'pause', 'play', 'next', 'last', 'faster' + ], doc="""The buttons to display on the player.""") + + visible_loop_options = param.List(default=[ + 'once', 'loop', 'reflect' + ], doc="The loop options to display on the player.") + + _rename: ClassVar[Mapping[str, str | None]] = {'name': "title"} _widget_type: ClassVar[type[Model]] = _BkPlayer @@ -67,6 +78,9 @@ class PlayerBase(Widget): __abstract = True def __init__(self, **params): + if loop_options := params.get("visible_loop_options", []): + if params.get("loop_policy", "once") not in loop_options: + params["loop_policy"] = loop_options[0] if 'value' in params and 'value_throttled' in self.param: params['value_throttled'] = params['value'] super().__init__(**params) From 64c32f7996e573df5e9a6709b78d1dc86ec1f1e9 Mon Sep 17 00:00:00 2001 From: Christoph Deil Date: Thu, 15 Aug 2024 05:44:06 +0100 Subject: [PATCH 72/91] Remove show in ChatFeed.ipynb (#7147) --- examples/reference/chat/ChatFeed.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/reference/chat/ChatFeed.ipynb b/examples/reference/chat/ChatFeed.ipynb index 3866e7f8d5..5b9b22c594 100644 --- a/examples/reference/chat/ChatFeed.ipynb +++ b/examples/reference/chat/ChatFeed.ipynb @@ -263,7 +263,7 @@ " return f\"Echoing {user!r}... {contents}\\n\\n{instance!r}\"\n", "\n", "chat_feed = pn.chat.ChatFeed(callback=echo_message)\n", - "chat_feed.show()" + "chat_feed" ] }, { From d1754ae04b4395ac9b1cda42f8578459b0e1ef82 Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Thu, 15 Aug 2024 00:34:28 -0700 Subject: [PATCH 73/91] Allow serialize_kwargs (#7146) --- panel/chat/feed.py | 5 +++-- panel/chat/interface.py | 8 ++++++-- panel/tests/chat/test_feed.py | 11 +++++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/panel/chat/feed.py b/panel/chat/feed.py index 222a9255fb..26352db3f9 100644 --- a/panel/chat/feed.py +++ b/panel/chat/feed.py @@ -874,7 +874,8 @@ def _serialize_for_transformers( messages: list[ChatMessage], role_names: dict[str, str | list[str]] | None = None, default_role: str | None = "assistant", - custom_serializer: Callable = None + custom_serializer: Callable | None = None, + **serialize_kwargs ) -> list[dict[str, Any]]: """ Exports the chat log for use with transformers. @@ -914,7 +915,7 @@ def _serialize_for_transformers( f"it returned a {type(content)} type" ) else: - content = str(message) + content = message.serialize(**serialize_kwargs) serialized_messages.append({"role": role, "content": content}) return serialized_messages diff --git a/panel/chat/interface.py b/panel/chat/interface.py index 3ef7bfc2e3..49998e62b3 100644 --- a/panel/chat/interface.py +++ b/panel/chat/interface.py @@ -584,7 +584,8 @@ def _serialize_for_transformers( messages: list[ChatMessage], role_names: dict[str, str | list[str]] | None = None, default_role: str | None = "assistant", - custom_serializer: Callable = None + custom_serializer: Callable = None, + **serialize_kwargs ) -> list[dict[str, Any]]: """ Exports the chat log for use with transformers. @@ -606,6 +607,8 @@ def _serialize_for_transformers( A custom function to format the ChatMessage's object. The function must accept one positional argument, the ChatMessage object, and return a string. If not provided, uses the serialize method on ChatMessage. + serialize_kwargs : dict + Additional keyword arguments to pass to the serializer. Returns ------- @@ -616,7 +619,8 @@ def _serialize_for_transformers( "user": [self.user], "assistant": [self.callback_user], } - return super()._serialize_for_transformers(messages, role_names, default_role, custom_serializer) + return super()._serialize_for_transformers( + messages, role_names, default_role, custom_serializer, **serialize_kwargs) @param.depends("_callback_state", watch=True) async def _update_input_disabled(self): diff --git a/panel/tests/chat/test_feed.py b/panel/tests/chat/test_feed.py index e0ed55e5f1..d255792b44 100644 --- a/panel/tests/chat/test_feed.py +++ b/panel/tests/chat/test_feed.py @@ -1313,6 +1313,17 @@ def __repr__(self): chat_feed.send(Test()) assert chat_feed.serialize() == [{"role": "user", "content": "Test()"}] + def test_serialize_kwargs(self, chat_feed): + chat_feed.send("Hello") + chat_feed.add_step("Hello", "World") + assert chat_feed.serialize( + prefix_with_container_label=False, + prefix_with_viewable_label=False + ) == [ + {'role': 'user', 'content': 'Hello'}, + {'role': 'user', 'content': '((Hello))'} + ] + @pytest.mark.xdist_group("chat") class TestChatFeedSerializeBase: From e7c8ced0556f0f52f4ea3699b0f5f4448531d529 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Aug 2024 12:31:37 +0200 Subject: [PATCH 74/91] Bump ws from 8.17.0 to 8.18.0 in /scripts/jupyterlite (#7139) Bumps [ws](https://github.com/websockets/ws) from 8.17.0 to 8.18.0. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/8.17.0...8.18.0) --- updated-dependencies: - dependency-name: ws dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- scripts/jupyterlite/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/jupyterlite/package-lock.json b/scripts/jupyterlite/package-lock.json index e0e1b1595f..4bb78f3385 100644 --- a/scripts/jupyterlite/package-lock.json +++ b/scripts/jupyterlite/package-lock.json @@ -21,9 +21,9 @@ } }, "node_modules/ws": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "engines": { "node": ">=10.0.0" }, From 5f123fcbe6af28d3e52018ad76a39a210bef2bb9 Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Fri, 16 Aug 2024 09:44:45 -0700 Subject: [PATCH 75/91] ChatFeed.add_step default to assistant (#7154) * default to assistant * fix role * Update panel/chat/feed.py Co-authored-by: Philipp Rudiger --------- Co-authored-by: Philipp Rudiger --- panel/chat/feed.py | 2 +- panel/tests/chat/test_feed.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/panel/chat/feed.py b/panel/chat/feed.py index 26352db3f9..955f04e0b4 100644 --- a/panel/chat/feed.py +++ b/panel/chat/feed.py @@ -801,7 +801,7 @@ def add_step( if layout_params: input_layout_params.update(layout_params) steps_layout = layout(step, **input_layout_params) - self.stream(steps_layout, user=user, avatar=avatar) + self.stream(steps_layout, user=user or self.callback_user, avatar=avatar) else: steps_layout.append(step) self._chat_log.scroll_to_latest() diff --git a/panel/tests/chat/test_feed.py b/panel/tests/chat/test_feed.py index d255792b44..096afb4cd0 100644 --- a/panel/tests/chat/test_feed.py +++ b/panel/tests/chat/test_feed.py @@ -206,6 +206,7 @@ def test_add_step(self, chat_feed): assert len(chat_feed) == 1 message = chat_feed.objects[0] assert isinstance(message, ChatMessage) + assert message.user == "Assistant" steps = message.object assert isinstance(steps, Column) @@ -278,7 +279,7 @@ def test_add_step_explict_not_append(self, chat_feed): assert len(chat_feed) == 2 message1 = chat_feed.objects[0] assert isinstance(message1, ChatMessage) - assert message1.user == "User" + assert message1.user == "Assistant" steps1 = message1.object assert isinstance(steps1, Column) assert len(steps1) == 1 @@ -288,7 +289,7 @@ def test_add_step_explict_not_append(self, chat_feed): message2 = chat_feed.objects[1] assert isinstance(message2, ChatMessage) - assert message2.user == "User" + assert message2.user == "Assistant" steps2 = message2.object assert isinstance(steps2, Column) assert len(steps2) == 1 @@ -1321,7 +1322,7 @@ def test_serialize_kwargs(self, chat_feed): prefix_with_viewable_label=False ) == [ {'role': 'user', 'content': 'Hello'}, - {'role': 'user', 'content': '((Hello))'} + {'role': 'assistant', 'content': '((Hello))'} ] From 530071c5cb2e95c6b8012274178575be496067b5 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 18 Aug 2024 11:09:59 +0200 Subject: [PATCH 76/91] Ensure ReactComponent does not error if view is removed before render (#7159) --- panel/models/react_component.ts | 14 ++++++++-- panel/tests/ui/test_custom.py | 45 ++++++++++++++++++++++++--------- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/panel/models/react_component.ts b/panel/models/react_component.ts index 5ba1661838..00a63f0534 100644 --- a/panel/models/react_component.ts +++ b/panel/models/react_component.ts @@ -73,7 +73,8 @@ class Child extends React.Component { } get element() { - return this.view.el + const view = this.view + return view == null ? null : view.el } componentDidMount() { @@ -89,8 +90,17 @@ class Child extends React.Component { }) } + append_child(ref) { + if (ref != null) { + const view = this.view + if (view != null) { + ref.appendChild(this.element) + } + } + } + render() { - return React.createElement('div', {className: "child-wrapper", ref: (ref) => ref && ref.appendChild(this.element)}) + return React.createElement('div', {className: "child-wrapper", ref: (ref) => this.append_child(ref)}) } } diff --git a/panel/tests/ui/test_custom.py b/panel/tests/ui/test_custom.py index dbb7b3aaa8..81769b321f 100644 --- a/panel/tests/ui/test_custom.py +++ b/panel/tests/ui/test_custom.py @@ -11,6 +11,7 @@ AnyWidgetComponent, Child, Children, JSComponent, ReactComponent, ) from panel.layout import Row +from panel.layout.base import ListLike from panel.pane import Markdown from panel.tests.util import serve_component, wait_until @@ -275,9 +276,9 @@ def test_child(page, component): wait_until(lambda: example.render_count == (2 if component is JSChild else 1), page) -class JSChildren(JSComponent): +class JSChildren(ListLike, JSComponent): - children = Children() + objects = Children() render_count = param.Integer(default=0) @@ -285,7 +286,7 @@ class JSChildren(JSComponent): export function render({ model }) { const div = document.createElement('div') div.id = "container" - div.append(...model.get_child('children')) + div.append(...model.get_child('objects')) model.render_count += 1 return div }""" @@ -297,38 +298,38 @@ class JSChildrenNoReturn(JSChildren): export function render({ model, view }) { const div = document.createElement('div') div.id = "container" - div.append(...model.get_child('children')) + div.append(...model.get_child('objects')) view.container.replaceChildren(div) model.render_count += 1 }""" -class ReactChildren(ReactComponent): +class ReactChildren(ListLike, ReactComponent): - children = Children() + objects = Children() render_count = param.Integer(default=0) _esm = """ export function render({ model }) { model.render_count += 1 - return
{model.get_child("children")}
+ return
{model.get_child("objects")}
}""" @pytest.mark.parametrize('component', [JSChildren, JSChildrenNoReturn, ReactChildren]) def test_children(page, component): - example = component(children=['A Markdown pane!']) + example = component(objects=['A Markdown pane!']) serve_component(page, example) expect(page.locator('#container')).to_have_text('A Markdown pane!') - example.children = ['A different Markdown pane!'] + example.objects = ['A different Markdown pane!'] expect(page.locator('#container')).to_have_text('A different Markdown pane!') - example.children = ['
1
', '
2
'] + example.objects = ['
1
', '
2
'] expect(page.locator('.foo').nth(0)).to_have_text('1') expect(page.locator('.foo').nth(1)).to_have_text('2') @@ -338,12 +339,32 @@ def test_children(page, component): assert example.render_count == (3 if issubclass(component, JSChildren) else 2) +@pytest.mark.parametrize('component', [JSChildren, JSChildrenNoReturn, ReactChildren]) +def test_children_add_and_remove_without_error(page, component): + example = component(objects=['A Markdown pane!']) + + msgs, _ = serve_component(page, example) + + expect(page.locator('#container')).to_have_text('A Markdown pane!') + + example.append('A different Markdown pane!') + example.pop(-1) + + expect(page.locator('#container')).to_have_text('A Markdown pane!') + + expect(page.locator('.markdown')).to_have_count(1) + + page.wait_for_timeout(500) + + assert [msg for msg in msgs if msg.type == 'error' and 'favicon' not in msg.location['url']] == [] + + @pytest.mark.parametrize('component', [JSChildren, JSChildrenNoReturn, ReactChildren]) def test_children_append_without_rerender(page, component): child = JSChild(child=Markdown( 'A Markdown pane!', css_classes=['first'] )) - example = component(children=[child]) + example = component(objects=[child]) serve_component(page, example) @@ -351,7 +372,7 @@ def test_children_append_without_rerender(page, component): wait_until(lambda: child.render_count == 1, page) - example.children = example.children+[Markdown( + example.objects = example.objects+[Markdown( 'A different Markdown pane!', css_classes=['second'] )] From 3764838c659cd99945538408e20f8a280fa26049 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 19 Aug 2024 15:38:47 +0200 Subject: [PATCH 77/91] Add docs explaining how to specify and bundle ESM dependencies (#7160) --- doc/how_to/custom_components/esm/build.md | 161 ++++++++++++++++++++++ doc/how_to/custom_components/index.md | 8 ++ 2 files changed, 169 insertions(+) create mode 100644 doc/how_to/custom_components/esm/build.md diff --git a/doc/how_to/custom_components/esm/build.md b/doc/how_to/custom_components/esm/build.md new file mode 100644 index 0000000000..c895c03f96 --- /dev/null +++ b/doc/how_to/custom_components/esm/build.md @@ -0,0 +1,161 @@ +# Handling of external resources + +The ESM components make it possible to load external libraries from NPM or GitHub easily using one of two approaches: + +1. Directly importing from `esm.sh` or another CDN or by defining a so called importmap. +2. Bundling the resources using `npm` and `esbuild`. + +In this guide we will cover how and when to use each of these approaches. + +## Imports + +So called [ECMA script modules](https://en.wikipedia.org/wiki/ECMAScript#6th_Edition_%E2%80%93_ECMAScript_2015) or ESM modules for short, made it much simpler to build reusable modules that could easily import other libraries. Specifically they introduced `import` and `export` specifiers, which allow developers to import other libraries and export specific functions, objects and classes for the consumption of others. + +These imports can reference modules directly on some CDN or you can define a so called [`importmap`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap), which allows you to specify where to load a library from. Let's start with a simple example, we are going to build a `ConfettiButton`. + +### Inline Imports + +Let us first specify the Python portion of our component, we are simply going to create a `JSComponent` that loads `confetti.js`: + +```python +import panel as pn + +from panel.custom import JSComponent + +pn.extension() + +class ConfettiButton(JSComponent): + + _esm = 'confetti.js' + +ConfettiButton().servable() +``` + +Now that we have our Python component let's build the Javascript (or TypeScript if you like): + +```javascript +/* confetti.js */ +import confetti from "https://esm.sh/canvas-confetti@1.6.0"; + +export function render() { + const button = document.createElement('button') + button.addEventListener('click', () => confetti()) + button.append('Click me!') + return button +} +``` + +Here we are importing the library directly from [esm.sh](https://esm.sh/), a fast and reliable CDN to fetch libraries compiled as modern ESM bundles from. + +:::{note} +esm.sh is very powerful and has many options for specifying shared dependencies or bundling dependencies together. Make sure to [check out the docs](https://esm.sh/#docs). +::: + +### Import Maps + +Once you move past initial development we recommend making use of import maps. To quote MDN: + +> An import map is a JSON object that allows developers to control how the browser resolves module specifiers when importing JavaScript modules. It provides a mapping between the text used as the module specifier in an import statement or import() operator, and the corresponding value that will replace the text when resolving the specifier. + +The import map can be declared directly on the `JSComponent` using the `_importmap` attribute. A minimum it must contain some imports: + +```python +class ConfettiButton(JSComponent): + + _importmap = { + "imports": { + "canvas-confetti": "https://esm.sh/canvas-confetti@1.6.0", + } + } + + _esm = 'confetti.js' +``` + +Now that we have separately declared the import we can update the `import` line in the `confetti.js` file: + +```javascript +/* confetti.js */ +import confetti from "canvas-confetti"; +``` + +This approach cleanly separates the definitions of the libraries and their versions from the actual code. Import maps have a bunch of other features but in most cases the imports section will be all you need. + +## Bundling + +Importing libraries directly from a CDN allows for extremely quick iteration but also means that the users of your components will have to have access to the internet to fetch the required modules. By bundling the component resources you can ship a self-contained module that includes all the dependencies, while also ensuring that you only fetch the parts of the libraries that are actually needed. + +### Tooling + +The tooling we recommend to bundle your component resources include `esbuild` and `npm`, both can conveniently be installed with `conda`: + +```bash +conda install esbuild npm +``` + +### Configuration + +To run the bundling we will need one additional file, the `package.json`, which, just like the import maps, determines the required packages and their versions. The `package.json` is a complex file with tons of configuration options but all we will need are the [dependencies](https://docs.npmjs.com/cli/v10/configuring-npm/package-json#dependencies). + +To recap here are the three files that we need: + +::::{tab-set} + +:::tab-item package.json +```json +{ + "name": "confetti-button", + "dependencies": { + "canvas-confetti": "^1.6.0" + } +} +``` +::: + +:::{tab-item} confetti.py +import panel as pn + +from panel.custom import JSComponent + +pn.extension() + +class ConfettiButton(JSComponent): + + _esm = 'confetti.bundled.js' + +ConfettiButton().servable() +::: + +:::{tab-item} confetti.js +import confetti from "canvas-confetti"; + +export function render() { + const button = document.createElement('button') + button.addEventListener('click', () => confetti()) + button.append('Click me!') + return button +} +::: + +:::: + +Once you have set up these three files you have to install the packages with `npm`: + +```bash +npm install +``` + +This will fetch the packages and install them into the local `node_modules` directory. Once that is complete we can run the bundling: + +```bash +esbuild confetti.js --bundle --format=esm --minify --outfile=confetti.bundled.js +``` + +This will create a new file called `confetti.bundled.js`, which includes all the dependencies (even CSS, image files and other static assets if you have imported them). + +The only thing left to do now is to update the `_esm` declaration to point to the new bundled file: + +```python +class ConfettiButton(JSComponent): + + _esm = 'confetti.bundled.js' +``` diff --git a/doc/how_to/custom_components/index.md b/doc/how_to/custom_components/index.md index 9de4359823..d8739d060d 100644 --- a/doc/how_to/custom_components/index.md +++ b/doc/how_to/custom_components/index.md @@ -58,6 +58,14 @@ Build custom components in Javascript using so called ESM components, which allo ::::{grid} 1 2 2 3 :gutter: 1 1 1 2 + +:::{grid-item-card} {octicon}`tools;2.5em;sd-mr-1 sd-animate-grow50` Building and Bundling ESM components +:link: esm/build +:link-type: doc + +How to specify and bundle external dependencies for ESM components. +::: + :::{grid-item-card} {octicon}`pencil;2.5em;sd-mr-1 sd-animate-grow50` Add callbacks to ESM components :link: esm/callbacks :link-type: doc From 35a2ad55bed33e3a2031ae12741b832933cc3c6f Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 19 Aug 2024 17:22:22 +0200 Subject: [PATCH 78/91] Convert and clean up custom component reference notebooks (#7161) --- doc/conf.py | 9 +- .../AnyWidgetComponent.ipynb | 394 +++++++++++ .../custom_components/AnyWidgetComponent.md | 310 --------- .../custom_components/JSComponent.ipynb | 594 ++++++++++++++++ .../custom_components/JSComponent.md | 456 ------------- .../custom_components/ReactComponent.ipynb | 632 ++++++++++++++++++ .../custom_components/ReactComponent.md | 483 ------------- .../reference/custom_components/Viewer.ipynb | 354 ++++++++++ .../reference/custom_components/Viewer.md | 258 ------- panel/models/reactive_esm.ts | 9 +- 10 files changed, 1988 insertions(+), 1511 deletions(-) create mode 100644 examples/reference/custom_components/AnyWidgetComponent.ipynb delete mode 100644 examples/reference/custom_components/AnyWidgetComponent.md create mode 100644 examples/reference/custom_components/JSComponent.ipynb delete mode 100644 examples/reference/custom_components/JSComponent.md create mode 100644 examples/reference/custom_components/ReactComponent.ipynb delete mode 100644 examples/reference/custom_components/ReactComponent.md create mode 100644 examples/reference/custom_components/Viewer.ipynb delete mode 100644 examples/reference/custom_components/Viewer.md diff --git a/doc/conf.py b/doc/conf.py index 0d814fefdc..185ebc643b 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -124,10 +124,10 @@ 'layouts', # 3 most important by expected usage. Rest alphabetically 'chat', - 'custom_components', 'global', 'indicators', 'templates', + 'custom_components', ], 'titles': { 'Vega': 'Altair & Vega', @@ -167,9 +167,14 @@ def get_requirements(): requirements[src] = deps return requirements + +html_js_files = [ + (None, {'body': '{"shimMode": true}', 'type': 'esms-options'}), + f'https://cdn.holoviz.org/panel/{js_version}/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js' +] + nbsite_pyodide_conf = { 'PYODIDE_URL': f'https://cdn.jsdelivr.net/pyodide/{PYODIDE_VERSION}/full/pyodide.js', - 'preamble': '', 'requirements': [bokeh_req, panel_req, 'pyodide-http'], 'requires': get_requirements(), } diff --git a/examples/reference/custom_components/AnyWidgetComponent.ipynb b/examples/reference/custom_components/AnyWidgetComponent.ipynb new file mode 100644 index 0000000000..3e410dda70 --- /dev/null +++ b/examples/reference/custom_components/AnyWidgetComponent.ipynb @@ -0,0 +1,394 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "031c5d57-b722-4999-88ac-686ac83d3ef1", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "\n", + "pn.extension()" + ] + }, + { + "cell_type": "markdown", + "id": "9908a714-692d-4513-aca8-b251a627cae4", + "metadata": {}, + "source": [ + "Panel's `AnyWidgetComponent` class simplifies the creation of custom Panel components using the [`AnyWidget`](https://anywidget.dev/) JavaScript API." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "93ab8716-5052-4a89-83b4-dd78576816ce", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "import param\n", + "\n", + "from panel.custom import AnyWidgetComponent\n", + "\n", + "\n", + "class CounterButton(AnyWidgetComponent):\n", + "\n", + " value = param.Integer()\n", + "\n", + " _esm = \"\"\"\n", + " function render({ model, el }) {\n", + " let count = () => model.get(\"value\");\n", + " let btn = document.createElement(\"button\");\n", + " btn.innerHTML = `count is ${count()}`;\n", + " btn.addEventListener(\"click\", () => {\n", + " model.set(\"value\", count() + 1);\n", + " model.save_changes();\n", + " });\n", + " model.on(\"change:value\", () => {\n", + " btn.innerHTML = `count is ${count()}`;\n", + " });\n", + " el.appendChild(btn);\n", + " }\n", + " export default { render };\n", + " \"\"\"\n", + "\n", + "CounterButton().servable()" + ] + }, + { + "cell_type": "markdown", + "id": "1a37ed44-8c89-40d6-9c01-b22c5a4c4d0a", + "metadata": {}, + "source": [ + ":::{note}\n", + "Panel's `AnyWidgetComponent` supports using the [`AnyWidget`](https://anywidget.dev/) API on the JavaScript side and the [`param`](https://param.holoviz.org/) parameters API on the Python side.\n", + "\n", + "If you are looking to create custom components using Python and Panel component only, check out [`Viewer`](Viewer.md).\n", + ":::\n", + "\n", + "\n", + "## API\n", + "\n", + "### AnyWidgetComponent Attributes\n", + "\n", + "- **`_esm`** (str | PurePath): This attribute accepts either a string or a path that points to an [ECMAScript module](https://nodejs.org/api/esm.html#modules-ecmascript-modules). The ECMAScript module should export a `default` object or function that returns an object. The object should contain a `render` function and optionally an `initialize` function. In a development environment such as a notebook or when using `--autoreload`, the module will automatically reload upon saving changes.\n", + "- **`_importmap`** (dict | None): This optional dictionary defines an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap), allowing you to customize how module specifiers are resolved.\n", + "- **`_stylesheets`** (optional list of strings): This optional attribute accepts a list of CSS strings or paths to CSS files. It supports automatic reloading in development environments.\n", + "\n", + ":::note\n", + "You may specify a path to a file as a string instead of a PurePath. The path should be specified relative to the file it is referenced in.\n", + ":::\n", + "\n", + "#### `render` Function\n", + "\n", + "The `_esm` `default` object must contain a `render` function. It accepts the following parameters:\n", + "\n", + "- **`model`**: Represents the parameters of the component and provides methods to `.get` values, `.set` values, and `.save_changes`.\n", + "- **`el`**: The parent HTML element to append HTML elements to.\n", + "\n", + "For more detail, see [`AnyWidget`](https://anywidget.dev/).\n", + "\n", + "## Usage\n", + "\n", + "### Styling with CSS\n", + "\n", + "Include CSS within the `_stylesheets` attribute to style the component. The CSS is injected directly into the component's HTML." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16d63729-efec-4033-8c3e-12295b3910e6", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "import param\n", + "\n", + "from panel.custom import AnyWidgetComponent\n", + "\n", + "\n", + "class StyledCounterButton(AnyWidgetComponent):\n", + "\n", + " value = param.Integer()\n", + "\n", + " _esm = \"\"\"\n", + " function render({ model, el }) {\n", + " let count = () => model.get(\"value\");\n", + " let btn = document.createElement(\"button\");\n", + " btn.innerHTML = `count is ${count()}`;\n", + " btn.addEventListener(\"click\", () => {\n", + " model.set(\"value\", count() + 1);\n", + " model.save_changes();\n", + " });\n", + " model.on(\"change:value\", () => {\n", + " btn.innerHTML = `count is ${count()}`;\n", + " });\n", + " el.appendChild(btn);\n", + " }\n", + " export default { render };\n", + " \"\"\"\n", + "\n", + " _stylesheets = [\n", + " \"\"\"\n", + " button {\n", + " background: #0072B5;\n", + " color: white;\n", + " border: none;\n", + " padding: 10px;\n", + " border-radius: 4px;\n", + " }\n", + " button:hover {\n", + " background: #4099da;\n", + " }\n", + " \"\"\"\n", + " ]\n", + "\n", + "StyledCounterButton().servable()" + ] + }, + { + "cell_type": "markdown", + "id": "7619dad4-4dfe-43a1-aac1-32c57ddffc58", + "metadata": {}, + "source": [ + "### Dependency Imports\n", + "\n", + "JavaScript dependencies can be directly imported via URLs, such as those from [`esm.sh`](https://esm.sh/)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e5bd2900-61d6-4112-8522-6a0239bf6d1f", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "\n", + "from panel.custom import AnyWidgetComponent\n", + "\n", + "\n", + "class ConfettiButton(AnyWidgetComponent):\n", + "\n", + " _esm = \"\"\"\n", + " import confetti from \"https://esm.sh/canvas-confetti@1.6.0\";\n", + "\n", + " function render({ el }) {\n", + " let btn = document.createElement(\"button\");\n", + " btn.innerHTML = \"Click Me\";\n", + " btn.addEventListener(\"click\", () => {\n", + " confetti();\n", + " });\n", + " el.appendChild(btn);\n", + " }\n", + " export default { render }\n", + " \"\"\"\n", + "\n", + "ConfettiButton().servable()\n" + ] + }, + { + "cell_type": "markdown", + "id": "cc5c58de-544d-4cac-b103-b6db1e4dc139", + "metadata": {}, + "source": [ + "Use the `_importmap` attribute for more concise module references." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c20cda5c-7176-4d3d-8b56-01acad7aa924", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "\n", + "from panel.custom import AnyWidgetComponent\n", + "\n", + "\n", + "class ConfettiButton(AnyWidgetComponent):\n", + "\n", + " _importmap = {\n", + " \"imports\": {\n", + " \"canvas-confetti\": \"https://esm.sh/canvas-confetti@1.6.0\",\n", + " }\n", + " }\n", + "\n", + " _esm = \"\"\"\n", + " import confetti from \"canvas-confetti\";\n", + "\n", + " function render({ el }) {\n", + " let btn = document.createElement(\"button\");\n", + " btn.innerHTML = \"Click Me\";\n", + " btn.addEventListener(\"click\", () => {\n", + " confetti();\n", + " });\n", + " el.appendChild(btn);\n", + " }\n", + " export default { render }\n", + " \"\"\"\n", + "\n", + "ConfettiButton().servable()" + ] + }, + { + "cell_type": "markdown", + "id": "c1d8d880-3b55-4eb4-998c-3fb265b47322", + "metadata": {}, + "source": [ + "See the [import map documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) for more information about the import map format.\n", + "\n", + "### External Files\n", + "\n", + "You can load JavaScript and CSS from files by providing the paths to these files.\n", + "\n", + "Create the file **counter_button.py**.\n", + "\n", + "```python\n", + "from pathlib import Path\n", + "\n", + "import param\n", + "import panel as pn\n", + "\n", + "from panel.custom import AnyWidgetComponent\n", + "\n", + "pn.extension()\n", + "\n", + "class CounterButton(AnyWidgetComponent):\n", + "\n", + " value = param.Integer()\n", + "\n", + " _esm = Path(\"counter_button.js\")\n", + " _stylesheets = [Path(\"counter_button.css\")]\n", + "\n", + "CounterButton().servable()\n", + "```\n", + "\n", + "Now create the file **counter_button.js**.\n", + "\n", + "```javascript\n", + "function render({ model, el }) {\n", + " let value = () => model.get(\"value\");\n", + " let btn = document.createElement(\"button\");\n", + " btn.innerHTML = `count is ${value()}`;\n", + " btn.addEventListener(\"click\", () => {\n", + " model.set('value', value() + 1);\n", + " model.save_changes();\n", + " });\n", + " model.on(\"change:value\", () => {\n", + " btn.innerHTML = `count is ${value()}`;\n", + " });\n", + " el.appendChild(btn);\n", + "}\n", + "export default { render }\n", + "```\n", + "\n", + "Now create the file **counter_button.css**.\n", + "\n", + "```css\n", + "button {\n", + " background: #0072B5;\n", + " color: white;\n", + " border: none;\n", + " padding: 10px;\n", + " border-radius: 4px;\n", + "}\n", + "button:hover {\n", + " background: #4099da;\n", + "}\n", + "```\n", + "\n", + "Serve the app with `panel serve counter_button.py --autoreload`.\n", + "\n", + "You can now edit the JavaScript or CSS file, and the changes will be automatically reloaded.\n", + "\n", + "- Try changing the `innerHTML` from `count is ${value()}` to `COUNT IS ${value()}` and observe the update. Note you must update `innerHTML` in two places.\n", + "- Try changing the background color from `#0072B5` to `#008080`.\n", + "\n", + "### React\n", + "\n", + "You can use React with `AnyWidget` as shown below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1d2b5ea2-18cd-47fa-a639-7535f5c1652d", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "import param\n", + "\n", + "from panel.custom import AnyWidgetComponent\n", + "\n", + "\n", + "class CounterButton(AnyWidgetComponent):\n", + "\n", + " value = param.Integer()\n", + "\n", + " _importmap = {\n", + " \"imports\": {\n", + " \"@anywidget/react\": \"https://esm.sh/@anywidget/react\",\n", + " \"react\": \"https://esm.sh/react\",\n", + " }\n", + " }\n", + "\n", + " _esm = \"\"\"\n", + " import * as React from \"react\"; /* mandatory import */\n", + " import { createRender, useModelState } from \"@anywidget/react\";\n", + "\n", + " const render = createRender(() => {\n", + " const [value, setValue] = useModelState(\"value\");\n", + " return (\n", + " \n", + " );\n", + " });\n", + " export default { render }\n", + " \"\"\"\n", + "\n", + "CounterButton().servable()" + ] + }, + { + "cell_type": "markdown", + "id": "a8e7f361-5df5-4fe0-b81a-a50d6680f0f9", + "metadata": {}, + "source": [ + ":::{note}\n", + "You will notice that Panel's `AnyWidgetComponent` can be used with React and [JSX](https://react.dev/learn/writing-markup-with-jsx) without any build tools. Instead of build tools, Panel uses [Sucrase](https://sucrase.io/) to transpile the JSX code to JavaScript on the client side.\n", + ":::\n", + "\n", + "## References\n", + "\n", + "### Tutorials\n", + "\n", + "- [Build Custom Components](../../../how_to/custom_components/reactive_esm/reactive_esm_layout.md)\n", + "\n", + "### How-To Guides\n", + "\n", + "- [Convert `AnyWidget` widgets](../../../how_to/migrate/anywidget/index.md)\n", + "\n", + "### Reference Guides\n", + "\n", + "- [`AnyWidgetComponent`](./AnyWidgetComponent.ipynb)\n", + "- [`JSComponent`](./JSComponent.ipynb)\n", + "- [`ReactComponent`](./ReactComponent.ipynb)" + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/reference/custom_components/AnyWidgetComponent.md b/examples/reference/custom_components/AnyWidgetComponent.md deleted file mode 100644 index 3fdd2012f1..0000000000 --- a/examples/reference/custom_components/AnyWidgetComponent.md +++ /dev/null @@ -1,310 +0,0 @@ -# `AnyWidgetComponent` - -Panel's `AnyWidgetComponent` class simplifies the creation of custom Panel components using the [`AnyWidget`](https://anywidget.dev/) JavaScript API. - -```{pyodide} -import panel as pn -import param - -from panel.custom import AnyWidgetComponent - -pn.extension() - -class CounterButton(AnyWidgetComponent): - - value = param.Integer() - - _esm = """ - function render({ model, el }) { - let count = () => model.get("value"); - let btn = document.createElement("button"); - btn.innerHTML = `count is ${count()}`; - btn.addEventListener("click", () => { - model.set("value", count() + 1); - model.save_changes(); - }); - model.on("change:value", () => { - btn.innerHTML = `count is ${count()}`; - }); - el.appendChild(btn); - } - export default { render }; - """ - -CounterButton().servable() -``` - -:::{note} -Panel's `AnyWidgetComponent` supports using the [`AnyWidget`](https://anywidget.dev/) API on the JavaScript side and the [`param`](https://param.holoviz.org/) parameters API on the Python side. - -If you are looking to create custom components using Python and Panel component only, check out [`Viewer`](Viewer.md). - -::: - -## API - -### AnyWidgetComponent Attributes - -- **`_esm`** (str | PurePath): This attribute accepts either a string or a path that points to an [ECMAScript module](https://nodejs.org/api/esm.html#modules-ecmascript-modules). The ECMAScript module should export a `default` object or function that returns an object. The object should contain a `render` function and optionally an `initialize` function. In a development environment such as a notebook or when using `--autoreload`, the module will automatically reload upon saving changes. -- **`_importmap`** (dict | None): This optional dictionary defines an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap), allowing you to customize how module specifiers are resolved. -- **`_stylesheets`** (optional list of strings): This optional attribute accepts a list of CSS strings or paths to CSS files. It supports automatic reloading in development environments. - -:::note - -You may specify a path to a file as a string instead of a PurePath. The path should be specified relative to the file it is referenced in. - -::: - -#### `render` Function - -The `_esm` `default` object must contain a `render` function. It accepts the following parameters: - -- **`model`**: Represents the parameters of the component and provides methods to `.get` values, `.set` values, and `.save_changes`. -- **`el`**: The parent HTML element to append HTML elements to. - -For more detail, see [`AnyWidget`](https://anywidget.dev/). - -## Usage - -### Styling with CSS - -Include CSS within the `_stylesheets` attribute to style the component. The CSS is injected directly into the component's HTML. - -```{pyodide} -import panel as pn -import param - -from panel.custom import AnyWidgetComponent - -pn.extension() - -class StyledCounterButton(AnyWidgetComponent): - - value = param.Integer() - - _esm = """ - function render({ model, el }) { - let count = () => model.get("value"); - let btn = document.createElement("button"); - btn.innerHTML = `count is ${count()}`; - btn.addEventListener("click", () => { - model.set("value", count() + 1); - model.save_changes(); - }); - model.on("change:value", () => { - btn.innerHTML = `count is ${count()}`; - }); - el.appendChild(btn); - } - export default { render }; - """ - - _stylesheets = [ - """ - button { - background: #0072B5; - color: white; - border: none; - padding: 10px; - border-radius: 4px; - } - button:hover { - background: #4099da; - } - """ - ] - -StyledCounterButton().servable() -``` - -## Dependency Imports - -JavaScript dependencies can be directly imported via URLs, such as those from [`esm.sh`](https://esm.sh/). - -```{pyodide} -import panel as pn -from panel.custom import AnyWidgetComponent - -pn.extension() - -class ConfettiButton(AnyWidgetComponent): - - _esm = """ - import confetti from "https://esm.sh/canvas-confetti@1.6.0"; - - function render({ el }) { - let btn = document.createElement("button"); - btn.innerHTML = "Click Me"; - btn.addEventListener("click", () => { - confetti(); - }); - el.appendChild(btn); - } - export default { render } - """ - -ConfettiButton().servable() -``` - -Use the `_import_map` attribute for more concise module references. - -```pydodide -import panel as pn -from panel.custom import AnyWidgetComponent - -pn.extension() - -class ConfettiButton(AnyWidgetComponent): - - _importmap = { - "imports": { - "canvas-confetti": "https://esm.sh/canvas-confetti@1.6.0", - } - } - - _esm = """ - import confetti from "canvas-confetti"; - - function render({ el }) { - let btn = document.createElement("button"); - btn.innerHTML = "Click Me"; - btn.addEventListener("click", () => { - confetti(); - }); - el.appendChild(btn); - } - export default { render } - """ - -ConfettiButton().servable() -``` - -See the [import map documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) for more information about the import map format. - -## External Files - -You can load JavaScript and CSS from files by providing the paths to these files. - -Create the file **counter_button.py**. - -```python -from pathlib import Path - -import param -import panel as pn - -from panel.custom import AnyWidgetComponent - -pn.extension() - -class CounterButton(AnyWidgetComponent): - - value = param.Integer() - - _esm = Path("counter_button.js") - _stylesheets = [Path("counter_button.css")] - -CounterButton().servable() -``` - -Now create the file **counter_button.js**. - -```javascript -function render({ model, el }) { - let value = () => model.get("value"); - let btn = document.createElement("button"); - btn.innerHTML = `count is ${value()}`; - btn.addEventListener("click", () => { - model.set('value', value() + 1); - model.save_changes(); - }); - model.on("change:value", () => { - btn.innerHTML = `count is ${value()}`; - }); - el.appendChild(btn); -} -export default { render } -``` - -Now create the file **counter_button.css**. - -```css -button { - background: #0072B5; - color: white; - border: none; - padding: 10px; - border-radius: 4px; -} -button:hover { - background: #4099da; -} -``` - -Serve the app with `panel serve counter_button.py --autoreload`. - -You can now edit the JavaScript or CSS file, and the changes will be automatically reloaded. - -- Try changing the `innerHTML` from `count is ${value()}` to `COUNT IS ${value()}` and observe the update. Note you must update `innerHTML` in two places. -- Try changing the background color from `#0072B5` to `#008080`. - -## React - -You can use React with `AnyWidget` as shown below. - -```pydodide -import panel as pn -import param - -from panel.custom import AnyWidgetComponent - -pn.extension() - -class CounterButton(AnyWidgetComponent): - - value = param.Integer() - - _importmap = { - "imports": { - "@anywidget/react": "https://esm.sh/@anywidget/react", - "react": "https://esm.sh/react@18.2.0", - } - } - - _esm = """ - import * as React from "react"; /* mandatory import */ - import { createRender, useModelState } from "@anywidget/react"; - - const render = createRender(() => { - const [value, setValue] = useModelState("value"); - return ( - - ); - }); - export default { render } - """ - -CounterButton().servable() -``` - -:::{note} -You will notice that Panel's `AnyWidgetComponent` can be used with React and [JSX](https://react.dev/learn/writing-markup-with-jsx) without any build tools. Instead of build tools, Panel uses [Sucrase](https://sucrase.io/) to transpile the JSX code to JavaScript on the client side. -::: - -## References - -### Tutorials - -- [Build Custom Components](../../../how_to/custom_components/reactive_esm/reactive_esm_layout.md) - -### How-To Guides - -- [Convert `AnyWidget` widgets](../../../how_to/migrate/anywidget/index.md) - -### Reference Guides - -- [`AnyWidgetComponent`](../../../reference/panes/AnyWidgetComponent.md) -- [`JSComponent`](../../../reference/panes/JSComponent.md) -- [`ReactComponent`](../../../reference/panes/ReactComponent.md) diff --git a/examples/reference/custom_components/JSComponent.ipynb b/examples/reference/custom_components/JSComponent.ipynb new file mode 100644 index 0000000000..fc87414d43 --- /dev/null +++ b/examples/reference/custom_components/JSComponent.ipynb @@ -0,0 +1,594 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "e888ffe5-e558-4ae2-b7c0-b52804eb8ce2", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "\n", + "pn.extension()" + ] + }, + { + "cell_type": "markdown", + "id": "34c90e0e-c932-4346-8452-72f9c3aeecbb", + "metadata": {}, + "source": [ + "`JSComponent` simplifies the creation of custom Panel components using JavaScript." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f7e59ce8-e218-41b9-bb9c-4c5685ed5b44", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "import param\n", + "\n", + "from panel.custom import JSComponent\n", + "\n", + "\n", + "class CounterButton(JSComponent):\n", + "\n", + " value = param.Integer()\n", + "\n", + " _esm = \"\"\"\n", + " export function render({ model }) {\n", + " let btn = document.createElement(\"button\");\n", + " btn.innerHTML = `count is ${model.value}`;\n", + " btn.addEventListener(\"click\", () => {\n", + " model.value += 1\n", + " });\n", + " model.on('value', () => {\n", + " btn.innerHTML = `count is ${model.value}`;\n", + " })\n", + " return btn\n", + " }\n", + " \"\"\"\n", + "\n", + "CounterButton().servable()" + ] + }, + { + "cell_type": "markdown", + "id": "188e2bf7-1bf0-4a61-a867-7770dd54eb46", + "metadata": {}, + "source": [ + ":::{note}\n", + "`JSComponent` was introduced in June 2024 as a successor to `ReactiveHTML`.\n", + "\n", + "`JSComponent` bears similarities to [`AnyWidget`](https://anywidget.dev/), but it is specifically optimized for use with Panel.\n", + "\n", + "If you are looking to create custom components using Python and Panel component only, check out [`Viewer`](Viewer.md).\n", + ":::\n", + "\n", + "## API\n", + "\n", + "### JSComponent Attributes\n", + "\n", + "- **`_esm`** (str | PurePath): This attribute accepts either a string or a path that points to an [ECMAScript module](https://nodejs.org/api/esm.html#modules-ecmascript-modules). The ECMAScript module should export a `render` function which returns the HTML element to display. In a development environment such as a notebook or when using `--autoreload`, the module will automatically reload upon saving changes.\n", + "- **`_importmap`** (dict | None): This optional dictionary defines an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap), allowing you to customize how module specifiers are resolved.\n", + "- **`_stylesheets`** (optional list of strings): This optional attribute accepts a list of CSS strings or paths to CSS files. It supports automatic reloading in development environments.\n", + "\n", + ":::note\n", + "\n", + "You may specify a path to a file as a string instead of a PurePath. The path should be specified relative to the file its specified in.\n", + "\n", + ":::\n", + "\n", + "### `render` Function\n", + "\n", + "The `_esm` attribute must export the `render` function. It accepts the following parameters:\n", + "\n", + "- **`model`**: Represents the Parameters of the component and provides methods to add (and remove) event listeners using `.on` and `.off`, render child elements using `.get_child`, and to `.send_event` back to Python.\n", + "- **`view`**: The Bokeh view.\n", + "- **`el`**: The HTML element that the component will be rendered into.\n", + "\n", + "Any HTML element returned from the `render` function will be appended to the HTML element (`el`) of the component but you may also manually append to and manipulate the `el` directly.\n", + "\n", + "### Callbacks\n", + "\n", + "The `model.on` and `model.off` methods allow registering event handlers inside the render function. This includes the ability to listen to parameter changes and register lifecycle hooks.\n", + "\n", + "#### Change Events\n", + "\n", + "The following signatures are valid when listening to change events:\n", + "\n", + "- `.on('', callback)`: Allows registering an event handler for a single parameter.\n", + "- `.on(['', ...], callback)`: Allows adding an event handler for multiple parameters at once.\n", + "- `.on('change:', callback)`: The `change:` prefix allows disambiguating change events from lifecycle hooks should a parameter name and lifecycle hook overlap.\n", + "\n", + "#### Lifecycle Hooks\n", + "\n", + "- `after_render`: Called once after the component has been fully rendered.\n", + "- `after_resize`: Called after the component has been resized.\n", + "- `remove`: Called when the component view is being removed from the DOM.\n", + "\n", + "## Usage\n", + "\n", + "### Styling with CSS\n", + "\n", + "Include CSS within the `_stylesheets` attribute to style the component. The CSS is injected directly into the component's HTML." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f0ca9f3-547e-4378-8cc8-f745b987b121", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "import param\n", + "\n", + "from panel.custom import JSComponent\n", + "\n", + "\n", + "class StyledCounterButton(JSComponent):\n", + "\n", + " value = param.Integer()\n", + "\n", + " _stylesheets = [\n", + " \"\"\"\n", + " button {\n", + " background: #0072B5;\n", + " color: white;\n", + " border: none;\n", + " padding: 10px;\n", + " border-radius: 4px;\n", + " }\n", + " button:hover {\n", + " background: #4099da;\n", + " }\n", + " \"\"\"\n", + " ]\n", + "\n", + " _esm = \"\"\"\n", + " export function render({ model }) {\n", + " const btn = document.createElement(\"button\");\n", + " btn.innerHTML = `count is ${model.value}`;\n", + " btn.addEventListener(\"click\", () => {\n", + " model.value += 1\n", + " });\n", + " model.on('value', () => {\n", + " btn.innerHTML = `count is ${model.value}`;\n", + " })\n", + " return btn\n", + " }\n", + " \"\"\"\n", + "\n", + "StyledCounterButton().servable()" + ] + }, + { + "cell_type": "markdown", + "id": "1d16400f-d238-48c6-aeca-73d65ed62027", + "metadata": {}, + "source": [ + "## Send Events from JavaScript to Python\n", + "\n", + "Events from JavaScript can be sent to Python using the `model.send_event` method. Define a *handler* in Python to manage these events. A *handler* is a method on the form `_handle_(self, event)`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a21ae67f-2077-4bf8-9184-b02bf84e4326", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "import param\n", + "\n", + "from panel.custom import JSComponent\n", + "\n", + "\n", + "class EventExample(JSComponent):\n", + "\n", + " value = param.Parameter()\n", + "\n", + " _esm = \"\"\"\n", + " export function render({ model }) {\n", + " const btn = document.createElement('button')\n", + " btn.innerHTML = `Click Me`\n", + " btn.onclick = (event) => model.send_event('click', event)\n", + " return btn\n", + " }\n", + " \"\"\"\n", + "\n", + " def _handle_click(self, event):\n", + " self.value = str(event.__dict__)\n", + "\n", + "button = EventExample()\n", + "pn.Column(\n", + " button, pn.widgets.TextAreaInput(value=button.param.value, height=200),\n", + ").servable()\n" + ] + }, + { + "cell_type": "markdown", + "id": "4979e5b0-7caa-4ffd-a8bd-40680e048935", + "metadata": {}, + "source": [ + "You can also define and send your own custom events:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f99cc078-3b4f-43d8-85aa-983263f1a800", + "metadata": {}, + "outputs": [], + "source": [ + "import datetime\n", + "\n", + "import panel as pn\n", + "import param\n", + "\n", + "from panel.custom import JSComponent\n", + "\n", + "\n", + "class CustomEventExample(JSComponent):\n", + "\n", + " value = param.String()\n", + "\n", + " _esm = \"\"\"\n", + " export function render({ model }) {\n", + " const btn = document.createElement('button')\n", + " btn.innerHTML = `Click Me`;\n", + " btn.onclick = (event) => {\n", + " const currentDate = new Date();\n", + " const custom_event = new CustomEvent(\"click\", { detail: currentDate.getTime() });\n", + " model.send_event('click', custom_event)\n", + " }\n", + " return btn\n", + " }\n", + " \"\"\"\n", + "\n", + " def _handle_click(self, event):\n", + " unix_timestamp = event.data[\"detail\"]/1000\n", + " python_datetime = datetime.datetime.fromtimestamp(unix_timestamp)\n", + " self.value = str(python_datetime)\n", + "\n", + "button = CustomEventExample()\n", + "pn.Column(\n", + " button, button.param.value,\n", + ").servable()\n" + ] + }, + { + "cell_type": "markdown", + "id": "d4b72890-3512-42ad-9352-30c89b7e9c52", + "metadata": {}, + "source": [ + "## Dependency Imports\n", + "\n", + "JavaScript dependencies can be directly imported via URLs, such as those from [`esm.sh`](https://esm.sh/)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0a617ff6-a168-4bc2-8348-304e3d85c70d", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "\n", + "from panel.custom import JSComponent\n", + "\n", + "\n", + "class ConfettiButton(JSComponent):\n", + "\n", + " _esm = \"\"\"\n", + " import confetti from \"https://esm.sh/canvas-confetti@1.6.0\";\n", + "\n", + " export function render() {\n", + " let btn = document.createElement(\"button\");\n", + " btn.innerHTML = \"Click Me\";\n", + " btn.addEventListener(\"click\", () => {\n", + " confetti()\n", + " });\n", + " return btn\n", + " }\n", + " \"\"\"\n", + "\n", + "ConfettiButton().servable()" + ] + }, + { + "cell_type": "markdown", + "id": "8dd08bad-a752-4102-ac7d-a09c4b96991d", + "metadata": {}, + "source": [ + "Use the `_importmap` attribute for more concise module references." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "936b653c-637e-4ecd-bc57-324a007c8802", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "\n", + "from panel.custom import JSComponent\n", + "\n", + "\n", + "class ConfettiButton(JSComponent):\n", + " _importmap = {\n", + " \"imports\": {\n", + " \"canvas-confetti\": \"https://esm.sh/canvas-confetti@1.6.0\",\n", + " }\n", + " }\n", + "\n", + " _esm = \"\"\"\n", + " import confetti from \"canvas-confetti\";\n", + "\n", + " export function render() {\n", + " let btn = document.createElement(\"button\");\n", + " btn.innerHTML = `Click Me`;\n", + " btn.addEventListener(\"click\", () => {\n", + " confetti()\n", + " });\n", + " return btn\n", + " }\n", + " \"\"\"\n", + "\n", + "ConfettiButton().servable()" + ] + }, + { + "cell_type": "markdown", + "id": "80a29850-659f-4254-ac94-0a2b78e9e541", + "metadata": {}, + "source": [ + "See [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) for more info about the import map format." + ] + }, + { + "cell_type": "markdown", + "id": "c890e2e9-9027-4a52-9d21-65fd135b8c38", + "metadata": {}, + "source": [ + "## External Files\n", + "\n", + "You can load JavaScript and CSS from files by providing the paths to these files.\n", + "\n", + "Create the file **counter_button.py**.\n", + "\n", + "```python\n", + "from pathlib import Path\n", + "\n", + "import param\n", + "import panel as pn\n", + "\n", + "from panel.custom import JSComponent\n", + "\n", + "pn.extension()\n", + "\n", + "class CounterButton(JSComponent):\n", + "\n", + " value = param.Integer()\n", + "\n", + " _esm = Path(\"counter_button.js\")\n", + " _stylesheets = [Path(\"counter_button.css\")]\n", + "\n", + "CounterButton().servable()\n", + "```\n", + "\n", + "Now create the file **counter_button.js**.\n", + "\n", + "```javascript\n", + "export function render({ model }) {\n", + " let btn = document.createElement(\"button\");\n", + " btn.innerHTML = `count is ${model.value}`;\n", + " btn.addEventListener(\"click\", () => {\n", + " model.value += 1;\n", + " });\n", + " model.on('value', () => {\n", + " btn.innerHTML = `count is ${model.value}`;\n", + " });\n", + " return btn;\n", + "}\n", + "```\n", + "\n", + "Now create the file **counter_button.css**.\n", + "\n", + "```css\n", + "button {\n", + " background: #0072B5;\n", + " color: white;\n", + " border: none;\n", + " padding: 10px;\n", + " border-radius: 4px;\n", + "}\n", + "button:hover {\n", + " background: #4099da;\n", + "}\n", + "```\n", + "\n", + "Serve the app with `panel serve counter_button.py --autoreload`.\n", + "\n", + "You can now edit the JavaScript or CSS file, and the changes will be automatically reloaded.\n", + "\n", + "- Try changing the `innerHTML` from `count is ${model.value}` to `COUNT IS ${model.value}` and observe the update. Note you must update `innerHTML` in two places.\n", + "- Try changing the background color from `#0072B5` to `#008080`.\n", + "\n", + "## Displaying A Single Child\n", + "\n", + "You can display Panel components (`Viewable`s) by defining a `Child` parameter.\n", + "\n", + "Lets start with the simplest example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6810c585-c55a-428b-85ac-ca1363dfd33a", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "\n", + "from panel.custom import Child, JSComponent\n", + "\n", + "class Example(JSComponent):\n", + "\n", + " child = Child()\n", + "\n", + " _esm = \"\"\"\n", + " export function render({ model }) {\n", + " const button = document.createElement(\"button\");\n", + " button.append(model.get_child(\"child\"))\n", + " return button\n", + " }\"\"\"\n", + "\n", + "Example(child=pn.panel(\"A **Markdown** pane!\")).servable()" + ] + }, + { + "cell_type": "markdown", + "id": "e8f2695f-2ced-427c-88d7-414e97e0ec60", + "metadata": {}, + "source": [ + "If you provide a non-`Viewable` child it will automatically be converted to a `Viewable` by `pn.panel`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d796b164-fe14-44da-8795-666e3d4117a2", + "metadata": {}, + "outputs": [], + "source": [ + "Example(child=\"A **Markdown** pane!\").servable()" + ] + }, + { + "cell_type": "markdown", + "id": "c7bf894f-579a-4e19-b7e9-011879db5fc4", + "metadata": {}, + "source": [ + "If you want to allow a certain type of Panel components only you can specify the specific type in the `class_` argument." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "853faef4-c890-4c81-b661-bdba7e89e9df", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "\n", + "from panel.custom import Child, JSComponent\n", + "\n", + "class Example(JSComponent):\n", + "\n", + " child = Child(class_=pn.pane.Markdown)\n", + "\n", + " _esm = \"\"\"\n", + " export function render({ model }) {\n", + " const button = document.createElement(\"button\");\n", + " button.append(model.get_child(\"child\"))\n", + " return button\n", + " }\"\"\"\n", + "\n", + "Example(child=pn.panel(\"A **Markdown** pane!\")).servable()" + ] + }, + { + "cell_type": "markdown", + "id": "d25ec6a5-5d6f-4def-a3f2-aff9fa0a01b3", + "metadata": {}, + "source": [ + "The `class_` argument also supports a tuple of types:\n", + "\n", + "```python\n", + " child = Child(class_=(pn.pane.Markdown, pn.pane.HTML))\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "bc15da31-0841-4166-8e70-bfa9ea97576a", + "metadata": {}, + "source": [ + "## Displaying a List of Children\n", + "\n", + "You can also display a `List` of `Viewable` objects using the `Children` parameter type:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "71f95d2b-e3fd-49de-890c-901cc0a7693e", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "\n", + "from panel.custom import Children, JSComponent\n", + "\n", + "\n", + "class Example(JSComponent):\n", + "\n", + " objects = Children()\n", + "\n", + " _esm = \"\"\"\n", + " export function render({ model }) {\n", + " const div = document.createElement('div')\n", + " div.append(...model.get_child(\"objects\"))\n", + " return div\n", + " }\"\"\"\n", + "\n", + "\n", + "Example(\n", + " objects=[pn.panel(\"A **Markdown** pane!\"), pn.widgets.Button(name=\"Click me!\"), {\"text\": \"I'm shown as a JSON Pane\"}]\n", + ").servable()\n" + ] + }, + { + "cell_type": "markdown", + "id": "d56af9f3-39bd-4c68-bb8e-f889c534326c", + "metadata": {}, + "source": [ + ":::note\n", + "You can change the `item_type` to a specific subtype of `Viewable` or a tuple of\n", + "`Viewable` subtypes.\n", + ":::\n", + "\n", + "## References\n", + "\n", + "### Tutorials\n", + "\n", + "- [Build Custom Components](../../../how_to/custom_components/reactive_esm/reactive_esm_layout.md)\n", + "\n", + "### How-To Guides\n", + "\n", + "- [Convert `AnyWidget` widgets](../../../how_to/migrate/anywidget/index.md)\n", + "\n", + "### Reference Guides\n", + "\n", + "- [`AnyWidgetComponent`](./AnyWidgetComponent.ipynb)\n", + "- [`JSComponent`](./JSComponent.ipynb)\n", + "- [`ReactComponent`](./ReactComponent.ipynb)" + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/reference/custom_components/JSComponent.md b/examples/reference/custom_components/JSComponent.md deleted file mode 100644 index 8914aaf83f..0000000000 --- a/examples/reference/custom_components/JSComponent.md +++ /dev/null @@ -1,456 +0,0 @@ -# `JSComponent` - -`JSComponent` simplifies the creation of custom Panel components using JavaScript. - -```{pyodide} -import panel as pn -import param - -from panel.custom import JSComponent - -pn.extension() - -class CounterButton(JSComponent): - - value = param.Integer() - - _esm = """ - export function render({ model }) { - let btn = document.createElement("button"); - btn.innerHTML = `count is ${model.value}`; - btn.addEventListener("click", () => { - model.value += 1 - }); - model.on('value', () => { - btn.innerHTML = `count is ${model.value}`; - }) - return btn - } - """ - -CounterButton().servable() -``` - -:::{note} -`JSComponent` was introduced in June 2024 as a successor to `ReactiveHTML`. - -`JSComponent` bears similarities to [`AnyWidget`](https://anywidget.dev/), but it is specifically optimized for use with Panel. - -If you are looking to create custom components using Python and Panel component only, check out [`Viewer`](Viewer.md). -::: - -## API - -### JSComponent Attributes - -- **`_esm`** (str | PurePath): This attribute accepts either a string or a path that points to an [ECMAScript module](https://nodejs.org/api/esm.html#modules-ecmascript-modules). The ECMAScript module should export a `render` function which returns the HTML element to display. In a development environment such as a notebook or when using `--autoreload`, the module will automatically reload upon saving changes. -- **`_importmap`** (dict | None): This optional dictionary defines an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap), allowing you to customize how module specifiers are resolved. -- **`_stylesheets`** (optional list of strings): This optional attribute accepts a list of CSS strings or paths to CSS files. It supports automatic reloading in development environments. - -:::note - -You may specify a path to a file as a string instead of a PurePath. The path should be specified relative to the file its specified in. - -::: - -### `render` Function - -The `_esm` attribute must export the `render` function. It accepts the following parameters: - -- **`model`**: Represents the Parameters of the component and provides methods to add (and remove) event listeners using `.on` and `.off`, render child elements using `.get_child`, and to `.send_event` back to Python. -- **`view`**: The Bokeh view. -- **`el`**: The HTML element that the component will be rendered into. - -Any HTML element returned from the `render` function will be appended to the HTML element (`el`) of the component but you may also manually append to and manipulate the `el` directly. - -### Callbacks - -The `model.on` and `model.off` methods allow registering event handlers inside the render function. This includes the ability to listen to parameter changes and register lifecycle hooks. - -#### Change Events - -The following signatures are valid when listening to change events: - -- `.on('', callback)`: Allows registering an event handler for a single parameter. -- `.on(['', ...], callback)`: Allows adding an event handler for multiple parameters at once. -- `.on('change:', callback)`: The `change:` prefix allows disambiguating change events from lifecycle hooks should a parameter name and lifecycle hook overlap. - -#### Lifecycle Hooks - -- `after_render`: Called once after the component has been fully rendered. -- `after_resize`: Called after the component has been resized. -- `remove`: Called when the component view is being removed from the DOM. - -## Usage - -### Styling with CSS - -Include CSS within the `_stylesheets` attribute to style the component. The CSS is injected directly into the component's HTML. - -```{pyodide} -import panel as pn -import param - -from panel.custom import JSComponent - -pn.extension() - -class StyledCounterButton(JSComponent): - - value = param.Integer() - - _stylesheets = [ - """ - button { - background: #0072B5; - color: white; - border: none; - padding: 10px; - border-radius: 4px; - } - button:hover { - background: #4099da; - } - """ - ] - - _esm = """ - export function render({ model }) { - const btn = document.createElement("button"); - btn.innerHTML = `count is ${model.value}`; - btn.addEventListener("click", () => { - model.value += 1 - }); - model.on('value', () => { - btn.innerHTML = `count is ${model.value}`; - }) - return btn - } - """ - -StyledCounterButton().servable() -``` - -## Send Events from JavaScript to Python - -Events from JavaScript can be sent to Python using the `model.send_event` method. Define a *handler* in Python to manage these events. A *handler* is a method on the form `_handle_(self, event)`: - -```{pyodide} -import panel as pn -import param - -from panel.custom import JSComponent - -pn.extension() - -class EventExample(JSComponent): - - value = param.Parameter() - - _esm = """ - export function render({ model }) { - const btn = document.createElement('button') - btn.innerHTML = `Click Me` - btn.onclick = (event) => model.send_event('click', event) - return btn - } - """ - - def _handle_click(self, event): - self.value = str(event.__dict__) - -button = EventExample() -pn.Column( - button, pn.widgets.TextAreaInput(value=button.param.value, height=200), -).servable() -``` - -You can also define and send your own custom events: - -```{pyodide} -import datetime - -import panel as pn -import param - -from panel.custom import JSComponent - -pn.extension() - -class CustomEventExample(JSComponent): - - value = param.String() - - _esm = """ - export function render({ model }) { - const btn = document.createElement('button') - btn.innerHTML = `Click Me`; - btn.onclick = (event) => { - const currentDate = new Date(); - const custom_event = new CustomEvent("click", { detail: currentDate.getTime() }); - model.send_event('click', custom_event) - } - return btn - } - """ - - def _handle_click(self, event): - unix_timestamp = event.data["detail"]/1000 - python_datetime = datetime.datetime.fromtimestamp(unix_timestamp) - self.value = str(python_datetime) - -button = CustomEventExample() -pn.Column( - button, button.param.value, -).servable() -``` - -## Dependency Imports - -JavaScript dependencies can be directly imported via URLs, such as those from [`esm.sh`](https://esm.sh/). - -```{pyodide} -import panel as pn -from panel.custom import JSComponent - -pn.extension() - -class ConfettiButton(JSComponent): - - _esm = """ - import confetti from "https://esm.sh/canvas-confetti@1.6.0"; - - export function render() { - let btn = document.createElement("button"); - btn.innerHTML = "Click Me"; - btn.addEventListener("click", () => { - confetti() - }); - return btn - } - """ - -ConfettiButton().servable() -``` - -Use the `_importmap` attribute for more concise module references. - -```{pyodide} -import panel as pn - -from panel.custom import JSComponent - -pn.extension() - -class ConfettiButton(JSComponent): - _importmap = { - "imports": { - "canvas-confetti": "https://esm.sh/canvas-confetti@1.6.0", - } - } - - _esm = """ - import confetti from "canvas-confetti"; - - export function render() { - let btn = document.createElement("button"); - btn.innerHTML = `Click Me`; - btn.addEventListener("click", () => { - confetti() - }); - return btn - } - """ - -ConfettiButton().servable() -``` - -See [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) for more info about the import map format. - -## External Files - -You can load JavaScript and CSS from files by providing the paths to these files. - -Create the file **counter_button.py**. - -```python -from pathlib import Path - -import param -import panel as pn - -from panel.custom import JSComponent - -pn.extension() - -class CounterButton(JSComponent): - - value = param.Integer() - - _esm = Path("counter_button.js") - _stylesheets = [Path("counter_button.css")] - -CounterButton().servable() -``` - -Now create the file **counter_button.js**. - -```javascript -export function render({ model }) { - let btn = document.createElement("button"); - btn.innerHTML = `count is ${model.value}`; - btn.addEventListener("click", () => { - model.value += 1; - }); - model.on('value', () => { - btn.innerHTML = `count is ${model.value}`; - }); - return btn; -} -``` - -Now create the file **counter_button.css**. - -```css -button { - background: #0072B5; - color: white; - border: none; - padding: 10px; - border-radius: 4px; -} -button:hover { - background: #4099da; -} -``` - -Serve the app with `panel serve counter_button.py --autoreload`. - -You can now edit the JavaScript or CSS file, and the changes will be automatically reloaded. - -- Try changing the `innerHTML` from `count is ${model.value}` to `COUNT IS ${model.value}` and observe the update. Note you must update `innerHTML` in two places. -- Try changing the background color from `#0072B5` to `#008080`. - -## Displaying A Single Child - -You can display Panel components (`Viewable`s) by defining a `Child` parameter. - -Lets start with the simplest example: - -```{pyodide} -import panel as pn - -from panel.custom import Child, JSComponent - -class Example(JSComponent): - - child = Child() - - _esm = """ - export function render({ model }) { - const button = document.createElement("button"); - button.append(model.get_child("child")) - return button - }""" - -Example(child=pn.panel("A **Markdown** pane!")).servable() -``` - -If you provide a non-`Viewable` child it will automatically be converted to a `Viewable` by `pn.panel`: - -```{pyodide} -Example(child="A **Markdown** pane!").servable() -``` - -If you want to allow a certain type of Panel components only you can specify the specific type in the `class_` argument. - -```{pyodide} -import panel as pn - -from panel.custom import Child, JSComponent - -class Example(JSComponent): - - child = Child(class_=pn.pane.Markdown) - - _esm = """ - export function render({ children }) { - const button = document.createElement("button"); - button.append(model.get_child("child")) - return button - }""" - -Example(child=pn.panel("A **Markdown** pane!")).servable() -``` - -The `class_` argument also supports a tuple of types: - -```{pyodide} -import panel as pn - -from panel.custom import Child, JSComponent - -class Example(JSComponent): - - child = Child(class_=(pn.pane.Markdown, pn.pane.HTML)) - - _esm = """ - export function render({ children }) { - const button = document.createElement("button"); - button.append(model.get_child("child")) - return button - }""" - -Example(child=pn.panel("A **Markdown** pane!")).servable() -``` - -## Displaying a List of Children - -You can also display a `List` of `Viewable` objects using the `Children` parameter type: - -```{pyodide} -import panel as pn - -from panel.custom import Children, JSComponent - -pn.extension() - -class Example(JSComponent): - - objects = Children() - - _esm = """ - export function render({ model }) { - const div = document.createElement('div') - div.append(...model.get_child("objects")) - return div - }""" - - -Example( - objects=[pn.panel("A **Markdown** pane!"), pn.widgets.Button(name="Click me!"), {"text": "I'm shown as a JSON Pane"}] -).servable() -``` - -:::note - -You can change the `item_type` to a specific subtype of `Viewable` or a tuple of -`Viewable` subtypes. - -::: - -## References - -### Tutorials - -- [Build Custom Components](../../../how_to/custom_components/reactive_esm/reactive_esm_layout.md) - -### How-To Guides - -- [Convert `AnyWidget` widgets](../../../how_to/migrate/anywidget/index.md) - -### Reference Guides - -- [`AnyWidgetComponent`](../../../reference/panes/AnyWidgetComponent.md) -- [`JSComponent`](../../../reference/panes/JSComponent.md) -- [`ReactComponent`](../../../reference/panes/ReactComponent.md) diff --git a/examples/reference/custom_components/ReactComponent.ipynb b/examples/reference/custom_components/ReactComponent.ipynb new file mode 100644 index 0000000000..ca5648b2fc --- /dev/null +++ b/examples/reference/custom_components/ReactComponent.ipynb @@ -0,0 +1,632 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "50cd605c-3e41-4909-9214-b0aafd171d25", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "\n", + "pn.extension()" + ] + }, + { + "cell_type": "markdown", + "id": "70eb493f-4216-4d8c-b763-f98b67933cec", + "metadata": {}, + "source": [ + "`ReactComponent` simplifies the creation of custom Panel components by allowing you to write standard [React](https://react.dev/) code without the need to pre-compile or requiring a deep understanding of Javascript build tooling." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce37ac2a-1fea-4e45-8581-b913e0e05097", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "import param\n", + "\n", + "from panel.custom import ReactComponent\n", + "\n", + "\n", + "class CounterButton(ReactComponent):\n", + "\n", + " value = param.Integer()\n", + "\n", + " _esm = \"\"\"\n", + " export function render({model}) {\n", + " const [value, setValue] = model.useState(\"value\");\n", + " return (\n", + " \n", + " )\n", + " }\n", + " \"\"\"\n", + "\n", + "CounterButton().servable()" + ] + }, + { + "cell_type": "markdown", + "id": "296416c3-c6b0-44ee-8ca3-64d9f42acc68", + "metadata": {}, + "source": [ + ":::{note}\n", + "`ReactComponent` extends the [`JSComponent`](JSComponent.md) class, which allows you to create custom Panel components using JavaScript.\n", + "\n", + "`ReactComponent` bears similarities to [`AnyWidget`](https://anywidget.dev/) and [`IpyReact`](https://github.com/widgetti/ipyreact), but `ReactComponent` is specifically optimized for use with Panel and React.\n", + "\n", + "If you are looking to create custom components using Python and Panel component only, check out [`Viewer`](Viewer.md).\n", + ":::\n", + "\n", + "## API\n", + "\n", + "### ReactComponent Attributes\n", + "\n", + "- **`_esm`** (str | PurePath): This attribute accepts either a string or a path that points to an [ECMAScript module](https://nodejs.org/api/esm.html#modules-ecmascript-modules). The ECMAScript module should export a `render` function which returns the HTML element to display. In a development environment such as a notebook or when using `--autoreload`, the module will automatically reload upon saving changes. You can use [`JSX`](https://react.dev/learn/writing-markup-with-jsx) and [`TypeScript`](https://www.typescriptlang.org/). The `_esm` script is transpiled on the fly using [Sucrase](https://sucrase.io/). The global namespace contains a `React` object that provides access to React hooks.\n", + "- **`_importmap`** (dict | None): This optional dictionary defines an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap), allowing you to customize how module specifiers are resolved.\n", + "- **`_stylesheets`** (List[str | PurePath] | None): This optional attribute accepts a list of CSS strings or paths to CSS files. It supports automatic reloading in development environments.\n", + "\n", + ":::note\n", + "\n", + "You may specify a path to a file as a string instead of a PurePath. The path should be specified relative to the file its specified in.\n", + "\n", + ":::\n", + "\n", + "#### `render` Function\n", + "\n", + "The `_esm` attribute must export the `render` function. It accepts the following parameters:\n", + "\n", + "- **`model`**: Represents the Parameters of the component and provides methods to add (and remove) event listeners using `.on` and `.off`, render child React components using `.get_child`, get a state hook for a parameter value using `.useState` and to `.send_event` back to Python.\n", + "- **`view`**: The Bokeh view.\n", + "- **`el`**: The HTML element that the component will be rendered into.\n", + "\n", + "Any React component returned from the `render` function will be appended to the HTML element (`el`) of the component.\n", + "\n", + "### State Hooks\n", + "\n", + "The recommended approach to build components that depend on parameters in Python is to create [`useState` hooks](https://react.dev/reference/react/useState) by calling `model.useState('')`. The `model.useState` method returns an array with exactly two values:\n", + "\n", + "1. The current state. During the first render, it will match the initialState you have passed.\n", + "2. The set function that lets you update the state to a different value and trigger a re-render.\n", + "\n", + "Using the state value in your React component will automatically re-render the component when it is updated.\n", + "\n", + "### Callbacks\n", + "\n", + "The `model.on` and `model.off` methods allow registering event handlers inside the render function. This includes the ability to listen to parameter changes and register lifecycle hooks.\n", + "\n", + "#### Change Events\n", + "\n", + "The following signatures are valid when listening to change events:\n", + "\n", + "- `.on('', callback)`: Allows registering an event handler for a single parameter.\n", + "- `.on(['', ...], callback)`: Allows adding an event handler for multiple parameters at once.\n", + "- `.on('change:', callback)`: The `change:` prefix allows disambiguating change events from lifecycle hooks should a parameter name and lifecycle hook overlap.\n", + "\n", + "#### Lifecycle Hooks\n", + "\n", + "- `after_render`: Called once after the component has been fully rendered.\n", + "- `after_resize`: Called after the component has been resized.\n", + "- `remove`: Called when the component view is being removed from the DOM.\n", + "\n", + "## Usage\n", + "\n", + "### Styling with CSS\n", + "\n", + "Include CSS within the `_stylesheets` attribute to style the component. The CSS is injected directly into the component's HTML." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3f11eac1-32c0-405a-9eb3-c5dd715a2d89", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "import param\n", + "\n", + "from panel.custom import ReactComponent\n", + "\n", + "\n", + "class CounterButton(ReactComponent):\n", + "\n", + " value = param.Integer()\n", + "\n", + " _stylesheets = [\n", + " \"\"\"\n", + " button {\n", + " background: #0072B5;\n", + " color: white;\n", + " border: none;\n", + " padding: 10px;\n", + " border-radius: 4px;\n", + " }\n", + " button:hover {\n", + " background: #4099da;\n", + " }\n", + " \"\"\"\n", + " ]\n", + "\n", + " _esm = \"\"\"\n", + " export function render({ model }) {\n", + " const [value, setValue] = model.useState(\"value\");\n", + " return (\n", + " \n", + " );\n", + " }\n", + " \"\"\"\n", + "\n", + "CounterButton().servable()" + ] + }, + { + "cell_type": "markdown", + "id": "d01593d5-e955-4d4c-a552-f5805f1f93bf", + "metadata": {}, + "source": [ + "## Send Events from JavaScript to Python\n", + "\n", + "Events from JavaScript can be sent to Python using the `model.send_event` method. Define a handler in Python to manage these events. A *handler* is a method on the form `_handle_(self, event)`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "58fbec62-51fd-4b8b-babf-d09957c19726", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "import param\n", + "\n", + "from panel.custom import ReactComponent\n", + "\n", + "pn.extension()\n", + "\n", + "class EventExample(ReactComponent):\n", + "\n", + " value = param.Parameter()\n", + "\n", + " _esm = \"\"\"\n", + " export function render({ model }) {\n", + " return (\n", + " \n", + " );\n", + " }\n", + " \"\"\"\n", + "\n", + " def _handle_click(self, event):\n", + " self.value = str(event.__dict__)\n", + "\n", + "button = EventExample()\n", + "pn.Column(\n", + " button, pn.widgets.TextAreaInput(value=button.param.value, height=200),\n", + ").servable()" + ] + }, + { + "cell_type": "markdown", + "id": "39dfcddb-09ab-4f87-816a-7a2c83b86ff4", + "metadata": {}, + "source": [ + "You can also define and send your own custom events:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "458d0dfe-a4e3-4326-999b-856426f1d4d4", + "metadata": {}, + "outputs": [], + "source": [ + "import datetime\n", + "\n", + "import panel as pn\n", + "import param\n", + "\n", + "from panel.custom import ReactComponent\n", + "\n", + "\n", + "class CustomEventExample(ReactComponent):\n", + "\n", + " value = param.String()\n", + "\n", + " _esm = \"\"\"\n", + " function send_event(model) {\n", + " const currentDate = new Date();\n", + " const custom_event = new CustomEvent(\"click\", { detail: currentDate.getTime() });\n", + " model.send_event('click', custom_event)\n", + " }\n", + "\n", + " export function render({ model }) {\n", + " return (\n", + " \n", + " );\n", + " }\n", + " \"\"\"\n", + "\n", + " def _handle_click(self, event):\n", + " unix_timestamp = event.data[\"detail\"]/1000\n", + " python_datetime = datetime.datetime.fromtimestamp(unix_timestamp)\n", + " self.value = str(python_datetime)\n", + "\n", + "button = CustomEventExample()\n", + "pn.Column(\n", + " button, button.param.value,\n", + ").servable()" + ] + }, + { + "cell_type": "markdown", + "id": "8dd62b24-2a26-49ff-8aad-386178128167", + "metadata": {}, + "source": [ + "## Dependency Imports\n", + "\n", + "JavaScript dependencies can be directly imported via URLs, such as those from [`esm.sh`](https://esm.sh/)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9df3c3ec-fe28-4043-b888-60418eae7e24", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "\n", + "from panel.custom import ReactComponent\n", + "\n", + "\n", + "class ConfettiButton(ReactComponent):\n", + "\n", + " _esm = \"\"\"\n", + " import confetti from \"https://esm.sh/canvas-confetti@1.6.0\";\n", + "\n", + " export function render() {\n", + " return (\n", + " \n", + " );\n", + " }\n", + " \"\"\"\n", + "\n", + "ConfettiButton().servable()" + ] + }, + { + "cell_type": "markdown", + "id": "0a9fe73e-c552-4cef-85e0-005386520d91", + "metadata": {}, + "source": [ + "Use the `_importmap` attribute for more concise module references." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5cac202e-f347-4132-b466-bd70713f027c", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "\n", + "from panel.custom import ReactComponent\n", + "\n", + "\n", + "class ConfettiButton(ReactComponent):\n", + " _importmap = {\n", + " \"imports\": {\n", + " \"canvas-confetti\": \"https://esm.sh/canvas-confetti@1.6.0\",\n", + " }\n", + " }\n", + "\n", + " _esm = \"\"\"\n", + " import confetti from \"canvas-confetti\";\n", + "\n", + " export function render() {\n", + " return (\n", + " \n", + " );\n", + " }\n", + " \"\"\"\n", + "\n", + "ConfettiButton().servable()" + ] + }, + { + "cell_type": "markdown", + "id": "6aec4fdd-152b-4fe8-a545-fd4b9daf989a", + "metadata": {}, + "source": [ + "See [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) for more info about the import map format.\n" + ] + }, + { + "cell_type": "markdown", + "id": "adc92e37-2f42-4166-80ca-712753021cea", + "metadata": {}, + "source": [ + "## External Files\n", + "\n", + "You can load JSX and CSS from files by providing the paths to these files.\n", + "\n", + "Create the file **counter_button.py**.\n", + "\n", + "```python\n", + "from pathlib import Path\n", + "\n", + "import param\n", + "import panel as pn\n", + "\n", + "from panel.custom import ReactComponent\n", + "\n", + "pn.extension()\n", + "\n", + "class CounterButton(ReactComponent):\n", + "\n", + " value = param.Integer()\n", + "\n", + " _esm = \"counter_button.jsx\"\n", + " _stylesheets = [Path(\"counter_button.css\")]\n", + "\n", + "CounterButton().servable()\n", + "```\n", + "\n", + "Now create the file **counter_button.jsx**.\n", + "\n", + "```javascript\n", + "export function render({ model }) {\n", + " const [value, setValue] = model.useState(\"value\");\n", + " return (\n", + " \n", + " );\n", + "}\n", + "```\n", + "\n", + "Now create the file **counter_button.css**.\n", + "\n", + "```css\n", + "button {\n", + " background: #0072B5;\n", + " color: white;\n", + " border: none;\n", + " padding: 10px;\n", + " border-radius: 4px;\n", + "}\n", + "button:hover {\n", + " background: #4099da;\n", + "}\n", + "```\n", + "\n", + "Serve the app with `panel serve counter_button.py --autoreload`.\n", + "\n", + "You can now edit the JSX or CSS file, and the changes will be automatically reloaded.\n", + "\n", + "- Try changing `count is {value}` to `COUNT IS {value}` and observe the update.\n", + "- Try changing the background color from `#0072B5` to `#008080`.\n", + "\n", + "## Displaying A Single Child\n", + "\n", + "You can display Panel components (`Viewable`s) by defining a `Child` parameter.\n", + "\n", + "Lets start with the simplest example\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19868da7-2d89-472c-a5a0-a286bb8319e3", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "\n", + "from panel.custom import Child, ReactComponent\n", + "\n", + "class Example(ReactComponent):\n", + "\n", + " child = Child()\n", + "\n", + " _esm = \"\"\"\n", + " export function render({ model }) {\n", + " return \n", + " }\n", + " \"\"\"\n", + "\n", + "Example(child=pn.panel(\"A **Markdown** pane!\")).servable()" + ] + }, + { + "cell_type": "markdown", + "id": "9373c469-befd-45d0-8049-3e4dd8be58bb", + "metadata": {}, + "source": [ + "If you provide a non-`Viewable` child it will automatically be converted to a `Viewable` by `pn.panel`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b1b98091-0e75-43c5-bb36-055d36e7bf5d", + "metadata": {}, + "outputs": [], + "source": [ + "Example(child=\"A **Markdown** pane!\").servable()" + ] + }, + { + "cell_type": "markdown", + "id": "9f3f2e65-ded4-4e19-8451-d10118a43d41", + "metadata": {}, + "source": [ + "If you want to allow a certain type of Panel components only you can specify the specific type in the `class_` argument." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f3419e6f-6990-412c-ab82-fd4fceba98c6", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "\n", + "from panel.custom import Child, ReactComponent\n", + "\n", + "class Example(ReactComponent):\n", + "\n", + " child = Child(class_=pn.pane.Markdown)\n", + "\n", + " _esm = \"\"\"\n", + " export function render({ model }) {\n", + " return \n", + " }\n", + " \"\"\"\n", + "\n", + "Example(child=pn.panel(\"A **Markdown** pane!\")).servable()" + ] + }, + { + "cell_type": "markdown", + "id": "868da08a-c73f-4338-96c7-4d2f6196fd2a", + "metadata": {}, + "source": [ + "The `class_` argument also supports a tuple of types:\n", + "\n", + "```python\n", + " child = Child(class_=(pn.pane.Markdown, pn.pane.HTML))\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "4d7d9fd8-a785-459f-95d2-c658547afe84", + "metadata": {}, + "source": [ + "## Displaying a List of Children\n", + "\n", + "You can also display a `List` of `Viewable` objects using the `Children` parameter type:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cded9c3f-9ce7-4e91-9ff9-cbb84f5074f7", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "\n", + "from panel.custom import Children, ReactComponent\n", + "\n", + "class Example(ReactComponent):\n", + "\n", + " objects = Children()\n", + "\n", + " _esm = \"\"\"\n", + " export function render({ model }) {\n", + " return
{model.get_child(\"objects\")}
\n", + " }\"\"\"\n", + "\n", + "\n", + "Example(\n", + " objects=[pn.panel(\"A **Markdown** pane!\"), pn.widgets.Button(name=\"Click me!\"), {\"text\": \"I'm shown as a JSON Pane\"}]\n", + ").servable()" + ] + }, + { + "cell_type": "markdown", + "id": "bf8087ea-c135-4edb-bb3a-e864f7b7be3e", + "metadata": {}, + "source": [ + ":::note\n", + "You can change the `item_type` to a specific subtype of `Viewable` or a tuple of `Viewable` subtypes.\n", + ":::\n", + "\n", + "## Using React Hooks\n", + "\n", + "The global namespace also contains a `React` object that provides access to React hooks. Here is an example of a simple counter button using the `useState` hook:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "472746d9-b237-4f85-9ba1-5abf9b6b130d", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "\n", + "from panel.custom import ReactComponent\n", + "\n", + "pn.extension()\n", + "\n", + "class CounterButton(ReactComponent):\n", + "\n", + " _esm = \"\"\"\n", + " let { useState } = React;\n", + "\n", + " export function render() {\n", + " const [value, setValue] = useState(0);\n", + " return (\n", + " \n", + " );\n", + " }\n", + " \"\"\"\n", + "\n", + "CounterButton().servable()" + ] + }, + { + "cell_type": "markdown", + "id": "6c93522e-ba75-49c9-a8e0-9dffd1c042f6", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "### Tutorials\n", + "\n", + "- [Build Custom Components](../../../how_to/custom_components/reactive_esm/reactive_esm_layout.md)\n", + "\n", + "### How-To Guides\n", + "\n", + "- [Convert `AnyWidget` widgets](../../../how_to/migrate/anywidget/index.md)\n", + "\n", + "### Reference Guides\n", + "\n", + "- [`AnyWidgetComponent`](./AnyWidgetComponent.ipynb)\n", + "- [`JSComponent`](./JSComponent.ipynb)\n", + "- [`ReactComponent`](./ReactComponent.ipynb)" + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/reference/custom_components/ReactComponent.md b/examples/reference/custom_components/ReactComponent.md deleted file mode 100644 index f80c4fc079..0000000000 --- a/examples/reference/custom_components/ReactComponent.md +++ /dev/null @@ -1,483 +0,0 @@ -# `ReactComponent` - -`ReactComponent` simplifies the creation of custom Panel components by allowing you to write standard [React](https://react.dev/) code without the need to pre-compile or requiring a deep understanding of Javascript build tooling. - -```{pyodide} -import panel as pn -import param - -from panel.custom import ReactComponent - -pn.extension() - -class CounterButton(ReactComponent): - - value = param.Integer() - - _esm = """ - export function render({model}) { - const [value, setValue] = model.useState("value"); - return ( - - ) - } - """ - -CounterButton().servable() -``` - -:::{note} - -`ReactComponent` extends the [`JSComponent`](JSComponent.md) class, which allows you to create custom Panel components using JavaScript. - -`ReactComponent` bears similarities to [`AnyWidget`](https://anywidget.dev/) and [`IpyReact`](https://github.com/widgetti/ipyreact), but `ReactComponent` is specifically optimized for use with Panel and React. - -If you are looking to create custom components using Python and Panel component only, check out [`Viewer`](Viewer.md). - -::: - -## API - -### ReactComponent Attributes - -- **`_esm`** (str | PurePath): This attribute accepts either a string or a path that points to an [ECMAScript module](https://nodejs.org/api/esm.html#modules-ecmascript-modules). The ECMAScript module should export a `render` function which returns the HTML element to display. In a development environment such as a notebook or when using `--autoreload`, the module will automatically reload upon saving changes. You can use [`JSX`](https://react.dev/learn/writing-markup-with-jsx) and [`TypeScript`](https://www.typescriptlang.org/). The `_esm` script is transpiled on the fly using [Sucrase](https://sucrase.io/). The global namespace contains a `React` object that provides access to React hooks. -- **`_importmap`** (dict | None): This optional dictionary defines an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap), allowing you to customize how module specifiers are resolved. -- **`_stylesheets`** (List[str | PurePath] | None): This optional attribute accepts a list of CSS strings or paths to CSS files. It supports automatic reloading in development environments. - -:::note - -You may specify a path to a file as a string instead of a PurePath. The path should be specified relative to the file its specified in. - -::: - -#### `render` Function - -The `_esm` attribute must export the `render` function. It accepts the following parameters: - -- **`model`**: Represents the Parameters of the component and provides methods to add (and remove) event listeners using `.on` and `.off`, render child React components using `.get_child`, get a state hook for a parameter value using `.useState` and to `.send_event` back to Python. -- **`view`**: The Bokeh view. -- **`el`**: The HTML element that the component will be rendered into. - -Any React component returned from the `render` function will be appended to the HTML element (`el`) of the component. - -### State Hooks - -The recommended approach to build components that depend on parameters in Python is to create [`useState` hooks](https://react.dev/reference/react/useState) by calling `model.useState('')`. The `model.useState` method returns an array with exactly two values: - -1. The current state. During the first render, it will match the initialState you have passed. -2. The set function that lets you update the state to a different value and trigger a re-render. - -Using the state value in your React component will automatically re-render the component when it is updated. - -### Callbacks - -The `model.on` and `model.off` methods allow registering event handlers inside the render function. This includes the ability to listen to parameter changes and register lifecycle hooks. - -#### Change Events - -The following signatures are valid when listening to change events: - -- `.on('', callback)`: Allows registering an event handler for a single parameter. -- `.on(['', ...], callback)`: Allows adding an event handler for multiple parameters at once. -- `.on('change:', callback)`: The `change:` prefix allows disambiguating change events from lifecycle hooks should a parameter name and lifecycle hook overlap. - -#### Lifecycle Hooks - -- `after_render`: Called once after the component has been fully rendered. -- `after_resize`: Called after the component has been resized. -- `remove`: Called when the component view is being removed from the DOM. - -## Usage - -### Styling with CSS - -Include CSS within the `_stylesheets` attribute to style the component. The CSS is injected directly into the component's HTML. - -```{pyodide} -import panel as pn -import param - -from panel.custom import ReactComponent - -pn.extension() - -class CounterButton(ReactComponent): - - value = param.Integer() - - _stylesheets = [ - """ - button { - background: #0072B5; - color: white; - border: none; - padding: 10px; - border-radius: 4px; - } - button:hover { - background: #4099da; - } - """ - ] - - _esm = """ - export function render({ model }) { - const [value, setValue] = model.useState("value"); - return ( - - ); - } - """ - -CounterButton().servable() -``` - -## Send Events from JavaScript to Python - -Events from JavaScript can be sent to Python using the `model.send_event` method. Define a handler in Python to manage these events. A *handler* is a method on the form `_handle_(self, event)`: - -```{pyodide} -import panel as pn -import param - -from panel.custom import ReactComponent - -pn.extension() - -class EventExample(ReactComponent): - - value = param.Parameter() - - _esm = """ - export function render({ model }) { - return ( - - ); - } - """ - - def _handle_click(self, event): - self.value = str(event.__dict__) - -button = EventExample() -pn.Column( - button, pn.widgets.TextAreaInput(value=button.param.value, height=200), -).servable() -``` - -You can also define and send your own custom events: - -```{pyodide} -import datetime - -import panel as pn -import param - -from panel.custom import ReactComponent - -pn.extension() - -class CustomEventExample(ReactComponent): - - value = param.String() - - _esm = """ - function send_event(model) { - const currentDate = new Date(); - const custom_event = new CustomEvent("click", { detail: currentDate.getTime() }); - model.send_event('click', custom_event) - } - - export function render({ model }) { - return ( - - ); - } - """ - - def _handle_click(self, event): - unix_timestamp = event.data["detail"]/1000 - python_datetime = datetime.datetime.fromtimestamp(unix_timestamp) - self.value = str(python_datetime) - -button = CustomEventExample() -pn.Column( - button, button.param.value, -).servable() -``` - -## Dependency Imports - -JavaScript dependencies can be directly imported via URLs, such as those from [`esm.sh`](https://esm.sh/). - -```{pyodide} -import panel as pn - -from panel.custom import ReactComponent - -pn.extension() - -class ConfettiButton(ReactComponent): - - _esm = """ - import confetti from "https://esm.sh/canvas-confetti@1.6.0"; - - export function render() { - return ( - - ); - } - """ - -ConfettiButton().servable() -``` - -Use the `_importmap` attribute for more concise module references. - -```{pyodide} -import panel as pn - -from panel.custom import ReactComponent - -pn.extension() - -class ConfettiButton(ReactComponent): - _importmap = { - "imports": { - "canvas-confetti": "https://esm.sh/canvas-confetti@1.6.0", - } - } - - _esm = """ - import confetti from "canvas-confetti"; - - export function render() { - return ( - - ); - } - """ - -ConfettiButton().servable() -``` - -See [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) for more info about the import map format. - -## External Files - -You can load JSX and CSS from files by providing the paths to these files. - -Create the file **counter_button.py**. - -```python -from pathlib import Path - -import param -import panel as pn - -from panel.custom import ReactComponent - -pn.extension() - -class CounterButton(ReactComponent): - - value = param.Integer() - - _esm = "counter_button.jsx" - _stylesheets = [Path("counter_button.css")] - -CounterButton().servable() -``` - -Now create the file **counter_button.jsx**. - -```javascript -export function render({ model }) { - const [value, setValue] = model.useState("value"); - return ( - - ); -} -``` - -Now create the file **counter_button.css**. - -```css -button { - background: #0072B5; - color: white; - border: none; - padding: 10px; - border-radius: 4px; -} -button:hover { - background: #4099da; -} -``` - -Serve the app with `panel serve counter_button.py --autoreload`. - -You can now edit the JSX or CSS file, and the changes will be automatically reloaded. - -- Try changing `count is {value}` to `COUNT IS {value}` and observe the update. -- Try changing the background color from `#0072B5` to `#008080`. - -## Displaying A Single Child - -You can display Panel components (`Viewable`s) by defining a `Child` parameter. - -Lets start with the simplest example - -```{pyodide} -import panel as pn - -from panel.custom import Child, ReactComponent - -class Example(ReactComponent): - - child = Child() - - _esm = """ - export function render({ model }) { - return - } - """ - -Example(child=pn.panel("A **Markdown** pane!")).servable() -``` - -If you provide a non-`Viewable` child it will automatically be converted to a `Viewable` by `pn.panel`: - -```{pyodide} -Example(child="A **Markdown** pane!").servable() -``` - -If you want to allow a certain type of Panel components only you can specify the specific type in the `class_` argument. - -```{pyodide} -import panel as pn - -from panel.custom import Child, ReactComponent - -class Example(ReactComponent): - - child = Child(class_=pn.pane.Markdown) - - _esm = """ - export function render({ model }) { - return - } - """ - -Example(child=pn.panel("A **Markdown** pane!")).servable() -``` - -The `class_` argument also supports a tuple of types: - -```{pyodide} -import panel as pn - -from panel.custom import Child, ReactComponent - -class Example(ReactComponent): - - child = Child(class_=(pn.pane.Markdown, pn.pane.HTML)) - - _esm = """ - export function render({ model }) { - return - } - """ - -Example(child=pn.panel("A **Markdown** pane!")).servable() -``` - -## Displaying a List of Children - -You can also display a `List` of `Viewable` objects using the `Children` parameter type: - -```{pyodide} -import panel as pn - -from panel.custom import Children, ReactComponent - -class Example(ReactComponent): - - objects = Children() - - _esm = """ - export function render({ model }) { - return
{model.get_child("objects")}
- }""" - - -Example( - objects=[pn.panel("A **Markdown** pane!"), pn.widgets.Button(name="Click me!"), {"text": "I'm shown as a JSON Pane"}] -).servable() -``` - -:::note - -You can change the `item_type` to a specific subtype of `Viewable` or a tuple of -`Viewable` subtypes. - -::: - -## Using React Hooks - -The global namespace also contains a `React` object that provides access to React hooks. Here is an example of a simple counter button using the `useState` hook: - -```{pyodide} -import panel as pn - -from panel.custom import ReactComponent - -pn.extension() - -class CounterButton(ReactComponent): - - _esm = """ - let { useState } = React; - - export function render() { - const [value, setValue] = useState(0); - return ( - - ); - } - """ - -CounterButton().servable() -``` - -## References - -### Tutorials - -- [Build Custom Components](../../../how_to/custom_components/reactive_esm/reactive_esm_layout.md) - -### How-To Guides - -- [Convert `AnyWidget` widgets](../../../how_to/migrate/anywidget/index.md) - -### Reference Guides - -- [`AnyWidgetComponent`](../../../reference/panes/AnyWidgetComponent.md) -- [`JSComponent`](../../../reference/panes/JSComponent.md) -- [`ReactComponent`](../../../reference/panes/ReactComponent.md) diff --git a/examples/reference/custom_components/Viewer.ipynb b/examples/reference/custom_components/Viewer.ipynb new file mode 100644 index 0000000000..51c07997d1 --- /dev/null +++ b/examples/reference/custom_components/Viewer.ipynb @@ -0,0 +1,354 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "6dd69519-afc9-4065-ad12-8253c708f5af", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "\n", + "pn.extension()" + ] + }, + { + "cell_type": "markdown", + "id": "a796364a-f1cd-411a-b7fd-d4354794474e", + "metadata": {}, + "source": [ + "`Viewer` simplifies the creation of custom Panel components using Python and Panel components only." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9164b959-7dc5-4579-b65c-1b3827803ca0", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "import param\n", + "\n", + "from panel.viewable import Viewer\n", + "\n", + "\n", + "class CounterButton(Viewer):\n", + "\n", + " value = param.Integer()\n", + "\n", + " def __init__(self, **params):\n", + " super().__init__()\n", + " self._layout = pn.widgets.Button(\n", + " name=self._button_name, on_click=self._on_click, **params\n", + " )\n", + "\n", + " def _on_click(self, event):\n", + " self.value += 1\n", + "\n", + " @param.depends(\"value\")\n", + " def _button_name(self):\n", + " return f\"count is {self.value}\"\n", + "\n", + " def __panel__(self):\n", + " return self._layout\n", + "\n", + "CounterButton()" + ] + }, + { + "cell_type": "markdown", + "id": "78fda29d-eedd-45e4-a02c-e0771ac6b182", + "metadata": {}, + "source": [ + ":::{note}\n", + "If you are looking to create new components using JavaScript, check out [`JSComponent`](JSComponent.md), [`ReactComponent`](ReactComponent.md), or [`AnyWidgetComponent`](AnyWidgetComponent.md) instead.\n", + ":::\n", + "\n", + "## API\n", + "\n", + "### Attributes\n", + "\n", + "None. The `Viewer` class does not have any special attributes. It is a simple `param.Parameterized` class with a few additional methods. This also means you will have to add or support parameters like `height`, `width`, `sizing_mode`, etc., yourself if needed.\n", + "\n", + "### Methods\n", + "\n", + "- **`__panel__`**: Must be implemented. Should return the Panel component or object to be displayed.\n", + "- **`servable`**: This method serves the component using Panel's built-in server when running `panel serve ...`.\n", + "- **`show`**: Displays the component in a new browser tab when running `python ...`.\n", + "\n", + "## Usage\n", + "\n", + "### Styling with CSS\n", + "\n", + "You can style the component by styling the component(s) returned by `__panel__` using their `styles` or `stylesheets` attributes.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "68f36432-40e1-4098-a195-12474dbf4a83", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "import param\n", + "\n", + "from panel.viewable import Viewer\n", + "\n", + "\n", + "class StyledCounterButton(Viewer):\n", + "\n", + " value = param.Integer()\n", + "\n", + " _stylesheets = [\n", + " \"\"\"\n", + " :host(.solid) .bk-btn.bk-btn-default\n", + " {\n", + " background: #0072B5;\n", + " color: white;\n", + " border: none;\n", + " padding: 10px;\n", + " border-radius: 4px;\n", + " }\n", + " :host(.solid) .bk-btn.bk-btn-default:hover {\n", + " background: #4099da;\n", + " }\n", + " \"\"\"\n", + " ]\n", + "\n", + " def __init__(self, **params):\n", + " super().__init__()\n", + "\n", + " self._layout = pn.widgets.Button(\n", + " name=self._button_name,\n", + " on_click=self._on_click,\n", + " stylesheets=self._stylesheets,\n", + " **params,\n", + " )\n", + "\n", + " def _on_click(self, event):\n", + " self.value += 1\n", + "\n", + " @param.depends(\"value\")\n", + " def _button_name(self):\n", + " return f\"Clicked {self.value} times\"\n", + "\n", + " def __panel__(self):\n", + " return self._layout\n", + "\n", + "\n", + "StyledCounterButton().servable()" + ] + }, + { + "cell_type": "markdown", + "id": "3b134810-94f9-4ccf-a257-aa07cb48d6f3", + "metadata": {}, + "source": [ + "See the [Apply CSS](../../how_to/styling/apply_css.md) guide for more information on styling Panel components.\n", + "\n", + "## Displaying A Single Child\n", + "\n", + "You can display Panel components (`Viewable`s) by defining a `Child` parameter.\n", + "\n", + "Let's start with the simplest example:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42f21642-ad17-4c21-b186-d894c90da554", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "\n", + "from panel.custom import Child\n", + "from panel.viewable import Viewer\n", + "\n", + "class SingleChild(Viewer):\n", + "\n", + " object = Child()\n", + "\n", + " def __panel__(self):\n", + " return pn.Column(\"A Single Child\", self.param.object.rx())\n", + "\n", + "single_child = SingleChild(object=pn.pane.Markdown(\"A **Markdown** pane!\"))\n", + "\n", + "single_child.servable()" + ] + }, + { + "cell_type": "markdown", + "id": "9ba02dcb-c83b-4212-ac1f-8e14e895d485", + "metadata": {}, + "source": [ + "Calling `self.param.object.rx()` creates a reactive expression which updates when the `object` parameter is updated.\n", + "\n", + "Let's replace the `object` with a `Button`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6e386a52-d256-47a9-9139-a0e81e7bad88", + "metadata": {}, + "outputs": [], + "source": [ + "single_child.object = pn.widgets.Button(name=\"Click me\")" + ] + }, + { + "cell_type": "markdown", + "id": "9dbf41ea-b18f-4421-977a-291612cda6f7", + "metadata": {}, + "source": [ + "Let's change it back" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "880176b1-5052-4da1-8fbb-cce9e39524ab", + "metadata": {}, + "outputs": [], + "source": [ + "single_child.object = pn.pane.Markdown(\"A **Markdown** pane!\")" + ] + }, + { + "cell_type": "markdown", + "id": "10cc810a-6001-49d3-a5e1-798252e96a5d", + "metadata": {}, + "source": [ + "If you provide a non-`Viewable` child it will automatically be converted to a `Viewable` by `pn.panel`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d929e9b1-62da-4122-bc78-7129988de486", + "metadata": {}, + "outputs": [], + "source": [ + "SingleChild(object=\"A **Markdown** pane!\").servable()" + ] + }, + { + "cell_type": "markdown", + "id": "417c59c4-4e0d-46d1-a762-98af457034d5", + "metadata": {}, + "source": [ + "If you want to allow a certain type of Panel components only, you can specify the specific type in the `class_` argument." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f02e8336-a3fa-4e58-b457-d71f5d84fd18", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "\n", + "from panel.custom import Child\n", + "from panel.viewable import Viewer\n", + "\n", + "class SingleChild(Viewer):\n", + "\n", + " object = Child(class_=pn.pane.Markdown)\n", + "\n", + " def __panel__(self):\n", + " return pn.Column(\"A Single Child\", self.param.object.rx())\n", + "\n", + "\n", + "SingleChild(object=pn.pane.Markdown(\"A **Markdown** pane!\")).servable()" + ] + }, + { + "cell_type": "markdown", + "id": "8ba7de66-21a3-4ab8-8eb2-0e6fd7d488fd", + "metadata": {}, + "source": [ + "The `class_` argument also supports a tuple of types:\n", + "\n", + "```python\n", + " object = Child(class_=(pn.pane.Markdown, pn.widgets.Button))\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "000a0276-717b-4984-9d1e-e936ba8c1a05", + "metadata": {}, + "source": [ + "## Displaying a List of Children\n", + "\n", + "You can also display a `List` of `Viewable` objects using the `Children` parameter type:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a4fa17c5-65d2-4c1a-a311-aad61a4bf433", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "\n", + "from panel.custom import Children\n", + "from panel.viewable import Viewer\n", + "\n", + "\n", + "class MultipleChildren(Viewer):\n", + "\n", + " objects = Children()\n", + "\n", + " def __init__(self, **params):\n", + " super().__init__(**params)\n", + " self._layout = pn.Column(objects=self.param['objects'], styles={\"background\": \"silver\"})\n", + "\n", + " def __panel__(self):\n", + " return self._layout\n", + "\n", + "\n", + "MultipleChildren(\n", + " objects=[\n", + " pn.panel(\"A **Markdown** pane!\"),\n", + " pn.widgets.Button(name=\"Click me!\"),\n", + " {\"text\": \"I'm shown as a JSON Pane\"},\n", + " ]\n", + ").servable()\n" + ] + }, + { + "cell_type": "markdown", + "id": "60ebdbd0-48c8-4f21-9a57-7b49e31523ae", + "metadata": {}, + "source": [ + ":::note\n", + "You can change the `item_type` to a specific subtype of `Viewable` or a tuple of `Viewable` subtypes.\n", + ":::\n", + "\n", + "## References\n", + "\n", + "### Tutorials\n", + "\n", + "- [Reusable Components](../../../tutorials/intermediate/reusable_components.md)\n", + "\n", + "### How-To Guides\n", + "\n", + "- [Combine Existing Widgets](../../../how_to/custom_components/custom_viewer.md)\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/reference/custom_components/Viewer.md b/examples/reference/custom_components/Viewer.md deleted file mode 100644 index aa634b1ffc..0000000000 --- a/examples/reference/custom_components/Viewer.md +++ /dev/null @@ -1,258 +0,0 @@ -# `Viewer` - -`Viewer` simplifies the creation of custom Panel components using Python and Panel components only. - -```{pyodide} -import panel as pn -import param - -from panel.viewable import Viewer - -pn.extension() - -class CounterButton(Viewer): - - value = param.Integer() - - def __init__(self, **params): - super().__init__() - - self._layout = pn.widgets.Button( - name=self._button_name, on_click=self._on_click, **params - ) - - def _on_click(self, event): - self.value += 1 - - @param.depends("value") - def _button_name(self): - return f"Clicked {self.value} times" - - def __panel__(self): - return self._layout - -CounterButton().servable() -``` - -:::{note} - -If you are looking to create new components using JavaScript, check out [`JSComponent`](JSComponent.md), [`ReactComponent`](ReactComponent.md), or [`AnyWidgetComponent`](AnyWidgetComponent.md) instead. - -::: - -## API - -### Attributes - -None. The `Viewer` class does not have any special attributes. It is a simple `param.Parameterized` class with a few additional methods. This also means you will have to add or support parameters like `height`, `width`, `sizing_mode`, etc., yourself if needed. - -### Methods - -- **`__panel__`**: Must be implemented. Should return the Panel component or object to be displayed. -- **`servable`**: This method serves the component using Panel's built-in server when running `panel serve ...`. -- **`show`**: Displays the component in a new browser tab when running `python ...`. - -## Usage - -### Styling with CSS - -You can style the component by styling the component(s) returned by `__panel__` using their `styles` or `stylesheets` attributes. - -```{pyodide} -import panel as pn -import param - -from panel.viewable import Viewer - -pn.extension() - - -class StyledCounterButton(Viewer): - - value = param.Integer() - - _stylesheets = [ - """ - :host(.solid) .bk-btn.bk-btn-default - { - background: #0072B5; - color: white; - border: none; - padding: 10px; - border-radius: 4px; - } - :host(.solid) .bk-btn.bk-btn-default:hover { - background: #4099da; - } - """ - ] - - def __init__(self, **params): - super().__init__() - - self._layout = pn.widgets.Button( - name=self._button_name, - on_click=self._on_click, - stylesheets=self._stylesheets, - **params, - ) - - def _on_click(self, event): - self.value += 1 - - @param.depends("value") - def _button_name(self): - return f"Clicked {self.value} times" - - def __panel__(self): - return self._layout - - -StyledCounterButton().servable() -``` - -See the [Apply CSS](../../how_to/styling/apply_css.md) guide for more information on styling Panel components. - -## Displaying A Single Child - -You can display Panel components (`Viewable`s) by defining a `Child` parameter. - -Let's start with the simplest example: - -```{pyodide} -import panel as pn - -from panel.custom import Child -from panel.viewable import Viewer - -class SingleChild(Viewer): - - object = Child() - - def __panel__(self): - return pn.Column("A Single Child", self._object) - - @pn.depends("object") - def _object(self): - return self.object - -single_child = SingleChild(object=pn.pane.Markdown("A **Markdown** pane!")) -single_child.servable() -``` - -The `_object` is a workaround to enable the `_layout` to replace the `object` component dynamically. - -Let's replace the `object` with a `Button`: - -```{pyodide} -single_child.object = pn.widgets.Button(name="Click me") -``` - -Let's change it back - -```{pyodide} -single_child.object = pn.pane.Markdown("A **Markdown** pane!") -``` - -If you provide a non-`Viewable` child it will automatically be converted to a `Viewable` by `pn.panel`: - -```{pyodide} -SingleChild(object="A **Markdown** pane!").servable() -``` - -If you want to allow a certain type of Panel components only, you can specify the specific type in the `class_` argument. - -```{pyodide} -import panel as pn - -from panel.custom import Child -from panel.viewable import Viewer - -class SingleChild(Viewer): - - object = Child(class_=pn.pane.Markdown) - - def __panel__(self): - return pn.Column("A Single Child", self._object) - - @pn.depends("object") - def _object(self): - return self.object - -SingleChild(object=pn.pane.Markdown("A **Markdown** pane!")).servable() -``` - -The `class_` argument also supports a tuple of types: - -```{pyodide} -import panel as pn - -from panel.custom import Child -from panel.viewable import Viewer - -class SingleChild(Viewer): - - object = Child(class_=(pn.pane.Markdown, pn.widgets.Button)) - - def __panel__(self): - return pn.Column("A Single Child", self._object) - - @pn.depends("object") - def _object(self): - return self.object - -SingleChild(object=pn.pane.Markdown("A **Markdown** pane!")).servable() -``` - -## Displaying a List of Children - -You can also display a `List` of `Viewable` objects using the `Children` parameter type: - -```{pyodide} -import panel as pn - -from panel.custom import Children -from panel.viewable import Viewer - - -class MultipleChildren(Viewer): - - objects = Children() - - def __init__(self, **params): - self._layout = pn.Column(styles={"background": "silver"}) - - super().__init__(**params) - - def __panel__(self): - return self._layout - - @pn.depends("objects", watch=True, on_init=True) - def _objects(self): - self._layout[:] = self.objects - - -MultipleChildren( - objects=[ - pn.panel("A **Markdown** pane!"), - pn.widgets.Button(name="Click me!"), - {"text": "I'm shown as a JSON Pane"}, - ] -).servable() -``` - -:::note - -You can change the `item_type` to a specific subtype of `Viewable` or a tuple of `Viewable` subtypes. - -::: - -## References - -### Tutorials - -- [Reusable Components](../../../tutorials/intermediate/reusable_components.md) - -### How-To Guides - -- [Combine Existing Widgets](../../../how_to/custom_components/custom_viewer.md) diff --git a/panel/models/reactive_esm.ts b/panel/models/reactive_esm.ts index ca63ab3ca1..432ad108bd 100644 --- a/panel/models/reactive_esm.ts +++ b/panel/models/reactive_esm.ts @@ -517,8 +517,12 @@ export class ReactiveESM extends HTMLBox { protected _declare_importmap(): void { if (this.importmap) { const importMap = {...this.importmap} - // @ts-ignore - importShim.addImportMap(importMap) + try { + // @ts-ignore + importShim.addImportMap(importMap) + } catch (e) { + console.warn(`Failed to add import map: ${e}`) + } } } @@ -570,6 +574,7 @@ export class ReactiveESM extends HTMLBox { // @ts-ignore this.compiled_module = importShim(url) const mod = await this.compiled_module + console.log(mod) let initialize if (mod.initialize) { initialize = mod.initialize From 3203554cd533d86f360d3c7a39cdc93b65e90e7d Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Mon, 19 Aug 2024 08:33:53 -0700 Subject: [PATCH 79/91] revamp layout (#7157) --- examples/reference/chat/ChatMessage.ipynb | 10 +++- panel/chat/icon.py | 7 ++- panel/chat/message.py | 71 ++++++++++++++--------- panel/dist/css/chat_copy_icon.css | 4 +- panel/dist/css/chat_message.css | 70 +++++++++++++--------- panel/dist/css/chat_step.css | 3 +- panel/dist/css/chat_steps.css | 2 +- panel/tests/chat/test_icon.py | 4 +- panel/tests/chat/test_message.py | 39 +++++++------ 9 files changed, 129 insertions(+), 81 deletions(-) diff --git a/examples/reference/chat/ChatMessage.ipynb b/examples/reference/chat/ChatMessage.ipynb index 211fcdbf68..2bfeb78814 100644 --- a/examples/reference/chat/ChatMessage.ipynb +++ b/examples/reference/chat/ChatMessage.ipynb @@ -399,12 +399,18 @@ " background-color: red;\n", " border-radius: 5%;\n", " }\n", + " .meta {\n", + " background-color: lightgreen;\n", + " }\n", " .header {\n", " background-color: green;\n", " }\n", " .footer {\n", " background-color: blue;\n", " }\n", + " .icons {\n", + " background-color: lightblue;\n", + " }\n", " .name {\n", " background-color: orange;\n", " }\n", @@ -431,10 +437,12 @@ " }\n", "\"\"\"\n", "\n", - "ChatMessage(\n", + "pn.chat.ChatMessage(\n", " \"Style me up!\",\n", " show_activity_dot=True,\n", " stylesheets=[path_to_stylesheet],\n", + " footer_objects=[pn.widgets.Button(name=\"Reply\", button_type=\"primary\")],\n", + " header_objects=[pn.widgets.TextInput(placeholder=\"Name\")],\n", ")" ] }, diff --git a/panel/chat/icon.py b/panel/chat/icon.py index 0d0db1dadd..cb91895dfb 100644 --- a/panel/chat/icon.py +++ b/panel/chat/icon.py @@ -7,7 +7,7 @@ import param from ..io.resources import CDN_DIST -from ..layout import Column +from ..layout import Column, Panel from ..reactive import ReactiveHTML from ..widgets.base import CompositeWidget from ..widgets.icon import ToggleIcon @@ -47,6 +47,9 @@ class ChatReactionIcons(CompositeWidget): value = param.List(default=[], doc="The active reactions.") + default_layout = param.ClassSelector( + default=Column, class_=Panel, is_instance=False) + _stylesheets: ClassVar[list[str]] = [f"{CDN_DIST}css/chat_reaction_icons.css"] _composite_type = Column @@ -69,7 +72,7 @@ def _render_icons(self): icon._reaction = option icon.param.watch(self._update_value, "value") self._rendered_icons[option] = icon - self._composite[:] = list(self._rendered_icons.values()) + self._composite[:] = [self.default_layout(*list(self._rendered_icons.values()))] @param.depends("value", watch=True) def _update_icons(self): diff --git a/panel/chat/message.py b/panel/chat/message.py index 11a8a8c9f9..b8d22fe2c4 100644 --- a/panel/chat/message.py +++ b/panel/chat/message.py @@ -252,7 +252,7 @@ def __init__(self, object=None, **params): reaction_icons = params.get("reaction_icons", {"favorite": "heart"}) if isinstance(reaction_icons, dict): - params["reaction_icons"] = ChatReactionIcons(options=reaction_icons) + params["reaction_icons"] = ChatReactionIcons(options=reaction_icons, default_layout=Row) self._internal = True super().__init__(object=object, **params) self.chat_copy_icon = ChatCopyIcon( @@ -264,20 +264,16 @@ def __init__(self, object=None, **params): self._build_layout() def _build_layout(self): - self._activity_dot = HTML( - "●", - css_classes=["activity-dot"], - visible=self.param.show_activity_dot, - stylesheets=self._stylesheets + self.param.stylesheets.rx(), - ) + self._icon_divider = HTML(" | ", width=1, css_classes=["divider"]) + self._left_col = left_col = Column( self._render_avatar(), max_width=60, height=100, css_classes=["left"], - stylesheets=self._stylesheets + self.param.stylesheets.rx(), visible=self.param.show_avatar, sizing_mode=None, + stylesheets=self._stylesheets + self.param.stylesheets.rx(), ) self.param.watch(self._update_avatar_pane, "avatar") @@ -285,7 +281,6 @@ def _build_layout(self): self._update_chat_copy_icon() self._center_row = Row( self._object_panel, - self._render_reaction_icons(), css_classes=["center"], stylesheets=self._stylesheets + self.param.stylesheets.rx(), sizing_mode=None @@ -294,42 +289,64 @@ def _build_layout(self): self.param.watch(self._update_reaction_icons, "reaction_icons") self._user_html = HTML( - self.param.user, height=20, css_classes=["name"], - visible=self.param.show_user, stylesheets=self._stylesheets, + self.param.user, height=20, + css_classes=["name"], + visible=self.param.show_user, + ) + + self._activity_dot = HTML( + "●", + css_classes=["activity-dot"], + visible=self.param.show_activity_dot, ) - header_objects = ( - [self._user_html] + - self.param.header_objects.rx() + - [self.chat_copy_icon, self._activity_dot] + meta_row = Row( + self._user_html, + self._activity_dot, + sizing_mode="stretch_width", + css_classes=["meta"], + stylesheets=self._stylesheets + self.param.stylesheets.rx(), ) - header_row = Row( - objects=header_objects, + + header_col = Column( + objects=self.param.header_objects.rx(), + sizing_mode="stretch_width", + css_classes=["header"], stylesheets=self._stylesheets + self.param.stylesheets.rx(), + ) + + footer_col = Column( + objects=self.param.footer_objects.rx(), sizing_mode="stretch_width", - css_classes=["header"] + css_classes=["footer"], + stylesheets=self._stylesheets + self.param.stylesheets.rx(), ) self._timestamp_html = HTML( self.param.timestamp.rx().strftime(self.param.timestamp_format), css_classes=["timestamp"], - visible=self.param.show_timestamp + visible=self.param.show_timestamp, ) - footer_col = Column( - objects=self.param.footer_objects.rx() + [self._timestamp_html], - stylesheets=self._stylesheets + self.param.stylesheets.rx(), + self._icons_row = Row( + self.chat_copy_icon, + self._icon_divider, + self._render_reaction_icons(), + css_classes=["icons"], sizing_mode="stretch_width", - css_classes=["footer"], + stylesheets=self._stylesheets + self.param.stylesheets.rx(), ) self._right_col = right_col = Column( - header_row, + meta_row, + header_col, self._center_row, footer_col, + self._timestamp_html, + self._icons_row, css_classes=["right"], + sizing_mode=None, stylesheets=self._stylesheets + self.param.stylesheets.rx(), - sizing_mode=None ) viewable_params = { p: self.param[p] for p in self.param if p in Viewable.param @@ -536,7 +553,7 @@ def _render_reaction_icons(self): return reaction_icons def _update_reaction_icons(self, _): - self._center_row[1] = self._render_reaction_icons() + self._icons_row[-1] = self._render_reaction_icons() def _update(self, ref, old_models): """ @@ -579,9 +596,11 @@ def _update_chat_copy_icon(self): if isinstance(object_panel, str) and self.show_copy_icon: self.chat_copy_icon.value = object_panel self.chat_copy_icon.visible = True + self._icon_divider.visible = True else: self.chat_copy_icon.value = "" self.chat_copy_icon.visible = False + self._icon_divider.visible = False def _cleanup(self, root=None) -> None: """ diff --git a/panel/dist/css/chat_copy_icon.css b/panel/dist/css/chat_copy_icon.css index 50b94ed15d..a03a6a1605 100644 --- a/panel/dist/css/chat_copy_icon.css +++ b/panel/dist/css/chat_copy_icon.css @@ -1,5 +1,3 @@ :host { - width: fit-content; - margin-block: 5px; - margin-inline: -6px; + margin-top: 5px; } diff --git a/panel/dist/css/chat_message.css b/panel/dist/css/chat_message.css index 3add5d9dba..4d75872094 100644 --- a/panel/dist/css/chat_message.css +++ b/panel/dist/css/chat_message.css @@ -7,13 +7,9 @@ } } -:host { +:host(.chat-message) { max-height: none; -} - -.chat-message { - display: flex; - flex-direction: row; + margin-top: 15px; } .avatar { @@ -21,8 +17,10 @@ flex-direction: column; justify-content: center; align-items: center; + align-self: center; - margin-top: calc(1em + 5px); + margin-top: 1em; + margin-bottom: 5px; min-width: 50px; width: 50px; min-height: 50px; @@ -40,11 +38,19 @@ 1px; } +.meta { + margin-bottom: -8px; +} + +.left { + display: flex; + flex-direction: column; +} + .right { display: flex; flex-direction: column; - margin-left: 5px; - max-width: calc(100% - 80px); + max-width: 100%; } .header { @@ -53,14 +59,11 @@ .name { font-size: 1em; - margin-bottom: 0px; - margin-top: 5px; } .center { width: calc(100% - 15px); /* Without this, words start on a new line */ - min-height: 4em; - margin-right: 10px; /* Space for reaction icons */ + min-height: 60px; padding: 0px; } @@ -73,9 +76,8 @@ 1px; font-size: 1.25em; min-height: 50px; - margin-top: 0px; + margin-block: 2px; margin-left: 10px; /* Space for avatar */ - margin-right: 5px; /* Space for reaction */ background-color: var(--panel-surface-color, #f1f1f1); min-width: 0; max-width: calc(100% - 40px); @@ -96,9 +98,13 @@ } .timestamp { - color: #a9a9a9; - display: flex; - margin-top: 0px; + opacity: 0.5; + transition: opacity 0.1s ease-in-out; + margin-block: 0px; +} + +.timestamp:hover { + opacity: 1; } .markdown { @@ -110,13 +116,13 @@ animation: icon-rotation 1.28s infinite cubic-bezier(0.68, -0.55, 0.27, 1.55); } -.reaction-icons { - display: flex; - flex-direction: column; - align-items: start; - width: fit-content; - margin-block: 0px; - margin-inline: 2px; +.icons { + opacity: 0.5; + transition: opacity 0.1s ease-in-out; +} + +.icons:hover { + opacity: 1; } @keyframes fadeOut { @@ -135,6 +141,18 @@ display: inline-block; animation: fadeOut 2s infinite cubic-bezier(0.68, -0.55, 0.27, 1.55); color: #32cd32; + /* since 1.25em, fix margins to everything is perceived to be more aligned */ font-size: 1.25em; - margin-block: 0px; + margin-left: -2.5px; + margin-top: 2px; + margin-bottom: 5px; +} + +.divider { + margin-right: 0px; + opacity: 0.2; +} + +.copy-icon { + margin-left: 10px; } diff --git a/panel/dist/css/chat_step.css b/panel/dist/css/chat_step.css index 334ec1d6a7..717b1b3d50 100644 --- a/panel/dist/css/chat_step.css +++ b/panel/dist/css/chat_step.css @@ -32,8 +32,7 @@ .step-avatar-container { width: 15px; height: 15px; - margin-inline: 5px; - margin-block: 5px; + margin: 3px; } .step-avatar { diff --git a/panel/dist/css/chat_steps.css b/panel/dist/css/chat_steps.css index 620eecbc74..578c358ba4 100644 --- a/panel/dist/css/chat_steps.css +++ b/panel/dist/css/chat_steps.css @@ -1,7 +1,7 @@ .card-header { border-bottom: 1px solid var(--panel-border-color, #e0e0e0); padding-block: 15px; - margin-bottom: 5px; + margin-block: 5px; box-shadow: color-mix(in srgb, var(--panel-shadow-color) 30%, transparent) 0px 1px 2px 0px, diff --git a/panel/tests/chat/test_icon.py b/panel/tests/chat/test_icon.py index 8c1f5d5217..0218b2a84c 100644 --- a/panel/tests/chat/test_icon.py +++ b/panel/tests/chat/test_icon.py @@ -19,7 +19,8 @@ def test_options(self): assert "dislike" in icons._rendered_icons assert icons._rendered_icons["dislike"].icon == "thumb-down" assert icons._rendered_icons["dislike"].active_icon == "" - assert len(icons._composite) == 2 + assert len(icons._composite) == 1 + assert len(icons._composite[0]) == 2 icons.options = {"favorite": "heart"} assert icons.options == {"favorite": "heart"} @@ -27,6 +28,7 @@ def test_options(self): assert icons._rendered_icons["favorite"].icon == "heart" assert icons._rendered_icons["favorite"].active_icon == "" assert len(icons._composite) == 1 + assert len(icons._composite[0]) == 1 def test_value(self): icons = ChatReactionIcons( diff --git a/panel/tests/chat/test_message.py b/panel/tests/chat/test_message.py index d0d1e9b44e..e3b694e56c 100644 --- a/panel/tests/chat/test_message.py +++ b/panel/tests/chat/test_message.py @@ -36,27 +36,28 @@ def test_layout(self): assert isinstance(avatar_pane, HTML) assert avatar_pane.object == "🧑" - header_row = columns[1][0] - user_pane = header_row[0] + meta_row = columns[1][0] + user_pane = meta_row[0] assert isinstance(user_pane, HTML) assert user_pane.object == "User" + header_row = columns[1][1] + assert isinstance(header_row[0], Markdown) + assert header_row[0].object == "Header Test" assert isinstance(header_row[1], Markdown) - assert header_row[1].object == "Header Test" - assert isinstance(header_row[2], Markdown) - assert header_row[2].object == "Header 2" + assert header_row[1].object == "Header 2" - center_row = columns[1][1] + center_row = columns[1][2] assert isinstance(center_row, Row) object_pane = center_row[0] assert isinstance(object_pane, Markdown) assert object_pane.object == "ABC" - icons = center_row[1] + icons = columns[1][5][2] assert isinstance(icons, ChatReactionIcons) - footer_col = columns[1][2] + footer_col = columns[1][3] assert isinstance(footer_col, Column) assert isinstance(footer_col[0], Markdown) @@ -64,7 +65,7 @@ def test_layout(self): assert isinstance(footer_col[1], Markdown) assert footer_col[1].object == "Footer 2" - timestamp_pane = footer_col[2] + timestamp_pane = columns[1][4][0] assert isinstance(timestamp_pane, HTML) def test_reactions_dynamic(self): @@ -79,7 +80,7 @@ def test_reaction_icons_dynamic(self): assert message.reaction_icons.options == {"favorite": "heart"} message.reaction_icons = ChatReactionIcons(options={"like": "thumb-up"}) - assert message._center_row[1] == message.reaction_icons + assert message._icons_row[-1] == message.reaction_icons def test_reactions_link(self): # on init @@ -150,19 +151,19 @@ def test_update_user(self): def test_update_object(self): message = ChatMessage(object="Test") columns = message._composite.objects - object_pane = columns[1][1][0] + object_pane = columns[1][2][0] assert isinstance(object_pane, Markdown) assert object_pane.object == "Test" message.object = TextInput(value="Also testing...") - object_pane = columns[1][1][0] + object_pane = columns[1][2][0] assert isinstance(object_pane, TextInput) assert object_pane.value == "Also testing..." message.object = _FileInputMessage( contents=b"I am a file", file_name="test.txt", mime_type="text/plain" ) - object_pane = columns[1][1][0] + object_pane = columns[1][2][0] assert isinstance(object_pane, Markdown) assert object_pane.object == "I am a file" @@ -170,39 +171,39 @@ def test_update_object(self): def test_update_timestamp(self): message = ChatMessage() columns = message._composite.objects - timestamp_pane = columns[1][2][0] + timestamp_pane = columns[1][4][0] assert isinstance(timestamp_pane, HTML) dt_str = datetime.datetime.now().strftime("%H:%M") assert timestamp_pane.object == dt_str message = ChatMessage(timestamp_tz="UTC") columns = message._composite.objects - timestamp_pane = columns[1][2][0] + timestamp_pane = columns[1][4][0] assert isinstance(timestamp_pane, HTML) dt_str = datetime.datetime.now(datetime.timezone.utc).strftime("%H:%M") assert timestamp_pane.object == dt_str message = ChatMessage(timestamp_tz="US/Pacific") columns = message._composite.objects - timestamp_pane = columns[1][2][0] + timestamp_pane = columns[1][4][0] assert isinstance(timestamp_pane, HTML) dt_str = datetime.datetime.now(tz=ZoneInfo("US/Pacific")).strftime("%H:%M") assert timestamp_pane.object == dt_str special_dt = datetime.datetime(2023, 6, 24, 15) message.timestamp = special_dt - timestamp_pane = columns[1][2][0] + timestamp_pane = columns[1][4][0] dt_str = special_dt.strftime("%H:%M") assert timestamp_pane.object == dt_str mm_dd_yyyy = "%b %d, %Y" message.timestamp_format = mm_dd_yyyy - timestamp_pane = columns[1][2][0] + timestamp_pane = columns[1][4][0] dt_str = special_dt.strftime(mm_dd_yyyy) assert timestamp_pane.object == dt_str message.show_timestamp = False - timestamp_pane = columns[1][2][0] + timestamp_pane = columns[1][4][0] assert not timestamp_pane.visible def test_does_not_turn_widget_into_str(self): From 23d8180f32662b929e87da0f09de18d8a445940d Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 19 Aug 2024 18:47:54 +0200 Subject: [PATCH 80/91] Add linting rule to error on console.log (#7162) --- doc/how_to/custom_components/index.md | 1 + panel/.eslintrc.js | 1 + panel/models/comm_manager.ts | 8 +++++--- panel/models/deckgl.ts | 1 - panel/models/player.ts | 1 - panel/models/reactive_esm.ts | 1 - panel/models/reactive_html.ts | 4 ++-- panel/models/speech_to_text.ts | 6 ++---- 8 files changed, 11 insertions(+), 12 deletions(-) diff --git a/doc/how_to/custom_components/index.md b/doc/how_to/custom_components/index.md index d8739d060d..9c284a099d 100644 --- a/doc/how_to/custom_components/index.md +++ b/doc/how_to/custom_components/index.md @@ -132,6 +132,7 @@ Build custom components wrapping Material UI using `ReactComponent`. :hidden: :maxdepth: 2 +esm/build esm/callbacks esm/custom_widgets esm/custom_layout diff --git a/panel/.eslintrc.js b/panel/.eslintrc.js index b31a84de68..22c52b0732 100644 --- a/panel/.eslintrc.js +++ b/panel/.eslintrc.js @@ -48,6 +48,7 @@ module.exports = { "@typescript-eslint/no-unnecessary-type-assertion": ["error"], "@typescript-eslint/no-unnecessary-type-constraint": ["error"], "@typescript-eslint/switch-exhaustiveness-check": ["error"], + "no-console": ["error", { "allow": ["warn", "error"] }], "no-self-assign": ["error", {"props": false}], "brace-style": ["error", "1tbs", {"allowSingleLine": true}], "comma-dangle": ["off"], diff --git a/panel/models/comm_manager.ts b/panel/models/comm_manager.ts index fd1f046d4c..939c0421fc 100644 --- a/panel/models/comm_manager.ts +++ b/panel/models/comm_manager.ts @@ -52,7 +52,7 @@ export class CommManager extends Model { this._blocked = false this._timeout = Date.now() if (((window as any).PyViz == undefined) || (!(window as any).PyViz.comm_manager)) { - console.log("Could not find comm manager on window.PyViz, ensure the extension is loaded.") + console.warn("Could not find comm manager on window.PyViz, ensure the extension is loaded.") } else { this.ns = (window as any).PyViz this.ns.comm_manager.register_target(this.plot_id, this.comm_id, (msg: any) => { @@ -163,9 +163,10 @@ export class CommManager extends Model { this._blocked = false } if ((metadata.msg_type == "Ready") && metadata.content) { + // eslint-disable-next-line no-console console.log("Python callback returned following output:", metadata.content) } else if (metadata.msg_type == "Error") { - console.log("Python failed with the following traceback:", metadata.traceback) + console.warn("Python failed with the following traceback:", metadata.traceback) } } @@ -176,9 +177,10 @@ export class CommManager extends Model { const plot_id = this.plot_id if ((metadata.msg_type == "Ready")) { if (metadata.content) { + // eslint-disable-next-line no-console console.log("Python callback returned following output:", metadata.content) } else if (metadata.msg_type == "Error") { - console.log("Python failed with the following traceback:", metadata.traceback) + console.warn("Python failed with the following traceback:", metadata.traceback) } } else if (plot_id != null) { let plot = null diff --git a/panel/models/deckgl.ts b/panel/models/deckgl.ts index 289c3e317d..7486dbbec7 100644 --- a/panel/models/deckgl.ts +++ b/panel/models/deckgl.ts @@ -20,7 +20,6 @@ function extractClasses() { classesDict[cls] = deck[cls] } const carto = (window as any).CartoLibrary - console.log(carto) const layers = Object.keys(carto.CARTO_LAYERS).filter(x => x.endsWith("Layer")) for (const layer of layers) { classesDict[layer] = carto.CARTO_LAYERS[layer] diff --git a/panel/models/player.ts b/panel/models/player.ts index 591da8537e..8a95462471 100644 --- a/panel/models/player.ts +++ b/panel/models/player.ts @@ -378,7 +378,6 @@ export class PlayerView extends WidgetView { break case "end": this.titleEl.style.textAlign = "right" - console.log(this.titleEl) break } } diff --git a/panel/models/reactive_esm.ts b/panel/models/reactive_esm.ts index 432ad108bd..0e9b82335d 100644 --- a/panel/models/reactive_esm.ts +++ b/panel/models/reactive_esm.ts @@ -574,7 +574,6 @@ export class ReactiveESM extends HTMLBox { // @ts-ignore this.compiled_module = importShim(url) const mod = await this.compiled_module - console.log(mod) let initialize if (mod.initialize) { initialize = mod.initialize diff --git a/panel/models/reactive_html.ts b/panel/models/reactive_html.ts index 25d0227023..eda843f2ba 100644 --- a/panel/models/reactive_html.ts +++ b/panel/models/reactive_html.ts @@ -200,7 +200,7 @@ export class ReactiveHTMLView extends HTMLBoxView { const script_fn = this._script_fns.get(property) if (script_fn === undefined) { if (!silent) { - console.log(`Script '${property}' could not be found.`) + console.warn(`Script '${property}' could not be found.`) } return } @@ -532,7 +532,7 @@ export class ReactiveHTMLView extends HTMLBoxView { this._changing = true this.model.data.setv(serialize_attrs(attrs)) } catch { - console.log("Could not serialize", attrs) + console.error("Could not serialize", attrs) } finally { this._changing = false } diff --git a/panel/models/speech_to_text.ts b/panel/models/speech_to_text.ts index b2636523be..67fca6b326 100644 --- a/panel/models/speech_to_text.ts +++ b/panel/models/speech_to_text.ts @@ -86,12 +86,10 @@ export class SpeechToTextView extends HTMLBoxView { this.model.results = serializeResults(event.results) } this.recognition.onerror = (event: any) => { - console.log("SpeechToText Error") - console.log(event) + console.error(`SpeechToText Error: ${event}`) } this.recognition.onnomatch = (event: any) => { - console.log("SpeechToText No Match") - console.log(event) + console.warn(`SpeechToText No Match: ${event}`) } this.recognition.onaudiostart = () => this.model.audio_started = true From 36d680ef96bcd24fe4884ab15108fb8b998a05cb Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 19 Aug 2024 19:35:29 +0200 Subject: [PATCH 81/91] Ensure no content warning is not displayed when template is added (#7164) --- panel/io/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panel/io/handlers.py b/panel/io/handlers.py index e30c144205..8b41319c1b 100644 --- a/panel/io/handlers.py +++ b/panel/io/handlers.py @@ -277,7 +277,7 @@ def post_check(): handler._runner.run(module, post_check) if post_run: post_run() - if not doc.roots and not allow_empty and config.autoreload: + if not doc.roots and not allow_empty and config.autoreload and doc not in state._templates: from ..pane import Alert Alert( ('Application did not publish any contents\n\n' From 32db5e0a20683d40d09147b7a0b5000a75a3013c Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 19 Aug 2024 19:36:00 +0200 Subject: [PATCH 82/91] Cache rendering code on ReactiveESM (#7163) * Cache rendering code on ReactiveESM * Update React component as well --- panel/models/react_component.ts | 50 ++++++++++++++++++--------------- panel/models/reactive_esm.ts | 45 ++++++++++++++++++----------- 2 files changed, 55 insertions(+), 40 deletions(-) diff --git a/panel/models/react_component.ts b/panel/models/react_component.ts index 00a63f0534..ce97971653 100644 --- a/panel/models/react_component.ts +++ b/panel/models/react_component.ts @@ -29,17 +29,17 @@ export class ReactComponentView extends ReactiveESMView { protected override _render_code(): string { let render_code = ` -if (rendered && view.model.usesReact) { - view._changing = true - const root = createRoot(view.container) - try { - root.render(rendered) - } catch(e) { - view.render_error(e) - } - view._changing = false - view.after_rendered() -}` + if (rendered && view.model.usesReact) { + view._changing = true + const root = createRoot(view.container) + try { + root.render(rendered) + } catch(e) { + view.render_error(e) + } + view._changing = false + view.after_rendered() + }` let import_code = ` import * as React from "react" import { createRoot } from "react-dom/client"` @@ -49,15 +49,15 @@ ${import_code} import createCache from "@emotion/cache" import { CacheProvider } from "@emotion/react"` render_code = ` -if (rendered) { - const cache = createCache({ - key: 'css', - prepend: true, - container: view.style_cache, - }) - rendered = React.createElement(CacheProvider, {value: cache}, rendered) -} -${render_code}` + if (rendered) { + const cache = createCache({ + key: 'css', + prepend: true, + container: view.style_cache, + }) + rendered = React.createElement(CacheProvider, {value: cache}, rendered) + } + ${render_code}` } return ` ${import_code} @@ -197,10 +197,14 @@ class Component extends React.Component { } } -const props = {view, model: react_proxy, data: view.model.data, el: view.container} -let rendered = React.createElement(Component, props) +function render() { + const props = {view, model: react_proxy, data: view.model.data, el: view.container} + let rendered = React.createElement(Component, props) + + ${render_code} +} -${render_code}` +export default {render}` } } diff --git a/panel/models/reactive_esm.ts b/panel/models/reactive_esm.ts index 0e9b82335d..e600a6d884 100644 --- a/panel/models/reactive_esm.ts +++ b/panel/models/reactive_esm.ts @@ -146,6 +146,7 @@ export class ReactiveESMView extends HTMLBoxView { accessed_children: string[] = [] compiled_module: any = null model_proxy: any + render_module: Promise | null = null _changing: boolean = false _child_callbacks: Map void)[]> _child_rendered: Map = new Map() @@ -156,6 +157,7 @@ export class ReactiveESMView extends HTMLBoxView { ["remove", []], ]) _rendered: boolean = false + _stale_children: boolean = false override initialize(): void { super.initialize() @@ -267,6 +269,12 @@ export class ReactiveESMView extends HTMLBoxView { if (this.model.compile_error) { this.render_error(this.model.compile_error) } else { + const code = this._render_code() + const render_url = URL.createObjectURL( + new Blob([code], {type: "text/javascript"}), + ) + // @ts-ignore + this.render_module = importShim(render_url) this.render_esm() } } @@ -275,16 +283,20 @@ export class ReactiveESMView extends HTMLBoxView { return ` const view = Bokeh.index.find_one_by_id('${this.model.id}') -const output = view.render_fn({ - view: view, model: view.model_proxy, data: view.model.data, el: view.container -}) +function render() { + const output = view.render_fn({ + view: view, model: view.model_proxy, data: view.model.data, el: view.container + }) -Promise.resolve(output).then((out) => { - if (out instanceof Element) { - view.container.replaceChildren(out) - } - view.after_rendered() -})` + Promise.resolve(output).then((out) => { + if (out instanceof Element) { + view.container.replaceChildren(out) + } + view.after_rendered() + }) +} + +export default {render}` } after_rendered(): void { @@ -293,12 +305,12 @@ Promise.resolve(output).then((out) => { cb() } this.render_children() - this.model_proxy.on(this.accessed_children, () => this.render_esm()) + this.model_proxy.on(this.accessed_children, () => { this._stale_children = true }) this._rendered = true } render_esm(): void { - if (this.model.compiled === null) { + if (this.model.compiled === null || this.render_module === null) { return } this.accessed_properties = [] @@ -306,12 +318,7 @@ Promise.resolve(output).then((out) => { (this._lifecycle_handlers.get(lf) || []).splice(0) } this.model.disconnect_watchers(this) - const code = this._render_code() - const render_url = URL.createObjectURL( - new Blob([code], {type: "text/javascript"}), - ) - // @ts-ignore - importShim(render_url) + this.render_module.then((mod: any) => mod.default.render()) } render_children() { @@ -402,6 +409,10 @@ Promise.resolve(output).then((out) => { callback(new_children) } } + if (this._stale_children) { + this.render_esm() + this._stale_children = false + } this._update_children() this.invalidate_layout() } From cee024e4cec9c27cbdc9ebd7412f761a02e972fa Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Tue, 20 Aug 2024 02:29:29 -0700 Subject: [PATCH 83/91] Fix Tabulator page size 0 (#7170) * fix page size 0 * Do no allow zero size table --------- Co-authored-by: Philipp Rudiger --- panel/models/tabulator.ts | 2 +- panel/widgets/tables.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index 67f05e781f..aaa613fe5d 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -658,7 +658,7 @@ export class DataTabulatorView extends HTMLBoxView { const remaining = table_height - height page_size += Math.floor(remaining / Math.min(...heights)) } - this.model.page_size = page_size + this.model.page_size = Math.max(page_size || 1, 1) } } this.setMaxPage() diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index 4b9a4794dc..77fa7e5245 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -1308,6 +1308,8 @@ def _cleanup(self, root: Model | None = None) -> None: def _process_events(self, events: dict[str, Any]) -> None: if 'expanded' in events: self._update_expanded(events.pop('expanded')) + if events.get('page_size') == 0: # page_size can't be 0 + events.pop('page_size') return super()._process_events(events) def _process_event(self, event) -> None: From 97b11fe55ccb075db4d8e334edd4375ecbb1fcf6 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 20 Aug 2024 11:31:43 +0200 Subject: [PATCH 84/91] Bump panel.js version to 1.5.0-b.5 --- panel/package-lock.json | 4 ++-- panel/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/panel/package-lock.json b/panel/package-lock.json index 9e774a6679..3b49cf4b41 100644 --- a/panel/package-lock.json +++ b/panel/package-lock.json @@ -1,12 +1,12 @@ { "name": "@holoviz/panel", - "version": "1.5.0-b.4", + "version": "1.5.0-b.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@holoviz/panel", - "version": "1.5.0-b.4", + "version": "1.5.0-b.5", "license": "BSD-3-Clause", "dependencies": { "@bokeh/bokehjs": "3.5.1", diff --git a/panel/package.json b/panel/package.json index c31a24c88d..a3e45944e4 100644 --- a/panel/package.json +++ b/panel/package.json @@ -1,6 +1,6 @@ { "name": "@holoviz/panel", - "version": "1.5.0-b.4", + "version": "1.5.0-b.5", "description": "The powerful data exploration & web app framework for Python.", "license": "BSD-3-Clause", "repository": { From 43f19c07f8e522cfd21b78741c994a9cf12518e8 Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Tue, 20 Aug 2024 09:27:26 -0700 Subject: [PATCH 85/91] Add on_keyup and value_input for code editor (#6919) --- examples/reference/widgets/CodeEditor.ipynb | 27 ++++++- panel/models/ace.py | 4 + panel/models/ace.ts | 25 +++++- panel/tests/ui/widgets/test_codeeditor.py | 84 +++++++++++++++++++++ panel/tests/widgets/test_codeeditor.py | 11 +++ panel/widgets/codeeditor.py | 16 +++- 6 files changed, 161 insertions(+), 6 deletions(-) create mode 100644 panel/tests/ui/widgets/test_codeeditor.py diff --git a/examples/reference/widgets/CodeEditor.ipynb b/examples/reference/widgets/CodeEditor.ipynb index 4232d91ed3..abab154058 100644 --- a/examples/reference/widgets/CodeEditor.ipynb +++ b/examples/reference/widgets/CodeEditor.ipynb @@ -37,11 +37,12 @@ " - `'type'`: type of annotation and the icon displayed {`warning` | `error`}\n", "* **``filename``** (str): If filename is provided the file extension will be used to determine the language\n", "* **``language``** (str): A string declaring which language to use for code syntax highlighting (default: 'text')\n", + "* **``on_keyup``** (bool): Whether to update the value on every key press or only upon loss of focus / hotkeys.\n", "* **``print_margin``** (boolean): Whether to show a print margin in the editor\n", "* **``theme``** (str): theme of the editor (default: 'chrome')\n", "* **``readonly``** (boolean): Whether the editor should be opened in read-only mode\n", - "* **``value``** (str): A string with (initial) code to set in the editor\n", - "\n", + "* **``value``** (str): State of the current code in the editor if `on_keyup`. Otherwise, only upon loss of focus, i.e. clicking outside the editor, or pressing or .\n", + "* **``value_input``** (str): State of the current code updated on every key press. Identical to `value` if `on_keyup`.\n", "___" ] }, @@ -50,7 +51,7 @@ "metadata": {}, "source": [ "To construct an `Ace` widget we must define it explicitly using `pn.widgets.Ace`. We can add some text as initial code.\n", - "Code inserted in the editor is automatically reflected in the `value`." + "Code inserted in the editor is automatically reflected in the `value_input` and `value`." ] }, { @@ -84,6 +85,26 @@ "\"\"\"" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "By default, the code editor will update the `value` on every key press, but you can set `on_keyup=False` to only update the `value` when the editor loses focus or pressing ``/ ``. Here, the code is printed when `value` is changed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def print_code(value):\n", + " print(value)\n", + "\n", + "editor = pn.widgets.CodeEditor(value=py_code, on_keyup=False)\n", + "pn.bind(print_code, editor.param.value)" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/panel/models/ace.py b/panel/models/ace.py index 391abac0f2..b898a6b363 100644 --- a/panel/models/ace.py +++ b/panel/models/ace.py @@ -50,6 +50,10 @@ def __js_skip__(cls): code = String(default='') + code_input = String(default='') + + on_keyup = Bool(default=True) + theme = Enum(ace_themes, default='chrome') filename = Nullable(String()) diff --git a/panel/models/ace.ts b/panel/models/ace.ts index e65cd9c229..584e3fc544 100644 --- a/panel/models/ace.ts +++ b/panel/models/ace.ts @@ -68,7 +68,20 @@ export class AcePlotView extends HTMLBoxView { this._update_language() this._editor.setReadOnly(this.model.readonly) this._editor.setShowPrintMargin(this.model.print_margin) - this._editor.on("change", () => this._update_code_from_editor()) + // if on keyup, update code from editor + if (this.model.on_keyup) { + this._editor.on("change", () => this._update_code_from_editor()) + } else { + this._editor.on("blur", () => this._update_code_from_editor()) + this._editor.commands.addCommand({ + name: "updateCodeFromEditor", + bindKey: {win: "Ctrl-Enter", mac: "Command-Enter"}, + exec: () => { + this._update_code_from_editor() + }, + }) + } + this._editor.on("change", () => this._update_code_input_from_editor()) } _update_code_from_model(): void { @@ -87,6 +100,12 @@ export class AcePlotView extends HTMLBoxView { } } + _update_code_input_from_editor(): void { + if (this._editor.getValue() != this.model.code_input) { + this.model.code_input = this._editor.getValue() + } + } + _update_theme(): void { this._editor.setTheme(`ace/theme/${this.model.theme}`) } @@ -120,6 +139,8 @@ export namespace AcePlot { export type Attrs = p.AttrsOf export type Props = HTMLBox.Props & { code: p.Property + code_input: p.Property + on_keyup: p.Property language: p.Property filename: p.Property theme: p.Property @@ -145,6 +166,8 @@ export class AcePlot extends HTMLBox { this.define(({Any, List, Bool, Str, Nullable}) => ({ code: [ Str, "" ], + code_input: [ Str, "" ], + on_keyup: [ Bool, true ], filename: [ Nullable(Str), null], language: [ Str, "" ], theme: [ Str, "chrome" ], diff --git a/panel/tests/ui/widgets/test_codeeditor.py b/panel/tests/ui/widgets/test_codeeditor.py new file mode 100644 index 0000000000..5b552d56df --- /dev/null +++ b/panel/tests/ui/widgets/test_codeeditor.py @@ -0,0 +1,84 @@ +import sys + +import pytest + +pytest.importorskip("playwright") + +from playwright.sync_api import expect + +from panel.tests.util import serve_component, wait_until +from panel.widgets import CodeEditor + +pytestmark = pytest.mark.ui + + +def test_code_editor_on_keyup(page): + + editor = CodeEditor(value="print('Hello World!')", on_keyup=True) + serve_component(page, editor) + ace_input = page.locator(".ace_content") + expect(ace_input).to_have_count(1) + ace_input.click() + + page.keyboard.press("Enter") + page.keyboard.type('print("Hello Panel!")') + + expect(page.locator(".ace_content")).to_have_text("print('Hello World!')\nprint(\"Hello Panel!\")", use_inner_text=True) + wait_until(lambda: editor.value_input == "print('Hello World!')\nprint(\"Hello Panel!\")") + assert editor.value == "print('Hello World!')\nprint(\"Hello Panel!\")" + + # clear the editor + editor.value = "" + expect(page.locator(".ace_content")).to_have_text("", use_inner_text=True) + assert editor.value == "" + assert editor.value_input == "" + + # enter Hello UI + ace_input.click() + page.keyboard.type('print("Hello UI!")') + expect(page.locator(".ace_content")).to_have_text("print(\"Hello UI!\")", use_inner_text=True) + assert editor.value == "print(\"Hello UI!\")" + + +def test_code_editor_not_on_keyup(page): + + editor = CodeEditor(value="print('Hello World!')", on_keyup=False) + serve_component(page, editor) + ace_input = page.locator(".ace_content") + expect(ace_input).to_have_count(1) + ace_input.click() + + page.keyboard.press("Enter") + page.keyboard.type('print("Hello Panel!")') + + expect(page.locator(".ace_content")).to_have_text("print('Hello World!')\nprint(\"Hello Panel!\")", use_inner_text=True) + wait_until(lambda: editor.value_input == "print('Hello World!')\nprint(\"Hello Panel!\")") + assert editor.value == "print('Hello World!')" + + # page click outside the editor; sync the value + page.locator("body").click() + assert editor.value_input == "print('Hello World!')\nprint(\"Hello Panel!\")" + wait_until(lambda: editor.value == "print('Hello World!')\nprint(\"Hello Panel!\")") + + # clear the editor + editor.value = "" + expect(page.locator(".ace_content")).to_have_text("", use_inner_text=True) + assert editor.value == "" + assert editor.value_input == "" + + # enter Hello UI + ace_input.click() + page.keyboard.type('print("Hello UI!")') + expect(page.locator(".ace_content")).to_have_text("print(\"Hello UI!\")", use_inner_text=True) + assert editor.value == "" + + # If windows: Ctrl+Enter to trigger value else if mac, Command+Enter + if sys.platform == "win32": + page.keyboard.down("Control") + page.keyboard.press("Enter") + page.keyboard.up("Control") + else: + page.keyboard.down("Meta") + page.keyboard.press("Enter") + page.keyboard.up("Meta") + wait_until(lambda: editor.value == "print(\"Hello UI!\")") diff --git a/panel/tests/widgets/test_codeeditor.py b/panel/tests/widgets/test_codeeditor.py index d85c8650de..fba2924c57 100644 --- a/panel/tests/widgets/test_codeeditor.py +++ b/panel/tests/widgets/test_codeeditor.py @@ -12,3 +12,14 @@ def test_ace(document, comm): # Try changes editor._process_events({"value": "Hi there!"}) assert editor.value == "Hi there!" + + +def test_ace_input(document, comm): + editor = CodeEditor(value="", language="python") + editor.value = "Hello World!" + assert editor.value == "Hello World!" + assert editor.value_input == "Hello World!" + + editor.value = "" + assert editor.value == "" + assert editor.value_input == "" diff --git a/panel/widgets/codeeditor.py b/panel/widgets/codeeditor.py index 0086bc298a..8e766afd85 100644 --- a/panel/widgets/codeeditor.py +++ b/panel/widgets/codeeditor.py @@ -40,6 +40,9 @@ class CodeEditor(Widget): language = param.String(default='text', doc="Language of the editor") + on_keyup = param.Boolean(default=True, doc=""" + Whether to update the value on every key press or only upon loss of focus / hotkeys.""") + print_margin = param.Boolean(default=False, doc=""" Whether to show the a print margin.""") @@ -49,9 +52,14 @@ class CodeEditor(Widget): theme = param.ObjectSelector(default="chrome", objects=list(ace_themes), doc="Theme of the editor") - value = param.String(default="", doc="State of the current code in the editor") + value = param.String(default="", doc=""" + State of the current code in the editor if `on_keyup`. Otherwise, only upon loss of focus, + i.e. clicking outside the editor, or pressing or .""") + + value_input = param.String(default="", doc=""" + State of the current code updated on every key press. Identical to `value` if `on_keyup`.""") - _rename: ClassVar[Mapping[str, str | None]] = {"value": "code", "name": None} + _rename: ClassVar[Mapping[str, str | None]] = {"value": "code", "value_input": "code_input", "name": None} def __init__(self, **params): if 'readonly' in params: @@ -64,6 +72,10 @@ def __init__(self, **params): ) self.jslink(self, readonly='disabled', bidirectional=True) + @param.depends("value", watch=True) + def _update_value_input(self): + self.value_input = self.value + def _get_model( self, doc: Document, root: Optional[Model] = None, parent: Optional[Model] = None, comm: Optional[Comm] = None From e2c520831e27cad72a1f3653d68f5a9dad5c90cd Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Tue, 20 Aug 2024 09:30:17 -0700 Subject: [PATCH 86/91] fix header input color (#7040) --- panel/theme/css/fast.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panel/theme/css/fast.css b/panel/theme/css/fast.css index 232d482b2d..d41f103dc0 100644 --- a/panel/theme/css/fast.css +++ b/panel/theme/css/fast.css @@ -307,7 +307,7 @@ table.panel-df { background-color: var(--neutral-fill-input-rest); border: 1px solid var(--accent-fill-rest); border-radius: calc(var(--control-corner-radius) * 1px); - color: var(--neutral-foreground-rest); + color: var(--foreground-on-accent-rest); font-size: var(--type-ramp-base-font-size); height: calc( (var(--base-height-multiplier) + var(--density)) * var(--design-unit) * 1px From be274d81afe0e54f6909b2254798af8d76585191 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 21 Aug 2024 11:47:47 +0200 Subject: [PATCH 87/91] Fix CodeEditor UI tests (#7177) --- panel/tests/ui/widgets/test_codeeditor.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/panel/tests/ui/widgets/test_codeeditor.py b/panel/tests/ui/widgets/test_codeeditor.py index 5b552d56df..1df565fcd4 100644 --- a/panel/tests/ui/widgets/test_codeeditor.py +++ b/panel/tests/ui/widgets/test_codeeditor.py @@ -24,7 +24,7 @@ def test_code_editor_on_keyup(page): page.keyboard.type('print("Hello Panel!")') expect(page.locator(".ace_content")).to_have_text("print('Hello World!')\nprint(\"Hello Panel!\")", use_inner_text=True) - wait_until(lambda: editor.value_input == "print('Hello World!')\nprint(\"Hello Panel!\")") + wait_until(lambda: editor.value_input == "print('Hello World!')\nprint(\"Hello Panel!\")", page) assert editor.value == "print('Hello World!')\nprint(\"Hello Panel!\")" # clear the editor @@ -37,7 +37,8 @@ def test_code_editor_on_keyup(page): ace_input.click() page.keyboard.type('print("Hello UI!")') expect(page.locator(".ace_content")).to_have_text("print(\"Hello UI!\")", use_inner_text=True) - assert editor.value == "print(\"Hello UI!\")" + + wait_until(lambda: editor.value == "print(\"Hello UI!\")", page) def test_code_editor_not_on_keyup(page): @@ -72,13 +73,9 @@ def test_code_editor_not_on_keyup(page): expect(page.locator(".ace_content")).to_have_text("print(\"Hello UI!\")", use_inner_text=True) assert editor.value == "" - # If windows: Ctrl+Enter to trigger value else if mac, Command+Enter - if sys.platform == "win32": - page.keyboard.down("Control") - page.keyboard.press("Enter") - page.keyboard.up("Control") - else: - page.keyboard.down("Meta") - page.keyboard.press("Enter") - page.keyboard.up("Meta") - wait_until(lambda: editor.value == "print(\"Hello UI!\")") + ctrl_key = 'Meta' if sys.platform == 'darwin' else 'Control' + page.keyboard.down(ctrl_key) + page.keyboard.press("Enter") + page.keyboard.up(ctrl_key) + + wait_until(lambda: editor.value == "print(\"Hello UI!\")", page) From 7ee45d48098da0614b6582f151be86e425494f35 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 21 Aug 2024 22:31:35 +0200 Subject: [PATCH 88/91] Reschedule events on pending writes errors (#7175) --- panel/io/document.py | 183 ++++++++++++++++++++++++++----------------- 1 file changed, 111 insertions(+), 72 deletions(-) diff --git a/panel/io/document.py b/panel/io/document.py index 744a805a53..dd0d8b629a 100644 --- a/panel/io/document.py +++ b/panel/io/document.py @@ -15,7 +15,7 @@ from contextlib import contextmanager from functools import partial, wraps from typing import ( - TYPE_CHECKING, Any, Callable, Iterator, Optional, + TYPE_CHECKING, Any, Callable, Iterator, ) from bokeh.application.application import SessionContext @@ -36,6 +36,8 @@ if TYPE_CHECKING: from bokeh.core.has_props import HasProps + from bokeh.protocol.message import Message + from bokeh.server.connection import ServerConnection logger = logging.getLogger(__name__) @@ -261,7 +263,60 @@ def _destroy_document(self, session): # Public API #--------------------------------------------------------------------- -def create_doc_if_none_exists(doc: Optional[Document]) -> Document: +def retrigger_events(doc: Document, events: list[DocumentChangedEvent]): + """ + Applies events that could not be processed previously. + """ + if doc.callbacks.hold_value: + doc.callbacks._held_events = events + list(doc.callbacks._held_events) + else: + _dispatch_events(doc, events) + +def write_events( + doc: Document, + connections: list[ServerConnection], + events: list[DocumentChangedEvent] +): + from tornado.websocket import WebSocketHandler + + futures = [] + for conn in connections: + if isinstance(conn._socket, WebSocketHandler): + futures += dispatch_tornado(conn, events) + elif (socket_type:= type(conn._socket)) in extra_socket_handlers: + futures += extra_socket_handlers[socket_type](conn, events) + else: + futures += dispatch_django(conn, events) + + if doc in _WRITE_FUTURES: + _WRITE_FUTURES[doc] += futures + else: + _WRITE_FUTURES[doc] = futures + + if state._unblocked(doc): + _dispatch_write_task(doc, _run_write_futures, doc) + else: + doc.add_next_tick_callback(partial(_run_write_futures, doc)) + +def schedule_write_events( + doc: Document, + connections: list[ServerConnection], + events: list[DocumentChangedEvent] +): + # Set up write locks + _WRITE_BLOCK[doc] = True + _WRITE_MSGS[doc] = msgs = _WRITE_MSGS.get(doc, {}) + # Create messages for remaining events + for conn in connections: + # Create a protocol message for any events that cannot be immediately dispatched + msg = conn.protocol.create('PATCH-DOC', events) + if conn in msgs: + msgs[conn].append(msg) + else: + msgs[conn] = [msg] + _dispatch_write_task(doc, _dispatch_msgs, doc) + +def create_doc_if_none_exists(doc: Document | None) -> Document: curdoc = doc or curdoc_locked() if curdoc is None: curdoc = Document() @@ -269,7 +324,7 @@ def create_doc_if_none_exists(doc: Optional[Document]) -> Document: curdoc = curdoc._doc return curdoc -def init_doc(doc: Optional[Document]) -> Document: +def init_doc(doc: Document | None) -> Document: curdoc = create_doc_if_none_exists(doc) if not curdoc.session_context: return curdoc @@ -323,7 +378,11 @@ def wrapper(*args, **kw): wrapper.lock = True # type: ignore return wrapper -def dispatch_tornado(conn, events=None, msg=None): +def dispatch_tornado( + conn: ServerConnection, + events: list[DocumentChangedEvent] | None = None, + msg: Message | None = None +): from tornado.websocket import WebSocketHandler socket = conn._socket ws_conn = getattr(socket, 'ws_connection', False) @@ -345,7 +404,11 @@ def dispatch_tornado(conn, events=None, msg=None): ]) return futures -def dispatch_django(conn, events=None, msg=None): +def dispatch_django( + conn: ServerConnection, + events: list[DocumentChangedEvent] | None = None, + msg: Message | None = None +): socket = conn._socket if msg is None: msg = conn.protocol.create('PATCH-DOC', events) @@ -390,14 +453,17 @@ def unlocked() -> Iterator: monkeypatch_events(curdoc.callbacks._held_events) return - from tornado.websocket import WebSocketHandler - connections = session._subscribed_connections - curdoc.hold() - events = None - remaining_events, dispatch_events = [], [] try: yield + finally: + # Whether or not there was an error in the body of context manager + # we may have captured some events. We will dispatch these + # either by running the write futures, by serializing them + # as actual messages and scheduling these messages to be written, + # by having bokeh dispatch them on calling unhold or by + # scheduling them to be triggered later. + connections = session._subscribed_connections locked = curdoc in _WRITE_MSGS or curdoc in _WRITE_BLOCK for conn in connections: socket = conn._socket @@ -405,76 +471,49 @@ def unlocked() -> Iterator: locked = True break - events = curdoc.callbacks._held_events + remaining_events, writeable_events = [], [] + events = list(curdoc.callbacks._held_events or []) curdoc.callbacks._held_events = [] monkeypatch_events(events) for event in events: if isinstance(event, DISPATCH_EVENTS) and not locked: - dispatch_events.append(event) + writeable_events.append(event) else: remaining_events.append(event) - futures = [] - for conn in connections: - if not dispatch_events: - continue - elif isinstance(conn._socket, WebSocketHandler): - futures += dispatch_tornado(conn, dispatch_events) - elif (socket_type:= type(conn._socket)) in extra_socket_handlers: - futures += extra_socket_handlers[socket_type](conn, dispatch_events) - else: - futures += dispatch_django(conn, dispatch_events) - - - if futures: - if curdoc in _WRITE_FUTURES: - _WRITE_FUTURES[curdoc] += futures - else: - _WRITE_FUTURES[curdoc] = futures - - if state._unblocked(curdoc): - _dispatch_write_task(curdoc, _run_write_futures, curdoc) - else: - curdoc.add_next_tick_callback(partial(_run_write_futures, curdoc)) - except Exception as e: - # If we error out during the yield, there won't be any events - # captured so we end up simply calling curdoc.unhold() and - # raising the exception. If instead we error during event - # dispatch we restore the events in the order they were created - # and then let the finally section create a protocol message - # to dispatch the events, ensuring that the events which were - # marked for immediate dispatch are not lost. - if events is not None: + try: + if writeable_events: + write_events(curdoc, connections, writeable_events) + except Exception: remaining_events = events - raise e - finally: - # If for whatever reasons there are still events that couldn't - # be dispatched we create a protocol message for these immediately - # and then schedule a task to write the message to the websocket - # on the next iteration of the event loop. - if remaining_events: - # Separate serializable and non-serializable events - leftover_events = [e for e in remaining_events if not isinstance(e, Serializable)] - remaining_events = [e for e in remaining_events if isinstance(e, Serializable)] - - # Set up write locks - if remaining_events: - _WRITE_BLOCK[curdoc] = True - _WRITE_MSGS[curdoc] = msgs = _WRITE_MSGS.get(curdoc, {}) - # Create messages for remaining events - for conn in connections: - if not remaining_events: - continue - # Create a protocol message for any events that cannot be immediately dispatched - msg = conn.protocol.create('PATCH-DOC', remaining_events) - if conn in msgs: - msgs[conn].append(msg) - else: - msgs[conn] = [msg] - - _dispatch_write_task(curdoc, _dispatch_msgs, curdoc) - curdoc.callbacks._held_events += leftover_events - curdoc.unhold() + finally: + # If for whatever reasons there are still events that couldn't + # be dispatched we create a protocol message for these immediately + # and then schedule a task to write the message to the websocket + # on the next iteration of the event loop. This ensures that + # the message reflects the event at the time it was generated + # potentially avoiding issues serializing subsequent models + # which assume the serializer has previously seen them. + serializable_events = [e for e in remaining_events if isinstance(e, Serializable)] + held_events = [e for e in remaining_events if not isinstance(e, Serializable)] + if serializable_events: + try: + schedule_write_events(curdoc, connections, serializable_events) + except Exception: + # If the serialization fails we let bokeh handle them + held_events = remaining_events + curdoc.callbacks._held_events += held_events + + # Last we attempt to let bokeh handle these remaining events + # if this also fails we reapply the event at a later point in + # time. This should not happen but since network writes + # are fickle we handle this case anyway. + try: + retriggered_events = list(curdoc.callbacks._held_events) + curdoc.unhold() + except RuntimeError: + curdoc.add_next_tick_callback(partial(retrigger_events, curdoc, retriggered_events)) + @contextmanager def immediate_dispatch(doc: Document | None = None): From 45809a8501f11c23a4489c60256fb378863d2840 Mon Sep 17 00:00:00 2001 From: Christoph Deil Date: Wed, 21 Aug 2024 21:32:01 +0100 Subject: [PATCH 89/91] Fix BokehDeprecationWarning in Tabs.ipynb (#7172) --- examples/reference/layouts/Tabs.ipynb | 56 +++++++++++++-------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/examples/reference/layouts/Tabs.ipynb b/examples/reference/layouts/Tabs.ipynb index 73897b7e7b..0f91fd9dd8 100644 --- a/examples/reference/layouts/Tabs.ipynb +++ b/examples/reference/layouts/Tabs.ipynb @@ -2,13 +2,13 @@ "cells": [ { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "import panel as pn\n", "pn.extension()" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -45,9 +45,7 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "from bokeh.plotting import figure\n", "\n", @@ -59,7 +57,9 @@ "\n", "tabs = pn.Tabs(('Scatter', p1), p2)\n", "tabs" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -70,15 +70,15 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "p3 = figure(width=300, height=300, name='Square')\n", - "p3.square([0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 2, 1, 0], size=10)\n", + "p3.scatter([0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 2, 1, 0], marker='square', size=10)\n", "\n", "tabs.append(p3)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -89,12 +89,12 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "tabs" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -112,13 +112,13 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "print(tabs.active)\n", "tabs.active = 0" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -131,14 +131,14 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "tabs = pn.Tabs(p1, p2, p3, dynamic=True)\n", "\n", "tabs" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -149,9 +149,7 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "import time\n", "import numpy as np\n", @@ -171,7 +169,9 @@ "tabs = pn.Tabs(p1, p2, p3, dynamic=True)\n", "\n", "tabs" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -184,9 +184,7 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "tabs = pn.Tabs(\n", " ('red', pn.Spacer(styles=dict(background='red'), width=100, height=100)),\n", @@ -196,7 +194,9 @@ ")\n", "\n", "tabs" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -209,12 +209,12 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "pn.Row(tabs, tabs.clone(active=1, tabs_location='right'), tabs.clone(active=2, tabs_location='below'), tabs.clone(tabs_location='left'))" - ] + ], + "outputs": [], + "execution_count": null } ], "metadata": { From e9b62ecb4f9fe9cd5c39f90e39ab95374bba80fb Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Thu, 22 Aug 2024 07:08:42 -0700 Subject: [PATCH 90/91] properly wrap css (#7176) Co-authored-by: Philipp Rudiger --- panel/dist/css/chat_step.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/panel/dist/css/chat_step.css b/panel/dist/css/chat_step.css index 717b1b3d50..1aa8cedad0 100644 --- a/panel/dist/css/chat_step.css +++ b/panel/dist/css/chat_step.css @@ -16,11 +16,14 @@ } :host(.step-header) { - width: max-content; + width: fit-content; } .step-title { + width: calc(100% - 50px); font-size: 1.25em; + text-align: left; + overflow-wrap: break-word; } .step-message { From 3b0d8c25c31fb9af5caeb0a7c90930504eab1289 Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Thu, 22 Aug 2024 07:09:05 -0700 Subject: [PATCH 91/91] Make it easy to prompt user for input in ChatFeed (#7148) --- examples/reference/chat/ChatFeed.ipynb | 117 +++++++++++++++++++++ panel/chat/feed.py | 87 +++++++++++++++- panel/chat/message.py | 2 + panel/tests/chat/test_feed.py | 134 +++++++++++++++++++++++++ 4 files changed, 339 insertions(+), 1 deletion(-) diff --git a/examples/reference/chat/ChatFeed.ipynb b/examples/reference/chat/ChatFeed.ipynb index 5b9b22c594..bf117aeb7c 100644 --- a/examples/reference/chat/ChatFeed.ipynb +++ b/examples/reference/chat/ChatFeed.ipynb @@ -731,6 +731,123 @@ "See [`ChatStep`](ChatStep.ipynb) for more details on how to use those components." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Prompt User\n", + "\n", + "It is possible to temporarily pause the execution of code and prompt the user to answer a question, or fill out a form, using `prompt_user`, which accepts any Panel `component` and a follow-up `callback` (with `component` and `instance` as args) to execute upon submission." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def repeat_answer(component, instance):\n", + " contents = component.value\n", + " instance.send(f\"Wow, {contents}, that's my favorite flavor too!\", respond=False, user=\"Ice Cream Bot\")\n", + "\n", + "\n", + "def show_interest(contents, user, instance):\n", + " if \"ice\" in contents or \"cream\" in contents:\n", + " answer_input = pn.widgets.TextInput(\n", + " placeholder=\"Enter your favorite ice cream flavor\"\n", + " )\n", + " instance.prompt_user(answer_input, callback=repeat_answer)\n", + " else:\n", + " return \"I'm not interested in that topic.\"\n", + "\n", + "\n", + "chat_feed = pn.chat.ChatFeed(\n", + " callback=show_interest,\n", + " callback_user=\"Ice Cream Bot\",\n", + ")\n", + "chat_feed" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "chat_feed.send(\"food\");" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also set a `predicate` to evaluate the component's state, e.g. widget has value. If provided, the submit button will be enabled when the predicate returns `True`. The `predicate` should accept the component as an argument." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def is_chocolate(component):\n", + " return \"chocolate\" in component.value.lower()\n", + "\n", + "\n", + "def repeat_answer(component, instance):\n", + " contents = component.value\n", + " instance.send(f\"Wow, {contents}, that's my favorite flavor too!\", respond=False, user=\"Ice Cream Bot\")\n", + "\n", + "\n", + "def show_interest(contents, user, instance):\n", + " if \"ice\" in contents or \"cream\" in contents:\n", + " answer_input = pn.widgets.TextInput(\n", + " placeholder=\"Enter your favorite ice cream flavor\"\n", + " )\n", + " instance.prompt_user(answer_input, callback=repeat_answer, predicate=is_chocolate)\n", + " else:\n", + " return \"I'm not interested in that topic.\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also set a `timeout` in seconds and `timeout_message` to prevent submission after a certain time." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def is_chocolate(component):\n", + " return \"chocolate\" in component.value.lower()\n", + "\n", + "\n", + "def repeat_answer(component, instance):\n", + " contents = component.value\n", + " instance.send(f\"Wow, {contents}, that's my favorite flavor too!\", respond=False, user=\"Ice Cream Bot\")\n", + "\n", + "\n", + "def show_interest(contents, user, instance):\n", + " if \"ice\" in contents or \"cream\" in contents:\n", + " answer_input = pn.widgets.TextInput(\n", + " placeholder=\"Enter your favorite ice cream flavor\"\n", + " )\n", + " instance.prompt_user(answer_input, callback=repeat_answer, predicate=is_chocolate, timeout=10, timeout_message=\"You're too slow!\")\n", + " else:\n", + " return \"I'm not interested in that topic.\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Lastly, use `button_params` and `timeout_button_params` to customize the appearance of the buttons and timeout button, respectively." + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/panel/chat/feed.py b/panel/chat/feed.py index 955f04e0b4..a0924f46e7 100644 --- a/panel/chat/feed.py +++ b/panel/chat/feed.py @@ -22,13 +22,17 @@ from .._param import Margin from ..io.resources import CDN_DIST -from ..layout import Column, Feed, ListPanel +from ..layout import ( + Column, Feed, ListPanel, WidgetBox, +) from ..layout.card import Card from ..layout.spacer import VSpacer from ..pane.image import SVG, ImageBase from ..pane.markup import HTML, Markdown from ..util import to_async_gen from ..viewable import Children +from ..widgets import Widget +from ..widgets.button import Button from .icon import ChatReactionIcons from .message import ChatMessage from .step import ChatStep @@ -198,6 +202,8 @@ class ChatFeed(ListPanel): _callback_state = param.ObjectSelector(objects=list(CallbackState), doc=""" The current state of the callback.""") + _prompt_trigger = param.Event(doc="Triggers the prompt input.") + _callback_trigger = param.Event(doc="Triggers the callback to respond.") _post_hook_trigger = param.Event(doc="Triggers the append callback.") @@ -807,6 +813,85 @@ def add_step( self._chat_log.scroll_to_latest() return step + def prompt_user( + self, + component: Widget | ListPanel, + callback: Callable | None = None, + predicate: Callable | None = None, + timeout: int = 120, + timeout_message: str = "Timed out", + button_params: dict | None = None, + timeout_button_params: dict | None = None, + **send_kwargs + ) -> None: + """ + Prompts the user to interact with a form component. + + Arguments + --------- + component : Widget | ListPanel + The component to prompt the user with. + callback : Callable + The callback to execute once the user submits the form. + The callback should accept two arguments: the component + and the ChatFeed instance. + predicate : Callable | None + A predicate to evaluate the component's state, e.g. widget has value. + If provided, the button will be enabled when the predicate returns True. + The predicate should accept the component as an argument. + timeout : int + The duration in seconds to wait before timing out. + timeout_message : str + The message to display when the timeout is reached. + button_params : dict | None + Additional parameters to pass to the submit button. + timeout_button_params : dict | None + Additional parameters to pass to the timeout button. + """ + async def _prepare_prompt(*_) -> None: + input_button_params = button_params or {} + if "name" not in input_button_params: + input_button_params["name"] = "Submit" + if "margin" not in input_button_params: + input_button_params["margin"] = (5, 10) + if "button_type" not in input_button_params: + input_button_params["button_type"] = "primary" + if "icon" not in input_button_params: + input_button_params["icon"] = "check" + submit_button = Button(**input_button_params) + + form = WidgetBox(component, submit_button, margin=(5, 10), css_classes=["message"]) + if "user" not in send_kwargs: + send_kwargs["user"] = "Input" + self.send(form, respond=False, **send_kwargs) + + for _ in range(timeout * 10): # sleeping for 0.1 seconds + is_fulfilled = predicate(component) if predicate else True + submit_button.disabled = not is_fulfilled + if submit_button.clicks > 0: + with param.parameterized.batch_call_watchers(self): + submit_button.visible = False + form.disabled = True + if callback is not None: + result = callback(component, self) + if isawaitable(result): + await result + break + await asyncio.sleep(0.1) + else: + input_timeout_button_params = timeout_button_params or {} + if "name" not in input_timeout_button_params: + input_timeout_button_params["name"] = timeout_message + if "button_type" not in input_timeout_button_params: + input_timeout_button_params["button_type"] = "light" + if "icon" not in input_timeout_button_params: + input_timeout_button_params["icon"] = "x" + with param.parameterized.batch_call_watchers(self): + submit_button.param.update(**input_timeout_button_params) + form.disabled = True + + param.parameterized.async_executor(_prepare_prompt) + def respond(self): """ Executes the callback with the latest message in the chat log. diff --git a/panel/chat/message.py b/panel/chat/message.py index b8d22fe2c4..67d8cc5c71 100644 --- a/panel/chat/message.py +++ b/panel/chat/message.py @@ -50,6 +50,7 @@ SYSTEM_LOGO = "⚙️" ERROR_LOGO = "❌" HELP_LOGO = "❓" +INPUT_LOGO = "❗" GPT_3_LOGO = "{dist_path}assets/logo/gpt-3.svg" GPT_4_LOGO = "{dist_path}assets/logo/gpt-4.svg" WOLFRAM_LOGO = "{dist_path}assets/logo/wolfram.svg" @@ -79,6 +80,7 @@ "exception": ERROR_LOGO, "error": ERROR_LOGO, "help": HELP_LOGO, + "input": INPUT_LOGO, # Human "adult": "🧑", "baby": "👶", diff --git a/panel/tests/chat/test_feed.py b/panel/tests/chat/test_feed.py index 096afb4cd0..64aa7304d4 100644 --- a/panel/tests/chat/test_feed.py +++ b/panel/tests/chat/test_feed.py @@ -608,6 +608,140 @@ def test_update_chat_log_params(self, chat_feed): assert chat_feed._chat_log.scroll_button_threshold == 10 assert chat_feed._chat_log.auto_scroll_limit == 10 + +@pytest.mark.xdist_group("chat") +class TestChatFeedPromptUser: + + async def test_prompt_user_basic(self, chat_feed): + text_input = TextInput() + + def callback(component, feed): + feed.send(component.value) + + async def prompt_and_submit(): + chat_feed.prompt_user(text_input, callback) + await async_wait_until(lambda: len(chat_feed.objects) == 1) + text_input.value = "test input" + submit_button = chat_feed.objects[-1].object[1] + submit_button.clicks += 1 + await async_wait_until(lambda: len(chat_feed.objects) == 2) + + await asyncio.wait_for(prompt_and_submit(), timeout=5.0) + assert chat_feed.objects[-1].object == "test input" + + async def test_prompt_user_with_predicate(self, chat_feed): + text_input = TextInput() + + def predicate(component): + return len(component.value) > 5 + + def callback(component, feed): + feed.send(component.value) + + async def prompt_and_submit(): + chat_feed.prompt_user(text_input, callback, predicate=predicate) + await async_wait_until(lambda: len(chat_feed.objects) == 1) + + text_input.value = "short" + submit_button = chat_feed.objects[-1].object[1] + assert submit_button.disabled + + text_input.value = "long enough" + await async_wait_until(lambda: not submit_button.disabled) + + submit_button.clicks += 1 + await async_wait_until(lambda: len(chat_feed.objects) == 2) + + await asyncio.wait_for(prompt_and_submit(), timeout=5.0) + assert chat_feed.objects[-1].object == "long enough" + + async def test_prompt_user_timeout(self, chat_feed): + text_input = TextInput() + + def callback(component, feed): + pytest.fail("Callback should not be called on timeout") + + async def prompt_and_wait(): + chat_feed.prompt_user(text_input, callback, timeout=1) + await async_wait_until(lambda: len(chat_feed.objects) == 1) + await async_wait_until(lambda: chat_feed.objects[-1].object[1].disabled) + + await asyncio.wait_for(prompt_and_wait(), timeout=5.0) + + submit_button = chat_feed.objects[-1].object[1] + assert submit_button.name == "Timed out" + assert submit_button.button_type == "light" + assert submit_button.icon == "x" + + async def test_prompt_user_custom_button_params(self, chat_feed): + text_input = TextInput() + + def callback(component, feed): + feed.send(component.value) + + custom_button_params = { + "name": "Custom Submit", + "button_type": "success", + "icon": "arrow-right" + } + + async def prompt_and_check(): + chat_feed.prompt_user(text_input, callback, button_params=custom_button_params) + await async_wait_until(lambda: len(chat_feed.objects) == 1) + + await asyncio.wait_for(prompt_and_check(), timeout=5.0) + + submit_button = chat_feed.objects[-1].object[1] + assert submit_button.name == "Custom Submit" + assert submit_button.button_type == "success" + assert submit_button.icon == "arrow-right" + + async def test_prompt_user_custom_timeout_button_params(self, chat_feed): + text_input = TextInput() + + def callback(component, feed): + pytest.fail("Callback should not be called on timeout") + + custom_timeout_params = { + "name": "Custom Timeout", + "button_type": "danger", + "icon": "alert-triangle" + } + + async def prompt_and_wait(): + chat_feed.prompt_user(text_input, callback, timeout=1, timeout_button_params=custom_timeout_params) + await async_wait_until(lambda: len(chat_feed.objects) == 1) + await async_wait_until(lambda: chat_feed.objects[-1].object[1].disabled) + + await asyncio.wait_for(prompt_and_wait(), timeout=5.0) + + submit_button = chat_feed.objects[-1].object[1] + assert submit_button.name == "Custom Timeout" + assert submit_button.button_type == "danger" + assert submit_button.icon == "alert-triangle" + + async def test_prompt_user_async(self, chat_feed): + text_input = TextInput() + + async def async_callback(component, feed): + await asyncio.sleep(0.1) + feed.send("Callback executed") + + async def prompt_and_submit(): + chat_feed.prompt_user(text_input, async_callback) + await async_wait_until(lambda: len(chat_feed.objects) == 1) + + submit_button = chat_feed.objects[-1].object[1] + submit_button.clicks += 1 + + await async_wait_until(lambda: len(chat_feed.objects) == 2) + + await asyncio.wait_for(prompt_and_submit(), timeout=5.0) + + assert chat_feed.objects[-1].object == "Callback executed" + assert chat_feed.objects[-2].object.disabled == True + + @pytest.mark.xdist_group("chat") class TestChatFeedCallback:

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 j)Wu|^u?p?}QYweEA}hMaQb(}qZ1HO4M#dJ> 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