From b3d7d62f9f4b9e0ed54ec1a97bb044843ebf7fba Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 30 Aug 2020 10:29:19 +0000 Subject: [PATCH 01/98] logo updates --- README.md | 7 + pycalver1k.svg | 335 +++++++++++++++++++++++++++++++++++++++++++++ pycalver1k_128.png | Bin 0 -> 6708 bytes 3 files changed, 342 insertions(+) create mode 100644 pycalver1k.svg create mode 100644 pycalver1k_128.png diff --git a/README.md b/README.md index 9b22567..4ddbc7c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,10 @@ +
+

+ logo +

+
+ + # [PyCalVer: Automatic Calendar Versioning][repo_ref] PyCalVer is a cli tool to search and replace version strings in the files of diff --git a/pycalver1k.svg b/pycalver1k.svg new file mode 100644 index 0000000..156e809 --- /dev/null +++ b/pycalver1k.svg @@ -0,0 +1,335 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + v2020.1001 + + + + + + + + + + diff --git a/pycalver1k_128.png b/pycalver1k_128.png new file mode 100644 index 0000000000000000000000000000000000000000..f35104831c795efd97a0930a4ba9ac187eefc50d GIT binary patch literal 6708 zcmV-48q4L0P)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H18No?J zK~#90?VWdcR8{)NKhrXkWYRk&6cGfZ7r_D%Q0!q{MNt%XEo%XF75%L!3h3fmcGndY z1r%|uATCN5EVM`wkS-mBKms8lZPKRv{uq+Xopa|-yCH*_FVB4*a__nCdG9&zdCz;^ z^PYDgj4;9oBaAS@Xo}FB$GYNyb~5*ifP?3ek1!eue*_#d*Zjt0s*y1f_+I9^>U=Sv zKVKDvA&V}+)iU?rK3@!I&sRlZgeJlQ(8LiIfF_Qx05oxUG6MC$t-uXH^C0WE9o~v8*0g8f1M(fp36?I9FXl1@1^W_d>rQE3SnzTlRM~1mP&;Ax47JeiuH#Ys(=stC|`rRo>Bvl?0Ump0MzXKc|f+G@8kft;k(gzA;AXR z0j%=#z1x9LRpdj`#|^a%+ymST*aLWXDKJT0c0vLZfUg61&jH*I>{XW?CmKoso(3v| zc>fLHh&rr<1P%fJ3gZ0=oNcN)8bSb$0ZW3UQHgUk@_WH-ob!J$SOom00xuFYga9l? z9qV1uJ}S{sy-gY-By2EFZ&QKW{e!?eGUc~=pfg-e zrWzT;fvWkP^N5lF+gMk*L9(w0;iKFnT8M4ZG+23TVtFI(gLZCP;V$Bl13T~;jr)~j<5hUakx(b z$5tz{%P_8uDB~7@-MCAG@ytB5MB=Z`RVUQ#gZ+M_fQdW&JKIXUpo2XhgU^bMN80 zSpE(zb$t0!LIblFMo{n~gtds46wRCG|AU-DW&C!FoR`&j8rKr@7IZ{J<}SCLBxUMR{k@$jJe-gC@FUc&-FUU>S<)`?aprx7Vb@#!MQ9} zmvxTd$G6Xa;Lfpp8+S}N!`6M(K9wJFZ4}d=Nl+YV>$X>M_lt$H9WRG#`kI(CA(4bw z&r3Kkd-{L=V+oh@^4%QFQ&cRs*p-&z<)m zSmJ;7n^)$PuyL0t+^36d{N*2oIQ(amkC&D6`AUoT_VobI0ubEr_xs8#v@?5=c#1kB zS;w1yO<>c98LXX=&eJ1fF-3@9^yU|(0?oxamF$$nf!}C1v*c$3~0{pwS?iE%4 zy*HX^mZ}$oIeTCf{Vp{M<=5}5l9b!HTO6sA9*id@S}V}Js)va`WQnJ?Htv?v+`3m( z;eWj=mPnI0?Ts55OH!P)iK}WHP8;W3nes{pI}eI9UmKfNx+qKkVFi83A*F~OYM1+Puos2B|Njkz0=343B)wzVrFS1Fr4mdU6MJ+v_rt90; z$gfAF119&h&8xvD2ftmxYksK|RCV|EMnTfuQB-PY%dgT7(`hwi^>Q}E_&>!n{!hNJ z>_oFqLX>=7G@x+YGyph&DsB6g}S7zsl=nnRgV*`*Q6$ zEW>ANaD2+Y;^%jrgR&z?<{Fk2jn?h0O}neeKV$P~gA|q6$&pz)-7+9Lp#DEuU}MQz zQJ^mGX7X-@r*|ilTUptU0yOM6P{TbF zPVv;g&rs&N*7va;ys>F_z)LfW1X6?gM3NY%^_5MQueUH_d_Ge?D8hkn@2Iv4@T8uy zv!dCsQ<@5R(HaYX&W@8Uzj%!}TC=mFJsMJo8i_7`oajS11 z=Z6Xi$Pv_`wULf(q}!X8N(bL>su0Sq-dagXxzu8B)7(JU4knNE-7lL%!wb_38TCRw zRn=0}pBSs-i&rx+7_{QM3g2XRF~9A0Jip_IaZ+CCVC3`pOr9b7^*gkR;N9obeB%-7 zrV&@z>@EY$!#n;?0UiZFL%mv&yRxq~3-2siT`rVcv|9A;zNZ z7aKJpR>#7r&FS3EDEqGLchx@ANN)P{j?#7TxRonMALonZVm8;d%?&J@-h$Lby~v|X zou0lyTYF!YNd-P-zv>)Kg=b!-0Z{-JuPK)rDjHb0N=5(%nSD~HdyZ7ocjRw;`;$nc z^F;B|#U|6!6_f3zv;N%8mo#N@qyFC^meONoI!V()Pw@ ze5@9|%bGtWQRn3y586rL_IWYE8phS2WBkKkx~DHV-#uH9B}(nx#XUb?&E8O3!wpxN`E*Hf-94+#!H=6P zXf+7lx}jH?i86XtI%i95y!7sA;r@*S%slr%iZ@}F@~-ei$vluFi-d(|#JyKV(L9gMafy%1!vn8U7_mMjjY2DnH zRKrqeG?1Ji?)~mP;yFpyt^?vP-K8c;m1ISgg9YD-YyC-MQY0Z^z9J<-RKlkUtrV4b z7J^*|#cxPW3@n#J&;k$>tz~dOscU%iu1dD-mNw>q-ccmPdftpbmRG~}ePRcU^02?) z3wm}G4^F)GvFM@&XJqc@OGN>6r}5*)a>^^DS}M}0;g0M4Q~k9_ir3MqnN+~-c6j?U zaU%&}%kE0nZ4q7TJ!HPs7pRf~WZ0k>maMM-jVC$j9VWA4aK(;&@=|TJ!RFn*{RU<8 zR6Xe~5jfXeZf5an>2FVcv8;r+n3IfuAemOp3^*L{(>4o_yp$(Q4>~Ojx=bqEazfhu z5);23_WdkFyUYmU%=KgMn*PzunlD{WPkj3nS}imG`o7A)2A+1i3X0YxR{pdo>lqOPYQYLl{}y7 z^MqHQOyik9C)NF3TxO@`;C+;q%QKc3r^9A*$cAMB{PCJtzWjF!$-Tc!I?0TA@}tvN zEPf}G8wS)*huij6(&M&69?u1&C+jILwRt?>@WR6>OnTIFsM_UsA9h_?`F=9USEMEB z>EF%s?;!N<9I4p(fUset2I9nA(!)$iTZ%JpoYYWN-67K$)ds1OQ<2m=u;w&!l%} z<&5kR1`Q)`ievMZj>FBP0WIvqYaM>g-SuqYvl&nvA>FSTP^H8Tt#8A4TIhp z=GLR5i2=Q%ye_A{-%###o$d`+_KYGU#dAG&J8ald!TN0$4(HZTSZpIEN=rtHo?e|J z8PF@ruXVqY4WOz3gb|u(a3e5`3l~jV04k~+!kRi*SSsxvm&2imkj@2bv%}=+r^)$6 z>iO#4!6dsx^MEq-Tr1SVl<(P)IJ}KnsH}ZwmR6`-8YygJ?{xPe7A@?0srSkzEc=3eK zo$rb%GYj}?rM$)%@pP`wonH%w1IE3c&#IrrjU0fIGCRYcI?m&-Lg~c{T zjX%MnRpM4_pxC*;in%gsU#Ep3{mt~~Xu@W5uxxFaAS4b4Jpa~d{xDdY+3Sam<@~Tw zoR%UC8irnBrfYj6mP!YUR+X~tN!$^`fw;iIA8ATBaLFjFVd4>B)Ll%*tfUU4j2*!CIMp z^A{dYk#s&dcPn2Sn=0!ncBip+t4QK>)CW!SFJ0}j%|%Xp1;ewMzVQs zTaWH&H4zd55FM#y;k3-U9m4^=qu4U9Em?hnvv~I&7U%x#tL674G#X?hmiD@+<*I6j z?A-5Zvn#@&amMm1(CpmK$d~_aSr@)G_U=Sh&uL9&hW}AefgmKN0B|NvSoBUyzF1nq zq%kR8&klHl)#i}hVs4?ZE!|dT6UH@2L@u5%CR!^A?r?|Pj-orAA^oG7F);&^QS0}P zV6X@o0RZUQ-bB~-!RNmPsIej&V4)PZ7cG-3(A6TBv(6(GqMQMuWr9BC=!_nwoMNY; z5E4^B7>$Rp05owlSplf5axi~I3F~C6`an@yZfD-|5}$)uLjfUW1DsFX|KbTgS>!z3 z^S}SJR19hJ6}SEMIN#=!q0zwVIjy;}XTvY!f<=?n0BbhakNj0XtK)20z1c~};gHe~ zWi(je{zePdZMBe@VelH; zA1Y|F0-)2vz~1UE(;^HSuI_tbjQmvMBv!A$ULaC*v1aCAO8>$IFnZ?e%&*t(!xAZADXT|XG zKbrd;7auHiT8J}CGnj!h6UilQFMyB$7VET&DX#b-;3g&&MKACQZwdu3(;E8KDq*H|4q^;({6__#Zb zD|(1=gU`NsiVfSPS^X9vw|stqxYMFPA-q z)3v>cU*@-!goUr!Y+=BtBSKlN7W#LKqHB92WfnUt*O$wlZ5Z4ynw7I!3HP(d9%HAQ z02Yap<`B*!YSnhfuDhOpMpN zq>YhL+3|e1Kn&&Hw6lUuJH=dvwa{wel_ye#%pzCyisFU=W^ z%AYpM&9Cw4+Y5_rLQG+eHE`{!$`lYEqf_*JHBZ%(mee>tr)kfn)2oyJafXw}q)HMw zx#FWK&1vr?5rsRnT9`UMoqpXyNBx6}M&QejiK7La;nq{0@c$&QT8NI+vgW^Sxc`sw z!mpjlAu&$JC$Bc=xd)RymPv}&vGMb^3?CZnQ|_VkWIf+Z%j7R!4xBayR2!CeULQ-G zIg@{VbcUV#tFYUlOM4@ak4)s|EZ?GSy0kZO`_RC`qLus&kwy(OCpF{Q2a@ZKa{qd$ zn!;im5e5xuNzUWpgZr7e>&953eApd~Gi&+cpDlRtU={yeSjg6K~qcKqo`~aM4Y(~NeBaAS@2qTQp!2bi{5ZMxFg*#dR0000< KMNUMnLSTXpsv!md literal 0 HcmV?d00001 From 7962a7cb9f313c0aebae2305bf7c259e659ee6ce Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 30 Aug 2020 10:30:20 +0000 Subject: [PATCH 02/98] fix logo url (for now) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ddbc7c..e387447 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- logo + logo

From 669e8944e991b6f2c17ee83221593f1baa66ef82 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 6 Sep 2020 20:20:36 +0000 Subject: [PATCH 03/98] wip: add v2 module placeholders --- license.header | 4 +- makefile | 2 + pycalver1k.svg | 183 ++++++-- pycalver1k2_128.png | Bin 0 -> 6608 bytes requirements/pypi.txt | 1 + scratch.py | 3 - setup.py | 65 +-- src/pycalver/__init__.py | 4 +- src/pycalver/__main__.py | 4 +- src/pycalver/cli.py | 4 +- src/pycalver/config.py | 2 + src/pycalver/lex_id.py | 169 ------- src/pycalver/parse.py | 6 +- src/pycalver/patterns.py | 4 +- src/pycalver/rewrite.py | 65 +-- src/pycalver/vcs.py | 61 ++- src/pycalver/version.py | 8 +- src/pycalver2/__init__.py | 8 + src/pycalver2/cli.py | 53 +++ src/pycalver2/patterns.py | 205 +++++++++ src/pycalver2/rewrite.py | 175 ++++++++ src/pycalver2/version.py | 590 +++++++++++++++++++++++++ test/fixtures/project_d/pyproject.toml | 6 + test/test_config.py | 3 +- test/test_lex_id.py | 67 --- test/test_patterns.py | 15 +- test/test_rewrite.py | 47 +- 27 files changed, 1361 insertions(+), 393 deletions(-) create mode 100644 pycalver1k2_128.png delete mode 100644 scratch.py delete mode 100644 src/pycalver/lex_id.py create mode 100644 src/pycalver2/__init__.py create mode 100644 src/pycalver2/cli.py create mode 100644 src/pycalver2/patterns.py create mode 100644 src/pycalver2/rewrite.py create mode 100644 src/pycalver2/version.py create mode 100644 test/fixtures/project_d/pyproject.toml delete mode 100644 test/test_lex_id.py diff --git a/license.header b/license.header index 6b5623a..cb08b33 100644 --- a/license.header +++ b/license.header @@ -1,9 +1,9 @@ Individual files contain the following tag instead of the full license text. This file is part of the pycalver project - https://gitlab.com/mbarkhau/pycalver + https://github.com/mbarkhau/pycalver - Copyright (c) 2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License + Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License SPDX-License-Identifier: MIT This enables machine processing of license information based on the SPDX diff --git a/makefile b/makefile index 25099b5..61364ab 100644 --- a/makefile +++ b/makefile @@ -61,3 +61,5 @@ test_compat: $(COMPAT_TEST_FILES) ENABLE_BACKTRACE=0 PYTHONPATH="" ENV=$${ENV-dev} \ $${env_py} -m pytest --verbose compat_test/; \ done; + + rm -rf compat_test/ diff --git a/pycalver1k.svg b/pycalver1k.svg index 156e809..b7133c9 100644 --- a/pycalver1k.svg +++ b/pycalver1k.svg @@ -14,8 +14,8 @@ version="1.1" id="svg8" inkscape:version="1.0 (b51213c273, 2020-08-10)" - sodipodi:docname="pycalver_1k.svg" - inkscape:export-filename="C:\Users\ManuelBarkhau\Dropbox\projects\pycalver\pycalver_128.png" + sodipodi:docname="pycalver1k.svg" + inkscape:export-filename="/home/mbarkhau/foss/pycalver/pycalver1k2_128.png" inkscape:export-xdpi="96" inkscape:export-ydpi="96"> + gradientTransform="matrix(1.0541065,0,0,0.87055183,-0.916204,-223.64659)" /> @@ -201,6 +201,59 @@ + + + + + + + + + + + + + - + id="layer6" + inkscape:label="bg" + style="display:inline"> + + + + + style="display:inline"> v20202020 + + + + + + .1001 + id="tspan919" + x="4.7286582" + y="13.507061" + style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16.9333px;font-family:'Iosevka Fixed SS05';-inkscape-font-specification:'Iosevka Fixed SS05 Medium';fill:#ffffff;stroke-width:0.265;stroke-miterlimit:4;stroke-dasharray:none">ver + x="1.5910856" + y="16.622082" /> + + + style="display:none" + sodipodi:insensitive="true"> + x="7.3927369" + y="1.7978847" /> + x="24.886431" + y="1.7978847" /> diff --git a/pycalver1k2_128.png b/pycalver1k2_128.png new file mode 100644 index 0000000000000000000000000000000000000000..30ea018ecdde598d84ba09200b06cc62ff9a6dd1 GIT binary patch literal 6608 zcmZu$bx;&uwB7{?=~@H?7HMHgDM@JwN$GAuU@2kg4(X7R1?iA3X;`|Ek_PDx>0aLc z-aqf3cV}+Qxp&UoneW{1J9o}kH5GY$94Z_D0D!OXUPc2o2mIHuF;Qc;xY;mjdhYaI z&lLb5=>D$*`=P~_C?lnttgf3T)Y{F%%*6`e;o-q)>-gE#!pzBv6Y64w&Kjt_koosc zcRHIItoIQ?`q5D!!0;6(Gh^5@Wje-qZ~L(&i!?FXngK_qQWp&E?CF zTC8Bfo*ii(Qy85y`KR*MiQjfi{CYG*Jp8Cl5Wq=u!H zRZf1s4BP42FaJXj;;+_TU|`^TRn>6inP%6CJ-7GD$|1ex#7s~`0Ptoz11hvOKR=%) z{N*RBUZ(JuuEY7-gm2$MUP*@`W@l4jCRSEVv0#Zq zk}h%(=@dqe>T29{tkbxaCAV zd~3Y3m z%=Lr);d8!6D{YH8H;M&`f9&dID4m_1gHGeX5-QQ3t99o#H!W8x4D%+cMBERv-8I5A;k-Oge8Y z`eT{MMOj$nxgDjX(CW`pIF0tzkRTk4jPKtSzd7HYSt*dcE1Ie%j|!y`^Wx)ol#zLM z`c@x8u3fI(QUObp*Z~n`f_*7-lPe3gq2mMx1P*7Z4yc#lW$*T}X-_RUn zYf;azk;;zaw3F#U(k%#G!Ki6 z$L5qqidCEf|1*GTe>A1^;T;YR4s7Gg-ef3+YUQlR^pTaNCD8^8Gjk+o_u1C)*P59! zy^S*>HGTb|AxqxWAGPDZH>MEQDp{gm7Z&tYV1|a&cUMPBG4h`uOG-+(De!{!!otEx zx?NpeF*X)lkq@5cu>M#Y+2I=06=LkRym1=X0tPL+*1E&L&fXLTS9m>1Nl8U`P8?hk z^fnZJ~5GA z*cb+D{P#gcii%b&W)YNKRYioN7dVeej+*Iu&)2yoSDR8Nofdy8oKUp$?NL{Su<0ed z9Zu02h6ruX>m^r07JJmOT+5{)A@lL(mvO8Juo6%L(=F zaq?uVbJZ5(dv5T5X=xM}DB{}k*U(5TzA-lJKN0{qN$LsCj)%?DU*n%oW>12n5_&qkscV&2ElZd_%iP zu2R}FktZ+BoMg6&2<+id*Qqd+R8fg7u6vL4Tl|n>?6}Hp=XgL^i-^xAkWyFWauX$MtsK`+pZxs;*u4 zmj~f~k9W>pw=*O+jZSN5%^oLX&%yk5WXSu!Pmzg#T0Ef9RQ!>oS;@(=Mr`fL|BBJ_ z3k!2|asplH@@#%*?aq|H+(DAEs0eLw43CWTe0!}awC1`!(%JmwKqm7}ukMrjaJ!g6 zmOt`#Q3@z3{EUx}Z*_Mf5B?8DOeFm_Hbwptj~ApoqC7mraiV7t-7e5d!U-zSMOh`-&LXm^Z7)?)KpE*i#Z!Tv|cy^l$AtQUt{?B`Spxu0J3KfXoMdR zASl)Gy}OV^(PM6Iu6`eKf8P$JjSO$zkfNScab;yHqgad_da`2g^VvN&6{t2y8kJoT zPz#MeTnlw`=Vd(A&6DW)`nFjT>Oz>4o>Nj%4h#(Zd++7dwo<~emxZ`bsrtWc$B&Y z?BtF7YXGT$~VOjmx{P`^^glpcHvK!*n}ysgb-o^GSUoD ziEQk3_1e06>tEte6=^npU~ZD9pCB45`+F2M%5~PL$e_D<^J-Giu71R`nc=O z@Q|R{IjxwAB~^lvX;3fc+jP8L(xRR!KQ>XP1}-^5rxm|y{lH)w{F`f}7krF2w>VlP zITAa;Vu=o{OjX$TEy1o>YmD8*tOVo7(agpiH0FlmcZeM6Ea4Qte&8!#tu*n47fE5b zqDoj~2tU9OBR-2H=<)aVxABs?6Ea6aU;YoFJ> z=Wtt1>mi}V`SUE0nJKfam4wUR5Iq3$(!C=}mONTtE$ryN7v*dd^!;?!X6SUDR*cS~ zQ3A^gur`*qhU52CD%^T4O9Q_oGblr7p4e8O)B&TT*B*@^ycT!3Al9i!A7O!Ae28_s zVPKM;T<3+Ks2L*n`ll(c$O-1tYN>G9q83Y9igv&G(4>oD z4#@QmxS(b*ew2Zkv=KpYA5J0_4J+$h^kvU0C-6jXTrlh|E4-QT<;deWE8oRr6%^_e z)~{C!vJ1I~zG+R-2)@8&w$nGI7%@`+Iy~EZ$;+rq4zv3J;v`RR+QX(R1AHofHY_j` z@LLIz1Vr~L<~FP1^6EW}YXR|Vm)&}B)UzY+oWxu>&l_U@nZE5~{ONq1X}3how$s+o zI1+RzaxkzrNKtmzO5Ti#O!>>M^7VPJFHcnwjwljD@d1+JP*(U0Y2q4lb1Q=m)d%|V zMpI|YO%j>7#cnpt9^gY#{Pm}VX%q?rM~x%`;Ao@-KMHK_*1R81uQXpp?8EZtdC#I@ zXPG$pR!MO|4u6ZfBUz+Zi?+AjyJs&Rv58-~*u!4oCCT5`e|$EK=B+xjG3ddjZYbxd zm*GCUmfX|{A1W2BnrqF-TA7oyEAtjFOV)Uil6E?BpFHOp?bE*YZyxhO-Ck~%!clsz zDk>|bk}%jKHDtZ|HBybuQ8cygP)_t#1%vl&Al%wdNI&k3id`7<@%ABe7udKDol?B| zk*k9YRzTi7j5^7Lv+LKF0mXc)JbW3wG`xcMh1rowj;nP8yyc*d?fB$A)zQ^e`Ghn9 zZS>)wM$)m9G-SP& zuhpyhOC_PpO0$%*M~2lYlsmY%toqlSeJma@2nR9X2*DhMQ_wjHN+g)MY!a_5=a<(z zk;Cm^g?_|liri>la!)OYmLy}@q;w3*Yh5OlsJ0gjWb%X)!3FJ|I7tNx3E$T;X2QbR zsP%oKzN(RcvgK?XY;Pw=(CobcI=1;2K5R^a_~I-{(c5yfKaI! zwhrx-EcaS<_7FJlhLzr2&o#n4KCcSmR1z=h`wJm*4D>Woa@7{k^n%$nXMHr zZlZKW6=@$!MibA`cKA-#_6@;+^E)6lDWi;BED-d^Qo2-C2kh9;pOl}cLO}aG@z}Yz zqL%K_;|#9^wUID%6ySn0&ihQrsj>>6PMz@IAZPPE?9dcHM`?Qs&>sfy@_8@(%>FTl z95a$I&`^Mu8s4Ef3^fp7iGJ5W{cNCpI)dYG0tlhX(QP6>S!6F|*ks++XQZ!NhOn4I zpI~39y??=8%AtEndA%_ zK*q=@;|mnaxy8_)J6$i@`+*lHG6Pce;jdjnN2LfFtMR)Ml-SL4k>}MV9=g+tBM6_1 z!JiPDBQ{Y^yC0`Yt!5ruaONf4WaETtIy&{1?((CV$)8}iR4!+O#sXka@GQB|6aEVP zp)X|!D*&e)!*=KPg>L?4L4Cp8HSVK@Hk8Aq-9AQ+Tz+f`RvACvwT@8OlYnpiqRmEK zoKh)LAJmn#g)2hOxp1qRowR+EOeU?VsBBGso?_BJbF$|?3NHz4^o}@h$8abQt=->` zt!a`4a+2|_BYS11QwRY`{m)i!pK|WUX#zLdovv;BYS`Y;dR0)A*z83G+v`j* z>+r@%%ceTrz*GrHcQliZC;QWyLf~A+Bqf!}Y?AhZh%SJ2nXlOQuAV&a$vM5@OdkKN z`}Fq)5zVm|^MF;-jQ8{R)94gbdLur+uq9{1iW^xJ z)F$d)UcF=I%(E?j5GcEHyB^YFT&8Qap&Y#lGo+@eD3Y~EmxR0-`?D;dk;Y=EB z>*h8|B~e>^ypFC0=Kg;8sY8`O<|%7TwdD|nyx>8VDeO;qfg<`IW2iT{BbB1F97XWT`!ZP-v$FqJz#3VWU zc^LO3b$zO`e)=GR26W@@013M)`ho5g8d@84I^avAX7pKhhqYk(KnGw*jb^NG5Ghu2 zks`s^`9i{X`gxAzLk7PQlc>yH;M4gsKg@&u_C6Ru1TOChR29iHV7X>>eeZOf(+7{ft9`n^L(iyJRkbW- zr7E5GZ-OQT=)`j56$q zUt;_pf4Kd16i*Wgei}B2B_Vm=KohAH8mF18{aqbOCy}ofd6>871s{F|J;Xky>n}Yw zay42U%2hYqo_pIFM1SU6S2ICWj*8fy1;*aq3XPV>%m0YHBx|^y``7U>r1~Wa(MOB= z`Flr*=Xsv`cf*o3@O-YMXJprEK*oZS21<@0<_@H5NsXsnvVq2Ew4`uY9`>C#N(iPH=K<^6_xDu~Jt0V9Pcr-dKzjA@6Gx+= z^09U}^UbTonxjR^Ad!I*kyK=(T~Aj-g{CU&iCD$Pr79lNw-`e8f^~zHHr-D>4}uj@ zimmA)_did2(90dLP!XI?Dq^~rMP&|uA;H=mnB+CmCV~B2Y&w20;QYnQ>dSk(l@C<@ zG<&LU)JCk?sULKpUz+VF)^J+0MwThgcE7!AeK4cs8QVVjd3tby34w#6JN-{X7s5LW zq!`*445GOF3xA9qQ*|kowH4XY2Dtjny_~d^!vAh!GG!F8$$Sfajaw)8F54e&&2n_)Jvp7G)D2aU3eU zY%HJd)fm+yZYTP9O|bOlN547+c85+)lu`XdCeBNAW8w>KN{UXb!Ew=G+;W>?pIbYC8T>_g1aAEr%Xa~l8 zJxpJ1BMs?W*=l6r%n7G5srcIYqBIY~gYHj5pbPqk+Qb*zVQZ-lr;YwnSmB@?*KZ`? zb~mh8`{Tn&JlT&U7=#!WL%KVJ&f`vQm55CqV45g1z%w;Q$hm^nW=wEZKb{@^ z8UOAry$Bn3T?aUf@Sp4nXDpZ`U0fW&QR*El92a(V-7EOnLe0>+ui8y)a!fEgdoJWB z6)sf_L@Ia_n^4_U4a^@#cp+^LZRV21%M}P&64bvN@(81KX1j>cTs!TU_=`rLvWEV^ z?Gtdp0_`E8sX%lmp`HF?+@n_YS^BQzZp_$S3OJ&!M5}Xc#Tg&V8AC%gH6x=Krc%W! zd+8>Y^G#WLyZ!Ge(MRIc^Ef}z7+F|0jvD?V&o9c)x!zn&UZD({mFghVt)XNJaUY?Y z+FIKNSQEzXVgnbmYL;@_%1|<*=1>aoaw+pQmxJ8%0Cl`>Lpy>hK{QqL?8*+WrsNVd z?ulP#z^>9T-IN$0-Lh~&YDOqYm|5haFzNqRHU77};fRI=zt<~x^5^w=LRDh`3bHCP J6;h@_{{w1-v1R}O literal 0 HcmV?d00001 diff --git a/requirements/pypi.txt b/requirements/pypi.txt index 0504771..0bd5192 100644 --- a/requirements/pypi.txt +++ b/requirements/pypi.txt @@ -12,3 +12,4 @@ typing; python_version < "3.5" click toml six +lexid diff --git a/scratch.py b/scratch.py deleted file mode 100644 index cfdbc6d..0000000 --- a/scratch.py +++ /dev/null @@ -1,3 +0,0 @@ -import sys - -print(sys.version) diff --git a/setup.py b/setup.py index 78c823f..8cf0107 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ # This file is part of the pycalver project -# https://gitlab.com/mbarkhau/pycalver +# https://github.com/mbarkhau/pycalver # -# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License # SPDX-License-Identifier: MIT import os @@ -26,10 +26,42 @@ install_requires = [ ] +long_description = "\n\n".join((read("README.md"), read("CHANGELOG.md"))) + + +# See https://pypi.python.org/pypi?%3Aaction=list_classifiers +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Environment :: Other Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: Unix", + "Operating System :: POSIX", + "Operating System :: Microsoft :: Windows", + "Operating System :: MacOS :: MacOS X", + "Programming Language :: Python", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Python Modules", +] + package_dir = {"": "src"} -if any(arg.startswith("bdist") for arg in sys.argv): +is_lib3to6_fix_required = ( + any(arg.startswith("bdist") for arg in sys.argv) + and ( + "Programming Language :: Python :: 2.7" in classifiers + or "Programming Language :: Python :: 2" in classifiers + ) +) + + +if is_lib3to6_fix_required: try: import lib3to6 package_dir = lib3to6.fix(package_dir) @@ -43,9 +75,6 @@ if any(arg.startswith("bdist") for arg in sys.argv): )) -long_description = "\n\n".join((read("README.md"), read("CHANGELOG.md"))) - - setuptools.setup( name="pycalver", license="MIT", @@ -57,32 +86,14 @@ setuptools.setup( description="CalVer for python libraries.", long_description=long_description, long_description_content_type="text/markdown", - packages=['pycalver'], + packages=setuptools.find_packages("src/"), package_dir=package_dir, install_requires=install_requires, entry_points=""" [console_scripts] pycalver=pycalver.cli:cli """, + python_requires=">=2.7", zip_safe=True, - - # See https://pypi.python.org/pypi?%3Aaction=list_classifiers - classifiers=[ - "Development Status :: 4 - Beta", - "Environment :: Console", - "Environment :: Other Environment", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: Unix", - "Operating System :: POSIX", - "Operating System :: Microsoft :: Windows", - "Operating System :: MacOS :: MacOS X", - "Programming Language :: Python", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - "Topic :: Software Development :: Libraries", - "Topic :: Software Development :: Libraries :: Python Modules", - ], + classifiers=classifiers, ) diff --git a/src/pycalver/__init__.py b/src/pycalver/__init__.py index c1658bf..0d7e37b 100644 --- a/src/pycalver/__init__.py +++ b/src/pycalver/__init__.py @@ -1,7 +1,7 @@ # This file is part of the pycalver project -# https://gitlab.com/mbarkhau/pycalver +# https://github.com/mbarkhau/pycalver # -# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License # SPDX-License-Identifier: MIT """PyCalVer: CalVer for Python Packages.""" diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 8e8a3e9..80130c2 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -1,8 +1,8 @@ #!/usr/bin/env python # This file is part of the pycalver project -# https://gitlab.com/mbarkhau/pycalver +# https://github.com/mbarkhau/pycalver # -# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License # SPDX-License-Identifier: MIT """ __main__ module for PyCalVer. diff --git a/src/pycalver/cli.py b/src/pycalver/cli.py index 0316382..47b305a 100755 --- a/src/pycalver/cli.py +++ b/src/pycalver/cli.py @@ -1,8 +1,8 @@ #!/usr/bin/env python # This file is part of the pycalver project -# https://gitlab.com/mbarkhau/pycalver +# https://github.com/mbarkhau/pycalver # -# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License # SPDX-License-Identifier: MIT """ CLI module for PyCalVer. diff --git a/src/pycalver/config.py b/src/pycalver/config.py index a214d0c..e5b41a8 100644 --- a/src/pycalver/config.py +++ b/src/pycalver/config.py @@ -234,6 +234,8 @@ def _parse_config(raw_cfg: RawConfig) -> Config: version_str: str = raw_cfg['current_version'] version_str = raw_cfg['current_version'] = version_str.strip("'\" ") + # TODO (mb 2020-09-06): new style pattern by default + # version_pattern: str = raw_cfg.get('version_pattern', "vYYYY0M.BUILD[-TAG]") version_pattern: str = raw_cfg.get('version_pattern', "{pycalver}") version_pattern = raw_cfg['version_pattern'] = version_pattern.strip("'\" ") diff --git a/src/pycalver/lex_id.py b/src/pycalver/lex_id.py deleted file mode 100644 index 5c6af28..0000000 --- a/src/pycalver/lex_id.py +++ /dev/null @@ -1,169 +0,0 @@ -# This file is part of the pycalver project -# https://gitlab.com/mbarkhau/pycalver -# -# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License -# SPDX-License-Identifier: MIT - -"""A scheme for lexically ordered numerical ids. - -Throughout the sequence this expression remains true, whether you -are dealing with integers or strings: - - older_id < newer_id - -The left most character/digit is only used to maintain lexical -order, so that the position in the sequence is maintained in the -remaining digits. - - sequence_pos = int(idval[1:], 10) - -lexical sequence_pos -0 0 -11 1 -12 2 -... -19 9 -220 20 -221 21 -... -298 98 -299 99 -3300 300 -3301 301 -... -3998 998 -3999 999 -44000 4000 -44001 4001 -... -899999998 99999998 -899999999 99999999 -9900000000 900000000 -9900000001 900000001 -... -9999999998 999999998 -9999999999 999999999 # maximum value - -You can add leading zeros to delay the expansion and/or increase -the maximum possible value. - -lexical sequence_pos -0001 1 -0002 2 -0003 3 -... -0999 999 -11000 1000 -11001 1001 -11002 1002 -... -19998 9998 -19999 9999 -220000 20000 -220001 20001 -... -899999999998 99999999998 -899999999999 99999999999 -9900000000000 900000000000 -9900000000001 900000000001 -... -9999999999998 999999999998 -9999999999999 999999999999 # maximum value - -This scheme is useful when you just want an ordered sequence of -numbers, but the numbers don't have any particular meaning or -arithmetical relation. The only relation they have to each other -is that numbers generated later in the sequence are greater than -ones generated earlier. -""" - - -MINIMUM_ID = "0" - - -def next_id(prev_id: str) -> str: - """Generate next lexical id. - - Increments by one and adds padding if required. - - >>> next_id("0098") - '0099' - >>> next_id("0099") - '0100' - >>> next_id("0999") - '11000' - >>> next_id("11000") - '11001' - """ - - num_digits = len(prev_id) - - if prev_id.count("9") == num_digits: - raise OverflowError("max lexical version reached: " + prev_id) - - _prev_id_val = int(prev_id, 10) - _maybe_next_id_val = int(_prev_id_val) + 1 - _maybe_next_id_str = f"{_maybe_next_id_val:0{num_digits}}" - - _is_padding_ok = prev_id[0] == _maybe_next_id_str[0] - _next_id_str: str - - if _is_padding_ok: - _next_id_str = _maybe_next_id_str - else: - _next_id_str = str(_maybe_next_id_val * 11) - return _next_id_str - - -def ord_val(lex_id: str) -> int: - """Parse the ordinal value of a lexical id. - - The ordinal value is the position in the sequence, - from repeated calls to next_id. - - >>> ord_val("0098") - 98 - >>> ord_val("0099") - 99 - >>> ord_val("0100") - 100 - >>> ord_val("11000") - 1000 - >>> ord_val("11001") - 1001 - """ - if len(lex_id) == 1: - return int(lex_id, 10) - else: - return int(lex_id[1:], 10) - - -def _main() -> None: - _curr_id = "01" - print(f"{'lexical':<13} {'numerical':>12}") - - while True: - print(f"{_curr_id:<13} {ord_val(_curr_id):>12}") - _next_id = next_id(_curr_id) - - if _next_id.count("9") == len(_next_id): - # all nines, we're done - print(f"{_next_id:<13} {ord_val(_next_id):>12}") - break - - if _next_id[0] != _curr_id[0] and len(_curr_id) > 1: - print(f"{_next_id:<13} {ord_val(_next_id):>12}") - _next_id = next_id(_next_id) - print(f"{_next_id:<13} {ord_val(_next_id):>12}") - _next_id = next_id(_next_id) - - print("...") - - # skip ahead - _next_id = _next_id[:1] + "9" * (len(_next_id) - 2) + "8" - - _curr_id = _next_id - - -if __name__ == '__main__': - _main() diff --git a/src/pycalver/parse.py b/src/pycalver/parse.py index 402ad39..aceb631 100644 --- a/src/pycalver/parse.py +++ b/src/pycalver/parse.py @@ -1,7 +1,7 @@ # This file is part of the pycalver project -# https://gitlab.com/mbarkhau/pycalver +# https://github.com/mbarkhau/pycalver # -# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License # SPDX-License-Identifier: MIT """Parse PyCalVer strings from files.""" @@ -22,6 +22,8 @@ class PatternMatch(typ.NamedTuple): PatternMatches = typ.Iterable[PatternMatch] +RegexpPatterns = typ.List[typ.Pattern[str]] + def _iter_for_pattern(lines: typ.List[str], pattern: str) -> PatternMatches: # The pattern is escaped, so that everything besides the format diff --git a/src/pycalver/patterns.py b/src/pycalver/patterns.py index b9b097f..23af859 100644 --- a/src/pycalver/patterns.py +++ b/src/pycalver/patterns.py @@ -1,7 +1,7 @@ # This file is part of the pycalver project -# https://gitlab.com/mbarkhau/pycalver +# https://github.com/mbarkhau/pycalver # -# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License # SPDX-License-Identifier: MIT """Compose Regular Expressions from Patterns. diff --git a/src/pycalver/rewrite.py b/src/pycalver/rewrite.py index 2a566c3..c06fa7d 100644 --- a/src/pycalver/rewrite.py +++ b/src/pycalver/rewrite.py @@ -1,7 +1,7 @@ # This file is part of the pycalver project -# https://gitlab.com/mbarkhau/pycalver +# https://github.com/mbarkhau/pycalver # -# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License # SPDX-License-Identifier: MIT """Rewrite files, updating occurences of version strings.""" @@ -13,8 +13,9 @@ import logging import pathlib2 as pl -from . import parse -from . import config +from pycalver import parse +from pycalver import config + from . import version from . import patterns @@ -53,6 +54,28 @@ class NoPatternMatch(Exception): """ +class RewrittenFileData(typ.NamedTuple): + """Container for line-wise content of rewritten files.""" + + path : str + line_sep : str + old_lines: typ.List[str] + new_lines: typ.List[str] + + +def iter_file_paths( + file_patterns: config.PatternsByGlob, +) -> typ.Iterable[typ.Tuple[pl.Path, config.Patterns]]: + for globstr, pattern_strs in file_patterns.items(): + file_paths = glob.glob(globstr) + if not any(file_paths): + errmsg = f"No files found for path/glob '{globstr}'" + raise IOError(errmsg) + for file_path_str in file_paths: + file_path = pl.Path(file_path_str) + yield (file_path, pattern_strs) + + def rewrite_lines( pattern_strs: typ.List[str], new_vinfo: version.VersionInfo, old_lines: typ.List[str] ) -> typ.List[str]: @@ -88,15 +111,6 @@ def rewrite_lines( return new_lines -class RewrittenFileData(typ.NamedTuple): - """Container for line-wise content of rewritten files.""" - - path : str - line_sep : str - old_lines: typ.List[str] - new_lines: typ.List[str] - - def rfd_from_content( pattern_strs: typ.List[str], new_vinfo: version.VersionInfo, content: str ) -> RewrittenFileData: @@ -122,19 +136,6 @@ def rfd_from_content( return RewrittenFileData("", line_sep, old_lines, new_lines) -def _iter_file_paths( - file_patterns: config.PatternsByGlob, -) -> typ.Iterable[typ.Tuple[pl.Path, config.Patterns]]: - for globstr, pattern_strs in file_patterns.items(): - file_paths = glob.glob(globstr) - if not any(file_paths): - errmsg = f"No files found for path/glob '{globstr}'" - raise IOError(errmsg) - for file_path_str in file_paths: - file_path = pl.Path(file_path_str) - yield (file_path, pattern_strs) - - def iter_rewritten( file_patterns: config.PatternsByGlob, new_vinfo: version.VersionInfo ) -> typ.Iterable[RewrittenFileData]: @@ -144,23 +145,23 @@ def iter_rewritten( >>> new_vinfo = version.parse_version_info("v201809.0123") >>> rewritten_datas = iter_rewritten(file_patterns, new_vinfo) >>> rfd = list(rewritten_datas)[0] - >>> assert rfd.new_lines == [ + >>> expected = [ ... '# This file is part of the pycalver project', - ... '# https://gitlab.com/mbarkhau/pycalver', + ... '# https://github.com/mbarkhau/pycalver', ... '#', - ... '# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License', + ... '# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License', ... '# SPDX-License-Identifier: MIT', ... '"""PyCalVer: CalVer for Python Packages."""', ... '', ... '__version__ = "v201809.0123"', ... '', ... ] - >>> + >>> assert rfd.new_lines[:len(expected)] == expected ''' fobj: typ.IO[str] - for file_path, pattern_strs in _iter_file_paths(file_patterns): + for file_path, pattern_strs in iter_file_paths(file_patterns): with file_path.open(mode="rt", encoding="utf-8") as fobj: content = fobj.read() @@ -204,7 +205,7 @@ def diff(new_vinfo: version.VersionInfo, file_patterns: config.PatternsByGlob) - full_diff = "" fobj: typ.IO[str] - for file_path, pattern_strs in sorted(_iter_file_paths(file_patterns)): + for file_path, pattern_strs in sorted(iter_file_paths(file_patterns)): with file_path.open(mode="rt", encoding="utf-8") as fobj: content = fobj.read() diff --git a/src/pycalver/vcs.py b/src/pycalver/vcs.py index 3430ba2..3ca6102 100644 --- a/src/pycalver/vcs.py +++ b/src/pycalver/vcs.py @@ -1,7 +1,7 @@ # This file is part of the pycalver project -# https://gitlab.com/mbarkhau/pycalver +# https://github.com/mbarkhau/pycalver # -# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License # SPDX-License-Identifier: MIT # # pycalver/vcs.py (this file) is based on code from the @@ -15,11 +15,14 @@ mercurial, then the git terms are used. For example "fetch" """ import os +import sys import typing as typ import logging import tempfile import subprocess as sp +from pycalver import config + logger = logging.getLogger("pycalver.vcs") @@ -179,3 +182,57 @@ def get_vcs_api() -> VCSAPI: return vcs_api raise OSError("No such directory .git/ or .hg/ ") + + +# cli helper methods + + +def assert_not_dirty(vcs_api: VCSAPI, filepaths: typ.Set[str], allow_dirty: bool) -> None: + dirty_files = vcs_api.status(required_files=filepaths) + + if dirty_files: + logger.warning(f"{vcs_api.name} working directory is not clean. Uncomitted file(s):") + for dirty_file in dirty_files: + logger.warning(" " + dirty_file) + + if not allow_dirty and dirty_files: + sys.exit(1) + + dirty_pattern_files = set(dirty_files) & filepaths + if dirty_pattern_files: + logger.error("Not commiting when pattern files are dirty:") + for dirty_file in dirty_pattern_files: + logger.warning(" " + dirty_file) + sys.exit(1) + + +def commit( + cfg : config.Config, + vcs_api : VCSAPI, + filepaths : typ.Set[str], + new_version : str, + commit_message: str, +) -> None: + for filepath in filepaths: + vcs_api.add(filepath) + + vcs_api.commit(commit_message) + + if cfg.commit and cfg.tag: + vcs_api.tag(new_version) + + if cfg.commit and cfg.tag and cfg.push: + vcs_api.push(new_version) + + +def get_tags(fetch: bool) -> typ.List[str]: + try: + vcs_api = get_vcs_api() + logger.debug(f"vcs found: {vcs_api.name}") + if fetch: + logger.info("fetching tags from remote (to turn off use: -n / --no-fetch)") + vcs_api.fetch() + return vcs_api.ls_tags() + except OSError: + logger.debug("No vcs found") + return [] diff --git a/src/pycalver/version.py b/src/pycalver/version.py index ab2bd2c..25afe53 100644 --- a/src/pycalver/version.py +++ b/src/pycalver/version.py @@ -1,7 +1,7 @@ # This file is part of the pycalver project -# https://gitlab.com/mbarkhau/pycalver +# https://github.com/mbarkhau/pycalver # -# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License # SPDX-License-Identifier: MIT """Functions related to version string manipulation.""" @@ -9,9 +9,9 @@ import typing as typ import logging import datetime as dt +import lexid import pkg_resources -from . import lex_id from . import patterns logger = logging.getLogger("pycalver.version") @@ -482,7 +482,7 @@ def incr( else: logger.warning(f"Version appears to be from the future '{old_version}'") - cur_vinfo = cur_vinfo._replace(bid=lex_id.next_id(cur_vinfo.bid)) + cur_vinfo = cur_vinfo._replace(bid=lexid.next_id(cur_vinfo.bid)) if major: cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0, patch=0) diff --git a/src/pycalver2/__init__.py b/src/pycalver2/__init__.py new file mode 100644 index 0000000..094d463 --- /dev/null +++ b/src/pycalver2/__init__.py @@ -0,0 +1,8 @@ +# This file is part of the pycalver project +# https://github.com/mbarkhau/pycalver +# +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# SPDX-License-Identifier: MIT +"""PyCalVer: CalVer for Python Packages.""" + +__version__ = "v202007.1036" diff --git a/src/pycalver2/cli.py b/src/pycalver2/cli.py new file mode 100644 index 0000000..107d4b8 --- /dev/null +++ b/src/pycalver2/cli.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +# This file is part of the pycalver project +# https://github.com/mbarkhau/pycalver +# +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# SPDX-License-Identifier: MIT +""" +CLI module for PyCalVer. + +Provided subcommands: show, test, init, bump +""" +import typing as typ +import logging + +import pycalver2.rewrite as v2rewrite +import pycalver2.version as v2version +from pycalver import config + +logger = logging.getLogger("pycalver2.cli") + + +def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.Config: + version_tags = [tag for tag in all_tags if v2version.is_valid(tag, cfg.version_pattern)] + if not version_tags: + logger.debug("no vcs tags found") + return cfg + + version_tags.sort(reverse=True) + logger.debug(f"found {len(version_tags)} tags: {version_tags[:2]}") + latest_version_tag = version_tags[0] + latest_version_pep440 = v2version.to_pep440(latest_version_tag) + if latest_version_tag <= cfg.current_version: + return cfg + + logger.info(f"Working dir version : {cfg.current_version}") + logger.info(f"Latest version from VCS tag: {latest_version_tag}") + return cfg._replace( + current_version=latest_version_tag, + pep440_version=latest_version_pep440, + ) + + +def rewrite( + cfg : config.Config, + new_version: str, +) -> None: + new_vinfo = v2version.parse_version_info(new_version, cfg.version_pattern) + v2rewrite.rewrite(cfg.file_patterns, new_vinfo) + + +def get_diff(cfg: config.Config, new_version: str) -> str: + new_vinfo = v2version.parse_version_info(new_version, cfg.version_pattern) + return v2rewrite.diff(new_vinfo, cfg.file_patterns) diff --git a/src/pycalver2/patterns.py b/src/pycalver2/patterns.py new file mode 100644 index 0000000..07bae57 --- /dev/null +++ b/src/pycalver2/patterns.py @@ -0,0 +1,205 @@ +# This file is part of the pycalver project +# https://github.com/mbarkhau/pycalver +# +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# SPDX-License-Identifier: MIT +"""Compose Regular Expressions from Patterns. + +>>> version_info = PYCALVER_RE.match("v201712.0123-alpha").groupdict() +>>> assert version_info == { +... "pycalver" : "v201712.0123-alpha", +... "vYYYYMM" : "v201712", +... "year" : "2017", +... "month" : "12", +... "build" : ".0123", +... "build_no" : "0123", +... "release" : "-alpha", +... "release_tag" : "alpha", +... } +>>> +>>> version_info = PYCALVER_RE.match("v201712.0033").groupdict() +>>> assert version_info == { +... "pycalver" : "v201712.0033", +... "vYYYYMM" : "v201712", +... "year" : "2017", +... "month" : "12", +... "build" : ".0033", +... "build_no" : "0033", +... "release" : None, +... "release_tag": None, +... } +""" + +import re +import typing as typ + +# https://regex101.com/r/fnj60p/10 +PYCALVER_PATTERN = r""" +\b +(?P + (?P + v # "v" version prefix + (?P\d{4}) + (?P\d{2}) + ) + (?P + \. # "." build nr prefix + (?P\d{4,}) + ) + (?P + \- # "-" release prefix + (?Palpha|beta|dev|rc|post) + )? +)(?:\s|$) +""" + +PYCALVER_RE: typ.Pattern[str] = re.compile(PYCALVER_PATTERN, flags=re.VERBOSE) + + +PATTERN_ESCAPES = [ + ("\u005c", "\u005c\u005c"), + ("-" , "\u005c-"), + ("." , "\u005c."), + ("+" , "\u005c+"), + ("*" , "\u005c*"), + ("?" , "\u005c?"), + ("{" , "\u005c{"), + ("}" , "\u005c}"), + ("[" , "\u005c["), + ("]" , "\u005c]"), + ("(" , "\u005c("), + (")" , "\u005c)"), +] + +# NOTE (mb 2020-09-04): These are depricated in favour of explicit patterns +COMPOSITE_PART_PATTERNS = { + 'pep440_pycalver': r"{year}{month}\.{BID}(?:{pep440_tag})?", + 'pycalver' : r"v{year}{month}\.{bid}(?:-{tag})?", + 'calver' : r"v{year}{month}", + 'semver' : r"{MAJOR}\.{MINOR}\.{PATCH}", + 'release_tag' : r"{tag}", + 'build' : r"\.{bid}", + 'release' : r"(?:-{tag})?", + # depricated + 'pep440_version': r"{year}{month}\.{BID}(?:{pep440_tag})?", +} + + +PART_PATTERNS = { + # recommended (based on calver.org) + 'YYYY': r"[1-9]\d{3}", + 'YY' : r"\d{1,2}", + '0Y' : r"\d{2}", + 'Q' : r"[1-4]", + 'MM' : r"(?:[1-9]|1[0-2])", + '0M' : r"(?:0[1-9]|1[0-2])", + 'DD' : r"([1-9]|[1-2][0-9]|3[0-1])", + '0D' : r"(0[1-9]|[1-2][0-9]|3[0-1])", + 'JJJ' : r"(?:[1-9]\d|[1-9]|[1-2]\d\d|3[0-5][0-9]|36[0-6])", + '00J' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])", + 'WW' : r"(?:[1-9]|[1-4]\d|5[0-2])", + '0W' : r"(?:[0-4]\d|5[0-2])", + 'UU' : r"(?:[1-9]|[0-4]\d|5[0-2])", + '0U' : r"(?:[0-4]\d|5[0-2])", + 'VV' : r"(?:[1-9]|[1-4]\d|5[0-3])", + '0V' : r"(?:[0-4]\d|5[0-3])", + 'GGGG': r"[1-9]\d{3}", + 'GG' : r"\d{1,2}", + '0G' : r"\d{2}", + # non calver parts + 'MAJOR': r"\d+", + 'MINOR': r"\d+", + 'PATCH': r"\d+", + 'MICRO': r"\d+", + 'BUILD': r"\d+", + 'TAG' : r"(?:alpha|beta|dev|rc|post|final)", + 'PYTAG': r"(?:a|b|dev|rc|post)?\d*", + # supported (but legacy) + 'year' : r"\d{4}", + 'month' : r"(?:0[0-9]|1[0-2])", + 'month_short': r"(?:1[0-2]|[1-9])", + 'build_no' : r"\d{4,}", + 'pep440_tag' : r"(?:a|b|dev|rc|post)?\d*", + 'tag' : r"(?:alpha|beta|dev|rc|post|final)", + 'yy' : r"\d{2}", + 'yyyy' : r"\d{4}", + 'quarter' : r"[1-4]", + 'iso_week' : r"(?:[0-4]\d|5[0-3])", + 'us_week' : r"(?:[0-4]\d|5[0-3])", + 'dom' : r"(0[1-9]|[1-2][0-9]|3[0-1])", + 'dom_short' : r"([1-9]|[1-2][0-9]|3[0-1])", + 'doy' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])", + 'doy_short' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])", + 'bid' : r"\d{4,}", + # dropped support (never documented) + # 'BID' : r"[1-9]\d*", + # 'MM' : r"\d{2,}", + # 'MMM' : r"\d{3,}", + # 'MMMM' : r"\d{4,}", + # 'MMMMM' : r"\d{5,}", + # 'PP' : r"\d{2,}", + # 'PPP' : r"\d{3,}", + # 'PPPP' : r"\d{4,}", + # 'PPPPP' : r"\d{5,}", + # 'BB' : r"[1-9]\d{1,}", + # 'BBB' : r"[1-9]\d{2,}", + # 'BBBB' : r"[1-9]\d{3,}", + # 'BBBBB' : r"[1-9]\d{4,}", + # 'BBBBBB' : r"[1-9]\d{5,}", + # 'BBBBBBB' : r"[1-9]\d{6,}", +} + + +FULL_PART_FORMATS = { + 'pep440_pycalver': "{year}{month:02}.{BID}{pep440_tag}", + 'pycalver' : "v{year}{month:02}.{bid}{release}", + 'calver' : "v{year}{month:02}", + 'semver' : "{MAJOR}.{MINOR}.{PATCH}", + 'release_tag' : "{tag}", + 'build' : ".{bid}", + # NOTE (mb 2019-01-04): since release is optional, it + # is treated specially in version.format + # 'release' : "-{tag}", + 'month' : "{month:02}", + 'month_short': "{month}", + 'build_no' : "{bid}", + 'iso_week' : "{iso_week:02}", + 'us_week' : "{us_week:02}", + 'dom' : "{dom:02}", + 'doy' : "{doy:03}", + 'dom_short' : "{dom}", + 'doy_short' : "{doy}", + # depricated + 'pep440_version': "{year}{month:02}.{BID}{pep440_tag}", + 'version' : "v{year}{month:02}.{bid}{release}", +} + + +def _replace_pattern_parts(pattern: str) -> str: + for part_name, part_pattern in PART_PATTERNS.items(): + named_part_pattern = f"(?P<{part_name}>{part_pattern})" + placeholder = "\u005c{" + part_name + "\u005c}" + pattern = pattern.replace(placeholder, named_part_pattern) + return pattern + + +def compile_pattern_str(pattern: str) -> str: + for char, escaped in PATTERN_ESCAPES: + pattern = pattern.replace(char, escaped) + + return _replace_pattern_parts(pattern) + + +def compile_pattern(pattern: str) -> typ.Pattern[str]: + pattern_str = compile_pattern_str(pattern) + return re.compile(pattern_str) + + +def _init_composite_patterns() -> None: + for part_name, part_pattern in COMPOSITE_PART_PATTERNS.items(): + part_pattern = part_pattern.replace("{", "\u005c{").replace("}", "\u005c}") + pattern_str = _replace_pattern_parts(part_pattern) + PART_PATTERNS[part_name] = pattern_str + + +_init_composite_patterns() diff --git a/src/pycalver2/rewrite.py b/src/pycalver2/rewrite.py new file mode 100644 index 0000000..7595099 --- /dev/null +++ b/src/pycalver2/rewrite.py @@ -0,0 +1,175 @@ +# This file is part of the pycalver project +# https://github.com/mbarkhau/pycalver +# +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# SPDX-License-Identifier: MIT +"""Rewrite files, updating occurences of version strings.""" + +import io +import typing as typ +import logging + +from pycalver import parse +from pycalver import config +from pycalver import rewrite as v1rewrite +from pycalver2 import version +from pycalver2 import patterns + +logger = logging.getLogger("pycalver2.rewrite") + + +def rewrite_lines( + pattern_strs: typ.List[str], new_vinfo: version.VersionInfo, old_lines: typ.List[str] +) -> typ.List[str]: + """TODO reenable doctest""" + pass + + """Replace occurances of pattern_strs in old_lines with new_vinfo. + + >>> new_vinfo = version.parse_version_info("v201811.0123-beta") + >>> pattern_strs = ['__version__ = "{pycalver}"'] + >>> rewrite_lines(pattern_strs, new_vinfo, ['__version__ = "v201809.0002-beta"']) + ['__version__ = "v201811.0123-beta"'] + + >>> pattern_strs = ['__version__ = "{pep440_version}"'] + >>> rewrite_lines(pattern_strs, new_vinfo, ['__version__ = "201809.2b0"']) + ['__version__ = "201811.123b0"'] + """ + new_lines = old_lines[:] + found_patterns = set() + + re_patterns = [patterns.compile_pattern(p) for p in pattern_strs] + for match in parse.iter_matches(old_lines, re_patterns): + found_patterns.add(match.pattern) + replacement = version.format_version(new_vinfo, match.pattern) + span_l, span_r = match.span + new_line = match.line[:span_l] + replacement + match.line[span_r:] + new_lines[match.lineno] = new_line + + non_matched_patterns = set(pattern_strs) - found_patterns + if non_matched_patterns: + for non_matched_pattern in non_matched_patterns: + logger.error(f"No match for pattern '{non_matched_pattern}'") + compiled_pattern_str = patterns.compile_pattern_str(non_matched_pattern) + logger.error(f"Pattern compiles to regex '{compiled_pattern_str}'") + raise v1rewrite.NoPatternMatch("Invalid pattern(s)") + else: + return new_lines + + +def rfd_from_content( + pattern_strs: typ.List[str], new_vinfo: version.VersionInfo, content: str +) -> v1rewrite.RewrittenFileData: + """TODO reenable doctest""" + pass + + r"""Rewrite pattern occurrences with version string. + + >>> new_vinfo = version.parse_version_info("v201809.0123") + >>> pattern_strs = ['__version__ = "{pycalver}"'] + >>> content = '__version__ = "v201809.0001-alpha"' + >>> rfd = rfd_from_content(pattern_strs, new_vinfo, content) + >>> rfd.new_lines + ['__version__ = "v201809.0123"'] + >>> + >>> new_vinfo = version.parse_version_info("v1.2.3", "v{semver}") + >>> pattern_strs = ['__version__ = "v{semver}"'] + >>> content = '__version__ = "v1.2.2"' + >>> rfd = rfd_from_content(pattern_strs, new_vinfo, content) + >>> rfd.new_lines + ['__version__ = "v1.2.3"'] + """ + line_sep = v1rewrite.detect_line_sep(content) + old_lines = content.split(line_sep) + new_lines = rewrite_lines(pattern_strs, new_vinfo, old_lines) + return v1rewrite.RewrittenFileData("", line_sep, old_lines, new_lines) + + +def iter_rewritten( + file_patterns: config.PatternsByGlob, new_vinfo: version.VersionInfo +) -> typ.Iterable[v1rewrite.RewrittenFileData]: + """TODO reenable doctest""" + pass + + r'''Iterate over files with version string replaced. + + >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']} + >>> new_vinfo = version.parse_version_info("v201809.0123") + >>> rewritten_datas = iter_rewritten(file_patterns, new_vinfo) + >>> rfd = list(rewritten_datas)[0] + >>> assert rfd.new_lines == [ + ... '# This file is part of the pycalver project', + ... '# https://gitlab.com/mbarkhau/pycalver', + ... '#', + ... '# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License', + ... '# SPDX-License-Identifier: MIT', + ... '"""PyCalVer: CalVer for Python Packages."""', + ... '', + ... '__version__ = "v201809.0123"', + ... '', + ... ] + >>> + ''' + + fobj: typ.IO[str] + + for file_path, pattern_strs in v1rewrite.iter_file_paths(file_patterns): + with file_path.open(mode="rt", encoding="utf-8") as fobj: + content = fobj.read() + + rfd = rfd_from_content(pattern_strs, new_vinfo, content) + yield rfd._replace(path=str(file_path)) + + +def diff(new_vinfo: version.VersionInfo, file_patterns: config.PatternsByGlob) -> str: + """TODO reenable doctest""" + pass + + r"""Generate diffs of rewritten files. + + >>> new_vinfo = version.parse_version_info("v201809.0123") + >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']} + >>> diff_str = diff(new_vinfo, file_patterns) + >>> lines = diff_str.split("\n") + >>> lines[:2] + ['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py'] + >>> assert lines[6].startswith('-__version__ = "v2') + >>> assert not lines[6].startswith('-__version__ = "v201809.0123"') + >>> lines[7] + '+__version__ = "v201809.0123"' + """ + + full_diff = "" + fobj: typ.IO[str] + + for file_path, pattern_strs in sorted(v1rewrite.iter_file_paths(file_patterns)): + with file_path.open(mode="rt", encoding="utf-8") as fobj: + content = fobj.read() + + try: + rfd = rfd_from_content(pattern_strs, new_vinfo, content) + except v1rewrite.NoPatternMatch: + # pylint:disable=raise-missing-from ; we support py2, so not an option + errmsg = f"No patterns matched for '{file_path}'" + raise v1rewrite.NoPatternMatch(errmsg) + + rfd = rfd._replace(path=str(file_path)) + lines = v1rewrite.diff_lines(rfd) + if len(lines) == 0: + errmsg = f"No patterns matched for '{file_path}'" + raise v1rewrite.NoPatternMatch(errmsg) + + full_diff += "\n".join(lines) + "\n" + + full_diff = full_diff.rstrip("\n") + return full_diff + + +def rewrite(file_patterns: config.PatternsByGlob, new_vinfo: version.VersionInfo) -> None: + """Rewrite project files, updating each with the new version.""" + fobj: typ.IO[str] + + for file_data in iter_rewritten(file_patterns, new_vinfo): + new_content = file_data.line_sep.join(file_data.new_lines) + with io.open(file_data.path, mode="wt", encoding="utf-8") as fobj: + fobj.write(new_content) diff --git a/src/pycalver2/version.py b/src/pycalver2/version.py new file mode 100644 index 0000000..4abaacf --- /dev/null +++ b/src/pycalver2/version.py @@ -0,0 +1,590 @@ +# This file is part of the pycalver project +# https://github.com/mbarkhau/pycalver +# +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# SPDX-License-Identifier: MIT +"""Functions related to version string manipulation.""" + +import typing as typ +import logging +import datetime as dt + +import lexid +import pkg_resources + +from . import patterns + +logger = logging.getLogger("pycalver.version") + + +# The test suite may replace this. +TODAY = dt.datetime.utcnow().date() + + +PATTERN_PART_FIELDS = { + 'YYYY' : 'year_y', + 'YY' : 'year_y', + '0Y' : 'year_y', + 'Q' : 'quarter', + 'MM' : 'month', + '0M' : 'month', + 'DD' : 'dom', + '0D' : 'dom', + 'JJJ' : 'doy', + '00J' : 'doy', + 'MAJOR': 'major', + 'MINOR': 'minor', + 'PATCH': 'patch', + 'MICRO': 'patch', + 'BUILD': 'bid', + 'TAG' : 'tag', + 'PYTAG': 'pytag', + 'WW' : 'week_w', + '0W' : 'week_w', + 'UU' : 'week_u', + '0U' : 'week_u', + 'VV' : 'week_v', + '0V' : 'week_v', + 'GGGG' : 'year_g', + 'GG' : 'year_g', + '0G' : 'year_g', +} + +ID_FIELDS_BY_PART = { + 'MAJOR': 'major', + 'MINOR': 'minor', + 'PATCH': 'patch', + 'MICRO': 'patch', +} + + +ZERO_VALUES = { + 'major': "0", + 'minor': "0", + 'patch': "0", + 'TAG' : "final", + 'PYTAG': "", +} + + +class CalendarInfo(typ.NamedTuple): + """Container for calendar components of version strings.""" + + year_y : int + year_g : int + quarter: int + month : int + dom : int + doy : int + week_w : int + week_u : int + week_v : int + + +def _date_from_doy(year: int, doy: int) -> dt.date: + """Parse date from year and day of year (1 indexed). + + >>> cases = [ + ... (2016, 1), (2016, 31), (2016, 31 + 1), (2016, 31 + 29), (2016, 31 + 30), + ... (2017, 1), (2017, 31), (2017, 31 + 1), (2017, 31 + 28), (2017, 31 + 29), + ... ] + >>> dates = [_date_from_doy(year, month) for year, month in cases] + >>> assert [(d.month, d.day) for d in dates] == [ + ... (1, 1), (1, 31), (2, 1), (2, 29), (3, 1), + ... (1, 1), (1, 31), (2, 1), (2, 28), (3, 1), + ... ] + """ + return dt.date(year, 1, 1) + dt.timedelta(days=doy - 1) + + +def _quarter_from_month(month: int) -> int: + """Calculate quarter (1 indexed) from month (1 indexed). + + >>> [_quarter_from_month(month) for month in range(1, 13)] + [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4] + """ + return ((month - 1) // 3) + 1 + + +def cal_info(date: dt.date = None) -> CalendarInfo: + """TODO reenable doctest""" + pass + + """Generate calendar components for current date. + + >>> from datetime import date + + >>> c = cal_info(date(2019, 1, 5)) + >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) + (2019, 1, 1, 5, 5, 0, 0) + + >>> c = cal_info(date(2019, 1, 6)) + >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) + (2019, 1, 1, 6, 6, 0, 1) + + >>> c = cal_info(date(2019, 1, 7)) + >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) + (2019, 1, 1, 7, 7, 1, 1) + + >>> c = cal_info(date(2019, 4, 7)) + >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) + (2019, 2, 4, 7, 97, 13, 14) + """ + if date is None: + date = TODAY + + kwargs = { + 'year_y' : date.year, + 'year_g' : int(date.strftime("%G"), base=10), + 'quarter': _quarter_from_month(date.month), + 'month' : date.month, + 'dom' : date.day, + 'doy' : int(date.strftime("%j"), base=10), + 'week_w' : int(date.strftime("%W"), base=10), + 'week_u' : int(date.strftime("%U"), base=10), + 'week_v' : int(date.strftime("%V"), base=10), + } + + return CalendarInfo(**kwargs) + + +class VersionInfo(typ.NamedTuple): + """Container for parsed version string.""" + + year_y : typ.Optional[int] + year_g : typ.Optional[int] + quarter: typ.Optional[int] + month : typ.Optional[int] + dom : typ.Optional[int] + doy : typ.Optional[int] + week_w : typ.Optional[int] + week_u : typ.Optional[int] + week_v : typ.Optional[int] + major : int + minor : int + patch : int + bid : str + tag : str + pytag : str + + +FieldKey = str +MatchGroupKey = str +MatchGroupStr = str + +PatternGroups = typ.Dict[MatchGroupKey, MatchGroupStr] +FieldValues = typ.Dict[FieldKey , MatchGroupStr] + + +def _parse_field_values(field_values: FieldValues) -> VersionInfo: + fvals = field_values + tag = fvals.get('tag') + if tag is None: + tag = "final" + + tag = TAG_ALIASES.get(tag, tag) + assert tag is not None + # TODO (mb 2020-09-06): parts of course + pytag = "TODO" + + bid = fvals['bid'] if 'bid' in fvals else "1001" + + year_y = int(fvals['year_y']) if 'year_y' in fvals else None + year_g = int(fvals['year_g']) if 'year_g' in fvals else None + doy = int(fvals['doy' ]) if 'doy' in fvals else None + + date: typ.Optional[dt.date] = None + + month: typ.Optional[int] = None + dom : typ.Optional[int] = None + + week_w: typ.Optional[int] = None + week_u: typ.Optional[int] = None + week_v: typ.Optional[int] = None + + if year_y and doy: + date = _date_from_doy(year_y, doy) + month = date.month + dom = date.day + else: + month = int(fvals['month']) if 'month' in fvals else None + dom = int(fvals['dom' ]) if 'dom' in fvals else None + + quarter = int(fvals['quarter']) if 'quarter' in fvals else None + if quarter is None and month: + quarter = _quarter_from_month(month) + + if year_y and month and dom: + date = dt.date(year_y, month, dom) + + if date: + # derive all fields from other previous values + doy = int(date.strftime("%j"), base=10) + week_w = int(date.strftime("%W"), base=10) + week_u = int(date.strftime("%U"), base=10) + week_v = int(date.strftime("%V"), base=10) + year_g = int(date.strftime("%G"), base=10) + + major = int(fvals['major']) if 'major' in fvals else 0 + minor = int(fvals['minor']) if 'minor' in fvals else 0 + patch = int(fvals['patch']) if 'patch' in fvals else 0 + + return VersionInfo( + year_y=year_y, + year_g=year_g, + quarter=quarter, + month=month, + dom=dom, + doy=doy, + week_w=week_w, + week_u=week_u, + week_v=week_v, + major=major, + minor=minor, + patch=patch, + bid=bid, + tag=tag, + pytag=pytag, + ) + + +def _is_calver(nfo: typ.Union[CalendarInfo, VersionInfo]) -> bool: + """TODO reenable doctest""" + pass + + """Check pattern for any calendar based parts. + + >>> _is_calver(cal_info()) + True + + >>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0018"}) + >>> _is_calver(vnfo) + True + + >>> vnfo = _parse_version_info({'MAJOR': "1", 'MINOR': "023", 'PATCH': "45"}) + >>> _is_calver(vnfo) + False + """ + for field in CalendarInfo._fields: + maybe_val: typ.Any = getattr(nfo, field, None) + if isinstance(maybe_val, int): + return True + + return False + + +TAG_ALIASES: typ.Dict[str, str] = { + 'a' : "alpha", + 'b' : "beta", + 'pre': "rc", +} + + +PEP440_TAGS: typ.Dict[str, str] = { + 'alpha': "a", + 'beta' : "b", + 'final': "", + 'rc' : "rc", + 'dev' : "dev", + 'post' : "post", +} + + +VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]] + + +class PatternError(Exception): + pass + + +def _parse_pattern_groups(pattern_groups: PatternGroups) -> FieldValues: + for part_name in pattern_groups.keys(): + is_valid_part_name = ( + part_name in patterns.COMPOSITE_PART_PATTERNS or part_name in PATTERN_PART_FIELDS + ) + if not is_valid_part_name: + err_msg = f"Invalid part '{part_name}'" + raise PatternError(err_msg) + + field_value_items = [ + (field_name, pattern_groups[part_name]) + for part_name, field_name in PATTERN_PART_FIELDS.items() + if part_name in pattern_groups.keys() + ] + + all_fields = [field_name for field_name, _ in field_value_items] + unique_fields = set(all_fields) + duplicate_fields = [f for f in unique_fields if all_fields.count(f) > 1] + + if any(duplicate_fields): + err_msg = f"Multiple parts for same field {duplicate_fields}." + raise PatternError(err_msg) + + return dict(field_value_items) + + +def _parse_version_info(pattern_groups: PatternGroups) -> VersionInfo: + """TODO reenable doctest""" + pass + + """Parse normalized VersionInfo from groups of a matched pattern. + + >>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0099"}) + >>> (vnfo.year_y, vnfo.month, vnfo.quarter, vnfo.bid, vnfo.tag) + (2018, 11, 4, '0099', 'final') + + >>> vnfo = _parse_version_info({'year': "2018", 'doy': "11", 'bid': "099", 'tag': "b"}) + >>> (vnfo.year_y, vnfo.month, vnfo.dom, vnfo.bid, vnfo.tag) + (2018, 1, 11, '099', 'beta') + + >>> vnfo = _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "45"}) + >>> (vnfo.major, vnfo.minor, vnfo.patch) + (1, 23, 45) + + >>> vnfo = _parse_version_info({'MAJOR': "1", 'MMM': "023", 'PPPP': "0045"}) + >>> (vnfo.major, vnfo.minor, vnfo.patch) + (1, 23, 45) + """ + field_values = _parse_pattern_groups(pattern_groups) + return _parse_field_values(field_values) + + +def parse_version_info(version_str: str, pattern: str = "{pycalver}") -> VersionInfo: + """TODO reenable doctest""" + pass + + """Parse normalized VersionInfo. + + >>> vnfo = parse_version_info("v201712.0033-beta", pattern="{pycalver}") + >>> assert vnfo == _parse_version_info({'year': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"}) + + >>> vnfo = parse_version_info("1.23.456", pattern="{semver}") + >>> assert vnfo == _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "456"}) + """ + regex = patterns.compile_pattern(pattern) + match = regex.match(version_str) + if match is None: + err_msg = ( + f"Invalid version string '{version_str}' for pattern '{pattern}'/'{regex.pattern}'" + ) + raise PatternError(err_msg) + + return _parse_version_info(match.groupdict()) + + +def is_valid(version_str: str, pattern: str = "{pycalver}") -> bool: + """TODO reenable doctest""" + pass + + """Check if a version matches a pattern. + + >>> is_valid("v201712.0033-beta", pattern="{pycalver}") + True + >>> is_valid("v201712.0033-beta", pattern="{semver}") + False + >>> is_valid("1.2.3", pattern="{semver}") + True + >>> is_valid("v201712.0033-beta", pattern="{semver}") + False + """ + try: + parse_version_info(version_str, pattern) + return True + except PatternError: + return False + + +TemplateKwargs = typ.Dict[str, typ.Union[str, int, None]] + + +def _derive_template_kwargs(vinfo: VersionInfo) -> TemplateKwargs: + """Generate kwargs for template from minimal VersionInfo. + + The VersionInfo Tuple only has the minimal representation + of a parsed version, not the values suitable for formatting. + It may for example have month=9, but not the formatted + representation '09' for '0M'. + """ + kwargs: TemplateKwargs = vinfo._asdict() + + tag = vinfo.tag + kwargs['TAG'] = tag + if tag == 'final': + kwargs['PYTAG'] = "" + else: + kwargs['PYTAG'] = PEP440_TAGS[tag] + "0" + + year_y = vinfo.year_y + if year_y: + kwargs['0Y' ] = str(year_y)[-2:] + kwargs['YY' ] = int(str(year_y)[-2:]) + kwargs['YYYY'] = year_y + + year_g = vinfo.year_g + if year_g: + kwargs['0G' ] = str(year_g)[-2:] + kwargs['GG' ] = int(str(year_g)[-2:]) + kwargs['GGGG'] = year_g + + kwargs['BUILD'] = int(vinfo.bid, 10) + + for part_name, field in ID_FIELDS_BY_PART.items(): + val = kwargs[field] + if part_name.lower() == field.lower(): + if isinstance(val, str): + kwargs[part_name] = int(val, base=10) + else: + kwargs[part_name] = val + else: + assert len(set(part_name)) == 1 + padded_len = len(part_name) + kwargs[part_name] = str(val).zfill(padded_len) + + return kwargs + + +def _compile_format_template(pattern: str, kwargs: TemplateKwargs) -> str: + # NOTE (mb 2020-09-04): Some parts are optional, we need the kwargs to + # determine if part is set to its zero value + format_tmpl = pattern + for part_name, full_part_format in patterns.FULL_PART_FORMATS.items(): + format_tmpl = format_tmpl.replace("{" + part_name + "}", full_part_format) + return format_tmpl + + +def format_version(vinfo: VersionInfo, pattern: str) -> str: + """TODO reenable doctest""" + pass + + """Generate version string. + + >>> import datetime as dt + >>> vinfo = parse_version_info("v201712.0033-beta", pattern="{pycalver}") + >>> vinfo_a = vinfo._replace(**cal_info(date=dt.date(2017, 1, 1))._asdict()) + >>> vinfo_b = vinfo._replace(**cal_info(date=dt.date(2017, 12, 31))._asdict()) + + >>> format_version(vinfo_a, pattern="v{yy}.{BID}{release}") + 'v17.33-beta' + >>> format_version(vinfo_a, pattern="vYY.BUILD[-TAG]") + 'v17.33-beta' + >>> format_version(vinfo_a, pattern="YYYY0M.BUILD[PYTAG]") + '201701.33b0' + + >>> format_version(vinfo_a, pattern="{pycalver}") + 'v201701.0033-beta' + >>> format_version(vinfo_b, pattern="{pycalver}") + 'v201712.0033-beta' + + >>> format_version(vinfo_a, pattern="v{year}w{iso_week}.{BID}{release}") + 'v2017w00.33-beta' + >>> format_version(vinfo_a, pattern="vYYYYwWW.BUILD[-TAG]") + 'v2017w00.33-beta' + >>> format_version(vinfo_b, pattern="v{year}w{iso_week}.{BID}{release}") + 'v2017w52.33-beta' + >>> format_version(vinfo_b, pattern="vYYYYwWW.BUILD[-TAG]") + 'v2017w52.33-beta' + + >>> format_version(vinfo_a, pattern="v{year}d{doy}.{bid}{release}") + 'v2017d001.0033-beta' + >>> format_version(vinfo_b, pattern="v{year}d{doy}.{bid}{release}") + 'v2017d365.0033-beta' + >>> format_version(vinfo_a, pattern="vYYYYdJJJ.BUILD[-TAG]") + 'v2017d001.0033-beta' + >>> format_version(vinfo_b, pattern="vYYYYdJJJ.BUILD[-TAG]") + 'v2017d365.0033-beta' + + >>> format_version(vinfo_a, pattern="vGGGGwVV.BUILD[-TAG]") + 'v2016w52.0033-beta' + + >>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final') + + >>> format_version(vinfo_c, pattern="v{year}w{iso_week}.{BID}-{tag}") + 'v2017w52.33-final' + >>> format_version(vinfo_c, pattern="v{year}w{iso_week}.{BID}{release}") + 'v2017w52.33' + >>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD-TAG") + 'v2017w52.33-final' + >>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD[-TAG]") + 'v2017w52.33' + + >>> format_version(vinfo_c, pattern="v{MAJOR}.{MINOR}.{PATCH}") + 'v1.2.34' + >>> format_version(vinfo_c, pattern="vMAJOR.MINOR.PATCH") + 'v1.2.34' + + >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='final') + >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-TAG") + 'v1.0.0-final' + >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH[-TAG]") + 'v1.0.0' + >>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.PATCH[-TAG]]") + 'v1.0' + >>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.MICRO[-TAG]]") + 'v1.0' + >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-TAG]]]") + 'v1' + """ + kwargs = _derive_template_kwargs(vinfo) + format_tmpl = _compile_format_template(pattern, kwargs) + + return format_tmpl.format(**kwargs) + + +def incr( + old_version: str, + pattern : str = "{pycalver}", + *, + release: str = None, + major : bool = False, + minor : bool = False, + patch : bool = False, +) -> typ.Optional[str]: + """Increment version string. + + 'old_version' is assumed to be a string that matches 'pattern' + """ + try: + old_vinfo = parse_version_info(old_version, pattern) + except PatternError as ex: + logger.error(str(ex)) + return None + + cur_vinfo = old_vinfo + + cur_cal_nfo = cal_info() + + old_date = (old_vinfo.year_y or 0, old_vinfo.month or 0, old_vinfo.dom or 0) + cur_date = (cur_cal_nfo.year_y , cur_cal_nfo.month , cur_cal_nfo.dom) + + if old_date <= cur_date: + cur_vinfo = cur_vinfo._replace(**cur_cal_nfo._asdict()) + else: + logger.warning(f"Version appears to be from the future '{old_version}'") + + cur_vinfo = cur_vinfo._replace(bid=lexid.incr(cur_vinfo.bid)) + + if major: + cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0, patch=0) + if minor: + cur_vinfo = cur_vinfo._replace(minor=cur_vinfo.minor + 1, patch=0) + if patch: + cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1) + + if release: + cur_vinfo = cur_vinfo._replace(tag=release) + + new_version = format_version(cur_vinfo, pattern) + if new_version == old_version: + logger.error("Invalid arguments or pattern, version did not change.") + return None + else: + return new_version + + +def to_pep440(version: str) -> str: + """Derive pep440 compliant version string from PyCalVer version string. + + >>> to_pep440("v201811.0007-beta") + '201811.7b0' + """ + return str(pkg_resources.parse_version(version)) diff --git a/test/fixtures/project_d/pyproject.toml b/test/fixtures/project_d/pyproject.toml new file mode 100644 index 0000000..65faaf4 --- /dev/null +++ b/test/fixtures/project_d/pyproject.toml @@ -0,0 +1,6 @@ +[pycalver] +current_version = "v2017q1.54321" +version_pattern = "vYYYYqQ.BUILD" +commit = true +tag = true +push = true diff --git a/test/test_config.py b/test/test_config.py index 4d15e36..ab8c4ba 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -2,11 +2,10 @@ # pylint:disable=protected-access ; allowed for test code import io +from test import util from pycalver import config -from . import util - PYCALVER_TOML_FIXTURE_1 = """ [pycalver] current_version = "v201808.0123-alpha" diff --git a/test/test_lex_id.py b/test/test_lex_id.py deleted file mode 100644 index 198e3ed..0000000 --- a/test/test_lex_id.py +++ /dev/null @@ -1,67 +0,0 @@ -# pylint:disable=protected-access ; allowed for test code - -import random - -from pycalver import lex_id - - -def test_next_id_basic(): - assert lex_id.next_id("01") == "02" - assert lex_id.next_id("09") == "110" - - -def test_next_id_overflow(): - try: - prev_id = "9999" - next_id = lex_id.next_id(prev_id) - assert False, (prev_id, "->", next_id) - except OverflowError: - pass - - -def test_next_id_random(): - for _ in range(1000): - prev_id = str(random.randint(1, 100 * 1000)) - try: - next_id = lex_id.next_id(prev_id) - assert prev_id < next_id - except OverflowError: - assert len(prev_id) == prev_id.count("9") - - -def test_ord_val(): - assert lex_id.ord_val("1" ) == 1 - assert lex_id.ord_val("01" ) == 1 - assert lex_id.ord_val("02" ) == 2 - assert lex_id.ord_val("09" ) == 9 - assert lex_id.ord_val("110") == 10 - - -def test_main(capsys): - lex_id._main() - captured = capsys.readouterr() - assert len(captured.err) == 0 - - lines = iter(captured.out.splitlines()) - header = next(lines) - - assert "lexical" in header - assert "numerical" in header - - ids = [] - ord_vals = [] - - for line in lines: - if "..." in line: - continue - _id, _ord_val = line.split() - - assert _id.endswith(_ord_val) - assert int(_ord_val) == int(_ord_val, 10) - - ids.append(_id.strip()) - ord_vals.append(int(_ord_val.strip())) - - assert len(ids) > 0 - assert sorted(ids) == ids - assert sorted(ord_vals) == ord_vals diff --git a/test/test_patterns.py b/test/test_patterns.py index 541bbfe..61d8d48 100644 --- a/test/test_patterns.py +++ b/test/test_patterns.py @@ -2,14 +2,17 @@ import re import pytest -from pycalver import patterns +import pycalver.patterns as v1patterns +import pycalver2.patterns as v2patterns + +# TODO (mb 2020-09-06): test for v2patterns def _part_re_by_name(name): - return re.compile(patterns.PART_PATTERNS[name]) + return re.compile(v1patterns.PART_PATTERNS[name]) -@pytest.mark.parametrize("part_name", patterns.PART_PATTERNS.keys()) +@pytest.mark.parametrize("part_name", v1patterns.PART_PATTERNS.keys()) def test_part_compilation(part_name): assert _part_re_by_name(part_name) @@ -64,7 +67,7 @@ PATTERN_CASES = [ @pytest.mark.parametrize("pattern_str, line, expected", PATTERN_CASES) def test_patterns(pattern_str, line, expected): - pattern_re = patterns.compile_pattern(pattern_str) + pattern_re = v1patterns.compile_pattern(pattern_str) result = pattern_re.search(line) if result is None: assert expected is None, (pattern_str, line) @@ -82,7 +85,7 @@ CLI_MAIN_FIXTURE = """ def test_pattern_escapes(): pattern = 'click.version_option(version="{pycalver}")' - pattern_re = patterns.compile_pattern(pattern) + pattern_re = v1patterns.compile_pattern(pattern) match = pattern_re.search(CLI_MAIN_FIXTURE) expected = 'click.version_option(version="v201812.0123-beta")' assert match.group(0) == expected @@ -95,7 +98,7 @@ package_metadata = {"name": "mypackage", "version": "v201812.0123-beta"} def test_curly_escapes(): pattern = 'package_metadata = {"name": "mypackage", "version": "{pycalver}"}' - pattern_re = patterns.compile_pattern(pattern) + pattern_re = v1patterns.compile_pattern(pattern) match = pattern_re.search(CURLY_BRACE_FIXTURE) expected = 'package_metadata = {"name": "mypackage", "version": "v201812.0123-beta"}' assert match.group(0) == expected diff --git a/test/test_rewrite.py b/test/test_rewrite.py index 872fd71..1eb0dd7 100644 --- a/test/test_rewrite.py +++ b/test/test_rewrite.py @@ -1,12 +1,13 @@ # pylint:disable=protected-access ; allowed for test code import copy +from test import util from pycalver import config -from pycalver import rewrite -from pycalver import version - -from . import util +from pycalver import rewrite as v1rewrite +from pycalver import version as v1version +from pycalver2 import rewrite as v2rewrite +from pycalver2 import version as v2version REWRITE_FIXTURE = """ # SPDX-License-Identifier: MIT @@ -17,8 +18,8 @@ __version__ = "v201809.0002-beta" def test_rewrite_lines(): old_lines = REWRITE_FIXTURE.splitlines() patterns = ['__version__ = "{pycalver}"'] - new_vinfo = version.parse_version_info("v201911.0003") - new_lines = rewrite.rewrite_lines(patterns, new_vinfo, old_lines) + new_vinfo = v1version.parse_version_info("v201911.0003") + new_lines = v1rewrite.rewrite_lines(patterns, new_vinfo, old_lines) assert len(new_lines) == len(old_lines) assert "v201911.0003" not in "\n".join(old_lines) @@ -31,8 +32,8 @@ def test_rewrite_final(): old_lines = REWRITE_FIXTURE.splitlines() patterns = ['__version__ = "v{year}{month}.{build_no}-{release_tag}"'] - new_vinfo = version.parse_version_info("v201911.0003") - new_lines = rewrite.rewrite_lines(patterns, new_vinfo, old_lines) + new_vinfo = v1version.parse_version_info("v201911.0003") + new_lines = v1rewrite.rewrite_lines(patterns, new_vinfo, old_lines) assert len(new_lines) == len(old_lines) assert "v201911.0003" not in "\n".join(old_lines) @@ -46,9 +47,8 @@ def test_iter_file_paths(): cfg = config.parse(ctx) assert cfg - file_paths = { - str(file_path) for file_path, patterns in rewrite._iter_file_paths(cfg.file_patterns) - } + _paths_and_patterns = v1rewrite.iter_file_paths(cfg.file_patterns) + file_paths = {str(file_path) for file_path, patterns in _paths_and_patterns} assert file_paths == {"pycalver.toml", "README.md"} @@ -59,9 +59,8 @@ def test_iter_file_globs(): cfg = config.parse(ctx) assert cfg - file_paths = { - str(file_path) for file_path, patterns in rewrite._iter_file_paths(cfg.file_patterns) - } + _paths_and_patterns = v1rewrite.iter_file_paths(cfg.file_patterns) + file_paths = {str(file_path) for file_path, patterns in _paths_and_patterns} assert file_paths == { "setup.cfg", @@ -80,7 +79,7 @@ def test_error_bad_path(): (project.dir / "setup.py").unlink() try: - list(rewrite._iter_file_paths(cfg.file_patterns)) + list(v1rewrite.iter_file_paths(cfg.file_patterns)) assert False, "expected IOError" except IOError as ex: assert "setup.py" in str(ex) @@ -96,10 +95,10 @@ def test_error_bad_pattern(): patterns["setup.py"] = patterns["setup.py"][0] + "invalid" try: - new_vinfo = version.parse_version_info("v201809.1234") - list(rewrite.diff(new_vinfo, patterns)) - assert False, "expected rewrite.NoPatternMatch" - except rewrite.NoPatternMatch as ex: + new_vinfo = v1version.parse_version_info("v201809.1234") + list(v1rewrite.diff(new_vinfo, patterns)) + assert False, "expected v1rewrite.NoPatternMatch" + except v1rewrite.NoPatternMatch as ex: assert "setup.py" in str(ex) @@ -109,21 +108,21 @@ __version__ = "2018.0002-beta" """ -def test_optional_release(): +def test_v1_optional_release(): old_lines = OPTIONAL_RELEASE_FIXTURE.splitlines() pattern = "{year}.{build_no}{release}" patterns = ['__version__ = "{year}.{build_no}{release}"'] - new_vinfo = version.parse_version_info("2019.0003", pattern) - new_lines = rewrite.rewrite_lines(patterns, new_vinfo, old_lines) + new_vinfo = v1version.parse_version_info("2019.0003", pattern) + new_lines = v1rewrite.rewrite_lines(patterns, new_vinfo, old_lines) assert len(new_lines) == len(old_lines) assert "2019.0003" not in "\n".join(old_lines) new_text = "\n".join(new_lines) assert "2019.0003" in new_text - new_vinfo = version.parse_version_info("2019.0004-beta", pattern) - new_lines = rewrite.rewrite_lines(patterns, new_vinfo, old_lines) + new_vinfo = v1version.parse_version_info("2019.0004-beta", pattern) + new_lines = v1rewrite.rewrite_lines(patterns, new_vinfo, old_lines) # make sure optional release tag is added back on assert len(new_lines) == len(old_lines) From 4caece28175c071e2c949ad24eac1f266340784d Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 6 Sep 2020 21:15:27 +0000 Subject: [PATCH 04/98] cli -> __main__ refactor --- src/pycalver/__main__.py | 382 ++++++++++++++++++++++++++++++++++++++- src/pycalver/cli.py | 381 +++----------------------------------- test/test_cli.py | 88 ++++----- 3 files changed, 450 insertions(+), 401 deletions(-) diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 80130c2..06b41cd 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -9,8 +9,384 @@ __main__ module for PyCalVer. Enables use as module: $ python -m pycalver --version """ +import sys +import typing as typ +import logging +import subprocess as sp + +import click + +import pycalver.cli as v1cli +import pycalver2.cli as v2cli +import pycalver.version as v1version +import pycalver2.version as v2version +from pycalver import vcs +from pycalver import config + +_VERBOSE = 0 + + +try: + import pretty_traceback + + pretty_traceback.install() +except ImportError: + pass # no need to fail because of missing dev dependency + + +click.disable_unicode_literals_warning = True + +logger = logging.getLogger("pycalver.__main__") + + +def _configure_logging(verbose: int = 0) -> None: + if verbose >= 2: + log_format = "%(asctime)s.%(msecs)03d %(levelname)-7s %(name)-17s - %(message)s" + log_level = logging.DEBUG + elif verbose == 1: + log_format = "%(levelname)-7s - %(message)s" + log_level = logging.INFO + else: + log_format = "%(levelname)-7s - %(message)s" + log_level = logging.INFO + + logging.basicConfig(level=log_level, format=log_format, datefmt="%Y-%m-%dT%H:%M:%S") + logger.debug("Logging configured.") + + +VALID_RELEASE_VALUES = ("alpha", "beta", "dev", "rc", "post", "final") + + +def _validate_release_tag(release: str) -> None: + if release in VALID_RELEASE_VALUES: + return + + logger.error(f"Invalid argument --release={release}") + logger.error(f"Valid arguments are: {', '.join(VALID_RELEASE_VALUES)}") + sys.exit(1) + + +@click.group() +@click.version_option(version="v202007.0036") +@click.help_option() +@click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") +def cli(verbose: int = 0) -> None: + """Automatically update PyCalVer version strings on python projects.""" + global _VERBOSE + _VERBOSE = verbose + + +@cli.command() +@click.argument("old_version") +@click.argument("pattern", default="{pycalver}") +@click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") +@click.option( + "--release", default=None, metavar="", help="Override release name of current_version" +) +@click.option("--major", is_flag=True, default=False, help="Increment major component.") +@click.option("--minor", is_flag=True, default=False, help="Increment minor component.") +@click.option("--patch", is_flag=True, default=False, help="Increment patch component.") +def test( + old_version: str, + pattern : str = "{pycalver}", + verbose : int = 0, + release : str = None, + major : bool = False, + minor : bool = False, + patch : bool = False, +) -> None: + """Increment a version number for demo purposes.""" + _configure_logging(verbose=max(_VERBOSE, verbose)) + + if release: + _validate_release_tag(release) + + new_version = _incr( + old_version, + pattern=pattern, + release=release, + major=major, + minor=minor, + patch=patch, + ) + if new_version is None: + logger.error(f"Invalid version '{old_version}' and/or pattern '{pattern}'.") + sys.exit(1) + + # TODO (mb 2020-09-05): version switch + pep440_version = v1version.to_pep440(new_version) + # pep440_version = v2version.to_pep440(new_version) + + click.echo(f"New Version: {new_version}") + click.echo(f"PEP440 : {pep440_version}") + + +@cli.command() +@click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") +@click.option( + "-f/-n", "--fetch/--no-fetch", is_flag=True, default=True, help="Sync tags from remote origin." +) +def show(verbose: int = 0, fetch: bool = True) -> None: + """Show current version of your project.""" + _configure_logging(verbose=max(_VERBOSE, verbose)) + + ctx: config.ProjectContext = config.init_project_ctx(project_path=".") + cfg: config.MaybeConfig = config.parse(ctx) + + if cfg is None: + logger.error("Could not parse configuration. Perhaps try 'pycalver init'.") + sys.exit(1) + + cfg = _update_cfg_from_vcs(cfg, fetch) + click.echo(f"Current Version: {cfg.current_version}") + click.echo(f"PEP440 : {cfg.pep440_version}") + + +def _try_print_diff(cfg: config.Config, new_version: str) -> None: + try: + # TODO (mb 2020-09-05): version switch + diff = v1cli.get_diff(cfg, new_version) + # diff = v2cli.get_diff(cfg, new_version) + + if sys.stdout.isatty(): + for line in diff.splitlines(): + if line.startswith("+++") or line.startswith("---"): + click.echo(line) + elif line.startswith("+"): + click.echo("\u001b[32m" + line + "\u001b[0m") + elif line.startswith("-"): + click.echo("\u001b[31m" + line + "\u001b[0m") + elif line.startswith("@"): + click.echo("\u001b[36m" + line + "\u001b[0m") + else: + click.echo(line) + else: + click.echo(diff) + except Exception as ex: + logger.error(str(ex), exc_info=True) + sys.exit(1) + + +def _incr( + old_version: str, + pattern : str = "{pycalver}", + *, + release: str = None, + major : bool = False, + minor : bool = False, + patch : bool = False, +) -> typ.Optional[str]: + is_v1_pattern = "{" in pattern + if is_v1_pattern: + return v1version.incr( + old_version, + pattern=pattern, + release=release, + major=major, + minor=minor, + patch=patch, + ) + else: + return v2version.incr( + old_version, + pattern=pattern, + release=release, + major=major, + minor=minor, + patch=patch, + ) + + +def _bump( + cfg : config.Config, + new_version : str, + commit_message: str, + allow_dirty : bool = False, +) -> None: + vcs_api: typ.Optional[vcs.VCSAPI] = None + + if cfg.commit: + try: + vcs_api = vcs.get_vcs_api() + except OSError: + logger.warning("Version Control System not found, aborting commit.") + + filepaths = set(cfg.file_patterns.keys()) + + if vcs_api: + vcs.assert_not_dirty(vcs_api, filepaths, allow_dirty) + + try: + # TODO (mb 2020-09-05): version switch + v1cli.rewrite(cfg, new_version) + # v2cli.rewrite(cfg, new_version) + except Exception as ex: + logger.error(str(ex)) + sys.exit(1) + + if vcs_api: + vcs.commit(cfg, vcs_api, filepaths, new_version, commit_message) + + +def _try_bump( + cfg : config.Config, + new_version : str, + commit_message: str, + allow_dirty : bool = False, +) -> None: + try: + _bump(cfg, new_version, commit_message, allow_dirty) + except sp.CalledProcessError as ex: + logger.error(f"Error running subcommand: {ex.cmd}") + if ex.stdout: + sys.stdout.write(ex.stdout.decode('utf-8')) + if ex.stderr: + sys.stderr.write(ex.stderr.decode('utf-8')) + sys.exit(1) + + +@cli.command() +@click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") +@click.option( + "--dry", default=False, is_flag=True, help="Display diff of changes, don't rewrite files." +) +def init(verbose: int = 0, dry: bool = False) -> None: + """Initialize [pycalver] configuration.""" + _configure_logging(verbose=max(_VERBOSE, verbose)) + + ctx: config.ProjectContext = config.init_project_ctx(project_path=".") + cfg: config.MaybeConfig = config.parse(ctx) + + if cfg: + logger.error(f"Configuration already initialized in {ctx.config_filepath}") + sys.exit(1) + + if dry: + click.echo(f"Exiting because of '--dry'. Would have written to {ctx.config_filepath}:") + cfg_text: str = config.default_config(ctx) + click.echo("\n " + "\n ".join(cfg_text.splitlines())) + sys.exit(0) + + config.write_content(ctx) + + +def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: + all_tags = vcs.get_tags(fetch=fetch) + + # TODO (mb 2020-09-05): version switch + cfg = v1cli.update_cfg_from_vcs(cfg, all_tags) + # cfg = v2cli.update_cfg_from_vcs(cfg, all_tags) + return cfg + + +@cli.command() +@click.option( + "-v", + "--verbose", + count=True, + help="Control log level. -vv for debug level.", +) +@click.option( + "-f/-n", + "--fetch/--no-fetch", + is_flag=True, + default=True, + help="Sync tags from remote origin.", +) +@click.option( + "--dry", + default=False, + is_flag=True, + help="Display diff of changes, don't rewrite files.", +) +@click.option( + "--release", + default=None, + metavar="", + help=( + f"Override release name of current_version. Valid options are: " + f"{', '.join(VALID_RELEASE_VALUES)}." + ), +) +@click.option( + "--allow-dirty", + default=False, + is_flag=True, + help=( + "Commit even when working directory is has uncomitted changes. " + "(WARNING: The commit will still be aborted if there are uncomitted " + "to files with version strings." + ), +) +@click.option("--major", is_flag=True, default=False, help="Increment major component.") +@click.option("--minor", is_flag=True, default=False, help="Increment minor component.") +@click.option("--patch", is_flag=True, default=False, help="Increment patch component.") +def bump( + release : typ.Optional[str] = None, + verbose : int = 0, + dry : bool = False, + allow_dirty: bool = False, + fetch : bool = True, + major : bool = False, + minor : bool = False, + patch : bool = False, +) -> None: + """Increment the current version string and update project files.""" + verbose = max(_VERBOSE, verbose) + _configure_logging(verbose) + + if release: + _validate_release_tag(release) + + ctx: config.ProjectContext = config.init_project_ctx(project_path=".") + cfg: config.MaybeConfig = config.parse(ctx) + + if cfg is None: + logger.error("Could not parse configuration. Perhaps try 'pycalver init'.") + sys.exit(1) + + cfg = _update_cfg_from_vcs(cfg, fetch) + + old_version = cfg.current_version + new_version = _incr( + old_version, + pattern=cfg.version_pattern, + release=release, + major=major, + minor=minor, + patch=patch, + ) + + if new_version is None: + is_semver = "{semver}" in cfg.version_pattern + has_semver_inc = major or minor or patch + if is_semver and not has_semver_inc: + logger.warning("bump --major/--minor/--patch required when using semver.") + else: + logger.error(f"Invalid version '{old_version}' and/or pattern '{cfg.version_pattern}'.") + sys.exit(1) + + logger.info(f"Old Version: {old_version}") + logger.info(f"New Version: {new_version}") + + if dry or verbose >= 2: + _try_print_diff(cfg, new_version) + + if dry: + return + + # # TODO (mb 2020-09-05): format from config + # commit_message_kwargs = { + # new_version + # old_version + # pep440_new_version + # pep440_old_version + # } + # cfg.commit_message = + commit_message = f"bump version to {new_version}" + + _try_bump(cfg, new_version, commit_message, allow_dirty) + if __name__ == '__main__': - from . import cli - - cli.cli() + cli() diff --git a/src/pycalver/cli.py b/src/pycalver/cli.py index 47b305a..da4cea7 100755 --- a/src/pycalver/cli.py +++ b/src/pycalver/cli.py @@ -9,372 +9,45 @@ CLI module for PyCalVer. Provided subcommands: show, test, init, bump """ -import sys import typing as typ import logging -import subprocess as sp - -import click - -from . import vcs -from . import config -from . import rewrite -from . import version - -_VERBOSE = 0 - - -try: - import pretty_traceback - - pretty_traceback.install() -except ImportError: - pass # no need to fail because of missing dev dependency - - -click.disable_unicode_literals_warning = True - - -VALID_RELEASE_VALUES = ("alpha", "beta", "dev", "rc", "post", "final") +import pycalver.rewrite as v1rewrite +import pycalver.version as v1version +from pycalver import config logger = logging.getLogger("pycalver.cli") -def _configure_logging(verbose: int = 0) -> None: - if verbose >= 2: - log_format = "%(asctime)s.%(msecs)03d %(levelname)-7s %(name)-17s - %(message)s" - log_level = logging.DEBUG - elif verbose == 1: - log_format = "%(levelname)-7s - %(message)s" - log_level = logging.INFO - else: - log_format = "%(levelname)-7s - %(message)s" - log_level = logging.INFO +def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.Config: + version_tags = [tag for tag in all_tags if v1version.is_valid(tag, cfg.version_pattern)] + if not version_tags: + logger.debug("no vcs tags found") + return cfg - logging.basicConfig(level=log_level, format=log_format, datefmt="%Y-%m-%dT%H:%M:%S") - logger.debug("Logging configured.") + version_tags.sort(reverse=True) + logger.debug(f"found {len(version_tags)} tags: {version_tags[:2]}") + latest_version_tag = version_tags[0] + latest_version_pep440 = v1version.to_pep440(latest_version_tag) + if latest_version_tag <= cfg.current_version: + return cfg - -def _validate_release_tag(release: str) -> None: - if release in VALID_RELEASE_VALUES: - return - - logger.error(f"Invalid argument --release={release}") - logger.error(f"Valid arguments are: {', '.join(VALID_RELEASE_VALUES)}") - sys.exit(1) - - -@click.group() -@click.version_option(version="v202007.0036") -@click.help_option() -@click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") -def cli(verbose: int = 0) -> None: - """Automatically update PyCalVer version strings on python projects.""" - global _VERBOSE - _VERBOSE = verbose - - -@cli.command() -@click.argument("old_version") -@click.argument("pattern", default="{pycalver}") -@click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") -@click.option( - "--release", default=None, metavar="", help="Override release name of current_version" -) -@click.option("--major", is_flag=True, default=False, help="Increment major component.") -@click.option("--minor", is_flag=True, default=False, help="Increment minor component.") -@click.option("--patch", is_flag=True, default=False, help="Increment patch component.") -def test( - old_version: str, - pattern : str = "{pycalver}", - verbose : int = 0, - release : str = None, - major : bool = False, - minor : bool = False, - patch : bool = False, -) -> None: - """Increment a version number for demo purposes.""" - _configure_logging(verbose=max(_VERBOSE, verbose)) - - if release: - _validate_release_tag(release) - - new_version = version.incr( - old_version, pattern=pattern, release=release, major=major, minor=minor, patch=patch + logger.info(f"Working dir version : {cfg.current_version}") + logger.info(f"Latest version from VCS tag: {latest_version_tag}") + return cfg._replace( + current_version=latest_version_tag, + pep440_version=latest_version_pep440, ) - if new_version is None: - logger.error(f"Invalid version '{old_version}' and/or pattern '{pattern}'.") - sys.exit(1) - - pep440_version = version.to_pep440(new_version) - - click.echo(f"New Version: {new_version}") - click.echo(f"PEP440 : {pep440_version}") -def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: - try: - vcs_api = vcs.get_vcs_api() - logger.debug(f"vcs found: {vcs_api.name}") - if fetch: - logger.info("fetching tags from remote (to turn off use: -n / --no-fetch)") - vcs_api.fetch() - - version_tags = [ - tag for tag in vcs_api.ls_tags() if version.is_valid(tag, cfg.version_pattern) - ] - if version_tags: - version_tags.sort(reverse=True) - logger.debug(f"found {len(version_tags)} tags: {version_tags[:2]}") - latest_version_tag = version_tags[0] - latest_version_pep440 = version.to_pep440(latest_version_tag) - if latest_version_tag > cfg.current_version: - logger.info(f"Working dir version : {cfg.current_version}") - logger.info(f"Latest version from {vcs_api.name:>3} tag: {latest_version_tag}") - cfg = cfg._replace( - current_version=latest_version_tag, pep440_version=latest_version_pep440 - ) - - else: - logger.debug("no vcs tags found") - except OSError: - logger.debug("No vcs found") - - return cfg - - -@cli.command() -@click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") -@click.option( - "-f/-n", "--fetch/--no-fetch", is_flag=True, default=True, help="Sync tags from remote origin." -) -def show(verbose: int = 0, fetch: bool = True) -> None: - """Show current version.""" - _configure_logging(verbose=max(_VERBOSE, verbose)) - - ctx: config.ProjectContext = config.init_project_ctx(project_path=".") - cfg: config.MaybeConfig = config.parse(ctx) - - if cfg is None: - logger.error("Could not parse configuration. Perhaps try 'pycalver init'.") - sys.exit(1) - - cfg = _update_cfg_from_vcs(cfg, fetch=fetch) - - click.echo(f"Current Version: {cfg.current_version}") - click.echo(f"PEP440 : {cfg.pep440_version}") - - -@cli.command() -@click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") -@click.option( - "--dry", default=False, is_flag=True, help="Display diff of changes, don't rewrite files." -) -def init(verbose: int = 0, dry: bool = False) -> None: - """Initialize [pycalver] configuration.""" - _configure_logging(verbose=max(_VERBOSE, verbose)) - - ctx: config.ProjectContext = config.init_project_ctx(project_path=".") - cfg: config.MaybeConfig = config.parse(ctx) - - if cfg: - logger.error(f"Configuration already initialized in {ctx.config_filepath}") - sys.exit(1) - - if dry: - click.echo(f"Exiting because of '--dry'. Would have written to {ctx.config_filepath}:") - cfg_text: str = config.default_config(ctx) - click.echo("\n " + "\n ".join(cfg_text.splitlines())) - sys.exit(0) - - config.write_content(ctx) - - -def _assert_not_dirty(vcs_api: vcs.VCSAPI, filepaths: typ.Set[str], allow_dirty: bool) -> None: - dirty_files = vcs_api.status(required_files=filepaths) - - if dirty_files: - logger.warning(f"{vcs_api.name} working directory is not clean. Uncomitted file(s):") - for dirty_file in dirty_files: - logger.warning(" " + dirty_file) - - if not allow_dirty and dirty_files: - sys.exit(1) - - dirty_pattern_files = set(dirty_files) & filepaths - if dirty_pattern_files: - logger.error("Not commiting when pattern files are dirty:") - for dirty_file in dirty_pattern_files: - logger.warning(" " + dirty_file) - sys.exit(1) - - -def _commit( - cfg: config.Config, new_version: str, vcs_api: vcs.VCSAPI, filepaths: typ.Set[str] +def rewrite( + cfg : config.Config, + new_version: str, ) -> None: - for filepath in filepaths: - vcs_api.add(filepath) - - vcs_api.commit(f"bump version to {new_version}") - - if cfg.commit and cfg.tag: - vcs_api.tag(new_version) - - if cfg.commit and cfg.tag and cfg.push: - vcs_api.push(new_version) + new_vinfo = v1version.parse_version_info(new_version, cfg.version_pattern) + v1rewrite.rewrite(cfg.file_patterns, new_vinfo) -def _bump(cfg: config.Config, new_version: str, allow_dirty: bool = False) -> None: - vcs_api: typ.Optional[vcs.VCSAPI] = None - - if cfg.commit: - try: - vcs_api = vcs.get_vcs_api() - except OSError: - logger.warning("Version Control System not found, aborting commit.") - - filepaths = set(cfg.file_patterns.keys()) - - if vcs_api: - _assert_not_dirty(vcs_api, filepaths, allow_dirty) - - try: - new_vinfo = version.parse_version_info(new_version, cfg.version_pattern) - rewrite.rewrite(cfg.file_patterns, new_vinfo) - except Exception as ex: - logger.error(str(ex)) - sys.exit(1) - - if vcs_api: - _commit(cfg, new_version, vcs_api, filepaths) - - -def _try_bump(cfg: config.Config, new_version: str, allow_dirty: bool = False) -> None: - try: - _bump(cfg, new_version, allow_dirty) - except sp.CalledProcessError as ex: - logger.error(f"Error running subcommand: {ex.cmd}") - if ex.stdout: - sys.stdout.write(ex.stdout.decode('utf-8')) - if ex.stderr: - sys.stderr.write(ex.stderr.decode('utf-8')) - sys.exit(1) - - -def _print_diff(cfg: config.Config, new_version: str) -> None: - new_vinfo = version.parse_version_info(new_version, cfg.version_pattern) - diff: str = rewrite.diff(new_vinfo, cfg.file_patterns) - - if sys.stdout.isatty(): - for line in diff.splitlines(): - if line.startswith("+++") or line.startswith("---"): - click.echo(line) - elif line.startswith("+"): - click.echo("\u001b[32m" + line + "\u001b[0m") - elif line.startswith("-"): - click.echo("\u001b[31m" + line + "\u001b[0m") - elif line.startswith("@"): - click.echo("\u001b[36m" + line + "\u001b[0m") - else: - click.echo(line) - else: - click.echo(diff) - - -def _try_print_diff(cfg: config.Config, new_version: str) -> None: - try: - _print_diff(cfg, new_version) - except Exception as ex: - logger.error(str(ex)) - sys.exit(1) - - -@cli.command() -@click.option("-v", "--verbose", count=True, help="Control log level. -vv for debug level.") -@click.option( - "-f/-n", "--fetch/--no-fetch", is_flag=True, default=True, help="Sync tags from remote origin." -) -@click.option( - "--dry", default=False, is_flag=True, help="Display diff of changes, don't rewrite files." -) -@click.option( - "--release", - default=None, - metavar="", - help=( - f"Override release name of current_version. Valid options are: " - f"{', '.join(VALID_RELEASE_VALUES)}." - ), -) -@click.option( - "--allow-dirty", - default=False, - is_flag=True, - help=( - "Commit even when working directory is has uncomitted changes. " - "(WARNING: The commit will still be aborted if there are uncomitted " - "to files with version strings." - ), -) -@click.option("--major", is_flag=True, default=False, help="Increment major component.") -@click.option("--minor", is_flag=True, default=False, help="Increment minor component.") -@click.option("--patch", is_flag=True, default=False, help="Increment patch component.") -def bump( - release : typ.Optional[str] = None, - verbose : int = 0, - dry : bool = False, - allow_dirty: bool = False, - fetch : bool = True, - major : bool = False, - minor : bool = False, - patch : bool = False, -) -> None: - """Increment the current version string and update project files.""" - verbose = max(_VERBOSE, verbose) - _configure_logging(verbose) - - if release: - _validate_release_tag(release) - - ctx: config.ProjectContext = config.init_project_ctx(project_path=".") - cfg: config.MaybeConfig = config.parse(ctx) - - if cfg is None: - logger.error("Could not parse configuration. Perhaps try 'pycalver init'.") - sys.exit(1) - - cfg = _update_cfg_from_vcs(cfg, fetch=fetch) - - old_version = cfg.current_version - new_version = version.incr( - old_version, - pattern=cfg.version_pattern, - release=release, - major=major, - minor=minor, - patch=patch, - ) - if new_version is None: - is_semver = "{semver}" in cfg.version_pattern - has_semver_inc = major or minor or patch - if is_semver and not has_semver_inc: - logger.warning("bump --major/--minor/--patch required when using semver.") - else: - logger.error(f"Invalid version '{old_version}' and/or pattern '{cfg.version_pattern}'.") - sys.exit(1) - - logger.info(f"Old Version: {old_version}") - logger.info(f"New Version: {new_version}") - - if dry or verbose >= 2: - _try_print_diff(cfg, new_version) - - if dry: - return - - _try_bump(cfg, new_version, allow_dirty) - - -if __name__ == '__main__': - cli() +def get_diff(cfg: config.Config, new_version: str) -> str: + new_vinfo = v1version.parse_version_info(new_version, cfg.version_pattern) + return v1rewrite.diff(new_vinfo, cfg.file_patterns) diff --git a/test/test_cli.py b/test/test_cli.py index 810fed8..38355a2 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -10,9 +10,9 @@ import pytest import pathlib2 as pl from click.testing import CliRunner -import pycalver.cli as cli import pycalver.config as config -import pycalver.patterns as patterns +import pycalver2.patterns as patterns +from pycalver.__main__ import cli SETUP_CFG_FIXTURE = """ [metadata] @@ -68,7 +68,7 @@ def runner(tmpdir): def test_help(runner): - result = runner.invoke(cli.cli, ['--help', "-vv"]) + result = runner.invoke(cli, ['--help', "-vv"]) assert result.exit_code == 0 assert "PyCalVer" in result.output assert "bump " in result.output @@ -78,7 +78,7 @@ def test_help(runner): def test_version(runner): - result = runner.invoke(cli.cli, ['--version', "-vv"]) + result = runner.invoke(cli, ['--version', "-vv"]) assert result.exit_code == 0 assert " version v20" in result.output match = patterns.PYCALVER_RE.search(result.output) @@ -89,7 +89,7 @@ def test_incr_default(runner): old_version = "v201701.0999-alpha" initial_version = config._initial_version() - result = runner.invoke(cli.cli, ['test', "-vv", old_version]) + result = runner.invoke(cli, ['test', "-vv", old_version]) assert result.exit_code == 0 new_version = initial_version.replace(".0001-alpha", ".11000-alpha") assert f"Version: {new_version}\n" in result.output @@ -100,31 +100,31 @@ def test_incr_semver(runner): old_version = "0.1.0" new_version = "0.1.1" - result = runner.invoke(cli.cli, ['test', "-vv", "--patch", old_version, "{semver}"]) + result = runner.invoke(cli, ['test', "-vv", "--patch", old_version, "{semver}"]) assert result.exit_code == 0 assert f"Version: {new_version}\n" in result.output - result = runner.invoke(cli.cli, ['test', "-vv", "--patch", old_version, semver_pattern]) + result = runner.invoke(cli, ['test', "-vv", "--patch", old_version, semver_pattern]) assert result.exit_code == 0 assert f"Version: {new_version}\n" in result.output old_version = "0.1.1" new_version = "0.2.0" - result = runner.invoke(cli.cli, ['test', "-vv", "--minor", old_version, semver_pattern]) + result = runner.invoke(cli, ['test', "-vv", "--minor", old_version, semver_pattern]) assert result.exit_code == 0 assert f"Version: {new_version}\n" in result.output old_version = "0.1.1" new_version = "1.0.0" - result = runner.invoke(cli.cli, ['test', "-vv", "--major", old_version, semver_pattern]) + result = runner.invoke(cli, ['test', "-vv", "--major", old_version, semver_pattern]) assert result.exit_code == 0 assert f"Version: {new_version}\n" in result.output def test_incr_semver_invalid(runner, caplog): - result = runner.invoke(cli.cli, ['test', "-vv", "--patch", "0.1.1"]) + result = runner.invoke(cli, ['test', "-vv", "--patch", "0.1.1"]) assert result.exit_code == 1 assert len(caplog.records) > 0 log_record = caplog.records[0] @@ -136,7 +136,7 @@ def test_incr_to_beta(runner): old_version = "v201701.0999-alpha" initial_version = config._initial_version() - result = runner.invoke(cli.cli, ['test', old_version, "-vv", "--release", "beta"]) + result = runner.invoke(cli, ['test', old_version, "-vv", "--release", "beta"]) assert result.exit_code == 0 new_version = initial_version.replace(".0001-alpha", ".11000-beta") assert f"Version: {new_version}\n" in result.output @@ -146,7 +146,7 @@ def test_incr_to_final(runner): old_version = "v201701.0999-alpha" initial_version = config._initial_version() - result = runner.invoke(cli.cli, ['test', old_version, "-vv", "--release", "final"]) + result = runner.invoke(cli, ['test', old_version, "-vv", "--release", "final"]) assert result.exit_code == 0 new_version = initial_version.replace(".0001-alpha", ".11000") assert f"Version: {new_version}\n" in result.output @@ -155,7 +155,7 @@ def test_incr_to_final(runner): def test_incr_invalid(runner): old_version = "v201701.0999-alpha" - result = runner.invoke(cli.cli, ['test', old_version, "-vv", "--release", "alfa"]) + result = runner.invoke(cli, ['test', old_version, "-vv", "--release", "alfa"]) assert result.exit_code == 1 @@ -184,7 +184,7 @@ def _add_project_files(*files): def test_nocfg(runner, caplog): _add_project_files("README.md") - result = runner.invoke(cli.cli, ['show', "-vv"]) + result = runner.invoke(cli, ['show', "-vv"]) assert result.exit_code == 1 assert any( bool("Could not parse configuration. Perhaps try 'pycalver init'." in r.message) @@ -195,7 +195,7 @@ def test_nocfg(runner, caplog): def test_novcs_nocfg_init(runner, caplog): _add_project_files("README.md") # dry mode test - result = runner.invoke(cli.cli, ['init', "-vv", "--dry"]) + result = runner.invoke(cli, ['init', "-vv", "--dry"]) assert result.exit_code == 0 assert not os.path.exists("pycalver.toml") @@ -206,7 +206,7 @@ def test_novcs_nocfg_init(runner, caplog): assert "File not found" in log.message # non dry mode - result = runner.invoke(cli.cli, ['init', "-vv"]) + result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 0 # check logging @@ -223,12 +223,12 @@ def test_novcs_nocfg_init(runner, caplog): assert base_str in cfg_content assert config.DEFAULT_TOML_README_MD_STR in cfg_content - result = runner.invoke(cli.cli, ['show', "-vv"]) + result = runner.invoke(cli, ['show', "-vv"]) assert result.exit_code == 0 assert f"Current Version: {config._initial_version()}\n" in result.output assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output - result = runner.invoke(cli.cli, ['init', "-vv"]) + result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 1 # check logging @@ -240,7 +240,7 @@ def test_novcs_nocfg_init(runner, caplog): def test_novcs_setupcfg_init(runner): _add_project_files("README.md", "setup.cfg") - result = runner.invoke(cli.cli, ['init', "-vv"]) + result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 0 with pl.Path("setup.cfg").open(mode="r", encoding="utf-8") as fobj: @@ -252,7 +252,7 @@ def test_novcs_setupcfg_init(runner): assert base_str in cfg_content assert config.DEFAULT_CONFIGPARSER_README_MD_STR in cfg_content - result = runner.invoke(cli.cli, ['show', "-vv"]) + result = runner.invoke(cli, ['show', "-vv"]) assert result.exit_code == 0 assert f"Current Version: {config._initial_version()}\n" in result.output assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output @@ -260,7 +260,7 @@ def test_novcs_setupcfg_init(runner): def test_novcs_pyproject_init(runner): _add_project_files("README.md", "pyproject.toml") - result = runner.invoke(cli.cli, ['init', "-vv"]) + result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 0 with pl.Path("pyproject.toml").open(mode="r", encoding="utf-8") as fobj: @@ -270,7 +270,7 @@ def test_novcs_pyproject_init(runner): assert base_str in cfg_content assert config.DEFAULT_TOML_README_MD_STR in cfg_content - result = runner.invoke(cli.cli, ['show']) + result = runner.invoke(cli, ['show']) assert result.exit_code == 0 assert f"Current Version: {config._initial_version()}\n" in result.output assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output @@ -292,10 +292,10 @@ def test_git_init(runner): _add_project_files("README.md") _vcs_init("git") - result = runner.invoke(cli.cli, ['init', "-vv"]) + result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 0 - result = runner.invoke(cli.cli, ['show']) + result = runner.invoke(cli, ['show']) assert result.exit_code == 0 assert f"Current Version: {config._initial_version()}\n" in result.output assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output @@ -305,10 +305,10 @@ def test_hg_init(runner): _add_project_files("README.md") _vcs_init("hg") - result = runner.invoke(cli.cli, ['init', "-vv"]) + result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 0 - result = runner.invoke(cli.cli, ['show']) + result = runner.invoke(cli, ['show']) assert result.exit_code == 0 assert f"Current Version: {config._initial_version()}\n" in result.output assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output @@ -320,7 +320,7 @@ def test_git_tag_eval(runner): # This will set a version that is older than the version tag # we set in the vcs, which should take precedence. - result = runner.invoke(cli.cli, ['init', "-vv"]) + result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 0 initial_version = config._initial_version() tag_version = initial_version.replace(".0001-alpha", ".0123-beta") @@ -328,7 +328,7 @@ def test_git_tag_eval(runner): shell("git", "tag", "--annotate", tag_version, "--message", f"bump version to {tag_version}") - result = runner.invoke(cli.cli, ['show', "-vv"]) + result = runner.invoke(cli, ['show', "-vv"]) assert result.exit_code == 0 assert f"Current Version: {tag_version}\n" in result.output assert f"PEP440 : {tag_version_pep440}\n" in result.output @@ -340,7 +340,7 @@ def test_hg_tag_eval(runner): # This will set a version that is older than the version tag # we set in the vcs, which should take precedence. - result = runner.invoke(cli.cli, ['init', "-vv"]) + result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 0 initial_version = config._initial_version() tag_version = initial_version.replace(".0001-alpha", ".0123-beta") @@ -348,7 +348,7 @@ def test_hg_tag_eval(runner): shell("hg", "tag", tag_version, "--message", f"bump version to {tag_version}") - result = runner.invoke(cli.cli, ['show', "-vv"]) + result = runner.invoke(cli, ['show', "-vv"]) assert result.exit_code == 0 assert f"Current Version: {tag_version}\n" in result.output assert f"PEP440 : {tag_version_pep440}\n" in result.output @@ -357,10 +357,10 @@ def test_hg_tag_eval(runner): def test_novcs_bump(runner): _add_project_files("README.md") - result = runner.invoke(cli.cli, ['init', "-vv"]) + result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 0 - result = runner.invoke(cli.cli, ['bump', "-vv"]) + result = runner.invoke(cli, ['bump', "-vv"]) assert result.exit_code == 0 calver = config._initial_version()[:7] @@ -370,7 +370,7 @@ def test_novcs_bump(runner): assert calver + ".0002-alpha !\n" in content assert calver[1:] + ".2a0 !\n" in content - result = runner.invoke(cli.cli, ['bump', "-vv", "--release", "beta"]) + result = runner.invoke(cli, ['bump', "-vv", "--release", "beta"]) assert result.exit_code == 0 with pl.Path("README.md").open() as fobj: @@ -383,13 +383,13 @@ def test_git_bump(runner): _add_project_files("README.md") _vcs_init("git") - result = runner.invoke(cli.cli, ['init', "-vv"]) + result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 0 shell("git", "add", "pycalver.toml") shell("git", "commit", "-m", "initial commit") - result = runner.invoke(cli.cli, ['bump', "-vv"]) + result = runner.invoke(cli, ['bump', "-vv"]) assert result.exit_code == 0 calver = config._initial_version()[:7] @@ -403,13 +403,13 @@ def test_hg_bump(runner): _add_project_files("README.md") _vcs_init("hg") - result = runner.invoke(cli.cli, ['init', "-vv"]) + result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 0 shell("hg", "add", "pycalver.toml") shell("hg", "commit", "-m", "initial commit") - result = runner.invoke(cli.cli, ['bump', "-vv"]) + result = runner.invoke(cli, ['bump', "-vv"]) assert result.exit_code == 0 calver = config._initial_version()[:7] @@ -423,7 +423,7 @@ def test_empty_git_bump(runner, caplog): shell("git", "init") with pl.Path("setup.cfg").open(mode="w") as fobj: fobj.write("") - result = runner.invoke(cli.cli, ['init', "-vv"]) + result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 0 with pl.Path("setup.cfg").open(mode="r") as fobj: @@ -434,7 +434,7 @@ def test_empty_git_bump(runner, caplog): assert "\n[pycalver:file_patterns]\n" in default_cfg_data assert "\nsetup.cfg =\n" in default_cfg_data - result = runner.invoke(cli.cli, ['bump']) + result = runner.invoke(cli, ['bump']) assert any(("working directory is not clean" in r.message) for r in caplog.records) assert any(("setup.cfg" in r.message) for r in caplog.records) @@ -444,7 +444,7 @@ def test_empty_hg_bump(runner, caplog): shell("hg", "init") with pl.Path("setup.cfg").open(mode="w") as fobj: fobj.write("") - result = runner.invoke(cli.cli, ['init', "-vv"]) + result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 0 with pl.Path("setup.cfg").open(mode="r") as fobj: @@ -455,7 +455,7 @@ def test_empty_hg_bump(runner, caplog): assert "\n[pycalver:file_patterns]\n" in default_cfg_text assert "\nsetup.cfg =\n" in default_cfg_text - result = runner.invoke(cli.cli, ['bump']) + result = runner.invoke(cli, ['bump']) assert any(("working directory is not clean" in r.message) for r in caplog.records) assert any(("setup.cfg" in r.message) for r in caplog.records) @@ -486,13 +486,13 @@ def test_bump_semver_warning(runner, caplog): _vcs_init("hg", files=["README.md", "setup.cfg"]) - result = runner.invoke(cli.cli, ['bump', "-vv", "-n", "--dry"]) + result = runner.invoke(cli, ['bump', "-vv", "-n", "--dry"]) assert result.exit_code == 1 assert any("version did not change" in r.message for r in caplog.records) assert any("--major/--minor/--patch required" in r.message for r in caplog.records) - result = runner.invoke(cli.cli, ['bump', "-vv", "-n", "--dry", "--patch"]) + result = runner.invoke(cli, ['bump', "-vv", "-n", "--dry", "--patch"]) assert result.exit_code == 0 @@ -507,7 +507,7 @@ def test_bump_semver_diff(runner, caplog): cases = [("--major", "1.0.0"), ("--minor", "0.2.0"), ("--patch", "0.1.1")] for flag, expected in cases: - result = runner.invoke(cli.cli, ['bump', "-vv", "-n", "--dry", flag]) + result = runner.invoke(cli, ['bump', "-vv", "-n", "--dry", flag]) assert result.exit_code == 0 assert len(caplog.records) == 0 From 75563672b47a51169ef4ea30abf2ca9ab509a2bf Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 6 Sep 2020 21:15:27 +0000 Subject: [PATCH 05/98] cli -> __main__ refactor --- pylint-ignore.md | 319 +++++++++++++++++++++++++---------------------- setup.py | 2 +- test/util.py | 2 +- 3 files changed, 169 insertions(+), 154 deletions(-) diff --git a/pylint-ignore.md b/pylint-ignore.md index 3bdf018..8cc8b32 100644 --- a/pylint-ignore.md +++ b/pylint-ignore.md @@ -1,204 +1,219 @@ -# `pylint-ignore` +# Pylint-Ignore **WARNING: This file is programatically generated.** -This file is parsed by `pylint-ignore` to determine which `pylint` -messages should be ignored. +This file is parsed by [`pylint-ignore`](https://pypi.org/project/pylint-ignore/) +to determine which +[Pylint messages](https://pylint.pycqa.org/en/stable/technical_reference/features.html) +should be ignored. - Do not edit this file manually. - To update, use `pylint-ignore --update-ignorefile` The recommended approach to using `pylint-ignore` is: -- If a message refers to a valid issue, update your code rather than - ignoring the message. -- If a message should *always* be ignored (globally), then to do so - via the usual `pylintrc` or `setup.cfg` files rather than this - `pylint-ignore.md` file. -- If a message is a false positive, add a comment of this form to your code: - `# pylint:disable= ; explanation why this is a false positive` +1. If a message refers to a valid issue, update your code rather than + ignoring the message. +2. If a message should *always* be ignored (globally), then to do so + via the usual `pylintrc` or `setup.cfg` files rather than this + `pylint-ignore.md` file. +3. If a message is a false positive, add a comment of this form to your code: + `# pylint:disable= ; explain why this is a false positive` -## File test/test_version.py - Line 145 - E1101 (no-member) +# Overview -- `message: Class 'VersionInfo' has no '_fields' member` -- `author : Manuel Barkhau ` -- `date : 2020-07-19T19:06:44` - -``` - 134: def test_part_field_mapping(): - ... - 143: - 144: a_fields = set(version.PATTERN_PART_FIELDS.values()) -> 145: b_fields = set(version.VersionInfo._fields) - 146: - 147: assert a_fields == b_fields -``` + - [W0511: fixme (5x)](#w0511-fixme) + - [W0603: global-statement (1x)](#w0603-global-statement) + - [W0613: unused-argument (1x)](#w0613-unused-argument) + - [W0703: broad-except (2x)](#w0703-broad-except) + - [C0412: ungrouped-imports (2x)](#c0412-ungrouped-imports) -## File src/pycalver/rewrite.py - Line 168 - E1101 (no-member) +# W0511: fixme -- `message: Instance of 'RewrittenFileData' has no '_replace' member` -- `author : Manuel Barkhau ` -- `date : 2020-07-19T18:50:33` - -``` - 138: def iter_rewritten( - ... - 166: - 167: rfd = rfd_from_content(pattern_strs, new_vinfo, content) -> 168: yield rfd._replace(path=str(file_path)) - 169: - 170: -``` - - -## File src/pycalver/rewrite.py - Line 217 - E1101 (no-member) - -- `message: Instance of 'RewrittenFileData' has no '_replace' member` -- `author : Manuel Barkhau ` -- `date : 2020-07-19T18:50:33` - -``` - 189: def diff(new_vinfo: version.VersionInfo, file_patterns: config.PatternsByGlob) -> str: - ... - 215: raise NoPatternMatch(errmsg) - 216: -> 217: rfd = rfd._replace(path=str(file_path)) - 218: lines = diff_lines(rfd) - 219: if len(lines) == 0: -``` - - -## File src/pycalver/version.py - Line 235 - E1101 (no-member) - -- `message: Class 'CalendarInfo' has no '_fields' member` -- `author : Manuel Barkhau ` -- `date : 2020-07-19T18:50:33` - -``` - 221: def _is_calver(nfo: typ.Union[CalendarInfo, VersionInfo]) -> bool: - ... - 233: False - 234: """ -> 235: for field in CalendarInfo._fields: - 236: maybe_val: typ.Any = getattr(nfo, field, None) - 237: if isinstance(maybe_val, int): -``` - - -## File src/pycalver/version.py - Line 481 - E1101 (no-member) - -- `message: Instance of 'CalendarInfo' has no '_asdict' member` -- `author : Manuel Barkhau ` -- `date : 2020-07-19T18:50:33` - -``` - 454: def incr( - ... - 479: - 480: if old_date <= cur_date: -> 481: cur_vinfo = cur_vinfo._replace(**cur_cal_nfo._asdict()) - 482: else: - 483: logger.warning(f"Version appears to be from the future '{old_version}'") -``` - - -## File src/pycalver/version.py - Line 481 - E1101 (no-member) - -- `message: Instance of 'VersionInfo' has no '_replace' member` -- `author : Manuel Barkhau ` -- `date : 2020-07-19T18:50:33` - -``` - 454: def incr( - ... - 479: - 480: if old_date <= cur_date: -> 481: cur_vinfo = cur_vinfo._replace(**cur_cal_nfo._asdict()) - 482: else: - 483: logger.warning(f"Version appears to be from the future '{old_version}'") -``` - - -## File test/util.py - Line 10 - R0903 (too-few-public-methods) - -- `message: Too few public methods (1/2)` -- `author : Manuel Barkhau ` -- `date : 2020-07-19T19:06:44` - -``` - 8: - 9: -> 10: class Shell: - 11: def __init__(self, cwd): - 12: self.cwd = cwd -``` - - -## File src/pycalver/vcs.py - Line 75 - W0511 (fixme) +## File src/pycalver/vcs.py - Line 78 - W0511 (fixme) - `message: TODO (mb 2018-11-15): Detect encoding of output?` - `author : Manuel Barkhau ` - `date : 2020-07-19T18:50:33` ``` - 65: def __call__(self, cmd_name: str, env: Env = None, **kwargs: str) -> str: + 68: def __call__(self, cmd_name: str, env: Env = None, **kwargs: str) -> str: ... - 73: output_data: bytes = sp.check_output(cmd_str.split(), env=env, stderr=sp.STDOUT) - 74: -> 75: # TODO (mb 2018-11-15): Detect encoding of output? - 76: _encoding = "utf-8" - 77: return output_data.decode(_encoding) + 76: output_data: bytes = sp.check_output(cmd_str.split(), env=env, stderr=sp.STDOUT) + 77: +> 78: # TODO (mb 2018-11-15): Detect encoding of output? + 79: _encoding = "utf-8" + 80: return output_data.decode(_encoding) ``` -## File src/pycalver/cli.py - Line 78 - W0603 (global-statement) +## File src/pycalver2/version.py - Line 184 - W0511 (fixme) + +- `message: TODO (mb 2020-09-05): pytag` +- `author : Manuel Barkhau ` +- `date : 2020-09-05T14:30:17` + +``` + 176: def _parse_field_values(field_values: FieldValues) -> VersionInfo: + ... + 182: tag = TAG_ALIASES.get(tag, tag) + 183: assert tag is not None +> 184: # TODO (mb 2020-09-05): pytag + 185: pytag = "" + 186: +``` + + +## File src/pycalver/__main__.py - Line 220 - W0511 (fixme) + +- `message: TODO (mb 2020-09-05): version switch` +- `author : Manuel Barkhau ` +- `date : 2020-09-05T14:30:17` + +``` + 200: def _bump( + ... + 218: + 219: try: +> 220: # TODO (mb 2020-09-05): version switch + 221: v1cli.rewrite(cfg, new_version) + 222: v2cli.rewrite(cfg, new_version) +``` + + +## File src/pycalver/__main__.py - Line 274 - W0511 (fixme) + +- `message: TODO (mb 2020-09-05): version switch` +- `author : Manuel Barkhau ` +- `date : 2020-09-05T14:30:17` + +``` + 272: + 273: def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: +> 274: # TODO (mb 2020-09-05): version switch + 275: all_tags = vcs.get_tags(fetch=fetch) + 276: +``` + + +## File src/pycalver/__main__.py - Line 378 - W0511 (fixme) + +- `message: # TODO (mb 2020-09-05): format from config` +- `author : Manuel Barkhau ` +- `date : 2020-09-05T14:30:17` + +``` + 324: def bump( + ... + 376: return + 377: +> 378: # # TODO (mb 2020-09-05): format from config + 379: # commit_message_kwargs = { + 380: # new_version +``` + + +# W0603: global-statement + +## File src/pycalver/__main__.py - Line 75 - W0603 (global-statement) - `message: Using the global statement` - `author : Manuel Barkhau ` -- `date : 2020-07-19T18:50:33` +- `date : 2020-09-05T14:30:17` ``` - 76: def cli(verbose: int = 0) -> None: - 77: """Automatically update PyCalVer version strings on python projects.""" -> 78: global _VERBOSE - 79: _VERBOSE = verbose - 80: + 73: def cli(verbose: int = 0) -> None: + 74: """Automatically update PyCalVer version strings on python projects.""" +> 75: global _VERBOSE + 76: _VERBOSE = verbose + 77: ``` -## File src/pycalver/vcs.py - Line 104 - W0703 (broad-except) +# W0613: unused-argument + +## File src/pycalver2/version.py - Line 431 - W0613 (unused-argument) + +- `message: Unused argument 'kwargs'` +- `author : Manuel Barkhau ` +- `date : 2020-09-05T14:30:17` + +``` + 429: + 430: +> 431: def _compile_format_template(pattern: str, kwargs: TemplateKwargs) -> str: + 432: # NOTE (mb 2020-09-04): Some parts are optional, we need the kwargs to + 433: # determine if part is set to its zero value +``` + + +# W0703: broad-except + +## File src/pycalver/vcs.py - Line 107 - W0703 (broad-except) - `message: Catching too general exception Exception` - `author : Manuel Barkhau ` - `date : 2020-07-19T18:50:33` ``` - 98: def has_remote(self) -> bool: + 101: def has_remote(self) -> bool: ... - 102: return False - 103: return True -> 104: except Exception: - 105: return False - 106: + 105: return False + 106: return True +> 107: except Exception: + 108: return False + 109: ``` -## File src/pycalver/cli.py - Line 292 - W0703 (broad-except) +## File src/pycalver/__main__.py - Line 223 - W0703 (broad-except) - `message: Catching too general exception Exception` - `author : Manuel Barkhau ` -- `date : 2020-07-19T18:50:33` +- `date : 2020-09-05T14:30:17` ``` - 289: def _try_print_diff(cfg: config.Config, new_version: str) -> None: + 200: def _bump( ... - 290: try: - 291: _print_diff(cfg, new_version) -> 292: except Exception as ex: - 293: logger.error(str(ex)) - 294: sys.exit(1) + 221: v1cli.rewrite(cfg, new_version) + 222: v2cli.rewrite(cfg, new_version) +> 223: except Exception as ex: + 224: logger.error(str(ex)) + 225: sys.exit(1) +``` + + +# C0412: ungrouped-imports + +## File src/pycalver/__main__.py - Line 21 - C0412 (ungrouped-imports) + +- `message: Imports from package pycalver are not grouped` +- `author : Manuel Barkhau ` +- `date : 2020-09-05T14:30:17` + +``` + 19: import pycalver.cli as v1cli + 20: import pycalver2.cli as v2cli +> 21: import pycalver.version as v1version + 22: import pycalver2.version as v2version + 23: from pycalver import vcs +``` + + +## File src/pycalver/__main__.py - Line 22 - C0412 (ungrouped-imports) + +- `message: Imports from package pycalver2 are not grouped` +- `author : Manuel Barkhau ` +- `date : 2020-09-05T14:30:17` + +``` + 20: import pycalver2.cli as v2cli + 21: import pycalver.version as v1version +> 22: import pycalver2.version as v2version + 23: from pycalver import vcs + 24: from pycalver import config ``` diff --git a/setup.py b/setup.py index 8cf0107..5ad112f 100644 --- a/setup.py +++ b/setup.py @@ -91,7 +91,7 @@ setuptools.setup( install_requires=install_requires, entry_points=""" [console_scripts] - pycalver=pycalver.cli:cli + pycalver=pycalver.__main__:cli """, python_requires=">=2.7", zip_safe=True, diff --git a/test/util.py b/test/util.py index e6b9acb..d9d4f68 100644 --- a/test/util.py +++ b/test/util.py @@ -76,7 +76,7 @@ class Project: for path_parts in FIXTURE_PATH_PARTS: maybe_file_path = self.dir.joinpath(*path_parts) if maybe_file_path.exists(): - self.shell(f"{cmd} add {str(maybe_file_path)}") + self.shell(cmd + " add " + str(maybe_file_path)) added_file_paths.append(maybe_file_path) assert len(added_file_paths) >= 2 From 31f9d51bf395e67996d0646e87f2e3771bec3cb6 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 6 Sep 2020 21:44:23 +0000 Subject: [PATCH 06/98] fmt lint etc. pp. --- src/pycalver/__main__.py | 5 +++-- src/pycalver/rewrite.py | 11 ++++++++--- src/pycalver2/cli.py | 1 + src/pycalver2/rewrite.py | 22 +++++++++++++++------- test/test_cli.py | 2 +- test/test_patterns.py | 2 +- test/test_rewrite.py | 5 +++-- 7 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 06b41cd..7149fc2 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -15,14 +15,15 @@ import logging import subprocess as sp import click +import pycalver2.version as v2version import pycalver.cli as v1cli -import pycalver2.cli as v2cli import pycalver.version as v1version -import pycalver2.version as v2version from pycalver import vcs from pycalver import config +# import pycalver2.cli as v2cli + _VERBOSE = 0 diff --git a/src/pycalver/rewrite.py b/src/pycalver/rewrite.py index c06fa7d..ddc5146 100644 --- a/src/pycalver/rewrite.py +++ b/src/pycalver/rewrite.py @@ -77,7 +77,9 @@ def iter_file_paths( def rewrite_lines( - pattern_strs: typ.List[str], new_vinfo: version.VersionInfo, old_lines: typ.List[str] + pattern_strs: typ.List[str], + new_vinfo : version.VersionInfo, + old_lines : typ.List[str], ) -> typ.List[str]: """Replace occurances of pattern_strs in old_lines with new_vinfo. @@ -112,7 +114,9 @@ def rewrite_lines( def rfd_from_content( - pattern_strs: typ.List[str], new_vinfo: version.VersionInfo, content: str + pattern_strs: typ.List[str], + new_vinfo : version.VersionInfo, + content : str, ) -> RewrittenFileData: r"""Rewrite pattern occurrences with version string. @@ -137,7 +141,8 @@ def rfd_from_content( def iter_rewritten( - file_patterns: config.PatternsByGlob, new_vinfo: version.VersionInfo + file_patterns: config.PatternsByGlob, + new_vinfo : version.VersionInfo, ) -> typ.Iterable[RewrittenFileData]: r'''Iterate over files with version string replaced. diff --git a/src/pycalver2/cli.py b/src/pycalver2/cli.py index 107d4b8..23d4913 100644 --- a/src/pycalver2/cli.py +++ b/src/pycalver2/cli.py @@ -14,6 +14,7 @@ import logging import pycalver2.rewrite as v2rewrite import pycalver2.version as v2version + from pycalver import config logger = logging.getLogger("pycalver2.cli") diff --git a/src/pycalver2/rewrite.py b/src/pycalver2/rewrite.py index 7595099..c5592d7 100644 --- a/src/pycalver2/rewrite.py +++ b/src/pycalver2/rewrite.py @@ -9,17 +9,20 @@ import io import typing as typ import logging +from pycalver2 import version +from pycalver2 import patterns + from pycalver import parse from pycalver import config from pycalver import rewrite as v1rewrite -from pycalver2 import version -from pycalver2 import patterns logger = logging.getLogger("pycalver2.rewrite") def rewrite_lines( - pattern_strs: typ.List[str], new_vinfo: version.VersionInfo, old_lines: typ.List[str] + pattern_strs: typ.List[str], + new_vinfo : version.VersionInfo, + old_lines : typ.List[str], ) -> typ.List[str]: """TODO reenable doctest""" pass @@ -38,8 +41,10 @@ def rewrite_lines( new_lines = old_lines[:] found_patterns = set() - re_patterns = [patterns.compile_pattern(p) for p in pattern_strs] - for match in parse.iter_matches(old_lines, re_patterns): + # re_patterns = [patterns.compile_pattern(p) for p in pattern_strs] + # matches = parse.iter_matches(old_lines, re_patterns) + matches = parse.iter_matches(old_lines, pattern_strs) + for match in matches: found_patterns.add(match.pattern) replacement = version.format_version(new_vinfo, match.pattern) span_l, span_r = match.span @@ -58,7 +63,9 @@ def rewrite_lines( def rfd_from_content( - pattern_strs: typ.List[str], new_vinfo: version.VersionInfo, content: str + pattern_strs: typ.List[str], + new_vinfo : version.VersionInfo, + content : str, ) -> v1rewrite.RewrittenFileData: """TODO reenable doctest""" pass @@ -86,7 +93,8 @@ def rfd_from_content( def iter_rewritten( - file_patterns: config.PatternsByGlob, new_vinfo: version.VersionInfo + file_patterns: config.PatternsByGlob, + new_vinfo : version.VersionInfo, ) -> typ.Iterable[v1rewrite.RewrittenFileData]: """TODO reenable doctest""" pass diff --git a/test/test_cli.py b/test/test_cli.py index 38355a2..fcac244 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -8,10 +8,10 @@ import subprocess as sp import pytest import pathlib2 as pl +import pycalver2.patterns as patterns from click.testing import CliRunner import pycalver.config as config -import pycalver2.patterns as patterns from pycalver.__main__ import cli SETUP_CFG_FIXTURE = """ diff --git a/test/test_patterns.py b/test/test_patterns.py index 61d8d48..89c44b2 100644 --- a/test/test_patterns.py +++ b/test/test_patterns.py @@ -1,9 +1,9 @@ import re import pytest +import pycalver2.patterns as v2patterns import pycalver.patterns as v1patterns -import pycalver2.patterns as v2patterns # TODO (mb 2020-09-06): test for v2patterns diff --git a/test/test_rewrite.py b/test/test_rewrite.py index 1eb0dd7..7d2693c 100644 --- a/test/test_rewrite.py +++ b/test/test_rewrite.py @@ -3,11 +3,12 @@ import copy from test import util +from pycalver2 import rewrite as v2rewrite +from pycalver2 import version as v2version + from pycalver import config from pycalver import rewrite as v1rewrite from pycalver import version as v1version -from pycalver2 import rewrite as v2rewrite -from pycalver2 import version as v2version REWRITE_FIXTURE = """ # SPDX-License-Identifier: MIT From e053af589ef2627859308a8c12641c3f905ff0b4 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Mon, 7 Sep 2020 21:43:55 +0000 Subject: [PATCH 07/98] wip refactoring --- setup.cfg | 8 +++++++- src/pycalver/__main__.py | 2 +- src/pycalver/rewrite.py | 29 ++++++++++++++--------------- src/pycalver/version.py | 8 ++++---- src/pycalver2/cli.py | 1 - src/pycalver2/patterns.py | 2 ++ src/pycalver2/rewrite.py | 5 ++--- src/pycalver2/version.py | 9 +++++---- test/test_cli.py | 2 +- test/test_patterns.py | 2 +- test/test_rewrite.py | 5 ++--- 11 files changed, 39 insertions(+), 34 deletions(-) diff --git a/setup.cfg b/setup.cfg index 8d1258d..d638129 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,7 +19,8 @@ warn_redundant_casts = True [tool:isort] -known_third_party = click,pathlib2 +known_first_party = pycalver,pycalver2 +known_third_party = click,pathlib2,lexid force_single_line = True length_sort = True @@ -161,3 +162,8 @@ disable = missing-class-docstring, missing-function-docstring, raise-missing-from, + duplicate-code, + +generated-members = + # members of typing.NamedTuple + "(_replace|_asdict|_fields)", diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 7149fc2..2f10642 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -15,10 +15,10 @@ import logging import subprocess as sp import click -import pycalver2.version as v2version import pycalver.cli as v1cli import pycalver.version as v1version +import pycalver2.version as v2version from pycalver import vcs from pycalver import config diff --git a/src/pycalver/rewrite.py b/src/pycalver/rewrite.py index ddc5146..fffdc52 100644 --- a/src/pycalver/rewrite.py +++ b/src/pycalver/rewrite.py @@ -13,12 +13,11 @@ import logging import pathlib2 as pl +import pycalver.version as v1version +import pycalver.patterns as v1patterns from pycalver import parse from pycalver import config -from . import version -from . import patterns - logger = logging.getLogger("pycalver.rewrite") @@ -78,12 +77,12 @@ def iter_file_paths( def rewrite_lines( pattern_strs: typ.List[str], - new_vinfo : version.VersionInfo, + new_vinfo : v1version.VersionInfo, old_lines : typ.List[str], ) -> typ.List[str]: """Replace occurances of pattern_strs in old_lines with new_vinfo. - >>> new_vinfo = version.parse_version_info("v201811.0123-beta") + >>> new_vinfo = v1version.parse_version_info("v201811.0123-beta") >>> pattern_strs = ['__version__ = "{pycalver}"'] >>> rewrite_lines(pattern_strs, new_vinfo, ['__version__ = "v201809.0002-beta"']) ['__version__ = "v201811.0123-beta"'] @@ -97,7 +96,7 @@ def rewrite_lines( for match in parse.iter_matches(old_lines, pattern_strs): found_patterns.add(match.pattern) - replacement = version.format_version(new_vinfo, match.pattern) + replacement = v1version.format_version(new_vinfo, match.pattern) span_l, span_r = match.span new_line = match.line[:span_l] + replacement + match.line[span_r:] new_lines[match.lineno] = new_line @@ -106,7 +105,7 @@ def rewrite_lines( if non_matched_patterns: for non_matched_pattern in non_matched_patterns: logger.error(f"No match for pattern '{non_matched_pattern}'") - compiled_pattern_str = patterns.compile_pattern_str(non_matched_pattern) + compiled_pattern_str = v1patterns.compile_pattern_str(non_matched_pattern) logger.error(f"Pattern compiles to regex '{compiled_pattern_str}'") raise NoPatternMatch("Invalid pattern(s)") else: @@ -115,19 +114,19 @@ def rewrite_lines( def rfd_from_content( pattern_strs: typ.List[str], - new_vinfo : version.VersionInfo, + new_vinfo : v1version.VersionInfo, content : str, ) -> RewrittenFileData: r"""Rewrite pattern occurrences with version string. - >>> new_vinfo = version.parse_version_info("v201809.0123") + >>> new_vinfo = v1version.parse_version_info("v201809.0123") >>> pattern_strs = ['__version__ = "{pycalver}"'] >>> content = '__version__ = "v201809.0001-alpha"' >>> rfd = rfd_from_content(pattern_strs, new_vinfo, content) >>> rfd.new_lines ['__version__ = "v201809.0123"'] >>> - >>> new_vinfo = version.parse_version_info("v1.2.3", "v{semver}") + >>> new_vinfo = v1version.parse_version_info("v1.2.3", "v{semver}") >>> pattern_strs = ['__version__ = "v{semver}"'] >>> content = '__version__ = "v1.2.2"' >>> rfd = rfd_from_content(pattern_strs, new_vinfo, content) @@ -142,12 +141,12 @@ def rfd_from_content( def iter_rewritten( file_patterns: config.PatternsByGlob, - new_vinfo : version.VersionInfo, + new_vinfo : v1version.VersionInfo, ) -> typ.Iterable[RewrittenFileData]: r'''Iterate over files with version string replaced. >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']} - >>> new_vinfo = version.parse_version_info("v201809.0123") + >>> new_vinfo = v1version.parse_version_info("v201809.0123") >>> rewritten_datas = iter_rewritten(file_patterns, new_vinfo) >>> rfd = list(rewritten_datas)[0] >>> expected = [ @@ -192,10 +191,10 @@ def diff_lines(rfd: RewrittenFileData) -> typ.List[str]: return list(lines) -def diff(new_vinfo: version.VersionInfo, file_patterns: config.PatternsByGlob) -> str: +def diff(new_vinfo: v1version.VersionInfo, file_patterns: config.PatternsByGlob) -> str: r"""Generate diffs of rewritten files. - >>> new_vinfo = version.parse_version_info("v201809.0123") + >>> new_vinfo = v1version.parse_version_info("v201809.0123") >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']} >>> diff_str = diff(new_vinfo, file_patterns) >>> lines = diff_str.split("\n") @@ -233,7 +232,7 @@ def diff(new_vinfo: version.VersionInfo, file_patterns: config.PatternsByGlob) - return full_diff -def rewrite(file_patterns: config.PatternsByGlob, new_vinfo: version.VersionInfo) -> None: +def rewrite(file_patterns: config.PatternsByGlob, new_vinfo: v1version.VersionInfo) -> None: """Rewrite project files, updating each with the new version.""" fobj: typ.IO[str] diff --git a/src/pycalver/version.py b/src/pycalver/version.py index 25afe53..e6597e0 100644 --- a/src/pycalver/version.py +++ b/src/pycalver/version.py @@ -12,7 +12,7 @@ import datetime as dt import lexid import pkg_resources -from . import patterns +import pycalver.patterns as v1patterns logger = logging.getLogger("pycalver.version") @@ -263,7 +263,7 @@ class PatternError(Exception): def _parse_pattern_groups(pattern_groups: PatternGroups) -> FieldValues: for part_name in pattern_groups.keys(): is_valid_part_name = ( - part_name in patterns.COMPOSITE_PART_PATTERNS or part_name in PATTERN_PART_FIELDS + part_name in v1patterns.COMPOSITE_PART_PATTERNS or part_name in PATTERN_PART_FIELDS ) if not is_valid_part_name: err_msg = f"Invalid part '{part_name}'" @@ -318,7 +318,7 @@ def parse_version_info(version_str: str, pattern: str = "{pycalver}") -> Version >>> vnfo = parse_version_info("1.23.456", pattern="{semver}") >>> assert vnfo == _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "456"}) """ - regex = patterns.compile_pattern(pattern) + regex = v1patterns.compile_pattern(pattern) match = regex.match(version_str) if match is None: err_msg = ( @@ -414,7 +414,7 @@ def format_version(vinfo: VersionInfo, pattern: str) -> str: 'v1.02.034' """ full_pattern = pattern - for part_name, full_part_format in patterns.FULL_PART_FORMATS.items(): + for part_name, full_part_format in v1patterns.FULL_PART_FORMATS.items(): full_pattern = full_pattern.replace("{" + part_name + "}", full_part_format) kwargs: typ.Dict[str, typ.Union[str, int, None]] = vinfo._asdict() diff --git a/src/pycalver2/cli.py b/src/pycalver2/cli.py index 23d4913..107d4b8 100644 --- a/src/pycalver2/cli.py +++ b/src/pycalver2/cli.py @@ -14,7 +14,6 @@ import logging import pycalver2.rewrite as v2rewrite import pycalver2.version as v2version - from pycalver import config logger = logging.getLogger("pycalver2.cli") diff --git a/src/pycalver2/patterns.py b/src/pycalver2/patterns.py index 07bae57..82cc585 100644 --- a/src/pycalver2/patterns.py +++ b/src/pycalver2/patterns.py @@ -176,6 +176,8 @@ FULL_PART_FORMATS = { def _replace_pattern_parts(pattern: str) -> str: + # The pattern is escaped, so that everything besides the format + # string variables is treated literally. for part_name, part_pattern in PART_PATTERNS.items(): named_part_pattern = f"(?P<{part_name}>{part_pattern})" placeholder = "\u005c{" + part_name + "\u005c}" diff --git a/src/pycalver2/rewrite.py b/src/pycalver2/rewrite.py index c5592d7..e2baeb2 100644 --- a/src/pycalver2/rewrite.py +++ b/src/pycalver2/rewrite.py @@ -9,12 +9,11 @@ import io import typing as typ import logging -from pycalver2 import version -from pycalver2 import patterns - from pycalver import parse from pycalver import config from pycalver import rewrite as v1rewrite +from pycalver2 import version +from pycalver2 import patterns logger = logging.getLogger("pycalver2.rewrite") diff --git a/src/pycalver2/version.py b/src/pycalver2/version.py index 4abaacf..ea78d3f 100644 --- a/src/pycalver2/version.py +++ b/src/pycalver2/version.py @@ -12,7 +12,8 @@ import datetime as dt import lexid import pkg_resources -from . import patterns +# import pycalver.patterns as v1patterns +import pycalver2.patterns as v2patterns logger = logging.getLogger("pycalver.version") @@ -300,7 +301,7 @@ class PatternError(Exception): def _parse_pattern_groups(pattern_groups: PatternGroups) -> FieldValues: for part_name in pattern_groups.keys(): is_valid_part_name = ( - part_name in patterns.COMPOSITE_PART_PATTERNS or part_name in PATTERN_PART_FIELDS + part_name in v2patterns.COMPOSITE_PART_PATTERNS or part_name in PATTERN_PART_FIELDS ) if not is_valid_part_name: err_msg = f"Invalid part '{part_name}'" @@ -361,7 +362,7 @@ def parse_version_info(version_str: str, pattern: str = "{pycalver}") -> Version >>> vnfo = parse_version_info("1.23.456", pattern="{semver}") >>> assert vnfo == _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "456"}) """ - regex = patterns.compile_pattern(pattern) + regex = v2patterns.compile_pattern(pattern) match = regex.match(version_str) if match is None: err_msg = ( @@ -447,7 +448,7 @@ def _compile_format_template(pattern: str, kwargs: TemplateKwargs) -> str: # NOTE (mb 2020-09-04): Some parts are optional, we need the kwargs to # determine if part is set to its zero value format_tmpl = pattern - for part_name, full_part_format in patterns.FULL_PART_FORMATS.items(): + for part_name, full_part_format in v2patterns.FULL_PART_FORMATS.items(): format_tmpl = format_tmpl.replace("{" + part_name + "}", full_part_format) return format_tmpl diff --git a/test/test_cli.py b/test/test_cli.py index fcac244..38355a2 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -8,10 +8,10 @@ import subprocess as sp import pytest import pathlib2 as pl -import pycalver2.patterns as patterns from click.testing import CliRunner import pycalver.config as config +import pycalver2.patterns as patterns from pycalver.__main__ import cli SETUP_CFG_FIXTURE = """ diff --git a/test/test_patterns.py b/test/test_patterns.py index 89c44b2..61d8d48 100644 --- a/test/test_patterns.py +++ b/test/test_patterns.py @@ -1,9 +1,9 @@ import re import pytest -import pycalver2.patterns as v2patterns import pycalver.patterns as v1patterns +import pycalver2.patterns as v2patterns # TODO (mb 2020-09-06): test for v2patterns diff --git a/test/test_rewrite.py b/test/test_rewrite.py index 7d2693c..1eb0dd7 100644 --- a/test/test_rewrite.py +++ b/test/test_rewrite.py @@ -3,12 +3,11 @@ import copy from test import util -from pycalver2 import rewrite as v2rewrite -from pycalver2 import version as v2version - from pycalver import config from pycalver import rewrite as v1rewrite from pycalver import version as v1version +from pycalver2 import rewrite as v2rewrite +from pycalver2 import version as v2version REWRITE_FIXTURE = """ # SPDX-License-Identifier: MIT From 3cbf0e82b13b8f085b1ed6348feb786ac7ed19be Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Tue, 8 Sep 2020 20:59:52 +0000 Subject: [PATCH 08/98] setup to reduce code duplication --- src/pycalver/parse.py | 20 ++- src/pycalver/patterns.py | 16 ++- src/pycalver/rewrite.py | 8 +- src/pycalver/version.py | 7 +- src/pycalver2/patterns.py | 7 +- src/pycalver2/rewrite.py | 152 +++++++++++----------- src/pycalver2/version.py | 257 ++++++++++++++++++-------------------- test/test_parse.py | 32 ++--- test/test_patterns.py | 20 +-- test/test_version.py | 19 +-- 10 files changed, 271 insertions(+), 267 deletions(-) diff --git a/src/pycalver/parse.py b/src/pycalver/parse.py index aceb631..427d30c 100644 --- a/src/pycalver/parse.py +++ b/src/pycalver/parse.py @@ -7,7 +7,7 @@ import typing as typ -from .patterns import compile_pattern +import pycalver.patterns as v1patterns class PatternMatch(typ.NamedTuple): @@ -15,37 +15,33 @@ class PatternMatch(typ.NamedTuple): lineno : int # zero based line : str - pattern: str + pattern: v1patterns.Pattern span : typ.Tuple[int, int] match : str PatternMatches = typ.Iterable[PatternMatch] -RegexpPatterns = typ.List[typ.Pattern[str]] - - -def _iter_for_pattern(lines: typ.List[str], pattern: str) -> PatternMatches: - # The pattern is escaped, so that everything besides the format - # string variables is treated literally. - pattern_re = compile_pattern(pattern) +def _iter_for_pattern(lines: typ.List[str], pattern: v1patterns.Pattern) -> PatternMatches: for lineno, line in enumerate(lines): - match = pattern_re.search(line) + match = pattern.regexp.search(line) if match: yield PatternMatch(lineno, line, pattern, match.span(), match.group(0)) -def iter_matches(lines: typ.List[str], patterns: typ.List[str]) -> PatternMatches: +def iter_matches(lines: typ.List[str], patterns: typ.List[v1patterns.Pattern]) -> PatternMatches: """Iterate over all matches of any pattern on any line. + >>> import pycalver.patterns as v1patterns >>> lines = ["__version__ = 'v201712.0002-alpha'"] >>> patterns = ["{pycalver}", "{pep440_pycalver}"] + >>> patterns = [v1patterns.compile_pattern(p) for p in patterns] >>> matches = list(iter_matches(lines, patterns)) >>> assert matches[0] == PatternMatch( ... lineno = 0, ... line = "__version__ = 'v201712.0002-alpha'", - ... pattern= "{pycalver}", + ... pattern= v1patterns.compile_pattern("{pycalver}"), ... span = (15, 33), ... match = "v201712.0002-alpha", ... ) diff --git a/src/pycalver/patterns.py b/src/pycalver/patterns.py index 23af859..9f9fdc2 100644 --- a/src/pycalver/patterns.py +++ b/src/pycalver/patterns.py @@ -177,7 +177,18 @@ PART_FORMATS = { } +class Pattern(typ.NamedTuple): + + raw : str # "{pycalver}", "{year}.{month}", "YYYY0M.BUILD" + regexp: typ.Pattern[str] + + +Patterns = typ.List[typ.Pattern[str]] + + def _replace_pattern_parts(pattern: str) -> str: + # The pattern is escaped, so that everything besides the format + # string variables is treated literally. for part_name, part_pattern in PART_PATTERNS.items(): named_part_pattern = f"(?P<{part_name}>{part_pattern})" placeholder = "\u005c{" + part_name + "\u005c}" @@ -192,9 +203,10 @@ def compile_pattern_str(pattern: str) -> str: return _replace_pattern_parts(pattern) -def compile_pattern(pattern: str) -> typ.Pattern[str]: +def compile_pattern(pattern: str) -> Pattern: pattern_str = compile_pattern_str(pattern) - return re.compile(pattern_str) + pattern_re = re.compile(pattern_str) + return Pattern(pattern, pattern_re) def _init_composite_patterns() -> None: diff --git a/src/pycalver/rewrite.py b/src/pycalver/rewrite.py index fffdc52..f63dc5a 100644 --- a/src/pycalver/rewrite.py +++ b/src/pycalver/rewrite.py @@ -94,9 +94,11 @@ def rewrite_lines( new_lines = old_lines[:] found_patterns = set() - for match in parse.iter_matches(old_lines, pattern_strs): - found_patterns.add(match.pattern) - replacement = v1version.format_version(new_vinfo, match.pattern) + patterns = [v1patterns.compile_pattern(p) for p in pattern_strs] + matches = parse.iter_matches(old_lines, patterns) + for match in matches: + found_patterns.add(match.pattern.raw) + replacement = v1version.format_version(new_vinfo, match.pattern.raw) span_l, span_r = match.span new_line = match.line[:span_l] + replacement + match.line[span_r:] new_lines[match.lineno] = new_line diff --git a/src/pycalver/version.py b/src/pycalver/version.py index e6597e0..ffcf927 100644 --- a/src/pycalver/version.py +++ b/src/pycalver/version.py @@ -318,11 +318,12 @@ def parse_version_info(version_str: str, pattern: str = "{pycalver}") -> Version >>> vnfo = parse_version_info("1.23.456", pattern="{semver}") >>> assert vnfo == _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "456"}) """ - regex = v1patterns.compile_pattern(pattern) - match = regex.match(version_str) + pattern_tup = v1patterns.compile_pattern(pattern) + match = pattern_tup.regexp.match(version_str) if match is None: err_msg = ( - f"Invalid version string '{version_str}' for pattern '{pattern}'/'{regex.pattern}'" + f"Invalid version string '{version_str}' " + f"for pattern '{pattern}'/'{pattern_tup.regexp.pattern}'" ) raise PatternError(err_msg) diff --git a/src/pycalver2/patterns.py b/src/pycalver2/patterns.py index 82cc585..3db6736 100644 --- a/src/pycalver2/patterns.py +++ b/src/pycalver2/patterns.py @@ -33,6 +33,8 @@ import re import typing as typ +import pycalver.patterns as v1patterns + # https://regex101.com/r/fnj60p/10 PYCALVER_PATTERN = r""" \b @@ -192,9 +194,10 @@ def compile_pattern_str(pattern: str) -> str: return _replace_pattern_parts(pattern) -def compile_pattern(pattern: str) -> typ.Pattern[str]: +def compile_pattern(pattern: str) -> v1patterns.Pattern: pattern_str = compile_pattern_str(pattern) - return re.compile(pattern_str) + pattern_re = re.compile(pattern_str) + return v1patterns.Pattern(pattern, pattern_re) def _init_composite_patterns() -> None: diff --git a/src/pycalver2/rewrite.py b/src/pycalver2/rewrite.py index e2baeb2..49cd6bb 100644 --- a/src/pycalver2/rewrite.py +++ b/src/pycalver2/rewrite.py @@ -9,43 +9,40 @@ import io import typing as typ import logging +import pycalver2.version as v2version +import pycalver2.patterns as v2patterns from pycalver import parse from pycalver import config from pycalver import rewrite as v1rewrite -from pycalver2 import version -from pycalver2 import patterns logger = logging.getLogger("pycalver2.rewrite") def rewrite_lines( pattern_strs: typ.List[str], - new_vinfo : version.VersionInfo, + new_vinfo : v2version.VersionInfo, old_lines : typ.List[str], ) -> typ.List[str]: - """TODO reenable doctest""" - pass + # TODO reenable doctest + # """Replace occurances of pattern_strs in old_lines with new_vinfo. - """Replace occurances of pattern_strs in old_lines with new_vinfo. + # >>> new_vinfo = version.parse_version_info("v201811.0123-beta") + # >>> pattern_strs = ['__version__ = "{pycalver}"'] + # >>> rewrite_lines(pattern_strs, new_vinfo, ['__version__ = "v201809.0002-beta"']) + # ['__version__ = "v201811.0123-beta"'] - >>> new_vinfo = version.parse_version_info("v201811.0123-beta") - >>> pattern_strs = ['__version__ = "{pycalver}"'] - >>> rewrite_lines(pattern_strs, new_vinfo, ['__version__ = "v201809.0002-beta"']) - ['__version__ = "v201811.0123-beta"'] - - >>> pattern_strs = ['__version__ = "{pep440_version}"'] - >>> rewrite_lines(pattern_strs, new_vinfo, ['__version__ = "201809.2b0"']) - ['__version__ = "201811.123b0"'] - """ + # >>> pattern_strs = ['__version__ = "{pep440_version}"'] + # >>> rewrite_lines(pattern_strs, new_vinfo, ['__version__ = "201809.2b0"']) + # ['__version__ = "201811.123b0"'] + # """ new_lines = old_lines[:] found_patterns = set() - # re_patterns = [patterns.compile_pattern(p) for p in pattern_strs] - # matches = parse.iter_matches(old_lines, re_patterns) - matches = parse.iter_matches(old_lines, pattern_strs) + patterns = [v2patterns.compile_pattern(p) for p in pattern_strs] + matches = parse.iter_matches(old_lines, patterns) for match in matches: - found_patterns.add(match.pattern) - replacement = version.format_version(new_vinfo, match.pattern) + found_patterns.add(match.pattern.raw) + replacement = v2version.format_version(new_vinfo, match.pattern.raw) span_l, span_r = match.span new_line = match.line[:span_l] + replacement + match.line[span_r:] new_lines[match.lineno] = new_line @@ -54,7 +51,7 @@ def rewrite_lines( if non_matched_patterns: for non_matched_pattern in non_matched_patterns: logger.error(f"No match for pattern '{non_matched_pattern}'") - compiled_pattern_str = patterns.compile_pattern_str(non_matched_pattern) + compiled_pattern_str = v2patterns.compile_pattern_str(non_matched_pattern) logger.error(f"Pattern compiles to regex '{compiled_pattern_str}'") raise v1rewrite.NoPatternMatch("Invalid pattern(s)") else: @@ -63,28 +60,26 @@ def rewrite_lines( def rfd_from_content( pattern_strs: typ.List[str], - new_vinfo : version.VersionInfo, + new_vinfo : v2version.VersionInfo, content : str, ) -> v1rewrite.RewrittenFileData: - """TODO reenable doctest""" - pass + # TODO reenable doctest + # r"""Rewrite pattern occurrences with version string. - r"""Rewrite pattern occurrences with version string. - - >>> new_vinfo = version.parse_version_info("v201809.0123") - >>> pattern_strs = ['__version__ = "{pycalver}"'] - >>> content = '__version__ = "v201809.0001-alpha"' - >>> rfd = rfd_from_content(pattern_strs, new_vinfo, content) - >>> rfd.new_lines - ['__version__ = "v201809.0123"'] - >>> - >>> new_vinfo = version.parse_version_info("v1.2.3", "v{semver}") - >>> pattern_strs = ['__version__ = "v{semver}"'] - >>> content = '__version__ = "v1.2.2"' - >>> rfd = rfd_from_content(pattern_strs, new_vinfo, content) - >>> rfd.new_lines - ['__version__ = "v1.2.3"'] - """ + # >>> new_vinfo = version.parse_version_info("v201809.0123") + # >>> pattern_strs = ['__version__ = "{pycalver}"'] + # >>> content = '__version__ = "v201809.0001-alpha"' + # >>> rfd = rfd_from_content(pattern_strs, new_vinfo, content) + # >>> rfd.new_lines + # ['__version__ = "v201809.0123"'] + # >>> + # >>> new_vinfo = version.parse_version_info("v1.2.3", "v{semver}") + # >>> pattern_strs = ['__version__ = "v{semver}"'] + # >>> content = '__version__ = "v1.2.2"' + # >>> rfd = rfd_from_content(pattern_strs, new_vinfo, content) + # >>> rfd.new_lines + # ['__version__ = "v1.2.3"'] + # """ line_sep = v1rewrite.detect_line_sep(content) old_lines = content.split(line_sep) new_lines = rewrite_lines(pattern_strs, new_vinfo, old_lines) @@ -93,30 +88,28 @@ def rfd_from_content( def iter_rewritten( file_patterns: config.PatternsByGlob, - new_vinfo : version.VersionInfo, + new_vinfo : v2version.VersionInfo, ) -> typ.Iterable[v1rewrite.RewrittenFileData]: - """TODO reenable doctest""" - pass + # TODO reenable doctest + # r'''Iterate over files with version string replaced. - r'''Iterate over files with version string replaced. - - >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']} - >>> new_vinfo = version.parse_version_info("v201809.0123") - >>> rewritten_datas = iter_rewritten(file_patterns, new_vinfo) - >>> rfd = list(rewritten_datas)[0] - >>> assert rfd.new_lines == [ - ... '# This file is part of the pycalver project', - ... '# https://gitlab.com/mbarkhau/pycalver', - ... '#', - ... '# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License', - ... '# SPDX-License-Identifier: MIT', - ... '"""PyCalVer: CalVer for Python Packages."""', - ... '', - ... '__version__ = "v201809.0123"', - ... '', - ... ] - >>> - ''' + # >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']} + # >>> new_vinfo = version.parse_version_info("v201809.0123") + # >>> rewritten_datas = iter_rewritten(file_patterns, new_vinfo) + # >>> rfd = list(rewritten_datas)[0] + # >>> assert rfd.new_lines == [ + # ... '# This file is part of the pycalver project', + # ... '# https://gitlab.com/mbarkhau/pycalver', + # ... '#', + # ... '# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License', + # ... '# SPDX-License-Identifier: MIT', + # ... '"""PyCalVer: CalVer for Python Packages."""', + # ... '', + # ... '__version__ = "v201809.0123"', + # ... '', + # ... ] + # >>> + # ''' fobj: typ.IO[str] @@ -128,23 +121,24 @@ def iter_rewritten( yield rfd._replace(path=str(file_path)) -def diff(new_vinfo: version.VersionInfo, file_patterns: config.PatternsByGlob) -> str: - """TODO reenable doctest""" - pass +def diff( + new_vinfo : v2version.VersionInfo, + file_patterns: config.PatternsByGlob, +) -> str: + # TODO reenable doctest + # r"""Generate diffs of rewritten files. - r"""Generate diffs of rewritten files. - - >>> new_vinfo = version.parse_version_info("v201809.0123") - >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']} - >>> diff_str = diff(new_vinfo, file_patterns) - >>> lines = diff_str.split("\n") - >>> lines[:2] - ['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py'] - >>> assert lines[6].startswith('-__version__ = "v2') - >>> assert not lines[6].startswith('-__version__ = "v201809.0123"') - >>> lines[7] - '+__version__ = "v201809.0123"' - """ + # >>> new_vinfo = version.parse_version_info("v201809.0123") + # >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']} + # >>> diff_str = diff(new_vinfo, file_patterns) + # >>> lines = diff_str.split("\n") + # >>> lines[:2] + # ['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py'] + # >>> assert lines[6].startswith('-__version__ = "v2') + # >>> assert not lines[6].startswith('-__version__ = "v201809.0123"') + # >>> lines[7] + # '+__version__ = "v201809.0123"' + # """ full_diff = "" fobj: typ.IO[str] @@ -172,7 +166,7 @@ def diff(new_vinfo: version.VersionInfo, file_patterns: config.PatternsByGlob) - return full_diff -def rewrite(file_patterns: config.PatternsByGlob, new_vinfo: version.VersionInfo) -> None: +def rewrite(file_patterns: config.PatternsByGlob, new_vinfo: v2version.VersionInfo) -> None: """Rewrite project files, updating each with the new version.""" fobj: typ.IO[str] diff --git a/src/pycalver2/version.py b/src/pycalver2/version.py index ea78d3f..e16484b 100644 --- a/src/pycalver2/version.py +++ b/src/pycalver2/version.py @@ -108,29 +108,27 @@ def _quarter_from_month(month: int) -> int: def cal_info(date: dt.date = None) -> CalendarInfo: - """TODO reenable doctest""" - pass + # TODO reenable doctest + # """Generate calendar components for current date. - """Generate calendar components for current date. + # >>> from datetime import date - >>> from datetime import date + # >>> c = cal_info(date(2019, 1, 5)) + # >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) + # (2019, 1, 1, 5, 5, 0, 0) - >>> c = cal_info(date(2019, 1, 5)) - >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) - (2019, 1, 1, 5, 5, 0, 0) + # >>> c = cal_info(date(2019, 1, 6)) + # >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) + # (2019, 1, 1, 6, 6, 0, 1) - >>> c = cal_info(date(2019, 1, 6)) - >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) - (2019, 1, 1, 6, 6, 0, 1) + # >>> c = cal_info(date(2019, 1, 7)) + # >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) + # (2019, 1, 1, 7, 7, 1, 1) - >>> c = cal_info(date(2019, 1, 7)) - >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) - (2019, 1, 1, 7, 7, 1, 1) - - >>> c = cal_info(date(2019, 4, 7)) - >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) - (2019, 2, 4, 7, 97, 13, 14) - """ + # >>> c = cal_info(date(2019, 4, 7)) + # >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) + # (2019, 2, 4, 7, 97, 13, 14) + # """ if date is None: date = TODAY @@ -250,22 +248,20 @@ def _parse_field_values(field_values: FieldValues) -> VersionInfo: def _is_calver(nfo: typ.Union[CalendarInfo, VersionInfo]) -> bool: - """TODO reenable doctest""" - pass + # TODO reenable doctest + # """Check pattern for any calendar based parts. - """Check pattern for any calendar based parts. + # >>> _is_calver(cal_info()) + # True - >>> _is_calver(cal_info()) - True + # >>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0018"}) + # >>> _is_calver(vnfo) + # True - >>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0018"}) - >>> _is_calver(vnfo) - True - - >>> vnfo = _parse_version_info({'MAJOR': "1", 'MINOR': "023", 'PATCH': "45"}) - >>> _is_calver(vnfo) - False - """ + # >>> vnfo = _parse_version_info({'MAJOR': "1", 'MINOR': "023", 'PATCH': "45"}) + # >>> _is_calver(vnfo) + # False + # """ for field in CalendarInfo._fields: maybe_val: typ.Any = getattr(nfo, field, None) if isinstance(maybe_val, int): @@ -325,48 +321,45 @@ def _parse_pattern_groups(pattern_groups: PatternGroups) -> FieldValues: def _parse_version_info(pattern_groups: PatternGroups) -> VersionInfo: - """TODO reenable doctest""" - pass + # TODO reenable doctest + # """Parse normalized VersionInfo from groups of a matched pattern. - """Parse normalized VersionInfo from groups of a matched pattern. + # >>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0099"}) + # >>> (vnfo.year_y, vnfo.month, vnfo.quarter, vnfo.bid, vnfo.tag) + # (2018, 11, 4, '0099', 'final') - >>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0099"}) - >>> (vnfo.year_y, vnfo.month, vnfo.quarter, vnfo.bid, vnfo.tag) - (2018, 11, 4, '0099', 'final') + # >>> vnfo = _parse_version_info({'year': "2018", 'doy': "11", 'bid': "099", 'tag': "b"}) + # >>> (vnfo.year_y, vnfo.month, vnfo.dom, vnfo.bid, vnfo.tag) + # (2018, 1, 11, '099', 'beta') - >>> vnfo = _parse_version_info({'year': "2018", 'doy': "11", 'bid': "099", 'tag': "b"}) - >>> (vnfo.year_y, vnfo.month, vnfo.dom, vnfo.bid, vnfo.tag) - (2018, 1, 11, '099', 'beta') + # >>> vnfo = _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "45"}) + # >>> (vnfo.major, vnfo.minor, vnfo.patch) + # (1, 23, 45) - >>> vnfo = _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "45"}) - >>> (vnfo.major, vnfo.minor, vnfo.patch) - (1, 23, 45) - - >>> vnfo = _parse_version_info({'MAJOR': "1", 'MMM': "023", 'PPPP': "0045"}) - >>> (vnfo.major, vnfo.minor, vnfo.patch) - (1, 23, 45) - """ + # >>> vnfo = _parse_version_info({'MAJOR': "1", 'MMM': "023", 'PPPP': "0045"}) + # >>> (vnfo.major, vnfo.minor, vnfo.patch) + # (1, 23, 45) + # """ field_values = _parse_pattern_groups(pattern_groups) return _parse_field_values(field_values) def parse_version_info(version_str: str, pattern: str = "{pycalver}") -> VersionInfo: - """TODO reenable doctest""" - pass + # TODO reenable doctest + # """Parse normalized VersionInfo. - """Parse normalized VersionInfo. + # >>> vnfo = parse_version_info("v201712.0033-beta", pattern="{pycalver}") + # >>> assert vnfo == _parse_version_info({'year': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"}) - >>> vnfo = parse_version_info("v201712.0033-beta", pattern="{pycalver}") - >>> assert vnfo == _parse_version_info({'year': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"}) - - >>> vnfo = parse_version_info("1.23.456", pattern="{semver}") - >>> assert vnfo == _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "456"}) - """ - regex = v2patterns.compile_pattern(pattern) - match = regex.match(version_str) + # >>> vnfo = parse_version_info("1.23.456", pattern="{semver}") + # >>> assert vnfo == _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "456"}) + # """ + pattern_tup = v2patterns.compile_pattern(pattern) + match = pattern_tup.regexp.match(version_str) if match is None: err_msg = ( - f"Invalid version string '{version_str}' for pattern '{pattern}'/'{regex.pattern}'" + f"Invalid version string '{version_str}' " + f"for pattern '{pattern}'/'{pattern_tup.regexp.pattern}'" ) raise PatternError(err_msg) @@ -374,20 +367,18 @@ def parse_version_info(version_str: str, pattern: str = "{pycalver}") -> Version def is_valid(version_str: str, pattern: str = "{pycalver}") -> bool: - """TODO reenable doctest""" - pass + # TODO reenable doctest + # """Check if a version matches a pattern. - """Check if a version matches a pattern. - - >>> is_valid("v201712.0033-beta", pattern="{pycalver}") - True - >>> is_valid("v201712.0033-beta", pattern="{semver}") - False - >>> is_valid("1.2.3", pattern="{semver}") - True - >>> is_valid("v201712.0033-beta", pattern="{semver}") - False - """ + # >>> is_valid("v201712.0033-beta", pattern="{pycalver}") + # True + # >>> is_valid("v201712.0033-beta", pattern="{semver}") + # False + # >>> is_valid("1.2.3", pattern="{semver}") + # True + # >>> is_valid("v201712.0033-beta", pattern="{semver}") + # False + # """ try: parse_version_info(version_str, pattern) return True @@ -454,77 +445,75 @@ def _compile_format_template(pattern: str, kwargs: TemplateKwargs) -> str: def format_version(vinfo: VersionInfo, pattern: str) -> str: - """TODO reenable doctest""" - pass + # TODO reenable doctest + # """Generate version string. - """Generate version string. + # >>> import datetime as dt + # >>> vinfo = parse_version_info("v201712.0033-beta", pattern="{pycalver}") + # >>> vinfo_a = vinfo._replace(**cal_info(date=dt.date(2017, 1, 1))._asdict()) + # >>> vinfo_b = vinfo._replace(**cal_info(date=dt.date(2017, 12, 31))._asdict()) - >>> import datetime as dt - >>> vinfo = parse_version_info("v201712.0033-beta", pattern="{pycalver}") - >>> vinfo_a = vinfo._replace(**cal_info(date=dt.date(2017, 1, 1))._asdict()) - >>> vinfo_b = vinfo._replace(**cal_info(date=dt.date(2017, 12, 31))._asdict()) + # >>> format_version(vinfo_a, pattern="v{yy}.{BID}{release}") + # 'v17.33-beta' + # >>> format_version(vinfo_a, pattern="vYY.BUILD[-TAG]") + # 'v17.33-beta' + # >>> format_version(vinfo_a, pattern="YYYY0M.BUILD[PYTAG]") + # '201701.33b0' - >>> format_version(vinfo_a, pattern="v{yy}.{BID}{release}") - 'v17.33-beta' - >>> format_version(vinfo_a, pattern="vYY.BUILD[-TAG]") - 'v17.33-beta' - >>> format_version(vinfo_a, pattern="YYYY0M.BUILD[PYTAG]") - '201701.33b0' + # >>> format_version(vinfo_a, pattern="{pycalver}") + # 'v201701.0033-beta' + # >>> format_version(vinfo_b, pattern="{pycalver}") + # 'v201712.0033-beta' - >>> format_version(vinfo_a, pattern="{pycalver}") - 'v201701.0033-beta' - >>> format_version(vinfo_b, pattern="{pycalver}") - 'v201712.0033-beta' + # >>> format_version(vinfo_a, pattern="v{year}w{iso_week}.{BID}{release}") + # 'v2017w00.33-beta' + # >>> format_version(vinfo_a, pattern="vYYYYwWW.BUILD[-TAG]") + # 'v2017w00.33-beta' + # >>> format_version(vinfo_b, pattern="v{year}w{iso_week}.{BID}{release}") + # 'v2017w52.33-beta' + # >>> format_version(vinfo_b, pattern="vYYYYwWW.BUILD[-TAG]") + # 'v2017w52.33-beta' - >>> format_version(vinfo_a, pattern="v{year}w{iso_week}.{BID}{release}") - 'v2017w00.33-beta' - >>> format_version(vinfo_a, pattern="vYYYYwWW.BUILD[-TAG]") - 'v2017w00.33-beta' - >>> format_version(vinfo_b, pattern="v{year}w{iso_week}.{BID}{release}") - 'v2017w52.33-beta' - >>> format_version(vinfo_b, pattern="vYYYYwWW.BUILD[-TAG]") - 'v2017w52.33-beta' + # >>> format_version(vinfo_a, pattern="v{year}d{doy}.{bid}{release}") + # 'v2017d001.0033-beta' + # >>> format_version(vinfo_b, pattern="v{year}d{doy}.{bid}{release}") + # 'v2017d365.0033-beta' + # >>> format_version(vinfo_a, pattern="vYYYYdJJJ.BUILD[-TAG]") + # 'v2017d001.0033-beta' + # >>> format_version(vinfo_b, pattern="vYYYYdJJJ.BUILD[-TAG]") + # 'v2017d365.0033-beta' - >>> format_version(vinfo_a, pattern="v{year}d{doy}.{bid}{release}") - 'v2017d001.0033-beta' - >>> format_version(vinfo_b, pattern="v{year}d{doy}.{bid}{release}") - 'v2017d365.0033-beta' - >>> format_version(vinfo_a, pattern="vYYYYdJJJ.BUILD[-TAG]") - 'v2017d001.0033-beta' - >>> format_version(vinfo_b, pattern="vYYYYdJJJ.BUILD[-TAG]") - 'v2017d365.0033-beta' + # >>> format_version(vinfo_a, pattern="vGGGGwVV.BUILD[-TAG]") + # 'v2016w52.0033-beta' - >>> format_version(vinfo_a, pattern="vGGGGwVV.BUILD[-TAG]") - 'v2016w52.0033-beta' + # >>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final') - >>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final') + # >>> format_version(vinfo_c, pattern="v{year}w{iso_week}.{BID}-{tag}") + # 'v2017w52.33-final' + # >>> format_version(vinfo_c, pattern="v{year}w{iso_week}.{BID}{release}") + # 'v2017w52.33' + # >>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD-TAG") + # 'v2017w52.33-final' + # >>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD[-TAG]") + # 'v2017w52.33' - >>> format_version(vinfo_c, pattern="v{year}w{iso_week}.{BID}-{tag}") - 'v2017w52.33-final' - >>> format_version(vinfo_c, pattern="v{year}w{iso_week}.{BID}{release}") - 'v2017w52.33' - >>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD-TAG") - 'v2017w52.33-final' - >>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD[-TAG]") - 'v2017w52.33' + # >>> format_version(vinfo_c, pattern="v{MAJOR}.{MINOR}.{PATCH}") + # 'v1.2.34' + # >>> format_version(vinfo_c, pattern="vMAJOR.MINOR.PATCH") + # 'v1.2.34' - >>> format_version(vinfo_c, pattern="v{MAJOR}.{MINOR}.{PATCH}") - 'v1.2.34' - >>> format_version(vinfo_c, pattern="vMAJOR.MINOR.PATCH") - 'v1.2.34' - - >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='final') - >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-TAG") - 'v1.0.0-final' - >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH[-TAG]") - 'v1.0.0' - >>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.PATCH[-TAG]]") - 'v1.0' - >>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.MICRO[-TAG]]") - 'v1.0' - >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-TAG]]]") - 'v1' - """ + # >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='final') + # >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-TAG") + # 'v1.0.0-final' + # >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH[-TAG]") + # 'v1.0.0' + # >>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.PATCH[-TAG]]") + # 'v1.0' + # >>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.MICRO[-TAG]]") + # 'v1.0' + # >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-TAG]]]") + # 'v1' + # """ kwargs = _derive_template_kwargs(vinfo) format_tmpl = _compile_format_template(pattern, kwargs) diff --git a/test/test_parse.py b/test/test_parse.py index 6f60923..e3106ad 100644 --- a/test/test_parse.py +++ b/test/test_parse.py @@ -1,3 +1,4 @@ +import pycalver.patterns as v1patterns from pycalver import parse SETUP_PY_FIXTURE = """ @@ -11,17 +12,17 @@ setuptools.setup( def test_default_parse_patterns(): - lines = SETUP_PY_FIXTURE.splitlines() - patterns = ["{pycalver}", "{pep440_pycalver}"] - - matches = list(parse.iter_matches(lines, patterns)) + lines = SETUP_PY_FIXTURE.splitlines() + patterns = ["{pycalver}", "{pep440_pycalver}"] + re_patterns = [v1patterns.compile_pattern(p) for p in patterns] + matches = list(parse.iter_matches(lines, re_patterns)) assert len(matches) == 2 assert matches[0].lineno == 3 assert matches[1].lineno == 6 - assert matches[0].pattern == patterns[0] - assert matches[1].pattern == patterns[1] + assert matches[0].pattern == re_patterns[0] + assert matches[1].pattern == re_patterns[1] assert matches[0].match == "v201712.0002-alpha" assert matches[1].match == "201712.2a0" @@ -30,16 +31,16 @@ def test_default_parse_patterns(): def test_explicit_parse_patterns(): lines = SETUP_PY_FIXTURE.splitlines() - patterns = ["__version__ = '{pycalver}'", "version='{pep440_pycalver}'"] - - matches = list(parse.iter_matches(lines, patterns)) + patterns = ["__version__ = '{pycalver}'", "version='{pep440_pycalver}'"] + re_patterns = [v1patterns.compile_pattern(p) for p in patterns] + matches = list(parse.iter_matches(lines, re_patterns)) assert len(matches) == 2 assert matches[0].lineno == 3 assert matches[1].lineno == 6 - assert matches[0].pattern == patterns[0] - assert matches[1].pattern == patterns[1] + assert matches[0].pattern == re_patterns[0] + assert matches[1].pattern == re_patterns[1] assert matches[0].match == "__version__ = 'v201712.0002-alpha'" assert matches[1].match == "version='201712.2a0'" @@ -57,16 +58,17 @@ README_RST_FIXTURE = """ def test_badge_parse_patterns(): lines = README_RST_FIXTURE.splitlines() - patterns = ["badge/CalVer-{calver}{build}-{release}-blue.svg", ":alt: CalVer {pycalver}"] + patterns = ["badge/CalVer-{calver}{build}-{release}-blue.svg", ":alt: CalVer {pycalver}"] + re_patterns = [v1patterns.compile_pattern(p) for p in patterns] + matches = list(parse.iter_matches(lines, re_patterns)) - matches = list(parse.iter_matches(lines, patterns)) assert len(matches) == 2 assert matches[0].lineno == 3 assert matches[1].lineno == 5 - assert matches[0].pattern == patterns[0] - assert matches[1].pattern == patterns[1] + assert matches[0].pattern == re_patterns[0] + assert matches[1].pattern == re_patterns[1] assert matches[0].match == "badge/CalVer-v201809.0002--beta-blue.svg" assert matches[1].match == ":alt: CalVer v201809.0002-beta" diff --git a/test/test_patterns.py b/test/test_patterns.py index 61d8d48..14f6012 100644 --- a/test/test_patterns.py +++ b/test/test_patterns.py @@ -67,8 +67,8 @@ PATTERN_CASES = [ @pytest.mark.parametrize("pattern_str, line, expected", PATTERN_CASES) def test_patterns(pattern_str, line, expected): - pattern_re = v1patterns.compile_pattern(pattern_str) - result = pattern_re.search(line) + pattern = v1patterns.compile_pattern(pattern_str) + result = pattern.regexp.search(line) if result is None: assert expected is None, (pattern_str, line) else: @@ -84,10 +84,10 @@ CLI_MAIN_FIXTURE = """ def test_pattern_escapes(): - pattern = 'click.version_option(version="{pycalver}")' - pattern_re = v1patterns.compile_pattern(pattern) - match = pattern_re.search(CLI_MAIN_FIXTURE) - expected = 'click.version_option(version="v201812.0123-beta")' + pattern_str = 'click.version_option(version="{pycalver}")' + pattern = v1patterns.compile_pattern(pattern_str) + match = pattern.regexp.search(CLI_MAIN_FIXTURE) + expected = 'click.version_option(version="v201812.0123-beta")' assert match.group(0) == expected @@ -97,8 +97,8 @@ package_metadata = {"name": "mypackage", "version": "v201812.0123-beta"} def test_curly_escapes(): - pattern = 'package_metadata = {"name": "mypackage", "version": "{pycalver}"}' - pattern_re = v1patterns.compile_pattern(pattern) - match = pattern_re.search(CURLY_BRACE_FIXTURE) - expected = 'package_metadata = {"name": "mypackage", "version": "v201812.0123-beta"}' + pattern_str = 'package_metadata = {"name": "mypackage", "version": "{pycalver}"}' + pattern = v1patterns.compile_pattern(pattern_str) + match = pattern.regexp.search(CURLY_BRACE_FIXTURE) + expected = 'package_metadata = {"name": "mypackage", "version": "v201812.0123-beta"}' assert match.group(0) == expected diff --git a/test/test_version.py b/test/test_version.py index e8d2207..a5e2767 100644 --- a/test/test_version.py +++ b/test/test_version.py @@ -136,15 +136,18 @@ def test_part_field_mapping(): b_names = set(patterns.PART_PATTERNS.keys()) c_names = set(patterns.COMPOSITE_PART_PATTERNS.keys()) - extra_names = a_names - b_names - assert not any(extra_names) - missing_names = b_names - a_names - assert missing_names == c_names + a_extra_names = a_names - b_names + assert not any(a_extra_names), sorted(a_extra_names) + b_extra_names = b_names - (a_names | c_names) + assert not any(b_extra_names), sorted(b_extra_names) a_fields = set(version.PATTERN_PART_FIELDS.values()) b_fields = set(version.VersionInfo._fields) - assert a_fields == b_fields + a_extra_fields = a_fields - b_fields + b_extra_fields = b_fields - a_fields + assert not any(a_extra_fields), sorted(a_extra_fields) + assert not any(b_extra_fields), sorted(b_extra_fields) def vnfo(**field_values): @@ -152,6 +155,8 @@ def vnfo(**field_values): PARSE_VERSION_TEST_CASES = [ + # TODO (mb 2020-09-06): add tests for new style patterns + # ["YYYY.MM.DD" , "2017.06.07", vnfo(year="2017", month="06", dom="07")], ["{year}.{month}.{dom}" , "2017.06.07", vnfo(year="2017", month="06", dom="07")], ["{year}.{month}.{dom_short}" , "2017.06.7" , vnfo(year="2017", month="06", dom="7" )], ["{year}.{month}.{dom_short}" , "2017.06.7" , vnfo(year="2017", month="06", dom="7" )], @@ -169,8 +174,8 @@ PARSE_VERSION_TEST_CASES = [ @pytest.mark.parametrize("pattern_str, line, expected_vinfo", PARSE_VERSION_TEST_CASES) def test_parse_versions(pattern_str, line, expected_vinfo): - pattern_re = patterns.compile_pattern(pattern_str) - version_match = pattern_re.search(line) + pattern = patterns.compile_pattern(pattern_str) + version_match = pattern.regexp.search(line) if expected_vinfo is None: assert version_match is None From 32ad101b9f3fe9c1e5d55fdc4cbcdc7a9c7f5d03 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 10 Sep 2020 19:56:06 +0000 Subject: [PATCH 09/98] remove dep on six --- README.md | 2 +- requirements/pypi.txt | 1 - src/pycalver/config.py | 3 +-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e387447..09343eb 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ The fastest way to setup a project is to use `pycalver init`. ```shell $ pip install pycalver ... -Installing collected packages: click pathlib2 typing toml six pycalver +Installing collected packages: click pathlib2 typing toml pycalver Successfully installed pycalver-202007.36 $ pycalver --version diff --git a/requirements/pypi.txt b/requirements/pypi.txt index 0bd5192..9dcff45 100644 --- a/requirements/pypi.txt +++ b/requirements/pypi.txt @@ -11,5 +11,4 @@ pathlib2 typing; python_version < "3.5" click toml -six lexid diff --git a/src/pycalver/config.py b/src/pycalver/config.py index e5b41a8..57223f6 100644 --- a/src/pycalver/config.py +++ b/src/pycalver/config.py @@ -11,7 +11,6 @@ import logging import datetime as dt import configparser -import six import toml import pathlib2 as pl @@ -168,7 +167,7 @@ def _parse_cfg(cfg_buffer: typ.IO[str]) -> RawConfig: for option, default_val in BOOL_OPTIONS.items(): val: OptionVal = raw_cfg.get(option, default_val) - if isinstance(val, six.text_type): + if isinstance(val, (bytes, str)): val = val.lower() in ("yes", "true", "1", "on") raw_cfg[option] = val From 28e09fd60a04a36e3d5401366b1fd3256ae00239 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 17 Sep 2020 16:24:21 +0000 Subject: [PATCH 10/98] readme updates --- CHANGELOG.md | 10 ++ README.md | 144 ++++++++++++++++++++++----- src/pycalver2/patterns.py | 199 +++++++++----------------------------- src/pycalver2/version.py | 7 +- test/test_cli.py | 4 +- test/test_patterns.py | 128 ++++++++++++++++++++++++ test/test_version.py | 78 ++++++++------- 7 files changed, 356 insertions(+), 214 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff167b2..957b86c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ # Changelog for https://gitlab.com/mbarkhau/pycalver +## NEXT + + - New gitlab #7: New style patterns, to be in line with CalVer.org + - Better support for week numbering. + - Better support for optional parts. + - New gitlab #10: `--pin-date` to keep date parts unchanged, and only increment non-date parts. + - Fix gitlab #8: Push tags only pushed tags, not actual commit. + - Fix gitlab #9: Make commit message configurable. + + ## v201907.0036 - Fix: Don't use git/hg command if `commit=False` is configured (thanks @valentin87) diff --git a/README.md b/README.md index 09343eb..9e79130 100644 --- a/README.md +++ b/README.md @@ -212,10 +212,108 @@ The internally used regular expression is also shown, which you can use to debug ### Pattern Search and Replacement -The `pycalver:file_patterns` section of the configuration is used both to search -and also to replace version strings in your projects files. Everything except +The neat thing about PyCalVer is that you don't have to separately declare the search pattern and the replacement template. You declare one pattern that is used to derive both. + for valid placeholders is treated as literal text. Available placeholders are: +You define your pattern in the `pycalver:version_pattern` option of your config. + +You can also define custom patterns in the items of the `pycalver:file_patterns` section of your configuration, but usually it will be easier to just use the special `{version}` and `{pep440_version}` patterns, which are derived from what you configure as your `version_pattern`. is used both to search and also to replace version strings in your projects files. Everything except for valid placeholders is treated as literal text. Available placeholders are: + +These patterns are closely based on https://calver.org/ + +| placeholder | range / example(s) | comment | +|-------------|---------------------|----------------------| +| YYYY | 2019, 2020... | `%Y` | +| YY | 18, 19..99, 1, 2 | `int(%y)` | +| 0Y | 18, 19..99, 01, 02 | `%y` | +| Q | 1, 2, 3, 4 | quarter | +| MM | 9, 10, 11, 12 | `int(%m)` | +| 0M | 09, 10, 11, 12 | `%m` | +| DD | 1, 2, 3..31 | `int(%d)` | +| 0D | 01, 02, 03..31 | `%d` | +| JJJ | 1,2,3..366 | `int(%j)` | +| 00J | 001, 002..366 | `%j` | +| BUILD | 1001, 1002, 23456 | build number (lexid) | +| TAG | alpha, beta, rc | `--release=` | +| PYTAG | a0, b0, rc | `--release=` | +| MAJOR | 0..9, 10..99, 100.. | `--major` | +| MINOR | 0..9, 10..99, 100.. | `--minor` | +| PATCH | 0..9, 10..99, 100.. | `--patch` | +| MICRO | 0..9, 10..99, 100.. | Synonym for PATCH | + + +### Week Numbering + +Week numbering is a bit special, as it depends on your definition of "week": + +- Does it start on a Monday or a Sunday? +- Range from 0-52 or 1-53 ? +- At the beginning/end of the year, do you have partial weeks or do you have a week that span mutliple years? +- If a week spans multiple years, what is the year number? + +| placeholder | range / example(s) | comment | +|-------------|---------------------|------------------------------------------------------------| +| `WW` | 0, 1, 2..52 | `int(%W)` | +| `0W` | 00, 01, 02..52 | `%W` | +| `UU` | 0, 1, 2..52 | `int(%U)` us_week | +| `0U` | 00, 01, 02..52 | `%U` us_week | +| `VV` | 1, 2..53 | `int(%V)` iso week | +| `0V` | 01, 02..53 | `%U` iso_week | +| `GGGG` | 2019, 2020... | ISO 8601 week-based year (corresponds to `strftime("%G")`) | +| `GG` | 19, 20...99, 0, 1 | Short ISO 8601 week-based year | +| `0G` | 19, 20...99, 00, 01 | Zero-padded ISO 8601 week-based year | + + +### Normalization Caveats + +Since other tools parse your version numbers, they may not care about your choice of formatting. In the case of Python, the packaging tools (such as pypi.org) follow [PEP440 normalization rules][pep_440_normalzation_ref]. + +According to these rules: + +- Any non-numerical prefix (such as `v`) is removed +- Leading zeros in parts are truncated `XX.08` -> `XX.8` +- Tags are converted to a short form (`-alpha` -> `a0`) + +For example: + +- Pattern: `vYY.0M.0D[-TAG]` +- Version: `v20.08.02-beta` +- PEP440 : `20.8.2b0` + +It may be confusing to your users to see versions displayed in two different forms. It is not immediately obvious that `v20.08.02-beta` is the same `20.8.2b0` on pypi. If you wish to avoid this, you should usa a pattern which is as close as possible to the normalized form of your version. + +| pattern | example | lexical | PEP440 | lexical | +|-----------------------|---------|---------|--------|---------| +| `YYYY.0M` | | yes | | no | +| `YYYY.MM` | | no | | no | +| `vYYYY.0W` | | yes | | no | +| `vYYYY.WW` | | no | | no | +| `YYYY.0M.0D` | | yes | | no | +| `YYYY.MM.DD` | | no | | no | +| `YYYY0M.BUILD[-TAG]` | | yes | | yes | +| `YY0M.BUILD[-TAG]` | | yes¹ | | yes¹ | +| `YYYY.BUILD[-TAG]` | | yes | | yes | +| `YYYY0M.MINOR[-TAG]` | | yes² | | yes | +| `YYYY.MM.MINOR[-TAG]` | | no | | no | +| `YYYY.0M.MINOR[-TAG]` | | yes² | | no | +| `YYYY.WW.MINOR[-TAG]` | | no | | no | +| `YYYY.0W.MINOR[-TAG]` | | yes² | | no | + +- ¹ Until 2099. If your project has new releases after 2099, future maintainers can change `YY`/`0Y` -> `YYYY` so that they don't release `00.xx`. +- ² As long as `MINOR <= 9` + + +### Legacy Patterns + +> These patterns use curly braces `{}` and were the initial implementation. They are still supported and still follow their original semantics. + +The `pycalver:file_patterns` section of the configuration uses a different set +of placeholders and does not use curly braces to mark placeholders. It is still +supported, but we don't recomend you use it. + +Available placeholders are: + | placeholder | range / example(s) | comment | |---------------------|---------------------|-----------------| @@ -298,29 +396,29 @@ bump` to update the components that are not updated automatically (eg. based on the calendar). ```shell -$ pycalver test 'v18.1.1' 'v{yy}.{MINOR}.{PATCH}' +$ pycalver test 'v18.1.1' 'vYY.MINOR.PATCH' New Version: v19.1.1 PEP440 : 19.1.1 -$ pycalver test 'v18.1.1' 'v{yy}.{MINOR}.{PATCH}' --patch +$ pycalver test 'v18.1.1' 'vYY.MINOR.PATCH' --patch New Version: v19.1.2 PEP440 : 19.1.2 -$ pycalver test 'v18.1.2' 'v{yy}.{MINOR}.{PATCH}' --minor +$ pycalver test 'v18.1.2' 'vYY.MINOR.PATCH' --minor New Version: v19.2.0 PEP440 : 19.2.0 -$ pycalver test 'v201811.0051-beta' '{pycalver}' -New Version: v201902.0052-beta -PEP440 : 201902.52b0 +$ pycalver test 'v201811.1051-beta' 'vYYYYMM.BUILD[-TAG]' +New Version: v201902.1052-beta +PEP440 : 201902.1052b0 -$ pycalver test 'v201811.0051-beta' '{pycalver}' --release rc -New Version: v201902.0052-rc -PEP440 : 201902.52rc0 +$ pycalver test 'v201811.0051-beta' 'vYYYYMM.BUILD[-TAG]' --release rc +New Version: v201902.1052-rc +PEP440 : 201902.1052rc0 -$ pycalver test 'v201811.0051-beta' '{pycalver}' --release final -New Version: v201902.0052 -PEP440 : 201902.52 +$ pycalver test 'v201811.0051-beta' 'vYYYYMM.BUILD[-TAG]' --release final +New Version: v201902.1052 +PEP440 : 201902.1052 ``` Note that pypi/setuptools/pip will normalize version strings to a format @@ -391,8 +489,8 @@ section: ```ini [pycalver] -current_version = "v201812.0006-beta" -version_pattern = "{pycalver}" +current_version = "202008.1006-beta" +version_pattern = "YYYY0M.BUILD[-TAG]" commit = True tag = True push = True @@ -429,8 +527,8 @@ $ pycalver bump --dry @@ -65,7 +65,7 @@ [pycalver] --current_version = v201812.0005-beta -+current_version = v201812.0006-beta +-current_version = v202008.1005-beta ++current_version = v202008.1006-beta commit = True tag = True push = True @@ -442,11 +540,11 @@ If everything looks OK, you can do `pycalver bump`. ``` $ pycalver bump --verbose INFO - fetching tags from remote (to turn off use: -n / --no-fetch) -INFO - Old Version: v201812.0005-beta -INFO - New Version: v201812.0006-beta +INFO - Old Version: v202008.0005-beta +INFO - New Version: v202008.0006-beta INFO - git commit --file /tmp/tmpph_npey9 -INFO - git tag --annotate v201812.0006-beta --message v201812.0006-beta -INFO - git push origin v201812.0006-beta +INFO - git tag --annotate v202008.0006-beta --message v202008.0006-beta +INFO - git push origin v202008.0006-beta ``` @@ -461,7 +559,7 @@ The PyCalVer format for version strings has three parts: | | o Release Tag (optional) | | | ---+--- --+-- --+-- - v201812 .0123 -beta + v202008 .0123 -beta ``` diff --git a/src/pycalver2/patterns.py b/src/pycalver2/patterns.py index 3db6736..1217493 100644 --- a/src/pycalver2/patterns.py +++ b/src/pycalver2/patterns.py @@ -3,61 +3,36 @@ # # Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License # SPDX-License-Identifier: MIT -"""Compose Regular Expressions from Patterns. +# """Compose Regular Expressions from Patterns. ->>> version_info = PYCALVER_RE.match("v201712.0123-alpha").groupdict() ->>> assert version_info == { -... "pycalver" : "v201712.0123-alpha", -... "vYYYYMM" : "v201712", -... "year" : "2017", -... "month" : "12", -... "build" : ".0123", -... "build_no" : "0123", -... "release" : "-alpha", -... "release_tag" : "alpha", -... } ->>> ->>> version_info = PYCALVER_RE.match("v201712.0033").groupdict() ->>> assert version_info == { -... "pycalver" : "v201712.0033", -... "vYYYYMM" : "v201712", -... "year" : "2017", -... "month" : "12", -... "build" : ".0033", -... "build_no" : "0033", -... "release" : None, -... "release_tag": None, -... } -""" +# >>> pattern = compile_pattern("vYYYY0M.BUILD[-TAG]") +# >>> version_info = pattern.regexp.match("v201712.0123-alpha") +# >>> assert version_info == { +# ... "version": "v201712.0123-alpha", +# ... "YYYY" : "2017", +# ... "0M" : "12", +# ... "BUILD" : "0123", +# ... "TAG" : "alpha", +# ... } +# >>> +# >>> version_info = pattern.regexp.match("201712.1234") +# >>> assert version_info is None + +# >>> version_info = pattern.regexp.match("v201712.1234") +# >>> assert version_info == { +# ... "version": "v201712.0123-alpha", +# ... "YYYY" : "2017", +# ... "0M" : "12", +# ... "BUILD" : "0123", +# ... "TAG" : None, +# ... } +# """ import re import typing as typ import pycalver.patterns as v1patterns -# https://regex101.com/r/fnj60p/10 -PYCALVER_PATTERN = r""" -\b -(?P - (?P - v # "v" version prefix - (?P\d{4}) - (?P\d{2}) - ) - (?P - \. # "." build nr prefix - (?P\d{4,}) - ) - (?P - \- # "-" release prefix - (?Palpha|beta|dev|rc|post) - )? -)(?:\s|$) -""" - -PYCALVER_RE: typ.Pattern[str] = re.compile(PYCALVER_PATTERN, flags=re.VERBOSE) - - PATTERN_ESCAPES = [ ("\u005c", "\u005c\u005c"), ("-" , "\u005c-"), @@ -73,107 +48,37 @@ PATTERN_ESCAPES = [ (")" , "\u005c)"), ] -# NOTE (mb 2020-09-04): These are depricated in favour of explicit patterns -COMPOSITE_PART_PATTERNS = { - 'pep440_pycalver': r"{year}{month}\.{BID}(?:{pep440_tag})?", - 'pycalver' : r"v{year}{month}\.{bid}(?:-{tag})?", - 'calver' : r"v{year}{month}", - 'semver' : r"{MAJOR}\.{MINOR}\.{PATCH}", - 'release_tag' : r"{tag}", - 'build' : r"\.{bid}", - 'release' : r"(?:-{tag})?", - # depricated - 'pep440_version': r"{year}{month}\.{BID}(?:{pep440_tag})?", -} - PART_PATTERNS = { - # recommended (based on calver.org) - 'YYYY': r"[1-9]\d{3}", - 'YY' : r"\d{1,2}", - '0Y' : r"\d{2}", + # Based on calver.org + 'YYYY': r"[1-9][0-9]{3}", + 'YY' : r"[1-9][0-9]?", + '0Y' : r"[0-9]{2}", 'Q' : r"[1-4]", 'MM' : r"(?:[1-9]|1[0-2])", '0M' : r"(?:0[1-9]|1[0-2])", - 'DD' : r"([1-9]|[1-2][0-9]|3[0-1])", - '0D' : r"(0[1-9]|[1-2][0-9]|3[0-1])", - 'JJJ' : r"(?:[1-9]\d|[1-9]|[1-2]\d\d|3[0-5][0-9]|36[0-6])", - '00J' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])", - 'WW' : r"(?:[1-9]|[1-4]\d|5[0-2])", - '0W' : r"(?:[0-4]\d|5[0-2])", - 'UU' : r"(?:[1-9]|[0-4]\d|5[0-2])", - '0U' : r"(?:[0-4]\d|5[0-2])", - 'VV' : r"(?:[1-9]|[1-4]\d|5[0-3])", - '0V' : r"(?:[0-4]\d|5[0-3])", - 'GGGG': r"[1-9]\d{3}", - 'GG' : r"\d{1,2}", - '0G' : r"\d{2}", + 'DD' : r"(?:[1-9]|[1-2][0-9]|3[0-1])", + '0D' : r"(?:0[1-9]|[1-2][0-9]|3[0-1])", + 'JJJ' : r"(?:[1-9]|[1-9][0-9]|[1-2][0-9][0-9]|3[0-5][0-9]|36[0-6])", + '00J' : r"(?:00[1-9]|0[1-9][0-9]|[1-2][0-9][0-9]|3[0-5][0-9]|36[0-6])", + # week numbering parts + 'WW' : r"(?:[0-9]|[1-4][0-9]|5[0-2])", + '0W' : r"(?:[0-4][0-9]|5[0-2])", + 'UU' : r"(?:[0-9]|[1-4][0-9]|5[0-2])", + '0U' : r"(?:[0-4][0-9]|5[0-2])", + 'VV' : r"(?:[1-9]|[1-4][0-9]|5[0-3])", + '0V' : r"(?:0[1-9]|[1-4][0-9]|5[0-3])", + 'GGGG': r"[1-9][0-9]{3}", + 'GG' : r"[1-9][0-9]?", + '0G' : r"[0-9]{2}", # non calver parts - 'MAJOR': r"\d+", - 'MINOR': r"\d+", - 'PATCH': r"\d+", - 'MICRO': r"\d+", - 'BUILD': r"\d+", + 'MAJOR': r"[0-9]+", + 'MINOR': r"[0-9]+", + 'PATCH': r"[0-9]+", + 'MICRO': r"[0-9]+", + 'BUILD': r"[0-9]+", 'TAG' : r"(?:alpha|beta|dev|rc|post|final)", - 'PYTAG': r"(?:a|b|dev|rc|post)?\d*", - # supported (but legacy) - 'year' : r"\d{4}", - 'month' : r"(?:0[0-9]|1[0-2])", - 'month_short': r"(?:1[0-2]|[1-9])", - 'build_no' : r"\d{4,}", - 'pep440_tag' : r"(?:a|b|dev|rc|post)?\d*", - 'tag' : r"(?:alpha|beta|dev|rc|post|final)", - 'yy' : r"\d{2}", - 'yyyy' : r"\d{4}", - 'quarter' : r"[1-4]", - 'iso_week' : r"(?:[0-4]\d|5[0-3])", - 'us_week' : r"(?:[0-4]\d|5[0-3])", - 'dom' : r"(0[1-9]|[1-2][0-9]|3[0-1])", - 'dom_short' : r"([1-9]|[1-2][0-9]|3[0-1])", - 'doy' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])", - 'doy_short' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])", - 'bid' : r"\d{4,}", - # dropped support (never documented) - # 'BID' : r"[1-9]\d*", - # 'MM' : r"\d{2,}", - # 'MMM' : r"\d{3,}", - # 'MMMM' : r"\d{4,}", - # 'MMMMM' : r"\d{5,}", - # 'PP' : r"\d{2,}", - # 'PPP' : r"\d{3,}", - # 'PPPP' : r"\d{4,}", - # 'PPPPP' : r"\d{5,}", - # 'BB' : r"[1-9]\d{1,}", - # 'BBB' : r"[1-9]\d{2,}", - # 'BBBB' : r"[1-9]\d{3,}", - # 'BBBBB' : r"[1-9]\d{4,}", - # 'BBBBBB' : r"[1-9]\d{5,}", - # 'BBBBBBB' : r"[1-9]\d{6,}", -} - - -FULL_PART_FORMATS = { - 'pep440_pycalver': "{year}{month:02}.{BID}{pep440_tag}", - 'pycalver' : "v{year}{month:02}.{bid}{release}", - 'calver' : "v{year}{month:02}", - 'semver' : "{MAJOR}.{MINOR}.{PATCH}", - 'release_tag' : "{tag}", - 'build' : ".{bid}", - # NOTE (mb 2019-01-04): since release is optional, it - # is treated specially in version.format - # 'release' : "-{tag}", - 'month' : "{month:02}", - 'month_short': "{month}", - 'build_no' : "{bid}", - 'iso_week' : "{iso_week:02}", - 'us_week' : "{us_week:02}", - 'dom' : "{dom:02}", - 'doy' : "{doy:03}", - 'dom_short' : "{dom}", - 'doy_short' : "{doy}", - # depricated - 'pep440_version': "{year}{month:02}.{BID}{pep440_tag}", - 'version' : "v{year}{month:02}.{bid}{release}", + 'PYTAG': r"(?:a|b|dev|rc|post)?[0-9]*", } @@ -198,13 +103,3 @@ def compile_pattern(pattern: str) -> v1patterns.Pattern: pattern_str = compile_pattern_str(pattern) pattern_re = re.compile(pattern_str) return v1patterns.Pattern(pattern, pattern_re) - - -def _init_composite_patterns() -> None: - for part_name, part_pattern in COMPOSITE_PART_PATTERNS.items(): - part_pattern = part_pattern.replace("{", "\u005c{").replace("}", "\u005c}") - pattern_str = _replace_pattern_parts(part_pattern) - PART_PATTERNS[part_name] = pattern_str - - -_init_composite_patterns() diff --git a/src/pycalver2/version.py b/src/pycalver2/version.py index e16484b..0f28b72 100644 --- a/src/pycalver2/version.py +++ b/src/pycalver2/version.py @@ -344,14 +344,13 @@ def _parse_version_info(pattern_groups: PatternGroups) -> VersionInfo: return _parse_field_values(field_values) -def parse_version_info(version_str: str, pattern: str = "{pycalver}") -> VersionInfo: - # TODO reenable doctest +def parse_version_info(version_str: str, pattern: str = "vYYYY0M.BUILD[-TAG]") -> VersionInfo: # """Parse normalized VersionInfo. - # >>> vnfo = parse_version_info("v201712.0033-beta", pattern="{pycalver}") + # >>> vnfo = parse_version_info("v201712.0033-beta", pattern="vYYYY0M.BUILD[-TAG]") # >>> assert vnfo == _parse_version_info({'year': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"}) - # >>> vnfo = parse_version_info("1.23.456", pattern="{semver}") + # >>> vnfo = parse_version_info("1.23.456", pattern="MAJOR.MINOR.PATCH") # >>> assert vnfo == _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "456"}) # """ pattern_tup = v2patterns.compile_pattern(pattern) diff --git a/test/test_cli.py b/test/test_cli.py index 38355a2..a5d494d 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -11,7 +11,7 @@ import pathlib2 as pl from click.testing import CliRunner import pycalver.config as config -import pycalver2.patterns as patterns +import pycalver.patterns as v1patterns from pycalver.__main__ import cli SETUP_CFG_FIXTURE = """ @@ -81,7 +81,7 @@ def test_version(runner): result = runner.invoke(cli, ['--version', "-vv"]) assert result.exit_code == 0 assert " version v20" in result.output - match = patterns.PYCALVER_RE.search(result.output) + match = v1patterns.PYCALVER_RE.search(result.output) assert match diff --git a/test/test_patterns.py b/test/test_patterns.py index 14f6012..66c7503 100644 --- a/test/test_patterns.py +++ b/test/test_patterns.py @@ -7,6 +7,134 @@ import pycalver2.patterns as v2patterns # TODO (mb 2020-09-06): test for v2patterns +V2_PART_PATTERN_CASES = [ + (['YYYY', 'GGGG'], "2020" , "2020"), + (['YYYY', 'GGGG'], "" , None), + (['YYYY', 'GGGG'], "A020" , None), + (['YYYY', 'GGGG'], "020" , None), + (['YYYY', 'GGGG'], "12020", None), + (['YY' , 'GG' ], "20" , "20"), + (['YY' , 'GG' ], "3" , "3"), + (['YY' , 'GG' ], "03" , None), + (['YY' , 'GG' ], "2X" , None), + (['YY' , 'GG' ], "" , None), + (['0Y' , '0G' ], "20" , "20"), + (['0Y' , '0G' ], "03" , "03"), + (['0Y' , '0G' ], "3" , None), + (['0Y' , '0G' ], "2X" , None), + (['0Y' , '0G' ], "" , None), + # quarter + (['Q'], "0", None), + (['Q'], "1", "1"), + (['Q'], "2", "2"), + (['Q'], "3", "3"), + (['Q'], "4", "4"), + (['Q'], "5", None), + (['Q'], "X", None), + # months + (['MM'], "0" , None), + (['MM'], "01", None), + (['MM'], "1" , "1"), + (['MM'], "10", "10"), + (['MM'], "12", "12"), + (['MM'], "13", None), + (['0M'], "00", None), + (['0M'], "1" , None), + (['0M'], "01", "01"), + (['MM'], "10", "10"), + (['MM'], "12", "12"), + (['MM'], "13", None), + # day of month + (['DD'], "0" , None), + (['DD'], "01", None), + (['DD'], "1" , "1"), + (['DD'], "10", "10"), + (['DD'], "31", "31"), + (['DD'], "32", None), + (['0D'], "00", None), + (['0D'], "1" , None), + (['0D'], "01", "01"), + (['0D'], "10", "10"), + (['0D'], "31", "31"), + (['0D'], "32", None), + (['DD'], "0" , None), + (['DD'], "01", None), + (['DD'], "1" , "1"), + (['DD'], "10", "10"), + (['DD'], "31", "31"), + (['DD'], "32", None), + (['0D'], "00", None), + (['0D'], "1" , None), + (['0D'], "01", "01"), + (['0D'], "10", "10"), + (['0D'], "31", "31"), + (['0D'], "32", None), + # day of year + (['JJJ'], "0" , None), + (['JJJ'], "01" , None), + (['JJJ'], "1" , "1"), + (['JJJ'], "10" , "10"), + (['JJJ'], "31" , "31"), + (['JJJ'], "32" , "32"), + (['JJJ'], "100", "100"), + (['JJJ'], "365", "365"), + (['JJJ'], "366", "366"), + (['JJJ'], "367", None), + (['00J'], "000", None), + (['00J'], "01" , None), + (['00J'], "1" , None), + (['00J'], "001", "001"), + (['00J'], "010", "010"), + (['00J'], "031", "031"), + (['00J'], "032", "032"), + (['00J'], "100", "100"), + (['00J'], "365", "365"), + (['00J'], "366", "366"), + (['00J'], "367", None), + # week numbers + (['WW', 'UU'], "00", None), + (['WW', 'UU'], "01", None), + (['WW', 'UU'], "0" , "0"), + (['WW', 'UU'], "1" , "1"), + (['WW', 'UU'], "10", "10"), + (['WW', 'UU'], "52", "52"), + (['WW', 'UU'], "53", None), + (['0W', '0U'], "00", "00"), + (['0W', '0U'], "01", "01"), + (['0W', '0U'], "0" , None), + (['0W', '0U'], "1" , None), + (['0W', '0U'], "10", "10"), + (['0W', '0U'], "52", "52"), + (['0W', '0U'], "53", None), + (['VV'], "00", None), + (['VV'], "01", None), + (['VV'], "0" , None), + (['VV'], "1" , "1"), + (['VV'], "10", "10"), + (['VV'], "52", "52"), + (['VV'], "53", "53"), + (['VV'], "54", None), + (['0V'], "00", None), + (['0V'], "01", "01"), + (['0V'], "0" , None), + (['0V'], "1" , None), + (['0V'], "10", "10"), + (['0V'], "52", "52"), + (['0V'], "53", "53"), + (['0V'], "54", None), + (['MAJOR', 'MINOR', 'PATCH', 'MICRO'], "0", "0"), + # ('TAG', ""), + # ('PYTAG', ""), +] + + +@pytest.mark.parametrize("parts, testcase, expected", V2_PART_PATTERN_CASES) +def test_part_patterns(parts, testcase, expected): + for part in parts: + pattern_str = v2patterns.PART_PATTERNS[part] + match = re.match("^" + pattern_str + "$", testcase) + assert (match is None and expected is None) or (match.group(0) == expected) + def _part_re_by_name(name): return re.compile(v1patterns.PART_PATTERNS[name]) diff --git a/test/test_version.py b/test/test_version.py index a5e2767..2cef58a 100644 --- a/test/test_version.py +++ b/test/test_version.py @@ -5,28 +5,30 @@ import datetime as dt import pytest -from pycalver import version -from pycalver import patterns +import pycalver.version as v1version +import pycalver2.version as v2version +import pycalver.patterns as v1patterns +import pycalver2.patterns as v2patterns def test_bump_beta(): cur_version = "v201712.0001-beta" - assert cur_version < version.incr(cur_version) - assert version.incr(cur_version).endswith("-beta") - assert version.incr(cur_version, release="alpha").endswith("-alpha") - assert version.incr(cur_version, release="final").endswith("0002") + assert cur_version < v1version.incr(cur_version) + assert v1version.incr(cur_version).endswith("-beta") + assert v1version.incr(cur_version, release="alpha").endswith("-alpha") + assert v1version.incr(cur_version, release="final").endswith("0002") def test_bump_final(): cur_version = "v201712.0001" - assert cur_version < version.incr(cur_version) - assert version.incr(cur_version).endswith(".0002") - assert version.incr(cur_version, release="alpha").endswith("-alpha") + assert cur_version < v1version.incr(cur_version) + assert v1version.incr(cur_version).endswith(".0002") + assert v1version.incr(cur_version, release="alpha").endswith("-alpha") - assert version.incr(cur_version, release="final").endswith(".0002") + assert v1version.incr(cur_version, release="final").endswith(".0002") pre_version = cur_version + "-beta" - assert version.incr(pre_version, release="final").endswith(".0002") + assert v1version.incr(pre_version, release="final").endswith(".0002") def test_bump_future(): @@ -34,7 +36,7 @@ def test_bump_future(): future_date = dt.datetime.today() + dt.timedelta(days=300) future_calver = future_date.strftime("v%Y%m") cur_version = future_calver + ".0001" - new_version = version.incr(cur_version) + new_version = v1version.incr(cur_version) assert cur_version < new_version @@ -42,11 +44,11 @@ def test_bump_random(monkeypatch): cur_date = dt.date(2016, 1, 1) + dt.timedelta(days=random.randint(1, 2000)) cur_version = cur_date.strftime("v%Y%m") + ".0001-dev" - monkeypatch.setattr(version, 'TODAY', cur_date) + monkeypatch.setattr(v1version, 'TODAY', cur_date) for _ in range(1000): cur_date += dt.timedelta(days=int((1 + random.random()) ** 10)) - new_version = version.incr( + new_version = v1version.incr( cur_version, release=random.choice([None, "alpha", "beta", "rc", "final", "post"]) ) assert cur_version < new_version @@ -55,7 +57,7 @@ def test_bump_random(monkeypatch): def test_parse_version_info(): version_str = "v201712.0001-alpha" - version_info = version.parse_version_info(version_str) + version_info = v1version.parse_version_info(version_str) # assert version_info.pep440_version == "201712.1a0" # assert version_info.version == "v201712.0001-alpha" @@ -65,7 +67,7 @@ def test_parse_version_info(): assert version_info.tag == "alpha" version_str = "v201712.0001" - version_info = version.parse_version_info(version_str) + version_info = v1version.parse_version_info(version_str) # assert version_info.pep440_version == "201712.1" # assert version_info.version == "v201712.0001" @@ -77,7 +79,7 @@ def test_parse_version_info(): def test_readme_pycalver1(): version_str = "v201712.0001-alpha" - version_info = patterns.PYCALVER_RE.match(version_str).groupdict() + version_info = v1patterns.PYCALVER_RE.match(version_str).groupdict() assert version_info == { 'pycalver' : "v201712.0001-alpha", @@ -93,7 +95,7 @@ def test_readme_pycalver1(): def test_readme_pycalver2(): version_str = "v201712.0033" - version_info = patterns.PYCALVER_RE.match(version_str).groupdict() + version_info = v1patterns.PYCALVER_RE.match(version_str).groupdict() assert version_info == { 'pycalver' : "v201712.0033", @@ -109,40 +111,40 @@ def test_readme_pycalver2(): def test_parse_error_empty(): try: - version.parse_version_info("") + v1version.parse_version_info("") assert False - except version.PatternError as err: + except v1version.PatternError as err: assert "Invalid version string" in str(err) def test_parse_error_noprefix(): try: - version.parse_version_info("201809.0002") + v1version.parse_version_info("201809.0002") assert False - except version.PatternError as err: + except v1version.PatternError as err: assert "Invalid version string" in str(err) def test_parse_error_nopadding(): try: - version.parse_version_info("v201809.2b0") + v1version.parse_version_info("v201809.2b0") assert False - except version.PatternError as err: + except v1version.PatternError as err: assert "Invalid version string" in str(err) -def test_part_field_mapping(): - a_names = set(version.PATTERN_PART_FIELDS.keys()) - b_names = set(patterns.PART_PATTERNS.keys()) - c_names = set(patterns.COMPOSITE_PART_PATTERNS.keys()) +def test_part_field_mapping_v1(): + a_names = set(v1version.PATTERN_PART_FIELDS.keys()) + b_names = set(v1patterns.PART_PATTERNS.keys()) + c_names = set(v1patterns.COMPOSITE_PART_PATTERNS.keys()) a_extra_names = a_names - b_names assert not any(a_extra_names), sorted(a_extra_names) b_extra_names = b_names - (a_names | c_names) assert not any(b_extra_names), sorted(b_extra_names) - a_fields = set(version.PATTERN_PART_FIELDS.values()) - b_fields = set(version.VersionInfo._fields) + a_fields = set(v1version.PATTERN_PART_FIELDS.values()) + b_fields = set(v1version.VersionInfo._fields) a_extra_fields = a_fields - b_fields b_extra_fields = b_fields - a_fields @@ -150,8 +152,18 @@ def test_part_field_mapping(): assert not any(b_extra_fields), sorted(b_extra_fields) +def test_part_field_mapping_v2(): + a_names = set(v2version.PATTERN_PART_FIELDS.keys()) + b_names = set(v2patterns.PART_PATTERNS.keys()) + + a_extra_names = a_names - b_names + assert not any(a_extra_names), sorted(a_extra_names) + b_extra_names = b_names - a_names + assert not any(b_extra_names), sorted(b_extra_names) + + def vnfo(**field_values): - return version._parse_field_values(field_values) + return v1version._parse_field_values(field_values) PARSE_VERSION_TEST_CASES = [ @@ -174,7 +186,7 @@ PARSE_VERSION_TEST_CASES = [ @pytest.mark.parametrize("pattern_str, line, expected_vinfo", PARSE_VERSION_TEST_CASES) def test_parse_versions(pattern_str, line, expected_vinfo): - pattern = patterns.compile_pattern(pattern_str) + pattern = v1patterns.compile_pattern(pattern_str) version_match = pattern.regexp.search(line) if expected_vinfo is None: @@ -184,6 +196,6 @@ def test_parse_versions(pattern_str, line, expected_vinfo): assert version_match is not None version_str = version_match.group(0) - version_info = version.parse_version_info(version_str, pattern_str) + version_info = v1version.parse_version_info(version_str, pattern_str) assert version_info == expected_vinfo From fbc92b109b33ab64b88b6bffe4c0f293c4977e58 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 17 Sep 2020 23:38:58 +0000 Subject: [PATCH 11/98] py2 paranoia --- test/test_cli.py | 11 +++++++++-- test/test_config.py | 11 +++++++++-- test/test_parse.py | 6 ++++++ test/test_patterns.py | 6 ++++++ test/test_rewrite.py | 9 ++++++++- test/test_version.py | 8 +++++++- 6 files changed, 45 insertions(+), 6 deletions(-) diff --git a/test/test_cli.py b/test/test_cli.py index a5d494d..82ff682 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -1,5 +1,8 @@ -# pylint:disable=redefined-outer-name ; pytest fixtures -# pylint:disable=protected-access ; allowed for test code +# -*- coding: utf-8 -*- +from __future__ import division +from __future__ import print_function +from __future__ import absolute_import +from __future__ import unicode_literals import os import time @@ -14,6 +17,10 @@ import pycalver.config as config import pycalver.patterns as v1patterns from pycalver.__main__ import cli +# pylint:disable=redefined-outer-name ; pytest fixtures +# pylint:disable=protected-access ; allowed for test code + + SETUP_CFG_FIXTURE = """ [metadata] license_file = LICENSE diff --git a/test/test_config.py b/test/test_config.py index ab8c4ba..bae5ade 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -1,11 +1,18 @@ -# pylint:disable=redefined-outer-name ; pytest fixtures -# pylint:disable=protected-access ; allowed for test code +# -*- coding: utf-8 -*- +from __future__ import division +from __future__ import print_function +from __future__ import absolute_import +from __future__ import unicode_literals import io from test import util from pycalver import config +# pylint:disable=redefined-outer-name ; pytest fixtures +# pylint:disable=protected-access ; allowed for test code + + PYCALVER_TOML_FIXTURE_1 = """ [pycalver] current_version = "v201808.0123-alpha" diff --git a/test/test_parse.py b/test/test_parse.py index e3106ad..ebd90e4 100644 --- a/test/test_parse.py +++ b/test/test_parse.py @@ -1,3 +1,9 @@ +# -*- coding: utf-8 -*- +from __future__ import division +from __future__ import print_function +from __future__ import absolute_import +from __future__ import unicode_literals + import pycalver.patterns as v1patterns from pycalver import parse diff --git a/test/test_patterns.py b/test/test_patterns.py index 66c7503..b67e992 100644 --- a/test/test_patterns.py +++ b/test/test_patterns.py @@ -1,3 +1,9 @@ +# -*- coding: utf-8 -*- +from __future__ import division +from __future__ import print_function +from __future__ import absolute_import +from __future__ import unicode_literals + import re import pytest diff --git a/test/test_rewrite.py b/test/test_rewrite.py index 1eb0dd7..78b6432 100644 --- a/test/test_rewrite.py +++ b/test/test_rewrite.py @@ -1,4 +1,8 @@ -# pylint:disable=protected-access ; allowed for test code +# -*- coding: utf-8 -*- +from __future__ import division +from __future__ import print_function +from __future__ import absolute_import +from __future__ import unicode_literals import copy from test import util @@ -9,6 +13,9 @@ from pycalver import version as v1version from pycalver2 import rewrite as v2rewrite from pycalver2 import version as v2version +# pylint:disable=protected-access ; allowed for test code + + REWRITE_FIXTURE = """ # SPDX-License-Identifier: MIT __version__ = "v201809.0002-beta" diff --git a/test/test_version.py b/test/test_version.py index 2cef58a..772e83b 100644 --- a/test/test_version.py +++ b/test/test_version.py @@ -1,4 +1,8 @@ -# pylint:disable=protected-access ; allowed for test code +# -*- coding: utf-8 -*- +from __future__ import division +from __future__ import print_function +from __future__ import absolute_import +from __future__ import unicode_literals import random import datetime as dt @@ -10,6 +14,8 @@ import pycalver2.version as v2version import pycalver.patterns as v1patterns import pycalver2.patterns as v2patterns +# pylint:disable=protected-access ; allowed for test code + def test_bump_beta(): cur_version = "v201712.0001-beta" From d4bd8a593195d9e46fe29884c1bc90f859c8e100 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 17 Sep 2020 23:40:55 +0000 Subject: [PATCH 12/98] move PATTERN_PART_FIELDS version -> patterns --- src/pycalver/patterns.py | 38 ++++++++++++++++++++++++++++++++++ src/pycalver/version.py | 43 +++------------------------------------ src/pycalver2/patterns.py | 30 +++++++++++++++++++++++++++ src/pycalver2/version.py | 29 -------------------------- test/test_patterns.py | 10 +++++++++ test/test_version.py | 19 +++++------------ 6 files changed, 86 insertions(+), 83 deletions(-) diff --git a/src/pycalver/patterns.py b/src/pycalver/patterns.py index 9f9fdc2..67e63a3 100644 --- a/src/pycalver/patterns.py +++ b/src/pycalver/patterns.py @@ -122,6 +122,44 @@ PART_PATTERNS = { } +PATTERN_PART_FIELDS = { + 'year' : 'year', + 'month' : 'month', + 'month_short': 'month', + 'pep440_tag' : 'tag', + 'tag' : 'tag', + 'yy' : 'year', + 'yyyy' : 'year', + 'quarter' : 'quarter', + 'iso_week' : 'iso_week', + 'us_week' : 'us_week', + 'dom' : 'dom', + 'doy' : 'doy', + 'dom_short' : 'dom', + 'doy_short' : 'doy', + 'MAJOR' : 'major', + 'MINOR' : 'minor', + 'MM' : 'minor', + 'MMM' : 'minor', + 'MMMM' : 'minor', + 'MMMMM' : 'minor', + 'PP' : 'patch', + 'PPP' : 'patch', + 'PPPP' : 'patch', + 'PPPPP' : 'patch', + 'PATCH' : 'patch', + 'build_no' : 'bid', + 'bid' : 'bid', + 'BID' : 'bid', + 'BB' : 'bid', + 'BBB' : 'bid', + 'BBBB' : 'bid', + 'BBBBB' : 'bid', + 'BBBBBB' : 'bid', + 'BBBBBBB' : 'bid', +} + + FULL_PART_FORMATS = { 'pep440_pycalver': "{year}{month:02}.{BID}{pep440_tag}", 'pycalver' : "v{year}{month:02}.{bid}{release}", diff --git a/src/pycalver/version.py b/src/pycalver/version.py index ffcf927..fef0c58 100644 --- a/src/pycalver/version.py +++ b/src/pycalver/version.py @@ -21,44 +21,6 @@ logger = logging.getLogger("pycalver.version") TODAY = dt.datetime.utcnow().date() -PATTERN_PART_FIELDS = { - 'year' : 'year', - 'month' : 'month', - 'month_short': 'month', - 'pep440_tag' : 'tag', - 'tag' : 'tag', - 'yy' : 'year', - 'yyyy' : 'year', - 'quarter' : 'quarter', - 'iso_week' : 'iso_week', - 'us_week' : 'us_week', - 'dom' : 'dom', - 'doy' : 'doy', - 'dom_short' : 'dom', - 'doy_short' : 'doy', - 'MAJOR' : 'major', - 'MINOR' : 'minor', - 'MM' : 'minor', - 'MMM' : 'minor', - 'MMMM' : 'minor', - 'MMMMM' : 'minor', - 'PP' : 'patch', - 'PPP' : 'patch', - 'PPPP' : 'patch', - 'PPPPP' : 'patch', - 'PATCH' : 'patch', - 'build_no' : 'bid', - 'bid' : 'bid', - 'BID' : 'bid', - 'BB' : 'bid', - 'BBB' : 'bid', - 'BBBB' : 'bid', - 'BBBBB' : 'bid', - 'BBBBBB' : 'bid', - 'BBBBBBB' : 'bid', -} - - class CalendarInfo(typ.NamedTuple): """Container for calendar components of version strings.""" @@ -263,7 +225,8 @@ class PatternError(Exception): def _parse_pattern_groups(pattern_groups: PatternGroups) -> FieldValues: for part_name in pattern_groups.keys(): is_valid_part_name = ( - part_name in v1patterns.COMPOSITE_PART_PATTERNS or part_name in PATTERN_PART_FIELDS + part_name in v1patterns.COMPOSITE_PART_PATTERNS + or part_name in v1patterns.PATTERN_PART_FIELDS ) if not is_valid_part_name: err_msg = f"Invalid part '{part_name}'" @@ -271,7 +234,7 @@ def _parse_pattern_groups(pattern_groups: PatternGroups) -> FieldValues: field_value_items = [ (field_name, pattern_groups[part_name]) - for part_name, field_name in PATTERN_PART_FIELDS.items() + for part_name, field_name in v1patterns.PATTERN_PART_FIELDS.items() if part_name in pattern_groups.keys() ] diff --git a/src/pycalver2/patterns.py b/src/pycalver2/patterns.py index 1217493..8c3496c 100644 --- a/src/pycalver2/patterns.py +++ b/src/pycalver2/patterns.py @@ -79,6 +79,36 @@ PART_PATTERNS = { 'BUILD': r"[0-9]+", 'TAG' : r"(?:alpha|beta|dev|rc|post|final)", 'PYTAG': r"(?:a|b|dev|rc|post)?[0-9]*", + +PATTERN_PART_FIELDS = { + 'YYYY' : 'year_y', + 'YY' : 'year_y', + '0Y' : 'year_y', + 'GGGG' : 'year_g', + 'GG' : 'year_g', + '0G' : 'year_g', + 'Q' : 'quarter', + 'MM' : 'month', + '0M' : 'month', + 'DD' : 'dom', + '0D' : 'dom', + 'JJJ' : 'doy', + '00J' : 'doy', + 'MAJOR': 'major', + 'MINOR': 'minor', + 'PATCH': 'patch', + 'MICRO': 'patch', + 'BUILD': 'bid', + 'TAG' : 'tag', + 'PYTAG': 'pytag', + 'NUM' : 'num', + 'WW' : 'week_w', + '0W' : 'week_w', + 'UU' : 'week_u', + '0U' : 'week_u', + 'VV' : 'week_v', + '0V' : 'week_v', +} } diff --git a/src/pycalver2/version.py b/src/pycalver2/version.py index 0f28b72..b715f30 100644 --- a/src/pycalver2/version.py +++ b/src/pycalver2/version.py @@ -22,35 +22,6 @@ logger = logging.getLogger("pycalver.version") TODAY = dt.datetime.utcnow().date() -PATTERN_PART_FIELDS = { - 'YYYY' : 'year_y', - 'YY' : 'year_y', - '0Y' : 'year_y', - 'Q' : 'quarter', - 'MM' : 'month', - '0M' : 'month', - 'DD' : 'dom', - '0D' : 'dom', - 'JJJ' : 'doy', - '00J' : 'doy', - 'MAJOR': 'major', - 'MINOR': 'minor', - 'PATCH': 'patch', - 'MICRO': 'patch', - 'BUILD': 'bid', - 'TAG' : 'tag', - 'PYTAG': 'pytag', - 'WW' : 'week_w', - '0W' : 'week_w', - 'UU' : 'week_u', - '0U' : 'week_u', - 'VV' : 'week_v', - '0V' : 'week_v', - 'GGGG' : 'year_g', - 'GG' : 'year_g', - '0G' : 'year_g', -} - ID_FIELDS_BY_PART = { 'MAJOR': 'major', 'MINOR': 'minor', diff --git a/test/test_patterns.py b/test/test_patterns.py index b67e992..774b66b 100644 --- a/test/test_patterns.py +++ b/test/test_patterns.py @@ -236,3 +236,13 @@ def test_curly_escapes(): match = pattern.regexp.search(CURLY_BRACE_FIXTURE) expected = 'package_metadata = {"name": "mypackage", "version": "v201812.0123-beta"}' assert match.group(0) == expected + + +def test_part_field_mapping_v2(): + a_names = set(v2patterns.PATTERN_PART_FIELDS.keys()) + b_names = set(v2patterns.PART_PATTERNS.keys()) + + a_extra_names = a_names - b_names + assert not any(a_extra_names), sorted(a_extra_names) + b_extra_names = b_names - a_names + assert not any(b_extra_names), sorted(b_extra_names) diff --git a/test/test_version.py b/test/test_version.py index 772e83b..09fb0af 100644 --- a/test/test_version.py +++ b/test/test_version.py @@ -10,9 +10,10 @@ import datetime as dt import pytest import pycalver.version as v1version -import pycalver2.version as v2version import pycalver.patterns as v1patterns -import pycalver2.patterns as v2patterns + +# import pycalver2.version as v2version +# import pycalver2.patterns as v2patterns # pylint:disable=protected-access ; allowed for test code @@ -140,7 +141,7 @@ def test_parse_error_nopadding(): def test_part_field_mapping_v1(): - a_names = set(v1version.PATTERN_PART_FIELDS.keys()) + a_names = set(v1patterns.PATTERN_PART_FIELDS.keys()) b_names = set(v1patterns.PART_PATTERNS.keys()) c_names = set(v1patterns.COMPOSITE_PART_PATTERNS.keys()) @@ -149,7 +150,7 @@ def test_part_field_mapping_v1(): b_extra_names = b_names - (a_names | c_names) assert not any(b_extra_names), sorted(b_extra_names) - a_fields = set(v1version.PATTERN_PART_FIELDS.values()) + a_fields = set(v1patterns.PATTERN_PART_FIELDS.values()) b_fields = set(v1version.VersionInfo._fields) a_extra_fields = a_fields - b_fields @@ -158,16 +159,6 @@ def test_part_field_mapping_v1(): assert not any(b_extra_fields), sorted(b_extra_fields) -def test_part_field_mapping_v2(): - a_names = set(v2version.PATTERN_PART_FIELDS.keys()) - b_names = set(v2patterns.PART_PATTERNS.keys()) - - a_extra_names = a_names - b_names - assert not any(a_extra_names), sorted(a_extra_names) - b_extra_names = b_names - a_names - assert not any(b_extra_names), sorted(b_extra_names) - - def vnfo(**field_values): return v1version._parse_field_values(field_values) From 5940fdbc40b885de20e02d00ab10b3a83b3838a9 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 17 Sep 2020 23:45:25 +0000 Subject: [PATCH 13/98] add v2 parsing --- src/pycalver2/patterns.py | 205 ++++++++++++---- src/pycalver2/version.py | 503 ++++++++++++++++++-------------------- test/test_patterns.py | 49 +++- 3 files changed, 435 insertions(+), 322 deletions(-) diff --git a/src/pycalver2/patterns.py b/src/pycalver2/patterns.py index 8c3496c..d1a5097 100644 --- a/src/pycalver2/patterns.py +++ b/src/pycalver2/patterns.py @@ -3,30 +3,33 @@ # # Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License # SPDX-License-Identifier: MIT -# """Compose Regular Expressions from Patterns. +"""Compose Regular Expressions from Patterns. -# >>> pattern = compile_pattern("vYYYY0M.BUILD[-TAG]") -# >>> version_info = pattern.regexp.match("v201712.0123-alpha") -# >>> assert version_info == { -# ... "version": "v201712.0123-alpha", -# ... "YYYY" : "2017", -# ... "0M" : "12", -# ... "BUILD" : "0123", -# ... "TAG" : "alpha", -# ... } -# >>> -# >>> version_info = pattern.regexp.match("201712.1234") -# >>> assert version_info is None +>>> pattern = compile_pattern("vYYYY0M.BUILD[-TAG]") +>>> version_info = pattern.regexp.match("v201712.0123-alpha") +>>> assert version_info.groupdict() == { +... "version": "v201712.0123-alpha", +... "year_y" : "2017", +... "month" : "12", +... "bid" : "0123", +... "tag" : "alpha", +... } +>>> +>>> version_info = pattern.regexp.match("201712.1234") +>>> assert version_info is None -# >>> version_info = pattern.regexp.match("v201712.1234") -# >>> assert version_info == { -# ... "version": "v201712.0123-alpha", -# ... "YYYY" : "2017", -# ... "0M" : "12", -# ... "BUILD" : "0123", -# ... "TAG" : None, -# ... } -# """ +>>> version_info = pattern.regexp.match("v201713.1234") +>>> assert version_info is None + +>>> version_info = pattern.regexp.match("v201712.1234") +>>> assert version_info.groupdict() == { +... "version": "v201712.1234", +... "year_y" : "2017", +... "month" : "12", +... "bid" : "1234", +... "tag" : None, +... } +""" import re import typing as typ @@ -42,43 +45,52 @@ PATTERN_ESCAPES = [ ("?" , "\u005c?"), ("{" , "\u005c{"), ("}" , "\u005c}"), - ("[" , "\u005c["), - ("]" , "\u005c]"), - ("(" , "\u005c("), - (")" , "\u005c)"), + # ("[" , "\u005c["), # [braces] are used for optional parts + # ("]" , "\u005c]"), + ("(", "\u005c("), + (")", "\u005c)"), ] +# NOTE (mb 2020-09-17): For patterns with different options, the longer +# patterns should be first/left (e.g. for 'MM', `1[0-2]` before `[1-9]`). +# This ensures that the longest match is done rather than the shortest. +# To have a consistent ordering, we always put the pattern that matches +# the larger number first (even if the patterns would otherwise be the +# same size). PART_PATTERNS = { # Based on calver.org 'YYYY': r"[1-9][0-9]{3}", 'YY' : r"[1-9][0-9]?", '0Y' : r"[0-9]{2}", - 'Q' : r"[1-4]", - 'MM' : r"(?:[1-9]|1[0-2])", - '0M' : r"(?:0[1-9]|1[0-2])", - 'DD' : r"(?:[1-9]|[1-2][0-9]|3[0-1])", - '0D' : r"(?:0[1-9]|[1-2][0-9]|3[0-1])", - 'JJJ' : r"(?:[1-9]|[1-9][0-9]|[1-2][0-9][0-9]|3[0-5][0-9]|36[0-6])", - '00J' : r"(?:00[1-9]|0[1-9][0-9]|[1-2][0-9][0-9]|3[0-5][0-9]|36[0-6])", - # week numbering parts - 'WW' : r"(?:[0-9]|[1-4][0-9]|5[0-2])", - '0W' : r"(?:[0-4][0-9]|5[0-2])", - 'UU' : r"(?:[0-9]|[1-4][0-9]|5[0-2])", - '0U' : r"(?:[0-4][0-9]|5[0-2])", - 'VV' : r"(?:[1-9]|[1-4][0-9]|5[0-3])", - '0V' : r"(?:0[1-9]|[1-4][0-9]|5[0-3])", 'GGGG': r"[1-9][0-9]{3}", 'GG' : r"[1-9][0-9]?", '0G' : r"[0-9]{2}", + 'Q' : r"[1-4]", + 'MM' : r"(?:1[0-2]|[1-9])", + '0M' : r"(?:1[0-2]|0[1-9])", + 'DD' : r"(?:3[0-1]|[1-2][0-9]|[1-9])", + '0D' : r"(?:3[0-1]|[1-2][0-9]|0[1-9])", + 'JJJ' : r"(?:36[0-6]|3[0-5][0-9]|[1-2][0-9][0-9]|[1-9][0-9]|[1-9])", + '00J' : r"(?:36[0-6]|3[0-5][0-9]|[1-2][0-9][0-9]|0[1-9][0-9]|00[1-9])", + # week numbering parts + 'WW': r"(?:5[0-2]|[1-4][0-9]|[0-9])", + '0W': r"(?:5[0-2]|[0-4][0-9])", + 'UU': r"(?:5[0-2]|[1-4][0-9]|[0-9])", + '0U': r"(?:5[0-2]|[0-4][0-9])", + 'VV': r"(?:5[0-3]|[1-4][0-9]|[1-9])", + '0V': r"(?:5[0-3]|[1-4][0-9]|0[1-9])", # non calver parts 'MAJOR': r"[0-9]+", 'MINOR': r"[0-9]+", 'PATCH': r"[0-9]+", 'MICRO': r"[0-9]+", 'BUILD': r"[0-9]+", - 'TAG' : r"(?:alpha|beta|dev|rc|post|final)", - 'PYTAG': r"(?:a|b|dev|rc|post)?[0-9]*", + 'TAG' : r"(?:alpha|beta|dev|pre|rc|post|final)", + 'PYTAG': r"(?:a|b|dev|rc|post)", + 'NUM' : r"[0-9]+", +} + PATTERN_PART_FIELDS = { 'YYYY' : 'year_y', @@ -109,17 +121,118 @@ PATTERN_PART_FIELDS = { 'VV' : 'week_v', '0V' : 'week_v', } + + +FieldValue = typ.Union[str, int] + + +def _fmt_num(val: FieldValue) -> str: + return str(val) + + +def _fmt_yy(year_y: FieldValue) -> str: + return str(int(str(year_y)[-2:])) + + +def _fmt_0y(year_y: FieldValue) -> str: + return "{0:02}".format(int(str(year_y)[-2:])) + + +def _fmt_gg(year_g: FieldValue) -> str: + return str(int(str(year_g)[-2:])) + + +def _fmt_0g(year_g: FieldValue) -> str: + return "{0:02}".format(int(str(year_g)[-2:])) + + +def _fmt_0m(month: FieldValue) -> str: + return "{0:02}".format(int(month)) + + +def _fmt_0d(dom: FieldValue) -> str: + return "{0:02}".format(int(dom)) + + +def _fmt_00j(doy: FieldValue) -> str: + return "{0:03}".format(int(doy)) + + +def _fmt_0w(week_w: FieldValue) -> str: + return "{0:02}".format(int(week_w)) + + +def _fmt_0u(week_u: FieldValue) -> str: + return "{0:02}".format(int(week_u)) + + +def _fmt_0v(week_v: FieldValue) -> str: + return "{0:02}".format(int(week_v)) + + +PART_FORMATS: typ.Dict[str, typ.Callable[[FieldValue], str]] = { + 'YYYY' : _fmt_num, + 'YY' : _fmt_yy, + '0Y' : _fmt_0y, + 'GGGG' : _fmt_num, + 'GG' : _fmt_gg, + '0G' : _fmt_0g, + 'Q' : _fmt_num, + 'MM' : _fmt_num, + '0M' : _fmt_0m, + 'DD' : _fmt_num, + '0D' : _fmt_0d, + 'JJJ' : _fmt_num, + '00J' : _fmt_00j, + 'MAJOR': _fmt_num, + 'MINOR': _fmt_num, + 'PATCH': _fmt_num, + 'MICRO': _fmt_num, + 'BUILD': _fmt_num, + 'TAG' : _fmt_num, + 'PYTAG': _fmt_num, + 'NUM' : _fmt_num, + 'WW' : _fmt_num, + '0W' : _fmt_0w, + 'UU' : _fmt_num, + '0U' : _fmt_0u, + 'VV' : _fmt_num, + '0V' : _fmt_0v, } def _replace_pattern_parts(pattern: str) -> str: # The pattern is escaped, so that everything besides the format # string variables is treated literally. + if "[" in pattern and "]" in pattern: + pattern = pattern.replace("[", "(?:") + pattern = pattern.replace("]", ")?") + + part_patterns_by_index: typ.Dict[typ.Tuple[int, int], typ.Tuple[int, int, str]] = {} for part_name, part_pattern in PART_PATTERNS.items(): - named_part_pattern = f"(?P<{part_name}>{part_pattern})" - placeholder = "\u005c{" + part_name + "\u005c}" - pattern = pattern.replace(placeholder, named_part_pattern) - return pattern + start_idx = pattern.find(part_name) + if start_idx < 0: + continue + + field = PATTERN_PART_FIELDS[part_name] + named_part_pattern = f"(?P<{field}>{part_pattern})" + end_idx = start_idx + len(part_name) + sort_key = (-end_idx, -len(part_name)) + part_patterns_by_index[sort_key] = (start_idx, end_idx, named_part_pattern) + + # NOTE (mb 2020-09-17): The sorting is done so that we process items: + # - right before left + # - longer before shorter + last_start_idx = len(pattern) + 1 + result_pattern = pattern + for _, (start_idx, end_idx, named_part_pattern) in sorted(part_patterns_by_index.items()): + if end_idx <= last_start_idx: + result_pattern = ( + result_pattern[:start_idx] + named_part_pattern + result_pattern[end_idx:] + ) + last_start_idx = start_idx + + return "(?P" + result_pattern + ")" def compile_pattern_str(pattern: str) -> str: diff --git a/src/pycalver2/version.py b/src/pycalver2/version.py index b715f30..e705293 100644 --- a/src/pycalver2/version.py +++ b/src/pycalver2/version.py @@ -12,9 +12,11 @@ import datetime as dt import lexid import pkg_resources -# import pycalver.patterns as v1patterns import pycalver2.patterns as v2patterns +# import pycalver.version as v1version +# import pycalver.patterns as v1patterns + logger = logging.getLogger("pycalver.version") @@ -34,11 +36,44 @@ ZERO_VALUES = { 'major': "0", 'minor': "0", 'patch': "0", - 'TAG' : "final", - 'PYTAG': "", + 'tag' : "final", + 'pytag': "", + 'num' : "0", } +TAG_BY_PEP440_TAG = { + 'a' : 'alpha', + 'b' : 'beta', + "" : 'final', + 'rc' : 'rc', + 'dev' : 'dev', + 'post': 'post', +} + + +PEP440_TAG_BY_TAG = { + 'alpha': "a", + 'beta' : "b", + 'final': "", + 'pre' : "rc", + 'rc' : "rc", + 'dev' : "dev", + 'post' : "post", +} + +assert set(TAG_BY_PEP440_TAG.keys()) == set(PEP440_TAG_BY_TAG.values()) +assert set(TAG_BY_PEP440_TAG.values()) < set(PEP440_TAG_BY_TAG.keys()) + +# PEP440_TAGS_REVERSE = { +# "a" : 'alpha', +# "b" : 'beta', +# "rc" : 'rc', +# "dev" : 'dev', +# "post": 'post', +# } + + class CalendarInfo(typ.NamedTuple): """Container for calendar components of version strings.""" @@ -79,27 +114,26 @@ def _quarter_from_month(month: int) -> int: def cal_info(date: dt.date = None) -> CalendarInfo: - # TODO reenable doctest - # """Generate calendar components for current date. + """Generate calendar components for current date. - # >>> from datetime import date + >>> import datetime as dt - # >>> c = cal_info(date(2019, 1, 5)) - # >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) - # (2019, 1, 1, 5, 5, 0, 0) + >>> c = cal_info(dt.date(2019, 1, 5)) + >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.week_w, c.week_u, c.week_v) + (2019, 1, 1, 5, 5, 0, 0, 1) - # >>> c = cal_info(date(2019, 1, 6)) - # >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) - # (2019, 1, 1, 6, 6, 0, 1) + >>> c = cal_info(dt.date(2019, 1, 6)) + >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.week_w, c.week_u, c.week_v) + (2019, 1, 1, 6, 6, 0, 1, 1) - # >>> c = cal_info(date(2019, 1, 7)) - # >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) - # (2019, 1, 1, 7, 7, 1, 1) + >>> c = cal_info(dt.date(2019, 1, 7)) + >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.week_w, c.week_u, c.week_v) + (2019, 1, 1, 7, 7, 1, 1, 2) - # >>> c = cal_info(date(2019, 4, 7)) - # >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) - # (2019, 2, 4, 7, 97, 13, 14) - # """ + >>> c = cal_info(dt.date(2019, 4, 7)) + >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.week_w, c.week_u, c.week_v) + (2019, 2, 4, 7, 97, 13, 14, 14) + """ if date is None: date = TODAY @@ -118,59 +152,105 @@ def cal_info(date: dt.date = None) -> CalendarInfo: return CalendarInfo(**kwargs) +MaybeInt = typ.Optional[int] + + class VersionInfo(typ.NamedTuple): """Container for parsed version string.""" - year_y : typ.Optional[int] - year_g : typ.Optional[int] - quarter: typ.Optional[int] - month : typ.Optional[int] - dom : typ.Optional[int] - doy : typ.Optional[int] - week_w : typ.Optional[int] - week_u : typ.Optional[int] - week_v : typ.Optional[int] + year_y : MaybeInt + year_g : MaybeInt + quarter: MaybeInt + month : MaybeInt + dom : MaybeInt + doy : MaybeInt + week_w : MaybeInt + week_u : MaybeInt + week_v : MaybeInt major : int minor : int patch : int bid : str tag : str pytag : str + num : MaybeInt +VALID_FIELD_KEYS = set(VersionInfo._fields) | {'version'} + FieldKey = str MatchGroupKey = str MatchGroupStr = str -PatternGroups = typ.Dict[MatchGroupKey, MatchGroupStr] -FieldValues = typ.Dict[FieldKey , MatchGroupStr] +PatternGroups = typ.Dict[FieldKey, MatchGroupStr] +FieldValues = typ.Dict[FieldKey, MatchGroupStr] -def _parse_field_values(field_values: FieldValues) -> VersionInfo: +def _parse_version_info(field_values: FieldValues) -> VersionInfo: + """Parse normalized VersionInfo from groups of a matched pattern. + + >>> vnfo = _parse_version_info({'year_y': "2018", 'month': "11", 'bid': "0099"}) + >>> (vnfo.year_y, vnfo.month, vnfo.quarter, vnfo.bid, vnfo.tag) + (2018, 11, 4, '0099', 'final') + + >>> vnfo = _parse_version_info({'year_y': "2018", 'doy': "11", 'bid': "099", 'tag': "beta"}) + >>> (vnfo.year_y, vnfo.month, vnfo.dom, vnfo.doy, vnfo.bid, vnfo.tag) + (2018, 1, 11, 11, '099', 'beta') + + >>> vnfo = _parse_version_info({'year_y': "2018", 'month': "6", 'dom': "15"}) + >>> (vnfo.year_y, vnfo.month, vnfo.dom, vnfo.doy) + (2018, 6, 15, 166) + + >>> vnfo = _parse_version_info({'major': "1", 'minor': "23", 'patch': "45"}) + >>> (vnfo.major, vnfo.minor, vnfo.patch) + (1, 23, 45) + + >>> vnfo = _parse_version_info({'major': "1", 'minor': "023", 'patch': "0045"}) + >>> (vnfo.major, vnfo.minor, vnfo.patch) + (1, 23, 45) + + >>> vnfo = _parse_version_info({'year_y': "2021", 'week_w': "02"}) + >>> (vnfo.year_y, vnfo.week_w) + (2021, 2) + >>> vnfo = _parse_version_info({'year_y': "2021", 'week_u': "02"}) + >>> (vnfo.year_y, vnfo.week_u) + (2021, 2) + >>> vnfo = _parse_version_info({'year_g': "2021", 'week_v': "02"}) + >>> (vnfo.year_g, vnfo.week_v) + (2021, 2) + + >>> vnfo = _parse_version_info({'year_y': "2021", 'month': "01", 'dom': "03"}) + >>> (vnfo.year_y, vnfo.month, vnfo.dom, vnfo.tag) + (2021, 1, 3, 'final') + >>> (vnfo.year_y, vnfo.week_w,vnfo.year_y, vnfo.week_u,vnfo.year_g, vnfo.week_v) + (2021, 0, 2021, 1, 2020, 53) + """ + for key in field_values: + assert key in VALID_FIELD_KEYS, key + fvals = field_values - tag = fvals.get('tag') - if tag is None: - tag = "final" + tag = fvals.get('tag' , "final") + pytag = fvals.get('pytag', "") - tag = TAG_ALIASES.get(tag, tag) - assert tag is not None - # TODO (mb 2020-09-06): parts of course - pytag = "TODO" + if tag and not pytag: + pytag = PEP440_TAG_BY_TAG[tag] + elif pytag and not tag: + tag = TAG_BY_PEP440_TAG[pytag] - bid = fvals['bid'] if 'bid' in fvals else "1001" - - year_y = int(fvals['year_y']) if 'year_y' in fvals else None - year_g = int(fvals['year_g']) if 'year_g' in fvals else None - doy = int(fvals['doy' ]) if 'doy' in fvals else None + num: MaybeInt = int(fvals['num']) if 'num' in fvals else None date: typ.Optional[dt.date] = None - month: typ.Optional[int] = None - dom : typ.Optional[int] = None + year_y: MaybeInt = int(fvals['year_y']) if 'year_y' in fvals else None + year_g: MaybeInt = int(fvals['year_g']) if 'year_g' in fvals else None - week_w: typ.Optional[int] = None - week_u: typ.Optional[int] = None - week_v: typ.Optional[int] = None + month: MaybeInt = int(fvals['month']) if 'month' in fvals else None + doy : MaybeInt = int(fvals['doy' ]) if 'doy' in fvals else None + dom : MaybeInt = int(fvals['dom' ]) if 'dom' in fvals else None + + week_w: MaybeInt = int(fvals['week_w']) if 'week_w' in fvals else None + week_u: MaybeInt = int(fvals['week_u']) if 'week_u' in fvals else None + week_v: MaybeInt = int(fvals['week_v']) if 'week_v' in fvals else None if year_y and doy: date = _date_from_doy(year_y, doy) @@ -180,26 +260,31 @@ def _parse_field_values(field_values: FieldValues) -> VersionInfo: month = int(fvals['month']) if 'month' in fvals else None dom = int(fvals['dom' ]) if 'dom' in fvals else None - quarter = int(fvals['quarter']) if 'quarter' in fvals else None - if quarter is None and month: - quarter = _quarter_from_month(month) - if year_y and month and dom: date = dt.date(year_y, month, dom) if date: # derive all fields from other previous values + year_y = int(date.strftime("%Y"), base=10) + year_g = int(date.strftime("%G"), base=10) + month = int(date.strftime("%m"), base=10) + dom = int(date.strftime("%d"), base=10) doy = int(date.strftime("%j"), base=10) week_w = int(date.strftime("%W"), base=10) week_u = int(date.strftime("%U"), base=10) week_v = int(date.strftime("%V"), base=10) - year_g = int(date.strftime("%G"), base=10) + + quarter = int(fvals['quarter']) if 'quarter' in fvals else None + if quarter is None and month: + quarter = _quarter_from_month(month) major = int(fvals['major']) if 'major' in fvals else 0 minor = int(fvals['minor']) if 'minor' in fvals else 0 patch = int(fvals['patch']) if 'patch' in fvals else 0 - return VersionInfo( + bid = fvals['bid'] if 'bid' in fvals else "1000" + + vnfo = VersionInfo( year_y=year_y, year_g=year_g, quarter=quarter, @@ -215,47 +300,9 @@ def _parse_field_values(field_values: FieldValues) -> VersionInfo: bid=bid, tag=tag, pytag=pytag, + num=num, ) - - -def _is_calver(nfo: typ.Union[CalendarInfo, VersionInfo]) -> bool: - # TODO reenable doctest - # """Check pattern for any calendar based parts. - - # >>> _is_calver(cal_info()) - # True - - # >>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0018"}) - # >>> _is_calver(vnfo) - # True - - # >>> vnfo = _parse_version_info({'MAJOR': "1", 'MINOR': "023", 'PATCH': "45"}) - # >>> _is_calver(vnfo) - # False - # """ - for field in CalendarInfo._fields: - maybe_val: typ.Any = getattr(nfo, field, None) - if isinstance(maybe_val, int): - return True - - return False - - -TAG_ALIASES: typ.Dict[str, str] = { - 'a' : "alpha", - 'b' : "beta", - 'pre': "rc", -} - - -PEP440_TAGS: typ.Dict[str, str] = { - 'alpha': "a", - 'beta' : "b", - 'final': "", - 'rc' : "rc", - 'dev' : "dev", - 'post' : "post", -} + return vnfo VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]] @@ -265,65 +312,17 @@ class PatternError(Exception): pass -def _parse_pattern_groups(pattern_groups: PatternGroups) -> FieldValues: - for part_name in pattern_groups.keys(): - is_valid_part_name = ( - part_name in v2patterns.COMPOSITE_PART_PATTERNS or part_name in PATTERN_PART_FIELDS - ) - if not is_valid_part_name: - err_msg = f"Invalid part '{part_name}'" - raise PatternError(err_msg) - - field_value_items = [ - (field_name, pattern_groups[part_name]) - for part_name, field_name in PATTERN_PART_FIELDS.items() - if part_name in pattern_groups.keys() - ] - - all_fields = [field_name for field_name, _ in field_value_items] - unique_fields = set(all_fields) - duplicate_fields = [f for f in unique_fields if all_fields.count(f) > 1] - - if any(duplicate_fields): - err_msg = f"Multiple parts for same field {duplicate_fields}." - raise PatternError(err_msg) - - return dict(field_value_items) - - -def _parse_version_info(pattern_groups: PatternGroups) -> VersionInfo: - # TODO reenable doctest - # """Parse normalized VersionInfo from groups of a matched pattern. - - # >>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0099"}) - # >>> (vnfo.year_y, vnfo.month, vnfo.quarter, vnfo.bid, vnfo.tag) - # (2018, 11, 4, '0099', 'final') - - # >>> vnfo = _parse_version_info({'year': "2018", 'doy': "11", 'bid': "099", 'tag': "b"}) - # >>> (vnfo.year_y, vnfo.month, vnfo.dom, vnfo.bid, vnfo.tag) - # (2018, 1, 11, '099', 'beta') - - # >>> vnfo = _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "45"}) - # >>> (vnfo.major, vnfo.minor, vnfo.patch) - # (1, 23, 45) - - # >>> vnfo = _parse_version_info({'MAJOR': "1", 'MMM': "023", 'PPPP': "0045"}) - # >>> (vnfo.major, vnfo.minor, vnfo.patch) - # (1, 23, 45) - # """ - field_values = _parse_pattern_groups(pattern_groups) - return _parse_field_values(field_values) - - def parse_version_info(version_str: str, pattern: str = "vYYYY0M.BUILD[-TAG]") -> VersionInfo: - # """Parse normalized VersionInfo. + """Parse normalized VersionInfo. - # >>> vnfo = parse_version_info("v201712.0033-beta", pattern="vYYYY0M.BUILD[-TAG]") - # >>> assert vnfo == _parse_version_info({'year': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"}) + >>> vnfo = parse_version_info("v201712.0033-beta", pattern="vYYYY0M.BUILD[-TAG]") + >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"} + >>> assert vnfo == _parse_version_info(fvals) - # >>> vnfo = parse_version_info("1.23.456", pattern="MAJOR.MINOR.PATCH") - # >>> assert vnfo == _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "456"}) - # """ + >>> vnfo = parse_version_info("1.23.456", pattern="MAJOR.MINOR.PATCH") + >>> fvals = {'major': "1", 'minor': "23", 'patch': "456"} + >>> assert vnfo == _parse_version_info(fvals) + """ pattern_tup = v2patterns.compile_pattern(pattern) match = pattern_tup.regexp.match(version_str) if match is None: @@ -332,23 +331,23 @@ def parse_version_info(version_str: str, pattern: str = "vYYYY0M.BUILD[-TAG]") - f"for pattern '{pattern}'/'{pattern_tup.regexp.pattern}'" ) raise PatternError(err_msg) - - return _parse_version_info(match.groupdict()) + else: + field_values = match.groupdict() + return _parse_version_info(field_values) -def is_valid(version_str: str, pattern: str = "{pycalver}") -> bool: - # TODO reenable doctest - # """Check if a version matches a pattern. +def is_valid(version_str: str, pattern: str = "vYYYY0M.BUILD[-TAG]") -> bool: + """Check if a version matches a pattern. - # >>> is_valid("v201712.0033-beta", pattern="{pycalver}") - # True - # >>> is_valid("v201712.0033-beta", pattern="{semver}") - # False - # >>> is_valid("1.2.3", pattern="{semver}") - # True - # >>> is_valid("v201712.0033-beta", pattern="{semver}") - # False - # """ + >>> is_valid("v201712.0033-beta", pattern="vYYYY0M.BUILD[-TAG]") + True + >>> is_valid("v201712.0033-beta", pattern="MAJOR.MINOR.PATCH") + False + >>> is_valid("1.2.3", pattern="MAJOR.MINOR.PATCH") + True + >>> is_valid("v201712.0033-beta", pattern="MAJOR.MINOR.PATCH") + False + """ try: parse_version_info(version_str, pattern) return True @@ -359,140 +358,106 @@ def is_valid(version_str: str, pattern: str = "{pycalver}") -> bool: TemplateKwargs = typ.Dict[str, typ.Union[str, int, None]] -def _derive_template_kwargs(vinfo: VersionInfo) -> TemplateKwargs: +def _format_part_values(vinfo: VersionInfo) -> typ.Dict[str, str]: """Generate kwargs for template from minimal VersionInfo. The VersionInfo Tuple only has the minimal representation of a parsed version, not the values suitable for formatting. It may for example have month=9, but not the formatted representation '09' for '0M'. + + >>> vinfo = parse_version_info("v200709.1033-beta", pattern="vYYYY0M.BUILD[-TAG]") + >>> kwargs = _format_part_values(vinfo) + >>> (kwargs['YYYY'], kwargs['0M'], kwargs['BUILD'], kwargs['TAG']) + ('2007', '09', '1033', 'beta') + >>> (kwargs['YY'], kwargs['0Y'], kwargs['MM'], kwargs['PYTAG']) + ('7', '07', '9', 'b') """ - kwargs: TemplateKwargs = vinfo._asdict() + vnfo_kwargs: TemplateKwargs = vinfo._asdict() + kwargs : typ.Dict[str, str] = {} - tag = vinfo.tag - kwargs['TAG'] = tag - if tag == 'final': - kwargs['PYTAG'] = "" - else: - kwargs['PYTAG'] = PEP440_TAGS[tag] + "0" + for part, field in v2patterns.PATTERN_PART_FIELDS.items(): + field_val = vnfo_kwargs[field] + if field_val is None: + continue - year_y = vinfo.year_y - if year_y: - kwargs['0Y' ] = str(year_y)[-2:] - kwargs['YY' ] = int(str(year_y)[-2:]) - kwargs['YYYY'] = year_y - - year_g = vinfo.year_g - if year_g: - kwargs['0G' ] = str(year_g)[-2:] - kwargs['GG' ] = int(str(year_g)[-2:]) - kwargs['GGGG'] = year_g - - kwargs['BUILD'] = int(vinfo.bid, 10) - - for part_name, field in ID_FIELDS_BY_PART.items(): - val = kwargs[field] - if part_name.lower() == field.lower(): - if isinstance(val, str): - kwargs[part_name] = int(val, base=10) - else: - kwargs[part_name] = val - else: - assert len(set(part_name)) == 1 - padded_len = len(part_name) - kwargs[part_name] = str(val).zfill(padded_len) + format_fn = v2patterns.PART_FORMATS[part] + kwargs[part] = format_fn(field_val) return kwargs -def _compile_format_template(pattern: str, kwargs: TemplateKwargs) -> str: - # NOTE (mb 2020-09-04): Some parts are optional, we need the kwargs to - # determine if part is set to its zero value - format_tmpl = pattern - for part_name, full_part_format in v2patterns.FULL_PART_FORMATS.items(): - format_tmpl = format_tmpl.replace("{" + part_name + "}", full_part_format) - return format_tmpl - - def format_version(vinfo: VersionInfo, pattern: str) -> str: - # TODO reenable doctest - # """Generate version string. + """Generate version string. - # >>> import datetime as dt - # >>> vinfo = parse_version_info("v201712.0033-beta", pattern="{pycalver}") - # >>> vinfo_a = vinfo._replace(**cal_info(date=dt.date(2017, 1, 1))._asdict()) - # >>> vinfo_b = vinfo._replace(**cal_info(date=dt.date(2017, 12, 31))._asdict()) + >>> import datetime as dt + >>> vinfo = parse_version_info("v200712.0033-beta", pattern="vYYYY0M.BUILD[-TAG]") + >>> vinfo_a = vinfo._replace(**cal_info(date=dt.date(2007, 1, 1))._asdict()) + >>> vinfo_b = vinfo._replace(**cal_info(date=dt.date(2007, 12, 31))._asdict()) - # >>> format_version(vinfo_a, pattern="v{yy}.{BID}{release}") - # 'v17.33-beta' - # >>> format_version(vinfo_a, pattern="vYY.BUILD[-TAG]") - # 'v17.33-beta' - # >>> format_version(vinfo_a, pattern="YYYY0M.BUILD[PYTAG]") - # '201701.33b0' + >>> format_version(vinfo_a, pattern="vYY.BUILD[-TAG]") + 'v7.33-beta' + >>> format_version(vinfo_a, pattern="v0Y.BUILD[-TAG]") + 'v07.33-beta' + >>> format_version(vinfo_a, pattern="YYYY0M.BUILD[PYTAG][NUM]") + '201701.33b0' - # >>> format_version(vinfo_a, pattern="{pycalver}") - # 'v201701.0033-beta' - # >>> format_version(vinfo_b, pattern="{pycalver}") - # 'v201712.0033-beta' + >>> format_version(vinfo_a, pattern="vYYYY0M.BUILD[-TAG]") + 'v201701.0033-beta' + >>> format_version(vinfo_b, pattern="vYYYY0M.BUILD[-TAG]") + 'v201712.0033-beta' - # >>> format_version(vinfo_a, pattern="v{year}w{iso_week}.{BID}{release}") - # 'v2017w00.33-beta' - # >>> format_version(vinfo_a, pattern="vYYYYwWW.BUILD[-TAG]") - # 'v2017w00.33-beta' - # >>> format_version(vinfo_b, pattern="v{year}w{iso_week}.{BID}{release}") - # 'v2017w52.33-beta' - # >>> format_version(vinfo_b, pattern="vYYYYwWW.BUILD[-TAG]") - # 'v2017w52.33-beta' + >>> format_version(vinfo_a, pattern="vYYYYwWW.BUILD[-TAG]") + 'v2017w00.33-beta' + >>> format_version(vinfo_b, pattern="vYYYYwWW.BUILD[-TAG]") + 'v2017w52.33-beta' - # >>> format_version(vinfo_a, pattern="v{year}d{doy}.{bid}{release}") - # 'v2017d001.0033-beta' - # >>> format_version(vinfo_b, pattern="v{year}d{doy}.{bid}{release}") - # 'v2017d365.0033-beta' - # >>> format_version(vinfo_a, pattern="vYYYYdJJJ.BUILD[-TAG]") - # 'v2017d001.0033-beta' - # >>> format_version(vinfo_b, pattern="vYYYYdJJJ.BUILD[-TAG]") - # 'v2017d365.0033-beta' + >>> format_version(vinfo_a, pattern="vYYYYdJJJ.BUILD[-TAG]") + 'v2017d001.0033-beta' + >>> format_version(vinfo_b, pattern="vYYYYdJJJ.BUILD[-TAG]") + 'v2017d365.0033-beta' - # >>> format_version(vinfo_a, pattern="vGGGGwVV.BUILD[-TAG]") - # 'v2016w52.0033-beta' + >>> format_version(vinfo_a, pattern="vGGGGwVV.BUILD[-TAG]") + 'v2016w52.0033-beta' - # >>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final') + >>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final') - # >>> format_version(vinfo_c, pattern="v{year}w{iso_week}.{BID}-{tag}") - # 'v2017w52.33-final' - # >>> format_version(vinfo_c, pattern="v{year}w{iso_week}.{BID}{release}") - # 'v2017w52.33' - # >>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD-TAG") - # 'v2017w52.33-final' - # >>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD[-TAG]") - # 'v2017w52.33' + >>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD-TAG") + 'v2017w52.33-final' + >>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD[-TAG]") + 'v2017w52.33' - # >>> format_version(vinfo_c, pattern="v{MAJOR}.{MINOR}.{PATCH}") - # 'v1.2.34' - # >>> format_version(vinfo_c, pattern="vMAJOR.MINOR.PATCH") - # 'v1.2.34' + >>> format_version(vinfo_c, pattern="vMAJOR.MINOR.PATCH") + 'v1.2.34' - # >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='final') - # >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-TAG") - # 'v1.0.0-final' - # >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH[-TAG]") - # 'v1.0.0' - # >>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.PATCH[-TAG]]") - # 'v1.0' - # >>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.MICRO[-TAG]]") - # 'v1.0' - # >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-TAG]]]") - # 'v1' - # """ - kwargs = _derive_template_kwargs(vinfo) - format_tmpl = _compile_format_template(pattern, kwargs) + >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='final') + >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-TAGNUM") + 'v1.0.0-final0' + >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-TAG[NUM]") + 'v1.0.0-final' + >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-TAG") + 'v1.0.0-final' + >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH[-TAG]") + 'v1.0.0' + >>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.PATCH[-TAG]]") + 'v1.0' + >>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.MICRO[-TAG]]") + 'v1.0' + >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-TAG]]]") + 'v1' + """ + kwargs = _format_part_values(vinfo) + part_values = sorted(kwargs.items(), key=lambda item: -len(item[0])) + version = pattern + for part, value in part_values: + version = version.replace(part, value) - return format_tmpl.format(**kwargs) + return version def incr( old_version: str, - pattern : str = "{pycalver}", + pattern : str = "vYYYY0M.BUILD[-TAG]", *, release: str = None, major : bool = False, diff --git a/test/test_patterns.py b/test/test_patterns.py index 774b66b..6e6d25f 100644 --- a/test/test_patterns.py +++ b/test/test_patterns.py @@ -129,8 +129,25 @@ V2_PART_PATTERN_CASES = [ (['0V'], "53", "53"), (['0V'], "54", None), (['MAJOR', 'MINOR', 'PATCH', 'MICRO'], "0", "0"), - # ('TAG', ""), - # ('PYTAG', ""), + (['TAG' ], "alpha" , "alpha"), + (['TAG' ], "alfa" , None), + (['TAG' ], "beta" , "beta"), + (['TAG' ], "dev" , "dev"), + (['TAG' ], "rc" , "rc"), + (['TAG' ], "post" , "post"), + (['TAG' ], "final" , "final"), + (['TAG' ], "latest", None), + (['PYTAG'], "a" , "a"), + (['PYTAG'], "b" , "b"), + (['PYTAG'], "dev" , "dev"), + (['PYTAG'], "rc" , "rc"), + (['PYTAG'], "post" , "post"), + (['PYTAG'], "post" , "post"), + (['PYTAG'], "x" , None), + (['NUM' ], "a" , None), + (['NUM' ], "0" , "0"), + (['NUM' ], "1" , "1"), + (['NUM' ], "10" , "10"), ] @@ -191,16 +208,16 @@ def test_re_pattern_parts(part_name, line, expected): assert result_val == expected, (part_name, line) -PATTERN_CASES = [ +PATTERN_V1_CASES = [ (r"v{year}.{month}.{MINOR}" , "v2017.11.1" , "v2017.11.1"), (r"v{year}.{month}.{MINOR}" , "v2017.07.12", "v2017.07.12"), - (r"v{year}.{month_short}.{MINOR}", "v2017.11.1" , "v2017.11.1"), - (r"v{year}.{month_short}.{MINOR}", "v2017.7.12" , "v2017.7.12"), + (r"v{year}.{month_short}.{PATCH}", "v2017.11.1" , "v2017.11.1"), + (r"v{year}.{month_short}.{PATCH}", "v2017.7.12" , "v2017.7.12"), ] -@pytest.mark.parametrize("pattern_str, line, expected", PATTERN_CASES) -def test_patterns(pattern_str, line, expected): +@pytest.mark.parametrize("pattern_str, line, expected", PATTERN_V1_CASES) +def test_patterns_v1(pattern_str, line, expected): pattern = v1patterns.compile_pattern(pattern_str) result = pattern.regexp.search(line) if result is None: @@ -210,6 +227,24 @@ def test_patterns(pattern_str, line, expected): assert result_val == expected, (pattern_str, line) +PATTERN_V2_CASES = [ + ("vYYYY.0M.MINOR" , "v2017.11.1" , "v2017.11.1"), + ("vYYYY.0M.MINOR" , "v2017.07.12", "v2017.07.12"), + ("YYYY.MM[.PATCH]", "2017.11.1" , "2017.11.1"), + ("YYYY.MM[.PATCH]", "2017.7.12" , "2017.7.12"), + ("YYYY.MM[.PATCH]", "2017.7" , "2017.7"), + ("YYYY0M.BUILD" , "201707.1000", "201707.1000"), +] + + +@pytest.mark.parametrize("pattern_str, line, expected", PATTERN_V2_CASES) +def test_patterns_v2(pattern_str, line, expected): + pattern = v2patterns.compile_pattern(pattern_str) + result = pattern.regexp.search(line) + result_val = None if result is None else result.group(0) + assert result_val == expected, (pattern_str, line, pattern.regexp.pattern) + + CLI_MAIN_FIXTURE = """ @click.group() @click.version_option(version="v201812.0123-beta") From f11c8c89f8588e96ab309bd018b9ff00bd108898 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 17 Sep 2020 23:46:30 +0000 Subject: [PATCH 14/98] inc BUILD by +1000 if < 1000 --- CHANGELOG.md | 1 + src/pycalver2/version.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 957b86c..c2f31a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - New gitlab #7: New style patterns, to be in line with CalVer.org - Better support for week numbering. - Better support for optional parts. + - New: Start `BUILD` parts at `1000` to avoid leading zero truncation. - New gitlab #10: `--pin-date` to keep date parts unchanged, and only increment non-date parts. - Fix gitlab #8: Push tags only pushed tags, not actual commit. - Fix gitlab #9: Make commit message configurable. diff --git a/src/pycalver2/version.py b/src/pycalver2/version.py index e705293..5a6bc5a 100644 --- a/src/pycalver2/version.py +++ b/src/pycalver2/version.py @@ -486,7 +486,12 @@ def incr( else: logger.warning(f"Version appears to be from the future '{old_version}'") - cur_vinfo = cur_vinfo._replace(bid=lexid.incr(cur_vinfo.bid)) + _bid = cur_vinfo.bid + if int(_bid) < 1000: + # prevent truncation of leading zeros + _bid = str(int(_bid) + 1000) + + cur_vinfo = cur_vinfo._replace(bid=lexid.incr(_bid)) if major: cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0, patch=0) From 851b302112fdefe168f798f2c5fa813b009ffee8 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 17 Sep 2020 23:46:55 +0000 Subject: [PATCH 15/98] cleanup --- src/pycalver/patterns.py | 57 ++++++++++++++++++++-------------------- src/pycalver/version.py | 8 +++--- 2 files changed, 33 insertions(+), 32 deletions(-) diff --git a/src/pycalver/patterns.py b/src/pycalver/patterns.py index 67e63a3..3b36dee 100644 --- a/src/pycalver/patterns.py +++ b/src/pycalver/patterns.py @@ -185,34 +185,35 @@ FULL_PART_FORMATS = { } -PART_FORMATS = { - 'major' : "[0-9]+", - 'minor' : "[0-9]{3,}", - 'patch' : "[0-9]{3,}", - 'bid' : "[0-9]{4,}", - 'MAJOR' : "[0-9]+", - 'MINOR' : "[0-9]+", - 'MM' : "[0-9]{2,}", - 'MMM' : "[0-9]{3,}", - 'MMMM' : "[0-9]{4,}", - 'MMMMM' : "[0-9]{5,}", - 'MMMMMM' : "[0-9]{6,}", - 'MMMMMMM': "[0-9]{7,}", - 'PATCH' : "[0-9]+", - 'PP' : "[0-9]{2,}", - 'PPP' : "[0-9]{3,}", - 'PPPP' : "[0-9]{4,}", - 'PPPPP' : "[0-9]{5,}", - 'PPPPPP' : "[0-9]{6,}", - 'PPPPPPP': "[0-9]{7,}", - 'BID' : "[1-9][0-9]*", - 'BB' : "[1-9][0-9]{1,}", - 'BBB' : "[1-9][0-9]{2,}", - 'BBBB' : "[1-9][0-9]{3,}", - 'BBBBB' : "[1-9][0-9]{4,}", - 'BBBBBB' : "[1-9][0-9]{5,}", - 'BBBBBBB': "[1-9][0-9]{6,}", -} +# TODO (mb 2020-09-17): I think this is garbage +# PART_FORMATS = { +# 'major' : "[0-9]+", +# 'minor' : "[0-9]{3,}", +# 'patch' : "[0-9]{3,}", +# 'bid' : "[0-9]{4,}", +# 'MAJOR' : "[0-9]+", +# 'MINOR' : "[0-9]+", +# 'MM' : "[0-9]{2,}", +# 'MMM' : "[0-9]{3,}", +# 'MMMM' : "[0-9]{4,}", +# 'MMMMM' : "[0-9]{5,}", +# 'MMMMMM' : "[0-9]{6,}", +# 'MMMMMMM': "[0-9]{7,}", +# 'PATCH' : "[0-9]+", +# 'PP' : "[0-9]{2,}", +# 'PPP' : "[0-9]{3,}", +# 'PPPP' : "[0-9]{4,}", +# 'PPPPP' : "[0-9]{5,}", +# 'PPPPPP' : "[0-9]{6,}", +# 'PPPPPPP': "[0-9]{7,}", +# 'BID' : "[1-9][0-9]*", +# 'BB' : "[1-9][0-9]{1,}", +# 'BBB' : "[1-9][0-9]{2,}", +# 'BBBB' : "[1-9][0-9]{3,}", +# 'BBBBB' : "[1-9][0-9]{4,}", +# 'BBBBBB' : "[1-9][0-9]{5,}", +# 'BBBBBBB': "[1-9][0-9]{6,}", +# } class Pattern(typ.NamedTuple): diff --git a/src/pycalver/version.py b/src/pycalver/version.py index fef0c58..6bf8658 100644 --- a/src/pycalver/version.py +++ b/src/pycalver/version.py @@ -245,8 +245,8 @@ def _parse_pattern_groups(pattern_groups: PatternGroups) -> FieldValues: if any(duplicate_fields): err_msg = f"Multiple parts for same field {duplicate_fields}." raise PatternError(err_msg) - - return dict(field_value_items) + else: + return dict(field_value_items) def _parse_version_info(pattern_groups: PatternGroups) -> VersionInfo: @@ -289,8 +289,8 @@ def parse_version_info(version_str: str, pattern: str = "{pycalver}") -> Version f"for pattern '{pattern}'/'{pattern_tup.regexp.pattern}'" ) raise PatternError(err_msg) - - return _parse_version_info(match.groupdict()) + else: + return _parse_version_info(match.groupdict()) def is_valid(version_str: str, pattern: str = "{pycalver}") -> bool: From 5927770ddda227ce402615443ef230dde80d05a4 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Fri, 18 Sep 2020 13:19:01 +0000 Subject: [PATCH 16/98] Add BLD part --- README.md | 126 ++++++++++++++++++++++---------------- src/pycalver2/patterns.py | 7 +++ test/test_version.py | 26 +++++++- 3 files changed, 103 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 9e79130..9774893 100644 --- a/README.md +++ b/README.md @@ -222,25 +222,27 @@ You can also define custom patterns in the items of the `pycalver:file_patterns` These patterns are closely based on https://calver.org/ -| placeholder | range / example(s) | comment | -|-------------|---------------------|----------------------| -| YYYY | 2019, 2020... | `%Y` | -| YY | 18, 19..99, 1, 2 | `int(%y)` | -| 0Y | 18, 19..99, 01, 02 | `%y` | -| Q | 1, 2, 3, 4 | quarter | -| MM | 9, 10, 11, 12 | `int(%m)` | -| 0M | 09, 10, 11, 12 | `%m` | -| DD | 1, 2, 3..31 | `int(%d)` | -| 0D | 01, 02, 03..31 | `%d` | -| JJJ | 1,2,3..366 | `int(%j)` | -| 00J | 001, 002..366 | `%j` | -| BUILD | 1001, 1002, 23456 | build number (lexid) | -| TAG | alpha, beta, rc | `--release=` | -| PYTAG | a0, b0, rc | `--release=` | -| MAJOR | 0..9, 10..99, 100.. | `--major` | -| MINOR | 0..9, 10..99, 100.. | `--minor` | -| PATCH | 0..9, 10..99, 100.. | `--patch` | -| MICRO | 0..9, 10..99, 100.. | Synonym for PATCH | +| placeholder | range / example(s) | comment | +|-------------|----------------------|------------------------| +| `YYYY` | 2019, 2020... | `%Y` | +| `YY` | 18, 19..99, 1, 2 | `int(%y)` | +| `0Y` | 18, 19..99, 01, 02 | `%y` | +| `Q` | 1, 2, 3, 4 | quarter | +| `MM` | 9, 10, 11, 12 | `int(%m)` | +| `0M` | 09, 10, 11, 12 | `%m` | +| `DD` | 1, 2, 3..31 | `int(%d)` | +| `0D` | 01, 02, 03..31 | `%d` | +| `JJJ` | 1,2,3..366 | `int(%j)` | +| `00J` | 001, 002..366 | `%j` | +| `BUILD` | 0011, 1001, 1002, .. | build number (lexid) | +| `BLD` | 11, 1001, 1002, .. | zero truncated `BUILD` | +| `TAG` | alpha, beta, rc | `--release=` | +| `PYTAG` | a, b, rc | `--release=` | +| `NUM` | 0, 1, 2... | release tag number | +| `MAJOR` | 0..9, 10..99, 100.. | `--major` | +| `MINOR` | 0..9, 10..99, 100.. | `--minor` | +| `PATCH` | 0..9, 10..99, 100.. | `--patch` | +| `MICRO` | 0..9, 10..99, 100.. | Synonym for `PATCH` | ### Week Numbering @@ -252,17 +254,17 @@ Week numbering is a bit special, as it depends on your definition of "week": - At the beginning/end of the year, do you have partial weeks or do you have a week that span mutliple years? - If a week spans multiple years, what is the year number? -| placeholder | range / example(s) | comment | -|-------------|---------------------|------------------------------------------------------------| -| `WW` | 0, 1, 2..52 | `int(%W)` | -| `0W` | 00, 01, 02..52 | `%W` | -| `UU` | 0, 1, 2..52 | `int(%U)` us_week | -| `0U` | 00, 01, 02..52 | `%U` us_week | -| `VV` | 1, 2..53 | `int(%V)` iso week | -| `0V` | 01, 02..53 | `%U` iso_week | -| `GGGG` | 2019, 2020... | ISO 8601 week-based year (corresponds to `strftime("%G")`) | -| `GG` | 19, 20...99, 0, 1 | Short ISO 8601 week-based year | -| `0G` | 19, 20...99, 00, 01 | Zero-padded ISO 8601 week-based year | +| placeholder | range / example(s) | comment | +|-------------|---------------------|-------------------------------------------| +| `WW` | 0, 1, 2..52 | `int(%W)` | +| `0W` | 00, 01, 02..52 | `%W` | +| `UU` | 0, 1, 2..52 | `int(%U)` us_week | +| `0U` | 00, 01, 02..52 | `%U` us_week | +| `VV` | 1, 2..53 | `int(%V)` iso week | +| `0V` | 01, 02..53 | `%V` iso_week | +| `GGGG` | 2019, 2020... | `strftime("%G")` ISO 8601 week-based year | +| `GG` | 19, 20...99, 0, 1 | Short ISO 8601 week-based year | +| `0G` | 19, 20...99, 00, 01 | Zero-padded ISO 8601 week-based year | ### Normalization Caveats @@ -283,22 +285,34 @@ For example: It may be confusing to your users to see versions displayed in two different forms. It is not immediately obvious that `v20.08.02-beta` is the same `20.8.2b0` on pypi. If you wish to avoid this, you should usa a pattern which is as close as possible to the normalized form of your version. -| pattern | example | lexical | PEP440 | lexical | -|-----------------------|---------|---------|--------|---------| -| `YYYY.0M` | | yes | | no | -| `YYYY.MM` | | no | | no | -| `vYYYY.0W` | | yes | | no | -| `vYYYY.WW` | | no | | no | -| `YYYY.0M.0D` | | yes | | no | -| `YYYY.MM.DD` | | no | | no | -| `YYYY0M.BUILD[-TAG]` | | yes | | yes | -| `YY0M.BUILD[-TAG]` | | yes¹ | | yes¹ | -| `YYYY.BUILD[-TAG]` | | yes | | yes | -| `YYYY0M.MINOR[-TAG]` | | yes² | | yes | -| `YYYY.MM.MINOR[-TAG]` | | no | | no | -| `YYYY.0M.MINOR[-TAG]` | | yes² | | no | -| `YYYY.WW.MINOR[-TAG]` | | no | | no | -| `YYYY.0W.MINOR[-TAG]` | | yes² | | no | +It may also be confusing to your users if they a list of version numbers, sorted lexiographically by some tool (e.g. a list of git tags) and a newer version is listed after older versions like this: + +``` +3.9.1 +3.8.1 +3.8.0 +3.10.0 +``` + +If you wish to avoid this, you should use a pattern which maintains lexiographical ordering. + + +| pattern | example | lexio. | PEP440 | lexio. | +|-----------------------|---------|--------|--------|--------| +| `YYYY0M.BUILD[-TAG]` | | yes | | yes | +| `YYYY.BUILD[-TAG]` | | yes | | yes | +| `YYYY0M.MINOR[-TAG]` | | yes² | | yes | +| `YY0M.BUILD[-TAG]` | | yes¹ | | yes¹ | +| `YYYY.MM.MINOR[-TAG]` | | no | | no | +| `YYYY.0M.MINOR[-TAG]` | | yes² | | no | +| `YYYY.WW.MINOR[-TAG]` | | no | | no | +| `YYYY.0W.MINOR[-TAG]` | | yes² | | no | +| `YYYY.0M.0D` | | yes | | no | +| `YYYY.MM.DD` | | no | | no | +| `vYYYY.0W` | | yes | | no | +| `vYYYY.WW` | | no | | no | +| `YYYY.0M` | | yes | | no | +| `YYYY.MM` | | no | | no | - ¹ Until 2099. If your project has new releases after 2099, future maintainers can change `YY`/`0Y` -> `YYYY` so that they don't release `00.xx`. - ² As long as `MINOR <= 9` @@ -317,8 +331,6 @@ Available placeholders are: | placeholder | range / example(s) | comment | |---------------------|---------------------|-----------------| -| `{pycalver}` | v201902.0001-beta | | -| `{pep440_pycalver}` | 201902.1b0 | | | `{year}` | 2019... | `%Y` | | `{yy}` | 18, 19..99, 01, 02 | `%y` | | `{quarter}` | 1, 2, 3, 4 | | @@ -327,16 +339,22 @@ Available placeholders are: | `{us_week}` | 00..53 | `%U` | | `{dom}` | 01..31 | `%d` | | `{doy}` | 001..366 | `%j` | -| `{build}` | .0123 | lexical id | -| `{build_no}` | 0123, 12345 | ... | +| `{build}` | .1023 | lexical id | +| `{build_no}` | 1023, 20345 | ... | | `{release}` | -alpha, -beta, -rc | --release= | | `{release_tag}` | alpha, beta, rc | ... | -| `{semver}` | 1.2.3 | | -| `{MAJOR}` | 1..9, 10..99, 100.. | --major | -| `{MINOR}` | 1..9, 10..99, 100.. | --minor | -| `{PATCH}` | 1..9, 10..99, 100.. | --patch | + +| placeholder | range / example(s) | comment | +|---------------------|---------------------|-----------------| +| `{pycalver}` | v201902.1001-beta | | +| `{pep440_pycalver}` | 201902.1b0 | | +| `{semver}` | 1.2.3 | | + + +### Pattern Usage + There are some limitations to keep in mind: 1. A version string cannot span multiple lines. diff --git a/src/pycalver2/patterns.py b/src/pycalver2/patterns.py index d1a5097..c6081f8 100644 --- a/src/pycalver2/patterns.py +++ b/src/pycalver2/patterns.py @@ -86,6 +86,7 @@ PART_PATTERNS = { 'PATCH': r"[0-9]+", 'MICRO': r"[0-9]+", 'BUILD': r"[0-9]+", + 'BLD' : r"[1-9][0-9]*", 'TAG' : r"(?:alpha|beta|dev|pre|rc|post|final)", 'PYTAG': r"(?:a|b|dev|rc|post)", 'NUM' : r"[0-9]+", @@ -111,6 +112,7 @@ PATTERN_PART_FIELDS = { 'PATCH': 'patch', 'MICRO': 'patch', 'BUILD': 'bid', + 'BLD' : 'bid', 'TAG' : 'tag', 'PYTAG': 'pytag', 'NUM' : 'num', @@ -130,6 +132,10 @@ def _fmt_num(val: FieldValue) -> str: return str(val) +def _fmt_bld(val: FieldValue) -> str: + return str(int(val)) + + def _fmt_yy(year_y: FieldValue) -> str: return str(int(str(year_y)[-2:])) @@ -189,6 +195,7 @@ PART_FORMATS: typ.Dict[str, typ.Callable[[FieldValue], str]] = { 'PATCH': _fmt_num, 'MICRO': _fmt_num, 'BUILD': _fmt_num, + 'BLD' : _fmt_bld, 'TAG' : _fmt_num, 'PYTAG': _fmt_num, 'NUM' : _fmt_num, diff --git a/test/test_version.py b/test/test_version.py index 09fb0af..696fec7 100644 --- a/test/test_version.py +++ b/test/test_version.py @@ -10,9 +10,9 @@ import datetime as dt import pytest import pycalver.version as v1version +import pycalver2.version as v2version import pycalver.patterns as v1patterns -# import pycalver2.version as v2version # import pycalver2.patterns as v2patterns # pylint:disable=protected-access ; allowed for test code @@ -182,7 +182,7 @@ PARSE_VERSION_TEST_CASES = [ @pytest.mark.parametrize("pattern_str, line, expected_vinfo", PARSE_VERSION_TEST_CASES) -def test_parse_versions(pattern_str, line, expected_vinfo): +def test_v1_parse_versions(pattern_str, line, expected_vinfo): pattern = v1patterns.compile_pattern(pattern_str) version_match = pattern.regexp.search(line) @@ -196,3 +196,25 @@ def test_parse_versions(pattern_str, line, expected_vinfo): version_info = v1version.parse_version_info(version_str, pattern_str) assert version_info == expected_vinfo + + +# def test_v2_parse_versions(pattern_str, line, expected_vinfo): +def test_v2_parse_versions(): + vnfo = v2version.parse_version_info("v201712.0033", pattern="vYYYY0M.BUILD[-TAG[NUM]]") + fvals = {'year_y': 2017, 'month': 12, 'bid': "0033"} + assert vnfo == v2version._parse_version_info(fvals) + + +def test_v2_format_version(): + pattern = "vYYYY0M.BUILD[-TAG[NUM]]" + in_version = "v200701.0033-beta" + + vinfo = v2version.parse_version_info(in_version, pattern=pattern) + out_version = v2version.format_version(vinfo, pattern=pattern) + assert in_version == out_version + + result = v2version.format_version(vinfo, pattern="v0Y.BUILD[-TAG]") + assert result == "v07.0033-beta" + + result = v2version.format_version(vinfo, pattern="vYY.BLD[-TAG]") + assert result == "v7.33-beta" From 8edb001782d1989a6fc28e4ff40aacd22a887dee Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Fri, 18 Sep 2020 13:20:24 +0000 Subject: [PATCH 17/98] implement formatting for new style patterns --- src/pycalver2/version.py | 210 +++++++++++++++++++++++++++++---------- 1 file changed, 155 insertions(+), 55 deletions(-) diff --git a/src/pycalver2/version.py b/src/pycalver2/version.py index 5a6bc5a..95f5155 100644 --- a/src/pycalver2/version.py +++ b/src/pycalver2/version.py @@ -24,21 +24,14 @@ logger = logging.getLogger("pycalver.version") TODAY = dt.datetime.utcnow().date() -ID_FIELDS_BY_PART = { - 'MAJOR': 'major', - 'MINOR': 'minor', - 'PATCH': 'patch', - 'MICRO': 'patch', -} - - ZERO_VALUES = { - 'major': "0", - 'minor': "0", - 'patch': "0", - 'tag' : "final", - 'pytag': "", - 'num' : "0", + 'MAJOR': "0", + 'MINOR': "0", + 'PATCH': "0", + 'MICRO': "0", + 'TAG' : "final", + 'PYTAG': "", + 'NUM' : "0", } @@ -170,10 +163,10 @@ class VersionInfo(typ.NamedTuple): major : int minor : int patch : int + num : int bid : str tag : str pytag : str - num : MaybeInt VALID_FIELD_KEYS = set(VersionInfo._fields) | {'version'} @@ -229,16 +222,14 @@ def _parse_version_info(field_values: FieldValues) -> VersionInfo: assert key in VALID_FIELD_KEYS, key fvals = field_values - tag = fvals.get('tag' , "final") - pytag = fvals.get('pytag', "") + tag = fvals.get('tag' ) or "final" + pytag = fvals.get('pytag') or "" if tag and not pytag: pytag = PEP440_TAG_BY_TAG[tag] elif pytag and not tag: tag = TAG_BY_PEP440_TAG[pytag] - num: MaybeInt = int(fvals['num']) if 'num' in fvals else None - date: typ.Optional[dt.date] = None year_y: MaybeInt = int(fvals['year_y']) if 'year_y' in fvals else None @@ -278,11 +269,12 @@ def _parse_version_info(field_values: FieldValues) -> VersionInfo: if quarter is None and month: quarter = _quarter_from_month(month) - major = int(fvals['major']) if 'major' in fvals else 0 - minor = int(fvals['minor']) if 'minor' in fvals else 0 - patch = int(fvals['patch']) if 'patch' in fvals else 0 - - bid = fvals['bid'] if 'bid' in fvals else "1000" + # NOTE (mb 2020-09-18): If a part is optional, fvals[] may be None + major = int(fvals.get('major') or 0) + minor = int(fvals.get('minor') or 0) + patch = int(fvals.get('patch') or 0) + num = int(fvals.get('num' ) or 0) + bid = fvals['bid'] if 'bid' in fvals else "1000" vnfo = VersionInfo( year_y=year_y, @@ -297,10 +289,10 @@ def _parse_version_info(field_values: FieldValues) -> VersionInfo: major=major, minor=minor, patch=patch, + num=num, bid=bid, tag=tag, pytag=pytag, - num=num, ) return vnfo @@ -312,13 +304,21 @@ class PatternError(Exception): pass -def parse_version_info(version_str: str, pattern: str = "vYYYY0M.BUILD[-TAG]") -> VersionInfo: +def parse_version_info(version_str: str, pattern: str = "vYYYY0M.BUILD[-TAG[NUM]]") -> VersionInfo: """Parse normalized VersionInfo. - >>> vnfo = parse_version_info("v201712.0033-beta", pattern="vYYYY0M.BUILD[-TAG]") + >>> vnfo = parse_version_info("v201712.0033-beta0", pattern="vYYYY0M.BUILD[-TAG[NUM]]") + >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033", 'tag': "beta", 'num': 0} + >>> assert vnfo == _parse_version_info(fvals) + + >>> vnfo = parse_version_info("v201712.0033-beta", pattern="vYYYY0M.BUILD[-TAG[NUM]]") >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"} >>> assert vnfo == _parse_version_info(fvals) + >>> vnfo = parse_version_info("v201712.0033", pattern="vYYYY0M.BUILD[-TAG[NUM]]") + >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033"} + >>> assert vnfo == _parse_version_info(fvals) + >>> vnfo = parse_version_info("1.23.456", pattern="MAJOR.MINOR.PATCH") >>> fvals = {'major': "1", 'minor': "23", 'patch': "456"} >>> assert vnfo == _parse_version_info(fvals) @@ -372,60 +372,101 @@ def _format_part_values(vinfo: VersionInfo) -> typ.Dict[str, str]: ('2007', '09', '1033', 'beta') >>> (kwargs['YY'], kwargs['0Y'], kwargs['MM'], kwargs['PYTAG']) ('7', '07', '9', 'b') + + >>> vinfo = parse_version_info("200709.1033b1", pattern="YYYY0M.BLD[PYTAGNUM]") + >>> kwargs = _format_part_values(vinfo) + >>> (kwargs['YYYY'], kwargs['0M'], kwargs['BUILD'], kwargs['PYTAG'], kwargs['NUM']) + ('2007', '09', '1033', 'b', '1') """ vnfo_kwargs: TemplateKwargs = vinfo._asdict() kwargs : typ.Dict[str, str] = {} for part, field in v2patterns.PATTERN_PART_FIELDS.items(): field_val = vnfo_kwargs[field] - if field_val is None: - continue - - format_fn = v2patterns.PART_FORMATS[part] - kwargs[part] = format_fn(field_val) + if field_val is not None: + format_fn = v2patterns.PART_FORMATS[part] + kwargs[part] = format_fn(field_val) return kwargs +def _make_segments(pattern: str) -> typ.List[str]: + pattern_segs_l: typ.List[str] = [] + pattern_segs_r: typ.List[str] = [] + + pattern_rest = pattern + while "[" in pattern_rest and "]" in pattern_rest: + try: + seg_l , pattern_rest = pattern_rest.split("[", 1) + pattern_rest, seg_r = pattern_rest.rsplit("]", 1) + except ValueError as val_err: + if "values to unpack" in str(val_err): + pat_err = PatternError(f"Unbalanced braces [] in '{pattern}'") + pat_err.__cause__ = val_err + raise pat_err + else: + raise + + pattern_segs_l.append(seg_l) + pattern_segs_r.append(seg_r) + + pattern_segs_l.append(pattern_rest) + + # NOTE (mb 2020-09-18): The pivot makes subsequent code a bit more simple + pivot = [""] + return pattern_segs_l + pivot + list(reversed(pattern_segs_r)) + + def format_version(vinfo: VersionInfo, pattern: str) -> str: """Generate version string. >>> import datetime as dt - >>> vinfo = parse_version_info("v200712.0033-beta", pattern="vYYYY0M.BUILD[-TAG]") + >>> vinfo = parse_version_info("v200712.0033-beta", pattern="vYYYY0M.BUILD[-TAG[NUM]]") >>> vinfo_a = vinfo._replace(**cal_info(date=dt.date(2007, 1, 1))._asdict()) >>> vinfo_b = vinfo._replace(**cal_info(date=dt.date(2007, 12, 31))._asdict()) - >>> format_version(vinfo_a, pattern="vYY.BUILD[-TAG]") - 'v7.33-beta' - >>> format_version(vinfo_a, pattern="v0Y.BUILD[-TAG]") + >>> format_version(vinfo_a, pattern="vYY.BLD[-PYTAGNUM]") + 'v7.33-b0' + + >>> format_version(vinfo_a, pattern="YYYY0M.BUILD[PYTAG[NUM]]") + '200701.0033b' + >>> format_version(vinfo_a, pattern="vYY.BLD[-PYTAGNUM]") + 'v7.33-b0' + >>> format_version(vinfo_a, pattern="v0Y.BLD[-TAG]") 'v07.33-beta' - >>> format_version(vinfo_a, pattern="YYYY0M.BUILD[PYTAG][NUM]") - '201701.33b0' >>> format_version(vinfo_a, pattern="vYYYY0M.BUILD[-TAG]") - 'v201701.0033-beta' + 'v200701.0033-beta' >>> format_version(vinfo_b, pattern="vYYYY0M.BUILD[-TAG]") - 'v201712.0033-beta' + 'v200712.0033-beta' - >>> format_version(vinfo_a, pattern="vYYYYwWW.BUILD[-TAG]") - 'v2017w00.33-beta' - >>> format_version(vinfo_b, pattern="vYYYYwWW.BUILD[-TAG]") - 'v2017w52.33-beta' + >>> format_version(vinfo_a, pattern="vYYYYw0W.BUILD[-TAG]") + 'v2007w01.0033-beta' + >>> format_version(vinfo_a, pattern="vYYYYwWW.BLD[-TAG]") + 'v2007w1.33-beta' + >>> format_version(vinfo_b, pattern="vYYYYw0W.BUILD[-TAG]") + 'v2007w53.0033-beta' + >>> format_version(vinfo_a, pattern="vYYYYd00J.BUILD[-TAG]") + 'v2007d001.0033-beta' >>> format_version(vinfo_a, pattern="vYYYYdJJJ.BUILD[-TAG]") - 'v2017d001.0033-beta' - >>> format_version(vinfo_b, pattern="vYYYYdJJJ.BUILD[-TAG]") - 'v2017d365.0033-beta' + 'v2007d1.0033-beta' + >>> format_version(vinfo_b, pattern="vYYYYd00J.BUILD[-TAG]") + 'v2007d365.0033-beta' - >>> format_version(vinfo_a, pattern="vGGGGwVV.BUILD[-TAG]") - 'v2016w52.0033-beta' + >>> format_version(vinfo_a, pattern="vGGGGwVV.BLD[PYTAGNUM]") + 'v2007w1.33b0' + >>> format_version(vinfo_a, pattern="vGGGGw0V.BUILD[-TAG]") + 'v2007w01.0033-beta' + >>> format_version(vinfo_b, pattern="vGGGGw0V.BUILD[-TAG]") + 'v2008w01.0033-beta' >>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final') >>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD-TAG") - 'v2017w52.33-final' + 'v2007w53.0033-final' >>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD[-TAG]") - 'v2017w52.33' + 'v2007w53.0033' >>> format_version(vinfo_c, pattern="vMAJOR.MINOR.PATCH") 'v1.2.34' @@ -445,14 +486,73 @@ def format_version(vinfo: VersionInfo, pattern: str) -> str: 'v1.0' >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-TAG]]]") 'v1' + + >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=1, tag='rc', num=0) + >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH]]") + 'v1.0.1' + >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-TAG[NUM]]]]") + 'v1.0.1-rc' + >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-TAGNUM]]]") + 'v1.0.1-rc0' + >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH]]") + 'v1.0.1' + + >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='rc', num=2) + >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-TAG[NUM]]]]") + 'v1.0.0-rc2' """ kwargs = _format_part_values(vinfo) part_values = sorted(kwargs.items(), key=lambda item: -len(item[0])) - version = pattern - for part, value in part_values: - version = version.replace(part, value) - return version + pattern_segs = _make_segments(pattern) + + iszero_segment = [True] * len(pattern_segs) + formatted_segs_l: typ.List[str] = [] + formatted_segs_r: typ.List[str] = [] + + used_part_values = [] + + idx_l = 0 + idx_r = len(pattern_segs) - 1 + while idx_l < idx_r: + # NOTE (mb 2020-09-18): All segments are optional, + # except the most left and the most right, + # i.e the ones NOT surrounded by braces. + # Empty string is a valid segment. + is_optional = idx_l > 0 + + seg_l = pattern_segs[idx_l] + seg_r = pattern_segs[idx_r] + + for part, part_value in part_values: + if part in seg_l: + used_part_values.append(part + "=" + part_value) + seg_l = seg_l.replace(part, part_value) + if not (is_optional and str(part_value) == ZERO_VALUES.get(part)): + iszero_segment[idx_l] = False + + if part in seg_r: + used_part_values.append(part + "=" + part_value) + seg_r = seg_r.replace(part, part_value) + if not (is_optional and str(part_value) == ZERO_VALUES[part]): + iszero_segment[idx_r] = False + + formatted_segs_l.append(seg_l) + formatted_segs_r.append(seg_r) + + idx_l += 1 + idx_r -= 1 + + formatted_segs = formatted_segs_l + list(reversed(formatted_segs_r)) + + has_val_to_right = False + for idx, is_zero in reversed(list(enumerate(iszero_segment))): + if is_zero and not has_val_to_right: + formatted_segs[idx] = "" + else: + has_val_to_right = True + + return "".join(formatted_segs) def incr( From 09299f30f1f8921bc8a4dbe275b51ced5098834b Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Fri, 18 Sep 2020 16:52:28 +0000 Subject: [PATCH 18/98] fixes for new style patterns --- src/pycalver2/rewrite.py | 25 ++++---- src/pycalver2/version.py | 122 ++++++++++++++++++++++----------------- test/test_version.py | 20 +++++++ 3 files changed, 104 insertions(+), 63 deletions(-) diff --git a/src/pycalver2/rewrite.py b/src/pycalver2/rewrite.py index 49cd6bb..5816485 100644 --- a/src/pycalver2/rewrite.py +++ b/src/pycalver2/rewrite.py @@ -23,18 +23,23 @@ def rewrite_lines( new_vinfo : v2version.VersionInfo, old_lines : typ.List[str], ) -> typ.List[str]: - # TODO reenable doctest - # """Replace occurances of pattern_strs in old_lines with new_vinfo. + """Replace occurances of pattern_strs in old_lines with new_vinfo. - # >>> new_vinfo = version.parse_version_info("v201811.0123-beta") - # >>> pattern_strs = ['__version__ = "{pycalver}"'] - # >>> rewrite_lines(pattern_strs, new_vinfo, ['__version__ = "v201809.0002-beta"']) - # ['__version__ = "v201811.0123-beta"'] + >>> new_vinfo = v2version.parse_version_info("v201811.0123-beta") + >>> pattern_strs = ['__version__ = "vYYYY0M.BUILD[-TAG]"'] + >>> old_lines = ['__version__ = "v201809.0002-alpha" '] + >>> rewrite_lines(pattern_strs, new_vinfo, old_lines) + ['__version__ = "v201811.0123-beta" '] - # >>> pattern_strs = ['__version__ = "{pep440_version}"'] - # >>> rewrite_lines(pattern_strs, new_vinfo, ['__version__ = "201809.2b0"']) - # ['__version__ = "201811.123b0"'] - # """ + >>> old_lines = ['__version__ = "v201809.0002-alpha" # comment'] + >>> rewrite_lines(pattern_strs, new_vinfo, old_lines) + ['__version__ = "v201811.0123-beta" # comment'] + + >>> pattern_strs = ['__version__ = "YYYY0M.BLD[PYTAGNUM]"'] + >>> old_lines = ['__version__ = "201809.2a0"'] + >>> rewrite_lines(pattern_strs, new_vinfo, old_lines) + ['__version__ = "201811.123b0"'] + """ new_lines = old_lines[:] found_patterns = set() diff --git a/src/pycalver2/version.py b/src/pycalver2/version.py index 95f5155..6d9fe71 100644 --- a/src/pycalver2/version.py +++ b/src/pycalver2/version.py @@ -411,10 +411,70 @@ def _make_segments(pattern: str) -> typ.List[str]: pattern_segs_r.append(seg_r) pattern_segs_l.append(pattern_rest) + return pattern_segs_l + list(reversed(pattern_segs_r)) - # NOTE (mb 2020-09-18): The pivot makes subsequent code a bit more simple - pivot = [""] - return pattern_segs_l + pivot + list(reversed(pattern_segs_r)) + +def _clear_zero_segments( + formatted_segs: typ.List[str], is_zero_segment: typ.List[bool] +) -> typ.List[str]: + non_zero_segs = list(formatted_segs) + + has_val_to_right = False + for idx, is_zero in reversed(list(enumerate(is_zero_segment))): + is_optional = 0 < idx < len(formatted_segs) - 1 + if is_optional: + if is_zero and not has_val_to_right: + non_zero_segs[idx] = "" + else: + has_val_to_right = True + + return non_zero_segs + + +def _format_segments( + vinfo : VersionInfo, + pattern_segs: typ.List[str], +) -> typ.List[str]: + kwargs = _format_part_values(vinfo) + part_values = sorted(kwargs.items(), key=lambda item: -len(item[0])) + + is_zero_segment = [True] * len(pattern_segs) + + formatted_segs_l: typ.List[str] = [] + formatted_segs_r: typ.List[str] = [] + + idx_l = 0 + idx_r = len(pattern_segs) - 1 + while idx_l <= idx_r: + # NOTE (mb 2020-09-18): All segments are optional, + # except the most left and the most right, + # i.e the ones NOT surrounded by braces. + # Empty string is a valid segment. + is_optional = idx_l > 0 + + seg_l = pattern_segs[idx_l] + seg_r = pattern_segs[idx_r] + + for part, part_value in part_values: + if part in seg_l: + seg_l = seg_l.replace(part, part_value) + if not (is_optional and str(part_value) == ZERO_VALUES.get(part)): + is_zero_segment[idx_l] = False + + if part in seg_r: + seg_r = seg_r.replace(part, part_value) + if not (is_optional and str(part_value) == ZERO_VALUES[part]): + is_zero_segment[idx_r] = False + + formatted_segs_l.append(seg_l) + if idx_l < idx_r: + formatted_segs_r.append(seg_r) + + idx_l += 1 + idx_r -= 1 + + formatted_segs = formatted_segs_l + list(reversed(formatted_segs_r)) + return _clear_zero_segments(formatted_segs, is_zero_segment) def format_version(vinfo: VersionInfo, pattern: str) -> str: @@ -500,57 +560,13 @@ def format_version(vinfo: VersionInfo, pattern: str) -> str: >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='rc', num=2) >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-TAG[NUM]]]]") 'v1.0.0-rc2' + + >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='rc', num=2) + >>> format_version(vinfo_d, pattern='__version__ = "vMAJOR[.MINOR[.PATCH[-TAG[NUM]]]]"') + '__version__ = "v1.0.0-rc2"' """ - kwargs = _format_part_values(vinfo) - part_values = sorted(kwargs.items(), key=lambda item: -len(item[0])) - - pattern_segs = _make_segments(pattern) - - iszero_segment = [True] * len(pattern_segs) - formatted_segs_l: typ.List[str] = [] - formatted_segs_r: typ.List[str] = [] - - used_part_values = [] - - idx_l = 0 - idx_r = len(pattern_segs) - 1 - while idx_l < idx_r: - # NOTE (mb 2020-09-18): All segments are optional, - # except the most left and the most right, - # i.e the ones NOT surrounded by braces. - # Empty string is a valid segment. - is_optional = idx_l > 0 - - seg_l = pattern_segs[idx_l] - seg_r = pattern_segs[idx_r] - - for part, part_value in part_values: - if part in seg_l: - used_part_values.append(part + "=" + part_value) - seg_l = seg_l.replace(part, part_value) - if not (is_optional and str(part_value) == ZERO_VALUES.get(part)): - iszero_segment[idx_l] = False - - if part in seg_r: - used_part_values.append(part + "=" + part_value) - seg_r = seg_r.replace(part, part_value) - if not (is_optional and str(part_value) == ZERO_VALUES[part]): - iszero_segment[idx_r] = False - - formatted_segs_l.append(seg_l) - formatted_segs_r.append(seg_r) - - idx_l += 1 - idx_r -= 1 - - formatted_segs = formatted_segs_l + list(reversed(formatted_segs_r)) - - has_val_to_right = False - for idx, is_zero in reversed(list(enumerate(iszero_segment))): - if is_zero and not has_val_to_right: - formatted_segs[idx] = "" - else: - has_val_to_right = True + pattern_segs = _make_segments(pattern) + formatted_segs = _format_segments(vinfo, pattern_segs) return "".join(formatted_segs) diff --git a/test/test_version.py b/test/test_version.py index 696fec7..43f3720 100644 --- a/test/test_version.py +++ b/test/test_version.py @@ -205,6 +205,17 @@ def test_v2_parse_versions(): assert vnfo == v2version._parse_version_info(fvals) +def test_make_segments(): + segs = v2version._make_segments("vYYYY0M.BUILD[-TAG[NUM]]") + assert segs == ["vYYYY0M.BUILD", "-TAG", "NUM", "", ""] + + segs = v2version._make_segments('__version__ = "YYYY0M.BLD[PYTAGNUM]"') + assert segs == ['__version__ = "YYYY0M.BLD', 'PYTAGNUM', '"'] + + segs = v2version._make_segments('__version__ = "YYYY.BUILD[-TAG]"') + assert segs == ['__version__ = "YYYY.BUILD', '-TAG', '"'] + + def test_v2_format_version(): pattern = "vYYYY0M.BUILD[-TAG[NUM]]" in_version = "v200701.0033-beta" @@ -218,3 +229,12 @@ def test_v2_format_version(): result = v2version.format_version(vinfo, pattern="vYY.BLD[-TAG]") assert result == "v7.33-beta" + + result = v2version.format_version(vinfo, pattern="vYY.BLD-TAG") + assert result == "v7.33-beta" + + result = v2version.format_version(vinfo, pattern='__version__ = "YYYY.BUILD[-TAG]"') + assert result == '__version__ = "2007.0033-beta"' + + result = v2version.format_version(vinfo, pattern='__version__ = "YYYY.BLD"') + assert result == '__version__ = "2007.33"' From 76fe72da43a0f0c4d498d30740324b4c041574bd Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Fri, 18 Sep 2020 17:02:03 +0000 Subject: [PATCH 19/98] reenable tests --- src/pycalver2/rewrite.py | 95 +++++++++++++++++++--------------------- test/test_rewrite.py | 24 ++++++++++ 2 files changed, 70 insertions(+), 49 deletions(-) diff --git a/src/pycalver2/rewrite.py b/src/pycalver2/rewrite.py index 5816485..03c1f86 100644 --- a/src/pycalver2/rewrite.py +++ b/src/pycalver2/rewrite.py @@ -9,11 +9,11 @@ import io import typing as typ import logging +import pycalver.rewrite as v1rewrite import pycalver2.version as v2version import pycalver2.patterns as v2patterns from pycalver import parse from pycalver import config -from pycalver import rewrite as v1rewrite logger = logging.getLogger("pycalver2.rewrite") @@ -68,23 +68,22 @@ def rfd_from_content( new_vinfo : v2version.VersionInfo, content : str, ) -> v1rewrite.RewrittenFileData: - # TODO reenable doctest - # r"""Rewrite pattern occurrences with version string. + r"""Rewrite pattern occurrences with version string. - # >>> new_vinfo = version.parse_version_info("v201809.0123") - # >>> pattern_strs = ['__version__ = "{pycalver}"'] - # >>> content = '__version__ = "v201809.0001-alpha"' - # >>> rfd = rfd_from_content(pattern_strs, new_vinfo, content) - # >>> rfd.new_lines - # ['__version__ = "v201809.0123"'] - # >>> - # >>> new_vinfo = version.parse_version_info("v1.2.3", "v{semver}") - # >>> pattern_strs = ['__version__ = "v{semver}"'] - # >>> content = '__version__ = "v1.2.2"' - # >>> rfd = rfd_from_content(pattern_strs, new_vinfo, content) - # >>> rfd.new_lines - # ['__version__ = "v1.2.3"'] - # """ + >>> new_vinfo = v2version.parse_version_info("v201809.0123") + >>> pattern_strs = ['__version__ = "vYYYY0M.BUILD[-TAG]"'] + >>> content = '__version__ = "v201809.0001-alpha"' + >>> rfd = rfd_from_content(pattern_strs, new_vinfo, content) + >>> rfd.new_lines + ['__version__ = "v201809.0123"'] + >>> + >>> new_vinfo = v2version.parse_version_info("v1.2.3", "vMAJOR.MINOR.PATCH") + >>> pattern_strs = ['__version__ = "vMAJOR.MINOR.PATCH"'] + >>> content = '__version__ = "v1.2.2"' + >>> rfd = rfd_from_content(pattern_strs, new_vinfo, content) + >>> rfd.new_lines + ['__version__ = "v1.2.3"'] + """ line_sep = v1rewrite.detect_line_sep(content) old_lines = content.split(line_sep) new_lines = rewrite_lines(pattern_strs, new_vinfo, old_lines) @@ -95,26 +94,25 @@ def iter_rewritten( file_patterns: config.PatternsByGlob, new_vinfo : v2version.VersionInfo, ) -> typ.Iterable[v1rewrite.RewrittenFileData]: - # TODO reenable doctest - # r'''Iterate over files with version string replaced. + r'''Iterate over files with version string replaced. - # >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']} - # >>> new_vinfo = version.parse_version_info("v201809.0123") - # >>> rewritten_datas = iter_rewritten(file_patterns, new_vinfo) - # >>> rfd = list(rewritten_datas)[0] - # >>> assert rfd.new_lines == [ - # ... '# This file is part of the pycalver project', - # ... '# https://gitlab.com/mbarkhau/pycalver', - # ... '#', - # ... '# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License', - # ... '# SPDX-License-Identifier: MIT', - # ... '"""PyCalVer: CalVer for Python Packages."""', - # ... '', - # ... '__version__ = "v201809.0123"', - # ... '', - # ... ] - # >>> - # ''' + >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "vYYYY0M.BUILD[-TAG]"']} + >>> new_vinfo = v2version.parse_version_info("v201809.0123") + >>> rewritten_datas = iter_rewritten(file_patterns, new_vinfo) + >>> rfd = list(rewritten_datas)[0] + >>> assert rfd.new_lines == [ + ... '# This file is part of the pycalver project', + ... '# https://github.com/mbarkhau/pycalver', + ... '#', + ... '# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License', + ... '# SPDX-License-Identifier: MIT', + ... '"""PyCalVer: CalVer for Python Packages."""', + ... '', + ... '__version__ = "v201809.0123"', + ... '', + ... ] + >>> + ''' fobj: typ.IO[str] @@ -130,20 +128,19 @@ def diff( new_vinfo : v2version.VersionInfo, file_patterns: config.PatternsByGlob, ) -> str: - # TODO reenable doctest - # r"""Generate diffs of rewritten files. + r"""Generate diffs of rewritten files. - # >>> new_vinfo = version.parse_version_info("v201809.0123") - # >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']} - # >>> diff_str = diff(new_vinfo, file_patterns) - # >>> lines = diff_str.split("\n") - # >>> lines[:2] - # ['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py'] - # >>> assert lines[6].startswith('-__version__ = "v2') - # >>> assert not lines[6].startswith('-__version__ = "v201809.0123"') - # >>> lines[7] - # '+__version__ = "v201809.0123"' - # """ + >>> new_vinfo = v2version.parse_version_info("v201809.0123") + >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "vYYYY0M.BUILD[-TAG]"']} + >>> diff_str = diff(new_vinfo, file_patterns) + >>> lines = diff_str.split("\n") + >>> lines[:2] + ['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py'] + >>> assert lines[6].startswith('-__version__ = "v2') + >>> assert not lines[6].startswith('-__version__ = "v201809.0123"') + >>> lines[7] + '+__version__ = "v201809.0123"' + """ full_diff = "" fobj: typ.IO[str] diff --git a/test/test_rewrite.py b/test/test_rewrite.py index 78b6432..7a90a3d 100644 --- a/test/test_rewrite.py +++ b/test/test_rewrite.py @@ -5,6 +5,7 @@ from __future__ import absolute_import from __future__ import unicode_literals import copy +import datetime as dt from test import util from pycalver import config @@ -135,3 +136,26 @@ def test_v1_optional_release(): assert len(new_lines) == len(old_lines) assert "2019.0004-beta" not in "\n".join(old_lines) assert "2019.0004-beta" in "\n".join(new_lines) + + +def test_v2_optional_release(): + old_lines = OPTIONAL_RELEASE_FIXTURE.splitlines() + pattern = "YYYY.BUILD[-TAG]" + patterns = ['__version__ = "YYYY.BUILD[-TAG]"'] + + new_vinfo = v2version.parse_version_info("2019.0003", pattern) + new_lines = v2rewrite.rewrite_lines(patterns, new_vinfo, old_lines) + + assert len(new_lines) == len(old_lines) + assert "2019.0003" not in "\n".join(old_lines) + new_text = "\n".join(new_lines) + assert "2019.0003" in new_text + assert '__version__ = "2019.0003"' in new_text + + new_vinfo = v2version.parse_version_info("2019.0004-beta", pattern) + new_lines = v2rewrite.rewrite_lines(patterns, new_vinfo, old_lines) + + # make sure optional release tag is added back on + assert len(new_lines) == len(old_lines) + assert "2019.0004-beta" not in "\n".join(old_lines) + assert "2019.0004-beta" in "\n".join(new_lines) From 033a324488356174f46aaf29bbf046b27a605a3f Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Fri, 18 Sep 2020 17:50:13 +0000 Subject: [PATCH 20/98] misc cleanup and linting --- README.md | 1 - pylint-ignore.md | 202 ++++++++++++++------------------------ setup.cfg | 5 +- src/pycalver/__main__.py | 3 + src/pycalver/patterns.py | 31 ------ src/pycalver/vcs.py | 6 +- src/pycalver/version.py | 67 ++++++++----- src/pycalver2/patterns.py | 3 - src/pycalver2/version.py | 89 +++++++++-------- test/test_patterns.py | 2 +- test/test_rewrite.py | 1 - test/test_version.py | 4 +- 12 files changed, 179 insertions(+), 235 deletions(-) diff --git a/README.md b/README.md index 9774893..4294453 100644 --- a/README.md +++ b/README.md @@ -242,7 +242,6 @@ These patterns are closely based on https://calver.org/ | `MAJOR` | 0..9, 10..99, 100.. | `--major` | | `MINOR` | 0..9, 10..99, 100.. | `--minor` | | `PATCH` | 0..9, 10..99, 100.. | `--patch` | -| `MICRO` | 0..9, 10..99, 100.. | Synonym for `PATCH` | ### Week Numbering diff --git a/pylint-ignore.md b/pylint-ignore.md index 8cc8b32..789183b 100644 --- a/pylint-ignore.md +++ b/pylint-ignore.md @@ -23,197 +23,145 @@ The recommended approach to using `pylint-ignore` is: # Overview - - [W0511: fixme (5x)](#w0511-fixme) - - [W0603: global-statement (1x)](#w0603-global-statement) - - [W0613: unused-argument (1x)](#w0613-unused-argument) - - [W0703: broad-except (2x)](#w0703-broad-except) - - [C0412: ungrouped-imports (2x)](#c0412-ungrouped-imports) + - [W0511: fixme (7x)](#w0511-fixme) + - [W0703: broad-except (1x)](#w0703-broad-except) # W0511: fixme +## File test/test_patterns.py - Line 14 - W0511 (fixme) + +- `message: TODO (mb 2020-09-06): test for v2patterns` +- `author : Manuel Barkhau ` +- `date : 2020-09-18T17:01:05` + +``` + 12: import pycalver2.patterns as v2patterns + 13: +> 14: # TODO (mb 2020-09-06): test for v2patterns + 15: + 16: V2_PART_PATTERN_CASES = [ +``` + + ## File src/pycalver/vcs.py - Line 78 - W0511 (fixme) -- `message: TODO (mb 2018-11-15): Detect encoding of output?` +- `message: TODO (mb 2018-11-15): Detect encoding of output? Use chardet?` - `author : Manuel Barkhau ` -- `date : 2020-07-19T18:50:33` +- `date : 2020-09-18T17:24:49` ``` 68: def __call__(self, cmd_name: str, env: Env = None, **kwargs: str) -> str: ... 76: output_data: bytes = sp.check_output(cmd_str.split(), env=env, stderr=sp.STDOUT) 77: -> 78: # TODO (mb 2018-11-15): Detect encoding of output? +> 78: # TODO (mb 2018-11-15): Detect encoding of output? Use chardet? 79: _encoding = "utf-8" 80: return output_data.decode(_encoding) ``` -## File src/pycalver2/version.py - Line 184 - W0511 (fixme) +## File test/test_version.py - Line 167 - W0511 (fixme) -- `message: TODO (mb 2020-09-05): pytag` +- `message: TODO (mb 2020-09-06): add tests for new style patterns` - `author : Manuel Barkhau ` -- `date : 2020-09-05T14:30:17` +- `date : 2020-09-18T17:01:05` ``` - 176: def _parse_field_values(field_values: FieldValues) -> VersionInfo: + 162: def vnfo(**field_values): ... - 182: tag = TAG_ALIASES.get(tag, tag) - 183: assert tag is not None -> 184: # TODO (mb 2020-09-05): pytag - 185: pytag = "" - 186: + 165: + 166: PARSE_VERSION_TEST_CASES = [ +> 167: # TODO (mb 2020-09-06): add tests for new style patterns + 168: # ["YYYY.MM.DD" , "2017.06.07", vnfo(year="2017", month="06", dom="07")], + 169: ["{year}.{month}.{dom}" , "2017.06.07", vnfo(year="2017", month="06", dom="07")], ``` -## File src/pycalver/__main__.py - Line 220 - W0511 (fixme) +## File src/pycalver/__main__.py - Line 229 - W0511 (fixme) - `message: TODO (mb 2020-09-05): version switch` - `author : Manuel Barkhau ` - `date : 2020-09-05T14:30:17` ``` - 200: def _bump( + 209: def _bump( ... - 218: - 219: try: -> 220: # TODO (mb 2020-09-05): version switch - 221: v1cli.rewrite(cfg, new_version) - 222: v2cli.rewrite(cfg, new_version) + 227: + 228: try: +> 229: # TODO (mb 2020-09-05): version switch + 230: v1cli.rewrite(cfg, new_version) + 231: # v2cli.rewrite(cfg, new_version) ``` -## File src/pycalver/__main__.py - Line 274 - W0511 (fixme) +## File src/pycalver/config.py - Line 236 - W0511 (fixme) + +- `message: TODO (mb 2020-09-06): new style pattern by default` +- `author : Manuel Barkhau ` +- `date : 2020-09-18T17:01:05` + +``` + 227: def _parse_config(raw_cfg: RawConfig) -> Config: + ... + 234: version_str = raw_cfg['current_version'] = version_str.strip("'\" ") + 235: +> 236: # TODO (mb 2020-09-06): new style pattern by default + 237: # version_pattern: str = raw_cfg.get('version_pattern', "vYYYY0M.BUILD[-TAG]") + 238: version_pattern: str = raw_cfg.get('version_pattern', "{pycalver}") +``` + + +## File src/pycalver/__main__.py - Line 285 - W0511 (fixme) - `message: TODO (mb 2020-09-05): version switch` - `author : Manuel Barkhau ` - `date : 2020-09-05T14:30:17` ``` - 272: - 273: def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: -> 274: # TODO (mb 2020-09-05): version switch - 275: all_tags = vcs.get_tags(fetch=fetch) - 276: + 282: def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: + ... + 283: all_tags = vcs.get_tags(fetch=fetch) + 284: +> 285: # TODO (mb 2020-09-05): version switch + 286: cfg = v1cli.update_cfg_from_vcs(cfg, all_tags) + 287: # cfg = v2cli.update_cfg_from_vcs(cfg, all_tags) ``` -## File src/pycalver/__main__.py - Line 378 - W0511 (fixme) +## File src/pycalver/__main__.py - Line 392 - W0511 (fixme) - `message: # TODO (mb 2020-09-05): format from config` - `author : Manuel Barkhau ` - `date : 2020-09-05T14:30:17` ``` - 324: def bump( + 336: def bump( ... - 376: return - 377: -> 378: # # TODO (mb 2020-09-05): format from config - 379: # commit_message_kwargs = { - 380: # new_version -``` - - -# W0603: global-statement - -## File src/pycalver/__main__.py - Line 75 - W0603 (global-statement) - -- `message: Using the global statement` -- `author : Manuel Barkhau ` -- `date : 2020-09-05T14:30:17` - -``` - 73: def cli(verbose: int = 0) -> None: - 74: """Automatically update PyCalVer version strings on python projects.""" -> 75: global _VERBOSE - 76: _VERBOSE = verbose - 77: -``` - - -# W0613: unused-argument - -## File src/pycalver2/version.py - Line 431 - W0613 (unused-argument) - -- `message: Unused argument 'kwargs'` -- `author : Manuel Barkhau ` -- `date : 2020-09-05T14:30:17` - -``` - 429: - 430: -> 431: def _compile_format_template(pattern: str, kwargs: TemplateKwargs) -> str: - 432: # NOTE (mb 2020-09-04): Some parts are optional, we need the kwargs to - 433: # determine if part is set to its zero value + 390: return + 391: +> 392: # # TODO (mb 2020-09-05): format from config + 393: # commit_message_kwargs = { + 394: # new_version ``` # W0703: broad-except -## File src/pycalver/vcs.py - Line 107 - W0703 (broad-except) - -- `message: Catching too general exception Exception` -- `author : Manuel Barkhau ` -- `date : 2020-07-19T18:50:33` - -``` - 101: def has_remote(self) -> bool: - ... - 105: return False - 106: return True -> 107: except Exception: - 108: return False - 109: -``` - - -## File src/pycalver/__main__.py - Line 223 - W0703 (broad-except) +## File src/pycalver/__main__.py - Line 232 - W0703 (broad-except) - `message: Catching too general exception Exception` - `author : Manuel Barkhau ` - `date : 2020-09-05T14:30:17` ``` - 200: def _bump( + 209: def _bump( ... - 221: v1cli.rewrite(cfg, new_version) - 222: v2cli.rewrite(cfg, new_version) -> 223: except Exception as ex: - 224: logger.error(str(ex)) - 225: sys.exit(1) -``` - - -# C0412: ungrouped-imports - -## File src/pycalver/__main__.py - Line 21 - C0412 (ungrouped-imports) - -- `message: Imports from package pycalver are not grouped` -- `author : Manuel Barkhau ` -- `date : 2020-09-05T14:30:17` - -``` - 19: import pycalver.cli as v1cli - 20: import pycalver2.cli as v2cli -> 21: import pycalver.version as v1version - 22: import pycalver2.version as v2version - 23: from pycalver import vcs -``` - - -## File src/pycalver/__main__.py - Line 22 - C0412 (ungrouped-imports) - -- `message: Imports from package pycalver2 are not grouped` -- `author : Manuel Barkhau ` -- `date : 2020-09-05T14:30:17` - -``` - 20: import pycalver2.cli as v2cli - 21: import pycalver.version as v1version -> 22: import pycalver2.version as v2version - 23: from pycalver import vcs - 24: from pycalver import config + 230: v1cli.rewrite(cfg, new_version) + 231: # v2cli.rewrite(cfg, new_version) +> 232: except Exception as ex: + 233: logger.error(str(ex)) + 234: sys.exit(1) ``` diff --git a/setup.cfg b/setup.cfg index d638129..520c0de 100644 --- a/setup.cfg +++ b/setup.cfg @@ -128,10 +128,10 @@ jobs = 4 output-format = colorized # Maximum number of locals for function / method body -max-locals = 20 +max-locals = 21 # Maximum number of arguments for function / method -max-args = 8 +max-args = 9 good-names = logger,i,ex @@ -163,6 +163,7 @@ disable = missing-function-docstring, raise-missing-from, duplicate-code, + ungrouped-imports, generated-members = # members of typing.NamedTuple diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 2f10642..6c4754f 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -73,6 +73,7 @@ def _validate_release_tag(release: str) -> None: @click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") def cli(verbose: int = 0) -> None: """Automatically update PyCalVer version strings on python projects.""" + # pylint:disable=global-statement; global flag is global. global _VERBOSE _VERBOSE = verbose @@ -164,6 +165,8 @@ def _try_print_diff(cfg: config.Config, new_version: str) -> None: else: click.echo(diff) except Exception as ex: + # pylint:disable=broad-except; Mostly we expect IOError here, but + # could be other things and there's no option to recover anyway. logger.error(str(ex), exc_info=True) sys.exit(1) diff --git a/src/pycalver/patterns.py b/src/pycalver/patterns.py index 3b36dee..a09bb01 100644 --- a/src/pycalver/patterns.py +++ b/src/pycalver/patterns.py @@ -185,37 +185,6 @@ FULL_PART_FORMATS = { } -# TODO (mb 2020-09-17): I think this is garbage -# PART_FORMATS = { -# 'major' : "[0-9]+", -# 'minor' : "[0-9]{3,}", -# 'patch' : "[0-9]{3,}", -# 'bid' : "[0-9]{4,}", -# 'MAJOR' : "[0-9]+", -# 'MINOR' : "[0-9]+", -# 'MM' : "[0-9]{2,}", -# 'MMM' : "[0-9]{3,}", -# 'MMMM' : "[0-9]{4,}", -# 'MMMMM' : "[0-9]{5,}", -# 'MMMMMM' : "[0-9]{6,}", -# 'MMMMMMM': "[0-9]{7,}", -# 'PATCH' : "[0-9]+", -# 'PP' : "[0-9]{2,}", -# 'PPP' : "[0-9]{3,}", -# 'PPPP' : "[0-9]{4,}", -# 'PPPPP' : "[0-9]{5,}", -# 'PPPPPP' : "[0-9]{6,}", -# 'PPPPPPP': "[0-9]{7,}", -# 'BID' : "[1-9][0-9]*", -# 'BB' : "[1-9][0-9]{1,}", -# 'BBB' : "[1-9][0-9]{2,}", -# 'BBBB' : "[1-9][0-9]{3,}", -# 'BBBBB' : "[1-9][0-9]{4,}", -# 'BBBBBB' : "[1-9][0-9]{5,}", -# 'BBBBBBB': "[1-9][0-9]{6,}", -# } - - class Pattern(typ.NamedTuple): raw : str # "{pycalver}", "{year}.{month}", "YYYY0M.BUILD" diff --git a/src/pycalver/vcs.py b/src/pycalver/vcs.py index 3ca6102..d7d1e95 100644 --- a/src/pycalver/vcs.py +++ b/src/pycalver/vcs.py @@ -75,7 +75,7 @@ class VCSAPI: logger.debug(cmd_str) output_data: bytes = sp.check_output(cmd_str.split(), env=env, stderr=sp.STDOUT) - # TODO (mb 2018-11-15): Detect encoding of output? + # TODO (mb 2018-11-15): Detect encoding of output? Use chardet? _encoding = "utf-8" return output_data.decode(_encoding) @@ -99,11 +99,13 @@ class VCSAPI: @property def has_remote(self) -> bool: + # pylint:disable=broad-except; Not sure how to anticipate all cases. try: output = self('show_remotes') if output.strip() == "": return False - return True + else: + return True except Exception: return False diff --git a/src/pycalver/version.py b/src/pycalver/version.py index 6bf8658..9282a71 100644 --- a/src/pycalver/version.py +++ b/src/pycalver/version.py @@ -21,16 +21,48 @@ logger = logging.getLogger("pycalver.version") TODAY = dt.datetime.utcnow().date() +MaybeInt = typ.Optional[int] + + class CalendarInfo(typ.NamedTuple): """Container for calendar components of version strings.""" - year : int - quarter : int - month : int - dom : int - doy : int - iso_week: int - us_week : int + year : MaybeInt + quarter : MaybeInt + month : MaybeInt + dom : MaybeInt + doy : MaybeInt + iso_week: MaybeInt + us_week : MaybeInt + + +class VersionInfo(typ.NamedTuple): + """Container for parsed version string.""" + + year : MaybeInt + quarter : MaybeInt + month : MaybeInt + dom : MaybeInt + doy : MaybeInt + iso_week: MaybeInt + us_week : MaybeInt + major : int + minor : int + patch : int + bid : str + tag : str + + +def _ver_to_cal_info(vinfo: VersionInfo) -> CalendarInfo: + return CalendarInfo( + vinfo.year, + vinfo.quarter, + vinfo.month, + vinfo.dom, + vinfo.doy, + vinfo.iso_week, + vinfo.us_week, + ) def _date_from_doy(year: int, doy: int) -> dt.date: @@ -95,23 +127,6 @@ def cal_info(date: dt.date = None) -> CalendarInfo: return CalendarInfo(**kwargs) -class VersionInfo(typ.NamedTuple): - """Container for parsed version string.""" - - year : typ.Optional[int] - quarter : typ.Optional[int] - month : typ.Optional[int] - dom : typ.Optional[int] - doy : typ.Optional[int] - iso_week: typ.Optional[int] - us_week : typ.Optional[int] - major : int - minor : int - patch : int - bid : str - tag : str - - FieldKey = str MatchGroupKey = str MatchGroupStr = str @@ -438,8 +453,8 @@ def incr( cur_cal_nfo = cal_info() - old_date = (old_vinfo.year or 0, old_vinfo.month or 0, old_vinfo.dom or 0) - cur_date = (cur_cal_nfo.year , cur_cal_nfo.month , cur_cal_nfo.dom) + old_date = (old_vinfo.year or 0 , old_vinfo.month or 0 , old_vinfo.dom or 0) + cur_date = (cur_cal_nfo.year or 0, cur_cal_nfo.month or 0, cur_cal_nfo.dom or 0) if old_date <= cur_date: cur_vinfo = cur_vinfo._replace(**cur_cal_nfo._asdict()) diff --git a/src/pycalver2/patterns.py b/src/pycalver2/patterns.py index c6081f8..2b27506 100644 --- a/src/pycalver2/patterns.py +++ b/src/pycalver2/patterns.py @@ -84,7 +84,6 @@ PART_PATTERNS = { 'MAJOR': r"[0-9]+", 'MINOR': r"[0-9]+", 'PATCH': r"[0-9]+", - 'MICRO': r"[0-9]+", 'BUILD': r"[0-9]+", 'BLD' : r"[1-9][0-9]*", 'TAG' : r"(?:alpha|beta|dev|pre|rc|post|final)", @@ -110,7 +109,6 @@ PATTERN_PART_FIELDS = { 'MAJOR': 'major', 'MINOR': 'minor', 'PATCH': 'patch', - 'MICRO': 'patch', 'BUILD': 'bid', 'BLD' : 'bid', 'TAG' : 'tag', @@ -193,7 +191,6 @@ PART_FORMATS: typ.Dict[str, typ.Callable[[FieldValue], str]] = { 'MAJOR': _fmt_num, 'MINOR': _fmt_num, 'PATCH': _fmt_num, - 'MICRO': _fmt_num, 'BUILD': _fmt_num, 'BLD' : _fmt_bld, 'TAG' : _fmt_num, diff --git a/src/pycalver2/version.py b/src/pycalver2/version.py index 6d9fe71..6093ff2 100644 --- a/src/pycalver2/version.py +++ b/src/pycalver2/version.py @@ -28,7 +28,6 @@ ZERO_VALUES = { 'MAJOR': "0", 'MINOR': "0", 'PATCH': "0", - 'MICRO': "0", 'TAG' : "final", 'PYTAG': "", 'NUM' : "0", @@ -67,18 +66,56 @@ assert set(TAG_BY_PEP440_TAG.values()) < set(PEP440_TAG_BY_TAG.keys()) # } +MaybeInt = typ.Optional[int] + + class CalendarInfo(typ.NamedTuple): """Container for calendar components of version strings.""" - year_y : int - year_g : int - quarter: int - month : int - dom : int - doy : int - week_w : int - week_u : int - week_v : int + year_y : MaybeInt + year_g : MaybeInt + quarter: MaybeInt + month : MaybeInt + dom : MaybeInt + doy : MaybeInt + week_w : MaybeInt + week_u : MaybeInt + week_v : MaybeInt + + +class VersionInfo(typ.NamedTuple): + """Container for parsed version string.""" + + year_y : MaybeInt + year_g : MaybeInt + quarter: MaybeInt + month : MaybeInt + dom : MaybeInt + doy : MaybeInt + week_w : MaybeInt + week_u : MaybeInt + week_v : MaybeInt + major : int + minor : int + patch : int + num : int + bid : str + tag : str + pytag : str + + +def _ver_to_cal_info(vinfo: VersionInfo) -> CalendarInfo: + return CalendarInfo( + vinfo.year_y, + vinfo.year_g, + vinfo.quarter, + vinfo.month, + vinfo.dom, + vinfo.doy, + vinfo.week_w, + vinfo.week_u, + vinfo.week_v, + ) def _date_from_doy(year: int, doy: int) -> dt.date: @@ -145,30 +182,6 @@ def cal_info(date: dt.date = None) -> CalendarInfo: return CalendarInfo(**kwargs) -MaybeInt = typ.Optional[int] - - -class VersionInfo(typ.NamedTuple): - """Container for parsed version string.""" - - year_y : MaybeInt - year_g : MaybeInt - quarter: MaybeInt - month : MaybeInt - dom : MaybeInt - doy : MaybeInt - week_w : MaybeInt - week_u : MaybeInt - week_v : MaybeInt - major : int - minor : int - patch : int - num : int - bid : str - tag : str - pytag : str - - VALID_FIELD_KEYS = set(VersionInfo._fields) | {'version'} FieldKey = str @@ -215,7 +228,7 @@ def _parse_version_info(field_values: FieldValues) -> VersionInfo: >>> vnfo = _parse_version_info({'year_y': "2021", 'month': "01", 'dom': "03"}) >>> (vnfo.year_y, vnfo.month, vnfo.dom, vnfo.tag) (2021, 1, 3, 'final') - >>> (vnfo.year_y, vnfo.week_w,vnfo.year_y, vnfo.week_u,vnfo.year_g, vnfo.week_v) + >>> (vnfo.year_y, vnfo.week_w, vnfo.year_y, vnfo.week_u,vnfo.year_g, vnfo.week_v) (2021, 0, 2021, 1, 2020, 53) """ for key in field_values: @@ -542,8 +555,6 @@ def format_version(vinfo: VersionInfo, pattern: str) -> str: 'v1.0.0' >>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.PATCH[-TAG]]") 'v1.0' - >>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.MICRO[-TAG]]") - 'v1.0' >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-TAG]]]") 'v1' @@ -594,8 +605,8 @@ def incr( cur_cal_nfo = cal_info() - old_date = (old_vinfo.year_y or 0, old_vinfo.month or 0, old_vinfo.dom or 0) - cur_date = (cur_cal_nfo.year_y , cur_cal_nfo.month , cur_cal_nfo.dom) + old_date = (old_vinfo.year_y or 0 , old_vinfo.month or 0 , old_vinfo.dom or 0) + cur_date = (cur_cal_nfo.year_y or 0, cur_cal_nfo.month or 0, cur_cal_nfo.dom or 0) if old_date <= cur_date: cur_vinfo = cur_vinfo._replace(**cur_cal_nfo._asdict()) diff --git a/test/test_patterns.py b/test/test_patterns.py index 6e6d25f..cf96507 100644 --- a/test/test_patterns.py +++ b/test/test_patterns.py @@ -128,7 +128,7 @@ V2_PART_PATTERN_CASES = [ (['0V'], "52", "52"), (['0V'], "53", "53"), (['0V'], "54", None), - (['MAJOR', 'MINOR', 'PATCH', 'MICRO'], "0", "0"), + (['MAJOR', 'MINOR', 'PATCH'], "0", "0"), (['TAG' ], "alpha" , "alpha"), (['TAG' ], "alfa" , None), (['TAG' ], "beta" , "beta"), diff --git a/test/test_rewrite.py b/test/test_rewrite.py index 7a90a3d..c9107fe 100644 --- a/test/test_rewrite.py +++ b/test/test_rewrite.py @@ -5,7 +5,6 @@ from __future__ import absolute_import from __future__ import unicode_literals import copy -import datetime as dt from test import util from pycalver import config diff --git a/test/test_version.py b/test/test_version.py index 43f3720..ee010a5 100644 --- a/test/test_version.py +++ b/test/test_version.py @@ -200,9 +200,9 @@ def test_v1_parse_versions(pattern_str, line, expected_vinfo): # def test_v2_parse_versions(pattern_str, line, expected_vinfo): def test_v2_parse_versions(): - vnfo = v2version.parse_version_info("v201712.0033", pattern="vYYYY0M.BUILD[-TAG[NUM]]") + _vnfo = v2version.parse_version_info("v201712.0033", pattern="vYYYY0M.BUILD[-TAG[NUM]]") fvals = {'year_y': 2017, 'month': 12, 'bid': "0033"} - assert vnfo == v2version._parse_version_info(fvals) + assert _vnfo == v2version._parse_version_info(fvals) def test_make_segments(): From f6f3a2fd002c8c23406737afe2b48341de772009 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Fri, 18 Sep 2020 17:51:07 +0000 Subject: [PATCH 21/98] add --pin-date flag --- src/pycalver/__main__.py | 21 +++++++++++++++++---- src/pycalver/version.py | 11 ++++++----- src/pycalver2/version.py | 11 ++++++----- test/test_cli.py | 7 +++++++ 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 6c4754f..9308217 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -88,6 +88,9 @@ def cli(verbose: int = 0) -> None: @click.option("--major", is_flag=True, default=False, help="Increment major component.") @click.option("--minor", is_flag=True, default=False, help="Increment minor component.") @click.option("--patch", is_flag=True, default=False, help="Increment patch component.") +@click.option( + "-p", "--pin-date", is_flag=True, default=False, help="Leave date components unchanged." +) def test( old_version: str, pattern : str = "{pycalver}", @@ -96,6 +99,7 @@ def test( major : bool = False, minor : bool = False, patch : bool = False, + pin_date : bool = False, ) -> None: """Increment a version number for demo purposes.""" _configure_logging(verbose=max(_VERBOSE, verbose)) @@ -110,6 +114,7 @@ def test( major=major, minor=minor, patch=patch, + pin_date=pin_date, ) if new_version is None: logger.error(f"Invalid version '{old_version}' and/or pattern '{pattern}'.") @@ -175,10 +180,11 @@ def _incr( old_version: str, pattern : str = "{pycalver}", *, - release: str = None, - major : bool = False, - minor : bool = False, - patch : bool = False, + release : str = None, + major : bool = False, + minor : bool = False, + patch : bool = False, + pin_date: bool = False, ) -> typ.Optional[str]: is_v1_pattern = "{" in pattern if is_v1_pattern: @@ -189,6 +195,7 @@ def _incr( major=major, minor=minor, patch=patch, + pin_date=pin_date, ) else: return v2version.incr( @@ -198,6 +205,7 @@ def _incr( major=major, minor=minor, patch=patch, + pin_date=pin_date, ) @@ -325,6 +333,9 @@ def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: @click.option("--major", is_flag=True, default=False, help="Increment major component.") @click.option("--minor", is_flag=True, default=False, help="Increment minor component.") @click.option("--patch", is_flag=True, default=False, help="Increment patch component.") +@click.option( + "-p", "--pin-date", is_flag=True, default=False, help="Leave date components unchanged." +) def bump( release : typ.Optional[str] = None, verbose : int = 0, @@ -334,6 +345,7 @@ def bump( major : bool = False, minor : bool = False, patch : bool = False, + pin_date : bool = False, ) -> None: """Increment the current version string and update project files.""" verbose = max(_VERBOSE, verbose) @@ -359,6 +371,7 @@ def bump( major=major, minor=minor, patch=patch, + pin_date=pin_date, ) if new_version is None: diff --git a/src/pycalver/version.py b/src/pycalver/version.py index 9282a71..d11c56e 100644 --- a/src/pycalver/version.py +++ b/src/pycalver/version.py @@ -434,10 +434,11 @@ def incr( old_version: str, pattern : str = "{pycalver}", *, - release: str = None, - major : bool = False, - minor : bool = False, - patch : bool = False, + release : str = None, + major : bool = False, + minor : bool = False, + patch : bool = False, + pin_date: bool = False, ) -> typ.Optional[str]: """Increment version string. @@ -451,7 +452,7 @@ def incr( cur_vinfo = old_vinfo - cur_cal_nfo = cal_info() + cur_cal_nfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info() old_date = (old_vinfo.year or 0 , old_vinfo.month or 0 , old_vinfo.dom or 0) cur_date = (cur_cal_nfo.year or 0, cur_cal_nfo.month or 0, cur_cal_nfo.dom or 0) diff --git a/src/pycalver2/version.py b/src/pycalver2/version.py index 6093ff2..a5e2df6 100644 --- a/src/pycalver2/version.py +++ b/src/pycalver2/version.py @@ -586,10 +586,11 @@ def incr( old_version: str, pattern : str = "vYYYY0M.BUILD[-TAG]", *, - release: str = None, - major : bool = False, - minor : bool = False, - patch : bool = False, + release : str = None, + major : bool = False, + minor : bool = False, + patch : bool = False, + pin_date: bool = False, ) -> typ.Optional[str]: """Increment version string. @@ -603,7 +604,7 @@ def incr( cur_vinfo = old_vinfo - cur_cal_nfo = cal_info() + cur_cal_nfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info() old_date = (old_vinfo.year_y or 0 , old_vinfo.month or 0 , old_vinfo.dom or 0) cur_date = (cur_cal_nfo.year_y or 0, cur_cal_nfo.month or 0, cur_cal_nfo.dom or 0) diff --git a/test/test_cli.py b/test/test_cli.py index 82ff682..2e99fde 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -102,6 +102,13 @@ def test_incr_default(runner): assert f"Version: {new_version}\n" in result.output +def test_incr_pin_date(runner): + old_version = "v201701.0999-alpha" + result = runner.invoke(cli, ['test', "-vv", "--pin-date", old_version]) + assert result.exit_code == 0 + assert "Version: v201701.11000-alpha\n" in result.output + + def test_incr_semver(runner): semver_pattern = "{MAJOR}.{MINOR}.{PATCH}" old_version = "0.1.0" From 7febf195ae6b9f4d6e7c0c2340f284fe1549ddc3 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Fri, 18 Sep 2020 19:22:17 +0000 Subject: [PATCH 22/98] more cleanup --- src/pycalver2/cli.py | 3 ++- src/pycalver2/version.py | 10 ---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/pycalver2/cli.py b/src/pycalver2/cli.py index 107d4b8..753f587 100644 --- a/src/pycalver2/cli.py +++ b/src/pycalver2/cli.py @@ -12,6 +12,7 @@ Provided subcommands: show, test, init, bump import typing as typ import logging +import pycalver.version as v1version import pycalver2.rewrite as v2rewrite import pycalver2.version as v2version from pycalver import config @@ -28,7 +29,7 @@ def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.C version_tags.sort(reverse=True) logger.debug(f"found {len(version_tags)} tags: {version_tags[:2]}") latest_version_tag = version_tags[0] - latest_version_pep440 = v2version.to_pep440(latest_version_tag) + latest_version_pep440 = v1version.to_pep440(latest_version_tag) if latest_version_tag <= cfg.current_version: return cfg diff --git a/src/pycalver2/version.py b/src/pycalver2/version.py index a5e2df6..21f2edd 100644 --- a/src/pycalver2/version.py +++ b/src/pycalver2/version.py @@ -10,7 +10,6 @@ import logging import datetime as dt import lexid -import pkg_resources import pycalver2.patterns as v2patterns @@ -637,12 +636,3 @@ def incr( return None else: return new_version - - -def to_pep440(version: str) -> str: - """Derive pep440 compliant version string from PyCalVer version string. - - >>> to_pep440("v201811.0007-beta") - '201811.7b0' - """ - return str(pkg_resources.parse_version(version)) From a8e658d1c4f389aa1d180e05c0d05f712128ce05 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Fri, 18 Sep 2020 19:34:51 +0000 Subject: [PATCH 23/98] add config parameter: `commit_message` --- README.md | 97 +++++++++++++++++++++++++++------------- src/pycalver/__main__.py | 16 +++---- src/pycalver/config.py | 9 ++++ src/pycalver/vcs.py | 33 ++++++++------ 4 files changed, 102 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 4294453..3ef3d0b 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Project/Repo: [![MIT License][license_img]][license_ref] [![Supported Python Versions][pyversions_img]][pyversions_ref] -[![PyCalVer v202007.0036][version_img]][version_ref] +[![PyCalVer v202007.1036][version_img]][version_ref] [![PyPI Releases][pypi_img]][pypi_ref] [![PyPI Downloads][downloads_img]][downloads_ref] @@ -89,8 +89,9 @@ WARNING - File not found: pycalver.toml Exiting because of '--dry'. Would have written to pycalver.toml: [pycalver] - current_version = "v201902.0001-alpha" - version_pattern = "{pycalver}" + current_version = "v202010.1001-alpha" + version_pattern = "vYYYY0M.BUILD[-TAG]" + commit_message = "bump version to {new_version}" commit = true tag = true push = true @@ -123,8 +124,9 @@ This will add the something like the following to your `setup.cfg` ```ini # setup.cfg [pycalver] -current_version = "v201902.0001-alpha" -version_pattern = "{pycalver}" +current_version = "v201902.1001-alpha" +version_pattern = "vYYYY0M.BUILD[-TAG]" +commit_message = "bump version to {new_version}" commit = True tag = True push = True @@ -164,35 +166,35 @@ it would have made. ```shell $ pycalver bump --dry --no-fetch -INFO - Old Version: v201901.0001-beta -INFO - New Version: v201902.0002-beta +INFO - Old Version: v201901.1001-beta +INFO - New Version: v201902.1002-beta --- README.md +++ README.md @@ -11,7 +11,7 @@ [![Supported Python Versions][pyversions_img]][pyversions_ref] --[![Version v201901.0001][version_img]][version_ref] -+[![Version v201902.0002][version_img]][version_ref] +-[![Version v201901.1001-beta][version_img]][version_ref] ++[![Version v201902.1002-beta][version_img]][version_ref] [![PyPI Releases][pypi_img]][pypi_ref] --- src/mymodule_v1/__init__.py +++ src/mymodule_v1/__init__.py @@ -1,1 +1,1 @@ --__version__ = "v201901.0001-beta" -+__version__ = "v201902.0002-beta" +-__version__ = "v201901.1001-beta" ++__version__ = "v201902.1002-beta" --- src/mymodule_v2/__init__.py +++ src/mymodule_v2/__init__.py @@ -1,1 +1,1 @@ --__version__ = "v201901.0001-beta" -+__version__ = "v201902.0002-beta" +-__version__ = "v201901.1001-beta" ++__version__ = "v201902.1002-beta" --- setup.py +++ setup.py @@ -44,7 +44,7 @@ name="myproject", -- version="201901.1b0", -+ version="201902.2b0", +- version="201901.1001b0", ++ version="201902.1002b0", license="MIT", ``` @@ -485,10 +487,10 @@ maintainer and no build system) this is a non-issue and you can always use ```shell $ time pycalver show --verbose INFO - fetching tags from remote (to turn off use: -n / --no-fetch) -INFO - Working dir version : v201812.0018 -INFO - Latest version from git tag: v201901.0019-beta -Current Version: v201901.0019-beta -PEP440 : 201901.19b0 +INFO - Working dir version : v202010.1018 +INFO - Latest version from git tag: v202010.1019-beta +Current Version: v202010.1019-beta +PEP440 : 202010.1019b0 real 0m4,254s @@ -506,8 +508,9 @@ section: ```ini [pycalver] -current_version = "202008.1006-beta" -version_pattern = "YYYY0M.BUILD[-TAG]" +current_version = "v202010.1006-beta" +version_pattern = "vYYYY0M.BUILD[-TAG]" +commit_message = "bump version to {new_version}" commit = True tag = True push = True @@ -544,11 +547,11 @@ $ pycalver bump --dry @@ -65,7 +65,7 @@ [pycalver] --current_version = v202008.1005-beta -+current_version = v202008.1006-beta +-current_version = v202010.1005-beta ++current_version = v202010.1006-beta + version_pattern = "vYYYY0M.BUILD[-TAG]" + commit_message = "bump version to {new_version}" commit = True - tag = True - push = True ... ``` @@ -557,13 +560,47 @@ If everything looks OK, you can do `pycalver bump`. ``` $ pycalver bump --verbose INFO - fetching tags from remote (to turn off use: -n / --no-fetch) -INFO - Old Version: v202008.0005-beta -INFO - New Version: v202008.0006-beta -INFO - git commit --file /tmp/tmpph_npey9 -INFO - git tag --annotate v202008.0006-beta --message v202008.0006-beta -INFO - git push origin v202008.0006-beta +INFO - Old Version: v202010.1005-beta +INFO - New Version: v202010.1006-beta +INFO - git commit --message 'bump version to v202010.1006-beta' +INFO - git tag --annotate v202010.1006-beta --message v202010.1006-beta +INFO - git push origin v202010.1006-beta ``` +### Config Parameters + +TODO: Descriptions + +| Config Parameter | Type | Description | +|-------------------|---------|------------------------------| +| `current_version` | string | | +| `version_pattern` | string | | +| `commit_message` | string | ¹Template fro commit message | +| `commit` | boolean | | +| `tag` | boolean | | +| `push` | boolean | | + +- ¹ Available placeholders: + - `{new_version}` + - `{old_version}` + - `{new_version_pep440}` + - `{old_version_pep440}` + + +### CLI Arguments + +TODO: Descriptions + +| CLI Argument | Description | +|---------------|-------------| +| --major | | +| --minor | | +| --patch | | +| --pin-date | | +| --no-fetch | | +| --dry | | +| --allow-dirty | | + ## The PyCalVer Format diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 9308217..54fc27d 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -392,15 +392,13 @@ def bump( if dry: return - # # TODO (mb 2020-09-05): format from config - # commit_message_kwargs = { - # new_version - # old_version - # pep440_new_version - # pep440_old_version - # } - # cfg.commit_message = - commit_message = f"bump version to {new_version}" + commit_message_kwargs = { + 'new_version' : new_version, + 'old_version' : old_version, + 'new_version_pep440': v1version.to_pep440(new_version), + 'old_version_pep440': v1version.to_pep440(old_version), + } + commit_message = cfg.commit_message.format(**commit_message_kwargs) _try_bump(cfg, new_version, commit_message, allow_dirty) diff --git a/src/pycalver/config.py b/src/pycalver/config.py index 57223f6..bfe26f6 100644 --- a/src/pycalver/config.py +++ b/src/pycalver/config.py @@ -23,6 +23,8 @@ PatternsByGlob = typ.Dict[str, Patterns] SUPPORTED_CONFIGS = ["setup.cfg", "pyproject.toml", "pycalver.toml"] +DEFAULT_COMMIT_MESSAGE = "bump version to {new_version}" + class ProjectContext(typ.NamedTuple): """Container class for project info.""" @@ -78,6 +80,7 @@ class Config(typ.NamedTuple): current_version: str version_pattern: str pep440_version : str + commit_message : str commit: bool tag : bool @@ -92,6 +95,7 @@ def _debug_str(cfg: Config) -> str: f"current_version='{cfg.current_version}'", "version_pattern='{pycalver}'", f"pep440_version='{cfg.pep440_version}'", + f"commit_message='{cfg.commit_message}'", f"commit={cfg.commit}", f"tag={cfg.tag}", f"push={cfg.push}", @@ -238,6 +242,8 @@ def _parse_config(raw_cfg: RawConfig) -> Config: version_pattern: str = raw_cfg.get('version_pattern', "{pycalver}") version_pattern = raw_cfg['version_pattern'] = version_pattern.strip("'\" ") + commit_message: str = raw_cfg.get('commit_message', DEFAULT_COMMIT_MESSAGE) + commit_message = raw_cfg['commit_message'] = commit_message.strip("'\" ") # NOTE (mb 2019-01-05): Provoke ValueError if version_pattern # and current_version are not compatible. version.parse_version_info(version_str, version_pattern) @@ -265,6 +271,7 @@ def _parse_config(raw_cfg: RawConfig) -> Config: current_version=version_str, version_pattern=version_pattern, pep440_version=pep440_version, + commit_message=commit_message, commit=commit, tag=tag, push=push, @@ -333,6 +340,7 @@ DEFAULT_CONFIGPARSER_BASE_TMPL = """ [pycalver] current_version = "{initial_version}" version_pattern = "{{pycalver}}" +commit_message = "bump version to {{new_version}}" commit = True tag = True push = True @@ -372,6 +380,7 @@ DEFAULT_TOML_BASE_TMPL = """ [pycalver] current_version = "{initial_version}" version_pattern = "{{pycalver}}" +commit_message = "bump version to {{new_version}}" commit = true tag = true push = true diff --git a/src/pycalver/vcs.py b/src/pycalver/vcs.py index d7d1e95..5eed4ce 100644 --- a/src/pycalver/vcs.py +++ b/src/pycalver/vcs.py @@ -33,7 +33,7 @@ VCS_SUBCOMMANDS_BY_NAME = { 'ls_tags' : "git tag --list", 'status' : "git status --porcelain", 'add_path' : "git add --update {path}", - 'commit' : "git commit --file {path}", + 'commit' : "git commit --message '{message}'", 'tag' : "git tag --annotate {tag} --message {tag}", 'push_tag' : "git push origin --follow-tags {tag}", 'show_remotes': "git config --get remote.origin.url", @@ -144,20 +144,25 @@ class VCSAPI: def commit(self, message: str) -> None: """Commit added files.""" - message_data = message.encode("utf-8") - - tmp_file = tempfile.NamedTemporaryFile("wb", delete=False) - assert " " not in tmp_file.name - - fobj: typ.IO[bytes] - - with tmp_file as fobj: - fobj.write(message_data) - env: Env = os.environ.copy() - env['HGENCODING'] = "utf-8" - self('commit', env=env, path=tmp_file.name) - os.unlink(tmp_file.name) + + if self.name == 'git': + self('commit', env=env, message=message) + else: + message_data = message.encode("utf-8") + tmp_file = tempfile.NamedTemporaryFile("wb", delete=False) + try: + assert " " not in tmp_file.name + + fobj: typ.IO[bytes] + + with tmp_file as fobj: + fobj.write(message_data) + + env['HGENCODING'] = "utf-8" + self('commit', env=env, path=tmp_file.name) + finally: + os.unlink(tmp_file.name) def tag(self, tag_name: str) -> None: """Create an annotated tag.""" From e1aaf7629b99dd0a4d821224db96d54fd4287c3b Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Fri, 18 Sep 2020 19:52:40 +0000 Subject: [PATCH 24/98] support for glob patterns --- CHANGELOG.md | 1 + LICENSE | 2 +- src/pycalver/__main__.py | 85 ++++++++++++++++++++++----------------- src/pycalver/config.py | 49 ++++++++++++++-------- src/pycalver2/patterns.py | 2 +- test/test_cli.py | 37 +++++++++-------- test/test_config.py | 43 +++++++++++++++++++- 7 files changed, 147 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2f31a7..abdcaa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Better support for optional parts. - New: Start `BUILD` parts at `1000` to avoid leading zero truncation. - New gitlab #10: `--pin-date` to keep date parts unchanged, and only increment non-date parts. + - New: enable globs for filenames in `pycalver:file_patterns` - Fix gitlab #8: Push tags only pushed tags, not actual commit. - Fix gitlab #9: Make commit message configurable. diff --git a/LICENSE b/LICENSE index 77be9d3..a7b7ab4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -MIT License Copyright (c) 2020 Manuel Barkhau (mbarkhau@gmail.com) +MIT License Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 54fc27d..bf7ae8e 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -17,13 +17,14 @@ import subprocess as sp import click import pycalver.cli as v1cli +import pycalver2.cli as v2cli import pycalver.version as v1version import pycalver2.version as v2version +import pycalver.rewrite as v1rewrite + from pycalver import vcs from pycalver import config -# import pycalver2.cli as v2cli - _VERBOSE = 0 @@ -149,31 +150,21 @@ def show(verbose: int = 0, fetch: bool = True) -> None: click.echo(f"PEP440 : {cfg.pep440_version}") -def _try_print_diff(cfg: config.Config, new_version: str) -> None: - try: - # TODO (mb 2020-09-05): version switch - diff = v1cli.get_diff(cfg, new_version) - # diff = v2cli.get_diff(cfg, new_version) - - if sys.stdout.isatty(): - for line in diff.splitlines(): - if line.startswith("+++") or line.startswith("---"): - click.echo(line) - elif line.startswith("+"): - click.echo("\u001b[32m" + line + "\u001b[0m") - elif line.startswith("-"): - click.echo("\u001b[31m" + line + "\u001b[0m") - elif line.startswith("@"): - click.echo("\u001b[36m" + line + "\u001b[0m") - else: - click.echo(line) - else: - click.echo(diff) - except Exception as ex: - # pylint:disable=broad-except; Mostly we expect IOError here, but - # could be other things and there's no option to recover anyway. - logger.error(str(ex), exc_info=True) - sys.exit(1) +def _print_diff(diff: str) -> None: + if sys.stdout.isatty(): + for line in diff.splitlines(): + if line.startswith("+++") or line.startswith("---"): + click.echo(line) + elif line.startswith("+"): + click.echo("\u001b[32m" + line + "\u001b[0m") + elif line.startswith("-"): + click.echo("\u001b[31m" + line + "\u001b[0m") + elif line.startswith("@"): + click.echo("\u001b[36m" + line + "\u001b[0m") + else: + click.echo(line) + else: + click.echo(diff) def _incr( @@ -229,10 +220,15 @@ def _bump( vcs.assert_not_dirty(vcs_api, filepaths, allow_dirty) try: - # TODO (mb 2020-09-05): version switch - v1cli.rewrite(cfg, new_version) - # v2cli.rewrite(cfg, new_version) + if cfg.is_new_pattern: + v2cli.rewrite(cfg, new_version) + else: + v1cli.rewrite(cfg, new_version) + except v1rewrite.NoPatternMatch as ex: + logger.error(str(ex)) + sys.exit(1) except Exception as ex: + # TODO (mb 2020-09-18): Investigate error messages logger.error(str(ex)) sys.exit(1) @@ -285,10 +281,10 @@ def init(verbose: int = 0, dry: bool = False) -> None: def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: all_tags = vcs.get_tags(fetch=fetch) - # TODO (mb 2020-09-05): version switch - cfg = v1cli.update_cfg_from_vcs(cfg, all_tags) - # cfg = v2cli.update_cfg_from_vcs(cfg, all_tags) - return cfg + if cfg.is_new_pattern: + return v2cli.update_cfg_from_vcs(cfg, all_tags) + else: + return v1cli.update_cfg_from_vcs(cfg, all_tags) @cli.command() @@ -375,7 +371,11 @@ def bump( ) if new_version is None: - is_semver = "{semver}" in cfg.version_pattern + is_semver = "{semver}" in cfg.version_pattern or ( + "MAJOR" in cfg.version_pattern + and "MAJOR" in cfg.version_pattern + and "PATCH" in cfg.version_pattern + ) has_semver_inc = major or minor or patch if is_semver and not has_semver_inc: logger.warning("bump --major/--minor/--patch required when using semver.") @@ -387,7 +387,20 @@ def bump( logger.info(f"New Version: {new_version}") if dry or verbose >= 2: - _try_print_diff(cfg, new_version) + try: + if cfg.is_new_pattern: + diff = v2cli.get_diff(cfg, new_version) + else: + diff = v1cli.get_diff(cfg, new_version) + _print_diff(diff) + except v1rewrite.NoPatternMatch as ex: + logger.error(str(ex)) + sys.exit(1) + except Exception as ex: + # pylint:disable=broad-except; Mostly we expect IOError here, but + # could be other things and there's no option to recover anyway. + logger.error(str(ex)) + sys.exit(1) if dry: return diff --git a/src/pycalver/config.py b/src/pycalver/config.py index bfe26f6..5576ac5 100644 --- a/src/pycalver/config.py +++ b/src/pycalver/config.py @@ -5,7 +5,7 @@ # SPDX-License-Identifier: MIT """Parse setup.cfg or pycalver.cfg files.""" -import os +import glob import typing as typ import logging import datetime as dt @@ -14,7 +14,8 @@ import configparser import toml import pathlib2 as pl -from . import version +import pycalver.version as v1version +import pycalver2.version as v2version logger = logging.getLogger("pycalver.config") @@ -82,9 +83,10 @@ class Config(typ.NamedTuple): pep440_version : str commit_message : str - commit: bool - tag : bool - push : bool + commit : bool + tag : bool + push : bool + is_new_pattern: bool file_patterns: PatternsByGlob @@ -93,12 +95,13 @@ def _debug_str(cfg: Config) -> str: cfg_str_parts = [ "Config Parsed: Config(", f"current_version='{cfg.current_version}'", - "version_pattern='{pycalver}'", + f"version_pattern='{cfg.version_pattern}'", f"pep440_version='{cfg.pep440_version}'", f"commit_message='{cfg.commit_message}'", f"commit={cfg.commit}", f"tag={cfg.tag}", f"push={cfg.push}", + f"is_new_pattern={cfg.is_new_pattern}", "file_patterns={", ] @@ -197,7 +200,7 @@ def _normalize_file_patterns(raw_cfg: RawConfig) -> FilePatterns: """ version_str : str = raw_cfg['current_version'] version_pattern: str = raw_cfg['version_pattern'] - pep440_version : str = version.to_pep440(version_str) + pep440_version : str = v1version.to_pep440(version_str) file_patterns: FilePatterns if 'file_patterns' in raw_cfg: @@ -205,9 +208,12 @@ def _normalize_file_patterns(raw_cfg: RawConfig) -> FilePatterns: else: file_patterns = {} - for filepath, patterns in list(file_patterns.items()): - if not os.path.exists(filepath): - logger.warning(f"Invalid config, no such file: {filepath}") + for filepath_glob, patterns in list(file_patterns.items()): + filepaths = glob.glob(filepath_glob) + if not filepaths: + logger.warning(f"Invalid config, no such file: {filepath_glob}") + # fallback to treating it as a simple path + filepaths = [filepath_glob] normalized_patterns: typ.List[str] = [] for pattern in patterns: @@ -219,11 +225,12 @@ def _normalize_file_patterns(raw_cfg: RawConfig) -> FilePatterns: elif version_pattern == "{semver}": normalized_pattern = normalized_pattern.replace("{pep440_version}", "{semver}") elif "{pep440_version}" in pattern: - logger.warning(f"Invalid config, cannot match '{pattern}' for '{filepath}'.") + logger.warning(f"Invalid config, cannot match '{pattern}' for '{filepath_glob}'.") logger.warning(f"No mapping of '{version_pattern}' to '{pep440_version}'") normalized_patterns.append(normalized_pattern) - file_patterns[filepath] = normalized_patterns + for filepath in filepaths: + file_patterns[filepath] = normalized_patterns return file_patterns @@ -237,18 +244,27 @@ def _parse_config(raw_cfg: RawConfig) -> Config: version_str: str = raw_cfg['current_version'] version_str = raw_cfg['current_version'] = version_str.strip("'\" ") - # TODO (mb 2020-09-06): new style pattern by default - # version_pattern: str = raw_cfg.get('version_pattern', "vYYYY0M.BUILD[-TAG]") version_pattern: str = raw_cfg.get('version_pattern', "{pycalver}") version_pattern = raw_cfg['version_pattern'] = version_pattern.strip("'\" ") commit_message: str = raw_cfg.get('commit_message', DEFAULT_COMMIT_MESSAGE) commit_message = raw_cfg['commit_message'] = commit_message.strip("'\" ") + + is_new_pattern = not ("{" in version_pattern or "}" in version_pattern) + + # TODO (mb 2020-09-18): Validate Pattern + # detect YY with WW or UU -> suggest GG with VV + # detect YYMM -> suggest YY0M + # detect YYWW -> suggest YY0W + # NOTE (mb 2019-01-05): Provoke ValueError if version_pattern # and current_version are not compatible. - version.parse_version_info(version_str, version_pattern) + if is_new_pattern: + v2version.parse_version_info(version_str, version_pattern) + else: + v1version.parse_version_info(version_str, version_pattern) - pep440_version = version.to_pep440(version_str) + pep440_version = v1version.to_pep440(version_str) commit = raw_cfg['commit'] tag = raw_cfg['tag'] @@ -275,6 +291,7 @@ def _parse_config(raw_cfg: RawConfig) -> Config: commit=commit, tag=tag, push=push, + is_new_pattern=is_new_pattern, file_patterns=file_patterns, ) logger.debug(_debug_str(cfg)) diff --git a/src/pycalver2/patterns.py b/src/pycalver2/patterns.py index 2b27506..8219422 100644 --- a/src/pycalver2/patterns.py +++ b/src/pycalver2/patterns.py @@ -236,7 +236,7 @@ def _replace_pattern_parts(pattern: str) -> str: ) last_start_idx = start_idx - return "(?P" + result_pattern + ")" + return result_pattern def compile_pattern_str(pattern: str) -> str: diff --git a/test/test_cli.py b/test/test_cli.py index 2e99fde..118ff55 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -98,7 +98,7 @@ def test_incr_default(runner): result = runner.invoke(cli, ['test', "-vv", old_version]) assert result.exit_code == 0 - new_version = initial_version.replace(".0001-alpha", ".11000-alpha") + new_version = initial_version.replace(".1001-alpha", ".11000-alpha") assert f"Version: {new_version}\n" in result.output @@ -152,7 +152,7 @@ def test_incr_to_beta(runner): result = runner.invoke(cli, ['test', old_version, "-vv", "--release", "beta"]) assert result.exit_code == 0 - new_version = initial_version.replace(".0001-alpha", ".11000-beta") + new_version = initial_version.replace(".1001-alpha", ".11000-beta") assert f"Version: {new_version}\n" in result.output @@ -162,7 +162,7 @@ def test_incr_to_final(runner): result = runner.invoke(cli, ['test', old_version, "-vv", "--release", "final"]) assert result.exit_code == 0 - new_version = initial_version.replace(".0001-alpha", ".11000") + new_version = initial_version.replace(".1001-alpha", ".11000") assert f"Version: {new_version}\n" in result.output @@ -178,8 +178,8 @@ def _add_project_files(*files): with pl.Path("README.md").open(mode="wt", encoding="utf-8") as fobj: fobj.write( """ - Hello World v201701.0002-alpha ! - aka. 201701.2a0 ! + Hello World v201701.1002-alpha ! + aka. 201701.1002a0 ! """ ) @@ -337,8 +337,8 @@ def test_git_tag_eval(runner): result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 0 initial_version = config._initial_version() - tag_version = initial_version.replace(".0001-alpha", ".0123-beta") - tag_version_pep440 = tag_version[1:7] + ".123b0" + tag_version = initial_version.replace(".1001-alpha", ".1123-beta") + tag_version_pep440 = tag_version[1:7] + ".1123b0" shell("git", "tag", "--annotate", tag_version, "--message", f"bump version to {tag_version}") @@ -357,8 +357,8 @@ def test_hg_tag_eval(runner): result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 0 initial_version = config._initial_version() - tag_version = initial_version.replace(".0001-alpha", ".0123-beta") - tag_version_pep440 = tag_version[1:7] + ".123b0" + tag_version = initial_version.replace(".1001-alpha", ".1123-beta") + tag_version_pep440 = tag_version[1:7] + ".1123b0" shell("hg", "tag", tag_version, "--message", f"bump version to {tag_version}") @@ -377,20 +377,20 @@ def test_novcs_bump(runner): result = runner.invoke(cli, ['bump', "-vv"]) assert result.exit_code == 0 - calver = config._initial_version()[:7] + calver = config._initial_version().split(".")[0] with pl.Path("README.md").open() as fobj: content = fobj.read() - assert calver + ".0002-alpha !\n" in content - assert calver[1:] + ".2a0 !\n" in content + assert calver + ".1002-alpha !\n" in content + assert calver[1:] + ".1002a0 !\n" in content result = runner.invoke(cli, ['bump', "-vv", "--release", "beta"]) assert result.exit_code == 0 with pl.Path("README.md").open() as fobj: content = fobj.read() - assert calver + ".0003-beta !\n" in content - assert calver[1:] + ".3b0 !\n" in content + assert calver + ".1003-beta !\n" in content + assert calver[1:] + ".1003b0 !\n" in content def test_git_bump(runner): @@ -410,7 +410,7 @@ def test_git_bump(runner): with pl.Path("README.md").open() as fobj: content = fobj.read() - assert calver + ".0002-alpha !\n" in content + assert calver + ".1002-alpha !\n" in content def test_hg_bump(runner): @@ -430,7 +430,7 @@ def test_hg_bump(runner): with pl.Path("README.md").open() as fobj: content = fobj.read() - assert calver + ".0002-alpha !\n" in content + assert calver + ".1002-alpha !\n" in content def test_empty_git_bump(runner, caplog): @@ -530,3 +530,8 @@ def test_bump_semver_diff(runner, caplog): assert "+++ setup.cfg" in out_lines assert "-current_version = \"0.1.0\"" in out_lines assert f"+current_version = \"{expected}\"" in out_lines + + +# def test_custom_commit_message(runner): +# # TODO (mb 2020-09-18): +# assert False diff --git a/test/test_config.py b/test/test_config.py index bae5ade..da611b1 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -63,6 +63,26 @@ setup.cfg = """ +NEW_PATTERN_CFG_FIXTURE = """ +[pycalver] +current_version = "v201808.1456-beta" +version_pattern = "vYYYY0M.BUILD[-TAG]" +commit_message = "bump version to {new_version}" +commit = True +tag = True +push = True + +[pycalver:file_patterns] +setup.py = + {version} + {pep440_version} +setup.cfg = + current_version = "{version}" +src/project/*.py = + Copyright (c) 2018-YYYY +""" + + def mk_buf(text): buf = io.StringIO() buf.write(text) @@ -104,7 +124,7 @@ def test_parse_toml_2(): assert cfg.file_patterns["pycalver.toml"] == ['current_version = "{semver}"'] -def test_parse_cfg(): +def test_parse_v1_cfg(): buf = mk_buf(SETUP_CFG_FIXTURE) raw_cfg = config._parse_cfg(buf) @@ -120,6 +140,25 @@ def test_parse_cfg(): assert cfg.file_patterns["setup.cfg"] == ['current_version = "{pycalver}"'] +def test_parse_v2_cfg(): + buf = mk_buf(NEW_PATTERN_CFG_FIXTURE) + + raw_cfg = config._parse_cfg(buf) + cfg = config._parse_config(raw_cfg) + assert cfg.current_version == "v201808.1456-beta" + assert cfg.commit_message == "bump version to {new_version}" + assert cfg.commit is True + assert cfg.tag is True + assert cfg.push is True + + assert "setup.py" in cfg.file_patterns + assert "setup.cfg" in cfg.file_patterns + # TODO (mb 2020-09-18): + # assert cfg.file_patterns["setup.py" ] == ["vYYYY0M.BUILD[-TAG]", "YYYY0M.BLD[PYTAGNUM]"] + # assert cfg.file_patterns["setup.cfg" ] == ['current_version = "vYYYY0M.BUILD[-TAG]"'] + # assert cfg.file_patterns["src/project/*.py"] == ['Copyright (c) 2018-YYYY"'] + + def test_parse_default_toml(): project_path = util.FIXTURES_DIR / "project_a" @@ -295,7 +334,7 @@ def test_parse_missing_version(tmpdir): "\n".join( ( "[pycalver]", - # f"current_version = v201808.0001-dev", + # f"current_version = v201808.1001-dev", "commit = False", ) ) From 8af50472446f4b7cdf454e46be06f95085967d65 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sat, 19 Sep 2020 22:35:48 +0000 Subject: [PATCH 25/98] module reorg --- CHANGELOG.md | 1 - pylint-ignore.md | 136 +++-- src/pycalver/__main__.py | 87 +-- src/pycalver/config.py | 277 +++++---- src/pycalver/parse.py | 17 +- src/pycalver/patterns.py | 215 +------ src/pycalver/rewrite.py | 219 +------ src/pycalver/{cli.py => v1cli.py} | 20 +- src/pycalver/v1patterns.py | 220 ++++++++ src/pycalver/v1rewrite.py | 193 +++++++ src/pycalver/v1version.py | 407 +++++++++++++ src/{pycalver2/cli.py => pycalver/v2cli.py} | 21 +- .../patterns.py => pycalver/v2patterns.py} | 70 ++- src/pycalver/v2rewrite.py | 198 +++++++ .../version.py => pycalver/v2version.py} | 322 ++++------- src/pycalver/version.py | 534 ++++-------------- src/pycalver2/__init__.py | 8 - src/pycalver2/rewrite.py | 178 ------ test/test_cli.py | 4 +- test/test_parse.py | 2 +- test/test_patterns.py | 4 +- test/test_rewrite.py | 22 +- test/test_version.py | 35 +- 23 files changed, 1658 insertions(+), 1532 deletions(-) rename src/pycalver/{cli.py => v1cli.py} (71%) create mode 100644 src/pycalver/v1patterns.py create mode 100644 src/pycalver/v1rewrite.py create mode 100644 src/pycalver/v1version.py rename src/{pycalver2/cli.py => pycalver/v2cli.py} (71%) rename src/{pycalver2/patterns.py => pycalver/v2patterns.py} (74%) create mode 100644 src/pycalver/v2rewrite.py rename src/{pycalver2/version.py => pycalver/v2version.py} (62%) delete mode 100644 src/pycalver2/__init__.py delete mode 100644 src/pycalver2/rewrite.py diff --git a/CHANGELOG.md b/CHANGELOG.md index abdcaa6..c2f31a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,6 @@ - Better support for optional parts. - New: Start `BUILD` parts at `1000` to avoid leading zero truncation. - New gitlab #10: `--pin-date` to keep date parts unchanged, and only increment non-date parts. - - New: enable globs for filenames in `pycalver:file_patterns` - Fix gitlab #8: Push tags only pushed tags, not actual commit. - Fix gitlab #9: Make commit message configurable. diff --git a/pylint-ignore.md b/pylint-ignore.md index 789183b..3d65cdb 100644 --- a/pylint-ignore.md +++ b/pylint-ignore.md @@ -23,7 +23,8 @@ The recommended approach to using `pylint-ignore` is: # Overview - - [W0511: fixme (7x)](#w0511-fixme) + - [E1123: unexpected-keyword-arg (1x)](#e1123-unexpected-keyword-arg) + - [W0511: fixme (9x)](#w0511-fixme) - [W0703: broad-except (1x)](#w0703-broad-except) @@ -36,7 +37,7 @@ The recommended approach to using `pylint-ignore` is: - `date : 2020-09-18T17:01:05` ``` - 12: import pycalver2.patterns as v2patterns + 12: from pycalver import v2patterns 13: > 14: # TODO (mb 2020-09-06): test for v2patterns 15: @@ -61,107 +62,138 @@ The recommended approach to using `pylint-ignore` is: ``` -## File test/test_version.py - Line 167 - W0511 (fixme) +## File test/test_config.py - Line 156 - W0511 (fixme) + +- `message: TODO (mb 2020-09-18):` +- `author : Manuel Barkhau ` +- `date : 2020-09-18T19:04:06` + +``` + 143: def test_parse_v2_cfg(): + ... + 154: assert "setup.py" in cfg.file_patterns + 155: assert "setup.cfg" in cfg.file_patterns +> 156: # TODO (mb 2020-09-18): + 157: # assert cfg.file_patterns["setup.py" ] == ["vYYYY0M.BUILD[-TAG]", "YYYY0M.BLD[PYTAGNUM]"] + 158: # assert cfg.file_patterns["setup.cfg" ] == ['current_version = "vYYYY0M.BUILD[-TAG]"'] +``` + + +## File test/test_version.py - Line 168 - W0511 (fixme) - `message: TODO (mb 2020-09-06): add tests for new style patterns` - `author : Manuel Barkhau ` - `date : 2020-09-18T17:01:05` ``` - 162: def vnfo(**field_values): + 163: def vnfo(**field_values): ... - 165: - 166: PARSE_VERSION_TEST_CASES = [ -> 167: # TODO (mb 2020-09-06): add tests for new style patterns - 168: # ["YYYY.MM.DD" , "2017.06.07", vnfo(year="2017", month="06", dom="07")], - 169: ["{year}.{month}.{dom}" , "2017.06.07", vnfo(year="2017", month="06", dom="07")], + 166: + 167: PARSE_VERSION_TEST_CASES = [ +> 168: # TODO (mb 2020-09-06): add tests for new style patterns + 169: # ["YYYY.MM.DD" , "2017.06.07", vnfo(year="2017", month="06", dom="07")], + 170: ["{year}.{month}.{dom}" , "2017.06.07", vnfo(year="2017", month="06", dom="07")], ``` -## File src/pycalver/__main__.py - Line 229 - W0511 (fixme) +## File src/pycalver/v1patterns.py - Line 212 - W0511 (fixme) -- `message: TODO (mb 2020-09-05): version switch` +- `message: TODO (mb 2020-09-19): replace {version} etc with version_pattern` - `author : Manuel Barkhau ` -- `date : 2020-09-05T14:30:17` +- `date : 2020-09-19T16:24:10` ``` - 209: def _bump( + 199: def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[str]: ... - 227: - 228: try: -> 229: # TODO (mb 2020-09-05): version switch - 230: v1cli.rewrite(cfg, new_version) - 231: # v2cli.rewrite(cfg, new_version) + 210: escaped_pattern = escaped_pattern.replace(char, escaped) + 211: +> 212: # TODO (mb 2020-09-19): replace {version} etc with version_pattern + 213: pattern_str = _replace_pattern_parts(escaped_pattern) + 214: return re.compile(pattern_str) ``` -## File src/pycalver/config.py - Line 236 - W0511 (fixme) +## File src/pycalver/__main__.py - Line 247 - W0511 (fixme) -- `message: TODO (mb 2020-09-06): new style pattern by default` +- `message: TODO (mb 2020-09-18): Investigate error messages` - `author : Manuel Barkhau ` -- `date : 2020-09-18T17:01:05` +- `date : 2020-09-19T16:24:10` ``` - 227: def _parse_config(raw_cfg: RawConfig) -> Config: + 219: def _bump( ... - 234: version_str = raw_cfg['current_version'] = version_str.strip("'\" ") - 235: -> 236: # TODO (mb 2020-09-06): new style pattern by default - 237: # version_pattern: str = raw_cfg.get('version_pattern', "vYYYY0M.BUILD[-TAG]") - 238: version_pattern: str = raw_cfg.get('version_pattern', "{pycalver}") + 245: sys.exit(1) + 246: except Exception as ex: +> 247: # TODO (mb 2020-09-18): Investigate error messages + 248: logger.error(str(ex)) + 249: sys.exit(1) ``` -## File src/pycalver/__main__.py - Line 285 - W0511 (fixme) +## File src/pycalver/v2patterns.py - Line 256 - W0511 (fixme) -- `message: TODO (mb 2020-09-05): version switch` +- `message: TODO (mb 2020-09-19): replace {version} etc with version_pattern` - `author : Manuel Barkhau ` -- `date : 2020-09-05T14:30:17` +- `date : 2020-09-19T16:24:10` ``` - 282: def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: + 240: def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[str]: ... - 283: all_tags = vcs.get_tags(fetch=fetch) - 284: -> 285: # TODO (mb 2020-09-05): version switch - 286: cfg = v1cli.update_cfg_from_vcs(cfg, all_tags) - 287: # cfg = v2cli.update_cfg_from_vcs(cfg, all_tags) + 254: print("<<<<", (normalized_pattern,)) + 255: +> 256: # TODO (mb 2020-09-19): replace {version} etc with version_pattern + 257: pattern_str = _replace_pattern_parts(escaped_pattern) + 258: return re.compile(pattern_str) ``` -## File src/pycalver/__main__.py - Line 392 - W0511 (fixme) +## File src/pycalver/config.py - Line 264 - W0511 (fixme) -- `message: # TODO (mb 2020-09-05): format from config` +- `message: TODO (mb 2020-09-18): Validate Pattern` - `author : Manuel Barkhau ` -- `date : 2020-09-05T14:30:17` +- `date : 2020-09-18T19:04:06` ``` - 336: def bump( + 250: def _parse_config(raw_cfg: RawConfig) -> Config: ... - 390: return - 391: -> 392: # # TODO (mb 2020-09-05): format from config - 393: # commit_message_kwargs = { - 394: # new_version + 262: is_new_pattern = "{" not in version_pattern and "}" not in version_pattern + 263: +> 264: # TODO (mb 2020-09-18): Validate Pattern + 265: # detect YY with WW or UU -> suggest GG with VV + 266: # detect YYMM -> suggest YY0M +``` + + +## File test/test_cli.py - Line 536 - W0511 (fixme) + +- `message: # TODO (mb 2020-09-18):` +- `author : Manuel Barkhau ` +- `date : 2020-09-18T19:35:32` + +``` + 534: + 535: # def test_custom_commit_message(runner): +> 536: # # TODO (mb 2020-09-18): + 537: # assert False ``` # W0703: broad-except -## File src/pycalver/__main__.py - Line 232 - W0703 (broad-except) +## File src/pycalver/__main__.py - Line 246 - W0703 (broad-except) - `message: Catching too general exception Exception` - `author : Manuel Barkhau ` - `date : 2020-09-05T14:30:17` ``` - 209: def _bump( + 219: def _bump( ... - 230: v1cli.rewrite(cfg, new_version) - 231: # v2cli.rewrite(cfg, new_version) -> 232: except Exception as ex: - 233: logger.error(str(ex)) - 234: sys.exit(1) + 244: logger.error(str(ex)) + 245: sys.exit(1) +> 246: except Exception as ex: + 247: # TODO (mb 2020-09-18): Investigate error messages + 248: logger.error(str(ex)) ``` diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index bf7ae8e..2806a45 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -16,14 +16,14 @@ import subprocess as sp import click -import pycalver.cli as v1cli -import pycalver2.cli as v2cli -import pycalver.version as v1version -import pycalver2.version as v2version -import pycalver.rewrite as v1rewrite - -from pycalver import vcs -from pycalver import config +from . import vcs +from . import v1cli +from . import v2cli +from . import config +from . import rewrite +from . import version +from . import v1version +from . import v2version _VERBOSE = 0 @@ -110,7 +110,7 @@ def test( new_version = _incr( old_version, - pattern=pattern, + raw_pattern=pattern, release=release, major=major, minor=minor, @@ -121,9 +121,7 @@ def test( logger.error(f"Invalid version '{old_version}' and/or pattern '{pattern}'.") sys.exit(1) - # TODO (mb 2020-09-05): version switch - pep440_version = v1version.to_pep440(new_version) - # pep440_version = v2version.to_pep440(new_version) + pep440_version = version.to_pep440(new_version) click.echo(f"New Version: {new_version}") click.echo(f"PEP440 : {pep440_version}") @@ -150,7 +148,7 @@ def show(verbose: int = 0, fetch: bool = True) -> None: click.echo(f"PEP440 : {cfg.pep440_version}") -def _print_diff(diff: str) -> None: +def _print_diff_str(diff: str) -> None: if sys.stdout.isatty(): for line in diff.splitlines(): if line.startswith("+++") or line.startswith("---"): @@ -167,9 +165,27 @@ def _print_diff(diff: str) -> None: click.echo(diff) +def _print_diff(cfg: config.Config, new_version: str) -> None: + try: + if cfg.is_new_pattern: + diff = v2cli.get_diff(cfg, new_version) + else: + diff = v1cli.get_diff(cfg, new_version) + + _print_diff_str(diff) + except rewrite.NoPatternMatch as ex: + logger.error(str(ex)) + sys.exit(1) + except Exception as ex: + # pylint:disable=broad-except; Mostly we expect IOError here, but + # could be other things and there's no option to recover anyway. + logger.error(str(ex)) + sys.exit(1) + + def _incr( old_version: str, - pattern : str = "{pycalver}", + raw_pattern: str = "{pycalver}", *, release : str = None, major : bool = False, @@ -177,11 +193,11 @@ def _incr( patch : bool = False, pin_date: bool = False, ) -> typ.Optional[str]: - is_v1_pattern = "{" in pattern - if is_v1_pattern: - return v1version.incr( + is_new_pattern = "{" in raw_pattern and "}" in raw_pattern + if is_new_pattern: + return v2version.incr( old_version, - pattern=pattern, + raw_pattern=raw_pattern, release=release, major=major, minor=minor, @@ -189,9 +205,9 @@ def _incr( pin_date=pin_date, ) else: - return v2version.incr( + return v1version.incr( old_version, - pattern=pattern, + raw_pattern=raw_pattern, release=release, major=major, minor=minor, @@ -221,10 +237,10 @@ def _bump( try: if cfg.is_new_pattern: - v2cli.rewrite(cfg, new_version) + v2cli.rewrite_files(cfg, new_version) else: - v1cli.rewrite(cfg, new_version) - except v1rewrite.NoPatternMatch as ex: + v1cli.rewrite_files(cfg, new_version) + except rewrite.NoPatternMatch as ex: logger.error(str(ex)) sys.exit(1) except Exception as ex: @@ -266,11 +282,11 @@ def init(verbose: int = 0, dry: bool = False) -> None: cfg: config.MaybeConfig = config.parse(ctx) if cfg: - logger.error(f"Configuration already initialized in {ctx.config_filepath}") + logger.error(f"Configuration already initialized in {ctx.config_rel_path}") sys.exit(1) if dry: - click.echo(f"Exiting because of '--dry'. Would have written to {ctx.config_filepath}:") + click.echo(f"Exiting because of '--dry'. Would have written to {ctx.config_rel_path}:") cfg_text: str = config.default_config(ctx) click.echo("\n " + "\n ".join(cfg_text.splitlines())) sys.exit(0) @@ -362,7 +378,7 @@ def bump( old_version = cfg.current_version new_version = _incr( old_version, - pattern=cfg.version_pattern, + raw_pattern=cfg.version_pattern, release=release, major=major, minor=minor, @@ -387,20 +403,7 @@ def bump( logger.info(f"New Version: {new_version}") if dry or verbose >= 2: - try: - if cfg.is_new_pattern: - diff = v2cli.get_diff(cfg, new_version) - else: - diff = v1cli.get_diff(cfg, new_version) - _print_diff(diff) - except v1rewrite.NoPatternMatch as ex: - logger.error(str(ex)) - sys.exit(1) - except Exception as ex: - # pylint:disable=broad-except; Mostly we expect IOError here, but - # could be other things and there's no option to recover anyway. - logger.error(str(ex)) - sys.exit(1) + _print_diff(cfg, new_version) if dry: return @@ -408,8 +411,8 @@ def bump( commit_message_kwargs = { 'new_version' : new_version, 'old_version' : old_version, - 'new_version_pep440': v1version.to_pep440(new_version), - 'old_version_pep440': v1version.to_pep440(old_version), + 'new_version_pep440': version.to_pep440(new_version), + 'old_version_pep440': version.to_pep440(old_version), } commit_message = cfg.commit_message.format(**commit_message_kwargs) diff --git a/src/pycalver/config.py b/src/pycalver/config.py index 5576ac5..d1ff1ea 100644 --- a/src/pycalver/config.py +++ b/src/pycalver/config.py @@ -14,13 +14,22 @@ import configparser import toml import pathlib2 as pl -import pycalver.version as v1version -import pycalver2.version as v2version +from . import version +from . import v1version +from . import v2version +from . import v1patterns +from . import v2patterns +from .patterns import Pattern logger = logging.getLogger("pycalver.config") -Patterns = typ.List[str] -PatternsByGlob = typ.Dict[str, Patterns] +RawPatterns = typ.List[str] +RawPatternsByFile = typ.Dict[str, RawPatterns] +FileRawPatternsItem = typ.Tuple[str, RawPatterns] + +PatternsByFile = typ.Dict[str, typ.List[Pattern]] +FilePatternsItem = typ.Tuple[str, typ.List[Pattern]] + SUPPORTED_CONFIGS = ["setup.cfg", "pyproject.toml", "pycalver.toml"] @@ -32,6 +41,7 @@ class ProjectContext(typ.NamedTuple): path : pl.Path config_filepath: pl.Path + config_rel_path: str config_format : str vcs_type : typ.Optional[str] @@ -60,6 +70,12 @@ def init_project_ctx(project_path: typ.Union[str, pl.Path, None] = ".") -> Proje config_filepath = path / "pycalver.toml" config_format = 'toml' + if config_filepath.is_absolute(): + config_rel_path = str(config_filepath.relative_to(path.absolute())) + else: + config_rel_path = str(config_filepath) + config_filepath = pl.Path.cwd() / config_filepath + vcs_type: typ.Optional[str] if (path / ".git").exists(): @@ -69,10 +85,11 @@ def init_project_ctx(project_path: typ.Union[str, pl.Path, None] = ".") -> Proje else: vcs_type = None - return ProjectContext(path, config_filepath, config_format, vcs_type) + return ProjectContext(path, config_filepath, config_rel_path, config_format, vcs_type) -RawConfig = typ.Dict[str, typ.Any] +RawConfig = typ.Dict[str, typ.Any] +MaybeRawConfig = typ.Optional[RawConfig] class Config(typ.NamedTuple): @@ -88,56 +105,46 @@ class Config(typ.NamedTuple): push : bool is_new_pattern: bool - file_patterns: PatternsByGlob + file_patterns: PatternsByFile + + +MaybeConfig = typ.Optional[Config] def _debug_str(cfg: Config) -> str: cfg_str_parts = [ "Config Parsed: Config(", - f"current_version='{cfg.current_version}'", - f"version_pattern='{cfg.version_pattern}'", - f"pep440_version='{cfg.pep440_version}'", - f"commit_message='{cfg.commit_message}'", - f"commit={cfg.commit}", - f"tag={cfg.tag}", - f"push={cfg.push}", - f"is_new_pattern={cfg.is_new_pattern}", - "file_patterns={", + f"\n current_version='{cfg.current_version}',", + f"\n version_pattern='{cfg.version_pattern}',", + f"\n pep440_version='{cfg.pep440_version}',", + f"\n commit_message='{cfg.commit_message}',", + f"\n commit={cfg.commit},", + f"\n tag={cfg.tag},", + f"\n push={cfg.push},", + f"\n is_new_pattern={cfg.is_new_pattern},", + "\n file_patterns={", ] for filepath, patterns in cfg.file_patterns.items(): for pattern in patterns: - cfg_str_parts.append(f"\n '{filepath}': '{pattern}'") + cfg_str_parts.append(f"\n '{filepath}': '{pattern.raw_pattern}',") - cfg_str_parts += ["\n})"] - return ", ".join(cfg_str_parts) + cfg_str_parts += ["\n }\n)"] + return "".join(cfg_str_parts) -MaybeConfig = typ.Optional[Config] -MaybeRawConfig = typ.Optional[RawConfig] +def _parse_cfg_file_patterns( + cfg_parser: configparser.RawConfigParser, +) -> typ.Iterable[FileRawPatternsItem]: + if not cfg_parser.has_section("pycalver:file_patterns"): + return -FilePatterns = typ.Dict[str, typ.List[str]] - - -def _parse_cfg_file_patterns(cfg_parser: configparser.RawConfigParser) -> FilePatterns: - file_patterns: FilePatterns = {} - - file_pattern_items: typ.List[typ.Tuple[str, str]] - if cfg_parser.has_section("pycalver:file_patterns"): - file_pattern_items = cfg_parser.items("pycalver:file_patterns") - else: - file_pattern_items = [] + file_pattern_items: typ.List[typ.Tuple[str, str]] = cfg_parser.items("pycalver:file_patterns") for filepath, patterns_str in file_pattern_items: - patterns: typ.List[str] = [] - for line in patterns_str.splitlines(): - pattern = line.strip() - if pattern: - patterns.append(pattern) - - file_patterns[filepath] = patterns - - return file_patterns + maybe_patterns = (line.strip() for line in patterns_str.splitlines()) + patterns = [p for p in maybe_patterns if p] + yield filepath, patterns class _ConfigParser(configparser.RawConfigParser): @@ -178,7 +185,7 @@ def _parse_cfg(cfg_buffer: typ.IO[str]) -> RawConfig: val = val.lower() in ("yes", "true", "1", "on") raw_cfg[option] = val - raw_cfg['file_patterns'] = _parse_cfg_file_patterns(cfg_parser) + raw_cfg['file_patterns'] = dict(_parse_cfg_file_patterns(cfg_parser)) return raw_cfg @@ -193,64 +200,66 @@ def _parse_toml(cfg_buffer: typ.IO[str]) -> RawConfig: return raw_cfg -def _normalize_file_patterns(raw_cfg: RawConfig) -> FilePatterns: - """Create consistent representation of file_patterns. +def _iter_glob_expanded_file_patterns( + raw_patterns_by_file: RawPatternsByFile, +) -> typ.Iterable[FileRawPatternsItem]: + for filepath_glob, raw_patterns in raw_patterns_by_file.items(): + filepaths = glob.glob(filepath_glob) + if filepaths: + for filepath in filepaths: + yield filepath, raw_patterns + else: + logger.warning(f"Invalid config, no such file: {filepath_glob}") + # fallback to treating it as a simple path + yield filepath_glob, raw_patterns + + +def _compile_v1_file_patterns(raw_cfg: RawConfig) -> typ.Iterable[FilePatternsItem]: + """Create inernal/compiled representation of the file_patterns config field. The result the same, regardless of the config format. """ - version_str : str = raw_cfg['current_version'] - version_pattern: str = raw_cfg['version_pattern'] - pep440_version : str = v1version.to_pep440(version_str) + # current_version: str = raw_cfg['current_version'] + # current_pep440_version = version.pep440_version(current_version) - file_patterns: FilePatterns - if 'file_patterns' in raw_cfg: - file_patterns = raw_cfg['file_patterns'] - else: - file_patterns = {} + version_pattern : str = raw_cfg['version_pattern'] + raw_patterns_by_file: RawPatternsByFile = raw_cfg['file_patterns'] - for filepath_glob, patterns in list(file_patterns.items()): - filepaths = glob.glob(filepath_glob) - if not filepaths: - logger.warning(f"Invalid config, no such file: {filepath_glob}") - # fallback to treating it as a simple path - filepaths = [filepath_glob] + for filepath, raw_patterns in _iter_glob_expanded_file_patterns(raw_patterns_by_file): + compiled_patterns = [ + v1patterns.compile_pattern(version_pattern, raw_pattern) for raw_pattern in raw_patterns + ] + yield filepath, compiled_patterns - normalized_patterns: typ.List[str] = [] - for pattern in patterns: - normalized_pattern = pattern.replace("{version}", version_pattern) - if version_pattern == "{pycalver}": - normalized_pattern = normalized_pattern.replace( - "{pep440_version}", "{pep440_pycalver}" - ) - elif version_pattern == "{semver}": - normalized_pattern = normalized_pattern.replace("{pep440_version}", "{semver}") - elif "{pep440_version}" in pattern: - logger.warning(f"Invalid config, cannot match '{pattern}' for '{filepath_glob}'.") - logger.warning(f"No mapping of '{version_pattern}' to '{pep440_version}'") - normalized_patterns.append(normalized_pattern) - for filepath in filepaths: - file_patterns[filepath] = normalized_patterns +def _compile_v2_file_patterns(raw_cfg: RawConfig) -> typ.Iterable[FilePatternsItem]: + """Create inernal/compiled representation of the file_patterns config field. - return file_patterns + The result the same, regardless of the config format. + """ + version_pattern : str = raw_cfg['version_pattern'] + raw_patterns_by_file: RawPatternsByFile = raw_cfg['file_patterns'] + + for filepath, raw_patterns in _iter_glob_expanded_file_patterns(raw_patterns_by_file): + compiled_patterns = [ + v2patterns.compile_pattern(version_pattern, raw_pattern) for raw_pattern in raw_patterns + ] + yield filepath, compiled_patterns def _parse_config(raw_cfg: RawConfig) -> Config: """Parse configuration which was loaded from an .ini/.cfg or .toml file.""" - if 'current_version' not in raw_cfg: - raise ValueError("Missing 'pycalver.current_version'") + current_version: str = raw_cfg['current_version'] + current_version = raw_cfg['current_version'] = current_version.strip("'\" ") - version_str: str = raw_cfg['current_version'] - version_str = raw_cfg['current_version'] = version_str.strip("'\" ") - - version_pattern: str = raw_cfg.get('version_pattern', "{pycalver}") + version_pattern: str = raw_cfg['version_pattern'] version_pattern = raw_cfg['version_pattern'] = version_pattern.strip("'\" ") commit_message: str = raw_cfg.get('commit_message', DEFAULT_COMMIT_MESSAGE) commit_message = raw_cfg['commit_message'] = commit_message.strip("'\" ") - is_new_pattern = not ("{" in version_pattern or "}" in version_pattern) + is_new_pattern = "{" not in version_pattern and "}" not in version_pattern # TODO (mb 2020-09-18): Validate Pattern # detect YY with WW or UU -> suggest GG with VV @@ -260,11 +269,11 @@ def _parse_config(raw_cfg: RawConfig) -> Config: # NOTE (mb 2019-01-05): Provoke ValueError if version_pattern # and current_version are not compatible. if is_new_pattern: - v2version.parse_version_info(version_str, version_pattern) + v2version.parse_version_info(current_version, version_pattern) else: - v1version.parse_version_info(version_str, version_pattern) + v1version.parse_version_info(current_version, version_pattern) - pep440_version = v1version.to_pep440(version_str) + pep440_version = version.to_pep440(current_version) commit = raw_cfg['commit'] tag = raw_cfg['tag'] @@ -281,10 +290,13 @@ def _parse_config(raw_cfg: RawConfig) -> Config: if push and not commit: raise ValueError("pycalver.commit = true required if pycalver.push = true") - file_patterns = _normalize_file_patterns(raw_cfg) + if is_new_pattern: + file_patterns = dict(_compile_v2_file_patterns(raw_cfg)) + else: + file_patterns = dict(_compile_v1_file_patterns(raw_cfg)) cfg = Config( - current_version=version_str, + current_version=current_version, version_pattern=version_pattern, pep440_version=pep440_version, commit_message=commit_message, @@ -298,11 +310,18 @@ def _parse_config(raw_cfg: RawConfig) -> Config: return cfg -def _parse_current_version_default_pattern(cfg: Config, raw_cfg_text: str) -> str: +def _parse_current_version_default_pattern(ctx: ProjectContext, raw_cfg: RawConfig) -> str: + fobj: typ.IO[str] + + with ctx.config_filepath.open(mode="rt", encoding="utf-8") as fobj: + raw_cfg_text = fobj.read() + is_pycalver_section = False for line in raw_cfg_text.splitlines(): if is_pycalver_section and line.startswith("current_version"): - return line.replace(cfg.current_version, cfg.version_pattern) + current_version: str = raw_cfg['current_version'] + version_pattern: str = raw_cfg['version_pattern'] + return line.replace(current_version, version_pattern) if line.strip() == "[pycalver]": is_pycalver_section = True @@ -312,44 +331,56 @@ def _parse_current_version_default_pattern(cfg: Config, raw_cfg_text: str) -> st raise ValueError("Could not parse pycalver.current_version") +def _parse_raw_config(ctx: ProjectContext) -> RawConfig: + with ctx.config_filepath.open(mode="rt", encoding="utf-8") as fobj: + if ctx.config_format == 'toml': + raw_cfg = _parse_toml(fobj) + elif ctx.config_format == 'cfg': + raw_cfg = _parse_cfg(fobj) + else: + err_msg = ( + f"Invalid config_format='{ctx.config_format}'." + "Supported formats are 'setup.cfg' and 'pyproject.toml'" + ) + raise RuntimeError(err_msg) + + if 'current_version' in raw_cfg: + if not isinstance(raw_cfg['current_version'], str): + err = f"Invalid type for pycalver.current_version = {raw_cfg['current_version']}" + raise TypeError(err) + else: + raise ValueError("Missing 'pycalver.current_version'") + + if 'version_pattern' in raw_cfg: + if not isinstance(raw_cfg['version_pattern'], str): + err = f"Invalid type for pycalver.version_pattern = {raw_cfg['version_pattern']}" + raise TypeError(err) + else: + raw_cfg['version_pattern'] = "{pycalver}" + + if 'file_patterns' not in raw_cfg: + raw_cfg['file_patterns'] = {} + + if ctx.config_rel_path not in raw_cfg['file_patterns']: + # NOTE (mb 2020-09-19): By default we always add + # a pattern for the config section itself. + raw_version_pattern = _parse_current_version_default_pattern(ctx, raw_cfg) + raw_cfg['file_patterns'][ctx.config_rel_path] = [raw_version_pattern] + + return raw_cfg + + def parse(ctx: ProjectContext) -> MaybeConfig: """Parse config file if available.""" if not ctx.config_filepath.exists(): - logger.warning(f"File not found: {ctx.config_filepath}") + logger.warning(f"File not found: {ctx.config_rel_path}") return None - fobj: typ.IO[str] - - cfg_path: str - if ctx.config_filepath.is_absolute(): - cfg_path = str(ctx.config_filepath.relative_to(ctx.path.absolute())) - else: - cfg_path = str(ctx.config_filepath) - - raw_cfg: RawConfig - try: - with ctx.config_filepath.open(mode="rt", encoding="utf-8") as fobj: - if ctx.config_format == 'toml': - raw_cfg = _parse_toml(fobj) - elif ctx.config_format == 'cfg': - raw_cfg = _parse_cfg(fobj) - else: - err_msg = "Invalid config_format='{ctx.config_format}'" - raise RuntimeError(err_msg) - - cfg: Config = _parse_config(raw_cfg) - - if cfg_path not in cfg.file_patterns: - fobj.seek(0) - raw_cfg_text = fobj.read() - cfg.file_patterns[cfg_path] = [ - _parse_current_version_default_pattern(cfg, raw_cfg_text) - ] - - return cfg - except ValueError as ex: - logger.warning(f"Couldn't parse {cfg_path}: {str(ex)}") + raw_cfg = _parse_raw_config(ctx) + return _parse_config(raw_cfg) + except (TypeError, ValueError) as ex: + logger.warning(f"Couldn't parse {ctx.config_rel_path}: {str(ex)}") return None @@ -445,11 +476,11 @@ DEFAULT_TOML_README_MD_STR = """ def _initial_version() -> str: - return dt.datetime.now().strftime("v%Y%m.0001-alpha") + return dt.datetime.now().strftime("v%Y%m.1001-alpha") def _initial_version_pep440() -> str: - return dt.datetime.now().strftime("%Y%m.1a0") + return dt.datetime.now().strftime("%Y%m.1001a0") def default_config(ctx: ProjectContext) -> str: @@ -506,4 +537,4 @@ def write_content(ctx: ProjectContext) -> None: with ctx.config_filepath.open(mode="at", encoding="utf-8") as fobj: fobj.write(cfg_content) - print(f"Updated {ctx.config_filepath}") + print(f"Updated {ctx.config_rel_path}") diff --git a/src/pycalver/parse.py b/src/pycalver/parse.py index 427d30c..0b80519 100644 --- a/src/pycalver/parse.py +++ b/src/pycalver/parse.py @@ -7,7 +7,7 @@ import typing as typ -import pycalver.patterns as v1patterns +from .patterns import Pattern class PatternMatch(typ.NamedTuple): @@ -15,7 +15,7 @@ class PatternMatch(typ.NamedTuple): lineno : int # zero based line : str - pattern: v1patterns.Pattern + pattern: Pattern span : typ.Tuple[int, int] match : str @@ -23,25 +23,26 @@ class PatternMatch(typ.NamedTuple): PatternMatches = typ.Iterable[PatternMatch] -def _iter_for_pattern(lines: typ.List[str], pattern: v1patterns.Pattern) -> PatternMatches: +def _iter_for_pattern(lines: typ.List[str], pattern: Pattern) -> PatternMatches: for lineno, line in enumerate(lines): match = pattern.regexp.search(line) if match: yield PatternMatch(lineno, line, pattern, match.span(), match.group(0)) -def iter_matches(lines: typ.List[str], patterns: typ.List[v1patterns.Pattern]) -> PatternMatches: +def iter_matches(lines: typ.List[str], patterns: typ.List[Pattern]) -> PatternMatches: """Iterate over all matches of any pattern on any line. - >>> import pycalver.patterns as v1patterns + >>> from . import v1patterns >>> lines = ["__version__ = 'v201712.0002-alpha'"] - >>> patterns = ["{pycalver}", "{pep440_pycalver}"] - >>> patterns = [v1patterns.compile_pattern(p) for p in patterns] + >>> version_pattern = "{pycalver}" + >>> raw_patterns = ["{pycalver}", "{pep440_pycalver}"] + >>> patterns = [v1patterns.compile_pattern(version_pattern, p) for p in raw_patterns] >>> matches = list(iter_matches(lines, patterns)) >>> assert matches[0] == PatternMatch( ... lineno = 0, ... line = "__version__ = 'v201712.0002-alpha'", - ... pattern= v1patterns.compile_pattern("{pycalver}"), + ... pattern= v1patterns.compile_pattern(version_pattern), ... span = (15, 33), ... match = "v201712.0002-alpha", ... ) diff --git a/src/pycalver/patterns.py b/src/pycalver/patterns.py index a09bb01..b5ad928 100644 --- a/src/pycalver/patterns.py +++ b/src/pycalver/patterns.py @@ -1,62 +1,14 @@ -# This file is part of the pycalver project -# https://github.com/mbarkhau/pycalver -# -# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License -# SPDX-License-Identifier: MIT -"""Compose Regular Expressions from Patterns. - ->>> version_info = PYCALVER_RE.match("v201712.0123-alpha").groupdict() ->>> assert version_info == { -... "pycalver" : "v201712.0123-alpha", -... "vYYYYMM" : "v201712", -... "year" : "2017", -... "month" : "12", -... "build" : ".0123", -... "build_no" : "0123", -... "release" : "-alpha", -... "release_tag" : "alpha", -... } ->>> ->>> version_info = PYCALVER_RE.match("v201712.0033").groupdict() ->>> assert version_info == { -... "pycalver" : "v201712.0033", -... "vYYYYMM" : "v201712", -... "year" : "2017", -... "month" : "12", -... "build" : ".0033", -... "build_no" : "0033", -... "release" : None, -... "release_tag": None, -... } -""" - -import re import typing as typ -# https://regex101.com/r/fnj60p/10 -PYCALVER_PATTERN = r""" -\b -(?P - (?P - v # "v" version prefix - (?P\d{4}) - (?P\d{2}) - ) - (?P - \. # "." build nr prefix - (?P\d{4,}) - ) - (?P - \- # "-" release prefix - (?Palpha|beta|dev|rc|post) - )? -)(?:\s|$) -""" -PYCALVER_RE: typ.Pattern[str] = re.compile(PYCALVER_PATTERN, flags=re.VERBOSE) +class Pattern(typ.NamedTuple): + + version_pattern: str # "{pycalver}", "{year}.{month}", "vYYYY0M.BUILD" + raw_pattern : str # '__version__ = "{version}"', "Copyright (c) YYYY" + regexp : typ.Pattern[str] -PATTERN_ESCAPES = [ +RE_PATTERN_ESCAPES = [ ("\u005c", "\u005c\u005c"), ("-" , "\u005c-"), ("." , "\u005c."), @@ -70,158 +22,3 @@ PATTERN_ESCAPES = [ ("(" , "\u005c("), (")" , "\u005c)"), ] - -COMPOSITE_PART_PATTERNS = { - 'pep440_pycalver': r"{year}{month}\.{BID}(?:{pep440_tag})?", - 'pycalver' : r"v{year}{month}\.{bid}(?:-{tag})?", - 'calver' : r"v{year}{month}", - 'semver' : r"{MAJOR}\.{MINOR}\.{PATCH}", - 'release_tag' : r"{tag}", - 'build' : r"\.{bid}", - 'release' : r"(?:-{tag})?", - # depricated - 'pep440_version': r"{year}{month}\.{BID}(?:{pep440_tag})?", -} - - -PART_PATTERNS = { - 'year' : r"\d{4}", - 'month' : r"(?:0[0-9]|1[0-2])", - 'month_short': r"(?:1[0-2]|[1-9])", - 'build_no' : r"\d{4,}", - 'pep440_tag' : r"(?:a|b|dev|rc|post)?\d*", - 'tag' : r"(?:alpha|beta|dev|rc|post|final)", - 'yy' : r"\d{2}", - 'yyyy' : r"\d{4}", - 'quarter' : r"[1-4]", - 'iso_week' : r"(?:[0-4]\d|5[0-3])", - 'us_week' : r"(?:[0-4]\d|5[0-3])", - 'dom' : r"(0[1-9]|[1-2][0-9]|3[0-1])", - 'dom_short' : r"([1-9]|[1-2][0-9]|3[0-1])", - 'doy' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])", - 'doy_short' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])", - 'MAJOR' : r"\d+", - 'MINOR' : r"\d+", - 'MM' : r"\d{2,}", - 'MMM' : r"\d{3,}", - 'MMMM' : r"\d{4,}", - 'MMMMM' : r"\d{5,}", - 'PATCH' : r"\d+", - 'PP' : r"\d{2,}", - 'PPP' : r"\d{3,}", - 'PPPP' : r"\d{4,}", - 'PPPPP' : r"\d{5,}", - 'bid' : r"\d{4,}", - 'BID' : r"[1-9]\d*", - 'BB' : r"[1-9]\d{1,}", - 'BBB' : r"[1-9]\d{2,}", - 'BBBB' : r"[1-9]\d{3,}", - 'BBBBB' : r"[1-9]\d{4,}", - 'BBBBBB' : r"[1-9]\d{5,}", - 'BBBBBBB' : r"[1-9]\d{6,}", -} - - -PATTERN_PART_FIELDS = { - 'year' : 'year', - 'month' : 'month', - 'month_short': 'month', - 'pep440_tag' : 'tag', - 'tag' : 'tag', - 'yy' : 'year', - 'yyyy' : 'year', - 'quarter' : 'quarter', - 'iso_week' : 'iso_week', - 'us_week' : 'us_week', - 'dom' : 'dom', - 'doy' : 'doy', - 'dom_short' : 'dom', - 'doy_short' : 'doy', - 'MAJOR' : 'major', - 'MINOR' : 'minor', - 'MM' : 'minor', - 'MMM' : 'minor', - 'MMMM' : 'minor', - 'MMMMM' : 'minor', - 'PP' : 'patch', - 'PPP' : 'patch', - 'PPPP' : 'patch', - 'PPPPP' : 'patch', - 'PATCH' : 'patch', - 'build_no' : 'bid', - 'bid' : 'bid', - 'BID' : 'bid', - 'BB' : 'bid', - 'BBB' : 'bid', - 'BBBB' : 'bid', - 'BBBBB' : 'bid', - 'BBBBBB' : 'bid', - 'BBBBBBB' : 'bid', -} - - -FULL_PART_FORMATS = { - 'pep440_pycalver': "{year}{month:02}.{BID}{pep440_tag}", - 'pycalver' : "v{year}{month:02}.{bid}{release}", - 'calver' : "v{year}{month:02}", - 'semver' : "{MAJOR}.{MINOR}.{PATCH}", - 'release_tag' : "{tag}", - 'build' : ".{bid}", - # NOTE (mb 2019-01-04): since release is optional, it - # is treates specially in version.format - # 'release' : "-{tag}", - 'month' : "{month:02}", - 'month_short': "{month}", - 'build_no' : "{bid}", - 'iso_week' : "{iso_week:02}", - 'us_week' : "{us_week:02}", - 'dom' : "{dom:02}", - 'doy' : "{doy:03}", - 'dom_short' : "{dom}", - 'doy_short' : "{doy}", - # depricated - 'pep440_version': "{year}{month:02}.{BID}{pep440_tag}", - 'version' : "v{year}{month:02}.{bid}{release}", -} - - -class Pattern(typ.NamedTuple): - - raw : str # "{pycalver}", "{year}.{month}", "YYYY0M.BUILD" - regexp: typ.Pattern[str] - - -Patterns = typ.List[typ.Pattern[str]] - - -def _replace_pattern_parts(pattern: str) -> str: - # The pattern is escaped, so that everything besides the format - # string variables is treated literally. - for part_name, part_pattern in PART_PATTERNS.items(): - named_part_pattern = f"(?P<{part_name}>{part_pattern})" - placeholder = "\u005c{" + part_name + "\u005c}" - pattern = pattern.replace(placeholder, named_part_pattern) - return pattern - - -def compile_pattern_str(pattern: str) -> str: - for char, escaped in PATTERN_ESCAPES: - pattern = pattern.replace(char, escaped) - - return _replace_pattern_parts(pattern) - - -def compile_pattern(pattern: str) -> Pattern: - pattern_str = compile_pattern_str(pattern) - pattern_re = re.compile(pattern_str) - return Pattern(pattern, pattern_re) - - -def _init_composite_patterns() -> None: - for part_name, part_pattern in COMPOSITE_PART_PATTERNS.items(): - part_pattern = part_pattern.replace("{", "\u005c{").replace("}", "\u005c}") - pattern_str = _replace_pattern_parts(part_pattern) - PART_PATTERNS[part_name] = pattern_str - - -_init_composite_patterns() diff --git a/src/pycalver/rewrite.py b/src/pycalver/rewrite.py index f63dc5a..cbd1a1a 100644 --- a/src/pycalver/rewrite.py +++ b/src/pycalver/rewrite.py @@ -1,24 +1,22 @@ -# This file is part of the pycalver project -# https://github.com/mbarkhau/pycalver -# -# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License -# SPDX-License-Identifier: MIT -"""Rewrite files, updating occurences of version strings.""" - -import io -import glob import typing as typ import difflib -import logging import pathlib2 as pl -import pycalver.version as v1version -import pycalver.patterns as v1patterns -from pycalver import parse -from pycalver import config +from . import config +from .patterns import Pattern -logger = logging.getLogger("pycalver.rewrite") + +class NoPatternMatch(Exception): + """Pattern not found in content. + + logger.error is used to show error info about the patterns so + that users can debug what is wrong with them. The class + itself doesn't capture that info. This approach is used so + that all patter issues can be shown, rather than bubbling + all the way up the stack on the very first pattern with no + matches. + """ def detect_line_sep(content: str) -> str: @@ -41,18 +39,6 @@ def detect_line_sep(content: str) -> str: return "\n" -class NoPatternMatch(Exception): - """Pattern not found in content. - - logger.error is used to show error info about the patterns so - that users can debug what is wrong with them. The class - itself doesn't capture that info. This approach is used so - that all patter issues can be shown, rather than bubbling - all the way up the stack on the very first pattern with no - matches. - """ - - class RewrittenFileData(typ.NamedTuple): """Container for line-wise content of rewritten files.""" @@ -62,117 +48,19 @@ class RewrittenFileData(typ.NamedTuple): new_lines: typ.List[str] -def iter_file_paths( - file_patterns: config.PatternsByGlob, -) -> typ.Iterable[typ.Tuple[pl.Path, config.Patterns]]: - for globstr, pattern_strs in file_patterns.items(): - file_paths = glob.glob(globstr) - if not any(file_paths): - errmsg = f"No files found for path/glob '{globstr}'" +PathPatternsItem = typ.Tuple[pl.Path, typ.List[Pattern]] + + +def iter_path_patterns_items( + file_patterns: config.PatternsByFile, +) -> typ.Iterable[PathPatternsItem]: + for filepath_str, patterns in file_patterns.items(): + filepath_obj = pl.Path(filepath_str) + if filepath_obj.exists(): + yield (filepath_obj, patterns) + else: + errmsg = f"File does not exist: '{filepath_str}'" raise IOError(errmsg) - for file_path_str in file_paths: - file_path = pl.Path(file_path_str) - yield (file_path, pattern_strs) - - -def rewrite_lines( - pattern_strs: typ.List[str], - new_vinfo : v1version.VersionInfo, - old_lines : typ.List[str], -) -> typ.List[str]: - """Replace occurances of pattern_strs in old_lines with new_vinfo. - - >>> new_vinfo = v1version.parse_version_info("v201811.0123-beta") - >>> pattern_strs = ['__version__ = "{pycalver}"'] - >>> rewrite_lines(pattern_strs, new_vinfo, ['__version__ = "v201809.0002-beta"']) - ['__version__ = "v201811.0123-beta"'] - - >>> pattern_strs = ['__version__ = "{pep440_version}"'] - >>> rewrite_lines(pattern_strs, new_vinfo, ['__version__ = "201809.2b0"']) - ['__version__ = "201811.123b0"'] - """ - new_lines = old_lines[:] - found_patterns = set() - - patterns = [v1patterns.compile_pattern(p) for p in pattern_strs] - matches = parse.iter_matches(old_lines, patterns) - for match in matches: - found_patterns.add(match.pattern.raw) - replacement = v1version.format_version(new_vinfo, match.pattern.raw) - span_l, span_r = match.span - new_line = match.line[:span_l] + replacement + match.line[span_r:] - new_lines[match.lineno] = new_line - - non_matched_patterns = set(pattern_strs) - found_patterns - if non_matched_patterns: - for non_matched_pattern in non_matched_patterns: - logger.error(f"No match for pattern '{non_matched_pattern}'") - compiled_pattern_str = v1patterns.compile_pattern_str(non_matched_pattern) - logger.error(f"Pattern compiles to regex '{compiled_pattern_str}'") - raise NoPatternMatch("Invalid pattern(s)") - else: - return new_lines - - -def rfd_from_content( - pattern_strs: typ.List[str], - new_vinfo : v1version.VersionInfo, - content : str, -) -> RewrittenFileData: - r"""Rewrite pattern occurrences with version string. - - >>> new_vinfo = v1version.parse_version_info("v201809.0123") - >>> pattern_strs = ['__version__ = "{pycalver}"'] - >>> content = '__version__ = "v201809.0001-alpha"' - >>> rfd = rfd_from_content(pattern_strs, new_vinfo, content) - >>> rfd.new_lines - ['__version__ = "v201809.0123"'] - >>> - >>> new_vinfo = v1version.parse_version_info("v1.2.3", "v{semver}") - >>> pattern_strs = ['__version__ = "v{semver}"'] - >>> content = '__version__ = "v1.2.2"' - >>> rfd = rfd_from_content(pattern_strs, new_vinfo, content) - >>> rfd.new_lines - ['__version__ = "v1.2.3"'] - """ - line_sep = detect_line_sep(content) - old_lines = content.split(line_sep) - new_lines = rewrite_lines(pattern_strs, new_vinfo, old_lines) - return RewrittenFileData("", line_sep, old_lines, new_lines) - - -def iter_rewritten( - file_patterns: config.PatternsByGlob, - new_vinfo : v1version.VersionInfo, -) -> typ.Iterable[RewrittenFileData]: - r'''Iterate over files with version string replaced. - - >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']} - >>> new_vinfo = v1version.parse_version_info("v201809.0123") - >>> rewritten_datas = iter_rewritten(file_patterns, new_vinfo) - >>> rfd = list(rewritten_datas)[0] - >>> expected = [ - ... '# This file is part of the pycalver project', - ... '# https://github.com/mbarkhau/pycalver', - ... '#', - ... '# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License', - ... '# SPDX-License-Identifier: MIT', - ... '"""PyCalVer: CalVer for Python Packages."""', - ... '', - ... '__version__ = "v201809.0123"', - ... '', - ... ] - >>> assert rfd.new_lines[:len(expected)] == expected - ''' - - fobj: typ.IO[str] - - for file_path, pattern_strs in iter_file_paths(file_patterns): - with file_path.open(mode="rt", encoding="utf-8") as fobj: - content = fobj.read() - - rfd = rfd_from_content(pattern_strs, new_vinfo, content) - yield rfd._replace(path=str(file_path)) def diff_lines(rfd: RewrittenFileData) -> typ.List[str]: @@ -188,57 +76,10 @@ def diff_lines(rfd: RewrittenFileData) -> typ.List[str]: ['--- ', '+++ ', '@@ -1 +1 @@', '-foo', '+bar'] """ lines = difflib.unified_diff( - a=rfd.old_lines, b=rfd.new_lines, lineterm="", fromfile=rfd.path, tofile=rfd.path + a=rfd.old_lines, + b=rfd.new_lines, + lineterm="", + fromfile=rfd.path, + tofile=rfd.path, ) return list(lines) - - -def diff(new_vinfo: v1version.VersionInfo, file_patterns: config.PatternsByGlob) -> str: - r"""Generate diffs of rewritten files. - - >>> new_vinfo = v1version.parse_version_info("v201809.0123") - >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']} - >>> diff_str = diff(new_vinfo, file_patterns) - >>> lines = diff_str.split("\n") - >>> lines[:2] - ['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py'] - >>> assert lines[6].startswith('-__version__ = "v2') - >>> assert not lines[6].startswith('-__version__ = "v201809.0123"') - >>> lines[7] - '+__version__ = "v201809.0123"' - """ - - full_diff = "" - fobj: typ.IO[str] - - for file_path, pattern_strs in sorted(iter_file_paths(file_patterns)): - with file_path.open(mode="rt", encoding="utf-8") as fobj: - content = fobj.read() - - try: - rfd = rfd_from_content(pattern_strs, new_vinfo, content) - except NoPatternMatch: - # pylint:disable=raise-missing-from ; we support py2, so not an option - errmsg = f"No patterns matched for '{file_path}'" - raise NoPatternMatch(errmsg) - - rfd = rfd._replace(path=str(file_path)) - lines = diff_lines(rfd) - if len(lines) == 0: - errmsg = f"No patterns matched for '{file_path}'" - raise NoPatternMatch(errmsg) - - full_diff += "\n".join(lines) + "\n" - - full_diff = full_diff.rstrip("\n") - return full_diff - - -def rewrite(file_patterns: config.PatternsByGlob, new_vinfo: v1version.VersionInfo) -> None: - """Rewrite project files, updating each with the new version.""" - fobj: typ.IO[str] - - for file_data in iter_rewritten(file_patterns, new_vinfo): - new_content = file_data.line_sep.join(file_data.new_lines) - with io.open(file_data.path, mode="wt", encoding="utf-8") as fobj: - fobj.write(new_content) diff --git a/src/pycalver/cli.py b/src/pycalver/v1cli.py similarity index 71% rename from src/pycalver/cli.py rename to src/pycalver/v1cli.py index da4cea7..d33ba12 100755 --- a/src/pycalver/cli.py +++ b/src/pycalver/v1cli.py @@ -12,11 +12,12 @@ Provided subcommands: show, test, init, bump import typing as typ import logging -import pycalver.rewrite as v1rewrite -import pycalver.version as v1version -from pycalver import config +from . import config +from . import version +from . import v1rewrite +from . import v1version -logger = logging.getLogger("pycalver.cli") +logger = logging.getLogger("pycalver.v1cli") def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.Config: @@ -28,7 +29,7 @@ def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.C version_tags.sort(reverse=True) logger.debug(f"found {len(version_tags)} tags: {version_tags[:2]}") latest_version_tag = version_tags[0] - latest_version_pep440 = v1version.to_pep440(latest_version_tag) + latest_version_pep440 = version.to_pep440(latest_version_tag) if latest_version_tag <= cfg.current_version: return cfg @@ -40,14 +41,15 @@ def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.C ) -def rewrite( +def rewrite_files( cfg : config.Config, new_version: str, ) -> None: new_vinfo = v1version.parse_version_info(new_version, cfg.version_pattern) - v1rewrite.rewrite(cfg.file_patterns, new_vinfo) + v1rewrite.rewrite_files(cfg.file_patterns, new_vinfo) def get_diff(cfg: config.Config, new_version: str) -> str: - new_vinfo = v1version.parse_version_info(new_version, cfg.version_pattern) - return v1rewrite.diff(new_vinfo, cfg.file_patterns) + old_vinfo = v1version.parse_version_info(cfg.current_version, cfg.version_pattern) + new_vinfo = v1version.parse_version_info(new_version , cfg.version_pattern) + return v1rewrite.diff(old_vinfo, new_vinfo, cfg.file_patterns) diff --git a/src/pycalver/v1patterns.py b/src/pycalver/v1patterns.py new file mode 100644 index 0000000..2171ac3 --- /dev/null +++ b/src/pycalver/v1patterns.py @@ -0,0 +1,220 @@ +# This file is part of the pycalver project +# https://github.com/mbarkhau/pycalver +# +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# SPDX-License-Identifier: MIT +"""Compose Regular Expressions from Patterns. + +>>> version_info = PYCALVER_RE.match("v201712.0123-alpha").groupdict() +>>> assert version_info == { +... "pycalver" : "v201712.0123-alpha", +... "vYYYYMM" : "v201712", +... "year" : "2017", +... "month" : "12", +... "build" : ".0123", +... "build_no" : "0123", +... "release" : "-alpha", +... "release_tag" : "alpha", +... } +>>> +>>> version_info = PYCALVER_RE.match("v201712.0033").groupdict() +>>> assert version_info == { +... "pycalver" : "v201712.0033", +... "vYYYYMM" : "v201712", +... "year" : "2017", +... "month" : "12", +... "build" : ".0033", +... "build_no" : "0033", +... "release" : None, +... "release_tag": None, +... } +""" + +import re +import typing as typ +import logging + +from .patterns import RE_PATTERN_ESCAPES +from .patterns import Pattern + +logger = logging.getLogger("pycalver.v1patterns") + +# https://regex101.com/r/fnj60p/10 +PYCALVER_PATTERN = r""" +\b +(?P + (?P + v # "v" version prefix + (?P\d{4}) + (?P\d{2}) + ) + (?P + \. # "." build nr prefix + (?P\d{4,}) + ) + (?P + \- # "-" release prefix + (?Palpha|beta|dev|rc|post) + )? +)(?:\s|$) +""" + +PYCALVER_RE: typ.Pattern[str] = re.compile(PYCALVER_PATTERN, flags=re.VERBOSE) + + +COMPOSITE_PART_PATTERNS = { + 'pep440_pycalver': r"{year}{month}\.{BID}(?:{pep440_tag})?", + 'pycalver' : r"v{year}{month}\.{bid}(?:-{tag})?", + 'calver' : r"v{year}{month}", + 'semver' : r"{MAJOR}\.{MINOR}\.{PATCH}", + 'release_tag' : r"{tag}", + 'build' : r"\.{bid}", + 'release' : r"(?:-{tag})?", + # depricated + 'pep440_version': r"{year}{month}\.{BID}(?:{pep440_tag})?", +} + + +PART_PATTERNS = { + 'year' : r"\d{4}", + 'month' : r"(?:0[0-9]|1[0-2])", + 'month_short': r"(?:1[0-2]|[1-9])", + 'build_no' : r"\d{4,}", + 'pep440_tag' : r"(?:a|b|dev|rc|post)?\d*", + 'tag' : r"(?:alpha|beta|dev|rc|post|final)", + 'yy' : r"\d{2}", + 'yyyy' : r"\d{4}", + 'quarter' : r"[1-4]", + 'iso_week' : r"(?:[0-4]\d|5[0-3])", + 'us_week' : r"(?:[0-4]\d|5[0-3])", + 'dom' : r"(0[1-9]|[1-2][0-9]|3[0-1])", + 'dom_short' : r"([1-9]|[1-2][0-9]|3[0-1])", + 'doy' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])", + 'doy_short' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])", + 'MAJOR' : r"\d+", + 'MINOR' : r"\d+", + 'MM' : r"\d{2,}", + 'MMM' : r"\d{3,}", + 'MMMM' : r"\d{4,}", + 'MMMMM' : r"\d{5,}", + 'PATCH' : r"\d+", + 'PP' : r"\d{2,}", + 'PPP' : r"\d{3,}", + 'PPPP' : r"\d{4,}", + 'PPPPP' : r"\d{5,}", + 'bid' : r"\d{4,}", + 'BID' : r"[1-9]\d*", + 'BB' : r"[1-9]\d{1,}", + 'BBB' : r"[1-9]\d{2,}", + 'BBBB' : r"[1-9]\d{3,}", + 'BBBBB' : r"[1-9]\d{4,}", + 'BBBBBB' : r"[1-9]\d{5,}", + 'BBBBBBB' : r"[1-9]\d{6,}", +} + + +PATTERN_PART_FIELDS = { + 'year' : 'year', + 'month' : 'month', + 'month_short': 'month', + 'pep440_tag' : 'tag', + 'tag' : 'tag', + 'yy' : 'year', + 'yyyy' : 'year', + 'quarter' : 'quarter', + 'iso_week' : 'iso_week', + 'us_week' : 'us_week', + 'dom' : 'dom', + 'doy' : 'doy', + 'dom_short' : 'dom', + 'doy_short' : 'doy', + 'MAJOR' : 'major', + 'MINOR' : 'minor', + 'MM' : 'minor', + 'MMM' : 'minor', + 'MMMM' : 'minor', + 'MMMMM' : 'minor', + 'PP' : 'patch', + 'PPP' : 'patch', + 'PPPP' : 'patch', + 'PPPPP' : 'patch', + 'PATCH' : 'patch', + 'build_no' : 'bid', + 'bid' : 'bid', + 'BID' : 'bid', + 'BB' : 'bid', + 'BBB' : 'bid', + 'BBBB' : 'bid', + 'BBBBB' : 'bid', + 'BBBBBB' : 'bid', + 'BBBBBBB' : 'bid', +} + + +FULL_PART_FORMATS = { + 'pep440_pycalver': "{year}{month:02}.{BID}{pep440_tag}", + 'pycalver' : "v{year}{month:02}.{bid}{release}", + 'calver' : "v{year}{month:02}", + 'semver' : "{MAJOR}.{MINOR}.{PATCH}", + 'release_tag' : "{tag}", + 'build' : ".{bid}", + # NOTE (mb 2019-01-04): since release is optional, it + # is treates specially in version.format + # 'release' : "-{tag}", + 'month' : "{month:02}", + 'month_short': "{month}", + 'build_no' : "{bid}", + 'iso_week' : "{iso_week:02}", + 'us_week' : "{us_week:02}", + 'dom' : "{dom:02}", + 'doy' : "{doy:03}", + 'dom_short' : "{dom}", + 'doy_short' : "{doy}", + # depricated + 'pep440_version': "{year}{month:02}.{BID}{pep440_tag}", + 'version' : "v{year}{month:02}.{bid}{release}", +} + + +def _replace_pattern_parts(pattern: str) -> str: + # The pattern is escaped, so that everything besides the format + # string variables is treated literally. + for part_name, part_pattern in PART_PATTERNS.items(): + named_part_pattern = f"(?P<{part_name}>{part_pattern})" + placeholder = "\u005c{" + part_name + "\u005c}" + pattern = pattern.replace(placeholder, named_part_pattern) + return pattern + + +def _init_composite_patterns() -> None: + for part_name, part_pattern in COMPOSITE_PART_PATTERNS.items(): + part_pattern = part_pattern.replace("{", "\u005c{").replace("}", "\u005c}") + pattern_str = _replace_pattern_parts(part_pattern) + PART_PATTERNS[part_name] = pattern_str + + +_init_composite_patterns() + + +def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[str]: + normalized_pattern = raw_pattern.replace(r"{version}", version_pattern) + if version_pattern == r"{pycalver}": + normalized_pattern = normalized_pattern.replace(r"{pep440_version}", r"{pep440_pycalver}") + elif version_pattern == r"{semver}": + normalized_pattern = normalized_pattern.replace(r"{pep440_version}", r"{semver}") + elif r"{pep440_version}" in raw_pattern: + logger.warning(f"No mapping of '{version_pattern}' to '{{pep440_version}}'") + + escaped_pattern = normalized_pattern + for char, escaped in RE_PATTERN_ESCAPES: + escaped_pattern = escaped_pattern.replace(char, escaped) + + # TODO (mb 2020-09-19): replace {version} etc with version_pattern + pattern_str = _replace_pattern_parts(escaped_pattern) + return re.compile(pattern_str) + + +def compile_pattern(version_pattern: str, raw_pattern: typ.Optional[str] = None) -> Pattern: + _raw_pattern = version_pattern if raw_pattern is None else raw_pattern + regexp = _compile_pattern_re(version_pattern, _raw_pattern) + return Pattern(version_pattern, _raw_pattern, regexp) diff --git a/src/pycalver/v1rewrite.py b/src/pycalver/v1rewrite.py new file mode 100644 index 0000000..2cdb487 --- /dev/null +++ b/src/pycalver/v1rewrite.py @@ -0,0 +1,193 @@ +# This file is part of the pycalver project +# https://github.com/mbarkhau/pycalver +# +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# SPDX-License-Identifier: MIT +"""Rewrite files, updating occurences of version strings.""" + +import io +import typing as typ +import logging + +from . import parse +from . import config +from . import rewrite +from . import version +from . import v1version +from .patterns import Pattern + +logger = logging.getLogger("pycalver.v1rewrite") + + +def rewrite_lines( + patterns : typ.List[Pattern], + new_vinfo: version.V1VersionInfo, + old_lines: typ.List[str], +) -> typ.List[str]: + """Replace occurances of patterns in old_lines with new_vinfo. + + >>> from .v1patterns import compile_pattern + >>> version_pattern = "{pycalver}" + >>> new_vinfo = v1version.parse_version_info("v201811.0123-beta", version_pattern) + >>> patterns = [compile_pattern(version_pattern, '__version__ = "{pycalver}"')] + >>> rewrite_lines(patterns, new_vinfo, ['__version__ = "v201809.0002-beta"']) + ['__version__ = "v201811.0123-beta"'] + + >>> patterns = [compile_pattern(version_pattern, '__version__ = "{pep440_version}"')] + >>> rewrite_lines(patterns, new_vinfo, ['__version__ = "201809.2b0"']) + ['__version__ = "201811.123b0"'] + """ + new_lines = old_lines[:] + found_patterns = set() + + for match in parse.iter_matches(old_lines, patterns): + found_patterns.add(match.pattern.raw_pattern) + replacement = v1version.format_version(new_vinfo, match.pattern.raw_pattern) + span_l, span_r = match.span + new_line = match.line[:span_l] + replacement + match.line[span_r:] + new_lines[match.lineno] = new_line + + non_matched_patterns = set(patterns) - found_patterns + if non_matched_patterns: + for nmp in non_matched_patterns: + logger.error(f"No match for pattern '{nmp.raw_pattern}'") + logger.error(f"Pattern compiles to regex '{nmp.regexp.pattern}'") + raise rewrite.NoPatternMatch("Invalid pattern(s)") + else: + return new_lines + + +def rfd_from_content( + patterns : typ.List[Pattern], + new_vinfo: version.V1VersionInfo, + content : str, + path : str = "", +) -> rewrite.RewrittenFileData: + r"""Rewrite pattern occurrences with version string. + + >>> from .v1patterns import compile_pattern + >>> patterns = [compile_pattern("{pycalver}", '__version__ = "{pycalver}"'] + >>> new_vinfo = v1version.parse_version_info("v201809.0123") + + >>> content = '__version__ = "v201809.0001-alpha"' + >>> rfd = rfd_from_content(patterns, new_vinfo, content) + >>> rfd.new_lines + ['__version__ = "v201809.0123"'] + + >>> patterns = [compile_pattern('{semver}', '__version__ = "v{semver}"')] + >>> new_vinfo = v1version.parse_version_info("v1.2.3", "v{semver}") + + >>> content = '__version__ = "v1.2.2"' + >>> rfd = rfd_from_content(patterns, new_vinfo, content) + >>> rfd.new_lines + ['__version__ = "v1.2.3"'] + """ + line_sep = rewrite.detect_line_sep(content) + old_lines = content.split(line_sep) + new_lines = rewrite_lines(patterns, new_vinfo, old_lines) + return rewrite.RewrittenFileData(path, line_sep, old_lines, new_lines) + + +def iter_rewritten( + file_patterns: config.PatternsByFile, + new_vinfo : version.V1VersionInfo, +) -> typ.Iterable[rewrite.RewrittenFileData]: + r'''Iterate over files with version string replaced. + + >>> version_pattern = "{pycalver}" + >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']} + >>> new_vinfo = v1version.parse_version_info("v201809.0123") + >>> rewritten_datas = iter_rewritten(version_pattern, file_patterns, new_vinfo) + >>> rfd = list(rewritten_datas)[0] + >>> expected = [ + ... '# This file is part of the pycalver project', + ... '# https://github.com/mbarkhau/pycalver', + ... '#', + ... '# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License', + ... '# SPDX-License-Identifier: MIT', + ... '"""PyCalVer: CalVer for Python Packages."""', + ... '', + ... '__version__ = "v201809.0123"', + ... '', + ... ] + >>> assert rfd.new_lines == expected + ''' + + fobj: typ.IO[str] + + for file_path, pattern_strs in rewrite.iter_path_patterns_items(file_patterns): + with file_path.open(mode="rt", encoding="utf-8") as fobj: + content = fobj.read() + + rfd = rfd_from_content(pattern_strs, new_vinfo, content) + yield rfd._replace(path=str(file_path)) + + +def diff( + old_vinfo : version.V1VersionInfo, + new_vinfo : version.V1VersionInfo, + file_patterns: config.PatternsByFile, +) -> str: + r"""Generate diffs of rewritten files. + + >>> old_vinfo = v1version.parse_version_info("v201809.0123") + >>> new_vinfo = v1version.parse_version_info("v201810.1124") + >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']} + >>> diff_str = diff(old_vinfo, new_vinfo, file_patterns) + >>> lines = diff_str.split("\n") + >>> lines[:2] + ['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py'] + >>> assert lines[6].startswith('-__version__ = "v2') + >>> assert not lines[6].startswith('-__version__ = "v201809.0123"') + >>> lines[7] + '+__version__ = "v201809.0123"' + + >>> file_patterns = {"LICENSE": ['Copyright (c) 2018-{year}']} + >>> diff_str = diff(old_vinfo, new_vinfo, file_patterns) + >>> assert not diff_str + """ + + full_diff = "" + fobj: typ.IO[str] + + for file_path, patterns in sorted(rewrite.iter_path_patterns_items(file_patterns)): + with file_path.open(mode="rt", encoding="utf-8") as fobj: + content = fobj.read() + + has_updated_version = False + for pattern in patterns: + old_str = v1version.format_version(old_vinfo, pattern.raw_pattern) + new_str = v1version.format_version(new_vinfo, pattern.raw_pattern) + if old_str != new_str: + has_updated_version = True + + try: + rfd = rfd_from_content(patterns, new_vinfo, content) + except rewrite.NoPatternMatch: + # pylint:disable=raise-missing-from ; we support py2, so not an option + errmsg = f"No patterns matched for '{file_path}'" + raise rewrite.NoPatternMatch(errmsg) + + rfd = rfd._replace(path=str(file_path)) + lines = rewrite.diff_lines(rfd) + if len(lines) == 0 and has_updated_version: + errmsg = f"No patterns matched for '{file_path}'" + raise rewrite.NoPatternMatch(errmsg) + + full_diff += "\n".join(lines) + "\n" + + full_diff = full_diff.rstrip("\n") + return full_diff + + +def rewrite_files( + file_patterns: config.PatternsByFile, + new_vinfo : version.V1VersionInfo, +) -> None: + """Rewrite project files, updating each with the new version.""" + fobj: typ.IO[str] + + for file_data in iter_rewritten(file_patterns, new_vinfo): + new_content = file_data.line_sep.join(file_data.new_lines) + with io.open(file_data.path, mode="wt", encoding="utf-8") as fobj: + fobj.write(new_content) diff --git a/src/pycalver/v1version.py b/src/pycalver/v1version.py new file mode 100644 index 0000000..d007cdd --- /dev/null +++ b/src/pycalver/v1version.py @@ -0,0 +1,407 @@ +# This file is part of the pycalver project +# https://github.com/mbarkhau/pycalver +# +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# SPDX-License-Identifier: MIT +"""Functions related to version string manipulation.""" + +import typing as typ +import logging +import datetime as dt + +from . import version +from . import v1patterns + +logger = logging.getLogger("pycalver.v1version") + + +CalInfo = typ.Union[version.V1CalendarInfo, version.V1VersionInfo] + + +def _is_later_than(old: CalInfo, new: CalInfo) -> bool: + """Is old > new based on non None fields.""" + for field in version.V1CalendarInfo._fields: + aval = getattr(old, field) + bval = getattr(new, field) + if not (aval is None or bval is None): + if aval > bval: + return True + return False + + +def _ver_to_cal_info(vnfo: version.V1VersionInfo) -> version.V1CalendarInfo: + return version.V1CalendarInfo( + vnfo.year, + vnfo.quarter, + vnfo.month, + vnfo.dom, + vnfo.doy, + vnfo.iso_week, + vnfo.us_week, + ) + + +def cal_info(date: dt.date = None) -> version.V1CalendarInfo: + """Generate calendar components for current date. + + >>> from datetime import date + + >>> c = cal_info(date(2019, 1, 5)) + >>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) + (2019, 1, 1, 5, 5, 0, 0) + + >>> c = cal_info(date(2019, 1, 6)) + >>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) + (2019, 1, 1, 6, 6, 0, 1) + + >>> c = cal_info(date(2019, 1, 7)) + >>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) + (2019, 1, 1, 7, 7, 1, 1) + + >>> c = cal_info(date(2019, 4, 7)) + >>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) + (2019, 2, 4, 7, 97, 13, 14) + """ + if date is None: + date = version.TODAY + + kwargs = { + 'year' : date.year, + 'quarter' : version.quarter_from_month(date.month), + 'month' : date.month, + 'dom' : date.day, + 'doy' : int(date.strftime("%j"), base=10), + 'iso_week': int(date.strftime("%W"), base=10), + 'us_week' : int(date.strftime("%U"), base=10), + } + + return version.V1CalendarInfo(**kwargs) + + +FieldKey = str +MatchGroupKey = str +MatchGroupStr = str + +PatternGroups = typ.Dict[MatchGroupKey, MatchGroupStr] +FieldValues = typ.Dict[FieldKey , MatchGroupStr] + + +def _parse_field_values(field_values: FieldValues) -> version.V1VersionInfo: + fvals = field_values + tag = fvals.get('tag') + if tag is None: + tag = "final" + tag = version.TAG_BY_PEP440_TAG.get(tag, tag) + assert tag is not None + + bid = fvals['bid'] if 'bid' in fvals else "0001" + + year = int(fvals['year']) if 'year' in fvals else None + doy = int(fvals['doy' ]) if 'doy' in fvals else None + + month: typ.Optional[int] + dom : typ.Optional[int] + + if year and doy: + date = version.date_from_doy(year, doy) + month = date.month + dom = date.day + else: + month = int(fvals['month']) if 'month' in fvals else None + dom = int(fvals['dom' ]) if 'dom' in fvals else None + + iso_week: typ.Optional[int] + us_week : typ.Optional[int] + + if year and month and dom: + date = dt.date(year, month, dom) + doy = int(date.strftime("%j"), base=10) + iso_week = int(date.strftime("%W"), base=10) + us_week = int(date.strftime("%U"), base=10) + else: + iso_week = None + us_week = None + + quarter = int(fvals['quarter']) if 'quarter' in fvals else None + if quarter is None and month: + quarter = version.quarter_from_month(month) + + major = int(fvals['major']) if 'major' in fvals else 0 + minor = int(fvals['minor']) if 'minor' in fvals else 0 + patch = int(fvals['patch']) if 'patch' in fvals else 0 + + return version.V1VersionInfo( + year=year, + quarter=quarter, + month=month, + dom=dom, + doy=doy, + iso_week=iso_week, + us_week=us_week, + major=major, + minor=minor, + patch=patch, + bid=bid, + tag=tag, + ) + + +def _is_calver(cinfo: CalInfo) -> bool: + """Check pattern for any calendar based parts. + + >>> _is_calver(cal_info()) + True + + >>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0018"}) + >>> _is_calver(vnfo) + True + + >>> vnfo = _parse_version_info({'MAJOR': "1", 'MINOR': "023", 'PATCH': "45"}) + >>> _is_calver(vnfo) + False + """ + for field in version.V1CalendarInfo._fields: + maybe_val: typ.Any = getattr(cinfo, field, None) + if isinstance(maybe_val, int): + return True + + return False + + +VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]] + + +def _parse_pattern_groups(pattern_groups: PatternGroups) -> FieldValues: + for part_name in pattern_groups.keys(): + is_valid_part_name = ( + part_name in v1patterns.COMPOSITE_PART_PATTERNS + or part_name in v1patterns.PATTERN_PART_FIELDS + ) + if not is_valid_part_name: + err_msg = f"Invalid part '{part_name}'" + raise version.PatternError(err_msg) + + field_value_items = [ + (field_name, pattern_groups[part_name]) + for part_name, field_name in v1patterns.PATTERN_PART_FIELDS.items() + if part_name in pattern_groups.keys() + ] + + all_fields = [field_name for field_name, _ in field_value_items] + unique_fields = set(all_fields) + duplicate_fields = [f for f in unique_fields if all_fields.count(f) > 1] + + if any(duplicate_fields): + err_msg = f"Multiple parts for same field {duplicate_fields}." + raise version.PatternError(err_msg) + else: + return dict(field_value_items) + + +def _parse_version_info(pattern_groups: PatternGroups) -> version.V1VersionInfo: + """Parse normalized V1VersionInfo from groups of a matched pattern. + + >>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0099"}) + >>> (vnfo.year, vnfo.month, vnfo.quarter, vnfo.bid, vnfo.tag) + (2018, 11, 4, '0099', 'final') + + >>> vnfo = _parse_version_info({'year': "2018", 'doy': "11", 'bid': "099", 'tag': "b"}) + >>> (vnfo.year, vnfo.month, vnfo.dom, vnfo.bid, vnfo.tag) + (2018, 1, 11, '099', 'beta') + + >>> vnfo = _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "45"}) + >>> (vnfo.major, vnfo.minor, vnfo.patch) + (1, 23, 45) + + >>> vnfo = _parse_version_info({'MAJOR': "1", 'MMM': "023", 'PPPP': "0045"}) + >>> (vnfo.major, vnfo.minor, vnfo.patch) + (1, 23, 45) + """ + field_values = _parse_pattern_groups(pattern_groups) + return _parse_field_values(field_values) + + +def parse_version_info(version_str: str, raw_pattern: str = "{pycalver}") -> version.V1VersionInfo: + """Parse normalized V1VersionInfo. + + >>> vnfo = parse_version_info("v201712.0033-beta", raw_pattern="{pycalver}") + >>> assert vnfo == _parse_version_info({'year': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"}) + + >>> vnfo = parse_version_info("1.23.456", raw_pattern="{semver}") + >>> assert vnfo == _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "456"}) + """ + pattern = v1patterns.compile_pattern(raw_pattern) + match = pattern.regexp.match(version_str) + if match is None: + err_msg = ( + f"Invalid version string '{version_str}' " + f"for pattern '{raw_pattern}'/'{pattern.regexp}'" + ) + raise version.PatternError(err_msg) + else: + return _parse_version_info(match.groupdict()) + + +def is_valid(version_str: str, raw_pattern: str = "{pycalver}") -> bool: + """Check if a version matches a pattern. + + >>> is_valid("v201712.0033-beta", raw_pattern="{pycalver}") + True + >>> is_valid("v201712.0033-beta", raw_pattern="{semver}") + False + >>> is_valid("1.2.3", raw_pattern="{semver}") + True + >>> is_valid("v201712.0033-beta", raw_pattern="{semver}") + False + """ + try: + parse_version_info(version_str, raw_pattern) + return True + except version.PatternError: + return False + + +ID_FIELDS_BY_PART = { + 'MAJOR' : 'major', + 'MINOR' : 'minor', + 'MM' : 'minor', + 'MMM' : 'minor', + 'MMMM' : 'minor', + 'MMMMM' : 'minor', + 'MMMMMM' : 'minor', + 'MMMMMMM': 'minor', + 'PATCH' : 'patch', + 'PP' : 'patch', + 'PPP' : 'patch', + 'PPPP' : 'patch', + 'PPPPP' : 'patch', + 'PPPPPP' : 'patch', + 'PPPPPPP': 'patch', + 'BID' : 'bid', + 'BB' : 'bid', + 'BBB' : 'bid', + 'BBBB' : 'bid', + 'BBBBB' : 'bid', + 'BBBBBB' : 'bid', + 'BBBBBBB': 'bid', +} + + +def format_version(vinfo: version.V1VersionInfo, raw_pattern: str) -> str: + """Generate version string. + + >>> import datetime as dt + >>> vinfo = parse_version_info("v201712.0033-beta", raw_pattern="{pycalver}") + >>> vinfo_a = vinfo._replace(**cal_info(date=dt.date(2017, 1, 1))._asdict()) + >>> vinfo_b = vinfo._replace(**cal_info(date=dt.date(2017, 12, 31))._asdict()) + >>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final') + + >>> format_version(vinfo_a, raw_pattern="v{yy}.{BID}{release}") + 'v17.33-beta' + >>> format_version(vinfo_a, raw_pattern="{pep440_version}") + '201701.33b0' + + >>> format_version(vinfo_a, raw_pattern="{pycalver}") + 'v201701.0033-beta' + >>> format_version(vinfo_b, raw_pattern="{pycalver}") + 'v201712.0033-beta' + + >>> format_version(vinfo_a, raw_pattern="v{year}w{iso_week}.{BID}{release}") + 'v2017w00.33-beta' + >>> format_version(vinfo_b, raw_pattern="v{year}w{iso_week}.{BID}{release}") + 'v2017w52.33-beta' + + >>> format_version(vinfo_a, raw_pattern="v{year}d{doy}.{bid}{release}") + 'v2017d001.0033-beta' + >>> format_version(vinfo_b, raw_pattern="v{year}d{doy}.{bid}{release}") + 'v2017d365.0033-beta' + + >>> format_version(vinfo_c, raw_pattern="v{year}w{iso_week}.{BID}-{tag}") + 'v2017w52.33-final' + >>> format_version(vinfo_c, raw_pattern="v{year}w{iso_week}.{BID}{release}") + 'v2017w52.33' + + >>> format_version(vinfo_c, raw_pattern="v{MAJOR}.{MINOR}.{PATCH}") + 'v1.2.34' + >>> format_version(vinfo_c, raw_pattern="v{MAJOR}.{MM}.{PPP}") + 'v1.02.034' + """ + full_pattern = raw_pattern + for part_name, full_part_format in v1patterns.FULL_PART_FORMATS.items(): + full_pattern = full_pattern.replace("{" + part_name + "}", full_part_format) + + kwargs: typ.Dict[str, typ.Union[str, int, None]] = vinfo._asdict() + + tag = vinfo.tag + if tag == 'final': + kwargs['release' ] = "" + kwargs['pep440_tag'] = "" + else: + kwargs['release' ] = "-" + tag + kwargs['pep440_tag'] = version.PEP440_TAG_BY_TAG[tag] + "0" + + kwargs['release_tag'] = tag + + year = vinfo.year + if year: + kwargs['yy' ] = str(year)[-2:] + kwargs['yyyy'] = year + + kwargs['BID'] = int(vinfo.bid, 10) + + for part_name, field in ID_FIELDS_BY_PART.items(): + val = kwargs[field] + if part_name.lower() == field.lower(): + if isinstance(val, str): + kwargs[part_name] = int(val, base=10) + else: + kwargs[part_name] = val + else: + assert len(set(part_name)) == 1 + padded_len = len(part_name) + kwargs[part_name] = str(val).zfill(padded_len) + + return full_pattern.format(**kwargs) + + +def incr( + old_version: str, + raw_pattern: str = "{pycalver}", + *, + release : typ.Optional[str] = None, + major : bool = False, + minor : bool = False, + patch : bool = False, + pin_date: bool = False, +) -> typ.Optional[str]: + """Increment version string. + + 'old_version' is assumed to be a string that matches 'pattern' + """ + try: + old_vinfo = parse_version_info(old_version, raw_pattern) + except version.PatternError as ex: + logger.error(str(ex)) + return None + + cur_cinfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info() + + if _is_later_than(old_vinfo, cur_cinfo): + cur_vinfo = old_vinfo._replace(**cur_cinfo._asdict()) + else: + logger.warning(f"Version appears to be from the future '{old_version}'") + cur_vinfo = old_vinfo + + cur_vinfo = version.incr_non_cal_parts( + cur_vinfo, + release, + major, + minor, + patch, + ) + new_version = format_version(cur_vinfo, raw_pattern) + if new_version == old_version: + logger.error("Invalid arguments or pattern, version did not change.") + return None + else: + return new_version diff --git a/src/pycalver2/cli.py b/src/pycalver/v2cli.py similarity index 71% rename from src/pycalver2/cli.py rename to src/pycalver/v2cli.py index 753f587..243f2f5 100644 --- a/src/pycalver2/cli.py +++ b/src/pycalver/v2cli.py @@ -12,12 +12,12 @@ Provided subcommands: show, test, init, bump import typing as typ import logging -import pycalver.version as v1version -import pycalver2.rewrite as v2rewrite -import pycalver2.version as v2version -from pycalver import config +from . import config +from . import version +from . import v2rewrite +from . import v2version -logger = logging.getLogger("pycalver2.cli") +logger = logging.getLogger("pycalver.v2cli") def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.Config: @@ -29,7 +29,7 @@ def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.C version_tags.sort(reverse=True) logger.debug(f"found {len(version_tags)} tags: {version_tags[:2]}") latest_version_tag = version_tags[0] - latest_version_pep440 = v1version.to_pep440(latest_version_tag) + latest_version_pep440 = version.to_pep440(latest_version_tag) if latest_version_tag <= cfg.current_version: return cfg @@ -41,14 +41,15 @@ def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.C ) -def rewrite( +def rewrite_files( cfg : config.Config, new_version: str, ) -> None: new_vinfo = v2version.parse_version_info(new_version, cfg.version_pattern) - v2rewrite.rewrite(cfg.file_patterns, new_vinfo) + v2rewrite.rewrite_files(cfg.file_patterns, new_vinfo) def get_diff(cfg: config.Config, new_version: str) -> str: - new_vinfo = v2version.parse_version_info(new_version, cfg.version_pattern) - return v2rewrite.diff(new_vinfo, cfg.file_patterns) + old_vinfo = v2version.parse_version_info(cfg.current_version, cfg.version_pattern) + new_vinfo = v2version.parse_version_info(new_version , cfg.version_pattern) + return v2rewrite.diff(old_vinfo, new_vinfo, cfg.file_patterns) diff --git a/src/pycalver2/patterns.py b/src/pycalver/v2patterns.py similarity index 74% rename from src/pycalver2/patterns.py rename to src/pycalver/v2patterns.py index 8219422..57a52b9 100644 --- a/src/pycalver2/patterns.py +++ b/src/pycalver/v2patterns.py @@ -33,30 +33,28 @@ import re import typing as typ +import logging -import pycalver.patterns as v1patterns +from .patterns import RE_PATTERN_ESCAPES +from .patterns import Pattern -PATTERN_ESCAPES = [ - ("\u005c", "\u005c\u005c"), - ("-" , "\u005c-"), - ("." , "\u005c."), - ("+" , "\u005c+"), - ("*" , "\u005c*"), - ("?" , "\u005c?"), - ("{" , "\u005c{"), - ("}" , "\u005c}"), - # ("[" , "\u005c["), # [braces] are used for optional parts - # ("]" , "\u005c]"), - ("(", "\u005c("), - (")", "\u005c)"), -] +logger = logging.getLogger("pycalver.v2patterns") + +# NOTE (mb 2020-09-17): For patterns with different options '(AAA|BB|C)', the +# patterns with more digits should be first/left of those with fewer digits: +# +# good: (?:1[0-2]|[1-9]) +# bad: (?:[1-9]|1[0-2]) +# +# This ensures that the longest match is done for a pattern. +# +# This implies that patterns for smaller numbers sometimes must be right of +# those for larger numbers. To be consistent we use this ordering not +# sometimes but always (even though in theory it wouldn't matter): +# +# good: (?:3[0-1]|[1-2][0-9]|[1-9]) +# bad: (?:[1-2][0-9]|3[0-1]|[1-9]) -# NOTE (mb 2020-09-17): For patterns with different options, the longer -# patterns should be first/left (e.g. for 'MM', `1[0-2]` before `[1-9]`). -# This ensures that the longest match is done rather than the shortest. -# To have a consistent ordering, we always put the pattern that matches -# the larger number first (even if the patterns would otherwise be the -# same size). PART_PATTERNS = { # Based on calver.org @@ -239,14 +237,28 @@ def _replace_pattern_parts(pattern: str) -> str: return result_pattern -def compile_pattern_str(pattern: str) -> str: - for char, escaped in PATTERN_ESCAPES: - pattern = pattern.replace(char, escaped) +def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[str]: + escaped_pattern = raw_pattern + for char, escaped in RE_PATTERN_ESCAPES: + # [] braces are used for optional parts, such as [-TAG]/[-beta] + is_semantic_char = char in "[]" + if not is_semantic_char: + # escape it so it is a literal in the re pattern + escaped_pattern = escaped_pattern.replace(char, escaped) - return _replace_pattern_parts(pattern) + escaped_pattern = raw_pattern.replace("[", "\u005c[").replace("]", "\u005c]") + normalized_pattern = escaped_pattern.replace("{version}", version_pattern) + print(">>>>", (raw_pattern ,)) + print("....", (escaped_pattern ,)) + print("....", (normalized_pattern,)) + print("<<<<", (normalized_pattern,)) + + # TODO (mb 2020-09-19): replace {version} etc with version_pattern + pattern_str = _replace_pattern_parts(escaped_pattern) + return re.compile(pattern_str) -def compile_pattern(pattern: str) -> v1patterns.Pattern: - pattern_str = compile_pattern_str(pattern) - pattern_re = re.compile(pattern_str) - return v1patterns.Pattern(pattern, pattern_re) +def compile_pattern(version_pattern: str, raw_pattern: typ.Optional[str] = None) -> Pattern: + _raw_pattern = version_pattern if raw_pattern is None else raw_pattern + regexp = _compile_pattern_re(version_pattern, _raw_pattern) + return Pattern(version_pattern, _raw_pattern, regexp) diff --git a/src/pycalver/v2rewrite.py b/src/pycalver/v2rewrite.py new file mode 100644 index 0000000..c82222a --- /dev/null +++ b/src/pycalver/v2rewrite.py @@ -0,0 +1,198 @@ +# This file is part of the pycalver project +# https://github.com/mbarkhau/pycalver +# +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# SPDX-License-Identifier: MIT +"""Rewrite files, updating occurences of version strings.""" + +import io +import typing as typ +import logging + +from . import parse +from . import config +from . import rewrite +from . import version +from . import v2version +from .patterns import Pattern + +logger = logging.getLogger("pycalver.v2rewrite") + + +def rewrite_lines( + patterns : typ.List[Pattern], + new_vinfo: version.V2VersionInfo, + old_lines: typ.List[str], +) -> typ.List[str]: + """Replace occurances of patterns in old_lines with new_vinfo. + + >>> from .v2patterns import compile_pattern + >>> version_pattern = "vYYYY0M.BUILD[-TAG]" + >>> new_vinfo = v2version.parse_version_info("v201811.0123-beta", version_pattern) + >>> patterns = [compile_pattern(version_pattern, '__version__ = "{version}"')] + >>> rewrite_lines(patterns, new_vinfo, ['__version__ = "v201809.0002-alpha" ']) + ['__version__ = "v201811.0123-beta" '] + + >>> rewrite_lines(patterns, new_vinfo, ['__version__ = "v201809.0002-alpha" # comment']) + ['__version__ = "v201811.0123-beta" # comment'] + + >>> patterns = [compile_pattern(version_pattern, '__version__ = "{pep440_version}"')] + >>> old_lines = ['__version__ = "201809.2a0"'] + >>> rewrite_lines(patterns, new_vinfo, old_lines) + ['__version__ = "201811.123b0"'] + """ + new_lines = old_lines[:] + found_patterns = set() + + for match in parse.iter_matches(old_lines, patterns): + found_patterns.add(match.pattern.raw_pattern) + replacement = v2version.format_version(new_vinfo, match.pattern.raw_pattern) + span_l, span_r = match.span + new_line = match.line[:span_l] + replacement + match.line[span_r:] + new_lines[match.lineno] = new_line + + non_matched_patterns = set(patterns) - found_patterns + if non_matched_patterns: + for nmp in non_matched_patterns: + logger.error(f"No match for pattern '{nmp.raw_pattern}'") + logger.error(f"Pattern compiles to regex '{nmp.regexp.pattern}'") + raise rewrite.NoPatternMatch("Invalid pattern(s)") + else: + return new_lines + + +def rfd_from_content( + patterns : typ.List[Pattern], + new_vinfo: version.V2VersionInfo, + content : str, + path : str = "", +) -> rewrite.RewrittenFileData: + r"""Rewrite pattern occurrences with version string. + + >>> version_pattern = "vYYYY0M.BUILD[-TAG]" + >>> new_vinfo = v2version.parse_version_info("v201809.0123", version_pattern) + >>> raw_patterns = ['__version__ = "vYYYY0M.BUILD[-TAG]"'] + >>> patterns = + >>> content = '__version__ = "v201809.0001-alpha"' + >>> rfd = rfd_from_content(patterns, new_vinfo, content) + >>> rfd.new_lines + ['__version__ = "v201809.0123"'] + + >>> version_pattern = "vMAJOR.MINOR.PATCH" + >>> new_vinfo = v2version.parse_version_info("v1.2.3", version_pattern) + >>> raw_patterns = ['__version__ = "vMAJOR.MINOR.PATCH"'] + >>> patterns = + >>> content = '__version__ = "v1.2.2"' + >>> rfd = rfd_from_content(patterns, new_vinfo, content) + >>> rfd.new_lines + ['__version__ = "v1.2.3"'] + """ + line_sep = rewrite.detect_line_sep(content) + old_lines = content.split(line_sep) + new_lines = rewrite_lines(patterns, new_vinfo, old_lines) + return rewrite.RewrittenFileData(path, line_sep, old_lines, new_lines) + + +def iter_rewritten( + file_patterns: config.PatternsByFile, + new_vinfo : version.V2VersionInfo, +) -> typ.Iterable[rewrite.RewrittenFileData]: + r'''Iterate over files with version string replaced. + + >>> version_pattern = "vYYYY0M.BUILD[-TAG]" + >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "vYYYY0M.BUILD[-TAG]"']} + >>> new_vinfo = v2version.parse_version_info("v201809.0123", version_pattern) + >>> rewritten_datas = iter_rewritten(file_patterns, new_vinfo) + >>> rfd = list(rewritten_datas)[0] + >>> expected = [ + ... '# This file is part of the pycalver project', + ... '# https://github.com/mbarkhau/pycalver', + ... '#', + ... '# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License', + ... '# SPDX-License-Identifier: MIT', + ... '"""PyCalVer: CalVer for Python Packages."""', + ... '', + ... '__version__ = "v201809.0123"', + ... '', + ... ] + >>> assert rfd.new_lines == expected + ''' + + fobj: typ.IO[str] + + for file_path, patterns in rewrite.iter_path_patterns_items(file_patterns): + with file_path.open(mode="rt", encoding="utf-8") as fobj: + content = fobj.read() + + rfd = rfd_from_content(patterns, new_vinfo, content) + yield rfd._replace(path=str(file_path)) + + +def diff( + old_vinfo : version.V2VersionInfo, + new_vinfo : version.V2VersionInfo, + file_patterns: config.PatternsByFile, +) -> str: + r"""Generate diffs of rewritten files. + + >>> old_vinfo = v2version.parse_version_info("v201809.0123", version_pattern) + >>> new_vinfo = v2version.parse_version_info("v201810.1124", version_pattern) + >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "vYYYY0M.BUILD[-TAG]"']} + >>> diff_str = diff(old_vinfo, new_vinfo, file_patterns) + >>> lines = diff_str.split("\n") + >>> lines[:2] + ['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py'] + >>> assert lines[6].startswith('-__version__ = "v2') + >>> assert not lines[6].startswith('-__version__ = "v201810.1124"') + >>> lines[7] + '+__version__ = "v201810.1124"' + + >>> file_patterns = {"LICENSE": ['Copyright (c) 2018-YYYY']} + >>> diff_str = diff(old_vinfo, new_vinfo, file_patterns) + >>> assert not diff_str + """ + + full_diff = "" + fobj: typ.IO[str] + + for file_path, patterns in sorted(rewrite.iter_path_patterns_items(file_patterns)): + with file_path.open(mode="rt", encoding="utf-8") as fobj: + content = fobj.read() + + patterns_with_change = 0 + for pattern in patterns: + old_str = v2version.format_version(old_vinfo, pattern.raw_pattern) + new_str = v2version.format_version(new_vinfo, pattern.raw_pattern) + if old_str != new_str: + patterns_with_change += 1 + + try: + rfd = rfd_from_content(patterns, new_vinfo, content) + except rewrite.NoPatternMatch: + # pylint:disable=raise-missing-from ; we support py2, so not an option + errmsg = f"No patterns matched for '{file_path}'" + raise rewrite.NoPatternMatch(errmsg) + + rfd = rfd._replace(path=str(file_path)) + lines = rewrite.diff_lines(rfd) + if len(lines) == 0 and patterns_with_change > 0: + errmsg = f"No patterns matched for '{file_path}'" + raise rewrite.NoPatternMatch(errmsg) + + full_diff += "\n".join(lines) + "\n" + + full_diff = full_diff.rstrip("\n") + return full_diff + + +def rewrite_files( + file_patterns: config.PatternsByFile, + new_vinfo : version.V2VersionInfo, +) -> None: + """Rewrite project files, updating each with the new version.""" + fobj: typ.IO[str] + + for file_data in iter_rewritten(file_patterns, new_vinfo): + new_content = file_data.line_sep.join(file_data.new_lines) + with io.open(file_data.path, mode="wt", encoding="utf-8") as fobj: + fobj.write(new_content) diff --git a/src/pycalver2/version.py b/src/pycalver/v2version.py similarity index 62% rename from src/pycalver2/version.py rename to src/pycalver/v2version.py index 21f2edd..72c7e9b 100644 --- a/src/pycalver2/version.py +++ b/src/pycalver/v2version.py @@ -9,102 +9,28 @@ import typing as typ import logging import datetime as dt -import lexid +from . import version +from . import v2patterns -import pycalver2.patterns as v2patterns - -# import pycalver.version as v1version -# import pycalver.patterns as v1patterns - -logger = logging.getLogger("pycalver.version") +logger = logging.getLogger("pycalver.v2version") -# The test suite may replace this. -TODAY = dt.datetime.utcnow().date() +CalInfo = typ.Union[version.V2CalendarInfo, version.V2VersionInfo] -ZERO_VALUES = { - 'MAJOR': "0", - 'MINOR': "0", - 'PATCH': "0", - 'TAG' : "final", - 'PYTAG': "", - 'NUM' : "0", -} +def _is_later_than(old: CalInfo, new: CalInfo) -> bool: + """Is old > new based on non None fields.""" + for field in version.V1CalendarInfo._fields: + aval = getattr(old, field) + bval = getattr(new, field) + if not (aval is None or bval is None): + if aval > bval: + return True + return False -TAG_BY_PEP440_TAG = { - 'a' : 'alpha', - 'b' : 'beta', - "" : 'final', - 'rc' : 'rc', - 'dev' : 'dev', - 'post': 'post', -} - - -PEP440_TAG_BY_TAG = { - 'alpha': "a", - 'beta' : "b", - 'final': "", - 'pre' : "rc", - 'rc' : "rc", - 'dev' : "dev", - 'post' : "post", -} - -assert set(TAG_BY_PEP440_TAG.keys()) == set(PEP440_TAG_BY_TAG.values()) -assert set(TAG_BY_PEP440_TAG.values()) < set(PEP440_TAG_BY_TAG.keys()) - -# PEP440_TAGS_REVERSE = { -# "a" : 'alpha', -# "b" : 'beta', -# "rc" : 'rc', -# "dev" : 'dev', -# "post": 'post', -# } - - -MaybeInt = typ.Optional[int] - - -class CalendarInfo(typ.NamedTuple): - """Container for calendar components of version strings.""" - - year_y : MaybeInt - year_g : MaybeInt - quarter: MaybeInt - month : MaybeInt - dom : MaybeInt - doy : MaybeInt - week_w : MaybeInt - week_u : MaybeInt - week_v : MaybeInt - - -class VersionInfo(typ.NamedTuple): - """Container for parsed version string.""" - - year_y : MaybeInt - year_g : MaybeInt - quarter: MaybeInt - month : MaybeInt - dom : MaybeInt - doy : MaybeInt - week_w : MaybeInt - week_u : MaybeInt - week_v : MaybeInt - major : int - minor : int - patch : int - num : int - bid : str - tag : str - pytag : str - - -def _ver_to_cal_info(vinfo: VersionInfo) -> CalendarInfo: - return CalendarInfo( +def _ver_to_cal_info(vinfo: version.V2VersionInfo) -> version.V2CalendarInfo: + return version.V2CalendarInfo( vinfo.year_y, vinfo.year_g, vinfo.quarter, @@ -117,32 +43,7 @@ def _ver_to_cal_info(vinfo: VersionInfo) -> CalendarInfo: ) -def _date_from_doy(year: int, doy: int) -> dt.date: - """Parse date from year and day of year (1 indexed). - - >>> cases = [ - ... (2016, 1), (2016, 31), (2016, 31 + 1), (2016, 31 + 29), (2016, 31 + 30), - ... (2017, 1), (2017, 31), (2017, 31 + 1), (2017, 31 + 28), (2017, 31 + 29), - ... ] - >>> dates = [_date_from_doy(year, month) for year, month in cases] - >>> assert [(d.month, d.day) for d in dates] == [ - ... (1, 1), (1, 31), (2, 1), (2, 29), (3, 1), - ... (1, 1), (1, 31), (2, 1), (2, 28), (3, 1), - ... ] - """ - return dt.date(year, 1, 1) + dt.timedelta(days=doy - 1) - - -def _quarter_from_month(month: int) -> int: - """Calculate quarter (1 indexed) from month (1 indexed). - - >>> [_quarter_from_month(month) for month in range(1, 13)] - [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4] - """ - return ((month - 1) // 3) + 1 - - -def cal_info(date: dt.date = None) -> CalendarInfo: +def cal_info(date: dt.date = None) -> version.V2CalendarInfo: """Generate calendar components for current date. >>> import datetime as dt @@ -164,12 +65,12 @@ def cal_info(date: dt.date = None) -> CalendarInfo: (2019, 2, 4, 7, 97, 13, 14, 14) """ if date is None: - date = TODAY + date = version.TODAY kwargs = { 'year_y' : date.year, 'year_g' : int(date.strftime("%G"), base=10), - 'quarter': _quarter_from_month(date.month), + 'quarter': version.quarter_from_month(date.month), 'month' : date.month, 'dom' : date.day, 'doy' : int(date.strftime("%j"), base=10), @@ -178,10 +79,12 @@ def cal_info(date: dt.date = None) -> CalendarInfo: 'week_v' : int(date.strftime("%V"), base=10), } - return CalendarInfo(**kwargs) + return version.V2CalendarInfo(**kwargs) -VALID_FIELD_KEYS = set(VersionInfo._fields) | {'version'} +VALID_FIELD_KEYS = set(version.V2VersionInfo._fields) | {'version'} + +MaybeInt = typ.Optional[int] FieldKey = str MatchGroupKey = str @@ -190,44 +93,46 @@ MatchGroupStr = str PatternGroups = typ.Dict[FieldKey, MatchGroupStr] FieldValues = typ.Dict[FieldKey, MatchGroupStr] +VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]] -def _parse_version_info(field_values: FieldValues) -> VersionInfo: - """Parse normalized VersionInfo from groups of a matched pattern. - >>> vnfo = _parse_version_info({'year_y': "2018", 'month': "11", 'bid': "0099"}) - >>> (vnfo.year_y, vnfo.month, vnfo.quarter, vnfo.bid, vnfo.tag) +def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: + """Parse normalized V2VersionInfo from groups of a matched pattern. + + >>> vinfo = _parse_version_info({'year_y': "2018", 'month': "11", 'bid': "0099"}) + >>> (vinfo.year_y, vinfo.month, vinfo.quarter, vinfo.bid, vinfo.tag) (2018, 11, 4, '0099', 'final') - >>> vnfo = _parse_version_info({'year_y': "2018", 'doy': "11", 'bid': "099", 'tag': "beta"}) - >>> (vnfo.year_y, vnfo.month, vnfo.dom, vnfo.doy, vnfo.bid, vnfo.tag) + >>> vinfo = _parse_version_info({'year_y': "2018", 'doy': "11", 'bid': "099", 'tag': "beta"}) + >>> (vinfo.year_y, vinfo.month, vinfo.dom, vinfo.doy, vinfo.bid, vinfo.tag) (2018, 1, 11, 11, '099', 'beta') - >>> vnfo = _parse_version_info({'year_y': "2018", 'month': "6", 'dom': "15"}) - >>> (vnfo.year_y, vnfo.month, vnfo.dom, vnfo.doy) + >>> vinfo = _parse_version_info({'year_y': "2018", 'month': "6", 'dom': "15"}) + >>> (vinfo.year_y, vinfo.month, vinfo.dom, vinfo.doy) (2018, 6, 15, 166) - >>> vnfo = _parse_version_info({'major': "1", 'minor': "23", 'patch': "45"}) - >>> (vnfo.major, vnfo.minor, vnfo.patch) + >>> vinfo = _parse_version_info({'major': "1", 'minor': "23", 'patch': "45"}) + >>> (vinfo.major, vinfo.minor, vinfo.patch) (1, 23, 45) - >>> vnfo = _parse_version_info({'major': "1", 'minor': "023", 'patch': "0045"}) - >>> (vnfo.major, vnfo.minor, vnfo.patch) + >>> vinfo = _parse_version_info({'major': "1", 'minor': "023", 'patch': "0045"}) + >>> (vinfo.major, vinfo.minor, vinfo.patch) (1, 23, 45) - >>> vnfo = _parse_version_info({'year_y': "2021", 'week_w': "02"}) - >>> (vnfo.year_y, vnfo.week_w) + >>> vinfo = _parse_version_info({'year_y': "2021", 'week_w': "02"}) + >>> (vinfo.year_y, vinfo.week_w) (2021, 2) - >>> vnfo = _parse_version_info({'year_y': "2021", 'week_u': "02"}) - >>> (vnfo.year_y, vnfo.week_u) + >>> vinfo = _parse_version_info({'year_y': "2021", 'week_u': "02"}) + >>> (vinfo.year_y, vinfo.week_u) (2021, 2) - >>> vnfo = _parse_version_info({'year_g': "2021", 'week_v': "02"}) - >>> (vnfo.year_g, vnfo.week_v) + >>> vinfo = _parse_version_info({'year_g': "2021", 'week_v': "02"}) + >>> (vinfo.year_g, vinfo.week_v) (2021, 2) - >>> vnfo = _parse_version_info({'year_y': "2021", 'month': "01", 'dom': "03"}) - >>> (vnfo.year_y, vnfo.month, vnfo.dom, vnfo.tag) + >>> vinfo = _parse_version_info({'year_y': "2021", 'month': "01", 'dom': "03"}) + >>> (vinfo.year_y, vinfo.month, vinfo.dom, vinfo.tag) (2021, 1, 3, 'final') - >>> (vnfo.year_y, vnfo.week_w, vnfo.year_y, vnfo.week_u,vnfo.year_g, vnfo.week_v) + >>> (vinfo.year_y, vinfo.week_w, vinfo.year_y, vinfo.week_u,vinfo.year_g, vinfo.week_v) (2021, 0, 2021, 1, 2020, 53) """ for key in field_values: @@ -238,9 +143,9 @@ def _parse_version_info(field_values: FieldValues) -> VersionInfo: pytag = fvals.get('pytag') or "" if tag and not pytag: - pytag = PEP440_TAG_BY_TAG[tag] + pytag = version.PEP440_TAG_BY_TAG[tag] elif pytag and not tag: - tag = TAG_BY_PEP440_TAG[pytag] + tag = version.TAG_BY_PEP440_TAG[pytag] date: typ.Optional[dt.date] = None @@ -256,7 +161,7 @@ def _parse_version_info(field_values: FieldValues) -> VersionInfo: week_v: MaybeInt = int(fvals['week_v']) if 'week_v' in fvals else None if year_y and doy: - date = _date_from_doy(year_y, doy) + date = version.date_from_doy(year_y, doy) month = date.month dom = date.day else: @@ -279,7 +184,7 @@ def _parse_version_info(field_values: FieldValues) -> VersionInfo: quarter = int(fvals['quarter']) if 'quarter' in fvals else None if quarter is None and month: - quarter = _quarter_from_month(month) + quarter = version.quarter_from_month(month) # NOTE (mb 2020-09-18): If a part is optional, fvals[] may be None major = int(fvals.get('major') or 0) @@ -288,7 +193,7 @@ def _parse_version_info(field_values: FieldValues) -> VersionInfo: num = int(fvals.get('num' ) or 0) bid = fvals['bid'] if 'bid' in fvals else "1000" - vnfo = VersionInfo( + vinfo = version.V2VersionInfo( year_y=year_y, year_g=year_g, quarter=quarter, @@ -306,74 +211,69 @@ def _parse_version_info(field_values: FieldValues) -> VersionInfo: tag=tag, pytag=pytag, ) - return vnfo + return vinfo -VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]] +def parse_version_info( + version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-TAG[NUM]]" +) -> version.V2VersionInfo: + """Parse normalized V2VersionInfo. - -class PatternError(Exception): - pass - - -def parse_version_info(version_str: str, pattern: str = "vYYYY0M.BUILD[-TAG[NUM]]") -> VersionInfo: - """Parse normalized VersionInfo. - - >>> vnfo = parse_version_info("v201712.0033-beta0", pattern="vYYYY0M.BUILD[-TAG[NUM]]") + >>> vinfo = parse_version_info("v201712.0033-beta0", raw_pattern="vYYYY0M.BUILD[-TAG[NUM]]") >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033", 'tag': "beta", 'num': 0} - >>> assert vnfo == _parse_version_info(fvals) + >>> assert vinfo == _parse_version_info(fvals) - >>> vnfo = parse_version_info("v201712.0033-beta", pattern="vYYYY0M.BUILD[-TAG[NUM]]") + >>> vinfo = parse_version_info("v201712.0033-beta", raw_pattern="vYYYY0M.BUILD[-TAG[NUM]]") >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"} - >>> assert vnfo == _parse_version_info(fvals) + >>> assert vinfo == _parse_version_info(fvals) - >>> vnfo = parse_version_info("v201712.0033", pattern="vYYYY0M.BUILD[-TAG[NUM]]") + >>> vinfo = parse_version_info("v201712.0033", raw_pattern="vYYYY0M.BUILD[-TAG[NUM]]") >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033"} - >>> assert vnfo == _parse_version_info(fvals) + >>> assert vinfo == _parse_version_info(fvals) - >>> vnfo = parse_version_info("1.23.456", pattern="MAJOR.MINOR.PATCH") + >>> vinfo = parse_version_info("1.23.456", raw_pattern="MAJOR.MINOR.PATCH") >>> fvals = {'major': "1", 'minor': "23", 'patch': "456"} - >>> assert vnfo == _parse_version_info(fvals) + >>> assert vinfo == _parse_version_info(fvals) """ - pattern_tup = v2patterns.compile_pattern(pattern) - match = pattern_tup.regexp.match(version_str) + pattern = v2patterns.compile_pattern(raw_pattern) + match = pattern.regexp.match(version_str) if match is None: err_msg = ( f"Invalid version string '{version_str}' " - f"for pattern '{pattern}'/'{pattern_tup.regexp.pattern}'" + f"for pattern '{raw_pattern}'/'{pattern.regexp.pattern}'" ) - raise PatternError(err_msg) + raise version.PatternError(err_msg) else: field_values = match.groupdict() return _parse_version_info(field_values) -def is_valid(version_str: str, pattern: str = "vYYYY0M.BUILD[-TAG]") -> bool: +def is_valid(version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-TAG]") -> bool: """Check if a version matches a pattern. - >>> is_valid("v201712.0033-beta", pattern="vYYYY0M.BUILD[-TAG]") + >>> is_valid("v201712.0033-beta", raw_pattern="vYYYY0M.BUILD[-TAG]") True - >>> is_valid("v201712.0033-beta", pattern="MAJOR.MINOR.PATCH") + >>> is_valid("v201712.0033-beta", raw_pattern="MAJOR.MINOR.PATCH") False - >>> is_valid("1.2.3", pattern="MAJOR.MINOR.PATCH") + >>> is_valid("1.2.3", raw_pattern="MAJOR.MINOR.PATCH") True - >>> is_valid("v201712.0033-beta", pattern="MAJOR.MINOR.PATCH") + >>> is_valid("v201712.0033-beta", raw_pattern="MAJOR.MINOR.PATCH") False """ try: - parse_version_info(version_str, pattern) + parse_version_info(version_str, raw_pattern) return True - except PatternError: + except version.PatternError: return False TemplateKwargs = typ.Dict[str, typ.Union[str, int, None]] -def _format_part_values(vinfo: VersionInfo) -> typ.Dict[str, str]: - """Generate kwargs for template from minimal VersionInfo. +def _format_part_values(vinfo: version.V2VersionInfo) -> typ.Dict[str, str]: + """Generate kwargs for template from minimal V2VersionInfo. - The VersionInfo Tuple only has the minimal representation + The V2VersionInfo Tuple only has the minimal representation of a parsed version, not the values suitable for formatting. It may for example have month=9, but not the formatted representation '09' for '0M'. @@ -402,18 +302,20 @@ def _format_part_values(vinfo: VersionInfo) -> typ.Dict[str, str]: return kwargs -def _make_segments(pattern: str) -> typ.List[str]: +def _make_segments(raw_pattern: str) -> typ.List[str]: pattern_segs_l: typ.List[str] = [] pattern_segs_r: typ.List[str] = [] - pattern_rest = pattern + pattern_rest = raw_pattern while "[" in pattern_rest and "]" in pattern_rest: try: seg_l , pattern_rest = pattern_rest.split("[", 1) pattern_rest, seg_r = pattern_rest.rsplit("]", 1) except ValueError as val_err: if "values to unpack" in str(val_err): - pat_err = PatternError(f"Unbalanced braces [] in '{pattern}'") + err = f"Unbalanced braces [] in '{raw_pattern}'" + pat_err = version.PatternError(err) + pat_err.__cause__ = val_err raise pat_err else: @@ -444,7 +346,7 @@ def _clear_zero_segments( def _format_segments( - vinfo : VersionInfo, + vinfo : version.V2VersionInfo, pattern_segs: typ.List[str], ) -> typ.List[str]: kwargs = _format_part_values(vinfo) @@ -470,12 +372,12 @@ def _format_segments( for part, part_value in part_values: if part in seg_l: seg_l = seg_l.replace(part, part_value) - if not (is_optional and str(part_value) == ZERO_VALUES.get(part)): + if not (is_optional and str(part_value) == version.ZERO_VALUES.get(part)): is_zero_segment[idx_l] = False if part in seg_r: seg_r = seg_r.replace(part, part_value) - if not (is_optional and str(part_value) == ZERO_VALUES[part]): + if not (is_optional and str(part_value) == version.ZERO_VALUES[part]): is_zero_segment[idx_r] = False formatted_segs_l.append(seg_l) @@ -489,7 +391,7 @@ def _format_segments( return _clear_zero_segments(formatted_segs, is_zero_segment) -def format_version(vinfo: VersionInfo, pattern: str) -> str: +def format_version(vinfo: version.V2VersionInfo, raw_pattern: str) -> str: """Generate version string. >>> import datetime as dt @@ -575,7 +477,7 @@ def format_version(vinfo: VersionInfo, pattern: str) -> str: >>> format_version(vinfo_d, pattern='__version__ = "vMAJOR[.MINOR[.PATCH[-TAG[NUM]]]]"') '__version__ = "v1.0.0-rc2"' """ - pattern_segs = _make_segments(pattern) + pattern_segs = _make_segments(raw_pattern) formatted_segs = _format_segments(vinfo, pattern_segs) return "".join(formatted_segs) @@ -583,9 +485,9 @@ def format_version(vinfo: VersionInfo, pattern: str) -> str: def incr( old_version: str, - pattern : str = "vYYYY0M.BUILD[-TAG]", + raw_pattern: str = "vYYYY0M.BUILD[-TAG]", *, - release : str = None, + release : typ.Optional[str] = None, major : bool = False, minor : bool = False, patch : bool = False, @@ -593,44 +495,30 @@ def incr( ) -> typ.Optional[str]: """Increment version string. - 'old_version' is assumed to be a string that matches 'pattern' + 'old_version' is assumed to be a string that matches 'raw_pattern' """ try: - old_vinfo = parse_version_info(old_version, pattern) - except PatternError as ex: + old_vinfo = parse_version_info(old_version, raw_pattern) + except version.PatternError as ex: logger.error(str(ex)) return None - cur_vinfo = old_vinfo + cur_cinfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info() - cur_cal_nfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info() - - old_date = (old_vinfo.year_y or 0 , old_vinfo.month or 0 , old_vinfo.dom or 0) - cur_date = (cur_cal_nfo.year_y or 0, cur_cal_nfo.month or 0, cur_cal_nfo.dom or 0) - - if old_date <= cur_date: - cur_vinfo = cur_vinfo._replace(**cur_cal_nfo._asdict()) - else: + if _is_later_than(old_vinfo, cur_cinfo): logger.warning(f"Version appears to be from the future '{old_version}'") + cur_vinfo = old_vinfo + else: + cur_vinfo = old_vinfo._replace(**cur_cinfo._asdict()) - _bid = cur_vinfo.bid - if int(_bid) < 1000: - # prevent truncation of leading zeros - _bid = str(int(_bid) + 1000) - - cur_vinfo = cur_vinfo._replace(bid=lexid.incr(_bid)) - - if major: - cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0, patch=0) - if minor: - cur_vinfo = cur_vinfo._replace(minor=cur_vinfo.minor + 1, patch=0) - if patch: - cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1) - - if release: - cur_vinfo = cur_vinfo._replace(tag=release) - - new_version = format_version(cur_vinfo, pattern) + cur_vinfo = version.incr_non_cal_parts( + cur_vinfo, + release, + major, + minor, + patch, + ) + new_version = format_version(cur_vinfo, raw_pattern) if new_version == old_version: logger.error("Invalid arguments or pattern, version did not change.") return None diff --git a/src/pycalver/version.py b/src/pycalver/version.py index d11c56e..76a9acb 100644 --- a/src/pycalver/version.py +++ b/src/pycalver/version.py @@ -1,30 +1,13 @@ -# This file is part of the pycalver project -# https://github.com/mbarkhau/pycalver -# -# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License -# SPDX-License-Identifier: MIT -"""Functions related to version string manipulation.""" - import typing as typ -import logging import datetime as dt import lexid import pkg_resources -import pycalver.patterns as v1patterns - -logger = logging.getLogger("pycalver.version") - - -# The test suite may replace this. -TODAY = dt.datetime.utcnow().date() - - MaybeInt = typ.Optional[int] -class CalendarInfo(typ.NamedTuple): +class V1CalendarInfo(typ.NamedTuple): """Container for calendar components of version strings.""" year : MaybeInt @@ -36,7 +19,7 @@ class CalendarInfo(typ.NamedTuple): us_week : MaybeInt -class VersionInfo(typ.NamedTuple): +class V1VersionInfo(typ.NamedTuple): """Container for parsed version string.""" year : MaybeInt @@ -53,26 +36,94 @@ class VersionInfo(typ.NamedTuple): tag : str -def _ver_to_cal_info(vinfo: VersionInfo) -> CalendarInfo: - return CalendarInfo( - vinfo.year, - vinfo.quarter, - vinfo.month, - vinfo.dom, - vinfo.doy, - vinfo.iso_week, - vinfo.us_week, - ) +class V2CalendarInfo(typ.NamedTuple): + """Container for calendar components of version strings.""" + + year_y : MaybeInt + year_g : MaybeInt + quarter: MaybeInt + month : MaybeInt + dom : MaybeInt + doy : MaybeInt + week_w : MaybeInt + week_u : MaybeInt + week_v : MaybeInt -def _date_from_doy(year: int, doy: int) -> dt.date: +class V2VersionInfo(typ.NamedTuple): + """Container for parsed version string.""" + + year_y : MaybeInt + year_g : MaybeInt + quarter: MaybeInt + month : MaybeInt + dom : MaybeInt + doy : MaybeInt + week_w : MaybeInt + week_u : MaybeInt + week_v : MaybeInt + major : int + minor : int + patch : int + num : int + bid : str + tag : str + pytag : str + + +VersionInfoType = typ.TypeVar('VersionInfoType', V1VersionInfo, V2VersionInfo) + + +# The test suite may replace this. +TODAY = dt.datetime.utcnow().date() + + +TAG_BY_PEP440_TAG = { + 'a' : 'alpha', + 'b' : 'beta', + "" : 'final', + 'rc' : 'rc', + 'dev' : 'dev', + 'post': 'post', +} + + +PEP440_TAG_BY_TAG = { + 'alpha': "a", + 'beta' : "b", + 'final': "", + 'pre' : "rc", + 'rc' : "rc", + 'dev' : "dev", + 'post' : "post", +} + +assert set(TAG_BY_PEP440_TAG.keys()) == set(PEP440_TAG_BY_TAG.values()) +assert set(TAG_BY_PEP440_TAG.values()) < set(PEP440_TAG_BY_TAG.keys()) + + +ZERO_VALUES = { + 'MAJOR': "0", + 'MINOR': "0", + 'PATCH': "0", + 'TAG' : "final", + 'PYTAG': "", + 'NUM' : "0", +} + + +class PatternError(Exception): + pass + + +def date_from_doy(year: int, doy: int) -> dt.date: """Parse date from year and day of year (1 indexed). >>> cases = [ ... (2016, 1), (2016, 31), (2016, 31 + 1), (2016, 31 + 29), (2016, 31 + 30), ... (2017, 1), (2017, 31), (2017, 31 + 1), (2017, 31 + 28), (2017, 31 + 29), ... ] - >>> dates = [_date_from_doy(year, month) for year, month in cases] + >>> dates = [date_from_doy(year, month) for year, month in cases] >>> assert [(d.month, d.day) for d in dates] == [ ... (1, 1), (1, 31), (2, 1), (2, 29), (3, 1), ... (1, 1), (1, 31), (2, 1), (2, 28), (3, 1), @@ -81,407 +132,15 @@ def _date_from_doy(year: int, doy: int) -> dt.date: return dt.date(year, 1, 1) + dt.timedelta(days=doy - 1) -def _quarter_from_month(month: int) -> int: +def quarter_from_month(month: int) -> int: """Calculate quarter (1 indexed) from month (1 indexed). - >>> [_quarter_from_month(month) for month in range(1, 13)] + >>> [quarter_from_month(month) for month in range(1, 13)] [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4] """ return ((month - 1) // 3) + 1 -def cal_info(date: dt.date = None) -> CalendarInfo: - """Generate calendar components for current date. - - >>> from datetime import date - - >>> c = cal_info(date(2019, 1, 5)) - >>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) - (2019, 1, 1, 5, 5, 0, 0) - - >>> c = cal_info(date(2019, 1, 6)) - >>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) - (2019, 1, 1, 6, 6, 0, 1) - - >>> c = cal_info(date(2019, 1, 7)) - >>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) - (2019, 1, 1, 7, 7, 1, 1) - - >>> c = cal_info(date(2019, 4, 7)) - >>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) - (2019, 2, 4, 7, 97, 13, 14) - """ - if date is None: - date = TODAY - - kwargs = { - 'year' : date.year, - 'quarter' : _quarter_from_month(date.month), - 'month' : date.month, - 'dom' : date.day, - 'doy' : int(date.strftime("%j"), base=10), - 'iso_week': int(date.strftime("%W"), base=10), - 'us_week' : int(date.strftime("%U"), base=10), - } - - return CalendarInfo(**kwargs) - - -FieldKey = str -MatchGroupKey = str -MatchGroupStr = str - -PatternGroups = typ.Dict[MatchGroupKey, MatchGroupStr] -FieldValues = typ.Dict[FieldKey , MatchGroupStr] - - -def _parse_field_values(field_values: FieldValues) -> VersionInfo: - fvals = field_values - tag = fvals.get('tag') - if tag is None: - tag = "final" - tag = TAG_ALIASES.get(tag, tag) - assert tag is not None - - bid = fvals['bid'] if 'bid' in fvals else "0001" - - year = int(fvals['year']) if 'year' in fvals else None - doy = int(fvals['doy' ]) if 'doy' in fvals else None - - month: typ.Optional[int] - dom : typ.Optional[int] - - if year and doy: - date = _date_from_doy(year, doy) - month = date.month - dom = date.day - else: - month = int(fvals['month']) if 'month' in fvals else None - dom = int(fvals['dom' ]) if 'dom' in fvals else None - - iso_week: typ.Optional[int] - us_week : typ.Optional[int] - - if year and month and dom: - date = dt.date(year, month, dom) - doy = int(date.strftime("%j"), base=10) - iso_week = int(date.strftime("%W"), base=10) - us_week = int(date.strftime("%U"), base=10) - else: - iso_week = None - us_week = None - - quarter = int(fvals['quarter']) if 'quarter' in fvals else None - if quarter is None and month: - quarter = _quarter_from_month(month) - - major = int(fvals['major']) if 'major' in fvals else 0 - minor = int(fvals['minor']) if 'minor' in fvals else 0 - patch = int(fvals['patch']) if 'patch' in fvals else 0 - - return VersionInfo( - year=year, - quarter=quarter, - month=month, - dom=dom, - doy=doy, - iso_week=iso_week, - us_week=us_week, - major=major, - minor=minor, - patch=patch, - bid=bid, - tag=tag, - ) - - -def _is_calver(nfo: typ.Union[CalendarInfo, VersionInfo]) -> bool: - """Check pattern for any calendar based parts. - - >>> _is_calver(cal_info()) - True - - >>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0018"}) - >>> _is_calver(vnfo) - True - - >>> vnfo = _parse_version_info({'MAJOR': "1", 'MINOR': "023", 'PATCH': "45"}) - >>> _is_calver(vnfo) - False - """ - for field in CalendarInfo._fields: - maybe_val: typ.Any = getattr(nfo, field, None) - if isinstance(maybe_val, int): - return True - - return False - - -TAG_ALIASES: typ.Dict[str, str] = {'a': "alpha", 'b': "beta", 'pre': "rc"} - - -PEP440_TAGS: typ.Dict[str, str] = { - 'alpha': "a", - 'beta' : "b", - 'final': "", - 'rc' : "rc", - 'dev' : "dev", - 'post' : "post", -} - - -VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]] - - -class PatternError(Exception): - pass - - -def _parse_pattern_groups(pattern_groups: PatternGroups) -> FieldValues: - for part_name in pattern_groups.keys(): - is_valid_part_name = ( - part_name in v1patterns.COMPOSITE_PART_PATTERNS - or part_name in v1patterns.PATTERN_PART_FIELDS - ) - if not is_valid_part_name: - err_msg = f"Invalid part '{part_name}'" - raise PatternError(err_msg) - - field_value_items = [ - (field_name, pattern_groups[part_name]) - for part_name, field_name in v1patterns.PATTERN_PART_FIELDS.items() - if part_name in pattern_groups.keys() - ] - - all_fields = [field_name for field_name, _ in field_value_items] - unique_fields = set(all_fields) - duplicate_fields = [f for f in unique_fields if all_fields.count(f) > 1] - - if any(duplicate_fields): - err_msg = f"Multiple parts for same field {duplicate_fields}." - raise PatternError(err_msg) - else: - return dict(field_value_items) - - -def _parse_version_info(pattern_groups: PatternGroups) -> VersionInfo: - """Parse normalized VersionInfo from groups of a matched pattern. - - >>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0099"}) - >>> (vnfo.year, vnfo.month, vnfo.quarter, vnfo.bid, vnfo.tag) - (2018, 11, 4, '0099', 'final') - - >>> vnfo = _parse_version_info({'year': "2018", 'doy': "11", 'bid': "099", 'tag': "b"}) - >>> (vnfo.year, vnfo.month, vnfo.dom, vnfo.bid, vnfo.tag) - (2018, 1, 11, '099', 'beta') - - >>> vnfo = _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "45"}) - >>> (vnfo.major, vnfo.minor, vnfo.patch) - (1, 23, 45) - - >>> vnfo = _parse_version_info({'MAJOR': "1", 'MMM': "023", 'PPPP': "0045"}) - >>> (vnfo.major, vnfo.minor, vnfo.patch) - (1, 23, 45) - """ - field_values = _parse_pattern_groups(pattern_groups) - return _parse_field_values(field_values) - - -def parse_version_info(version_str: str, pattern: str = "{pycalver}") -> VersionInfo: - """Parse normalized VersionInfo. - - >>> vnfo = parse_version_info("v201712.0033-beta", pattern="{pycalver}") - >>> assert vnfo == _parse_version_info({'year': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"}) - - >>> vnfo = parse_version_info("1.23.456", pattern="{semver}") - >>> assert vnfo == _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "456"}) - """ - pattern_tup = v1patterns.compile_pattern(pattern) - match = pattern_tup.regexp.match(version_str) - if match is None: - err_msg = ( - f"Invalid version string '{version_str}' " - f"for pattern '{pattern}'/'{pattern_tup.regexp.pattern}'" - ) - raise PatternError(err_msg) - else: - return _parse_version_info(match.groupdict()) - - -def is_valid(version_str: str, pattern: str = "{pycalver}") -> bool: - """Check if a version matches a pattern. - - >>> is_valid("v201712.0033-beta", pattern="{pycalver}") - True - >>> is_valid("v201712.0033-beta", pattern="{semver}") - False - >>> is_valid("1.2.3", pattern="{semver}") - True - >>> is_valid("v201712.0033-beta", pattern="{semver}") - False - """ - try: - parse_version_info(version_str, pattern) - return True - except PatternError: - return False - - -ID_FIELDS_BY_PART = { - 'MAJOR' : 'major', - 'MINOR' : 'minor', - 'MM' : 'minor', - 'MMM' : 'minor', - 'MMMM' : 'minor', - 'MMMMM' : 'minor', - 'MMMMMM' : 'minor', - 'MMMMMMM': 'minor', - 'PATCH' : 'patch', - 'PP' : 'patch', - 'PPP' : 'patch', - 'PPPP' : 'patch', - 'PPPPP' : 'patch', - 'PPPPPP' : 'patch', - 'PPPPPPP': 'patch', - 'BID' : 'bid', - 'BB' : 'bid', - 'BBB' : 'bid', - 'BBBB' : 'bid', - 'BBBBB' : 'bid', - 'BBBBBB' : 'bid', - 'BBBBBBB': 'bid', -} - - -def format_version(vinfo: VersionInfo, pattern: str) -> str: - """Generate version string. - - >>> import datetime as dt - >>> vinfo = parse_version_info("v201712.0033-beta", pattern="{pycalver}") - >>> vinfo_a = vinfo._replace(**cal_info(date=dt.date(2017, 1, 1))._asdict()) - >>> vinfo_b = vinfo._replace(**cal_info(date=dt.date(2017, 12, 31))._asdict()) - >>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final') - - >>> format_version(vinfo_a, pattern="v{yy}.{BID}{release}") - 'v17.33-beta' - >>> format_version(vinfo_a, pattern="{pep440_version}") - '201701.33b0' - - >>> format_version(vinfo_a, pattern="{pycalver}") - 'v201701.0033-beta' - >>> format_version(vinfo_b, pattern="{pycalver}") - 'v201712.0033-beta' - - >>> format_version(vinfo_a, pattern="v{year}w{iso_week}.{BID}{release}") - 'v2017w00.33-beta' - >>> format_version(vinfo_b, pattern="v{year}w{iso_week}.{BID}{release}") - 'v2017w52.33-beta' - - >>> format_version(vinfo_a, pattern="v{year}d{doy}.{bid}{release}") - 'v2017d001.0033-beta' - >>> format_version(vinfo_b, pattern="v{year}d{doy}.{bid}{release}") - 'v2017d365.0033-beta' - - >>> format_version(vinfo_c, pattern="v{year}w{iso_week}.{BID}-{tag}") - 'v2017w52.33-final' - >>> format_version(vinfo_c, pattern="v{year}w{iso_week}.{BID}{release}") - 'v2017w52.33' - - >>> format_version(vinfo_c, pattern="v{MAJOR}.{MINOR}.{PATCH}") - 'v1.2.34' - >>> format_version(vinfo_c, pattern="v{MAJOR}.{MM}.{PPP}") - 'v1.02.034' - """ - full_pattern = pattern - for part_name, full_part_format in v1patterns.FULL_PART_FORMATS.items(): - full_pattern = full_pattern.replace("{" + part_name + "}", full_part_format) - - kwargs: typ.Dict[str, typ.Union[str, int, None]] = vinfo._asdict() - - tag = vinfo.tag - if tag == 'final': - kwargs['release' ] = "" - kwargs['pep440_tag'] = "" - else: - kwargs['release' ] = "-" + tag - kwargs['pep440_tag'] = PEP440_TAGS[tag] + "0" - - kwargs['release_tag'] = tag - - year = vinfo.year - if year: - kwargs['yy' ] = str(year)[-2:] - kwargs['yyyy'] = year - - kwargs['BID'] = int(vinfo.bid, 10) - - for part_name, field in ID_FIELDS_BY_PART.items(): - val = kwargs[field] - if part_name.lower() == field.lower(): - if isinstance(val, str): - kwargs[part_name] = int(val, base=10) - else: - kwargs[part_name] = val - else: - assert len(set(part_name)) == 1 - padded_len = len(part_name) - kwargs[part_name] = str(val).zfill(padded_len) - - return full_pattern.format(**kwargs) - - -def incr( - old_version: str, - pattern : str = "{pycalver}", - *, - release : str = None, - major : bool = False, - minor : bool = False, - patch : bool = False, - pin_date: bool = False, -) -> typ.Optional[str]: - """Increment version string. - - 'old_version' is assumed to be a string that matches 'pattern' - """ - try: - old_vinfo = parse_version_info(old_version, pattern) - except PatternError as ex: - logger.error(str(ex)) - return None - - cur_vinfo = old_vinfo - - cur_cal_nfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info() - - old_date = (old_vinfo.year or 0 , old_vinfo.month or 0 , old_vinfo.dom or 0) - cur_date = (cur_cal_nfo.year or 0, cur_cal_nfo.month or 0, cur_cal_nfo.dom or 0) - - if old_date <= cur_date: - cur_vinfo = cur_vinfo._replace(**cur_cal_nfo._asdict()) - else: - logger.warning(f"Version appears to be from the future '{old_version}'") - - cur_vinfo = cur_vinfo._replace(bid=lexid.next_id(cur_vinfo.bid)) - - if major: - cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0, patch=0) - if minor: - cur_vinfo = cur_vinfo._replace(minor=cur_vinfo.minor + 1, patch=0) - if patch: - cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1) - - if release: - cur_vinfo = cur_vinfo._replace(tag=release) - - new_version = format_version(cur_vinfo, pattern) - if new_version == old_version: - logger.error("Invalid arguments or pattern, version did not change.") - return None - else: - return new_version - - def to_pep440(version: str) -> str: """Derive pep440 compliant version string from PyCalVer version string. @@ -489,3 +148,28 @@ def to_pep440(version: str) -> str: '201811.7b0' """ return str(pkg_resources.parse_version(version)) + + +def incr_non_cal_parts( + vinfo : VersionInfoType, + release: typ.Optional[str], + major : bool, + minor : bool, + patch : bool, +) -> VersionInfoType: + _bid = vinfo.bid + if int(_bid) < 1000: + # prevent truncation of leading zeros + _bid = str(int(_bid) + 1000) + + vinfo = vinfo._replace(bid=lexid.next_id(_bid)) + + if release: + vinfo = vinfo._replace(tag=release) + if major: + vinfo = vinfo._replace(major=vinfo.major + 1, minor=0, patch=0) + if minor: + vinfo = vinfo._replace(minor=vinfo.minor + 1, patch=0) + if patch: + vinfo = vinfo._replace(patch=vinfo.patch + 1) + return vinfo diff --git a/src/pycalver2/__init__.py b/src/pycalver2/__init__.py deleted file mode 100644 index 094d463..0000000 --- a/src/pycalver2/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file is part of the pycalver project -# https://github.com/mbarkhau/pycalver -# -# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License -# SPDX-License-Identifier: MIT -"""PyCalVer: CalVer for Python Packages.""" - -__version__ = "v202007.1036" diff --git a/src/pycalver2/rewrite.py b/src/pycalver2/rewrite.py deleted file mode 100644 index 03c1f86..0000000 --- a/src/pycalver2/rewrite.py +++ /dev/null @@ -1,178 +0,0 @@ -# This file is part of the pycalver project -# https://github.com/mbarkhau/pycalver -# -# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License -# SPDX-License-Identifier: MIT -"""Rewrite files, updating occurences of version strings.""" - -import io -import typing as typ -import logging - -import pycalver.rewrite as v1rewrite -import pycalver2.version as v2version -import pycalver2.patterns as v2patterns -from pycalver import parse -from pycalver import config - -logger = logging.getLogger("pycalver2.rewrite") - - -def rewrite_lines( - pattern_strs: typ.List[str], - new_vinfo : v2version.VersionInfo, - old_lines : typ.List[str], -) -> typ.List[str]: - """Replace occurances of pattern_strs in old_lines with new_vinfo. - - >>> new_vinfo = v2version.parse_version_info("v201811.0123-beta") - >>> pattern_strs = ['__version__ = "vYYYY0M.BUILD[-TAG]"'] - >>> old_lines = ['__version__ = "v201809.0002-alpha" '] - >>> rewrite_lines(pattern_strs, new_vinfo, old_lines) - ['__version__ = "v201811.0123-beta" '] - - >>> old_lines = ['__version__ = "v201809.0002-alpha" # comment'] - >>> rewrite_lines(pattern_strs, new_vinfo, old_lines) - ['__version__ = "v201811.0123-beta" # comment'] - - >>> pattern_strs = ['__version__ = "YYYY0M.BLD[PYTAGNUM]"'] - >>> old_lines = ['__version__ = "201809.2a0"'] - >>> rewrite_lines(pattern_strs, new_vinfo, old_lines) - ['__version__ = "201811.123b0"'] - """ - new_lines = old_lines[:] - found_patterns = set() - - patterns = [v2patterns.compile_pattern(p) for p in pattern_strs] - matches = parse.iter_matches(old_lines, patterns) - for match in matches: - found_patterns.add(match.pattern.raw) - replacement = v2version.format_version(new_vinfo, match.pattern.raw) - span_l, span_r = match.span - new_line = match.line[:span_l] + replacement + match.line[span_r:] - new_lines[match.lineno] = new_line - - non_matched_patterns = set(pattern_strs) - found_patterns - if non_matched_patterns: - for non_matched_pattern in non_matched_patterns: - logger.error(f"No match for pattern '{non_matched_pattern}'") - compiled_pattern_str = v2patterns.compile_pattern_str(non_matched_pattern) - logger.error(f"Pattern compiles to regex '{compiled_pattern_str}'") - raise v1rewrite.NoPatternMatch("Invalid pattern(s)") - else: - return new_lines - - -def rfd_from_content( - pattern_strs: typ.List[str], - new_vinfo : v2version.VersionInfo, - content : str, -) -> v1rewrite.RewrittenFileData: - r"""Rewrite pattern occurrences with version string. - - >>> new_vinfo = v2version.parse_version_info("v201809.0123") - >>> pattern_strs = ['__version__ = "vYYYY0M.BUILD[-TAG]"'] - >>> content = '__version__ = "v201809.0001-alpha"' - >>> rfd = rfd_from_content(pattern_strs, new_vinfo, content) - >>> rfd.new_lines - ['__version__ = "v201809.0123"'] - >>> - >>> new_vinfo = v2version.parse_version_info("v1.2.3", "vMAJOR.MINOR.PATCH") - >>> pattern_strs = ['__version__ = "vMAJOR.MINOR.PATCH"'] - >>> content = '__version__ = "v1.2.2"' - >>> rfd = rfd_from_content(pattern_strs, new_vinfo, content) - >>> rfd.new_lines - ['__version__ = "v1.2.3"'] - """ - line_sep = v1rewrite.detect_line_sep(content) - old_lines = content.split(line_sep) - new_lines = rewrite_lines(pattern_strs, new_vinfo, old_lines) - return v1rewrite.RewrittenFileData("", line_sep, old_lines, new_lines) - - -def iter_rewritten( - file_patterns: config.PatternsByGlob, - new_vinfo : v2version.VersionInfo, -) -> typ.Iterable[v1rewrite.RewrittenFileData]: - r'''Iterate over files with version string replaced. - - >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "vYYYY0M.BUILD[-TAG]"']} - >>> new_vinfo = v2version.parse_version_info("v201809.0123") - >>> rewritten_datas = iter_rewritten(file_patterns, new_vinfo) - >>> rfd = list(rewritten_datas)[0] - >>> assert rfd.new_lines == [ - ... '# This file is part of the pycalver project', - ... '# https://github.com/mbarkhau/pycalver', - ... '#', - ... '# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License', - ... '# SPDX-License-Identifier: MIT', - ... '"""PyCalVer: CalVer for Python Packages."""', - ... '', - ... '__version__ = "v201809.0123"', - ... '', - ... ] - >>> - ''' - - fobj: typ.IO[str] - - for file_path, pattern_strs in v1rewrite.iter_file_paths(file_patterns): - with file_path.open(mode="rt", encoding="utf-8") as fobj: - content = fobj.read() - - rfd = rfd_from_content(pattern_strs, new_vinfo, content) - yield rfd._replace(path=str(file_path)) - - -def diff( - new_vinfo : v2version.VersionInfo, - file_patterns: config.PatternsByGlob, -) -> str: - r"""Generate diffs of rewritten files. - - >>> new_vinfo = v2version.parse_version_info("v201809.0123") - >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "vYYYY0M.BUILD[-TAG]"']} - >>> diff_str = diff(new_vinfo, file_patterns) - >>> lines = diff_str.split("\n") - >>> lines[:2] - ['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py'] - >>> assert lines[6].startswith('-__version__ = "v2') - >>> assert not lines[6].startswith('-__version__ = "v201809.0123"') - >>> lines[7] - '+__version__ = "v201809.0123"' - """ - - full_diff = "" - fobj: typ.IO[str] - - for file_path, pattern_strs in sorted(v1rewrite.iter_file_paths(file_patterns)): - with file_path.open(mode="rt", encoding="utf-8") as fobj: - content = fobj.read() - - try: - rfd = rfd_from_content(pattern_strs, new_vinfo, content) - except v1rewrite.NoPatternMatch: - # pylint:disable=raise-missing-from ; we support py2, so not an option - errmsg = f"No patterns matched for '{file_path}'" - raise v1rewrite.NoPatternMatch(errmsg) - - rfd = rfd._replace(path=str(file_path)) - lines = v1rewrite.diff_lines(rfd) - if len(lines) == 0: - errmsg = f"No patterns matched for '{file_path}'" - raise v1rewrite.NoPatternMatch(errmsg) - - full_diff += "\n".join(lines) + "\n" - - full_diff = full_diff.rstrip("\n") - return full_diff - - -def rewrite(file_patterns: config.PatternsByGlob, new_vinfo: v2version.VersionInfo) -> None: - """Rewrite project files, updating each with the new version.""" - fobj: typ.IO[str] - - for file_data in iter_rewritten(file_patterns, new_vinfo): - new_content = file_data.line_sep.join(file_data.new_lines) - with io.open(file_data.path, mode="wt", encoding="utf-8") as fobj: - fobj.write(new_content) diff --git a/test/test_cli.py b/test/test_cli.py index 118ff55..1983c89 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -13,8 +13,8 @@ import pytest import pathlib2 as pl from click.testing import CliRunner -import pycalver.config as config -import pycalver.patterns as v1patterns +from pycalver import config +from pycalver import v1patterns from pycalver.__main__ import cli # pylint:disable=redefined-outer-name ; pytest fixtures diff --git a/test/test_parse.py b/test/test_parse.py index ebd90e4..33ec7b6 100644 --- a/test/test_parse.py +++ b/test/test_parse.py @@ -4,8 +4,8 @@ from __future__ import print_function from __future__ import absolute_import from __future__ import unicode_literals -import pycalver.patterns as v1patterns from pycalver import parse +from pycalver import v1patterns SETUP_PY_FIXTURE = """ # setup.py diff --git a/test/test_patterns.py b/test/test_patterns.py index cf96507..37c3b45 100644 --- a/test/test_patterns.py +++ b/test/test_patterns.py @@ -8,8 +8,8 @@ import re import pytest -import pycalver.patterns as v1patterns -import pycalver2.patterns as v2patterns +from pycalver import v1patterns +from pycalver import v2patterns # TODO (mb 2020-09-06): test for v2patterns diff --git a/test/test_rewrite.py b/test/test_rewrite.py index c9107fe..9b2e61e 100644 --- a/test/test_rewrite.py +++ b/test/test_rewrite.py @@ -8,10 +8,11 @@ import copy from test import util from pycalver import config -from pycalver import rewrite as v1rewrite -from pycalver import version as v1version -from pycalver2 import rewrite as v2rewrite -from pycalver2 import version as v2version +from pycalver import rewrite +from pycalver import v1rewrite +from pycalver import v1version +from pycalver import v2rewrite +from pycalver import v2version # pylint:disable=protected-access ; allowed for test code @@ -54,7 +55,7 @@ def test_iter_file_paths(): cfg = config.parse(ctx) assert cfg - _paths_and_patterns = v1rewrite.iter_file_paths(cfg.file_patterns) + _paths_and_patterns = rewrite.iter_path_patterns_items(cfg.file_patterns) file_paths = {str(file_path) for file_path, patterns in _paths_and_patterns} assert file_paths == {"pycalver.toml", "README.md"} @@ -66,7 +67,7 @@ def test_iter_file_globs(): cfg = config.parse(ctx) assert cfg - _paths_and_patterns = v1rewrite.iter_file_paths(cfg.file_patterns) + _paths_and_patterns = rewrite.iter_path_patterns_items(cfg.file_patterns) file_paths = {str(file_path) for file_path, patterns in _paths_and_patterns} assert file_paths == { @@ -86,7 +87,7 @@ def test_error_bad_path(): (project.dir / "setup.py").unlink() try: - list(v1rewrite.iter_file_paths(cfg.file_patterns)) + list(rewrite.iter_path_patterns_items(cfg.file_patterns)) assert False, "expected IOError" except IOError as ex: assert "setup.py" in str(ex) @@ -102,10 +103,11 @@ def test_error_bad_pattern(): patterns["setup.py"] = patterns["setup.py"][0] + "invalid" try: + old_vinfo = v1version.parse_version_info("v201808.0233") new_vinfo = v1version.parse_version_info("v201809.1234") - list(v1rewrite.diff(new_vinfo, patterns)) - assert False, "expected v1rewrite.NoPatternMatch" - except v1rewrite.NoPatternMatch as ex: + list(v1rewrite.diff(old_vinfo, new_vinfo, patterns)) + assert False, "expected rewrite.NoPatternMatch" + except rewrite.NoPatternMatch as ex: assert "setup.py" in str(ex) diff --git a/test/test_version.py b/test/test_version.py index ee010a5..db90f24 100644 --- a/test/test_version.py +++ b/test/test_version.py @@ -9,9 +9,10 @@ import datetime as dt import pytest -import pycalver.version as v1version -import pycalver2.version as v2version -import pycalver.patterns as v1patterns +from pycalver import version +from pycalver import v1version +from pycalver import v2version +from pycalver import v1patterns # import pycalver2.patterns as v2patterns @@ -51,7 +52,7 @@ def test_bump_random(monkeypatch): cur_date = dt.date(2016, 1, 1) + dt.timedelta(days=random.randint(1, 2000)) cur_version = cur_date.strftime("v%Y%m") + ".0001-dev" - monkeypatch.setattr(v1version, 'TODAY', cur_date) + monkeypatch.setattr(version, 'TODAY', cur_date) for _ in range(1000): cur_date += dt.timedelta(days=int((1 + random.random()) ** 10)) @@ -120,7 +121,7 @@ def test_parse_error_empty(): try: v1version.parse_version_info("") assert False - except v1version.PatternError as err: + except version.PatternError as err: assert "Invalid version string" in str(err) @@ -128,7 +129,7 @@ def test_parse_error_noprefix(): try: v1version.parse_version_info("201809.0002") assert False - except v1version.PatternError as err: + except version.PatternError as err: assert "Invalid version string" in str(err) @@ -136,7 +137,7 @@ def test_parse_error_nopadding(): try: v1version.parse_version_info("v201809.2b0") assert False - except v1version.PatternError as err: + except version.PatternError as err: assert "Invalid version string" in str(err) @@ -151,7 +152,7 @@ def test_part_field_mapping_v1(): assert not any(b_extra_names), sorted(b_extra_names) a_fields = set(v1patterns.PATTERN_PART_FIELDS.values()) - b_fields = set(v1version.VersionInfo._fields) + b_fields = set(version.V1VersionInfo._fields) a_extra_fields = a_fields - b_fields b_extra_fields = b_fields - a_fields @@ -200,7 +201,7 @@ def test_v1_parse_versions(pattern_str, line, expected_vinfo): # def test_v2_parse_versions(pattern_str, line, expected_vinfo): def test_v2_parse_versions(): - _vnfo = v2version.parse_version_info("v201712.0033", pattern="vYYYY0M.BUILD[-TAG[NUM]]") + _vnfo = v2version.parse_version_info("v201712.0033", raw_pattern="vYYYY0M.BUILD[-TAG[NUM]]") fvals = {'year_y': 2017, 'month': 12, 'bid': "0033"} assert _vnfo == v2version._parse_version_info(fvals) @@ -217,24 +218,24 @@ def test_make_segments(): def test_v2_format_version(): - pattern = "vYYYY0M.BUILD[-TAG[NUM]]" + version_pattern = "vYYYY0M.BUILD[-TAG[NUM]]" in_version = "v200701.0033-beta" - vinfo = v2version.parse_version_info(in_version, pattern=pattern) - out_version = v2version.format_version(vinfo, pattern=pattern) + vinfo = v2version.parse_version_info(in_version, raw_pattern=version_pattern) + out_version = v2version.format_version(vinfo, raw_pattern=version_pattern) assert in_version == out_version - result = v2version.format_version(vinfo, pattern="v0Y.BUILD[-TAG]") + result = v2version.format_version(vinfo, raw_pattern="v0Y.BUILD[-TAG]") assert result == "v07.0033-beta" - result = v2version.format_version(vinfo, pattern="vYY.BLD[-TAG]") + result = v2version.format_version(vinfo, raw_pattern="vYY.BLD[-TAG]") assert result == "v7.33-beta" - result = v2version.format_version(vinfo, pattern="vYY.BLD-TAG") + result = v2version.format_version(vinfo, raw_pattern="vYY.BLD-TAG") assert result == "v7.33-beta" - result = v2version.format_version(vinfo, pattern='__version__ = "YYYY.BUILD[-TAG]"') + result = v2version.format_version(vinfo, raw_pattern='__version__ = "YYYY.BUILD[-TAG]"') assert result == '__version__ = "2007.0033-beta"' - result = v2version.format_version(vinfo, pattern='__version__ = "YYYY.BLD"') + result = v2version.format_version(vinfo, raw_pattern='__version__ = "YYYY.BLD"') assert result == '__version__ = "2007.33"' From 5a64983b8e72858d35402ff541e29b2ee84180d3 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 24 Sep 2020 11:16:02 +0000 Subject: [PATCH 26/98] formatting with segment tree --- src/pycalver/__main__.py | 17 ++-- src/pycalver/config.py | 29 ++++-- src/pycalver/patterns.py | 5 + src/pycalver/rewrite.py | 5 + src/pycalver/v1cli.py | 3 +- src/pycalver/v1patterns.py | 7 +- src/pycalver/v1rewrite.py | 6 +- src/pycalver/v1version.py | 50 ++++++---- src/pycalver/v2cli.py | 3 +- src/pycalver/v2patterns.py | 76 ++++++++++----- src/pycalver/v2rewrite.py | 33 ++++--- src/pycalver/v2version.py | 185 +++++++++++++++++++++++++++++-------- src/pycalver/version.py | 56 ++++------- test/test_version.py | 4 +- 14 files changed, 325 insertions(+), 154 deletions(-) diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 2806a45..8f3dc41 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -24,6 +24,7 @@ from . import rewrite from . import version from . import v1version from . import v2version +from . import v1patterns _VERBOSE = 0 @@ -104,13 +105,14 @@ def test( ) -> None: """Increment a version number for demo purposes.""" _configure_logging(verbose=max(_VERBOSE, verbose)) + raw_pattern = pattern if release: _validate_release_tag(release) new_version = _incr( old_version, - raw_pattern=pattern, + raw_pattern=raw_pattern, release=release, major=major, minor=minor, @@ -118,7 +120,7 @@ def test( pin_date=pin_date, ) if new_version is None: - logger.error(f"Invalid version '{old_version}' and/or pattern '{pattern}'.") + logger.error(f"Invalid version '{old_version}' and/or pattern '{raw_pattern}'.") sys.exit(1) pep440_version = version.to_pep440(new_version) @@ -185,7 +187,7 @@ def _print_diff(cfg: config.Config, new_version: str) -> None: def _incr( old_version: str, - raw_pattern: str = "{pycalver}", + raw_pattern: str, *, release : str = None, major : bool = False, @@ -193,9 +195,10 @@ def _incr( patch : bool = False, pin_date: bool = False, ) -> typ.Optional[str]: - is_new_pattern = "{" in raw_pattern and "}" in raw_pattern - if is_new_pattern: - return v2version.incr( + v1_parts = list(v1patterns.PART_PATTERNS) + list(v1patterns.FULL_PART_FORMATS) + has_v1_part = any("{" + part + "}" in raw_pattern for part in v1_parts) + if has_v1_part: + return v1version.incr( old_version, raw_pattern=raw_pattern, release=release, @@ -205,7 +208,7 @@ def _incr( pin_date=pin_date, ) else: - return v1version.incr( + return v2version.incr( old_version, raw_pattern=raw_pattern, release=release, diff --git a/src/pycalver/config.py b/src/pycalver/config.py index d1ff1ea..ecfb774 100644 --- a/src/pycalver/config.py +++ b/src/pycalver/config.py @@ -1,10 +1,11 @@ # This file is part of the pycalver project # https://gitlab.com/mbarkhau/pycalver # -# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License # SPDX-License-Identifier: MIT """Parse setup.cfg or pycalver.cfg files.""" +import re import glob import typing as typ import logging @@ -125,7 +126,7 @@ def _debug_str(cfg: Config) -> str: "\n file_patterns={", ] - for filepath, patterns in cfg.file_patterns.items(): + for filepath, patterns in sorted(cfg.file_patterns.items()): for pattern in patterns: cfg_str_parts.append(f"\n '{filepath}': '{pattern.raw_pattern}',") @@ -261,6 +262,14 @@ def _parse_config(raw_cfg: RawConfig) -> Config: is_new_pattern = "{" not in version_pattern and "}" not in version_pattern + if is_new_pattern: + invalid_chars = re.search(r"([\s]+)", raw_cfg['version_pattern']) + if invalid_chars: + raise ValueError( + f"Invalid character(s) '{invalid_chars.group(1)}'" + f" in pycalver.version_pattern = {raw_cfg['version_pattern']}" + ) + # TODO (mb 2020-09-18): Validate Pattern # detect YY with WW or UU -> suggest GG with VV # detect YYMM -> suggest YY0M @@ -372,17 +381,17 @@ def _parse_raw_config(ctx: ProjectContext) -> RawConfig: def parse(ctx: ProjectContext) -> MaybeConfig: """Parse config file if available.""" - if not ctx.config_filepath.exists(): + if ctx.config_filepath.exists(): + try: + raw_cfg = _parse_raw_config(ctx) + return _parse_config(raw_cfg) + except (TypeError, ValueError) as ex: + logger.warning(f"Couldn't parse {ctx.config_rel_path}: {str(ex)}") + return None + else: logger.warning(f"File not found: {ctx.config_rel_path}") return None - try: - raw_cfg = _parse_raw_config(ctx) - return _parse_config(raw_cfg) - except (TypeError, ValueError) as ex: - logger.warning(f"Couldn't parse {ctx.config_rel_path}: {str(ex)}") - return None - DEFAULT_CONFIGPARSER_BASE_TMPL = """ [pycalver] diff --git a/src/pycalver/patterns.py b/src/pycalver/patterns.py index b5ad928..157d62b 100644 --- a/src/pycalver/patterns.py +++ b/src/pycalver/patterns.py @@ -1,3 +1,8 @@ +# This file is part of the pycalver project +# https://github.com/mbarkhau/pycalver +# +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# SPDX-License-Identifier: MIT import typing as typ diff --git a/src/pycalver/rewrite.py b/src/pycalver/rewrite.py index cbd1a1a..93a9b53 100644 --- a/src/pycalver/rewrite.py +++ b/src/pycalver/rewrite.py @@ -1,3 +1,8 @@ +# This file is part of the pycalver project +# https://github.com/mbarkhau/pycalver +# +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# SPDX-License-Identifier: MIT import typing as typ import difflib diff --git a/src/pycalver/v1cli.py b/src/pycalver/v1cli.py index d33ba12..b34d0b2 100755 --- a/src/pycalver/v1cli.py +++ b/src/pycalver/v1cli.py @@ -27,7 +27,8 @@ def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.C return cfg version_tags.sort(reverse=True) - logger.debug(f"found {len(version_tags)} tags: {version_tags[:2]}") + _debug_tags = ", ".join(version_tags[:3]) + logger.debug(f"found tags: {_debug_tags} ... ({len(version_tags)} in total)") latest_version_tag = version_tags[0] latest_version_pep440 = version.to_pep440(latest_version_tag) if latest_version_tag <= cfg.current_version: diff --git a/src/pycalver/v1patterns.py b/src/pycalver/v1patterns.py index 2171ac3..a3ace0c 100644 --- a/src/pycalver/v1patterns.py +++ b/src/pycalver/v1patterns.py @@ -34,6 +34,7 @@ import re import typing as typ import logging +from . import utils from .patterns import RE_PATTERN_ESCAPES from .patterns import Pattern @@ -80,8 +81,8 @@ PART_PATTERNS = { 'month' : r"(?:0[0-9]|1[0-2])", 'month_short': r"(?:1[0-2]|[1-9])", 'build_no' : r"\d{4,}", - 'pep440_tag' : r"(?:a|b|dev|rc|post)?\d*", - 'tag' : r"(?:alpha|beta|dev|rc|post|final)", + 'pep440_tag' : r"(?:post|dev|rc|a|b)?\d*", + 'tag' : r"(?:preview|final|alpha|beta|post|pre|dev|rc|a|b|c|r)", 'yy' : r"\d{2}", 'yyyy' : r"\d{4}", 'quarter' : r"[1-4]", @@ -183,6 +184,7 @@ def _replace_pattern_parts(pattern: str) -> str: named_part_pattern = f"(?P<{part_name}>{part_pattern})" placeholder = "\u005c{" + part_name + "\u005c}" pattern = pattern.replace(placeholder, named_part_pattern) + return pattern @@ -214,6 +216,7 @@ def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[s return re.compile(pattern_str) +@utils.memo def compile_pattern(version_pattern: str, raw_pattern: typ.Optional[str] = None) -> Pattern: _raw_pattern = version_pattern if raw_pattern is None else raw_pattern regexp = _compile_pattern_re(version_pattern, _raw_pattern) diff --git a/src/pycalver/v1rewrite.py b/src/pycalver/v1rewrite.py index 2cdb487..1bf652c 100644 --- a/src/pycalver/v1rewrite.py +++ b/src/pycalver/v1rewrite.py @@ -37,11 +37,11 @@ def rewrite_lines( >>> rewrite_lines(patterns, new_vinfo, ['__version__ = "201809.2b0"']) ['__version__ = "201811.123b0"'] """ - new_lines = old_lines[:] - found_patterns = set() + found_patterns: typ.Set[Pattern] = set() + new_lines = old_lines[:] for match in parse.iter_matches(old_lines, patterns): - found_patterns.add(match.pattern.raw_pattern) + found_patterns.add(match.pattern) replacement = v1version.format_version(new_vinfo, match.pattern.raw_pattern) span_l, span_r = match.span new_line = match.line[:span_l] + replacement + match.line[span_r:] diff --git a/src/pycalver/v1version.py b/src/pycalver/v1version.py index d007cdd..11fdb82 100644 --- a/src/pycalver/v1version.py +++ b/src/pycalver/v1version.py @@ -9,6 +9,8 @@ import typing as typ import logging import datetime as dt +import lexid + from . import version from . import v1patterns @@ -18,15 +20,19 @@ logger = logging.getLogger("pycalver.v1version") CalInfo = typ.Union[version.V1CalendarInfo, version.V1VersionInfo] -def _is_later_than(old: CalInfo, new: CalInfo) -> bool: - """Is old > new based on non None fields.""" +def _is_cal_gt(left: CalInfo, right: CalInfo) -> bool: + """Is left > right for non-None fields.""" + + lvals = [] + rvals = [] for field in version.V1CalendarInfo._fields: - aval = getattr(old, field) - bval = getattr(new, field) - if not (aval is None or bval is None): - if aval > bval: - return True - return False + lval = getattr(left , field) + rval = getattr(right, field) + if not (lval is None or rval is None): + lvals.append(lval) + rvals.append(rval) + + return lvals > rvals def _ver_to_cal_info(vnfo: version.V1VersionInfo) -> version.V1CalendarInfo: @@ -235,7 +241,7 @@ def parse_version_info(version_str: str, raw_pattern: str = "{pycalver}") -> ver if match is None: err_msg = ( f"Invalid version string '{version_str}' " - f"for pattern '{raw_pattern}'/'{pattern.regexp}'" + f"for pattern '{raw_pattern}'/'{pattern.regexp.pattern}'" ) raise version.PatternError(err_msg) else: @@ -386,19 +392,23 @@ def incr( cur_cinfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info() - if _is_later_than(old_vinfo, cur_cinfo): - cur_vinfo = old_vinfo._replace(**cur_cinfo._asdict()) - else: - logger.warning(f"Version appears to be from the future '{old_version}'") + if _is_cal_gt(old_vinfo, cur_cinfo): + logger.warning(f"Old version appears to be from the future '{old_version}'") cur_vinfo = old_vinfo + else: + cur_vinfo = old_vinfo._replace(**cur_cinfo._asdict()) + + cur_vinfo = cur_vinfo._replace(bid=lexid.next_id(cur_vinfo.bid)) + + if release: + cur_vinfo = cur_vinfo._replace(tag=release) + if major: + cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0, patch=0) + if minor: + cur_vinfo = cur_vinfo._replace(minor=cur_vinfo.minor + 1, patch=0) + if patch: + cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1) - cur_vinfo = version.incr_non_cal_parts( - cur_vinfo, - release, - major, - minor, - patch, - ) new_version = format_version(cur_vinfo, raw_pattern) if new_version == old_version: logger.error("Invalid arguments or pattern, version did not change.") diff --git a/src/pycalver/v2cli.py b/src/pycalver/v2cli.py index 243f2f5..f29ecac 100644 --- a/src/pycalver/v2cli.py +++ b/src/pycalver/v2cli.py @@ -27,7 +27,8 @@ def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.C return cfg version_tags.sort(reverse=True) - logger.debug(f"found {len(version_tags)} tags: {version_tags[:2]}") + _debug_tags = ", ".join(version_tags[:3]) + logger.debug(f"found tags: {_debug_tags} ... ({len(version_tags)} in total)") latest_version_tag = version_tags[0] latest_version_pep440 = version.to_pep440(latest_version_tag) if latest_version_tag <= cfg.current_version: diff --git a/src/pycalver/v2patterns.py b/src/pycalver/v2patterns.py index 57a52b9..8001208 100644 --- a/src/pycalver/v2patterns.py +++ b/src/pycalver/v2patterns.py @@ -35,6 +35,7 @@ import re import typing as typ import logging +from . import utils from .patterns import RE_PATTERN_ESCAPES from .patterns import Pattern @@ -84,8 +85,8 @@ PART_PATTERNS = { 'PATCH': r"[0-9]+", 'BUILD': r"[0-9]+", 'BLD' : r"[1-9][0-9]*", - 'TAG' : r"(?:alpha|beta|dev|pre|rc|post|final)", - 'PYTAG': r"(?:a|b|dev|rc|post)", + 'TAG' : r"(?:preview|final|alpha|beta|post|pre|dev|rc|a|b|c|r)", + 'PYTAG': r"(?:post|dev|rc|a|b)", 'NUM' : r"[0-9]+", } @@ -203,24 +204,56 @@ PART_FORMATS: typ.Dict[str, typ.Callable[[FieldValue], str]] = { } +def _convert_to_pep440(version_pattern: str) -> str: + # NOTE (mb 2020-09-20): This does not support some + # corner cases as specified in PEP440, in particular + # related to post and dev releases. + + version_pattern = version_pattern.lstrip("v") + + part_names = list(PATTERN_PART_FIELDS.keys()) + part_names.sort(key=len, reverse=True) + if version_pattern == "vYYYY0M.BUILD[-TAG]": + return "YYYY0M.BLD[PYTAGNUM]" + + # TODO (mb 2020-09-20) + raise NotImplementedError + + +def normalize_pattern(version_pattern: str, raw_pattern: str) -> str: + normalized_pattern = raw_pattern + if "{version}" in raw_pattern: + normalized_pattern = normalized_pattern.replace("{version}", version_pattern) + + if "{pep440_version}" in normalized_pattern: + pep440_version_pattern = _convert_to_pep440(version_pattern) + normalized_pattern = normalized_pattern.replace("{pep440_version}", pep440_version_pattern) + + return normalized_pattern + + def _replace_pattern_parts(pattern: str) -> str: # The pattern is escaped, so that everything besides the format # string variables is treated literally. - if "[" in pattern and "]" in pattern: - pattern = pattern.replace("[", "(?:") - pattern = pattern.replace("]", ")?") + while True: + new_pattern, n = re.subn(r"([^\\]|^)\[", r"\1(?:", pattern) + new_pattern, m = re.subn(r"([^\\]|^)\]", r"\1)?" , new_pattern) + pattern = new_pattern + if n + m == 0: + break + + SortKey = typ.Tuple[int, int] + PostitionedPart = typ.Tuple[int, int, str] + part_patterns_by_index: typ.Dict[SortKey, PostitionedPart] = {} - part_patterns_by_index: typ.Dict[typ.Tuple[int, int], typ.Tuple[int, int, str]] = {} for part_name, part_pattern in PART_PATTERNS.items(): start_idx = pattern.find(part_name) - if start_idx < 0: - continue - - field = PATTERN_PART_FIELDS[part_name] - named_part_pattern = f"(?P<{field}>{part_pattern})" - end_idx = start_idx + len(part_name) - sort_key = (-end_idx, -len(part_name)) - part_patterns_by_index[sort_key] = (start_idx, end_idx, named_part_pattern) + if start_idx >= 0: + field = PATTERN_PART_FIELDS[part_name] + named_part_pattern = f"(?P<{field}>{part_pattern})" + end_idx = start_idx + len(part_name) + sort_key = (-end_idx, -len(part_name)) + part_patterns_by_index[sort_key] = (start_idx, end_idx, named_part_pattern) # NOTE (mb 2020-09-17): The sorting is done so that we process items: # - right before left @@ -238,26 +271,21 @@ def _replace_pattern_parts(pattern: str) -> str: def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[str]: - escaped_pattern = raw_pattern + normalized_pattern = normalize_pattern(version_pattern, raw_pattern) + escaped_pattern = normalized_pattern for char, escaped in RE_PATTERN_ESCAPES: # [] braces are used for optional parts, such as [-TAG]/[-beta] - is_semantic_char = char in "[]" + # and need to be escaped manually. + is_semantic_char = char in "[]\\" if not is_semantic_char: # escape it so it is a literal in the re pattern escaped_pattern = escaped_pattern.replace(char, escaped) - escaped_pattern = raw_pattern.replace("[", "\u005c[").replace("]", "\u005c]") - normalized_pattern = escaped_pattern.replace("{version}", version_pattern) - print(">>>>", (raw_pattern ,)) - print("....", (escaped_pattern ,)) - print("....", (normalized_pattern,)) - print("<<<<", (normalized_pattern,)) - - # TODO (mb 2020-09-19): replace {version} etc with version_pattern pattern_str = _replace_pattern_parts(escaped_pattern) return re.compile(pattern_str) +@utils.memo def compile_pattern(version_pattern: str, raw_pattern: typ.Optional[str] = None) -> Pattern: _raw_pattern = version_pattern if raw_pattern is None else raw_pattern regexp = _compile_pattern_re(version_pattern, _raw_pattern) diff --git a/src/pycalver/v2rewrite.py b/src/pycalver/v2rewrite.py index c82222a..49e4c22 100644 --- a/src/pycalver/v2rewrite.py +++ b/src/pycalver/v2rewrite.py @@ -14,6 +14,7 @@ from . import config from . import rewrite from . import version from . import v2version +from . import v2patterns from .patterns import Pattern logger = logging.getLogger("pycalver.v2rewrite") @@ -41,12 +42,15 @@ def rewrite_lines( >>> rewrite_lines(patterns, new_vinfo, old_lines) ['__version__ = "201811.123b0"'] """ - new_lines = old_lines[:] - found_patterns = set() + found_patterns: typ.Set[Pattern] = set() + new_lines = old_lines[:] for match in parse.iter_matches(old_lines, patterns): - found_patterns.add(match.pattern.raw_pattern) - replacement = v2version.format_version(new_vinfo, match.pattern.raw_pattern) + found_patterns.add(match.pattern) + normalized_pattern = v2patterns.normalize_pattern( + match.pattern.version_pattern, match.pattern.raw_pattern + ) + replacement = v2version.format_version(new_vinfo, normalized_pattern) span_l, span_r = match.span new_line = match.line[:span_l] + replacement + match.line[span_r:] new_lines[match.lineno] = new_line @@ -93,6 +97,18 @@ def rfd_from_content( return rewrite.RewrittenFileData(path, line_sep, old_lines, new_lines) +def _patterns_with_change( + old_vinfo: version.V2VersionInfo, new_vinfo: version.V2VersionInfo, patterns: typ.List[Pattern] +) -> int: + patterns_with_change = 0 + for pattern in patterns: + old_str = v2version.format_version(old_vinfo, pattern.raw_pattern) + new_str = v2version.format_version(new_vinfo, pattern.raw_pattern) + if old_str != new_str: + patterns_with_change += 1 + return patterns_with_change + + def iter_rewritten( file_patterns: config.PatternsByFile, new_vinfo : version.V2VersionInfo, @@ -159,13 +175,6 @@ def diff( with file_path.open(mode="rt", encoding="utf-8") as fobj: content = fobj.read() - patterns_with_change = 0 - for pattern in patterns: - old_str = v2version.format_version(old_vinfo, pattern.raw_pattern) - new_str = v2version.format_version(new_vinfo, pattern.raw_pattern) - if old_str != new_str: - patterns_with_change += 1 - try: rfd = rfd_from_content(patterns, new_vinfo, content) except rewrite.NoPatternMatch: @@ -175,6 +184,8 @@ def diff( rfd = rfd._replace(path=str(file_path)) lines = rewrite.diff_lines(rfd) + + patterns_with_change = _patterns_with_change(old_vinfo, new_vinfo, patterns) if len(lines) == 0 and patterns_with_change > 0: errmsg = f"No patterns matched for '{file_path}'" raise rewrite.NoPatternMatch(errmsg) diff --git a/src/pycalver/v2version.py b/src/pycalver/v2version.py index 72c7e9b..22a93af 100644 --- a/src/pycalver/v2version.py +++ b/src/pycalver/v2version.py @@ -9,6 +9,8 @@ import typing as typ import logging import datetime as dt +import lexid + from . import version from . import v2patterns @@ -18,15 +20,19 @@ logger = logging.getLogger("pycalver.v2version") CalInfo = typ.Union[version.V2CalendarInfo, version.V2VersionInfo] -def _is_later_than(old: CalInfo, new: CalInfo) -> bool: - """Is old > new based on non None fields.""" - for field in version.V1CalendarInfo._fields: - aval = getattr(old, field) - bval = getattr(new, field) - if not (aval is None or bval is None): - if aval > bval: - return True - return False +def _is_cal_gt(left: CalInfo, right: CalInfo) -> bool: + """Is left > right for non-None fields.""" + + lvals = [] + rvals = [] + for field in version.V2CalendarInfo._fields: + lval = getattr(left , field) + rval = getattr(right, field) + if not (lval is None or rval is None): + lvals.append(lval) + rvals.append(rval) + + return lvals > rvals def _ver_to_cal_info(vinfo: version.V2VersionInfo) -> version.V2CalendarInfo: @@ -268,9 +274,10 @@ def is_valid(version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-TAG]") -> bool TemplateKwargs = typ.Dict[str, typ.Union[str, int, None]] +PartValues = typ.List[typ.Tuple[str, str]] -def _format_part_values(vinfo: version.V2VersionInfo) -> typ.Dict[str, str]: +def _format_part_values(vinfo: version.V2VersionInfo) -> PartValues: """Generate kwargs for template from minimal V2VersionInfo. The V2VersionInfo Tuple only has the minimal representation @@ -279,14 +286,14 @@ def _format_part_values(vinfo: version.V2VersionInfo) -> typ.Dict[str, str]: representation '09' for '0M'. >>> vinfo = parse_version_info("v200709.1033-beta", pattern="vYYYY0M.BUILD[-TAG]") - >>> kwargs = _format_part_values(vinfo) + >>> kwargs = dict(_format_part_values(vinfo)) >>> (kwargs['YYYY'], kwargs['0M'], kwargs['BUILD'], kwargs['TAG']) ('2007', '09', '1033', 'beta') >>> (kwargs['YY'], kwargs['0Y'], kwargs['MM'], kwargs['PYTAG']) ('7', '07', '9', 'b') >>> vinfo = parse_version_info("200709.1033b1", pattern="YYYY0M.BLD[PYTAGNUM]") - >>> kwargs = _format_part_values(vinfo) + >>> kwargs = dict(_format_part_values(vinfo)) >>> (kwargs['YYYY'], kwargs['0M'], kwargs['BUILD'], kwargs['PYTAG'], kwargs['NUM']) ('2007', '09', '1033', 'b', '1') """ @@ -299,7 +306,7 @@ def _format_part_values(vinfo: version.V2VersionInfo) -> typ.Dict[str, str]: format_fn = v2patterns.PART_FORMATS[part] kwargs[part] = format_fn(field_val) - return kwargs + return sorted(kwargs.items(), key=lambda item: -len(item[0])) def _make_segments(raw_pattern: str) -> typ.List[str]: @@ -345,12 +352,63 @@ def _clear_zero_segments( return non_zero_segs +Segment = str +# mypy limitation wrt. cyclic definition +# SegmentTree = typ.List[typ.Union[Segment, "SegmentTree"]] +SegmentTree = typ.Any + + +def _parse_segment_tree(raw_pattern: str) -> SegmentTree: + """Generate segment tree from pattern string. + + >>> tree = _parse_segment_tree("aa[bb[cc]]") + >>> assert tree == ["aa", ["bb", ["cc"]]] + >>> tree = _parse_segment_tree("aa[bb[cc]dd[ee]ff]gg") + >>> assert tree == ["aa", ["bb", ["cc"], "dd", ["ee"], "ff"], "gg"] + """ + + internal_root: SegmentTree = [] + branch_stack : typ.List[SegmentTree] = [internal_root] + segment_start_index = -1 + + raw_pattern = "[" + raw_pattern + "]" + + for i, char in enumerate(raw_pattern): + is_escaped = i > 0 and raw_pattern[i - 1] == "\\" + if char in "[]" and not is_escaped: + start = segment_start_index + 1 + end = i + if start < end: + branch_stack[-1].append(raw_pattern[start:end]) + + if char == "[": + new_branch: SegmentTree = [] + branch_stack[-1].append(new_branch) + branch_stack.append(new_branch) + segment_start_index = i + elif char == "]": + if len(branch_stack) == 1: + err = f"Unbalanced brace(s) in '{raw_pattern}'" + raise ValueError(err) + + branch_stack.pop() + segment_start_index = i + else: + raise NotImplementedError("Unreachable") + + if len(branch_stack) > 1: + err = f"Unclosed brace in '{raw_pattern}'" + raise ValueError(err) + + return internal_root[0] + + def _format_segments( - vinfo : version.V2VersionInfo, pattern_segs: typ.List[str], + part_values : PartValues, ) -> typ.List[str]: - kwargs = _format_part_values(vinfo) - part_values = sorted(kwargs.items(), key=lambda item: -len(item[0])) + # NOTE (mb 2020-09-21): Old implementaion that doesn't cover corner + # cases relating to escaped braces. is_zero_segment = [True] * len(pattern_segs) @@ -361,23 +419,25 @@ def _format_segments( idx_r = len(pattern_segs) - 1 while idx_l <= idx_r: # NOTE (mb 2020-09-18): All segments are optional, - # except the most left and the most right, - # i.e the ones NOT surrounded by braces. - # Empty string is a valid segment. - is_optional = idx_l > 0 + # except the most left and the most right. + # In other words the ones NOT surrounded by braces are + # required. Empty string is a valid segment. + is_required_seg = idx_l == 0 seg_l = pattern_segs[idx_l] seg_r = pattern_segs[idx_r] for part, part_value in part_values: if part in seg_l: - seg_l = seg_l.replace(part, part_value) - if not (is_optional and str(part_value) == version.ZERO_VALUES.get(part)): + seg_l = seg_l.replace(part, part_value) + is_zero_seg = str(part_value) == version.ZERO_VALUES.get(part) + if is_required_seg or not is_zero_seg: is_zero_segment[idx_l] = False if part in seg_r: - seg_r = seg_r.replace(part, part_value) - if not (is_optional and str(part_value) == version.ZERO_VALUES[part]): + seg_r = seg_r.replace(part, part_value) + is_zero_seg = str(part_value) == version.ZERO_VALUES.get(part) + if is_required_seg or not is_zero_seg: is_zero_segment[idx_r] = False formatted_segs_l.append(seg_l) @@ -391,6 +451,43 @@ def _format_segments( return _clear_zero_segments(formatted_segs, is_zero_segment) +FormattedSegmentParts = typ.List[str] + + +def _format_segment_tree( + seg_tree: SegmentTree, + part_values : PartValues, +) -> FormattedSegmentParts: + result_parts = [] + for seg in seg_tree: + if isinstance(seg, list): + result_parts.extend(_format_segment_tree(seg, part_values)) + else: + # NOTE (mb 2020-09-24): If a segment has any zero parts, + # the whole segment is skipped. + is_zero_seg = False + formatted_seg = seg + # unescape braces + formatted_seg = formatted_seg.replace(r"\[", r"[") + formatted_seg = formatted_seg.replace(r"\]", r"]") + # replace non zero parts + for part, part_value in part_values: + if part in formatted_seg: + is_zero_part = ( + part in version.ZERO_VALUES + and str(part_value) == version.ZERO_VALUES[part] + ) + if is_zero_part: + is_zero_seg = True + else: + formatted_seg = formatted_seg.replace(part, part_value) + + if not is_zero_seg: + result_parts.append(formatted_seg) + + return result_parts + + def format_version(vinfo: version.V2VersionInfo, raw_pattern: str) -> str: """Generate version string. @@ -477,10 +574,15 @@ def format_version(vinfo: version.V2VersionInfo, raw_pattern: str) -> str: >>> format_version(vinfo_d, pattern='__version__ = "vMAJOR[.MINOR[.PATCH[-TAG[NUM]]]]"') '__version__ = "v1.0.0-rc2"' """ - pattern_segs = _make_segments(raw_pattern) - formatted_segs = _format_segments(vinfo, pattern_segs) + part_values = _format_part_values(vinfo) - return "".join(formatted_segs) + # pattern_segs = _make_segments(raw_pattern) + # formatted_segs = _format_segments(pattern_segs, part_values) + # version_str = "".join(formatted_segs) + + seg_tree = _parse_segment_tree(raw_pattern) + version_str_parts = _format_segment_tree(seg_tree, part_values) + return "".join(version_str_parts) def incr( @@ -505,19 +607,30 @@ def incr( cur_cinfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info() - if _is_later_than(old_vinfo, cur_cinfo): - logger.warning(f"Version appears to be from the future '{old_version}'") + if _is_cal_gt(old_vinfo, cur_cinfo): + logger.warning(f"Old version appears to be from the future '{old_version}'") cur_vinfo = old_vinfo else: cur_vinfo = old_vinfo._replace(**cur_cinfo._asdict()) - cur_vinfo = version.incr_non_cal_parts( - cur_vinfo, - release, - major, - minor, - patch, - ) + # prevent truncation of leading zeros + if int(cur_vinfo.bid) < 1000: + cur_vinfo = cur_vinfo._replace(bid=str(int(cur_vinfo.bid) + 1000)) + + cur_vinfo = cur_vinfo._replace(bid=lexid.next_id(cur_vinfo.bid)) + + if release: + cur_vinfo = cur_vinfo._replace(tag=release) + if major: + cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0, patch=0) + if minor: + cur_vinfo = cur_vinfo._replace(minor=cur_vinfo.minor + 1, patch=0) + if patch: + cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1) + + # TODO (mb 2020-09-20): New Rollover Behaviour: + # Reset major, minor, patch to zero if any part to the left of it is incremented + new_version = format_version(cur_vinfo, raw_pattern) if new_version == old_version: logger.error("Invalid arguments or pattern, version did not change.") diff --git a/src/pycalver/version.py b/src/pycalver/version.py index 76a9acb..c3b06bf 100644 --- a/src/pycalver/version.py +++ b/src/pycalver/version.py @@ -1,7 +1,11 @@ +# This file is part of the pycalver project +# https://github.com/mbarkhau/pycalver +# +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# SPDX-License-Identifier: MIT import typing as typ import datetime as dt -import lexid import pkg_resources MaybeInt = typ.Optional[int] @@ -71,9 +75,6 @@ class V2VersionInfo(typ.NamedTuple): pytag : str -VersionInfoType = typ.TypeVar('VersionInfoType', V1VersionInfo, V2VersionInfo) - - # The test suite may replace this. TODAY = dt.datetime.utcnow().date() @@ -81,7 +82,7 @@ TODAY = dt.datetime.utcnow().date() TAG_BY_PEP440_TAG = { 'a' : 'alpha', 'b' : 'beta', - "" : 'final', + '' : 'final', 'rc' : 'rc', 'dev' : 'dev', 'post': 'post', @@ -89,13 +90,19 @@ TAG_BY_PEP440_TAG = { PEP440_TAG_BY_TAG = { - 'alpha': "a", - 'beta' : "b", - 'final': "", - 'pre' : "rc", - 'rc' : "rc", - 'dev' : "dev", - 'post' : "post", + 'a' : 'a', + 'b' : 'b', + 'dev' : 'dev', + 'alpha' : 'a', + 'beta' : 'b', + 'preview': 'rc', + 'pre' : 'rc', + 'rc' : 'rc', + 'c' : 'rc', + 'final' : '', + 'post' : 'post', + 'r' : 'post', + 'rev' : 'post', } assert set(TAG_BY_PEP440_TAG.keys()) == set(PEP440_TAG_BY_TAG.values()) @@ -148,28 +155,3 @@ def to_pep440(version: str) -> str: '201811.7b0' """ return str(pkg_resources.parse_version(version)) - - -def incr_non_cal_parts( - vinfo : VersionInfoType, - release: typ.Optional[str], - major : bool, - minor : bool, - patch : bool, -) -> VersionInfoType: - _bid = vinfo.bid - if int(_bid) < 1000: - # prevent truncation of leading zeros - _bid = str(int(_bid) + 1000) - - vinfo = vinfo._replace(bid=lexid.next_id(_bid)) - - if release: - vinfo = vinfo._replace(tag=release) - if major: - vinfo = vinfo._replace(major=vinfo.major + 1, minor=0, patch=0) - if minor: - vinfo = vinfo._replace(minor=vinfo.minor + 1, patch=0) - if patch: - vinfo = vinfo._replace(patch=vinfo.patch + 1) - return vinfo diff --git a/test/test_version.py b/test/test_version.py index db90f24..6e469cf 100644 --- a/test/test_version.py +++ b/test/test_version.py @@ -218,8 +218,8 @@ def test_make_segments(): def test_v2_format_version(): - version_pattern = "vYYYY0M.BUILD[-TAG[NUM]]" - in_version = "v200701.0033-beta" + version_pattern = "vYYYY0M.BUILD[-TAG[NUM]]" + in_version = "v200701.0033-beta" vinfo = v2version.parse_version_info(in_version, raw_pattern=version_pattern) out_version = v2version.format_version(vinfo, raw_pattern=version_pattern) From f21c0f1d0ff00836d08a2b9eb6f6906f580dcd51 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 24 Sep 2020 11:16:48 +0000 Subject: [PATCH 27/98] cleanup old seg formatting approach --- src/pycalver/v2version.py | 79 --------------------------------------- 1 file changed, 79 deletions(-) diff --git a/src/pycalver/v2version.py b/src/pycalver/v2version.py index 22a93af..62929b0 100644 --- a/src/pycalver/v2version.py +++ b/src/pycalver/v2version.py @@ -309,32 +309,6 @@ def _format_part_values(vinfo: version.V2VersionInfo) -> PartValues: return sorted(kwargs.items(), key=lambda item: -len(item[0])) -def _make_segments(raw_pattern: str) -> typ.List[str]: - pattern_segs_l: typ.List[str] = [] - pattern_segs_r: typ.List[str] = [] - - pattern_rest = raw_pattern - while "[" in pattern_rest and "]" in pattern_rest: - try: - seg_l , pattern_rest = pattern_rest.split("[", 1) - pattern_rest, seg_r = pattern_rest.rsplit("]", 1) - except ValueError as val_err: - if "values to unpack" in str(val_err): - err = f"Unbalanced braces [] in '{raw_pattern}'" - pat_err = version.PatternError(err) - - pat_err.__cause__ = val_err - raise pat_err - else: - raise - - pattern_segs_l.append(seg_l) - pattern_segs_r.append(seg_r) - - pattern_segs_l.append(pattern_rest) - return pattern_segs_l + list(reversed(pattern_segs_r)) - - def _clear_zero_segments( formatted_segs: typ.List[str], is_zero_segment: typ.List[bool] ) -> typ.List[str]: @@ -403,54 +377,6 @@ def _parse_segment_tree(raw_pattern: str) -> SegmentTree: return internal_root[0] -def _format_segments( - pattern_segs: typ.List[str], - part_values : PartValues, -) -> typ.List[str]: - # NOTE (mb 2020-09-21): Old implementaion that doesn't cover corner - # cases relating to escaped braces. - - is_zero_segment = [True] * len(pattern_segs) - - formatted_segs_l: typ.List[str] = [] - formatted_segs_r: typ.List[str] = [] - - idx_l = 0 - idx_r = len(pattern_segs) - 1 - while idx_l <= idx_r: - # NOTE (mb 2020-09-18): All segments are optional, - # except the most left and the most right. - # In other words the ones NOT surrounded by braces are - # required. Empty string is a valid segment. - is_required_seg = idx_l == 0 - - seg_l = pattern_segs[idx_l] - seg_r = pattern_segs[idx_r] - - for part, part_value in part_values: - if part in seg_l: - seg_l = seg_l.replace(part, part_value) - is_zero_seg = str(part_value) == version.ZERO_VALUES.get(part) - if is_required_seg or not is_zero_seg: - is_zero_segment[idx_l] = False - - if part in seg_r: - seg_r = seg_r.replace(part, part_value) - is_zero_seg = str(part_value) == version.ZERO_VALUES.get(part) - if is_required_seg or not is_zero_seg: - is_zero_segment[idx_r] = False - - formatted_segs_l.append(seg_l) - if idx_l < idx_r: - formatted_segs_r.append(seg_r) - - idx_l += 1 - idx_r -= 1 - - formatted_segs = formatted_segs_l + list(reversed(formatted_segs_r)) - return _clear_zero_segments(formatted_segs, is_zero_segment) - - FormattedSegmentParts = typ.List[str] @@ -575,11 +501,6 @@ def format_version(vinfo: version.V2VersionInfo, raw_pattern: str) -> str: '__version__ = "v1.0.0-rc2"' """ part_values = _format_part_values(vinfo) - - # pattern_segs = _make_segments(raw_pattern) - # formatted_segs = _format_segments(pattern_segs, part_values) - # version_str = "".join(formatted_segs) - seg_tree = _parse_segment_tree(raw_pattern) version_str_parts = _format_segment_tree(seg_tree, part_values) return "".join(version_str_parts) From b07edc9c8a458e5b4b6de53c3186beb53cc2b529 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 24 Sep 2020 19:21:57 +0000 Subject: [PATCH 28/98] formatting for new style versions --- README.md | 50 +++++------ pylint-ignore.md | 95 ++++++++++---------- setup.cfg | 26 +++--- src/pycalver/v1version.py | 12 +-- src/pycalver/v2patterns.py | 178 ++++++++++++++++++++++--------------- src/pycalver/v2rewrite.py | 12 +-- src/pycalver/v2version.py | 85 +++++++++--------- src/pycalver/version.py | 20 ++--- test/test_config.py | 6 +- test/test_patterns.py | 38 ++++---- test/test_rewrite.py | 4 +- test/test_version.py | 23 ++--- 12 files changed, 288 insertions(+), 261 deletions(-) diff --git a/README.md b/README.md index 3ef3d0b..db3fe38 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Exiting because of '--dry'. Would have written to pycalver.toml: [pycalver] current_version = "v202010.1001-alpha" - version_pattern = "vYYYY0M.BUILD[-TAG]" + version_pattern = "vYYYY0M.BUILD[-RELEASE]" commit_message = "bump version to {new_version}" commit = true tag = true @@ -125,7 +125,7 @@ This will add the something like the following to your `setup.cfg` # setup.cfg [pycalver] current_version = "v201902.1001-alpha" -version_pattern = "vYYYY0M.BUILD[-TAG]" +version_pattern = "vYYYY0M.BUILD[-RELEASE]" commit_message = "bump version to {new_version}" commit = True tag = True @@ -238,7 +238,7 @@ These patterns are closely based on https://calver.org/ | `00J` | 001, 002..366 | `%j` | | `BUILD` | 0011, 1001, 1002, .. | build number (lexid) | | `BLD` | 11, 1001, 1002, .. | zero truncated `BUILD` | -| `TAG` | alpha, beta, rc | `--release=` | +| `RELEASE` | alpha, beta, rc | `--release=` | | `PYTAG` | a, b, rc | `--release=` | | `NUM` | 0, 1, 2... | release tag number | | `MAJOR` | 0..9, 10..99, 100.. | `--major` | @@ -280,7 +280,7 @@ According to these rules: For example: -- Pattern: `vYY.0M.0D[-TAG]` +- Pattern: `vYY.0M.0D[-RELEASE]` - Version: `v20.08.02-beta` - PEP440 : `20.8.2b0` @@ -298,22 +298,22 @@ It may also be confusing to your users if they a list of version numbers, sorted If you wish to avoid this, you should use a pattern which maintains lexiographical ordering. -| pattern | example | lexio. | PEP440 | lexio. | -|-----------------------|---------|--------|--------|--------| -| `YYYY0M.BUILD[-TAG]` | | yes | | yes | -| `YYYY.BUILD[-TAG]` | | yes | | yes | -| `YYYY0M.MINOR[-TAG]` | | yes² | | yes | -| `YY0M.BUILD[-TAG]` | | yes¹ | | yes¹ | -| `YYYY.MM.MINOR[-TAG]` | | no | | no | -| `YYYY.0M.MINOR[-TAG]` | | yes² | | no | -| `YYYY.WW.MINOR[-TAG]` | | no | | no | -| `YYYY.0W.MINOR[-TAG]` | | yes² | | no | -| `YYYY.0M.0D` | | yes | | no | -| `YYYY.MM.DD` | | no | | no | -| `vYYYY.0W` | | yes | | no | -| `vYYYY.WW` | | no | | no | -| `YYYY.0M` | | yes | | no | -| `YYYY.MM` | | no | | no | +| pattern | example | lexio. | PEP440 | lexio. | +|---------------------------|---------|--------|--------|--------| +| `YYYY0M.BUILD[-RELEASE]` | | yes | | yes | +| `YYYY.BUILD[-RELEASE]` | | yes | | yes | +| `YYYY0M.MINOR[-RELEASE]` | | yes² | | yes | +| `YY0M.BUILD[-RELEASE]` | | yes¹ | | yes¹ | +| `YYYY.MM.MINOR[-RELEASE]` | | no | | no | +| `YYYY.0M.MINOR[-RELEASE]` | | yes² | | no | +| `YYYY.WW.MINOR[-RELEASE]` | | no | | no | +| `YYYY.0W.MINOR[-RELEASE]` | | yes² | | no | +| `YYYY.0M.0D` | | yes | | no | +| `YYYY.MM.DD` | | no | | no | +| `vYYYY.0W` | | yes | | no | +| `vYYYY.WW` | | no | | no | +| `YYYY.0M` | | yes | | no | +| `YYYY.MM` | | no | | no | - ¹ Until 2099. If your project has new releases after 2099, future maintainers can change `YY`/`0Y` -> `YYYY` so that they don't release `00.xx`. - ² As long as `MINOR <= 9` @@ -427,15 +427,15 @@ $ pycalver test 'v18.1.2' 'vYY.MINOR.PATCH' --minor New Version: v19.2.0 PEP440 : 19.2.0 -$ pycalver test 'v201811.1051-beta' 'vYYYYMM.BUILD[-TAG]' +$ pycalver test 'v201811.1051-beta' 'vYYYYMM.BUILD[-RELEASE]' New Version: v201902.1052-beta PEP440 : 201902.1052b0 -$ pycalver test 'v201811.0051-beta' 'vYYYYMM.BUILD[-TAG]' --release rc +$ pycalver test 'v201811.0051-beta' 'vYYYYMM.BUILD[-RELEASE]' --release rc New Version: v201902.1052-rc PEP440 : 201902.1052rc0 -$ pycalver test 'v201811.0051-beta' 'vYYYYMM.BUILD[-TAG]' --release final +$ pycalver test 'v201811.0051-beta' 'vYYYYMM.BUILD[-RELEASE]' --release final New Version: v201902.1052 PEP440 : 201902.1052 ``` @@ -509,7 +509,7 @@ section: ```ini [pycalver] current_version = "v202010.1006-beta" -version_pattern = "vYYYY0M.BUILD[-TAG]" +version_pattern = "vYYYY0M.BUILD[-RELEASE]" commit_message = "bump version to {new_version}" commit = True tag = True @@ -549,7 +549,7 @@ $ pycalver bump --dry [pycalver] -current_version = v202010.1005-beta +current_version = v202010.1006-beta - version_pattern = "vYYYY0M.BUILD[-TAG]" + version_pattern = "vYYYY0M.BUILD[-RELEASE]" commit_message = "bump version to {new_version}" commit = True ... diff --git a/pylint-ignore.md b/pylint-ignore.md index 3d65cdb..ed68081 100644 --- a/pylint-ignore.md +++ b/pylint-ignore.md @@ -23,7 +23,6 @@ The recommended approach to using `pylint-ignore` is: # Overview - - [E1123: unexpected-keyword-arg (1x)](#e1123-unexpected-keyword-arg) - [W0511: fixme (9x)](#w0511-fixme) - [W0703: broad-except (1x)](#w0703-broad-except) @@ -74,8 +73,8 @@ The recommended approach to using `pylint-ignore` is: 154: assert "setup.py" in cfg.file_patterns 155: assert "setup.cfg" in cfg.file_patterns > 156: # TODO (mb 2020-09-18): - 157: # assert cfg.file_patterns["setup.py" ] == ["vYYYY0M.BUILD[-TAG]", "YYYY0M.BLD[PYTAGNUM]"] - 158: # assert cfg.file_patterns["setup.cfg" ] == ['current_version = "vYYYY0M.BUILD[-TAG]"'] + 157: # assert cfg.file_patterns["setup.py" ] == ["vYYYY0M.BUILD[-RELEASE]", "YYYY0M.BLD[PYTAGNUM]"] + 158: # assert cfg.file_patterns["setup.cfg" ] == ['current_version = "vYYYY0M.BUILD[-RELEASE]"'] ``` @@ -96,71 +95,54 @@ The recommended approach to using `pylint-ignore` is: ``` -## File src/pycalver/v1patterns.py - Line 212 - W0511 (fixme) +## File src/pycalver/v1patterns.py - Line 214 - W0511 (fixme) - `message: TODO (mb 2020-09-19): replace {version} etc with version_pattern` - `author : Manuel Barkhau ` - `date : 2020-09-19T16:24:10` ``` - 199: def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[str]: + 201: def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[str]: ... - 210: escaped_pattern = escaped_pattern.replace(char, escaped) - 211: -> 212: # TODO (mb 2020-09-19): replace {version} etc with version_pattern - 213: pattern_str = _replace_pattern_parts(escaped_pattern) - 214: return re.compile(pattern_str) + 212: escaped_pattern = escaped_pattern.replace(char, escaped) + 213: +> 214: # TODO (mb 2020-09-19): replace {version} etc with version_pattern + 215: pattern_str = _replace_pattern_parts(escaped_pattern) + 216: return re.compile(pattern_str) ``` -## File src/pycalver/__main__.py - Line 247 - W0511 (fixme) +## File src/pycalver/__main__.py - Line 250 - W0511 (fixme) - `message: TODO (mb 2020-09-18): Investigate error messages` - `author : Manuel Barkhau ` - `date : 2020-09-19T16:24:10` ``` - 219: def _bump( + 222: def _bump( ... - 245: sys.exit(1) - 246: except Exception as ex: -> 247: # TODO (mb 2020-09-18): Investigate error messages - 248: logger.error(str(ex)) - 249: sys.exit(1) + 248: sys.exit(1) + 249: except Exception as ex: +> 250: # TODO (mb 2020-09-18): Investigate error messages + 251: logger.error(str(ex)) + 252: sys.exit(1) ``` -## File src/pycalver/v2patterns.py - Line 256 - W0511 (fixme) - -- `message: TODO (mb 2020-09-19): replace {version} etc with version_pattern` -- `author : Manuel Barkhau ` -- `date : 2020-09-19T16:24:10` - -``` - 240: def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[str]: - ... - 254: print("<<<<", (normalized_pattern,)) - 255: -> 256: # TODO (mb 2020-09-19): replace {version} etc with version_pattern - 257: pattern_str = _replace_pattern_parts(escaped_pattern) - 258: return re.compile(pattern_str) -``` - - -## File src/pycalver/config.py - Line 264 - W0511 (fixme) +## File src/pycalver/config.py - Line 273 - W0511 (fixme) - `message: TODO (mb 2020-09-18): Validate Pattern` - `author : Manuel Barkhau ` - `date : 2020-09-18T19:04:06` ``` - 250: def _parse_config(raw_cfg: RawConfig) -> Config: + 251: def _parse_config(raw_cfg: RawConfig) -> Config: ... - 262: is_new_pattern = "{" not in version_pattern and "}" not in version_pattern - 263: -> 264: # TODO (mb 2020-09-18): Validate Pattern - 265: # detect YY with WW or UU -> suggest GG with VV - 266: # detect YYMM -> suggest YY0M + 271: ) + 272: +> 273: # TODO (mb 2020-09-18): Validate Pattern + 274: # detect YY with WW or UU -> suggest GG with VV + 275: # detect YYMM -> suggest YY0M ``` @@ -178,22 +160,39 @@ The recommended approach to using `pylint-ignore` is: ``` +## File src/pycalver/v2version.py - Line 551 - W0511 (fixme) + +- `message: TODO (mb 2020-09-20): New Rollover Behaviour:` +- `author : Manuel Barkhau ` +- `date : 2020-09-20T17:36:38` + +``` + 508: def incr( + ... + 549: cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1) + 550: +> 551: # TODO (mb 2020-09-20): New Rollover Behaviour: + 552: # Reset major, minor, patch to zero if any part to the left of it is incremented + 553: +``` + + # W0703: broad-except -## File src/pycalver/__main__.py - Line 246 - W0703 (broad-except) +## File src/pycalver/__main__.py - Line 249 - W0703 (broad-except) - `message: Catching too general exception Exception` - `author : Manuel Barkhau ` - `date : 2020-09-05T14:30:17` ``` - 219: def _bump( + 222: def _bump( ... - 244: logger.error(str(ex)) - 245: sys.exit(1) -> 246: except Exception as ex: - 247: # TODO (mb 2020-09-18): Investigate error messages - 248: logger.error(str(ex)) + 247: logger.error(str(ex)) + 248: sys.exit(1) +> 249: except Exception as ex: + 250: # TODO (mb 2020-09-18): Investigate error messages + 251: logger.error(str(ex)) ``` diff --git a/setup.cfg b/setup.cfg index 520c0de..22e319c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -92,28 +92,34 @@ addopts = --doctest-modules [pycalver] -current_version = v202007.1036 -version_pattern = "{pycalver}" +current_version = "v202007.1036" +version_pattern = "vYYYY0M.BUILD[-RELEASE[NUM]]" +commit_message = "bump version to {new_version}" commit = True tag = True push = True [pycalver:file_patterns] bootstrapit.sh = - PACKAGE_VERSION="{pycalver}" + PACKAGE_VERSION="{version}" setup.cfg = - current_version = {pycalver} + current_version = "{version}" setup.py = - version="{pep440_pycalver}" + version="{pep440_version}" src/pycalver/__init__.py = - __version__ = "{pycalver}" -src/pycalver/cli.py = - click.version_option(version="{pycalver}") + __version__ = "{version}" +src/pycalver/__main__.py = + click.version_option(version="{version}") +src/pycalver*/*.py = + Copyright (c) 2018-YYYY +LICENSE = + Copyright (c) 2018-YYYY +license.header = + Copyright (c) 2018-YYYY README.md = - [PyCalVer {version}] + \[PyCalVer {version}\] img.shields.io/static/v1.svg?label=PyCalVer&message={version}&color=blue Successfully installed pycalver-{pep440_version} - pycalver, version {version} [tool:pylint] diff --git a/src/pycalver/v1version.py b/src/pycalver/v1version.py index 11fdb82..978eb39 100644 --- a/src/pycalver/v1version.py +++ b/src/pycalver/v1version.py @@ -97,7 +97,7 @@ def _parse_field_values(field_values: FieldValues) -> version.V1VersionInfo: tag = fvals.get('tag') if tag is None: tag = "final" - tag = version.TAG_BY_PEP440_TAG.get(tag, tag) + tag = version.RELEASE_BY_PEP440_TAG.get(tag, tag) assert tag is not None bid = fvals['bid'] if 'bid' in fvals else "0001" @@ -338,15 +338,15 @@ def format_version(vinfo: version.V1VersionInfo, raw_pattern: str) -> str: kwargs: typ.Dict[str, typ.Union[str, int, None]] = vinfo._asdict() - tag = vinfo.tag - if tag == 'final': + release_tag = vinfo.tag + if release_tag == 'final': kwargs['release' ] = "" kwargs['pep440_tag'] = "" else: - kwargs['release' ] = "-" + tag - kwargs['pep440_tag'] = version.PEP440_TAG_BY_TAG[tag] + "0" + kwargs['release' ] = "-" + release_tag + kwargs['pep440_tag'] = version.PEP440_TAG_BY_RELEASE[release_tag] + "0" - kwargs['release_tag'] = tag + kwargs['release_tag'] = release_tag year = vinfo.year if year: diff --git a/src/pycalver/v2patterns.py b/src/pycalver/v2patterns.py index 8001208..5306209 100644 --- a/src/pycalver/v2patterns.py +++ b/src/pycalver/v2patterns.py @@ -5,7 +5,7 @@ # SPDX-License-Identifier: MIT """Compose Regular Expressions from Patterns. ->>> pattern = compile_pattern("vYYYY0M.BUILD[-TAG]") +>>> pattern = compile_pattern("vYYYY0M.BUILD[-RELEASE]") >>> version_info = pattern.regexp.match("v201712.0123-alpha") >>> assert version_info.groupdict() == { ... "version": "v201712.0123-alpha", @@ -80,45 +80,57 @@ PART_PATTERNS = { 'VV': r"(?:5[0-3]|[1-4][0-9]|[1-9])", '0V': r"(?:5[0-3]|[1-4][0-9]|0[1-9])", # non calver parts - 'MAJOR': r"[0-9]+", - 'MINOR': r"[0-9]+", - 'PATCH': r"[0-9]+", - 'BUILD': r"[0-9]+", - 'BLD' : r"[1-9][0-9]*", - 'TAG' : r"(?:preview|final|alpha|beta|post|pre|dev|rc|a|b|c|r)", - 'PYTAG': r"(?:post|dev|rc|a|b)", - 'NUM' : r"[0-9]+", + 'MAJOR' : r"[0-9]+", + 'MINOR' : r"[0-9]+", + 'PATCH' : r"[0-9]+", + 'BUILD' : r"[0-9]+", + 'BLD' : r"[1-9][0-9]*", + 'RELEASE': r"(?:preview|final|alpha|beta|post|pre|dev|rc|a|b|c|r)", + 'PYTAG' : r"(?:post|dev|rc|a|b)", + 'NUM' : r"[0-9]+", } PATTERN_PART_FIELDS = { - 'YYYY' : 'year_y', - 'YY' : 'year_y', - '0Y' : 'year_y', - 'GGGG' : 'year_g', - 'GG' : 'year_g', - '0G' : 'year_g', - 'Q' : 'quarter', - 'MM' : 'month', - '0M' : 'month', - 'DD' : 'dom', - '0D' : 'dom', - 'JJJ' : 'doy', - '00J' : 'doy', - 'MAJOR': 'major', - 'MINOR': 'minor', - 'PATCH': 'patch', - 'BUILD': 'bid', - 'BLD' : 'bid', - 'TAG' : 'tag', - 'PYTAG': 'pytag', - 'NUM' : 'num', - 'WW' : 'week_w', - '0W' : 'week_w', - 'UU' : 'week_u', - '0U' : 'week_u', - 'VV' : 'week_v', - '0V' : 'week_v', + 'YYYY' : 'year_y', + 'YY' : 'year_y', + '0Y' : 'year_y', + 'GGGG' : 'year_g', + 'GG' : 'year_g', + '0G' : 'year_g', + 'Q' : 'quarter', + 'MM' : 'month', + '0M' : 'month', + 'DD' : 'dom', + '0D' : 'dom', + 'JJJ' : 'doy', + '00J' : 'doy', + 'MAJOR' : 'major', + 'MINOR' : 'minor', + 'PATCH' : 'patch', + 'BUILD' : 'bid', + 'BLD' : 'bid', + 'RELEASE': 'tag', + 'PYTAG' : 'pytag', + 'NUM' : 'num', + 'WW' : 'week_w', + '0W' : 'week_w', + 'UU' : 'week_u', + '0U' : 'week_u', + 'VV' : 'week_v', + '0V' : 'week_v', +} + + +PEP440_PART_SUBSTITUTIONS = { + '0W' : "WW", + '0U' : "UU", + '0V' : "VV", + '0M' : "MM", + '0D' : "DD", + '00J' : "JJJ", + 'BUILD' : "BLD", + 'RELEASE': "PYTAG", } @@ -174,33 +186,33 @@ def _fmt_0v(week_v: FieldValue) -> str: PART_FORMATS: typ.Dict[str, typ.Callable[[FieldValue], str]] = { - 'YYYY' : _fmt_num, - 'YY' : _fmt_yy, - '0Y' : _fmt_0y, - 'GGGG' : _fmt_num, - 'GG' : _fmt_gg, - '0G' : _fmt_0g, - 'Q' : _fmt_num, - 'MM' : _fmt_num, - '0M' : _fmt_0m, - 'DD' : _fmt_num, - '0D' : _fmt_0d, - 'JJJ' : _fmt_num, - '00J' : _fmt_00j, - 'MAJOR': _fmt_num, - 'MINOR': _fmt_num, - 'PATCH': _fmt_num, - 'BUILD': _fmt_num, - 'BLD' : _fmt_bld, - 'TAG' : _fmt_num, - 'PYTAG': _fmt_num, - 'NUM' : _fmt_num, - 'WW' : _fmt_num, - '0W' : _fmt_0w, - 'UU' : _fmt_num, - '0U' : _fmt_0u, - 'VV' : _fmt_num, - '0V' : _fmt_0v, + 'YYYY' : _fmt_num, + 'YY' : _fmt_yy, + '0Y' : _fmt_0y, + 'GGGG' : _fmt_num, + 'GG' : _fmt_gg, + '0G' : _fmt_0g, + 'Q' : _fmt_num, + 'MM' : _fmt_num, + '0M' : _fmt_0m, + 'DD' : _fmt_num, + '0D' : _fmt_0d, + 'JJJ' : _fmt_num, + '00J' : _fmt_00j, + 'MAJOR' : _fmt_num, + 'MINOR' : _fmt_num, + 'PATCH' : _fmt_num, + 'BUILD' : _fmt_num, + 'BLD' : _fmt_bld, + 'RELEASE': _fmt_num, + 'PYTAG' : _fmt_num, + 'NUM' : _fmt_num, + 'WW' : _fmt_num, + '0W' : _fmt_0w, + 'UU' : _fmt_num, + '0U' : _fmt_0u, + 'VV' : _fmt_num, + '0V' : _fmt_0v, } @@ -209,15 +221,37 @@ def _convert_to_pep440(version_pattern: str) -> str: # corner cases as specified in PEP440, in particular # related to post and dev releases. - version_pattern = version_pattern.lstrip("v") + pep440_pattern = version_pattern + + if pep440_pattern.startswith("v"): + pep440_pattern = pep440_pattern[1:] + + pep440_pattern = pep440_pattern.replace(r"\[", "") + pep440_pattern = pep440_pattern.replace(r"\]", "") + + pep440_pattern, _ = re.subn(r"[^a-zA-Z0-9\.\[\]]", "", pep440_pattern) part_names = list(PATTERN_PART_FIELDS.keys()) part_names.sort(key=len, reverse=True) - if version_pattern == "vYYYY0M.BUILD[-TAG]": - return "YYYY0M.BLD[PYTAGNUM]" - # TODO (mb 2020-09-20) - raise NotImplementedError + for part_name in part_names: + if part_name not in version_pattern: + continue + if part_name not in PEP440_PART_SUBSTITUTIONS: + continue + + substitution = PEP440_PART_SUBSTITUTIONS[part_name] + + is_numerical_part = part_name not in ('RELEASE', 'PYTAG') + if is_numerical_part: + part_index = pep440_pattern.find(part_name) + is_zero_truncation_part = part_index == 0 or pep440_pattern[part_index - 1] == "." + if is_zero_truncation_part: + pep440_pattern = pep440_pattern.replace(part_name, substitution) + else: + pep440_pattern = pep440_pattern.replace(part_name, substitution) + + return pep440_pattern def normalize_pattern(version_pattern: str, raw_pattern: str) -> str: @@ -236,10 +270,10 @@ def _replace_pattern_parts(pattern: str) -> str: # The pattern is escaped, so that everything besides the format # string variables is treated literally. while True: - new_pattern, n = re.subn(r"([^\\]|^)\[", r"\1(?:", pattern) - new_pattern, m = re.subn(r"([^\\]|^)\]", r"\1)?" , new_pattern) + new_pattern, _n = re.subn(r"([^\\]|^)\[", r"\1(?:", pattern) + new_pattern, _m = re.subn(r"([^\\]|^)\]", r"\1)?" , new_pattern) pattern = new_pattern - if n + m == 0: + if _n + _m == 0: break SortKey = typ.Tuple[int, int] @@ -274,7 +308,7 @@ def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[s normalized_pattern = normalize_pattern(version_pattern, raw_pattern) escaped_pattern = normalized_pattern for char, escaped in RE_PATTERN_ESCAPES: - # [] braces are used for optional parts, such as [-TAG]/[-beta] + # [] braces are used for optional parts, such as [-RELEASE]/[-beta] # and need to be escaped manually. is_semantic_char = char in "[]\\" if not is_semantic_char: diff --git a/src/pycalver/v2rewrite.py b/src/pycalver/v2rewrite.py index 49e4c22..ce1013c 100644 --- a/src/pycalver/v2rewrite.py +++ b/src/pycalver/v2rewrite.py @@ -28,7 +28,7 @@ def rewrite_lines( """Replace occurances of patterns in old_lines with new_vinfo. >>> from .v2patterns import compile_pattern - >>> version_pattern = "vYYYY0M.BUILD[-TAG]" + >>> version_pattern = "vYYYY0M.BUILD[-RELEASE]" >>> new_vinfo = v2version.parse_version_info("v201811.0123-beta", version_pattern) >>> patterns = [compile_pattern(version_pattern, '__version__ = "{version}"')] >>> rewrite_lines(patterns, new_vinfo, ['__version__ = "v201809.0002-alpha" ']) @@ -73,9 +73,9 @@ def rfd_from_content( ) -> rewrite.RewrittenFileData: r"""Rewrite pattern occurrences with version string. - >>> version_pattern = "vYYYY0M.BUILD[-TAG]" + >>> version_pattern = "vYYYY0M.BUILD[-RELEASE]" >>> new_vinfo = v2version.parse_version_info("v201809.0123", version_pattern) - >>> raw_patterns = ['__version__ = "vYYYY0M.BUILD[-TAG]"'] + >>> raw_patterns = ['__version__ = "vYYYY0M.BUILD[-RELEASE]"'] >>> patterns = >>> content = '__version__ = "v201809.0001-alpha"' >>> rfd = rfd_from_content(patterns, new_vinfo, content) @@ -115,8 +115,8 @@ def iter_rewritten( ) -> typ.Iterable[rewrite.RewrittenFileData]: r'''Iterate over files with version string replaced. - >>> version_pattern = "vYYYY0M.BUILD[-TAG]" - >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "vYYYY0M.BUILD[-TAG]"']} + >>> version_pattern = "vYYYY0M.BUILD[-RELEASE]" + >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "vYYYY0M.BUILD[-RELEASE]"']} >>> new_vinfo = v2version.parse_version_info("v201809.0123", version_pattern) >>> rewritten_datas = iter_rewritten(file_patterns, new_vinfo) >>> rfd = list(rewritten_datas)[0] @@ -153,7 +153,7 @@ def diff( >>> old_vinfo = v2version.parse_version_info("v201809.0123", version_pattern) >>> new_vinfo = v2version.parse_version_info("v201810.1124", version_pattern) - >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "vYYYY0M.BUILD[-TAG]"']} + >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "vYYYY0M.BUILD[-RELEASE]"']} >>> diff_str = diff(old_vinfo, new_vinfo, file_patterns) >>> lines = diff_str.split("\n") >>> lines[:2] diff --git a/src/pycalver/v2version.py b/src/pycalver/v2version.py index 62929b0..ddf3ea2 100644 --- a/src/pycalver/v2version.py +++ b/src/pycalver/v2version.py @@ -149,9 +149,9 @@ def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: pytag = fvals.get('pytag') or "" if tag and not pytag: - pytag = version.PEP440_TAG_BY_TAG[tag] + pytag = version.PEP440_TAG_BY_RELEASE[tag] elif pytag and not tag: - tag = version.TAG_BY_PEP440_TAG[pytag] + tag = version.RELEASE_BY_PEP440_TAG[pytag] date: typ.Optional[dt.date] = None @@ -221,19 +221,19 @@ def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: def parse_version_info( - version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-TAG[NUM]]" + version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-RELEASE[NUM]]" ) -> version.V2VersionInfo: """Parse normalized V2VersionInfo. - >>> vinfo = parse_version_info("v201712.0033-beta0", raw_pattern="vYYYY0M.BUILD[-TAG[NUM]]") + >>> vinfo = parse_version_info("v201712.0033-beta0", raw_pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033", 'tag': "beta", 'num': 0} >>> assert vinfo == _parse_version_info(fvals) - >>> vinfo = parse_version_info("v201712.0033-beta", raw_pattern="vYYYY0M.BUILD[-TAG[NUM]]") + >>> vinfo = parse_version_info("v201712.0033-beta", raw_pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"} >>> assert vinfo == _parse_version_info(fvals) - >>> vinfo = parse_version_info("v201712.0033", raw_pattern="vYYYY0M.BUILD[-TAG[NUM]]") + >>> vinfo = parse_version_info("v201712.0033", raw_pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033"} >>> assert vinfo == _parse_version_info(fvals) @@ -254,10 +254,10 @@ def parse_version_info( return _parse_version_info(field_values) -def is_valid(version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-TAG]") -> bool: +def is_valid(version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-RELEASE[NUM]]") -> bool: """Check if a version matches a pattern. - >>> is_valid("v201712.0033-beta", raw_pattern="vYYYY0M.BUILD[-TAG]") + >>> is_valid("v201712.0033-beta", raw_pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") True >>> is_valid("v201712.0033-beta", raw_pattern="MAJOR.MINOR.PATCH") False @@ -274,7 +274,7 @@ def is_valid(version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-TAG]") -> bool TemplateKwargs = typ.Dict[str, typ.Union[str, int, None]] -PartValues = typ.List[typ.Tuple[str, str]] +PartValues = typ.List[typ.Tuple[str, str]] def _format_part_values(vinfo: version.V2VersionInfo) -> PartValues: @@ -285,9 +285,9 @@ def _format_part_values(vinfo: version.V2VersionInfo) -> PartValues: It may for example have month=9, but not the formatted representation '09' for '0M'. - >>> vinfo = parse_version_info("v200709.1033-beta", pattern="vYYYY0M.BUILD[-TAG]") + >>> vinfo = parse_version_info("v200709.1033-beta", pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") >>> kwargs = dict(_format_part_values(vinfo)) - >>> (kwargs['YYYY'], kwargs['0M'], kwargs['BUILD'], kwargs['TAG']) + >>> (kwargs['YYYY'], kwargs['0M'], kwargs['BUILD'], kwargs['RELEASE[NUM]']) ('2007', '09', '1033', 'beta') >>> (kwargs['YY'], kwargs['0Y'], kwargs['MM'], kwargs['PYTAG']) ('7', '07', '9', 'b') @@ -381,8 +381,8 @@ FormattedSegmentParts = typ.List[str] def _format_segment_tree( - seg_tree: SegmentTree, - part_values : PartValues, + seg_tree : SegmentTree, + part_values: PartValues, ) -> FormattedSegmentParts: result_parts = [] for seg in seg_tree: @@ -391,7 +391,7 @@ def _format_segment_tree( else: # NOTE (mb 2020-09-24): If a segment has any zero parts, # the whole segment is skipped. - is_zero_seg = False + is_zero_seg = False formatted_seg = seg # unescape braces formatted_seg = formatted_seg.replace(r"\[", r"[") @@ -400,8 +400,7 @@ def _format_segment_tree( for part, part_value in part_values: if part in formatted_seg: is_zero_part = ( - part in version.ZERO_VALUES - and str(part_value) == version.ZERO_VALUES[part] + part in version.ZERO_VALUES and str(part_value) == version.ZERO_VALUES[part] ) if is_zero_part: is_zero_seg = True @@ -418,7 +417,7 @@ def format_version(vinfo: version.V2VersionInfo, raw_pattern: str) -> str: """Generate version string. >>> import datetime as dt - >>> vinfo = parse_version_info("v200712.0033-beta", pattern="vYYYY0M.BUILD[-TAG[NUM]]") + >>> vinfo = parse_version_info("v200712.0033-beta", pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") >>> vinfo_a = vinfo._replace(**cal_info(date=dt.date(2007, 1, 1))._asdict()) >>> vinfo_b = vinfo._replace(**cal_info(date=dt.date(2007, 12, 31))._asdict()) @@ -429,86 +428,86 @@ def format_version(vinfo: version.V2VersionInfo, raw_pattern: str) -> str: '200701.0033b' >>> format_version(vinfo_a, pattern="vYY.BLD[-PYTAGNUM]") 'v7.33-b0' - >>> format_version(vinfo_a, pattern="v0Y.BLD[-TAG]") + >>> format_version(vinfo_a, pattern="v0Y.BLD[-RELEASE[NUM]]") 'v07.33-beta' - >>> format_version(vinfo_a, pattern="vYYYY0M.BUILD[-TAG]") + >>> format_version(vinfo_a, pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") 'v200701.0033-beta' - >>> format_version(vinfo_b, pattern="vYYYY0M.BUILD[-TAG]") + >>> format_version(vinfo_b, pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") 'v200712.0033-beta' - >>> format_version(vinfo_a, pattern="vYYYYw0W.BUILD[-TAG]") + >>> format_version(vinfo_a, pattern="vYYYYw0W.BUILD[-RELEASE[NUM]]") 'v2007w01.0033-beta' - >>> format_version(vinfo_a, pattern="vYYYYwWW.BLD[-TAG]") + >>> format_version(vinfo_a, pattern="vYYYYwWW.BLD[-RELEASE[NUM]]") 'v2007w1.33-beta' - >>> format_version(vinfo_b, pattern="vYYYYw0W.BUILD[-TAG]") + >>> format_version(vinfo_b, pattern="vYYYYw0W.BUILD[-RELEASE[NUM]]") 'v2007w53.0033-beta' - >>> format_version(vinfo_a, pattern="vYYYYd00J.BUILD[-TAG]") + >>> format_version(vinfo_a, pattern="vYYYYd00J.BUILD[-RELEASE[NUM]]") 'v2007d001.0033-beta' - >>> format_version(vinfo_a, pattern="vYYYYdJJJ.BUILD[-TAG]") + >>> format_version(vinfo_a, pattern="vYYYYdJJJ.BUILD[-RELEASE[NUM]]") 'v2007d1.0033-beta' - >>> format_version(vinfo_b, pattern="vYYYYd00J.BUILD[-TAG]") + >>> format_version(vinfo_b, pattern="vYYYYd00J.BUILD[-RELEASE[NUM]]") 'v2007d365.0033-beta' >>> format_version(vinfo_a, pattern="vGGGGwVV.BLD[PYTAGNUM]") 'v2007w1.33b0' - >>> format_version(vinfo_a, pattern="vGGGGw0V.BUILD[-TAG]") + >>> format_version(vinfo_a, pattern="vGGGGw0V.BUILD[-RELEASE[NUM]]") 'v2007w01.0033-beta' - >>> format_version(vinfo_b, pattern="vGGGGw0V.BUILD[-TAG]") + >>> format_version(vinfo_b, pattern="vGGGGw0V.BUILD[-RELEASE[NUM]]") 'v2008w01.0033-beta' >>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final') - >>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD-TAG") + >>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD-RELEASE") 'v2007w53.0033-final' - >>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD[-TAG]") + >>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD[-RELEASE[NUM]]") 'v2007w53.0033' >>> format_version(vinfo_c, pattern="vMAJOR.MINOR.PATCH") 'v1.2.34' >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='final') - >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-TAGNUM") + >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-RELEASENUM") 'v1.0.0-final0' - >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-TAG[NUM]") + >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-RELEASE[NUM]") 'v1.0.0-final' - >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-TAG") + >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-RELEASE") 'v1.0.0-final' - >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH[-TAG]") + >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH[-RELEASE[NUM]]") 'v1.0.0' - >>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.PATCH[-TAG]]") + >>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.PATCH[-RELEASE[NUM]]]") 'v1.0' - >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-TAG]]]") + >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]") 'v1' >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=1, tag='rc', num=0) >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH]]") 'v1.0.1' - >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-TAG[NUM]]]]") + >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]") 'v1.0.1-rc' - >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-TAGNUM]]]") + >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-RELEASENUM]]]") 'v1.0.1-rc0' >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH]]") 'v1.0.1' >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='rc', num=2) - >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-TAG[NUM]]]]") + >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]") 'v1.0.0-rc2' >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='rc', num=2) - >>> format_version(vinfo_d, pattern='__version__ = "vMAJOR[.MINOR[.PATCH[-TAG[NUM]]]]"') + >>> format_version(vinfo_d, pattern='__version__ = "vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]"') '__version__ = "v1.0.0-rc2"' """ - part_values = _format_part_values(vinfo) - seg_tree = _parse_segment_tree(raw_pattern) + part_values = _format_part_values(vinfo) + seg_tree = _parse_segment_tree(raw_pattern) version_str_parts = _format_segment_tree(seg_tree, part_values) return "".join(version_str_parts) def incr( old_version: str, - raw_pattern: str = "vYYYY0M.BUILD[-TAG]", + raw_pattern: str = "vYYYY0M.BUILD[-RELEASE]", *, release : typ.Optional[str] = None, major : bool = False, diff --git a/src/pycalver/version.py b/src/pycalver/version.py index c3b06bf..9169cd2 100644 --- a/src/pycalver/version.py +++ b/src/pycalver/version.py @@ -79,7 +79,7 @@ class V2VersionInfo(typ.NamedTuple): TODAY = dt.datetime.utcnow().date() -TAG_BY_PEP440_TAG = { +RELEASE_BY_PEP440_TAG = { 'a' : 'alpha', 'b' : 'beta', '' : 'final', @@ -89,7 +89,7 @@ TAG_BY_PEP440_TAG = { } -PEP440_TAG_BY_TAG = { +PEP440_TAG_BY_RELEASE = { 'a' : 'a', 'b' : 'b', 'dev' : 'dev', @@ -105,17 +105,17 @@ PEP440_TAG_BY_TAG = { 'rev' : 'post', } -assert set(TAG_BY_PEP440_TAG.keys()) == set(PEP440_TAG_BY_TAG.values()) -assert set(TAG_BY_PEP440_TAG.values()) < set(PEP440_TAG_BY_TAG.keys()) +assert set(RELEASE_BY_PEP440_TAG.keys()) == set(PEP440_TAG_BY_RELEASE.values()) +assert set(RELEASE_BY_PEP440_TAG.values()) < set(PEP440_TAG_BY_RELEASE.keys()) ZERO_VALUES = { - 'MAJOR': "0", - 'MINOR': "0", - 'PATCH': "0", - 'TAG' : "final", - 'PYTAG': "", - 'NUM' : "0", + 'MAJOR' : "0", + 'MINOR' : "0", + 'PATCH' : "0", + 'RELEASE': "final", + 'PYTAG' : "", + 'NUM' : "0", } diff --git a/test/test_config.py b/test/test_config.py index da611b1..5953f8d 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -66,7 +66,7 @@ setup.cfg = NEW_PATTERN_CFG_FIXTURE = """ [pycalver] current_version = "v201808.1456-beta" -version_pattern = "vYYYY0M.BUILD[-TAG]" +version_pattern = "vYYYY0M.BUILD[-RELEASE]" commit_message = "bump version to {new_version}" commit = True tag = True @@ -154,8 +154,8 @@ def test_parse_v2_cfg(): assert "setup.py" in cfg.file_patterns assert "setup.cfg" in cfg.file_patterns # TODO (mb 2020-09-18): - # assert cfg.file_patterns["setup.py" ] == ["vYYYY0M.BUILD[-TAG]", "YYYY0M.BLD[PYTAGNUM]"] - # assert cfg.file_patterns["setup.cfg" ] == ['current_version = "vYYYY0M.BUILD[-TAG]"'] + # assert cfg.file_patterns["setup.py" ] == ["vYYYY0M.BUILD[-RELEASE]", "YYYY0M.BLD[PYTAGNUM]"] + # assert cfg.file_patterns["setup.cfg" ] == ['current_version = "vYYYY0M.BUILD[-RELEASE]"'] # assert cfg.file_patterns["src/project/*.py"] == ['Copyright (c) 2018-YYYY"'] diff --git a/test/test_patterns.py b/test/test_patterns.py index 37c3b45..aebb99c 100644 --- a/test/test_patterns.py +++ b/test/test_patterns.py @@ -129,25 +129,25 @@ V2_PART_PATTERN_CASES = [ (['0V'], "53", "53"), (['0V'], "54", None), (['MAJOR', 'MINOR', 'PATCH'], "0", "0"), - (['TAG' ], "alpha" , "alpha"), - (['TAG' ], "alfa" , None), - (['TAG' ], "beta" , "beta"), - (['TAG' ], "dev" , "dev"), - (['TAG' ], "rc" , "rc"), - (['TAG' ], "post" , "post"), - (['TAG' ], "final" , "final"), - (['TAG' ], "latest", None), - (['PYTAG'], "a" , "a"), - (['PYTAG'], "b" , "b"), - (['PYTAG'], "dev" , "dev"), - (['PYTAG'], "rc" , "rc"), - (['PYTAG'], "post" , "post"), - (['PYTAG'], "post" , "post"), - (['PYTAG'], "x" , None), - (['NUM' ], "a" , None), - (['NUM' ], "0" , "0"), - (['NUM' ], "1" , "1"), - (['NUM' ], "10" , "10"), + (['RELEASE'], "alpha" , "alpha"), + (['RELEASE'], "alfa" , None), + (['RELEASE'], "beta" , "beta"), + (['RELEASE'], "dev" , "dev"), + (['RELEASE'], "rc" , "rc"), + (['RELEASE'], "post" , "post"), + (['RELEASE'], "final" , "final"), + (['RELEASE'], "latest", None), + (['PYTAG' ], "a" , "a"), + (['PYTAG' ], "b" , "b"), + (['PYTAG' ], "dev" , "dev"), + (['PYTAG' ], "rc" , "rc"), + (['PYTAG' ], "post" , "post"), + (['PYTAG' ], "post" , "post"), + (['PYTAG' ], "x" , None), + (['NUM' ], "a" , None), + (['NUM' ], "0" , "0"), + (['NUM' ], "1" , "1"), + (['NUM' ], "10" , "10"), ] diff --git a/test/test_rewrite.py b/test/test_rewrite.py index 9b2e61e..92cd854 100644 --- a/test/test_rewrite.py +++ b/test/test_rewrite.py @@ -141,8 +141,8 @@ def test_v1_optional_release(): def test_v2_optional_release(): old_lines = OPTIONAL_RELEASE_FIXTURE.splitlines() - pattern = "YYYY.BUILD[-TAG]" - patterns = ['__version__ = "YYYY.BUILD[-TAG]"'] + pattern = "YYYY.BUILD[-RELEASE]" + patterns = ['__version__ = "YYYY.BUILD[-RELEASE]"'] new_vinfo = v2version.parse_version_info("2019.0003", pattern) new_lines = v2rewrite.rewrite_lines(patterns, new_vinfo, old_lines) diff --git a/test/test_version.py b/test/test_version.py index 6e469cf..3ed968b 100644 --- a/test/test_version.py +++ b/test/test_version.py @@ -201,40 +201,29 @@ def test_v1_parse_versions(pattern_str, line, expected_vinfo): # def test_v2_parse_versions(pattern_str, line, expected_vinfo): def test_v2_parse_versions(): - _vnfo = v2version.parse_version_info("v201712.0033", raw_pattern="vYYYY0M.BUILD[-TAG[NUM]]") + _vnfo = v2version.parse_version_info("v201712.0033", raw_pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") fvals = {'year_y': 2017, 'month': 12, 'bid': "0033"} assert _vnfo == v2version._parse_version_info(fvals) -def test_make_segments(): - segs = v2version._make_segments("vYYYY0M.BUILD[-TAG[NUM]]") - assert segs == ["vYYYY0M.BUILD", "-TAG", "NUM", "", ""] - - segs = v2version._make_segments('__version__ = "YYYY0M.BLD[PYTAGNUM]"') - assert segs == ['__version__ = "YYYY0M.BLD', 'PYTAGNUM', '"'] - - segs = v2version._make_segments('__version__ = "YYYY.BUILD[-TAG]"') - assert segs == ['__version__ = "YYYY.BUILD', '-TAG', '"'] - - def test_v2_format_version(): - version_pattern = "vYYYY0M.BUILD[-TAG[NUM]]" + version_pattern = "vYYYY0M.BUILD[-RELEASE[NUM]]" in_version = "v200701.0033-beta" vinfo = v2version.parse_version_info(in_version, raw_pattern=version_pattern) out_version = v2version.format_version(vinfo, raw_pattern=version_pattern) assert in_version == out_version - result = v2version.format_version(vinfo, raw_pattern="v0Y.BUILD[-TAG]") + result = v2version.format_version(vinfo, raw_pattern="v0Y.BUILD[-RELEASE]") assert result == "v07.0033-beta" - result = v2version.format_version(vinfo, raw_pattern="vYY.BLD[-TAG]") + result = v2version.format_version(vinfo, raw_pattern="vYY.BLD[-RELEASE]") assert result == "v7.33-beta" - result = v2version.format_version(vinfo, raw_pattern="vYY.BLD-TAG") + result = v2version.format_version(vinfo, raw_pattern="vYY.BLD-RELEASE") assert result == "v7.33-beta" - result = v2version.format_version(vinfo, raw_pattern='__version__ = "YYYY.BUILD[-TAG]"') + result = v2version.format_version(vinfo, raw_pattern='__version__ = "YYYY.BUILD[-RELEASE]"') assert result == '__version__ = "2007.0033-beta"' result = v2version.format_version(vinfo, raw_pattern='__version__ = "YYYY.BLD"') From f31a172dcbdf70d23926401ccba2f0055867f3f6 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 24 Sep 2020 19:22:18 +0000 Subject: [PATCH 29/98] add make depgraph --- makefile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/makefile b/makefile index 61364ab..4b28595 100644 --- a/makefile +++ b/makefile @@ -63,3 +63,10 @@ test_compat: $(COMPAT_TEST_FILES) done; rm -rf compat_test/ + +depgraph: + pydeps src/pycalver \ + --no-show --noise-level 3 \ + --reverse --include-missing \ + -x 'click.*' 'toml.*' 'pretty_traceback.*' \ + -o pycalver_deps.svg From e237c4a7e25a8a9c4581f29e2066c765b64d376b Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 24 Sep 2020 19:22:32 +0000 Subject: [PATCH 30/98] add missing module --- src/pycalver/utils.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/pycalver/utils.py diff --git a/src/pycalver/utils.py b/src/pycalver/utils.py new file mode 100644 index 0000000..5f7c5bc --- /dev/null +++ b/src/pycalver/utils.py @@ -0,0 +1,24 @@ +# This file is part of the pycalver project +# https://github.com/mbarkhau/pycalver +# +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# SPDX-License-Identifier: MIT +import typing as typ +import functools + +# NOTE (mb 2020-09-24): The main use of the memo function is +# not as a performance optimization, but to reduce logging +# spam. + + +def memo(func: typ.Callable) -> typ.Callable: + cache = {} + + @functools.wraps(func) + def wrapper(*args): + key = str(args) + if key not in cache: + cache[key] = func(*args) + return cache[key] + + return wrapper From ada75ade8abf79bd7aceb7a1e4c735ebee582b0d Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 24 Sep 2020 20:33:27 +0000 Subject: [PATCH 31/98] fixes for new pattern formatting --- src/pycalver/v2version.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/pycalver/v2version.py b/src/pycalver/v2version.py index ddf3ea2..fcf53ac 100644 --- a/src/pycalver/v2version.py +++ b/src/pycalver/v2version.py @@ -249,6 +249,12 @@ def parse_version_info( f"for pattern '{raw_pattern}'/'{pattern.regexp.pattern}'" ) raise version.PatternError(err_msg) + elif len(match.group()) < len(version_str): + err_msg = ( + f"Incomplete match '{match.group()}' for version string '{version_str}' " + f"with pattern '{raw_pattern}'/'{pattern.regexp.pattern}'" + ) + raise version.PatternError(err_msg) else: field_values = match.groupdict() return _parse_version_info(field_values) @@ -389,9 +395,8 @@ def _format_segment_tree( if isinstance(seg, list): result_parts.extend(_format_segment_tree(seg, part_values)) else: - # NOTE (mb 2020-09-24): If a segment has any zero parts, - # the whole segment is skipped. - is_zero_seg = False + # If a segment has any non-zero parts, the whole segment is used. + non_zero_parts = 0 formatted_seg = seg # unescape braces formatted_seg = formatted_seg.replace(r"\[", r"[") @@ -403,11 +408,12 @@ def _format_segment_tree( part in version.ZERO_VALUES and str(part_value) == version.ZERO_VALUES[part] ) if is_zero_part: - is_zero_seg = True + formatted_seg = formatted_seg.replace(part, "") else: + non_zero_parts += 1 formatted_seg = formatted_seg.replace(part, part_value) - if not is_zero_seg: + if non_zero_parts: result_parts.append(formatted_seg) return result_parts @@ -507,7 +513,7 @@ def format_version(vinfo: version.V2VersionInfo, raw_pattern: str) -> str: def incr( old_version: str, - raw_pattern: str = "vYYYY0M.BUILD[-RELEASE]", + raw_pattern: str = "vYYYY0M.BUILD[-RELEASE[NUM]]", *, release : typ.Optional[str] = None, major : bool = False, From b7cb2855f09fbd56446affd9c0dfd5e9b0d6ca8b Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 24 Sep 2020 20:34:03 +0000 Subject: [PATCH 32/98] add --release-num cli flag --- CHANGELOG.md | 1 + README.md | 19 ++++++++++--------- src/pycalver/__main__.py | 33 ++++++++++++++++++++++----------- src/pycalver/v1version.py | 7 +++++-- src/pycalver/v2version.py | 9 +++++++-- 5 files changed, 45 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2f31a7..8aa7acc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Better support for optional parts. - New: Start `BUILD` parts at `1000` to avoid leading zero truncation. - New gitlab #10: `--pin-date` to keep date parts unchanged, and only increment non-date parts. + - New add `--release-num` to increment the `alphaN`/`betaN`/`a0`/`b0`/etc. release number - Fix gitlab #8: Push tags only pushed tags, not actual commit. - Fix gitlab #9: Make commit message configurable. diff --git a/README.md b/README.md index db3fe38..d02232c 100644 --- a/README.md +++ b/README.md @@ -591,15 +591,16 @@ TODO: Descriptions TODO: Descriptions -| CLI Argument | Description | -|---------------|-------------| -| --major | | -| --minor | | -| --patch | | -| --pin-date | | -| --no-fetch | | -| --dry | | -| --allow-dirty | | +| CLI Argument | Description | +|------------------|-------------| +| --major | | +| -m --minor | | +| -p --patch | | +| -r --release-num | | +| --pin-date | | +| --no-fetch | | +| --dry | | +| --allow-dirty | | ## The PyCalVer Format diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 8f3dc41..0c28381 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -85,14 +85,19 @@ def cli(verbose: int = 0) -> None: @click.argument("pattern", default="{pycalver}") @click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") @click.option( - "--release", default=None, metavar="", help="Override release name of current_version" + "--release", + default=None, + metavar="", + help=( + f"Override release name of current_version. Valid options are: " + f"{', '.join(VALID_RELEASE_VALUES)}." + ), ) @click.option("--major", is_flag=True, default=False, help="Increment major component.") -@click.option("--minor", is_flag=True, default=False, help="Increment minor component.") -@click.option("--patch", is_flag=True, default=False, help="Increment patch component.") -@click.option( - "-p", "--pin-date", is_flag=True, default=False, help="Leave date components unchanged." -) +@click.option("-m", "--minor", is_flag=True, default=False, help="Increment minor component.") +@click.option("-p", "--patch", is_flag=True, default=False, help="Increment patch component.") +@click.option("-r", "--release-num", is_flag=True, default=False, help="Increment release number.") +@click.option("--pin-date", is_flag=True, default=False, help="Leave date components unchanged.") def test( old_version: str, pattern : str = "{pycalver}", @@ -101,6 +106,7 @@ def test( major : bool = False, minor : bool = False, patch : bool = False, + release_num: bool = False, pin_date : bool = False, ) -> None: """Increment a version number for demo purposes.""" @@ -117,6 +123,7 @@ def test( major=major, minor=minor, patch=patch, + release_num=release_num, pin_date=pin_date, ) if new_version is None: @@ -193,6 +200,7 @@ def _incr( major : bool = False, minor : bool = False, patch : bool = False, + release_num: bool = False, pin_date: bool = False, ) -> typ.Optional[str]: v1_parts = list(v1patterns.PART_PATTERNS) + list(v1patterns.FULL_PART_FORMATS) @@ -205,6 +213,7 @@ def _incr( major=major, minor=minor, patch=patch, + release_num=release_num, pin_date=pin_date, ) else: @@ -215,6 +224,7 @@ def _incr( major=major, minor=minor, patch=patch, + release_num=release_num, pin_date=pin_date, ) @@ -346,11 +356,10 @@ def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: ), ) @click.option("--major", is_flag=True, default=False, help="Increment major component.") -@click.option("--minor", is_flag=True, default=False, help="Increment minor component.") -@click.option("--patch", is_flag=True, default=False, help="Increment patch component.") -@click.option( - "-p", "--pin-date", is_flag=True, default=False, help="Leave date components unchanged." -) +@click.option("-m", "--minor", is_flag=True, default=False, help="Increment minor component.") +@click.option("-p", "--patch", is_flag=True, default=False, help="Increment patch component.") +@click.option("-r", "--release-num", is_flag=True, default=False, help="Increment release number.") +@click.option("--pin-date", is_flag=True, default=False, help="Leave date components unchanged.") def bump( release : typ.Optional[str] = None, verbose : int = 0, @@ -360,6 +369,7 @@ def bump( major : bool = False, minor : bool = False, patch : bool = False, + release_num: bool = False, pin_date : bool = False, ) -> None: """Increment the current version string and update project files.""" @@ -386,6 +396,7 @@ def bump( major=major, minor=minor, patch=patch, + release_num=release_num, pin_date=pin_date, ) diff --git a/src/pycalver/v1version.py b/src/pycalver/v1version.py index 978eb39..7659d8e 100644 --- a/src/pycalver/v1version.py +++ b/src/pycalver/v1version.py @@ -378,6 +378,7 @@ def incr( major : bool = False, minor : bool = False, patch : bool = False, + release_num: bool = False, pin_date: bool = False, ) -> typ.Optional[str]: """Increment version string. @@ -400,14 +401,16 @@ def incr( cur_vinfo = cur_vinfo._replace(bid=lexid.next_id(cur_vinfo.bid)) - if release: - cur_vinfo = cur_vinfo._replace(tag=release) if major: cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0, patch=0) if minor: cur_vinfo = cur_vinfo._replace(minor=cur_vinfo.minor + 1, patch=0) if patch: cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1) + if release_num: + cur_vinfo = cur_vinfo._replace(num=cur_vinfo.num + 1) + if release: + cur_vinfo = cur_vinfo._replace(tag=release) new_version = format_version(cur_vinfo, raw_pattern) if new_version == old_version: diff --git a/src/pycalver/v2version.py b/src/pycalver/v2version.py index fcf53ac..a4ef187 100644 --- a/src/pycalver/v2version.py +++ b/src/pycalver/v2version.py @@ -519,6 +519,7 @@ def incr( major : bool = False, minor : bool = False, patch : bool = False, + release_num: bool = False, pin_date: bool = False, ) -> typ.Optional[str]: """Increment version string. @@ -545,14 +546,18 @@ def incr( cur_vinfo = cur_vinfo._replace(bid=lexid.next_id(cur_vinfo.bid)) - if release: - cur_vinfo = cur_vinfo._replace(tag=release) if major: cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0, patch=0) if minor: cur_vinfo = cur_vinfo._replace(minor=cur_vinfo.minor + 1, patch=0) if patch: cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1) + if release_num: + cur_vinfo = cur_vinfo._replace(num=cur_vinfo.num + 1) + if release: + if release != cur_vinfo.tag: + cur_vinfo = cur_vinfo._replace(num=0) + cur_vinfo = cur_vinfo._replace(tag=release) # TODO (mb 2020-09-20): New Rollover Behaviour: # Reset major, minor, patch to zero if any part to the left of it is incremented From 56c9f9b36c10183e22c8ba66b4b47af756cc4eea Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 24 Sep 2020 21:21:36 +0000 Subject: [PATCH 33/98] misc updates --- CHANGELOG.md | 1 + README.md | 230 +++++++++++-------------------------- setup.py | 4 +- src/pycalver/__main__.py | 2 +- src/pycalver/v1patterns.py | 2 +- test/test_cli.py | 11 +- 6 files changed, 77 insertions(+), 173 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8aa7acc..8518113 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - New add `--release-num` to increment the `alphaN`/`betaN`/`a0`/`b0`/etc. release number - Fix gitlab #8: Push tags only pushed tags, not actual commit. - Fix gitlab #9: Make commit message configurable. + - Switch main repo from gitlab to github. ## v201907.0036 diff --git a/README.md b/README.md index d02232c..3fa315a 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,7 @@ # [PyCalVer: Automatic Calendar Versioning][repo_ref] -PyCalVer is a cli tool to search and replace version strings in the files of -your project. - -By default PyCalVer uses a format that looks like this: -`v201812.0123-beta`, but it can be configured to generate version strings -in many formats, including SemVer and other CalVer variants. - +PyCalVer is a CLI-tool to search and replace version strings in your project files. This project follows the pattern conventions from [calver.org][calver_org_ref]. Project/Repo: @@ -73,18 +67,15 @@ Code Quality/CI: The fastest way to setup a project is to use `pycalver init`. - ```shell $ pip install pycalver ... Installing collected packages: click pathlib2 typing toml pycalver -Successfully installed pycalver-202007.36 - -$ pycalver --version -pycalver, version v202007.0036 +Successfully installed pycalver-202009.1037 $ cd myproject -~/myproject$ pycalver init --dry +~/myproject/ +$ pycalver init --dry WARNING - File not found: pycalver.toml Exiting because of '--dry'. Would have written to pycalver.toml: @@ -106,14 +97,16 @@ Exiting because of '--dry'. Would have written to pycalver.toml: ] ``` -If you already have a `setup.cfg` file, the `init` sub-command will write to that -instead. +If you already have a `setup.cfg` file, the `init` sub-command will +write to that instead. ``` -~/myproject$ ls +~/myproject +$ ls README.md setup.cfg setup.py -~/myproject$ pycalver init +~/myproject +$ pycalver init WARNING - Couldn't parse setup.cfg: Missing [pycalver] section. Updated setup.cfg ``` @@ -150,14 +143,14 @@ make. ```ini [pycalver:file_patterns] setup.cfg = - current_version = {pycalver} + current_version = {version} setup.py = - version="{pep440_pycalver}" + version="{pep440_version}" src/mymodule_v*/__init__.py = - __version__ = "{pycalver}" + __version__ = "{version}" README.md = - [PyCalVer {calver}{build}{release}] - img.shields.io/static/v1.svg?label=PyCalVer&message={pycalver}&color=blue + [CalVer {version}] + img.shields.io/static/v1.svg?label=CalVer&message={version}&color=blue ``` To see if a pattern is found, you can use `pycalver bump --dry`, which will @@ -202,8 +195,8 @@ If there is no match for a pattern, bump will report an error. ```shell $ pycalver bump --dry --no-fetch -INFO - Old Version: v201901.0001-beta -INFO - New Version: v201902.0002-beta +INFO - Old Version: v201901.1001-beta +INFO - New Version: v201902.1002-beta ERROR - No match for pattern 'img.shields.io/static/v1.svg?label=PyCalVer&message={pycalver}&color=blue' ERROR - Pattern compiles to regex 'img\.shields\.io/static/v1\.svg\?label=PyCalVer&message=(?Pv(?P\d{4})(?P(?:0[0-9]|1[0-2]))\.(?P\d{4,})(?:-(?P (?:alpha|beta|dev|rc|post|final)))?)&color=blue' @@ -368,7 +361,7 @@ that shields.io parses the two "-" dashes before `beta` as one literal "-"): ``` -https://img.shields.io/badge/myproject-v201812.0116--beta-blue.svg +https://img.shields.io/badge/myproject-v202010.1116--beta-blue.svg ``` While you could use the following pattern, which will work fine for a @@ -376,20 +369,20 @@ while: ```ini README.md = - /badge/myproject-v{year}{month}.{build_no}--{release_tag}-blue.svg + /badge/myproject-{vYYYY0M.BUILD[--RELEASE]}-blue.svg ``` Eventually this will break, when you do a `final` release, at which point the following will be put in your README.md: ``` -https://img.shields.io/badge/myproject-v201812.0117--final-blue.svg +https://img.shields.io/badge/myproject-v202010.1117--final-blue.svg ``` When what you probably wanted was this (with the `--final` tag omitted): ``` -https://img.shields.io/badge/myproject-v201812.0117-blue.svg +https://img.shields.io/badge/myproject-v202010.1117-blue.svg ``` ### Examples @@ -397,18 +390,21 @@ https://img.shields.io/badge/myproject-v201812.0117-blue.svg The easiest way to test a pattern is with the `pycalver test` sub-command. ```shell -$ pycalver test 'v18w01' 'v{yy}w{iso_week}' +$ pycalver test 'v18w01' 'vYYw0W' New Version: v19w06 PEP440 : v19w06 -$ pycalver test 'v18.01' 'v{yy}w{iso_week}' +# TODO (mb 2020-09-24): Update regexp pattern + +$ pycalver test 'v18.01' 'vYYw0W' ERROR - Invalid version string 'v18.01' for pattern - 'v{yy}w{iso_week}'/'v(?P\d{2})w(?P(?:[0-4]\d|5[0-3]))' -ERROR - Invalid version 'v18.01' and/or pattern 'v{yy}w{iso_week}'. + 'vYYw0W'/'v(?P\d{2})w(?P<0W>(?:[0-4]\d|5[0-2]))' +ERROR - Invalid version 'v18.01' and/or pattern 'vYYw0W'. ``` -As you can see, each pattern is internally translated to a regular -expression. +As you can see, each pattern is internally translated to a regular expression. +All version strings in your project must match either this regular expression or +the corresponding regular expression for the PEP440 version string. The `pycalver test` sub-command accepts the same cli flags as `pycalver bump` to update the components that are not updated automatically (eg. @@ -474,15 +470,8 @@ The current version that will be bumped is defined either as supported VCS), the value of `pycalver.current_version` in `setup.cfg` / `pyproject.toml` / `pycalver.toml`. -As part of doing `pycalver bump`, your local VCS index is updated using -`git fetch --tags`/`hg pull`. This reduces the risk that some tags are -unknown locally and makes it less likely that the same version string is -generated for different commits, which would result in an ambiguous version -tag. This can happen if multiple maintainers produce a release at the same -time or if a build system is triggered multiple times and multiple builds -run concurrently to each other. For a small project (with only one -maintainer and no build system) this is a non-issue and you can always use -`-n/--no-fetch` to skip updating the tags. +As part of doing `pycalver bump` and `pycalver show`, your local VCS +index is updated using `git fetch --tags`/`hg pull`. ```shell $ time pycalver show --verbose @@ -499,6 +488,20 @@ $ time pycalver show --verbose --no-fetch real 0m0,840s ``` +Here we see that: + +- The VCS had a newer version than we had locally. +- It took 4 seconds to fetch the tags from the remote repository. + +This approach reduces the risk that new tags are unknown locally and makes it +less likely that the same version string is generated for different commits, +which would result in an ambiguous version tag. This can happen if multiple +maintainers produce a release at the same time or if a build system is triggered +multiple times and multiple builds run concurrently to each other. + +For a small project (with only one maintainer and no build system) this is a +non-issue and you can always use `-n/--no-fetch` to skip updating the tags. + ### Bump It Up @@ -745,115 +748,12 @@ with extra zeros (see [Lexical Ids](#lexical-ids) ). ### Lexical Ids -The build number padding may eventually be exhausted. In order to preserve -lexical ordering, build numbers for the `{build_no}` pattern are -incremented in a special way. Examples will perhaps illustrate more -clearly. - -```python -"0001" -"0002" -"0003" -... -"0999" -"11000" -"11001" -... -"19998" -"19999" -"220000" -"220001" -``` - -What is happening here is that the left-most digit is incremented -early/preemptively. Whenever the left-most digit would change, the padding -of the id is expanded through a multiplication by 11. - -```python ->>> prev_id = "0999" ->>> num_digits = len(prev_id) ->>> num_digits -4 ->>> prev_int = int(prev_id, 10) ->>> prev_int -999 ->>> maybe_next_int = prev_int + 1 ->>> maybe_next_int -1000 ->>> maybe_next_id = f"{maybe_next_int:0{num_digits}}" ->>> maybe_next_id -"1000" ->>> is_padding_ok = prev_id[0] == maybe_next_id[0] ->>> is_padding_ok -False ->>> if is_padding_ok: -... # normal case -... next_id = maybe_next_id -... else: -... # extra padding needed -... next_int = maybe_next_int * 11 -... next_id = str(next_int) ->>> next_id -"11000" -``` - -This behaviour ensures that the following semantic is always preserved: -`new_version > old_version`. This will be true, regardless of padding -expansion. To illustrate the issue this solves, consider what would happen -if we did not expand the padding and instead just incremented numerically. - -```python -"0001" -"0002" -"0003" -... -"0999" -"1000" -... -"9999" -"10000" -``` - -Here we eventually run into a build number where the lexical ordering is -not preserved, since `"10000" > "9999" == False` (because the string `"1"` -is lexically smaller than `"9"`). With large enough padding this may be a -non issue, but it's better to not have to think about it. - -Just as an example of why lexical ordering is a nice property to have, -there are lots of software which read git tags, but which have no logic to -parse version strings. This software can nonetheless order the version tags -correctly using commonly available lexical ordering. At the most basic -level it can allow you to use the UNIX `sort` command, for example to parse VCS tags. -```shell -$ printf "v0.9.0\nv0.10.0\nv0.11.0\n" | sort -v0.10.0 -v0.11.0 -v0.9.0 - -$ printf "v0.9.0\nv0.10.0\nv0.11.0\n" | sort -n -v0.10.0 -v0.11.0 -v0.9.0 - -$ printf "0998\n0999\n11000\n11001\n11002\n" | sort -0998 -0999 -11000 -11001 -11002 -``` This sorting even works correctly in JavaScript! -``` -> var versions = ["11002", "11001", "11000", "0999", "0998"]; -> versions.sort(); -["0998", "0999", "11000", "11001", "11002"] -``` - ## Semantics of PyCalVer @@ -1001,7 +901,7 @@ release *only* if 1. no `final` release is available 2. the `--pre` flag is explicitly used, or 3. if the requirement specifier _explicitly_ includes the version number of a - pre release, eg. `pip install mypkg==v201812.0007-alpha`. + pre release, eg. `pip install mypkg==v202009.1007-alpha`. Should a release include a bug (heaven forbid and despite all precautions), then the maintainer should publish a new release which either fixes the bug @@ -1012,20 +912,20 @@ package which included the bug, they only have to do `pip install --upgrade Perhaps a timeline will illustrate more clearly: ``` -v201812.0665 # last stable release -v201812.0666-beta # pre release for testers -v201901.0667 # final release after testing +v202008.1665 # last stable release +v202008.1666-beta # pre release for testers +v201901.1667 # final release after testing -# bug is discovered which effects v201812.0666-beta and v201901.0667 +# bug is discovered which effects v202008.1666-beta and v201901.1667 -v201901.0668-beta # fix is issued for testers -v201901.0669 # fix is issued everybody +v201901.1668-beta # fix is issued for testers +v201901.1669 # fix is issued everybody # Alternatively, revert before fixing -v201901.0668 # same as v201812.0665 -v201901.0669-beta # reintroduce change from v201812.0666-beta + fix -v201901.0670 # final release after testing +v201901.1668 # same as v202008.1665 +v201901.1669-beta # reintroduce change from v202008.1666-beta + fix +v201901.1670 # final release after testing ``` In the absolute worst case, a change is discovered to break backward @@ -1048,18 +948,18 @@ package will perhaps have 99% overlap to the previous one and the old one may eventually be abandoned. ``` -mypkg v201812.0665 # last stable release -mypkg v201812.0666-rc # pre release for testers -mypkg v201901.0667 # final release after testing period +mypkg v202008.1665 # last stable release +mypkg v202008.1666-rc # pre release for testers +mypkg v201901.1667 # final release after testing period -# bug is discovered in v201812.0666-beta and v201901.0667 +# bug is discovered in v202008.1666-beta and v201901.1667 -mypkg v201901.0668 # same as v201812.0665 +mypkg v201901.1668 # same as v202008.1665 # new package is created with compatibility breaking code -mypkg2 v201901.0669 # same as v201901.0667 -mypkg v201901.0669 # updated readme, declaring support +mypkg2 v201901.1669 # same as v201901.1667 +mypkg v201901.1669 # updated readme, declaring support # level for mypkg, pointing to mypgk2 # and documenting how to upgrade. ``` @@ -1116,6 +1016,8 @@ of the software as a whole, it is metadata about a particular release artifact of a package, eg. a `.whl` file. +[calver_org_ref]: https://calver.org/ + [repo_ref]: https://gitlab.com/mbarkhau/pycalver [setuptools_ref]: https://setuptools.readthedocs.io/en/latest/setuptools.html#specifying-your-project-s-version @@ -1124,6 +1026,8 @@ artifact of a package, eg. a `.whl` file. [pep_440_ref]: https://www.python.org/dev/peps/pep-0440/ +[pep_440_normalzation_ref]: https://www.python.org/dev/peps/pep-0440/#id31 + [zeno_1_dot_0_ref]: http://sedimental.org/designing_a_version.html#semver-and-release-blockage [pep_101_ref]: https://www.python.org/dev/peps/pep-0101/ diff --git a/setup.py b/setup.py index 5ad112f..18477ea 100644 --- a/setup.py +++ b/setup.py @@ -80,8 +80,8 @@ setuptools.setup( license="MIT", author="Manuel Barkhau", author_email="mbarkhau@gmail.com", - url="https://gitlab.com/mbarkhau/pycalver", - version="202007.36", + url="https://github.com/mbarkhau/pycalver", + version="202007.1036", keywords="version versioning bumpversion calver", description="CalVer for python libraries.", long_description=long_description, diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 0c28381..04ae1f6 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -241,7 +241,7 @@ def _bump( try: vcs_api = vcs.get_vcs_api() except OSError: - logger.warning("Version Control System not found, aborting commit.") + logger.warning("Version Control System not found, skipping commit.") filepaths = set(cfg.file_patterns.keys()) diff --git a/src/pycalver/v1patterns.py b/src/pycalver/v1patterns.py index a3ace0c..8569674 100644 --- a/src/pycalver/v1patterns.py +++ b/src/pycalver/v1patterns.py @@ -160,7 +160,7 @@ FULL_PART_FORMATS = { 'release_tag' : "{tag}", 'build' : ".{bid}", # NOTE (mb 2019-01-04): since release is optional, it - # is treates specially in version.format + # is treated specially in v1version.format_version # 'release' : "-{tag}", 'month' : "{month:02}", 'month_short': "{month}", diff --git a/test/test_cli.py b/test/test_cli.py index 1983c89..308af00 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -175,13 +175,12 @@ def test_incr_invalid(runner): def _add_project_files(*files): if "README.md" in files: + README_TEXT = """ + Hello World v201701.1002-alpha ! + aka. 201701.1002a0 ! + """ with pl.Path("README.md").open(mode="wt", encoding="utf-8") as fobj: - fobj.write( - """ - Hello World v201701.1002-alpha ! - aka. 201701.1002a0 ! - """ - ) + fobj.write(README_TEXT) if "setup.cfg" in files: with pl.Path("setup.cfg").open(mode="wt", encoding="utf-8") as fobj: From 49e19fbf8915504e8c8c95f1c85c814a7024dad3 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Fri, 2 Oct 2020 20:52:54 +0000 Subject: [PATCH 34/98] much bugfixing --- pylint-ignore.md | 104 +++++++--------- setup.cfg | 2 +- src/pycalver/__main__.py | 45 +++---- src/pycalver/config.py | 60 +++++---- src/pycalver/parse.py | 39 +++++- src/pycalver/v1cli.py | 8 -- src/pycalver/v1patterns.py | 33 ++--- src/pycalver/v1rewrite.py | 64 ++-------- src/pycalver/v1version.py | 12 +- src/pycalver/v2cli.py | 8 -- src/pycalver/v2patterns.py | 25 ++-- src/pycalver/v2rewrite.py | 69 ++--------- src/pycalver/v2version.py | 248 ++++++++++++++++++++++--------------- src/pycalver/vcs.py | 4 +- src/pycalver/version.py | 4 + test/test_cli.py | 118 ++++++++++++++---- test/test_config.py | 69 +++++++---- test/test_rewrite.py | 226 ++++++++++++++++++++++++++++----- 18 files changed, 687 insertions(+), 451 deletions(-) diff --git a/pylint-ignore.md b/pylint-ignore.md index ed68081..9bbd051 100644 --- a/pylint-ignore.md +++ b/pylint-ignore.md @@ -23,7 +23,7 @@ The recommended approach to using `pylint-ignore` is: # Overview - - [W0511: fixme (9x)](#w0511-fixme) + - [W0511: fixme (8x)](#w0511-fixme) - [W0703: broad-except (1x)](#w0703-broad-except) @@ -44,37 +44,20 @@ The recommended approach to using `pylint-ignore` is: ``` -## File src/pycalver/vcs.py - Line 78 - W0511 (fixme) +## File src/pycalver/vcs.py - Line 80 - W0511 (fixme) - `message: TODO (mb 2018-11-15): Detect encoding of output? Use chardet?` - `author : Manuel Barkhau ` - `date : 2020-09-18T17:24:49` ``` - 68: def __call__(self, cmd_name: str, env: Env = None, **kwargs: str) -> str: + 69: def __call__(self, cmd_name: str, env: Env = None, **kwargs: str) -> str: ... - 76: output_data: bytes = sp.check_output(cmd_str.split(), env=env, stderr=sp.STDOUT) - 77: -> 78: # TODO (mb 2018-11-15): Detect encoding of output? Use chardet? - 79: _encoding = "utf-8" - 80: return output_data.decode(_encoding) -``` - - -## File test/test_config.py - Line 156 - W0511 (fixme) - -- `message: TODO (mb 2020-09-18):` -- `author : Manuel Barkhau ` -- `date : 2020-09-18T19:04:06` - -``` - 143: def test_parse_v2_cfg(): - ... - 154: assert "setup.py" in cfg.file_patterns - 155: assert "setup.cfg" in cfg.file_patterns -> 156: # TODO (mb 2020-09-18): - 157: # assert cfg.file_patterns["setup.py" ] == ["vYYYY0M.BUILD[-RELEASE]", "YYYY0M.BLD[PYTAGNUM]"] - 158: # assert cfg.file_patterns["setup.cfg" ] == ['current_version = "vYYYY0M.BUILD[-RELEASE]"'] + 78: output_data: bytes = sp.check_output(cmd_parts, env=env, stderr=sp.STDOUT) + 79: +> 80: # TODO (mb 2018-11-15): Detect encoding of output? Use chardet? + 81: _encoding = "utf-8" + 82: return output_data.decode(_encoding) ``` @@ -95,37 +78,37 @@ The recommended approach to using `pylint-ignore` is: ``` -## File src/pycalver/v1patterns.py - Line 214 - W0511 (fixme) +## File test/test_config.py - Line 170 - W0511 (fixme) -- `message: TODO (mb 2020-09-19): replace {version} etc with version_pattern` +- `message: TODO (mb 2020-09-18):` - `author : Manuel Barkhau ` -- `date : 2020-09-19T16:24:10` +- `date : 2020-09-18T19:04:06` ``` - 201: def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[str]: + 156: def test_parse_v2_cfg(): ... - 212: escaped_pattern = escaped_pattern.replace(char, escaped) - 213: -> 214: # TODO (mb 2020-09-19): replace {version} etc with version_pattern - 215: pattern_str = _replace_pattern_parts(escaped_pattern) - 216: return re.compile(pattern_str) + 168: assert "setup.cfg" in cfg.file_patterns + 169: +> 170: # TODO (mb 2020-09-18): + 171: # raw_patterns_by_file = _parse_raw_patterns_by_file(cfg) + 172: # assert raw_patterns_by_file["setup.py" ] == ["vYYYY0M.BUILD[-RELEASE]", "YYYY0M.BLD[PYTAGNUM]"] ``` -## File src/pycalver/__main__.py - Line 250 - W0511 (fixme) +## File src/pycalver/__main__.py - Line 259 - W0511 (fixme) - `message: TODO (mb 2020-09-18): Investigate error messages` - `author : Manuel Barkhau ` - `date : 2020-09-19T16:24:10` ``` - 222: def _bump( + 231: def _bump( ... - 248: sys.exit(1) - 249: except Exception as ex: -> 250: # TODO (mb 2020-09-18): Investigate error messages - 251: logger.error(str(ex)) - 252: sys.exit(1) + 257: sys.exit(1) + 258: except Exception as ex: +> 259: # TODO (mb 2020-09-18): Investigate error messages + 260: logger.error(str(ex)) + 261: sys.exit(1) ``` @@ -146,53 +129,52 @@ The recommended approach to using `pylint-ignore` is: ``` -## File test/test_cli.py - Line 536 - W0511 (fixme) +## File test/test_cli.py - Line 599 - W0511 (fixme) - `message: # TODO (mb 2020-09-18):` - `author : Manuel Barkhau ` - `date : 2020-09-18T19:35:32` ``` - 534: - 535: # def test_custom_commit_message(runner): -> 536: # # TODO (mb 2020-09-18): - 537: # assert False + 597: + 598: # def test_custom_commit_message(runner): +> 599: # # TODO (mb 2020-09-18): + 600: # assert False ``` -## File src/pycalver/v2version.py - Line 551 - W0511 (fixme) +## File src/pycalver/v2version.py - Line 616 - W0511 (fixme) - `message: TODO (mb 2020-09-20): New Rollover Behaviour:` - `author : Manuel Barkhau ` - `date : 2020-09-20T17:36:38` ``` - 508: def incr( + 578: def incr( ... - 549: cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1) - 550: -> 551: # TODO (mb 2020-09-20): New Rollover Behaviour: - 552: # Reset major, minor, patch to zero if any part to the left of it is incremented - 553: + 614: ) + 615: +> 616: # TODO (mb 2020-09-20): New Rollover Behaviour: + 617: # Reset major, minor, patch to zero if any part to the left of it is incremented + 618: ``` # W0703: broad-except -## File src/pycalver/__main__.py - Line 249 - W0703 (broad-except) +## File src/pycalver/__main__.py - Line 258 - W0703 (broad-except) - `message: Catching too general exception Exception` - `author : Manuel Barkhau ` - `date : 2020-09-05T14:30:17` ``` - 222: def _bump( + 231: def _bump( ... - 247: logger.error(str(ex)) - 248: sys.exit(1) -> 249: except Exception as ex: - 250: # TODO (mb 2020-09-18): Investigate error messages - 251: logger.error(str(ex)) + 256: logger.error(str(ex)) + 257: sys.exit(1) +> 258: except Exception as ex: + 259: # TODO (mb 2020-09-18): Investigate error messages + 260: logger.error(str(ex)) ``` - diff --git a/setup.cfg b/setup.cfg index 22e319c..3e3ebc7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -137,7 +137,7 @@ output-format = colorized max-locals = 21 # Maximum number of arguments for function / method -max-args = 9 +max-args = 12 good-names = logger,i,ex diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 04ae1f6..1d474de 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -22,7 +22,9 @@ from . import v2cli from . import config from . import rewrite from . import version +from . import v1rewrite from . import v1version +from . import v2rewrite from . import v2version from . import v1patterns @@ -93,10 +95,10 @@ def cli(verbose: int = 0) -> None: f"{', '.join(VALID_RELEASE_VALUES)}." ), ) -@click.option("--major", is_flag=True, default=False, help="Increment major component.") -@click.option("-m", "--minor", is_flag=True, default=False, help="Increment minor component.") -@click.option("-p", "--patch", is_flag=True, default=False, help="Increment patch component.") -@click.option("-r", "--release-num", is_flag=True, default=False, help="Increment release number.") +@click.option("--major" , is_flag=True, default=False, help="Increment major component.") +@click.option("-m" , "--minor" , is_flag=True, default=False, help="Increment minor component.") +@click.option("-p" , "--patch" , is_flag=True, default=False, help="Increment patch component.") +@click.option("-r" , "--release-num", is_flag=True, default=False, help="Increment release number.") @click.option("--pin-date", is_flag=True, default=False, help="Leave date components unchanged.") def test( old_version: str, @@ -145,8 +147,7 @@ def show(verbose: int = 0, fetch: bool = True) -> None: """Show current version of your project.""" _configure_logging(verbose=max(_VERBOSE, verbose)) - ctx: config.ProjectContext = config.init_project_ctx(project_path=".") - cfg: config.MaybeConfig = config.parse(ctx) + _, cfg = config.init(project_path=".") if cfg is None: logger.error("Could not parse configuration. Perhaps try 'pycalver init'.") @@ -188,7 +189,7 @@ def _print_diff(cfg: config.Config, new_version: str) -> None: except Exception as ex: # pylint:disable=broad-except; Mostly we expect IOError here, but # could be other things and there's no option to recover anyway. - logger.error(str(ex)) + logger.error(str(ex), exc_info=True) sys.exit(1) @@ -196,12 +197,12 @@ def _incr( old_version: str, raw_pattern: str, *, - release : str = None, - major : bool = False, - minor : bool = False, - patch : bool = False, + release : str = None, + major : bool = False, + minor : bool = False, + patch : bool = False, release_num: bool = False, - pin_date: bool = False, + pin_date : bool = False, ) -> typ.Optional[str]: v1_parts = list(v1patterns.PART_PATTERNS) + list(v1patterns.FULL_PART_FORMATS) has_v1_part = any("{" + part + "}" in raw_pattern for part in v1_parts) @@ -250,9 +251,11 @@ def _bump( try: if cfg.is_new_pattern: - v2cli.rewrite_files(cfg, new_version) + new_v2_vinfo = v2version.parse_version_info(new_version, cfg.version_pattern) + v2rewrite.rewrite_files(cfg.file_patterns, new_v2_vinfo) else: - v1cli.rewrite_files(cfg, new_version) + new_v1_vinfo = v1version.parse_version_info(new_version, cfg.version_pattern) + v1rewrite.rewrite_files(cfg.file_patterns, new_v1_vinfo) except rewrite.NoPatternMatch as ex: logger.error(str(ex)) sys.exit(1) @@ -291,8 +294,7 @@ def init(verbose: int = 0, dry: bool = False) -> None: """Initialize [pycalver] configuration.""" _configure_logging(verbose=max(_VERBOSE, verbose)) - ctx: config.ProjectContext = config.init_project_ctx(project_path=".") - cfg: config.MaybeConfig = config.parse(ctx) + ctx, cfg = config.init(project_path=".") if cfg: logger.error(f"Configuration already initialized in {ctx.config_rel_path}") @@ -355,10 +357,10 @@ def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: "to files with version strings." ), ) -@click.option("--major", is_flag=True, default=False, help="Increment major component.") -@click.option("-m", "--minor", is_flag=True, default=False, help="Increment minor component.") -@click.option("-p", "--patch", is_flag=True, default=False, help="Increment patch component.") -@click.option("-r", "--release-num", is_flag=True, default=False, help="Increment release number.") +@click.option("--major" , is_flag=True, default=False, help="Increment major component.") +@click.option("-m" , "--minor" , is_flag=True, default=False, help="Increment minor component.") +@click.option("-p" , "--patch" , is_flag=True, default=False, help="Increment patch component.") +@click.option("-r" , "--release-num", is_flag=True, default=False, help="Increment release number.") @click.option("--pin-date", is_flag=True, default=False, help="Leave date components unchanged.") def bump( release : typ.Optional[str] = None, @@ -379,8 +381,7 @@ def bump( if release: _validate_release_tag(release) - ctx: config.ProjectContext = config.init_project_ctx(project_path=".") - cfg: config.MaybeConfig = config.parse(ctx) + _, cfg = config.init(project_path=".") if cfg is None: logger.error("Could not parse configuration. Perhaps try 'pycalver init'.") diff --git a/src/pycalver/config.py b/src/pycalver/config.py index ecfb774..717709e 100644 --- a/src/pycalver/config.py +++ b/src/pycalver/config.py @@ -188,6 +188,8 @@ def _parse_cfg(cfg_buffer: typ.IO[str]) -> RawConfig: raw_cfg['file_patterns'] = dict(_parse_cfg_file_patterns(cfg_parser)) + _set_raw_config_defaults(raw_cfg) + return raw_cfg @@ -198,6 +200,8 @@ def _parse_toml(cfg_buffer: typ.IO[str]) -> RawConfig: for option, default_val in BOOL_OPTIONS.items(): raw_cfg[option] = raw_cfg.get(option, default_val) + _set_raw_config_defaults(raw_cfg) + return raw_cfg @@ -227,9 +231,7 @@ def _compile_v1_file_patterns(raw_cfg: RawConfig) -> typ.Iterable[FilePatternsIt raw_patterns_by_file: RawPatternsByFile = raw_cfg['file_patterns'] for filepath, raw_patterns in _iter_glob_expanded_file_patterns(raw_patterns_by_file): - compiled_patterns = [ - v1patterns.compile_pattern(version_pattern, raw_pattern) for raw_pattern in raw_patterns - ] + compiled_patterns = v1patterns.compile_patterns(version_pattern, raw_patterns) yield filepath, compiled_patterns @@ -242,9 +244,7 @@ def _compile_v2_file_patterns(raw_cfg: RawConfig) -> typ.Iterable[FilePatternsIt raw_patterns_by_file: RawPatternsByFile = raw_cfg['file_patterns'] for filepath, raw_patterns in _iter_glob_expanded_file_patterns(raw_patterns_by_file): - compiled_patterns = [ - v2patterns.compile_pattern(version_pattern, raw_pattern) for raw_pattern in raw_patterns - ] + compiled_patterns = v2patterns.compile_patterns(version_pattern, raw_patterns) yield filepath, compiled_patterns @@ -319,12 +319,7 @@ def _parse_config(raw_cfg: RawConfig) -> Config: return cfg -def _parse_current_version_default_pattern(ctx: ProjectContext, raw_cfg: RawConfig) -> str: - fobj: typ.IO[str] - - with ctx.config_filepath.open(mode="rt", encoding="utf-8") as fobj: - raw_cfg_text = fobj.read() - +def _parse_current_version_default_pattern(raw_cfg: RawConfig, raw_cfg_text: str) -> str: is_pycalver_section = False for line in raw_cfg_text.splitlines(): if is_pycalver_section and line.startswith("current_version"): @@ -340,19 +335,7 @@ def _parse_current_version_default_pattern(ctx: ProjectContext, raw_cfg: RawConf raise ValueError("Could not parse pycalver.current_version") -def _parse_raw_config(ctx: ProjectContext) -> RawConfig: - with ctx.config_filepath.open(mode="rt", encoding="utf-8") as fobj: - if ctx.config_format == 'toml': - raw_cfg = _parse_toml(fobj) - elif ctx.config_format == 'cfg': - raw_cfg = _parse_cfg(fobj) - else: - err_msg = ( - f"Invalid config_format='{ctx.config_format}'." - "Supported formats are 'setup.cfg' and 'pyproject.toml'" - ) - raise RuntimeError(err_msg) - +def _set_raw_config_defaults(raw_cfg: RawConfig) -> None: if 'current_version' in raw_cfg: if not isinstance(raw_cfg['current_version'], str): err = f"Invalid type for pycalver.current_version = {raw_cfg['current_version']}" @@ -370,10 +353,27 @@ def _parse_raw_config(ctx: ProjectContext) -> RawConfig: if 'file_patterns' not in raw_cfg: raw_cfg['file_patterns'] = {} + +def _parse_raw_config(ctx: ProjectContext) -> RawConfig: + with ctx.config_filepath.open(mode="rt", encoding="utf-8") as fobj: + if ctx.config_format == 'toml': + raw_cfg = _parse_toml(fobj) + elif ctx.config_format == 'cfg': + raw_cfg = _parse_cfg(fobj) + else: + err_msg = ( + f"Invalid config_format='{ctx.config_format}'." + "Supported formats are 'setup.cfg' and 'pyproject.toml'" + ) + raise RuntimeError(err_msg) + if ctx.config_rel_path not in raw_cfg['file_patterns']: # NOTE (mb 2020-09-19): By default we always add # a pattern for the config section itself. - raw_version_pattern = _parse_current_version_default_pattern(ctx, raw_cfg) + with ctx.config_filepath.open(mode="rt", encoding="utf-8") as fobj: + raw_cfg_text = fobj.read() + + raw_version_pattern = _parse_current_version_default_pattern(raw_cfg, raw_cfg_text) raw_cfg['file_patterns'][ctx.config_rel_path] = [raw_version_pattern] return raw_cfg @@ -393,6 +393,14 @@ def parse(ctx: ProjectContext) -> MaybeConfig: return None +def init( + project_path: typ.Union[str, pl.Path, None] = "." +) -> typ.Tuple[ProjectContext, MaybeConfig]: + ctx = init_project_ctx(project_path) + cfg = parse(ctx) + return (ctx, cfg) + + DEFAULT_CONFIGPARSER_BASE_TMPL = """ [pycalver] current_version = "{initial_version}" diff --git a/src/pycalver/parse.py b/src/pycalver/parse.py index 0b80519..205c01a 100644 --- a/src/pycalver/parse.py +++ b/src/pycalver/parse.py @@ -9,14 +9,43 @@ import typing as typ from .patterns import Pattern +LineNo = int +Start = int +End = int + + +class LineSpan(typ.NamedTuple): + lineno: LineNo + start : Start + end : End + + +LineSpans = typ.List[LineSpan] + + +def _has_overlap(needle: LineSpan, haystack: LineSpans) -> bool: + for span in haystack: + # assume needle is in the center + has_overlap = ( + span.lineno == needle.lineno + # needle starts before (or at) span end + and needle.start <= span.end + # needle ends after (or at) span start + and needle.end >= span.start + ) + if has_overlap: + return True + + return False + class PatternMatch(typ.NamedTuple): """Container to mark a version string in a file.""" - lineno : int # zero based + lineno : LineNo # zero based line : str pattern: Pattern - span : typ.Tuple[int, int] + span : typ.Tuple[Start, End] match : str @@ -47,6 +76,10 @@ def iter_matches(lines: typ.List[str], patterns: typ.List[Pattern]) -> PatternMa ... match = "v201712.0002-alpha", ... ) """ + matched_spans: LineSpans = [] for pattern in patterns: for match in _iter_for_pattern(lines, pattern): - yield match + needle_span = LineSpan(match.lineno, *match.span) + if not _has_overlap(needle_span, matched_spans): + yield match + matched_spans.append(needle_span) diff --git a/src/pycalver/v1cli.py b/src/pycalver/v1cli.py index b34d0b2..47c3c2d 100755 --- a/src/pycalver/v1cli.py +++ b/src/pycalver/v1cli.py @@ -42,14 +42,6 @@ def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.C ) -def rewrite_files( - cfg : config.Config, - new_version: str, -) -> None: - new_vinfo = v1version.parse_version_info(new_version, cfg.version_pattern) - v1rewrite.rewrite_files(cfg.file_patterns, new_vinfo) - - def get_diff(cfg: config.Config, new_version: str) -> str: old_vinfo = v1version.parse_version_info(cfg.current_version, cfg.version_pattern) new_vinfo = v1version.parse_version_info(new_version , cfg.version_pattern) diff --git a/src/pycalver/v1patterns.py b/src/pycalver/v1patterns.py index 8569674..f3d22ce 100644 --- a/src/pycalver/v1patterns.py +++ b/src/pycalver/v1patterns.py @@ -81,8 +81,8 @@ PART_PATTERNS = { 'month' : r"(?:0[0-9]|1[0-2])", 'month_short': r"(?:1[0-2]|[1-9])", 'build_no' : r"\d{4,}", - 'pep440_tag' : r"(?:post|dev|rc|a|b)?\d*", - 'tag' : r"(?:preview|final|alpha|beta|post|pre|dev|rc|a|b|c|r)", + 'pep440_tag' : r"(?:a|b|dev|rc|post)?\d*", + 'tag' : r"(?:alpha|beta|dev|rc|post|final)", 'yy' : r"\d{2}", 'yyyy' : r"\d{4}", 'quarter' : r"[1-4]", @@ -198,26 +198,29 @@ def _init_composite_patterns() -> None: _init_composite_patterns() -def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[str]: - normalized_pattern = raw_pattern.replace(r"{version}", version_pattern) - if version_pattern == r"{pycalver}": - normalized_pattern = normalized_pattern.replace(r"{pep440_version}", r"{pep440_pycalver}") - elif version_pattern == r"{semver}": - normalized_pattern = normalized_pattern.replace(r"{pep440_version}", r"{semver}") - elif r"{pep440_version}" in raw_pattern: - logger.warning(f"No mapping of '{version_pattern}' to '{{pep440_version}}'") - +def _compile_pattern_re(normalized_pattern: str) -> typ.Pattern[str]: escaped_pattern = normalized_pattern for char, escaped in RE_PATTERN_ESCAPES: escaped_pattern = escaped_pattern.replace(char, escaped) - # TODO (mb 2020-09-19): replace {version} etc with version_pattern pattern_str = _replace_pattern_parts(escaped_pattern) return re.compile(pattern_str) @utils.memo def compile_pattern(version_pattern: str, raw_pattern: typ.Optional[str] = None) -> Pattern: - _raw_pattern = version_pattern if raw_pattern is None else raw_pattern - regexp = _compile_pattern_re(version_pattern, _raw_pattern) - return Pattern(version_pattern, _raw_pattern, regexp) + _raw_pattern = version_pattern if raw_pattern is None else raw_pattern + normalized_pattern = _raw_pattern.replace(r"{version}", version_pattern) + if version_pattern == r"{pycalver}": + normalized_pattern = normalized_pattern.replace(r"{pep440_version}", r"{pep440_pycalver}") + elif version_pattern == r"{semver}": + normalized_pattern = normalized_pattern.replace(r"{pep440_version}", r"{semver}") + elif r"{pep440_version}" in _raw_pattern: + logger.warning(f"No mapping of '{version_pattern}' to '{{pep440_version}}'") + + regexp = _compile_pattern_re(normalized_pattern) + return Pattern(version_pattern, normalized_pattern, regexp) + + +def compile_patterns(version_pattern: str, raw_patterns: typ.List[str]) -> typ.List[Pattern]: + return [compile_pattern(version_pattern, raw_pattern) for raw_pattern in raw_patterns] diff --git a/src/pycalver/v1rewrite.py b/src/pycalver/v1rewrite.py index 1bf652c..e2f6313 100644 --- a/src/pycalver/v1rewrite.py +++ b/src/pycalver/v1rewrite.py @@ -24,19 +24,7 @@ def rewrite_lines( new_vinfo: version.V1VersionInfo, old_lines: typ.List[str], ) -> typ.List[str]: - """Replace occurances of patterns in old_lines with new_vinfo. - - >>> from .v1patterns import compile_pattern - >>> version_pattern = "{pycalver}" - >>> new_vinfo = v1version.parse_version_info("v201811.0123-beta", version_pattern) - >>> patterns = [compile_pattern(version_pattern, '__version__ = "{pycalver}"')] - >>> rewrite_lines(patterns, new_vinfo, ['__version__ = "v201809.0002-beta"']) - ['__version__ = "v201811.0123-beta"'] - - >>> patterns = [compile_pattern(version_pattern, '__version__ = "{pep440_version}"')] - >>> rewrite_lines(patterns, new_vinfo, ['__version__ = "201809.2b0"']) - ['__version__ = "201811.123b0"'] - """ + """Replace occurances of patterns in old_lines with new_vinfo.""" found_patterns: typ.Set[Pattern] = set() new_lines = old_lines[:] @@ -65,10 +53,12 @@ def rfd_from_content( ) -> rewrite.RewrittenFileData: r"""Rewrite pattern occurrences with version string. - >>> from .v1patterns import compile_pattern - >>> patterns = [compile_pattern("{pycalver}", '__version__ = "{pycalver}"'] + >>> version_pattern = "{pycalver}" >>> new_vinfo = v1version.parse_version_info("v201809.0123") + >>> from .v1patterns import compile_pattern + >>> patterns = [compile_pattern(version_pattern, '__version__ = "{pycalver}"')] + >>> content = '__version__ = "v201809.0001-alpha"' >>> rfd = rfd_from_content(patterns, new_vinfo, content) >>> rfd.new_lines @@ -92,26 +82,7 @@ def iter_rewritten( file_patterns: config.PatternsByFile, new_vinfo : version.V1VersionInfo, ) -> typ.Iterable[rewrite.RewrittenFileData]: - r'''Iterate over files with version string replaced. - - >>> version_pattern = "{pycalver}" - >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']} - >>> new_vinfo = v1version.parse_version_info("v201809.0123") - >>> rewritten_datas = iter_rewritten(version_pattern, file_patterns, new_vinfo) - >>> rfd = list(rewritten_datas)[0] - >>> expected = [ - ... '# This file is part of the pycalver project', - ... '# https://github.com/mbarkhau/pycalver', - ... '#', - ... '# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License', - ... '# SPDX-License-Identifier: MIT', - ... '"""PyCalVer: CalVer for Python Packages."""', - ... '', - ... '__version__ = "v201809.0123"', - ... '', - ... ] - >>> assert rfd.new_lines == expected - ''' + """Iterate over files with version string replaced.""" fobj: typ.IO[str] @@ -128,24 +99,7 @@ def diff( new_vinfo : version.V1VersionInfo, file_patterns: config.PatternsByFile, ) -> str: - r"""Generate diffs of rewritten files. - - >>> old_vinfo = v1version.parse_version_info("v201809.0123") - >>> new_vinfo = v1version.parse_version_info("v201810.1124") - >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']} - >>> diff_str = diff(old_vinfo, new_vinfo, file_patterns) - >>> lines = diff_str.split("\n") - >>> lines[:2] - ['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py'] - >>> assert lines[6].startswith('-__version__ = "v2') - >>> assert not lines[6].startswith('-__version__ = "v201809.0123"') - >>> lines[7] - '+__version__ = "v201809.0123"' - - >>> file_patterns = {"LICENSE": ['Copyright (c) 2018-{year}']} - >>> diff_str = diff(old_vinfo, new_vinfo, file_patterns) - >>> assert not diff_str - """ + """Generate diffs of rewritten files.""" full_diff = "" fobj: typ.IO[str] @@ -165,13 +119,13 @@ def diff( rfd = rfd_from_content(patterns, new_vinfo, content) except rewrite.NoPatternMatch: # pylint:disable=raise-missing-from ; we support py2, so not an option - errmsg = f"No patterns matched for '{file_path}'" + errmsg = f"No patterns matched for file '{file_path}'" raise rewrite.NoPatternMatch(errmsg) rfd = rfd._replace(path=str(file_path)) lines = rewrite.diff_lines(rfd) if len(lines) == 0 and has_updated_version: - errmsg = f"No patterns matched for '{file_path}'" + errmsg = f"No patterns matched for file '{file_path}'" raise rewrite.NoPatternMatch(errmsg) full_diff += "\n".join(lines) + "\n" diff --git a/src/pycalver/v1version.py b/src/pycalver/v1version.py index 7659d8e..defc4a9 100644 --- a/src/pycalver/v1version.py +++ b/src/pycalver/v1version.py @@ -374,12 +374,12 @@ def incr( old_version: str, raw_pattern: str = "{pycalver}", *, - release : typ.Optional[str] = None, - major : bool = False, - minor : bool = False, - patch : bool = False, + release : typ.Optional[str] = None, + major : bool = False, + minor : bool = False, + patch : bool = False, release_num: bool = False, - pin_date: bool = False, + pin_date : bool = False, ) -> typ.Optional[str]: """Increment version string. @@ -408,7 +408,7 @@ def incr( if patch: cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1) if release_num: - cur_vinfo = cur_vinfo._replace(num=cur_vinfo.num + 1) + raise NotImplementedError("--release-num not supported for old style patterns") if release: cur_vinfo = cur_vinfo._replace(tag=release) diff --git a/src/pycalver/v2cli.py b/src/pycalver/v2cli.py index f29ecac..88f23a4 100644 --- a/src/pycalver/v2cli.py +++ b/src/pycalver/v2cli.py @@ -42,14 +42,6 @@ def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.C ) -def rewrite_files( - cfg : config.Config, - new_version: str, -) -> None: - new_vinfo = v2version.parse_version_info(new_version, cfg.version_pattern) - v2rewrite.rewrite_files(cfg.file_patterns, new_vinfo) - - def get_diff(cfg: config.Config, new_version: str) -> str: old_vinfo = v2version.parse_version_info(cfg.current_version, cfg.version_pattern) new_vinfo = v2version.parse_version_info(new_version , cfg.version_pattern) diff --git a/src/pycalver/v2patterns.py b/src/pycalver/v2patterns.py index 5306209..6e33891 100644 --- a/src/pycalver/v2patterns.py +++ b/src/pycalver/v2patterns.py @@ -8,7 +8,6 @@ >>> pattern = compile_pattern("vYYYY0M.BUILD[-RELEASE]") >>> version_info = pattern.regexp.match("v201712.0123-alpha") >>> assert version_info.groupdict() == { -... "version": "v201712.0123-alpha", ... "year_y" : "2017", ... "month" : "12", ... "bid" : "0123", @@ -23,7 +22,6 @@ >>> version_info = pattern.regexp.match("v201712.1234") >>> assert version_info.groupdict() == { -... "version": "v201712.1234", ... "year_y" : "2017", ... "month" : "12", ... "bid" : "1234", @@ -251,6 +249,13 @@ def _convert_to_pep440(version_pattern: str) -> str: else: pep440_pattern = pep440_pattern.replace(part_name, substitution) + # PYTAG and NUM must be adjacent and also be the last (optional) part + if 'PYTAGNUM' not in pep440_pattern: + pep440_pattern = pep440_pattern.replace("PYTAG", "") + pep440_pattern = pep440_pattern.replace("NUM" , "") + pep440_pattern = pep440_pattern.replace("[]" , "") + pep440_pattern += "[PYTAGNUM]" + return pep440_pattern @@ -304,9 +309,8 @@ def _replace_pattern_parts(pattern: str) -> str: return result_pattern -def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[str]: - normalized_pattern = normalize_pattern(version_pattern, raw_pattern) - escaped_pattern = normalized_pattern +def _compile_pattern_re(normalized_pattern: str) -> typ.Pattern[str]: + escaped_pattern = normalized_pattern for char, escaped in RE_PATTERN_ESCAPES: # [] braces are used for optional parts, such as [-RELEASE]/[-beta] # and need to be escaped manually. @@ -321,6 +325,11 @@ def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[s @utils.memo def compile_pattern(version_pattern: str, raw_pattern: typ.Optional[str] = None) -> Pattern: - _raw_pattern = version_pattern if raw_pattern is None else raw_pattern - regexp = _compile_pattern_re(version_pattern, _raw_pattern) - return Pattern(version_pattern, _raw_pattern, regexp) + _raw_pattern = version_pattern if raw_pattern is None else raw_pattern + normalized_pattern = normalize_pattern(version_pattern, _raw_pattern) + regexp = _compile_pattern_re(normalized_pattern) + return Pattern(version_pattern, normalized_pattern, regexp) + + +def compile_patterns(version_pattern: str, raw_patterns: typ.List[str]) -> typ.List[Pattern]: + return [compile_pattern(version_pattern, raw_pattern) for raw_pattern in raw_patterns] diff --git a/src/pycalver/v2rewrite.py b/src/pycalver/v2rewrite.py index ce1013c..a6d85bd 100644 --- a/src/pycalver/v2rewrite.py +++ b/src/pycalver/v2rewrite.py @@ -25,23 +25,7 @@ def rewrite_lines( new_vinfo: version.V2VersionInfo, old_lines: typ.List[str], ) -> typ.List[str]: - """Replace occurances of patterns in old_lines with new_vinfo. - - >>> from .v2patterns import compile_pattern - >>> version_pattern = "vYYYY0M.BUILD[-RELEASE]" - >>> new_vinfo = v2version.parse_version_info("v201811.0123-beta", version_pattern) - >>> patterns = [compile_pattern(version_pattern, '__version__ = "{version}"')] - >>> rewrite_lines(patterns, new_vinfo, ['__version__ = "v201809.0002-alpha" ']) - ['__version__ = "v201811.0123-beta" '] - - >>> rewrite_lines(patterns, new_vinfo, ['__version__ = "v201809.0002-alpha" # comment']) - ['__version__ = "v201811.0123-beta" # comment'] - - >>> patterns = [compile_pattern(version_pattern, '__version__ = "{pep440_version}"')] - >>> old_lines = ['__version__ = "201809.2a0"'] - >>> rewrite_lines(patterns, new_vinfo, old_lines) - ['__version__ = "201811.123b0"'] - """ + """Replace occurances of patterns in old_lines with new_vinfo.""" found_patterns: typ.Set[Pattern] = set() new_lines = old_lines[:] @@ -73,10 +57,10 @@ def rfd_from_content( ) -> rewrite.RewrittenFileData: r"""Rewrite pattern occurrences with version string. + >>> from .v2patterns import compile_pattern >>> version_pattern = "vYYYY0M.BUILD[-RELEASE]" >>> new_vinfo = v2version.parse_version_info("v201809.0123", version_pattern) - >>> raw_patterns = ['__version__ = "vYYYY0M.BUILD[-RELEASE]"'] - >>> patterns = + >>> patterns = [compile_pattern(version_pattern, '__version__ = "vYYYY0M.BUILD[-RELEASE]"')] >>> content = '__version__ = "v201809.0001-alpha"' >>> rfd = rfd_from_content(patterns, new_vinfo, content) >>> rfd.new_lines @@ -84,8 +68,7 @@ def rfd_from_content( >>> version_pattern = "vMAJOR.MINOR.PATCH" >>> new_vinfo = v2version.parse_version_info("v1.2.3", version_pattern) - >>> raw_patterns = ['__version__ = "vMAJOR.MINOR.PATCH"'] - >>> patterns = + >>> patterns = [compile_pattern(version_pattern, '__version__ = "vMAJOR.MINOR.PATCH"')] >>> content = '__version__ = "v1.2.2"' >>> rfd = rfd_from_content(patterns, new_vinfo, content) >>> rfd.new_lines @@ -113,26 +96,7 @@ def iter_rewritten( file_patterns: config.PatternsByFile, new_vinfo : version.V2VersionInfo, ) -> typ.Iterable[rewrite.RewrittenFileData]: - r'''Iterate over files with version string replaced. - - >>> version_pattern = "vYYYY0M.BUILD[-RELEASE]" - >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "vYYYY0M.BUILD[-RELEASE]"']} - >>> new_vinfo = v2version.parse_version_info("v201809.0123", version_pattern) - >>> rewritten_datas = iter_rewritten(file_patterns, new_vinfo) - >>> rfd = list(rewritten_datas)[0] - >>> expected = [ - ... '# This file is part of the pycalver project', - ... '# https://github.com/mbarkhau/pycalver', - ... '#', - ... '# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License', - ... '# SPDX-License-Identifier: MIT', - ... '"""PyCalVer: CalVer for Python Packages."""', - ... '', - ... '__version__ = "v201809.0123"', - ... '', - ... ] - >>> assert rfd.new_lines == expected - ''' + """Iterate over files with version string replaced.""" fobj: typ.IO[str] @@ -149,24 +113,7 @@ def diff( new_vinfo : version.V2VersionInfo, file_patterns: config.PatternsByFile, ) -> str: - r"""Generate diffs of rewritten files. - - >>> old_vinfo = v2version.parse_version_info("v201809.0123", version_pattern) - >>> new_vinfo = v2version.parse_version_info("v201810.1124", version_pattern) - >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "vYYYY0M.BUILD[-RELEASE]"']} - >>> diff_str = diff(old_vinfo, new_vinfo, file_patterns) - >>> lines = diff_str.split("\n") - >>> lines[:2] - ['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py'] - >>> assert lines[6].startswith('-__version__ = "v2') - >>> assert not lines[6].startswith('-__version__ = "v201810.1124"') - >>> lines[7] - '+__version__ = "v201810.1124"' - - >>> file_patterns = {"LICENSE": ['Copyright (c) 2018-YYYY']} - >>> diff_str = diff(old_vinfo, new_vinfo, file_patterns) - >>> assert not diff_str - """ + r"""Generate diffs of rewritten files.""" full_diff = "" fobj: typ.IO[str] @@ -179,7 +126,7 @@ def diff( rfd = rfd_from_content(patterns, new_vinfo, content) except rewrite.NoPatternMatch: # pylint:disable=raise-missing-from ; we support py2, so not an option - errmsg = f"No patterns matched for '{file_path}'" + errmsg = f"No patterns matched for file '{file_path}'" raise rewrite.NoPatternMatch(errmsg) rfd = rfd._replace(path=str(file_path)) @@ -187,7 +134,7 @@ def diff( patterns_with_change = _patterns_with_change(old_vinfo, new_vinfo, patterns) if len(lines) == 0 and patterns_with_change > 0: - errmsg = f"No patterns matched for '{file_path}'" + errmsg = f"No patterns matched for file '{file_path}'" raise rewrite.NoPatternMatch(errmsg) full_diff += "\n".join(lines) + "\n" diff --git a/src/pycalver/v2version.py b/src/pycalver/v2version.py index a4ef187..764af2c 100644 --- a/src/pycalver/v2version.py +++ b/src/pycalver/v2version.py @@ -145,7 +145,7 @@ def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: assert key in VALID_FIELD_KEYS, key fvals = field_values - tag = fvals.get('tag' ) or "final" + tag = fvals.get('tag' ) or "" pytag = fvals.get('pytag') or "" if tag and not pytag: @@ -153,6 +153,9 @@ def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: elif pytag and not tag: tag = version.RELEASE_BY_PEP440_TAG[pytag] + if not tag: + tag = "final" + date: typ.Optional[dt.date] = None year_y: MaybeInt = int(fvals['year_y']) if 'year_y' in fvals else None @@ -221,22 +224,22 @@ def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: def parse_version_info( - version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-RELEASE[NUM]]" + version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-RELEASE]" ) -> version.V2VersionInfo: """Parse normalized V2VersionInfo. - >>> vinfo = parse_version_info("v201712.0033-beta0", raw_pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") - >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033", 'tag': "beta", 'num': 0} - >>> assert vinfo == _parse_version_info(fvals) - - >>> vinfo = parse_version_info("v201712.0033-beta", raw_pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") + >>> vinfo = parse_version_info("v201712.0033-beta", raw_pattern="vYYYY0M.BUILD[-RELEASE]") >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"} >>> assert vinfo == _parse_version_info(fvals) - >>> vinfo = parse_version_info("v201712.0033", raw_pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") + >>> vinfo = parse_version_info("v201712.0033", raw_pattern="vYYYY0M.BUILD[-RELEASE]") >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033"} >>> assert vinfo == _parse_version_info(fvals) + >>> vinfo = parse_version_info("201712.33b0", raw_pattern="YYYY0M.BLD[PYTAGNUM]") + >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "33", 'tag': "beta", 'num': 0} + >>> assert vinfo == _parse_version_info(fvals) + >>> vinfo = parse_version_info("1.23.456", raw_pattern="MAJOR.MINOR.PATCH") >>> fvals = {'major': "1", 'minor': "23", 'patch': "456"} >>> assert vinfo == _parse_version_info(fvals) @@ -291,14 +294,14 @@ def _format_part_values(vinfo: version.V2VersionInfo) -> PartValues: It may for example have month=9, but not the formatted representation '09' for '0M'. - >>> vinfo = parse_version_info("v200709.1033-beta", pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") + >>> vinfo = parse_version_info("v200709.1033-beta", raw_pattern="vYYYY0M.BUILD[-RELEASE]") >>> kwargs = dict(_format_part_values(vinfo)) - >>> (kwargs['YYYY'], kwargs['0M'], kwargs['BUILD'], kwargs['RELEASE[NUM]']) + >>> (kwargs['YYYY'], kwargs['0M'], kwargs['BUILD'], kwargs['RELEASE']) ('2007', '09', '1033', 'beta') >>> (kwargs['YY'], kwargs['0Y'], kwargs['MM'], kwargs['PYTAG']) ('7', '07', '9', 'b') - >>> vinfo = parse_version_info("200709.1033b1", pattern="YYYY0M.BLD[PYTAGNUM]") + >>> vinfo = parse_version_info("200709.1033b1", raw_pattern="YYYY0M.BLD[PYTAGNUM]") >>> kwargs = dict(_format_part_values(vinfo)) >>> (kwargs['YYYY'], kwargs['0M'], kwargs['BUILD'], kwargs['PYTAG'], kwargs['NUM']) ('2007', '09', '1033', 'b', '1') @@ -386,141 +389,202 @@ def _parse_segment_tree(raw_pattern: str) -> SegmentTree: FormattedSegmentParts = typ.List[str] +class FormatedSeg(typ.NamedTuple): + is_literal: bool + is_zero : bool + result : str + + +def _format_segment(seg: Segment, part_values: PartValues) -> FormatedSeg: + zero_part_count = 0 + + # find all parts, regardless of zero value + used_parts: typ.List[typ.Tuple[str, str]] = [] + + for part, part_value in part_values: + if part in seg: + used_parts.append((part, part_value)) + if version.is_zero_val(part, part_value): + zero_part_count += 1 + + result = seg + # unescape braces + result = result.replace(r"\[", r"[") + result = result.replace(r"\]", r"]") + + for part, part_value in used_parts: + result = result.replace(part, part_value) + + # If a segment has no parts at all, it is a literal string + # (typically a prefix or sufix) and should be output as is. + is_literal_seg = len(used_parts) == 0 + if is_literal_seg: + return FormatedSeg(True, False, result) + elif zero_part_count > 0 and zero_part_count == len(used_parts): + # all zero, omit segment completely + return FormatedSeg(False, True, result) + else: + return FormatedSeg(False, False, result) + + def _format_segment_tree( seg_tree : SegmentTree, part_values: PartValues, -) -> FormattedSegmentParts: - result_parts = [] +) -> FormatedSeg: + # print("??>>>", seg_tree) + # NOTE (mb 2020-10-02): starting from the right, if there is any non-zero + # part, all further parts going left will be used. In other words, a part + # is only omitted, if all parts to the right of it were also omitted. + result_parts: typ.List[str] = [] + is_zero = True for seg in seg_tree: if isinstance(seg, list): - result_parts.extend(_format_segment_tree(seg, part_values)) + formatted_seg = _format_segment_tree(seg, part_values) else: - # If a segment has any non-zero parts, the whole segment is used. - non_zero_parts = 0 - formatted_seg = seg - # unescape braces - formatted_seg = formatted_seg.replace(r"\[", r"[") - formatted_seg = formatted_seg.replace(r"\]", r"]") - # replace non zero parts - for part, part_value in part_values: - if part in formatted_seg: - is_zero_part = ( - part in version.ZERO_VALUES and str(part_value) == version.ZERO_VALUES[part] - ) - if is_zero_part: - formatted_seg = formatted_seg.replace(part, "") - else: - non_zero_parts += 1 - formatted_seg = formatted_seg.replace(part, part_value) + formatted_seg = _format_segment(seg, part_values) - if non_zero_parts: - result_parts.append(formatted_seg) + if formatted_seg.is_literal: + result_parts.append(formatted_seg.result) + else: + is_zero = is_zero and formatted_seg.is_zero + result_parts.append(formatted_seg.result) - return result_parts + # print("<<<<", is_zero, result_parts) + result = "" if is_zero else "".join(result_parts) + return FormatedSeg(False, is_zero, result) def format_version(vinfo: version.V2VersionInfo, raw_pattern: str) -> str: """Generate version string. >>> import datetime as dt - >>> vinfo = parse_version_info("v200712.0033-beta", pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") + >>> vinfo = parse_version_info("v200712.0033-beta", raw_pattern="vYYYY0M.BUILD[-RELEASE]") >>> vinfo_a = vinfo._replace(**cal_info(date=dt.date(2007, 1, 1))._asdict()) >>> vinfo_b = vinfo._replace(**cal_info(date=dt.date(2007, 12, 31))._asdict()) - >>> format_version(vinfo_a, pattern="vYY.BLD[-PYTAGNUM]") + >>> format_version(vinfo_a, raw_pattern="vYY.BLD[-PYTAGNUM]") 'v7.33-b0' - >>> format_version(vinfo_a, pattern="YYYY0M.BUILD[PYTAG[NUM]]") + >>> format_version(vinfo_a, raw_pattern="YYYY0M.BUILD[PYTAG[NUM]]") '200701.0033b' - >>> format_version(vinfo_a, pattern="vYY.BLD[-PYTAGNUM]") + >>> format_version(vinfo_a, raw_pattern="vYY.BLD[-PYTAGNUM]") 'v7.33-b0' - >>> format_version(vinfo_a, pattern="v0Y.BLD[-RELEASE[NUM]]") + >>> format_version(vinfo_a, raw_pattern="v0Y.BLD[-RELEASE[NUM]]") 'v07.33-beta' - >>> format_version(vinfo_a, pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_a, raw_pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") 'v200701.0033-beta' - >>> format_version(vinfo_b, pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_b, raw_pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") 'v200712.0033-beta' - >>> format_version(vinfo_a, pattern="vYYYYw0W.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_a, raw_pattern="vYYYYw0W.BUILD[-RELEASE[NUM]]") 'v2007w01.0033-beta' - >>> format_version(vinfo_a, pattern="vYYYYwWW.BLD[-RELEASE[NUM]]") + >>> format_version(vinfo_a, raw_pattern="vYYYYwWW.BLD[-RELEASE[NUM]]") 'v2007w1.33-beta' - >>> format_version(vinfo_b, pattern="vYYYYw0W.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_b, raw_pattern="vYYYYw0W.BUILD[-RELEASE[NUM]]") 'v2007w53.0033-beta' - >>> format_version(vinfo_a, pattern="vYYYYd00J.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_a, raw_pattern="vYYYYd00J.BUILD[-RELEASE[NUM]]") 'v2007d001.0033-beta' - >>> format_version(vinfo_a, pattern="vYYYYdJJJ.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_a, raw_pattern="vYYYYdJJJ.BUILD[-RELEASE[NUM]]") 'v2007d1.0033-beta' - >>> format_version(vinfo_b, pattern="vYYYYd00J.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_b, raw_pattern="vYYYYd00J.BUILD[-RELEASE[NUM]]") 'v2007d365.0033-beta' - >>> format_version(vinfo_a, pattern="vGGGGwVV.BLD[PYTAGNUM]") + >>> format_version(vinfo_a, raw_pattern="vGGGGwVV.BLD[PYTAGNUM]") 'v2007w1.33b0' - >>> format_version(vinfo_a, pattern="vGGGGw0V.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_a, raw_pattern="vGGGGw0V.BUILD[-RELEASE[NUM]]") 'v2007w01.0033-beta' - >>> format_version(vinfo_b, pattern="vGGGGw0V.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_b, raw_pattern="vGGGGw0V.BUILD[-RELEASE[NUM]]") 'v2008w01.0033-beta' >>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final') - >>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD-RELEASE") + >>> format_version(vinfo_c, raw_pattern="vYYYYwWW.BUILD-RELEASE") 'v2007w53.0033-final' - >>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_c, raw_pattern="vYYYYwWW.BUILD[-RELEASE[NUM]]") 'v2007w53.0033' - >>> format_version(vinfo_c, pattern="vMAJOR.MINOR.PATCH") + >>> format_version(vinfo_c, raw_pattern="vMAJOR.MINOR.PATCH") 'v1.2.34' >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='final') - >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-RELEASENUM") + >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH-RELEASENUM") 'v1.0.0-final0' - >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-RELEASE[NUM]") + >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH-RELEASE[NUM]") 'v1.0.0-final' - >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-RELEASE") + >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH-RELEASE") 'v1.0.0-final' - >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH[-RELEASE[NUM]]") + >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH[-RELEASE[NUM]]") 'v1.0.0' - >>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.PATCH[-RELEASE[NUM]]]") + >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR[.PATCH[-RELEASE[NUM]]]") 'v1.0' - >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]") + >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]") 'v1' - >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=1, tag='rc', num=0) - >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH]]") - 'v1.0.1' - >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]") - 'v1.0.1-rc' - >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-RELEASENUM]]]") - 'v1.0.1-rc0' - >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH]]") - 'v1.0.1' + >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=2, tag='rc', pytag='rc', num=0) + >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH]]") + 'v1.0.2' + >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]") + 'v1.0.2-rc' + >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[PYTAGNUM]]]") + 'v1.0.2rc0' + >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH]]") + 'v1.0.2' >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='rc', num=2) - >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]") + >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]") 'v1.0.0-rc2' >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='rc', num=2) - >>> format_version(vinfo_d, pattern='__version__ = "vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]"') + >>> format_version(vinfo_d, raw_pattern='__version__ = "vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]"') '__version__ = "v1.0.0-rc2"' """ - part_values = _format_part_values(vinfo) - seg_tree = _parse_segment_tree(raw_pattern) - version_str_parts = _format_segment_tree(seg_tree, part_values) - return "".join(version_str_parts) + part_values = _format_part_values(vinfo) + seg_tree = _parse_segment_tree(raw_pattern) + formatted_seg = _format_segment_tree(seg_tree, part_values) + return formatted_seg.result + + +def _incr_numeric( + vinfo : version.V2VersionInfo, + major : bool, + minor : bool, + patch : bool, + release : typ.Optional[str], + release_num: bool, +) -> version.V2VersionInfo: + # prevent truncation of leading zeros + if int(vinfo.bid) < 1000: + vinfo = vinfo._replace(bid=str(int(vinfo.bid) + 1000)) + + vinfo = vinfo._replace(bid=lexid.next_id(vinfo.bid)) + + if major: + vinfo = vinfo._replace(major=vinfo.major + 1, minor=0, patch=0) + if minor: + vinfo = vinfo._replace(minor=vinfo.minor + 1, patch=0) + if patch: + vinfo = vinfo._replace(patch=vinfo.patch + 1) + if release_num: + vinfo = vinfo._replace(num=vinfo.num + 1) + if release: + if release != vinfo.tag: + vinfo = vinfo._replace(num=0) + vinfo = vinfo._replace(tag=release) + return vinfo def incr( old_version: str, raw_pattern: str = "vYYYY0M.BUILD[-RELEASE[NUM]]", *, - release : typ.Optional[str] = None, - major : bool = False, - minor : bool = False, - patch : bool = False, + release : typ.Optional[str] = None, + major : bool = False, + minor : bool = False, + patch : bool = False, release_num: bool = False, - pin_date: bool = False, + pin_date : bool = False, ) -> typ.Optional[str]: """Increment version string. @@ -540,24 +604,14 @@ def incr( else: cur_vinfo = old_vinfo._replace(**cur_cinfo._asdict()) - # prevent truncation of leading zeros - if int(cur_vinfo.bid) < 1000: - cur_vinfo = cur_vinfo._replace(bid=str(int(cur_vinfo.bid) + 1000)) - - cur_vinfo = cur_vinfo._replace(bid=lexid.next_id(cur_vinfo.bid)) - - if major: - cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0, patch=0) - if minor: - cur_vinfo = cur_vinfo._replace(minor=cur_vinfo.minor + 1, patch=0) - if patch: - cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1) - if release_num: - cur_vinfo = cur_vinfo._replace(num=cur_vinfo.num + 1) - if release: - if release != cur_vinfo.tag: - cur_vinfo = cur_vinfo._replace(num=0) - cur_vinfo = cur_vinfo._replace(tag=release) + cur_vinfo = _incr_numeric( + cur_vinfo, + major=major, + minor=minor, + patch=patch, + release=release, + release_num=release_num, + ) # TODO (mb 2020-09-20): New Rollover Behaviour: # Reset major, minor, patch to zero if any part to the left of it is incremented diff --git a/src/pycalver/vcs.py b/src/pycalver/vcs.py index 5eed4ce..a1f062a 100644 --- a/src/pycalver/vcs.py +++ b/src/pycalver/vcs.py @@ -16,6 +16,7 @@ mercurial, then the git terms are used. For example "fetch" import os import sys +import shlex import typing as typ import logging import tempfile @@ -73,7 +74,8 @@ class VCSAPI: logger.info(cmd_str) else: logger.debug(cmd_str) - output_data: bytes = sp.check_output(cmd_str.split(), env=env, stderr=sp.STDOUT) + cmd_parts = shlex.split(cmd_str) + output_data: bytes = sp.check_output(cmd_parts, env=env, stderr=sp.STDOUT) # TODO (mb 2018-11-15): Detect encoding of output? Use chardet? _encoding = "utf-8" diff --git a/src/pycalver/version.py b/src/pycalver/version.py index 9169cd2..dfe7c1f 100644 --- a/src/pycalver/version.py +++ b/src/pycalver/version.py @@ -119,6 +119,10 @@ ZERO_VALUES = { } +def is_zero_val(part: str, part_value: str) -> bool: + return part in ZERO_VALUES and part_value == ZERO_VALUES[part] + + class PatternError(Exception): pass diff --git a/test/test_cli.py b/test/test_cli.py index 308af00..75a83d6 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -4,7 +4,9 @@ from __future__ import print_function from __future__ import absolute_import from __future__ import unicode_literals +import io import os +import re import time import shutil import subprocess as sp @@ -13,6 +15,8 @@ import pytest import pathlib2 as pl from click.testing import CliRunner +from pycalver import v1cli +from pycalver import v2cli from pycalver import config from pycalver import v1patterns from pycalver.__main__ import cli @@ -21,6 +25,12 @@ from pycalver.__main__ import cli # pylint:disable=protected-access ; allowed for test code +README_TEXT_FIXTURE = """ + Hello World v201701.1002-alpha ! + aka. 201701.1002a0 ! +""" + + SETUP_CFG_FIXTURE = """ [metadata] license_file = LICENSE @@ -110,31 +120,33 @@ def test_incr_pin_date(runner): def test_incr_semver(runner): - semver_pattern = "{MAJOR}.{MINOR}.{PATCH}" - old_version = "0.1.0" - new_version = "0.1.1" + semver_patterns = [ + "{semver}", + "{MAJOR}.{MINOR}.{PATCH}", + "MAJOR.MINOR.PATCH", + ] - result = runner.invoke(cli, ['test', "-vv", "--patch", old_version, "{semver}"]) - assert result.exit_code == 0 - assert f"Version: {new_version}\n" in result.output + for semver_pattern in semver_patterns: + old_version = "0.1.0" + new_version = "0.1.1" - result = runner.invoke(cli, ['test', "-vv", "--patch", old_version, semver_pattern]) - assert result.exit_code == 0 - assert f"Version: {new_version}\n" in result.output + result = runner.invoke(cli, ['test', "-vv", "--patch", old_version, semver_pattern]) + assert result.exit_code == 0 + assert f"Version: {new_version}\n" in result.output - old_version = "0.1.1" - new_version = "0.2.0" + old_version = "0.1.1" + new_version = "0.2.0" - result = runner.invoke(cli, ['test', "-vv", "--minor", old_version, semver_pattern]) - assert result.exit_code == 0 - assert f"Version: {new_version}\n" in result.output + result = runner.invoke(cli, ['test', "-vv", "--minor", old_version, semver_pattern]) + assert result.exit_code == 0 + assert f"Version: {new_version}\n" in result.output - old_version = "0.1.1" - new_version = "1.0.0" + old_version = "0.1.1" + new_version = "1.0.0" - result = runner.invoke(cli, ['test', "-vv", "--major", old_version, semver_pattern]) - assert result.exit_code == 0 - assert f"Version: {new_version}\n" in result.output + result = runner.invoke(cli, ['test', "-vv", "--major", old_version, semver_pattern]) + assert result.exit_code == 0 + assert f"Version: {new_version}\n" in result.output def test_incr_semver_invalid(runner, caplog): @@ -175,12 +187,8 @@ def test_incr_invalid(runner): def _add_project_files(*files): if "README.md" in files: - README_TEXT = """ - Hello World v201701.1002-alpha ! - aka. 201701.1002a0 ! - """ with pl.Path("README.md").open(mode="wt", encoding="utf-8") as fobj: - fobj.write(README_TEXT) + fobj.write(README_TEXT_FIXTURE) if "setup.cfg" in files: with pl.Path("setup.cfg").open(mode="wt", encoding="utf-8") as fobj: @@ -195,6 +203,22 @@ def _add_project_files(*files): fobj.write(PYPROJECT_TOML_FIXTURE) +def _update_config_val(filename, **kwargs): + with io.open(filename, mode="r", encoding="utf-8") as fobj: + old_cfg_text = fobj.read() + + new_cfg_text = old_cfg_text + for key, val in kwargs.items(): + replacement = "{} = {}\n".format(key, val) + if replacement not in new_cfg_text: + pattern = r"^{} = .*$".format(key) + new_cfg_text = re.sub(pattern, replacement, new_cfg_text, flags=re.MULTILINE) + assert old_cfg_text != new_cfg_text + + with io.open(filename, mode="w", encoding="utf-8") as fobj: + fobj.write(new_cfg_text) + + def test_nocfg(runner, caplog): _add_project_files("README.md") result = runner.invoke(cli, ['show', "-vv"]) @@ -491,7 +515,7 @@ setup.cfg = """ -def test_bump_semver_warning(runner, caplog): +def test_v1_bump_semver_warning(runner, caplog): _add_project_files("README.md") with pl.Path("setup.cfg").open(mode="w") as fobj: @@ -509,7 +533,7 @@ def test_bump_semver_warning(runner, caplog): assert result.exit_code == 0 -def test_bump_semver_diff(runner, caplog): +def test_v1_bump_semver_diff(runner, caplog): _add_project_files("README.md") with pl.Path("setup.cfg").open(mode="w") as fobj: @@ -531,6 +555,48 @@ def test_bump_semver_diff(runner, caplog): assert f"+current_version = \"{expected}\"" in out_lines +def test_v1_get_diff(runner): + _add_project_files("README.md", "setup.cfg") + result = runner.invoke(cli, ['init', "-vv"]) + assert result.exit_code == 0 + + _update_config_val("setup.cfg", version_pattern='"{pycalver}"') + + _, cfg = config.init() + new_version = "v202010.1003-beta" + diff_str = v1cli.get_diff(cfg, new_version) + diff_lines = set(diff_str.splitlines()) + + assert "- Hello World v201701.1002-alpha !" in diff_lines + assert "- aka. 201701.1002a0 !" in diff_lines + assert "+ Hello World v202010.1003-beta !" in diff_lines + assert "+ aka. 202010.1003b0 !" in diff_lines + + assert '-current_version = "v202010.1001-alpha"' in diff_lines + assert '+current_version = "v202010.1003-beta"' in diff_lines + + +def test_v2_get_diff(runner): + _add_project_files("README.md", "setup.cfg") + result = runner.invoke(cli, ['init', "-vv"]) + assert result.exit_code == 0 + + _update_config_val("setup.cfg", version_pattern='"vYYYY0M.BUILD[-RELEASE]"') + + _, cfg = config.init() + new_version = "v202010.1003-beta" + diff_str = v2cli.get_diff(cfg, new_version) + diff_lines = set(diff_str.splitlines()) + + assert "- Hello World v201701.1002-alpha !" in diff_lines + assert "- aka. 201701.1002a0 !" in diff_lines + assert "+ Hello World v202010.1003-beta !" in diff_lines + assert "+ aka. 202010.1003b0 !" in diff_lines + + assert '-current_version = "v202010.1001-alpha"' in diff_lines + assert '+current_version = "v202010.1003-beta"' in diff_lines + + # def test_custom_commit_message(runner): # # TODO (mb 2020-09-18): # assert False diff --git a/test/test_config.py b/test/test_config.py index 5953f8d..09807aa 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -90,6 +90,13 @@ def mk_buf(text): return buf +def _parse_raw_patterns_by_filepath(cfg): + return { + filepath: [pattern.raw_pattern for pattern in patterns] + for filepath, patterns in cfg.file_patterns.items() + } + + def test_parse_toml_1(): buf = mk_buf(PYCALVER_TOML_FIXTURE_1) @@ -103,8 +110,10 @@ def test_parse_toml_1(): assert cfg.push is True assert "pycalver.toml" in cfg.file_patterns - assert cfg.file_patterns["README.md" ] == ["{pycalver}", "{pep440_pycalver}"] - assert cfg.file_patterns["pycalver.toml"] == ['current_version = "{pycalver}"'] + + raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) + assert raw_patterns_by_filepath["README.md" ] == ["{pycalver}", "{pep440_pycalver}"] + assert raw_patterns_by_filepath["pycalver.toml"] == ['current_version = "{pycalver}"'] def test_parse_toml_2(): @@ -120,8 +129,10 @@ def test_parse_toml_2(): assert cfg.push is False assert "pycalver.toml" in cfg.file_patterns - assert cfg.file_patterns["README.md" ] == ["{semver}", "{semver}"] - assert cfg.file_patterns["pycalver.toml"] == ['current_version = "{semver}"'] + + raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) + assert raw_patterns_by_filepath["README.md" ] == ["{semver}", "{semver}"] + assert raw_patterns_by_filepath["pycalver.toml"] == ['current_version = "{semver}"'] def test_parse_v1_cfg(): @@ -136,8 +147,10 @@ def test_parse_v1_cfg(): assert cfg.push is True assert "setup.cfg" in cfg.file_patterns - assert cfg.file_patterns["setup.py" ] == ["{pycalver}", "{pep440_pycalver}"] - assert cfg.file_patterns["setup.cfg"] == ['current_version = "{pycalver}"'] + + raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) + assert raw_patterns_by_filepath["setup.py" ] == ["{pycalver}", "{pep440_pycalver}"] + assert raw_patterns_by_filepath["setup.cfg"] == ['current_version = "{pycalver}"'] def test_parse_v2_cfg(): @@ -153,10 +166,12 @@ def test_parse_v2_cfg(): assert "setup.py" in cfg.file_patterns assert "setup.cfg" in cfg.file_patterns + # TODO (mb 2020-09-18): - # assert cfg.file_patterns["setup.py" ] == ["vYYYY0M.BUILD[-RELEASE]", "YYYY0M.BLD[PYTAGNUM]"] - # assert cfg.file_patterns["setup.cfg" ] == ['current_version = "vYYYY0M.BUILD[-RELEASE]"'] - # assert cfg.file_patterns["src/project/*.py"] == ['Copyright (c) 2018-YYYY"'] + # raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) + # assert raw_patterns_by_filepath["setup.py" ] == ["vYYYY0M.BUILD[-RELEASE]", "YYYY0M.BLD[PYTAGNUM]"] + # assert raw_patterns_by_filepath["setup.cfg" ] == ['current_version = "vYYYY0M.BUILD[-RELEASE]"'] + # assert raw_patterns_by_filepath["src/project/*.py"] == ['Copyright (c) 2018-YYYY"'] def test_parse_default_toml(): @@ -186,8 +201,9 @@ def test_parse_default_cfg(): def test_parse_project_toml(): - project_path = util.FIXTURES_DIR / "project_a" - config_path = util.FIXTURES_DIR / "project_a" / "pycalver.toml" + project_path = util.FIXTURES_DIR / "project_a" + config_path = util.FIXTURES_DIR / "project_a" / "pycalver.toml" + config_rel_path = "pycalver.toml" with config_path.open() as fobj: config_data = fobj.read() @@ -195,7 +211,7 @@ def test_parse_project_toml(): assert "v201710.0123-alpha" in config_data ctx = config.init_project_ctx(project_path) - assert ctx == config.ProjectContext(project_path, config_path, "toml", None) + assert ctx == config.ProjectContext(project_path, config_path, config_rel_path, "toml", None) cfg = config.parse(ctx) @@ -210,8 +226,9 @@ def test_parse_project_toml(): def test_parse_project_cfg(): - project_path = util.FIXTURES_DIR / "project_b" - config_path = util.FIXTURES_DIR / "project_b" / "setup.cfg" + project_path = util.FIXTURES_DIR / "project_b" + config_path = util.FIXTURES_DIR / "project_b" / "setup.cfg" + config_rel_path = "setup.cfg" with config_path.open() as fobj: config_data = fobj.read() @@ -219,7 +236,7 @@ def test_parse_project_cfg(): assert "v201307.0456-beta" in config_data ctx = config.init_project_ctx(project_path) - assert ctx == config.ProjectContext(project_path, config_path, 'cfg', None) + assert ctx == config.ProjectContext(project_path, config_path, config_rel_path, 'cfg', None) cfg = config.parse(ctx) @@ -241,9 +258,10 @@ def test_parse_toml_file(tmpdir): project_path = tmpdir.mkdir("minimal") setup_cfg = project_path.join("pycalver.toml") setup_cfg.write(PYCALVER_TOML_FIXTURE_1) + setup_cfg_rel_path = "pycalver.toml" ctx = config.init_project_ctx(project_path) - assert ctx == config.ProjectContext(project_path, setup_cfg, 'toml', None) + assert ctx == config.ProjectContext(project_path, setup_cfg, setup_cfg_rel_path, 'toml', None) cfg = config.parse(ctx) @@ -253,19 +271,21 @@ def test_parse_toml_file(tmpdir): assert cfg.commit is True assert cfg.push is True - assert cfg.file_patterns == { + raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) + assert raw_patterns_by_filepath == { "README.md" : ["{pycalver}", "{pep440_pycalver}"], "pycalver.toml": ['current_version = "{pycalver}"'], } def test_parse_default_pattern(): - project_path = util.FIXTURES_DIR / "project_c" - config_path = util.FIXTURES_DIR / "project_c" / "pyproject.toml" + project_path = util.FIXTURES_DIR / "project_c" + config_path = util.FIXTURES_DIR / "project_c" / "pyproject.toml" + config_rel_path = "pyproject.toml" ctx = config.init_project_ctx(project_path) - assert ctx == config.ProjectContext(project_path, config_path, "toml", None) + assert ctx == config.ProjectContext(project_path, config_path, config_rel_path, "toml", None) cfg = config.parse(ctx) @@ -276,7 +296,8 @@ def test_parse_default_pattern(): assert cfg.tag is True assert cfg.push is True - assert cfg.file_patterns == { + raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) + assert raw_patterns_by_filepath == { "pyproject.toml": [r'current_version = "v{year}q{quarter}.{build_no}"'] } @@ -285,9 +306,10 @@ def test_parse_cfg_file(tmpdir): project_path = tmpdir.mkdir("minimal") setup_cfg = project_path.join("setup.cfg") setup_cfg.write(SETUP_CFG_FIXTURE) + setup_cfg_rel_path = "setup.cfg" ctx = config.init_project_ctx(project_path) - assert ctx == config.ProjectContext(project_path, setup_cfg, 'cfg', None) + assert ctx == config.ProjectContext(project_path, setup_cfg, setup_cfg_rel_path, 'cfg', None) cfg = config.parse(ctx) @@ -297,7 +319,8 @@ def test_parse_cfg_file(tmpdir): assert cfg.commit is True assert cfg.push is True - assert cfg.file_patterns == { + raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) + assert raw_patterns_by_filepath == { "setup.py" : ["{pycalver}", "{pep440_pycalver}"], "setup.cfg": ['current_version = "{pycalver}"'], } diff --git a/test/test_rewrite.py b/test/test_rewrite.py index 92cd854..9d50961 100644 --- a/test/test_rewrite.py +++ b/test/test_rewrite.py @@ -4,6 +4,7 @@ from __future__ import print_function from __future__ import absolute_import from __future__ import unicode_literals +import re import copy from test import util @@ -13,35 +14,77 @@ from pycalver import v1rewrite from pycalver import v1version from pycalver import v2rewrite from pycalver import v2version +from pycalver import v1patterns +from pycalver import v2patterns # pylint:disable=protected-access ; allowed for test code +# Fix for Python<3.7 +# https://stackoverflow.com/a/56935186/62997 +copy._deepcopy_dispatch[type(re.compile(''))] = lambda r, _: r + + REWRITE_FIXTURE = """ # SPDX-License-Identifier: MIT __version__ = "v201809.0002-beta" """ -def test_rewrite_lines(): - old_lines = REWRITE_FIXTURE.splitlines() - patterns = ['__version__ = "{pycalver}"'] +def test_v1_rewrite_lines_basic(): + pattern = v1patterns.compile_pattern("{pycalver}", '__version__ = "{pycalver}"') new_vinfo = v1version.parse_version_info("v201911.0003") - new_lines = v1rewrite.rewrite_lines(patterns, new_vinfo, old_lines) + + old_lines = REWRITE_FIXTURE.splitlines() + new_lines = v1rewrite.rewrite_lines([pattern], new_vinfo, old_lines) assert len(new_lines) == len(old_lines) assert "v201911.0003" not in "\n".join(old_lines) assert "v201911.0003" in "\n".join(new_lines) -def test_rewrite_final(): +def test_v1_rewrite_lines(): + version_pattern = "{pycalver}" + new_vinfo = v1version.parse_version_info("v201811.0123-beta", version_pattern) + patterns = [v1patterns.compile_pattern(version_pattern, '__version__ = "{pycalver}"')] + lines = v1rewrite.rewrite_lines(patterns, new_vinfo, ['__version__ = "v201809.0002-beta"']) + assert lines == ['__version__ = "v201811.0123-beta"'] + + patterns = [v1patterns.compile_pattern(version_pattern, '__version__ = "{pep440_version}"')] + lines = v1rewrite.rewrite_lines(patterns, new_vinfo, ['__version__ = "201809.2b0"']) + assert lines == ['__version__ = "201811.123b0"'] + + +def test_v2_rewrite_lines(): + version_pattern = "vYYYY0M.BUILD[-RELEASE]" + new_vinfo = v2version.parse_version_info("v201811.0123-beta", version_pattern) + patterns = [v2patterns.compile_pattern(version_pattern, '__version__ = "{version}"')] + lines = v2rewrite.rewrite_lines(patterns, new_vinfo, ['__version__ = "v201809.0002-alpha" ']) + assert lines == ['__version__ = "v201811.0123-beta" '] + + lines = v2rewrite.rewrite_lines( + patterns, new_vinfo, ['__version__ = "v201809.0002-alpha" # comment'] + ) + assert lines == ['__version__ = "v201811.0123-beta" # comment'] + + patterns = [v2patterns.compile_pattern(version_pattern, '__version__ = "YYYY0M.BLD[PYTAGNUM]"')] + old_lines = ['__version__ = "201809.2a0"'] + lines = v2rewrite.rewrite_lines(patterns, new_vinfo, old_lines) + assert lines == ['__version__ = "201811.123b0"'] + + +def test_v1_rewrite_final(): # Patterns written with {release_tag} placeholder preserve # the release tag even if the new version is -final - old_lines = REWRITE_FIXTURE.splitlines() - patterns = ['__version__ = "v{year}{month}.{build_no}-{release_tag}"'] + pattern = v1patterns.compile_pattern( + "v{year}{month}.{build_no}-{release_tag}", + '__version__ = "v{year}{month}.{build_no}-{release_tag}"', + ) new_vinfo = v1version.parse_version_info("v201911.0003") - new_lines = v1rewrite.rewrite_lines(patterns, new_vinfo, old_lines) + + old_lines = REWRITE_FIXTURE.splitlines() + new_lines = v1rewrite.rewrite_lines([pattern], new_vinfo, old_lines) assert len(new_lines) == len(old_lines) assert "v201911.0003" not in "\n".join(old_lines) @@ -93,14 +136,19 @@ def test_error_bad_path(): assert "setup.py" in str(ex) -def test_error_bad_pattern(): +def test_v1_error_bad_pattern(): with util.Project(project="b") as project: ctx = config.init_project_ctx(project.dir) cfg = config.parse(ctx) assert cfg - patterns = copy.deepcopy(cfg.file_patterns) - patterns["setup.py"] = patterns["setup.py"][0] + "invalid" + patterns = copy.deepcopy(cfg.file_patterns) + original_pattern = patterns["setup.py"][0] + invalid_pattern = v1patterns.compile_pattern( + original_pattern.version_pattern, + original_pattern.raw_pattern + ".invalid", + ) + patterns["setup.py"] = [invalid_pattern] try: old_vinfo = v1version.parse_version_info("v201808.0233") @@ -118,45 +166,163 @@ __version__ = "2018.0002-beta" def test_v1_optional_release(): - old_lines = OPTIONAL_RELEASE_FIXTURE.splitlines() - pattern = "{year}.{build_no}{release}" - patterns = ['__version__ = "{year}.{build_no}{release}"'] + version_pattern = "{year}.{build_no}{release}" + new_vinfo = v1version.parse_version_info("2019.0003", version_pattern) - new_vinfo = v1version.parse_version_info("2019.0003", pattern) - new_lines = v1rewrite.rewrite_lines(patterns, new_vinfo, old_lines) + raw_pattern = '__version__ = "{year}.{build_no}{release}"' + pattern = v1patterns.compile_pattern(version_pattern, raw_pattern) + + old_lines = OPTIONAL_RELEASE_FIXTURE.splitlines() + new_lines = v1rewrite.rewrite_lines([pattern], new_vinfo, old_lines) assert len(new_lines) == len(old_lines) assert "2019.0003" not in "\n".join(old_lines) - new_text = "\n".join(new_lines) - assert "2019.0003" in new_text + assert "2019.0003" in "\n".join(new_lines) + assert '__version__ = "2019.0003"' in "\n".join(new_lines) - new_vinfo = v1version.parse_version_info("2019.0004-beta", pattern) - new_lines = v1rewrite.rewrite_lines(patterns, new_vinfo, old_lines) + new_vinfo = v1version.parse_version_info("2019.0004-beta", version_pattern) + new_lines = v1rewrite.rewrite_lines([pattern], new_vinfo, old_lines) # make sure optional release tag is added back on assert len(new_lines) == len(old_lines) assert "2019.0004-beta" not in "\n".join(old_lines) assert "2019.0004-beta" in "\n".join(new_lines) + assert '__version__ = "2019.0004-beta"' in "\n".join(new_lines) def test_v2_optional_release(): - old_lines = OPTIONAL_RELEASE_FIXTURE.splitlines() - pattern = "YYYY.BUILD[-RELEASE]" - patterns = ['__version__ = "YYYY.BUILD[-RELEASE]"'] + version_pattern = "YYYY.BUILD[-RELEASE]" + new_vinfo = v2version.parse_version_info("2019.0003", version_pattern) - new_vinfo = v2version.parse_version_info("2019.0003", pattern) - new_lines = v2rewrite.rewrite_lines(patterns, new_vinfo, old_lines) + raw_pattern = '__version__ = "YYYY.BUILD[-RELEASE]"' + pattern = v2patterns.compile_pattern(version_pattern, raw_pattern) + + old_lines = OPTIONAL_RELEASE_FIXTURE.splitlines() + new_lines = v2rewrite.rewrite_lines([pattern], new_vinfo, old_lines) assert len(new_lines) == len(old_lines) assert "2019.0003" not in "\n".join(old_lines) - new_text = "\n".join(new_lines) - assert "2019.0003" in new_text - assert '__version__ = "2019.0003"' in new_text + assert "2019.0003" in "\n".join(new_lines) + assert '__version__ = "2019.0003"' in "\n".join(new_lines) - new_vinfo = v2version.parse_version_info("2019.0004-beta", pattern) - new_lines = v2rewrite.rewrite_lines(patterns, new_vinfo, old_lines) + new_vinfo = v2version.parse_version_info("2019.0004-beta", version_pattern) + new_lines = v2rewrite.rewrite_lines([pattern], new_vinfo, old_lines) # make sure optional release tag is added back on assert len(new_lines) == len(old_lines) assert "2019.0004-beta" not in "\n".join(old_lines) assert "2019.0004-beta" in "\n".join(new_lines) + assert '__version__ = "2019.0004-beta"' in "\n".join(new_lines) + + +def test_v1_iter_rewritten(): + version_pattern = "{pycalver}" + new_vinfo = v1version.parse_version_info("v201809.0123") + + file_patterns = { + "src/pycalver/__init__.py": [ + v1patterns.compile_pattern(version_pattern, '__version__ = "{pycalver}"'), + ] + } + rewritten_datas = v1rewrite.iter_rewritten(file_patterns, new_vinfo) + rfd = list(rewritten_datas)[0] + expected = [ + "# This file is part of the pycalver project", + "# https://github.com/mbarkhau/pycalver", + "#", + "# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License", + "# SPDX-License-Identifier: MIT", + '"""PyCalVer: CalVer for Python Packages."""', + '', + '__version__ = "v201809.0123"', + '', + ] + assert rfd.new_lines == expected + + +def test_v2_iter_rewritten(): + version_pattern = "vYYYY0M.BUILD[-RELEASE]" + new_vinfo = v2version.parse_version_info("v201809.0123", version_pattern) + + file_patterns = { + "src/pycalver/__init__.py": [ + v2patterns.compile_pattern(version_pattern, '__version__ = "vYYYY0M.BUILD[-RELEASE]"'), + ] + } + + rewritten_datas = v2rewrite.iter_rewritten(file_patterns, new_vinfo) + rfd = list(rewritten_datas)[0] + expected = [ + "# This file is part of the pycalver project", + "# https://github.com/mbarkhau/pycalver", + "#", + "# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License", + "# SPDX-License-Identifier: MIT", + '"""PyCalVer: CalVer for Python Packages."""', + '', + '__version__ = "v201809.0123"', + '', + ] + assert rfd.new_lines == expected + + +def test_v1_diff(): + version_pattern = "{pycalver}" + raw_pattern = '__version__ = "{pycalver}"' + pattern = v1patterns.compile_pattern(version_pattern, raw_pattern) + file_patterns = {"src/pycalver/__init__.py": [pattern]} + + old_vinfo = v1version.parse_version_info("v201809.0123") + new_vinfo = v1version.parse_version_info("v201910.1124") + + diff_str = v1rewrite.diff(old_vinfo, new_vinfo, file_patterns) + lines = diff_str.split("\n") + + assert lines[:2] == ["--- src/pycalver/__init__.py", "+++ src/pycalver/__init__.py"] + + assert lines[6].startswith('-__version__ = "v20') + assert lines[7].startswith('+__version__ = "v20') + + assert not lines[6].startswith('-__version__ = "v201809.0123"') + + assert lines[7] == '+__version__ = "v201910.1124"' + + raw_pattern = "Copyright (c) 2018-{year}" + pattern = v1patterns.compile_pattern(version_pattern, raw_pattern) + file_patterns = {'LICENSE': [pattern]} + diff_str = v1rewrite.diff(old_vinfo, new_vinfo, file_patterns) + + lines = diff_str.split("\n") + assert lines[3].startswith("-MIT License Copyright (c) 2018-20") + assert lines[4].startswith("+MIT License Copyright (c) 2018-2019") + + +def test_v2_diff(): + version_pattern = "vYYYY0M.BUILD[-RELEASE]" + raw_pattern = '__version__ = "vYYYY0M.BUILD[-RELEASE]"' + pattern = v2patterns.compile_pattern(version_pattern, raw_pattern) + file_patterns = {"src/pycalver/__init__.py": [pattern]} + + old_vinfo = v2version.parse_version_info("v201809.0123", version_pattern) + new_vinfo = v2version.parse_version_info("v201910.1124", version_pattern) + + diff_str = v2rewrite.diff(old_vinfo, new_vinfo, file_patterns) + lines = diff_str.split("\n") + + assert lines[:2] == ["--- src/pycalver/__init__.py", "+++ src/pycalver/__init__.py"] + + assert lines[6].startswith('-__version__ = "v20') + assert lines[7].startswith('+__version__ = "v20') + + assert not lines[6].startswith('-__version__ = "v201809.0123"') + + assert lines[7] == '+__version__ = "v201910.1124"' + + raw_pattern = "Copyright (c) 2018-YYYY" + pattern = v2patterns.compile_pattern(version_pattern, raw_pattern) + file_patterns = {'LICENSE': [pattern]} + diff_str = v2rewrite.diff(old_vinfo, new_vinfo, file_patterns) + + lines = diff_str.split("\n") + assert lines[3].startswith("-MIT License Copyright (c) 2018-20") + assert lines[4].startswith("+MIT License Copyright (c) 2018-2019") From 4d08aea1210231b969b15c272c0a07b7cb7e99c7 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Fri, 2 Oct 2020 21:32:51 +0000 Subject: [PATCH 35/98] test commit_message config --- pylint-ignore.md | 16 +------------ test/test_cli.py | 61 ++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 19 deletions(-) diff --git a/pylint-ignore.md b/pylint-ignore.md index 9bbd051..5143e55 100644 --- a/pylint-ignore.md +++ b/pylint-ignore.md @@ -23,7 +23,7 @@ The recommended approach to using `pylint-ignore` is: # Overview - - [W0511: fixme (8x)](#w0511-fixme) + - [W0511: fixme (7x)](#w0511-fixme) - [W0703: broad-except (1x)](#w0703-broad-except) @@ -129,20 +129,6 @@ The recommended approach to using `pylint-ignore` is: ``` -## File test/test_cli.py - Line 599 - W0511 (fixme) - -- `message: # TODO (mb 2020-09-18):` -- `author : Manuel Barkhau ` -- `date : 2020-09-18T19:35:32` - -``` - 597: - 598: # def test_custom_commit_message(runner): -> 599: # # TODO (mb 2020-09-18): - 600: # assert False -``` - - ## File src/pycalver/v2version.py - Line 616 - W0511 (fixme) - `message: TODO (mb 2020-09-20): New Rollover Behaviour:` diff --git a/test/test_cli.py b/test/test_cli.py index 75a83d6..c1105f8 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -8,6 +8,7 @@ import io import os import re import time +import shlex import shutil import subprocess as sp @@ -209,7 +210,7 @@ def _update_config_val(filename, **kwargs): new_cfg_text = old_cfg_text for key, val in kwargs.items(): - replacement = "{} = {}\n".format(key, val) + replacement = "{} = {}".format(key, val) if replacement not in new_cfg_text: pattern = r"^{} = .*$".format(key) new_cfg_text = re.sub(pattern, replacement, new_cfg_text, flags=re.MULTILINE) @@ -597,6 +598,58 @@ def test_v2_get_diff(runner): assert '+current_version = "v202010.1003-beta"' in diff_lines -# def test_custom_commit_message(runner): -# # TODO (mb 2020-09-18): -# assert False +def test_hg_commit_message(runner, caplog): + _add_project_files("README.md", "setup.cfg") + result = runner.invoke(cli, ['init', "-vv"]) + assert result.exit_code == 0 + + commit_message = """ + "bump from {old_version} ({old_version_pep440}) to {new_version} ({new_version_pep440})" + """ + _update_config_val("setup.cfg", current_version='"v201903.1001-alpha"') + _update_config_val("setup.cfg", commit_message=commit_message.strip()) + + _vcs_init("hg", ["README.md", "setup.cfg"]) + assert len(caplog.records) > 0 + + result = runner.invoke(cli, ['bump', "-vv", "--pin-date", "--release", "beta"]) + assert result.exit_code == 0 + + tags = shell("hg", "tags").decode("utf-8") + assert "v201903.1002-beta" in tags + + commits = shell(*shlex.split("hg log -l 2")).decode("utf-8").split("\n\n") + + summary = commits[1].split("summary:")[-1] + assert ( + "bump from v201903.1001-alpha (201903.1001a0) to v201903.1002-beta (201903.1002b0)" + in summary + ) + + +def test_git_commit_message(runner, caplog): + _add_project_files("README.md", "setup.cfg") + result = runner.invoke(cli, ['init', "-vv"]) + assert result.exit_code == 0 + + commit_message = """ + "bump: {old_version} ({old_version_pep440}) -> {new_version} ({new_version_pep440})" + """ + _update_config_val("setup.cfg", current_version='"v201903.1001-alpha"') + _update_config_val("setup.cfg", commit_message=commit_message.strip()) + + _vcs_init("git", ["README.md", "setup.cfg"]) + assert len(caplog.records) > 0 + + result = runner.invoke(cli, ['bump', "-vv", "--pin-date", "--release", "beta"]) + assert result.exit_code == 0 + + tags = shell("git", "tag", "--list").decode("utf-8") + assert "v201903.1002-beta" in tags + + commits = shell(*shlex.split("git log -l 2")).decode("utf-8").split("\n\n") + + summary = commits[1] + assert ( + "bump: v201903.1001-alpha (201903.1001a0) -> v201903.1002-beta (201903.1002b0)" in summary + ) From 67feeaf8eff6815ee2148e862a7cd3794488e79c Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Fri, 2 Oct 2020 21:50:09 +0000 Subject: [PATCH 36/98] more regression testing --- README.md | 8 ++++---- test/test_cli.py | 25 ++++++++++++++++++++----- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 3fa315a..a417254 100644 --- a/README.md +++ b/README.md @@ -231,12 +231,12 @@ These patterns are closely based on https://calver.org/ | `00J` | 001, 002..366 | `%j` | | `BUILD` | 0011, 1001, 1002, .. | build number (lexid) | | `BLD` | 11, 1001, 1002, .. | zero truncated `BUILD` | +| `MAJOR` | 0..9, 10..99, 100.. | `--major` | +| `MINOR` | 0..9, 10..99, 100.. | `-m/--minor` | +| `PATCH` | 0..9, 10..99, 100.. | `-p/--patch` | +| `NUM` | 0, 1, 2... | `-r/--release-num` | | `RELEASE` | alpha, beta, rc | `--release=` | | `PYTAG` | a, b, rc | `--release=` | -| `NUM` | 0, 1, 2... | release tag number | -| `MAJOR` | 0..9, 10..99, 100.. | `--major` | -| `MINOR` | 0..9, 10..99, 100.. | `--minor` | -| `PATCH` | 0..9, 10..99, 100.. | `--patch` | ### Week Numbering diff --git a/test/test_cli.py b/test/test_cli.py index c1105f8..1c01513 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -104,13 +104,17 @@ def test_version(runner): def test_incr_default(runner): - old_version = "v201701.0999-alpha" - initial_version = config._initial_version() + old_version = "v201701.0004-alpha" - result = runner.invoke(cli, ['test', "-vv", old_version]) + cmd = ['test', "-vv", "--pin-date", "--release", "beta", old_version] + result = runner.invoke(cli, cmd) assert result.exit_code == 0 - new_version = initial_version.replace(".1001-alpha", ".11000-alpha") - assert f"Version: {new_version}\n" in result.output + assert f"Version: v201701.0005-beta\n" in result.output + + cmd = ['test', "-vv", "--pin-date", "--release", "beta", old_version, "vYYYY0M.BUILD[-RELEASE]"] + result = runner.invoke(cli, cmd) + assert result.exit_code == 0 + assert f"Version: v201701.1005-beta\n" in result.output def test_incr_pin_date(runner): @@ -179,6 +183,17 @@ def test_incr_to_final(runner): assert f"Version: {new_version}\n" in result.output +def test_incr_release_num(runner): + semver = "MAJOR.MINOR.PATCH[PYTAGNUM]" + + old_version = "0.1.0b0" + new_version = "0.1.0b1" + + result = runner.invoke(cli, ['test', "-vv", "--release-num", old_version, semver]) + assert result.exit_code == 0 + assert f"Version: {new_version}\n" in result.output + + def test_incr_invalid(runner): old_version = "v201701.0999-alpha" From 7b060121802a9f87ac55851435c3dec8df3b0104 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Fri, 2 Oct 2020 22:43:45 +0000 Subject: [PATCH 37/98] add --date argument --- src/pycalver/__main__.py | 78 +++++++++++++++++++++++++++++++-------- src/pycalver/v1version.py | 3 +- src/pycalver/v2version.py | 3 +- test/test_patterns.py | 2 - 4 files changed, 66 insertions(+), 20 deletions(-) diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 1d474de..0d0a145 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -12,6 +12,7 @@ Enables use as module: $ python -m pycalver --version import sys import typing as typ import logging +import datetime as dt import subprocess as sp import click @@ -62,7 +63,31 @@ def _configure_logging(verbose: int = 0) -> None: VALID_RELEASE_VALUES = ("alpha", "beta", "dev", "rc", "post", "final") -def _validate_release_tag(release: str) -> None: +_current_date = dt.date.today().isoformat() + + +def _validate_date(date: typ.Optional[str], pin_date: bool) -> dt.date: + if date and pin_date: + logger.error(f"Can only use either --pin-date or --date='{date}', not both.") + sys.exit(1) + + if date is None: + return + + try: + dt_val = dt.datetime.strptime(date, "%Y-%m-%d") + return dt_val.date() + except ValueError: + logger.error( + f"Invalid parameter --date='{date}', must match format YYYY-0M-0D.", exc_info=True + ) + sys.exit(1) + + +def _validate_release_tag(release: typ.Optional[str]) -> None: + if release is None: + return + if release in VALID_RELEASE_VALUES: return @@ -96,27 +121,35 @@ def cli(verbose: int = 0) -> None: ), ) @click.option("--major" , is_flag=True, default=False, help="Increment major component.") -@click.option("-m" , "--minor" , is_flag=True, default=False, help="Increment minor component.") -@click.option("-p" , "--patch" , is_flag=True, default=False, help="Increment patch component.") +@click.option("-m" , "--minor", is_flag=True, default=False, help="Increment minor component.") +@click.option("-p" , "--patch", is_flag=True, default=False, help="Increment patch component.") @click.option("-r" , "--release-num", is_flag=True, default=False, help="Increment release number.") @click.option("--pin-date", is_flag=True, default=False, help="Leave date components unchanged.") +@click.option( + "-d", + "--date", + default=None, + metavar="", + help=f"Set explicit date in format YYYY-0M-0D (eg. {_current_date}).", +) def test( old_version: str, - pattern : str = "{pycalver}", - verbose : int = 0, - release : str = None, + pattern : str = "{pycalver}", + verbose : int = 0, + release : str = None, major : bool = False, minor : bool = False, patch : bool = False, release_num: bool = False, + date : typ.Optional[str] = None, pin_date : bool = False, ) -> None: """Increment a version number for demo purposes.""" _configure_logging(verbose=max(_VERBOSE, verbose)) raw_pattern = pattern - if release: - _validate_release_tag(release) + _validate_release_tag(release) + _date = _validate_date(date, pin_date) new_version = _incr( old_version, @@ -127,6 +160,7 @@ def test( patch=patch, release_num=release_num, pin_date=pin_date, + date=_date, ) if new_version is None: logger.error(f"Invalid version '{old_version}' and/or pattern '{raw_pattern}'.") @@ -197,12 +231,13 @@ def _incr( old_version: str, raw_pattern: str, *, - release : str = None, + release : str = None, major : bool = False, minor : bool = False, patch : bool = False, release_num: bool = False, pin_date : bool = False, + date : typ.Optional[dt.date] = None, ) -> typ.Optional[str]: v1_parts = list(v1patterns.PART_PATTERNS) + list(v1patterns.FULL_PART_FORMATS) has_v1_part = any("{" + part + "}" in raw_pattern for part in v1_parts) @@ -216,6 +251,7 @@ def _incr( patch=patch, release_num=release_num, pin_date=pin_date, + date=date, ) else: return v2version.incr( @@ -227,6 +263,7 @@ def _incr( patch=patch, release_num=release_num, pin_date=pin_date, + date=date, ) @@ -357,14 +394,21 @@ def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: "to files with version strings." ), ) -@click.option("--major" , is_flag=True, default=False, help="Increment major component.") -@click.option("-m" , "--minor" , is_flag=True, default=False, help="Increment minor component.") -@click.option("-p" , "--patch" , is_flag=True, default=False, help="Increment patch component.") -@click.option("-r" , "--release-num", is_flag=True, default=False, help="Increment release number.") +@click.option("--major", is_flag=True, default=False, help="Increment major component.") +@click.option("-m", "--minor", is_flag=True, default=False, help="Increment minor component.") +@click.option("-p", "--patch", is_flag=True, default=False, help="Increment patch component.") +@click.option("-r", "--release-num", is_flag=True, default=False, help="Increment release number.") @click.option("--pin-date", is_flag=True, default=False, help="Leave date components unchanged.") +@click.option( + "-d", + "--date", + default=None, + metavar="", + help=f"Set explicit date in format YYYY-0M-0D (eg. {_current_date}).", +) def bump( release : typ.Optional[str] = None, - verbose : int = 0, + verbose : int = 0, dry : bool = False, allow_dirty: bool = False, fetch : bool = True, @@ -373,13 +417,14 @@ def bump( patch : bool = False, release_num: bool = False, pin_date : bool = False, + date : typ.Optional[str] = None, ) -> None: """Increment the current version string and update project files.""" verbose = max(_VERBOSE, verbose) _configure_logging(verbose) - if release: - _validate_release_tag(release) + _validate_release_tag(release) + _date = _validate_date(date, pin_date) _, cfg = config.init(project_path=".") @@ -399,6 +444,7 @@ def bump( patch=patch, release_num=release_num, pin_date=pin_date, + date=_date, ) if new_version is None: diff --git a/src/pycalver/v1version.py b/src/pycalver/v1version.py index defc4a9..1cecbd2 100644 --- a/src/pycalver/v1version.py +++ b/src/pycalver/v1version.py @@ -380,6 +380,7 @@ def incr( patch : bool = False, release_num: bool = False, pin_date : bool = False, + date : typ.Optional[dt.date] = None, ) -> typ.Optional[str]: """Increment version string. @@ -391,7 +392,7 @@ def incr( logger.error(str(ex)) return None - cur_cinfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info() + cur_cinfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info(date) if _is_cal_gt(old_vinfo, cur_cinfo): logger.warning(f"Old version appears to be from the future '{old_version}'") diff --git a/src/pycalver/v2version.py b/src/pycalver/v2version.py index 764af2c..f04ec1c 100644 --- a/src/pycalver/v2version.py +++ b/src/pycalver/v2version.py @@ -585,6 +585,7 @@ def incr( patch : bool = False, release_num: bool = False, pin_date : bool = False, + date : typ.Optional[dt.date] = None, ) -> typ.Optional[str]: """Increment version string. @@ -596,7 +597,7 @@ def incr( logger.error(str(ex)) return None - cur_cinfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info() + cur_cinfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info(date) if _is_cal_gt(old_vinfo, cur_cinfo): logger.warning(f"Old version appears to be from the future '{old_version}'") diff --git a/test/test_patterns.py b/test/test_patterns.py index aebb99c..2621fc4 100644 --- a/test/test_patterns.py +++ b/test/test_patterns.py @@ -11,8 +11,6 @@ import pytest from pycalver import v1patterns from pycalver import v2patterns -# TODO (mb 2020-09-06): test for v2patterns - V2_PART_PATTERN_CASES = [ (['YYYY', 'GGGG'], "2020" , "2020"), (['YYYY', 'GGGG'], "" , None), From ec4d051e7cefff8094a02458270d296ed8d0b747 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Fri, 2 Oct 2020 22:44:13 +0000 Subject: [PATCH 38/98] weeknum testing --- pylint-ignore.md | 99 ++++++++++++++-------------------------- src/pycalver/__main__.py | 4 +- test/test_cli.py | 58 +++++++++++++++++++++-- test/test_version.py | 57 +++++++++++++---------- 4 files changed, 125 insertions(+), 93 deletions(-) diff --git a/pylint-ignore.md b/pylint-ignore.md index 5143e55..493356b 100644 --- a/pylint-ignore.md +++ b/pylint-ignore.md @@ -23,27 +23,12 @@ The recommended approach to using `pylint-ignore` is: # Overview - - [W0511: fixme (7x)](#w0511-fixme) + - [W0511: fixme (5x)](#w0511-fixme) - [W0703: broad-except (1x)](#w0703-broad-except) # W0511: fixme -## File test/test_patterns.py - Line 14 - W0511 (fixme) - -- `message: TODO (mb 2020-09-06): test for v2patterns` -- `author : Manuel Barkhau ` -- `date : 2020-09-18T17:01:05` - -``` - 12: from pycalver import v2patterns - 13: -> 14: # TODO (mb 2020-09-06): test for v2patterns - 15: - 16: V2_PART_PATTERN_CASES = [ -``` - - ## File src/pycalver/vcs.py - Line 80 - W0511 (fixme) - `message: TODO (mb 2018-11-15): Detect encoding of output? Use chardet?` @@ -61,23 +46,6 @@ The recommended approach to using `pylint-ignore` is: ``` -## File test/test_version.py - Line 168 - W0511 (fixme) - -- `message: TODO (mb 2020-09-06): add tests for new style patterns` -- `author : Manuel Barkhau ` -- `date : 2020-09-18T17:01:05` - -``` - 163: def vnfo(**field_values): - ... - 166: - 167: PARSE_VERSION_TEST_CASES = [ -> 168: # TODO (mb 2020-09-06): add tests for new style patterns - 169: # ["YYYY.MM.DD" , "2017.06.07", vnfo(year="2017", month="06", dom="07")], - 170: ["{year}.{month}.{dom}" , "2017.06.07", vnfo(year="2017", month="06", dom="07")], -``` - - ## File test/test_config.py - Line 170 - W0511 (fixme) - `message: TODO (mb 2020-09-18):` @@ -90,25 +58,8 @@ The recommended approach to using `pylint-ignore` is: 168: assert "setup.cfg" in cfg.file_patterns 169: > 170: # TODO (mb 2020-09-18): - 171: # raw_patterns_by_file = _parse_raw_patterns_by_file(cfg) - 172: # assert raw_patterns_by_file["setup.py" ] == ["vYYYY0M.BUILD[-RELEASE]", "YYYY0M.BLD[PYTAGNUM]"] -``` - - -## File src/pycalver/__main__.py - Line 259 - W0511 (fixme) - -- `message: TODO (mb 2020-09-18): Investigate error messages` -- `author : Manuel Barkhau ` -- `date : 2020-09-19T16:24:10` - -``` - 231: def _bump( - ... - 257: sys.exit(1) - 258: except Exception as ex: -> 259: # TODO (mb 2020-09-18): Investigate error messages - 260: logger.error(str(ex)) - 261: sys.exit(1) + 171: # raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) + 172: # assert raw_patterns_by_filepath["setup.py" ] == ["vYYYY0M.BUILD[-RELEASE]", "YYYY0M.BLD[PYTAGNUM]"] ``` @@ -129,7 +80,24 @@ The recommended approach to using `pylint-ignore` is: ``` -## File src/pycalver/v2version.py - Line 616 - W0511 (fixme) +## File src/pycalver/__main__.py - Line 300 - W0511 (fixme) + +- `message: TODO (mb 2020-09-18): Investigate error messages` +- `author : Manuel Barkhau ` +- `date : 2020-09-19T16:24:10` + +``` + 270: def _bump( + ... + 298: sys.exit(1) + 299: except Exception as ex: +> 300: # TODO (mb 2020-09-18): Investigate error messages + 301: logger.error(str(ex)) + 302: sys.exit(1) +``` + + +## File src/pycalver/v2version.py - Line 617 - W0511 (fixme) - `message: TODO (mb 2020-09-20): New Rollover Behaviour:` - `author : Manuel Barkhau ` @@ -138,29 +106,30 @@ The recommended approach to using `pylint-ignore` is: ``` 578: def incr( ... - 614: ) - 615: -> 616: # TODO (mb 2020-09-20): New Rollover Behaviour: - 617: # Reset major, minor, patch to zero if any part to the left of it is incremented - 618: + 615: ) + 616: +> 617: # TODO (mb 2020-09-20): New Rollover Behaviour: + 618: # Reset major, minor, patch to zero if any part to the left of it is incremented + 619: ``` # W0703: broad-except -## File src/pycalver/__main__.py - Line 258 - W0703 (broad-except) +## File src/pycalver/__main__.py - Line 299 - W0703 (broad-except) - `message: Catching too general exception Exception` - `author : Manuel Barkhau ` - `date : 2020-09-05T14:30:17` ``` - 231: def _bump( + 270: def _bump( ... - 256: logger.error(str(ex)) - 257: sys.exit(1) -> 258: except Exception as ex: - 259: # TODO (mb 2020-09-18): Investigate error messages - 260: logger.error(str(ex)) + 297: logger.error(str(ex)) + 298: sys.exit(1) +> 299: except Exception as ex: + 300: # TODO (mb 2020-09-18): Investigate error messages + 301: logger.error(str(ex)) ``` + diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 0d0a145..5d3679a 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -66,13 +66,13 @@ VALID_RELEASE_VALUES = ("alpha", "beta", "dev", "rc", "post", "final") _current_date = dt.date.today().isoformat() -def _validate_date(date: typ.Optional[str], pin_date: bool) -> dt.date: +def _validate_date(date: typ.Optional[str], pin_date: bool) -> typ.Optional[dt.date]: if date and pin_date: logger.error(f"Can only use either --pin-date or --date='{date}', not both.") sys.exit(1) if date is None: - return + return None try: dt_val = dt.datetime.strptime(date, "%Y-%m-%d") diff --git a/test/test_cli.py b/test/test_cli.py index 1c01513..b7d3de4 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -109,12 +109,12 @@ def test_incr_default(runner): cmd = ['test', "-vv", "--pin-date", "--release", "beta", old_version] result = runner.invoke(cli, cmd) assert result.exit_code == 0 - assert f"Version: v201701.0005-beta\n" in result.output + assert "Version: v201701.0005-beta\n" in result.output - cmd = ['test', "-vv", "--pin-date", "--release", "beta", old_version, "vYYYY0M.BUILD[-RELEASE]"] + cmd = ['test', "-vv", "--pin-date", "--release", "beta", old_version, "vYYYY0M.BUILD[-RELEASE]"] result = runner.invoke(cli, cmd) assert result.exit_code == 0 - assert f"Version: v201701.1005-beta\n" in result.output + assert "Version: v201701.1005-beta\n" in result.output def test_incr_pin_date(runner): @@ -613,6 +613,58 @@ def test_v2_get_diff(runner): assert '+current_version = "v202010.1003-beta"' in diff_lines +WEEKNUM_TEST_CASES = [ + # 2020-12-26 Sat + ("2020-12-26", "YYYY.0W", "2020.51"), + ("2020-12-26", "YYYY.0U", "2020.51"), + ("2020-12-26", "GGGG.0V", "2020.52"), + # 2020-12-27 Sun + ("2020-12-27", "YYYY.0W", "2020.51"), + ("2020-12-27", "YYYY.0U", "2020.52"), + ("2020-12-27", "GGGG.0V", "2020.52"), + # 2020-12-28 Mon + ("2020-12-28", "YYYY.0W", "2020.52"), + ("2020-12-28", "YYYY.0U", "2020.52"), + ("2020-12-28", "GGGG.0V", "2020.53"), + # 2020-12-29 Tue + ("2020-12-29", "YYYY.0W", "2020.52"), + ("2020-12-29", "YYYY.0U", "2020.52"), + ("2020-12-29", "GGGG.0V", "2020.53"), + # 2020-12-30 Wed + ("2020-12-30", "YYYY.0W", "2020.52"), + ("2020-12-30", "YYYY.0U", "2020.52"), + ("2020-12-30", "GGGG.0V", "2020.53"), + # 2020-12-31 Thu + ("2020-12-31", "YYYY.0W", "2020.52"), + ("2020-12-31", "YYYY.0U", "2020.52"), + ("2020-12-31", "GGGG.0V", "2020.53"), + # 2021-01-01 Fri + ("2021-01-01", "YYYY.0W", "2021.00"), + ("2021-01-01", "YYYY.0U", "2021.00"), + ("2021-01-01", "GGGG.0V", "2020.53"), + # 2021-01-02 Sat + ("2021-01-02", "YYYY.0W", "2021.00"), + ("2021-01-02", "YYYY.0U", "2021.00"), + ("2021-01-02", "GGGG.0V", "2020.53"), + # 2021-01-03 Sun + ("2021-01-03", "YYYY.0W", "2021.00"), + ("2021-01-03", "YYYY.0U", "2021.01"), + ("2021-01-03", "GGGG.0V", "2020.53"), + # 2021-01-04 Mon + ("2021-01-04", "YYYY.0W", "2021.01"), + ("2021-01-04", "YYYY.0U", "2021.01"), + ("2021-01-04", "GGGG.0V", "2021.01"), +] + + +@pytest.mark.parametrize("date, pattern, expected", WEEKNUM_TEST_CASES) +def test_weeknum(date, pattern, expected, runner): + cmd = shlex.split(f"test -vv --date {date} 2020.40 {pattern}") + result = runner.invoke(cli, cmd) + assert result.exit_code == 0 + assert "New Version: " + expected in result.output + + def test_hg_commit_message(runner, caplog): _add_project_files("README.md", "setup.cfg") result = runner.invoke(cli, ['init', "-vv"]) diff --git a/test/test_version.py b/test/test_version.py index 3ed968b..4025ec4 100644 --- a/test/test_version.py +++ b/test/test_version.py @@ -13,8 +13,7 @@ from pycalver import version from pycalver import v1version from pycalver import v2version from pycalver import v1patterns - -# import pycalver2.patterns as v2patterns +from pycalver import v2patterns # pylint:disable=protected-access ; allowed for test code @@ -160,46 +159,58 @@ def test_part_field_mapping_v1(): assert not any(b_extra_fields), sorted(b_extra_fields) -def vnfo(**field_values): +def v1vnfo(**field_values): return v1version._parse_field_values(field_values) -PARSE_VERSION_TEST_CASES = [ - # TODO (mb 2020-09-06): add tests for new style patterns - # ["YYYY.MM.DD" , "2017.06.07", vnfo(year="2017", month="06", dom="07")], - ["{year}.{month}.{dom}" , "2017.06.07", vnfo(year="2017", month="06", dom="07")], - ["{year}.{month}.{dom_short}" , "2017.06.7" , vnfo(year="2017", month="06", dom="7" )], - ["{year}.{month}.{dom_short}" , "2017.06.7" , vnfo(year="2017", month="06", dom="7" )], - ["{year}.{month_short}.{dom_short}", "2017.6.7" , vnfo(year="2017", month="6" , dom="7" )], +def v2vnfo(**field_values): + return v2version._parse_version_info(field_values) + + +PARSE_V1_VERSION_TEST_CASES = [ + ["{year}.{month}.{dom}" , "2017.06.07", v1vnfo(year="2017", month="06", dom="07")], + ["{year}.{month}.{dom_short}" , "2017.06.7" , v1vnfo(year="2017", month="06", dom="7" )], + ["{year}.{month}.{dom_short}" , "2017.06.7" , v1vnfo(year="2017", month="06", dom="7" )], + ["{year}.{month_short}.{dom_short}", "2017.6.7" , v1vnfo(year="2017", month="6" , dom="7" )], ["{year}.{month}.{dom}" , "2017.6.07" , None], ["{year}.{month}.{dom}" , "2017.06.7" , None], ["{year}.{month_short}.{dom}" , "2017.06.7" , None], ["{year}.{month}.{dom_short}" , "2017.6.07" , None], - ["{year}.{month_short}.{MINOR}" , "2017.6.7" , vnfo(year="2017", month="6" , minor="7" )], - ["{year}.{month}.{MINOR}" , "2017.06.7" , vnfo(year="2017", month="06", minor="7" )], - ["{year}.{month}.{MINOR}" , "2017.06.07", vnfo(year="2017", month="06", minor="07")], + ["{year}.{month_short}.{MINOR}" , "2017.6.7" , v1vnfo(year="2017", month="6" , minor="7" )], + ["{year}.{month}.{MINOR}" , "2017.06.7" , v1vnfo(year="2017", month="06", minor="7" )], + ["{year}.{month}.{MINOR}" , "2017.06.07", v1vnfo(year="2017", month="06", minor="07")], ["{year}.{month}.{MINOR}" , "2017.6.7" , None], + ["YYYY.0M.0D" , "2017.06.07", v2vnfo(year_y="2017", month="06", dom="07")], + ["YYYY.MM.DD" , "2017.6.7" , v2vnfo(year_y="2017", month="6" , dom="7" )], + ["YYYY.MM.MD" , "2017.06.07", None], + ["YYYY.0M.0D" , "2017.6.7" , None], ] -@pytest.mark.parametrize("pattern_str, line, expected_vinfo", PARSE_VERSION_TEST_CASES) +@pytest.mark.parametrize("pattern_str, line, expected_vinfo", PARSE_V1_VERSION_TEST_CASES) def test_v1_parse_versions(pattern_str, line, expected_vinfo): - pattern = v1patterns.compile_pattern(pattern_str) - version_match = pattern.regexp.search(line) + if "{" in pattern_str: + pattern = v1patterns.compile_pattern(pattern_str) + version_match = pattern.regexp.search(line) + else: + pattern = v2patterns.compile_pattern(pattern_str) + version_match = pattern.regexp.search(line) if expected_vinfo is None: assert version_match is None - return + else: + assert version_match is not None - assert version_match is not None + version_str = version_match.group(0) - version_str = version_match.group(0) - version_info = v1version.parse_version_info(version_str, pattern_str) - - assert version_info == expected_vinfo + if "{" in pattern_str: + version_info = v1version.parse_version_info(version_str, pattern_str) + assert version_info == expected_vinfo + else: + version_info = v2version.parse_version_info(version_str, pattern_str) + assert version_info == expected_vinfo -# def test_v2_parse_versions(pattern_str, line, expected_vinfo): def test_v2_parse_versions(): _vnfo = v2version.parse_version_info("v201712.0033", raw_pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") fvals = {'year_y': 2017, 'month': 12, 'bid': "0033"} From fb2f3f11fd67cace81b94ad8ce6bde99b7b7e639 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Fri, 2 Oct 2020 23:02:02 +0000 Subject: [PATCH 39/98] add week pattern validation --- src/pycalver/config.py | 2 ++ src/pycalver/v2version.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/pycalver/config.py b/src/pycalver/config.py index 717709e..c65d4a1 100644 --- a/src/pycalver/config.py +++ b/src/pycalver/config.py @@ -269,6 +269,8 @@ def _parse_config(raw_cfg: RawConfig) -> Config: f"Invalid character(s) '{invalid_chars.group(1)}'" f" in pycalver.version_pattern = {raw_cfg['version_pattern']}" ) + if not v2version.is_valid_week_pattern(version_pattern): + raise ValueError(f"Invalid week number pattern: {version_pattern}") # TODO (mb 2020-09-18): Validate Pattern # detect YY with WW or UU -> suggest GG with VV diff --git a/src/pycalver/v2version.py b/src/pycalver/v2version.py index f04ec1c..be20f0f 100644 --- a/src/pycalver/v2version.py +++ b/src/pycalver/v2version.py @@ -575,6 +575,27 @@ def _incr_numeric( return vinfo +def is_valid_week_pattern(raw_pattern) -> bool: + has_yy_part = any(part in raw_pattern for part in ["YYYY", "YY", "0Y"]) + has_ww_part = any(part in raw_pattern for part in ["WW" , "0W", "UU", "0U"]) + has_gg_part = any(part in raw_pattern for part in ["GGGG", "GG", "0G"]) + has_vv_part = any(part in raw_pattern for part in ["VV" , "0V"]) + if not ((has_yy_part or has_gg_part) and (has_ww_part or has_vv_part)): + return True + elif has_yy_part and has_vv_part: + alt1 = raw_pattern.replace("V", "W") + alt2 = raw_pattern.replace("Y", "G") + logger.error(f"Invalid pattern: '{raw_pattern}'. Maybe try {alt1} or {alt2}") + return False + elif has_gg_part and has_ww_part: + alt1 = raw_pattern.replace("W", "V").replace("U", "V") + alt2 = raw_pattern.replace("G", "Y") + logger.error(f"Invalid pattern: '{raw_pattern}'. Maybe try {alt1} or {alt2}") + return False + else: + return True + + def incr( old_version: str, raw_pattern: str = "vYYYY0M.BUILD[-RELEASE[NUM]]", @@ -591,6 +612,9 @@ def incr( 'old_version' is assumed to be a string that matches 'raw_pattern' """ + if not is_valid_week_pattern(raw_pattern): + return None + try: old_vinfo = parse_version_info(old_version, raw_pattern) except version.PatternError as ex: From e10f858c40e78cf8360f45abbc0761c799bd1b69 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Fri, 2 Oct 2020 23:25:15 +0000 Subject: [PATCH 40/98] show pattern when --verbose --- src/pycalver/__main__.py | 23 ++++++++++++++----- src/pycalver/config.py | 8 +++---- src/pycalver/v2version.py | 48 +++++++++++++++++++-------------------- 3 files changed, 45 insertions(+), 34 deletions(-) diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 5d3679a..3c370de 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -28,9 +28,7 @@ from . import v1version from . import v2rewrite from . import v2version from . import v1patterns - -_VERBOSE = 0 - +from . import v2patterns try: import pretty_traceback @@ -45,7 +43,14 @@ click.disable_unicode_literals_warning = True logger = logging.getLogger("pycalver.__main__") +_VERBOSE = 0 + + def _configure_logging(verbose: int = 0) -> None: + # pylint:disable=global-statement; global flag is global. + global _VERBOSE + _VERBOSE = verbose + if verbose >= 2: log_format = "%(asctime)s.%(msecs)03d %(levelname)-7s %(name)-17s - %(message)s" log_level = logging.DEBUG @@ -102,9 +107,7 @@ def _validate_release_tag(release: typ.Optional[str]) -> None: @click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") def cli(verbose: int = 0) -> None: """Automatically update PyCalVer version strings on python projects.""" - # pylint:disable=global-statement; global flag is global. - global _VERBOSE - _VERBOSE = verbose + _configure_logging(verbose=max(_VERBOSE, verbose)) @cli.command() @@ -241,6 +244,14 @@ def _incr( ) -> typ.Optional[str]: v1_parts = list(v1patterns.PART_PATTERNS) + list(v1patterns.FULL_PART_FORMATS) has_v1_part = any("{" + part + "}" in raw_pattern for part in v1_parts) + if _VERBOSE: + if has_v1_part: + pattern = v1patterns.compile_pattern(raw_pattern) + else: + pattern = v2patterns.compile_pattern(raw_pattern) + + logger.info(f"Using pattern {raw_pattern}/{pattern.regexp.pattern}") + if has_v1_part: return v1version.incr( old_version, diff --git a/src/pycalver/config.py b/src/pycalver/config.py index c65d4a1..3d47155 100644 --- a/src/pycalver/config.py +++ b/src/pycalver/config.py @@ -406,8 +406,8 @@ def init( DEFAULT_CONFIGPARSER_BASE_TMPL = """ [pycalver] current_version = "{initial_version}" -version_pattern = "{{pycalver}}" -commit_message = "bump version to {{new_version}}" +version_pattern = "vYYYY0M.BUILD[-RELEASE]" +commit_message = "bump version {{old_version}} -> {{new_version}}" commit = True tag = True push = True @@ -446,8 +446,8 @@ README.md = DEFAULT_TOML_BASE_TMPL = """ [pycalver] current_version = "{initial_version}" -version_pattern = "{{pycalver}}" -commit_message = "bump version to {{new_version}}" +version_pattern = "vYYYY0M.BUILD[-RELEASE]" +commit_message = "bump version {{old_version}} -> {{new_version}}" commit = true tag = true push = true diff --git a/src/pycalver/v2version.py b/src/pycalver/v2version.py index be20f0f..745c8fc 100644 --- a/src/pycalver/v2version.py +++ b/src/pycalver/v2version.py @@ -263,10 +263,10 @@ def parse_version_info( return _parse_version_info(field_values) -def is_valid(version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-RELEASE[NUM]]") -> bool: +def is_valid(version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-RELEASE]") -> bool: """Check if a version matches a pattern. - >>> is_valid("v201712.0033-beta", raw_pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") + >>> is_valid("v201712.0033-beta", raw_pattern="vYYYY0M.BUILD[-RELEASE]") True >>> is_valid("v201712.0033-beta", raw_pattern="MAJOR.MINOR.PATCH") False @@ -465,44 +465,44 @@ def format_version(vinfo: version.V2VersionInfo, raw_pattern: str) -> str: >>> format_version(vinfo_a, raw_pattern="vYY.BLD[-PYTAGNUM]") 'v7.33-b0' - >>> format_version(vinfo_a, raw_pattern="YYYY0M.BUILD[PYTAG[NUM]]") - '200701.0033b' >>> format_version(vinfo_a, raw_pattern="vYY.BLD[-PYTAGNUM]") 'v7.33-b0' - >>> format_version(vinfo_a, raw_pattern="v0Y.BLD[-RELEASE[NUM]]") + >>> format_version(vinfo_a, raw_pattern="YYYY0M.BUILD[PYTAG[NUM]]") + '200701.0033b' + >>> format_version(vinfo_a, raw_pattern="v0Y.BLD[-RELEASE]") 'v07.33-beta' - >>> format_version(vinfo_a, raw_pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_a, raw_pattern="vYYYY0M.BUILD[-RELEASE]") 'v200701.0033-beta' - >>> format_version(vinfo_b, raw_pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_b, raw_pattern="vYYYY0M.BUILD[-RELEASE]") 'v200712.0033-beta' - >>> format_version(vinfo_a, raw_pattern="vYYYYw0W.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_a, raw_pattern="vYYYYw0W.BUILD[-RELEASE]") 'v2007w01.0033-beta' - >>> format_version(vinfo_a, raw_pattern="vYYYYwWW.BLD[-RELEASE[NUM]]") + >>> format_version(vinfo_a, raw_pattern="vYYYYwWW.BLD[-RELEASE]") 'v2007w1.33-beta' - >>> format_version(vinfo_b, raw_pattern="vYYYYw0W.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_b, raw_pattern="vYYYYw0W.BUILD[-RELEASE]") 'v2007w53.0033-beta' - >>> format_version(vinfo_a, raw_pattern="vYYYYd00J.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_a, raw_pattern="vYYYYd00J.BUILD[-RELEASE]") 'v2007d001.0033-beta' - >>> format_version(vinfo_a, raw_pattern="vYYYYdJJJ.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_a, raw_pattern="vYYYYdJJJ.BUILD[-RELEASE]") 'v2007d1.0033-beta' - >>> format_version(vinfo_b, raw_pattern="vYYYYd00J.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_b, raw_pattern="vYYYYd00J.BUILD[-RELEASE]") 'v2007d365.0033-beta' >>> format_version(vinfo_a, raw_pattern="vGGGGwVV.BLD[PYTAGNUM]") 'v2007w1.33b0' - >>> format_version(vinfo_a, raw_pattern="vGGGGw0V.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_a, raw_pattern="vGGGGw0V.BUILD[-RELEASE]") 'v2007w01.0033-beta' - >>> format_version(vinfo_b, raw_pattern="vGGGGw0V.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_b, raw_pattern="vGGGGw0V.BUILD[-RELEASE]") 'v2008w01.0033-beta' >>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final') >>> format_version(vinfo_c, raw_pattern="vYYYYwWW.BUILD-RELEASE") 'v2007w53.0033-final' - >>> format_version(vinfo_c, raw_pattern="vYYYYwWW.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_c, raw_pattern="vYYYYwWW.BUILD[-RELEASE]") 'v2007w53.0033' >>> format_version(vinfo_c, raw_pattern="vMAJOR.MINOR.PATCH") @@ -511,21 +511,21 @@ def format_version(vinfo: version.V2VersionInfo, raw_pattern: str) -> str: >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='final') >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH-RELEASENUM") 'v1.0.0-final0' - >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH-RELEASE[NUM]") + >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH-RELEASE") 'v1.0.0-final' >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH-RELEASE") 'v1.0.0-final' - >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH[-RELEASE[NUM]]") + >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH[-RELEASE]") 'v1.0.0' - >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR[.PATCH[-RELEASE[NUM]]]") + >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR[.PATCH[-RELEASE]]") 'v1.0' - >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]") + >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[-RELEASE]]]") 'v1' >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=2, tag='rc', pytag='rc', num=0) >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH]]") 'v1.0.2' - >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]") + >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[-RELEASE]]]") 'v1.0.2-rc' >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[PYTAGNUM]]]") 'v1.0.2rc0' @@ -533,11 +533,11 @@ def format_version(vinfo: version.V2VersionInfo, raw_pattern: str) -> str: 'v1.0.2' >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='rc', num=2) - >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]") + >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[-RELEASENUM]]]") 'v1.0.0-rc2' >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='rc', num=2) - >>> format_version(vinfo_d, raw_pattern='__version__ = "vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]"') + >>> format_version(vinfo_d, raw_pattern='__version__ = "vMAJOR[.MINOR[.PATCH[-RELEASENUM]]]"') '__version__ = "v1.0.0-rc2"' """ part_values = _format_part_values(vinfo) @@ -598,7 +598,7 @@ def is_valid_week_pattern(raw_pattern) -> bool: def incr( old_version: str, - raw_pattern: str = "vYYYY0M.BUILD[-RELEASE[NUM]]", + raw_pattern: str = "vYYYY0M.BUILD[-RELEASE]", *, release : typ.Optional[str] = None, major : bool = False, From e2b274a7bf09f044743349de978008644123e347 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sat, 3 Oct 2020 16:44:30 +0000 Subject: [PATCH 41/98] add pretty printing for regex patterns --- CHANGELOG.md | 2 + pylint-ignore.md | 54 +++++++++---------- requirements/pypi.txt | 1 + src/pycalver/__main__.py | 109 ++++++++++++++++++++++++++++++++++++++- src/pycalver/pysix.py | 42 +++++++++++++++ src/pycalver/regexfmt.py | 68 ++++++++++++++++++++++++ test/test_cli.py | 46 +++++++++++------ 7 files changed, 278 insertions(+), 44 deletions(-) create mode 100644 src/pycalver/pysix.py create mode 100644 src/pycalver/regexfmt.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 8518113..36f2886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,12 @@ - Better support for week numbering. - Better support for optional parts. - New: Start `BUILD` parts at `1000` to avoid leading zero truncation. + - New gitlab #2: Added `grep` subcommand to find and debug patterns. - New gitlab #10: `--pin-date` to keep date parts unchanged, and only increment non-date parts. - New add `--release-num` to increment the `alphaN`/`betaN`/`a0`/`b0`/etc. release number - Fix gitlab #8: Push tags only pushed tags, not actual commit. - Fix gitlab #9: Make commit message configurable. + - Fix gitlab #11: Show regexp when `--verbose` is used. - Switch main repo from gitlab to github. diff --git a/pylint-ignore.md b/pylint-ignore.md index 493356b..e0098a1 100644 --- a/pylint-ignore.md +++ b/pylint-ignore.md @@ -63,7 +63,7 @@ The recommended approach to using `pylint-ignore` is: ``` -## File src/pycalver/config.py - Line 273 - W0511 (fixme) +## File src/pycalver/config.py - Line 275 - W0511 (fixme) - `message: TODO (mb 2020-09-18): Validate Pattern` - `author : Manuel Barkhau ` @@ -72,64 +72,64 @@ The recommended approach to using `pylint-ignore` is: ``` 251: def _parse_config(raw_cfg: RawConfig) -> Config: ... - 271: ) - 272: -> 273: # TODO (mb 2020-09-18): Validate Pattern - 274: # detect YY with WW or UU -> suggest GG with VV - 275: # detect YYMM -> suggest YY0M + 273: raise ValueError(f"Invalid week number pattern: {version_pattern}") + 274: +> 275: # TODO (mb 2020-09-18): Validate Pattern + 276: # detect YY with WW or UU -> suggest GG with VV + 277: # detect YYMM -> suggest YY0M ``` -## File src/pycalver/__main__.py - Line 300 - W0511 (fixme) +## File src/pycalver/__main__.py - Line 317 - W0511 (fixme) - `message: TODO (mb 2020-09-18): Investigate error messages` - `author : Manuel Barkhau ` - `date : 2020-09-19T16:24:10` ``` - 270: def _bump( + 287: def _bump( ... - 298: sys.exit(1) - 299: except Exception as ex: -> 300: # TODO (mb 2020-09-18): Investigate error messages - 301: logger.error(str(ex)) - 302: sys.exit(1) + 315: sys.exit(1) + 316: except Exception as ex: +> 317: # TODO (mb 2020-09-18): Investigate error messages + 318: logger.error(str(ex)) + 319: sys.exit(1) ``` -## File src/pycalver/v2version.py - Line 617 - W0511 (fixme) +## File src/pycalver/v2version.py - Line 641 - W0511 (fixme) - `message: TODO (mb 2020-09-20): New Rollover Behaviour:` - `author : Manuel Barkhau ` - `date : 2020-09-20T17:36:38` ``` - 578: def incr( + 599: def incr( ... - 615: ) - 616: -> 617: # TODO (mb 2020-09-20): New Rollover Behaviour: - 618: # Reset major, minor, patch to zero if any part to the left of it is incremented - 619: + 639: ) + 640: +> 641: # TODO (mb 2020-09-20): New Rollover Behaviour: + 642: # Reset major, minor, patch to zero if any part to the left of it is incremented + 643: ``` # W0703: broad-except -## File src/pycalver/__main__.py - Line 299 - W0703 (broad-except) +## File src/pycalver/__main__.py - Line 316 - W0703 (broad-except) - `message: Catching too general exception Exception` - `author : Manuel Barkhau ` - `date : 2020-09-05T14:30:17` ``` - 270: def _bump( + 287: def _bump( ... - 297: logger.error(str(ex)) - 298: sys.exit(1) -> 299: except Exception as ex: - 300: # TODO (mb 2020-09-18): Investigate error messages - 301: logger.error(str(ex)) + 314: logger.error(str(ex)) + 315: sys.exit(1) +> 316: except Exception as ex: + 317: # TODO (mb 2020-09-18): Investigate error messages + 318: logger.error(str(ex)) ``` diff --git a/requirements/pypi.txt b/requirements/pypi.txt index 9dcff45..c3400e8 100644 --- a/requirements/pypi.txt +++ b/requirements/pypi.txt @@ -12,3 +12,4 @@ typing; python_version < "3.5" click toml lexid +colorama>=0.4 diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 3c370de..26d3bb8 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -9,6 +9,7 @@ __main__ module for PyCalVer. Enables use as module: $ python -m pycalver --version """ +import io import sys import typing as typ import logging @@ -16,6 +17,7 @@ import datetime as dt import subprocess as sp import click +import colorama from . import vcs from . import v1cli @@ -23,6 +25,8 @@ from . import v2cli from . import config from . import rewrite from . import version +from . import patterns +from . import regexfmt from . import v1rewrite from . import v1version from . import v2rewrite @@ -149,7 +153,7 @@ def test( ) -> None: """Increment a version number for demo purposes.""" _configure_logging(verbose=max(_VERBOSE, verbose)) - raw_pattern = pattern + raw_pattern = pattern # use internal naming convention _validate_release_tag(release) _date = _validate_date(date, pin_date) @@ -491,5 +495,108 @@ def bump( _try_bump(cfg, new_version, commit_message, allow_dirty) +def _grep_text(pattern: patterns.Pattern, text: str, color: bool) -> int: + match_count = 0 + all_lines = text.splitlines() + for match in pattern.regexp.finditer(text): + match_count += 1 + match_start, match_end = match.span() + + line_idx = text[:match_start].count("\n") + line_start = text.rfind("\n", 0, match_start) + 1 + line_end = text.find("\n", match_end, -1) + if color: + matched_line = ( + text[line_start:match_start] + + colorama.Style.BRIGHT + + text[match_start:match_end] + + colorama.Style.RESET_ALL + + text[match_end:line_end] + ) + else: + matched_line = ( + text[line_start:match_start] + + text[match_start:match_end] + + text[match_end:line_end] + ) + + lines_offset = max(0, line_idx - 1) + 1 + lines = all_lines[line_idx - 1 : line_idx + 2] + + if line_idx == 0: + lines[0] = matched_line + else: + lines[1] = matched_line + + for i, line in enumerate(lines): + print(f"{lines_offset + i:>4}: {line}") + + print() + return match_count + + +def _grep( + raw_pattern: str, + file_ios : typ.Tuple[io.TextIOWrapper], + color : bool, +) -> None: + pattern = v2patterns.compile_pattern(raw_pattern) + + match_count = 0 + for file_io in file_ios: + text = file_io.read() + + _match_count = _grep_text(pattern, text, color) + + print() + print(f"Found {_match_count} match for pattern '{raw_pattern}' in {file_io.name}") + print() + + match_count += _match_count + + if match_count == 0 or _VERBOSE: + pyexpr_regex = regexfmt.pyexpr_regex(pattern.regexp.pattern) + + print(f"# pycalver pattern: '{raw_pattern}'") + print("# " + regexfmt.regex101_url(pattern)) + print(pyexpr_regex) + print() + + if match_count == 0: + sys.exit(1) + + +@cli.command() +@click.option( + "-v", + "--verbose", + count=True, + help="Control log level. -vv for debug level.", +) +@click.argument("pattern") +@click.argument('files', nargs=-1, type=click.File('r')) +def grep( + pattern: str, + files : typ.Tuple[io.TextIOWrapper], + verbose: int = 0, +) -> None: + """Search files for a version pattern.""" + verbose = max(_VERBOSE, verbose) + _configure_logging(verbose) + + raw_pattern = pattern # use internal naming convention + + isatty = getattr(sys.stdout, 'isatty', lambda: False) + + if isatty(): + colorama.init() + try: + _grep(raw_pattern, files, color=True) + finally: + colorama.deinit() + else: + _grep(raw_pattern, files, color=False) + + if __name__ == '__main__': cli() diff --git a/src/pycalver/pysix.py b/src/pycalver/pysix.py new file mode 100644 index 0000000..36e77e9 --- /dev/null +++ b/src/pycalver/pysix.py @@ -0,0 +1,42 @@ +import sys +import typing as typ + +PY2 = sys.version < "3" + + +try: + from urllib.parse import quote as py3_stdlib_quote +except ImportError: + from urllib import quote as py2_stdlib_quote # type: ignore + + +# NOTE (mb 2016-05-23): quote in python2 expects bytes argument. + + +def quote( + string : str, + safe : str = "/", + encoding: typ.Optional[str] = None, + errors : typ.Optional[str] = None, +) -> str: + if not isinstance(string, str): + errmsg = f"Expected str/unicode but got {type(string)}" # type: ignore + raise TypeError(errmsg) + + if encoding is None: + _encoding = "utf-8" + else: + _encoding = encoding + + if errors is None: + _errors = "strict" + else: + _errors = errors + + if PY2: + data = string.encode(_encoding) + + res = py2_stdlib_quote(data, safe=safe.encode(_encoding)) + return res.decode(_encoding, errors=_errors) + else: + return py3_stdlib_quote(string, safe=safe, encoding=_encoding, errors=_errors) diff --git a/src/pycalver/regexfmt.py b/src/pycalver/regexfmt.py new file mode 100644 index 0000000..05c3ccb --- /dev/null +++ b/src/pycalver/regexfmt.py @@ -0,0 +1,68 @@ +import re +import textwrap + +from . import pysix +from . import patterns + + +def format_regex(regex: str) -> str: + r"""Format a regex pattern suitible for flags=re.VERBOSE. + + >>> regex = r"\[CalVer v(?P[1-9][0-9]{3})(?P(?:1[0-2]|0[1-9]))" + >>> print(format_regex(regex)) + \[CalVer[ ]v + (?P[1-9][0-9]{3}) + (?P + (?:1[0-2]|0[1-9]) + ) + """ + # provoke error for invalid regex + re.compile(regex) + + tmp_regex = regex.replace(" ", r"[ ]") + tmp_regex, _ = re.subn(r"([^\\])?\)(\?)?", "\\1)\\2\n", tmp_regex) + tmp_regex, _ = re.subn(r"([^\\])\(" , "\\1\n(" , tmp_regex) + tmp_regex, _ = re.subn(r"^\)\)" , ")\n)" , tmp_regex, flags=re.MULTILINE) + lines = tmp_regex.splitlines() + indented_lines = [] + level = 0 + for line in lines: + if line.strip(): + increment = line.count("(") - line.count(")") + if increment >= 0: + line = " " * level + line + level += increment + else: + level += increment + line = " " * level + line + indented_lines.append(line) + + formatted_regex = "\n".join(indented_lines) + + # provoke error if there is a bug in the formatting code + re.compile(formatted_regex) + return formatted_regex + + +def pyexpr_regex(regex: str) -> str: + try: + formatted_regex = format_regex(regex) + formatted_regex = textwrap.indent(formatted_regex.rstrip(), " ") + return 're.compile(r"""\n' + formatted_regex + '\n""", flags=re.VERBOSE)' + except re.error: + return f"re.compile({repr(regex)})" + + +def regex101_url(pattern: patterns.Pattern) -> str: + try: + regex_text = format_regex(pattern.regexp.pattern) + except re.error: + regex_text = pattern.regexp.pattern + + return "".join( + ( + "https://regex101.com/", + "?flavor=python", + "&flags=gmx" "®ex=" + pysix.quote(regex_text), + ) + ) diff --git a/test/test_cli.py b/test/test_cli.py index b7d3de4..eed8c2b 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -28,7 +28,7 @@ from pycalver.__main__ import cli README_TEXT_FIXTURE = """ Hello World v201701.1002-alpha ! - aka. 201701.1002a0 ! + [aka. 201701.1002a0 !] """ @@ -421,7 +421,7 @@ def test_novcs_bump(runner): with pl.Path("README.md").open() as fobj: content = fobj.read() assert calver + ".1002-alpha !\n" in content - assert calver[1:] + ".1002a0 !\n" in content + assert calver[1:] + ".1002a0 !]\n" in content result = runner.invoke(cli, ['bump', "-vv", "--release", "beta"]) assert result.exit_code == 0 @@ -429,7 +429,7 @@ def test_novcs_bump(runner): with pl.Path("README.md").open() as fobj: content = fobj.read() assert calver + ".1003-beta !\n" in content - assert calver[1:] + ".1003b0 !\n" in content + assert calver[1:] + ".1003b0 !]\n" in content def test_git_bump(runner): @@ -584,9 +584,9 @@ def test_v1_get_diff(runner): diff_lines = set(diff_str.splitlines()) assert "- Hello World v201701.1002-alpha !" in diff_lines - assert "- aka. 201701.1002a0 !" in diff_lines + assert "- [aka. 201701.1002a0 !]" in diff_lines assert "+ Hello World v202010.1003-beta !" in diff_lines - assert "+ aka. 202010.1003b0 !" in diff_lines + assert "+ [aka. 202010.1003b0 !]" in diff_lines assert '-current_version = "v202010.1001-alpha"' in diff_lines assert '+current_version = "v202010.1003-beta"' in diff_lines @@ -605,9 +605,9 @@ def test_v2_get_diff(runner): diff_lines = set(diff_str.splitlines()) assert "- Hello World v201701.1002-alpha !" in diff_lines - assert "- aka. 201701.1002a0 !" in diff_lines + assert "- [aka. 201701.1002a0 !]" in diff_lines assert "+ Hello World v202010.1003-beta !" in diff_lines - assert "+ aka. 202010.1003b0 !" in diff_lines + assert "+ [aka. 202010.1003b0 !]" in diff_lines assert '-current_version = "v202010.1001-alpha"' in diff_lines assert '+current_version = "v202010.1003-beta"' in diff_lines @@ -687,11 +687,9 @@ def test_hg_commit_message(runner, caplog): commits = shell(*shlex.split("hg log -l 2")).decode("utf-8").split("\n\n") - summary = commits[1].split("summary:")[-1] - assert ( - "bump from v201903.1001-alpha (201903.1001a0) to v201903.1002-beta (201903.1002b0)" - in summary - ) + expected = "bump from v201903.1001-alpha (201903.1001a0) to v201903.1002-beta (201903.1002b0)" + summary = commits[1].split("summary:")[-1] + assert expected in summary def test_git_commit_message(runner, caplog): @@ -716,7 +714,23 @@ def test_git_commit_message(runner, caplog): commits = shell(*shlex.split("git log -l 2")).decode("utf-8").split("\n\n") - summary = commits[1] - assert ( - "bump: v201903.1001-alpha (201903.1001a0) -> v201903.1002-beta (201903.1002b0)" in summary - ) + expected = "bump: v201903.1001-alpha (201903.1001a0) -> v201903.1002-beta (201903.1002b0)" + assert expected in commits[1] + + +def test_grep(runner): + _add_project_files("README.md") + + result = runner.invoke(cli, ['grep', r"vYYYY0M.BUILD[-RELEASE]", "README.md"]) + assert result.exit_code == 0 + assert "Found 1 match for pattern" in result.output + + search_re = r"^\s+2:\s+Hello World v201701\.1002-alpha !" + assert re.search(search_re, result.output, flags=re.MULTILINE) + + result = runner.invoke(cli, ['grep', r"\[aka. YYYY0M.BLD[PYTAGNUM] \!\]", "README.md"]) + assert result.exit_code == 0 + assert "Found 1 match for pattern" in result.output + + search_re = r"^\s+3:\s+\[aka\. 201701\.1002a0 \!\]" + assert re.search(search_re, result.output, flags=re.MULTILINE) From 4c8c9b128a8e5ad69287e31ea9d68d1fb6dcba1c Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sat, 3 Oct 2020 17:57:58 +0000 Subject: [PATCH 42/98] disallow --release=dev dev releases don't work like other kinds of releases, they would have to be implemented as a different kind of part. --- CHANGELOG.md | 1 + src/pycalver/__main__.py | 2 +- test/test_patterns.py | 2 -- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36f2886..4643202 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Fix gitlab #8: Push tags only pushed tags, not actual commit. - Fix gitlab #9: Make commit message configurable. - Fix gitlab #11: Show regexp when `--verbose` is used. + - Fix: Disallow `--release=dev` which has different semantics than other release tags. - Switch main repo from gitlab to github. diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 26d3bb8..3fa3708 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -69,7 +69,7 @@ def _configure_logging(verbose: int = 0) -> None: logger.debug("Logging configured.") -VALID_RELEASE_VALUES = ("alpha", "beta", "dev", "rc", "post", "final") +VALID_RELEASE_VALUES = ("alpha", "beta", "rc", "post", "final") _current_date = dt.date.today().isoformat() diff --git a/test/test_patterns.py b/test/test_patterns.py index 2621fc4..e51be13 100644 --- a/test/test_patterns.py +++ b/test/test_patterns.py @@ -130,14 +130,12 @@ V2_PART_PATTERN_CASES = [ (['RELEASE'], "alpha" , "alpha"), (['RELEASE'], "alfa" , None), (['RELEASE'], "beta" , "beta"), - (['RELEASE'], "dev" , "dev"), (['RELEASE'], "rc" , "rc"), (['RELEASE'], "post" , "post"), (['RELEASE'], "final" , "final"), (['RELEASE'], "latest", None), (['PYTAG' ], "a" , "a"), (['PYTAG' ], "b" , "b"), - (['PYTAG' ], "dev" , "dev"), (['PYTAG' ], "rc" , "rc"), (['PYTAG' ], "post" , "post"), (['PYTAG' ], "post" , "post"), From 2f421daf16804b2ff697c4151a0996665354e137 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sat, 3 Oct 2020 18:04:08 +0000 Subject: [PATCH 43/98] avoid duplicate pattern groups Since patterns are always wrapped in a named group anyway, we don't need to do another unnamed group also. This makes the regular expressions more readable. --- src/pycalver/v2patterns.py | 28 ++++++++++++++-------------- test/test_patterns.py | 28 +++++++++++++++++----------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/pycalver/v2patterns.py b/src/pycalver/v2patterns.py index 6e33891..3336dce 100644 --- a/src/pycalver/v2patterns.py +++ b/src/pycalver/v2patterns.py @@ -64,27 +64,27 @@ PART_PATTERNS = { 'GG' : r"[1-9][0-9]?", '0G' : r"[0-9]{2}", 'Q' : r"[1-4]", - 'MM' : r"(?:1[0-2]|[1-9])", - '0M' : r"(?:1[0-2]|0[1-9])", - 'DD' : r"(?:3[0-1]|[1-2][0-9]|[1-9])", - '0D' : r"(?:3[0-1]|[1-2][0-9]|0[1-9])", - 'JJJ' : r"(?:36[0-6]|3[0-5][0-9]|[1-2][0-9][0-9]|[1-9][0-9]|[1-9])", - '00J' : r"(?:36[0-6]|3[0-5][0-9]|[1-2][0-9][0-9]|0[1-9][0-9]|00[1-9])", + 'MM' : r"1[0-2]|[1-9]", + '0M' : r"1[0-2]|0[1-9]", + 'DD' : r"3[0-1]|[1-2][0-9]|[1-9]", + '0D' : r"3[0-1]|[1-2][0-9]|0[1-9]", + 'JJJ' : r"36[0-6]|3[0-5][0-9]|[1-2][0-9][0-9]|[1-9][0-9]|[1-9]", + '00J' : r"36[0-6]|3[0-5][0-9]|[1-2][0-9][0-9]|0[1-9][0-9]|00[1-9]", # week numbering parts - 'WW': r"(?:5[0-2]|[1-4][0-9]|[0-9])", - '0W': r"(?:5[0-2]|[0-4][0-9])", - 'UU': r"(?:5[0-2]|[1-4][0-9]|[0-9])", - '0U': r"(?:5[0-2]|[0-4][0-9])", - 'VV': r"(?:5[0-3]|[1-4][0-9]|[1-9])", - '0V': r"(?:5[0-3]|[1-4][0-9]|0[1-9])", + 'WW': r"5[0-2]|[1-4][0-9]|[0-9]", + '0W': r"5[0-2]|[0-4][0-9]", + 'UU': r"5[0-2]|[1-4][0-9]|[0-9]", + '0U': r"5[0-2]|[0-4][0-9]", + 'VV': r"5[0-3]|[1-4][0-9]|[1-9]", + '0V': r"5[0-3]|[1-4][0-9]|0[1-9]", # non calver parts 'MAJOR' : r"[0-9]+", 'MINOR' : r"[0-9]+", 'PATCH' : r"[0-9]+", 'BUILD' : r"[0-9]+", 'BLD' : r"[1-9][0-9]*", - 'RELEASE': r"(?:preview|final|alpha|beta|post|pre|dev|rc|a|b|c|r)", - 'PYTAG' : r"(?:post|dev|rc|a|b)", + 'RELEASE': r"preview|final|alpha|beta|post|rc", + 'PYTAG' : r"post|rc|a|b", 'NUM' : r"[0-9]+", } diff --git a/test/test_patterns.py b/test/test_patterns.py index e51be13..7c6302b 100644 --- a/test/test_patterns.py +++ b/test/test_patterns.py @@ -147,21 +147,27 @@ V2_PART_PATTERN_CASES = [ ] +def _compile_part_re(pattern_str): + grouped_pattern_str = r"(?:" + pattern_str + r")" + return re.compile(grouped_pattern_str) + + @pytest.mark.parametrize("parts, testcase, expected", V2_PART_PATTERN_CASES) -def test_part_patterns(parts, testcase, expected): +def test_v2_part_patterns(parts, testcase, expected): for part in parts: - pattern_str = v2patterns.PART_PATTERNS[part] - match = re.match("^" + pattern_str + "$", testcase) + part_re = _compile_part_re(v2patterns.PART_PATTERNS[part]) + match = part_re.match(testcase) assert (match is None and expected is None) or (match.group(0) == expected) -def _part_re_by_name(name): - return re.compile(v1patterns.PART_PATTERNS[name]) +@pytest.mark.parametrize("part_name", v2patterns.PART_PATTERNS.keys()) +def test_v1_part_compilation(part_name): + assert _compile_part_re(v2patterns.PART_PATTERNS[part_name]) @pytest.mark.parametrize("part_name", v1patterns.PART_PATTERNS.keys()) -def test_part_compilation(part_name): - assert _part_re_by_name(part_name) +def test_v2_part_compilation(part_name): + assert _compile_part_re(v1patterns.PART_PATTERNS[part_name]) PATTERN_PART_CASES = [ @@ -194,8 +200,8 @@ PATTERN_PART_CASES = [ @pytest.mark.parametrize("part_name, line, expected", PATTERN_PART_CASES) -def test_re_pattern_parts(part_name, line, expected): - part_re = _part_re_by_name(part_name) +def test_v1_re_pattern_parts(part_name, line, expected): + part_re = _compile_part_re(v1patterns.PART_PATTERNS[part_name]) result = part_re.search(line) if result is None: assert expected is None, (part_name, line) @@ -213,7 +219,7 @@ PATTERN_V1_CASES = [ @pytest.mark.parametrize("pattern_str, line, expected", PATTERN_V1_CASES) -def test_patterns_v1(pattern_str, line, expected): +def test_v1_patterns(pattern_str, line, expected): pattern = v1patterns.compile_pattern(pattern_str) result = pattern.regexp.search(line) if result is None: @@ -234,7 +240,7 @@ PATTERN_V2_CASES = [ @pytest.mark.parametrize("pattern_str, line, expected", PATTERN_V2_CASES) -def test_patterns_v2(pattern_str, line, expected): +def test_v2_patterns(pattern_str, line, expected): pattern = v2patterns.compile_pattern(pattern_str) result = pattern.regexp.search(line) result_val = None if result is None else result.group(0) From f705164e75d341e2e9ac229ac81ed3b6e45ce400 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sat, 3 Oct 2020 18:36:56 +0000 Subject: [PATCH 44/98] better error messages and fixups --- makefile.bootstrapit.make | 4 +- src/pycalver/__main__.py | 211 +++++++++++++++++++------------------- src/pycalver/pysix.py | 5 + src/pycalver/regexfmt.py | 17 ++- src/pycalver/v1rewrite.py | 9 +- src/pycalver/v2rewrite.py | 10 +- test/test_patterns.py | 22 ++-- 7 files changed, 155 insertions(+), 123 deletions(-) diff --git a/makefile.bootstrapit.make b/makefile.bootstrapit.make index fada680..ac91372 100644 --- a/makefile.bootstrapit.make +++ b/makefile.bootstrapit.make @@ -395,7 +395,7 @@ test: --cov-report term \ --html=reports/pytest/index.html \ --junitxml reports/pytest.xml \ - -k "$${PYTEST_FILTER}" \ + -k "$${PYTEST_FILTER-$${FLTR}}" \ $(shell cd src/ && ls -1 */__init__.py | awk '{ sub(/\/__init__.py/, "", $$1); print "--cov "$$1 }') \ test/ src/; @@ -515,7 +515,7 @@ devtest: --capture=no \ --exitfirst \ --failed-first \ - -k "$${PYTEST_FILTER}" \ + -k "$${PYTEST_FILTER-$${FLTR}}" \ test/ src/; @rm -rf "src/__pycache__"; diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 3fa3708..54204ad 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -148,8 +148,8 @@ def test( minor : bool = False, patch : bool = False, release_num: bool = False, - date : typ.Optional[str] = None, pin_date : bool = False, + date : typ.Optional[str] = None, ) -> None: """Increment a version number for demo purposes.""" _configure_logging(verbose=max(_VERBOSE, verbose)) @@ -179,6 +179,109 @@ def test( click.echo(f"PEP440 : {pep440_version}") +def _grep_text(pattern: patterns.Pattern, text: str, color: bool) -> int: + match_count = 0 + all_lines = text.splitlines() + for match in pattern.regexp.finditer(text): + match_count += 1 + match_start, match_end = match.span() + + line_idx = text[:match_start].count("\n") + line_start = text.rfind("\n", 0, match_start) + 1 + line_end = text.find("\n", match_end, -1) + if color: + matched_line = ( + text[line_start:match_start] + + colorama.Style.BRIGHT + + text[match_start:match_end] + + colorama.Style.RESET_ALL + + text[match_end:line_end] + ) + else: + matched_line = ( + text[line_start:match_start] + + text[match_start:match_end] + + text[match_end:line_end] + ) + + lines_offset = max(0, line_idx - 1) + 1 + lines = all_lines[line_idx - 1 : line_idx + 2] + + if line_idx == 0: + lines[0] = matched_line + else: + lines[1] = matched_line + + for i, line in enumerate(lines): + print(f"{lines_offset + i:>4}: {line}") + + print() + return match_count + + +def _grep( + raw_pattern: str, + file_ios : typ.Tuple[io.TextIOWrapper], + color : bool, +) -> None: + pattern = v2patterns.compile_pattern(raw_pattern) + + match_count = 0 + for file_io in file_ios: + text = file_io.read() + + _match_count = _grep_text(pattern, text, color) + + print() + print(f"Found {_match_count} match for pattern '{raw_pattern}' in {file_io.name}") + print() + + match_count += _match_count + + if match_count == 0 or _VERBOSE: + pyexpr_regex = regexfmt.pyexpr_regex(pattern.regexp.pattern) + + print(f"# pycalver pattern: '{raw_pattern}'") + print("# " + regexfmt.regex101_url(pattern.regexp.pattern)) + print(pyexpr_regex) + print() + + if match_count == 0: + sys.exit(1) + + +@cli.command() +@click.option( + "-v", + "--verbose", + count=True, + help="Control log level. -vv for debug level.", +) +@click.argument("pattern") +@click.argument('files', nargs=-1, type=click.File('r')) +def grep( + pattern: str, + files : typ.Tuple[io.TextIOWrapper], + verbose: int = 0, +) -> None: + """Search file(s) for a version pattern.""" + verbose = max(_VERBOSE, verbose) + _configure_logging(verbose) + + raw_pattern = pattern # use internal naming convention + + isatty = getattr(sys.stdout, 'isatty', lambda: False) + + if isatty(): + colorama.init() + try: + _grep(raw_pattern, files, color=True) + finally: + colorama.deinit() + else: + _grep(raw_pattern, files, color=False) + + @cli.command() @click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") @click.option( @@ -254,7 +357,8 @@ def _incr( else: pattern = v2patterns.compile_pattern(raw_pattern) - logger.info(f"Using pattern {raw_pattern}/{pattern.regexp.pattern}") + logger.info("Using pattern " + raw_pattern) + logger.info("regex = " + regexfmt.pyexpr_regex(pattern.regexp.pattern)) if has_v1_part: return v1version.incr( @@ -495,108 +599,5 @@ def bump( _try_bump(cfg, new_version, commit_message, allow_dirty) -def _grep_text(pattern: patterns.Pattern, text: str, color: bool) -> int: - match_count = 0 - all_lines = text.splitlines() - for match in pattern.regexp.finditer(text): - match_count += 1 - match_start, match_end = match.span() - - line_idx = text[:match_start].count("\n") - line_start = text.rfind("\n", 0, match_start) + 1 - line_end = text.find("\n", match_end, -1) - if color: - matched_line = ( - text[line_start:match_start] - + colorama.Style.BRIGHT - + text[match_start:match_end] - + colorama.Style.RESET_ALL - + text[match_end:line_end] - ) - else: - matched_line = ( - text[line_start:match_start] - + text[match_start:match_end] - + text[match_end:line_end] - ) - - lines_offset = max(0, line_idx - 1) + 1 - lines = all_lines[line_idx - 1 : line_idx + 2] - - if line_idx == 0: - lines[0] = matched_line - else: - lines[1] = matched_line - - for i, line in enumerate(lines): - print(f"{lines_offset + i:>4}: {line}") - - print() - return match_count - - -def _grep( - raw_pattern: str, - file_ios : typ.Tuple[io.TextIOWrapper], - color : bool, -) -> None: - pattern = v2patterns.compile_pattern(raw_pattern) - - match_count = 0 - for file_io in file_ios: - text = file_io.read() - - _match_count = _grep_text(pattern, text, color) - - print() - print(f"Found {_match_count} match for pattern '{raw_pattern}' in {file_io.name}") - print() - - match_count += _match_count - - if match_count == 0 or _VERBOSE: - pyexpr_regex = regexfmt.pyexpr_regex(pattern.regexp.pattern) - - print(f"# pycalver pattern: '{raw_pattern}'") - print("# " + regexfmt.regex101_url(pattern)) - print(pyexpr_regex) - print() - - if match_count == 0: - sys.exit(1) - - -@cli.command() -@click.option( - "-v", - "--verbose", - count=True, - help="Control log level. -vv for debug level.", -) -@click.argument("pattern") -@click.argument('files', nargs=-1, type=click.File('r')) -def grep( - pattern: str, - files : typ.Tuple[io.TextIOWrapper], - verbose: int = 0, -) -> None: - """Search files for a version pattern.""" - verbose = max(_VERBOSE, verbose) - _configure_logging(verbose) - - raw_pattern = pattern # use internal naming convention - - isatty = getattr(sys.stdout, 'isatty', lambda: False) - - if isatty(): - colorama.init() - try: - _grep(raw_pattern, files, color=True) - finally: - colorama.deinit() - else: - _grep(raw_pattern, files, color=False) - - if __name__ == '__main__': cli() diff --git a/src/pycalver/pysix.py b/src/pycalver/pysix.py index 36e77e9..354f2d5 100644 --- a/src/pycalver/pysix.py +++ b/src/pycalver/pysix.py @@ -1,3 +1,8 @@ +# This file is part of the pycalver project +# https://github.com/mbarkhau/pycalver +# +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# SPDX-License-Identifier: MIT import sys import typing as typ diff --git a/src/pycalver/regexfmt.py b/src/pycalver/regexfmt.py index 05c3ccb..7123b1a 100644 --- a/src/pycalver/regexfmt.py +++ b/src/pycalver/regexfmt.py @@ -1,8 +1,15 @@ +# This file is part of the pycalver project +# https://github.com/mbarkhau/pycalver +# +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# SPDX-License-Identifier: MIT import re +import logging import textwrap from . import pysix -from . import patterns + +logger = logging.getLogger("pycalver.regexfmt") def format_regex(regex: str) -> str: @@ -53,16 +60,16 @@ def pyexpr_regex(regex: str) -> str: return f"re.compile({repr(regex)})" -def regex101_url(pattern: patterns.Pattern) -> str: +def regex101_url(regex_pattern: str) -> str: try: - regex_text = format_regex(pattern.regexp.pattern) + regex_pattern = format_regex(regex_pattern) except re.error: - regex_text = pattern.regexp.pattern + logger.warning(f"Error formatting regex '{repr(regex_pattern)}'") return "".join( ( "https://regex101.com/", "?flavor=python", - "&flags=gmx" "®ex=" + pysix.quote(regex_text), + "&flags=gmx" "®ex=" + pysix.quote(regex_pattern), ) ) diff --git a/src/pycalver/v1rewrite.py b/src/pycalver/v1rewrite.py index e2f6313..4993bfd 100644 --- a/src/pycalver/v1rewrite.py +++ b/src/pycalver/v1rewrite.py @@ -13,6 +13,7 @@ from . import parse from . import config from . import rewrite from . import version +from . import regexfmt from . import v1version from .patterns import Pattern @@ -39,7 +40,13 @@ def rewrite_lines( if non_matched_patterns: for nmp in non_matched_patterns: logger.error(f"No match for pattern '{nmp.raw_pattern}'") - logger.error(f"Pattern compiles to regex '{nmp.regexp.pattern}'") + msg = ( + "\n# " + + regexfmt.regex101_url(nmp.regexp.pattern) + + "\nregex = " + + regexfmt.pyexpr_regex(nmp.regexp.pattern) + ) + logger.error(msg) raise rewrite.NoPatternMatch("Invalid pattern(s)") else: return new_lines diff --git a/src/pycalver/v2rewrite.py b/src/pycalver/v2rewrite.py index a6d85bd..2be2022 100644 --- a/src/pycalver/v2rewrite.py +++ b/src/pycalver/v2rewrite.py @@ -13,6 +13,7 @@ from . import parse from . import config from . import rewrite from . import version +from . import regexfmt from . import v2version from . import v2patterns from .patterns import Pattern @@ -43,7 +44,14 @@ def rewrite_lines( if non_matched_patterns: for nmp in non_matched_patterns: logger.error(f"No match for pattern '{nmp.raw_pattern}'") - logger.error(f"Pattern compiles to regex '{nmp.regexp.pattern}'") + msg = ( + "\n# " + + regexfmt.regex101_url(nmp.regexp.pattern) + + "\nregex = " + + regexfmt.pyexpr_regex(nmp.regexp.pattern) + ) + logger.error(msg) + logger.error(msg) raise rewrite.NoPatternMatch("Invalid pattern(s)") else: return new_lines diff --git a/test/test_patterns.py b/test/test_patterns.py index 7c6302b..981a9af 100644 --- a/test/test_patterns.py +++ b/test/test_patterns.py @@ -148,8 +148,9 @@ V2_PART_PATTERN_CASES = [ def _compile_part_re(pattern_str): - grouped_pattern_str = r"(?:" + pattern_str + r")" - return re.compile(grouped_pattern_str) + grouped_pattern_str = r"^(?:" + pattern_str + r")$" + # print("\n", grouped_pattern_str) + return re.compile(grouped_pattern_str, flags=re.MULTILINE) @pytest.mark.parametrize("parts, testcase, expected", V2_PART_PATTERN_CASES) @@ -157,7 +158,10 @@ def test_v2_part_patterns(parts, testcase, expected): for part in parts: part_re = _compile_part_re(v2patterns.PART_PATTERNS[part]) match = part_re.match(testcase) - assert (match is None and expected is None) or (match.group(0) == expected) + if match is None: + assert expected is None + else: + assert match.group(0) == expected @pytest.mark.parametrize("part_name", v2patterns.PART_PATTERNS.keys()) @@ -176,8 +180,8 @@ PATTERN_PART_CASES = [ ("pep440_pycalver", "201712.0033-alpha" , None), ("pycalver" , "v201712.0034" , "v201712.0034"), ("pycalver" , "v201712.0035-alpha" , "v201712.0035-alpha"), - ("pycalver" , "v201712.0036-alpha0", "v201712.0036-alpha"), - ("pycalver" , "v201712.0037-pre" , "v201712.0037"), + ("pycalver" , "v201712.0036-alpha0", None), + ("pycalver" , "v201712.0037-pre" , None), # pre not available for v1 patterns ("pycalver" , "201712.38a0" , None), ("pycalver" , "201712.0039" , None), ("semver" , "1.23.456" , "1.23.456"), @@ -194,17 +198,17 @@ PATTERN_PART_CASES = [ ("release" , "-dev" , "-dev"), ("release" , "-rc" , "-rc"), ("release" , "-post" , "-post"), - ("release" , "-pre" , ""), - ("release" , "alpha" , ""), + ("release" , "-pre" , None), # pre not available for v1 patterns + ("release" , "alpha" , None), # missing dash "-" prefix ] @pytest.mark.parametrize("part_name, line, expected", PATTERN_PART_CASES) def test_v1_re_pattern_parts(part_name, line, expected): part_re = _compile_part_re(v1patterns.PART_PATTERNS[part_name]) - result = part_re.search(line) + result = part_re.match(line) if result is None: - assert expected is None, (part_name, line) + assert expected is None, (part_name, line, result) else: result_val = result.group(0) assert result_val == expected, (part_name, line) From 3d23386dfd4ce0a8c5ef0ba5de04dca8f3f8405f Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sat, 3 Oct 2020 18:37:35 +0000 Subject: [PATCH 45/98] update commit_message config --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 3e3ebc7..9e87290 100644 --- a/setup.cfg +++ b/setup.cfg @@ -94,7 +94,7 @@ addopts = --doctest-modules [pycalver] current_version = "v202007.1036" version_pattern = "vYYYY0M.BUILD[-RELEASE[NUM]]" -commit_message = "bump version to {new_version}" +commit_message = "bump {old_version} -> {new_version}" commit = True tag = True push = True From a312da2a4c13b9f4c19377790d70f0dbdc9a86fc Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sat, 3 Oct 2020 18:40:42 +0000 Subject: [PATCH 46/98] bump v202007.1036 -> v202010.1037-beta --- README.md | 6 +++--- bootstrapit.sh | 2 +- setup.cfg | 2 +- setup.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a417254..1c47a97 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Project/Repo: [![MIT License][license_img]][license_ref] [![Supported Python Versions][pyversions_img]][pyversions_ref] -[![PyCalVer v202007.1036][version_img]][version_ref] +[![PyCalVer v202010.1037-beta][version_img]][version_ref] [![PyPI Releases][pypi_img]][pypi_ref] [![PyPI Downloads][downloads_img]][downloads_ref] @@ -71,7 +71,7 @@ The fastest way to setup a project is to use `pycalver init`. $ pip install pycalver ... Installing collected packages: click pathlib2 typing toml pycalver -Successfully installed pycalver-202009.1037 +Successfully installed pycalver-202010.1037b0 $ cd myproject ~/myproject/ @@ -1060,7 +1060,7 @@ artifact of a package, eg. a `.whl` file. [downloads_img]: https://pepy.tech/badge/pycalver/month [downloads_ref]: https://pepy.tech/project/pycalver -[version_img]: https://img.shields.io/static/v1.svg?label=PyCalVer&message=v202007.0036&color=blue +[version_img]: https://img.shields.io/static/v1.svg?label=PyCalVer&message=v202010.1037-beta&color=blue [version_ref]: https://pypi.org/project/pycalver/ [pypi_img]: https://img.shields.io/badge/PyPI-wheels-green.svg diff --git a/bootstrapit.sh b/bootstrapit.sh index 6c94052..76085b7 100644 --- a/bootstrapit.sh +++ b/bootstrapit.sh @@ -13,7 +13,7 @@ PACKAGE_NAME="pycalver" GIT_REPO_NAMESPACE="mbarkhau" GIT_REPO_DOMAIN="gitlab.com" -PACKAGE_VERSION="v202007.0036" +PACKAGE_VERSION="v202010.1037-beta" DEFAULT_PYTHON_VERSION="python=3.6" SUPPORTED_PYTHON_VERSIONS="python=2.7 python=3.5 python=3.6 python=3.7 pypy2.7 pypy3.5" diff --git a/setup.cfg b/setup.cfg index 9e87290..c1189d0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -92,7 +92,7 @@ addopts = --doctest-modules [pycalver] -current_version = "v202007.1036" +current_version = "v202010.1037-beta" version_pattern = "vYYYY0M.BUILD[-RELEASE[NUM]]" commit_message = "bump {old_version} -> {new_version}" commit = True diff --git a/setup.py b/setup.py index 18477ea..8a71c39 100644 --- a/setup.py +++ b/setup.py @@ -81,7 +81,7 @@ setuptools.setup( author="Manuel Barkhau", author_email="mbarkhau@gmail.com", url="https://github.com/mbarkhau/pycalver", - version="202007.1036", + version="202010.1037b0", keywords="version versioning bumpversion calver", description="CalVer for python libraries.", long_description=long_description, From 3fded04662a482c078f0c1fae7b18a86fdf9d755 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sat, 3 Oct 2020 18:43:14 +0000 Subject: [PATCH 47/98] attempt to fix #8 --- src/pycalver/vcs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pycalver/vcs.py b/src/pycalver/vcs.py index a1f062a..93de8da 100644 --- a/src/pycalver/vcs.py +++ b/src/pycalver/vcs.py @@ -36,7 +36,7 @@ VCS_SUBCOMMANDS_BY_NAME = { 'add_path' : "git add --update {path}", 'commit' : "git commit --message '{message}'", 'tag' : "git tag --annotate {tag} --message {tag}", - 'push_tag' : "git push origin --follow-tags {tag}", + 'push_tag' : "git push origin --follow-tags {tag} HEAD", 'show_remotes': "git config --get remote.origin.url", }, 'hg': { From cf8401526a2818bec1aa30f9906e9b315ade8e2b Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sat, 3 Oct 2020 18:43:23 +0000 Subject: [PATCH 48/98] bump v202010.1037-beta -> v202010.1038-beta --- README.md | 6 +++--- bootstrapit.sh | 2 +- setup.cfg | 2 +- setup.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1c47a97..d89a18f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Project/Repo: [![MIT License][license_img]][license_ref] [![Supported Python Versions][pyversions_img]][pyversions_ref] -[![PyCalVer v202010.1037-beta][version_img]][version_ref] +[![PyCalVer v202010.1038-beta][version_img]][version_ref] [![PyPI Releases][pypi_img]][pypi_ref] [![PyPI Downloads][downloads_img]][downloads_ref] @@ -71,7 +71,7 @@ The fastest way to setup a project is to use `pycalver init`. $ pip install pycalver ... Installing collected packages: click pathlib2 typing toml pycalver -Successfully installed pycalver-202010.1037b0 +Successfully installed pycalver-202010.1038b0 $ cd myproject ~/myproject/ @@ -1060,7 +1060,7 @@ artifact of a package, eg. a `.whl` file. [downloads_img]: https://pepy.tech/badge/pycalver/month [downloads_ref]: https://pepy.tech/project/pycalver -[version_img]: https://img.shields.io/static/v1.svg?label=PyCalVer&message=v202010.1037-beta&color=blue +[version_img]: https://img.shields.io/static/v1.svg?label=PyCalVer&message=v202010.1038-beta&color=blue [version_ref]: https://pypi.org/project/pycalver/ [pypi_img]: https://img.shields.io/badge/PyPI-wheels-green.svg diff --git a/bootstrapit.sh b/bootstrapit.sh index 76085b7..d033b2a 100644 --- a/bootstrapit.sh +++ b/bootstrapit.sh @@ -13,7 +13,7 @@ PACKAGE_NAME="pycalver" GIT_REPO_NAMESPACE="mbarkhau" GIT_REPO_DOMAIN="gitlab.com" -PACKAGE_VERSION="v202010.1037-beta" +PACKAGE_VERSION="v202010.1038-beta" DEFAULT_PYTHON_VERSION="python=3.6" SUPPORTED_PYTHON_VERSIONS="python=2.7 python=3.5 python=3.6 python=3.7 pypy2.7 pypy3.5" diff --git a/setup.cfg b/setup.cfg index c1189d0..575abee 100644 --- a/setup.cfg +++ b/setup.cfg @@ -92,7 +92,7 @@ addopts = --doctest-modules [pycalver] -current_version = "v202010.1037-beta" +current_version = "v202010.1038-beta" version_pattern = "vYYYY0M.BUILD[-RELEASE[NUM]]" commit_message = "bump {old_version} -> {new_version}" commit = True diff --git a/setup.py b/setup.py index 8a71c39..e7f9017 100644 --- a/setup.py +++ b/setup.py @@ -81,7 +81,7 @@ setuptools.setup( author="Manuel Barkhau", author_email="mbarkhau@gmail.com", url="https://github.com/mbarkhau/pycalver", - version="202010.1037b0", + version="202010.1038b0", keywords="version versioning bumpversion calver", description="CalVer for python libraries.", long_description=long_description, From 14115a2791524719c1ae4e565f3d4e90607529b0 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sat, 3 Oct 2020 19:57:37 +0000 Subject: [PATCH 49/98] fix: multiple file_patterns entries for same file --- src/pycalver/config.py | 80 ++++++++++++++++++++++++++---------------- test/test_cli.py | 30 ++++++++++++++++ 2 files changed, 80 insertions(+), 30 deletions(-) diff --git a/src/pycalver/config.py b/src/pycalver/config.py index 3d47155..99353a8 100644 --- a/src/pycalver/config.py +++ b/src/pycalver/config.py @@ -248,44 +248,69 @@ def _compile_v2_file_patterns(raw_cfg: RawConfig) -> typ.Iterable[FilePatternsIt yield filepath, compiled_patterns +def _compile_file_patterns(raw_cfg: RawConfig, is_new_pattern: bool) -> PatternsByFile: + if is_new_pattern: + _file_pattern_items = _compile_v2_file_patterns(raw_cfg) + else: + _file_pattern_items = _compile_v1_file_patterns(raw_cfg) + + # NOTE (mb 2020-10-03): There can be multiple items for the same + # path, so this is not an option: + # + # return dict(_file_pattern_items) + + file_patterns: PatternsByFile = {} + for path, patterns in _file_pattern_items: + if path in file_patterns: + file_patterns[path].extend(patterns) + else: + file_patterns[path] = patterns + return file_patterns + + +def _validate_version_with_pattern( + current_version: str, + version_pattern: str, + is_new_pattern : bool, +) -> None: + """Provoke ValueError if version_pattern and current_version are not compatible.""" + if is_new_pattern: + v2version.parse_version_info(current_version, version_pattern) + else: + v1version.parse_version_info(current_version, version_pattern) + + if is_new_pattern: + invalid_chars = re.search(r"([\s]+)", version_pattern) + if invalid_chars: + errmsg = ( + f"Invalid character(s) '{invalid_chars.group(1)}'" + f' in pycalver.version_pattern = "{version_pattern}"' + ) + raise ValueError(errmsg) + if not v2version.is_valid_week_pattern(version_pattern): + errmsg = f"Invalid week number pattern: {version_pattern}" + raise ValueError(errmsg) + + def _parse_config(raw_cfg: RawConfig) -> Config: """Parse configuration which was loaded from an .ini/.cfg or .toml file.""" + commit_message: str = raw_cfg.get('commit_message', DEFAULT_COMMIT_MESSAGE) + commit_message = raw_cfg['commit_message'] = commit_message.strip("'\" ") + current_version: str = raw_cfg['current_version'] current_version = raw_cfg['current_version'] = current_version.strip("'\" ") version_pattern: str = raw_cfg['version_pattern'] version_pattern = raw_cfg['version_pattern'] = version_pattern.strip("'\" ") - commit_message: str = raw_cfg.get('commit_message', DEFAULT_COMMIT_MESSAGE) - commit_message = raw_cfg['commit_message'] = commit_message.strip("'\" ") - is_new_pattern = "{" not in version_pattern and "}" not in version_pattern - - if is_new_pattern: - invalid_chars = re.search(r"([\s]+)", raw_cfg['version_pattern']) - if invalid_chars: - raise ValueError( - f"Invalid character(s) '{invalid_chars.group(1)}'" - f" in pycalver.version_pattern = {raw_cfg['version_pattern']}" - ) - if not v2version.is_valid_week_pattern(version_pattern): - raise ValueError(f"Invalid week number pattern: {version_pattern}") - - # TODO (mb 2020-09-18): Validate Pattern - # detect YY with WW or UU -> suggest GG with VV - # detect YYMM -> suggest YY0M - # detect YYWW -> suggest YY0W - - # NOTE (mb 2019-01-05): Provoke ValueError if version_pattern - # and current_version are not compatible. - if is_new_pattern: - v2version.parse_version_info(current_version, version_pattern) - else: - v1version.parse_version_info(current_version, version_pattern) + _validate_version_with_pattern(current_version, version_pattern, is_new_pattern) pep440_version = version.to_pep440(current_version) + file_patterns = _compile_file_patterns(raw_cfg, is_new_pattern) + commit = raw_cfg['commit'] tag = raw_cfg['tag'] push = raw_cfg['push'] @@ -301,11 +326,6 @@ def _parse_config(raw_cfg: RawConfig) -> Config: if push and not commit: raise ValueError("pycalver.commit = true required if pycalver.push = true") - if is_new_pattern: - file_patterns = dict(_compile_v2_file_patterns(raw_cfg)) - else: - file_patterns = dict(_compile_v1_file_patterns(raw_cfg)) - cfg = Config( current_version=current_version, version_pattern=version_pattern, diff --git a/test/test_cli.py b/test/test_cli.py index eed8c2b..f225101 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -734,3 +734,33 @@ def test_grep(runner): search_re = r"^\s+3:\s+\[aka\. 201701\.1002a0 \!\]" assert re.search(search_re, result.output, flags=re.MULTILINE) + + +SETUP_CFG_MULTIMATCH_FILE_PATTERNS_FIXTURE = r""" +[pycalver] +current_version = "v201701.1002-alpha" +version_pattern = "{pycalver}" + +[pycalver:file_patterns] +setup.cfg = + current_version = "{version}" +README.md = + Hello World {version} ! +README.* = + [aka. {pep440_version} !] +""" + + +def test_multimatch_file_patterns(runner): + _add_project_files("README.md") + with pl.Path("setup.cfg").open(mode="w", encoding="utf-8") as fobj: + fobj.write(SETUP_CFG_MULTIMATCH_FILE_PATTERNS_FIXTURE) + + result = runner.invoke(cli, ['bump', '--release', 'beta']) + assert result.exit_code == 0 + + with pl.Path("README.md").open(mode="r", encoding="utf-8") as fobj: + readme_text = fobj.read() + + assert "Hello World v202010.1003-beta !" in readme_text + assert "[aka. 202010.1003b0 !]" in readme_text From 179208cf3097e13246ec2d67207d0153a626d5ef Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sat, 3 Oct 2020 20:03:59 +0000 Subject: [PATCH 50/98] doc updates --- CHANGELOG.md | 5 ++++- README.md | 15 +++++++------ pylint-ignore.md | 47 +++++++++++++--------------------------- src/pycalver/__main__.py | 18 +++++++-------- 4 files changed, 36 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4643202..7d45e90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,15 @@ - Better support for optional parts. - New: Start `BUILD` parts at `1000` to avoid leading zero truncation. - New gitlab #2: Added `grep` subcommand to find and debug patterns. + - New: Added better error messages to debug regular expressions. - New gitlab #10: `--pin-date` to keep date parts unchanged, and only increment non-date parts. - - New add `--release-num` to increment the `alphaN`/`betaN`/`a0`/`b0`/etc. release number + - New: Added `--release-num` to increment the `alphaN`/`betaN`/`a0`/`b0`/etc. release number + - New: Added `--date=` parameter to set explicit date (instead of current date). - Fix gitlab #8: Push tags only pushed tags, not actual commit. - Fix gitlab #9: Make commit message configurable. - Fix gitlab #11: Show regexp when `--verbose` is used. - Fix: Disallow `--release=dev` which has different semantics than other release tags. + - Fix: Entries in `file_patterns` were ignored if there were multiple for the same file. - Switch main repo from gitlab to github. diff --git a/README.md b/README.md index d89a18f..c780a0c 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ $ cd myproject ~/myproject/ $ pycalver init --dry WARNING - File not found: pycalver.toml -Exiting because of '--dry'. Would have written to pycalver.toml: +Exiting because of '-d/--dry'. Would have written to pycalver.toml: [pycalver] current_version = "v202010.1001-alpha" @@ -541,7 +541,7 @@ The steps performed by `bump` are: 5. *Tag* the new commit. 6. *Push* the new commit and tag. -Again, you can use `--dry` to inspect the changes first. +Again, you can use `-d/--dry` to inspect the changes first. ``` $ pycalver bump --dry @@ -597,12 +597,13 @@ TODO: Descriptions | CLI Argument | Description | |------------------|-------------| | --major | | -| -m --minor | | -| -p --patch | | -| -r --release-num | | +| -m/--minor | | +| -p/--patch | | +| -r/--release-num | | +| --date | | | --pin-date | | -| --no-fetch | | -| --dry | | +| -n/--no-fetch | | +| -d/--dry | | | --allow-dirty | | diff --git a/pylint-ignore.md b/pylint-ignore.md index e0098a1..c20c367 100644 --- a/pylint-ignore.md +++ b/pylint-ignore.md @@ -63,37 +63,20 @@ The recommended approach to using `pylint-ignore` is: ``` -## File src/pycalver/config.py - Line 275 - W0511 (fixme) - -- `message: TODO (mb 2020-09-18): Validate Pattern` -- `author : Manuel Barkhau ` -- `date : 2020-09-18T19:04:06` - -``` - 251: def _parse_config(raw_cfg: RawConfig) -> Config: - ... - 273: raise ValueError(f"Invalid week number pattern: {version_pattern}") - 274: -> 275: # TODO (mb 2020-09-18): Validate Pattern - 276: # detect YY with WW or UU -> suggest GG with VV - 277: # detect YYMM -> suggest YY0M -``` - - -## File src/pycalver/__main__.py - Line 317 - W0511 (fixme) +## File src/pycalver/__main__.py - Line 419 - W0511 (fixme) - `message: TODO (mb 2020-09-18): Investigate error messages` - `author : Manuel Barkhau ` - `date : 2020-09-19T16:24:10` ``` - 287: def _bump( + 389: def _bump( ... - 315: sys.exit(1) - 316: except Exception as ex: -> 317: # TODO (mb 2020-09-18): Investigate error messages - 318: logger.error(str(ex)) - 319: sys.exit(1) + 417: sys.exit(1) + 418: except Exception as ex: +> 419: # TODO (mb 2020-09-18): Investigate error messages + 420: logger.error(str(ex)) + 421: sys.exit(1) ``` @@ -101,7 +84,7 @@ The recommended approach to using `pylint-ignore` is: - `message: TODO (mb 2020-09-20): New Rollover Behaviour:` - `author : Manuel Barkhau ` -- `date : 2020-09-20T17:36:38` +- `date : 2020-10-03T19:31:28` ``` 599: def incr( @@ -116,20 +99,20 @@ The recommended approach to using `pylint-ignore` is: # W0703: broad-except -## File src/pycalver/__main__.py - Line 316 - W0703 (broad-except) +## File src/pycalver/__main__.py - Line 418 - W0703 (broad-except) - `message: Catching too general exception Exception` - `author : Manuel Barkhau ` - `date : 2020-09-05T14:30:17` ``` - 287: def _bump( + 389: def _bump( ... - 314: logger.error(str(ex)) - 315: sys.exit(1) -> 316: except Exception as ex: - 317: # TODO (mb 2020-09-18): Investigate error messages - 318: logger.error(str(ex)) + 416: logger.error(str(ex)) + 417: sys.exit(1) +> 418: except Exception as ex: + 419: # TODO (mb 2020-09-18): Investigate error messages + 420: logger.error(str(ex)) ``` diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 54204ad..d7b3083 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -133,11 +133,10 @@ def cli(verbose: int = 0) -> None: @click.option("-r" , "--release-num", is_flag=True, default=False, help="Increment release number.") @click.option("--pin-date", is_flag=True, default=False, help="Leave date components unchanged.") @click.option( - "-d", "--date", default=None, - metavar="", - help=f"Set explicit date in format YYYY-0M-0D (eg. {_current_date}).", + metavar="", + help=f"Set explicit date in format YYYY-0M-0D\n(eg. {_current_date}).", ) def test( old_version: str, @@ -212,10 +211,11 @@ def _grep_text(pattern: patterns.Pattern, text: str, color: bool) -> int: else: lines[1] = matched_line + print() + for i, line in enumerate(lines): print(f"{lines_offset + i:>4}: {line}") - print() return match_count @@ -444,7 +444,7 @@ def _try_bump( @cli.command() @click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") @click.option( - "--dry", default=False, is_flag=True, help="Display diff of changes, don't rewrite files." + '-d', "--dry", default=False, is_flag=True, help="Display diff of changes, don't rewrite files." ) def init(verbose: int = 0, dry: bool = False) -> None: """Initialize [pycalver] configuration.""" @@ -457,7 +457,7 @@ def init(verbose: int = 0, dry: bool = False) -> None: sys.exit(1) if dry: - click.echo(f"Exiting because of '--dry'. Would have written to {ctx.config_rel_path}:") + click.echo(f"Exiting because of '-d/--dry'. Would have written to {ctx.config_rel_path}:") cfg_text: str = config.default_config(ctx) click.echo("\n " + "\n ".join(cfg_text.splitlines())) sys.exit(0) @@ -489,6 +489,7 @@ def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: help="Sync tags from remote origin.", ) @click.option( + "-d", "--dry", default=False, is_flag=True, @@ -519,11 +520,10 @@ def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: @click.option("-r", "--release-num", is_flag=True, default=False, help="Increment release number.") @click.option("--pin-date", is_flag=True, default=False, help="Leave date components unchanged.") @click.option( - "-d", "--date", default=None, - metavar="", - help=f"Set explicit date in format YYYY-0M-0D (eg. {_current_date}).", + metavar="", + help=f"Set explicit date in format YYYY-0M-0D\n(eg. {_current_date}).", ) def bump( release : typ.Optional[str] = None, From 37777ee133c7bffe1b713bf4559b60d8ef5fb0c1 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sat, 3 Oct 2020 20:08:57 +0000 Subject: [PATCH 51/98] bump v202010.1038-beta -> v202010.1039-beta --- README.md | 6 +++--- bootstrapit.sh | 2 +- setup.cfg | 2 +- setup.py | 2 +- src/pycalver/__init__.py | 2 +- src/pycalver/__main__.py | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index c780a0c..14f0383 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Project/Repo: [![MIT License][license_img]][license_ref] [![Supported Python Versions][pyversions_img]][pyversions_ref] -[![PyCalVer v202010.1038-beta][version_img]][version_ref] +[![PyCalVer v202010.1039-beta][version_img]][version_ref] [![PyPI Releases][pypi_img]][pypi_ref] [![PyPI Downloads][downloads_img]][downloads_ref] @@ -71,7 +71,7 @@ The fastest way to setup a project is to use `pycalver init`. $ pip install pycalver ... Installing collected packages: click pathlib2 typing toml pycalver -Successfully installed pycalver-202010.1038b0 +Successfully installed pycalver-202010.1039b0 $ cd myproject ~/myproject/ @@ -1061,7 +1061,7 @@ artifact of a package, eg. a `.whl` file. [downloads_img]: https://pepy.tech/badge/pycalver/month [downloads_ref]: https://pepy.tech/project/pycalver -[version_img]: https://img.shields.io/static/v1.svg?label=PyCalVer&message=v202010.1038-beta&color=blue +[version_img]: https://img.shields.io/static/v1.svg?label=PyCalVer&message=v202010.1039-beta&color=blue [version_ref]: https://pypi.org/project/pycalver/ [pypi_img]: https://img.shields.io/badge/PyPI-wheels-green.svg diff --git a/bootstrapit.sh b/bootstrapit.sh index d033b2a..70560a5 100644 --- a/bootstrapit.sh +++ b/bootstrapit.sh @@ -13,7 +13,7 @@ PACKAGE_NAME="pycalver" GIT_REPO_NAMESPACE="mbarkhau" GIT_REPO_DOMAIN="gitlab.com" -PACKAGE_VERSION="v202010.1038-beta" +PACKAGE_VERSION="v202010.1039-beta" DEFAULT_PYTHON_VERSION="python=3.6" SUPPORTED_PYTHON_VERSIONS="python=2.7 python=3.5 python=3.6 python=3.7 pypy2.7 pypy3.5" diff --git a/setup.cfg b/setup.cfg index 575abee..8a41aca 100644 --- a/setup.cfg +++ b/setup.cfg @@ -92,7 +92,7 @@ addopts = --doctest-modules [pycalver] -current_version = "v202010.1038-beta" +current_version = "v202010.1039-beta" version_pattern = "vYYYY0M.BUILD[-RELEASE[NUM]]" commit_message = "bump {old_version} -> {new_version}" commit = True diff --git a/setup.py b/setup.py index e7f9017..9fc93ca 100644 --- a/setup.py +++ b/setup.py @@ -81,7 +81,7 @@ setuptools.setup( author="Manuel Barkhau", author_email="mbarkhau@gmail.com", url="https://github.com/mbarkhau/pycalver", - version="202010.1038b0", + version="202010.1039b0", keywords="version versioning bumpversion calver", description="CalVer for python libraries.", long_description=long_description, diff --git a/src/pycalver/__init__.py b/src/pycalver/__init__.py index 0d7e37b..6237ecd 100644 --- a/src/pycalver/__init__.py +++ b/src/pycalver/__init__.py @@ -5,4 +5,4 @@ # SPDX-License-Identifier: MIT """PyCalVer: CalVer for Python Packages.""" -__version__ = "v202007.0036" +__version__ = "v202010.1039-beta" diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index d7b3083..3ac32db 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -106,7 +106,7 @@ def _validate_release_tag(release: typ.Optional[str]) -> None: @click.group() -@click.version_option(version="v202007.0036") +@click.version_option(version="v202010.1039-beta") @click.help_option() @click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") def cli(verbose: int = 0) -> None: From 879ff4a945ee03ca93bd05d0587b77b019010cb1 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sat, 3 Oct 2020 23:42:52 +0000 Subject: [PATCH 52/98] wip: implement v2 rollover behavior --- CHANGELOG.md | 1 + pylint-ignore.md | 45 ++++-------- src/pycalver/v2version.py | 139 ++++++++++++++++++++++++-------------- src/pycalver/version.py | 16 ++++- test/test_cli.py | 31 +++++++++ 5 files changed, 148 insertions(+), 84 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d45e90..e442bee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Better support for week numbering. - Better support for optional parts. - New: Start `BUILD` parts at `1000` to avoid leading zero truncation. + - New: `MAJOR`/`MINOR`/`PATCH`/`INC` will roll over when a date part changes to their left. - New gitlab #2: Added `grep` subcommand to find and debug patterns. - New: Added better error messages to debug regular expressions. - New gitlab #10: `--pin-date` to keep date parts unchanged, and only increment non-date parts. diff --git a/pylint-ignore.md b/pylint-ignore.md index c20c367..d4e5c53 100644 --- a/pylint-ignore.md +++ b/pylint-ignore.md @@ -63,56 +63,39 @@ The recommended approach to using `pylint-ignore` is: ``` -## File src/pycalver/__main__.py - Line 419 - W0511 (fixme) +## File src/pycalver/__main__.py - Line 421 - W0511 (fixme) - `message: TODO (mb 2020-09-18): Investigate error messages` - `author : Manuel Barkhau ` - `date : 2020-09-19T16:24:10` ``` - 389: def _bump( + 391: def _bump( ... - 417: sys.exit(1) - 418: except Exception as ex: -> 419: # TODO (mb 2020-09-18): Investigate error messages - 420: logger.error(str(ex)) - 421: sys.exit(1) -``` - - -## File src/pycalver/v2version.py - Line 641 - W0511 (fixme) - -- `message: TODO (mb 2020-09-20): New Rollover Behaviour:` -- `author : Manuel Barkhau ` -- `date : 2020-10-03T19:31:28` - -``` - 599: def incr( - ... - 639: ) - 640: -> 641: # TODO (mb 2020-09-20): New Rollover Behaviour: - 642: # Reset major, minor, patch to zero if any part to the left of it is incremented - 643: + 419: sys.exit(1) + 420: except Exception as ex: +> 421: # TODO (mb 2020-09-18): Investigate error messages + 422: logger.error(str(ex)) + 423: sys.exit(1) ``` # W0703: broad-except -## File src/pycalver/__main__.py - Line 418 - W0703 (broad-except) +## File src/pycalver/__main__.py - Line 420 - W0703 (broad-except) - `message: Catching too general exception Exception` - `author : Manuel Barkhau ` - `date : 2020-09-05T14:30:17` ``` - 389: def _bump( + 391: def _bump( ... - 416: logger.error(str(ex)) - 417: sys.exit(1) -> 418: except Exception as ex: - 419: # TODO (mb 2020-09-18): Investigate error messages - 420: logger.error(str(ex)) + 418: logger.error(str(ex)) + 419: sys.exit(1) +> 420: except Exception as ex: + 421: # TODO (mb 2020-09-18): Investigate error messages + 422: logger.error(str(ex)) ``` diff --git a/src/pycalver/v2version.py b/src/pycalver/v2version.py index 745c8fc..3af15e1 100644 --- a/src/pycalver/v2version.py +++ b/src/pycalver/v2version.py @@ -318,36 +318,19 @@ def _format_part_values(vinfo: version.V2VersionInfo) -> PartValues: return sorted(kwargs.items(), key=lambda item: -len(item[0])) -def _clear_zero_segments( - formatted_segs: typ.List[str], is_zero_segment: typ.List[bool] -) -> typ.List[str]: - non_zero_segs = list(formatted_segs) - - has_val_to_right = False - for idx, is_zero in reversed(list(enumerate(is_zero_segment))): - is_optional = 0 < idx < len(formatted_segs) - 1 - if is_optional: - if is_zero and not has_val_to_right: - non_zero_segs[idx] = "" - else: - has_val_to_right = True - - return non_zero_segs - - Segment = str # mypy limitation wrt. cyclic definition # SegmentTree = typ.List[typ.Union[Segment, "SegmentTree"]] SegmentTree = typ.Any -def _parse_segment_tree(raw_pattern: str) -> SegmentTree: +def _parse_segtree(raw_pattern: str) -> SegmentTree: """Generate segment tree from pattern string. - >>> tree = _parse_segment_tree("aa[bb[cc]]") - >>> assert tree == ["aa", ["bb", ["cc"]]] - >>> tree = _parse_segment_tree("aa[bb[cc]dd[ee]ff]gg") - >>> assert tree == ["aa", ["bb", ["cc"], "dd", ["ee"], "ff"], "gg"] + >>> _parse_segtree('aa[bb[cc]]') + ['aa', ['bb', ['cc']]] + >>> _parse_segtree('aa[bb[cc]dd[ee]ff]gg') + ['aa', ['bb', ['cc'], 'dd', ['ee'], 'ff'], 'gg'] """ internal_root: SegmentTree = [] @@ -428,7 +411,7 @@ def _format_segment(seg: Segment, part_values: PartValues) -> FormatedSeg: def _format_segment_tree( - seg_tree : SegmentTree, + segtree : SegmentTree, part_values: PartValues, ) -> FormatedSeg: # print("??>>>", seg_tree) @@ -437,7 +420,7 @@ def _format_segment_tree( # is only omitted, if all parts to the right of it were also omitted. result_parts: typ.List[str] = [] is_zero = True - for seg in seg_tree: + for seg in segtree: if isinstance(seg, list): formatted_seg = _format_segment_tree(seg, part_values) else: @@ -541,38 +524,95 @@ def format_version(vinfo: version.V2VersionInfo, raw_pattern: str) -> str: '__version__ = "v1.0.0-rc2"' """ part_values = _format_part_values(vinfo) - seg_tree = _parse_segment_tree(raw_pattern) - formatted_seg = _format_segment_tree(seg_tree, part_values) + segtree = _parse_segtree(raw_pattern) + formatted_seg = _format_segment_tree(segtree, part_values) return formatted_seg.result +def _iter_flat_segtree(segtree: SegmentTree) -> typ.Iterable[Segment]: + """Flatten a SegmentTree (mixed nested list of lists or str). + + >>> list(_iter_flat_segtree(['aa', ['bb', ['cc'], 'dd', ['ee'], 'ff'], 'gg'])) + ['aa', 'bb', 'cc', 'dd', 'ee', 'ff', 'gg'] + """ + for subtree in segtree: + if isinstance(subtree, list): + for seg in _iter_flat_segtree(subtree): + yield seg + else: + yield subtree + + +def _parse_pattern_fields(raw_pattern: str) -> typ.List[str]: + parts = list(v2patterns.PATTERN_PART_FIELDS.keys()) + parts.sort(key=len, reverse=True) + + segtree = _parse_segtree(raw_pattern) + segments = _iter_flat_segtree(segtree) + + fields_by_index = {} + for segment_index, segment in enumerate(segments): + for part in parts: + part_index = segment.find(part) + if part_index >= 0: + field = v2patterns.PATTERN_PART_FIELDS[part] + fields_by_index[segment_index, part_index] = field + + return [field for _, field in sorted(fields_by_index.items())] + + +def _iter_reset_field_items( + fields : typ.List[str], + old_vinfo: version.V2VersionInfo, + cur_vinfo: version.V2VersionInfo, +) -> typ.Iterable[typ.Tuple[str, str]]: + # Any field to the left of another can reset all to the right + has_reset = False + for field in fields: + zero_val = version.V2_FIELD_ZERO_VALUES.get(field) + if has_reset and zero_val is not None: + yield field, zero_val + elif getattr(old_vinfo, field) != getattr(cur_vinfo, field): + has_reset = True + + def _incr_numeric( - vinfo : version.V2VersionInfo, + raw_pattern: str, + old_vinfo : version.V2VersionInfo, + cur_vinfo : version.V2VersionInfo, major : bool, minor : bool, patch : bool, - release : typ.Optional[str], + tag : typ.Optional[str], release_num: bool, ) -> version.V2VersionInfo: + # Reset major/minor/patch/num/inc to zero if any part to the left of it is incremented + fields = _parse_pattern_fields(raw_pattern) + reset_fields = dict(_iter_reset_field_items(fields, old_vinfo, cur_vinfo)) + + cur_kwargs = cur_vinfo._asdict() + cur_kwargs.update(reset_fields) + cur_vinfo = version.V2VersionInfo(**cur_kwargs) + # prevent truncation of leading zeros - if int(vinfo.bid) < 1000: - vinfo = vinfo._replace(bid=str(int(vinfo.bid) + 1000)) + if int(cur_vinfo.bid) < 1000: + cur_vinfo = cur_vinfo._replace(bid=str(int(cur_vinfo.bid) + 1000)) - vinfo = vinfo._replace(bid=lexid.next_id(vinfo.bid)) + cur_vinfo = cur_vinfo._replace(bid=lexid.next_id(cur_vinfo.bid)) - if major: - vinfo = vinfo._replace(major=vinfo.major + 1, minor=0, patch=0) - if minor: - vinfo = vinfo._replace(minor=vinfo.minor + 1, patch=0) - if patch: - vinfo = vinfo._replace(patch=vinfo.patch + 1) - if release_num: - vinfo = vinfo._replace(num=vinfo.num + 1) - if release: - if release != vinfo.tag: - vinfo = vinfo._replace(num=0) - vinfo = vinfo._replace(tag=release) - return vinfo + if major and 'major' not in reset_fields: + cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0, patch=0) + if minor and 'minor' not in reset_fields: + cur_vinfo = cur_vinfo._replace(minor=cur_vinfo.minor + 1, patch=0) + if patch and 'patch' not in reset_fields: + cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1) + if release_num and 'release_num' not in reset_fields: + cur_vinfo = cur_vinfo._replace(num=cur_vinfo.num + 1) + if tag and 'tag' not in reset_fields: + if tag != cur_vinfo.tag: + cur_vinfo = cur_vinfo._replace(num=0) + cur_vinfo = cur_vinfo._replace(tag=tag) + return cur_vinfo def is_valid_week_pattern(raw_pattern) -> bool: @@ -580,9 +620,7 @@ def is_valid_week_pattern(raw_pattern) -> bool: has_ww_part = any(part in raw_pattern for part in ["WW" , "0W", "UU", "0U"]) has_gg_part = any(part in raw_pattern for part in ["GGGG", "GG", "0G"]) has_vv_part = any(part in raw_pattern for part in ["VV" , "0V"]) - if not ((has_yy_part or has_gg_part) and (has_ww_part or has_vv_part)): - return True - elif has_yy_part and has_vv_part: + if has_yy_part and has_vv_part: alt1 = raw_pattern.replace("V", "W") alt2 = raw_pattern.replace("Y", "G") logger.error(f"Invalid pattern: '{raw_pattern}'. Maybe try {alt1} or {alt2}") @@ -630,17 +668,16 @@ def incr( cur_vinfo = old_vinfo._replace(**cur_cinfo._asdict()) cur_vinfo = _incr_numeric( + raw_pattern, + old_vinfo, cur_vinfo, major=major, minor=minor, patch=patch, - release=release, + tag=tag, release_num=release_num, ) - # TODO (mb 2020-09-20): New Rollover Behaviour: - # Reset major, minor, patch to zero if any part to the left of it is incremented - new_version = format_version(cur_vinfo, raw_pattern) if new_version == old_version: logger.error("Invalid arguments or pattern, version did not change.") diff --git a/src/pycalver/version.py b/src/pycalver/version.py index dfe7c1f..fb42532 100644 --- a/src/pycalver/version.py +++ b/src/pycalver/version.py @@ -109,18 +109,30 @@ assert set(RELEASE_BY_PEP440_TAG.keys()) == set(PEP440_TAG_BY_RELEASE.values()) assert set(RELEASE_BY_PEP440_TAG.values()) < set(PEP440_TAG_BY_RELEASE.keys()) -ZERO_VALUES = { +PART_ZERO_VALUES = { 'MAJOR' : "0", 'MINOR' : "0", 'PATCH' : "0", 'RELEASE': "final", 'PYTAG' : "", 'NUM' : "0", + 'INC' : "0", +} + + +V2_FIELD_ZERO_VALUES = { + 'major': "0", + 'minor': "0", + 'patch': "0", + 'tag' : "final", + 'pytag': "", + 'num' : "0", + 'inc' : "0", } def is_zero_val(part: str, part_value: str) -> bool: - return part in ZERO_VALUES and part_value == ZERO_VALUES[part] + return part in PART_ZERO_VALUES and part_value == PART_ZERO_VALUES[part] class PatternError(Exception): diff --git a/test/test_cli.py b/test/test_cli.py index f225101..a9b15be 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -10,6 +10,7 @@ import re import time import shlex import shutil +import datetime as dt import subprocess as sp import pytest @@ -21,6 +22,7 @@ from pycalver import v2cli from pycalver import config from pycalver import v1patterns from pycalver.__main__ import cli +from pycalver.__main__ import incr_dispatch # pylint:disable=redefined-outer-name ; pytest fixtures # pylint:disable=protected-access ; allowed for test code @@ -764,3 +766,32 @@ def test_multimatch_file_patterns(runner): assert "Hello World v202010.1003-beta !" in readme_text assert "[aka. 202010.1003b0 !]" in readme_text + + +def _kwargs(month, minor): + return {'date': dt.date(2020, month, 1), 'minor': minor} + + +ROLLOVER_TEST_CASES = [ + # v1 cases + ["{year}.{month}.{MINOR}", "2020.10.3", "2020.10.4", _kwargs(10, True)], + ["{year}.{month}.{MINOR}", "2020.10.3", None, _kwargs(10, False)], + ["{year}.{month}.{MINOR}", "2020.10.3", "2020.11.4", _kwargs(11, True)], + ["{year}.{month}.{MINOR}", "2020.10.3", "2020.11.3", _kwargs(11, False)], + # v2 cases + ["YYYY.MM.MINOR" , "2020.10.3", "2020.10.4", _kwargs(10, True)], + ["YYYY.MM.MINOR" , "2020.10.3", None, _kwargs(10, False)], + ["YYYY.MM.MINOR" , "2020.10.3", "2020.11.0", _kwargs(11, True)], + ["YYYY.MM.MINOR" , "2020.10.3", "2020.11.0", _kwargs(11, False)], + ["YYYY.MM[.MINOR]", "2020.10.3", "2020.10.4", _kwargs(10, True)], + ["YYYY.MM[.MINOR]", "2020.10.3", "2020.11", _kwargs(11, False)], +] + + +@pytest.mark.parametrize("version_pattern, old_version, expected, kwargs", ROLLOVER_TEST_CASES) +def test_rollover(version_pattern, old_version, expected, kwargs): + new_version = incr_dispatch(old_version, raw_pattern=version_pattern, **kwargs) + if new_version is None: + assert expected is None + else: + assert new_version == expected From 98647519c20cb59c923f5717eefdf8bf7f381004 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sat, 3 Oct 2020 23:44:09 +0000 Subject: [PATCH 53/98] more consistent internal naming (release -> tag) --- src/pycalver/__main__.py | 39 +++++++++++++++++++++------------------ src/pycalver/v1version.py | 6 +++--- src/pycalver/v2version.py | 2 +- test/test_version.py | 12 ++++++------ 4 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 3ac32db..1c45e6b 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -69,7 +69,7 @@ def _configure_logging(verbose: int = 0) -> None: logger.debug("Logging configured.") -VALID_RELEASE_VALUES = ("alpha", "beta", "rc", "post", "final") +VALID_RELEASE_TAG_VALUES = ("alpha", "beta", "rc", "post", "final") _current_date = dt.date.today().isoformat() @@ -93,15 +93,15 @@ def _validate_date(date: typ.Optional[str], pin_date: bool) -> typ.Optional[dt.d sys.exit(1) -def _validate_release_tag(release: typ.Optional[str]) -> None: - if release is None: +def _validate_release_tag(tag: typ.Optional[str]) -> None: + if tag is None: return - if release in VALID_RELEASE_VALUES: + if tag in VALID_RELEASE_TAG_VALUES: return - logger.error(f"Invalid argument --release={release}") - logger.error(f"Valid arguments are: {', '.join(VALID_RELEASE_VALUES)}") + logger.error(f"Invalid argument --release={tag}") + logger.error(f"Valid arguments are: {', '.join(VALID_RELEASE_TAG_VALUES)}") sys.exit(1) @@ -124,7 +124,7 @@ def cli(verbose: int = 0) -> None: metavar="", help=( f"Override release name of current_version. Valid options are: " - f"{', '.join(VALID_RELEASE_VALUES)}." + f"{', '.join(VALID_RELEASE_TAG_VALUES)}." ), ) @click.option("--major" , is_flag=True, default=False, help="Increment major component.") @@ -154,13 +154,14 @@ def test( _configure_logging(verbose=max(_VERBOSE, verbose)) raw_pattern = pattern # use internal naming convention - _validate_release_tag(release) + tag = release # use internal naming convention + _validate_release_tag(tag) _date = _validate_date(date, pin_date) - new_version = _incr( + new_version = incr_dispatch( old_version, raw_pattern=raw_pattern, - release=release, + tag=tag, major=major, minor=minor, patch=patch, @@ -337,11 +338,11 @@ def _print_diff(cfg: config.Config, new_version: str) -> None: sys.exit(1) -def _incr( +def incr_dispatch( old_version: str, raw_pattern: str, *, - release : str = None, + tag : str = None, major : bool = False, minor : bool = False, patch : bool = False, @@ -351,6 +352,7 @@ def _incr( ) -> typ.Optional[str]: v1_parts = list(v1patterns.PART_PATTERNS) + list(v1patterns.FULL_PART_FORMATS) has_v1_part = any("{" + part + "}" in raw_pattern for part in v1_parts) + if _VERBOSE: if has_v1_part: pattern = v1patterns.compile_pattern(raw_pattern) @@ -364,7 +366,7 @@ def _incr( return v1version.incr( old_version, raw_pattern=raw_pattern, - release=release, + tag=tag, major=major, minor=minor, patch=patch, @@ -376,7 +378,7 @@ def _incr( return v2version.incr( old_version, raw_pattern=raw_pattern, - release=release, + tag=tag, major=major, minor=minor, patch=patch, @@ -501,7 +503,7 @@ def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: metavar="", help=( f"Override release name of current_version. Valid options are: " - f"{', '.join(VALID_RELEASE_VALUES)}." + f"{', '.join(VALID_RELEASE_TAG_VALUES)}." ), ) @click.option( @@ -542,7 +544,8 @@ def bump( verbose = max(_VERBOSE, verbose) _configure_logging(verbose) - _validate_release_tag(release) + tag = release # use internal naming convention + _validate_release_tag(tag) _date = _validate_date(date, pin_date) _, cfg = config.init(project_path=".") @@ -554,10 +557,10 @@ def bump( cfg = _update_cfg_from_vcs(cfg, fetch) old_version = cfg.current_version - new_version = _incr( + new_version = incr_dispatch( old_version, raw_pattern=cfg.version_pattern, - release=release, + tag=tag, major=major, minor=minor, patch=patch, diff --git a/src/pycalver/v1version.py b/src/pycalver/v1version.py index 1cecbd2..dac43a0 100644 --- a/src/pycalver/v1version.py +++ b/src/pycalver/v1version.py @@ -374,7 +374,7 @@ def incr( old_version: str, raw_pattern: str = "{pycalver}", *, - release : typ.Optional[str] = None, + tag : typ.Optional[str] = None, major : bool = False, minor : bool = False, patch : bool = False, @@ -410,8 +410,8 @@ def incr( cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1) if release_num: raise NotImplementedError("--release-num not supported for old style patterns") - if release: - cur_vinfo = cur_vinfo._replace(tag=release) + if tag: + cur_vinfo = cur_vinfo._replace(tag=tag) new_version = format_version(cur_vinfo, raw_pattern) if new_version == old_version: diff --git a/src/pycalver/v2version.py b/src/pycalver/v2version.py index 3af15e1..fc7e88e 100644 --- a/src/pycalver/v2version.py +++ b/src/pycalver/v2version.py @@ -638,7 +638,7 @@ def incr( old_version: str, raw_pattern: str = "vYYYY0M.BUILD[-RELEASE]", *, - release : typ.Optional[str] = None, + tag : typ.Optional[str] = None, major : bool = False, minor : bool = False, patch : bool = False, diff --git a/test/test_version.py b/test/test_version.py index 4025ec4..7dadb50 100644 --- a/test/test_version.py +++ b/test/test_version.py @@ -22,20 +22,20 @@ def test_bump_beta(): cur_version = "v201712.0001-beta" assert cur_version < v1version.incr(cur_version) assert v1version.incr(cur_version).endswith("-beta") - assert v1version.incr(cur_version, release="alpha").endswith("-alpha") - assert v1version.incr(cur_version, release="final").endswith("0002") + assert v1version.incr(cur_version, tag="alpha").endswith("-alpha") + assert v1version.incr(cur_version, tag="final").endswith("0002") def test_bump_final(): cur_version = "v201712.0001" assert cur_version < v1version.incr(cur_version) assert v1version.incr(cur_version).endswith(".0002") - assert v1version.incr(cur_version, release="alpha").endswith("-alpha") + assert v1version.incr(cur_version, tag="alpha").endswith("-alpha") - assert v1version.incr(cur_version, release="final").endswith(".0002") + assert v1version.incr(cur_version, tag="final").endswith(".0002") pre_version = cur_version + "-beta" - assert v1version.incr(pre_version, release="final").endswith(".0002") + assert v1version.incr(pre_version, tag="final").endswith(".0002") def test_bump_future(): @@ -56,7 +56,7 @@ def test_bump_random(monkeypatch): for _ in range(1000): cur_date += dt.timedelta(days=int((1 + random.random()) ** 10)) new_version = v1version.incr( - cur_version, release=random.choice([None, "alpha", "beta", "rc", "final", "post"]) + cur_version, tag=random.choice([None, "alpha", "beta", "rc", "final", "post"]) ) assert cur_version < new_version cur_version = new_version From f92c7347aeb4a3f6558577ea2f2d37484442581a Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sat, 3 Oct 2020 23:44:38 +0000 Subject: [PATCH 54/98] doc updates --- pylint-ignore.md | 19 ++++++++++++++++++- src/pycalver/v1cli.py | 2 ++ src/pycalver/v2version.py | 2 -- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/pylint-ignore.md b/pylint-ignore.md index d4e5c53..0857d3c 100644 --- a/pylint-ignore.md +++ b/pylint-ignore.md @@ -23,12 +23,29 @@ The recommended approach to using `pylint-ignore` is: # Overview - - [W0511: fixme (5x)](#w0511-fixme) + - [W0511: fixme (4x)](#w0511-fixme) - [W0703: broad-except (1x)](#w0703-broad-except) # W0511: fixme +## File src/pycalver/v1cli.py - Line 29 - W0511 (fixme) + +- `message: TODO (mb 2020-10-03): This codepath is not tested since switch to` +- `author : Manuel Barkhau ` +- `date : 2020-10-03T23:40:01` + +``` + 23: def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.Config: + ... + 27: return cfg + 28: +> 29: # TODO (mb 2020-10-03): This codepath is not tested since switch to + 30: # v2 as default version_pattern. + 31: version_tags.sort(reverse=True) +``` + + ## File src/pycalver/vcs.py - Line 80 - W0511 (fixme) - `message: TODO (mb 2018-11-15): Detect encoding of output? Use chardet?` diff --git a/src/pycalver/v1cli.py b/src/pycalver/v1cli.py index 47c3c2d..a0c1c21 100755 --- a/src/pycalver/v1cli.py +++ b/src/pycalver/v1cli.py @@ -26,6 +26,8 @@ def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.C logger.debug("no vcs tags found") return cfg + # TODO (mb 2020-10-03): This codepath is not tested since switch to + # v2 as default version_pattern. version_tags.sort(reverse=True) _debug_tags = ", ".join(version_tags[:3]) logger.debug(f"found tags: {_debug_tags} ... ({len(version_tags)} in total)") diff --git a/src/pycalver/v2version.py b/src/pycalver/v2version.py index fc7e88e..d0773eb 100644 --- a/src/pycalver/v2version.py +++ b/src/pycalver/v2version.py @@ -414,7 +414,6 @@ def _format_segment_tree( segtree : SegmentTree, part_values: PartValues, ) -> FormatedSeg: - # print("??>>>", seg_tree) # NOTE (mb 2020-10-02): starting from the right, if there is any non-zero # part, all further parts going left will be used. In other words, a part # is only omitted, if all parts to the right of it were also omitted. @@ -432,7 +431,6 @@ def _format_segment_tree( is_zero = is_zero and formatted_seg.is_zero result_parts.append(formatted_seg.result) - # print("<<<<", is_zero, result_parts) result = "" if is_zero else "".join(result_parts) return FormatedSeg(False, is_zero, result) From d23689634c37e2f5aafc39eca97335d8e2e08acf Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 4 Oct 2020 11:28:53 +0000 Subject: [PATCH 55/98] implement INC0 and INC1 parts --- CHANGELOG.md | 1 + README.md | 8 +++++--- setup.cfg | 5 +++-- src/pycalver/v2patterns.py | 11 ++++++++++- src/pycalver/v2version.py | 20 +++++++++++++++++--- src/pycalver/version.py | 9 ++++++--- test/test_cli.py | 32 ++++++++++++++++++++------------ 7 files changed, 62 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e442bee..86c0543 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Better support for week numbering. - Better support for optional parts. - New: Start `BUILD` parts at `1000` to avoid leading zero truncation. + - New: Add `INC0` (0-based) and `INC1` (1-based) parts that do auto increment and rollover. - New: `MAJOR`/`MINOR`/`PATCH`/`INC` will roll over when a date part changes to their left. - New gitlab #2: Added `grep` subcommand to find and debug patterns. - New: Added better error messages to debug regular expressions. diff --git a/README.md b/README.md index 14f0383..1904398 100644 --- a/README.md +++ b/README.md @@ -229,14 +229,16 @@ These patterns are closely based on https://calver.org/ | `0D` | 01, 02, 03..31 | `%d` | | `JJJ` | 1,2,3..366 | `int(%j)` | | `00J` | 001, 002..366 | `%j` | -| `BUILD` | 0011, 1001, 1002, .. | build number (lexid) | -| `BLD` | 11, 1001, 1002, .. | zero truncated `BUILD` | | `MAJOR` | 0..9, 10..99, 100.. | `--major` | | `MINOR` | 0..9, 10..99, 100.. | `-m/--minor` | | `PATCH` | 0..9, 10..99, 100.. | `-p/--patch` | -| `NUM` | 0, 1, 2... | `-r/--release-num` | +| `INC0` | 0, 1, 2... | | +| `INC1` | 1, 2... | | +| `BUILD` | 0011, 1001, 1002, .. | build number (lexid) | +| `BLD` | 11, 1001, 1002, .. | zero truncated `BUILD` | | `RELEASE` | alpha, beta, rc | `--release=` | | `PYTAG` | a, b, rc | `--release=` | +| `NUM` | 0, 1, 2... | `-r/--release-num` | ### Week Numbering diff --git a/setup.cfg b/setup.cfg index 8a41aca..37d1964 100644 --- a/setup.cfg +++ b/setup.cfg @@ -126,7 +126,8 @@ README.md = score = no reports = no -jobs = 4 +# pylint spams the same message multiple times if jobs > 1 +jobs = 1 # Set the output format. Available formats are text, parseable, colorized, # msvs (visual studio) and html. You can also give a reporter class, eg @@ -134,7 +135,7 @@ jobs = 4 output-format = colorized # Maximum number of locals for function / method body -max-locals = 21 +max-locals = 25 # Maximum number of arguments for function / method max-args = 12 diff --git a/src/pycalver/v2patterns.py b/src/pycalver/v2patterns.py index 3336dce..3d85c9f 100644 --- a/src/pycalver/v2patterns.py +++ b/src/pycalver/v2patterns.py @@ -86,6 +86,8 @@ PART_PATTERNS = { 'RELEASE': r"preview|final|alpha|beta|post|rc", 'PYTAG' : r"post|rc|a|b", 'NUM' : r"[0-9]+", + 'INC0' : r"[0-9]+", + 'INC1' : r"[1-9][0-9]*", } @@ -111,6 +113,8 @@ PATTERN_PART_FIELDS = { 'RELEASE': 'tag', 'PYTAG' : 'pytag', 'NUM' : 'num', + 'INC0' : 'inc0', + 'INC1' : 'inc1', 'WW' : 'week_w', '0W' : 'week_w', 'UU' : 'week_u', @@ -183,7 +187,10 @@ def _fmt_0v(week_v: FieldValue) -> str: return "{0:02}".format(int(week_v)) -PART_FORMATS: typ.Dict[str, typ.Callable[[FieldValue], str]] = { +FormatterFunc = typ.Callable[[FieldValue], str] + + +PART_FORMATS: typ.Dict[str, FormatterFunc] = { 'YYYY' : _fmt_num, 'YY' : _fmt_yy, '0Y' : _fmt_0y, @@ -205,6 +212,8 @@ PART_FORMATS: typ.Dict[str, typ.Callable[[FieldValue], str]] = { 'RELEASE': _fmt_num, 'PYTAG' : _fmt_num, 'NUM' : _fmt_num, + 'INC0' : _fmt_num, + 'INC1' : _fmt_num, 'WW' : _fmt_num, '0W' : _fmt_0w, 'UU' : _fmt_num, diff --git a/src/pycalver/v2version.py b/src/pycalver/v2version.py index d0773eb..57f966a 100644 --- a/src/pycalver/v2version.py +++ b/src/pycalver/v2version.py @@ -201,6 +201,8 @@ def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: patch = int(fvals.get('patch') or 0) num = int(fvals.get('num' ) or 0) bid = fvals['bid'] if 'bid' in fvals else "1000" + inc0 = int(fvals.get('inc0') or 0) + inc1 = int(fvals.get('inc1') or 1) vinfo = version.V2VersionInfo( year_y=year_y, @@ -219,6 +221,8 @@ def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: bid=bid, tag=tag, pytag=pytag, + inc0=inc0, + inc1=inc1, ) return vinfo @@ -567,9 +571,9 @@ def _iter_reset_field_items( # Any field to the left of another can reset all to the right has_reset = False for field in fields: - zero_val = version.V2_FIELD_ZERO_VALUES.get(field) - if has_reset and zero_val is not None: - yield field, zero_val + initial_val = version.V2_FIELD_INITIAL_VALUES.get(field) + if has_reset and initial_val is not None: + yield field, initial_val elif getattr(old_vinfo, field) != getattr(cur_vinfo, field): has_reset = True @@ -598,6 +602,16 @@ def _incr_numeric( cur_vinfo = cur_vinfo._replace(bid=lexid.next_id(cur_vinfo.bid)) + if 'inc0' in reset_fields: + cur_vinfo = cur_vinfo._replace(inc0=0) + else: + cur_vinfo = cur_vinfo._replace(inc0=cur_vinfo.inc0 + 1) + + if 'inc1' in reset_fields: + cur_vinfo = cur_vinfo._replace(inc1=1) + else: + cur_vinfo = cur_vinfo._replace(inc1=cur_vinfo.inc1 + 1) + if major and 'major' not in reset_fields: cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0, patch=0) if minor and 'minor' not in reset_fields: diff --git a/src/pycalver/version.py b/src/pycalver/version.py index fb42532..afa4a20 100644 --- a/src/pycalver/version.py +++ b/src/pycalver/version.py @@ -73,6 +73,8 @@ class V2VersionInfo(typ.NamedTuple): bid : str tag : str pytag : str + inc0 : int + inc1 : int # The test suite may replace this. @@ -116,18 +118,19 @@ PART_ZERO_VALUES = { 'RELEASE': "final", 'PYTAG' : "", 'NUM' : "0", - 'INC' : "0", + 'INC0' : "0", } -V2_FIELD_ZERO_VALUES = { +V2_FIELD_INITIAL_VALUES = { 'major': "0", 'minor': "0", 'patch': "0", 'tag' : "final", 'pytag': "", 'num' : "0", - 'inc' : "0", + 'inc0' : "0", + 'inc1' : "1", } diff --git a/test/test_cli.py b/test/test_cli.py index a9b15be..5f361db 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -768,23 +768,31 @@ def test_multimatch_file_patterns(runner): assert "[aka. 202010.1003b0 !]" in readme_text -def _kwargs(month, minor): - return {'date': dt.date(2020, month, 1), 'minor': minor} +def _kwargs(year, month, minor=False): + return {'date': dt.date(year, month, 1), 'minor': minor} ROLLOVER_TEST_CASES = [ # v1 cases - ["{year}.{month}.{MINOR}", "2020.10.3", "2020.10.4", _kwargs(10, True)], - ["{year}.{month}.{MINOR}", "2020.10.3", None, _kwargs(10, False)], - ["{year}.{month}.{MINOR}", "2020.10.3", "2020.11.4", _kwargs(11, True)], - ["{year}.{month}.{MINOR}", "2020.10.3", "2020.11.3", _kwargs(11, False)], + ["{year}.{month}.{MINOR}", "2020.10.3", "2020.10.4", _kwargs(2020, 10, True)], + ["{year}.{month}.{MINOR}", "2020.10.3", None, _kwargs(2020, 10, False)], + ["{year}.{month}.{MINOR}", "2020.10.3", "2020.11.4", _kwargs(2020, 11, True)], + ["{year}.{month}.{MINOR}", "2020.10.3", "2020.11.3", _kwargs(2020, 11, False)], # v2 cases - ["YYYY.MM.MINOR" , "2020.10.3", "2020.10.4", _kwargs(10, True)], - ["YYYY.MM.MINOR" , "2020.10.3", None, _kwargs(10, False)], - ["YYYY.MM.MINOR" , "2020.10.3", "2020.11.0", _kwargs(11, True)], - ["YYYY.MM.MINOR" , "2020.10.3", "2020.11.0", _kwargs(11, False)], - ["YYYY.MM[.MINOR]", "2020.10.3", "2020.10.4", _kwargs(10, True)], - ["YYYY.MM[.MINOR]", "2020.10.3", "2020.11", _kwargs(11, False)], + ["YYYY.MM.MINOR" , "2020.10.3", "2020.10.4", _kwargs(2020, 10, True)], + ["YYYY.MM.MINOR" , "2020.10.3", None, _kwargs(2020, 10, False)], + ["YYYY.MM.MINOR" , "2020.10.3", "2020.11.0", _kwargs(2020, 11, True)], + ["YYYY.MM.MINOR" , "2020.10.3", "2020.11.0", _kwargs(2020, 11, False)], + ["YYYY.MM[.MINOR]", "2020.10.3", "2020.10.4", _kwargs(2020, 10, True)], + ["YYYY.MM[.MINOR]", "2020.10.3", "2020.11", _kwargs(2020, 11, False)], + ["YYYY.MM.MINOR" , "2020.10.3", "2021.10.0", _kwargs(2021, 10, False)], + # incr0/incr1 part + ["YYYY.MM.INC0", "2020.10.3", "2020.10.4", _kwargs(2020, 10)], + ["YYYY.MM.INC0", "2020.10.3", "2020.11.0", _kwargs(2020, 11)], + ["YYYY.MM.INC0", "2020.10.3", "2021.10.0", _kwargs(2021, 10)], + ["YYYY.MM.INC1", "2020.10.3", "2020.10.4", _kwargs(2020, 10)], + ["YYYY.MM.INC1", "2020.10.3", "2020.11.1", _kwargs(2020, 11)], + ["YYYY.MM.INC1", "2020.10.3", "2021.10.1", _kwargs(2021, 10)], ] From 6b1a3e45d36be7e9a5a7b48715bc2b78e76b0a9d Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 4 Oct 2020 12:10:38 +0000 Subject: [PATCH 56/98] improve test coverage --- pylint-ignore.md | 96 --------------------------------------- src/pycalver/__main__.py | 36 +++++++-------- src/pycalver/v1cli.py | 2 - src/pycalver/v2version.py | 1 + src/pycalver/vcs.py | 4 +- test/test_cli.py | 96 ++++++++++++++++++++++++--------------- test/test_config.py | 12 +++-- test/test_version.py | 33 ++++++++++++++ 8 files changed, 118 insertions(+), 162 deletions(-) diff --git a/pylint-ignore.md b/pylint-ignore.md index 0857d3c..ac42083 100644 --- a/pylint-ignore.md +++ b/pylint-ignore.md @@ -20,99 +20,3 @@ The recommended approach to using `pylint-ignore` is: 3. If a message is a false positive, add a comment of this form to your code: `# pylint:disable= ; explain why this is a false positive` - -# Overview - - - [W0511: fixme (4x)](#w0511-fixme) - - [W0703: broad-except (1x)](#w0703-broad-except) - - -# W0511: fixme - -## File src/pycalver/v1cli.py - Line 29 - W0511 (fixme) - -- `message: TODO (mb 2020-10-03): This codepath is not tested since switch to` -- `author : Manuel Barkhau ` -- `date : 2020-10-03T23:40:01` - -``` - 23: def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.Config: - ... - 27: return cfg - 28: -> 29: # TODO (mb 2020-10-03): This codepath is not tested since switch to - 30: # v2 as default version_pattern. - 31: version_tags.sort(reverse=True) -``` - - -## File src/pycalver/vcs.py - Line 80 - W0511 (fixme) - -- `message: TODO (mb 2018-11-15): Detect encoding of output? Use chardet?` -- `author : Manuel Barkhau ` -- `date : 2020-09-18T17:24:49` - -``` - 69: def __call__(self, cmd_name: str, env: Env = None, **kwargs: str) -> str: - ... - 78: output_data: bytes = sp.check_output(cmd_parts, env=env, stderr=sp.STDOUT) - 79: -> 80: # TODO (mb 2018-11-15): Detect encoding of output? Use chardet? - 81: _encoding = "utf-8" - 82: return output_data.decode(_encoding) -``` - - -## File test/test_config.py - Line 170 - W0511 (fixme) - -- `message: TODO (mb 2020-09-18):` -- `author : Manuel Barkhau ` -- `date : 2020-09-18T19:04:06` - -``` - 156: def test_parse_v2_cfg(): - ... - 168: assert "setup.cfg" in cfg.file_patterns - 169: -> 170: # TODO (mb 2020-09-18): - 171: # raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) - 172: # assert raw_patterns_by_filepath["setup.py" ] == ["vYYYY0M.BUILD[-RELEASE]", "YYYY0M.BLD[PYTAGNUM]"] -``` - - -## File src/pycalver/__main__.py - Line 421 - W0511 (fixme) - -- `message: TODO (mb 2020-09-18): Investigate error messages` -- `author : Manuel Barkhau ` -- `date : 2020-09-19T16:24:10` - -``` - 391: def _bump( - ... - 419: sys.exit(1) - 420: except Exception as ex: -> 421: # TODO (mb 2020-09-18): Investigate error messages - 422: logger.error(str(ex)) - 423: sys.exit(1) -``` - - -# W0703: broad-except - -## File src/pycalver/__main__.py - Line 420 - W0703 (broad-except) - -- `message: Catching too general exception Exception` -- `author : Manuel Barkhau ` -- `date : 2020-09-05T14:30:17` - -``` - 391: def _bump( - ... - 418: logger.error(str(ex)) - 419: sys.exit(1) -> 420: except Exception as ex: - 421: # TODO (mb 2020-09-18): Investigate error messages - 422: logger.error(str(ex)) -``` - - diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 1c45e6b..c2afc15 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -303,19 +303,24 @@ def show(verbose: int = 0, fetch: bool = True) -> None: click.echo(f"PEP440 : {cfg.pep440_version}") +def _colored_diff_lines(diff: str) -> typ.Iterable[str]: + for line in diff.splitlines(): + if line.startswith("+++") or line.startswith("---"): + yield line + elif line.startswith("+"): + yield "\u001b[32m" + line + "\u001b[0m" + elif line.startswith("-"): + yield "\u001b[31m" + line + "\u001b[0m" + elif line.startswith("@"): + yield "\u001b[36m" + line + "\u001b[0m" + else: + yield line + + def _print_diff_str(diff: str) -> None: + colored_diff = "\n".join(_colored_diff_lines(diff)) if sys.stdout.isatty(): - for line in diff.splitlines(): - if line.startswith("+++") or line.startswith("---"): - click.echo(line) - elif line.startswith("+"): - click.echo("\u001b[32m" + line + "\u001b[0m") - elif line.startswith("-"): - click.echo("\u001b[31m" + line + "\u001b[0m") - elif line.startswith("@"): - click.echo("\u001b[36m" + line + "\u001b[0m") - else: - click.echo(line) + click.echo(colored_diff) else: click.echo(diff) @@ -331,11 +336,6 @@ def _print_diff(cfg: config.Config, new_version: str) -> None: except rewrite.NoPatternMatch as ex: logger.error(str(ex)) sys.exit(1) - except Exception as ex: - # pylint:disable=broad-except; Mostly we expect IOError here, but - # could be other things and there's no option to recover anyway. - logger.error(str(ex), exc_info=True) - sys.exit(1) def incr_dispatch( @@ -417,10 +417,6 @@ def _bump( except rewrite.NoPatternMatch as ex: logger.error(str(ex)) sys.exit(1) - except Exception as ex: - # TODO (mb 2020-09-18): Investigate error messages - logger.error(str(ex)) - sys.exit(1) if vcs_api: vcs.commit(cfg, vcs_api, filepaths, new_version, commit_message) diff --git a/src/pycalver/v1cli.py b/src/pycalver/v1cli.py index a0c1c21..47c3c2d 100755 --- a/src/pycalver/v1cli.py +++ b/src/pycalver/v1cli.py @@ -26,8 +26,6 @@ def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.C logger.debug("no vcs tags found") return cfg - # TODO (mb 2020-10-03): This codepath is not tested since switch to - # v2 as default version_pattern. version_tags.sort(reverse=True) _debug_tags = ", ".join(version_tags[:3]) logger.debug(f"found tags: {_debug_tags} ... ({len(version_tags)} in total)") diff --git a/src/pycalver/v2version.py b/src/pycalver/v2version.py index 57f966a..dabcd53 100644 --- a/src/pycalver/v2version.py +++ b/src/pycalver/v2version.py @@ -141,6 +141,7 @@ def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: >>> (vinfo.year_y, vinfo.week_w, vinfo.year_y, vinfo.week_u,vinfo.year_g, vinfo.week_v) (2021, 0, 2021, 1, 2020, 53) """ + # pylint:disable=dangerous-default-value; We don't mutate args, mypy would fail if we did. for key in field_values: assert key in VALID_FIELD_KEYS, key diff --git a/src/pycalver/vcs.py b/src/pycalver/vcs.py index 93de8da..918778e 100644 --- a/src/pycalver/vcs.py +++ b/src/pycalver/vcs.py @@ -77,9 +77,7 @@ class VCSAPI: cmd_parts = shlex.split(cmd_str) output_data: bytes = sp.check_output(cmd_parts, env=env, stderr=sp.STDOUT) - # TODO (mb 2018-11-15): Detect encoding of output? Use chardet? - _encoding = "utf-8" - return output_data.decode(_encoding) + return output_data.decode("utf-8") @property def is_usable(self) -> bool: diff --git a/test/test_cli.py b/test/test_cli.py index 5f361db..99d79f5 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -241,10 +241,8 @@ def test_nocfg(runner, caplog): _add_project_files("README.md") result = runner.invoke(cli, ['show', "-vv"]) assert result.exit_code == 1 - assert any( - bool("Could not parse configuration. Perhaps try 'pycalver init'." in r.message) - for r in caplog.records - ) + expected_msg = "Could not parse configuration. Perhaps try 'pycalver init'." + assert any(expected_msg in r.message for r in caplog.records) def test_novcs_nocfg_init(runner, caplog): @@ -343,33 +341,46 @@ def _vcs_init(vcs, files=("README.md",)): shell(f"{vcs}", "commit", "-m", "initial commit") -def test_git_init(runner): +DEFAULT_VERSION_PATTERNS = [ + '"{pycalver}"', + '"vYYYY0M.BUILD[-RELEASE]"', +] + + +@pytest.mark.parametrize("version_pattern", DEFAULT_VERSION_PATTERNS) +def test_git_init(runner, version_pattern): _add_project_files("README.md") _vcs_init("git") result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 0 + _update_config_val("pycalver.toml", version_pattern=version_pattern) + result = runner.invoke(cli, ['show']) assert result.exit_code == 0 assert f"Current Version: {config._initial_version()}\n" in result.output assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output -def test_hg_init(runner): +@pytest.mark.parametrize("version_pattern", DEFAULT_VERSION_PATTERNS) +def test_hg_init(runner, version_pattern): _add_project_files("README.md") _vcs_init("hg") result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 0 + _update_config_val("pycalver.toml", version_pattern=version_pattern) + result = runner.invoke(cli, ['show']) assert result.exit_code == 0 assert f"Current Version: {config._initial_version()}\n" in result.output assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output -def test_git_tag_eval(runner): +@pytest.mark.parametrize("version_pattern", DEFAULT_VERSION_PATTERNS) +def test_v1_git_tag_eval(runner, version_pattern): _add_project_files("README.md") _vcs_init("git") @@ -377,6 +388,9 @@ def test_git_tag_eval(runner): # we set in the vcs, which should take precedence. result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 0 + + _update_config_val("pycalver.toml", version_pattern=version_pattern) + initial_version = config._initial_version() tag_version = initial_version.replace(".1001-alpha", ".1123-beta") tag_version_pep440 = tag_version[1:7] + ".1123b0" @@ -389,7 +403,8 @@ def test_git_tag_eval(runner): assert f"PEP440 : {tag_version_pep440}\n" in result.output -def test_hg_tag_eval(runner): +@pytest.mark.parametrize("version_pattern", DEFAULT_VERSION_PATTERNS) +def test_hg_tag_eval(runner, version_pattern): _add_project_files("README.md") _vcs_init("hg") @@ -397,6 +412,9 @@ def test_hg_tag_eval(runner): # we set in the vcs, which should take precedence. result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 0 + + _update_config_val("pycalver.toml", version_pattern=version_pattern) + initial_version = config._initial_version() tag_version = initial_version.replace(".1001-alpha", ".1123-beta") tag_version_pep440 = tag_version[1:7] + ".1123b0" @@ -409,12 +427,15 @@ def test_hg_tag_eval(runner): assert f"PEP440 : {tag_version_pep440}\n" in result.output -def test_novcs_bump(runner): +@pytest.mark.parametrize("version_pattern", DEFAULT_VERSION_PATTERNS) +def test_novcs_bump(runner, version_pattern): _add_project_files("README.md") result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 0 + _update_config_val("pycalver.toml", version_pattern=version_pattern) + result = runner.invoke(cli, ['bump', "-vv"]) assert result.exit_code == 0 @@ -434,13 +455,16 @@ def test_novcs_bump(runner): assert calver[1:] + ".1003b0 !]\n" in content -def test_git_bump(runner): +@pytest.mark.parametrize("version_pattern", DEFAULT_VERSION_PATTERNS) +def test_git_bump(runner, version_pattern): _add_project_files("README.md") _vcs_init("git") result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 0 + _update_config_val("pycalver.toml", version_pattern=version_pattern) + shell("git", "add", "pycalver.toml") shell("git", "commit", "-m", "initial commit") @@ -454,13 +478,16 @@ def test_git_bump(runner): assert calver + ".1002-alpha !\n" in content -def test_hg_bump(runner): +@pytest.mark.parametrize("version_pattern", DEFAULT_VERSION_PATTERNS) +def test_hg_bump(runner, version_pattern): _add_project_files("README.md") _vcs_init("hg") result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 0 + _update_config_val("pycalver.toml", version_pattern=version_pattern) + shell("hg", "add", "pycalver.toml") shell("hg", "commit", "-m", "initial commit") @@ -533,12 +560,21 @@ setup.cfg = """ -def test_v1_bump_semver_warning(runner, caplog): +DEFAULT_SEMVER_PATTERNS = [ + '"{semver}"', + '"MAJOR.MINOR.PATCH"', +] + + +@pytest.mark.parametrize("version_pattern", DEFAULT_SEMVER_PATTERNS) +def test_v1_bump_semver_warning(runner, caplog, version_pattern): _add_project_files("README.md") with pl.Path("setup.cfg").open(mode="w") as fobj: fobj.write(SETUP_CFG_SEMVER_FIXTURE) + _update_config_val("setup.cfg", version_pattern=version_pattern) + _vcs_init("hg", files=["README.md", "setup.cfg"]) result = runner.invoke(cli, ['bump', "-vv", "-n", "--dry"]) @@ -551,12 +587,15 @@ def test_v1_bump_semver_warning(runner, caplog): assert result.exit_code == 0 -def test_v1_bump_semver_diff(runner, caplog): +@pytest.mark.parametrize("version_pattern", DEFAULT_SEMVER_PATTERNS) +def test_v1_bump_semver_diff(runner, caplog, version_pattern): _add_project_files("README.md") with pl.Path("setup.cfg").open(mode="w") as fobj: fobj.write(SETUP_CFG_SEMVER_FIXTURE) + _update_config_val("setup.cfg", version_pattern=version_pattern) + _vcs_init("hg", files=["README.md", "setup.cfg"]) cases = [("--major", "1.0.0"), ("--minor", "0.2.0"), ("--patch", "0.1.1")] @@ -573,38 +612,23 @@ def test_v1_bump_semver_diff(runner, caplog): assert f"+current_version = \"{expected}\"" in out_lines -def test_v1_get_diff(runner): +@pytest.mark.parametrize("version_pattern", DEFAULT_VERSION_PATTERNS) +def test_get_diff(runner, version_pattern): _add_project_files("README.md", "setup.cfg") result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 0 - _update_config_val("setup.cfg", version_pattern='"{pycalver}"') + _update_config_val("setup.cfg", version_pattern=version_pattern) _, cfg = config.init() new_version = "v202010.1003-beta" - diff_str = v1cli.get_diff(cfg, new_version) - diff_lines = set(diff_str.splitlines()) - assert "- Hello World v201701.1002-alpha !" in diff_lines - assert "- [aka. 201701.1002a0 !]" in diff_lines - assert "+ Hello World v202010.1003-beta !" in diff_lines - assert "+ [aka. 202010.1003b0 !]" in diff_lines + if cfg.is_new_pattern: + diff_str = v2cli.get_diff(cfg, new_version) + else: + diff_str = v1cli.get_diff(cfg, new_version) - assert '-current_version = "v202010.1001-alpha"' in diff_lines - assert '+current_version = "v202010.1003-beta"' in diff_lines - - -def test_v2_get_diff(runner): - _add_project_files("README.md", "setup.cfg") - result = runner.invoke(cli, ['init', "-vv"]) - assert result.exit_code == 0 - - _update_config_val("setup.cfg", version_pattern='"vYYYY0M.BUILD[-RELEASE]"') - - _, cfg = config.init() - new_version = "v202010.1003-beta" - diff_str = v2cli.get_diff(cfg, new_version) - diff_lines = set(diff_str.splitlines()) + diff_lines = set(diff_str.splitlines()) assert "- Hello World v201701.1002-alpha !" in diff_lines assert "- [aka. 201701.1002a0 !]" in diff_lines diff --git a/test/test_config.py b/test/test_config.py index 09807aa..7d66cbb 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -167,11 +167,13 @@ def test_parse_v2_cfg(): assert "setup.py" in cfg.file_patterns assert "setup.cfg" in cfg.file_patterns - # TODO (mb 2020-09-18): - # raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) - # assert raw_patterns_by_filepath["setup.py" ] == ["vYYYY0M.BUILD[-RELEASE]", "YYYY0M.BLD[PYTAGNUM]"] - # assert raw_patterns_by_filepath["setup.cfg" ] == ['current_version = "vYYYY0M.BUILD[-RELEASE]"'] - # assert raw_patterns_by_filepath["src/project/*.py"] == ['Copyright (c) 2018-YYYY"'] + raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) + assert raw_patterns_by_filepath["setup.py"] == [ + "vYYYY0M.BUILD[-RELEASE]", + "YYYY0M.BLD[PYTAGNUM]", + ] + assert raw_patterns_by_filepath["setup.cfg"] == ['current_version = "vYYYY0M.BUILD[-RELEASE]"'] + assert raw_patterns_by_filepath["src/project/*.py"] == ["Copyright (c) 2018-YYYY"] def test_parse_default_toml(): diff --git a/test/test_version.py b/test/test_version.py index 7dadb50..18672c3 100644 --- a/test/test_version.py +++ b/test/test_version.py @@ -239,3 +239,36 @@ def test_v2_format_version(): result = v2version.format_version(vinfo, raw_pattern='__version__ = "YYYY.BLD"') assert result == '__version__ = "2007.33"' + + +WEEK_PATTERN_TEXT_CASES = [ + ("YYYYWW.PATCH", True), + ("YYYYUU.PATCH", True), + ("GGGGVV.PATCH", True), + ("YYWW.PATCH" , True), + ("YYUU.PATCH" , True), + ("GGVV.PATCH" , True), + ("0YWW.PATCH" , True), + ("0YUU.PATCH" , True), + ("0GVV.PATCH" , True), + ("0Y0W.PATCH" , True), + ("0Y0U.PATCH" , True), + ("0G0V.PATCH" , True), + ("GGGGWW.PATCH", False), + ("GGGGUU.PATCH", False), + ("YYYYVV.PATCH", False), + ("GGWW.PATCH" , False), + ("GGUU.PATCH" , False), + ("YYVV.PATCH" , False), + ("0GWW.PATCH" , False), + ("0GUU.PATCH" , False), + ("0YVV.PATCH" , False), + ("0G0W.PATCH" , False), + ("0G0U.PATCH" , False), + ("0Y0V.PATCH" , False), +] + + +@pytest.mark.parametrize("pattern, expected", WEEK_PATTERN_TEXT_CASES) +def test_is_valid_week_pattern(pattern, expected): + assert v2version.is_valid_week_pattern(pattern) == expected From dbd23ef1495c5964cf3d7e039cfcd0c57e10e25f Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 4 Oct 2020 12:12:37 +0000 Subject: [PATCH 57/98] bump v202010.1039-beta -> v202010.1040-beta --- README.md | 6 +++--- bootstrapit.sh | 2 +- setup.cfg | 2 +- setup.py | 2 +- src/pycalver/__init__.py | 2 +- src/pycalver/__main__.py | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1904398..0604294 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Project/Repo: [![MIT License][license_img]][license_ref] [![Supported Python Versions][pyversions_img]][pyversions_ref] -[![PyCalVer v202010.1039-beta][version_img]][version_ref] +[![PyCalVer v202010.1040-beta][version_img]][version_ref] [![PyPI Releases][pypi_img]][pypi_ref] [![PyPI Downloads][downloads_img]][downloads_ref] @@ -71,7 +71,7 @@ The fastest way to setup a project is to use `pycalver init`. $ pip install pycalver ... Installing collected packages: click pathlib2 typing toml pycalver -Successfully installed pycalver-202010.1039b0 +Successfully installed pycalver-202010.1040b0 $ cd myproject ~/myproject/ @@ -1063,7 +1063,7 @@ artifact of a package, eg. a `.whl` file. [downloads_img]: https://pepy.tech/badge/pycalver/month [downloads_ref]: https://pepy.tech/project/pycalver -[version_img]: https://img.shields.io/static/v1.svg?label=PyCalVer&message=v202010.1039-beta&color=blue +[version_img]: https://img.shields.io/static/v1.svg?label=PyCalVer&message=v202010.1040-beta&color=blue [version_ref]: https://pypi.org/project/pycalver/ [pypi_img]: https://img.shields.io/badge/PyPI-wheels-green.svg diff --git a/bootstrapit.sh b/bootstrapit.sh index 70560a5..7eb962c 100644 --- a/bootstrapit.sh +++ b/bootstrapit.sh @@ -13,7 +13,7 @@ PACKAGE_NAME="pycalver" GIT_REPO_NAMESPACE="mbarkhau" GIT_REPO_DOMAIN="gitlab.com" -PACKAGE_VERSION="v202010.1039-beta" +PACKAGE_VERSION="v202010.1040-beta" DEFAULT_PYTHON_VERSION="python=3.6" SUPPORTED_PYTHON_VERSIONS="python=2.7 python=3.5 python=3.6 python=3.7 pypy2.7 pypy3.5" diff --git a/setup.cfg b/setup.cfg index 37d1964..5f915a4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -92,7 +92,7 @@ addopts = --doctest-modules [pycalver] -current_version = "v202010.1039-beta" +current_version = "v202010.1040-beta" version_pattern = "vYYYY0M.BUILD[-RELEASE[NUM]]" commit_message = "bump {old_version} -> {new_version}" commit = True diff --git a/setup.py b/setup.py index 9fc93ca..594c8fa 100644 --- a/setup.py +++ b/setup.py @@ -81,7 +81,7 @@ setuptools.setup( author="Manuel Barkhau", author_email="mbarkhau@gmail.com", url="https://github.com/mbarkhau/pycalver", - version="202010.1039b0", + version="202010.1040b0", keywords="version versioning bumpversion calver", description="CalVer for python libraries.", long_description=long_description, diff --git a/src/pycalver/__init__.py b/src/pycalver/__init__.py index 6237ecd..d38768f 100644 --- a/src/pycalver/__init__.py +++ b/src/pycalver/__init__.py @@ -5,4 +5,4 @@ # SPDX-License-Identifier: MIT """PyCalVer: CalVer for Python Packages.""" -__version__ = "v202010.1039-beta" +__version__ = "v202010.1040-beta" diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index c2afc15..8d66a17 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -106,7 +106,7 @@ def _validate_release_tag(tag: typ.Optional[str]) -> None: @click.group() -@click.version_option(version="v202010.1039-beta") +@click.version_option(version="v202010.1040-beta") @click.help_option() @click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") def cli(verbose: int = 0) -> None: From 2a3dbdb80e47e9ad1979bbdce31915aea3954de8 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 4 Oct 2020 12:27:30 +0000 Subject: [PATCH 58/98] add lint_pylint_errors --- makefile.bootstrapit.make | 9 +++++++++ src/pycalver/v2version.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/makefile.bootstrapit.make b/makefile.bootstrapit.make index ac91372..74a60fb 100644 --- a/makefile.bootstrapit.make +++ b/makefile.bootstrapit.make @@ -336,6 +336,15 @@ lint_flake8: @printf "\e[1F\e[9C ok\n" +## Run pylint. +.PHONY: lint_pylint_errors +lint_pylint_errors: + @printf "pylint ..\n"; + @$(DEV_ENV)/bin/pylint --errors-only --jobs=4 --rcfile=setup.cfg \ + src/ test/ + @printf "\e[1F\e[9C ok\n" + + ## Run pylint. .PHONY: lint_pylint lint_pylint: diff --git a/src/pycalver/v2version.py b/src/pycalver/v2version.py index dabcd53..4ee941d 100644 --- a/src/pycalver/v2version.py +++ b/src/pycalver/v2version.py @@ -628,7 +628,7 @@ def _incr_numeric( return cur_vinfo -def is_valid_week_pattern(raw_pattern) -> bool: +def is_valid_week_pattern(raw_pattern: str) -> bool: has_yy_part = any(part in raw_pattern for part in ["YYYY", "YY", "0Y"]) has_ww_part = any(part in raw_pattern for part in ["WW" , "0W", "UU", "0U"]) has_gg_part = any(part in raw_pattern for part in ["GGGG", "GG", "0G"]) From e5c7766cbc9173a70e76970e0bcdcfbf3d4d1ee4 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 4 Oct 2020 13:02:42 +0000 Subject: [PATCH 59/98] fix docstring --- src/pycalver/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pycalver/config.py b/src/pycalver/config.py index 99353a8..07b52f3 100644 --- a/src/pycalver/config.py +++ b/src/pycalver/config.py @@ -390,11 +390,11 @@ def _parse_raw_config(ctx: ProjectContext) -> RawConfig: raise RuntimeError(err_msg) if ctx.config_rel_path not in raw_cfg['file_patterns']: - # NOTE (mb 2020-09-19): By default we always add - # a pattern for the config section itself. with ctx.config_filepath.open(mode="rt", encoding="utf-8") as fobj: raw_cfg_text = fobj.read() + # NOTE (mb 2020-09-19): By default we always add + # a pattern for the config section itself. raw_version_pattern = _parse_current_version_default_pattern(raw_cfg, raw_cfg_text) raw_cfg['file_patterns'][ctx.config_rel_path] = [raw_version_pattern] From 803b0bf6a662931ed969c2370a9db54388ee4228 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 4 Oct 2020 17:11:49 +0000 Subject: [PATCH 60/98] auto populate cli reference in README.md --- README.md | 170 +++++++++++++++++++++++++++++++++++------------------- makefile | 19 ++++++ 2 files changed, 129 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 0604294..1173589 100644 --- a/README.md +++ b/README.md @@ -5,25 +5,31 @@ -# [PyCalVer: Automatic Calendar Versioning][repo_ref] +# [PyCalVer: Automatic Calendar Versioning][url_repo] + + +PyCalVer is a CLI-tool to search and replace version strings ([calver][url_calver_org], [semver][url_semver_org] or other) in your project files. + +[url_repo]: https://gitlab.com/mbarkhau/pycalver +[url_calver_org]: https://calver.org/ +[url_semver_org]: https://semver.org/ -PyCalVer is a CLI-tool to search and replace version strings in your project files. This project follows the pattern conventions from [calver.org][calver_org_ref]. Project/Repo: -[![MIT License][license_img]][license_ref] -[![Supported Python Versions][pyversions_img]][pyversions_ref] -[![PyCalVer v202010.1040-beta][version_img]][version_ref] -[![PyPI Releases][pypi_img]][pypi_ref] -[![PyPI Downloads][downloads_img]][downloads_ref] +[![MIT License][img_license]][url_license] +[![Supported Python Versions][img_pyversions]][url_pyversions] +[![PyCalVer v202010.1040-beta][img_version]][url_version] +[![PyPI Releases][img_pypi]][url_pypi] +[![PyPI Downloads][img_downloads]][url_downloads] Code Quality/CI: -[![GitHub Build Status][github_build_img]][github_build_ref] -[![GitLab Build Status][gitlab_build_img]][gitlab_build_ref] -[![Type Checked with mypy][mypy_img]][mypy_ref] -[![Code Coverage][codecov_img]][codecov_ref] -[![Code Style: sjfmt][style_img]][style_ref] +[![GitHub Build Status][img_github_build]][url_github_build] +[![GitLab Build Status][img_gitlab_build]][url_gitlab_build] +[![Type Checked with mypy][img_mypy]][url_mypy] +[![Code Coverage][img_codecov]][url_codecov] +[![Code Style: sjfmt][img_style]][url_style] | Name | role | since | until | @@ -31,6 +37,38 @@ Code Quality/CI: | Manuel Barkhau (mbarkhau@gmail.com) | author/maintainer | 2018-09 | - | + +[img_github_build]: https://github.com/mbarkhau/pycalver/workflows/CI/badge.svg +[url_github_build]: https://github.com/mbarkhau/pycalver/actions?query=workflow%3ACI + +[img_gitlab_build]: https://gitlab.com/mbarkhau/pycalver/badges/master/pipeline.svg +[url_gitlab_build]: https://gitlab.com/mbarkhau/pycalver/pipelines + +[img_codecov]: https://gitlab.com/mbarkhau/pycalver/badges/master/coverage.svg +[url_codecov]: https://mbarkhau.gitlab.io/pycalver/cov + +[img_license]: https://img.shields.io/badge/License-MIT-blue.svg +[url_license]: https://gitlab.com/mbarkhau/pycalver/blob/master/LICENSE + +[img_mypy]: https://img.shields.io/badge/mypy-checked-green.svg +[url_mypy]: https://mbarkhau.gitlab.io/pycalver/mypycov + +[img_style]: https://img.shields.io/badge/code%20style-%20sjfmt-f71.svg +[url_style]: https://gitlab.com/mbarkhau/straitjacket/ + +[img_downloads]: https://pepy.tech/badge/pycalver/month +[url_downloads]: https://pepy.tech/project/pycalver + +[img_version]: https://img.shields.io/static/v1.svg?label=PyCalVer&message=v202010.1040-beta&color=blue +[url_version]: https://pypi.org/project/pycalver/ + +[img_pypi]: https://img.shields.io/badge/PyPI-wheels-green.svg +[url_pypi]: https://pypi.org/project/pycalver/#files + +[img_pyversions]: https://img.shields.io/pypi/pyversions/pycalver.svg +[url_pyversions]: https://pypi.python.org/pypi/pycalver + + -| CLI Argument | Description | -|------------------|-------------| -| --major | | -| -m/--minor | | -| -p/--patch | | -| -r/--release-num | | -| --date | | -| --pin-date | | -| -n/--no-fetch | | -| -d/--dry | | -| --allow-dirty | | +``` +$ pycalver --help +Usage: pycalver [OPTIONS] COMMAND [ARGS]... + + Automatically update PyCalVer version strings on python projects. + +Options: + --version Show the version and exit. + --help Show this message and exit. + -v, --verbose Control log level. -vv for debug level. + +Commands: + bump Increment the current version string and update project files. + grep Search file(s) for a version pattern. + init Initialize [pycalver] configuration. + show Show current version of your project. + test Increment a version number for demo purposes. +``` + + + + + +``` +$ pycalver bump --help +Usage: pycalver bump [OPTIONS] + + Increment the current version string and update project files. + +Options: + -v, --verbose Control log level. -vv for debug level. + -f, --fetch / -n, --no-fetch Sync tags from remote origin. + -d, --dry Display diff of changes, don't rewrite files. + --release Override release name of current_version. + Valid options are: alpha, beta, rc, post, + final. + + --allow-dirty Commit even when working directory is has + uncomitted changes. (WARNING: The commit will + still be aborted if there are uncomitted to + files with version strings. + + --major Increment major component. + -m, --minor Increment minor component. + -p, --patch Increment patch component. + -r, --release-num Increment release number. + --pin-date Leave date components unchanged. + --date Set explicit date in format YYYY-0M-0D (e.g. + 2020-10-04). + + --help Show this message and exit. +``` + + + + + + ## The PyCalVer Format @@ -1019,10 +1104,6 @@ of the software as a whole, it is metadata about a particular release artifact of a package, eg. a `.whl` file. -[calver_org_ref]: https://calver.org/ - -[repo_ref]: https://gitlab.com/mbarkhau/pycalver - [setuptools_ref]: https://setuptools.readthedocs.io/en/latest/setuptools.html#specifying-your-project-s-version [ssot_ref]: https://en.wikipedia.org/wiki/Single_source_of_truth @@ -1041,34 +1122,3 @@ artifact of a package, eg. a `.whl` file. [cookiecutter_ref]: https://cookiecutter.readthedocs.io - -[github_build_img]: https://github.com/mbarkhau/pycalver/workflows/CI/badge.svg -[github_build_ref]: https://github.com/mbarkhau/pycalver/actions?query=workflow%3ACI - -[gitlab_build_img]: https://gitlab.com/mbarkhau/pycalver/badges/master/pipeline.svg -[gitlab_build_ref]: https://gitlab.com/mbarkhau/pycalver/pipelines - -[codecov_img]: https://gitlab.com/mbarkhau/pycalver/badges/master/coverage.svg -[codecov_ref]: https://mbarkhau.gitlab.io/pycalver/cov - -[license_img]: https://img.shields.io/badge/License-MIT-blue.svg -[license_ref]: https://gitlab.com/mbarkhau/pycalver/blob/master/LICENSE - -[mypy_img]: https://img.shields.io/badge/mypy-checked-green.svg -[mypy_ref]: https://mbarkhau.gitlab.io/pycalver/mypycov - -[style_img]: https://img.shields.io/badge/code%20style-%20sjfmt-f71.svg -[style_ref]: https://gitlab.com/mbarkhau/straitjacket/ - -[downloads_img]: https://pepy.tech/badge/pycalver/month -[downloads_ref]: https://pepy.tech/project/pycalver - -[version_img]: https://img.shields.io/static/v1.svg?label=PyCalVer&message=v202010.1040-beta&color=blue -[version_ref]: https://pypi.org/project/pycalver/ - -[pypi_img]: https://img.shields.io/badge/PyPI-wheels-green.svg -[pypi_ref]: https://pypi.org/project/pycalver/#files - -[pyversions_img]: https://img.shields.io/pypi/pyversions/pycalver.svg -[pyversions_ref]: https://pypi.python.org/pypi/pycalver - diff --git a/makefile b/makefile index 4b28595..3a7a07e 100644 --- a/makefile +++ b/makefile @@ -70,3 +70,22 @@ depgraph: --reverse --include-missing \ -x 'click.*' 'toml.*' 'pretty_traceback.*' \ -o pycalver_deps.svg + + +README.md: src/pycalver/__main__.py makefile + @git add README.md + @printf '\n```\n$$ pycalver --help\n' > /tmp/pycalver_help.txt + @$(DEV_ENV)/bin/pycalver --help >> /tmp/pycalver_help.txt + @printf '```\n\n' >> /tmp/pycalver_help.txt + + sed -i -ne '// {p; r /tmp/pycalver_help.txt' \ + -e ':a; n; // {p; b}; ba}; p' \ + README.md + + @printf '\n```\n$$ pycalver bump --help\n' > /tmp/pycalver_help.txt + @$(DEV_ENV)/bin/pycalver bump --help >> /tmp/pycalver_help.txt + @printf '```\n\n' >> /tmp/pycalver_help.txt + + sed -i -ne '// {p; r /tmp/pycalver_help.txt' \ + -e ':a; n; // {p; b}; ba}; p' \ + README.md From c3955efcc098cf88438bdea5f52491500c5b0bf6 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 4 Oct 2020 17:12:26 +0000 Subject: [PATCH 61/98] simplify lib3to6 code --- setup.py | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/setup.py b/setup.py index 594c8fa..27132c5 100644 --- a/setup.py +++ b/setup.py @@ -52,27 +52,9 @@ classifiers = [ package_dir = {"": "src"} -is_lib3to6_fix_required = ( - any(arg.startswith("bdist") for arg in sys.argv) - and ( - "Programming Language :: Python :: 2.7" in classifiers - or "Programming Language :: Python :: 2" in classifiers - ) -) - - -if is_lib3to6_fix_required: - try: - import lib3to6 - package_dir = lib3to6.fix(package_dir) - except ImportError: - if sys.version_info < (3, 6): - raise - else: - sys.stderr.write(( - "WARNING: Creating non-universal bdist of pycalver, " - "this should only be used for development.\n" - )) +if any(arg.startswith("bdist") for arg in sys.argv): + import lib3to6 + package_dir = lib3to6.fix(package_dir) setuptools.setup( From 8439d4290ae0404c90263ade41b6775c212ca66a Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 4 Oct 2020 17:13:41 +0000 Subject: [PATCH 62/98] cleanups --- README.md | 7 ++----- makefile | 3 ++- makefile.bootstrapit.make | 2 +- src/pycalver/__main__.py | 4 ++-- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1173589..065becd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- logo + logo

@@ -834,16 +834,13 @@ To maintain lexical ordering of version numbers, the version number is padded with extra zeros (see [Lexical Ids](#lexical-ids) ). -### Lexical Ids - -VCS tags. +## Semantics of PyCalVer This sorting even works correctly in JavaScript! -## Semantics of PyCalVer > Disclaimer: This section can of course only be aspirational. There is nothing > to prevent package maintainers from publishing packages with different diff --git a/makefile b/makefile index 3a7a07e..58ab0c9 100644 --- a/makefile +++ b/makefile @@ -64,7 +64,8 @@ test_compat: $(COMPAT_TEST_FILES) rm -rf compat_test/ -depgraph: + +pycalver_deps.svg: pydeps src/pycalver \ --no-show --noise-level 3 \ --reverse --include-missing \ diff --git a/makefile.bootstrapit.make b/makefile.bootstrapit.make index 74a60fb..13aef98 100644 --- a/makefile.bootstrapit.make +++ b/makefile.bootstrapit.make @@ -336,7 +336,7 @@ lint_flake8: @printf "\e[1F\e[9C ok\n" -## Run pylint. +## Run pylint --errors-only. .PHONY: lint_pylint_errors lint_pylint_errors: @printf "pylint ..\n"; diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 8d66a17..9267542 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -136,7 +136,7 @@ def cli(verbose: int = 0) -> None: "--date", default=None, metavar="", - help=f"Set explicit date in format YYYY-0M-0D\n(eg. {_current_date}).", + help=f"Set explicit date in format YYYY-0M-0D (e.g. {_current_date}).", ) def test( old_version: str, @@ -521,7 +521,7 @@ def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: "--date", default=None, metavar="", - help=f"Set explicit date in format YYYY-0M-0D\n(eg. {_current_date}).", + help=f"Set explicit date in format YYYY-0M-0D (e.g. {_current_date}).", ) def bump( release : typ.Optional[str] = None, From 65791ff26a866704ca605658fc197db519e0cf4c Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 4 Oct 2020 17:13:48 +0000 Subject: [PATCH 63/98] auto update of scripts/bootstrapit_update.sh --- scripts/bootstrapit_update.sh | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/scripts/bootstrapit_update.sh b/scripts/bootstrapit_update.sh index 2a9247c..00d837a 100644 --- a/scripts/bootstrapit_update.sh +++ b/scripts/bootstrapit_update.sh @@ -75,6 +75,20 @@ if [[ -f "makefile.extra.make" && -f "makefile.config.make" ]]; then exit 1 fi +# One time update of makefile capitalization +if [[ -f "makefile" && -f "makefile.bootstrapit.make" ]]; then + printf "Change capitalization of makefile -> Makefile # because too many rustled jimmies\n\n" + printf " mv makefile Makefile\n" + printf " mv makefile.bootstrapit.make Makefile.bootstrapit.make\n" + sed -i 's/include makefile.bootstrapit.make/include Makefile.bootstrapit.make/g' makefile + git add makefile + git mv makefile Makefile; + git mv makefile.bootstrapit.make Makefile.bootstrapit.make; + + printf "Please commit the renamed files and run bootstrapit_update.sh again." + exit 1 +fi + # Argument parsing from # https://stackoverflow.com/a/14203146/62997 UPDATE_ALL=0 @@ -346,9 +360,7 @@ elif [[ -z "${IGNORE_IF_EXISTS[*]}" ]]; then "CHANGELOG.md" "README.md" "setup.py" - "makefile" "requirements/pypi.txt" - "requirements/development.txt" "requirements/conda.txt" "requirements/vendor.txt" "src/${MODULE_NAME}/__init__.py" @@ -398,8 +410,8 @@ copy_template MANIFEST.in; copy_template setup.py; copy_template setup.cfg; -copy_template makefile; -copy_template makefile.bootstrapit.make; +copy_template Makefile; +copy_template Makefile.bootstrapit.make; copy_template activate; copy_template docker_base.Dockerfile; copy_template Dockerfile; From b27207d1fff83bd25349ff8cbaebfc779e9ad57c Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 4 Oct 2020 17:13:58 +0000 Subject: [PATCH 64/98] makefile -> Makefile --- makefile => Makefile | 2 +- makefile.bootstrapit.make => Makefile.bootstrapit.make | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename makefile => Makefile (98%) rename makefile.bootstrapit.make => Makefile.bootstrapit.make (100%) diff --git a/makefile b/Makefile similarity index 98% rename from makefile rename to Makefile index 58ab0c9..06b2225 100644 --- a/makefile +++ b/Makefile @@ -21,7 +21,7 @@ DEVELOPMENT_PYTHON_VERSION := python=3.8 SUPPORTED_PYTHON_VERSIONS := python=2.7 python=3.5 python=3.6 python=3.8 pypy2.7 pypy3.5 -include makefile.bootstrapit.make +include Makefile.bootstrapit.make ## -- Extra/Custom/Project Specific Tasks -- diff --git a/makefile.bootstrapit.make b/Makefile.bootstrapit.make similarity index 100% rename from makefile.bootstrapit.make rename to Makefile.bootstrapit.make From 8ab164496233dbbe5368d57a332e785f85d4c245 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 4 Oct 2020 17:21:45 +0000 Subject: [PATCH 65/98] project maintenance updates --- .github/workflows/ci.yml | 4 ++-- .gitlab-ci.yml | 4 ++-- CONTRIBUTING.md | 4 ++-- Dockerfile | 4 ++-- Makefile.bootstrapit.make | 9 ++++----- bootstrapit.sh | 9 ++++++--- docker_base.Dockerfile | 6 +++--- setup.cfg | 14 ++++++-------- 8 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5cdfcfa..bc1cb98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,9 +21,9 @@ jobs: path: | ~/miniconda3 build/*.txt - key: ${{ runner.OS }}-conda-cache-${{ hashFiles('requirements/*.txt', 'setup.py', 'makefile*') }} + key: ${{ runner.OS }}-conda-cache-${{ hashFiles('requirements/*.txt', 'setup.py', 'Makefile*') }} restore-keys: | - ${{ runner.OS }}-conda-cache-${{ hashFiles('requirements/*.txt', 'setup.py', 'makefile*') }} + ${{ runner.OS }}-conda-cache-${{ hashFiles('requirements/*.txt', 'setup.py', 'Makefile*') }} - name: make conda run: diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0977023..8cd378a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,8 +21,8 @@ lint: unit: # NOTE: Resource_group is conservative and can be disabled # for simple tests. It should be enabled if the tests - # need exclusive access to some resource common external - # resource. This will prevent multiple pipelines from + # need exclusive access to some common resource. The + # resource_group will prevent multiple pipelines from # running concurrently. # resource_group: test-unit stage: test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 08dee2e..7abde12 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -213,9 +213,9 @@ projects by reducing the burden of project setup to a minimum. CHANGELOG.md # short documentation of release history LICENSE # for public libraries (MIT preferred) - makefile # project specific configuration + Makefile # project specific configuration # variables and make targets - makefile.bootstrapit.make # bootstrapit make include library + Makefile.bootstrapit.make # bootstrapit make include library docker_base.Dockerfile # base image for CI (only conda envs) Dockerfile # image with source of the project diff --git a/Dockerfile b/Dockerfile index b0119f4..2cedc12 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,8 +10,8 @@ ADD pylint-ignore.md pylint-ignore.md ADD README.md README.md ADD CHANGELOG.md CHANGELOG.md ADD LICENSE LICENSE -ADD makefile makefile -ADD makefile.bootstrapit.make makefile.bootstrapit.make +ADD Makefile Makefile +ADD Makefile.bootstrapit.make Makefile.bootstrapit.make ADD scripts/exit_0_if_empty.py scripts/exit_0_if_empty.py ENV PYTHONPATH="src/:vendor/" diff --git a/Makefile.bootstrapit.make b/Makefile.bootstrapit.make index 13aef98..cb91823 100644 --- a/Makefile.bootstrapit.make +++ b/Makefile.bootstrapit.make @@ -57,8 +57,7 @@ CONDA_ENV_BIN_PYTHON_PATHS := \ empty := literal_space := $(empty) $(empty) -BDIST_WHEEL_PYTHON_TAG := \ - $(subst python,py,$(subst $(literal_space),.,$(subst .,,$(subst =,,$(SUPPORTED_PYTHON_VERSIONS))))) +BDIST_WHEEL_PYTHON_TAG := py2.py3 SDIST_FILE_CMD = ls -1t dist/*.tar.gz | head -n 1 @@ -182,7 +181,7 @@ help: helpMessage = ""; \ } \ }' \ - makefile.bootstrapit.make makefile + Makefile.bootstrapit.make Makefile @if [[ ! -f $(DEV_ENV_PY) ]]; then \ echo "Missing python interpreter at $(DEV_ENV_PY) !"; \ @@ -236,7 +235,7 @@ helpverbose: helpMessage = ""; \ } \ }' \ - makefile.bootstrapit.make makefile + Makefile.bootstrapit.make Makefile ## -- Project Setup -- @@ -568,7 +567,7 @@ bump_version: .PHONY: dist_build dist_build: $(DEV_ENV_PY) setup.py sdist; - $(DEV_ENV_PY) setup.py bdist_wheel --python-tag=py2.py3; + $(DEV_ENV_PY) setup.py bdist_wheel --python-tag=$(BDIST_WHEEL_PYTHON_TAG); @rm -rf src/*.egg-info diff --git a/bootstrapit.sh b/bootstrapit.sh index 7eb962c..d88ce11 100644 --- a/bootstrapit.sh +++ b/bootstrapit.sh @@ -11,12 +11,15 @@ LICENSE_ID="MIT" PACKAGE_NAME="pycalver" GIT_REPO_NAMESPACE="mbarkhau" -GIT_REPO_DOMAIN="gitlab.com" +GIT_REPO_DOMAIN="github.com" PACKAGE_VERSION="v202010.1040-beta" -DEFAULT_PYTHON_VERSION="python=3.6" -SUPPORTED_PYTHON_VERSIONS="python=2.7 python=3.5 python=3.6 python=3.7 pypy2.7 pypy3.5" +DEFAULT_PYTHON_VERSION="python=3.8" +SUPPORTED_PYTHON_VERSIONS="python=2.7 python=3.6 python=3.8 pypy2.7 pypy3.5" + +DOCKER_REGISTRY_DOMAIN=registry.gitlab.com + IS_PUBLIC=1 diff --git a/docker_base.Dockerfile b/docker_base.Dockerfile index bff005d..5cb7b50 100644 --- a/docker_base.Dockerfile +++ b/docker_base.Dockerfile @@ -1,7 +1,7 @@ # Stages: # root : Common image, both for the builder and for the final image. # This contains only minimal dependencies required in both cases -# for miniconda and the makefile. +# for miniconda and the Makefile. # env_builder: stage in which the conda envrionment is created # and dependencies are installed # base : the final image containing only the required environment files, @@ -37,8 +37,8 @@ RUN if ! test -z "${ENV_SSH_PRIVATE_RSA_KEY}"; then \ ADD requirements/ requirements/ ADD scripts/ scripts/ -ADD makefile.bootstrapit.make makefile.bootstrapit.make -ADD makefile makefile +ADD Makefile.bootstrapit.make Makefile.bootstrapit.make +ADD Makefile Makefile # install envs (relatively stable) ADD requirements/conda.txt requirements/conda.txt diff --git a/setup.cfg b/setup.cfg index 5f915a4..315ea5b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,7 +19,7 @@ warn_redundant_casts = True [tool:isort] -known_first_party = pycalver,pycalver2 +known_first_party = pycalver known_third_party = click,pathlib2,lexid force_single_line = True length_sort = True @@ -83,9 +83,6 @@ exclude = dist/ .mypy_cache -# Hopefully this can be resolved, so D404, D405 start working -# https://github.com/PyCQA/pydocstyle/pull/188 - [tool:pytest] addopts = --doctest-modules @@ -93,7 +90,7 @@ addopts = --doctest-modules [pycalver] current_version = "v202010.1040-beta" -version_pattern = "vYYYY0M.BUILD[-RELEASE[NUM]]" +version_pattern = "vYYYY0M.BUILD[-RELEASE]" commit_message = "bump {old_version} -> {new_version}" commit = True tag = True @@ -109,7 +106,7 @@ setup.py = src/pycalver/__init__.py = __version__ = "{version}" src/pycalver/__main__.py = - click.version_option(version="{version}") + @click.version_option(version="{version}") src/pycalver*/*.py = Copyright (c) 2018-YYYY LICENSE = @@ -126,8 +123,7 @@ README.md = score = no reports = no -# pylint spams the same message multiple times if jobs > 1 -jobs = 1 +jobs = 4 # Set the output format. Available formats are text, parseable, colorized, # msvs (visual studio) and html. You can also give a reporter class, eg @@ -154,6 +150,8 @@ ignore-comments=yes ignore-docstrings=yes ignore-imports=yes +ignored-argument-names=args|kwargs + # https://pylint.pycqa.org/en/stable/technical_reference/features.html disable = bad-continuation, From b466ca9d3950fae62a8c81487258f2744b531a8c Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 4 Oct 2020 20:43:37 +0000 Subject: [PATCH 66/98] better output for pycalver grep --- src/pycalver/__main__.py | 72 +++++++++++++++++++++++++--------------- src/pycalver/config.py | 10 +++--- 2 files changed, 52 insertions(+), 30 deletions(-) diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 9267542..0d6a677 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -110,7 +110,7 @@ def _validate_release_tag(tag: typ.Optional[str]) -> None: @click.help_option() @click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") def cli(verbose: int = 0) -> None: - """Automatically update PyCalVer version strings on python projects.""" + """Automatically update PyCalVer version strings in all project files.""" _configure_logging(verbose=max(_VERBOSE, verbose)) @@ -121,7 +121,7 @@ def cli(verbose: int = 0) -> None: @click.option( "--release", default=None, - metavar="", + metavar="", help=( f"Override release name of current_version. Valid options are: " f"{', '.join(VALID_RELEASE_TAG_VALUES)}." @@ -135,7 +135,7 @@ def cli(verbose: int = 0) -> None: @click.option( "--date", default=None, - metavar="", + metavar="", help=f"Set explicit date in format YYYY-0M-0D (e.g. {_current_date}).", ) def test( @@ -179,11 +179,9 @@ def test( click.echo(f"PEP440 : {pep440_version}") -def _grep_text(pattern: patterns.Pattern, text: str, color: bool) -> int: - match_count = 0 +def _grep_text(pattern: patterns.Pattern, text: str, color: bool) -> typ.Iterable[str]: all_lines = text.splitlines() for match in pattern.regexp.finditer(text): - match_count += 1 match_start, match_end = match.span() line_idx = text[:match_start].count("\n") @@ -212,12 +210,11 @@ def _grep_text(pattern: patterns.Pattern, text: str, color: bool) -> int: else: lines[1] = matched_line - print() - - for i, line in enumerate(lines): - print(f"{lines_offset + i:>4}: {line}") - - return match_count + prefixed_lines = [ + f"{lines_offset + i:>4}: {line}" + for i, line in enumerate(lines) + ] + yield "\n".join(prefixed_lines) def _grep( @@ -231,18 +228,21 @@ def _grep( for file_io in file_ios: text = file_io.read() - _match_count = _grep_text(pattern, text, color) + match_strs = list(_grep_text(pattern, text, color)) + if len(match_strs) > 0: + print(file_io.name) + for match_str in match_strs: + print(match_str) + print() - print() - print(f"Found {_match_count} match for pattern '{raw_pattern}' in {file_io.name}") - print() + match_count += len(match_strs) - match_count += _match_count + if match_count == 0: + logger.error(f"Pattern not found: '{raw_pattern}'") if match_count == 0 or _VERBOSE: pyexpr_regex = regexfmt.pyexpr_regex(pattern.regexp.pattern) - print(f"# pycalver pattern: '{raw_pattern}'") print("# " + regexfmt.regex101_url(pattern.regexp.pattern)) print(pyexpr_regex) print() @@ -258,12 +258,19 @@ def _grep( count=True, help="Control log level. -vv for debug level.", ) +@click.option( + "--version-pattern", + default=None, + metavar="", + help="Pattern to use for placeholders: {version}/{pep440_version}", +) @click.argument("pattern") @click.argument('files', nargs=-1, type=click.File('r')) def grep( - pattern: str, - files : typ.Tuple[io.TextIOWrapper], - verbose: int = 0, + pattern : str, + files : typ.Tuple[io.TextIOWrapper], + version_pattern: typ.Optional[str] = None, + verbose : int = 0, ) -> None: """Search file(s) for a version pattern.""" verbose = max(_VERBOSE, verbose) @@ -271,16 +278,29 @@ def grep( raw_pattern = pattern # use internal naming convention + is_version_pattern_required = "{version}" in raw_pattern or "{pep440_version}" in raw_pattern + + if is_version_pattern_required and version_pattern is None: + logger.error( + "Argument --version-pattern= is required" + " for placeholders: {version}/{pep440_version}." + ) + sys.exit(1) + elif is_version_pattern_required: + normalize_pattern = v2patterns.normalize_pattern(version_pattern, raw_pattern) + else: + normalize_pattern = raw_pattern + isatty = getattr(sys.stdout, 'isatty', lambda: False) if isatty(): colorama.init() try: - _grep(raw_pattern, files, color=True) + _grep(normalize_pattern, files, color=True) finally: colorama.deinit() else: - _grep(raw_pattern, files, color=False) + _grep(normalize_pattern, files, color=False) @cli.command() @@ -448,7 +468,7 @@ def init(verbose: int = 0, dry: bool = False) -> None: """Initialize [pycalver] configuration.""" _configure_logging(verbose=max(_VERBOSE, verbose)) - ctx, cfg = config.init(project_path=".") + ctx, cfg = config.init(project_path=".", cfg_missing_ok=True) if cfg: logger.error(f"Configuration already initialized in {ctx.config_rel_path}") @@ -496,7 +516,7 @@ def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: @click.option( "--release", default=None, - metavar="", + metavar="", help=( f"Override release name of current_version. Valid options are: " f"{', '.join(VALID_RELEASE_TAG_VALUES)}." @@ -520,7 +540,7 @@ def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: @click.option( "--date", default=None, - metavar="", + metavar="", help=f"Set explicit date in format YYYY-0M-0D (e.g. {_current_date}).", ) def bump( diff --git a/src/pycalver/config.py b/src/pycalver/config.py index 07b52f3..39eeab9 100644 --- a/src/pycalver/config.py +++ b/src/pycalver/config.py @@ -401,7 +401,7 @@ def _parse_raw_config(ctx: ProjectContext) -> RawConfig: return raw_cfg -def parse(ctx: ProjectContext) -> MaybeConfig: +def parse(ctx: ProjectContext, cfg_missing_ok: bool = False) -> MaybeConfig: """Parse config file if available.""" if ctx.config_filepath.exists(): try: @@ -411,15 +411,17 @@ def parse(ctx: ProjectContext) -> MaybeConfig: logger.warning(f"Couldn't parse {ctx.config_rel_path}: {str(ex)}") return None else: - logger.warning(f"File not found: {ctx.config_rel_path}") + if not cfg_missing_ok: + logger.warning(f"File not found: {ctx.config_rel_path}") return None def init( - project_path: typ.Union[str, pl.Path, None] = "." + project_path: typ.Union[str, pl.Path, None] = ".", + cfg_missing_ok: bool = False, ) -> typ.Tuple[ProjectContext, MaybeConfig]: ctx = init_project_ctx(project_path) - cfg = parse(ctx) + cfg = parse(ctx, cfg_missing_ok) return (ctx, cfg) From fd1b7bd05fff20f530e4f4fe66b642c5f9cff787 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 4 Oct 2020 20:46:20 +0000 Subject: [PATCH 67/98] readme updates --- README.md | 281 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 147 insertions(+), 134 deletions(-) diff --git a/README.md b/README.md index 065becd..efe4920 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ # [PyCalVer: Automatic Calendar Versioning][url_repo] -PyCalVer is a CLI-tool to search and replace version strings ([calver][url_calver_org], [semver][url_semver_org] or other) in your project files. +PyCalVer is a CLI-tool to search and replace version strings in your project files ([calver][url_calver_org], [semver][url_semver_org] or otherwise) . [url_repo]: https://gitlab.com/mbarkhau/pycalver [url_calver_org]: https://calver.org/ @@ -101,6 +101,123 @@ Code Quality/CI: ## Usage +### Search and Replace + +With PyCalVer, you only have to specify one `version_pattern` which is used both to search for version strings as well as to generate the replacement when you do `pycalver bump`. Compare this e.g. to `bumpversion` where you declare separate configurations for `parse` and `serialize`. + +``` +[bumpversion] +current_version = 1.alpha +parse = (?P\d+)\.(?P.*) +serialize = + {major}.{release} + {major} +``` + +A similar version schema with PyCalVer would be: + +``` +[pycalver] +current_version = 1.alpha +version_pattern = MAJOR.RELEASE +``` + +Similarly you must specify file specific search and replace strings. + +``` +[bumpversion:file:requirements.txt] +search = MyProject=={current_version} +replace = MyProject=={new_version} +``` + +The same with PyCalVer would be: + +``` +[pycalver:file_patterns] +requirements.txt + MyProject=={version} +``` + +The string `{version}` is a placeholder which references whatever you specified in your `version_pattern`. +You can also be explicit and write the expanded version yourself if you prefer: + +``` +[pycalver:file_patterns] +requirements.txt + MyProject==MAJOR.RELEASE +``` + +> You may be asking at this point, "what if I want to match `MAJOR.RELEASE` as a literal string?". +> Well, tough luck. Realistically speaking, this has not been an issue. + +In other words, you don't specify regular expressions manually, they are generated for by PyCalVer based on the parts defined below. Everything except for a valid part (in all caps) is treated as literal text. + +### Patterns/Parts + +> These patterns are closely based on [calver.org][url_calver_org_scheme]. + +[url_calver_org_scheme]: https://calver.org/#scheme + +| part | range / example(s) | comment | +|-----------|---------------------------|--------------------------------------------| +| `YYYY` | 2019, 2020... | Full year, based on `strftime('%Y')` | +| `YY` | 18, 19..99, 1, 2 | Short year, based on `int(strftime('%y'))` | +| `MM` | 9, 10, 11, 12 | Month, based on `int(strftime('%m'))` | +| `DD` | 1, 2, 3..31 | Day, based on `int(strftime('%d'))` | +| `MAJOR` | 0..9, 10..99, 100.. | `pycalver bump --major` | +| `MINOR` | 0..9, 10..99, 100.. | `pycalver bump --minor` | +| `PATCH` | 0..9, 10..99, 100.. | `pycalver bump --patch` | +| `RELEASE` | alpha, beta, rc, post | `--release=` | +| `PYTAG` | a, b, rc, post | `--release=` | +| `NUM` | 0, 1, 2... | `-r/--release-num` | +| `BUILD` | 1001, 1002 .. 1999, 22000 | build number (maintains lexical order) | +| `INC0` | 0, 1, 2... | 0-based auto incrementing number | +| `INC1` | 1, 2... | 1-based auto incrementing number | + + +The above are the most commonly used. The following are also available, but you should be aware of the [Normalization Caveats](#normalization-caveats) if you want to use them. + + +| part | range / example(s) | comment | +|--------|---------------------|----------------------------------------------| +| `Q` | 1, 2, 3, 4 | Quarter | +| `0Y` | 18, 19..99, 01, 02 | Short Year `strftime('%y')`(zero-padded) | +| `0M` | 09, 10, 11, 12 | Month `strftime('%m')` (zero-padded) | +| `0D` | 01, 02, 03..31 | Day `strftime('%d')` (zero-padded) | +| `JJJ` | 1,2,3..366 | Day of year `int(strftime('%j'))` | +| `00J` | 001, 002..366 | Day of year `strftime('%j')` (zero-padded) | +| `WW` | 0, 1, 2..52 | Week number¹ `int(strftime('%W'))` | +| `0W` | 00, 01, 02..52 | Week number¹ `strftime('%W')` (zero-padded) | +| `UU` | 0, 1, 2..52 | Week number² `int(strftime('%U'))` | +| `0U` | 00, 01, 02..52 | Week number² `strftime('%U')` (zero-padded) | +| `VV` | 1, 2..53 | Week number¹³ `int(strftime('%V'))` | +| `0V` | 01, 02..53 | Week number¹³ `strftime('%V')` (zero-padded) | +| `GGGG` | 2019, 2020... | `strftime("%G")` ISO 8601 week-based year | +| `GG` | 19, 20...99, 0, 1 | Short ISO 8601 week-based year | +| `0G` | 19, 20...99, 00, 01 | Zero-padded ISO 8601 week-based year | + +- ¹ Monday is the first day of the week. +- ² Sunday is the first day of the week. +- ³ ISO 8601 week. Week 1 contains Jan 4th. + +> On Week Numbering +> +> Week numbering is a bit special, as it depends on your definition of "week": +> +> - Does it start on a Monday or a Sunday? +> - Range from 0-52 or 1-53 ? +> - At the beginning/end of the year, do you have partial weeks or do +> you have a week that span mutliple years? +> - If a week spans multiple years, what is the year number? +> +> If you use `VV`/`0V`, be aware that you cannot also use `YYYY`. +> Instead use `GGGG`. This is to avoid an edge case where your version +> number would run backwards if it was created around New Year. + + +### Rollover + +TODO ### Configuration The fastest way to setup a project is to use `pycalver init`. @@ -242,116 +359,7 @@ ERROR - Pattern compiles to regex 'img\.shields\.io/static/v1\.svg\?label=PyCa The internally used regular expression is also shown, which you can use to debug the issue, for example on [regex101.com](https://regex101.com/r/ajQDTz/2). - -### Pattern Search and Replacement - -The neat thing about PyCalVer is that you don't have to separately declare the search pattern and the replacement template. You declare one pattern that is used to derive both. - -for valid placeholders is treated as literal text. Available placeholders are: - -You define your pattern in the `pycalver:version_pattern` option of your config. - -You can also define custom patterns in the items of the `pycalver:file_patterns` section of your configuration, but usually it will be easier to just use the special `{version}` and `{pep440_version}` patterns, which are derived from what you configure as your `version_pattern`. is used both to search and also to replace version strings in your projects files. Everything except for valid placeholders is treated as literal text. Available placeholders are: - -These patterns are closely based on https://calver.org/ - -| placeholder | range / example(s) | comment | -|-------------|----------------------|------------------------| -| `YYYY` | 2019, 2020... | `%Y` | -| `YY` | 18, 19..99, 1, 2 | `int(%y)` | -| `0Y` | 18, 19..99, 01, 02 | `%y` | -| `Q` | 1, 2, 3, 4 | quarter | -| `MM` | 9, 10, 11, 12 | `int(%m)` | -| `0M` | 09, 10, 11, 12 | `%m` | -| `DD` | 1, 2, 3..31 | `int(%d)` | -| `0D` | 01, 02, 03..31 | `%d` | -| `JJJ` | 1,2,3..366 | `int(%j)` | -| `00J` | 001, 002..366 | `%j` | -| `MAJOR` | 0..9, 10..99, 100.. | `--major` | -| `MINOR` | 0..9, 10..99, 100.. | `-m/--minor` | -| `PATCH` | 0..9, 10..99, 100.. | `-p/--patch` | -| `INC0` | 0, 1, 2... | | -| `INC1` | 1, 2... | | -| `BUILD` | 0011, 1001, 1002, .. | build number (lexid) | -| `BLD` | 11, 1001, 1002, .. | zero truncated `BUILD` | -| `RELEASE` | alpha, beta, rc | `--release=` | -| `PYTAG` | a, b, rc | `--release=` | -| `NUM` | 0, 1, 2... | `-r/--release-num` | - - -### Week Numbering - -Week numbering is a bit special, as it depends on your definition of "week": - -- Does it start on a Monday or a Sunday? -- Range from 0-52 or 1-53 ? -- At the beginning/end of the year, do you have partial weeks or do you have a week that span mutliple years? -- If a week spans multiple years, what is the year number? - -| placeholder | range / example(s) | comment | -|-------------|---------------------|-------------------------------------------| -| `WW` | 0, 1, 2..52 | `int(%W)` | -| `0W` | 00, 01, 02..52 | `%W` | -| `UU` | 0, 1, 2..52 | `int(%U)` us_week | -| `0U` | 00, 01, 02..52 | `%U` us_week | -| `VV` | 1, 2..53 | `int(%V)` iso week | -| `0V` | 01, 02..53 | `%V` iso_week | -| `GGGG` | 2019, 2020... | `strftime("%G")` ISO 8601 week-based year | -| `GG` | 19, 20...99, 0, 1 | Short ISO 8601 week-based year | -| `0G` | 19, 20...99, 00, 01 | Zero-padded ISO 8601 week-based year | - - -### Normalization Caveats - -Since other tools parse your version numbers, they may not care about your choice of formatting. In the case of Python, the packaging tools (such as pypi.org) follow [PEP440 normalization rules][pep_440_normalzation_ref]. - -According to these rules: - -- Any non-numerical prefix (such as `v`) is removed -- Leading zeros in parts are truncated `XX.08` -> `XX.8` -- Tags are converted to a short form (`-alpha` -> `a0`) - -For example: - -- Pattern: `vYY.0M.0D[-RELEASE]` -- Version: `v20.08.02-beta` -- PEP440 : `20.8.2b0` - -It may be confusing to your users to see versions displayed in two different forms. It is not immediately obvious that `v20.08.02-beta` is the same `20.8.2b0` on pypi. If you wish to avoid this, you should usa a pattern which is as close as possible to the normalized form of your version. - -It may also be confusing to your users if they a list of version numbers, sorted lexiographically by some tool (e.g. a list of git tags) and a newer version is listed after older versions like this: - -``` -3.9.1 -3.8.1 -3.8.0 -3.10.0 -``` - -If you wish to avoid this, you should use a pattern which maintains lexiographical ordering. - - -| pattern | example | lexio. | PEP440 | lexio. | -|---------------------------|---------|--------|--------|--------| -| `YYYY0M.BUILD[-RELEASE]` | | yes | | yes | -| `YYYY.BUILD[-RELEASE]` | | yes | | yes | -| `YYYY0M.MINOR[-RELEASE]` | | yes² | | yes | -| `YY0M.BUILD[-RELEASE]` | | yes¹ | | yes¹ | -| `YYYY.MM.MINOR[-RELEASE]` | | no | | no | -| `YYYY.0M.MINOR[-RELEASE]` | | yes² | | no | -| `YYYY.WW.MINOR[-RELEASE]` | | no | | no | -| `YYYY.0W.MINOR[-RELEASE]` | | yes² | | no | -| `YYYY.0M.0D` | | yes | | no | -| `YYYY.MM.DD` | | no | | no | -| `vYYYY.0W` | | yes | | no | -| `vYYYY.WW` | | no | | no | -| `YYYY.0M` | | yes | | no | -| `YYYY.MM` | | no | | no | - -- ¹ Until 2099. If your project has new releases after 2099, future maintainers can change `YY`/`0Y` -> `YYYY` so that they don't release `00.xx`. -- ² As long as `MINOR <= 9` - - +TODO Update above link ### Legacy Patterns > These patterns use curly braces `{}` and were the initial implementation. They are still supported and still follow their original semantics. @@ -493,12 +501,15 @@ the local checkout, the same version might be generated for different commits. To avoid this issue, pycalver treats VCS tags as the canonical / -[SSOT][ssot_ref] for the most recent version and attempts to +[SSOT][url_ssot] for the most recent version and attempts to change this state in the most atomic way possible. This is why some actions of the `pycalver` command can take a while, as it is synchronizing with the remote repository to get the most recent versions and to push any new version tags as soon as possible. +[url_ssot]: https://en.wikipedia.org/wiki/Single_source_of_truth + + ### The Current Version @@ -689,9 +700,12 @@ Options: - +### Related Projects/Alternatives + +The bump2version project maintains a good list of alternative and related projects: [bump2version/RELATED.md][url_bump2version_related] + +[url_bump2version_related] https://github.com/c4urself/bump2version/blob/master/RELATED.md - ## The PyCalVer Format @@ -705,7 +719,7 @@ The PyCalVer format for version strings has three parts: | | o Release Tag (optional) | | | ---+--- --+-- --+-- - v202008 .0123 -beta + v202010 .1001 -beta ``` @@ -722,9 +736,9 @@ v202207.18133 v202207.18134 ``` -This slightly verbose format was chosen in part to be distinctive from +This format was chosen in part to be distinctive from others, so that users of your package can see at a glance that your project -will strive to maintain the one semantic that really matters: **newer == +will strive to maintain the one semantic that really matters: **newer is better**. To convince you of the merits of not breaking things, here are some @@ -805,33 +819,34 @@ To see how version strings are incremented, we can use `pycalver test`: ```shell -$ pycalver test v201801.0033-beta -New Version: v201902.0034-beta -PEP440 : 201902.34b0 +$ pycalver test v201801.1033-beta +New Version: v201902.1034-beta +PEP440 : 201902.1034b0 ``` This is the simple case: - - The calendar component is updated to the current year and - month. + - The calendar component is updated to the current year and month. - The build number is incremented by 1. - The optional release tag is preserved as is. -You can explicitly update the release tag by using the -`--release=` argument: +You can explicitly update the release tag by using the `--release=` argument: ```shell -$ pycalver test v201801.0033-alpha --release=beta -New Version: v201902.0034-beta -PEP440 : 201902.34b0 +$ pycalver test v201801.1033-alpha --release=beta +New Version: v201902.1034-beta +PEP440 : 201902.1034b0 -$ pycalver test v201902.0034-beta --release=final -New Version: v201902.0035 -PEP440 : 201902.35 +$ pycalver test v201902.1034-beta --release=final +New Version: v201902.1035 +PEP440 : 201902.1035 ``` -To maintain lexical ordering of version numbers, the version number is padded -with extra zeros (see [Lexical Ids](#lexical-ids) ). +To maintain lexical ordering of version numbers, the version number is padded with extra zeros using [Lexical Ids][url_pypi_lexid]. +This means that the expression `older_id < newer_id` will always be true, whether you are dealing with integers or strings. + +[url_pypi_lexid]: https://pypi.org/project/lexid/ + ## Semantics of PyCalVer @@ -1103,8 +1118,6 @@ artifact of a package, eg. a `.whl` file. [setuptools_ref]: https://setuptools.readthedocs.io/en/latest/setuptools.html#specifying-your-project-s-version -[ssot_ref]: https://en.wikipedia.org/wiki/Single_source_of_truth - [pep_440_ref]: https://www.python.org/dev/peps/pep-0440/ [pep_440_normalzation_ref]: https://www.python.org/dev/peps/pep-0440/#id31 From 2eb9b516b8d40f7423a4bca02b1af5036596d756 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 4 Oct 2020 21:17:20 +0000 Subject: [PATCH 68/98] fix tests --- src/pycalver/__main__.py | 7 ++--- src/pycalver/config.py | 2 +- test/test_cli.py | 67 ++++++++++++++++++++++++++-------------- 3 files changed, 47 insertions(+), 29 deletions(-) diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 0d6a677..08072ea 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -180,7 +180,7 @@ def test( def _grep_text(pattern: patterns.Pattern, text: str, color: bool) -> typ.Iterable[str]: - all_lines = text.splitlines() + all_lines = text.splitlines() for match in pattern.regexp.finditer(text): match_start, match_end = match.span() @@ -210,10 +210,7 @@ def _grep_text(pattern: patterns.Pattern, text: str, color: bool) -> typ.Iterabl else: lines[1] = matched_line - prefixed_lines = [ - f"{lines_offset + i:>4}: {line}" - for i, line in enumerate(lines) - ] + prefixed_lines = [f"{lines_offset + i:>4}: {line}" for i, line in enumerate(lines)] yield "\n".join(prefixed_lines) diff --git a/src/pycalver/config.py b/src/pycalver/config.py index 39eeab9..a88dd72 100644 --- a/src/pycalver/config.py +++ b/src/pycalver/config.py @@ -417,7 +417,7 @@ def parse(ctx: ProjectContext, cfg_missing_ok: bool = False) -> MaybeConfig: def init( - project_path: typ.Union[str, pl.Path, None] = ".", + project_path : typ.Union[str, pl.Path, None] = ".", cfg_missing_ok: bool = False, ) -> typ.Tuple[ProjectContext, MaybeConfig]: ctx = init_project_ctx(project_path) diff --git a/test/test_cli.py b/test/test_cli.py index 99d79f5..99a1e69 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -252,22 +252,10 @@ def test_novcs_nocfg_init(runner, caplog): assert result.exit_code == 0 assert not os.path.exists("pycalver.toml") - # check logging - assert len(caplog.records) == 1 - log = caplog.records[0] - assert log.levelname == 'WARNING' - assert "File not found" in log.message - # non dry mode result = runner.invoke(cli, ['init', "-vv"]) assert result.exit_code == 0 - # check logging - assert len(caplog.records) == 2 - log = caplog.records[1] - assert log.levelname == 'WARNING' - assert "File not found" in log.message - assert os.path.exists("pycalver.toml") with pl.Path("pycalver.toml").open(mode="r", encoding="utf-8") as fobj: cfg_content = fobj.read() @@ -285,8 +273,8 @@ def test_novcs_nocfg_init(runner, caplog): assert result.exit_code == 1 # check logging - assert len(caplog.records) == 3 - log = caplog.records[2] + assert len(caplog.records) == 1 + log = caplog.records[0] assert log.levelname == 'ERROR' assert "Configuration already initialized" in log.message @@ -747,19 +735,52 @@ def test_git_commit_message(runner, caplog): def test_grep(runner): _add_project_files("README.md") - result = runner.invoke(cli, ['grep', r"vYYYY0M.BUILD[-RELEASE]", "README.md"]) - assert result.exit_code == 0 - assert "Found 1 match for pattern" in result.output - + # search_re = r"^\s+2:\s+Hello World v201701\.1002-alpha !" - assert re.search(search_re, result.output, flags=re.MULTILINE) - result = runner.invoke(cli, ['grep', r"\[aka. YYYY0M.BLD[PYTAGNUM] \!\]", "README.md"]) - assert result.exit_code == 0 - assert "Found 1 match for pattern" in result.output + result1 = runner.invoke(cli, ['grep', r"vYYYY0M.BUILD[-RELEASE]", "README.md"]) + assert result1.exit_code == 0 + assert "README.md" in result1.output + assert re.search(search_re, result1.output, flags=re.MULTILINE) + + result2 = runner.invoke( + cli, + [ + 'grep', + "--version-pattern", + r"vYYYY0M.BUILD[-RELEASE]", + "{version}", + "README.md", + ], + ) + assert result2.exit_code == 0 + assert "README.md" in result2.output + assert re.search(search_re, result2.output, flags=re.MULTILINE) + + assert result1.output == result2.output search_re = r"^\s+3:\s+\[aka\. 201701\.1002a0 \!\]" - assert re.search(search_re, result.output, flags=re.MULTILINE) + + result3 = runner.invoke(cli, ['grep', r"\[aka. YYYY0M.BLD[PYTAGNUM] \!\]", "README.md"]) + assert result3.exit_code == 0 + assert "README.md" in result3.output + assert re.search(search_re, result3.output, flags=re.MULTILINE) + + result4 = runner.invoke( + cli, + [ + 'grep', + "--version-pattern", + r"vYYYY0M.BUILD[-RELEASE]", + r"\[aka. {pep440_version} \!\]", + "README.md", + ], + ) + assert result4.exit_code == 0 + assert "README.md" in result4.output + assert re.search(search_re, result4.output, flags=re.MULTILINE) + + assert result3.output == result4.output SETUP_CFG_MULTIMATCH_FILE_PATTERNS_FIXTURE = r""" From b5df665251a5974a24987e706de840311f1b471d Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 4 Oct 2020 21:21:24 +0000 Subject: [PATCH 69/98] pacify mypy --- src/pycalver/__main__.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 08072ea..8be9063 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -277,16 +277,18 @@ def grep( is_version_pattern_required = "{version}" in raw_pattern or "{pep440_version}" in raw_pattern - if is_version_pattern_required and version_pattern is None: - logger.error( - "Argument --version-pattern= is required" - " for placeholders: {version}/{pep440_version}." - ) - sys.exit(1) - elif is_version_pattern_required: - normalize_pattern = v2patterns.normalize_pattern(version_pattern, raw_pattern) + if version_pattern is None: + if is_version_pattern_required: + logger.error( + "Argument --version-pattern= is required" + " for placeholders: {version}/{pep440_version}." + ) + sys.exit(1) else: - normalize_pattern = raw_pattern + if is_version_pattern_required: + normalize_pattern = v2patterns.normalize_pattern(version_pattern, raw_pattern) + else: + normalize_pattern = raw_pattern isatty = getattr(sys.stdout, 'isatty', lambda: False) From 43fe8df1fd43a2dfc349ab3d587d1c75032e9e67 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 4 Oct 2020 21:37:03 +0000 Subject: [PATCH 70/98] fix grep regression --- .gitlab-ci.yml | 15 +++++++++++++-- src/pycalver/__main__.py | 14 ++++++++------ test/test_cli.py | 30 +++++++++--------------------- 3 files changed, 30 insertions(+), 29 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8cd378a..e213607 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,7 +18,7 @@ lint: allow_failure: false -unit: +test: # NOTE: Resource_group is conservative and can be disabled # for simple tests. It should be enabled if the tests # need exclusive access to some common resource. The @@ -29,7 +29,6 @@ unit: image: registry.gitlab.com/mbarkhau/pycalver/base script: - make test - - make test_compat coverage: '/^(TOTAL|src).*?(\d+\%)$/' artifacts: reports: @@ -39,6 +38,18 @@ unit: - reports/testcov/ allow_failure: false +test_compat: + # NOTE: Resource_group is conservative and can be disabled + # for simple tests. It should be enabled if the tests + # need exclusive access to some common resource. The + # resource_group will prevent multiple pipelines from + # running concurrently. + # resource_group: test-unit + stage: test + image: registry.gitlab.com/mbarkhau/pycalver/base + script: + - make test_compat + allow_failure: false pages: stage: build diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 8be9063..7053ac9 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -285,21 +285,23 @@ def grep( ) sys.exit(1) else: - if is_version_pattern_required: - normalize_pattern = v2patterns.normalize_pattern(version_pattern, raw_pattern) - else: - normalize_pattern = raw_pattern + version_pattern = "vYYYY0M.BUILD[-RELEASE]" + + if is_version_pattern_required: + normalized_pattern = v2patterns.normalize_pattern(version_pattern, raw_pattern) + else: + normalized_pattern = raw_pattern isatty = getattr(sys.stdout, 'isatty', lambda: False) if isatty(): colorama.init() try: - _grep(normalize_pattern, files, color=True) + _grep(normalized_pattern, files, color=True) finally: colorama.deinit() else: - _grep(normalize_pattern, files, color=False) + _grep(normalized_pattern, files, color=False) @cli.command() diff --git a/test/test_cli.py b/test/test_cli.py index 99a1e69..e3922d7 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -738,21 +738,14 @@ def test_grep(runner): # search_re = r"^\s+2:\s+Hello World v201701\.1002-alpha !" - result1 = runner.invoke(cli, ['grep', r"vYYYY0M.BUILD[-RELEASE]", "README.md"]) + cmd1 = r'grep "vYYYY0M.BUILD[-RELEASE]" README.md' + result1 = runner.invoke(cli, shlex.split(cmd1)) assert result1.exit_code == 0 assert "README.md" in result1.output assert re.search(search_re, result1.output, flags=re.MULTILINE) - result2 = runner.invoke( - cli, - [ - 'grep', - "--version-pattern", - r"vYYYY0M.BUILD[-RELEASE]", - "{version}", - "README.md", - ], - ) + cmd2 = r'grep --version-pattern "vYYYY0M.BUILD[-RELEASE]" "{version}" README.md' + result2 = runner.invoke(cli, shlex.split(cmd2)) assert result2.exit_code == 0 assert "README.md" in result2.output assert re.search(search_re, result2.output, flags=re.MULTILINE) @@ -761,21 +754,16 @@ def test_grep(runner): search_re = r"^\s+3:\s+\[aka\. 201701\.1002a0 \!\]" - result3 = runner.invoke(cli, ['grep', r"\[aka. YYYY0M.BLD[PYTAGNUM] \!\]", "README.md"]) + cmd3 = r'grep "\[aka. YYYY0M.BLD[PYTAGNUM] \!\]" README.md' + result3 = runner.invoke(cli, shlex.split(cmd3)) assert result3.exit_code == 0 assert "README.md" in result3.output assert re.search(search_re, result3.output, flags=re.MULTILINE) - result4 = runner.invoke( - cli, - [ - 'grep', - "--version-pattern", - r"vYYYY0M.BUILD[-RELEASE]", - r"\[aka. {pep440_version} \!\]", - "README.md", - ], + cmd4 = ( + r'grep --version-pattern "vYYYY0M.BUILD[-RELEASE]" "\[aka. {pep440_version} \!\]" README.md' ) + result4 = runner.invoke(cli, shlex.split(cmd4)) assert result4.exit_code == 0 assert "README.md" in result4.output assert re.search(search_re, result4.output, flags=re.MULTILINE) From 2660ec03d32a1e12363962699fd007b811cf131d Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 4 Oct 2020 21:44:30 +0000 Subject: [PATCH 71/98] pacify mypy --- src/pycalver/__main__.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 7053ac9..8682761 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -277,18 +277,19 @@ def grep( is_version_pattern_required = "{version}" in raw_pattern or "{pep440_version}" in raw_pattern - if version_pattern is None: - if is_version_pattern_required: - logger.error( - "Argument --version-pattern= is required" - " for placeholders: {version}/{pep440_version}." - ) - sys.exit(1) + if is_version_pattern_required and version_pattern is None: + logger.error( + "Argument --version-pattern= is required" + " for placeholders: {version}/{pep440_version}." + ) + sys.exit(1) + elif version_pattern is None: + _version_pattern = "INVALID" # pacify mypy, it's not referenced in raw_pattern else: - version_pattern = "vYYYY0M.BUILD[-RELEASE]" + _version_pattern = version_pattern if is_version_pattern_required: - normalized_pattern = v2patterns.normalize_pattern(version_pattern, raw_pattern) + normalized_pattern = v2patterns.normalize_pattern(_version_pattern, raw_pattern) else: normalized_pattern = raw_pattern From 54a58929579a0eca5e538e999cb3157efa1bb478 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Mon, 5 Oct 2020 17:54:33 +0000 Subject: [PATCH 72/98] fix #12: sorting of tags ignored version number semantics --- CHANGELOG.md | 5 +- setup.cfg | 2 +- src/pycalver/__main__.py | 52 ++++++++++++++--- src/pycalver/v1cli.py | 48 --------------- src/pycalver/v2cli.py | 48 --------------- test/test_cli.py | 123 ++++++++++++++++++--------------------- 6 files changed, 105 insertions(+), 173 deletions(-) delete mode 100755 src/pycalver/v1cli.py delete mode 100644 src/pycalver/v2cli.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 86c0543..1e5aaf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,11 +12,12 @@ - New gitlab #2: Added `grep` subcommand to find and debug patterns. - New: Added better error messages to debug regular expressions. - New gitlab #10: `--pin-date` to keep date parts unchanged, and only increment non-date parts. + - New gitlab #9: Make commit message configurable. - New: Added `--release-num` to increment the `alphaN`/`betaN`/`a0`/`b0`/etc. release number - New: Added `--date=` parameter to set explicit date (instead of current date). - - Fix gitlab #8: Push tags only pushed tags, not actual commit. - - Fix gitlab #9: Make commit message configurable. + - Fix gitlab #12: Error sorting non-lexical version tags (e.g. semver). - Fix gitlab #11: Show regexp when `--verbose` is used. + - Fix gitlab #8: Push now also pushes HEAD (used to only push the tag). - Fix: Disallow `--release=dev` which has different semantics than other release tags. - Fix: Entries in `file_patterns` were ignored if there were multiple for the same file. - Switch main repo from gitlab to github. diff --git a/setup.cfg b/setup.cfg index 315ea5b..8e3ef87 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,7 +20,7 @@ warn_redundant_casts = True [tool:isort] known_first_party = pycalver -known_third_party = click,pathlib2,lexid +known_third_party = click,pathlib2,lexid,pkg_resources force_single_line = True length_sort = True diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 8682761..ea399ee 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -18,10 +18,9 @@ import subprocess as sp import click import colorama +import pkg_resources from . import vcs -from . import v1cli -from . import v2cli from . import config from . import rewrite from . import version @@ -339,6 +338,25 @@ def _colored_diff_lines(diff: str) -> typ.Iterable[str]: yield line +def _v2_get_diff(cfg: config.Config, new_version: str) -> str: + old_vinfo = v2version.parse_version_info(cfg.current_version, cfg.version_pattern) + new_vinfo = v2version.parse_version_info(new_version, cfg.version_pattern) + return v2rewrite.diff(old_vinfo, new_vinfo, cfg.file_patterns) + + +def _v1_get_diff(cfg: config.Config, new_version: str) -> str: + old_vinfo = v1version.parse_version_info(cfg.current_version, cfg.version_pattern) + new_vinfo = v1version.parse_version_info(new_version, cfg.version_pattern) + return v1rewrite.diff(old_vinfo, new_vinfo, cfg.file_patterns) + + +def get_diff(cfg, new_version) -> str: + if cfg.is_new_pattern: + return _v2_get_diff(cfg, new_version) + else: + return _v1_get_diff(cfg, new_version) + + def _print_diff_str(diff: str) -> None: colored_diff = "\n".join(_colored_diff_lines(diff)) if sys.stdout.isatty(): @@ -349,11 +367,7 @@ def _print_diff_str(diff: str) -> None: def _print_diff(cfg: config.Config, new_version: str) -> None: try: - if cfg.is_new_pattern: - diff = v2cli.get_diff(cfg, new_version) - else: - diff = v1cli.get_diff(cfg, new_version) - + diff = get_diff(cfg, new_version) _print_diff_str(diff) except rewrite.NoPatternMatch as ex: logger.error(str(ex)) @@ -489,9 +503,29 @@ def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: all_tags = vcs.get_tags(fetch=fetch) if cfg.is_new_pattern: - return v2cli.update_cfg_from_vcs(cfg, all_tags) + version_tags = [tag for tag in all_tags if v2version.is_valid(tag, cfg.version_pattern)] else: - return v1cli.update_cfg_from_vcs(cfg, all_tags) + version_tags = [tag for tag in all_tags if v1version.is_valid(tag, cfg.version_pattern)] + + if not version_tags: + logger.debug("no vcs tags found") + return cfg + else: + version_tags.sort(key=pkg_resources.parse_version, reverse=True) + + _debug_tags = ", ".join(version_tags[:3]) + logger.debug(f"found tags: {_debug_tags} ... ({len(version_tags)} in total)") + latest_version_tag = version_tags[0] + latest_version_pep440 = version.to_pep440(latest_version_tag) + if latest_version_tag <= cfg.current_version: + return cfg + else: + logger.info(f"Working dir version : {cfg.current_version}") + logger.info(f"Latest version from VCS tag: {latest_version_tag}") + return cfg._replace( + current_version=latest_version_tag, + pep440_version=latest_version_pep440, + ) @cli.command() diff --git a/src/pycalver/v1cli.py b/src/pycalver/v1cli.py deleted file mode 100755 index 47c3c2d..0000000 --- a/src/pycalver/v1cli.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python -# This file is part of the pycalver project -# https://github.com/mbarkhau/pycalver -# -# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License -# SPDX-License-Identifier: MIT -""" -CLI module for PyCalVer. - -Provided subcommands: show, test, init, bump -""" -import typing as typ -import logging - -from . import config -from . import version -from . import v1rewrite -from . import v1version - -logger = logging.getLogger("pycalver.v1cli") - - -def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.Config: - version_tags = [tag for tag in all_tags if v1version.is_valid(tag, cfg.version_pattern)] - if not version_tags: - logger.debug("no vcs tags found") - return cfg - - version_tags.sort(reverse=True) - _debug_tags = ", ".join(version_tags[:3]) - logger.debug(f"found tags: {_debug_tags} ... ({len(version_tags)} in total)") - latest_version_tag = version_tags[0] - latest_version_pep440 = version.to_pep440(latest_version_tag) - if latest_version_tag <= cfg.current_version: - return cfg - - logger.info(f"Working dir version : {cfg.current_version}") - logger.info(f"Latest version from VCS tag: {latest_version_tag}") - return cfg._replace( - current_version=latest_version_tag, - pep440_version=latest_version_pep440, - ) - - -def get_diff(cfg: config.Config, new_version: str) -> str: - old_vinfo = v1version.parse_version_info(cfg.current_version, cfg.version_pattern) - new_vinfo = v1version.parse_version_info(new_version , cfg.version_pattern) - return v1rewrite.diff(old_vinfo, new_vinfo, cfg.file_patterns) diff --git a/src/pycalver/v2cli.py b/src/pycalver/v2cli.py deleted file mode 100644 index 88f23a4..0000000 --- a/src/pycalver/v2cli.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python -# This file is part of the pycalver project -# https://github.com/mbarkhau/pycalver -# -# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License -# SPDX-License-Identifier: MIT -""" -CLI module for PyCalVer. - -Provided subcommands: show, test, init, bump -""" -import typing as typ -import logging - -from . import config -from . import version -from . import v2rewrite -from . import v2version - -logger = logging.getLogger("pycalver.v2cli") - - -def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.Config: - version_tags = [tag for tag in all_tags if v2version.is_valid(tag, cfg.version_pattern)] - if not version_tags: - logger.debug("no vcs tags found") - return cfg - - version_tags.sort(reverse=True) - _debug_tags = ", ".join(version_tags[:3]) - logger.debug(f"found tags: {_debug_tags} ... ({len(version_tags)} in total)") - latest_version_tag = version_tags[0] - latest_version_pep440 = version.to_pep440(latest_version_tag) - if latest_version_tag <= cfg.current_version: - return cfg - - logger.info(f"Working dir version : {cfg.current_version}") - logger.info(f"Latest version from VCS tag: {latest_version_tag}") - return cfg._replace( - current_version=latest_version_tag, - pep440_version=latest_version_pep440, - ) - - -def get_diff(cfg: config.Config, new_version: str) -> str: - old_vinfo = v2version.parse_version_info(cfg.current_version, cfg.version_pattern) - new_vinfo = v2version.parse_version_info(new_version , cfg.version_pattern) - return v2rewrite.diff(old_vinfo, new_vinfo, cfg.file_patterns) diff --git a/test/test_cli.py b/test/test_cli.py index e3922d7..ca07acd 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -17,12 +17,9 @@ import pytest import pathlib2 as pl from click.testing import CliRunner -from pycalver import v1cli -from pycalver import v2cli from pycalver import config +from pycalver import __main__ as main from pycalver import v1patterns -from pycalver.__main__ import cli -from pycalver.__main__ import incr_dispatch # pylint:disable=redefined-outer-name ; pytest fixtures # pylint:disable=protected-access ; allowed for test code @@ -88,7 +85,7 @@ def runner(tmpdir): def test_help(runner): - result = runner.invoke(cli, ['--help', "-vv"]) + result = runner.invoke(main.cli, ['--help', "-vv"]) assert result.exit_code == 0 assert "PyCalVer" in result.output assert "bump " in result.output @@ -98,7 +95,7 @@ def test_help(runner): def test_version(runner): - result = runner.invoke(cli, ['--version', "-vv"]) + result = runner.invoke(main.cli, ['--version', "-vv"]) assert result.exit_code == 0 assert " version v20" in result.output match = v1patterns.PYCALVER_RE.search(result.output) @@ -109,19 +106,19 @@ def test_incr_default(runner): old_version = "v201701.0004-alpha" cmd = ['test', "-vv", "--pin-date", "--release", "beta", old_version] - result = runner.invoke(cli, cmd) + result = runner.invoke(main.cli, cmd) assert result.exit_code == 0 assert "Version: v201701.0005-beta\n" in result.output cmd = ['test', "-vv", "--pin-date", "--release", "beta", old_version, "vYYYY0M.BUILD[-RELEASE]"] - result = runner.invoke(cli, cmd) + result = runner.invoke(main.cli, cmd) assert result.exit_code == 0 assert "Version: v201701.1005-beta\n" in result.output def test_incr_pin_date(runner): old_version = "v201701.0999-alpha" - result = runner.invoke(cli, ['test', "-vv", "--pin-date", old_version]) + result = runner.invoke(main.cli, ['test', "-vv", "--pin-date", old_version]) assert result.exit_code == 0 assert "Version: v201701.11000-alpha\n" in result.output @@ -137,27 +134,27 @@ def test_incr_semver(runner): old_version = "0.1.0" new_version = "0.1.1" - result = runner.invoke(cli, ['test', "-vv", "--patch", old_version, semver_pattern]) + result = runner.invoke(main.cli, ['test', "-vv", "--patch", old_version, semver_pattern]) assert result.exit_code == 0 assert f"Version: {new_version}\n" in result.output old_version = "0.1.1" new_version = "0.2.0" - result = runner.invoke(cli, ['test', "-vv", "--minor", old_version, semver_pattern]) + result = runner.invoke(main.cli, ['test', "-vv", "--minor", old_version, semver_pattern]) assert result.exit_code == 0 assert f"Version: {new_version}\n" in result.output old_version = "0.1.1" new_version = "1.0.0" - result = runner.invoke(cli, ['test', "-vv", "--major", old_version, semver_pattern]) + result = runner.invoke(main.cli, ['test', "-vv", "--major", old_version, semver_pattern]) assert result.exit_code == 0 assert f"Version: {new_version}\n" in result.output def test_incr_semver_invalid(runner, caplog): - result = runner.invoke(cli, ['test', "-vv", "--patch", "0.1.1"]) + result = runner.invoke(main.cli, ['test', "-vv", "--patch", "0.1.1"]) assert result.exit_code == 1 assert len(caplog.records) > 0 log_record = caplog.records[0] @@ -169,7 +166,7 @@ def test_incr_to_beta(runner): old_version = "v201701.0999-alpha" initial_version = config._initial_version() - result = runner.invoke(cli, ['test', old_version, "-vv", "--release", "beta"]) + result = runner.invoke(main.cli, ['test', old_version, "-vv", "--release", "beta"]) assert result.exit_code == 0 new_version = initial_version.replace(".1001-alpha", ".11000-beta") assert f"Version: {new_version}\n" in result.output @@ -179,7 +176,7 @@ def test_incr_to_final(runner): old_version = "v201701.0999-alpha" initial_version = config._initial_version() - result = runner.invoke(cli, ['test', old_version, "-vv", "--release", "final"]) + result = runner.invoke(main.cli, ['test', old_version, "-vv", "--release", "final"]) assert result.exit_code == 0 new_version = initial_version.replace(".1001-alpha", ".11000") assert f"Version: {new_version}\n" in result.output @@ -191,7 +188,7 @@ def test_incr_release_num(runner): old_version = "0.1.0b0" new_version = "0.1.0b1" - result = runner.invoke(cli, ['test', "-vv", "--release-num", old_version, semver]) + result = runner.invoke(main.cli, ['test', "-vv", "--release-num", old_version, semver]) assert result.exit_code == 0 assert f"Version: {new_version}\n" in result.output @@ -199,7 +196,7 @@ def test_incr_release_num(runner): def test_incr_invalid(runner): old_version = "v201701.0999-alpha" - result = runner.invoke(cli, ['test', old_version, "-vv", "--release", "alfa"]) + result = runner.invoke(main.cli, ['test', old_version, "-vv", "--release", "alfa"]) assert result.exit_code == 1 @@ -239,7 +236,7 @@ def _update_config_val(filename, **kwargs): def test_nocfg(runner, caplog): _add_project_files("README.md") - result = runner.invoke(cli, ['show', "-vv"]) + result = runner.invoke(main.cli, ['show', "-vv"]) assert result.exit_code == 1 expected_msg = "Could not parse configuration. Perhaps try 'pycalver init'." assert any(expected_msg in r.message for r in caplog.records) @@ -248,12 +245,12 @@ def test_nocfg(runner, caplog): def test_novcs_nocfg_init(runner, caplog): _add_project_files("README.md") # dry mode test - result = runner.invoke(cli, ['init', "-vv", "--dry"]) + result = runner.invoke(main.cli, ['init', "-vv", "--dry"]) assert result.exit_code == 0 assert not os.path.exists("pycalver.toml") # non dry mode - result = runner.invoke(cli, ['init', "-vv"]) + result = runner.invoke(main.cli, ['init', "-vv"]) assert result.exit_code == 0 assert os.path.exists("pycalver.toml") @@ -264,12 +261,12 @@ def test_novcs_nocfg_init(runner, caplog): assert base_str in cfg_content assert config.DEFAULT_TOML_README_MD_STR in cfg_content - result = runner.invoke(cli, ['show', "-vv"]) + result = runner.invoke(main.cli, ['show', "-vv"]) assert result.exit_code == 0 assert f"Current Version: {config._initial_version()}\n" in result.output assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output - result = runner.invoke(cli, ['init', "-vv"]) + result = runner.invoke(main.cli, ['init', "-vv"]) assert result.exit_code == 1 # check logging @@ -281,7 +278,7 @@ def test_novcs_nocfg_init(runner, caplog): def test_novcs_setupcfg_init(runner): _add_project_files("README.md", "setup.cfg") - result = runner.invoke(cli, ['init', "-vv"]) + result = runner.invoke(main.cli, ['init', "-vv"]) assert result.exit_code == 0 with pl.Path("setup.cfg").open(mode="r", encoding="utf-8") as fobj: @@ -293,7 +290,7 @@ def test_novcs_setupcfg_init(runner): assert base_str in cfg_content assert config.DEFAULT_CONFIGPARSER_README_MD_STR in cfg_content - result = runner.invoke(cli, ['show', "-vv"]) + result = runner.invoke(main.cli, ['show', "-vv"]) assert result.exit_code == 0 assert f"Current Version: {config._initial_version()}\n" in result.output assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output @@ -301,7 +298,7 @@ def test_novcs_setupcfg_init(runner): def test_novcs_pyproject_init(runner): _add_project_files("README.md", "pyproject.toml") - result = runner.invoke(cli, ['init', "-vv"]) + result = runner.invoke(main.cli, ['init', "-vv"]) assert result.exit_code == 0 with pl.Path("pyproject.toml").open(mode="r", encoding="utf-8") as fobj: @@ -311,7 +308,7 @@ def test_novcs_pyproject_init(runner): assert base_str in cfg_content assert config.DEFAULT_TOML_README_MD_STR in cfg_content - result = runner.invoke(cli, ['show']) + result = runner.invoke(main.cli, ['show']) assert result.exit_code == 0 assert f"Current Version: {config._initial_version()}\n" in result.output assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output @@ -340,12 +337,12 @@ def test_git_init(runner, version_pattern): _add_project_files("README.md") _vcs_init("git") - result = runner.invoke(cli, ['init', "-vv"]) + result = runner.invoke(main.cli, ['init', "-vv"]) assert result.exit_code == 0 _update_config_val("pycalver.toml", version_pattern=version_pattern) - result = runner.invoke(cli, ['show']) + result = runner.invoke(main.cli, ['show']) assert result.exit_code == 0 assert f"Current Version: {config._initial_version()}\n" in result.output assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output @@ -356,12 +353,12 @@ def test_hg_init(runner, version_pattern): _add_project_files("README.md") _vcs_init("hg") - result = runner.invoke(cli, ['init', "-vv"]) + result = runner.invoke(main.cli, ['init', "-vv"]) assert result.exit_code == 0 _update_config_val("pycalver.toml", version_pattern=version_pattern) - result = runner.invoke(cli, ['show']) + result = runner.invoke(main.cli, ['show']) assert result.exit_code == 0 assert f"Current Version: {config._initial_version()}\n" in result.output assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output @@ -374,7 +371,7 @@ def test_v1_git_tag_eval(runner, version_pattern): # This will set a version that is older than the version tag # we set in the vcs, which should take precedence. - result = runner.invoke(cli, ['init', "-vv"]) + result = runner.invoke(main.cli, ['init', "-vv"]) assert result.exit_code == 0 _update_config_val("pycalver.toml", version_pattern=version_pattern) @@ -385,7 +382,7 @@ def test_v1_git_tag_eval(runner, version_pattern): shell("git", "tag", "--annotate", tag_version, "--message", f"bump version to {tag_version}") - result = runner.invoke(cli, ['show', "-vv"]) + result = runner.invoke(main.cli, ['show', "-vv"]) assert result.exit_code == 0 assert f"Current Version: {tag_version}\n" in result.output assert f"PEP440 : {tag_version_pep440}\n" in result.output @@ -398,7 +395,7 @@ def test_hg_tag_eval(runner, version_pattern): # This will set a version that is older than the version tag # we set in the vcs, which should take precedence. - result = runner.invoke(cli, ['init', "-vv"]) + result = runner.invoke(main.cli, ['init', "-vv"]) assert result.exit_code == 0 _update_config_val("pycalver.toml", version_pattern=version_pattern) @@ -409,7 +406,7 @@ def test_hg_tag_eval(runner, version_pattern): shell("hg", "tag", tag_version, "--message", f"bump version to {tag_version}") - result = runner.invoke(cli, ['show', "-vv"]) + result = runner.invoke(main.cli, ['show', "-vv"]) assert result.exit_code == 0 assert f"Current Version: {tag_version}\n" in result.output assert f"PEP440 : {tag_version_pep440}\n" in result.output @@ -419,12 +416,12 @@ def test_hg_tag_eval(runner, version_pattern): def test_novcs_bump(runner, version_pattern): _add_project_files("README.md") - result = runner.invoke(cli, ['init', "-vv"]) + result = runner.invoke(main.cli, ['init', "-vv"]) assert result.exit_code == 0 _update_config_val("pycalver.toml", version_pattern=version_pattern) - result = runner.invoke(cli, ['bump', "-vv"]) + result = runner.invoke(main.cli, ['bump', "-vv"]) assert result.exit_code == 0 calver = config._initial_version().split(".")[0] @@ -434,7 +431,7 @@ def test_novcs_bump(runner, version_pattern): assert calver + ".1002-alpha !\n" in content assert calver[1:] + ".1002a0 !]\n" in content - result = runner.invoke(cli, ['bump', "-vv", "--release", "beta"]) + result = runner.invoke(main.cli, ['bump', "-vv", "--release", "beta"]) assert result.exit_code == 0 with pl.Path("README.md").open() as fobj: @@ -448,7 +445,7 @@ def test_git_bump(runner, version_pattern): _add_project_files("README.md") _vcs_init("git") - result = runner.invoke(cli, ['init', "-vv"]) + result = runner.invoke(main.cli, ['init', "-vv"]) assert result.exit_code == 0 _update_config_val("pycalver.toml", version_pattern=version_pattern) @@ -456,7 +453,7 @@ def test_git_bump(runner, version_pattern): shell("git", "add", "pycalver.toml") shell("git", "commit", "-m", "initial commit") - result = runner.invoke(cli, ['bump', "-vv"]) + result = runner.invoke(main.cli, ['bump', "-vv"]) assert result.exit_code == 0 calver = config._initial_version()[:7] @@ -471,7 +468,7 @@ def test_hg_bump(runner, version_pattern): _add_project_files("README.md") _vcs_init("hg") - result = runner.invoke(cli, ['init', "-vv"]) + result = runner.invoke(main.cli, ['init', "-vv"]) assert result.exit_code == 0 _update_config_val("pycalver.toml", version_pattern=version_pattern) @@ -479,7 +476,7 @@ def test_hg_bump(runner, version_pattern): shell("hg", "add", "pycalver.toml") shell("hg", "commit", "-m", "initial commit") - result = runner.invoke(cli, ['bump', "-vv"]) + result = runner.invoke(main.cli, ['bump', "-vv"]) assert result.exit_code == 0 calver = config._initial_version()[:7] @@ -493,7 +490,7 @@ def test_empty_git_bump(runner, caplog): shell("git", "init") with pl.Path("setup.cfg").open(mode="w") as fobj: fobj.write("") - result = runner.invoke(cli, ['init', "-vv"]) + result = runner.invoke(main.cli, ['init', "-vv"]) assert result.exit_code == 0 with pl.Path("setup.cfg").open(mode="r") as fobj: @@ -504,7 +501,7 @@ def test_empty_git_bump(runner, caplog): assert "\n[pycalver:file_patterns]\n" in default_cfg_data assert "\nsetup.cfg =\n" in default_cfg_data - result = runner.invoke(cli, ['bump']) + result = runner.invoke(main.cli, ['bump']) assert any(("working directory is not clean" in r.message) for r in caplog.records) assert any(("setup.cfg" in r.message) for r in caplog.records) @@ -514,7 +511,7 @@ def test_empty_hg_bump(runner, caplog): shell("hg", "init") with pl.Path("setup.cfg").open(mode="w") as fobj: fobj.write("") - result = runner.invoke(cli, ['init', "-vv"]) + result = runner.invoke(main.cli, ['init', "-vv"]) assert result.exit_code == 0 with pl.Path("setup.cfg").open(mode="r") as fobj: @@ -525,7 +522,7 @@ def test_empty_hg_bump(runner, caplog): assert "\n[pycalver:file_patterns]\n" in default_cfg_text assert "\nsetup.cfg =\n" in default_cfg_text - result = runner.invoke(cli, ['bump']) + result = runner.invoke(main.cli, ['bump']) assert any(("working directory is not clean" in r.message) for r in caplog.records) assert any(("setup.cfg" in r.message) for r in caplog.records) @@ -565,13 +562,13 @@ def test_v1_bump_semver_warning(runner, caplog, version_pattern): _vcs_init("hg", files=["README.md", "setup.cfg"]) - result = runner.invoke(cli, ['bump', "-vv", "-n", "--dry"]) + result = runner.invoke(main.cli, ['bump', "-vv", "-n", "--dry"]) assert result.exit_code == 1 assert any("version did not change" in r.message for r in caplog.records) assert any("--major/--minor/--patch required" in r.message for r in caplog.records) - result = runner.invoke(cli, ['bump', "-vv", "-n", "--dry", "--patch"]) + result = runner.invoke(main.cli, ['bump', "-vv", "-n", "--dry", "--patch"]) assert result.exit_code == 0 @@ -589,7 +586,7 @@ def test_v1_bump_semver_diff(runner, caplog, version_pattern): cases = [("--major", "1.0.0"), ("--minor", "0.2.0"), ("--patch", "0.1.1")] for flag, expected in cases: - result = runner.invoke(cli, ['bump', "-vv", "-n", "--dry", flag]) + result = runner.invoke(main.cli, ['bump', "-vv", "-n", "--dry", flag]) assert result.exit_code == 0 assert len(caplog.records) == 0 @@ -603,7 +600,7 @@ def test_v1_bump_semver_diff(runner, caplog, version_pattern): @pytest.mark.parametrize("version_pattern", DEFAULT_VERSION_PATTERNS) def test_get_diff(runner, version_pattern): _add_project_files("README.md", "setup.cfg") - result = runner.invoke(cli, ['init', "-vv"]) + result = runner.invoke(main.cli, ['init', "-vv"]) assert result.exit_code == 0 _update_config_val("setup.cfg", version_pattern=version_pattern) @@ -611,11 +608,7 @@ def test_get_diff(runner, version_pattern): _, cfg = config.init() new_version = "v202010.1003-beta" - if cfg.is_new_pattern: - diff_str = v2cli.get_diff(cfg, new_version) - else: - diff_str = v1cli.get_diff(cfg, new_version) - + diff_str = main.get_diff(cfg, new_version) diff_lines = set(diff_str.splitlines()) assert "- Hello World v201701.1002-alpha !" in diff_lines @@ -674,14 +667,14 @@ WEEKNUM_TEST_CASES = [ @pytest.mark.parametrize("date, pattern, expected", WEEKNUM_TEST_CASES) def test_weeknum(date, pattern, expected, runner): cmd = shlex.split(f"test -vv --date {date} 2020.40 {pattern}") - result = runner.invoke(cli, cmd) + result = runner.invoke(main.cli, cmd) assert result.exit_code == 0 assert "New Version: " + expected in result.output def test_hg_commit_message(runner, caplog): _add_project_files("README.md", "setup.cfg") - result = runner.invoke(cli, ['init', "-vv"]) + result = runner.invoke(main.cli, ['init', "-vv"]) assert result.exit_code == 0 commit_message = """ @@ -693,7 +686,7 @@ def test_hg_commit_message(runner, caplog): _vcs_init("hg", ["README.md", "setup.cfg"]) assert len(caplog.records) > 0 - result = runner.invoke(cli, ['bump', "-vv", "--pin-date", "--release", "beta"]) + result = runner.invoke(main.cli, ['bump', "-vv", "--pin-date", "--release", "beta"]) assert result.exit_code == 0 tags = shell("hg", "tags").decode("utf-8") @@ -708,7 +701,7 @@ def test_hg_commit_message(runner, caplog): def test_git_commit_message(runner, caplog): _add_project_files("README.md", "setup.cfg") - result = runner.invoke(cli, ['init', "-vv"]) + result = runner.invoke(main.cli, ['init', "-vv"]) assert result.exit_code == 0 commit_message = """ @@ -720,7 +713,7 @@ def test_git_commit_message(runner, caplog): _vcs_init("git", ["README.md", "setup.cfg"]) assert len(caplog.records) > 0 - result = runner.invoke(cli, ['bump', "-vv", "--pin-date", "--release", "beta"]) + result = runner.invoke(main.cli, ['bump', "-vv", "--pin-date", "--release", "beta"]) assert result.exit_code == 0 tags = shell("git", "tag", "--list").decode("utf-8") @@ -739,13 +732,13 @@ def test_grep(runner): search_re = r"^\s+2:\s+Hello World v201701\.1002-alpha !" cmd1 = r'grep "vYYYY0M.BUILD[-RELEASE]" README.md' - result1 = runner.invoke(cli, shlex.split(cmd1)) + result1 = runner.invoke(main.cli, shlex.split(cmd1)) assert result1.exit_code == 0 assert "README.md" in result1.output assert re.search(search_re, result1.output, flags=re.MULTILINE) cmd2 = r'grep --version-pattern "vYYYY0M.BUILD[-RELEASE]" "{version}" README.md' - result2 = runner.invoke(cli, shlex.split(cmd2)) + result2 = runner.invoke(main.cli, shlex.split(cmd2)) assert result2.exit_code == 0 assert "README.md" in result2.output assert re.search(search_re, result2.output, flags=re.MULTILINE) @@ -755,7 +748,7 @@ def test_grep(runner): search_re = r"^\s+3:\s+\[aka\. 201701\.1002a0 \!\]" cmd3 = r'grep "\[aka. YYYY0M.BLD[PYTAGNUM] \!\]" README.md' - result3 = runner.invoke(cli, shlex.split(cmd3)) + result3 = runner.invoke(main.cli, shlex.split(cmd3)) assert result3.exit_code == 0 assert "README.md" in result3.output assert re.search(search_re, result3.output, flags=re.MULTILINE) @@ -763,7 +756,7 @@ def test_grep(runner): cmd4 = ( r'grep --version-pattern "vYYYY0M.BUILD[-RELEASE]" "\[aka. {pep440_version} \!\]" README.md' ) - result4 = runner.invoke(cli, shlex.split(cmd4)) + result4 = runner.invoke(main.cli, shlex.split(cmd4)) assert result4.exit_code == 0 assert "README.md" in result4.output assert re.search(search_re, result4.output, flags=re.MULTILINE) @@ -791,7 +784,7 @@ def test_multimatch_file_patterns(runner): with pl.Path("setup.cfg").open(mode="w", encoding="utf-8") as fobj: fobj.write(SETUP_CFG_MULTIMATCH_FILE_PATTERNS_FIXTURE) - result = runner.invoke(cli, ['bump', '--release', 'beta']) + result = runner.invoke(main.cli, ['bump', '--release', 'beta']) assert result.exit_code == 0 with pl.Path("README.md").open(mode="r", encoding="utf-8") as fobj: @@ -831,7 +824,7 @@ ROLLOVER_TEST_CASES = [ @pytest.mark.parametrize("version_pattern, old_version, expected, kwargs", ROLLOVER_TEST_CASES) def test_rollover(version_pattern, old_version, expected, kwargs): - new_version = incr_dispatch(old_version, raw_pattern=version_pattern, **kwargs) + new_version = main.incr_dispatch(old_version, raw_pattern=version_pattern, **kwargs) if new_version is None: assert expected is None else: From 768b34ffb87a1bae9750b96c8d1cde34a64b7a77 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Mon, 5 Oct 2020 18:20:16 +0000 Subject: [PATCH 73/98] add regression test for #12 --- src/pycalver/__main__.py | 21 ++++++++++++++------- test/test_cli.py | 25 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index ea399ee..58fb6dd 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -499,7 +499,7 @@ def init(verbose: int = 0, dry: bool = False) -> None: config.write_content(ctx) -def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: +def get_latest_vcs_version_tag(cfg: config.Config, fetch: bool) -> typ.Optional[str]: all_tags = vcs.get_tags(fetch=fetch) if cfg.is_new_pattern: @@ -507,17 +507,24 @@ def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: else: version_tags = [tag for tag in all_tags if v1version.is_valid(tag, cfg.version_pattern)] - if not version_tags: + if version_tags: + version_tags.sort(key=pkg_resources.parse_version, reverse=True) + _debug_tags = ", ".join(version_tags[:3]) + logger.debug(f"found tags: {_debug_tags} ... ({len(version_tags)} in total)") + return version_tags[0] + else: + return None + + +def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: + latest_version_tag = get_latest_vcs_version_tag(cfg, fetch) + if latest_version_tag is None: logger.debug("no vcs tags found") return cfg else: - version_tags.sort(key=pkg_resources.parse_version, reverse=True) - - _debug_tags = ", ".join(version_tags[:3]) - logger.debug(f"found tags: {_debug_tags} ... ({len(version_tags)} in total)") - latest_version_tag = version_tags[0] latest_version_pep440 = version.to_pep440(latest_version_tag) if latest_version_tag <= cfg.current_version: + # current_version already newer/up-to-date return cfg else: logger.info(f"Working dir version : {cfg.current_version}") diff --git a/test/test_cli.py b/test/test_cli.py index ca07acd..49f856a 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -829,3 +829,28 @@ def test_rollover(version_pattern, old_version, expected, kwargs): assert expected is None else: assert new_version == expected + + +def test_get_latest_vcs_version_tag(runner): + result = runner.invoke(main.cli, ['init', "-vv"]) + assert result.exit_code == 0 + + _update_config_val("pycalver.toml", push="false") + _update_config_val("pycalver.toml", current_version='"0.1.8"') + _update_config_val("pycalver.toml", version_pattern='"MAJOR.MINOR.PATCH"') + + _vcs_init("git", files=["pycalver.toml"]) + + result = runner.invoke(main.cli, ['bump', "--patch"]) + assert result.exit_code == 0 + + _, cfg = config.init() + latest_version = main.get_latest_vcs_version_tag(cfg, fetch=False) + assert latest_version == "0.1.9" + + result = runner.invoke(main.cli, ['bump', "--patch"]) + assert result.exit_code == 0 + + _, cfg = config.init() + latest_version = main.get_latest_vcs_version_tag(cfg, fetch=False) + assert latest_version == "0.1.10" From 792e9356f9e2cf40788dc75c0f33eb4bb7842b6f Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Mon, 5 Oct 2020 18:22:34 +0000 Subject: [PATCH 74/98] update keywords --- bootstrapit.sh | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bootstrapit.sh b/bootstrapit.sh index d88ce11..40cc5fc 100644 --- a/bootstrapit.sh +++ b/bootstrapit.sh @@ -4,7 +4,7 @@ AUTHOR_NAME="Manuel Barkhau" AUTHOR_EMAIL="mbarkhau@gmail.com" -KEYWORDS="version versioning bumpversion calver" +KEYWORDS="version versioning calver semver bumpversion pep440" DESCRIPTION="CalVer for python packages." LICENSE_ID="MIT" diff --git a/setup.py b/setup.py index 27132c5..afae4c7 100644 --- a/setup.py +++ b/setup.py @@ -64,7 +64,7 @@ setuptools.setup( author_email="mbarkhau@gmail.com", url="https://github.com/mbarkhau/pycalver", version="202010.1040b0", - keywords="version versioning bumpversion calver", + keywords="version versioning calver semver bumpversion pep440", description="CalVer for python libraries.", long_description=long_description, long_description_content_type="text/markdown", From a86e5827ab62ab51fc082e2cb23b3590f0922458 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Mon, 5 Oct 2020 19:23:57 +0000 Subject: [PATCH 75/98] mv __main__.py cli.py --- src/pycalver/{__main__.py => cli.py} | 12 +-- test/test_cli.py | 126 +++++++++++++-------------- 2 files changed, 65 insertions(+), 73 deletions(-) rename src/pycalver/{__main__.py => cli.py} (99%) diff --git a/src/pycalver/__main__.py b/src/pycalver/cli.py similarity index 99% rename from src/pycalver/__main__.py rename to src/pycalver/cli.py index 58fb6dd..a42322b 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/cli.py @@ -4,11 +4,7 @@ # # Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License # SPDX-License-Identifier: MIT -""" -__main__ module for PyCalVer. - -Enables use as module: $ python -m pycalver --version -""" +"""cli module for PyCalVer.""" import io import sys import typing as typ @@ -43,7 +39,7 @@ except ImportError: click.disable_unicode_literals_warning = True -logger = logging.getLogger("pycalver.__main__") +logger = logging.getLogger("pycalver.cli") _VERBOSE = 0 @@ -659,7 +655,3 @@ def bump( commit_message = cfg.commit_message.format(**commit_message_kwargs) _try_bump(cfg, new_version, commit_message, allow_dirty) - - -if __name__ == '__main__': - cli() diff --git a/test/test_cli.py b/test/test_cli.py index 49f856a..c67a8f0 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -18,7 +18,7 @@ import pathlib2 as pl from click.testing import CliRunner from pycalver import config -from pycalver import __main__ as main +from pycalver import cli from pycalver import v1patterns # pylint:disable=redefined-outer-name ; pytest fixtures @@ -85,7 +85,7 @@ def runner(tmpdir): def test_help(runner): - result = runner.invoke(main.cli, ['--help', "-vv"]) + result = runner.invoke(cli.cli, ['--help', "-vv"]) assert result.exit_code == 0 assert "PyCalVer" in result.output assert "bump " in result.output @@ -95,7 +95,7 @@ def test_help(runner): def test_version(runner): - result = runner.invoke(main.cli, ['--version', "-vv"]) + result = runner.invoke(cli.cli, ['--version', "-vv"]) assert result.exit_code == 0 assert " version v20" in result.output match = v1patterns.PYCALVER_RE.search(result.output) @@ -106,19 +106,19 @@ def test_incr_default(runner): old_version = "v201701.0004-alpha" cmd = ['test', "-vv", "--pin-date", "--release", "beta", old_version] - result = runner.invoke(main.cli, cmd) + result = runner.invoke(cli.cli, cmd) assert result.exit_code == 0 assert "Version: v201701.0005-beta\n" in result.output cmd = ['test', "-vv", "--pin-date", "--release", "beta", old_version, "vYYYY0M.BUILD[-RELEASE]"] - result = runner.invoke(main.cli, cmd) + result = runner.invoke(cli.cli, cmd) assert result.exit_code == 0 assert "Version: v201701.1005-beta\n" in result.output def test_incr_pin_date(runner): old_version = "v201701.0999-alpha" - result = runner.invoke(main.cli, ['test', "-vv", "--pin-date", old_version]) + result = runner.invoke(cli.cli, ['test', "-vv", "--pin-date", old_version]) assert result.exit_code == 0 assert "Version: v201701.11000-alpha\n" in result.output @@ -134,27 +134,27 @@ def test_incr_semver(runner): old_version = "0.1.0" new_version = "0.1.1" - result = runner.invoke(main.cli, ['test', "-vv", "--patch", old_version, semver_pattern]) + result = runner.invoke(cli.cli, ['test', "-vv", "--patch", old_version, semver_pattern]) assert result.exit_code == 0 assert f"Version: {new_version}\n" in result.output old_version = "0.1.1" new_version = "0.2.0" - result = runner.invoke(main.cli, ['test', "-vv", "--minor", old_version, semver_pattern]) + result = runner.invoke(cli.cli, ['test', "-vv", "--minor", old_version, semver_pattern]) assert result.exit_code == 0 assert f"Version: {new_version}\n" in result.output old_version = "0.1.1" new_version = "1.0.0" - result = runner.invoke(main.cli, ['test', "-vv", "--major", old_version, semver_pattern]) + result = runner.invoke(cli.cli, ['test', "-vv", "--major", old_version, semver_pattern]) assert result.exit_code == 0 assert f"Version: {new_version}\n" in result.output def test_incr_semver_invalid(runner, caplog): - result = runner.invoke(main.cli, ['test', "-vv", "--patch", "0.1.1"]) + result = runner.invoke(cli.cli, ['test', "-vv", "--patch", "0.1.1"]) assert result.exit_code == 1 assert len(caplog.records) > 0 log_record = caplog.records[0] @@ -166,7 +166,7 @@ def test_incr_to_beta(runner): old_version = "v201701.0999-alpha" initial_version = config._initial_version() - result = runner.invoke(main.cli, ['test', old_version, "-vv", "--release", "beta"]) + result = runner.invoke(cli.cli, ['test', old_version, "-vv", "--release", "beta"]) assert result.exit_code == 0 new_version = initial_version.replace(".1001-alpha", ".11000-beta") assert f"Version: {new_version}\n" in result.output @@ -176,7 +176,7 @@ def test_incr_to_final(runner): old_version = "v201701.0999-alpha" initial_version = config._initial_version() - result = runner.invoke(main.cli, ['test', old_version, "-vv", "--release", "final"]) + result = runner.invoke(cli.cli, ['test', old_version, "-vv", "--release", "final"]) assert result.exit_code == 0 new_version = initial_version.replace(".1001-alpha", ".11000") assert f"Version: {new_version}\n" in result.output @@ -188,7 +188,7 @@ def test_incr_release_num(runner): old_version = "0.1.0b0" new_version = "0.1.0b1" - result = runner.invoke(main.cli, ['test', "-vv", "--release-num", old_version, semver]) + result = runner.invoke(cli.cli, ['test', "-vv", "--release-num", old_version, semver]) assert result.exit_code == 0 assert f"Version: {new_version}\n" in result.output @@ -196,7 +196,7 @@ def test_incr_release_num(runner): def test_incr_invalid(runner): old_version = "v201701.0999-alpha" - result = runner.invoke(main.cli, ['test', old_version, "-vv", "--release", "alfa"]) + result = runner.invoke(cli.cli, ['test', old_version, "-vv", "--release", "alfa"]) assert result.exit_code == 1 @@ -236,7 +236,7 @@ def _update_config_val(filename, **kwargs): def test_nocfg(runner, caplog): _add_project_files("README.md") - result = runner.invoke(main.cli, ['show', "-vv"]) + result = runner.invoke(cli.cli, ['show', "-vv"]) assert result.exit_code == 1 expected_msg = "Could not parse configuration. Perhaps try 'pycalver init'." assert any(expected_msg in r.message for r in caplog.records) @@ -245,12 +245,12 @@ def test_nocfg(runner, caplog): def test_novcs_nocfg_init(runner, caplog): _add_project_files("README.md") # dry mode test - result = runner.invoke(main.cli, ['init', "-vv", "--dry"]) + result = runner.invoke(cli.cli, ['init', "-vv", "--dry"]) assert result.exit_code == 0 assert not os.path.exists("pycalver.toml") # non dry mode - result = runner.invoke(main.cli, ['init', "-vv"]) + result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 assert os.path.exists("pycalver.toml") @@ -261,12 +261,12 @@ def test_novcs_nocfg_init(runner, caplog): assert base_str in cfg_content assert config.DEFAULT_TOML_README_MD_STR in cfg_content - result = runner.invoke(main.cli, ['show', "-vv"]) + result = runner.invoke(cli.cli, ['show', "-vv"]) assert result.exit_code == 0 assert f"Current Version: {config._initial_version()}\n" in result.output assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output - result = runner.invoke(main.cli, ['init', "-vv"]) + result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 1 # check logging @@ -278,7 +278,7 @@ def test_novcs_nocfg_init(runner, caplog): def test_novcs_setupcfg_init(runner): _add_project_files("README.md", "setup.cfg") - result = runner.invoke(main.cli, ['init', "-vv"]) + result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 with pl.Path("setup.cfg").open(mode="r", encoding="utf-8") as fobj: @@ -290,7 +290,7 @@ def test_novcs_setupcfg_init(runner): assert base_str in cfg_content assert config.DEFAULT_CONFIGPARSER_README_MD_STR in cfg_content - result = runner.invoke(main.cli, ['show', "-vv"]) + result = runner.invoke(cli.cli, ['show', "-vv"]) assert result.exit_code == 0 assert f"Current Version: {config._initial_version()}\n" in result.output assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output @@ -298,7 +298,7 @@ def test_novcs_setupcfg_init(runner): def test_novcs_pyproject_init(runner): _add_project_files("README.md", "pyproject.toml") - result = runner.invoke(main.cli, ['init', "-vv"]) + result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 with pl.Path("pyproject.toml").open(mode="r", encoding="utf-8") as fobj: @@ -308,7 +308,7 @@ def test_novcs_pyproject_init(runner): assert base_str in cfg_content assert config.DEFAULT_TOML_README_MD_STR in cfg_content - result = runner.invoke(main.cli, ['show']) + result = runner.invoke(cli.cli, ['show']) assert result.exit_code == 0 assert f"Current Version: {config._initial_version()}\n" in result.output assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output @@ -337,12 +337,12 @@ def test_git_init(runner, version_pattern): _add_project_files("README.md") _vcs_init("git") - result = runner.invoke(main.cli, ['init', "-vv"]) + result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 _update_config_val("pycalver.toml", version_pattern=version_pattern) - result = runner.invoke(main.cli, ['show']) + result = runner.invoke(cli.cli, ['show']) assert result.exit_code == 0 assert f"Current Version: {config._initial_version()}\n" in result.output assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output @@ -353,12 +353,12 @@ def test_hg_init(runner, version_pattern): _add_project_files("README.md") _vcs_init("hg") - result = runner.invoke(main.cli, ['init', "-vv"]) + result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 _update_config_val("pycalver.toml", version_pattern=version_pattern) - result = runner.invoke(main.cli, ['show']) + result = runner.invoke(cli.cli, ['show']) assert result.exit_code == 0 assert f"Current Version: {config._initial_version()}\n" in result.output assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output @@ -371,7 +371,7 @@ def test_v1_git_tag_eval(runner, version_pattern): # This will set a version that is older than the version tag # we set in the vcs, which should take precedence. - result = runner.invoke(main.cli, ['init', "-vv"]) + result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 _update_config_val("pycalver.toml", version_pattern=version_pattern) @@ -382,7 +382,7 @@ def test_v1_git_tag_eval(runner, version_pattern): shell("git", "tag", "--annotate", tag_version, "--message", f"bump version to {tag_version}") - result = runner.invoke(main.cli, ['show', "-vv"]) + result = runner.invoke(cli.cli, ['show', "-vv"]) assert result.exit_code == 0 assert f"Current Version: {tag_version}\n" in result.output assert f"PEP440 : {tag_version_pep440}\n" in result.output @@ -395,7 +395,7 @@ def test_hg_tag_eval(runner, version_pattern): # This will set a version that is older than the version tag # we set in the vcs, which should take precedence. - result = runner.invoke(main.cli, ['init', "-vv"]) + result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 _update_config_val("pycalver.toml", version_pattern=version_pattern) @@ -406,7 +406,7 @@ def test_hg_tag_eval(runner, version_pattern): shell("hg", "tag", tag_version, "--message", f"bump version to {tag_version}") - result = runner.invoke(main.cli, ['show', "-vv"]) + result = runner.invoke(cli.cli, ['show', "-vv"]) assert result.exit_code == 0 assert f"Current Version: {tag_version}\n" in result.output assert f"PEP440 : {tag_version_pep440}\n" in result.output @@ -416,12 +416,12 @@ def test_hg_tag_eval(runner, version_pattern): def test_novcs_bump(runner, version_pattern): _add_project_files("README.md") - result = runner.invoke(main.cli, ['init', "-vv"]) + result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 _update_config_val("pycalver.toml", version_pattern=version_pattern) - result = runner.invoke(main.cli, ['bump', "-vv"]) + result = runner.invoke(cli.cli, ['bump', "-vv"]) assert result.exit_code == 0 calver = config._initial_version().split(".")[0] @@ -431,7 +431,7 @@ def test_novcs_bump(runner, version_pattern): assert calver + ".1002-alpha !\n" in content assert calver[1:] + ".1002a0 !]\n" in content - result = runner.invoke(main.cli, ['bump', "-vv", "--release", "beta"]) + result = runner.invoke(cli.cli, ['bump', "-vv", "--release", "beta"]) assert result.exit_code == 0 with pl.Path("README.md").open() as fobj: @@ -445,7 +445,7 @@ def test_git_bump(runner, version_pattern): _add_project_files("README.md") _vcs_init("git") - result = runner.invoke(main.cli, ['init', "-vv"]) + result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 _update_config_val("pycalver.toml", version_pattern=version_pattern) @@ -453,7 +453,7 @@ def test_git_bump(runner, version_pattern): shell("git", "add", "pycalver.toml") shell("git", "commit", "-m", "initial commit") - result = runner.invoke(main.cli, ['bump', "-vv"]) + result = runner.invoke(cli.cli, ['bump', "-vv"]) assert result.exit_code == 0 calver = config._initial_version()[:7] @@ -468,7 +468,7 @@ def test_hg_bump(runner, version_pattern): _add_project_files("README.md") _vcs_init("hg") - result = runner.invoke(main.cli, ['init', "-vv"]) + result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 _update_config_val("pycalver.toml", version_pattern=version_pattern) @@ -476,7 +476,7 @@ def test_hg_bump(runner, version_pattern): shell("hg", "add", "pycalver.toml") shell("hg", "commit", "-m", "initial commit") - result = runner.invoke(main.cli, ['bump', "-vv"]) + result = runner.invoke(cli.cli, ['bump', "-vv"]) assert result.exit_code == 0 calver = config._initial_version()[:7] @@ -490,7 +490,7 @@ def test_empty_git_bump(runner, caplog): shell("git", "init") with pl.Path("setup.cfg").open(mode="w") as fobj: fobj.write("") - result = runner.invoke(main.cli, ['init', "-vv"]) + result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 with pl.Path("setup.cfg").open(mode="r") as fobj: @@ -501,7 +501,7 @@ def test_empty_git_bump(runner, caplog): assert "\n[pycalver:file_patterns]\n" in default_cfg_data assert "\nsetup.cfg =\n" in default_cfg_data - result = runner.invoke(main.cli, ['bump']) + result = runner.invoke(cli.cli, ['bump']) assert any(("working directory is not clean" in r.message) for r in caplog.records) assert any(("setup.cfg" in r.message) for r in caplog.records) @@ -511,7 +511,7 @@ def test_empty_hg_bump(runner, caplog): shell("hg", "init") with pl.Path("setup.cfg").open(mode="w") as fobj: fobj.write("") - result = runner.invoke(main.cli, ['init', "-vv"]) + result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 with pl.Path("setup.cfg").open(mode="r") as fobj: @@ -522,7 +522,7 @@ def test_empty_hg_bump(runner, caplog): assert "\n[pycalver:file_patterns]\n" in default_cfg_text assert "\nsetup.cfg =\n" in default_cfg_text - result = runner.invoke(main.cli, ['bump']) + result = runner.invoke(cli.cli, ['bump']) assert any(("working directory is not clean" in r.message) for r in caplog.records) assert any(("setup.cfg" in r.message) for r in caplog.records) @@ -562,13 +562,13 @@ def test_v1_bump_semver_warning(runner, caplog, version_pattern): _vcs_init("hg", files=["README.md", "setup.cfg"]) - result = runner.invoke(main.cli, ['bump', "-vv", "-n", "--dry"]) + result = runner.invoke(cli.cli, ['bump', "-vv", "-n", "--dry"]) assert result.exit_code == 1 assert any("version did not change" in r.message for r in caplog.records) assert any("--major/--minor/--patch required" in r.message for r in caplog.records) - result = runner.invoke(main.cli, ['bump', "-vv", "-n", "--dry", "--patch"]) + result = runner.invoke(cli.cli, ['bump', "-vv", "-n", "--dry", "--patch"]) assert result.exit_code == 0 @@ -586,7 +586,7 @@ def test_v1_bump_semver_diff(runner, caplog, version_pattern): cases = [("--major", "1.0.0"), ("--minor", "0.2.0"), ("--patch", "0.1.1")] for flag, expected in cases: - result = runner.invoke(main.cli, ['bump', "-vv", "-n", "--dry", flag]) + result = runner.invoke(cli.cli, ['bump', "-vv", "-n", "--dry", flag]) assert result.exit_code == 0 assert len(caplog.records) == 0 @@ -600,7 +600,7 @@ def test_v1_bump_semver_diff(runner, caplog, version_pattern): @pytest.mark.parametrize("version_pattern", DEFAULT_VERSION_PATTERNS) def test_get_diff(runner, version_pattern): _add_project_files("README.md", "setup.cfg") - result = runner.invoke(main.cli, ['init', "-vv"]) + result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 _update_config_val("setup.cfg", version_pattern=version_pattern) @@ -608,7 +608,7 @@ def test_get_diff(runner, version_pattern): _, cfg = config.init() new_version = "v202010.1003-beta" - diff_str = main.get_diff(cfg, new_version) + diff_str = cli.get_diff(cfg, new_version) diff_lines = set(diff_str.splitlines()) assert "- Hello World v201701.1002-alpha !" in diff_lines @@ -667,14 +667,14 @@ WEEKNUM_TEST_CASES = [ @pytest.mark.parametrize("date, pattern, expected", WEEKNUM_TEST_CASES) def test_weeknum(date, pattern, expected, runner): cmd = shlex.split(f"test -vv --date {date} 2020.40 {pattern}") - result = runner.invoke(main.cli, cmd) + result = runner.invoke(cli.cli, cmd) assert result.exit_code == 0 assert "New Version: " + expected in result.output def test_hg_commit_message(runner, caplog): _add_project_files("README.md", "setup.cfg") - result = runner.invoke(main.cli, ['init', "-vv"]) + result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 commit_message = """ @@ -686,7 +686,7 @@ def test_hg_commit_message(runner, caplog): _vcs_init("hg", ["README.md", "setup.cfg"]) assert len(caplog.records) > 0 - result = runner.invoke(main.cli, ['bump', "-vv", "--pin-date", "--release", "beta"]) + result = runner.invoke(cli.cli, ['bump', "-vv", "--pin-date", "--release", "beta"]) assert result.exit_code == 0 tags = shell("hg", "tags").decode("utf-8") @@ -701,7 +701,7 @@ def test_hg_commit_message(runner, caplog): def test_git_commit_message(runner, caplog): _add_project_files("README.md", "setup.cfg") - result = runner.invoke(main.cli, ['init', "-vv"]) + result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 commit_message = """ @@ -713,7 +713,7 @@ def test_git_commit_message(runner, caplog): _vcs_init("git", ["README.md", "setup.cfg"]) assert len(caplog.records) > 0 - result = runner.invoke(main.cli, ['bump', "-vv", "--pin-date", "--release", "beta"]) + result = runner.invoke(cli.cli, ['bump', "-vv", "--pin-date", "--release", "beta"]) assert result.exit_code == 0 tags = shell("git", "tag", "--list").decode("utf-8") @@ -732,13 +732,13 @@ def test_grep(runner): search_re = r"^\s+2:\s+Hello World v201701\.1002-alpha !" cmd1 = r'grep "vYYYY0M.BUILD[-RELEASE]" README.md' - result1 = runner.invoke(main.cli, shlex.split(cmd1)) + result1 = runner.invoke(cli.cli, shlex.split(cmd1)) assert result1.exit_code == 0 assert "README.md" in result1.output assert re.search(search_re, result1.output, flags=re.MULTILINE) cmd2 = r'grep --version-pattern "vYYYY0M.BUILD[-RELEASE]" "{version}" README.md' - result2 = runner.invoke(main.cli, shlex.split(cmd2)) + result2 = runner.invoke(cli.cli, shlex.split(cmd2)) assert result2.exit_code == 0 assert "README.md" in result2.output assert re.search(search_re, result2.output, flags=re.MULTILINE) @@ -748,7 +748,7 @@ def test_grep(runner): search_re = r"^\s+3:\s+\[aka\. 201701\.1002a0 \!\]" cmd3 = r'grep "\[aka. YYYY0M.BLD[PYTAGNUM] \!\]" README.md' - result3 = runner.invoke(main.cli, shlex.split(cmd3)) + result3 = runner.invoke(cli.cli, shlex.split(cmd3)) assert result3.exit_code == 0 assert "README.md" in result3.output assert re.search(search_re, result3.output, flags=re.MULTILINE) @@ -756,7 +756,7 @@ def test_grep(runner): cmd4 = ( r'grep --version-pattern "vYYYY0M.BUILD[-RELEASE]" "\[aka. {pep440_version} \!\]" README.md' ) - result4 = runner.invoke(main.cli, shlex.split(cmd4)) + result4 = runner.invoke(cli.cli, shlex.split(cmd4)) assert result4.exit_code == 0 assert "README.md" in result4.output assert re.search(search_re, result4.output, flags=re.MULTILINE) @@ -784,7 +784,7 @@ def test_multimatch_file_patterns(runner): with pl.Path("setup.cfg").open(mode="w", encoding="utf-8") as fobj: fobj.write(SETUP_CFG_MULTIMATCH_FILE_PATTERNS_FIXTURE) - result = runner.invoke(main.cli, ['bump', '--release', 'beta']) + result = runner.invoke(cli.cli, ['bump', '--release', 'beta']) assert result.exit_code == 0 with pl.Path("README.md").open(mode="r", encoding="utf-8") as fobj: @@ -824,7 +824,7 @@ ROLLOVER_TEST_CASES = [ @pytest.mark.parametrize("version_pattern, old_version, expected, kwargs", ROLLOVER_TEST_CASES) def test_rollover(version_pattern, old_version, expected, kwargs): - new_version = main.incr_dispatch(old_version, raw_pattern=version_pattern, **kwargs) + new_version = cli.incr_dispatch(old_version, raw_pattern=version_pattern, **kwargs) if new_version is None: assert expected is None else: @@ -832,7 +832,7 @@ def test_rollover(version_pattern, old_version, expected, kwargs): def test_get_latest_vcs_version_tag(runner): - result = runner.invoke(main.cli, ['init', "-vv"]) + result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 _update_config_val("pycalver.toml", push="false") @@ -841,16 +841,16 @@ def test_get_latest_vcs_version_tag(runner): _vcs_init("git", files=["pycalver.toml"]) - result = runner.invoke(main.cli, ['bump', "--patch"]) + result = runner.invoke(cli.cli, ['bump', "--patch"]) assert result.exit_code == 0 _, cfg = config.init() - latest_version = main.get_latest_vcs_version_tag(cfg, fetch=False) + latest_version = cli.get_latest_vcs_version_tag(cfg, fetch=False) assert latest_version == "0.1.9" - result = runner.invoke(main.cli, ['bump', "--patch"]) + result = runner.invoke(cli.cli, ['bump', "--patch"]) assert result.exit_code == 0 _, cfg = config.init() - latest_version = main.get_latest_vcs_version_tag(cfg, fetch=False) + latest_version = cli.get_latest_vcs_version_tag(cfg, fetch=False) assert latest_version == "0.1.10" From ca2052105d5d9425eadeffa406be6c8aa63379cc Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Mon, 5 Oct 2020 19:24:14 +0000 Subject: [PATCH 76/98] add __main__.py back as wrapper --- src/pycalver/__main__.py | 15 +++++++++++++++ test/test_cli.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 src/pycalver/__main__.py diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py new file mode 100644 index 0000000..f3cf730 --- /dev/null +++ b/src/pycalver/__main__.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +# This file is part of the pycalver project +# https://github.com/mbarkhau/pycalver +# +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# SPDX-License-Identifier: MIT +""" +__main__ module for PyCalVer. + +Enables use as module: $ python -m pycalver --version +""" +from . import cli + +if __name__ == '__main__': + cli.cli() diff --git a/test/test_cli.py b/test/test_cli.py index c67a8f0..698fbe9 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -17,8 +17,8 @@ import pytest import pathlib2 as pl from click.testing import CliRunner -from pycalver import config from pycalver import cli +from pycalver import config from pycalver import v1patterns # pylint:disable=redefined-outer-name ; pytest fixtures From b5178631fd86366f348d1abb12960b23c3f9bf6c Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Mon, 5 Oct 2020 19:32:13 +0000 Subject: [PATCH 77/98] fix duplicate log message --- src/pycalver/cli.py | 4 ++++ src/pycalver/v2rewrite.py | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pycalver/cli.py b/src/pycalver/cli.py index a42322b..18eb917 100755 --- a/src/pycalver/cli.py +++ b/src/pycalver/cli.py @@ -655,3 +655,7 @@ def bump( commit_message = cfg.commit_message.format(**commit_message_kwargs) _try_bump(cfg, new_version, commit_message, allow_dirty) + + +if __name__ == '__main__': + cli() diff --git a/src/pycalver/v2rewrite.py b/src/pycalver/v2rewrite.py index 2be2022..440b146 100644 --- a/src/pycalver/v2rewrite.py +++ b/src/pycalver/v2rewrite.py @@ -51,7 +51,6 @@ def rewrite_lines( + regexfmt.pyexpr_regex(nmp.regexp.pattern) ) logger.error(msg) - logger.error(msg) raise rewrite.NoPatternMatch("Invalid pattern(s)") else: return new_lines From 74bb8a0751e32329b3394f20050971ab516c0a82 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Mon, 5 Oct 2020 19:32:40 +0000 Subject: [PATCH 78/98] fix for renamed module --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 8e3ef87..d43bb81 100644 --- a/setup.cfg +++ b/setup.cfg @@ -105,7 +105,7 @@ setup.py = version="{pep440_version}" src/pycalver/__init__.py = __version__ = "{version}" -src/pycalver/__main__.py = +src/pycalver/cli.py = @click.version_option(version="{version}") src/pycalver*/*.py = Copyright (c) 2018-YYYY From 173593c8f72da335c0127eff9eee5b8a5835e708 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Mon, 5 Oct 2020 19:33:40 +0000 Subject: [PATCH 79/98] fix for renamed module --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index afae4c7..9a8746d 100644 --- a/setup.py +++ b/setup.py @@ -73,7 +73,7 @@ setuptools.setup( install_requires=install_requires, entry_points=""" [console_scripts] - pycalver=pycalver.__main__:cli + pycalver=pycalver.cli:cli """, python_requires=">=2.7", zip_safe=True, From bcf3d88f47916be73eed5ba232010a1ca9024f0b Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Mon, 5 Oct 2020 19:43:36 +0000 Subject: [PATCH 80/98] bump v202010.1040-beta -> v202010.1041-beta --- README.md | 6 +++--- bootstrapit.sh | 2 +- setup.cfg | 2 +- setup.py | 2 +- src/pycalver/__init__.py | 2 +- src/pycalver/cli.py | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index efe4920..602ebb7 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Project/Repo: [![MIT License][img_license]][url_license] [![Supported Python Versions][img_pyversions]][url_pyversions] -[![PyCalVer v202010.1040-beta][img_version]][url_version] +[![PyCalVer v202010.1041-beta][img_version]][url_version] [![PyPI Releases][img_pypi]][url_pypi] [![PyPI Downloads][img_downloads]][url_downloads] @@ -59,7 +59,7 @@ Code Quality/CI: [img_downloads]: https://pepy.tech/badge/pycalver/month [url_downloads]: https://pepy.tech/project/pycalver -[img_version]: https://img.shields.io/static/v1.svg?label=PyCalVer&message=v202010.1040-beta&color=blue +[img_version]: https://img.shields.io/static/v1.svg?label=PyCalVer&message=v202010.1041-beta&color=blue [url_version]: https://pypi.org/project/pycalver/ [img_pypi]: https://img.shields.io/badge/PyPI-wheels-green.svg @@ -226,7 +226,7 @@ The fastest way to setup a project is to use `pycalver init`. $ pip install pycalver ... Installing collected packages: click pathlib2 typing toml pycalver -Successfully installed pycalver-202010.1040b0 +Successfully installed pycalver-202010.1041b0 $ cd myproject ~/myproject/ diff --git a/bootstrapit.sh b/bootstrapit.sh index 40cc5fc..ada65ad 100644 --- a/bootstrapit.sh +++ b/bootstrapit.sh @@ -13,7 +13,7 @@ PACKAGE_NAME="pycalver" GIT_REPO_NAMESPACE="mbarkhau" GIT_REPO_DOMAIN="github.com" -PACKAGE_VERSION="v202010.1040-beta" +PACKAGE_VERSION="v202010.1041-beta" DEFAULT_PYTHON_VERSION="python=3.8" SUPPORTED_PYTHON_VERSIONS="python=2.7 python=3.6 python=3.8 pypy2.7 pypy3.5" diff --git a/setup.cfg b/setup.cfg index d43bb81..ad0eff8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -89,7 +89,7 @@ addopts = --doctest-modules [pycalver] -current_version = "v202010.1040-beta" +current_version = "v202010.1041-beta" version_pattern = "vYYYY0M.BUILD[-RELEASE]" commit_message = "bump {old_version} -> {new_version}" commit = True diff --git a/setup.py b/setup.py index 9a8746d..ee494f4 100644 --- a/setup.py +++ b/setup.py @@ -63,7 +63,7 @@ setuptools.setup( author="Manuel Barkhau", author_email="mbarkhau@gmail.com", url="https://github.com/mbarkhau/pycalver", - version="202010.1040b0", + version="202010.1041b0", keywords="version versioning calver semver bumpversion pep440", description="CalVer for python libraries.", long_description=long_description, diff --git a/src/pycalver/__init__.py b/src/pycalver/__init__.py index d38768f..79e2557 100644 --- a/src/pycalver/__init__.py +++ b/src/pycalver/__init__.py @@ -5,4 +5,4 @@ # SPDX-License-Identifier: MIT """PyCalVer: CalVer for Python Packages.""" -__version__ = "v202010.1040-beta" +__version__ = "v202010.1041-beta" diff --git a/src/pycalver/cli.py b/src/pycalver/cli.py index 18eb917..f4c8793 100755 --- a/src/pycalver/cli.py +++ b/src/pycalver/cli.py @@ -101,7 +101,7 @@ def _validate_release_tag(tag: typ.Optional[str]) -> None: @click.group() -@click.version_option(version="v202010.1040-beta") +@click.version_option(version="v202010.1041-beta") @click.help_option() @click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") def cli(verbose: int = 0) -> None: From 4bde59d362216d2e00cd5166a2017ac42bb048b5 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Mon, 5 Oct 2020 20:04:26 +0000 Subject: [PATCH 81/98] run actions for branches and PRs --- .github/workflows/ci.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc1cb98..bd9281b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,6 @@ name: CI -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] +on: [push, pull_request] jobs: From 7b2e7293da2266a6abec2119468fdcee2fd61f33 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Mon, 5 Oct 2020 20:52:04 +0000 Subject: [PATCH 82/98] add flake8-2020 --- requirements/integration.txt | 1 + setup.cfg | 2 +- src/pycalver/pysix.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/requirements/integration.txt b/requirements/integration.txt index 1395383..3391511 100644 --- a/requirements/integration.txt +++ b/requirements/integration.txt @@ -18,6 +18,7 @@ flake8-docstrings flake8-builtins flake8-comprehensions flake8-junit-report +flake8-2020 pylint-ignore>=2020.1013 mypy # pylint doesn't support isort>=5 for now diff --git a/setup.cfg b/setup.cfg index ad0eff8..4e9f261 100644 --- a/setup.cfg +++ b/setup.cfg @@ -75,7 +75,7 @@ ignore = D400 # First line should be in imperative mood D401 -select = A,AAA,D,C,E,F,W,H,B,D212,D404,D405,D406,B901,B950 +select = A,AAA,D,C,E,F,W,H,B,D212,D404,D405,D406,B901,B950,YTT exclude = .git __pycache__ diff --git a/src/pycalver/pysix.py b/src/pycalver/pysix.py index 354f2d5..f11c7d8 100644 --- a/src/pycalver/pysix.py +++ b/src/pycalver/pysix.py @@ -6,7 +6,7 @@ import sys import typing as typ -PY2 = sys.version < "3" +PY2 = sys.version_info.major < 3 try: From 7ef468e0e88be0d06df51ce97a67169354a971f7 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Wed, 7 Oct 2020 21:16:24 +0000 Subject: [PATCH 83/98] update changelog --- CHANGELOG.md | 122 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 78 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e5aaf4..dbfe0d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,92 +1,126 @@ -# Changelog for https://gitlab.com/mbarkhau/pycalver +# Changelog for https://github.com/mbarkhau/pycalver ## NEXT - - New gitlab #7: New style patterns, to be in line with CalVer.org - - Better support for week numbering. - - Better support for optional parts. - - New: Start `BUILD` parts at `1000` to avoid leading zero truncation. - - New: Add `INC0` (0-based) and `INC1` (1-based) parts that do auto increment and rollover. - - New: `MAJOR`/`MINOR`/`PATCH`/`INC` will roll over when a date part changes to their left. - - New gitlab #2: Added `grep` subcommand to find and debug patterns. - - New: Added better error messages to debug regular expressions. - - New gitlab #10: `--pin-date` to keep date parts unchanged, and only increment non-date parts. - - New gitlab #9: Make commit message configurable. - - New: Added `--release-num` to increment the `alphaN`/`betaN`/`a0`/`b0`/etc. release number - - New: Added `--date=` parameter to set explicit date (instead of current date). - - Fix gitlab #12: Error sorting non-lexical version tags (e.g. semver). - - Fix gitlab #11: Show regexp when `--verbose` is used. - - Fix gitlab #8: Push now also pushes HEAD (used to only push the tag). - - Fix: Disallow `--release=dev` which has different semantics than other release tags. - - Fix: Entries in `file_patterns` were ignored if there were multiple for the same file. - - Switch main repo from gitlab to github. +This release includes a new syntax for patterns. + +``` +version_pattern = "vYYYY0M.BUILD[-RELEASE]" # new style +version_pattern = "v{year}{month}{build}{release}" # old style + +version_pattern = "MAJOR.MINOR.PATCH" # new style semver +version_pattern = "{MAJOR}.{MINOR}.{PATCH}" # old style semver +``` + +The main reasons for this switch were: +- To enable optional parts using braces `[PART]`. +- To align the syntax with the conventions used on CalVer.org + +The previous syntax will continue to be supported, but all documentation has been updated to primarily reference new style patterns. + +- Switch main repo from gitlab to github. +- New [gitlab#7][gitlab_i7]: New style pattern syntax. + - Better support for week numbers. + - Better support for optional parts. + - New: `BUILD` part now starts at `1000` instead of `0001` to avoid truncation of leading zeros. + - New: Add `INC0` (0-based) and `INC1` (1-based) parts that do auto increment and rollover. + - New: `MAJOR`/`MINOR`/`PATCH`/`INC` will roll over when a date part changes to their left. +- New [gitlab#2][gitlab_i2]: Added `grep` sub-command to help with debugging of patterns. +- New [gitlab#10][gitlab_i10]: `--pin-date` to keep date parts unchanged, and only increment non-date parts. +- New: Added `--date=` parameter to set explicit date (instead of current date). +- New: Added `--release-num` to increment the `alphaN`/`betaN`/`a0`/`b0`/etc. release number +- New: Added better error messages to debug regular expressions. +- New [gitlab#9][gitlab_i9]: Make commit message configurable. +- Fix [gitlab#12][gitlab_i12]: Error with sorting non-lexical version tags (e.g. SemVer). +- Fix [gitlab#11][gitlab_i11]: Show regexp when `--verbose` is used. +- Fix [gitlab#8][gitlab_i8]: `pycalver push ` will now also push HEAD (previously only the tag itself was pushed). +- Fix: Disallow `--release=dev`. The semantics of a `dev` releases are different than for other release tags and further development would be required to support them correctly. +- Fix: Entries in `file_patterns` were ignored if there were multiple entries for the same file. + +This release no longer includes the `pycalver.lexid` module, which has been moved into its own package: [pypi.org/project/lexid/](https://pypi.org/project/lexid/). + +Many thanks to contributors of this release: @LucidOne, @khanguslee, @chaudum + +[gitlab_i7]:https://gitlab.com/mbarkhau/pycalver/-/issues/7 +[gitlab_i2]: https://gitlab.com/mbarkhau/pycalver/-/issues/2 +[gitlab_i10]: https://gitlab.com/mbarkhau/pycalver/-/issues/10 +[gitlab_i9]: https://gitlab.com/mbarkhau/pycalver/-/issues/9 +[gitlab_i12]: https://gitlab.com/mbarkhau/pycalver/-/issues/12 +[gitlab_i11]: https://gitlab.com/mbarkhau/pycalver/-/issues/11 +[gitlab_i8]: https://gitlab.com/mbarkhau/pycalver/-/issues/8 ## v201907.0036 - - Fix: Don't use git/hg command if `commit=False` is configured (thanks @valentin87) +- Fix: Don't use git/hg command if `commit=False` is configured (thanks @valentin87) ## v201907.0035 - - Fix gitlab#6: Add parts `{month_short}`, `{dom_short}`, `{doy_short}`. - - Fix gitlab#5: Better warning when using bump with semver (one of --major/--minor/--patch is required) - - Fix gitlab#4: Make {release} part optional, so that versions generated by --release=final are parsed. +- Fix [gitlab#6][gitlab_i6]: Add parts `{month_short}`, `{dom_short}`, `{doy_short}`. +- Fix [gitlab#5][gitlab_i5]: Better warning when using bump with SemVer (one of --major/--minor/--patch is required) +- Fix [gitlab#4][gitlab_i4]: Make {release} part optional, so that versions generated by --release=final are parsed. + +[gitlab_i6]: https://gitlab.com/mbarkhau/pycalver/-/issues/6 +[gitlab_i5]: https://gitlab.com/mbarkhau/pycalver/-/issues/5 +[gitlab_i4]: https://gitlab.com/mbarkhau/pycalver/-/issues/4 ## v201903.0030 - - Fix: Use pattern from config instead of hardcoded {pycalver} pattern. - - Fix: Better error messages for git/hg issues. - - Add: Implicit default pattern for config file. +- Fix: Use pattern from config instead of hard-coded {pycalver} pattern. +- Fix: Better error messages for git/hg issues. +- Add: Implicit default pattern for config file. ## v201903.0028 - - Fix: Add warnings when configured files are not under version control. - - Add: Coloured output for bump --dry +- Fix: Add warnings when configured files are not under version control. +- Add: Colored output for bump --dry ## v201902.0027 - - Fix: Allow --release=post - - Fix: Better error reporting for bad patterns - - Fix: Regex escaping issue with "?" +- Fix: Allow --release=post +- Fix: Better error reporting for bad patterns +- Fix: Regex escaping issue with "?" ## v201902.0024 - - Added: Support for globs in file patterns. - - Fixed: Better error reporting for invalid config. +- Added: Support for globs in file patterns. +- Fixed: Better error reporting for invalid config. ## v201902.0020 - - Added: Support for many more custom version patterns. +- Added: Support for many more custom version patterns. ## v201812.0018 - - Fixed: Better handling of pattern replacements with "-final" releases. +- Fixed: Better handling of pattern replacements with "-final" releases. ## v201812.0017 - - Fixed github#2. `pycalver init` was broken. - - Fixed pattern escaping issues. - - Added lots more tests for cli. - - Cleaned up documentation. +- Fixed [github#2]. `pycalver init` was broken. +- Fixed pattern escaping issues. +- Added lots more tests for cli. +- Cleaned up documentation. + +[gihlab_i2]: https://github.com/mbarkhau/pycalver/-/issues/2 ## v201812.0011-beta - - Add version tags using git/hg. - - Use git/hg tags as SSOT for most recent version. - - Start using https://gitlab.com/mbarkhau/bootstrapit - - Move to https://gitlab.com/mbarkhau/pycalver +- Add version tags using git/hg. +- Use git/hg tags as SSOT for most recent version. +- Start using https://gitlab.com/mbarkhau/bootstrapit +- Move to https://gitlab.com/mbarkhau/pycalver ## v201809.0001-alpha - - Initial release +- Initial release From 86a321b2878a1cb294a7102eea73db2547df3cc3 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 8 Oct 2020 20:02:27 +0000 Subject: [PATCH 84/98] fix parsing of two digit years --- src/pycalver/v1version.py | 9 ++++++++- src/pycalver/v2version.py | 9 +++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/pycalver/v1version.py b/src/pycalver/v1version.py index dac43a0..af1af25 100644 --- a/src/pycalver/v1version.py +++ b/src/pycalver/v1version.py @@ -103,7 +103,10 @@ def _parse_field_values(field_values: FieldValues) -> version.V1VersionInfo: bid = fvals['bid'] if 'bid' in fvals else "0001" year = int(fvals['year']) if 'year' in fvals else None - doy = int(fvals['doy' ]) if 'doy' in fvals else None + if year is not None and year < 100: + year += 2000 + + doy = int(fvals['doy']) if 'doy' in fvals else None month: typ.Optional[int] dom : typ.Optional[int] @@ -211,6 +214,10 @@ def _parse_version_info(pattern_groups: PatternGroups) -> version.V1VersionInfo: >>> (vnfo.year, vnfo.month, vnfo.quarter, vnfo.bid, vnfo.tag) (2018, 11, 4, '0099', 'final') + >>> vnfo = _parse_version_info({'year': "18", 'month': "11"}) + >>> (vnfo.year, vnfo.month, vnfo.quarter) + (2018, 11, 4) + >>> vnfo = _parse_version_info({'year': "2018", 'doy': "11", 'bid': "099", 'tag': "b"}) >>> (vnfo.year, vnfo.month, vnfo.dom, vnfo.bid, vnfo.tag) (2018, 1, 11, '099', 'beta') diff --git a/src/pycalver/v2version.py b/src/pycalver/v2version.py index 4ee941d..f3a85cd 100644 --- a/src/pycalver/v2version.py +++ b/src/pycalver/v2version.py @@ -109,6 +109,10 @@ def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: >>> (vinfo.year_y, vinfo.month, vinfo.quarter, vinfo.bid, vinfo.tag) (2018, 11, 4, '0099', 'final') + >>> vinfo = _parse_version_info({'year_y': "18", 'month': "11"}) + >>> (vinfo.year_y, vinfo.month, vinfo.quarter) + (2018, 11, 4) + >>> vinfo = _parse_version_info({'year_y': "2018", 'doy': "11", 'bid': "099", 'tag': "beta"}) >>> (vinfo.year_y, vinfo.month, vinfo.dom, vinfo.doy, vinfo.bid, vinfo.tag) (2018, 1, 11, 11, '099', 'beta') @@ -162,6 +166,11 @@ def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: year_y: MaybeInt = int(fvals['year_y']) if 'year_y' in fvals else None year_g: MaybeInt = int(fvals['year_g']) if 'year_g' in fvals else None + if year_y is not None and year_y < 1000: + year_y += 2000 + if year_g is not None and year_g < 1000: + year_g += 2000 + month: MaybeInt = int(fvals['month']) if 'month' in fvals else None doy : MaybeInt = int(fvals['doy' ]) if 'doy' in fvals else None dom : MaybeInt = int(fvals['dom' ]) if 'dom' in fvals else None From 3efb72dd3c39c3756e89918077cafd5793ecbafe Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 8 Oct 2020 20:36:58 +0000 Subject: [PATCH 85/98] cli usability improvements --- setup.cfg | 2 +- src/pycalver/cli.py | 73 +++++++++++++++---- src/pycalver/v2version.py | 143 +++++++++++++++++++++----------------- src/pycalver/version.py | 2 - test/test_cli.py | 2 +- 5 files changed, 143 insertions(+), 79 deletions(-) diff --git a/setup.cfg b/setup.cfg index 4e9f261..b7ba17d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -131,7 +131,7 @@ jobs = 4 output-format = colorized # Maximum number of locals for function / method body -max-locals = 25 +max-locals = 20 # Maximum number of arguments for function / method max-args = 12 diff --git a/src/pycalver/cli.py b/src/pycalver/cli.py index f4c8793..f12186c 100755 --- a/src/pycalver/cli.py +++ b/src/pycalver/cli.py @@ -100,6 +100,52 @@ def _validate_release_tag(tag: typ.Optional[str]) -> None: sys.exit(1) +def _validate_flags( + raw_pattern: str, + major : bool, + minor : bool, + patch : bool, +) -> None: + if "{" in raw_pattern and "}" in raw_pattern: + # only validate for new style patterns + return + + valid = True + if major and "MAJOR" not in raw_pattern: + logger.error(f"Flag --major is not applicable to pattern '{raw_pattern}'") + valid = False + if minor and "MINOR" not in raw_pattern: + logger.error(f"Flag --minor is not applicable to pattern '{raw_pattern}'") + valid = False + if patch and "PATCH" not in raw_pattern: + logger.error(f"Flag --patch is not applicable to pattern '{raw_pattern}'") + valid = False + + if not valid: + sys.exit(1) + + +def _log_no_change(subcmd: str, version_pattern: str, old_version: str) -> None: + msg = ( + f"Version did not change: '{old_version}'. " + f"Invalid version and/or pattern '{version_pattern}'." + ) + logger.error(msg) + + is_semver = "{semver}" in version_pattern or ( + "MAJOR" in version_pattern and "MAJOR" in version_pattern and "PATCH" in version_pattern + ) + if is_semver: + logger.warning(f"pycalver {subcmd} [--major/--minor/--patch] required for use with SemVer.") + else: + available_flags = [ + "--" + part.lower() for part in ['MAJOR', 'MINOR', 'PATCH'] if part in version_pattern + ] + if available_flags: + available_flags_str = "/".join(available_flags) + logger.info(f"Perhaps try: pycalver {subcmd} {available_flags_str} ") + + @click.group() @click.version_option(version="v202010.1041-beta") @click.help_option() @@ -151,6 +197,7 @@ def test( tag = release # use internal naming convention _validate_release_tag(tag) + _validate_flags(raw_pattern, major, minor, patch) _date = _validate_date(date, pin_date) new_version = incr_dispatch( @@ -165,7 +212,7 @@ def test( date=_date, ) if new_version is None: - logger.error(f"Invalid version '{old_version}' and/or pattern '{raw_pattern}'.") + _log_no_change('test', raw_pattern, old_version) sys.exit(1) pep440_version = version.to_pep440(new_version) @@ -395,7 +442,7 @@ def incr_dispatch( logger.info("regex = " + regexfmt.pyexpr_regex(pattern.regexp.pattern)) if has_v1_part: - return v1version.incr( + new_version = v1version.incr( old_version, raw_pattern=raw_pattern, tag=tag, @@ -407,7 +454,7 @@ def incr_dispatch( date=date, ) else: - return v2version.incr( + new_version = v2version.incr( old_version, raw_pattern=raw_pattern, tag=tag, @@ -419,6 +466,15 @@ def incr_dispatch( date=date, ) + if new_version is None: + return None + elif pkg_resources.parse_version(new_version) <= pkg_resources.parse_version(old_version): + logger.error("Invariant violated: New version must be greater than old version ") + logger.error(f" Result: '{new_version}' > '{old_version}' -> False") + return None + else: + return new_version + def _bump( cfg : config.Config, @@ -625,16 +681,7 @@ def bump( ) if new_version is None: - is_semver = "{semver}" in cfg.version_pattern or ( - "MAJOR" in cfg.version_pattern - and "MAJOR" in cfg.version_pattern - and "PATCH" in cfg.version_pattern - ) - has_semver_inc = major or minor or patch - if is_semver and not has_semver_inc: - logger.warning("bump --major/--minor/--patch required when using semver.") - else: - logger.error(f"Invalid version '{old_version}' and/or pattern '{cfg.version_pattern}'.") + _log_no_change('bump', cfg.version_pattern, old_version) sys.exit(1) logger.info(f"Old Version: {old_version}") diff --git a/src/pycalver/v2version.py b/src/pycalver/v2version.py index f3a85cd..bf41395 100644 --- a/src/pycalver/v2version.py +++ b/src/pycalver/v2version.py @@ -102,65 +102,26 @@ FieldValues = typ.Dict[FieldKey, MatchGroupStr] VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]] -def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: - """Parse normalized V2VersionInfo from groups of a matched pattern. +def _parse_calendar_info(field_values: FieldValues) -> version.V2CalendarInfo: + """Parse normalized V2CalendarInfo from groups of a matched pattern. - >>> vinfo = _parse_version_info({'year_y': "2018", 'month': "11", 'bid': "0099"}) - >>> (vinfo.year_y, vinfo.month, vinfo.quarter, vinfo.bid, vinfo.tag) - (2018, 11, 4, '0099', 'final') - - >>> vinfo = _parse_version_info({'year_y': "18", 'month': "11"}) - >>> (vinfo.year_y, vinfo.month, vinfo.quarter) - (2018, 11, 4) - - >>> vinfo = _parse_version_info({'year_y': "2018", 'doy': "11", 'bid': "099", 'tag': "beta"}) - >>> (vinfo.year_y, vinfo.month, vinfo.dom, vinfo.doy, vinfo.bid, vinfo.tag) - (2018, 1, 11, 11, '099', 'beta') - - >>> vinfo = _parse_version_info({'year_y': "2018", 'month': "6", 'dom': "15"}) - >>> (vinfo.year_y, vinfo.month, vinfo.dom, vinfo.doy) - (2018, 6, 15, 166) - - >>> vinfo = _parse_version_info({'major': "1", 'minor': "23", 'patch': "45"}) - >>> (vinfo.major, vinfo.minor, vinfo.patch) - (1, 23, 45) - - >>> vinfo = _parse_version_info({'major': "1", 'minor': "023", 'patch': "0045"}) - >>> (vinfo.major, vinfo.minor, vinfo.patch) - (1, 23, 45) - - >>> vinfo = _parse_version_info({'year_y': "2021", 'week_w': "02"}) - >>> (vinfo.year_y, vinfo.week_w) + >>> cinfo = _parse_version_info({'year_y': "2021", 'week_w': "02"}) + >>> (cinfo.year_y, cinfo.week_w) (2021, 2) - >>> vinfo = _parse_version_info({'year_y': "2021", 'week_u': "02"}) - >>> (vinfo.year_y, vinfo.week_u) + >>> cinfo = _parse_version_info({'year_y': "2021", 'week_u': "02"}) + >>> (cinfo.year_y, cinfo.week_u) (2021, 2) - >>> vinfo = _parse_version_info({'year_g': "2021", 'week_v': "02"}) - >>> (vinfo.year_g, vinfo.week_v) + >>> cinfo = _parse_version_info({'year_g': "2021", 'week_v': "02"}) + >>> (cinfo.year_g, cinfo.week_v) (2021, 2) - >>> vinfo = _parse_version_info({'year_y': "2021", 'month': "01", 'dom': "03"}) - >>> (vinfo.year_y, vinfo.month, vinfo.dom, vinfo.tag) - (2021, 1, 3, 'final') - >>> (vinfo.year_y, vinfo.week_w, vinfo.year_y, vinfo.week_u,vinfo.year_g, vinfo.week_v) + >>> cinfo = _parse_version_info({'year_y': "2021", 'month': "01", 'dom': "03"}) + >>> (cinfo.year_y, cinfo.month, cinfo.dom) + (2021, 1, 3) + >>> (cinfo.year_y, cinfo.week_w, cinfo.year_y, cinfo.week_u,cinfo.year_g, cinfo.week_v) (2021, 0, 2021, 1, 2020, 53) """ - # pylint:disable=dangerous-default-value; We don't mutate args, mypy would fail if we did. - for key in field_values: - assert key in VALID_FIELD_KEYS, key - fvals = field_values - tag = fvals.get('tag' ) or "" - pytag = fvals.get('pytag') or "" - - if tag and not pytag: - pytag = version.PEP440_TAG_BY_RELEASE[tag] - elif pytag and not tag: - tag = version.RELEASE_BY_PEP440_TAG[pytag] - - if not tag: - tag = "final" - date: typ.Optional[dt.date] = None year_y: MaybeInt = int(fvals['year_y']) if 'year_y' in fvals else None @@ -205,16 +166,7 @@ def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: if quarter is None and month: quarter = version.quarter_from_month(month) - # NOTE (mb 2020-09-18): If a part is optional, fvals[] may be None - major = int(fvals.get('major') or 0) - minor = int(fvals.get('minor') or 0) - patch = int(fvals.get('patch') or 0) - num = int(fvals.get('num' ) or 0) - bid = fvals['bid'] if 'bid' in fvals else "1000" - inc0 = int(fvals.get('inc0') or 0) - inc1 = int(fvals.get('inc1') or 1) - - vinfo = version.V2VersionInfo( + return version.V2CalendarInfo( year_y=year_y, year_g=year_g, quarter=quarter, @@ -224,6 +176,74 @@ def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: week_w=week_w, week_u=week_u, week_v=week_v, + ) + + +def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: + """Parse normalized V2VersionInfo from groups of a matched pattern. + + >>> vinfo = _parse_version_info({'year_y': "2018", 'month': "11", 'bid': "0099"}) + >>> (vinfo.year_y, vinfo.month, vinfo.quarter, vinfo.bid, vinfo.tag) + (2018, 11, 4, '0099', 'final') + + >>> vinfo = _parse_version_info({'year_y': "18", 'month': "11"}) + >>> (vinfo.year_y, vinfo.month, vinfo.quarter) + (2018, 11, 4) + + >>> vinfo = _parse_version_info({'year_y': "2018", 'doy': "11", 'bid': "099", 'tag': "beta"}) + >>> (vinfo.year_y, vinfo.month, vinfo.dom, vinfo.doy, vinfo.bid, vinfo.tag) + (2018, 1, 11, 11, '099', 'beta') + + >>> vinfo = _parse_version_info({'year_y': "2018", 'month': "6", 'dom': "15"}) + >>> (vinfo.year_y, vinfo.month, vinfo.dom, vinfo.doy) + (2018, 6, 15, 166) + + >>> vinfo = _parse_version_info({'major': "1", 'minor': "23", 'patch': "45"}) + >>> (vinfo.major, vinfo.minor, vinfo.patch) + (1, 23, 45) + + >>> vinfo = _parse_version_info({'major': "1", 'minor': "023", 'patch': "0045"}) + >>> (vinfo.major, vinfo.minor, vinfo.patch, vinfo.tag) + (1, 23, 45, 'final') + """ + # pylint:disable=dangerous-default-value; We don't mutate args, mypy would fail if we did. + for key in field_values: + assert key in VALID_FIELD_KEYS, key + + cinfo = _parse_calendar_info(field_values) + + fvals = field_values + + tag = fvals.get('tag' ) or "" + pytag = fvals.get('pytag') or "" + + if tag and not pytag: + pytag = version.PEP440_TAG_BY_RELEASE[tag] + elif pytag and not tag: + tag = version.RELEASE_BY_PEP440_TAG[pytag] + + if not tag: + tag = "final" + + # NOTE (mb 2020-09-18): If a part is optional, fvals[] may be None + major = int(fvals.get('major') or 0) + minor = int(fvals.get('minor') or 0) + patch = int(fvals.get('patch') or 0) + num = int(fvals.get('num' ) or 0) + bid = fvals['bid'] if 'bid' in fvals else "1000" + inc0 = int(fvals.get('inc0') or 0) + inc1 = int(fvals.get('inc1') or 1) + + return version.V2VersionInfo( + year_y=cinfo.year_y, + year_g=cinfo.year_g, + quarter=cinfo.quarter, + month=cinfo.month, + dom=cinfo.dom, + doy=cinfo.doy, + week_w=cinfo.week_w, + week_u=cinfo.week_u, + week_v=cinfo.week_v, major=major, minor=minor, patch=patch, @@ -234,7 +254,6 @@ def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: inc0=inc0, inc1=inc1, ) - return vinfo def parse_version_info( diff --git a/src/pycalver/version.py b/src/pycalver/version.py index afa4a20..b83e443 100644 --- a/src/pycalver/version.py +++ b/src/pycalver/version.py @@ -126,8 +126,6 @@ V2_FIELD_INITIAL_VALUES = { 'major': "0", 'minor': "0", 'patch': "0", - 'tag' : "final", - 'pytag': "", 'num' : "0", 'inc0' : "0", 'inc1' : "1", diff --git a/test/test_cli.py b/test/test_cli.py index 698fbe9..30b8527 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -566,7 +566,7 @@ def test_v1_bump_semver_warning(runner, caplog, version_pattern): assert result.exit_code == 1 assert any("version did not change" in r.message for r in caplog.records) - assert any("--major/--minor/--patch required" in r.message for r in caplog.records) + assert any("[--major/--minor/--patch] required" in r.message for r in caplog.records) result = runner.invoke(cli.cli, ['bump', "-vv", "-n", "--dry", "--patch"]) assert result.exit_code == 0 From 15c560a0c58bb64e0e4bea7f902683936267f2ce Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Fri, 9 Oct 2020 22:11:55 +0000 Subject: [PATCH 86/98] readme updates --- Makefile | 19 +- README.md | 770 +++++++++++++++++++++--------- requirements/development.txt | 3 + scripts/update_readme_examples.py | 216 +++++++++ 4 files changed, 764 insertions(+), 244 deletions(-) create mode 100644 scripts/update_readme_examples.py diff --git a/Makefile b/Makefile index 06b2225..9139654 100644 --- a/Makefile +++ b/Makefile @@ -73,20 +73,7 @@ pycalver_deps.svg: -o pycalver_deps.svg -README.md: src/pycalver/__main__.py makefile +## Update cli reference in README.md +README.md: src/pycalver/__main__.py scripts/update_readme_examples.py Makefile @git add README.md - @printf '\n```\n$$ pycalver --help\n' > /tmp/pycalver_help.txt - @$(DEV_ENV)/bin/pycalver --help >> /tmp/pycalver_help.txt - @printf '```\n\n' >> /tmp/pycalver_help.txt - - sed -i -ne '// {p; r /tmp/pycalver_help.txt' \ - -e ':a; n; // {p; b}; ba}; p' \ - README.md - - @printf '\n```\n$$ pycalver bump --help\n' > /tmp/pycalver_help.txt - @$(DEV_ENV)/bin/pycalver bump --help >> /tmp/pycalver_help.txt - @printf '```\n\n' >> /tmp/pycalver_help.txt - - sed -i -ne '// {p; r /tmp/pycalver_help.txt' \ - -e ':a; n; // {p; b}; ba}; p' \ - README.md + @$(DEV_ENV)/bin/python scripts/update_readme_examples.py diff --git a/README.md b/README.md index 602ebb7..170efa2 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,11 @@ # [PyCalVer: Automatic Calendar Versioning][url_repo] +PyCalVer is a CLI-tool to search and replace all version strings in your project files ([calver][url_calver_org], [semver][url_semver_org] or otherwise). PyCalVer has support for -PyCalVer is a CLI-tool to search and replace version strings in your project files ([calver][url_calver_org], [semver][url_semver_org] or otherwise) . +- Configurable version patterns +- Git, Mercurial or no VCS +- Operates only on plaintext files, so it can be used for any project, not just python projects. [url_repo]: https://gitlab.com/mbarkhau/pycalver [url_calver_org]: https://calver.org/ @@ -69,99 +72,402 @@ Code Quality/CI: [url_pyversions]: https://pypi.python.org/pypi/pycalver - [](TOC) -- [Usage](#usage) +- [PyCalVer: Automatic Calendar Versioning](#pycalver-automatic-calendar-versioning) + - [Usage](#usage) - [Configuration](#configuration) - [Pattern Search and Replacement](#pattern-search-and-replacement) + - [Week Numbering](#week-numbering) + - [Normalization Caveats](#normalization-caveats) + - [Legacy Patterns](#legacy-patterns) + - [Pattern Usage](#pattern-usage) - [Examples](#examples) - [Version State](#version-state) - [The Current Version](#the-current-version) - [Bump It Up](#bump-it-up) -- [The PyCalVer Format](#the-pycalver-format) + - [Config Parameters](#config-parameters) + - [CLI Reference](#cli-reference) + - [The PyCalVer Format](#the-pycalver-format) - [Parsing](#parsing) - [Incrementing Behaviour](#incrementing-behaviour) - - [Lexical Ids](#lexical-ids) -- [Semantics of PyCalVer](#semantics-of-pycalver) + - [Semantics of PyCalVer](#semantics-of-pycalver) + - [Pitch](#pitch) + - [blah](#blah) - [Intentional Breaking Changes](#intentional-breaking-changes) - [Costs and Benefits](#costs-and-benefits) - [Unintentional Breaking Changes](#unintentional-breaking-changes) - [Pinning is not a Panacea](#pinning-is-not-a-panacea) - - [Zeno's 1.0 and The Eternal Beta](#zeno-s-1-0-and-the-eternal-beta) + - [Zeno's 1.0 and The Eternal Beta](#zenos-10-and-the-eternal-beta) [](TOC) -## Usage +## Overview ### Search and Replace -With PyCalVer, you only have to specify one `version_pattern` which is used both to search for version strings as well as to generate the replacement when you do `pycalver bump`. Compare this e.g. to `bumpversion` where you declare separate configurations for `parse` and `serialize`. +With PyCalVer, you only configure a single `version_pattern` which is then used -``` -[bumpversion] -current_version = 1.alpha -parse = (?P\d+)\.(?P.*) -serialize = - {major}.{release} - {major} -``` +1. Search for version strings in your project files +2. Replace these occurrences with an updated/bumped version number. -A similar version schema with PyCalVer would be: +Your configuration might look something like this: ``` [pycalver] -current_version = 1.alpha -version_pattern = MAJOR.RELEASE -``` +current_version = "2020.9" +version_pattern = "YYYY.MM" -Similarly you must specify file specific search and replace strings. - -``` -[bumpversion:file:requirements.txt] -search = MyProject=={current_version} -replace = MyProject=={new_version} -``` - -The same with PyCalVer would be: - -``` [pycalver:file_patterns] -requirements.txt - MyProject=={version} +src/mymodule/__init__.py + __version__ = "{version}" +src/mymodule/__main__.py + @click.version_option(version="{version}") +setup.py + version="{version}", ``` -The string `{version}` is a placeholder which references whatever you specified in your `version_pattern`. -You can also be explicit and write the expanded version yourself if you prefer: +> Throughout the examples, we use the `--date` argument. Without this argument PyCalVer will just use the current date. We use it here so that you can easily reproduce the examples. +Using this configuration, the output of `pycalver bump --dry` might look something like this: + +```diff +$ pycalver bump --date 2020-10-01 --dry +INFO - fetching tags from remote (to turn off use: -n / --no-fetch) +INFO - Old Version: 2020.9 +INFO - New Version: 2020.10 +--- setup.py ++++ setup.py +@@ -63,7 +63,7 @@ + setuptools.setup( + name="mymodule", +- version="2020.9", ++ version="2020.10", + description=description, + long_description=long_description, + +--- src/mymodule/__init__.py ++++ src/mymodule/__init__.py +@@ -3,3 +3,3 @@ + +-__version__ = "2020.9" ++__version__ = "2020.10" + + +--- src/mymodule/__main__.py ++++ src/mymodule/__main__.py +@@ -101,7 +101,7 @@ + + @click.group() +-@click.version_option(version="2020.9") ++@click.version_option(version="2020.10") + @click.help_option() + @click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") ``` + + + +### Related Projects/Alternatives + +If PyCalVer does not serve your purposes, you may wish to look at the [bump2version][url_bump2version] project, by which PyCalVer was heavily inspired. You may also wish to take a look at their list of related projects: [bump2version/RELATED.md][url_bump2version_related] + +[url_bump2version] https://github.com/c4urself/bump2version/ + +[url_bump2version_related] https://github.com/c4urself/bump2version/blob/master/RELATED.md + +## Example Usage + +### Testing a version pattern + +You can validate a pattern and how it is incremented using `pycalver test`. + +```shell +$ pycalver test --date 2018-09-22 '2018.37' 'YYYY.WW' +New Version: 2018.38 +PEP440 : 2018.38 + +$ pycalver test --date 2018-09-22 '2018.37' 'YYYY.MM' # expected to fail +ERROR - Incomplete match '2018.3' for version string '2018.37' with pattern 'YYYY.MM'/'(?P[1-9][0-9]{3})\.(?P1[0-2]|[1-9])' +ERROR - Version did not change: '2018.37'. Invalid version and/or pattern 'YYYY.MM'. +``` + +This illustrates that each pattern is internally translated to a regular expression which must match your version string. The `--verbose` flag shows a slightly more readable form. + +```shell +$ pycalver test --date 2018-09-22 'v2018.37' 'YYYY.WW' --verbose +INFO - Using pattern YYYY.WW +INFO - regex = re.compile(r""" + (?P[1-9][0-9]{3}) + \. + (?P5[0-2]|[1-4][0-9]|[0-9]) +""", flags=re.VERBOSE) +ERROR - Invalid version string 'v2018.37' for pattern ... +``` + +In other words, you don't specify regular expressions manually, they are generated for by PyCalVer based on the parts defined in the [Parts Overview](#parts-overview). + + +### SemVer: `MAJOR`/`MINOR`/`PATCH` + +You can do tradition SemVer without any kind of calendar component if you like. + +```shell +$ pycalver test '1.2.3' 'MAJOR.MINOR.PATCH' --patch +New Version: 1.2.4 +PEP440 : 1.2.4 + +$ pycalver test '1.2.3' 'MAJOR.MINOR.PATCH' --minor +New Version: 1.3.0 +PEP440 : 1.3.0 + +$ pycalver test '1.2.3' 'MAJOR.MINOR.PATCH' --major +New Version: 2.0.0 +PEP440 : 2.0.0 +``` + +These are the same CLI flags as are accepted by the `pycalver bump` command. + +In the context of a CalVer version, a typical use would be to include a `PATCH` part in your version pattern, so that you can create multiple releases in the same month. + +```shell +$ pycalver test --date 2018-09-22 '2018.9.0' 'YYYY.MM.PATCH' +ERROR - Invalid arguments or pattern, version did not change. +ERROR - Version did not change: '2018.9.0'. Invalid version and/or pattern 'YYYY.MM.PATCH'. +INFO - Perhaps try: pycalver test --patch + +$ pycalver test --date 2018-09-22 '2018.9.0' 'YYYY.MM.PATCH' --patch +New Version: 2018.9.1 +PEP440 : 2018.9.1 +``` + +The `PATCH` part will roll over back to zero when leading parts change (in this case the year and month). + +```shell +$ pycalver test --date 2018-10-22 '2018.9.1' 'YYYY.MM.PATCH' +New Version: 2018.10.0 +PEP440 : 2018.10.0 +``` + +This will happen even if you use the `--patch` argument, so that your first release of the month has a `PATCH` of 0 instead of 1. + +```shell +$ pycalver test --date 2018-10-22 '2018.9.1' 'YYYY.MM.PATCH' --patch +New Version: 2018.10.0 +PEP440 : 2018.10.0 +``` + + +### Auto Incrementing Parts: `BUILD`/`INC0`/`INC1` + +The following parts are incremented automatically, and do not use/require a CLI flag: `BUILD`/`INC0`/`INC1`. This means you can just do `pycalver bump` without any further CLI flags and special cases, which can simplify your build scripts. + +```shell +$ pycalver test --date 2018-09-22 '2018.9.1' 'YYYY.MM.INC0' +New Version: 2018.9.2 +PEP440 : 2018.9.2 + +$ pycalver test --date 2018-10-22 '2018.9.2' 'YYYY.MM.INC0' +New Version: 2018.10.0 +PEP440 : 2018.10.0 + +$ pycalver test --date 2018-10-22 '2018.9.2' 'YYYY.MM.INC1' +New Version: 2018.10.1 +PEP440 : 2018.10.1 +``` + +If it is rare for you to make multiple releases within a given period, you can make such a part optional using the `[PART]` syntax with square braces: + +```shell +$ pycalver test --date 2018-09-22 '2018.9' 'YYYY.MM[.INC0]' +New Version: 2018.9.1 +PEP440 : 2018.9.1 + +$ pycalver test --date 2018-10-22 '2018.9.1' 'YYYY.MM[.INC0]' +New Version: 2018.10 +PEP440 : 2018.10 +``` + +If the extra `INC0` part is needed, it is added. If the date rolls over and it's no longer needed, it is omitted. Any literal text enclosed in the braces (such as a separator) will also be added or omitted as needed. + +### Persistent Parts: `BUILD`/`RELEASE`/`PYTAG` + +The `BUILD` and `RELEASE` parts are not reset. Instead they are carried forward. + +```shell +$ pycalver test --date 2018-09-22 '201809.1051-beta' 'YYYY0M.BUILD[-RELEASE]' +New Version: 201809.1052-beta +PEP440 : 201809.1052b0 + +$ pycalver test --date 2018-09-22 '201809.1051-beta' 'YYYY0M.BUILD[-RELEASE]' --release rc +New Version: 201809.1052-rc +PEP440 : 201809.1052rc0 +``` + +To remove a release tag, mark it as final with `--release final`. + +```shell +$ pycalver test --date 2018-09-22 '201809.1051-beta' 'YYYY0M.BUILD[-RELEASE]' --release final +New Version: 201809.1052 +PEP440 : 201809.1052 +``` + +### Searching for Patterns with `grep` + +Using `pycalver grep`, you can search for occurrences of a version pattern in your project files. + +```shell +$ pycalver grep '__version__ = "YYYY.MM[-RELEASENUM]"' src/module/__init__.py +src/module/__init__.py + 3: + 4: __version__ = "2020.9-beta1" + 5: +``` + +Note that everything in the pattern is treated as literal text, except for a valid part (in all caps). + +When you write your configuration, you can avoid repeating your version pattern in every search pattern, by using these placeholders + +- `{version}` +- `{pep440_version}` + +Applied to the above example, you can instead use this: + +```shell +$ pycalver grep --version-pattern "YYYY.MM[-RELEASENUM]" '__version__ = "{version}"' src/module/__init__.py +src/module/__init__.py + 3: + 4: __version__ = "2020.9-beta1" + 5: +``` + +The corresponding configuration would look like this. + +```ini +[pycalver] +current_version = "2020.9-beta1" +version_pattern = "YYYY.MM[-RELEASENUM]" +... + [pycalver:file_patterns] -requirements.txt - MyProject==MAJOR.RELEASE +src/module/__init__.py + __version__ = "{version}" +... ``` -> You may be asking at this point, "what if I want to match `MAJOR.RELEASE` as a literal string?". -> Well, tough luck. Realistically speaking, this has not been an issue. +If your pattern produces non PEP440 version numbers, you may wish to use the placeholder `{pep440_version}` in your search pattern and specify your `--version-pattern` separately. -In other words, you don't specify regular expressions manually, they are generated for by PyCalVer based on the parts defined below. Everything except for a valid part (in all caps) is treated as literal text. +```shell +$ pycalver grep --version-pattern "YYYY.MM[-RELEASENUM]" 'version="{pep440_version}"' setup.py +setup.py + 65: url="https://github.com/org/project", + 66: version="2020.9b1", + 67: description=description, +``` -### Patterns/Parts +The placeholder `{version}` matches `2020.9-beta1`, while the placeholder `{pep440_version}` matches `2020.9b1` (excluding the "v" prefix, the "-" separator and with a short form release tag "b1" instead of "beta1"). These two placeholders make it possible to mostly use your preferred format for version strings, but use a [PEP440][url_pep_440] compliant/normalized version string where appropriate. -> These patterns are closely based on [calver.org][url_calver_org_scheme]. +[url_pep_440]: https://www.python.org/dev/peps/pep-0440/ + +As a further illustration of how the search and replace works, you might want use a file pattern entry to keep the year of your copyright header up to date. + +``` +$ python -m pycalver grep 'Copyright (c) 2018-YYYY' src/mymodule/*.py | head +src/mymodule/__init__.py + 3: + 4: # Copyright (c) 2018-2020 Vandelay Industries - All rights reserved. + 5: + +src/mymodule/config.py + 3: + 4: # Copyright (c) 2018-2020 Vandelay Industries - All rights reserved. + 5: +``` + +The corresponding configuration for this pattern would look like this. + +```ini +[pycalver:file_patterns] +... +src/mymodule/*.py + Copyright (c) 2018-YYYY Vandelay Industries - All rights reserved. +``` + + +## Reference + +### Command Line + + + +``` +$ pycalver --help +Usage: pycalver [OPTIONS] COMMAND [ARGS]... + + Automatically update PyCalVer version strings in all project files. + +Options: + --version Show the version and exit. + --help Show this message and exit. + -v, --verbose Control log level. -vv for debug level. + +Commands: + bump Increment the current version string and update project files. + grep Search file(s) for a version pattern. + init Initialize [pycalver] configuration. + show Show current version of your project. + test Increment a version number for demo purposes. +``` + + + + + +``` +$ pycalver bump --help +Usage: pycalver bump [OPTIONS] + + Increment the current version string and update project files. + +Options: + -v, --verbose Control log level. -vv for debug level. + -f, --fetch / -n, --no-fetch Sync tags from remote origin. + -d, --dry Display diff of changes, don't rewrite files. + --release Override release name of current_version. + Valid options are: alpha, beta, rc, post, + final. + + --allow-dirty Commit even when working directory is has + uncomitted changes. (WARNING: The commit will + still be aborted if there are uncomitted to + files with version strings. + + --major Increment major component. + -m, --minor Increment minor component. + -p, --patch Increment patch component. + -r, --release-num Increment release number (rc1, rc2, rc3..). + --pin-date Leave date components unchanged. + --date Set explicit date in format YYYY-0M-0D (e.g. + 2020-10-09). + + --help Show this message and exit. +``` + + + + +### Part Overview + +> Where possible, these patterns match the conventions from [calver.org][url_calver_org_scheme]. [url_calver_org_scheme]: https://calver.org/#scheme | part | range / example(s) | comment | |-----------|---------------------------|--------------------------------------------| | `YYYY` | 2019, 2020... | Full year, based on `strftime('%Y')` | -| `YY` | 18, 19..99, 1, 2 | Short year, based on `int(strftime('%y'))` | +| `YY` | 18, 19..99, 0, 1 | Short year, based on `int(strftime('%y'))` | | `MM` | 9, 10, 11, 12 | Month, based on `int(strftime('%m'))` | | `DD` | 1, 2, 3..31 | Day, based on `int(strftime('%d'))` | | `MAJOR` | 0..9, 10..99, 100.. | `pycalver bump --major` | @@ -175,13 +481,13 @@ In other words, you don't specify regular expressions manually, they are generat | `INC1` | 1, 2... | 1-based auto incrementing number | -The above are the most commonly used. The following are also available, but you should be aware of the [Normalization Caveats](#normalization-caveats) if you want to use them. +The following are also available, but you should review the [Normalization Caveats](#normalization-caveats) before you decide to use them. -| part | range / example(s) | comment | -|--------|---------------------|----------------------------------------------| +| part | range / example(s) | comment | +| ------ | ------------------- | -------------------------------------------- | | `Q` | 1, 2, 3, 4 | Quarter | -| `0Y` | 18, 19..99, 01, 02 | Short Year `strftime('%y')`(zero-padded) | +| `0Y` | 18, 19..99, 00, 01 | Short Year `strftime('%y')`(zero-padded) | | `0M` | 09, 10, 11, 12 | Month `strftime('%m')` (zero-padded) | | `0D` | 01, 02, 03..31 | Day `strftime('%d')` (zero-padded) | | `JJJ` | 1,2,3..366 | Day of year `int(strftime('%j'))` | @@ -200,27 +506,104 @@ The above are the most commonly used. The following are also available, but you - ² Sunday is the first day of the week. - ³ ISO 8601 week. Week 1 contains Jan 4th. -> On Week Numbering -> -> Week numbering is a bit special, as it depends on your definition of "week": -> -> - Does it start on a Monday or a Sunday? -> - Range from 0-52 or 1-53 ? -> - At the beginning/end of the year, do you have partial weeks or do -> you have a week that span mutliple years? -> - If a week spans multiple years, what is the year number? -> -> If you use `VV`/`0V`, be aware that you cannot also use `YYYY`. -> Instead use `GGGG`. This is to avoid an edge case where your version -> number would run backwards if it was created around New Year. + +### Normalization Caveats + +Package managers and installation tools will parse your version numbers. When doing so, your version number may go through a normalization process and may not be displayed as you specified it. In the case of Python, the packaging tools (such as pip, twine, setuptools) follow [PEP440 normalization rules][pep_440_normalzation_ref]. + +According to these rules (among other things): + +- Any non-numerical prefix (such as `v`) is removed +- Leading zeros in delimited parts are truncated `XX.08` -> `XX.8` +- Tags are converted to a short form (`-alpha` -> `a0`) + +For example: + +- Pattern: `vYY.0M.0D[-RELEASE]` +- Version: `v20.08.02-beta` +- PEP440 : `20.8.2b0` + +It may not be obvious to everyone that `v20.08.02-beta` is the same `20.8.2b0` on pypi. To avoid this confusion, you should choose a pattern which is always in a normalized form or as close to it as possible. + +A further consideration for the choice of your version format is that it may be processed by tools that *do not* interpret it as a version number, but treat it just like any other string. It may also be confusing to your users if they a list of version numbers, sorted lexicographically by some tool (e.g. from `git tags`) and versions are not listed in order of their release as here: + +``` +$ git tag +18.6b4 +18.9b0 +19.10b0 +19.3b0 +20.8b0 +20.8b1 +``` + +If you wish to avoid this, you should use a pattern which maintains lexicographical ordering. + +### Pattern Examples + + + +| pattern | examples | PEP440 | lexico. | +|---------------------------------|-------------------------------------|--------|---------| +| `MAJOR.MINOR.PATCH[PYTAGNUM]` | `0.13.10 0.16.10rc1` | yes | no | +| `MAJOR.MINOR[.PATCH[PYTAGNUM]]` | `1.11 0.3.0b5` | yes | no | +| `YYYY.BUILD[PYTAGNUM]` | `2020.1031 2020.1148a0` | yes | yes | +| `YYYY.BUILD[-RELEASE]` | `2021.1393-beta 2022.1279` | no | yes | +| `YYYY.INC0[PYTAGNUM]` | `2020.10 2021.12b2` | yes | no | +| `YYYY0M.PATCH[-RELEASE]` | `202005.12 202210.15-beta` | no | no¹ | +| `YYYY0M.BUILD[-RELEASE]` | `202106.1071 202106.1075-beta` | no | yes | +| `YYYY.0M` | `2020.02 2022.09` | no | yes | +| `YYYY.MM` | `2020.8 2020.10` | yes | no | +| `YYYY.WW` | `2020.8 2021.14` | yes | no | +| `YYYY.MM.PATCH[PYTAGNUM]` | `2020.3.12b0 2021.6.19b0` | yes | no | +| `YYYY.0M.PATCH[PYTAGNUM]` | `2020.10.15b0 2022.07.7b0` | no | no¹ | +| `YYYY.MM.INC0` | `2021.6.2 2022.8.9` | yes | no | +| `YYYY.MM.DD` | `2020.5.18 2021.8.2` | yes | no | +| `YYYY.0M.0D` | `2020.08.24 2022.05.03` | no | yes | +| `YY.0M.PATCH` | `21.04.2 21.11.12` | no | no² | + + + +- ¹ If `PATCH > 9` +- ² For `2100` YY produces `00`... -### Rollover +### Week Numbering -TODO -### Configuration +Week numbering is a bit special, as it depends on your definition of "week": -The fastest way to setup a project is to use `pycalver init`. +- Does it start on a Monday or a Sunday? +- Range from 0-52 or 1-53 ? +- At the beginning/end of the year, do you have partial weeks or do you have a week that span multiple years? +- If a week spans multiple years, what is the year number? + +If you use `VV`/`0V`, be aware that you cannot also use `YYYY`. +Instead use `GGGG`. This is to avoid an edge case where your version +number would run backwards if it was created around New Year. + + + + +``` + YYYY WW UU GGGG VV +2020-12-26 (Sat): 2020 51 51 2020 52 +2020-12-27 (Sun): 2020 51 52 2020 52 +2020-12-28 (Mon): 2020 52 52 2020 53 +2020-12-29 (Tue): 2020 52 52 2020 53 +2020-12-30 (Wed): 2020 52 52 2020 53 +2020-12-31 (Thu): 2020 52 52 2020 53 +2021-01-01 (Fri): 2021 00 00 2020 53 +2021-01-02 (Sat): 2021 00 00 2020 53 +2021-01-03 (Sun): 2021 00 01 2020 53 +2021-01-04 (Mon): 2021 01 01 2021 01 +``` + + + + +## Configuration + +The fastest way to setup the configuration for project is to use `pycalver init`. ```shell $ pip install pycalver @@ -231,7 +614,6 @@ Successfully installed pycalver-202010.1041b0 $ cd myproject ~/myproject/ $ pycalver init --dry -WARNING - File not found: pycalver.toml Exiting because of '-d/--dry'. Would have written to pycalver.toml: [pycalver] @@ -252,22 +634,15 @@ Exiting because of '-d/--dry'. Would have written to pycalver.toml: ] ``` -If you already have a `setup.cfg` file, the `init` sub-command will -write to that instead. +If you already have configuration file in your project (such as a `setup.cfg` file), then `pycalver init` will update that file instead. ``` -~/myproject -$ ls -README.md setup.cfg setup.py - ~/myproject $ pycalver init -WARNING - Couldn't parse setup.cfg: Missing [pycalver] section. Updated setup.cfg ``` -This will add the something like the following to your `setup.cfg` -(depending on what files already exist in your project): +Your `setup.cfg` may now look something like this: ```ini # setup.cfg @@ -281,7 +656,7 @@ push = True [pycalver:file_patterns] setup.cfg = - current_version = {version} + current_version = "{version}" setup.py = "{version}", "{pep440_version}", @@ -290,10 +665,63 @@ README.md = {pep440_version} ``` -This probably won't cover every version number used in your project and you -will have to manually add entries to `pycalver:file_patterns`. Something -like the following may illustrate additional changes you might need to -make. +For the entries in `[pycalver:file_patterns]` you can expect two failure modes: + +- A pattern won't match a version number in the associated file. +- A pattern will match something it shouldn't (less likely). + +To debug such issues, you can use `pycalver grep` . + +``` +$ pycalver grep 'Copyright (c) 2018-YYYY' src/module/*.py +src/module/__init__.py + 3: # + 4: # Copyright (c) 2018-2020 Vandelay Industries - All rights reserved. + 5: # SPDX-License-Identifier: MIT + +src/module/config.py + 3: # + 4: # Copyright (c) 2018-2020 Vandelay Industries - All rights reserved. + 5: # SPDX-License-Identifier: MIT +``` + +Of course, you may not get the pattern correct right away. If your pattern is not found, `pycalver grep` will show an error message with the regular expression it uses, to help you debug the issue. + +``` +$ pycalver grep 'Copyright 2018-YYYY' src/pycalver/*.py +ERROR - Pattern not found: 'Copyright 2018-YYYY' +# https://regex101.com/?flavor=python&flags=gmx®ex=Copyright%5B%20%5D2018%5C-%0A%28%3FP%3Cyear_y%3E%5B1-9%5D%5B0-9%5D%7B3%7D%29 +re.compile(r""" + Copyright[ ]2018\- + (?P[1-9][0-9]{3}) +""", flags=re.VERBOSE) +``` + + + +Let's say you want to keep a badge your README.md up to date. + +``` +$ pycalver grep --version-pattern='vYYYY0M.BUILD[-RELEASE]' 'img.shields.io/static/v1.svg?label=PyCalVer&message={version}&color=blue' README.md + + 61: + 62: [img_version]: https://img.shields.io/static/v1.svg?label=PyCalVer&message=v202010.1040-beta&color=blue + 63: [url_version]: https://pypi.org/org/package/ + +Found 1 match for pattern 'img.shields.io/static/v1.svg?label=PyCalVer&message=vYYYY0M.BUILD[-RELEASE]&color=blue' in README.md +``` + + + +This probably won't cover all version numbers present in your project, so you will have to manually add entries to `pycalver:file_patterns`. To determine what to add, you can use `pycalver grep` : + +``` +$ pycalver grep 'Copyright (c) 2018-YYYY' src/project/*.py +``` + + + +Something like the following may illustrate additional changes you might need to make. ```ini [pycalver:file_patterns] @@ -349,11 +777,12 @@ INFO - New Version: v201902.1002-beta If there is no match for a pattern, bump will report an error. ```shell +# TODO (mb 2020-08-29): update regex pattern $ pycalver bump --dry --no-fetch INFO - Old Version: v201901.1001-beta INFO - New Version: v201902.1002-beta -ERROR - No match for pattern 'img.shields.io/static/v1.svg?label=PyCalVer&message={pycalver}&color=blue' -ERROR - Pattern compiles to regex 'img\.shields\.io/static/v1\.svg\?label=PyCalVer&message=(?Pv(?P\d{4})(?P(?:0[0-9]|1[0-2]))\.(?P\d{4,})(?:-(?P +ERROR - No match for pattern 'img.shields.io/static/v1.svg?label=CalVer&message={version}&color=blue' +ERROR - Pattern compiles to regex 'img\.shields\.io/static/v1\.svg\?label=CalVer&message=(?P\d{4})(?P(?:0[0-9]|1[0-2]))\.(?P\d{4,})(?:-(?P (?:alpha|beta|dev|rc|post|final)))?)&color=blue' ``` @@ -397,6 +826,8 @@ Available placeholders are: ### Pattern Usage + + There are some limitations to keep in mind: 1. A version string cannot span multiple lines. @@ -433,61 +864,7 @@ When what you probably wanted was this (with the `--final` tag omitted): https://img.shields.io/badge/myproject-v202010.1117-blue.svg ``` -### Examples -The easiest way to test a pattern is with the `pycalver test` sub-command. - -```shell -$ pycalver test 'v18w01' 'vYYw0W' -New Version: v19w06 -PEP440 : v19w06 - -# TODO (mb 2020-09-24): Update regexp pattern - -$ pycalver test 'v18.01' 'vYYw0W' -ERROR - Invalid version string 'v18.01' for pattern - 'vYYw0W'/'v(?P\d{2})w(?P<0W>(?:[0-4]\d|5[0-2]))' -ERROR - Invalid version 'v18.01' and/or pattern 'vYYw0W'. -``` - -As you can see, each pattern is internally translated to a regular expression. -All version strings in your project must match either this regular expression or -the corresponding regular expression for the PEP440 version string. - -The `pycalver test` sub-command accepts the same cli flags as `pycalver -bump` to update the components that are not updated automatically (eg. -based on the calendar). - -```shell -$ pycalver test 'v18.1.1' 'vYY.MINOR.PATCH' -New Version: v19.1.1 -PEP440 : 19.1.1 - -$ pycalver test 'v18.1.1' 'vYY.MINOR.PATCH' --patch -New Version: v19.1.2 -PEP440 : 19.1.2 - -$ pycalver test 'v18.1.2' 'vYY.MINOR.PATCH' --minor -New Version: v19.2.0 -PEP440 : 19.2.0 - -$ pycalver test 'v201811.1051-beta' 'vYYYYMM.BUILD[-RELEASE]' -New Version: v201902.1052-beta -PEP440 : 201902.1052b0 - -$ pycalver test 'v201811.0051-beta' 'vYYYYMM.BUILD[-RELEASE]' --release rc -New Version: v201902.1052-rc -PEP440 : 201902.1052rc0 - -$ pycalver test 'v201811.0051-beta' 'vYYYYMM.BUILD[-RELEASE]' --release final -New Version: v201902.1052 -PEP440 : 201902.1052 -``` - -Note that pypi/setuptools/pip will normalize version strings to a format -defined in [PEP440][pep_440_ref]. You can use a format that deviates from -this, just be aware that version strings processed by these tools will look -different. ### Version State @@ -623,89 +1000,20 @@ INFO - git push origin v202010.1006-beta ### Config Parameters -TODO: Descriptions - -| Config Parameter | Type | Description | -|-------------------|---------|------------------------------| -| `current_version` | string | | -| `version_pattern` | string | | -| `commit_message` | string | ¹Template fro commit message | -| `commit` | boolean | | -| `tag` | boolean | | -| `push` | boolean | | - -- ¹ Available placeholders: - - `{new_version}` - - `{old_version}` - - `{new_version_pep440}` - - `{old_version_pep440}` + -### CLI Reference - - - -``` -$ pycalver --help -Usage: pycalver [OPTIONS] COMMAND [ARGS]... - - Automatically update PyCalVer version strings on python projects. - -Options: - --version Show the version and exit. - --help Show this message and exit. - -v, --verbose Control log level. -vv for debug level. - -Commands: - bump Increment the current version string and update project files. - grep Search file(s) for a version pattern. - init Initialize [pycalver] configuration. - show Show current version of your project. - test Increment a version number for demo purposes. -``` - - - - - -``` -$ pycalver bump --help -Usage: pycalver bump [OPTIONS] - - Increment the current version string and update project files. - -Options: - -v, --verbose Control log level. -vv for debug level. - -f, --fetch / -n, --no-fetch Sync tags from remote origin. - -d, --dry Display diff of changes, don't rewrite files. - --release Override release name of current_version. - Valid options are: alpha, beta, rc, post, - final. - - --allow-dirty Commit even when working directory is has - uncomitted changes. (WARNING: The commit will - still be aborted if there are uncomitted to - files with version strings. - - --major Increment major component. - -m, --minor Increment minor component. - -p, --patch Increment patch component. - -r, --release-num Increment release number. - --pin-date Leave date components unchanged. - --date Set explicit date in format YYYY-0M-0D (e.g. - 2020-10-04). - - --help Show this message and exit. -``` - - - -### Related Projects/Alternatives - -The bump2version project maintains a good list of alternative and related projects: [bump2version/RELATED.md][url_bump2version_related] - -[url_bump2version_related] https://github.com/c4urself/bump2version/blob/master/RELATED.md +| Config Parameter | Type | Description | +| ----------------- | -------- | ---------------------------- | +| `current_version` | string | | +| `version_pattern` | string | | +| `commit_message` | string | Template for commit message¹ | +| `commit` | boolean | | +| `tag` | boolean² | | +| `push` | boolean² | | +- ¹ Available placeholders: `{new_version}`, `{old_version}`, `{new_version_pep440}`, `{old_version_pep440}` +- ² Requires `commit = True` ## The PyCalVer Format @@ -767,7 +1075,7 @@ import re PYCALVER_PATTERN = r""" \b (?P - (?P + (?P v # "v" version prefix (?P\d{4}) (?P\d{2}) @@ -789,7 +1097,7 @@ version_match = PYCALVER_REGEX.match(version_str) assert version_match.groupdict() == { "pycalver" : "v201712.0001-alpha", - "vYYYYMM" : "v201712", + "vYYYY0M" : "v201712", "year" : "2017", "month" : "12", "build" : ".0001", @@ -803,7 +1111,7 @@ version_match = PYCALVER_REGEX.match(version_str) assert version_match.groupdict() == { "pycalver" : "v201712.0033", - "vYYYYMM" : "v201712", + "vYYYY0M" : "v201712", "year" : "2017", "month" : "12", "build" : ".0033", @@ -851,15 +1159,21 @@ This means that the expression `older_id < newer_id` will always be true, whethe ## Semantics of PyCalVer +> Disclaimer: This section is of course only aspirational. Nothing will +> stop a package maintainer from publishing updates that violate the +> semantics presented here. -This sorting even works correctly in JavaScript! +### Pitch + +- dates are good information + - how old is the software + - is the software maintained + - is my dependency outdated + - can I trust an update? - -> Disclaimer: This section can of course only be aspirational. There is nothing -> to prevent package maintainers from publishing packages with different -> semantics than what is presented here. +### blah PyCalVer places a greater burden on package maintainers than SemVer. Backward incompatibility is not encoded in the version string, because diff --git a/requirements/development.txt b/requirements/development.txt index d2d0505..f729ddc 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -46,3 +46,6 @@ graphviz # run failed tests first pytest-cache + +# to update the readme examples +rich diff --git a/scripts/update_readme_examples.py b/scripts/update_readme_examples.py new file mode 100644 index 0000000..9dc1a31 --- /dev/null +++ b/scripts/update_readme_examples.py @@ -0,0 +1,216 @@ +import io +import sys +import shlex +import random +import difflib +import datetime as dt +import subprocess as sp +import pkg_resources + +import rich +import rich.box +import rich.table + +from pycalver import v2version + + +def update(content, marker, value): + begin_marker = f"" + end_marker = f"" + + prefix, rest = content.split(begin_marker) + _ , suffix = rest.split(end_marker) + return prefix + begin_marker + value + end_marker + suffix + + +def _color_line(line): + if line.startswith("+++") or line.startswith("---"): + return line + elif line.startswith("+"): + return "\u001b[32m" + line + "\u001b[0m" + elif line.startswith("-"): + return "\u001b[31m" + line + "\u001b[0m" + elif line.startswith("@"): + return "\u001b[36m" + line + "\u001b[0m" + else: + return line + + +def print_diff(old_content, new_content): + diff_lines = difflib.unified_diff( + a=old_content.splitlines(), + b=new_content.splitlines(), + lineterm="", + ) + + for line in diff_lines: + print(_color_line(line)) + + +def update_md_code_output(content, command): + output_data = sp.check_output(shlex.split(command)) + output = output_data.decode("utf-8") + + replacement = "\n\n```\n" + "$ " + command + "\n" + output + "```\n\n" + return update(content, command, replacement) + + +def weeknum_example(): + base_date = dt.date(2020, 12, 26) + + rows = [] + for i in range(10): + d = base_date + dt.timedelta(days=i) + row = d.strftime("%Y-%m-%d (%a): %Y %W %U %G %V") + rows.append(row) + + content = "\n".join([" YYYY WW UU GGGG VV"] + rows) + return "\n\n```\n" + content + "\n```\n\n" + + +def pattern_examples(): + patterns = [ + ("MAJOR.MINOR.PATCH[PYTAGNUM]" , ""), + ("MAJOR.MINOR[.PATCH[PYTAGNUM]]", ""), + ("YYYY.BUILD[PYTAGNUM]" , ""), + ("YYYY.BUILD[-RELEASE]" , ""), + ("YYYY.INC0[PYTAGNUM]" , ""), + ("YYYY0M.PATCH[-RELEASE]" , "¹"), + ("YYYY0M.BUILD[-RELEASE]" , ""), + ("YYYY.0M" , ""), + ("YYYY.MM" , ""), + ("YYYY.WW" , ""), + ("YYYY.MM.PATCH[PYTAGNUM]" , ""), + ("YYYY.0M.PATCH[PYTAGNUM]" , "¹"), + ("YYYY.MM.INC0" , ""), + ("YYYY.MM.DD" , ""), + ("YYYY.0M.0D" , ""), + ("YY.0M.PATCH" , "²"), + ] + + rand = random.Random(0) + field_values = [ + { + 'year_y': rand.randrange(2020, 2023), + 'month' : rand.randrange( 1, 12), + 'dom' : rand.randrange( 1, 28), + 'major' : rand.randrange( 0, 1), + 'minor' : rand.randrange( 0, 20), + 'patch' : rand.randrange( 0, 20), + 'inc0' : rand.randrange( 0, 20), + 'bid' : rand.randrange(1000, 1500), + 'tag' : rand.choice(["final", "beta"]), + } + for _ in range(100) + ] + + rows = [] + for raw_pattern, lexico_caveat in patterns: + sort_keys = ['year_y'] + if "0M" in raw_pattern or "MM" in raw_pattern: + sort_keys.append('month') + if "0D" in raw_pattern or "DD" in raw_pattern: + sort_keys.append('dom') + if "PATCH" in raw_pattern: + sort_keys.append('patch') + if "INC0" in raw_pattern: + sort_keys.append('inc0') + if "BUILD" in raw_pattern: + sort_keys.append('bid') + if "PYTAG" in raw_pattern: + sort_keys.append('tag') + + field_values.sort(key=lambda fv: tuple(fv[k] for k in sort_keys)) + field_values[-1]['year_y'] = 2101 + + example_versions = [] + notag_versions = [] + pep440_versions = [] + + for fvals in field_values: + vinfo = v2version.parse_field_values_to_vinfo(fvals) + example_version = v2version.format_version(vinfo, raw_pattern) + example_versions.append(example_version) + + pep440_version = str(pkg_resources.parse_version(example_version)) + pep440_versions.append(pep440_version) + + notag_fvals = fvals.copy() + notag_fvals['tag'] = 'final' + + notag_vinfo = v2version.parse_field_values_to_vinfo(notag_fvals) + notag_version = v2version.format_version(notag_vinfo, raw_pattern) + notag_versions.append(notag_version) + + sample = rand.sample(sorted(example_versions, key=len, reverse=True)[:-1], 2) + sample.sort(key=pkg_resources.parse_version) + + is_pep440 = pep440_versions == example_versions + is_lexico = sorted(notag_versions) == notag_versions + + pattern_col = f"`{raw_pattern}`" + pep440_col = "yes" if is_pep440 else "no" + lexico_col = ("yes" if is_lexico else "no") + lexico_caveat + sample_str = " ".join([v.ljust(16) for v in sample]).strip() + examples_col = "`" + sample_str + "`" + + # row = (pattern_col, examples_col, pep440_col) + # sort_key = (is_pep440 , -len(raw_pattern)) + + row = (pattern_col, examples_col, pep440_col, lexico_col) + sort_key = (is_pep440 , is_lexico , -len(raw_pattern)) + + rows.append((sort_key, row)) + + # rows.sort(reverse=True) + + patterns_table = rich.table.Table(show_header=True, box=rich.box.ASCII) + patterns_table.add_column("pattern") + patterns_table.add_column("examples") + patterns_table.add_column("PEP440") + patterns_table.add_column("lexico.") + + for _, row in rows: + patterns_table.add_row(*row) + + buf = io.StringIO() + rich.print(patterns_table, file=buf) + table_str = buf.getvalue() + table_str = "\n".join(table_str.splitlines()[1:-1]) + table_str = table_str.replace("-+-", "-|-") + return "\n\n" + table_str + "\n\n" + + +old_content = io.open("README.md").read() + +new_content = old_content +new_content = update_md_code_output(new_content, "pycalver --help") +new_content = update_md_code_output(new_content, "pycalver bump --help") +new_content = update(new_content, "pattern_examples", pattern_examples()) +new_content = update(new_content, "weeknum_example" , weeknum_example()) + + +if old_content == new_content: + print("Nothing changed") +elif "--dry" in sys.argv: + print_diff(old_content, new_content) +else: + with io.open("README.md", mode="w") as fobj: + fobj.write(new_content) + + +# @printf '\n```\n$$ pycalver --help\n' > /tmp/pycalver_help.txt +# @$(DEV_ENV)/bin/pycalver --help >> /tmp/pycalver_help.txt +# @printf '```\n\n' >> /tmp/pycalver_help.txt + +# sed -i -ne '// {p; r /tmp/pycalver_help.txt' \ +# -e ':a; n; // {p; b}; ba}; p' \ +# README.md + +# @printf '\n```\n$$ pycalver bump --help\n' > /tmp/pycalver_help.txt +# @$(DEV_ENV)/bin/pycalver bump --help >> /tmp/pycalver_help.txt +# @printf '```\n\n' >> /tmp/pycalver_help.txt + +# sed -i -ne '// {p; r /tmp/pycalver_help.txt' \ +# -e ':a; n; // {p; b}; ba}; p' \ +# README.md From e3067a06ea8d39afd541a814957c3efa4e2e31a0 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Fri, 9 Oct 2020 22:23:17 +0000 Subject: [PATCH 87/98] misc cleanup --- src/pycalver/cli.py | 71 ++++++++++++++++++++++++--------------- src/pycalver/v2version.py | 36 ++++++++++---------- test/test_version.py | 4 +-- 3 files changed, 64 insertions(+), 47 deletions(-) diff --git a/src/pycalver/cli.py b/src/pycalver/cli.py index f12186c..db6bdb4 100755 --- a/src/pycalver/cli.py +++ b/src/pycalver/cli.py @@ -146,13 +146,34 @@ def _log_no_change(subcmd: str, version_pattern: str, old_version: str) -> None: logger.info(f"Perhaps try: pycalver {subcmd} {available_flags_str} ") +def _get_normalized_pattern(raw_pattern: str, version_pattern: typ.Optional[str]) -> str: + is_version_pattern_required = "{version}" in raw_pattern or "{pep440_version}" in raw_pattern + + if is_version_pattern_required and version_pattern is None: + logger.error( + "Argument --version-pattern= is required" + " for placeholders: {version}/{pep440_version}." + ) + sys.exit(1) + elif version_pattern is None: + _version_pattern = "INVALID" # pacify mypy, it's not referenced in raw_pattern + else: + _version_pattern = version_pattern + + if is_version_pattern_required: + return v2patterns.normalize_pattern(_version_pattern, raw_pattern) + else: + return raw_pattern + + @click.group() @click.version_option(version="v202010.1041-beta") @click.help_option() @click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") def cli(verbose: int = 0) -> None: """Automatically update PyCalVer version strings in all project files.""" - _configure_logging(verbose=max(_VERBOSE, verbose)) + if verbose: + _configure_logging(verbose=max(_VERBOSE, verbose)) @cli.command() @@ -168,10 +189,16 @@ def cli(verbose: int = 0) -> None: f"{', '.join(VALID_RELEASE_TAG_VALUES)}." ), ) -@click.option("--major" , is_flag=True, default=False, help="Increment major component.") -@click.option("-m" , "--minor", is_flag=True, default=False, help="Increment minor component.") -@click.option("-p" , "--patch", is_flag=True, default=False, help="Increment patch component.") -@click.option("-r" , "--release-num", is_flag=True, default=False, help="Increment release number.") +@click.option("--major", is_flag=True, default=False, help="Increment major component.") +@click.option("-m" , "--minor", is_flag=True, default=False, help="Increment minor component.") +@click.option("-p" , "--patch", is_flag=True, default=False, help="Increment patch component.") +@click.option( + "-r", + "--release-num", + is_flag=True, + default=False, + help="Increment release number (rc1, rc2, rc3..).", +) @click.option("--pin-date", is_flag=True, default=False, help="Leave date components unchanged.") @click.option( "--date", @@ -193,10 +220,11 @@ def test( ) -> None: """Increment a version number for demo purposes.""" _configure_logging(verbose=max(_VERBOSE, verbose)) - raw_pattern = pattern # use internal naming convention - tag = release # use internal naming convention _validate_release_tag(tag) + + raw_pattern = pattern # use internal naming convention + _validate_flags(raw_pattern, major, minor, patch) _date = _validate_date(date, pin_date) @@ -315,25 +343,8 @@ def grep( verbose = max(_VERBOSE, verbose) _configure_logging(verbose) - raw_pattern = pattern # use internal naming convention - - is_version_pattern_required = "{version}" in raw_pattern or "{pep440_version}" in raw_pattern - - if is_version_pattern_required and version_pattern is None: - logger.error( - "Argument --version-pattern= is required" - " for placeholders: {version}/{pep440_version}." - ) - sys.exit(1) - elif version_pattern is None: - _version_pattern = "INVALID" # pacify mypy, it's not referenced in raw_pattern - else: - _version_pattern = version_pattern - - if is_version_pattern_required: - normalized_pattern = v2patterns.normalize_pattern(_version_pattern, raw_pattern) - else: - normalized_pattern = raw_pattern + raw_pattern = pattern # use internal naming convention + normalized_pattern = _get_normalized_pattern(raw_pattern, version_pattern) isatty = getattr(sys.stdout, 'isatty', lambda: False) @@ -630,7 +641,13 @@ def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: @click.option("--major", is_flag=True, default=False, help="Increment major component.") @click.option("-m", "--minor", is_flag=True, default=False, help="Increment minor component.") @click.option("-p", "--patch", is_flag=True, default=False, help="Increment patch component.") -@click.option("-r", "--release-num", is_flag=True, default=False, help="Increment release number.") +@click.option( + "-r", + "--release-num", + is_flag=True, + default=False, + help="Increment release number (rc1, rc2, rc3..).", +) @click.option("--pin-date", is_flag=True, default=False, help="Leave date components unchanged.") @click.option( "--date", diff --git a/src/pycalver/v2version.py b/src/pycalver/v2version.py index bf41395..dfd1135 100644 --- a/src/pycalver/v2version.py +++ b/src/pycalver/v2version.py @@ -102,20 +102,20 @@ FieldValues = typ.Dict[FieldKey, MatchGroupStr] VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]] -def _parse_calendar_info(field_values: FieldValues) -> version.V2CalendarInfo: +def parse_field_values_to_cinfo(field_values: FieldValues) -> version.V2CalendarInfo: """Parse normalized V2CalendarInfo from groups of a matched pattern. - >>> cinfo = _parse_version_info({'year_y': "2021", 'week_w': "02"}) + >>> cinfo = parse_field_values_to_cinfo({'year_y': "2021", 'week_w': "02"}) >>> (cinfo.year_y, cinfo.week_w) (2021, 2) - >>> cinfo = _parse_version_info({'year_y': "2021", 'week_u': "02"}) + >>> cinfo = parse_field_values_to_cinfo({'year_y': "2021", 'week_u': "02"}) >>> (cinfo.year_y, cinfo.week_u) (2021, 2) - >>> cinfo = _parse_version_info({'year_g': "2021", 'week_v': "02"}) + >>> cinfo = parse_field_values_to_cinfo({'year_g': "2021", 'week_v': "02"}) >>> (cinfo.year_g, cinfo.week_v) (2021, 2) - >>> cinfo = _parse_version_info({'year_y': "2021", 'month': "01", 'dom': "03"}) + >>> cinfo = parse_field_values_to_cinfo({'year_y': "2021", 'month': "01", 'dom': "03"}) >>> (cinfo.year_y, cinfo.month, cinfo.dom) (2021, 1, 3) >>> (cinfo.year_y, cinfo.week_w, cinfo.year_y, cinfo.week_u,cinfo.year_g, cinfo.week_v) @@ -179,30 +179,30 @@ def _parse_calendar_info(field_values: FieldValues) -> version.V2CalendarInfo: ) -def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: +def parse_field_values_to_vinfo(field_values: FieldValues) -> version.V2VersionInfo: """Parse normalized V2VersionInfo from groups of a matched pattern. - >>> vinfo = _parse_version_info({'year_y': "2018", 'month': "11", 'bid': "0099"}) + >>> vinfo = parse_field_values_to_vinfo({'year_y': "2018", 'month': "11", 'bid': "0099"}) >>> (vinfo.year_y, vinfo.month, vinfo.quarter, vinfo.bid, vinfo.tag) (2018, 11, 4, '0099', 'final') - >>> vinfo = _parse_version_info({'year_y': "18", 'month': "11"}) + >>> vinfo = parse_field_values_to_vinfo({'year_y': "18", 'month': "11"}) >>> (vinfo.year_y, vinfo.month, vinfo.quarter) (2018, 11, 4) - >>> vinfo = _parse_version_info({'year_y': "2018", 'doy': "11", 'bid': "099", 'tag': "beta"}) + >>> vinfo = parse_field_values_to_vinfo({'year_y': "2018", 'doy': "11", 'bid': "099", 'tag': "beta"}) >>> (vinfo.year_y, vinfo.month, vinfo.dom, vinfo.doy, vinfo.bid, vinfo.tag) (2018, 1, 11, 11, '099', 'beta') - >>> vinfo = _parse_version_info({'year_y': "2018", 'month': "6", 'dom': "15"}) + >>> vinfo = parse_field_values_to_vinfo({'year_y': "2018", 'month': "6", 'dom': "15"}) >>> (vinfo.year_y, vinfo.month, vinfo.dom, vinfo.doy) (2018, 6, 15, 166) - >>> vinfo = _parse_version_info({'major': "1", 'minor': "23", 'patch': "45"}) + >>> vinfo = parse_field_values_to_vinfo({'major': "1", 'minor': "23", 'patch': "45"}) >>> (vinfo.major, vinfo.minor, vinfo.patch) (1, 23, 45) - >>> vinfo = _parse_version_info({'major': "1", 'minor': "023", 'patch': "0045"}) + >>> vinfo = parse_field_values_to_vinfo({'major': "1", 'minor': "023", 'patch': "0045"}) >>> (vinfo.major, vinfo.minor, vinfo.patch, vinfo.tag) (1, 23, 45, 'final') """ @@ -210,7 +210,7 @@ def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: for key in field_values: assert key in VALID_FIELD_KEYS, key - cinfo = _parse_calendar_info(field_values) + cinfo = parse_field_values_to_cinfo(field_values) fvals = field_values @@ -263,19 +263,19 @@ def parse_version_info( >>> vinfo = parse_version_info("v201712.0033-beta", raw_pattern="vYYYY0M.BUILD[-RELEASE]") >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"} - >>> assert vinfo == _parse_version_info(fvals) + >>> assert vinfo == parse_field_values_to_vinfo(fvals) >>> vinfo = parse_version_info("v201712.0033", raw_pattern="vYYYY0M.BUILD[-RELEASE]") >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033"} - >>> assert vinfo == _parse_version_info(fvals) + >>> assert vinfo == parse_field_values_to_vinfo(fvals) >>> vinfo = parse_version_info("201712.33b0", raw_pattern="YYYY0M.BLD[PYTAGNUM]") >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "33", 'tag': "beta", 'num': 0} - >>> assert vinfo == _parse_version_info(fvals) + >>> assert vinfo == parse_field_values_to_vinfo(fvals) >>> vinfo = parse_version_info("1.23.456", raw_pattern="MAJOR.MINOR.PATCH") >>> fvals = {'major': "1", 'minor': "23", 'patch': "456"} - >>> assert vinfo == _parse_version_info(fvals) + >>> assert vinfo == parse_field_values_to_vinfo(fvals) """ pattern = v2patterns.compile_pattern(raw_pattern) match = pattern.regexp.match(version_str) @@ -293,7 +293,7 @@ def parse_version_info( raise version.PatternError(err_msg) else: field_values = match.groupdict() - return _parse_version_info(field_values) + return parse_field_values_to_vinfo(field_values) def is_valid(version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-RELEASE]") -> bool: diff --git a/test/test_version.py b/test/test_version.py index 18672c3..b6e6bae 100644 --- a/test/test_version.py +++ b/test/test_version.py @@ -164,7 +164,7 @@ def v1vnfo(**field_values): def v2vnfo(**field_values): - return v2version._parse_version_info(field_values) + return v2version.parse_field_values_to_vinfo(field_values) PARSE_V1_VERSION_TEST_CASES = [ @@ -214,7 +214,7 @@ def test_v1_parse_versions(pattern_str, line, expected_vinfo): def test_v2_parse_versions(): _vnfo = v2version.parse_version_info("v201712.0033", raw_pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") fvals = {'year_y': 2017, 'month': 12, 'bid': "0033"} - assert _vnfo == v2version._parse_version_info(fvals) + assert _vnfo == v2version.parse_field_values_to_vinfo(fvals) def test_v2_format_version(): From 54ab1151f1d8b540c8c4ba5d72473edecf8b1f6c Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 11 Oct 2020 14:53:49 +0000 Subject: [PATCH 88/98] fix " escaping --- src/pycalver/regexfmt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pycalver/regexfmt.py b/src/pycalver/regexfmt.py index 7123b1a..3c5e5b2 100644 --- a/src/pycalver/regexfmt.py +++ b/src/pycalver/regexfmt.py @@ -27,6 +27,7 @@ def format_regex(regex: str) -> str: re.compile(regex) tmp_regex = regex.replace(" ", r"[ ]") + tmp_regex = tmp_regex.replace('"', r'\"') tmp_regex, _ = re.subn(r"([^\\])?\)(\?)?", "\\1)\\2\n", tmp_regex) tmp_regex, _ = re.subn(r"([^\\])\(" , "\\1\n(" , tmp_regex) tmp_regex, _ = re.subn(r"^\)\)" , ")\n)" , tmp_regex, flags=re.MULTILINE) From 145401de336ede4089563b3d211e047ecb6f0fa9 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Wed, 14 Oct 2020 22:17:18 +0000 Subject: [PATCH 89/98] update tests for new defaults --- bootstrapit.sh | 2 +- requirements/pypi.txt | 4 + setup.cfg | 12 +- setup.py | 6 +- src/{pycalver => pycalver2}/__init__.py | 2 +- src/{pycalver => pycalver2}/__main__.py | 0 src/{pycalver => pycalver2}/cli.py | 115 +++++----- src/{pycalver => pycalver2}/config.py | 10 +- src/{pycalver => pycalver2}/parse.py | 0 src/{pycalver => pycalver2}/patterns.py | 0 src/{pycalver => pycalver2}/pysix.py | 0 src/{pycalver => pycalver2}/regexfmt.py | 2 +- src/{pycalver => pycalver2}/rewrite.py | 0 src/{pycalver => pycalver2}/utils.py | 0 src/{pycalver => pycalver2}/v1patterns.py | 33 ++- src/{pycalver => pycalver2}/v1rewrite.py | 2 +- src/{pycalver => pycalver2}/v1version.py | 24 +- src/{pycalver => pycalver2}/v2patterns.py | 160 ++++++------- src/{pycalver => pycalver2}/v2rewrite.py | 6 +- src/{pycalver => pycalver2}/v2version.py | 88 ++++---- src/{pycalver => pycalver2}/vcs.py | 2 +- src/{pycalver => pycalver2}/version.py | 22 +- test/fixtures/project_a/README.md | 2 +- test/fixtures/project_a/pycalver.toml | 3 +- test/test_cli.py | 260 ++++++++++++++-------- test/test_config.py | 12 +- test/test_parse.py | 4 +- test/test_patterns.py | 38 ++-- test/test_rewrite.py | 81 +++---- test/test_version.py | 22 +- 30 files changed, 495 insertions(+), 417 deletions(-) rename src/{pycalver => pycalver2}/__init__.py (87%) rename src/{pycalver => pycalver2}/__main__.py (100%) rename src/{pycalver => pycalver2}/cli.py (92%) rename src/{pycalver => pycalver2}/config.py (98%) rename src/{pycalver => pycalver2}/parse.py (100%) rename src/{pycalver => pycalver2}/patterns.py (100%) rename src/{pycalver => pycalver2}/pysix.py (100%) rename src/{pycalver => pycalver2}/regexfmt.py (97%) rename src/{pycalver => pycalver2}/rewrite.py (100%) rename src/{pycalver => pycalver2}/utils.py (100%) rename src/{pycalver => pycalver2}/v1patterns.py (86%) rename src/{pycalver => pycalver2}/v1rewrite.py (99%) rename src/{pycalver => pycalver2}/v1version.py (95%) rename src/{pycalver => pycalver2}/v2patterns.py (79%) rename src/{pycalver => pycalver2}/v2rewrite.py (97%) rename src/{pycalver => pycalver2}/v2version.py (92%) rename src/{pycalver => pycalver2}/vcs.py (99%) rename src/{pycalver => pycalver2}/version.py (90%) diff --git a/bootstrapit.sh b/bootstrapit.sh index ada65ad..4a03da4 100644 --- a/bootstrapit.sh +++ b/bootstrapit.sh @@ -13,7 +13,7 @@ PACKAGE_NAME="pycalver" GIT_REPO_NAMESPACE="mbarkhau" GIT_REPO_DOMAIN="github.com" -PACKAGE_VERSION="v202010.1041-beta" +PACKAGE_VERSION="v2020.1041-beta" DEFAULT_PYTHON_VERSION="python=3.8" SUPPORTED_PYTHON_VERSIONS="python=2.7 python=3.6 python=3.8 pypy2.7 pypy3.5" diff --git a/requirements/pypi.txt b/requirements/pypi.txt index c3400e8..1062912 100644 --- a/requirements/pypi.txt +++ b/requirements/pypi.txt @@ -13,3 +13,7 @@ click toml lexid colorama>=0.4 + +# needed pkg_resources.parse_version +setuptools<46.0.0; python_version < "3.5" +setuptools>=46.0.0; python_version >= "3.5" diff --git a/setup.cfg b/setup.cfg index b7ba17d..7de1c91 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,7 +19,7 @@ warn_redundant_casts = True [tool:isort] -known_first_party = pycalver +known_first_party = pycalver2 known_third_party = click,pathlib2,lexid,pkg_resources force_single_line = True length_sort = True @@ -89,8 +89,8 @@ addopts = --doctest-modules [pycalver] -current_version = "v202010.1041-beta" -version_pattern = "vYYYY0M.BUILD[-RELEASE]" +current_version = "v2020.1041-beta" +version_pattern = "vYYYY.BUILD[-TAG]" commit_message = "bump {old_version} -> {new_version}" commit = True tag = True @@ -103,11 +103,11 @@ setup.cfg = current_version = "{version}" setup.py = version="{pep440_version}" -src/pycalver/__init__.py = +src/pycalver2/__init__.py = __version__ = "{version}" -src/pycalver/cli.py = +src/pycalver2/cli.py = @click.version_option(version="{version}") -src/pycalver*/*.py = +src/pycalver2*/*.py = Copyright (c) 2018-YYYY LICENSE = Copyright (c) 2018-YYYY diff --git a/setup.py b/setup.py index ee494f4..f0e5a54 100644 --- a/setup.py +++ b/setup.py @@ -58,12 +58,12 @@ if any(arg.startswith("bdist") for arg in sys.argv): setuptools.setup( - name="pycalver", + name="pycalver2", license="MIT", author="Manuel Barkhau", author_email="mbarkhau@gmail.com", url="https://github.com/mbarkhau/pycalver", - version="202010.1041b0", + version="2020.1041b0", keywords="version versioning calver semver bumpversion pep440", description="CalVer for python libraries.", long_description=long_description, @@ -73,7 +73,7 @@ setuptools.setup( install_requires=install_requires, entry_points=""" [console_scripts] - pycalver=pycalver.cli:cli + calver=pycalver2.cli:cli """, python_requires=">=2.7", zip_safe=True, diff --git a/src/pycalver/__init__.py b/src/pycalver2/__init__.py similarity index 87% rename from src/pycalver/__init__.py rename to src/pycalver2/__init__.py index 79e2557..938d3fe 100644 --- a/src/pycalver/__init__.py +++ b/src/pycalver2/__init__.py @@ -5,4 +5,4 @@ # SPDX-License-Identifier: MIT """PyCalVer: CalVer for Python Packages.""" -__version__ = "v202010.1041-beta" +__version__ = "v2020.1041-beta" diff --git a/src/pycalver/__main__.py b/src/pycalver2/__main__.py similarity index 100% rename from src/pycalver/__main__.py rename to src/pycalver2/__main__.py diff --git a/src/pycalver/cli.py b/src/pycalver2/cli.py similarity index 92% rename from src/pycalver/cli.py rename to src/pycalver2/cli.py index db6bdb4..6fc4c26 100755 --- a/src/pycalver/cli.py +++ b/src/pycalver2/cli.py @@ -39,7 +39,7 @@ except ImportError: click.disable_unicode_literals_warning = True -logger = logging.getLogger("pycalver.cli") +logger = logging.getLogger("pycalver2.cli") _VERBOSE = 0 @@ -95,7 +95,7 @@ def _validate_release_tag(tag: typ.Optional[str]) -> None: if tag in VALID_RELEASE_TAG_VALUES: return - logger.error(f"Invalid argument --release={tag}") + logger.error(f"Invalid argument --tag={tag}") logger.error(f"Valid arguments are: {', '.join(VALID_RELEASE_TAG_VALUES)}") sys.exit(1) @@ -126,24 +126,21 @@ def _validate_flags( def _log_no_change(subcmd: str, version_pattern: str, old_version: str) -> None: - msg = ( - f"Version did not change: '{old_version}'. " - f"Invalid version and/or pattern '{version_pattern}'." - ) + msg = f"Invalid version '{old_version}' and/or pattern '{version_pattern}'." logger.error(msg) is_semver = "{semver}" in version_pattern or ( "MAJOR" in version_pattern and "MAJOR" in version_pattern and "PATCH" in version_pattern ) if is_semver: - logger.warning(f"pycalver {subcmd} [--major/--minor/--patch] required for use with SemVer.") + logger.warning(f"calver {subcmd} [--major/--minor/--patch] required for use with SemVer.") else: available_flags = [ "--" + part.lower() for part in ['MAJOR', 'MINOR', 'PATCH'] if part in version_pattern ] if available_flags: available_flags_str = "/".join(available_flags) - logger.info(f"Perhaps try: pycalver {subcmd} {available_flags_str} ") + logger.info(f"Perhaps try: calver {subcmd} {available_flags_str} ") def _get_normalized_pattern(raw_pattern: str, version_pattern: typ.Optional[str]) -> str: @@ -167,37 +164,36 @@ def _get_normalized_pattern(raw_pattern: str, version_pattern: typ.Optional[str] @click.group() -@click.version_option(version="v202010.1041-beta") +@click.version_option(version="v2020.1041-beta") @click.help_option() @click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") def cli(verbose: int = 0) -> None: - """Automatically update PyCalVer version strings in all project files.""" + """Automatically update CalVer version strings in plaintext files.""" if verbose: _configure_logging(verbose=max(_VERBOSE, verbose)) @cli.command() @click.argument("old_version") -@click.argument("pattern", default="{pycalver}") -@click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") -@click.option( - "--release", - default=None, - metavar="", - help=( - f"Override release name of current_version. Valid options are: " - f"{', '.join(VALID_RELEASE_TAG_VALUES)}." - ), -) +@click.argument("pattern", default="vYYYY.BUILD[-TAG]") +@click.option('-v' , '--verbose', count=True, help="Control log level. -vv for debug level.") @click.option("--major", is_flag=True, default=False, help="Increment major component.") @click.option("-m" , "--minor", is_flag=True, default=False, help="Increment minor component.") @click.option("-p" , "--patch", is_flag=True, default=False, help="Increment patch component.") @click.option( - "-r", - "--release-num", + "--tag", + default=None, + metavar="", + help=( + f"Override release tag of current_version. Valid options are: " + f"{', '.join(VALID_RELEASE_TAG_VALUES)}." + ), +) +@click.option( + "--tag-num", is_flag=True, default=False, - help="Increment release number (rc1, rc2, rc3..).", + help="Increment release tag number (rc1, rc2, rc3..).", ) @click.option("--pin-date", is_flag=True, default=False, help="Leave date components unchanged.") @click.option( @@ -208,19 +204,18 @@ def cli(verbose: int = 0) -> None: ) def test( old_version: str, - pattern : str = "{pycalver}", + pattern : str = "vYYYY.BUILD[-TAG]", verbose : int = 0, - release : str = None, + tag : str = None, major : bool = False, minor : bool = False, patch : bool = False, - release_num: bool = False, + tag_num : bool = False, pin_date : bool = False, date : typ.Optional[str] = None, ) -> None: """Increment a version number for demo purposes.""" _configure_logging(verbose=max(_VERBOSE, verbose)) - tag = release # use internal naming convention _validate_release_tag(tag) raw_pattern = pattern # use internal naming convention @@ -231,11 +226,11 @@ def test( new_version = incr_dispatch( old_version, raw_pattern=raw_pattern, - tag=tag, major=major, minor=minor, patch=patch, - release_num=release_num, + tag=tag, + tag_num=tag_num, pin_date=pin_date, date=_date, ) @@ -370,7 +365,7 @@ def show(verbose: int = 0, fetch: bool = True) -> None: _, cfg = config.init(project_path=".") if cfg is None: - logger.error("Could not parse configuration. Perhaps try 'pycalver init'.") + logger.error("Could not parse configuration. Perhaps try 'calver init'.") sys.exit(1) cfg = _update_cfg_from_vcs(cfg, fetch) @@ -432,13 +427,13 @@ def incr_dispatch( old_version: str, raw_pattern: str, *, - tag : str = None, - major : bool = False, - minor : bool = False, - patch : bool = False, - release_num: bool = False, - pin_date : bool = False, - date : typ.Optional[dt.date] = None, + major : bool = False, + minor : bool = False, + patch : bool = False, + tag : str = None, + tag_num : bool = False, + pin_date: bool = False, + date : typ.Optional[dt.date] = None, ) -> typ.Optional[str]: v1_parts = list(v1patterns.PART_PATTERNS) + list(v1patterns.FULL_PART_FORMATS) has_v1_part = any("{" + part + "}" in raw_pattern for part in v1_parts) @@ -456,11 +451,11 @@ def incr_dispatch( new_version = v1version.incr( old_version, raw_pattern=raw_pattern, - tag=tag, major=major, minor=minor, patch=patch, - release_num=release_num, + tag=tag, + tag_num=tag_num, pin_date=pin_date, date=date, ) @@ -468,11 +463,11 @@ def incr_dispatch( new_version = v2version.incr( old_version, raw_pattern=raw_pattern, - tag=tag, major=major, minor=minor, patch=patch, - release_num=release_num, + tag=tag, + tag_num=tag_num, pin_date=pin_date, date=date, ) @@ -481,7 +476,7 @@ def incr_dispatch( return None elif pkg_resources.parse_version(new_version) <= pkg_resources.parse_version(old_version): logger.error("Invariant violated: New version must be greater than old version ") - logger.error(f" Result: '{new_version}' > '{old_version}' -> False") + logger.error(f" Failed Invariant: '{new_version}' > '{old_version}'") return None else: return new_version @@ -619,15 +614,6 @@ def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: is_flag=True, help="Display diff of changes, don't rewrite files.", ) -@click.option( - "--release", - default=None, - metavar="", - help=( - f"Override release name of current_version. Valid options are: " - f"{', '.join(VALID_RELEASE_TAG_VALUES)}." - ), -) @click.option( "--allow-dirty", default=False, @@ -642,11 +628,20 @@ def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: @click.option("-m", "--minor", is_flag=True, default=False, help="Increment minor component.") @click.option("-p", "--patch", is_flag=True, default=False, help="Increment patch component.") @click.option( - "-r", - "--release-num", + "-t", + "--tag", + default=None, + metavar="", + help=( + f"Override release tag of current_version. Valid options are: " + f"{', '.join(VALID_RELEASE_TAG_VALUES)}." + ), +) +@click.option( + "--tag-num", is_flag=True, default=False, - help="Increment release number (rc1, rc2, rc3..).", + help="Increment release tag number (rc1, rc2, rc3..).", ) @click.option("--pin-date", is_flag=True, default=False, help="Leave date components unchanged.") @click.option( @@ -656,7 +651,6 @@ def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: help=f"Set explicit date in format YYYY-0M-0D (e.g. {_current_date}).", ) def bump( - release : typ.Optional[str] = None, verbose : int = 0, dry : bool = False, allow_dirty: bool = False, @@ -664,15 +658,14 @@ def bump( major : bool = False, minor : bool = False, patch : bool = False, - release_num: bool = False, + tag : typ.Optional[str] = None, + tag_num : bool = False, pin_date : bool = False, date : typ.Optional[str] = None, ) -> None: """Increment the current version string and update project files.""" verbose = max(_VERBOSE, verbose) _configure_logging(verbose) - - tag = release # use internal naming convention _validate_release_tag(tag) _date = _validate_date(date, pin_date) @@ -688,11 +681,11 @@ def bump( new_version = incr_dispatch( old_version, raw_pattern=cfg.version_pattern, - tag=tag, major=major, minor=minor, patch=patch, - release_num=release_num, + tag=tag, + tag_num=tag_num, pin_date=pin_date, date=_date, ) diff --git a/src/pycalver/config.py b/src/pycalver2/config.py similarity index 98% rename from src/pycalver/config.py rename to src/pycalver2/config.py index a88dd72..16dbb77 100644 --- a/src/pycalver/config.py +++ b/src/pycalver2/config.py @@ -22,7 +22,7 @@ from . import v1patterns from . import v2patterns from .patterns import Pattern -logger = logging.getLogger("pycalver.config") +logger = logging.getLogger("pycalver2.config") RawPatterns = typ.List[str] RawPatternsByFile = typ.Dict[str, RawPatterns] @@ -428,7 +428,7 @@ def init( DEFAULT_CONFIGPARSER_BASE_TMPL = """ [pycalver] current_version = "{initial_version}" -version_pattern = "vYYYY0M.BUILD[-RELEASE]" +version_pattern = "vYYYY.BUILD[-TAG]" commit_message = "bump version {{old_version}} -> {{new_version}}" commit = True tag = True @@ -468,7 +468,7 @@ README.md = DEFAULT_TOML_BASE_TMPL = """ [pycalver] current_version = "{initial_version}" -version_pattern = "vYYYY0M.BUILD[-RELEASE]" +version_pattern = "vYYYY.BUILD[-TAG]" commit_message = "bump version {{old_version}} -> {{new_version}}" commit = true tag = true @@ -517,11 +517,11 @@ DEFAULT_TOML_README_MD_STR = """ def _initial_version() -> str: - return dt.datetime.now().strftime("v%Y%m.1001-alpha") + return dt.datetime.utcnow().strftime("v%Y.1001-alpha") def _initial_version_pep440() -> str: - return dt.datetime.now().strftime("%Y%m.1001a0") + return dt.datetime.utcnow().strftime("%Y.1001a0") def default_config(ctx: ProjectContext) -> str: diff --git a/src/pycalver/parse.py b/src/pycalver2/parse.py similarity index 100% rename from src/pycalver/parse.py rename to src/pycalver2/parse.py diff --git a/src/pycalver/patterns.py b/src/pycalver2/patterns.py similarity index 100% rename from src/pycalver/patterns.py rename to src/pycalver2/patterns.py diff --git a/src/pycalver/pysix.py b/src/pycalver2/pysix.py similarity index 100% rename from src/pycalver/pysix.py rename to src/pycalver2/pysix.py diff --git a/src/pycalver/regexfmt.py b/src/pycalver2/regexfmt.py similarity index 97% rename from src/pycalver/regexfmt.py rename to src/pycalver2/regexfmt.py index 3c5e5b2..7542d5f 100644 --- a/src/pycalver/regexfmt.py +++ b/src/pycalver2/regexfmt.py @@ -9,7 +9,7 @@ import textwrap from . import pysix -logger = logging.getLogger("pycalver.regexfmt") +logger = logging.getLogger("pycalver2.regexfmt") def format_regex(regex: str) -> str: diff --git a/src/pycalver/rewrite.py b/src/pycalver2/rewrite.py similarity index 100% rename from src/pycalver/rewrite.py rename to src/pycalver2/rewrite.py diff --git a/src/pycalver/utils.py b/src/pycalver2/utils.py similarity index 100% rename from src/pycalver/utils.py rename to src/pycalver2/utils.py diff --git a/src/pycalver/v1patterns.py b/src/pycalver2/v1patterns.py similarity index 86% rename from src/pycalver/v1patterns.py rename to src/pycalver2/v1patterns.py index f3d22ce..cb5698b 100644 --- a/src/pycalver/v1patterns.py +++ b/src/pycalver2/v1patterns.py @@ -38,7 +38,7 @@ from . import utils from .patterns import RE_PATTERN_ESCAPES from .patterns import Pattern -logger = logging.getLogger("pycalver.v1patterns") +logger = logging.getLogger("pycalver2.v1patterns") # https://regex101.com/r/fnj60p/10 PYCALVER_PATTERN = r""" @@ -207,18 +207,31 @@ def _compile_pattern_re(normalized_pattern: str) -> typ.Pattern[str]: return re.compile(pattern_str) +def _normalized_pattern(version_pattern: str, raw_pattern: str) -> str: + res = raw_pattern.replace(r"{version}", version_pattern) + if version_pattern == r"{pycalver}": + res = res.replace(r"{pep440_version}", r"{pep440_pycalver}") + elif version_pattern == r"{semver}": + res = res.replace(r"{pep440_version}", r"{semver}") + elif version_pattern == r"v{year}{month}{build}{release}": + res = res.replace(r"{pep440_version}", r"{year}{month}.{BID}{pep440_tag}") + elif version_pattern == r"{year}{month}{build}{release}": + res = res.replace(r"{pep440_version}", r"{year}{month}.{BID}{pep440_tag}") + elif version_pattern == r"v{year}{build}{release}": + res = res.replace(r"{pep440_version}", r"{year}.{BID}{pep440_tag}") + elif version_pattern == r"{year}{build}{release}": + res = res.replace(r"{pep440_version}", r"{year}.{BID}{pep440_tag}") + elif r"{pep440_version}" in raw_pattern: + logger.warning(f"No mapping of '{version_pattern}' to '{{pep440_version}}'") + + return res + + @utils.memo def compile_pattern(version_pattern: str, raw_pattern: typ.Optional[str] = None) -> Pattern: _raw_pattern = version_pattern if raw_pattern is None else raw_pattern - normalized_pattern = _raw_pattern.replace(r"{version}", version_pattern) - if version_pattern == r"{pycalver}": - normalized_pattern = normalized_pattern.replace(r"{pep440_version}", r"{pep440_pycalver}") - elif version_pattern == r"{semver}": - normalized_pattern = normalized_pattern.replace(r"{pep440_version}", r"{semver}") - elif r"{pep440_version}" in _raw_pattern: - logger.warning(f"No mapping of '{version_pattern}' to '{{pep440_version}}'") - - regexp = _compile_pattern_re(normalized_pattern) + normalized_pattern = _normalized_pattern(version_pattern, _raw_pattern) + regexp = _compile_pattern_re(normalized_pattern) return Pattern(version_pattern, normalized_pattern, regexp) diff --git a/src/pycalver/v1rewrite.py b/src/pycalver2/v1rewrite.py similarity index 99% rename from src/pycalver/v1rewrite.py rename to src/pycalver2/v1rewrite.py index 4993bfd..99c8da0 100644 --- a/src/pycalver/v1rewrite.py +++ b/src/pycalver2/v1rewrite.py @@ -17,7 +17,7 @@ from . import regexfmt from . import v1version from .patterns import Pattern -logger = logging.getLogger("pycalver.v1rewrite") +logger = logging.getLogger("pycalver2.v1rewrite") def rewrite_lines( diff --git a/src/pycalver/v1version.py b/src/pycalver2/v1version.py similarity index 95% rename from src/pycalver/v1version.py rename to src/pycalver2/v1version.py index af1af25..4f6fc80 100644 --- a/src/pycalver/v1version.py +++ b/src/pycalver2/v1version.py @@ -14,7 +14,7 @@ import lexid from . import version from . import v1patterns -logger = logging.getLogger("pycalver.v1version") +logger = logging.getLogger("pycalver2.v1version") CalInfo = typ.Union[version.V1CalendarInfo, version.V1VersionInfo] @@ -97,7 +97,7 @@ def _parse_field_values(field_values: FieldValues) -> version.V1VersionInfo: tag = fvals.get('tag') if tag is None: tag = "final" - tag = version.RELEASE_BY_PEP440_TAG.get(tag, tag) + tag = version.TAG_BY_PEP440_TAG.get(tag, tag) assert tag is not None bid = fvals['bid'] if 'bid' in fvals else "0001" @@ -351,7 +351,7 @@ def format_version(vinfo: version.V1VersionInfo, raw_pattern: str) -> str: kwargs['pep440_tag'] = "" else: kwargs['release' ] = "-" + release_tag - kwargs['pep440_tag'] = version.PEP440_TAG_BY_RELEASE[release_tag] + "0" + kwargs['pep440_tag'] = version.PEP440_TAG_BY_TAG[release_tag] + "0" kwargs['release_tag'] = release_tag @@ -381,13 +381,13 @@ def incr( old_version: str, raw_pattern: str = "{pycalver}", *, - tag : typ.Optional[str] = None, - major : bool = False, - minor : bool = False, - patch : bool = False, - release_num: bool = False, - pin_date : bool = False, - date : typ.Optional[dt.date] = None, + major : bool = False, + minor : bool = False, + patch : bool = False, + tag : typ.Optional[str] = None, + tag_num : bool = False, + pin_date: bool = False, + date : typ.Optional[dt.date] = None, ) -> typ.Optional[str]: """Increment version string. @@ -415,8 +415,8 @@ def incr( cur_vinfo = cur_vinfo._replace(minor=cur_vinfo.minor + 1, patch=0) if patch: cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1) - if release_num: - raise NotImplementedError("--release-num not supported for old style patterns") + if tag_num: + raise NotImplementedError("--tag-num not supported for old style patterns") if tag: cur_vinfo = cur_vinfo._replace(tag=tag) diff --git a/src/pycalver/v2patterns.py b/src/pycalver2/v2patterns.py similarity index 79% rename from src/pycalver/v2patterns.py rename to src/pycalver2/v2patterns.py index 3d85c9f..de31238 100644 --- a/src/pycalver/v2patterns.py +++ b/src/pycalver2/v2patterns.py @@ -5,7 +5,7 @@ # SPDX-License-Identifier: MIT """Compose Regular Expressions from Patterns. ->>> pattern = compile_pattern("vYYYY0M.BUILD[-RELEASE]") +>>> pattern = compile_pattern("vYYYY0M.BUILD[-TAG]") >>> version_info = pattern.regexp.match("v201712.0123-alpha") >>> assert version_info.groupdict() == { ... "year_y" : "2017", @@ -37,7 +37,7 @@ from . import utils from .patterns import RE_PATTERN_ESCAPES from .patterns import Pattern -logger = logging.getLogger("pycalver.v2patterns") +logger = logging.getLogger("pycalver2.v2patterns") # NOTE (mb 2020-09-17): For patterns with different options '(AAA|BB|C)', the # patterns with more digits should be first/left of those with fewer digits: @@ -78,61 +78,61 @@ PART_PATTERNS = { 'VV': r"5[0-3]|[1-4][0-9]|[1-9]", '0V': r"5[0-3]|[1-4][0-9]|0[1-9]", # non calver parts - 'MAJOR' : r"[0-9]+", - 'MINOR' : r"[0-9]+", - 'PATCH' : r"[0-9]+", - 'BUILD' : r"[0-9]+", - 'BLD' : r"[1-9][0-9]*", - 'RELEASE': r"preview|final|alpha|beta|post|rc", - 'PYTAG' : r"post|rc|a|b", - 'NUM' : r"[0-9]+", - 'INC0' : r"[0-9]+", - 'INC1' : r"[1-9][0-9]*", + 'MAJOR': r"[0-9]+", + 'MINOR': r"[0-9]+", + 'PATCH': r"[0-9]+", + 'BUILD': r"[0-9]+", + 'BLD' : r"[1-9][0-9]*", + 'TAG' : r"preview|final|alpha|beta|post|rc", + 'PYTAG': r"post|rc|a|b", + 'NUM' : r"[0-9]+", + 'INC0' : r"[0-9]+", + 'INC1' : r"[1-9][0-9]*", } PATTERN_PART_FIELDS = { - 'YYYY' : 'year_y', - 'YY' : 'year_y', - '0Y' : 'year_y', - 'GGGG' : 'year_g', - 'GG' : 'year_g', - '0G' : 'year_g', - 'Q' : 'quarter', - 'MM' : 'month', - '0M' : 'month', - 'DD' : 'dom', - '0D' : 'dom', - 'JJJ' : 'doy', - '00J' : 'doy', - 'MAJOR' : 'major', - 'MINOR' : 'minor', - 'PATCH' : 'patch', - 'BUILD' : 'bid', - 'BLD' : 'bid', - 'RELEASE': 'tag', - 'PYTAG' : 'pytag', - 'NUM' : 'num', - 'INC0' : 'inc0', - 'INC1' : 'inc1', - 'WW' : 'week_w', - '0W' : 'week_w', - 'UU' : 'week_u', - '0U' : 'week_u', - 'VV' : 'week_v', - '0V' : 'week_v', + 'YYYY' : 'year_y', + 'YY' : 'year_y', + '0Y' : 'year_y', + 'GGGG' : 'year_g', + 'GG' : 'year_g', + '0G' : 'year_g', + 'Q' : 'quarter', + 'MM' : 'month', + '0M' : 'month', + 'DD' : 'dom', + '0D' : 'dom', + 'JJJ' : 'doy', + '00J' : 'doy', + 'MAJOR': 'major', + 'MINOR': 'minor', + 'PATCH': 'patch', + 'BUILD': 'bid', + 'BLD' : 'bid', + 'TAG' : 'tag', + 'PYTAG': 'pytag', + 'NUM' : 'num', + 'INC0' : 'inc0', + 'INC1' : 'inc1', + 'WW' : 'week_w', + '0W' : 'week_w', + 'UU' : 'week_u', + '0U' : 'week_u', + 'VV' : 'week_v', + '0V' : 'week_v', } PEP440_PART_SUBSTITUTIONS = { - '0W' : "WW", - '0U' : "UU", - '0V' : "VV", - '0M' : "MM", - '0D' : "DD", - '00J' : "JJJ", - 'BUILD' : "BLD", - 'RELEASE': "PYTAG", + '0W' : "WW", + '0U' : "UU", + '0V' : "VV", + '0M' : "MM", + '0D' : "DD", + '00J' : "JJJ", + 'BUILD': "BLD", + 'TAG' : "PYTAG", } @@ -191,35 +191,35 @@ FormatterFunc = typ.Callable[[FieldValue], str] PART_FORMATS: typ.Dict[str, FormatterFunc] = { - 'YYYY' : _fmt_num, - 'YY' : _fmt_yy, - '0Y' : _fmt_0y, - 'GGGG' : _fmt_num, - 'GG' : _fmt_gg, - '0G' : _fmt_0g, - 'Q' : _fmt_num, - 'MM' : _fmt_num, - '0M' : _fmt_0m, - 'DD' : _fmt_num, - '0D' : _fmt_0d, - 'JJJ' : _fmt_num, - '00J' : _fmt_00j, - 'MAJOR' : _fmt_num, - 'MINOR' : _fmt_num, - 'PATCH' : _fmt_num, - 'BUILD' : _fmt_num, - 'BLD' : _fmt_bld, - 'RELEASE': _fmt_num, - 'PYTAG' : _fmt_num, - 'NUM' : _fmt_num, - 'INC0' : _fmt_num, - 'INC1' : _fmt_num, - 'WW' : _fmt_num, - '0W' : _fmt_0w, - 'UU' : _fmt_num, - '0U' : _fmt_0u, - 'VV' : _fmt_num, - '0V' : _fmt_0v, + 'YYYY' : _fmt_num, + 'YY' : _fmt_yy, + '0Y' : _fmt_0y, + 'GGGG' : _fmt_num, + 'GG' : _fmt_gg, + '0G' : _fmt_0g, + 'Q' : _fmt_num, + 'MM' : _fmt_num, + '0M' : _fmt_0m, + 'DD' : _fmt_num, + '0D' : _fmt_0d, + 'JJJ' : _fmt_num, + '00J' : _fmt_00j, + 'MAJOR': _fmt_num, + 'MINOR': _fmt_num, + 'PATCH': _fmt_num, + 'BUILD': _fmt_num, + 'BLD' : _fmt_bld, + 'TAG' : _fmt_num, + 'PYTAG': _fmt_num, + 'NUM' : _fmt_num, + 'INC0' : _fmt_num, + 'INC1' : _fmt_num, + 'WW' : _fmt_num, + '0W' : _fmt_0w, + 'UU' : _fmt_num, + '0U' : _fmt_0u, + 'VV' : _fmt_num, + '0V' : _fmt_0v, } @@ -249,7 +249,7 @@ def _convert_to_pep440(version_pattern: str) -> str: substitution = PEP440_PART_SUBSTITUTIONS[part_name] - is_numerical_part = part_name not in ('RELEASE', 'PYTAG') + is_numerical_part = part_name not in ('TAG', 'PYTAG') if is_numerical_part: part_index = pep440_pattern.find(part_name) is_zero_truncation_part = part_index == 0 or pep440_pattern[part_index - 1] == "." @@ -321,7 +321,7 @@ def _replace_pattern_parts(pattern: str) -> str: def _compile_pattern_re(normalized_pattern: str) -> typ.Pattern[str]: escaped_pattern = normalized_pattern for char, escaped in RE_PATTERN_ESCAPES: - # [] braces are used for optional parts, such as [-RELEASE]/[-beta] + # [] braces are used for optional parts, such as [-TAG]/[-beta] # and need to be escaped manually. is_semantic_char = char in "[]\\" if not is_semantic_char: diff --git a/src/pycalver/v2rewrite.py b/src/pycalver2/v2rewrite.py similarity index 97% rename from src/pycalver/v2rewrite.py rename to src/pycalver2/v2rewrite.py index 440b146..9eb0661 100644 --- a/src/pycalver/v2rewrite.py +++ b/src/pycalver2/v2rewrite.py @@ -18,7 +18,7 @@ from . import v2version from . import v2patterns from .patterns import Pattern -logger = logging.getLogger("pycalver.v2rewrite") +logger = logging.getLogger("pycalver2.v2rewrite") def rewrite_lines( @@ -65,9 +65,9 @@ def rfd_from_content( r"""Rewrite pattern occurrences with version string. >>> from .v2patterns import compile_pattern - >>> version_pattern = "vYYYY0M.BUILD[-RELEASE]" + >>> version_pattern = "vYYYY0M.BUILD[-TAG]" >>> new_vinfo = v2version.parse_version_info("v201809.0123", version_pattern) - >>> patterns = [compile_pattern(version_pattern, '__version__ = "vYYYY0M.BUILD[-RELEASE]"')] + >>> patterns = [compile_pattern(version_pattern, '__version__ = "vYYYY0M.BUILD[-TAG]"')] >>> content = '__version__ = "v201809.0001-alpha"' >>> rfd = rfd_from_content(patterns, new_vinfo, content) >>> rfd.new_lines diff --git a/src/pycalver/v2version.py b/src/pycalver2/v2version.py similarity index 92% rename from src/pycalver/v2version.py rename to src/pycalver2/v2version.py index dfd1135..1164d47 100644 --- a/src/pycalver/v2version.py +++ b/src/pycalver2/v2version.py @@ -14,7 +14,7 @@ import lexid from . import version from . import v2patterns -logger = logging.getLogger("pycalver.v2version") +logger = logging.getLogger("pycalver2.v2version") CalInfo = typ.Union[version.V2CalendarInfo, version.V2VersionInfo] @@ -218,9 +218,9 @@ def parse_field_values_to_vinfo(field_values: FieldValues) -> version.V2VersionI pytag = fvals.get('pytag') or "" if tag and not pytag: - pytag = version.PEP440_TAG_BY_RELEASE[tag] + pytag = version.PEP440_TAG_BY_TAG[tag] elif pytag and not tag: - tag = version.RELEASE_BY_PEP440_TAG[pytag] + tag = version.TAG_BY_PEP440_TAG[pytag] if not tag: tag = "final" @@ -257,15 +257,15 @@ def parse_field_values_to_vinfo(field_values: FieldValues) -> version.V2VersionI def parse_version_info( - version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-RELEASE]" + version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-TAG]" ) -> version.V2VersionInfo: """Parse normalized V2VersionInfo. - >>> vinfo = parse_version_info("v201712.0033-beta", raw_pattern="vYYYY0M.BUILD[-RELEASE]") + >>> vinfo = parse_version_info("v201712.0033-beta", raw_pattern="vYYYY0M.BUILD[-TAG]") >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"} >>> assert vinfo == parse_field_values_to_vinfo(fvals) - >>> vinfo = parse_version_info("v201712.0033", raw_pattern="vYYYY0M.BUILD[-RELEASE]") + >>> vinfo = parse_version_info("v201712.0033", raw_pattern="vYYYY0M.BUILD[-TAG]") >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033"} >>> assert vinfo == parse_field_values_to_vinfo(fvals) @@ -296,10 +296,10 @@ def parse_version_info( return parse_field_values_to_vinfo(field_values) -def is_valid(version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-RELEASE]") -> bool: +def is_valid(version_str: str, raw_pattern: str = "vYYYY.BUILD[-TAG]") -> bool: """Check if a version matches a pattern. - >>> is_valid("v201712.0033-beta", raw_pattern="vYYYY0M.BUILD[-RELEASE]") + >>> is_valid("v201712.0033-beta", raw_pattern="vYYYY0M.BUILD[-TAG]") True >>> is_valid("v201712.0033-beta", raw_pattern="MAJOR.MINOR.PATCH") False @@ -327,9 +327,9 @@ def _format_part_values(vinfo: version.V2VersionInfo) -> PartValues: It may for example have month=9, but not the formatted representation '09' for '0M'. - >>> vinfo = parse_version_info("v200709.1033-beta", raw_pattern="vYYYY0M.BUILD[-RELEASE]") + >>> vinfo = parse_version_info("v200709.1033-beta", raw_pattern="vYYYY0M.BUILD[-TAG]") >>> kwargs = dict(_format_part_values(vinfo)) - >>> (kwargs['YYYY'], kwargs['0M'], kwargs['BUILD'], kwargs['RELEASE']) + >>> (kwargs['YYYY'], kwargs['0M'], kwargs['BUILD'], kwargs['TAG']) ('2007', '09', '1033', 'beta') >>> (kwargs['YY'], kwargs['0Y'], kwargs['MM'], kwargs['PYTAG']) ('7', '07', '9', 'b') @@ -472,7 +472,7 @@ def format_version(vinfo: version.V2VersionInfo, raw_pattern: str) -> str: """Generate version string. >>> import datetime as dt - >>> vinfo = parse_version_info("v200712.0033-beta", raw_pattern="vYYYY0M.BUILD[-RELEASE]") + >>> vinfo = parse_version_info("v200712.0033-beta", raw_pattern="vYYYY0M.BUILD[-TAG]") >>> vinfo_a = vinfo._replace(**cal_info(date=dt.date(2007, 1, 1))._asdict()) >>> vinfo_b = vinfo._replace(**cal_info(date=dt.date(2007, 12, 31))._asdict()) @@ -483,63 +483,63 @@ def format_version(vinfo: version.V2VersionInfo, raw_pattern: str) -> str: 'v7.33-b0' >>> format_version(vinfo_a, raw_pattern="YYYY0M.BUILD[PYTAG[NUM]]") '200701.0033b' - >>> format_version(vinfo_a, raw_pattern="v0Y.BLD[-RELEASE]") + >>> format_version(vinfo_a, raw_pattern="v0Y.BLD[-TAG]") 'v07.33-beta' - >>> format_version(vinfo_a, raw_pattern="vYYYY0M.BUILD[-RELEASE]") + >>> format_version(vinfo_a, raw_pattern="vYYYY0M.BUILD[-TAG]") 'v200701.0033-beta' - >>> format_version(vinfo_b, raw_pattern="vYYYY0M.BUILD[-RELEASE]") + >>> format_version(vinfo_b, raw_pattern="vYYYY0M.BUILD[-TAG]") 'v200712.0033-beta' - >>> format_version(vinfo_a, raw_pattern="vYYYYw0W.BUILD[-RELEASE]") + >>> format_version(vinfo_a, raw_pattern="vYYYYw0W.BUILD[-TAG]") 'v2007w01.0033-beta' - >>> format_version(vinfo_a, raw_pattern="vYYYYwWW.BLD[-RELEASE]") + >>> format_version(vinfo_a, raw_pattern="vYYYYwWW.BLD[-TAG]") 'v2007w1.33-beta' - >>> format_version(vinfo_b, raw_pattern="vYYYYw0W.BUILD[-RELEASE]") + >>> format_version(vinfo_b, raw_pattern="vYYYYw0W.BUILD[-TAG]") 'v2007w53.0033-beta' - >>> format_version(vinfo_a, raw_pattern="vYYYYd00J.BUILD[-RELEASE]") + >>> format_version(vinfo_a, raw_pattern="vYYYYd00J.BUILD[-TAG]") 'v2007d001.0033-beta' - >>> format_version(vinfo_a, raw_pattern="vYYYYdJJJ.BUILD[-RELEASE]") + >>> format_version(vinfo_a, raw_pattern="vYYYYdJJJ.BUILD[-TAG]") 'v2007d1.0033-beta' - >>> format_version(vinfo_b, raw_pattern="vYYYYd00J.BUILD[-RELEASE]") + >>> format_version(vinfo_b, raw_pattern="vYYYYd00J.BUILD[-TAG]") 'v2007d365.0033-beta' >>> format_version(vinfo_a, raw_pattern="vGGGGwVV.BLD[PYTAGNUM]") 'v2007w1.33b0' - >>> format_version(vinfo_a, raw_pattern="vGGGGw0V.BUILD[-RELEASE]") + >>> format_version(vinfo_a, raw_pattern="vGGGGw0V.BUILD[-TAG]") 'v2007w01.0033-beta' - >>> format_version(vinfo_b, raw_pattern="vGGGGw0V.BUILD[-RELEASE]") + >>> format_version(vinfo_b, raw_pattern="vGGGGw0V.BUILD[-TAG]") 'v2008w01.0033-beta' >>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final') - >>> format_version(vinfo_c, raw_pattern="vYYYYwWW.BUILD-RELEASE") + >>> format_version(vinfo_c, raw_pattern="vYYYYwWW.BUILD-TAG") 'v2007w53.0033-final' - >>> format_version(vinfo_c, raw_pattern="vYYYYwWW.BUILD[-RELEASE]") + >>> format_version(vinfo_c, raw_pattern="vYYYYwWW.BUILD[-TAG]") 'v2007w53.0033' >>> format_version(vinfo_c, raw_pattern="vMAJOR.MINOR.PATCH") 'v1.2.34' >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='final') - >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH-RELEASENUM") + >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH-TAGNUM") 'v1.0.0-final0' - >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH-RELEASE") + >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH-TAG") 'v1.0.0-final' - >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH-RELEASE") + >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH-TAG") 'v1.0.0-final' - >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH[-RELEASE]") + >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH[-TAG]") 'v1.0.0' - >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR[.PATCH[-RELEASE]]") + >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR[.PATCH[-TAG]]") 'v1.0' - >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[-RELEASE]]]") + >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[-TAG]]]") 'v1' >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=2, tag='rc', pytag='rc', num=0) >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH]]") 'v1.0.2' - >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[-RELEASE]]]") + >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[-TAG]]]") 'v1.0.2-rc' >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[PYTAGNUM]]]") 'v1.0.2rc0' @@ -547,11 +547,11 @@ def format_version(vinfo: version.V2VersionInfo, raw_pattern: str) -> str: 'v1.0.2' >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='rc', num=2) - >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[-RELEASENUM]]]") + >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[-TAGNUM]]]") 'v1.0.0-rc2' >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='rc', num=2) - >>> format_version(vinfo_d, raw_pattern='__version__ = "vMAJOR[.MINOR[.PATCH[-RELEASENUM]]]"') + >>> format_version(vinfo_d, raw_pattern='__version__ = "vMAJOR[.MINOR[.PATCH[-TAGNUM]]]"') '__version__ = "v1.0.0-rc2"' """ part_values = _format_part_values(vinfo) @@ -615,7 +615,7 @@ def _incr_numeric( minor : bool, patch : bool, tag : typ.Optional[str], - release_num: bool, + tag_num : bool, ) -> version.V2VersionInfo: # Reset major/minor/patch/num/inc to zero if any part to the left of it is incremented fields = _parse_pattern_fields(raw_pattern) @@ -647,7 +647,7 @@ def _incr_numeric( cur_vinfo = cur_vinfo._replace(minor=cur_vinfo.minor + 1, patch=0) if patch and 'patch' not in reset_fields: cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1) - if release_num and 'release_num' not in reset_fields: + if tag_num and 'tag_num' not in reset_fields: cur_vinfo = cur_vinfo._replace(num=cur_vinfo.num + 1) if tag and 'tag' not in reset_fields: if tag != cur_vinfo.tag: @@ -677,15 +677,15 @@ def is_valid_week_pattern(raw_pattern: str) -> bool: def incr( old_version: str, - raw_pattern: str = "vYYYY0M.BUILD[-RELEASE]", + raw_pattern: str = "vYYYY0M.BUILD[-TAG]", *, - tag : typ.Optional[str] = None, - major : bool = False, - minor : bool = False, - patch : bool = False, - release_num: bool = False, - pin_date : bool = False, - date : typ.Optional[dt.date] = None, + major : bool = False, + minor : bool = False, + patch : bool = False, + tag : typ.Optional[str] = None, + tag_num : bool = False, + pin_date: bool = False, + date : typ.Optional[dt.date] = None, ) -> typ.Optional[str]: """Increment version string. @@ -716,7 +716,7 @@ def incr( minor=minor, patch=patch, tag=tag, - release_num=release_num, + tag_num=tag_num, ) new_version = format_version(cur_vinfo, raw_pattern) diff --git a/src/pycalver/vcs.py b/src/pycalver2/vcs.py similarity index 99% rename from src/pycalver/vcs.py rename to src/pycalver2/vcs.py index 918778e..90f4c1c 100644 --- a/src/pycalver/vcs.py +++ b/src/pycalver2/vcs.py @@ -24,7 +24,7 @@ import subprocess as sp from pycalver import config -logger = logging.getLogger("pycalver.vcs") +logger = logging.getLogger("pycalver2.vcs") VCS_SUBCOMMANDS_BY_NAME = { diff --git a/src/pycalver/version.py b/src/pycalver2/version.py similarity index 90% rename from src/pycalver/version.py rename to src/pycalver2/version.py index b83e443..8c2d277 100644 --- a/src/pycalver/version.py +++ b/src/pycalver2/version.py @@ -81,7 +81,7 @@ class V2VersionInfo(typ.NamedTuple): TODAY = dt.datetime.utcnow().date() -RELEASE_BY_PEP440_TAG = { +TAG_BY_PEP440_TAG = { 'a' : 'alpha', 'b' : 'beta', '' : 'final', @@ -91,7 +91,7 @@ RELEASE_BY_PEP440_TAG = { } -PEP440_TAG_BY_RELEASE = { +PEP440_TAG_BY_TAG = { 'a' : 'a', 'b' : 'b', 'dev' : 'dev', @@ -107,18 +107,18 @@ PEP440_TAG_BY_RELEASE = { 'rev' : 'post', } -assert set(RELEASE_BY_PEP440_TAG.keys()) == set(PEP440_TAG_BY_RELEASE.values()) -assert set(RELEASE_BY_PEP440_TAG.values()) < set(PEP440_TAG_BY_RELEASE.keys()) +assert set(TAG_BY_PEP440_TAG.keys()) == set(PEP440_TAG_BY_TAG.values()) +assert set(TAG_BY_PEP440_TAG.values()) < set(PEP440_TAG_BY_TAG.keys()) PART_ZERO_VALUES = { - 'MAJOR' : "0", - 'MINOR' : "0", - 'PATCH' : "0", - 'RELEASE': "final", - 'PYTAG' : "", - 'NUM' : "0", - 'INC0' : "0", + 'MAJOR': "0", + 'MINOR': "0", + 'PATCH': "0", + 'TAG' : "final", + 'PYTAG': "", + 'NUM' : "0", + 'INC0' : "0", } diff --git a/test/fixtures/project_a/README.md b/test/fixtures/project_a/README.md index 6d069c5..ae141f2 100644 --- a/test/fixtures/project_a/README.md +++ b/test/fixtures/project_a/README.md @@ -1,3 +1,3 @@ # PyCalVer README Fixture -Current Version: v201612.0123-alpha +Current Version: v2016.0123-alpha diff --git a/test/fixtures/project_a/pycalver.toml b/test/fixtures/project_a/pycalver.toml index 438ccf8..418cbb5 100644 --- a/test/fixtures/project_a/pycalver.toml +++ b/test/fixtures/project_a/pycalver.toml @@ -1,5 +1,6 @@ [pycalver] -current_version = "v201710.0123-alpha" +current_version = "v2017.0123-alpha" +version_pattern = "vYYYY.BUILD[-TAG]" commit = true tag = true push = true diff --git a/test/test_cli.py b/test/test_cli.py index 30b8527..4d968fc 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -17,17 +17,20 @@ import pytest import pathlib2 as pl from click.testing import CliRunner -from pycalver import cli -from pycalver import config -from pycalver import v1patterns +from pycalver2 import cli +from pycalver2 import config +from pycalver2 import v2patterns # pylint:disable=redefined-outer-name ; pytest fixtures # pylint:disable=protected-access ; allowed for test code +# pylint:disable=unused-argument ; allowed for test code README_TEXT_FIXTURE = """ - Hello World v201701.1002-alpha ! - [aka. 201701.1002a0 !] + Hello World v2017.1002-alpha ! + [aka. 2017.1002a0 !] + Hello World v201707.1002-alpha ! + [aka. 201707.1002a0 !] """ @@ -61,6 +64,16 @@ def shell(*cmd): return sp.check_output(cmd, env=ENV) +DEBUG_LOG = 0 + + +def _debug_records(caplog): + if DEBUG_LOG: + print() + for record in caplog.records: + print(record) + + @pytest.fixture def runner(tmpdir): runner = CliRunner(env=ENV) @@ -87,7 +100,7 @@ def runner(tmpdir): def test_help(runner): result = runner.invoke(cli.cli, ['--help', "-vv"]) assert result.exit_code == 0 - assert "PyCalVer" in result.output + assert "CalVer" in result.output assert "bump " in result.output assert "test " in result.output assert "init " in result.output @@ -98,29 +111,37 @@ def test_version(runner): result = runner.invoke(cli.cli, ['--version', "-vv"]) assert result.exit_code == 0 assert " version v20" in result.output - match = v1patterns.PYCALVER_RE.search(result.output) + pattern = v2patterns.compile_pattern("vYYYY.BUILD[-TAG]") + match = pattern.regexp.search(result.output) assert match def test_incr_default(runner): - old_version = "v201701.0004-alpha" + old_version = "v201709.1004-alpha" - cmd = ['test', "-vv", "--pin-date", "--release", "beta", old_version] + cmd = ['test', "-vv", "--pin-date", "--tag", "beta", old_version, "{pycalver}"] result = runner.invoke(cli.cli, cmd) assert result.exit_code == 0 - assert "Version: v201701.0005-beta\n" in result.output + assert "Version: v201709.1005-beta\n" in result.output - cmd = ['test', "-vv", "--pin-date", "--release", "beta", old_version, "vYYYY0M.BUILD[-RELEASE]"] + old_version = "v2017.1004-alpha" + + cmd = ['test', "-vv", "--pin-date", "--tag", "beta", old_version, "v{year}{build}{release}"] result = runner.invoke(cli.cli, cmd) assert result.exit_code == 0 - assert "Version: v201701.1005-beta\n" in result.output + assert "Version: v2017.1005-beta\n" in result.output + + cmd = ['test', "-vv", "--pin-date", "--tag", "beta", old_version, "vYYYY.BUILD[-TAG]"] + result = runner.invoke(cli.cli, cmd) + assert result.exit_code == 0 + assert "Version: v2017.1005-beta\n" in result.output def test_incr_pin_date(runner): - old_version = "v201701.0999-alpha" + old_version = "v2017.1999-alpha" result = runner.invoke(cli.cli, ['test', "-vv", "--pin-date", old_version]) assert result.exit_code == 0 - assert "Version: v201701.11000-alpha\n" in result.output + assert "Version: v2017.22000-alpha\n" in result.output def test_incr_semver(runner): @@ -158,27 +179,28 @@ def test_incr_semver_invalid(runner, caplog): assert result.exit_code == 1 assert len(caplog.records) > 0 log_record = caplog.records[0] - assert "Invalid version string" in log_record.message - assert "for pattern '{pycalver}'" in log_record.message + assert "--patch is not applicable to pattern" in log_record.message + assert "to pattern 'vYYYY.BUILD[-TAG]'" in log_record.message def test_incr_to_beta(runner): - old_version = "v201701.0999-alpha" + old_version = "v2017.1999-alpha" initial_version = config._initial_version() - result = runner.invoke(cli.cli, ['test', old_version, "-vv", "--release", "beta"]) + result = runner.invoke(cli.cli, ['test', old_version, "-vv", "--tag", "beta"]) assert result.exit_code == 0 - new_version = initial_version.replace(".1001-alpha", ".11000-beta") + new_version = initial_version.replace(".1001-alpha", ".22000-beta") assert f"Version: {new_version}\n" in result.output -def test_incr_to_final(runner): - old_version = "v201701.0999-alpha" +def test_incr_to_final(runner, caplog): + old_version = "v2017.1999-alpha" initial_version = config._initial_version() - result = runner.invoke(cli.cli, ['test', old_version, "-vv", "--release", "final"]) + result = runner.invoke(cli.cli, ['test', old_version, "-vv", "--tag", "final"]) + _debug_records(caplog) assert result.exit_code == 0 - new_version = initial_version.replace(".1001-alpha", ".11000") + new_version = initial_version.replace(".1001-alpha", ".22000") assert f"Version: {new_version}\n" in result.output @@ -188,15 +210,15 @@ def test_incr_release_num(runner): old_version = "0.1.0b0" new_version = "0.1.0b1" - result = runner.invoke(cli.cli, ['test', "-vv", "--release-num", old_version, semver]) + result = runner.invoke(cli.cli, ['test', "-vv", "--tag-num", old_version, semver]) assert result.exit_code == 0 assert f"Version: {new_version}\n" in result.output def test_incr_invalid(runner): - old_version = "v201701.0999-alpha" + old_version = "v2017.1999-alpha" - result = runner.invoke(cli.cli, ['test', old_version, "-vv", "--release", "alfa"]) + result = runner.invoke(cli.cli, ['test', old_version, "-vv", "--tag", "alfa"]) assert result.exit_code == 1 @@ -238,7 +260,7 @@ def test_nocfg(runner, caplog): _add_project_files("README.md") result = runner.invoke(cli.cli, ['show', "-vv"]) assert result.exit_code == 1 - expected_msg = "Could not parse configuration. Perhaps try 'pycalver init'." + expected_msg = "Could not parse configuration. Perhaps try 'calver init'." assert any(expected_msg in r.message for r in caplog.records) @@ -326,46 +348,57 @@ def _vcs_init(vcs, files=("README.md",)): shell(f"{vcs}", "commit", "-m", "initial commit") +_today = dt.datetime.utcnow().date() + + DEFAULT_VERSION_PATTERNS = [ - '"{pycalver}"', - '"vYYYY0M.BUILD[-RELEASE]"', + ('"vYYYY0M.BUILD[-TAG]"' , _today.strftime("v%Y%m.1001-alpha"), _today.strftime("%Y%m.1001a0")), + ('"vYYYY.BUILD[-TAG]"' , _today.strftime("v%Y.1001-alpha"), _today.strftime("%Y.1001a0")), + ('"{pycalver}"' , _today.strftime("v%Y%m.1001-alpha"), _today.strftime("%Y%m.1001a0")), + ('"v{year}{build}{release}"', _today.strftime("v%Y.1001-alpha"), _today.strftime("%Y.1001a0")), ] -@pytest.mark.parametrize("version_pattern", DEFAULT_VERSION_PATTERNS) -def test_git_init(runner, version_pattern): +@pytest.mark.parametrize("version_pattern, cur_version, cur_pep440", DEFAULT_VERSION_PATTERNS) +def test_git_init(runner, version_pattern, cur_version, cur_pep440): _add_project_files("README.md") _vcs_init("git") result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 - _update_config_val("pycalver.toml", version_pattern=version_pattern) + _update_config_val( + "pycalver.toml", + version_pattern=version_pattern, + current_version='"' + cur_version + '"', + ) result = runner.invoke(cli.cli, ['show']) assert result.exit_code == 0 - assert f"Current Version: {config._initial_version()}\n" in result.output - assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output + assert f"Current Version: {cur_version}\n" in result.output -@pytest.mark.parametrize("version_pattern", DEFAULT_VERSION_PATTERNS) -def test_hg_init(runner, version_pattern): +@pytest.mark.parametrize("version_pattern, cur_version, cur_pep440", DEFAULT_VERSION_PATTERNS) +def test_hg_init(runner, version_pattern, cur_version, cur_pep440): _add_project_files("README.md") _vcs_init("hg") result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 - _update_config_val("pycalver.toml", version_pattern=version_pattern) + _update_config_val( + "pycalver.toml", + version_pattern=version_pattern, + current_version='"' + cur_version + '"', + ) result = runner.invoke(cli.cli, ['show']) assert result.exit_code == 0 - assert f"Current Version: {config._initial_version()}\n" in result.output - assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output + assert f"Current Version: {cur_version}\n" in result.output -@pytest.mark.parametrize("version_pattern", DEFAULT_VERSION_PATTERNS) -def test_v1_git_tag_eval(runner, version_pattern): +@pytest.mark.parametrize("version_pattern, cur_version, cur_pep440", DEFAULT_VERSION_PATTERNS) +def test_v1_git_tag_eval(runner, version_pattern, cur_version, cur_pep440): _add_project_files("README.md") _vcs_init("git") @@ -374,22 +407,24 @@ def test_v1_git_tag_eval(runner, version_pattern): result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 - _update_config_val("pycalver.toml", version_pattern=version_pattern) + _update_config_val( + "pycalver.toml", + version_pattern=version_pattern, + current_version='"' + cur_version + '"', + ) - initial_version = config._initial_version() - tag_version = initial_version.replace(".1001-alpha", ".1123-beta") - tag_version_pep440 = tag_version[1:7] + ".1123b0" + tag_version = cur_version.replace(".1001-alpha", ".1123-beta") + assert tag_version != cur_version shell("git", "tag", "--annotate", tag_version, "--message", f"bump version to {tag_version}") result = runner.invoke(cli.cli, ['show', "-vv"]) assert result.exit_code == 0 assert f"Current Version: {tag_version}\n" in result.output - assert f"PEP440 : {tag_version_pep440}\n" in result.output -@pytest.mark.parametrize("version_pattern", DEFAULT_VERSION_PATTERNS) -def test_hg_tag_eval(runner, version_pattern): +@pytest.mark.parametrize("version_pattern, cur_version, cur_pep440", DEFAULT_VERSION_PATTERNS) +def test_hg_tag_eval(runner, version_pattern, cur_version, cur_pep440): _add_project_files("README.md") _vcs_init("hg") @@ -398,11 +433,14 @@ def test_hg_tag_eval(runner, version_pattern): result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 - _update_config_val("pycalver.toml", version_pattern=version_pattern) + _update_config_val( + "pycalver.toml", + version_pattern=version_pattern, + current_version='"' + cur_version + '"', + ) - initial_version = config._initial_version() - tag_version = initial_version.replace(".1001-alpha", ".1123-beta") - tag_version_pep440 = tag_version[1:7] + ".1123b0" + tag_version = cur_version.replace(".1001-alpha", ".1123-beta") + tag_version_pep440 = tag_version[1:].split(".")[0] + ".1123b0" shell("hg", "tag", tag_version, "--message", f"bump version to {tag_version}") @@ -412,26 +450,33 @@ def test_hg_tag_eval(runner, version_pattern): assert f"PEP440 : {tag_version_pep440}\n" in result.output -@pytest.mark.parametrize("version_pattern", DEFAULT_VERSION_PATTERNS) -def test_novcs_bump(runner, version_pattern): +@pytest.mark.parametrize("version_pattern, cur_version, cur_pep440", DEFAULT_VERSION_PATTERNS) +def test_novcs_bump(runner, version_pattern, cur_version, cur_pep440): _add_project_files("README.md") result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 - _update_config_val("pycalver.toml", version_pattern=version_pattern) + _update_config_val( + "pycalver.toml", + version_pattern=version_pattern, + current_version='"' + cur_version + '"', + ) + + with pl.Path("README.md").open(mode="r") as fobj: + content = fobj.read() result = runner.invoke(cli.cli, ['bump', "-vv"]) assert result.exit_code == 0 - calver = config._initial_version().split(".")[0] + calver = cur_version.split(".")[0] with pl.Path("README.md").open() as fobj: content = fobj.read() assert calver + ".1002-alpha !\n" in content assert calver[1:] + ".1002a0 !]\n" in content - result = runner.invoke(cli.cli, ['bump', "-vv", "--release", "beta"]) + result = runner.invoke(cli.cli, ['bump', "-vv", "--tag", "beta"]) assert result.exit_code == 0 with pl.Path("README.md").open() as fobj: @@ -440,38 +485,47 @@ def test_novcs_bump(runner, version_pattern): assert calver[1:] + ".1003b0 !]\n" in content -@pytest.mark.parametrize("version_pattern", DEFAULT_VERSION_PATTERNS) -def test_git_bump(runner, version_pattern): +@pytest.mark.parametrize("version_pattern, cur_version, cur_pep440", DEFAULT_VERSION_PATTERNS) +def test_git_bump(runner, caplog, version_pattern, cur_version, cur_pep440): _add_project_files("README.md") _vcs_init("git") result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 - _update_config_val("pycalver.toml", version_pattern=version_pattern) + _update_config_val( + "pycalver.toml", + version_pattern=version_pattern, + current_version='"' + cur_version + '"', + ) shell("git", "add", "pycalver.toml") shell("git", "commit", "-m", "initial commit") result = runner.invoke(cli.cli, ['bump', "-vv"]) + _debug_records(caplog) assert result.exit_code == 0 - calver = config._initial_version()[:7] + calver = cur_version.split(".")[0] with pl.Path("README.md").open() as fobj: content = fobj.read() assert calver + ".1002-alpha !\n" in content -@pytest.mark.parametrize("version_pattern", DEFAULT_VERSION_PATTERNS) -def test_hg_bump(runner, version_pattern): +@pytest.mark.parametrize("version_pattern, cur_version, cur_pep440", DEFAULT_VERSION_PATTERNS) +def test_hg_bump(runner, version_pattern, cur_version, cur_pep440): _add_project_files("README.md") _vcs_init("hg") result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 - _update_config_val("pycalver.toml", version_pattern=version_pattern) + _update_config_val( + "pycalver.toml", + version_pattern=version_pattern, + current_version='"' + cur_version + '"', + ) shell("hg", "add", "pycalver.toml") shell("hg", "commit", "-m", "initial commit") @@ -479,7 +533,7 @@ def test_hg_bump(runner, version_pattern): result = runner.invoke(cli.cli, ['bump', "-vv"]) assert result.exit_code == 0 - calver = config._initial_version()[:7] + calver = cur_version.split(".")[0] with pl.Path("README.md").open() as fobj: content = fobj.read() @@ -490,6 +544,7 @@ def test_empty_git_bump(runner, caplog): shell("git", "init") with pl.Path("setup.cfg").open(mode="w") as fobj: fobj.write("") + result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 @@ -511,6 +566,7 @@ def test_empty_hg_bump(runner, caplog): shell("hg", "init") with pl.Path("setup.cfg").open(mode="w") as fobj: fobj.write("") + result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 @@ -597,27 +653,38 @@ def test_v1_bump_semver_diff(runner, caplog, version_pattern): assert f"+current_version = \"{expected}\"" in out_lines -@pytest.mark.parametrize("version_pattern", DEFAULT_VERSION_PATTERNS) -def test_get_diff(runner, version_pattern): +@pytest.mark.parametrize("version_pattern, cur_version, cur_pep440", DEFAULT_VERSION_PATTERNS) +def test_get_diff(runner, version_pattern, cur_version, cur_pep440): _add_project_files("README.md", "setup.cfg") result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 - _update_config_val("setup.cfg", version_pattern=version_pattern) + if len(cur_pep440) == 11: + old_version = "v2017.1002-alpha" + old_pep440 = "2017.1002a0" + elif len(cur_pep440) == 13: + old_version = "v201707.1002-alpha" + old_pep440 = "201707.1002a0" + else: + assert False, len(cur_pep440) + _update_config_val( + "setup.cfg", + version_pattern=version_pattern, + current_version='"' + old_version + '"', + ) _, cfg = config.init() - new_version = "v202010.1003-beta" - - diff_str = cli.get_diff(cfg, new_version) + diff_str = cli.get_diff(cfg, cur_version) diff_lines = set(diff_str.splitlines()) - assert "- Hello World v201701.1002-alpha !" in diff_lines - assert "- [aka. 201701.1002a0 !]" in diff_lines - assert "+ Hello World v202010.1003-beta !" in diff_lines - assert "+ [aka. 202010.1003b0 !]" in diff_lines + assert f"- Hello World {old_version} !" in diff_lines + assert f"+ Hello World {cur_version} !" in diff_lines - assert '-current_version = "v202010.1001-alpha"' in diff_lines - assert '+current_version = "v202010.1003-beta"' in diff_lines + assert f"- [aka. {old_pep440} !]" in diff_lines + assert f"+ [aka. {cur_pep440} !]" in diff_lines + + assert f'-current_version = "{old_version}"' in diff_lines + assert f'+current_version = "{cur_version}"' in diff_lines WEEKNUM_TEST_CASES = [ @@ -680,21 +747,21 @@ def test_hg_commit_message(runner, caplog): commit_message = """ "bump from {old_version} ({old_version_pep440}) to {new_version} ({new_version_pep440})" """ - _update_config_val("setup.cfg", current_version='"v201903.1001-alpha"') + _update_config_val("setup.cfg", current_version='"v2019.1001-alpha"') _update_config_val("setup.cfg", commit_message=commit_message.strip()) _vcs_init("hg", ["README.md", "setup.cfg"]) assert len(caplog.records) > 0 - result = runner.invoke(cli.cli, ['bump', "-vv", "--pin-date", "--release", "beta"]) + result = runner.invoke(cli.cli, ['bump', "-vv", "--pin-date", "--tag", "beta"]) assert result.exit_code == 0 tags = shell("hg", "tags").decode("utf-8") - assert "v201903.1002-beta" in tags + assert "v2019.1002-beta" in tags commits = shell(*shlex.split("hg log -l 2")).decode("utf-8").split("\n\n") - expected = "bump from v201903.1001-alpha (201903.1001a0) to v201903.1002-beta (201903.1002b0)" + expected = "bump from v2019.1001-alpha (2019.1001a0) to v2019.1002-beta (2019.1002b0)" summary = commits[1].split("summary:")[-1] assert expected in summary @@ -707,37 +774,36 @@ def test_git_commit_message(runner, caplog): commit_message = """ "bump: {old_version} ({old_version_pep440}) -> {new_version} ({new_version_pep440})" """ - _update_config_val("setup.cfg", current_version='"v201903.1001-alpha"') + _update_config_val("setup.cfg", current_version='"v2019.1001-alpha"') _update_config_val("setup.cfg", commit_message=commit_message.strip()) _vcs_init("git", ["README.md", "setup.cfg"]) assert len(caplog.records) > 0 - result = runner.invoke(cli.cli, ['bump', "-vv", "--pin-date", "--release", "beta"]) + result = runner.invoke(cli.cli, ['bump', "-vv", "--pin-date", "--tag", "beta"]) assert result.exit_code == 0 tags = shell("git", "tag", "--list").decode("utf-8") - assert "v201903.1002-beta" in tags + assert "v2019.1002-beta" in tags commits = shell(*shlex.split("git log -l 2")).decode("utf-8").split("\n\n") - expected = "bump: v201903.1001-alpha (201903.1001a0) -> v201903.1002-beta (201903.1002b0)" + expected = "bump: v2019.1001-alpha (2019.1001a0) -> v2019.1002-beta (2019.1002b0)" assert expected in commits[1] def test_grep(runner): _add_project_files("README.md") - # - search_re = r"^\s+2:\s+Hello World v201701\.1002-alpha !" + search_re = r"^\s+2:\s+Hello World v2017\.1002-alpha !" - cmd1 = r'grep "vYYYY0M.BUILD[-RELEASE]" README.md' + cmd1 = r'grep "vYYYY.BUILD[-TAG]" README.md' result1 = runner.invoke(cli.cli, shlex.split(cmd1)) assert result1.exit_code == 0 assert "README.md" in result1.output assert re.search(search_re, result1.output, flags=re.MULTILINE) - cmd2 = r'grep --version-pattern "vYYYY0M.BUILD[-RELEASE]" "{version}" README.md' + cmd2 = r'grep --version-pattern "vYYYY.BUILD[-TAG]" "{version}" README.md' result2 = runner.invoke(cli.cli, shlex.split(cmd2)) assert result2.exit_code == 0 assert "README.md" in result2.output @@ -745,17 +811,15 @@ def test_grep(runner): assert result1.output == result2.output - search_re = r"^\s+3:\s+\[aka\. 201701\.1002a0 \!\]" + search_re = r"^\s+3:\s+\[aka\. 2017\.1002a0 \!\]" - cmd3 = r'grep "\[aka. YYYY0M.BLD[PYTAGNUM] \!\]" README.md' + cmd3 = r'grep "\[aka. YYYY.BLD[PYTAGNUM] \!\]" README.md' result3 = runner.invoke(cli.cli, shlex.split(cmd3)) assert result3.exit_code == 0 assert "README.md" in result3.output assert re.search(search_re, result3.output, flags=re.MULTILINE) - cmd4 = ( - r'grep --version-pattern "vYYYY0M.BUILD[-RELEASE]" "\[aka. {pep440_version} \!\]" README.md' - ) + cmd4 = r'grep --version-pattern "vYYYY.BUILD[-TAG]" "\[aka. {pep440_version} \!\]" README.md' result4 = runner.invoke(cli.cli, shlex.split(cmd4)) assert result4.exit_code == 0 assert "README.md" in result4.output @@ -784,14 +848,14 @@ def test_multimatch_file_patterns(runner): with pl.Path("setup.cfg").open(mode="w", encoding="utf-8") as fobj: fobj.write(SETUP_CFG_MULTIMATCH_FILE_PATTERNS_FIXTURE) - result = runner.invoke(cli.cli, ['bump', '--release', 'beta']) + result = runner.invoke(cli.cli, ['bump', '--tag', 'beta', '--date', "2020-11-22"]) assert result.exit_code == 0 with pl.Path("README.md").open(mode="r", encoding="utf-8") as fobj: - readme_text = fobj.read() + content = fobj.read() - assert "Hello World v202010.1003-beta !" in readme_text - assert "[aka. 202010.1003b0 !]" in readme_text + assert "Hello World v202011.1003-beta !" in content + assert "[aka. 202011.1003b0 !]" in content def _kwargs(year, month, minor=False): diff --git a/test/test_config.py b/test/test_config.py index 7d66cbb..33c79fe 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -7,7 +7,7 @@ from __future__ import unicode_literals import io from test import util -from pycalver import config +from pycalver2 import config # pylint:disable=redefined-outer-name ; pytest fixtures # pylint:disable=protected-access ; allowed for test code @@ -66,7 +66,7 @@ setup.cfg = NEW_PATTERN_CFG_FIXTURE = """ [pycalver] current_version = "v201808.1456-beta" -version_pattern = "vYYYY0M.BUILD[-RELEASE]" +version_pattern = "vYYYY0M.BUILD[-TAG]" commit_message = "bump version to {new_version}" commit = True tag = True @@ -169,10 +169,10 @@ def test_parse_v2_cfg(): raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) assert raw_patterns_by_filepath["setup.py"] == [ - "vYYYY0M.BUILD[-RELEASE]", + "vYYYY0M.BUILD[-TAG]", "YYYY0M.BLD[PYTAGNUM]", ] - assert raw_patterns_by_filepath["setup.cfg"] == ['current_version = "vYYYY0M.BUILD[-RELEASE]"'] + assert raw_patterns_by_filepath["setup.cfg"] == ['current_version = "vYYYY0M.BUILD[-TAG]"'] assert raw_patterns_by_filepath["src/project/*.py"] == ["Copyright (c) 2018-YYYY"] @@ -210,7 +210,7 @@ def test_parse_project_toml(): with config_path.open() as fobj: config_data = fobj.read() - assert "v201710.0123-alpha" in config_data + assert "v2017.0123-alpha" in config_data ctx = config.init_project_ctx(project_path) assert ctx == config.ProjectContext(project_path, config_path, config_rel_path, "toml", None) @@ -219,7 +219,7 @@ def test_parse_project_toml(): assert cfg - assert cfg.current_version == "v201710.0123-alpha" + assert cfg.current_version == "v2017.0123-alpha" assert cfg.commit is True assert cfg.tag is True assert cfg.push is True diff --git a/test/test_parse.py b/test/test_parse.py index 33ec7b6..f571ec3 100644 --- a/test/test_parse.py +++ b/test/test_parse.py @@ -4,8 +4,8 @@ from __future__ import print_function from __future__ import absolute_import from __future__ import unicode_literals -from pycalver import parse -from pycalver import v1patterns +from pycalver2 import parse +from pycalver2 import v1patterns SETUP_PY_FIXTURE = """ # setup.py diff --git a/test/test_patterns.py b/test/test_patterns.py index 981a9af..ae67fbe 100644 --- a/test/test_patterns.py +++ b/test/test_patterns.py @@ -8,8 +8,8 @@ import re import pytest -from pycalver import v1patterns -from pycalver import v2patterns +from pycalver2 import v1patterns +from pycalver2 import v2patterns V2_PART_PATTERN_CASES = [ (['YYYY', 'GGGG'], "2020" , "2020"), @@ -127,23 +127,23 @@ V2_PART_PATTERN_CASES = [ (['0V'], "53", "53"), (['0V'], "54", None), (['MAJOR', 'MINOR', 'PATCH'], "0", "0"), - (['RELEASE'], "alpha" , "alpha"), - (['RELEASE'], "alfa" , None), - (['RELEASE'], "beta" , "beta"), - (['RELEASE'], "rc" , "rc"), - (['RELEASE'], "post" , "post"), - (['RELEASE'], "final" , "final"), - (['RELEASE'], "latest", None), - (['PYTAG' ], "a" , "a"), - (['PYTAG' ], "b" , "b"), - (['PYTAG' ], "rc" , "rc"), - (['PYTAG' ], "post" , "post"), - (['PYTAG' ], "post" , "post"), - (['PYTAG' ], "x" , None), - (['NUM' ], "a" , None), - (['NUM' ], "0" , "0"), - (['NUM' ], "1" , "1"), - (['NUM' ], "10" , "10"), + (['TAG' ], "alpha" , "alpha"), + (['TAG' ], "alfa" , None), + (['TAG' ], "beta" , "beta"), + (['TAG' ], "rc" , "rc"), + (['TAG' ], "post" , "post"), + (['TAG' ], "final" , "final"), + (['TAG' ], "latest", None), + (['PYTAG'], "a" , "a"), + (['PYTAG'], "b" , "b"), + (['PYTAG'], "rc" , "rc"), + (['PYTAG'], "post" , "post"), + (['PYTAG'], "post" , "post"), + (['PYTAG'], "x" , None), + (['NUM' ], "a" , None), + (['NUM' ], "0" , "0"), + (['NUM' ], "1" , "1"), + (['NUM' ], "10" , "10"), ] diff --git a/test/test_rewrite.py b/test/test_rewrite.py index 9d50961..cec7bc7 100644 --- a/test/test_rewrite.py +++ b/test/test_rewrite.py @@ -8,14 +8,14 @@ import re import copy from test import util -from pycalver import config -from pycalver import rewrite -from pycalver import v1rewrite -from pycalver import v1version -from pycalver import v2rewrite -from pycalver import v2version -from pycalver import v1patterns -from pycalver import v2patterns +from pycalver2 import config +from pycalver2 import rewrite +from pycalver2 import v1rewrite +from pycalver2 import v1version +from pycalver2 import v2rewrite +from pycalver2 import v2version +from pycalver2 import v1patterns +from pycalver2 import v2patterns # pylint:disable=protected-access ; allowed for test code @@ -56,7 +56,7 @@ def test_v1_rewrite_lines(): def test_v2_rewrite_lines(): - version_pattern = "vYYYY0M.BUILD[-RELEASE]" + version_pattern = "vYYYY0M.BUILD[-TAG]" new_vinfo = v2version.parse_version_info("v201811.0123-beta", version_pattern) patterns = [v2patterns.compile_pattern(version_pattern, '__version__ = "{version}"')] lines = v2rewrite.rewrite_lines(patterns, new_vinfo, ['__version__ = "v201809.0002-alpha" ']) @@ -191,10 +191,10 @@ def test_v1_optional_release(): def test_v2_optional_release(): - version_pattern = "YYYY.BUILD[-RELEASE]" + version_pattern = "YYYY.BUILD[-TAG]" new_vinfo = v2version.parse_version_info("2019.0003", version_pattern) - raw_pattern = '__version__ = "YYYY.BUILD[-RELEASE]"' + raw_pattern = '__version__ = "YYYY.BUILD[-TAG]"' pattern = v2patterns.compile_pattern(version_pattern, raw_pattern) old_lines = OPTIONAL_RELEASE_FIXTURE.splitlines() @@ -216,14 +216,13 @@ def test_v2_optional_release(): def test_v1_iter_rewritten(): - version_pattern = "{pycalver}" - new_vinfo = v1version.parse_version_info("v201809.0123") + version_pattern = "v{year}{build}{release}" + new_vinfo = v1version.parse_version_info("v2018.0123", version_pattern) - file_patterns = { - "src/pycalver/__init__.py": [ - v1patterns.compile_pattern(version_pattern, '__version__ = "{pycalver}"'), - ] - } + init_pattern = v1patterns.compile_pattern( + version_pattern, '__version__ = "v{year}{build}{release}"' + ) + file_patterns = {"src/pycalver2/__init__.py": [init_pattern]} rewritten_datas = v1rewrite.iter_rewritten(file_patterns, new_vinfo) rfd = list(rewritten_datas)[0] expected = [ @@ -234,19 +233,19 @@ def test_v1_iter_rewritten(): "# SPDX-License-Identifier: MIT", '"""PyCalVer: CalVer for Python Packages."""', '', - '__version__ = "v201809.0123"', + '__version__ = "v2018.0123"', '', ] assert rfd.new_lines == expected def test_v2_iter_rewritten(): - version_pattern = "vYYYY0M.BUILD[-RELEASE]" - new_vinfo = v2version.parse_version_info("v201809.0123", version_pattern) + version_pattern = "vYYYY.BUILD[-TAG]" + new_vinfo = v2version.parse_version_info("v2018.0123", version_pattern) file_patterns = { - "src/pycalver/__init__.py": [ - v2patterns.compile_pattern(version_pattern, '__version__ = "vYYYY0M.BUILD[-RELEASE]"'), + "src/pycalver2/__init__.py": [ + v2patterns.compile_pattern(version_pattern, '__version__ = "vYYYY.BUILD[-TAG]"'), ] } @@ -260,32 +259,36 @@ def test_v2_iter_rewritten(): "# SPDX-License-Identifier: MIT", '"""PyCalVer: CalVer for Python Packages."""', '', - '__version__ = "v201809.0123"', + '__version__ = "v2018.0123"', '', ] assert rfd.new_lines == expected def test_v1_diff(): - version_pattern = "{pycalver}" - raw_pattern = '__version__ = "{pycalver}"' + version_pattern = "v{year}{build}{release}" + raw_pattern = '__version__ = "v{year}{build}{release}"' pattern = v1patterns.compile_pattern(version_pattern, raw_pattern) - file_patterns = {"src/pycalver/__init__.py": [pattern]} + file_patterns = {"src/pycalver2/__init__.py": [pattern]} old_vinfo = v1version.parse_version_info("v201809.0123") - new_vinfo = v1version.parse_version_info("v201910.1124") + new_vinfo = v1version.parse_version_info("v201911.1124") + assert new_vinfo > old_vinfo + + old_vinfo = v1version.parse_version_info("v2018.0123", version_pattern) + new_vinfo = v1version.parse_version_info("v2019.1124", version_pattern) diff_str = v1rewrite.diff(old_vinfo, new_vinfo, file_patterns) lines = diff_str.split("\n") - assert lines[:2] == ["--- src/pycalver/__init__.py", "+++ src/pycalver/__init__.py"] + assert lines[:2] == ["--- src/pycalver2/__init__.py", "+++ src/pycalver2/__init__.py"] assert lines[6].startswith('-__version__ = "v20') assert lines[7].startswith('+__version__ = "v20') - assert not lines[6].startswith('-__version__ = "v201809.0123"') + assert not lines[6].startswith('-__version__ = "v2018.0123"') - assert lines[7] == '+__version__ = "v201910.1124"' + assert lines[7] == '+__version__ = "v2019.1124"' raw_pattern = "Copyright (c) 2018-{year}" pattern = v1patterns.compile_pattern(version_pattern, raw_pattern) @@ -298,25 +301,25 @@ def test_v1_diff(): def test_v2_diff(): - version_pattern = "vYYYY0M.BUILD[-RELEASE]" - raw_pattern = '__version__ = "vYYYY0M.BUILD[-RELEASE]"' + version_pattern = "vYYYY.BUILD[-TAG]" + raw_pattern = '__version__ = "vYYYY.BUILD[-TAG]"' pattern = v2patterns.compile_pattern(version_pattern, raw_pattern) - file_patterns = {"src/pycalver/__init__.py": [pattern]} + file_patterns = {"src/pycalver2/__init__.py": [pattern]} - old_vinfo = v2version.parse_version_info("v201809.0123", version_pattern) - new_vinfo = v2version.parse_version_info("v201910.1124", version_pattern) + old_vinfo = v2version.parse_version_info("v2018.0123", version_pattern) + new_vinfo = v2version.parse_version_info("v2019.1124", version_pattern) diff_str = v2rewrite.diff(old_vinfo, new_vinfo, file_patterns) lines = diff_str.split("\n") - assert lines[:2] == ["--- src/pycalver/__init__.py", "+++ src/pycalver/__init__.py"] + assert lines[:2] == ["--- src/pycalver2/__init__.py", "+++ src/pycalver2/__init__.py"] assert lines[6].startswith('-__version__ = "v20') assert lines[7].startswith('+__version__ = "v20') - assert not lines[6].startswith('-__version__ = "v201809.0123"') + assert not lines[6].startswith('-__version__ = "v2018.0123"') - assert lines[7] == '+__version__ = "v201910.1124"' + assert lines[7] == '+__version__ = "v2019.1124"' raw_pattern = "Copyright (c) 2018-YYYY" pattern = v2patterns.compile_pattern(version_pattern, raw_pattern) diff --git a/test/test_version.py b/test/test_version.py index b6e6bae..2b7b0a1 100644 --- a/test/test_version.py +++ b/test/test_version.py @@ -9,11 +9,11 @@ import datetime as dt import pytest -from pycalver import version -from pycalver import v1version -from pycalver import v2version -from pycalver import v1patterns -from pycalver import v2patterns +from pycalver2 import version +from pycalver2 import v1version +from pycalver2 import v2version +from pycalver2 import v1patterns +from pycalver2 import v2patterns # pylint:disable=protected-access ; allowed for test code @@ -212,29 +212,29 @@ def test_v1_parse_versions(pattern_str, line, expected_vinfo): def test_v2_parse_versions(): - _vnfo = v2version.parse_version_info("v201712.0033", raw_pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") + _vnfo = v2version.parse_version_info("v201712.0033", raw_pattern="vYYYY0M.BUILD[-TAG[NUM]]") fvals = {'year_y': 2017, 'month': 12, 'bid': "0033"} assert _vnfo == v2version.parse_field_values_to_vinfo(fvals) def test_v2_format_version(): - version_pattern = "vYYYY0M.BUILD[-RELEASE[NUM]]" + version_pattern = "vYYYY0M.BUILD[-TAG[NUM]]" in_version = "v200701.0033-beta" vinfo = v2version.parse_version_info(in_version, raw_pattern=version_pattern) out_version = v2version.format_version(vinfo, raw_pattern=version_pattern) assert in_version == out_version - result = v2version.format_version(vinfo, raw_pattern="v0Y.BUILD[-RELEASE]") + result = v2version.format_version(vinfo, raw_pattern="v0Y.BUILD[-TAG]") assert result == "v07.0033-beta" - result = v2version.format_version(vinfo, raw_pattern="vYY.BLD[-RELEASE]") + result = v2version.format_version(vinfo, raw_pattern="vYY.BLD[-TAG]") assert result == "v7.33-beta" - result = v2version.format_version(vinfo, raw_pattern="vYY.BLD-RELEASE") + result = v2version.format_version(vinfo, raw_pattern="vYY.BLD-TAG") assert result == "v7.33-beta" - result = v2version.format_version(vinfo, raw_pattern='__version__ = "YYYY.BUILD[-RELEASE]"') + result = v2version.format_version(vinfo, raw_pattern='__version__ = "YYYY.BUILD[-TAG]"') assert result == '__version__ = "2007.0033-beta"' result = v2version.format_version(vinfo, raw_pattern='__version__ = "YYYY.BLD"') From 70a166275eeca88955cff6c4fc35e99db567a488 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 15 Oct 2020 11:38:43 +0000 Subject: [PATCH 90/98] fix importerror --- src/pycalver2/vcs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pycalver2/vcs.py b/src/pycalver2/vcs.py index 90f4c1c..ab25193 100644 --- a/src/pycalver2/vcs.py +++ b/src/pycalver2/vcs.py @@ -22,7 +22,7 @@ import logging import tempfile import subprocess as sp -from pycalver import config +from . import config logger = logging.getLogger("pycalver2.vcs") From 5f66a42c176a08f9dc1e533ce61eb897fe57e0ac Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 15 Oct 2020 16:53:19 +0000 Subject: [PATCH 91/98] update requirements --- Makefile.bootstrapit.make | 2 -- requirements/integration.txt | 9 ++++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Makefile.bootstrapit.make b/Makefile.bootstrapit.make index cb91823..9bdfaa1 100644 --- a/Makefile.bootstrapit.make +++ b/Makefile.bootstrapit.make @@ -300,7 +300,6 @@ git_hooks: lint_isort: @printf "isort ...\n" @$(DEV_ENV)/bin/isort \ - --recursive \ --check-only \ --line-width=$(MAX_LINE_LEN) \ --project $(MODULE_NAME) \ @@ -425,7 +424,6 @@ test: .PHONY: fmt_isort fmt_isort: @$(DEV_ENV)/bin/isort \ - --recursive \ --line-width=$(MAX_LINE_LEN) \ --project $(MODULE_NAME) \ src/ test/; diff --git a/requirements/integration.txt b/requirements/integration.txt index 3391511..4076d54 100644 --- a/requirements/integration.txt +++ b/requirements/integration.txt @@ -20,16 +20,15 @@ flake8-comprehensions flake8-junit-report flake8-2020 pylint-ignore>=2020.1013 -mypy -# pylint doesn't support isort>=5 for now -# https://github.com/PyCQA/pylint/issues/3722 -isort<5 +mypy>=0.790 +isort # http://doc.pytest.org/en/latest/py27-py34-deprecation.html # The pytest 4.6 series will be the last to support Python 2.7 # and 3.4, and is scheduled to be released by mid-2019. # pytest 5.0 and onwards will support only Python 3.5+. -pytest<5.0 +pytest; python_version >= "3.5" +pytest<5.0; python_version < "3.5" pytest-cov # https://github.com/pytest-dev/pytest-html/blob/master/CHANGES.rst # pytest-html 2.0+ doesn't support python2.7 From a3499c19a6ff514419e4b1ea11769b646060c2b0 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 15 Oct 2020 19:54:26 +0000 Subject: [PATCH 92/98] update defaults and tests --- README.md | 328 +++++++++--------- scripts/update_readme_examples.py | 8 +- setup.cfg | 10 +- setup.py | 2 +- src/pycalver2/cli.py | 2 +- src/pycalver2/config.py | 131 ++++--- src/pycalver2/vcs.py | 3 +- test/fixtures/project_a/README.md | 2 +- .../project_a/{pycalver.toml => calver.toml} | 6 +- test/fixtures/project_b/setup.cfg | 4 +- test/fixtures/project_b/setup.py | 7 +- test/fixtures/project_c/pyproject.toml | 2 +- test/fixtures/project_d/pyproject.toml | 2 +- test/test_cli.py | 65 ++-- test/test_config.py | 130 ++++--- test/test_rewrite.py | 3 +- test/util.py | 1 + 17 files changed, 400 insertions(+), 306 deletions(-) rename test/fixtures/project_a/{pycalver.toml => calver.toml} (79%) diff --git a/README.md b/README.md index 170efa2..6806f98 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ -# [PyCalVer: Automatic Calendar Versioning][url_repo] +# [Python CalVer: Automatic Calendar Versioning][url_repo] -PyCalVer is a CLI-tool to search and replace all version strings in your project files ([calver][url_calver_org], [semver][url_semver_org] or otherwise). PyCalVer has support for +Python CalVer provides the CLI command `calver`. You can use it to search and update version strings in your project files. It has a flexible pattern syntax to support many version string schemes ([calver][url_calver_org], [semver][url_semver_org] or otherwise). PyCalVer features: - Configurable version patterns - Git, Mercurial or no VCS @@ -22,7 +22,7 @@ Project/Repo: [![MIT License][img_license]][url_license] [![Supported Python Versions][img_pyversions]][url_pyversions] -[![PyCalVer v202010.1041-beta][img_version]][url_version] +[![CalVer v2020.1041-beta][img_version]][url_version] [![PyPI Releases][img_pypi]][url_pypi] [![PyPI Downloads][img_downloads]][url_downloads] @@ -62,7 +62,7 @@ Code Quality/CI: [img_downloads]: https://pepy.tech/badge/pycalver/month [url_downloads]: https://pepy.tech/project/pycalver -[img_version]: https://img.shields.io/static/v1.svg?label=PyCalVer&message=v202010.1041-beta&color=blue +[img_version]: https://img.shields.io/static/v1.svg?label=CalVer&message=v2020.1041-beta&color=blue [url_version]: https://pypi.org/project/pycalver/ [img_pypi]: https://img.shields.io/badge/PyPI-wheels-green.svg @@ -72,8 +72,6 @@ Code Quality/CI: [url_pyversions]: https://pypi.python.org/pypi/pycalver - - [](TOC) - [PyCalVer: Automatic Calendar Versioning](#pycalver-automatic-calendar-versioning) @@ -109,7 +107,7 @@ Code Quality/CI: ### Search and Replace -With PyCalVer, you only configure a single `version_pattern` which is then used +With `calver`, you configure a single `version_pattern` which is then used to 1. Search for version strings in your project files 2. Replace these occurrences with an updated/bumped version number. @@ -117,11 +115,11 @@ With PyCalVer, you only configure a single `version_pattern` which is then used Your configuration might look something like this: ``` -[pycalver] -current_version = "2020.9" -version_pattern = "YYYY.MM" +[calver] +current_version = "2020.9.0" +version_pattern = "YYYY.MM.PATCH" -[pycalver:file_patterns] +[calver:file_patterns] src/mymodule/__init__.py __version__ = "{version}" src/mymodule/__main__.py @@ -130,22 +128,22 @@ setup.py version="{version}", ``` -> Throughout the examples, we use the `--date` argument. Without this argument PyCalVer will just use the current date. We use it here so that you can easily reproduce the examples. +> Throughout the examples, we use the `--date` argument. Without this argument `calver` will just use the current UTC date. We use it here so that you can easily reproduce the examples. -Using this configuration, the output of `pycalver bump --dry` might look something like this: +Using this configuration, the output of `calver bump --dry` might look something like this: ```diff -$ pycalver bump --date 2020-10-01 --dry +$ calver bump --date 2020-10-21 --dry INFO - fetching tags from remote (to turn off use: -n / --no-fetch) -INFO - Old Version: 2020.9 -INFO - New Version: 2020.10 +INFO - Old Version: 2020.9.0 +INFO - New Version: 2020.10.0 --- setup.py +++ setup.py @@ -63,7 +63,7 @@ setuptools.setup( name="mymodule", -- version="2020.9", -+ version="2020.10", +- version="2020.9.0", ++ version="2020.10.0", description=description, long_description=long_description, @@ -153,8 +151,8 @@ INFO - New Version: 2020.10 +++ src/mymodule/__init__.py @@ -3,3 +3,3 @@ --__version__ = "2020.9" -+__version__ = "2020.10" +-__version__ = "2020.9.0" ++__version__ = "2020.10.0" --- src/mymodule/__main__.py @@ -162,8 +160,8 @@ INFO - New Version: 2020.10 @@ -101,7 +101,7 @@ @click.group() --@click.version_option(version="2020.9") -+@click.version_option(version="2020.10") +-@click.version_option(version="2020.9.0") ++@click.version_option(version="2020.10.0") @click.help_option() @click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") ``` @@ -182,22 +180,22 @@ If PyCalVer does not serve your purposes, you may wish to look at the [bump2vers ### Testing a version pattern -You can validate a pattern and how it is incremented using `pycalver test`. +You can validate a pattern and how it is incremented using `calver test`. ```shell -$ pycalver test --date 2018-09-22 '2018.37' 'YYYY.WW' -New Version: 2018.38 -PEP440 : 2018.38 +$ calver test --date 2020-09-22 '2020.37' 'YYYY.WW' +New Version: 2020.38 +PEP440 : 2020.38 -$ pycalver test --date 2018-09-22 '2018.37' 'YYYY.MM' # expected to fail -ERROR - Incomplete match '2018.3' for version string '2018.37' with pattern 'YYYY.MM'/'(?P[1-9][0-9]{3})\.(?P1[0-2]|[1-9])' -ERROR - Version did not change: '2018.37'. Invalid version and/or pattern 'YYYY.MM'. +$ calver test --date 2020-09-22 '2020.37' 'YYYY.MM' # expected to fail because 37 is not valid for part MM +ERROR - Incomplete match '2020.3' for version string '2020.37' with pattern 'YYYY.MM'/'(?P[1-9][0-9]{3})\.(?P1[0-2]|[1-9])' +ERROR - Invalid version '2020.37' and/or pattern 'YYYY.MM'. ``` This illustrates that each pattern is internally translated to a regular expression which must match your version string. The `--verbose` flag shows a slightly more readable form. ```shell -$ pycalver test --date 2018-09-22 'v2018.37' 'YYYY.WW' --verbose +$ calver test --date 2018-09-22 'v2018.37' 'YYYY.WW' --verbose INFO - Using pattern YYYY.WW INFO - regex = re.compile(r""" (?P[1-9][0-9]{3}) @@ -215,30 +213,30 @@ In other words, you don't specify regular expressions manually, they are generat You can do tradition SemVer without any kind of calendar component if you like. ```shell -$ pycalver test '1.2.3' 'MAJOR.MINOR.PATCH' --patch +$ calver test '1.2.3' 'MAJOR.MINOR.PATCH' --patch New Version: 1.2.4 PEP440 : 1.2.4 -$ pycalver test '1.2.3' 'MAJOR.MINOR.PATCH' --minor +$ calver test '1.2.3' 'MAJOR.MINOR.PATCH' --minor New Version: 1.3.0 PEP440 : 1.3.0 -$ pycalver test '1.2.3' 'MAJOR.MINOR.PATCH' --major +$ calver test '1.2.3' 'MAJOR.MINOR.PATCH' --major New Version: 2.0.0 PEP440 : 2.0.0 ``` -These are the same CLI flags as are accepted by the `pycalver bump` command. +These are the same CLI flags as are accepted by the `calver bump` command. In the context of a CalVer version, a typical use would be to include a `PATCH` part in your version pattern, so that you can create multiple releases in the same month. ```shell -$ pycalver test --date 2018-09-22 '2018.9.0' 'YYYY.MM.PATCH' +$ calver test --date 2018-09-22 '2018.9.0' 'YYYY.MM.PATCH' ERROR - Invalid arguments or pattern, version did not change. ERROR - Version did not change: '2018.9.0'. Invalid version and/or pattern 'YYYY.MM.PATCH'. -INFO - Perhaps try: pycalver test --patch +INFO - Perhaps try: calver test --patch -$ pycalver test --date 2018-09-22 '2018.9.0' 'YYYY.MM.PATCH' --patch +$ calver test --date 2018-09-22 '2018.9.0' 'YYYY.MM.PATCH' --patch New Version: 2018.9.1 PEP440 : 2018.9.1 ``` @@ -246,7 +244,7 @@ PEP440 : 2018.9.1 The `PATCH` part will roll over back to zero when leading parts change (in this case the year and month). ```shell -$ pycalver test --date 2018-10-22 '2018.9.1' 'YYYY.MM.PATCH' +$ calver test --date 2018-10-22 '2018.9.1' 'YYYY.MM.PATCH' New Version: 2018.10.0 PEP440 : 2018.10.0 ``` @@ -254,54 +252,55 @@ PEP440 : 2018.10.0 This will happen even if you use the `--patch` argument, so that your first release of the month has a `PATCH` of 0 instead of 1. ```shell -$ pycalver test --date 2018-10-22 '2018.9.1' 'YYYY.MM.PATCH' --patch +$ calver test --date 2018-10-22 '2018.9.1' 'YYYY.MM.PATCH' --patch New Version: 2018.10.0 PEP440 : 2018.10.0 ``` -### Auto Incrementing Parts: `BUILD`/`INC0`/`INC1` +### Auto Increment Parts: `BUILD`/`INC0`/`INC1` -The following parts are incremented automatically, and do not use/require a CLI flag: `BUILD`/`INC0`/`INC1`. This means you can just do `pycalver bump` without any further CLI flags and special cases, which can simplify your build scripts. +The following parts are incremented automatically, and do not use/require a CLI flag: `BUILD`/`INC0`/`INC1`. This means you can just do `calver bump` without any further CLI flags and special cases, which can simplify your build scripts. ```shell -$ pycalver test --date 2018-09-22 '2018.9.1' 'YYYY.MM.INC0' +$ calver test --date 2018-09-22 '2018.9.1' 'YYYY.MM.INC0' New Version: 2018.9.2 PEP440 : 2018.9.2 -$ pycalver test --date 2018-10-22 '2018.9.2' 'YYYY.MM.INC0' +$ calver test --date 2018-10-22 '2018.9.2' 'YYYY.MM.INC0' New Version: 2018.10.0 PEP440 : 2018.10.0 -$ pycalver test --date 2018-10-22 '2018.9.2' 'YYYY.MM.INC1' +$ calver test --date 2018-10-22 '2018.9.2' 'YYYY.MM.INC1' New Version: 2018.10.1 PEP440 : 2018.10.1 ``` -If it is rare for you to make multiple releases within a given period, you can make such a part optional using the `[PART]` syntax with square braces: +If it is rare for you to make multiple releases within a given period, you can make such a part optional using the `[PART]` syntax with square brackets: ```shell -$ pycalver test --date 2018-09-22 '2018.9' 'YYYY.MM[.INC0]' +$ calver test --date 2018-09-22 '2018.9' 'YYYY.MM[.INC0]' New Version: 2018.9.1 PEP440 : 2018.9.1 -$ pycalver test --date 2018-10-22 '2018.9.1' 'YYYY.MM[.INC0]' +$ calver test --date 2018-10-22 '2018.9.1' 'YYYY.MM[.INC0]' New Version: 2018.10 PEP440 : 2018.10 ``` -If the extra `INC0` part is needed, it is added. If the date rolls over and it's no longer needed, it is omitted. Any literal text enclosed in the braces (such as a separator) will also be added or omitted as needed. +If the extra `INC0` part is needed, it is added. If the date rolls over and it's no longer needed, it is omitted. Any literal text enclosed in the brackets (such as a separator) will also be added or omitted as needed. + ### Persistent Parts: `BUILD`/`RELEASE`/`PYTAG` The `BUILD` and `RELEASE` parts are not reset. Instead they are carried forward. ```shell -$ pycalver test --date 2018-09-22 '201809.1051-beta' 'YYYY0M.BUILD[-RELEASE]' +$ calver test --date 2018-09-22 '201809.1051-beta' 'YYYY0M.BUILD[-RELEASE]' New Version: 201809.1052-beta PEP440 : 201809.1052b0 -$ pycalver test --date 2018-09-22 '201809.1051-beta' 'YYYY0M.BUILD[-RELEASE]' --release rc +$ calver test --date 2018-09-22 '201809.1051-beta' 'YYYY0M.BUILD[-RELEASE]' --release rc New Version: 201809.1052-rc PEP440 : 201809.1052rc0 ``` @@ -309,17 +308,24 @@ PEP440 : 201809.1052rc0 To remove a release tag, mark it as final with `--release final`. ```shell -$ pycalver test --date 2018-09-22 '201809.1051-beta' 'YYYY0M.BUILD[-RELEASE]' --release final +$ calver test --date 2018-09-22 '201809.1051-beta' 'YYYY0M.BUILD[-RELEASE]' --release final New Version: 201809.1052 PEP440 : 201809.1052 ``` + ### Searching for Patterns with `grep` -Using `pycalver grep`, you can search for occurrences of a version pattern in your project files. +When searching for a pattern, There are some limitations to keep in mind: + + 1. A version string cannot span multiple lines. + 2. There is no mechanism for escaping parts. + 3. Brackets `[]` can be escaped with backslash. + +Using `calver grep`, you can search for occurrences of a version pattern in your project files. ```shell -$ pycalver grep '__version__ = "YYYY.MM[-RELEASENUM]"' src/module/__init__.py +$ calver grep '__version__ = "YYYY.MM[-RELEASENUM]"' src/module/__init__.py src/module/__init__.py 3: 4: __version__ = "2020.9-beta1" @@ -336,7 +342,7 @@ When you write your configuration, you can avoid repeating your version pattern Applied to the above example, you can instead use this: ```shell -$ pycalver grep --version-pattern "YYYY.MM[-RELEASENUM]" '__version__ = "{version}"' src/module/__init__.py +$ calver grep --version-pattern "YYYY.MM[-RELEASENUM]" '__version__ = "{version}"' src/module/__init__.py src/module/__init__.py 3: 4: __version__ = "2020.9-beta1" @@ -346,12 +352,12 @@ src/module/__init__.py The corresponding configuration would look like this. ```ini -[pycalver] +[calver] current_version = "2020.9-beta1" version_pattern = "YYYY.MM[-RELEASENUM]" ... -[pycalver:file_patterns] +[calver:file_patterns] src/module/__init__.py __version__ = "{version}" ... @@ -360,7 +366,7 @@ src/module/__init__.py If your pattern produces non PEP440 version numbers, you may wish to use the placeholder `{pep440_version}` in your search pattern and specify your `--version-pattern` separately. ```shell -$ pycalver grep --version-pattern "YYYY.MM[-RELEASENUM]" 'version="{pep440_version}"' setup.py +$ calver grep --version-pattern "YYYY.MM[-RELEASENUM]" 'version="{pep440_version}"' setup.py setup.py 65: url="https://github.com/org/project", 66: version="2020.9b1", @@ -374,7 +380,7 @@ The placeholder `{version}` matches `2020.9-beta1`, while the placeholder `{pep4 As a further illustration of how the search and replace works, you might want use a file pattern entry to keep the year of your copyright header up to date. ``` -$ python -m pycalver grep 'Copyright (c) 2018-YYYY' src/mymodule/*.py | head +$ calver grep 'Copyright (c) 2018-YYYY' src/mymodule/*.py | head src/mymodule/__init__.py 3: 4: # Copyright (c) 2018-2020 Vandelay Industries - All rights reserved. @@ -389,7 +395,7 @@ src/mymodule/config.py The corresponding configuration for this pattern would look like this. ```ini -[pycalver:file_patterns] +[calver:file_patterns] ... src/mymodule/*.py Copyright (c) 2018-YYYY Vandelay Industries - All rights reserved. @@ -400,11 +406,11 @@ src/mymodule/*.py ### Command Line - + ``` -$ pycalver --help -Usage: pycalver [OPTIONS] COMMAND [ARGS]... +$ calver --help +Usage: calver [OPTIONS] COMMAND [ARGS]... Automatically update PyCalVer version strings in all project files. @@ -416,18 +422,18 @@ Options: Commands: bump Increment the current version string and update project files. grep Search file(s) for a version pattern. - init Initialize [pycalver] configuration. + init Initialize [calver] configuration. show Show current version of your project. test Increment a version number for demo purposes. ``` - + - + ``` -$ pycalver bump --help -Usage: pycalver bump [OPTIONS] +$ calver bump --help +Usage: calver bump [OPTIONS] Increment the current version string and update project files. @@ -455,7 +461,7 @@ Options: --help Show this message and exit. ``` - + ### Part Overview @@ -464,21 +470,21 @@ Options: [url_calver_org_scheme]: https://calver.org/#scheme -| part | range / example(s) | comment | -|-----------|---------------------------|--------------------------------------------| -| `YYYY` | 2019, 2020... | Full year, based on `strftime('%Y')` | -| `YY` | 18, 19..99, 0, 1 | Short year, based on `int(strftime('%y'))` | -| `MM` | 9, 10, 11, 12 | Month, based on `int(strftime('%m'))` | -| `DD` | 1, 2, 3..31 | Day, based on `int(strftime('%d'))` | -| `MAJOR` | 0..9, 10..99, 100.. | `pycalver bump --major` | -| `MINOR` | 0..9, 10..99, 100.. | `pycalver bump --minor` | -| `PATCH` | 0..9, 10..99, 100.. | `pycalver bump --patch` | -| `RELEASE` | alpha, beta, rc, post | `--release=` | -| `PYTAG` | a, b, rc, post | `--release=` | -| `NUM` | 0, 1, 2... | `-r/--release-num` | -| `BUILD` | 1001, 1002 .. 1999, 22000 | build number (maintains lexical order) | -| `INC0` | 0, 1, 2... | 0-based auto incrementing number | -| `INC1` | 1, 2... | 1-based auto incrementing number | +| part | range / example(s) | comment | +|---------|---------------------------|--------------------------------------------| +| `YYYY` | 2019, 2020... | Full year, based on `strftime('%Y')` | +| `YY` | 18, 19..99, 0, 1 | Short year, based on `int(strftime('%y'))` | +| `MM` | 9, 10, 11, 12 | Month, based on `int(strftime('%m'))` | +| `DD` | 1, 2, 3..31 | Day, based on `int(strftime('%d'))` | +| `MAJOR` | 0..9, 10..99, 100.. | `calver bump --major` | +| `MINOR` | 0..9, 10..99, 100.. | `calver bump --minor` | +| `PATCH` | 0..9, 10..99, 100.. | `calver bump --patch` | +| `TAG` | alpha, beta, rc, post | `--tag=` | +| `PYTAG` | a, b, rc, post | `--tag=` | +| `NUM` | 0, 1, 2... | `-r/--release-num` | +| `BUILD` | 1001, 1002 .. 1999, 22000 | build number (maintains lexical order) | +| `INC0` | 0, 1, 2... | 0-based auto incrementing number | +| `INC1` | 1, 2... | 1-based auto incrementing number | The following are also available, but you should review the [Normalization Caveats](#normalization-caveats) before you decide to use them. @@ -565,7 +571,7 @@ If you wish to avoid this, you should use a pattern which maintains lexicographi - ¹ If `PATCH > 9` -- ² For `2100` YY produces `00`... +- ² For the year 2100, the part `YY` will produce 0 ### Week Numbering @@ -603,20 +609,22 @@ number would run backwards if it was created around New Year. ## Configuration -The fastest way to setup the configuration for project is to use `pycalver init`. +### Configuration Setup + +The fastest way to setup the configuration for project is to use `calver init`. ```shell -$ pip install pycalver +$ pip install python-calver ... -Installing collected packages: click pathlib2 typing toml pycalver -Successfully installed pycalver-202010.1041b0 +Installing collected packages: click lexid pathlib2 typing toml python-calver +Successfully installed python-calver-2020.1041b0 $ cd myproject ~/myproject/ -$ pycalver init --dry -Exiting because of '-d/--dry'. Would have written to pycalver.toml: +$ calver init --dry +Exiting because of '-d/--dry'. Would have written to calver.toml: - [pycalver] + [calver] current_version = "v202010.1001-alpha" version_pattern = "vYYYY0M.BUILD[-RELEASE]" commit_message = "bump version to {new_version}" @@ -624,41 +632,39 @@ Exiting because of '-d/--dry'. Would have written to pycalver.toml: tag = true push = true - [pycalver.file_patterns] + [calver.file_patterns] "README.md" = [ "{version}", "{pep440_version}", ] - "pycalver.toml" = [ + "calver.toml" = [ 'current_version = "{version}"', ] ``` -If you already have configuration file in your project (such as a `setup.cfg` file), then `pycalver init` will update that file instead. +If you already have configuration file in your project (such as a `setup.cfg` file), then `calver init` will update that file instead. ``` ~/myproject -$ pycalver init +$ calver init Updated setup.cfg ``` Your `setup.cfg` may now look something like this: ```ini -# setup.cfg -[pycalver] -current_version = "v201902.1001-alpha" -version_pattern = "vYYYY0M.BUILD[-RELEASE]" +[calver] +current_version = "2019.1001-alpha" +version_pattern = "YYYY.BUILD[-RELEASE]" commit_message = "bump version to {new_version}" commit = True tag = True push = True -[pycalver:file_patterns] +[calver:file_patterns] setup.cfg = current_version = "{version}" setup.py = - "{version}", "{pep440_version}", README.md = {version} @@ -749,28 +755,28 @@ INFO - New Version: v201902.1002-beta @@ -11,7 +11,7 @@ [![Supported Python Versions][pyversions_img]][pyversions_ref] --[![Version v201901.1001-beta][version_img]][version_ref] -+[![Version v201902.1002-beta][version_img]][version_ref] +-[![Version 2019.1001-beta][version_img]][version_ref] ++[![Version 2019.1002-beta][version_img]][version_ref] [![PyPI Releases][pypi_img]][pypi_ref] --- src/mymodule_v1/__init__.py +++ src/mymodule_v1/__init__.py @@ -1,1 +1,1 @@ --__version__ = "v201901.1001-beta" -+__version__ = "v201902.1002-beta" +-__version__ = "2019.1001-beta" ++__version__ = "2019.1002-beta" --- src/mymodule_v2/__init__.py +++ src/mymodule_v2/__init__.py @@ -1,1 +1,1 @@ --__version__ = "v201901.1001-beta" -+__version__ = "v201902.1002-beta" +-__version__ = "2019.1001-beta" ++__version__ = "2019.1002-beta" --- setup.py +++ setup.py @@ -44,7 +44,7 @@ name="myproject", -- version="201901.1001b0", -+ version="201902.1002b0", +- version="2019.1001b0", ++ version="2019.1002b0", license="MIT", ``` @@ -1022,26 +1028,25 @@ The PyCalVer format for version strings has three parts: ``` - o Year and Month of Release - | o Sequential Build Number - | | o Release Tag (optional) - | | | - ---+--- --+-- --+-- - v202010 .1001 -beta - + o Year of Release + | o Sequential Build Number + | | o Release Tag (optional) + | | | + --+-- --+-- --+-- + v2020 .1001 -beta ``` Some examples: ``` -v201711.0001-alpha -v201712.0027-beta -v201801.0031 -v201801.0032-post +2017.1001-alpha +2017.1027-beta +2018.1031 +2018.1032-post ... -v202207.18133 -v202207.18134 +2022.28133 +2022.28134 ``` This format was chosen in part to be distinctive from @@ -1071,14 +1076,13 @@ These version strings can be parsed with the following regular expression: ```python import re -# https://regex101.com/r/fnj60p/10 +# https://regex101.com/r/fnj60p/14 PYCALVER_PATTERN = r""" \b (?P - (?P + (?P v # "v" version prefix (?P\d{4}) - (?P\d{2}) ) (?P \. # "." build nr prefix @@ -1092,30 +1096,28 @@ PYCALVER_PATTERN = r""" """ PYCALVER_REGEX = re.compile(PYCALVER_PATTERN, flags=re.VERBOSE) -version_str = "v201712.0001-alpha" +version_str = "v2017.1001-alpha" version_match = PYCALVER_REGEX.match(version_str) assert version_match.groupdict() == { - "pycalver" : "v201712.0001-alpha", - "vYYYY0M" : "v201712", + "pycalver" : "v2017.1001-alpha", + "vYYYY0M" : "v2017", "year" : "2017", - "month" : "12", - "build" : ".0001", - "build_no" : "0001", + "build" : ".1001", + "build_no" : "1001", "release" : "-alpha", "release_tag": "alpha", } -version_str = "v201712.0033" +version_str = "v201712.1033" version_match = PYCALVER_REGEX.match(version_str) assert version_match.groupdict() == { - "pycalver" : "v201712.0033", - "vYYYY0M" : "v201712", + "pycalver" : "v2017.1033", + "vYYYY" : "v2017", "year" : "2017", - "month" : "12", - "build" : ".0033", - "build_no" : "0033", + "build" : ".1033", + "build_no" : "1033", "release" : None, "release_tag": None, } @@ -1124,12 +1126,12 @@ assert version_match.groupdict() == { ### Incrementing Behaviour To see how version strings are incremented, we can use -`pycalver test`: +`calver test`: ```shell -$ pycalver test v201801.1033-beta -New Version: v201902.1034-beta -PEP440 : 201902.1034b0 +$ calver test v2018.1033-beta +New Version: v2019.1034-beta +PEP440 : 2019.1034b0 ``` This is the simple case: @@ -1141,13 +1143,13 @@ This is the simple case: You can explicitly update the release tag by using the `--release=` argument: ```shell -$ pycalver test v201801.1033-alpha --release=beta -New Version: v201902.1034-beta -PEP440 : 201902.1034b0 +$ calver test v2018.1033-alpha --release=beta +New Version: v2019.1034-beta +PEP440 : 2019.1034b0 -$ pycalver test v201902.1034-beta --release=final -New Version: v201902.1035 -PEP440 : 201902.1035 +$ calver test v2019.1034-beta --release=final +New Version: v2019.1035 +PEP440 : 2019.1035 ``` To maintain lexical ordering of version numbers, the version number is padded with extra zeros using [Lexical Ids][url_pypi_lexid]. @@ -1326,20 +1328,20 @@ package which included the bug, they only have to do `pip install --upgrade Perhaps a timeline will illustrate more clearly: ``` -v202008.1665 # last stable release -v202008.1666-beta # pre release for testers -v201901.1667 # final release after testing +v2020.1665 # last stable release +v2020.1666-beta # pre release for testers +v2019.1667 # final release after testing -# bug is discovered which effects v202008.1666-beta and v201901.1667 +# bug is discovered which effects v2020.1666-beta and v2019.1667 -v201901.1668-beta # fix is issued for testers -v201901.1669 # fix is issued everybody +v2019.1668-beta # fix is issued for testers +v2019.1669 # fix is issued everybody # Alternatively, revert before fixing -v201901.1668 # same as v202008.1665 -v201901.1669-beta # reintroduce change from v202008.1666-beta + fix -v201901.1670 # final release after testing +v2019.1668 # same as v2020.1665 +v2019.1669-beta # reintroduce change from v2020.1666-beta + fix +v2019.1670 # final release after testing ``` In the absolute worst case, a change is discovered to break backward @@ -1362,18 +1364,18 @@ package will perhaps have 99% overlap to the previous one and the old one may eventually be abandoned. ``` -mypkg v202008.1665 # last stable release -mypkg v202008.1666-rc # pre release for testers -mypkg v201901.1667 # final release after testing period +mypkg v2020.1665 # last stable release +mypkg v2020.1666-rc # pre release for testers +mypkg v2019.1667 # final release after testing period -# bug is discovered in v202008.1666-beta and v201901.1667 +# bug is discovered in v2020.1666-beta and v2019.1667 -mypkg v201901.1668 # same as v202008.1665 +mypkg v2019.1668 # same as v2020.1665 # new package is created with compatibility breaking code -mypkg2 v201901.1669 # same as v201901.1667 -mypkg v201901.1669 # updated readme, declaring support +mypkg2 v2019.1669 # same as v2019.1667 +mypkg v2019.1669 # updated readme, declaring support # level for mypkg, pointing to mypgk2 # and documenting how to upgrade. ``` diff --git a/scripts/update_readme_examples.py b/scripts/update_readme_examples.py index 9dc1a31..2c60bdf 100644 --- a/scripts/update_readme_examples.py +++ b/scripts/update_readme_examples.py @@ -184,8 +184,8 @@ def pattern_examples(): old_content = io.open("README.md").read() new_content = old_content -new_content = update_md_code_output(new_content, "pycalver --help") -new_content = update_md_code_output(new_content, "pycalver bump --help") +new_content = update_md_code_output(new_content, "calver --help") +new_content = update_md_code_output(new_content, "calver bump --help") new_content = update(new_content, "pattern_examples", pattern_examples()) new_content = update(new_content, "weeknum_example" , weeknum_example()) @@ -199,8 +199,8 @@ else: fobj.write(new_content) -# @printf '\n```\n$$ pycalver --help\n' > /tmp/pycalver_help.txt -# @$(DEV_ENV)/bin/pycalver --help >> /tmp/pycalver_help.txt +# @printf '\n```\n$$ calver --help\n' > /tmp/pycalver_help.txt +# @$(DEV_ENV)/bin/calver --help >> /tmp/pycalver_help.txt # @printf '```\n\n' >> /tmp/pycalver_help.txt # sed -i -ne '// {p; r /tmp/pycalver_help.txt' \ diff --git a/setup.cfg b/setup.cfg index 7de1c91..e63ac04 100644 --- a/setup.cfg +++ b/setup.cfg @@ -88,7 +88,7 @@ exclude = addopts = --doctest-modules -[pycalver] +[calver] current_version = "v2020.1041-beta" version_pattern = "vYYYY.BUILD[-TAG]" commit_message = "bump {old_version} -> {new_version}" @@ -96,7 +96,7 @@ commit = True tag = True push = True -[pycalver:file_patterns] +[calver:file_patterns] bootstrapit.sh = PACKAGE_VERSION="{version}" setup.cfg = @@ -114,9 +114,9 @@ LICENSE = license.header = Copyright (c) 2018-YYYY README.md = - \[PyCalVer {version}\] - img.shields.io/static/v1.svg?label=PyCalVer&message={version}&color=blue - Successfully installed pycalver-{pep440_version} + \[CalVer {version}\] + img.shields.io/static/v1.svg?label=CalVer&message={version}&color=blue + Successfully installed python-calver-{pep440_version} [tool:pylint] diff --git a/setup.py b/setup.py index f0e5a54..f73329d 100644 --- a/setup.py +++ b/setup.py @@ -58,7 +58,7 @@ if any(arg.startswith("bdist") for arg in sys.argv): setuptools.setup( - name="pycalver2", + name="python-calver", license="MIT", author="Manuel Barkhau", author_email="mbarkhau@gmail.com", diff --git a/src/pycalver2/cli.py b/src/pycalver2/cli.py index 6fc4c26..e5b3ea2 100755 --- a/src/pycalver2/cli.py +++ b/src/pycalver2/cli.py @@ -539,7 +539,7 @@ def _try_bump( '-d', "--dry", default=False, is_flag=True, help="Display diff of changes, don't rewrite files." ) def init(verbose: int = 0, dry: bool = False) -> None: - """Initialize [pycalver] configuration.""" + """Initialize [calver] configuration.""" _configure_logging(verbose=max(_VERBOSE, verbose)) ctx, cfg = config.init(project_path=".", cfg_missing_ok=True) diff --git a/src/pycalver2/config.py b/src/pycalver2/config.py index 16dbb77..e4f8587 100644 --- a/src/pycalver2/config.py +++ b/src/pycalver2/config.py @@ -32,7 +32,7 @@ PatternsByFile = typ.Dict[str, typ.List[Pattern]] FilePatternsItem = typ.Tuple[str, typ.List[Pattern]] -SUPPORTED_CONFIGS = ["setup.cfg", "pyproject.toml", "pycalver.toml"] +SUPPORTED_CONFIGS = ["setup.cfg", "pyproject.toml", "pycalver.toml", "calver.toml"] DEFAULT_COMMIT_MESSAGE = "bump version to {new_version}" @@ -47,6 +47,33 @@ class ProjectContext(typ.NamedTuple): vcs_type : typ.Optional[str] +def _parse_config_and_format(path: pl.Path) -> typ.Tuple[pl.Path, str, str]: + if (path / "pycalver.toml").exists(): + config_filepath = path / "pycalver.toml" + config_format = 'toml' + elif (path / "calver.toml").exists(): + config_filepath = path / "calver.toml" + config_format = 'toml' + elif (path / "pyproject.toml").exists(): + config_filepath = path / "pyproject.toml" + config_format = 'toml' + elif (path / "setup.cfg").exists(): + config_filepath = path / "setup.cfg" + config_format = 'cfg' + else: + # fallback to creating a new calver.toml + config_filepath = path / "calver.toml" + config_format = 'toml' + + if config_filepath.is_absolute(): + config_rel_path = str(config_filepath.relative_to(path.absolute())) + else: + config_rel_path = str(config_filepath) + config_filepath = pl.Path.cwd() / config_filepath + + return (config_filepath, config_rel_path, config_format) + + def init_project_ctx(project_path: typ.Union[str, pl.Path, None] = ".") -> ProjectContext: """Initialize ProjectContext from a path.""" if isinstance(project_path, pl.Path): @@ -57,25 +84,7 @@ def init_project_ctx(project_path: typ.Union[str, pl.Path, None] = ".") -> Proje # assume it's a str/unicode path = pl.Path(project_path) - if (path / "pycalver.toml").exists(): - config_filepath = path / "pycalver.toml" - config_format = 'toml' - elif (path / "pyproject.toml").exists(): - config_filepath = path / "pyproject.toml" - config_format = 'toml' - elif (path / "setup.cfg").exists(): - config_filepath = path / "setup.cfg" - config_format = 'cfg' - else: - # fallback to creating a new pycalver.toml - config_filepath = path / "pycalver.toml" - config_format = 'toml' - - if config_filepath.is_absolute(): - config_rel_path = str(config_filepath.relative_to(path.absolute())) - else: - config_rel_path = str(config_filepath) - config_filepath = pl.Path.cwd() / config_filepath + config_filepath, config_rel_path, config_format = _parse_config_and_format(path) vcs_type: typ.Optional[str] @@ -137,10 +146,14 @@ def _debug_str(cfg: Config) -> str: def _parse_cfg_file_patterns( cfg_parser: configparser.RawConfigParser, ) -> typ.Iterable[FileRawPatternsItem]: - if not cfg_parser.has_section("pycalver:file_patterns"): - return + file_pattern_items: typ.List[typ.Tuple[str, str]] - file_pattern_items: typ.List[typ.Tuple[str, str]] = cfg_parser.items("pycalver:file_patterns") + if cfg_parser.has_section("pycalver:file_patterns"): + file_pattern_items = cfg_parser.items("pycalver:file_patterns") + elif cfg_parser.has_section("calver:file_patterns"): + file_pattern_items = cfg_parser.items("calver:file_patterns") + else: + return for filepath, patterns_str in file_pattern_items: maybe_patterns = (line.strip() for line in patterns_str.splitlines()) @@ -175,10 +188,13 @@ def _parse_cfg(cfg_buffer: typ.IO[str]) -> RawConfig: else: cfg_parser.readfp(cfg_buffer) # python2 compat - if not cfg_parser.has_section("pycalver"): - raise ValueError("Missing [pycalver] section.") - - raw_cfg: RawConfig = dict(cfg_parser.items("pycalver")) + raw_cfg: RawConfig + if cfg_parser.has_section("pycalver"): + raw_cfg = dict(cfg_parser.items("pycalver")) + elif cfg_parser.has_section("calver"): + raw_cfg = dict(cfg_parser.items("calver")) + else: + raise ValueError("Missing [calver] section.") for option, default_val in BOOL_OPTIONS.items(): val: OptionVal = raw_cfg.get(option, default_val) @@ -194,8 +210,15 @@ def _parse_cfg(cfg_buffer: typ.IO[str]) -> RawConfig: def _parse_toml(cfg_buffer: typ.IO[str]) -> RawConfig: - raw_full_cfg: typ.Any = toml.load(cfg_buffer) - raw_cfg : RawConfig = raw_full_cfg.get('pycalver', {}) + raw_full_cfg: typ.Any = toml.load(cfg_buffer) + raw_cfg : RawConfig + + if 'pycalver' in raw_full_cfg: + raw_cfg = raw_full_cfg['pycalver'] + elif 'calver' in raw_full_cfg: + raw_cfg = raw_full_cfg['calver'] + else: + raw_cfg = {} for option, default_val in BOOL_OPTIONS.items(): raw_cfg[option] = raw_cfg.get(option, default_val) @@ -284,7 +307,7 @@ def _validate_version_with_pattern( if invalid_chars: errmsg = ( f"Invalid character(s) '{invalid_chars.group(1)}'" - f' in pycalver.version_pattern = "{version_pattern}"' + f' in version_pattern = "{version_pattern}"' ) raise ValueError(errmsg) if not v2version.is_valid_week_pattern(version_pattern): @@ -321,10 +344,10 @@ def _parse_config(raw_cfg: RawConfig) -> Config: push = raw_cfg['push'] = False if tag and not commit: - raise ValueError("pycalver.commit = true required if pycalver.tag = true") + raise ValueError("commit=True required if tag=True") if push and not commit: - raise ValueError("pycalver.commit = true required if pycalver.push = true") + raise ValueError("commit=True required if push=True") cfg = Config( current_version=current_version, @@ -351,26 +374,26 @@ def _parse_current_version_default_pattern(raw_cfg: RawConfig, raw_cfg_text: str if line.strip() == "[pycalver]": is_pycalver_section = True + elif line.strip() == "[calver]": + is_pycalver_section = True elif line and line[0] == "[" and line[-1] == "]": is_pycalver_section = False - raise ValueError("Could not parse pycalver.current_version") + raise ValueError("Could not parse 'current_version'") def _set_raw_config_defaults(raw_cfg: RawConfig) -> None: - if 'current_version' in raw_cfg: - if not isinstance(raw_cfg['current_version'], str): - err = f"Invalid type for pycalver.current_version = {raw_cfg['current_version']}" - raise TypeError(err) - else: - raise ValueError("Missing 'pycalver.current_version'") + if 'version_pattern' not in raw_cfg: + raise TypeError("Missing version_pattern") + elif not isinstance(raw_cfg['version_pattern'], str): + err = f"Invalid type for version_pattern = {raw_cfg['version_pattern']}" + raise TypeError(err) - if 'version_pattern' in raw_cfg: - if not isinstance(raw_cfg['version_pattern'], str): - err = f"Invalid type for pycalver.version_pattern = {raw_cfg['version_pattern']}" - raise TypeError(err) - else: - raw_cfg['version_pattern'] = "{pycalver}" + if 'current_version' not in raw_cfg: + raise ValueError("Missing 'current_version' configuration") + elif not isinstance(raw_cfg['current_version'], str): + err = f"Invalid type for current_version = {raw_cfg['current_version']}" + raise TypeError(err) if 'file_patterns' not in raw_cfg: raw_cfg['file_patterns'] = {} @@ -426,7 +449,7 @@ def init( DEFAULT_CONFIGPARSER_BASE_TMPL = """ -[pycalver] +[calver] current_version = "{initial_version}" version_pattern = "vYYYY.BUILD[-TAG]" commit_message = "bump version {{old_version}} -> {{new_version}}" @@ -434,7 +457,7 @@ commit = True tag = True push = True -[pycalver:file_patterns] +[calver:file_patterns] """.lstrip() @@ -466,7 +489,7 @@ README.md = DEFAULT_TOML_BASE_TMPL = """ -[pycalver] +[calver] current_version = "{initial_version}" version_pattern = "vYYYY.BUILD[-TAG]" commit_message = "bump version {{old_version}} -> {{new_version}}" @@ -474,7 +497,7 @@ commit = true tag = true push = true -[pycalver.file_patterns] +[calver.file_patterns] """.lstrip() @@ -485,6 +508,13 @@ DEFAULT_TOML_PYCALVER_STR = """ """.lstrip() +DEFAULT_TOML_CALVER_STR = """ +"calver.toml" = [ + 'current_version = "{version}"', +] +""".lstrip() + + DEFAULT_TOML_PYPROJECT_STR = """ "pyproject.toml" = [ 'current_version = "{version}"', @@ -542,6 +572,7 @@ def default_config(ctx: ProjectContext) -> str: default_pattern_strs_by_filename = { "pyproject.toml": DEFAULT_TOML_PYPROJECT_STR, "pycalver.toml" : DEFAULT_TOML_PYCALVER_STR, + "calver.toml" : DEFAULT_TOML_CALVER_STR, "setup.py" : DEFAULT_TOML_SETUP_PY_STR, "README.rst" : DEFAULT_TOML_README_RST_STR, "README.md" : DEFAULT_TOML_README_MD_STR, @@ -561,7 +592,7 @@ def default_config(ctx: ProjectContext) -> str: if ctx.config_format == 'cfg': cfg_str += DEFAULT_CONFIGPARSER_SETUP_CFG_STR if ctx.config_format == 'toml': - cfg_str += DEFAULT_TOML_PYCALVER_STR + cfg_str += DEFAULT_TOML_CALVER_STR cfg_str += "\n" diff --git a/src/pycalver2/vcs.py b/src/pycalver2/vcs.py index ab25193..29a9100 100644 --- a/src/pycalver2/vcs.py +++ b/src/pycalver2/vcs.py @@ -4,9 +4,10 @@ # Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License # SPDX-License-Identifier: MIT # -# pycalver/vcs.py (this file) is based on code from the +# pycalver2/vcs.py (this file) is based on code from the # bumpversion project: https://github.com/peritus/bumpversion # Copyright (c) 2013-2014 Filip Noetzel - MIT License + """Minimal Git and Mercirial API. If terminology for similar concepts differs between git and diff --git a/test/fixtures/project_a/README.md b/test/fixtures/project_a/README.md index ae141f2..34780a9 100644 --- a/test/fixtures/project_a/README.md +++ b/test/fixtures/project_a/README.md @@ -1,3 +1,3 @@ -# PyCalVer README Fixture +# Python CalVer README Fixture Current Version: v2016.0123-alpha diff --git a/test/fixtures/project_a/pycalver.toml b/test/fixtures/project_a/calver.toml similarity index 79% rename from test/fixtures/project_a/pycalver.toml rename to test/fixtures/project_a/calver.toml index 418cbb5..3b30ca6 100644 --- a/test/fixtures/project_a/pycalver.toml +++ b/test/fixtures/project_a/calver.toml @@ -1,12 +1,12 @@ -[pycalver] +[calver] current_version = "v2017.0123-alpha" version_pattern = "vYYYY.BUILD[-TAG]" commit = true tag = true push = true -[pycalver.file_patterns] -"pycalver.toml" = [ +[calver.file_patterns] +"calver.toml" = [ 'current_version = "{version}"', ] diff --git a/test/fixtures/project_b/setup.cfg b/test/fixtures/project_b/setup.cfg index 36ed0b4..08158aa 100644 --- a/test/fixtures/project_b/setup.cfg +++ b/test/fixtures/project_b/setup.cfg @@ -1,11 +1,11 @@ -[pycalver] +[calver] current_version = v201307.0456-beta version_pattern = {pycalver} commit = True tag = True push = True -[pycalver:file_patterns] +[calver:file_patterns] setup.cfg = current_version = {version} setup.py = diff --git a/test/fixtures/project_b/setup.py b/test/fixtures/project_b/setup.py index d4082bb..942a85b 100644 --- a/test/fixtures/project_b/setup.py +++ b/test/fixtures/project_b/setup.py @@ -1,3 +1,8 @@ import setuptools -setuptools.setup(name="mylib", license="MIT", version="201307.456b0", keywords="awesome library") +setuptools.setup( + name="mylib", + license="MIT", + version="201307.456b0", + keywords="awesome library", +) diff --git a/test/fixtures/project_c/pyproject.toml b/test/fixtures/project_c/pyproject.toml index e460fb7..aae94e8 100644 --- a/test/fixtures/project_c/pyproject.toml +++ b/test/fixtures/project_c/pyproject.toml @@ -1,4 +1,4 @@ -[pycalver] +[calver] current_version = "v2017q1.54321" version_pattern = "v{year}q{quarter}.{build_no}" commit = true diff --git a/test/fixtures/project_d/pyproject.toml b/test/fixtures/project_d/pyproject.toml index 65faaf4..c59dcaf 100644 --- a/test/fixtures/project_d/pyproject.toml +++ b/test/fixtures/project_d/pyproject.toml @@ -1,4 +1,4 @@ -[pycalver] +[calver] current_version = "v2017q1.54321" version_pattern = "vYYYYqQ.BUILD" commit = true diff --git a/test/test_cli.py b/test/test_cli.py index 4d968fc..ed244c5 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -42,7 +42,7 @@ license_file = LICENSE universal = 1 """ -PYCALVER_TOML_FIXTURE = """ +CALVER_TOML_FIXTURE = """ """ PYPROJECT_TOML_FIXTURE = """ @@ -51,11 +51,11 @@ requires = ["setuptools", "wheel"] """ ENV = { - 'GIT_AUTHOR_NAME' : "pycalver_tester", - 'GIT_COMMITTER_NAME' : "pycalver_tester", - 'GIT_AUTHOR_EMAIL' : "pycalver_tester@nowhere.com", - 'GIT_COMMITTER_EMAIL': "pycalver_tester@nowhere.com", - 'HGUSER' : "pycalver_tester", + 'GIT_AUTHOR_NAME' : "calver_tester", + 'GIT_COMMITTER_NAME' : "calver_tester", + 'GIT_AUTHOR_EMAIL' : "calver_tester@nowhere.com", + 'GIT_COMMITTER_EMAIL': "calver_tester@nowhere.com", + 'HGUSER' : "calver_tester", 'PATH' : os.environ['PATH'], } @@ -64,11 +64,11 @@ def shell(*cmd): return sp.check_output(cmd, env=ENV) -DEBUG_LOG = 0 +ECHO_CAPLOG = os.getenv('ECHO_CAPLOG') == "1" def _debug_records(caplog): - if DEBUG_LOG: + if ECHO_CAPLOG: print() for record in caplog.records: print(record) @@ -233,7 +233,11 @@ def _add_project_files(*files): if "pycalver.toml" in files: with pl.Path("pycalver.toml").open(mode="wt", encoding="utf-8") as fobj: - fobj.write(PYCALVER_TOML_FIXTURE) + fobj.write(CALVER_TOML_FIXTURE) + + if "calver.toml" in files: + with pl.Path("calver.toml").open(mode="wt", encoding="utf-8") as fobj: + fobj.write(CALVER_TOML_FIXTURE) if "pyproject.toml" in files: with pl.Path("pyproject.toml").open(mode="wt", encoding="utf-8") as fobj: @@ -269,14 +273,14 @@ def test_novcs_nocfg_init(runner, caplog): # dry mode test result = runner.invoke(cli.cli, ['init', "-vv", "--dry"]) assert result.exit_code == 0 - assert not os.path.exists("pycalver.toml") + assert not os.path.exists("calver.toml") # non dry mode result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 - assert os.path.exists("pycalver.toml") - with pl.Path("pycalver.toml").open(mode="r", encoding="utf-8") as fobj: + assert os.path.exists("calver.toml") + with pl.Path("calver.toml").open(mode="r", encoding="utf-8") as fobj: cfg_content = fobj.read() base_str = config.DEFAULT_TOML_BASE_TMPL.format(initial_version=config._initial_version()) @@ -318,9 +322,10 @@ def test_novcs_setupcfg_init(runner): assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output -def test_novcs_pyproject_init(runner): +def test_novcs_pyproject_init(runner, caplog): _add_project_files("README.md", "pyproject.toml") result = runner.invoke(cli.cli, ['init', "-vv"]) + _debug_records(caplog) assert result.exit_code == 0 with pl.Path("pyproject.toml").open(mode="r", encoding="utf-8") as fobj: @@ -368,7 +373,7 @@ def test_git_init(runner, version_pattern, cur_version, cur_pep440): assert result.exit_code == 0 _update_config_val( - "pycalver.toml", + "calver.toml", version_pattern=version_pattern, current_version='"' + cur_version + '"', ) @@ -387,7 +392,7 @@ def test_hg_init(runner, version_pattern, cur_version, cur_pep440): assert result.exit_code == 0 _update_config_val( - "pycalver.toml", + "calver.toml", version_pattern=version_pattern, current_version='"' + cur_version + '"', ) @@ -408,7 +413,7 @@ def test_v1_git_tag_eval(runner, version_pattern, cur_version, cur_pep440): assert result.exit_code == 0 _update_config_val( - "pycalver.toml", + "calver.toml", version_pattern=version_pattern, current_version='"' + cur_version + '"', ) @@ -434,7 +439,7 @@ def test_hg_tag_eval(runner, version_pattern, cur_version, cur_pep440): assert result.exit_code == 0 _update_config_val( - "pycalver.toml", + "calver.toml", version_pattern=version_pattern, current_version='"' + cur_version + '"', ) @@ -458,7 +463,7 @@ def test_novcs_bump(runner, version_pattern, cur_version, cur_pep440): assert result.exit_code == 0 _update_config_val( - "pycalver.toml", + "calver.toml", version_pattern=version_pattern, current_version='"' + cur_version + '"', ) @@ -494,12 +499,12 @@ def test_git_bump(runner, caplog, version_pattern, cur_version, cur_pep440): assert result.exit_code == 0 _update_config_val( - "pycalver.toml", + "calver.toml", version_pattern=version_pattern, current_version='"' + cur_version + '"', ) - shell("git", "add", "pycalver.toml") + shell("git", "add", "calver.toml") shell("git", "commit", "-m", "initial commit") result = runner.invoke(cli.cli, ['bump', "-vv"]) @@ -522,12 +527,12 @@ def test_hg_bump(runner, version_pattern, cur_version, cur_pep440): assert result.exit_code == 0 _update_config_val( - "pycalver.toml", + "calver.toml", version_pattern=version_pattern, current_version='"' + cur_version + '"', ) - shell("hg", "add", "pycalver.toml") + shell("hg", "add", "calver.toml") shell("hg", "commit", "-m", "initial commit") result = runner.invoke(cli.cli, ['bump', "-vv"]) @@ -551,9 +556,9 @@ def test_empty_git_bump(runner, caplog): with pl.Path("setup.cfg").open(mode="r") as fobj: default_cfg_data = fobj.read() - assert "[pycalver]\n" in default_cfg_data + assert "[calver]\n" in default_cfg_data assert "\ncurrent_version = " in default_cfg_data - assert "\n[pycalver:file_patterns]\n" in default_cfg_data + assert "\n[calver:file_patterns]\n" in default_cfg_data assert "\nsetup.cfg =\n" in default_cfg_data result = runner.invoke(cli.cli, ['bump']) @@ -573,9 +578,9 @@ def test_empty_hg_bump(runner, caplog): with pl.Path("setup.cfg").open(mode="r") as fobj: default_cfg_text = fobj.read() - assert "[pycalver]\n" in default_cfg_text + assert "[calver]\n" in default_cfg_text assert "\ncurrent_version = " in default_cfg_text - assert "\n[pycalver:file_patterns]\n" in default_cfg_text + assert "\n[calver:file_patterns]\n" in default_cfg_text assert "\nsetup.cfg =\n" in default_cfg_text result = runner.invoke(cli.cli, ['bump']) @@ -899,11 +904,11 @@ def test_get_latest_vcs_version_tag(runner): result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 - _update_config_val("pycalver.toml", push="false") - _update_config_val("pycalver.toml", current_version='"0.1.8"') - _update_config_val("pycalver.toml", version_pattern='"MAJOR.MINOR.PATCH"') + _update_config_val("calver.toml", push="false") + _update_config_val("calver.toml", current_version='"0.1.8"') + _update_config_val("calver.toml", version_pattern='"MAJOR.MINOR.PATCH"') - _vcs_init("git", files=["pycalver.toml"]) + _vcs_init("git", files=["calver.toml"]) result = runner.invoke(cli.cli, ['bump', "--patch"]) assert result.exit_code == 0 diff --git a/test/test_config.py b/test/test_config.py index 33c79fe..c071944 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -15,7 +15,8 @@ from pycalver2 import config PYCALVER_TOML_FIXTURE_1 = """ [pycalver] -current_version = "v201808.0123-alpha" +current_version = "v2020.1003-alpha" +version_pattern = "vYYYY.BUILD[-TAG]" commit = true tag = true push = true @@ -35,6 +36,9 @@ PYCALVER_TOML_FIXTURE_2 = """ [pycalver] current_version = "1.2.3" version_pattern = "{semver}" +commit = false +tag = false +push = false [pycalver.file_patterns] "README.md" = [ @@ -46,15 +50,34 @@ version_pattern = "{semver}" ] """ +CALVER_TOML_FIXTURE_3 = """ +[calver] +current_version = "v201808.0123-alpha" +version_pattern = "vYYYY0M.BUILD[-TAG]" +commit = true +tag = true +push = true + +[calver.file_patterns] +"README.md" = [ + "{version}", + "{pep440_version}", +] +"calver.toml" = [ + 'current_version = "{version}"', +] +""" + SETUP_CFG_FIXTURE = """ -[pycalver] +[calver] current_version = "v201808.0456-beta" +version_pattern = "vYYYY0M.BUILD[-TAG]" commit = True tag = True push = True -[pycalver:file_patterns] +[calver:file_patterns] setup.py = {version} {pep440_version} @@ -64,7 +87,7 @@ setup.cfg = NEW_PATTERN_CFG_FIXTURE = """ -[pycalver] +[calver] current_version = "v201808.1456-beta" version_pattern = "vYYYY0M.BUILD[-TAG]" commit_message = "bump version to {new_version}" @@ -72,7 +95,7 @@ commit = True tag = True push = True -[pycalver:file_patterns] +[calver:file_patterns] setup.py = {version} {pep440_version} @@ -103,17 +126,18 @@ def test_parse_toml_1(): raw_cfg = config._parse_toml(buf) cfg = config._parse_config(raw_cfg) - assert cfg.current_version == "v201808.0123-alpha" - assert cfg.version_pattern == "{pycalver}" + assert cfg.current_version == "v2020.1003-alpha" + assert cfg.version_pattern == "vYYYY.BUILD[-TAG]" assert cfg.commit is True assert cfg.tag is True assert cfg.push is True - assert "pycalver.toml" in cfg.file_patterns + files = set(cfg.file_patterns) + assert "pycalver.toml" in files - raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) - assert raw_patterns_by_filepath["README.md" ] == ["{pycalver}", "{pep440_pycalver}"] - assert raw_patterns_by_filepath["pycalver.toml"] == ['current_version = "{pycalver}"'] + raw_patterns_by_path = _parse_raw_patterns_by_filepath(cfg) + assert raw_patterns_by_path["README.md" ] == ["vYYYY.BUILD[-TAG]", "YYYY.BLD[PYTAGNUM]"] + assert raw_patterns_by_path["pycalver.toml"] == ['current_version = "vYYYY.BUILD[-TAG]"'] def test_parse_toml_2(): @@ -130,9 +154,29 @@ def test_parse_toml_2(): assert "pycalver.toml" in cfg.file_patterns - raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) - assert raw_patterns_by_filepath["README.md" ] == ["{semver}", "{semver}"] - assert raw_patterns_by_filepath["pycalver.toml"] == ['current_version = "{semver}"'] + raw_patterns_by_path = _parse_raw_patterns_by_filepath(cfg) + assert raw_patterns_by_path["README.md" ] == ["{semver}", "{semver}"] + assert raw_patterns_by_path["pycalver.toml"] == ['current_version = "{semver}"'] + + +def test_parse_toml_3(): + buf = mk_buf(CALVER_TOML_FIXTURE_3) + + raw_cfg = config._parse_toml(buf) + cfg = config._parse_config(raw_cfg) + + assert cfg.current_version == "v201808.0123-alpha" + assert cfg.version_pattern == "vYYYY0M.BUILD[-TAG]" + assert cfg.commit is True + assert cfg.tag is True + assert cfg.push is True + + files = set(cfg.file_patterns) + assert "calver.toml" in files + + raw_patterns_by_path = _parse_raw_patterns_by_filepath(cfg) + assert raw_patterns_by_path["README.md" ] == ["vYYYY0M.BUILD[-TAG]", "YYYY0M.BLD[PYTAGNUM]"] + assert raw_patterns_by_path["calver.toml"] == ['current_version = "vYYYY0M.BUILD[-TAG]"'] def test_parse_v1_cfg(): @@ -146,11 +190,12 @@ def test_parse_v1_cfg(): assert cfg.tag is True assert cfg.push is True - assert "setup.cfg" in cfg.file_patterns + files = set(cfg.file_patterns) + assert "setup.cfg" in files - raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) - assert raw_patterns_by_filepath["setup.py" ] == ["{pycalver}", "{pep440_pycalver}"] - assert raw_patterns_by_filepath["setup.cfg"] == ['current_version = "{pycalver}"'] + raw_patterns_by_path = _parse_raw_patterns_by_filepath(cfg) + assert raw_patterns_by_path["setup.py" ] == ["vYYYY0M.BUILD[-TAG]", "YYYY0M.BLD[PYTAGNUM]"] + assert raw_patterns_by_path["setup.cfg"] == ['current_version = "vYYYY0M.BUILD[-TAG]"'] def test_parse_v2_cfg(): @@ -164,16 +209,14 @@ def test_parse_v2_cfg(): assert cfg.tag is True assert cfg.push is True - assert "setup.py" in cfg.file_patterns - assert "setup.cfg" in cfg.file_patterns + files = set(cfg.file_patterns) + assert "setup.py" in files + assert "setup.cfg" in files - raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) - assert raw_patterns_by_filepath["setup.py"] == [ - "vYYYY0M.BUILD[-TAG]", - "YYYY0M.BLD[PYTAGNUM]", - ] - assert raw_patterns_by_filepath["setup.cfg"] == ['current_version = "vYYYY0M.BUILD[-TAG]"'] - assert raw_patterns_by_filepath["src/project/*.py"] == ["Copyright (c) 2018-YYYY"] + raw_patterns_by_path = _parse_raw_patterns_by_filepath(cfg) + assert raw_patterns_by_path["setup.py"] == ["vYYYY0M.BUILD[-TAG]", "YYYY0M.BLD[PYTAGNUM]"] + assert raw_patterns_by_path["setup.cfg"] == ['current_version = "vYYYY0M.BUILD[-TAG]"'] + assert raw_patterns_by_path["src/project/*.py"] == ["Copyright (c) 2018-YYYY"] def test_parse_default_toml(): @@ -204,8 +247,8 @@ def test_parse_default_cfg(): def test_parse_project_toml(): project_path = util.FIXTURES_DIR / "project_a" - config_path = util.FIXTURES_DIR / "project_a" / "pycalver.toml" - config_rel_path = "pycalver.toml" + config_path = util.FIXTURES_DIR / "project_a" / "calver.toml" + config_rel_path = "calver.toml" with config_path.open() as fobj: config_data = fobj.read() @@ -224,7 +267,8 @@ def test_parse_project_toml(): assert cfg.tag is True assert cfg.push is True - assert set(cfg.file_patterns.keys()) == {"pycalver.toml", "README.md"} + files = set(cfg.file_patterns.keys()) + assert files == {"calver.toml", "README.md"} def test_parse_project_cfg(): @@ -258,25 +302,26 @@ def test_parse_project_cfg(): def test_parse_toml_file(tmpdir): project_path = tmpdir.mkdir("minimal") - setup_cfg = project_path.join("pycalver.toml") - setup_cfg.write(PYCALVER_TOML_FIXTURE_1) - setup_cfg_rel_path = "pycalver.toml" + cfg_file = project_path.join("pycalver.toml") + cfg_file.write(PYCALVER_TOML_FIXTURE_1) + cfg_file_rel_path = "pycalver.toml" ctx = config.init_project_ctx(project_path) - assert ctx == config.ProjectContext(project_path, setup_cfg, setup_cfg_rel_path, 'toml', None) + assert ctx == config.ProjectContext(project_path, cfg_file, cfg_file_rel_path, 'toml', None) cfg = config.parse(ctx) assert cfg - assert cfg.current_version == "v201808.0123-alpha" + assert cfg.current_version == "v2020.1003-alpha" + assert cfg.version_pattern == "vYYYY.BUILD[-TAG]" assert cfg.tag is True assert cfg.commit is True assert cfg.push is True raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) assert raw_patterns_by_filepath == { - "README.md" : ["{pycalver}", "{pep440_pycalver}"], - "pycalver.toml": ['current_version = "{pycalver}"'], + "README.md" : ["vYYYY.BUILD[-TAG]", "YYYY.BLD[PYTAGNUM]"], + "pycalver.toml": ['current_version = "vYYYY.BUILD[-TAG]"'], } @@ -294,6 +339,8 @@ def test_parse_default_pattern(): assert cfg assert cfg.current_version == "v2017q1.54321" + # assert cfg.version_pattern == "vYYYYqQ.BUILD" + assert cfg.version_pattern == "v{year}q{quarter}.{build_no}" assert cfg.commit is True assert cfg.tag is True assert cfg.push is True @@ -317,14 +364,15 @@ def test_parse_cfg_file(tmpdir): assert cfg assert cfg.current_version == "v201808.0456-beta" + assert cfg.version_pattern == "vYYYY0M.BUILD[-TAG]" assert cfg.tag is True assert cfg.commit is True assert cfg.push is True raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) assert raw_patterns_by_filepath == { - "setup.py" : ["{pycalver}", "{pep440_pycalver}"], - "setup.cfg": ['current_version = "{pycalver}"'], + "setup.py" : ["vYYYY0M.BUILD[-TAG]", "YYYY0M.BLD[PYTAGNUM]"], + "setup.cfg": ['current_version = "vYYYY0M.BUILD[-TAG]"'], } @@ -358,7 +406,7 @@ def test_parse_missing_version(tmpdir): setup_path.write( "\n".join( ( - "[pycalver]", + "[calver]", # f"current_version = v201808.1001-dev", "commit = False", ) @@ -374,7 +422,7 @@ def test_parse_missing_version(tmpdir): def test_parse_invalid_version(tmpdir): setup_path = tmpdir.mkdir("fail").join("setup.cfg") - setup_path.write("\n".join(("[pycalver]", "current_version = 0.1.0", "commit = False"))) + setup_path.write("\n".join(("[calver]", "current_version = 0.1.0", "commit = False"))) ctx = config.init_project_ctx(setup_path) assert ctx diff --git a/test/test_rewrite.py b/test/test_rewrite.py index cec7bc7..9794ea0 100644 --- a/test/test_rewrite.py +++ b/test/test_rewrite.py @@ -95,13 +95,14 @@ def test_v1_rewrite_final(): def test_iter_file_paths(): with util.Project(project="a") as project: ctx = config.init_project_ctx(project.dir) + assert ctx cfg = config.parse(ctx) assert cfg _paths_and_patterns = rewrite.iter_path_patterns_items(cfg.file_patterns) file_paths = {str(file_path) for file_path, patterns in _paths_and_patterns} - assert file_paths == {"pycalver.toml", "README.md"} + assert file_paths == {"calver.toml", "README.md"} def test_iter_file_globs(): diff --git a/test/util.py b/test/util.py index d9d4f68..86d7a7a 100644 --- a/test/util.py +++ b/test/util.py @@ -29,6 +29,7 @@ FIXTURE_PATH_PARTS = [ ["setup.cfg"], ["setup.py"], ["pycalver.toml"], + ["calver.toml"], ["src", "module_v1", "__init__.py"], ["src", "module_v2", "__init__.py"], ] From 1c21e22720a3df8156529efc0669617437758a90 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 15 Oct 2020 22:26:30 +0000 Subject: [PATCH 93/98] bugfixes for semver --- src/pycalver2/cli.py | 4 ++-- src/pycalver2/v2version.py | 26 ++++++++++++++++++++++++++ test/test_cli.py | 33 +++++++++++++++++++++++++-------- 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/src/pycalver2/cli.py b/src/pycalver2/cli.py index e5b3ea2..d1d6f1c 100755 --- a/src/pycalver2/cli.py +++ b/src/pycalver2/cli.py @@ -175,7 +175,7 @@ def cli(verbose: int = 0) -> None: @cli.command() @click.argument("old_version") -@click.argument("pattern", default="vYYYY.BUILD[-TAG]") +@click.argument("pattern") @click.option('-v' , '--verbose', count=True, help="Control log level. -vv for debug level.") @click.option("--major", is_flag=True, default=False, help="Increment major component.") @click.option("-m" , "--minor", is_flag=True, default=False, help="Increment minor component.") @@ -204,7 +204,7 @@ def cli(verbose: int = 0) -> None: ) def test( old_version: str, - pattern : str = "vYYYY.BUILD[-TAG]", + pattern : str, verbose : int = 0, tag : str = None, major : bool = False, diff --git a/src/pycalver2/v2version.py b/src/pycalver2/v2version.py index 1164d47..5601ea2 100644 --- a/src/pycalver2/v2version.py +++ b/src/pycalver2/v2version.py @@ -617,6 +617,24 @@ def _incr_numeric( tag : typ.Optional[str], tag_num : bool, ) -> version.V2VersionInfo: + """Increment (and reset to zero) non CalVer parts. + + >>> raw_pattern = 'MAJOR.MINOR.PATCH[PYTAGNUM]' + >>> old_vinfo = parse_field_values_to_vinfo({'major': "1", 'minor': "2", 'patch': "3"}) + >>> cur_vinfo = old_vinfo + >>> new_vinfo = _incr_numeric( + ... raw_pattern, + ... cur_vinfo, + ... old_vinfo, + ... major=False, + ... minor=False, + ... patch=True, + ... tag='beta', + ... tag_num=False, + ... ) + >>> (new_vinfo.major, new_vinfo.minor, new_vinfo.patch, new_vinfo.tag, new_vinfo.pytag, new_vinfo.num) + (1, 2, 4, 'beta', 'b', 0) + """ # Reset major/minor/patch/num/inc to zero if any part to the left of it is incremented fields = _parse_pattern_fields(raw_pattern) reset_fields = dict(_iter_reset_field_items(fields, old_vinfo, cur_vinfo)) @@ -653,6 +671,14 @@ def _incr_numeric( if tag != cur_vinfo.tag: cur_vinfo = cur_vinfo._replace(num=0) cur_vinfo = cur_vinfo._replace(tag=tag) + + if cur_vinfo.tag and not cur_vinfo.pytag: + pytag = version.PEP440_TAG_BY_TAG[cur_vinfo.tag] + cur_vinfo = cur_vinfo._replace(pytag=pytag) + elif cur_vinfo.pytag and not cur_vinfo.tag: + tag = version.TAG_BY_PEP440_TAG[cur_vinfo.pytag] + cur_vinfo = cur_vinfo._replace(tag=tag) + return cur_vinfo diff --git a/test/test_cli.py b/test/test_cli.py index ed244c5..43b1ec2 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -139,7 +139,8 @@ def test_incr_default(runner): def test_incr_pin_date(runner): old_version = "v2017.1999-alpha" - result = runner.invoke(cli.cli, ['test', "-vv", "--pin-date", old_version]) + pattern = "vYYYY.BUILD[-TAG]" + result = runner.invoke(cli.cli, ['test', "-vv", "--pin-date", old_version, pattern]) assert result.exit_code == 0 assert "Version: v2017.22000-alpha\n" in result.output @@ -175,7 +176,8 @@ def test_incr_semver(runner): def test_incr_semver_invalid(runner, caplog): - result = runner.invoke(cli.cli, ['test', "-vv", "--patch", "0.1.1"]) + pattern = "vYYYY.BUILD[-TAG]" + result = runner.invoke(cli.cli, ['test', "-vv", "0.1.1", pattern, "--patch"]) assert result.exit_code == 1 assert len(caplog.records) > 0 log_record = caplog.records[0] @@ -184,41 +186,56 @@ def test_incr_semver_invalid(runner, caplog): def test_incr_to_beta(runner): + pattern = "vYYYY.BUILD[-TAG]" old_version = "v2017.1999-alpha" initial_version = config._initial_version() - result = runner.invoke(cli.cli, ['test', old_version, "-vv", "--tag", "beta"]) + result = runner.invoke(cli.cli, ['test', "-vv", old_version, pattern, "--tag", "beta"]) assert result.exit_code == 0 new_version = initial_version.replace(".1001-alpha", ".22000-beta") assert f"Version: {new_version}\n" in result.output def test_incr_to_final(runner, caplog): + pattern = "vYYYY.BUILD[-TAG]" old_version = "v2017.1999-alpha" initial_version = config._initial_version() - result = runner.invoke(cli.cli, ['test', old_version, "-vv", "--tag", "final"]) + result = runner.invoke(cli.cli, ['test', "-vv", old_version, pattern, "--tag", "final"]) _debug_records(caplog) assert result.exit_code == 0 new_version = initial_version.replace(".1001-alpha", ".22000") assert f"Version: {new_version}\n" in result.output -def test_incr_release_num(runner): - semver = "MAJOR.MINOR.PATCH[PYTAGNUM]" +SEMVER = "MAJOR.MINOR.PATCH[PYTAGNUM]" + +def test_incr_tag(runner): + old_version = "0.1.0" + new_version = "0.1.1b0" + + result = runner.invoke( + cli.cli, ['test', "-vv", old_version, SEMVER, "--patch", "--tag", "beta"] + ) + assert result.exit_code == 0 + assert f"Version: {new_version}\n" in result.output + + +def test_incr_tag_num(runner): old_version = "0.1.0b0" new_version = "0.1.0b1" - result = runner.invoke(cli.cli, ['test', "-vv", "--tag-num", old_version, semver]) + result = runner.invoke(cli.cli, ['test', "-vv", old_version, SEMVER, "--tag-num"]) assert result.exit_code == 0 assert f"Version: {new_version}\n" in result.output def test_incr_invalid(runner): + pattern = "vYYYY.BUILD[-TAG]" old_version = "v2017.1999-alpha" - result = runner.invoke(cli.cli, ['test', old_version, "-vv", "--tag", "alfa"]) + result = runner.invoke(cli.cli, ['test', "-vv", old_version, pattern, "--tag", "alfa"]) assert result.exit_code == 1 From 2c01699b99db5471ed1aab7a697831d39b112f1d Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 15 Oct 2020 22:32:56 +0000 Subject: [PATCH 94/98] linter pacification --- setup.cfg | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index e63ac04..93087c6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,7 +27,7 @@ length_sort = True [flake8] max-line-length = 100 -max-complexity = 10 +max-complexity = 12 ignore = # Missing trailing comma (handled by sjfmt) C812 @@ -136,6 +136,9 @@ max-locals = 20 # Maximum number of arguments for function / method max-args = 12 +# Maximum number of branch for function / method body +max-branches = 14 + good-names = logger,i,ex # These are packages that are implemented as c extensions and From bbf5bfa31c99bc7f751e30ed68b23f07d9110764 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 18 Oct 2020 20:47:35 +0000 Subject: [PATCH 95/98] pycalver -> bumpver --- CHANGELOG.md | 28 +- Makefile | 2 +- README.md | 1468 ++++++----------- bootstrapit.sh | 10 +- scripts/update_readme_examples.py | 29 +- setup.cfg | 20 +- setup.py | 10 +- src/{pycalver2 => bumpver}/__init__.py | 4 +- src/{pycalver2 => bumpver}/__main__.py | 4 +- src/{pycalver2 => bumpver}/cli.py | 35 +- src/{pycalver2 => bumpver}/config.py | 81 +- src/{pycalver2 => bumpver}/parse.py | 0 src/{pycalver2 => bumpver}/patterns.py | 0 src/{pycalver2 => bumpver}/pysix.py | 0 src/{pycalver2 => bumpver}/regexfmt.py | 2 +- src/{pycalver2 => bumpver}/rewrite.py | 0 src/{pycalver2 => bumpver}/utils.py | 0 src/{pycalver2 => bumpver}/v1patterns.py | 2 +- src/{pycalver2 => bumpver}/v1rewrite.py | 2 +- src/{pycalver2 => bumpver}/v1version.py | 2 +- src/{pycalver2 => bumpver}/v2patterns.py | 4 +- src/{pycalver2 => bumpver}/v2rewrite.py | 2 +- src/{pycalver2 => bumpver}/v2version.py | 2 +- src/{pycalver2 => bumpver}/vcs.py | 4 +- src/{pycalver2 => bumpver}/version.py | 0 .../project_a/{calver.toml => bumpver.toml} | 6 +- test/fixtures/project_b/setup.cfg | 4 +- test/fixtures/project_c/pyproject.toml | 2 +- test/test_cli.py | 134 +- test/test_config.py | 32 +- test/test_parse.py | 4 +- test/test_patterns.py | 4 +- test/test_rewrite.py | 88 +- test/test_version.py | 10 +- test/util.py | 4 +- 35 files changed, 764 insertions(+), 1235 deletions(-) rename src/{pycalver2 => bumpver}/__init__.py (71%) rename src/{pycalver2 => bumpver}/__main__.py (77%) rename src/{pycalver2 => bumpver}/cli.py (96%) rename src/{pycalver2 => bumpver}/config.py (90%) rename src/{pycalver2 => bumpver}/parse.py (100%) rename src/{pycalver2 => bumpver}/patterns.py (100%) rename src/{pycalver2 => bumpver}/pysix.py (100%) rename src/{pycalver2 => bumpver}/regexfmt.py (97%) rename src/{pycalver2 => bumpver}/rewrite.py (100%) rename src/{pycalver2 => bumpver}/utils.py (100%) rename src/{pycalver2 => bumpver}/v1patterns.py (99%) rename src/{pycalver2 => bumpver}/v1rewrite.py (99%) rename src/{pycalver2 => bumpver}/v1version.py (99%) rename src/{pycalver2 => bumpver}/v2patterns.py (98%) rename src/{pycalver2 => bumpver}/v2rewrite.py (99%) rename src/{pycalver2 => bumpver}/v2version.py (99%) rename src/{pycalver2 => bumpver}/vcs.py (98%) rename src/{pycalver2 => bumpver}/version.py (100%) rename test/fixtures/project_a/{calver.toml => bumpver.toml} (80%) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbfe0d8..8c5fb41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ # Changelog for https://github.com/mbarkhau/pycalver -## NEXT +## BumpVer 2020.1042-beta + +Rename package and module from PyCalVer to BumpVer. This name change is due to confusion that this project is either Python specific, or only suitible for CalVer versioning schemes, neither of which is the case. This release includes a new syntax for patterns. @@ -34,7 +36,7 @@ The previous syntax will continue to be supported, but all documentation has bee - New [gitlab#9][gitlab_i9]: Make commit message configurable. - Fix [gitlab#12][gitlab_i12]: Error with sorting non-lexical version tags (e.g. SemVer). - Fix [gitlab#11][gitlab_i11]: Show regexp when `--verbose` is used. -- Fix [gitlab#8][gitlab_i8]: `pycalver push ` will now also push HEAD (previously only the tag itself was pushed). +- Fix [gitlab#8][gitlab_i8]: `bumpver update` will now also push HEAD (previously only the tag itself was pushed). - Fix: Disallow `--release=dev`. The semantics of a `dev` releases are different than for other release tags and further development would be required to support them correctly. - Fix: Entries in `file_patterns` were ignored if there were multiple entries for the same file. @@ -51,12 +53,12 @@ Many thanks to contributors of this release: @LucidOne, @khanguslee, @chaudum [gitlab_i8]: https://gitlab.com/mbarkhau/pycalver/-/issues/8 -## v201907.0036 +## PyCalVer v201907.0036 - Fix: Don't use git/hg command if `commit=False` is configured (thanks @valentin87) -## v201907.0035 +## PyCalVer v201907.0035 - Fix [gitlab#6][gitlab_i6]: Add parts `{month_short}`, `{dom_short}`, `{doy_short}`. - Fix [gitlab#5][gitlab_i5]: Better warning when using bump with SemVer (one of --major/--minor/--patch is required) @@ -67,43 +69,43 @@ Many thanks to contributors of this release: @LucidOne, @khanguslee, @chaudum [gitlab_i4]: https://gitlab.com/mbarkhau/pycalver/-/issues/4 -## v201903.0030 +## PyCalVer v201903.0030 - Fix: Use pattern from config instead of hard-coded {pycalver} pattern. - Fix: Better error messages for git/hg issues. - Add: Implicit default pattern for config file. -## v201903.0028 +## PyCalVer v201903.0028 - Fix: Add warnings when configured files are not under version control. - Add: Colored output for bump --dry -## v201902.0027 +## PyCalVer v201902.0027 - Fix: Allow --release=post - Fix: Better error reporting for bad patterns - Fix: Regex escaping issue with "?" -## v201902.0024 +## PyCalVer v201902.0024 - Added: Support for globs in file patterns. - Fixed: Better error reporting for invalid config. -## v201902.0020 +## PyCalVer v201902.0020 - Added: Support for many more custom version patterns. -## v201812.0018 +## PyCalVer v201812.0018 - Fixed: Better handling of pattern replacements with "-final" releases. -## v201812.0017 +## PyCalVer v201812.0017 - Fixed [github#2]. `pycalver init` was broken. - Fixed pattern escaping issues. @@ -113,7 +115,7 @@ Many thanks to contributors of this release: @LucidOne, @khanguslee, @chaudum [gihlab_i2]: https://github.com/mbarkhau/pycalver/-/issues/2 -## v201812.0011-beta +## PyCalVer v201812.0011-beta - Add version tags using git/hg. - Use git/hg tags as SSOT for most recent version. @@ -121,6 +123,6 @@ Many thanks to contributors of this release: @LucidOne, @khanguslee, @chaudum - Move to https://gitlab.com/mbarkhau/pycalver -## v201809.0001-alpha +## PyCalVer v201809.0001-alpha - Initial release diff --git a/Makefile b/Makefile index 9139654..1d477a6 100644 --- a/Makefile +++ b/Makefile @@ -74,6 +74,6 @@ pycalver_deps.svg: ## Update cli reference in README.md -README.md: src/pycalver/__main__.py scripts/update_readme_examples.py Makefile +README.md: src/pycalver2/cli.py scripts/update_readme_examples.py Makefile @git add README.md @$(DEV_ENV)/bin/python scripts/update_readme_examples.py diff --git a/README.md b/README.md index 6806f98..aa729cb 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,28 @@

- logo + logo

-# [Python CalVer: Automatic Calendar Versioning][url_repo] +# [BumpVer: Automatic Versioning][url_repo] -Python CalVer provides the CLI command `calver`. You can use it to search and update version strings in your project files. It has a flexible pattern syntax to support many version string schemes ([calver][url_calver_org], [semver][url_semver_org] or otherwise). PyCalVer features: +With the CLI command `bumpver`, you can search for and update version strings in your project files. It has a flexible pattern syntax to support many version schemes ([SemVer][url_semver_org], [CalVer][url_calver_org] or otherwise). BumpVer features: - Configurable version patterns -- Git, Mercurial or no VCS -- Operates only on plaintext files, so it can be used for any project, not just python projects. +- Optional Git or Mercurial integration +- Works with plaintext, so you can use it with any project. -[url_repo]: https://gitlab.com/mbarkhau/pycalver -[url_calver_org]: https://calver.org/ +[url_repo]: https://gitlab.com/mbarkhau/bumpver [url_semver_org]: https://semver.org/ +[url_calver_org]: https://calver.org/ Project/Repo: [![MIT License][img_license]][url_license] [![Supported Python Versions][img_pyversions]][url_pyversions] -[![CalVer v2020.1041-beta][img_version]][url_version] +[![CalVer 2020.1041-beta][img_version]][url_version] [![PyPI Releases][img_pypi]][url_pypi] [![PyPI Downloads][img_downloads]][url_downloads] @@ -40,310 +40,385 @@ Code Quality/CI: | Manuel Barkhau (mbarkhau@gmail.com) | author/maintainer | 2018-09 | - | +[img_github_build]: https://github.com/mbarkhau/bumpver/workflows/CI/badge.svg +[url_github_build]: https://github.com/mbarkhau/bumpver/actions?query=workflow%3ACI -[img_github_build]: https://github.com/mbarkhau/pycalver/workflows/CI/badge.svg -[url_github_build]: https://github.com/mbarkhau/pycalver/actions?query=workflow%3ACI +[img_gitlab_build]: https://gitlab.com/mbarkhau/bumpver/badges/master/pipeline.svg +[url_gitlab_build]: https://gitlab.com/mbarkhau/bumpver/pipelines -[img_gitlab_build]: https://gitlab.com/mbarkhau/pycalver/badges/master/pipeline.svg -[url_gitlab_build]: https://gitlab.com/mbarkhau/pycalver/pipelines - -[img_codecov]: https://gitlab.com/mbarkhau/pycalver/badges/master/coverage.svg -[url_codecov]: https://mbarkhau.gitlab.io/pycalver/cov +[img_codecov]: https://gitlab.com/mbarkhau/bumpver/badges/master/coverage.svg +[url_codecov]: https://mbarkhau.gitlab.io/bumpver/cov [img_license]: https://img.shields.io/badge/License-MIT-blue.svg -[url_license]: https://gitlab.com/mbarkhau/pycalver/blob/master/LICENSE +[url_license]: https://gitlab.com/mbarkhau/bumpver/blob/master/LICENSE [img_mypy]: https://img.shields.io/badge/mypy-checked-green.svg -[url_mypy]: https://mbarkhau.gitlab.io/pycalver/mypycov +[url_mypy]: https://mbarkhau.gitlab.io/bumpver/mypycov [img_style]: https://img.shields.io/badge/code%20style-%20sjfmt-f71.svg [url_style]: https://gitlab.com/mbarkhau/straitjacket/ -[img_downloads]: https://pepy.tech/badge/pycalver/month -[url_downloads]: https://pepy.tech/project/pycalver +[img_downloads]: https://pepy.tech/badge/bumpver/month +[url_downloads]: https://pepy.tech/project/bumpver -[img_version]: https://img.shields.io/static/v1.svg?label=CalVer&message=v2020.1041-beta&color=blue -[url_version]: https://pypi.org/project/pycalver/ +[img_version]: https://img.shields.io/static/v1.svg?label=CalVer&message=2020.1041-beta&color=blue +[url_version]: https://pypi.org/project/bumpver/ [img_pypi]: https://img.shields.io/badge/PyPI-wheels-green.svg -[url_pypi]: https://pypi.org/project/pycalver/#files +[url_pypi]: https://pypi.org/project/bumpver/#files -[img_pyversions]: https://img.shields.io/pypi/pyversions/pycalver.svg -[url_pyversions]: https://pypi.python.org/pypi/pycalver +[img_pyversions]: https://img.shields.io/pypi/pyversions/bumpver.svg +[url_pyversions]: https://pypi.python.org/pypi/bumpver + -[](TOC) + -- [PyCalVer: Automatic Calendar Versioning](#pycalver-automatic-calendar-versioning) - - [Usage](#usage) - - [Configuration](#configuration) - - [Pattern Search and Replacement](#pattern-search-and-replacement) - - [Week Numbering](#week-numbering) - - [Normalization Caveats](#normalization-caveats) - - [Legacy Patterns](#legacy-patterns) - - [Pattern Usage](#pattern-usage) - - [Examples](#examples) - - [Version State](#version-state) - - [The Current Version](#the-current-version) - - [Bump It Up](#bump-it-up) - - [Config Parameters](#config-parameters) - - [CLI Reference](#cli-reference) - - [The PyCalVer Format](#the-pycalver-format) - - [Parsing](#parsing) - - [Incrementing Behaviour](#incrementing-behaviour) - - [Semantics of PyCalVer](#semantics-of-pycalver) - - [Pitch](#pitch) - - [blah](#blah) - - [Intentional Breaking Changes](#intentional-breaking-changes) - - [Costs and Benefits](#costs-and-benefits) - - [Unintentional Breaking Changes](#unintentional-breaking-changes) - - [Pinning is not a Panacea](#pinning-is-not-a-panacea) - - [Zeno's 1.0 and The Eternal Beta](#zenos-10-and-the-eternal-beta) +- [Overview](#overview) + - [Search and Replace](#search-and-replace) + - [Related Projects/Alternatives](#related-projectsalternatives) +- [Example Usage](#example-usage) + - [Testing a version pattern](#testing-a-version-pattern) + - [SemVer: `MAJOR`/`MINOR`/`PATCH`](#semver-majorminorpatch) + - [Auto Increment Parts: `BUILD`/`INC0`/`INC1`](#auto-increment-parts-buildinc0inc1) + - [Persistent Parts: `BUILD`/`TAG`/`PYTAG`](#persistent-parts-buildtagpytag) + - [Searching for Patterns with `grep`](#searching-for-patterns-with-grep) +- [Reference](#reference) + - [Command Line](#command-line) + - [Part Overview](#part-overview) + - [Normalization Caveats](#normalization-caveats) + - [Pattern Examples](#pattern-examples) + - [Week Numbering](#week-numbering) +- [Configuration](#configuration) + - [Configuration Setup](#configuration-setup) + - [Debugging Configuration](#debugging-configuration) +- [Bump It Up](#bump-it-up) + - [Version State](#version-state) + - [The Current Version](#the-current-version) + - [Dry Mode](#dry-mode) + - [VCS Parameters (git/mercurial)](#vcs-parameters-gitmercurial) +- [Depricated Pattern Syntax](#depricated-pattern-syntax) -[](TOC) + ## Overview ### Search and Replace -With `calver`, you configure a single `version_pattern` which is then used to +With `bumpver`, you configure a single `version_pattern` which is then used to 1. Search for version strings in your project files -2. Replace these occurrences with an updated/bumped version number. +2. Replace these with an updated/bumped version number. Your configuration might look something like this: ``` -[calver] -current_version = "2020.9.0" -version_pattern = "YYYY.MM.PATCH" +[bumpver] +current_version = "1.5.2" +version_pattern = "MAJOR.MINOR.PATCH" -[calver:file_patterns] +[bumpver:file_patterns] +setup.py + version="{version}", src/mymodule/__init__.py __version__ = "{version}" -src/mymodule/__main__.py - @click.version_option(version="{version}") -setup.py - version="{version}", ``` -> Throughout the examples, we use the `--date` argument. Without this argument `calver` will just use the current UTC date. We use it here so that you can easily reproduce the examples. - -Using this configuration, the output of `calver bump --dry` might look something like this: +Using this configuration, the output of `bumpver update --dry` might look something like this: ```diff -$ calver bump --date 2020-10-21 --dry -INFO - fetching tags from remote (to turn off use: -n / --no-fetch) -INFO - Old Version: 2020.9.0 -INFO - New Version: 2020.10.0 +$ bumpver update --patch --dry +INFO - Old Version: 1.5.2 +INFO - New Version: 1.5.3 --- setup.py +++ setup.py @@ -63,7 +63,7 @@ - setuptools.setup( name="mymodule", -- version="2020.9.0", -+ version="2020.10.0", +- version="1.5.2", ++ version="1.5.3", description=description, - long_description=long_description, --- src/mymodule/__init__.py +++ src/mymodule/__init__.py @@ -3,3 +3,3 @@ --__version__ = "2020.9.0" -+__version__ = "2020.10.0" - - ---- src/mymodule/__main__.py -+++ src/mymodule/__main__.py -@@ -101,7 +101,7 @@ - - @click.group() --@click.version_option(version="2020.9.0") -+@click.version_option(version="2020.10.0") - @click.help_option() - @click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") +-__version__ = "1.5.2" ++__version__ = "1.5.3" ``` +### Name Change PyCalVer -> BumpVer + +This project was originally developed under the name PyCalVer, with the intent to support various CalVer schemes. The package and CLI command have since been renamed from PyCalVer/`pycalver` to BumpVer/`bumpver`. + +This name change is due to confusion that this project is either Python specific, or only suitible for CalVer versioning schemes, neither of which is the case. + ### Related Projects/Alternatives -If PyCalVer does not serve your purposes, you may wish to look at the [bump2version][url_bump2version] project, by which PyCalVer was heavily inspired. You may also wish to take a look at their list of related projects: [bump2version/RELATED.md][url_bump2version_related] +If you are looking for an alternative, BumpVer was heavily influenced by [bumpversion / bump2version][url_bump2version]. You may also wish to take a look at their list of related projects: [bump2version/RELATED.md][url_bump2version_related] [url_bump2version] https://github.com/c4urself/bump2version/ [url_bump2version_related] https://github.com/c4urself/bump2version/blob/master/RELATED.md + ## Example Usage -### Testing a version pattern - -You can validate a pattern and how it is incremented using `calver test`. +You can override the date used by `bumpver` with the `--date=` option. Adding this every time would be distracting, so the examples assume the following date: ```shell -$ calver test --date 2020-09-22 '2020.37' 'YYYY.WW' -New Version: 2020.38 -PEP440 : 2020.38 +$ date --iso +2020-10-15 +``` -$ calver test --date 2020-09-22 '2020.37' 'YYYY.MM' # expected to fail because 37 is not valid for part MM + +### Testing a `version_pattern` + +To test a `version_pattern` and how to increment it, you can use `bumpver test`: + +```shell +$ bumpver test 'v2020.37' 'vYYYY.WW' +New Version: v2020.41 +``` + +A `version_pattern` consists of three kinds of characters: + +- Literal text, such as `v`, `.`, and `-`, typically used as delimiters. +- A [valid part](#parts-overview) such as `YYYY`/`WW` in the previous example. +- Square brackets `[]` to mark an optional segment. + +The following example uses all three: `vYYYY.WW[-TAG]` + +``` + vYYYY.WW[-TAG] +literal text ^ ^ ^ +``` + +```shell +$ bumpver test 'v2020.37-beta' 'vYYYY.WW[-TAG]' +New Version: v2020.41-beta +PEP440 : 2020.41b0 +``` + +Here we see the week number changed from 37 to 41. The test command also shows the normalized version pattern according to [PEP440][pep_440_ref]. This removes the `"v"` prefix and shortens the release tag from `-beta` to `b0`. + +[pep_440_ref]: https://www.python.org/dev/peps/pep-0440/ + +To remove the release tag, use the option `--tag=final`. + +```shell +$ bumpver test 'v2020.37-beta' 'vYYYY.WW[-TAG]' --tag=final +New Version: v2020.41 +PEP440 : 2020.41 +``` + +### Using `MAJOR`/`MINOR`/`PATCH` (SemVer Parts) + +A CalVer `version_pattern` may not require any flags to determine which part should be incremented, so long as the date has changed. +With SemVer you must always specify one of `--major/--minor/--patch`. + +```shell +$ bumpver test '1.2.3' 'MAJOR.MINOR.PATCH[PYTAGNUM]' --major +New Version: 2.0.0 + +$ bumpver test '1.2.3' 'MAJOR.MINOR.PATCH[PYTAGNUM]' --minor +New Version: 1.3.0 + +$ bumpver test '1.2.3' 'MAJOR.MINOR.PATCH[PYTAGNUM]' --patch +New Version: 1.2.4 + +$ bumpver test '1.2.3' 'MAJOR.MINOR.PATCH[PYTAGNUM]' --patch --tag=beta +New Version: 1.2.4b0 + +$ bumpver test '1.2.4b0' 'MAJOR.MINOR.PATCH[PYTAGNUM]' --tag-num +New Version: 1.2.4b1 +``` + +These non date based parts also make sense for a CalVer `version_pattern`, so that you can create multiple releases in the same month. It is common to include e.g. a `PATCH` part. + +```shell +$ bumpver test '2020.10.0' 'YYYY.MM.PATCH' --patch +New Version: 2020.10.1 +``` + +Without this flag, we would get an error if the date is still in October. + +```shell +$ date --iso +2020-10-15 + +$ bumpver test '2020.10.0' 'YYYY.MM.PATCH' +ERROR - Invalid arguments or pattern, version did not change. +ERROR - Version did not change: '2020.10.0'. Invalid version and/or pattern 'YYYY.MM.PATCH'. +INFO - Perhaps try: bumpver test --patch +``` + +Once the date is in November, the `PATCH` part will roll over back to zero. This happens whenever parts to the left change (in this case the year and month), just as it does if `MAJOR` or `MINOR` were incremented in SemVer. + +```shell +$ bumpver test '2020.10.1' 'YYYY.MM.PATCH' --date 2020-11-01 +New Version: 2020.11.0 +``` + +The rollover to zero will happen even if you use the `--patch` argument, so that your first release in a month will always have a `PATCH` set to 0 instead of 1. You can make the `PATCH` part optional with `[.PATCH]` and always supply the `--patch` flag in your build script. This will cause the part to be omitted when 0 and added when > 0. + +```shell +$ bumpver test '2020.9.1' 'YYYY.MM[.PATCH]' --patch +New Version: 2020.10 + +$ bumpver test '2020.10' 'YYYY.MM[.PATCH]' --patch +New Version: 2020.10.1 + +$ bumpver test '2020.10.1' 'YYYY.MM[.PATCH]' --patch +New Version: 2020.10.2 +``` + + +With CalVer, the version is based on a calendar date, so you only have to specify such flags if you've already published a release for the current date. Without such a flag, BumpVer will show the error, that the "version did not change". + +```shell +$ bumpver test 'v2020.41-beta0' 'vYYYY.WW[-TAGNUM]' +ERROR - Invalid arguments or pattern, version did not change. +ERROR - Invalid version 'v2020.41-beta0' and/or pattern 'vYYYY.WW[-TAGNUM]'. +``` + +In this case you have to change one of the parts that are not based on a calendar date. + +```shell +$ bumpver test 'v2020.41-beta0' 'vYYYY.WW[-TAGNUM]' --tag-num +New Version: v2020.41-beta1 +PEP440 : 2020.41b1 + +$ bumpver test 'v2020.41-beta0' 'vYYYY.WW[-TAGNUM]' --tag=final +New Version: v2020.41 +PEP440 : 2020.41 +``` + +If a pattern is not applicable to a version string, then you will get an error message. + +```shell +$ bumpver test '2020.37' 'YYYY.MM' # expected to fail because 37 is not valid for part MM ERROR - Incomplete match '2020.3' for version string '2020.37' with pattern 'YYYY.MM'/'(?P[1-9][0-9]{3})\.(?P1[0-2]|[1-9])' ERROR - Invalid version '2020.37' and/or pattern 'YYYY.MM'. ``` -This illustrates that each pattern is internally translated to a regular expression which must match your version string. The `--verbose` flag shows a slightly more readable form. +This illustrates that each pattern is internally translated to a regular expression which must match the version string. The `--verbose` flag will show a verbose form of the regular expression, which may help to debug the discrepancy between the pattern and the version. ```shell -$ calver test --date 2018-09-22 'v2018.37' 'YYYY.WW' --verbose +$ bumpver test 'v2020.37' 'YYYY.WW' --verbose # missing "v" prefix INFO - Using pattern YYYY.WW INFO - regex = re.compile(r""" (?P[1-9][0-9]{3}) \. (?P5[0-2]|[1-4][0-9]|[0-9]) """, flags=re.VERBOSE) -ERROR - Invalid version string 'v2018.37' for pattern ... +ERROR - Invalid version string 'v2020.37' for pattern ... ``` -In other words, you don't specify regular expressions manually, they are generated for by PyCalVer based on the parts defined in the [Parts Overview](#parts-overview). - - -### SemVer: `MAJOR`/`MINOR`/`PATCH` - -You can do tradition SemVer without any kind of calendar component if you like. +To fix the above, you can either remove the "v" prefix from the version or add it to the pattern. ```shell -$ calver test '1.2.3' 'MAJOR.MINOR.PATCH' --patch -New Version: 1.2.4 -PEP440 : 1.2.4 - -$ calver test '1.2.3' 'MAJOR.MINOR.PATCH' --minor -New Version: 1.3.0 -PEP440 : 1.3.0 - -$ calver test '1.2.3' 'MAJOR.MINOR.PATCH' --major -New Version: 2.0.0 -PEP440 : 2.0.0 -``` - -These are the same CLI flags as are accepted by the `calver bump` command. - -In the context of a CalVer version, a typical use would be to include a `PATCH` part in your version pattern, so that you can create multiple releases in the same month. - -```shell -$ calver test --date 2018-09-22 '2018.9.0' 'YYYY.MM.PATCH' -ERROR - Invalid arguments or pattern, version did not change. -ERROR - Version did not change: '2018.9.0'. Invalid version and/or pattern 'YYYY.MM.PATCH'. -INFO - Perhaps try: calver test --patch - -$ calver test --date 2018-09-22 '2018.9.0' 'YYYY.MM.PATCH' --patch -New Version: 2018.9.1 -PEP440 : 2018.9.1 -``` - -The `PATCH` part will roll over back to zero when leading parts change (in this case the year and month). - -```shell -$ calver test --date 2018-10-22 '2018.9.1' 'YYYY.MM.PATCH' -New Version: 2018.10.0 -PEP440 : 2018.10.0 -``` - -This will happen even if you use the `--patch` argument, so that your first release of the month has a `PATCH` of 0 instead of 1. - -```shell -$ calver test --date 2018-10-22 '2018.9.1' 'YYYY.MM.PATCH' --patch -New Version: 2018.10.0 -PEP440 : 2018.10.0 +$ bumpver test 'v2020.37' 'vYYYY.WW' # added "v" prefix +New Version: v2020.41 +PEP440 : 2020.41 ``` ### Auto Increment Parts: `BUILD`/`INC0`/`INC1` -The following parts are incremented automatically, and do not use/require a CLI flag: `BUILD`/`INC0`/`INC1`. This means you can just do `calver bump` without any further CLI flags and special cases, which can simplify your build scripts. +These parts are incremented automatically, and do not use/require a CLI flag: `BUILD`/`INC0`/`INC1`. ```shell -$ calver test --date 2018-09-22 '2018.9.1' 'YYYY.MM.INC0' -New Version: 2018.9.2 -PEP440 : 2018.9.2 +$ bumpver test '2020.10.1' 'YYYY.MM.INC0' +New Version: 2020.10.2 -$ calver test --date 2018-10-22 '2018.9.2' 'YYYY.MM.INC0' -New Version: 2018.10.0 -PEP440 : 2018.10.0 - -$ calver test --date 2018-10-22 '2018.9.2' 'YYYY.MM.INC1' -New Version: 2018.10.1 -PEP440 : 2018.10.1 +$ bumpver test '2020.10.2' 'YYYY.MM.INC0' --date 2020-11-01 +New Version: 2020.11.0 ``` -If it is rare for you to make multiple releases within a given period, you can make such a part optional using the `[PART]` syntax with square brackets: +You can make the part optional using the `[PART]` syntax and it will be added/removed as needed. ```shell -$ calver test --date 2018-09-22 '2018.9' 'YYYY.MM[.INC0]' -New Version: 2018.9.1 -PEP440 : 2018.9.1 +$ bumpver test '2020.10' 'YYYY.MM[.INC0]' +New Version: 2020.10.1 -$ calver test --date 2018-10-22 '2018.9.1' 'YYYY.MM[.INC0]' -New Version: 2018.10 -PEP440 : 2018.10 +$ bumpver test '2020.10.1' 'YYYY.MM[.INC0]' --date 2020-11-01 +New Version: 2020.11 ``` -If the extra `INC0` part is needed, it is added. If the date rolls over and it's no longer needed, it is omitted. Any literal text enclosed in the brackets (such as a separator) will also be added or omitted as needed. +### Persistent Parts: `BUILD`/`TAG`/`PYTAG` -### Persistent Parts: `BUILD`/`RELEASE`/`PYTAG` - -The `BUILD` and `RELEASE` parts are not reset. Instead they are carried forward. +The `BUILD` and `TAG` parts will not rollover/reset. Instead they are carried forward from one version to the next. ```shell -$ calver test --date 2018-09-22 '201809.1051-beta' 'YYYY0M.BUILD[-RELEASE]' -New Version: 201809.1052-beta -PEP440 : 201809.1052b0 +$ bumpver test 'v2020.1051-beta' 'vYYYY.BUILD[-TAG]' +New Version: v2020.1052-beta +PEP440 : 2020.1052b0 -$ calver test --date 2018-09-22 '201809.1051-beta' 'YYYY0M.BUILD[-RELEASE]' --release rc -New Version: 201809.1052-rc -PEP440 : 201809.1052rc0 +$ bumpver test 'v2020.1051-beta' 'vYYYY.BUILD[-TAG]' --date 2021-01-01 +New Version: v2021.1052-beta +PEP440 : 2021.1052b0 + +$ bumpver test 'v2020.1051-beta' 'vYYYY.BUILD[-TAG]' --tag=rc +New Version: v2020.1052-rc +PEP440 : 2020.1052rc0 ``` -To remove a release tag, mark it as final with `--release final`. +To remove a release tag, mark it as final with `--tag=final`. ```shell -$ calver test --date 2018-09-22 '201809.1051-beta' 'YYYY0M.BUILD[-RELEASE]' --release final -New Version: 201809.1052 -PEP440 : 201809.1052 +$ bumpver test 'v2020.1051-beta' 'vYYYY.BUILD[-TAG]' --tag=final +New Version: v2020.1052 +PEP440 : 2020.1052 ``` ### Searching for Patterns with `grep` -When searching for a pattern, There are some limitations to keep in mind: - - 1. A version string cannot span multiple lines. - 2. There is no mechanism for escaping parts. - 3. Brackets `[]` can be escaped with backslash. - -Using `calver grep`, you can search for occurrences of a version pattern in your project files. +You can use `bumpver grep` to test and debug entries for your configuration. ```shell -$ calver grep '__version__ = "YYYY.MM[-RELEASENUM]"' src/module/__init__.py -src/module/__init__.py +$ bumpver grep \ + '__version__ = "YYYY.MM[-TAGNUM]"' \ + src/module/__init__.py + 3: 4: __version__ = "2020.9-beta1" 5: ``` +When searching your project files for version strings, there are some limitations to keep in mind: + + 1. A version string cannot span multiple lines. + 2. Brackets `[]` can be escaped with backslash: `\[\]`. + 3. There is no way to escape a valid part (so you cannot match the literal text `YYYY`). + Note that everything in the pattern is treated as literal text, except for a valid part (in all caps). +``` + __version__ = "YYYY.MM[-TAGNUM]" +literal text ^^^^^^^^^^^^^^^ ^ ^ ^ +``` + When you write your configuration, you can avoid repeating your version pattern in every search pattern, by using these placeholders - `{version}` - `{pep440_version}` -Applied to the above example, you can instead use this: +Applied to the above example, you can instead write this: ```shell -$ calver grep --version-pattern "YYYY.MM[-RELEASENUM]" '__version__ = "{version}"' src/module/__init__.py -src/module/__init__.py +$ bumpver grep \ + --version-pattern "YYYY.MM[-TAGNUM]" \ + '__version__ = "{version}"' \ + src/module/__init__.py + 3: 4: __version__ = "2020.9-beta1" 5: @@ -352,35 +427,35 @@ src/module/__init__.py The corresponding configuration would look like this. ```ini -[calver] +[bumpver] current_version = "2020.9-beta1" -version_pattern = "YYYY.MM[-RELEASENUM]" +version_pattern = "YYYY.MM[-TAGNUM]" ... -[calver:file_patterns] +[bumpver:file_patterns] src/module/__init__.py __version__ = "{version}" ... ``` -If your pattern produces non PEP440 version numbers, you may wish to use the placeholder `{pep440_version}` in your search pattern and specify your `--version-pattern` separately. +If you use a version pattern that is not in the PEP440 normalized form (such as the one above), you can nonetheless match version strings in your project files which *are* in the [PEP440 normalized form][url_pep_440]. To do this, you can use the placeholder `{pep440_version}` instead of the `{version}` placeholder. ```shell -$ calver grep --version-pattern "YYYY.MM[-RELEASENUM]" 'version="{pep440_version}"' setup.py +$ bumpver grep --version-pattern "YYYY.MM[-TAGNUM]" 'version="{pep440_version}"' setup.py setup.py 65: url="https://github.com/org/project", 66: version="2020.9b1", 67: description=description, ``` -The placeholder `{version}` matches `2020.9-beta1`, while the placeholder `{pep440_version}` matches `2020.9b1` (excluding the "v" prefix, the "-" separator and with a short form release tag "b1" instead of "beta1"). These two placeholders make it possible to mostly use your preferred format for version strings, but use a [PEP440][url_pep_440] compliant/normalized version string where appropriate. +The placeholder `{version}` matches `2020.9-beta1`, while the placeholder `{pep440_version}` matches `2020.9b1` (excluding the "v" prefix, the "-" separator and with a short form release tag "b1" instead of "beta1"). These two placeholders make it possible to mostly use your preferred format for version strings, but use a PEP440 compliant/normalized version string where appropriate. [url_pep_440]: https://www.python.org/dev/peps/pep-0440/ -As a further illustration of how the search and replace works, you might want use a file pattern entry to keep the year of your copyright header up to date. +As a ~~neat trick~~ further illustration of how the search and replace works, you might wish to keep the year of your copyright headers up to date. -``` -$ calver grep 'Copyright (c) 2018-YYYY' src/mymodule/*.py | head +```shell +$ bumpver grep 'Copyright (c) 2018-YYYY' src/mymodule/*.py | head src/mymodule/__init__.py 3: 4: # Copyright (c) 2018-2020 Vandelay Industries - All rights reserved. @@ -395,24 +470,40 @@ src/mymodule/config.py The corresponding configuration for this pattern would look like this. ```ini -[calver:file_patterns] +[bumpver:file_patterns] ... src/mymodule/*.py Copyright (c) 2018-YYYY Vandelay Industries - All rights reserved. ``` +Note that there must be a match for every entry in `file_patterns`. If there is no match, `bumpver` will show an error. This ensures that a pattern is not skipped when your project changes. In this case the side effect is to make sure that every file has a copyright header. + +```shell +$ bumpver update --dry +ERROR - No match for pattern 'Copyright (c) 2018-YYYY Vandelay Industries - All rights reserved.' +ERROR - +# https://regex101.com/?flavor=python&flags=gmx®ex=Copyright%5B%20%5D%5C%28c%5C%29%0A%5B%20%5D2018%5C-%0A%28%3FP%3Cyear_y%3E%5B1-9%5D%5B0-9%5D%7B3%7D%29%0A%5B%20%5DVandelay%5B%20%5DIndustries%5B%20%5D%5C-%5B%20%5DAll%5B%20%5Drights%5B%20%5Dreserved%5C. +regex = re.compile(r""" + Copyright[ ]\(c\) + [ ]2018\- + (?P[1-9][0-9]{3}) + [ ]Vandelay[ ]Industries[ ]\-[ ]All[ ]rights[ ]reserved\. +""", flags=re.VERBOSE) +ERROR - No patterns matched for file 'src/mymodule/utils.py' +``` + ## Reference ### Command Line - + ``` -$ calver --help -Usage: calver [OPTIONS] COMMAND [ARGS]... +$ bumpver --help +Usage: bumpver [OPTIONS] COMMAND [ARGS]... - Automatically update PyCalVer version strings in all project files. + Automatically update CalVer version strings in plaintext files. Options: --version Show the version and exit. @@ -420,20 +511,20 @@ Options: -v, --verbose Control log level. -vv for debug level. Commands: - bump Increment the current version string and update project files. - grep Search file(s) for a version pattern. - init Initialize [calver] configuration. - show Show current version of your project. - test Increment a version number for demo purposes. + update Increment the current version string and update project files. + grep Search file(s) for a version pattern. + init Initialize [bumpver] configuration. + show Show current version of your project. + test Increment a version number for demo purposes. ``` - + - + ``` -$ calver bump --help -Usage: calver bump [OPTIONS] +$ bumpver update --help +Usage: bumpver update [OPTIONS] Increment the current version string and update project files. @@ -441,10 +532,6 @@ Options: -v, --verbose Control log level. -vv for debug level. -f, --fetch / -n, --no-fetch Sync tags from remote origin. -d, --dry Display diff of changes, don't rewrite files. - --release Override release name of current_version. - Valid options are: alpha, beta, rc, post, - final. - --allow-dirty Commit even when working directory is has uncomitted changes. (WARNING: The commit will still be aborted if there are uncomitted to @@ -453,35 +540,40 @@ Options: --major Increment major component. -m, --minor Increment minor component. -p, --patch Increment patch component. - -r, --release-num Increment release number (rc1, rc2, rc3..). + -t, --tag Override release tag of current_version. Valid + options are: alpha, beta, rc, post, final. + + --tag-num Increment release tag number (rc1, rc2, + rc3..). + --pin-date Leave date components unchanged. --date Set explicit date in format YYYY-0M-0D (e.g. - 2020-10-09). + 2020-10-16). --help Show this message and exit. ``` - + ### Part Overview -> Where possible, these patterns match the conventions from [calver.org][url_calver_org_scheme]. +> Where possible, these patterns match the conventions from [CalVer.org][url_calver_org_scheme]. [url_calver_org_scheme]: https://calver.org/#scheme -| part | range / example(s) | comment | +| part | range / example(s) | info | |---------|---------------------------|--------------------------------------------| +| `MAJOR` | 0..9, 10..99, 100.. | `bumpver update --major` | +| `MINOR` | 0..9, 10..99, 100.. | `bumpver update --minor` | +| `PATCH` | 0..9, 10..99, 100.. | `bumpver update --patch` | +| `TAG` | alpha, beta, rc, post | `--tag=` | +| `PYTAG` | a, b, rc, post | `--tag=` | +| `NUM` | 0, 1, 2... | `-r/--tag-num` | | `YYYY` | 2019, 2020... | Full year, based on `strftime('%Y')` | | `YY` | 18, 19..99, 0, 1 | Short year, based on `int(strftime('%y'))` | | `MM` | 9, 10, 11, 12 | Month, based on `int(strftime('%m'))` | | `DD` | 1, 2, 3..31 | Day, based on `int(strftime('%d'))` | -| `MAJOR` | 0..9, 10..99, 100.. | `calver bump --major` | -| `MINOR` | 0..9, 10..99, 100.. | `calver bump --minor` | -| `PATCH` | 0..9, 10..99, 100.. | `calver bump --patch` | -| `TAG` | alpha, beta, rc, post | `--tag=` | -| `PYTAG` | a, b, rc, post | `--tag=` | -| `NUM` | 0, 1, 2... | `-r/--release-num` | | `BUILD` | 1001, 1002 .. 1999, 22000 | build number (maintains lexical order) | | `INC0` | 0, 1, 2... | 0-based auto incrementing number | | `INC1` | 1, 2... | 1-based auto incrementing number | @@ -515,7 +607,7 @@ The following are also available, but you should review the [Normalization Cavea ### Normalization Caveats -Package managers and installation tools will parse your version numbers. When doing so, your version number may go through a normalization process and may not be displayed as you specified it. In the case of Python, the packaging tools (such as pip, twine, setuptools) follow [PEP440 normalization rules][pep_440_normalzation_ref]. +Package managers and installation tools will parse your version numbers. When doing so, your version number may go through a normalization process and may not be exactly as you specified. In the case of Python, the packaging tools (such as pip, twine, [setuptools][setuptools_ref]) follow [PEP440 normalization rules][pep_440_normalzation_ref]. According to these rules (among other things): @@ -525,13 +617,13 @@ According to these rules (among other things): For example: -- Pattern: `vYY.0M.0D[-RELEASE]` +- Pattern: `vYY.0M.0D[-TAG]` - Version: `v20.08.02-beta` - PEP440 : `20.8.2b0` -It may not be obvious to everyone that `v20.08.02-beta` is the same `20.8.2b0` on pypi. To avoid this confusion, you should choose a pattern which is always in a normalized form or as close to it as possible. +I am not aware of any technical reason to use a normalized representation everywhere in your project. However, if you choose a pattern which is always in a normalized, it will help to avoid confusion. For example, it may not be obvious at a glance, that `v20.08.02-beta` is the same as `20.8.2b0` . -A further consideration for the choice of your version format is that it may be processed by tools that *do not* interpret it as a version number, but treat it just like any other string. It may also be confusing to your users if they a list of version numbers, sorted lexicographically by some tool (e.g. from `git tags`) and versions are not listed in order of their release as here: +A further consideration for the choice of your `version_pattern` is that it may be processed by tools that *do not* interpret it as a version number, but treat it just like any other string. It may also be confusing to your users if they a list of version numbers, sorted lexicographically by some tool (e.g. from `git tags`) and versions are not listed in order of their release: ``` $ git tag @@ -545,6 +637,11 @@ $ git tag If you wish to avoid this, you should use a pattern which maintains lexicographical ordering. +[setuptools_ref]: https://setuptools.readthedocs.io/en/latest/setuptools.html#specifying-your-project-s-version + +[pep_440_normalzation_ref]: https://www.python.org/dev/peps/pep-0440/#id31 + + ### Pattern Examples @@ -554,10 +651,10 @@ If you wish to avoid this, you should use a pattern which maintains lexicographi | `MAJOR.MINOR.PATCH[PYTAGNUM]` | `0.13.10 0.16.10rc1` | yes | no | | `MAJOR.MINOR[.PATCH[PYTAGNUM]]` | `1.11 0.3.0b5` | yes | no | | `YYYY.BUILD[PYTAGNUM]` | `2020.1031 2020.1148a0` | yes | yes | -| `YYYY.BUILD[-RELEASE]` | `2021.1393-beta 2022.1279` | no | yes | +| `YYYY.BUILD[-TAG]` | `2021.1393-beta 2022.1279` | no | yes | | `YYYY.INC0[PYTAGNUM]` | `2020.10 2021.12b2` | yes | no | -| `YYYY0M.PATCH[-RELEASE]` | `202005.12 202210.15-beta` | no | no¹ | -| `YYYY0M.BUILD[-RELEASE]` | `202106.1071 202106.1075-beta` | no | yes | +| `YYYY0M.PATCH[-TAG]` | `202005.12 202210.15-beta` | no | no¹ | +| `YYYY0M.BUILD[-TAG]` | `202106.1071 202106.1075-beta` | no | yes | | `YYYY.0M` | `2020.02 2022.09` | no | yes | | `YYYY.MM` | `2020.8 2020.10` | yes | no | | `YYYY.WW` | `2020.8 2021.14` | yes | no | @@ -578,10 +675,9 @@ If you wish to avoid this, you should use a pattern which maintains lexicographi Week numbering is a bit special, as it depends on your definition of "week": -- Does it start on a Monday or a Sunday? -- Range from 0-52 or 1-53 ? -- At the beginning/end of the year, do you have partial weeks or do you have a week that span multiple years? -- If a week spans multiple years, what is the year number? +- First day of the week is either Monday or Sunday. +- Range either from 0-52 or 1-53. +- At the beginning/end of the year, you either have partial weeks or a week that spans multiple years. If you use `VV`/`0V`, be aware that you cannot also use `YYYY`. Instead use `GGGG`. This is to avoid an edge case where your version @@ -590,7 +686,7 @@ number would run backwards if it was created around New Year. -``` +```sql YYYY WW UU GGGG VV 2020-12-26 (Sat): 2020 51 51 2020 52 2020-12-27 (Sun): 2020 51 52 2020 52 @@ -611,145 +707,184 @@ number would run backwards if it was created around New Year. ### Configuration Setup -The fastest way to setup the configuration for project is to use `calver init`. +The create an initial configuration for project with `bumpver init`. ```shell -$ pip install python-calver +$ pip install bumpver ... -Installing collected packages: click lexid pathlib2 typing toml python-calver -Successfully installed python-calver-2020.1041b0 +Installing collected packages: click toml lexid bumpver +Successfully installed bumpver-2020.1042 $ cd myproject ~/myproject/ -$ calver init --dry -Exiting because of '-d/--dry'. Would have written to calver.toml: - [calver] - current_version = "v202010.1001-alpha" - version_pattern = "vYYYY0M.BUILD[-RELEASE]" +$ bumpver init --dry +Exiting because of '-d/--dry'. Would have written to bumpver.toml: + + [bumpver] + current_version = "2020.1001a0" + version_pattern = "YYYY.BUILD[PYTAGNUM]" commit_message = "bump version to {new_version}" commit = true tag = true push = true - [calver.file_patterns] + [bumpver.file_patterns] "README.md" = [ "{version}", "{pep440_version}", ] - "calver.toml" = [ + "bumpver.toml" = [ 'current_version = "{version}"', ] ``` -If you already have configuration file in your project (such as a `setup.cfg` file), then `calver init` will update that file instead. +If you already have configuration file in your project (such as `setup.cfg` or `pyproject.toml`), then `bumpver init` will update that file instead. ``` ~/myproject -$ calver init +$ bumpver init Updated setup.cfg ``` Your `setup.cfg` may now look something like this: ```ini -[calver] +[bumpver] current_version = "2019.1001-alpha" -version_pattern = "YYYY.BUILD[-RELEASE]" +version_pattern = "YYYY.BUILD[-TAG]" commit_message = "bump version to {new_version}" commit = True tag = True push = True -[calver:file_patterns] +[bumpver:file_patterns] setup.cfg = current_version = "{version}" setup.py = - "{pep440_version}", + version="{pep440_version}", README.md = {version} {pep440_version} ``` -For the entries in `[pycalver:file_patterns]` you can expect two failure modes: -- A pattern won't match a version number in the associated file. -- A pattern will match something it shouldn't (less likely). +### Debugging Configuration -To debug such issues, you can use `pycalver grep` . +For the entries in `[bumpver:file_patterns]` you can expect two failure modes: -``` -$ pycalver grep 'Copyright (c) 2018-YYYY' src/module/*.py -src/module/__init__.py - 3: # - 4: # Copyright (c) 2018-2020 Vandelay Industries - All rights reserved. - 5: # SPDX-License-Identifier: MIT +- False negative: A pattern *will not* match a version number in the associated file *which it should* match. +- False positive: A pattern *will* match something it *should not match* (less likely). -src/module/config.py - 3: # - 4: # Copyright (c) 2018-2020 Vandelay Industries - All rights reserved. - 5: # SPDX-License-Identifier: MIT -``` - -Of course, you may not get the pattern correct right away. If your pattern is not found, `pycalver grep` will show an error message with the regular expression it uses, to help you debug the issue. - -``` -$ pycalver grep 'Copyright 2018-YYYY' src/pycalver/*.py -ERROR - Pattern not found: 'Copyright 2018-YYYY' -# https://regex101.com/?flavor=python&flags=gmx®ex=Copyright%5B%20%5D2018%5C-%0A%28%3FP%3Cyear_y%3E%5B1-9%5D%5B0-9%5D%7B3%7D%29 -re.compile(r""" - Copyright[ ]2018\- - (?P[1-9][0-9]{3}) -""", flags=re.VERBOSE) -``` - - - -Let's say you want to keep a badge your README.md up to date. - -``` -$ pycalver grep --version-pattern='vYYYY0M.BUILD[-RELEASE]' 'img.shields.io/static/v1.svg?label=PyCalVer&message={version}&color=blue' README.md - - 61: - 62: [img_version]: https://img.shields.io/static/v1.svg?label=PyCalVer&message=v202010.1040-beta&color=blue - 63: [url_version]: https://pypi.org/org/package/ - -Found 1 match for pattern 'img.shields.io/static/v1.svg?label=PyCalVer&message=vYYYY0M.BUILD[-RELEASE]&color=blue' in README.md -``` - - - -This probably won't cover all version numbers present in your project, so you will have to manually add entries to `pycalver:file_patterns`. To determine what to add, you can use `pycalver grep` : - -``` -$ pycalver grep 'Copyright (c) 2018-YYYY' src/project/*.py -``` - - - -Something like the following may illustrate additional changes you might need to make. - -```ini -[pycalver:file_patterns] -setup.cfg = - current_version = {version} -setup.py = - version="{pep440_version}" -src/mymodule_v*/__init__.py = - __version__ = "{version}" -README.md = - [CalVer {version}] - img.shields.io/static/v1.svg?label=CalVer&message={version}&color=blue -``` - -To see if a pattern is found, you can use `pycalver bump --dry`, which will -leave your project files untouched and only show you a diff of the changes -it would have made. +Most obviously you will see such cases when you first attempt to use `bumpver update`: ```shell -$ pycalver bump --dry --no-fetch -INFO - Old Version: v201901.1001-beta -INFO - New Version: v201902.1002-beta +$ bumpver update --dry --no-fetch +INFO - Old Version: 2020.1001-alpha +INFO - New Version: 2020.1002-alpha +ERROR - No match for pattern 'version="YYYY.BUILD[PYTAGNUM]",' +ERROR - +# https://regex101.com/?flavor=python&flags=gmx®ex=version%3D%5C%22%0A%28%3FP%3Cyear_y%3E%5B1-9%5D%5B0-9%5D%7B3%7D%29%0A%5C.%0A%28%3FP%3Cbid%3E%5B1-9%5D%5B0-9%5D%2A%29%0A%28%3F%3A%0A%20%20%20%20%28%3FP%3Cpytag%3Epost%7Crc%7Ca%7Cb%29%0A%20%20%20%20%28%3FP%3Cnum%3E%5B0-9%5D%2B%29%0A%29%3F%0A%5C%22%2C +regex = re.compile(r""" + version=\" + (?P[1-9][0-9]{3}) + \. + (?P[1-9][0-9]*) + (?: + (?Ppost|rc|a|b) + (?P[0-9]+) + )? + \", +""", flags=re.VERBOSE) +ERROR - No patterns matched for file 'setup.py' +``` + +The internally used regular expression is also shown, which you can use to debug the issue, for example on [regex101.com](https://regex101.com/r/ajQDTz/2). + +To debug such issues, you can simplify your pattern and see if you can find a match with `bumpver grep` . + +```shell +$ bumpver grep 'YYYY.BUILD[PYTAGNUM]' setup.py + 45: name='myproject', + 46: version='2019.1001b0', + 47: license='MIT', + +``` + +Here we can see that the pattern for setup.py should be changed to used single quotes instead of doublequotes. + +As with `bumpver update`, if your pattern is not found, `bumpver grep` will show an error message with the regular expression it uses, to help you debug the issue. + +```shell +$ bumpver grep 'YYYY.BUILD[PYTAGNUM]' setup.py +ERROR - Pattern not found: 'YYYY.BUILD[PYTAGNUM]' +# https://regex101.com/... +``` + +An example of a more complex pattern is one where you want to keep a version badge in your README up to date. + +```shell +$ bumpver grep 'shields.io/badge/CalVer-YYYY.BUILD[--TAG]-blue' README.md + 61: + 62: [img_version]: https://img.shields.io/badge/CalVer-2020.1001--beta-blue + 63: [url_version]: https://pypi.org/org/package/ +``` + + +## Bump It Up + + +### Version State + +The `current_version` is considered global state and must be stored somewhere. Typically this might be in a `VERSION` file, or some other file which is part of the repository. This creates the risk that parallel branches can have different states. If the `current_version` were defined only by files in the local checkout, the same version might be generated on different systems for different commits. + +To avoid this issue, `bumpver` treats Git/Mercurial tags as the canonical / [SSOT][url_ssot] for the most recent version and attempts to change this state in the most atomic way possible. This is why some actions of the `bumpver` command can take a few seconds, as it is synchronizing with the remote repository to get the most recent versions and to push any new version tags as soon as possible. + +[url_ssot]: https://en.wikipedia.org/wiki/Single_source_of_truth + + +### The Current Version + +The current version is either + + - Typically: The largest Git/Mercurial tag which matches the `version_pattern` from your config, sorted using [`pkg_resources.parse_version`][url_setuptools_pkg_resources]. + - Rarely: Before any tags have been created, the value of `current_version` in `bumpver.toml` / `setup.cfg` / `pyproject.toml`. + +[url_setuptools_pkg_resources]: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#parsing-utilities + +As part of doing `bumpver update` and `bumpver show`, your local tags are updated using `git fetch --tags`/`hg pull`. + +```shell +$ bumpver show -vv +2020-10-18T20:20:58.062 DEBUG bumpver.cli - Logging configured. +2020-10-18T20:20:58.065 DEBUG bumpver.config - Config Parsed: Config( + ... +2020-10-18T20:20:58.067 DEBUG bumpver.vcs - vcs found: git +2020-10-18T20:20:58.067 INFO bumpver.vcs - fetching tags from remote (to turn off use: -n / --no-fetch) +2020-10-18T20:20:58.068 DEBUG bumpver.vcs - git fetch +2020-10-18T20:21:00.886 DEBUG bumpver.vcs - git tag --list +2020-10-18T20:21:00.890 INFO bumpver.cli - Latest version from git tag: 2020.1019 +Current Version: 2020.1019 +``` + +Here we see that: + +- Git had a newer version than we had locally (`2020.1019` vs `2020.1018`). +- It took 2 seconds to fetch the tags from the remote repository. + +The approach of fetching tags before the version is bumped/incremented, helps to reduce the risk that the newest tag is not known locally. This means that it less likely for the same version to be generated by different systems for different commits. This would result in an ambiguous version tag, which may not be the end of the world, but is better to avoid. Typically this might happen if you have a build system where multiple builds are triggered at the same time. + +For a small project (with only one maintainer and no automated packaging) this is a non-issue and you can always use `-n/--no-fetch` to skip fetching the tags. + + +### Dry Mode + +Once you have a valid configuration, you can use `bumpver update --dry` to see the changes it would make (and leave your project files untouched). + +```diff +$ bumpver update --dry --no-fetch +INFO - Old Version: 2019.1001-beta +INFO - New Version: 2019.1002-beta --- README.md +++ README.md @@ -11,7 +11,7 @@ @@ -780,671 +915,50 @@ INFO - New Version: v201902.1002-beta license="MIT", ``` -If there is no match for a pattern, bump will report an error. -```shell -# TODO (mb 2020-08-29): update regex pattern -$ pycalver bump --dry --no-fetch -INFO - Old Version: v201901.1001-beta -INFO - New Version: v201902.1002-beta -ERROR - No match for pattern 'img.shields.io/static/v1.svg?label=CalVer&message={version}&color=blue' -ERROR - Pattern compiles to regex 'img\.shields\.io/static/v1\.svg\?label=CalVer&message=(?P\d{4})(?P(?:0[0-9]|1[0-2]))\.(?P\d{4,})(?:-(?P -(?:alpha|beta|dev|rc|post|final)))?)&color=blue' -``` +### VCS Parameters (git/mercurial) -The internally used regular expression is also shown, which you can use to debug the issue, for example on [regex101.com](https://regex101.com/r/ajQDTz/2). +The individual steps performed by `bumpver update`: -TODO Update above link -### Legacy Patterns +0. Check that you have no local changes that are uncommitted. +1. *Fetch* the most recent global VCS tags from origin. +2. Generate the updated version string. +3. Replace version strings in all files configured in `file_patterns`. +4. *Commit* the updated files. +5. *Tag* the new commit. +6. *Push* the new commit and tag. -> These patterns use curly braces `{}` and were the initial implementation. They are still supported and still follow their original semantics. +The configuration for these steps can be done with the following parameters: -The `pycalver:file_patterns` section of the configuration uses a different set -of placeholders and does not use curly braces to mark placeholders. It is still -supported, but we don't recomend you use it. +| Parameter | Type | Description | +|------------------|----------|-----------------------------------------| +| `commit_message` | string¹ | Template for commit message in step 4. | +| `commit` | boolean | Create a commit with all updated files. | +| `tag` | boolean² | Tag the newly created commit. | +| `push` | boolean² | Push to the default remote. | -Available placeholders are: +- ¹ Available placeholders for the `commit_message`: `{new_version}`, `{old_version}`, `{new_version_pep440}`, `{old_version_pep440}` +- ² Requires `commit = True` - -| placeholder | range / example(s) | comment | -|---------------------|---------------------|-----------------| -| `{year}` | 2019... | `%Y` | -| `{yy}` | 18, 19..99, 01, 02 | `%y` | -| `{quarter}` | 1, 2, 3, 4 | | -| `{month}` | 09, 10, 11, 12 | `%m` | -| `{iso_week}` | 00..53 | `%W` | -| `{us_week}` | 00..53 | `%U` | -| `{dom}` | 01..31 | `%d` | -| `{doy}` | 001..366 | `%j` | -| `{build}` | .1023 | lexical id | -| `{build_no}` | 1023, 20345 | ... | -| `{release}` | -alpha, -beta, -rc | --release= | -| `{release_tag}` | alpha, beta, rc | ... | - - - -| placeholder | range / example(s) | comment | -|---------------------|---------------------|-----------------| -| `{pycalver}` | v201902.1001-beta | | -| `{pep440_pycalver}` | 201902.1b0 | | -| `{semver}` | 1.2.3 | | - - -### Pattern Usage - - - -There are some limitations to keep in mind: - - 1. A version string cannot span multiple lines. - 2. Characters generated by a placeholder cannot be escaped. - 3. The timezone is always UTC. - -The lack of escaping may for example be an issue with badge URLs. -You may want to put the following text in your README.md (note -that shields.io parses the two "-" dashes before `beta` as one -literal "-"): - -``` -https://img.shields.io/badge/myproject-v202010.1116--beta-blue.svg -``` - -While you could use the following pattern, which will work fine for a -while: +An example configuration might look like this: ```ini -README.md = - /badge/myproject-{vYYYY0M.BUILD[--RELEASE]}-blue.svg -``` - -Eventually this will break, when you do a `final` release, at -which point the following will be put in your README.md: - -``` -https://img.shields.io/badge/myproject-v202010.1117--final-blue.svg -``` - -When what you probably wanted was this (with the `--final` tag omitted): - -``` -https://img.shields.io/badge/myproject-v202010.1117-blue.svg -``` - - - - -### Version State - -The "current version" is considered global state that needs to be -stored somewhere. Typically this might be stored in a `VERSION` -file, or some other file which is part of the repository. This -creates the risk that parallel branches can have different -states. If the "current version" were defined only by files in -the local checkout, the same version might be generated for -different commits. - -To avoid this issue, pycalver treats VCS tags as the canonical / -[SSOT][url_ssot] for the most recent version and attempts to -change this state in the most atomic way possible. This is why -some actions of the `pycalver` command can take a while, as it is -synchronizing with the remote repository to get the most recent -versions and to push any new version tags as soon as possible. - -[url_ssot]: https://en.wikipedia.org/wiki/Single_source_of_truth - - - -### The Current Version - -The current version that will be bumped is defined either as - - - Typically: The lexically largest git/mercurial tag which matches the - `version_pattern` from your config. - - Initially: Before any tags have been created (or you're not using a - supported VCS), the value of `pycalver.current_version` in `setup.cfg` / - `pyproject.toml` / `pycalver.toml`. - -As part of doing `pycalver bump` and `pycalver show`, your local VCS -index is updated using `git fetch --tags`/`hg pull`. - -```shell -$ time pycalver show --verbose -INFO - fetching tags from remote (to turn off use: -n / --no-fetch) -INFO - Working dir version : v202010.1018 -INFO - Latest version from git tag: v202010.1019-beta -Current Version: v202010.1019-beta -PEP440 : 202010.1019b0 - -real 0m4,254s - -$ time pycalver show --verbose --no-fetch +[bumpver] ... -real 0m0,840s -``` - -Here we see that: - -- The VCS had a newer version than we had locally. -- It took 4 seconds to fetch the tags from the remote repository. - -This approach reduces the risk that new tags are unknown locally and makes it -less likely that the same version string is generated for different commits, -which would result in an ambiguous version tag. This can happen if multiple -maintainers produce a release at the same time or if a build system is triggered -multiple times and multiple builds run concurrently to each other. - -For a small project (with only one maintainer and no build system) this is a -non-issue and you can always use `-n/--no-fetch` to skip updating the tags. - - -### Bump It Up - -To increment the current version and publish a new version, you can use the -`pycalver bump` sub-command. `bump` is configured in the `pycalver` config -section: - -```ini -[pycalver] -current_version = "v202010.1006-beta" -version_pattern = "vYYYY0M.BUILD[-RELEASE]" commit_message = "bump version to {new_version}" commit = True tag = True push = True ``` -This configuration is appropriate to create a commit which +If everything looks OK, you can do `bumpver update`. -1. contains the changes to the version strings, -2. contains no other changes (unrelated to bumping the version), -3. is tagged with the new version, -4. has a version tag that is unique in the repository. - -In order to make sure only changes to version strings are in the commit, -you need to make sure you have a clean VCS checkout when you invoke -`pycalver bump`. - -The steps performed by `bump` are: - -0. Check that your repo doesn't have any local changes. -1. *Fetch* the most recent global VCS tags from origin - (`-n`/`--no-fetch` to disable). -2. Generate a new version, incremented from the current version. -3. Update version strings in all files configured in `file_patterns`. -4. *Commit* the updated version strings. -5. *Tag* the new commit. -6. *Push* the new commit and tag. - -Again, you can use `-d/--dry` to inspect the changes first. - -``` -$ pycalver bump --dry ---- setup.cfg -+++ setup.cfg -@@ -65,7 +65,7 @@ - - [pycalver] --current_version = v202010.1005-beta -+current_version = v202010.1006-beta - version_pattern = "vYYYY0M.BUILD[-RELEASE]" - commit_message = "bump version to {new_version}" - commit = True -... -``` - -If everything looks OK, you can do `pycalver bump`. - -``` -$ pycalver bump --verbose +```shell +$ bumpver update --verbose INFO - fetching tags from remote (to turn off use: -n / --no-fetch) -INFO - Old Version: v202010.1005-beta -INFO - New Version: v202010.1006-beta -INFO - git commit --message 'bump version to v202010.1006-beta' -INFO - git tag --annotate v202010.1006-beta --message v202010.1006-beta -INFO - git push origin v202010.1006-beta +INFO - Old Version: 2020.1005 +INFO - New Version: 2020.1006 +INFO - git commit --message 'bump version to 2020.1006' +INFO - git tag --annotate 2020.1006 --message 2020.1006 +INFO - git push origin --follow-tags 2020.1006 HEAD ``` - -### Config Parameters - - - - -| Config Parameter | Type | Description | -| ----------------- | -------- | ---------------------------- | -| `current_version` | string | | -| `version_pattern` | string | | -| `commit_message` | string | Template for commit message¹ | -| `commit` | boolean | | -| `tag` | boolean² | | -| `push` | boolean² | | - -- ¹ Available placeholders: `{new_version}`, `{old_version}`, `{new_version_pep440}`, `{old_version_pep440}` -- ² Requires `commit = True` - - -## The PyCalVer Format - -The PyCalVer format for version strings has three parts: - -``` - - o Year of Release - | o Sequential Build Number - | | o Release Tag (optional) - | | | - --+-- --+-- --+-- - v2020 .1001 -beta - -``` - -Some examples: - -``` -2017.1001-alpha -2017.1027-beta -2018.1031 -2018.1032-post -... -2022.28133 -2022.28134 -``` - -This format was chosen in part to be distinctive from -others, so that users of your package can see at a glance that your project -will strive to maintain the one semantic that really matters: **newer is -better**. - -To convince you of the merits of not breaking things, here are some -resources which PyCalVer was inspired by: - - - ["Speculation" talk by Rich - Hicky](https://www.youtube.com/watch?v=oyLBGkS5ICk) - - [Designing a Version by Mahmoud - Hashemi](http://sedimental.org/designing_a_version.html) - - [calver.org](https://calver.org/) - - ["The cargo cult of versioning" by Kartik - Agaram](http://akkartik.name/post/versioning) - - The [bumpversion][bumpversion_ref] project, upon which - PyCalVer is partially based. - - ["Our Software Dependency Problem" by Russ Cox](https://research.swtch.com/deps) - - -### Parsing - -These version strings can be parsed with the following regular expression: - -```python -import re - -# https://regex101.com/r/fnj60p/14 -PYCALVER_PATTERN = r""" -\b -(?P - (?P - v # "v" version prefix - (?P\d{4}) - ) - (?P - \. # "." build nr prefix - (?P\d{4,}) - ) - (?P - \- # "-" release prefix - (?Palpha|beta|dev|rc|post) - )? -)(?:\s|$) -""" -PYCALVER_REGEX = re.compile(PYCALVER_PATTERN, flags=re.VERBOSE) - -version_str = "v2017.1001-alpha" -version_match = PYCALVER_REGEX.match(version_str) - -assert version_match.groupdict() == { - "pycalver" : "v2017.1001-alpha", - "vYYYY0M" : "v2017", - "year" : "2017", - "build" : ".1001", - "build_no" : "1001", - "release" : "-alpha", - "release_tag": "alpha", -} - -version_str = "v201712.1033" -version_match = PYCALVER_REGEX.match(version_str) - -assert version_match.groupdict() == { - "pycalver" : "v2017.1033", - "vYYYY" : "v2017", - "year" : "2017", - "build" : ".1033", - "build_no" : "1033", - "release" : None, - "release_tag": None, -} -``` - -### Incrementing Behaviour - -To see how version strings are incremented, we can use -`calver test`: - -```shell -$ calver test v2018.1033-beta -New Version: v2019.1034-beta -PEP440 : 2019.1034b0 -``` - -This is the simple case: - - - The calendar component is updated to the current year and month. - - The build number is incremented by 1. - - The optional release tag is preserved as is. - -You can explicitly update the release tag by using the `--release=` argument: - -```shell -$ calver test v2018.1033-alpha --release=beta -New Version: v2019.1034-beta -PEP440 : 2019.1034b0 - -$ calver test v2019.1034-beta --release=final -New Version: v2019.1035 -PEP440 : 2019.1035 -``` - -To maintain lexical ordering of version numbers, the version number is padded with extra zeros using [Lexical Ids][url_pypi_lexid]. -This means that the expression `older_id < newer_id` will always be true, whether you are dealing with integers or strings. - -[url_pypi_lexid]: https://pypi.org/project/lexid/ - - - -## Semantics of PyCalVer - -> Disclaimer: This section is of course only aspirational. Nothing will -> stop a package maintainer from publishing updates that violate the -> semantics presented here. - - -### Pitch - -- dates are good information - - how old is the software - - is the software maintained - - is my dependency outdated - - can I trust an update? - - -### blah - -PyCalVer places a greater burden on package maintainers than SemVer. -Backward incompatibility is not encoded in the version string, because -**maintainers should not intentionally introduce breaking changes**. This -is great for users of a package, who can worry a bit less about an update -causing their project to break. A paranoid user can of course still pin to -a known good version, and freezing dependencies for deployments is still a -good practice, but for development, users ideally shouldn't need any -version specifiers in their requirements.txt. This way they always get the -newest bug fixes and features. - -Part of the reason for the distinctive PyCalVer version string, is for -users to be able to recognize, just from looking at the version string, -that a package comes with the promise (or at least aspiration) that it -won't break, that it is safe for users to update. Compare this to a SemVer -version string, where maintainers explicitly state that an update _might_ -break their program and that they _may_ have to do extra work after -updating and even if it hasn't in the past, the package maintainers -anticipate that they might make such breaking changes in the future. - -In other words, the onus is on the user of a package to update their -software, if they want to update to the latest version of a package. With -PyCalVer the onus is on package maintainer to maintain backward -compatibility. - -Ideally users can trust the promise of a maintainer that the following -semantics will always be true: - - - Newer is compatible. - - Newer has fewer bugs. - - Newer has more features. - - Newer has equal or better performance. - -Alas, the world is not ideal. So how do users and maintainers deal with changes -that violate these promises? - - -### Intentional Breaking Changes - -> Namespaces are a honking great idea -> - let's do more of those! -> -> - The Zen of Python - -If you must make a breaking change to a package, **instead of incrementing a -number**, the recommended approach with PyCalVer is to **create a whole new -namespace**. Put differently, the major version becomes part of the name of the -module or even of the package. Typically you might add a numerical suffix, eg. -`mypkg -> mypkg2`. - -In the case of python distributions, you can include multiple module -packages like this. - -```python -# setup.py -setuptools.setup( - name="my-package", - license="MIT", - packages=["mypkg", "mypkg2"], - package_dir={"": "src"}, - ... -) -``` - -In other words, you can ship older versions side by side with newer ones, -and users can import whichever one they need. Alternatively you can publish -a new package distribution, with new namespace, but please consider also -renaming the module. - -```python -# setup.py -setuptools.setup( - name="my-package-v2", - license="MIT", - packages=["mypkg2"], - package_dir={"": "src"}, - ... -) -``` - -Users will have an easier time working with your package if `import mypkg2` -is enough to determine which version of your project they are using. A further -benefit of creating multiple modules is that users can import both old and -new modules in the same environment and can use some packages which depend -on the old version as well as some that depend on the new version. The -downside for users, is that they may have to do minimal changes to their -code, even if the breaking change did not affect them. - -```diff -- import mypkg -+ import mypkg2 - - def usage_code(): -- mypkg.myfun() -+ mypkg2.myfun() -``` - - -### Costs and Benefits - -If this seems like overkill because it's a lot of work for you as a -maintainer, consider first investing some time in your tools, so you -minimize future work required to create new packages. I've [done this for -my personal projects][bootstrapit_ref], but you may find [other -approaches][cookiecutter_ref] to be more appropriate for your use. - -If this seems like overkill because you're not convinced that imposing a -very small burden on users is such a big deal, consider that your own -projects may indirectly depend on dozens of libraries which you've never -even heard of. If every maintainer introduced breaking changes only once -per year, users who depend on only a dozen libraries would be dealing with -packaging issues every month! In other words: *Breaking things is a big -deal*. A bit of extra effort for a few maintainers seems like a fair trade -to lower the effort imposed on many users, who would be perfectly happy to -continue using the old code until _they_ decide when to upgrade. - - -### Unintentional Breaking Changes - -The other kind of breaking change is the non-intentional kind, otherwise -known as a "bug" or "regression". Realize first of all, that it is -impossible for any versioning system to encode that this has happened: -Since the maintainer isn't knowingly introducing a bug they naturally can't -update their version numbers to reflect what they don't know about. Instead -we have to deal with these issues after the fact. - -The first thing a package maintainer can do is to minimize the chance of -inflicting buggy software on users. After any non-trivial (potentially breaking) -change, it is a good practice to first create an `-alpha`/`-beta`/`-rc` release. -These so called `--pre` releases are intended to be downloaded only by the few -and the brave: Those who are willing to participate in testing. After any issues -are ironed out with the `--pre` releases, a `final` release can be made for the -wider public. - -Note that the default behaviour of `pip install ` (without any version -specifier) is to download the latest `final` release. It will download a `--pre` -release *only* if - - 1. no `final` release is available - 2. the `--pre` flag is explicitly used, or - 3. if the requirement specifier _explicitly_ includes the version number of a - pre release, eg. `pip install mypkg==v202009.1007-alpha`. - -Should a release include a bug (heaven forbid and despite all precautions), -then the maintainer should publish a new release which either fixes the bug -or reverts the change. If users previously downloaded a version of the -package which included the bug, they only have to do `pip install --upgrade -` and the issue will be resolved. - -Perhaps a timeline will illustrate more clearly: - -``` -v2020.1665 # last stable release -v2020.1666-beta # pre release for testers -v2019.1667 # final release after testing - -# bug is discovered which effects v2020.1666-beta and v2019.1667 - -v2019.1668-beta # fix is issued for testers -v2019.1669 # fix is issued everybody - -# Alternatively, revert before fixing - -v2019.1668 # same as v2020.1665 -v2019.1669-beta # reintroduce change from v2020.1666-beta + fix -v2019.1670 # final release after testing -``` - -In the absolute worst case, a change is discovered to break backward -compatibility, but the change is nonetheless considered to be desirable. At that -point, a new release should be made to revert the change. - -This allows 1. users who _were_ exposed to the breaking change to update to the -latest release and get the old (working) code again, and 2. users who _were not_ -exposed to the breaking change to never even know anything was broken. - -Remember that the goal is to always make things easy for users who have -your package as a dependency. If there is any issue whatsoever, all they -should have to do is `pip install --update`. If this doesn't work, they may -have to *temporarily* pin to a known good version, until a fixed release -has been published. - -After this immediate fire has been extinguished, if the breaking change is -worth keeping, then **create a new module or even a new package**. This -package will perhaps have 99% overlap to the previous one and the old one -may eventually be abandoned. - -``` -mypkg v2020.1665 # last stable release -mypkg v2020.1666-rc # pre release for testers -mypkg v2019.1667 # final release after testing period - -# bug is discovered in v2020.1666-beta and v2019.1667 - -mypkg v2019.1668 # same as v2020.1665 - -# new package is created with compatibility breaking code - -mypkg2 v2019.1669 # same as v2019.1667 -mypkg v2019.1669 # updated readme, declaring support - # level for mypkg, pointing to mypgk2 - # and documenting how to upgrade. -``` - - -### Pinning is not a Panacea - -Freezing your dependencies by using `pip freeze` to create a file with packages -pinned to specific version numbers is great to get a stable and repeatable -deployment. - -The main problem with pinning is that it is another burden imposed on users, -and it is a burden which in practice only some can bear. The vast majority of -users either 1) pin their dependencies and update them without determining what -changed or if it is safe for them to update, or 2) pin their dependencies and -forget about them. In case 1 the only benefit is that users might at least be -aware of when an update happened, so they can perhaps correlate that a new bug -in their software might be related to a recent update. Other than that, keeping -tabs on dependencies and updating without diligence is hardly better than not -having pinned at all. In case 2, an insurmountable debt will pile up and the -dependencies of a project are essentially frozen in the past. - -Yes, it is true that users will be better off if they have sufficient test -coverage to determine for themselves that their code is not broken even after -their dependencies are updated. It is also true however, that a package -maintainer is usually in a better position to judge if a change might cause -something to break. - - -### Zeno's 1.0 and The Eternal Beta - -There are two opposite approaches to backward compatibility which find a -reflection in the version numbers they use. In the case of SemVer, if a -project has a commitment to backward compatibility, it may end up never -incriminating the major version, leading to the [Zeno 1.0 -paradox][zeno_1_dot_0_ref]. On the other end are projects that avoid any -commitment to backward compatibility and forever keep the "beta" label. - -Of course an unpaid Open Source developer *does not owe anybody a -commitment to backward compatibility*. Especially when a project is young -and going through major changes, such a commitment may not make any sense. -For these cases you can still use PyCalVer, just so long as there is a big -fat warning at the top of your README, that your project is not ready for -production yet. - -Note that there is a difference between software that is considered to be -in a "beta" state and individual releases which have a `-beta` tag. These -do not mean the same thing. In the case of releases of python packages, the -release tag (`-alpha`, `-beta`, `-rc`) says something about the stability -of a *particular release*. This is similar ([perhaps -identical][pep_101_ref]) to the meaning of release tags used by the CPython -interpreter. A release tag is not a statement about the general stability -of the software as a whole, it is metadata about a particular release -artifact of a package, eg. a `.whl` file. - - -[setuptools_ref]: https://setuptools.readthedocs.io/en/latest/setuptools.html#specifying-your-project-s-version - -[pep_440_ref]: https://www.python.org/dev/peps/pep-0440/ - -[pep_440_normalzation_ref]: https://www.python.org/dev/peps/pep-0440/#id31 - -[zeno_1_dot_0_ref]: http://sedimental.org/designing_a_version.html#semver-and-release-blockage - -[pep_101_ref]: https://www.python.org/dev/peps/pep-0101/ - -[bumpversion_ref]: https://github.com/peritus/bumpversion - -[bootstrapit_ref]: https://gitlab.com/mbarkhau/bootstrapit - -[cookiecutter_ref]: https://cookiecutter.readthedocs.io - diff --git a/bootstrapit.sh b/bootstrapit.sh index 4a03da4..cde4d52 100644 --- a/bootstrapit.sh +++ b/bootstrapit.sh @@ -4,19 +4,19 @@ AUTHOR_NAME="Manuel Barkhau" AUTHOR_EMAIL="mbarkhau@gmail.com" -KEYWORDS="version versioning calver semver bumpversion pep440" -DESCRIPTION="CalVer for python packages." +KEYWORDS="version bumpver calver semver versioning bumpversion pep440" +DESCRIPTION="Bump version numbers in project files." LICENSE_ID="MIT" -PACKAGE_NAME="pycalver" +PACKAGE_NAME="bumpver" GIT_REPO_NAMESPACE="mbarkhau" GIT_REPO_DOMAIN="github.com" -PACKAGE_VERSION="v2020.1041-beta" +PACKAGE_VERSION="2020.1041-beta" DEFAULT_PYTHON_VERSION="python=3.8" -SUPPORTED_PYTHON_VERSIONS="python=2.7 python=3.6 python=3.8 pypy2.7 pypy3.5" +SUPPORTED_PYTHON_VERSIONS="python=2.7 python=3.6 pypy2.7 pypy3.5 python=3.8" DOCKER_REGISTRY_DOMAIN=registry.gitlab.com diff --git a/scripts/update_readme_examples.py b/scripts/update_readme_examples.py index 2c60bdf..8eba9e3 100644 --- a/scripts/update_readme_examples.py +++ b/scripts/update_readme_examples.py @@ -11,7 +11,7 @@ import rich import rich.box import rich.table -from pycalver import v2version +from bumpcalver import v2version def update(content, marker, value): @@ -73,10 +73,10 @@ def pattern_examples(): ("MAJOR.MINOR.PATCH[PYTAGNUM]" , ""), ("MAJOR.MINOR[.PATCH[PYTAGNUM]]", ""), ("YYYY.BUILD[PYTAGNUM]" , ""), - ("YYYY.BUILD[-RELEASE]" , ""), + ("YYYY.BUILD[-TAG]" , ""), ("YYYY.INC0[PYTAGNUM]" , ""), - ("YYYY0M.PATCH[-RELEASE]" , "¹"), - ("YYYY0M.BUILD[-RELEASE]" , ""), + ("YYYY0M.PATCH[-TAG]" , "¹"), + ("YYYY0M.BUILD[-TAG]" , ""), ("YYYY.0M" , ""), ("YYYY.MM" , ""), ("YYYY.WW" , ""), @@ -184,8 +184,8 @@ def pattern_examples(): old_content = io.open("README.md").read() new_content = old_content -new_content = update_md_code_output(new_content, "calver --help") -new_content = update_md_code_output(new_content, "calver bump --help") +new_content = update_md_code_output(new_content, "bumpver --help") +new_content = update_md_code_output(new_content, "bumpver update --help") new_content = update(new_content, "pattern_examples", pattern_examples()) new_content = update(new_content, "weeknum_example" , weeknum_example()) @@ -197,20 +197,3 @@ elif "--dry" in sys.argv: else: with io.open("README.md", mode="w") as fobj: fobj.write(new_content) - - -# @printf '\n```\n$$ calver --help\n' > /tmp/pycalver_help.txt -# @$(DEV_ENV)/bin/calver --help >> /tmp/pycalver_help.txt -# @printf '```\n\n' >> /tmp/pycalver_help.txt - -# sed -i -ne '// {p; r /tmp/pycalver_help.txt' \ -# -e ':a; n; // {p; b}; ba}; p' \ -# README.md - -# @printf '\n```\n$$ pycalver bump --help\n' > /tmp/pycalver_help.txt -# @$(DEV_ENV)/bin/pycalver bump --help >> /tmp/pycalver_help.txt -# @printf '```\n\n' >> /tmp/pycalver_help.txt - -# sed -i -ne '// {p; r /tmp/pycalver_help.txt' \ -# -e ':a; n; // {p; b}; ba}; p' \ -# README.md diff --git a/setup.cfg b/setup.cfg index 93087c6..7e4a785 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,7 +19,7 @@ warn_redundant_casts = True [tool:isort] -known_first_party = pycalver2 +known_first_party = bumpver known_third_party = click,pathlib2,lexid,pkg_resources force_single_line = True length_sort = True @@ -88,26 +88,26 @@ exclude = addopts = --doctest-modules -[calver] -current_version = "v2020.1041-beta" -version_pattern = "vYYYY.BUILD[-TAG]" +[bumpver] +current_version = "2020.1041-beta" +version_pattern = "YYYY.BUILD[-TAG]" commit_message = "bump {old_version} -> {new_version}" commit = True tag = True push = True -[calver:file_patterns] +[bumpver:file_patterns] bootstrapit.sh = PACKAGE_VERSION="{version}" setup.cfg = current_version = "{version}" setup.py = - version="{pep440_version}" -src/pycalver2/__init__.py = + version="{pep440_version}", +src/bumpver/__init__.py = __version__ = "{version}" -src/pycalver2/cli.py = +src/bumpver/cli.py = @click.version_option(version="{version}") -src/pycalver2*/*.py = +src/bumpver/*.py = Copyright (c) 2018-YYYY LICENSE = Copyright (c) 2018-YYYY @@ -116,7 +116,7 @@ license.header = README.md = \[CalVer {version}\] img.shields.io/static/v1.svg?label=CalVer&message={version}&color=blue - Successfully installed python-calver-{pep440_version} + Successfully installed bumpver-{pep440_version} [tool:pylint] diff --git a/setup.py b/setup.py index f73329d..0c35a5d 100644 --- a/setup.py +++ b/setup.py @@ -58,14 +58,14 @@ if any(arg.startswith("bdist") for arg in sys.argv): setuptools.setup( - name="python-calver", + name="bumpver", license="MIT", author="Manuel Barkhau", author_email="mbarkhau@gmail.com", - url="https://github.com/mbarkhau/pycalver", + url="https://github.com/mbarkhau/bumpver", version="2020.1041b0", - keywords="version versioning calver semver bumpversion pep440", - description="CalVer for python libraries.", + keywords="version bumpver calver semver versioning bumpversion pep440", + description="Bump version numbers in project files.", long_description=long_description, long_description_content_type="text/markdown", packages=setuptools.find_packages("src/"), @@ -73,7 +73,7 @@ setuptools.setup( install_requires=install_requires, entry_points=""" [console_scripts] - calver=pycalver2.cli:cli + bumpver=bumpver.cli:cli """, python_requires=">=2.7", zip_safe=True, diff --git a/src/pycalver2/__init__.py b/src/bumpver/__init__.py similarity index 71% rename from src/pycalver2/__init__.py rename to src/bumpver/__init__.py index 938d3fe..c6ce541 100644 --- a/src/pycalver2/__init__.py +++ b/src/bumpver/__init__.py @@ -3,6 +3,6 @@ # # Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License # SPDX-License-Identifier: MIT -"""PyCalVer: CalVer for Python Packages.""" +"""BumpVer: A CLI program for versioning.""" -__version__ = "v2020.1041-beta" +__version__ = "2020.1041-beta" diff --git a/src/pycalver2/__main__.py b/src/bumpver/__main__.py similarity index 77% rename from src/pycalver2/__main__.py rename to src/bumpver/__main__.py index f3cf730..588684c 100644 --- a/src/pycalver2/__main__.py +++ b/src/bumpver/__main__.py @@ -5,9 +5,9 @@ # Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License # SPDX-License-Identifier: MIT """ -__main__ module for PyCalVer. +__main__ module for BumpVer. -Enables use as module: $ python -m pycalver --version +Enables use as module: $ python -m bumpver --version """ from . import cli diff --git a/src/pycalver2/cli.py b/src/bumpver/cli.py similarity index 96% rename from src/pycalver2/cli.py rename to src/bumpver/cli.py index d1d6f1c..357e2b4 100755 --- a/src/pycalver2/cli.py +++ b/src/bumpver/cli.py @@ -4,7 +4,7 @@ # # Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License # SPDX-License-Identifier: MIT -"""cli module for PyCalVer.""" +"""cli module for BumpVer.""" import io import sys import typing as typ @@ -39,7 +39,7 @@ except ImportError: click.disable_unicode_literals_warning = True -logger = logging.getLogger("pycalver2.cli") +logger = logging.getLogger("bumpver.cli") _VERBOSE = 0 @@ -140,7 +140,7 @@ def _log_no_change(subcmd: str, version_pattern: str, old_version: str) -> None: ] if available_flags: available_flags_str = "/".join(available_flags) - logger.info(f"Perhaps try: calver {subcmd} {available_flags_str} ") + logger.info(f"Perhaps try: bumpver {subcmd} {available_flags_str} ") def _get_normalized_pattern(raw_pattern: str, version_pattern: typ.Optional[str]) -> str: @@ -164,7 +164,7 @@ def _get_normalized_pattern(raw_pattern: str, version_pattern: typ.Optional[str] @click.group() -@click.version_option(version="v2020.1041-beta") +@click.version_option(version="2020.1041-beta") @click.help_option() @click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") def cli(verbose: int = 0) -> None: @@ -241,7 +241,8 @@ def test( pep440_version = version.to_pep440(new_version) click.echo(f"New Version: {new_version}") - click.echo(f"PEP440 : {pep440_version}") + if new_version != pep440_version: + click.echo(f"PEP440 : {pep440_version}") def _grep_text(pattern: patterns.Pattern, text: str, color: bool) -> typ.Iterable[str]: @@ -292,7 +293,8 @@ def _grep( match_strs = list(_grep_text(pattern, text, color)) if len(match_strs) > 0: - print(file_io.name) + if len(file_ios) > 1: + print(file_io.name) for match_str in match_strs: print(match_str) print() @@ -365,7 +367,7 @@ def show(verbose: int = 0, fetch: bool = True) -> None: _, cfg = config.init(project_path=".") if cfg is None: - logger.error("Could not parse configuration. Perhaps try 'calver init'.") + logger.error("Could not parse configuration. Perhaps try 'bumpver init'.") sys.exit(1) cfg = _update_cfg_from_vcs(cfg, fetch) @@ -418,6 +420,9 @@ def _print_diff(cfg: config.Config, new_version: str) -> None: try: diff = get_diff(cfg, new_version) _print_diff_str(diff) + except OSError as err: + logger.error(str(err)) + sys.exit(1) except rewrite.NoPatternMatch as ex: logger.error(str(ex)) sys.exit(1) @@ -482,7 +487,7 @@ def incr_dispatch( return new_version -def _bump( +def _update( cfg : config.Config, new_version : str, commit_message: str, @@ -516,14 +521,14 @@ def _bump( vcs.commit(cfg, vcs_api, filepaths, new_version, commit_message) -def _try_bump( +def _try_update( cfg : config.Config, new_version : str, commit_message: str, allow_dirty : bool = False, ) -> None: try: - _bump(cfg, new_version, commit_message, allow_dirty) + _update(cfg, new_version, commit_message, allow_dirty) except sp.CalledProcessError as ex: logger.error(f"Error running subcommand: {ex.cmd}") if ex.stdout: @@ -650,7 +655,7 @@ def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: metavar="", help=f"Set explicit date in format YYYY-0M-0D (e.g. {_current_date}).", ) -def bump( +def update( verbose : int = 0, dry : bool = False, allow_dirty: bool = False, @@ -663,7 +668,7 @@ def bump( pin_date : bool = False, date : typ.Optional[str] = None, ) -> None: - """Increment the current version string and update project files.""" + """Update project files with the incremented version string.""" verbose = max(_VERBOSE, verbose) _configure_logging(verbose) _validate_release_tag(tag) @@ -672,7 +677,7 @@ def bump( _, cfg = config.init(project_path=".") if cfg is None: - logger.error("Could not parse configuration. Perhaps try 'pycalver init'.") + logger.error("Could not parse configuration. Perhaps try 'bumpver init'.") sys.exit(1) cfg = _update_cfg_from_vcs(cfg, fetch) @@ -691,7 +696,7 @@ def bump( ) if new_version is None: - _log_no_change('bump', cfg.version_pattern, old_version) + _log_no_change('update', cfg.version_pattern, old_version) sys.exit(1) logger.info(f"Old Version: {old_version}") @@ -711,7 +716,7 @@ def bump( } commit_message = cfg.commit_message.format(**commit_message_kwargs) - _try_bump(cfg, new_version, commit_message, allow_dirty) + _try_update(cfg, new_version, commit_message, allow_dirty) if __name__ == '__main__': diff --git a/src/pycalver2/config.py b/src/bumpver/config.py similarity index 90% rename from src/pycalver2/config.py rename to src/bumpver/config.py index e4f8587..69ad6c6 100644 --- a/src/pycalver2/config.py +++ b/src/bumpver/config.py @@ -3,7 +3,7 @@ # # Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License # SPDX-License-Identifier: MIT -"""Parse setup.cfg or pycalver.cfg files.""" +"""Parse bumpver.toml, setup.cfg or pyproject.toml files.""" import re import glob @@ -22,7 +22,7 @@ from . import v1patterns from . import v2patterns from .patterns import Pattern -logger = logging.getLogger("pycalver2.config") +logger = logging.getLogger("bumpver.config") RawPatterns = typ.List[str] RawPatternsByFile = typ.Dict[str, RawPatterns] @@ -32,7 +32,7 @@ PatternsByFile = typ.Dict[str, typ.List[Pattern]] FilePatternsItem = typ.Tuple[str, typ.List[Pattern]] -SUPPORTED_CONFIGS = ["setup.cfg", "pyproject.toml", "pycalver.toml", "calver.toml"] +SUPPORTED_CONFIGS = ["setup.cfg", "pyproject.toml", "pycalver.toml", "bumpver.toml"] DEFAULT_COMMIT_MESSAGE = "bump version to {new_version}" @@ -51,8 +51,8 @@ def _parse_config_and_format(path: pl.Path) -> typ.Tuple[pl.Path, str, str]: if (path / "pycalver.toml").exists(): config_filepath = path / "pycalver.toml" config_format = 'toml' - elif (path / "calver.toml").exists(): - config_filepath = path / "calver.toml" + elif (path / "bumpver.toml").exists(): + config_filepath = path / "bumpver.toml" config_format = 'toml' elif (path / "pyproject.toml").exists(): config_filepath = path / "pyproject.toml" @@ -61,8 +61,8 @@ def _parse_config_and_format(path: pl.Path) -> typ.Tuple[pl.Path, str, str]: config_filepath = path / "setup.cfg" config_format = 'cfg' else: - # fallback to creating a new calver.toml - config_filepath = path / "calver.toml" + # fallback to creating a new bumpver.toml + config_filepath = path / "bumpver.toml" config_format = 'toml' if config_filepath.is_absolute(): @@ -150,8 +150,8 @@ def _parse_cfg_file_patterns( if cfg_parser.has_section("pycalver:file_patterns"): file_pattern_items = cfg_parser.items("pycalver:file_patterns") - elif cfg_parser.has_section("calver:file_patterns"): - file_pattern_items = cfg_parser.items("calver:file_patterns") + elif cfg_parser.has_section("bumpver:file_patterns"): + file_pattern_items = cfg_parser.items("bumpver:file_patterns") else: return @@ -191,10 +191,10 @@ def _parse_cfg(cfg_buffer: typ.IO[str]) -> RawConfig: raw_cfg: RawConfig if cfg_parser.has_section("pycalver"): raw_cfg = dict(cfg_parser.items("pycalver")) - elif cfg_parser.has_section("calver"): - raw_cfg = dict(cfg_parser.items("calver")) + elif cfg_parser.has_section("bumpver"): + raw_cfg = dict(cfg_parser.items("bumpver")) else: - raise ValueError("Missing [calver] section.") + raise ValueError("Missing [bumpver] section.") for option, default_val in BOOL_OPTIONS.items(): val: OptionVal = raw_cfg.get(option, default_val) @@ -213,10 +213,10 @@ def _parse_toml(cfg_buffer: typ.IO[str]) -> RawConfig: raw_full_cfg: typ.Any = toml.load(cfg_buffer) raw_cfg : RawConfig - if 'pycalver' in raw_full_cfg: + if 'bumpver' in raw_full_cfg: + raw_cfg = raw_full_cfg['bumpver'] + elif 'pycalver' in raw_full_cfg: raw_cfg = raw_full_cfg['pycalver'] - elif 'calver' in raw_full_cfg: - raw_cfg = raw_full_cfg['calver'] else: raw_cfg = {} @@ -297,10 +297,18 @@ def _validate_version_with_pattern( is_new_pattern : bool, ) -> None: """Provoke ValueError if version_pattern and current_version are not compatible.""" - if is_new_pattern: - v2version.parse_version_info(current_version, version_pattern) - else: - v1version.parse_version_info(current_version, version_pattern) + try: + if is_new_pattern: + v2version.parse_version_info(current_version, version_pattern) + else: + v1version.parse_version_info(current_version, version_pattern) + except version.PatternError: + errmsg = ( + "Invalid configuration. " + f"current_version='{current_version}' is invalid for " + f"version_pattern='{version_pattern}'" + ) + raise ValueError(errmsg) if is_new_pattern: invalid_chars = re.search(r"([\s]+)", version_pattern) @@ -328,6 +336,7 @@ def _parse_config(raw_cfg: RawConfig) -> Config: version_pattern = raw_cfg['version_pattern'] = version_pattern.strip("'\" ") is_new_pattern = "{" not in version_pattern and "}" not in version_pattern + _validate_version_with_pattern(current_version, version_pattern, is_new_pattern) pep440_version = version.to_pep440(current_version) @@ -365,19 +374,19 @@ def _parse_config(raw_cfg: RawConfig) -> Config: def _parse_current_version_default_pattern(raw_cfg: RawConfig, raw_cfg_text: str) -> str: - is_pycalver_section = False + is_config_section = False for line in raw_cfg_text.splitlines(): - if is_pycalver_section and line.startswith("current_version"): + if is_config_section and line.startswith("current_version"): current_version: str = raw_cfg['current_version'] version_pattern: str = raw_cfg['version_pattern'] return line.replace(current_version, version_pattern) if line.strip() == "[pycalver]": - is_pycalver_section = True - elif line.strip() == "[calver]": - is_pycalver_section = True + is_config_section = True + elif line.strip() == "[bumpver]": + is_config_section = True elif line and line[0] == "[" and line[-1] == "]": - is_pycalver_section = False + is_config_section = False raise ValueError("Could not parse 'current_version'") @@ -449,15 +458,15 @@ def init( DEFAULT_CONFIGPARSER_BASE_TMPL = """ -[calver] +[bumpver] current_version = "{initial_version}" -version_pattern = "vYYYY.BUILD[-TAG]" +version_pattern = "YYYY.BUILD[-TAG]" commit_message = "bump version {{old_version}} -> {{new_version}}" commit = True tag = True push = True -[calver:file_patterns] +[bumpver:file_patterns] """.lstrip() @@ -489,15 +498,15 @@ README.md = DEFAULT_TOML_BASE_TMPL = """ -[calver] +[bumpver] current_version = "{initial_version}" -version_pattern = "vYYYY.BUILD[-TAG]" +version_pattern = "YYYY.BUILD[-TAG]" commit_message = "bump version {{old_version}} -> {{new_version}}" commit = true tag = true push = true -[calver.file_patterns] +[bumpver.file_patterns] """.lstrip() @@ -508,8 +517,8 @@ DEFAULT_TOML_PYCALVER_STR = """ """.lstrip() -DEFAULT_TOML_CALVER_STR = """ -"calver.toml" = [ +DEFAULT_TOML_BUMPVER_STR = """ +"bumpver.toml" = [ 'current_version = "{version}"', ] """.lstrip() @@ -547,7 +556,7 @@ DEFAULT_TOML_README_MD_STR = """ def _initial_version() -> str: - return dt.datetime.utcnow().strftime("v%Y.1001-alpha") + return dt.datetime.utcnow().strftime("%Y.1001-alpha") def _initial_version_pep440() -> str: @@ -572,7 +581,7 @@ def default_config(ctx: ProjectContext) -> str: default_pattern_strs_by_filename = { "pyproject.toml": DEFAULT_TOML_PYPROJECT_STR, "pycalver.toml" : DEFAULT_TOML_PYCALVER_STR, - "calver.toml" : DEFAULT_TOML_CALVER_STR, + "bumpver.toml" : DEFAULT_TOML_BUMPVER_STR, "setup.py" : DEFAULT_TOML_SETUP_PY_STR, "README.rst" : DEFAULT_TOML_README_RST_STR, "README.md" : DEFAULT_TOML_README_MD_STR, @@ -592,7 +601,7 @@ def default_config(ctx: ProjectContext) -> str: if ctx.config_format == 'cfg': cfg_str += DEFAULT_CONFIGPARSER_SETUP_CFG_STR if ctx.config_format == 'toml': - cfg_str += DEFAULT_TOML_CALVER_STR + cfg_str += DEFAULT_TOML_BUMPVER_STR cfg_str += "\n" diff --git a/src/pycalver2/parse.py b/src/bumpver/parse.py similarity index 100% rename from src/pycalver2/parse.py rename to src/bumpver/parse.py diff --git a/src/pycalver2/patterns.py b/src/bumpver/patterns.py similarity index 100% rename from src/pycalver2/patterns.py rename to src/bumpver/patterns.py diff --git a/src/pycalver2/pysix.py b/src/bumpver/pysix.py similarity index 100% rename from src/pycalver2/pysix.py rename to src/bumpver/pysix.py diff --git a/src/pycalver2/regexfmt.py b/src/bumpver/regexfmt.py similarity index 97% rename from src/pycalver2/regexfmt.py rename to src/bumpver/regexfmt.py index 7542d5f..8d7a850 100644 --- a/src/pycalver2/regexfmt.py +++ b/src/bumpver/regexfmt.py @@ -9,7 +9,7 @@ import textwrap from . import pysix -logger = logging.getLogger("pycalver2.regexfmt") +logger = logging.getLogger("bumpver.regexfmt") def format_regex(regex: str) -> str: diff --git a/src/pycalver2/rewrite.py b/src/bumpver/rewrite.py similarity index 100% rename from src/pycalver2/rewrite.py rename to src/bumpver/rewrite.py diff --git a/src/pycalver2/utils.py b/src/bumpver/utils.py similarity index 100% rename from src/pycalver2/utils.py rename to src/bumpver/utils.py diff --git a/src/pycalver2/v1patterns.py b/src/bumpver/v1patterns.py similarity index 99% rename from src/pycalver2/v1patterns.py rename to src/bumpver/v1patterns.py index cb5698b..265fffd 100644 --- a/src/pycalver2/v1patterns.py +++ b/src/bumpver/v1patterns.py @@ -38,7 +38,7 @@ from . import utils from .patterns import RE_PATTERN_ESCAPES from .patterns import Pattern -logger = logging.getLogger("pycalver2.v1patterns") +logger = logging.getLogger("bumpver.v1patterns") # https://regex101.com/r/fnj60p/10 PYCALVER_PATTERN = r""" diff --git a/src/pycalver2/v1rewrite.py b/src/bumpver/v1rewrite.py similarity index 99% rename from src/pycalver2/v1rewrite.py rename to src/bumpver/v1rewrite.py index 99c8da0..bb594dd 100644 --- a/src/pycalver2/v1rewrite.py +++ b/src/bumpver/v1rewrite.py @@ -17,7 +17,7 @@ from . import regexfmt from . import v1version from .patterns import Pattern -logger = logging.getLogger("pycalver2.v1rewrite") +logger = logging.getLogger("bumpver.v1rewrite") def rewrite_lines( diff --git a/src/pycalver2/v1version.py b/src/bumpver/v1version.py similarity index 99% rename from src/pycalver2/v1version.py rename to src/bumpver/v1version.py index 4f6fc80..b149974 100644 --- a/src/pycalver2/v1version.py +++ b/src/bumpver/v1version.py @@ -14,7 +14,7 @@ import lexid from . import version from . import v1patterns -logger = logging.getLogger("pycalver2.v1version") +logger = logging.getLogger("bumpver.v1version") CalInfo = typ.Union[version.V1CalendarInfo, version.V1VersionInfo] diff --git a/src/pycalver2/v2patterns.py b/src/bumpver/v2patterns.py similarity index 98% rename from src/pycalver2/v2patterns.py rename to src/bumpver/v2patterns.py index de31238..ccf41ee 100644 --- a/src/pycalver2/v2patterns.py +++ b/src/bumpver/v2patterns.py @@ -37,7 +37,7 @@ from . import utils from .patterns import RE_PATTERN_ESCAPES from .patterns import Pattern -logger = logging.getLogger("pycalver2.v2patterns") +logger = logging.getLogger("bumpver.v2patterns") # NOTE (mb 2020-09-17): For patterns with different options '(AAA|BB|C)', the # patterns with more digits should be first/left of those with fewer digits: @@ -248,6 +248,8 @@ def _convert_to_pep440(version_pattern: str) -> str: continue substitution = PEP440_PART_SUBSTITUTIONS[part_name] + if substitution in pep440_pattern: + continue is_numerical_part = part_name not in ('TAG', 'PYTAG') if is_numerical_part: diff --git a/src/pycalver2/v2rewrite.py b/src/bumpver/v2rewrite.py similarity index 99% rename from src/pycalver2/v2rewrite.py rename to src/bumpver/v2rewrite.py index 9eb0661..e70ea12 100644 --- a/src/pycalver2/v2rewrite.py +++ b/src/bumpver/v2rewrite.py @@ -18,7 +18,7 @@ from . import v2version from . import v2patterns from .patterns import Pattern -logger = logging.getLogger("pycalver2.v2rewrite") +logger = logging.getLogger("bumpver.v2rewrite") def rewrite_lines( diff --git a/src/pycalver2/v2version.py b/src/bumpver/v2version.py similarity index 99% rename from src/pycalver2/v2version.py rename to src/bumpver/v2version.py index 5601ea2..4dfe62d 100644 --- a/src/pycalver2/v2version.py +++ b/src/bumpver/v2version.py @@ -14,7 +14,7 @@ import lexid from . import version from . import v2patterns -logger = logging.getLogger("pycalver2.v2version") +logger = logging.getLogger("bumpver.v2version") CalInfo = typ.Union[version.V2CalendarInfo, version.V2VersionInfo] diff --git a/src/pycalver2/vcs.py b/src/bumpver/vcs.py similarity index 98% rename from src/pycalver2/vcs.py rename to src/bumpver/vcs.py index 29a9100..87c144d 100644 --- a/src/pycalver2/vcs.py +++ b/src/bumpver/vcs.py @@ -4,7 +4,7 @@ # Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License # SPDX-License-Identifier: MIT # -# pycalver2/vcs.py (this file) is based on code from the +# bumpver/vcs.py (this file) is based on code from the # bumpversion project: https://github.com/peritus/bumpversion # Copyright (c) 2013-2014 Filip Noetzel - MIT License @@ -25,7 +25,7 @@ import subprocess as sp from . import config -logger = logging.getLogger("pycalver2.vcs") +logger = logging.getLogger("bumpver.vcs") VCS_SUBCOMMANDS_BY_NAME = { diff --git a/src/pycalver2/version.py b/src/bumpver/version.py similarity index 100% rename from src/pycalver2/version.py rename to src/bumpver/version.py diff --git a/test/fixtures/project_a/calver.toml b/test/fixtures/project_a/bumpver.toml similarity index 80% rename from test/fixtures/project_a/calver.toml rename to test/fixtures/project_a/bumpver.toml index 3b30ca6..b65155d 100644 --- a/test/fixtures/project_a/calver.toml +++ b/test/fixtures/project_a/bumpver.toml @@ -1,12 +1,12 @@ -[calver] +[bumpver] current_version = "v2017.0123-alpha" version_pattern = "vYYYY.BUILD[-TAG]" commit = true tag = true push = true -[calver.file_patterns] -"calver.toml" = [ +[bumpver.file_patterns] +"bumpver.toml" = [ 'current_version = "{version}"', ] diff --git a/test/fixtures/project_b/setup.cfg b/test/fixtures/project_b/setup.cfg index 08158aa..ccc0cf4 100644 --- a/test/fixtures/project_b/setup.cfg +++ b/test/fixtures/project_b/setup.cfg @@ -1,11 +1,11 @@ -[calver] +[bumpver] current_version = v201307.0456-beta version_pattern = {pycalver} commit = True tag = True push = True -[calver:file_patterns] +[bumpver:file_patterns] setup.cfg = current_version = {version} setup.py = diff --git a/test/fixtures/project_c/pyproject.toml b/test/fixtures/project_c/pyproject.toml index aae94e8..95b7f3b 100644 --- a/test/fixtures/project_c/pyproject.toml +++ b/test/fixtures/project_c/pyproject.toml @@ -1,4 +1,4 @@ -[calver] +[bumpver] current_version = "v2017q1.54321" version_pattern = "v{year}q{quarter}.{build_no}" commit = true diff --git a/test/test_cli.py b/test/test_cli.py index 43b1ec2..a9db268 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -17,9 +17,9 @@ import pytest import pathlib2 as pl from click.testing import CliRunner -from pycalver2 import cli -from pycalver2 import config -from pycalver2 import v2patterns +from bumpver import cli +from bumpver import config +from bumpver import v2patterns # pylint:disable=redefined-outer-name ; pytest fixtures # pylint:disable=protected-access ; allowed for test code @@ -51,11 +51,11 @@ requires = ["setuptools", "wheel"] """ ENV = { - 'GIT_AUTHOR_NAME' : "calver_tester", - 'GIT_COMMITTER_NAME' : "calver_tester", - 'GIT_AUTHOR_EMAIL' : "calver_tester@nowhere.com", - 'GIT_COMMITTER_EMAIL': "calver_tester@nowhere.com", - 'HGUSER' : "calver_tester", + 'GIT_AUTHOR_NAME' : "bumpver_tester", + 'GIT_COMMITTER_NAME' : "bumpver_tester", + 'GIT_AUTHOR_EMAIL' : "bumpver_tester@nowhere.com", + 'GIT_COMMITTER_EMAIL': "bumpver_tester@nowhere.com", + 'HGUSER' : "bumpver_tester", 'PATH' : os.environ['PATH'], } @@ -81,7 +81,7 @@ def runner(tmpdir): _debug = 0 if _debug: - tmpdir = pl.Path("..") / "tmp_test_pycalver_project" + tmpdir = pl.Path("..") / "tmp_test_bumpver_project" if tmpdir.exists(): time.sleep(0.2) shutil.rmtree(str(tmpdir)) @@ -101,7 +101,7 @@ def test_help(runner): result = runner.invoke(cli.cli, ['--help', "-vv"]) assert result.exit_code == 0 assert "CalVer" in result.output - assert "bump " in result.output + assert "update " in result.output assert "test " in result.output assert "init " in result.output assert "show " in result.output @@ -110,8 +110,8 @@ def test_help(runner): def test_version(runner): result = runner.invoke(cli.cli, ['--version', "-vv"]) assert result.exit_code == 0 - assert " version v20" in result.output - pattern = v2patterns.compile_pattern("vYYYY.BUILD[-TAG]") + assert " version 20" in result.output + pattern = v2patterns.compile_pattern("YYYY.BUILD[-TAG]") match = pattern.regexp.search(result.output) assert match @@ -186,25 +186,23 @@ def test_incr_semver_invalid(runner, caplog): def test_incr_to_beta(runner): - pattern = "vYYYY.BUILD[-TAG]" - old_version = "v2017.1999-alpha" - initial_version = config._initial_version() + pattern = "vYYYY.BUILD[-TAG]" + old_version = "v2017.1999-alpha" + new_version = dt.datetime.utcnow().strftime("v%Y.22000-beta") result = runner.invoke(cli.cli, ['test', "-vv", old_version, pattern, "--tag", "beta"]) assert result.exit_code == 0 - new_version = initial_version.replace(".1001-alpha", ".22000-beta") assert f"Version: {new_version}\n" in result.output def test_incr_to_final(runner, caplog): - pattern = "vYYYY.BUILD[-TAG]" - old_version = "v2017.1999-alpha" - initial_version = config._initial_version() + pattern = "vYYYY.BUILD[-TAG]" + old_version = "v2017.1999-alpha" + new_version = dt.datetime.utcnow().strftime("v%Y.22000") result = runner.invoke(cli.cli, ['test', "-vv", old_version, pattern, "--tag", "final"]) _debug_records(caplog) assert result.exit_code == 0 - new_version = initial_version.replace(".1001-alpha", ".22000") assert f"Version: {new_version}\n" in result.output @@ -252,14 +250,14 @@ def _add_project_files(*files): with pl.Path("pycalver.toml").open(mode="wt", encoding="utf-8") as fobj: fobj.write(CALVER_TOML_FIXTURE) - if "calver.toml" in files: - with pl.Path("calver.toml").open(mode="wt", encoding="utf-8") as fobj: - fobj.write(CALVER_TOML_FIXTURE) - if "pyproject.toml" in files: with pl.Path("pyproject.toml").open(mode="wt", encoding="utf-8") as fobj: fobj.write(PYPROJECT_TOML_FIXTURE) + if "bumpver.toml" in files: + with pl.Path("bumpver.toml").open(mode="wt", encoding="utf-8") as fobj: + fobj.write(CALVER_TOML_FIXTURE) + def _update_config_val(filename, **kwargs): with io.open(filename, mode="r", encoding="utf-8") as fobj: @@ -281,7 +279,8 @@ def test_nocfg(runner, caplog): _add_project_files("README.md") result = runner.invoke(cli.cli, ['show', "-vv"]) assert result.exit_code == 1 - expected_msg = "Could not parse configuration. Perhaps try 'calver init'." + expected_msg = "Could not parse configuration. Perhaps try 'bumpver init'." + _debug_records(caplog) assert any(expected_msg in r.message for r in caplog.records) @@ -290,14 +289,14 @@ def test_novcs_nocfg_init(runner, caplog): # dry mode test result = runner.invoke(cli.cli, ['init', "-vv", "--dry"]) assert result.exit_code == 0 - assert not os.path.exists("calver.toml") + assert not os.path.exists("bumpver.toml") # non dry mode result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 - assert os.path.exists("calver.toml") - with pl.Path("calver.toml").open(mode="r", encoding="utf-8") as fobj: + assert os.path.exists("bumpver.toml") + with pl.Path("bumpver.toml").open(mode="r", encoding="utf-8") as fobj: cfg_content = fobj.read() base_str = config.DEFAULT_TOML_BASE_TMPL.format(initial_version=config._initial_version()) @@ -305,6 +304,7 @@ def test_novcs_nocfg_init(runner, caplog): assert config.DEFAULT_TOML_README_MD_STR in cfg_content result = runner.invoke(cli.cli, ['show', "-vv"]) + _debug_records(caplog) assert result.exit_code == 0 assert f"Current Version: {config._initial_version()}\n" in result.output assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output @@ -390,7 +390,7 @@ def test_git_init(runner, version_pattern, cur_version, cur_pep440): assert result.exit_code == 0 _update_config_val( - "calver.toml", + "bumpver.toml", version_pattern=version_pattern, current_version='"' + cur_version + '"', ) @@ -409,7 +409,7 @@ def test_hg_init(runner, version_pattern, cur_version, cur_pep440): assert result.exit_code == 0 _update_config_val( - "calver.toml", + "bumpver.toml", version_pattern=version_pattern, current_version='"' + cur_version + '"', ) @@ -430,7 +430,7 @@ def test_v1_git_tag_eval(runner, version_pattern, cur_version, cur_pep440): assert result.exit_code == 0 _update_config_val( - "calver.toml", + "bumpver.toml", version_pattern=version_pattern, current_version='"' + cur_version + '"', ) @@ -456,7 +456,7 @@ def test_hg_tag_eval(runner, version_pattern, cur_version, cur_pep440): assert result.exit_code == 0 _update_config_val( - "calver.toml", + "bumpver.toml", version_pattern=version_pattern, current_version='"' + cur_version + '"', ) @@ -480,7 +480,7 @@ def test_novcs_bump(runner, version_pattern, cur_version, cur_pep440): assert result.exit_code == 0 _update_config_val( - "calver.toml", + "bumpver.toml", version_pattern=version_pattern, current_version='"' + cur_version + '"', ) @@ -488,7 +488,7 @@ def test_novcs_bump(runner, version_pattern, cur_version, cur_pep440): with pl.Path("README.md").open(mode="r") as fobj: content = fobj.read() - result = runner.invoke(cli.cli, ['bump', "-vv"]) + result = runner.invoke(cli.cli, ['update', "-vv"]) assert result.exit_code == 0 calver = cur_version.split(".")[0] @@ -498,7 +498,7 @@ def test_novcs_bump(runner, version_pattern, cur_version, cur_pep440): assert calver + ".1002-alpha !\n" in content assert calver[1:] + ".1002a0 !]\n" in content - result = runner.invoke(cli.cli, ['bump', "-vv", "--tag", "beta"]) + result = runner.invoke(cli.cli, ['update', "-vv", "--tag", "beta"]) assert result.exit_code == 0 with pl.Path("README.md").open() as fobj: @@ -516,15 +516,15 @@ def test_git_bump(runner, caplog, version_pattern, cur_version, cur_pep440): assert result.exit_code == 0 _update_config_val( - "calver.toml", + "bumpver.toml", version_pattern=version_pattern, current_version='"' + cur_version + '"', ) - shell("git", "add", "calver.toml") + shell("git", "add", "bumpver.toml") shell("git", "commit", "-m", "initial commit") - result = runner.invoke(cli.cli, ['bump', "-vv"]) + result = runner.invoke(cli.cli, ['update', "-vv"]) _debug_records(caplog) assert result.exit_code == 0 @@ -544,15 +544,15 @@ def test_hg_bump(runner, version_pattern, cur_version, cur_pep440): assert result.exit_code == 0 _update_config_val( - "calver.toml", + "bumpver.toml", version_pattern=version_pattern, current_version='"' + cur_version + '"', ) - shell("hg", "add", "calver.toml") + shell("hg", "add", "bumpver.toml") shell("hg", "commit", "-m", "initial commit") - result = runner.invoke(cli.cli, ['bump', "-vv"]) + result = runner.invoke(cli.cli, ['update', "-vv"]) assert result.exit_code == 0 calver = cur_version.split(".")[0] @@ -573,12 +573,12 @@ def test_empty_git_bump(runner, caplog): with pl.Path("setup.cfg").open(mode="r") as fobj: default_cfg_data = fobj.read() - assert "[calver]\n" in default_cfg_data + assert "[bumpver]\n" in default_cfg_data assert "\ncurrent_version = " in default_cfg_data - assert "\n[calver:file_patterns]\n" in default_cfg_data + assert "\n[bumpver:file_patterns]\n" in default_cfg_data assert "\nsetup.cfg =\n" in default_cfg_data - result = runner.invoke(cli.cli, ['bump']) + result = runner.invoke(cli.cli, ['update']) assert any(("working directory is not clean" in r.message) for r in caplog.records) assert any(("setup.cfg" in r.message) for r in caplog.records) @@ -595,12 +595,12 @@ def test_empty_hg_bump(runner, caplog): with pl.Path("setup.cfg").open(mode="r") as fobj: default_cfg_text = fobj.read() - assert "[calver]\n" in default_cfg_text + assert "[bumpver]\n" in default_cfg_text assert "\ncurrent_version = " in default_cfg_text - assert "\n[calver:file_patterns]\n" in default_cfg_text + assert "\n[bumpver:file_patterns]\n" in default_cfg_text assert "\nsetup.cfg =\n" in default_cfg_text - result = runner.invoke(cli.cli, ['bump']) + result = runner.invoke(cli.cli, ['update']) assert any(("working directory is not clean" in r.message) for r in caplog.records) assert any(("setup.cfg" in r.message) for r in caplog.records) @@ -640,13 +640,13 @@ def test_v1_bump_semver_warning(runner, caplog, version_pattern): _vcs_init("hg", files=["README.md", "setup.cfg"]) - result = runner.invoke(cli.cli, ['bump', "-vv", "-n", "--dry"]) + result = runner.invoke(cli.cli, ['update', "-vv", "-n", "--dry"]) assert result.exit_code == 1 assert any("version did not change" in r.message for r in caplog.records) assert any("[--major/--minor/--patch] required" in r.message for r in caplog.records) - result = runner.invoke(cli.cli, ['bump', "-vv", "-n", "--dry", "--patch"]) + result = runner.invoke(cli.cli, ['update', "-vv", "-n", "--dry", "--patch"]) assert result.exit_code == 0 @@ -664,7 +664,7 @@ def test_v1_bump_semver_diff(runner, caplog, version_pattern): cases = [("--major", "1.0.0"), ("--minor", "0.2.0"), ("--patch", "0.1.1")] for flag, expected in cases: - result = runner.invoke(cli.cli, ['bump', "-vv", "-n", "--dry", flag]) + result = runner.invoke(cli.cli, ['update', "-vv", "-n", "--dry", flag]) assert result.exit_code == 0 assert len(caplog.records) == 0 @@ -769,13 +769,17 @@ def test_hg_commit_message(runner, caplog): commit_message = """ "bump from {old_version} ({old_version_pep440}) to {new_version} ({new_version_pep440})" """ - _update_config_val("setup.cfg", current_version='"v2019.1001-alpha"') - _update_config_val("setup.cfg", commit_message=commit_message.strip()) + _update_config_val( + "setup.cfg", + current_version='"v2019.1001-alpha"', + version_pattern="vYYYY.BUILD[-TAG]", + commit_message=commit_message.strip(), + ) _vcs_init("hg", ["README.md", "setup.cfg"]) assert len(caplog.records) > 0 - result = runner.invoke(cli.cli, ['bump', "-vv", "--pin-date", "--tag", "beta"]) + result = runner.invoke(cli.cli, ['update', "-vv", "--pin-date", "--tag", "beta"]) assert result.exit_code == 0 tags = shell("hg", "tags").decode("utf-8") @@ -796,13 +800,17 @@ def test_git_commit_message(runner, caplog): commit_message = """ "bump: {old_version} ({old_version_pep440}) -> {new_version} ({new_version_pep440})" """ - _update_config_val("setup.cfg", current_version='"v2019.1001-alpha"') - _update_config_val("setup.cfg", commit_message=commit_message.strip()) + _update_config_val( + "setup.cfg", + current_version='"v2019.1001-alpha"', + version_pattern="vYYYY.BUILD[-TAG]", + commit_message=commit_message.strip(), + ) _vcs_init("git", ["README.md", "setup.cfg"]) assert len(caplog.records) > 0 - result = runner.invoke(cli.cli, ['bump', "-vv", "--pin-date", "--tag", "beta"]) + result = runner.invoke(cli.cli, ['update', "-vv", "--pin-date", "--tag", "beta"]) assert result.exit_code == 0 tags = shell("git", "tag", "--list").decode("utf-8") @@ -870,7 +878,7 @@ def test_multimatch_file_patterns(runner): with pl.Path("setup.cfg").open(mode="w", encoding="utf-8") as fobj: fobj.write(SETUP_CFG_MULTIMATCH_FILE_PATTERNS_FIXTURE) - result = runner.invoke(cli.cli, ['bump', '--tag', 'beta', '--date', "2020-11-22"]) + result = runner.invoke(cli.cli, ['update', '--tag', 'beta', '--date', "2020-11-22"]) assert result.exit_code == 0 with pl.Path("README.md").open(mode="r", encoding="utf-8") as fobj: @@ -921,20 +929,20 @@ def test_get_latest_vcs_version_tag(runner): result = runner.invoke(cli.cli, ['init', "-vv"]) assert result.exit_code == 0 - _update_config_val("calver.toml", push="false") - _update_config_val("calver.toml", current_version='"0.1.8"') - _update_config_val("calver.toml", version_pattern='"MAJOR.MINOR.PATCH"') + _update_config_val("bumpver.toml", push="false") + _update_config_val("bumpver.toml", current_version='"0.1.8"') + _update_config_val("bumpver.toml", version_pattern='"MAJOR.MINOR.PATCH"') - _vcs_init("git", files=["calver.toml"]) + _vcs_init("git", files=["bumpver.toml"]) - result = runner.invoke(cli.cli, ['bump', "--patch"]) + result = runner.invoke(cli.cli, ['update', "--patch"]) assert result.exit_code == 0 _, cfg = config.init() latest_version = cli.get_latest_vcs_version_tag(cfg, fetch=False) assert latest_version == "0.1.9" - result = runner.invoke(cli.cli, ['bump', "--patch"]) + result = runner.invoke(cli.cli, ['update', "--patch"]) assert result.exit_code == 0 _, cfg = config.init() diff --git a/test/test_config.py b/test/test_config.py index c071944..94ae52d 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -7,7 +7,7 @@ from __future__ import unicode_literals import io from test import util -from pycalver2 import config +from bumpver import config # pylint:disable=redefined-outer-name ; pytest fixtures # pylint:disable=protected-access ; allowed for test code @@ -51,33 +51,33 @@ push = false """ CALVER_TOML_FIXTURE_3 = """ -[calver] +[bumpver] current_version = "v201808.0123-alpha" version_pattern = "vYYYY0M.BUILD[-TAG]" commit = true tag = true push = true -[calver.file_patterns] +[bumpver.file_patterns] "README.md" = [ "{version}", "{pep440_version}", ] -"calver.toml" = [ +"bumpver.toml" = [ 'current_version = "{version}"', ] """ SETUP_CFG_FIXTURE = """ -[calver] +[bumpver] current_version = "v201808.0456-beta" version_pattern = "vYYYY0M.BUILD[-TAG]" commit = True tag = True push = True -[calver:file_patterns] +[bumpver:file_patterns] setup.py = {version} {pep440_version} @@ -87,7 +87,7 @@ setup.cfg = NEW_PATTERN_CFG_FIXTURE = """ -[calver] +[bumpver] current_version = "v201808.1456-beta" version_pattern = "vYYYY0M.BUILD[-TAG]" commit_message = "bump version to {new_version}" @@ -95,7 +95,7 @@ commit = True tag = True push = True -[calver:file_patterns] +[bumpver:file_patterns] setup.py = {version} {pep440_version} @@ -172,11 +172,11 @@ def test_parse_toml_3(): assert cfg.push is True files = set(cfg.file_patterns) - assert "calver.toml" in files + assert "bumpver.toml" in files raw_patterns_by_path = _parse_raw_patterns_by_filepath(cfg) - assert raw_patterns_by_path["README.md" ] == ["vYYYY0M.BUILD[-TAG]", "YYYY0M.BLD[PYTAGNUM]"] - assert raw_patterns_by_path["calver.toml"] == ['current_version = "vYYYY0M.BUILD[-TAG]"'] + assert raw_patterns_by_path["README.md" ] == ["vYYYY0M.BUILD[-TAG]", "YYYY0M.BLD[PYTAGNUM]"] + assert raw_patterns_by_path["bumpver.toml"] == ['current_version = "vYYYY0M.BUILD[-TAG]"'] def test_parse_v1_cfg(): @@ -247,8 +247,8 @@ def test_parse_default_cfg(): def test_parse_project_toml(): project_path = util.FIXTURES_DIR / "project_a" - config_path = util.FIXTURES_DIR / "project_a" / "calver.toml" - config_rel_path = "calver.toml" + config_path = util.FIXTURES_DIR / "project_a" / "bumpver.toml" + config_rel_path = "bumpver.toml" with config_path.open() as fobj: config_data = fobj.read() @@ -268,7 +268,7 @@ def test_parse_project_toml(): assert cfg.push is True files = set(cfg.file_patterns.keys()) - assert files == {"calver.toml", "README.md"} + assert files == {"bumpver.toml", "README.md"} def test_parse_project_cfg(): @@ -406,7 +406,7 @@ def test_parse_missing_version(tmpdir): setup_path.write( "\n".join( ( - "[calver]", + "[bumpver]", # f"current_version = v201808.1001-dev", "commit = False", ) @@ -422,7 +422,7 @@ def test_parse_missing_version(tmpdir): def test_parse_invalid_version(tmpdir): setup_path = tmpdir.mkdir("fail").join("setup.cfg") - setup_path.write("\n".join(("[calver]", "current_version = 0.1.0", "commit = False"))) + setup_path.write("\n".join(("[bumpver]", "current_version = 0.1.0", "commit = False"))) ctx = config.init_project_ctx(setup_path) assert ctx diff --git a/test/test_parse.py b/test/test_parse.py index f571ec3..5bccaa2 100644 --- a/test/test_parse.py +++ b/test/test_parse.py @@ -4,8 +4,8 @@ from __future__ import print_function from __future__ import absolute_import from __future__ import unicode_literals -from pycalver2 import parse -from pycalver2 import v1patterns +from bumpver import parse +from bumpver import v1patterns SETUP_PY_FIXTURE = """ # setup.py diff --git a/test/test_patterns.py b/test/test_patterns.py index ae67fbe..9222280 100644 --- a/test/test_patterns.py +++ b/test/test_patterns.py @@ -8,8 +8,8 @@ import re import pytest -from pycalver2 import v1patterns -from pycalver2 import v2patterns +from bumpver import v1patterns +from bumpver import v2patterns V2_PART_PATTERN_CASES = [ (['YYYY', 'GGGG'], "2020" , "2020"), diff --git a/test/test_rewrite.py b/test/test_rewrite.py index 9794ea0..a306920 100644 --- a/test/test_rewrite.py +++ b/test/test_rewrite.py @@ -8,14 +8,14 @@ import re import copy from test import util -from pycalver2 import config -from pycalver2 import rewrite -from pycalver2 import v1rewrite -from pycalver2 import v1version -from pycalver2 import v2rewrite -from pycalver2 import v2version -from pycalver2 import v1patterns -from pycalver2 import v2patterns +from bumpver import config +from bumpver import rewrite +from bumpver import v1rewrite +from bumpver import v1version +from bumpver import v2rewrite +from bumpver import v2version +from bumpver import v1patterns +from bumpver import v2patterns # pylint:disable=protected-access ; allowed for test code @@ -102,7 +102,7 @@ def test_iter_file_paths(): _paths_and_patterns = rewrite.iter_path_patterns_items(cfg.file_patterns) file_paths = {str(file_path) for file_path, patterns in _paths_and_patterns} - assert file_paths == {"calver.toml", "README.md"} + assert file_paths == {"bumpver.toml", "README.md"} def test_iter_file_globs(): @@ -217,13 +217,13 @@ def test_v2_optional_release(): def test_v1_iter_rewritten(): - version_pattern = "v{year}{build}{release}" - new_vinfo = v1version.parse_version_info("v2018.0123", version_pattern) + version_pattern = "{year}{build}{release}" + new_vinfo = v1version.parse_version_info("2018.0123", version_pattern) init_pattern = v1patterns.compile_pattern( - version_pattern, '__version__ = "v{year}{build}{release}"' + version_pattern, '__version__ = "{year}{build}{release}"' ) - file_patterns = {"src/pycalver2/__init__.py": [init_pattern]} + file_patterns = {"src/bumpver/__init__.py": [init_pattern]} rewritten_datas = v1rewrite.iter_rewritten(file_patterns, new_vinfo) rfd = list(rewritten_datas)[0] expected = [ @@ -232,21 +232,21 @@ def test_v1_iter_rewritten(): "#", "# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License", "# SPDX-License-Identifier: MIT", - '"""PyCalVer: CalVer for Python Packages."""', + '"""BumpVer: A CLI program for versioning."""', '', - '__version__ = "v2018.0123"', + '__version__ = "2018.0123"', '', ] assert rfd.new_lines == expected def test_v2_iter_rewritten(): - version_pattern = "vYYYY.BUILD[-TAG]" - new_vinfo = v2version.parse_version_info("v2018.0123", version_pattern) + version_pattern = "YYYY.BUILD[-TAG]" + new_vinfo = v2version.parse_version_info("2018.0123", version_pattern) file_patterns = { - "src/pycalver2/__init__.py": [ - v2patterns.compile_pattern(version_pattern, '__version__ = "vYYYY.BUILD[-TAG]"'), + "src/bumpver/__init__.py": [ + v2patterns.compile_pattern(version_pattern, '__version__ = "YYYY.BUILD[-TAG]"'), ] } @@ -258,38 +258,41 @@ def test_v2_iter_rewritten(): "#", "# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License", "# SPDX-License-Identifier: MIT", - '"""PyCalVer: CalVer for Python Packages."""', + '"""BumpVer: A CLI program for versioning."""', '', - '__version__ = "v2018.0123"', + '__version__ = "2018.0123"', '', ] assert rfd.new_lines == expected def test_v1_diff(): - version_pattern = "v{year}{build}{release}" - raw_pattern = '__version__ = "v{year}{build}{release}"' + version_pattern = "{year}{build}{release}" + raw_pattern = '__version__ = "{year}{build}{release}"' pattern = v1patterns.compile_pattern(version_pattern, raw_pattern) - file_patterns = {"src/pycalver2/__init__.py": [pattern]} + file_patterns = {"src/bumpver/__init__.py": [pattern]} old_vinfo = v1version.parse_version_info("v201809.0123") new_vinfo = v1version.parse_version_info("v201911.1124") assert new_vinfo > old_vinfo - old_vinfo = v1version.parse_version_info("v2018.0123", version_pattern) - new_vinfo = v1version.parse_version_info("v2019.1124", version_pattern) + old_vinfo = v1version.parse_version_info("2018.0123", version_pattern) + new_vinfo = v1version.parse_version_info("2019.1124", version_pattern) diff_str = v1rewrite.diff(old_vinfo, new_vinfo, file_patterns) lines = diff_str.split("\n") - assert lines[:2] == ["--- src/pycalver2/__init__.py", "+++ src/pycalver2/__init__.py"] + assert lines[:2] == [ + "--- src/bumpver/__init__.py", + "+++ src/bumpver/__init__.py", + ] - assert lines[6].startswith('-__version__ = "v20') - assert lines[7].startswith('+__version__ = "v20') + assert lines[6].startswith('-__version__ = "20') + assert lines[7].startswith('+__version__ = "20') - assert not lines[6].startswith('-__version__ = "v2018.0123"') + assert not lines[6].startswith('-__version__ = "2018.0123"') - assert lines[7] == '+__version__ = "v2019.1124"' + assert lines[7] == '+__version__ = "2019.1124"' raw_pattern = "Copyright (c) 2018-{year}" pattern = v1patterns.compile_pattern(version_pattern, raw_pattern) @@ -302,25 +305,28 @@ def test_v1_diff(): def test_v2_diff(): - version_pattern = "vYYYY.BUILD[-TAG]" - raw_pattern = '__version__ = "vYYYY.BUILD[-TAG]"' + version_pattern = "YYYY.BUILD[-TAG]" + raw_pattern = '__version__ = "YYYY.BUILD[-TAG]"' pattern = v2patterns.compile_pattern(version_pattern, raw_pattern) - file_patterns = {"src/pycalver2/__init__.py": [pattern]} + file_patterns = {"src/bumpver/__init__.py": [pattern]} - old_vinfo = v2version.parse_version_info("v2018.0123", version_pattern) - new_vinfo = v2version.parse_version_info("v2019.1124", version_pattern) + old_vinfo = v2version.parse_version_info("2018.0123", version_pattern) + new_vinfo = v2version.parse_version_info("2019.1124", version_pattern) diff_str = v2rewrite.diff(old_vinfo, new_vinfo, file_patterns) lines = diff_str.split("\n") - assert lines[:2] == ["--- src/pycalver2/__init__.py", "+++ src/pycalver2/__init__.py"] + assert lines[:2] == [ + "--- src/bumpver/__init__.py", + "+++ src/bumpver/__init__.py", + ] - assert lines[6].startswith('-__version__ = "v20') - assert lines[7].startswith('+__version__ = "v20') + assert lines[6].startswith('-__version__ = "20') + assert lines[7].startswith('+__version__ = "20') - assert not lines[6].startswith('-__version__ = "v2018.0123"') + assert not lines[6].startswith('-__version__ = "2018.0123"') - assert lines[7] == '+__version__ = "v2019.1124"' + assert lines[7] == '+__version__ = "2019.1124"' raw_pattern = "Copyright (c) 2018-YYYY" pattern = v2patterns.compile_pattern(version_pattern, raw_pattern) diff --git a/test/test_version.py b/test/test_version.py index 2b7b0a1..b2bfa84 100644 --- a/test/test_version.py +++ b/test/test_version.py @@ -9,11 +9,11 @@ import datetime as dt import pytest -from pycalver2 import version -from pycalver2 import v1version -from pycalver2 import v2version -from pycalver2 import v1patterns -from pycalver2 import v2patterns +from bumpver import version +from bumpver import v1version +from bumpver import v2version +from bumpver import v1patterns +from bumpver import v2patterns # pylint:disable=protected-access ; allowed for test code diff --git a/test/util.py b/test/util.py index 86d7a7a..6b62c92 100644 --- a/test/util.py +++ b/test/util.py @@ -29,7 +29,7 @@ FIXTURE_PATH_PARTS = [ ["setup.cfg"], ["setup.py"], ["pycalver.toml"], - ["calver.toml"], + ["bumpver.toml"], ["src", "module_v1", "__init__.py"], ["src", "module_v2", "__init__.py"], ] @@ -41,7 +41,7 @@ class Project: self.tmpdir = tmpdir self.prev_cwd = os.getcwd() - self.dir = tmpdir / "pycalver_project" + self.dir = tmpdir / "bumpver_project" self.dir.mkdir() if project is None: From 72cb75d2b2067e93845e4bf2eaab5074cf265885 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 18 Oct 2020 20:49:00 +0000 Subject: [PATCH 96/98] bugfix --- test/test_cli.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/test_cli.py b/test/test_cli.py index a9db268..fbfcca6 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -830,13 +830,11 @@ def test_grep(runner): cmd1 = r'grep "vYYYY.BUILD[-TAG]" README.md' result1 = runner.invoke(cli.cli, shlex.split(cmd1)) assert result1.exit_code == 0 - assert "README.md" in result1.output assert re.search(search_re, result1.output, flags=re.MULTILINE) cmd2 = r'grep --version-pattern "vYYYY.BUILD[-TAG]" "{version}" README.md' result2 = runner.invoke(cli.cli, shlex.split(cmd2)) assert result2.exit_code == 0 - assert "README.md" in result2.output assert re.search(search_re, result2.output, flags=re.MULTILINE) assert result1.output == result2.output @@ -846,13 +844,11 @@ def test_grep(runner): cmd3 = r'grep "\[aka. YYYY.BLD[PYTAGNUM] \!\]" README.md' result3 = runner.invoke(cli.cli, shlex.split(cmd3)) assert result3.exit_code == 0 - assert "README.md" in result3.output assert re.search(search_re, result3.output, flags=re.MULTILINE) cmd4 = r'grep --version-pattern "vYYYY.BUILD[-TAG]" "\[aka. {pep440_version} \!\]" README.md' result4 = runner.invoke(cli.cli, shlex.split(cmd4)) assert result4.exit_code == 0 - assert "README.md" in result4.output assert re.search(search_re, result4.output, flags=re.MULTILINE) assert result3.output == result4.output From aed87ee5dd1670d01448a7f304922711195a77c4 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 18 Oct 2020 20:57:17 +0000 Subject: [PATCH 97/98] use bumpver in make bump --- Makefile.bootstrapit.make | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.bootstrapit.make b/Makefile.bootstrapit.make index 9bdfaa1..970adc3 100644 --- a/Makefile.bootstrapit.make +++ b/Makefile.bootstrapit.make @@ -558,7 +558,7 @@ freeze: ## Bump Version number in all files .PHONY: bump_version bump_version: - $(DEV_ENV)/bin/pycalver bump; + $(DEV_ENV)/bin/bumpver update; ## Create python sdist and bdist_wheel files From daa2ec7dfca583ef19ca9046828b4d454f338ca2 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 18 Oct 2020 21:01:31 +0000 Subject: [PATCH 98/98] release prep --- CHANGELOG.md | 7 ++++++- Makefile.bootstrapit.make | 2 ++ setup.cfg | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c5fb41..91295c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog for https://github.com/mbarkhau/pycalver -## BumpVer 2020.1042-beta +## BumpVer 2020.1100-beta Rename package and module from PyCalVer to BumpVer. This name change is due to confusion that this project is either Python specific, or only suitible for CalVer versioning schemes, neither of which is the case. @@ -53,6 +53,11 @@ Many thanks to contributors of this release: @LucidOne, @khanguslee, @chaudum [gitlab_i8]: https://gitlab.com/mbarkhau/pycalver/-/issues/8 +## PyCalVer v202010.1042 + +- Add deprication warning to README.md + + ## PyCalVer v201907.0036 - Fix: Don't use git/hg command if `commit=False` is configured (thanks @valentin87) diff --git a/Makefile.bootstrapit.make b/Makefile.bootstrapit.make index 970adc3..842cfb5 100644 --- a/Makefile.bootstrapit.make +++ b/Makefile.bootstrapit.make @@ -564,6 +564,8 @@ bump_version: ## Create python sdist and bdist_wheel files .PHONY: dist_build dist_build: + @rm -rf build/lib3to6_out/ + @rm -rf build/bdist* $(DEV_ENV_PY) setup.py sdist; $(DEV_ENV_PY) setup.py bdist_wheel --python-tag=$(BDIST_WHEEL_PYTHON_TAG); @rm -rf src/*.egg-info diff --git a/setup.cfg b/setup.cfg index 7e4a785..2efe452 100644 --- a/setup.cfg +++ b/setup.cfg @@ -89,7 +89,7 @@ addopts = --doctest-modules [bumpver] -current_version = "2020.1041-beta" +current_version = "2020.1099-beta" version_pattern = "YYYY.BUILD[-TAG]" commit_message = "bump {old_version} -> {new_version}" commit = True