From a230d4c9381a9585dff73130b482f25966d62916 Mon Sep 17 00:00:00 2001 From: Tue Herlau <tuhe@dtu.dk> Date: Mon, 30 Aug 2021 17:50:12 +0200 Subject: [PATCH] updates --- .../__pycache__/unitgrade2.cpython-38.pyc | Bin 26946 -> 29204 bytes .../unitgrade_helpers2.cpython-38.pyc | Bin 6825 -> 6959 bytes unitgrade2/unitgrade2.py | 162 +++++++++++------- unitgrade2/unitgrade_helpers2.py | 131 +------------- 4 files changed, 110 insertions(+), 183 deletions(-) diff --git a/unitgrade2/__pycache__/unitgrade2.cpython-38.pyc b/unitgrade2/__pycache__/unitgrade2.cpython-38.pyc index a70e7b1190a2294f68b5e051feddd744b6c88a79..3492c9eca22889350f2a5707f0579dc18533a9c0 100644 GIT binary patch delta 15097 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 delta 13026 zcmbR8gz?ZNM!ry9UM>a(1_s0b8i@zWC-TYECowTFq%cG=q%fv1<uK+lMKLilq%fzj z<S<7ugK3s1Rxr&L#RjI?qu5iJQ&@92ayg?op=!CJxWIDUQQTmfCyED5^G5N4X}%~v zFwGyu52gj81i-XlRJ~vdTMBy%LzGYo2bdO4;RMqnDO@SsEeuhj?hGkBDZDKVDZI^0 zQDW{4DSRpXEet9Asp8GdQ4;P9DFP{iEet7wsgljiQBv*<DMBg2Eet8bsmxi@&CF3U z?hGj+DWWY5DWa(|&CF4<?hGknDdH^*DdMSe&CK;t^6m^N5-E}`3@MVS@+nfi%ux#N z3@OqnGA#@#GR;g;iYc-waxDx|N-6Rw3M~v#%I*v)iYZDh3@J*@Oi?N+%qhw_s<~=W zYK#o-3@Iupsx1sDs;SIb>RB4i%u$*tYANb13{hGs8Y!AB3{l!CJ}KHMIxUP*Iw`s- zdM)(~QMxJ2Df&5jx%yH1(BOmxqd}BGib0BD3qzD)3Ui844p**Glo8k(;}nw?h7^-j z<}Bl8<|vaC<`mN$(_FJCGq4Hp$Z=;#F-tLTVMsAgWzI5hW{$E*u}HCOVTiI!VNS8i zvC6fskFw6SiL%MHjj{!6wa($nwTrT2WJtA3u}QH_u}iT}aY$!rW{R>;@lA0`ac*IZ za&TuzaY=D)VMuX>2C`#{TZ(%NLzGjBM~Y_)LzHu>bE;D_W0X^hSBiHFLzGJjOE80` zUsVfNdS;12VoHjFo`Ra1LV9M&<X)x{Aw~uU22I9WT*-;a8L9C_sfj6*n1dL(CkHco z@`*4oFnk6j*&<#B28PKOV&%+A7-|@r8G{*$*dY3ti%WBhI2jliin!qf4~PZQAqryf zfwUfsked8Xm){Vqi>Zj6fq~%`Yg$fzei2U1VqndaBUp?W*(P_h$UEKQNJ=bDjZeue zy2X;0n45}b<1L}$)RNKykbHbnWqfi@VsWtq0|P@b3&;nI94tj*lLOR5Cb0(Dft<RM z@fJ&QYEBxuDJ&V8DX9t|eN13|Root#DJiLWdLVH>O_m~|$rD)Rw4_0<1DS)Szet>c zfk7UoorAGR9AYWw<gcvBjBJx5*;Le|K)z>7DosmEEke_Niz7ZhGcU6wK3-+=Vzvky zaCk8nDS~vcmKSA~q!uZG0-mV|YzTX9YH@L5I(kqPsWLDyXiPR|w^aoh0I@?7)y1Gl z0!2Y-afZ(1nd~y|LLhk=5Fra9R6qpCoFa7)3!7Kv7#J9oL0$z}!okSF!ogA`2lDP@ z0S?Z369xu`RE8+V6ox1!PysiGv7I4}5mfZFa78hvaDmGn#weB)jug%ohA7r{1{Q`W zwqOQL?pvJBi3KI4MVWc&ZYvpYv6bfKmX@R@gHkFuN*EXzKyk{<z`(!{vK|!6HH<Y3 z@eC!5%?w#gHB2?keUXfe3@Hq-gvwl`1rEaE(t^~YB9PxSS-?RE5obv(N-w^}mR$~_ zS27j}!o3U%Ca`)#kWC<K87A{{8r6F-Fff!bWPxmDDq(JBs9}g_sbR=sEmA3At6|7u zX=W^8h-b{>0I@-=8pa}r5{4}H8isfdkoppacuugJxoY5!<SqgwoFY)-yTu+~T#}Ms zT2jOcayMsuaS13mGV{UiEs~pjhErD*lmv=E!3>FDa1`-O=HrrN<e03%RW1OE7EP{O zY^fC`sd*_y29wuwNtx<{yau-iqTL82W{gNzMLHk>uz6sDXYxNTt1xT{#1y2^668eQ zcu1B@PR+@Qk4JKm9!Lo|7&RCe7z{wc2yz$)qYw)hUy%kly+rb^;R8kI^M_R;>)apS zvM-t}%-6-pJb50UaR@jvz?Or`lUp2xrK!awnfZA|+8~obae0fiB(o$Z6&9sVAaSr4 zbU`ez10enYaX}hFCadztyMmmd$rL3NUr>~qoS$1zT9TR)4>qaD8Ke?hj^U3lPE1RU zPfsm@s7%^?lHZI`)efW{T&giACKcaePb^L>DoU)p#gdkjSaOS{xUi_Ch<~z*pfZOe zD45xbq$c|dN@#<$-C{|}OiPPm&MV4A3N=uG-Qr3tE>0~f@yjpDP2W68P>HeLoq>U& zieE<|uRt$1u|fl+Q`5G{17s#=5y-S6kRysfsk6u%6fMlTi4{eDAlLbWhyV})a>7bh zNb(N^2?T)%u<l?G3mjHp0_+M7h%0=IN*E>^3;#+4MO2X+NFm5I5X-r+SzZ(hQUtaf zluwILT~HJTk_B4>CO~!-IWRCV)KBgc33mfW4&yBml*lPatjx(zOew+^Gs4KR2vyiM zSzFXY3S<-{Xwmge?iCeh?47(+v_=!0nLvdEL?zrwxA>q*BR(;&xIDFJ;$$x|e<P5Q znv6yEpnzva4@t0IFah$@EtZnh;*u$wPm66}WC4|7lP|=IPPUg+oV-;+L>m+)nv6x@ zXaH*g6JULO#TohK@le}S;%80%BC$m~4rC)^5je)cO2Gt@`qYYo)MQXXo;Uf6gzh9M zZl)s5$^25&*g(-%6*zgLl=ox<DK?&4j9i*b5PO3rb4ll_f?QG*14=WTt`*6t1>h_O z&T*V2MTyC&Nr}nXw<gb#mg9&8xq-h(YVsjz?F3MeMX?to78fVx<V3L*XXd2ll@x(8 z-Yt%T{Nm!wq?}ZUHSwVMPGDeQh+<F9FDgn+F1f{+T9g731$zQafPKOR%3cNenRz9} z(<l4N9I*hoy$BR`noLC?+l!JxPGEurd?pqXSc{YMi&9Ta_LQ}(2NxZPiVd8#K@kT| zvbXp^g-LEAD8x!Kb5n~VK&GXEf`P3(H8VY<1YG(TxqyU2Ktwo*0HxESNDwOuM1X<~ z;;<|bHycD)fe3B}1~F*U2wV(-yjNt+z`)==SzJz+(Ry-#n(XEzx$R7%ARl3*>&beG zl8hH7dno$pgHjwgX+lf6Tg*kN#kZJCDoSp#=auFrr4|)~r6B~*<b#UBjQ1yBRh(6? z335mhD8Sf@Kt*s-KFD`0S^1fHxA=>4;|q%N(~DAzi{q0Li*E6!R)Au=BpzlaC@6}G zLAu!>!3~P=TWtBIB?YA=MFk+E7>kRFKz4v55KMr*2?~@)3=EU+DVd=+E<i2-8wyTv z5GR8*f(fwhJO&1a^N7lVgP()BC=V1|lPi>y7}+M@R#wsl=UH$PWd$`2Q33$uFz$Fz z=~0kalo}uZX0n#bNi#$;0NJF;bc;#P;1+XsY9*2zK-S;lvdPITE&*jeyUz@hHC3a$ z8$l+rlvEa^-eQlB&r2<jkJp3-3CIaWH6VFTaI-TdGr8mzQ+Dw!#w0Xzi&_~N7~W4l zr)unvnk;W|gLUSnmSp6o6xD))6QmSLJIKLBAh#7&GB7Z_Mg$ELBNtOq<>Uah&EPy< zWHXsleTM)@xhBgk_RPFuP=Ro3@<nw~6;R4s$y8Jaaw=1L36kkxgButa7?>w>X;jsN z6ECQy)MUKHo}OBgSW;3{)CY1Eq6kH3VPs$^2Bl9n1||j$22I8yPz|ife2XnUvm`aQ zxCk6{Y>5R0sd*`ykmB+ddu}Q?(cWV8gg66iFv!eXteI>eZT}f2b7}_afehAUESd;P zf{+}4i?u8<rxc^nJC%Wffn#!qrZc3M!F-D~9^6epv>(7$fC2_$BgnEMP~7oM{-fz_ z2DNB9l10pAi8&CpU|nzxpk7lkxPQPjIgnjh1*8?C4vQ7HSU@d$vB^iY(j7oeRZW&6 zkW;~dbW0#3u{b_CF()TA1=8>@nu%mT3n=^`216V$Ia^zXQEGCBwv_;C`YCdrd`Vl` ze-<c;Kt2G6*Da2MqWpr?qLNCmjknl}Q%g!xi@<&c+fy_Lq#kTEf&lxni-CdR8^dH5 z9WBPL$@w~3j8T&l-GnEv)!}hS<p4M08JZcRI8#_t*jgB(xKaec&3MKr?i8UE?i8LD z#weZ?-W0wThA7??;S~N9ffmLnz7(cl22GL4g1X|9b#+tK7J_`re2Y0br}!3Q;Vnkw zRwLLEAO#wecj)R)KJCsqIZ;oz9u&};%thUx1Pn@-X_+~xMJ*stU<-KGc#ymvD0zY- zi?NC)D77HJs6-De;g$@lS3q3~5C*mFLB@mXhhk814{F{oU;>lOH4F<_7BVcH>|r2L z&y>QD%~|A7!;r;M!&JkV!q^KMwqXcn&}8z{WW2=&YN{ufXmZ|S$}hgfQjl1Zaf>}M zsTjgY$t(gV$`v3tgSxm;EafSgkSMvuU7T7H4{f?Yi}|9JV6Q?2qXf(IC;u@JFh`3m zP);=jd6NU=6GjOpA!ZK7Dt?$}At@`qh;eeDVLK!9<eP>v61TWQQj1G`@)J{1i;5P3 zEMWoFf43&{8tDaq3)T%FCvF7Euz?y~sYOLgK_bgQ#Ac8%YjH_pQOPaV;?m5L)LX12 z`6U>+<rYhBVrHK6<Zh!)jGHIh8Jni;25AS`SF{+!+5;lMrm@9?haieTy-%p;qj-wI z(FK)3SdZ%cTYT}5u74u9g;wl8`MPm|7bq?@nZaHIn_09Ml-wZA-Y5=efEI(p7s+F& zCKs(^U|@)zJkdnG9^Cq1bkpQ7LeF@hpkOacEh+}}NkBfk#Z!=&oE@K;3rcFmQ9_w{ zDVb%NDW!=yFtMUekhej}y9k^RK=D@u4w-!*36QNwxgO*uBS_J|fPsNQ5fss&f`o&S z1qPXz7`a$En7M?wcsO{9K&p}%Cx76VnA~q#69Y=+zZh%44XBVJa7%!t$O7bSmbA>g zl%h!>J4-;J!&X!X5@yTG$;?YFzQvexi@6}VB#Nmp=N4lol8eB(vy_2>p>%S(nLK0Z z=5{k#Mj3Fm!kCDz4pggjOg><4&Zsx}hq>ZpSqmA}TkK)3K~Da`uD94hPDsqjxy785 zng{73fSN*@EJcNr<1C~sZZQ>A++t45!6=8z7#J9uK|vt^O7l!CjC_n-jC_ndj2w)7 zj74RWH&~P~vP|BuC&B12+0N30ovFyChG7B6<a$f7dd^#nCAT=@i%RqIGV{{Gy}esZ zDJ4axKp_F{L4h*TEylc}9iU)=4i#Jj$$|vH8qmE7io*#Y_kaex7#P{0P=bktu}a)A zxg@hJH2~VunyhD~Tn}mwrhvu|7)ltkm}(fZm{XW~L443S0?Pu{g$#@gC2R}W7J|kW z*g-=Xn#_J7paP~Ou_!&YM3bZFIw-O@OEN&CcA0tUx7b3!%%W`|aW;rJ*at=69L596 z;PK#0o0_7G(b$CfZWhQBApbK=vNo>=wf|}uvKWf_N*GfZn;DxJQ<y+QEj0{TObeL7 zj%2A}NMUYfD)IvjfPf1}Flzx@3d=%9Mh1}R0(Owf6p&X~OE|JP7jS|2CEN?RLEg*a zkpLB+tbSFB!6o?x1(|v23W*BEAm-#f){>CC#|!o@xaD7T7vxq@iiH#+_rd-xEl5cO zl|#un`NgS-p%lpY6(|6T?t!#nt0*{2Qj2mk^Abx^7l1qu8utXnAO|BGBNwB{<Y=4q zjLeg{ZH?=9fox&{)hgiLO(v+56vdj8npl>4izOwsIQbT9UUGg;aTHflVo`ireo<~> z2{_+r^4?-8DNM;Nx(RXvV*CYE+Z2J_&svn2m!1mo<U^1mkdtq*7U!g<78Kn82|NIe zwrN7zKt<qU7*sMq(jhoUg5rJc<OjA!^-Li}S3oJ39lf*yrBX<ecmy&Mqyu6Ano*Y- z7#R9NVF5}A9E@CC9BdrCMIcd4##`L+nZ<eeCE#)&lvzO;5RO3wWhAJ7R19(xVgwLW zKi*>Ztqca$7Jiz{=q^Jz0L%huE_%bjz_1VG3Q+aU!C3T$Ve-ac;mLh=VOpT#eI+Br z5KfR+z`DQ$SSx7!`4A%mLlNI(K6~8=P-3~ooSRqx9^1RcoLiKy$#jb&Gq1QLF)ul_ zr~=c7Tf#6|a03mLagwtSf?NY?DljlgF&2FWxkr5RDti;iU=k~&Wx|%8T9R9oe~TkC z7cwXYb|z?S7t}L|kJn_n#R6*S7cD`vb+WR9lEhJv6`){eU=(2FVdP>g`T<hrx;fV2 zK4U$oFbAioqC}7=sHU#su*uC&Da}c>D>?y+bZ&?%K-F_{cF_e;lH~-s2&@KD+Jow_ zTO4WeIr+(nImM7LVM~io&QD3b#h#oGX*h$zOq2Z<OL2B)LD5f;Z95p885p9tK|QLx zlC&bwAUvr3e2WcYe-sDE0C4*T91&mw6dSj=!A;yEu(_uv|8vr0Vql!C>0Fco_72Ds zU^i$&1CR&Q<tR!o12yxDAwFPEPc12$0P-H&EvB?0uoofYl%Sjt#RXOy4+`I+Pavm) z?FJJ(lm9zQO1%e#Dx_%UU=(5&Vd7zAVc}saVqu(Y<Z@HP7F7>eBbWd=A3XiR#sp4G z#;!Kt@~8;pa!oF<J8tnLmlhSJ!h+-vIK<Q9({u8ZK%on2pWb3hPrk(q8rLXF1r7Uy zy6Yg{K%xT_N1AN6SiwPhiv<*lw^%{ii=#NwO7oJzy-rpTtN0d6no?<AGE$}mx$qWm zdS+Q_X+cRwDo7R6WC1s2CPBu@rf%Ad4WO2LJ$&$;BZ?K&YM;ZH!k)s>!V<*>mf=j{ zYT<}t2eY|Tcv?83I8r&YxU#rYSW|fCFo7rjSfY4RB*0UCj8VLhslF&a$OK;$KV)Jr zN&q~JFBr_ADS3-KG$gg6Bq+7GG^d31C1~hzGNZedJSgXCG9!)Nau$~+C#Mz{rxt_S zeUqcyr6j>^Pf!gB@h8ZO;Bgr7$-VA*!M9jlgM$2nZn3yIdirQG-{LDM%FHWqEh@?{ z^2sbNxy6<WViw=xNK4GjDJ@DZ##Yze;zsBymY(eAQOO1#P~JHCtcQUHI7xzrnr<<t z78MntI^`BeVoD0wI+e+)o~g?6j0_A#pk`T-Du{)uxJa21G#RtjQ=JoJUXcVN1H<G4 zY*LfId&=5E!$K6K4crV<VPs%{74cx@;6w-}K!I?JD={S{7!q8DlLNgB!95~SVF9X0 zia|YeP-R`zRl>Lc+@=6^TT&PoGSxDbux7DAs$o#irIxvb6GW#lm2jmn*Dz#pr?5yc zl<?FrH#15wG&6}Xh%<na3Ak^=UdxigUdvj;Sd^5)k;jz6S<76*l){zGTGTZ8r<Zbl zkt-<ofXW_lu6PM5AHl`bO2%8vps5E?oe;$m;20cyiwzWKt}aDa85kITF)3*L;#4S3 zP0r6tDc02FyCn!7^F$tzf(&Sbe0Yl~Ik^a2Yayyn(A?CmdI3;388RLp4~|qwI}$Yf ze2Xd12U>s>mlVMUY!nz77@}B9ic^z|Z!r}Y-(rX6-=YkVN5G*CCO|=61d0+TCI*IL zP<IW~ED&PkVq{~~0=2!FK+Vm`-@VQ1K|Svp1{Q`ah7?9~1}27hh7`tHrWD3nP~VZU zhH)WdElUl{0;UwEg^Vf8k_@#hDJ(TiDU9L_HOyd=C5>4O)IMTPVXb9NVXFmCBNe5U zu-34G8;9(@pmrBqo>mDvn9otdk-}WVmcl8)P{LVL&ya;2b6hFhpqPWqFoD~SJbsW` z1JwKn$5IuamX?-6p=zd{YNn=wYVk|Z;)Eg*Mh1qJOhw?PE+|QXYm_2QQ2GSdH(H?d z$$g6hG^Sk~RGN2-FFm!yCAGLdxhNAnWPFRYxF9F9qzKd<Ez$?+12r0o3_vXK5Hl!N zATh!k4>CoQ9Wt&6PVJ!9(JjWTC@w^d7Uh8A#0aDx94BA`6fw6r!A)6EH#U$-hJgV* z+snYj#|-KfaWV3-NPuIAjZtXwcVF$vv&}g9Kyw_R{E@<t%~WJH+0su%G=-&?sg}8h zxrQO06&z5kenqa6%lu5)%t2mdo4nCav>x151&<@yf!qQ25vaTZ4QPvi`@ZpLL&;#Z zV2^{{3?{%1=YjNBgTXU=8K9&EshGGJSwIy&6ALpBW0eG=^qlPFuff;BzyNAWGl1RR zGP%J&ufBvMg{y{1grONa*Qv?v2g!=?r~=35FGdA$jDz!BkuJywpfQnOjJBG*MfM;r z*pc9@1@HWzH=x0B2Tl?22n;PU1_cphKrf1|Fdo#uD8d#~eDOu8#U=SgsSq!hfV>Q8 zWOFfcfxOHpz|6uZ#VGZkjcM}60EK!XaEqx3F<c8a6`Y*FEm%2_tJOgSsB|p?k5aHC zCl;sP;sB)w=fvXFA`Or<DDxKCf>;h9!Wl$>2RFg70Vcr3H7H(+gc%taL_ltn1!Z+e z_nL==kCTUmhf_q5i=RWBgQrNCadJuEu3%90Uj#1b#6W?-a*HW5A3Rx$sMs_igAI^Y z3TQ?IA`eoJR-u6Fw${mwLFu938i5Td^xYU47;bSFfooCl7;%v}$P=I%5>`=wO$7S| zOo04yi#t9yF*_Af`F2gV4c2o7wfT@j6daCTAfs@GhdUz!LklQ8K>p-l<Y6sxpS&Ts zh2<@1Nk<^N=w#mz0S%CYi{63qD6VGsBuLAtHAGMIJ*WW(?ZJSW+@J~~Ek7So%im(n zO-#>Bo;dkVh>?*G$aTIT0_-nu5DV-%_VOaoXmZg6kmEoOU|{57;b1KC-|P^|#t5zx zL3KniXzsCwVFANJh7`sU#u~;NrVhqt<}@a825_;=RKvWGvDl%5p@t!iv4(LTQw>ue zsF-G004fkz7BVq1<Z*$9{y^$L#W$!A<9CY{JW!>{dW!?hh|f$Z3Is(GKV+OLe)9P+ zWhZb00x=0)4+@}Z4B)~Q<gHt*pf+6!!sA8$j0_A5LH+^xkbzNxak5Fcq65g|nk+>i zEk*Gl|A&JJkXne=I1m@4qbQP*fng3vKR7&CSQz;@iXs^&2dW8sgVqp$`e((fP}XEb zF{gmmUob|orn03lr7*XEhIH9eSW;M97@|19>oXXlIKczEpdnuHny|@?5n?GRO3+py zAG8(74{ZesKwE)=DaxrrsX{4?DUx%TQ>0R)TUeunQ)E(PTNt85Qsh$PL5s^$6jBsh z7^1|$Ek^NR22GV)oT0GX>o@sAtW-QG8>WCNMFud*2qu|8#WZC75%VpM`214PG)*da zc<&ZFBnpf)88P}+pa=ksxNU@o5!2*{QA(5V#_~?y6Q^gK2+GDx;BH6}h>f+)$OE4F zhg!LPvTVHZWDghK`W*0T5Kua+WvXFpW~gN@0jIheCJBaGmKufyj0+iRS!)<TZ8FfH zNeW{cQw=M)B)0>t5XobzVFjf&P&EV^`Ke(5sj^`}(!rF*R0B?tAm3{;`$5K_Kw|`& zY;utC9f9JE{31|g46BQaVnC@QYVv{@k(46P`Wa~Z=N1>J;`N2hHG!*o@H`%SYEEK7 zacasfrebKq1f@Rkz~e1Y+Rn)V#STQ<9#AMkYAw(x2`Kj|F-@MC5M%*LewxgXa0HFz z7Nvp$7E(#vVobiph`!9BD2kDR;WX1^<wU*7)6ID6&A=5MI4~JOZ75Jvb|HfcL##|K za}8q+a~h*KLoE|1h-#P?G8T(4G87sWiqwGS7MT3Nxk8f#Z00Ra(0Zwo{QR8aTdc|X zrFkW{1fc07J~=<HBr!7&+~WXs*O;;miokjFxGyN7fISW-KzZvHe`#K3VQDHvb9`pX z1!UiY>uhk7uu2l1e)XU_Cfg(`F=|YXPm~lXVFoRO0%fuqhIkf8G){i*qEb(|?|BDY zf`Zn5s32^BtrXL<f%M|-^lWkxbCXgM?KD{tmC!Bjc+luQ#OXz#iK{4HXt9--l3I~k ztf>Zx8>DJV4;};{--9csn@p4MBz6aa#?XtBKoQCc8F~XZia?^EWh}`hU^%QgM<hNq zueh`b7KW)6nZ+f=_n0OxPfCM~ov;)YgHkKpz#=A)HBh&KqUIKRN@`kSX%5D;Wh^5D z!xN^-&ywXP7bIJO*UYVCMlucTUL=ExK;<wvp+E?*CqY@|1;~&Pb7%@ib<Rp?tp#!k zL>oML6h$*KFuY-!T$rLh*+^4zvSNxPxC$yN1$n3pM1a+T39w~(j0_APKxqh6u`)1n zFjh$+#Va^^jNnOBPm}o;Yg%SeaY>OUC}KbzNQ{{)P`eG(Ix7ZEz1A?eg0l~#>jA3c zWJ;J)7;BiDnHWK33Md;fEMTc&u3=orRKv8887#^Q;X{_qX)^g$$xZf7mC8`aPOa2r zD=Gka1LRRi-wL#bs0bAB;4%i<4lXJODFQk27E>lh5yAsmN90;qnwaDK1>}2Bj|1d; zE=DdUvB|g7N?bt|RMBdXFV}(y&^%w!au5rgEWj-oY~#1wkmL=X)%(LVc}n^+Gf<@8 zVk%G4<SD8Gxe9LbE!NCp(2N_%f?FJ*Mp8;<Q86?%PcF!i&<1IRO;fWbg9m#V(btI< zfhMm0Gfm!{;ZhG;!UFA8Fc<NGGB&itX9R~jBPi5Mn6p?Gu!6dQMPVguHH;}t%}o6g zwM-@K3phZ<7^Dl5#Tm~K&S1(A$PmO3!H~yQ!j!^X1J(~RRg=Z9N(eM#2O1v-ClZCU zqWs*+5t*v>pezXSD7c}H@GTo8rxn$K0s%B~b&Ca59)QbdShO{Q<eETD7?za85{wC) zLPiD#E@lRXVo>-qFmf@nFmZsJ?V!O)5vD59(2&W$GNtMz&>QXGY831;P>>=`dV%Vq zqD+vjpt2v5w!rOCaGrxFD{#gt0%evWR*>RK5CM)&Q0Nzd+*O2Vyt;$)7r5*KXR-uF z28K1DWTpmM7y#-EGO|GC$yhn~IaoP3d008vxL82TRykNiI5?O&xQjq~CI_<1S;ALk zg7dm2W03&JZm<nt0&D|l5wJKYU_isu42(q}{^WxZqMMttc^SpP#R%gq&Xm;T{G!B? z{31=JA_2z9OLH{%5k_<~GB7AizLVo90}9+CKTw&+Skwi#tP{lInQWOWD=Y=F2{aJG zz{bbH!N|c>)U!D=SC}caosogTPm{Ii8>k{<0}q5k(=$j0oL@DWA?+cMKonnYVr5dQ z3&dHe82MBLG&G%`QXHOHk^xneU!*_zVZLyEAH;c};T}y!H%(Wtd%+_;MW7&n^ut*5 zN^=V;A>%gSbO>sS7kPj@0&0jBRe|y|IA@;$v2KHi7a#)Em?;7!(V`qsT4c|yEXps< zOS#38TU?rZi!C>;9=u>2)F=cmv%bZapOlrFTv8+l(%l3i!0Ve_K~{m5x)kMs#6ZKZ z;59QvphXQupvlo9(A;GaXa*5Hr4hwfnwMDuY7^;!=QN5yt80pOf`XG1)Z_tASrolt zsApg(0<DLK49GKptO2d)yTw*gSpb?x1Se81kgy<#09Cd{N+1@fV;Kc%L_jP7%{!(R z6@hwoMO#5m1a-8)9e`V$py69^lcgvCq!ZM!C<+3xf*Bct8E$dKJA)SLd*-F(gIoOI z6bnvxMLi%@pcn!tStJ6KR&T-Q2tZ?T#h?Z&3k!JgYE7Y<gHSyKtALCEw~#hp2nQbr z51%d%lL#{h3kMsQ3Wop>qW}|!01uN;E)OFQlMoLFhX^ANqYx99fKWCEGlwt-6958Y BLwf)K diff --git a/unitgrade2/__pycache__/unitgrade_helpers2.cpython-38.pyc b/unitgrade2/__pycache__/unitgrade_helpers2.cpython-38.pyc index 5f1fcf25687cedd7203740db7a57b848a3ce5503..efd8ce6f58cc6a9f0d3cd6104d4cbe6e9982131d 100644 GIT binary patch delta 2663 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 delta 2518 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 Xe<^u>X09*}HX#-vb_PZUMuvX?rY=bK diff --git a/unitgrade2/unitgrade2.py b/unitgrade2/unitgrade2.py index 08c9d48..c094340 100644 --- a/unitgrade2/unitgrade2.py +++ b/unitgrade2/unitgrade2.py @@ -45,12 +45,13 @@ class Logger(object): pass class Capturing(list): - def __init__(self, *args, unmute=False, **kwargs): + def __init__(self, *args, stdout=None, unmute=False, **kwargs): + self._stdout = stdout self.unmute = unmute super().__init__(*args, **kwargs) def __enter__(self, capture_errors=True): # don't put arguments here. - self._stdout = sys.stdout + self._stdout = sys.stdout if self._stdout == None else self._stdout self._stringio = StringIO() if self.unmute: sys.stdout = Logger(self._stringio) @@ -70,6 +71,20 @@ class Capturing(list): if self.capture_errors: sys.sterr = self._sterr +class Capturing2(Capturing): + def __exit__(self, *args): + lines = self._stringio.getvalue().splitlines() + txt = "\n".join(lines) + numbers = extract_numbers(txt) + self.extend(lines) + del self._stringio # free up some memory + sys.stdout = self._stdout + if self.capture_errors: + sys.sterr = self._sterr + + self.output = txt + self.numbers = numbers + class QItem(unittest.TestCase): title = None @@ -313,12 +328,13 @@ class Report(): modules = os.path.normpath(relative_path[:-3]).split(os.sep) return root_dir, relative_path, modules - def __init__(self, strict=False, payload=None): working_directory = os.path.abspath(os.path.dirname(self._file())) self.wdir, self.name = setup_dir_by_class(self, working_directory) # self.computed_answers_file = os.path.join(self.wdir, self.name + "_resources_do_not_hand_in.dat") + for (q,_) in self.questions: + q.nL = self.nL # Set maximum line length. if payload is not None: self.set_payload(payload, strict=strict) @@ -360,28 +376,6 @@ class Report(): for q, _ in self.questions: q._cache = payloads[q.__qualname__] - # for item in q.items: - # if q.name not in payloads or item.name not in payloads[q.name]: - # s = f"> Broken resource dictionary submitted to unitgrade for question {q.name} and subquestion {item.name}. Framework will not work." - # if strict: - # raise Exception(s) - # else: - # print(s) - # else: - # item._correct_answer_payload = payloads[q.name][item.name]['payload'] - # item.estimated_time = payloads[q.name][item.name].get("time", 1) - # q.estimated_time = payloads[q.name].get("time", 1) - # if "precomputed" in payloads[q.name][item.name]: # Consider removing later. - # item._precomputed_payload = payloads[q.name][item.name]['precomputed'] - # try: - # if "title" in payloads[q.name][item.name]: # can perhaps be removed later. - # item.title = payloads[q.name][item.name]['title'] - # except Exception as e: # Cannot set attribute error. The title is a function (and probably should not be). - # pass - # # print("bad", e) - # self.payloads = payloads - - def rm_progress_bar(txt): # More robust version. Apparently length of bar can depend on various factors, so check for order of symbols. nlines = [] @@ -404,10 +398,9 @@ def extract_numbers(txt): all = [float(a) if ('.' in a or "e" in a) else int(a) for a in all] if len(all) > 500: print(txt) - raise Exception("unitgrade.unitgrade.py: Warning, many numbers!", len(all)) + raise Exception("unitgrade.unitgrade.py: Warning, too many numbers!", len(all)) return all - class ActiveProgress(): def __init__(self, t, start=True, title="my progress bar",show_progress_bar=True): self.t = t @@ -459,8 +452,9 @@ class ActiveProgress(): from unittest.suite import _isnotsuite -class MySuite(unittest.suite.TestSuite): # Not sure we need this one anymore. - pass +# class MySuite(unittest.suite.TestSuite): # Not sure we need this one anymore. +# raise Exception("no suite") +# pass def instance_call_stack(instance): s = "-".join(map(lambda x: x.__name__, instance.__class__.mro())) @@ -616,8 +610,11 @@ class UTextResult(unittest.TextTestResult): n = UTextResult.number item_title = self.getDescription(test) - item_title = item_title.split("\n")[0] - + # item_title = item_title.split("\n")[0] + item_title = test.shortDescription() # Better for printing (get from cache). + if item_title == None: + # For unittest framework where getDescription may return None. + item_title = self.getDescription(test) # test.countTestCases() self.item_title_print = "*** q%i.%i) %s" % (n + 1, j + 1, item_title) estimated_time = 10 @@ -632,7 +629,7 @@ class UTextResult(unittest.TextTestResult): def _setupStdout(self): if self._previousTestClass == None: - total_estimated_time = 2 + total_estimated_time = 1 if hasattr(self.__class__, 'q_title_print'): q_title_print = self.__class__.q_title_print else: @@ -688,7 +685,10 @@ def cache(foo, typed=False): """ maxsize = None def wrapper(self, *args, **kwargs): - key = self.cache_id() + ("cache", _make_key(args, kwargs, typed)) + key = (self.cache_id(), ("@cache", foo.__name__, _make_key(args, kwargs, typed)) ) + # key = (self.cache_id(), '@cache') + # if self._cache_contains[key] + if not self._cache_contains(key): value = foo(self, *args, **kwargs) self._cache_put(key, value) @@ -700,15 +700,56 @@ def cache(foo, typed=False): class UTestCase(unittest.TestCase): _outcome = None # A dictionary which stores the user-computed outcomes of all the tests. This differs from the cache. - _cache = None # Read-only cache. - _cache2 = None # User-written cache + _cache = None # Read-only cache. Ensures method always produce same result. + _cache2 = None # User-written cache. + + def capture(self): + return Capturing2(stdout=self._stdout) + + @classmethod + def question_title(cls): + """ Return the question title """ + return cls.__doc__.strip().splitlines()[0].strip() if cls.__doc__ != None else cls.__qualname__ @classmethod def reset(cls): + print("Warning, I am not sure UTestCase.reset() is needed anymore and it seems very hacky.") cls._outcome = None cls._cache = None cls._cache2 = None + def _callSetUp(self): + self._stdout = sys.stdout + import io + sys.stdout = io.StringIO() + super().setUp() + # print("Setting up...") + + def _callTearDown(self): + sys.stdout = self._stdout + super().tearDown() + # print("asdfsfd") + + def shortDescriptionStandard(self): + sd = super().shortDescription() + if sd == None: + sd = self._testMethodName + return sd + + def shortDescription(self): + # self._testMethodDoc.strip().splitlines()[0].strip() + sd = self.shortDescriptionStandard() + title = self._cache_get( (self.cache_id(), 'title'), sd ) + return title if title != None else sd + + @property + def title(self): + return self.shortDescription() + + @title.setter + def title(self, value): + self._cache_put((self.cache_id(), 'title'), value) + def _get_outcome(self): if not (self.__class__, '_outcome') or self.__class__._outcome == None: self.__class__._outcome = {} @@ -716,37 +757,31 @@ class UTestCase(unittest.TestCase): def _callTestMethod(self, testMethod): t = time.time() + self._ensure_cache_exists() # Make sure cache is there. + if self._testMethodDoc != None: + # Ensure the cache is eventually updated with the right docstring. + self._cache_put((self.cache_id(), 'title'), self.shortDescriptionStandard() ) + # Fix temp cache here (for using the @cache decorator) + self._cache2[ (self.cache_id(), 'assert') ] = {} + res = testMethod() elapsed = time.time() - t - # if res == None: - # res = {} - # res['time'] = elapsed - sd = self.shortDescription() - self._cache_put( (self.cache_id(), 'title'), self._testMethodName if sd == None else sd) - # self._test_fun_output = res + # self._cache_put( (self.cache_id(), 'title'), self.shortDescription() ) + self._get_outcome()[self.cache_id()] = res self._cache_put( (self.cache_id(), "time"), elapsed) - # This is my base test class. So what is new about it? def cache_id(self): c = self.__class__.__qualname__ m = self._testMethodName return (c,m) - def unique_cache_id(self): - k0 = self.cache_id() - key = () - for i in itertools.count(): - key = k0 + (i,) - if not self._cache2_contains(key): - break - return key - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._load_cache() - self.cache_indexes = defaultdict(lambda: 0) + self._assert_cache_index = 0 + # self.cache_indexes = defaultdict(lambda: 0) def _ensure_cache_exists(self): if not hasattr(self.__class__, '_cache') or self.__class__._cache == None: @@ -766,17 +801,22 @@ class UTestCase(unittest.TestCase): self._ensure_cache_exists() return key in self.__class__._cache - def _cache2_contains(self, key): - self._ensure_cache_exists() - return key in self.__class__._cache2 + def wrap_assert(self, assert_fun, first, *args, **kwargs): + key = (self.cache_id(), 'assert') + if not self._cache_contains(key): + print("Warning, framework missing", key) + cache = self._cache_get(key, {}) + id = self._assert_cache_index + if not id in cache: + print("Warning, framework missing cache index", key, "id =", id) + _expected = cache.get(id, first) + assert_fun(first, _expected, *args, **kwargs) + cache[id] = first + self._cache_put(key, cache) + self._assert_cache_index += 1 def assertEqualC(self, first: Any, msg: Any = ...) -> None: - id = self.unique_cache_id() - if not self._cache_contains(id): - print("Warning, framework missing key", id) - - self.assertEqual(first, self._cache_get(id, first), msg) - self._cache_put(id, first) + self.wrap_assert(self.assertEqual, first, msg) def _cache_file(self): return os.path.dirname(inspect.getfile(self.__class__) ) + "/unitgrade/" + self.__class__.__name__ + ".pkl" diff --git a/unitgrade2/unitgrade_helpers2.py b/unitgrade2/unitgrade_helpers2.py index 3562336..8421562 100644 --- a/unitgrade2/unitgrade_helpers2.py +++ b/unitgrade2/unitgrade_helpers2.py @@ -5,7 +5,7 @@ import pyfiglet from unitgrade2 import Hidden, myround, msum, mfloor, ActiveProgress from unitgrade2 import __version__ import unittest -from unitgrade2.unitgrade2 import MySuite +# from unitgrade2.unitgrade2 import MySuite from unitgrade2.unitgrade2 import UTextResult import inspect @@ -64,53 +64,6 @@ def evaluate_report_student(report, question=None, qitem=None, unmute=None, pass show_tol_err=show_tol_err) - # try: # For registering stats. - # import unitgrade_private - # import irlc.lectures - # import xlwings - # from openpyxl import Workbook - # import pandas as pd - # from collections import defaultdict - # dd = defaultdict(lambda: []) - # error_computed = [] - # for k1, (q, _) in enumerate(report.questions): - # for k2, item in enumerate(q.items): - # dd['question_index'].append(k1) - # dd['item_index'].append(k2) - # dd['question'].append(q.name) - # dd['item'].append(item.name) - # dd['tol'].append(0 if not hasattr(item, 'tol') else item.tol) - # error_computed.append(0 if not hasattr(item, 'error_computed') else item.error_computed) - # - # qstats = report.wdir + "/" + report.name + ".xlsx" - # - # if os.path.isfile(qstats): - # d_read = pd.read_excel(qstats).to_dict() - # else: - # d_read = dict() - # - # for k in range(1000): - # key = 'run_'+str(k) - # if key in d_read: - # dd[key] = list(d_read['run_0'].values()) - # else: - # dd[key] = error_computed - # break - # - # workbook = Workbook() - # worksheet = workbook.active - # for col, key in enumerate(dd.keys()): - # worksheet.cell(row=1, column=col+1).value = key - # for row, item in enumerate(dd[key]): - # worksheet.cell(row=row+2, column=col+1).value = item - # - # workbook.save(qstats) - # workbook.close() - # - # except ModuleNotFoundError as e: - # s = 234 - # pass - if question is None: print("Provisional evaluation") tabulate(table_data) @@ -142,7 +95,12 @@ class UnitgradeTextRunner(unittest.TextTestRunner): class SequentialTestLoader(unittest.TestLoader): def getTestCaseNames(self, testCaseClass): test_names = super().getTestCaseNames(testCaseClass) - testcase_methods = list(testCaseClass.__dict__.keys()) + # testcase_methods = list(testCaseClass.__dict__.keys()) + ls = [] + for C in testCaseClass.mro(): + if issubclass(C, unittest.TestCase): + ls = list(C.__dict__.keys()) + ls + testcase_methods = ls test_names.sort(key=testcase_methods.index) return test_names @@ -174,12 +132,12 @@ def evaluate_report(report, question=None, qitem=None, passall=False, verbose=Fa for n, (q, w) in enumerate(report.questions): # q = q() - q_hidden = False + # q_hidden = False # q_hidden = issubclass(q.__class__, Hidden) if question is not None and n+1 != question: continue suite = loader.loadTestsFromTestCase(q) - qtitle = q.__name__ + qtitle = q.question_title() if hasattr(q, 'question_title') else q.__qualname__ q_title_print = "Question %i: %s"%(n+1, qtitle) print(q_title_print, end="") q.possible = 0 @@ -193,77 +151,6 @@ def evaluate_report(report, question=None, qitem=None, passall=False, verbose=Fa UTextResult.number = n res = UTextTestRunner(verbosity=2, resultclass=UTextResult).run(suite) - # res = UTextTestRunner(verbosity=2, resultclass=unittest.TextTestResult).run(suite) - z = 234 - # for j, item in enumerate(q.items): - # if qitem is not None and question is not None and j+1 != qitem: - # continue - # - # if q_with_outstanding_init is not None: # check for None bc. this must be called to set titles. - # # if not item.question.has_called_init_: - # start = time.time() - # - # cc = None - # if show_progress_bar: - # total_estimated_time = q.estimated_time # Use this. The time is estimated for the q itself. # sum( [q2.estimated_time for q2 in q_with_outstanding_init] ) - # cc = ActiveProgress(t=total_estimated_time, title=q_title_print) - # from unitgrade import Capturing # DON'T REMOVE THIS LINE - # with eval('Capturing')(unmute=unmute): # Clunky import syntax is required bc. of minify issue. - # try: - # for q2 in q_with_outstanding_init: - # q2.init() - # q2.has_called_init_ = True - # - # # item.question.init() # Initialize the question. Useful for sharing resources. - # except Exception as e: - # if not passall: - # if not silent: - # print(" ") - # print("="*30) - # print(f"When initializing question {q.title} the initialization code threw an error") - # print(e) - # print("The remaining parts of this question will likely fail.") - # print("="*30) - # - # if show_progress_bar: - # cc.terminate() - # sys.stdout.flush() - # print(q_title_print, end="") - # - # q_time =np.round( time.time()-start, 2) - # - # print(" "* max(0,nL - len(q_title_print) ) + (" (" + str(q_time) + " seconds)" if q_time >= 0.1 else "") ) # if q.name in report.payloads else "") - # print("=" * nL) - # q_with_outstanding_init = None - # - # # item.question = q # Set the parent question instance for later reference. - # item_title_print = ss = "*** q%i.%i) %s"%(n+1, j+1, item.title) - # - # if show_progress_bar: - # cc = ActiveProgress(t=item.estimated_time, title=item_title_print) - # else: - # print(item_title_print + ( '.'*max(0, nL-4-len(ss)) ), end="") - # hidden = issubclass(item.__class__, Hidden) - # # if not hidden: - # # print(ss, end="") - # # sys.stdout.flush() - # start = time.time() - # - # (current, possible) = item.get_points(show_expected=show_expected, show_computed=show_computed,unmute=unmute, passall=passall, silent=silent) - # q_[j] = {'w': item.weight, 'possible': possible, 'obtained': current, 'hidden': hidden, 'computed': str(item._computed_answer), 'title': item.title} - # tsecs = np.round(time.time()-start, 2) - # if show_progress_bar: - # cc.terminate() - # sys.stdout.flush() - # print(item_title_print + ('.' * max(0, nL - 4 - len(ss))), end="") - # - # if not hidden: - # ss = "PASS" if current == possible else "*** FAILED" - # if tsecs >= 0.1: - # ss += " ("+ str(tsecs) + " seconds)" - # print(ss) - - # ws, possible, obtained = upack(q_) possible = res.testsRun obtained = len(res.successes) -- GitLab