!>Ym0zK2(gzQ5$~l0%~ZGWKFcFqlXOJPR20pS()&Sqq9)
z`aflf_&903^rjl_f^)rjK}XY7a_g_0GDf;U8MkN?W4m7b^gBS~PUdg__kF6kVXq$H
z@M0o7RkaHmw)QTa1|X2?py6?QJ!%6MtywF`vj5>fk8_iz$`+)943}6JyfJIAUofi7
zjPqrcI)bvJ`aCb*NZVp_iwld
z0zVXdd!ZlD;E9kmy)Xp3V<&pAaOhd=XB~S$^I!IUpvZc5vAB7Mo-44`b3HrGw#wLO
zog-O^g>&&z;i4ue49STb?Zw;@9z;_t3)rlL3vn|hi
zoFg*g0Z-^%Ifj(*)`EPvvjwlk`r}-}`e|e~cSelCa!kWRczG0fr9E(vXyvrMqV5q=
z2jGb_-xqXtfA8U+AyRiXMYbD|_1?ftl9gW-qm=w|w8Ng{tncYb+?5qaal1g^c?NJe
zO!5f-Y$&PjlF!Fs8(n~
z@m|EkA~@`|GpfB21<1-Z#aJ>`Fc7{H!Uc#{>;nT*=+w%WeCC4m7qpSd
zFVm9TiO2w~2>nJeXw#1bM@KmPGLhnw+g78r^ybxuEO4G4F~n?voG^|bFM->oQj?TY
z@$i4|`FmC8^B~WV4uS}YZA`{z!C+HmwGBRChiDY-L>v8?ao*C|M9SH-pxKZJEE{|o
zh}}6abJ|EIn|LqR6Dvdx!-xdWX103<9|L;;J?tv+b~#k+A0!d&^X_;w53*iy6Y_5x
zwk3Orho6AXu)jxJhg~L^1a(>eJ?#F!Z5~QWWsnO|WPXs2Rq%Y+vbEjSE%$TUJZY0z
zW+)p3k@@XfxNYo9w%k{wu5sE}2ms5Xl8RQ)xr(z<4%&=BH75#Rd
znFP5geRVU%;~*7?16c;0%&3-j8|k_Jl)M!1wT5O;u3wOR^+b6nOE%J6eD>;a&CCM4!z0@gNT^Vg8kwN4JsSYl{JD6Lm=v^e>eQpgqShX{t-uJ
zF66Wgv}Uj314P(hVbb#v^1M+JO_yVNlp@Az}=J-o$>h&?TKgkkmr9k
z(yWE+$i}tsBQs63tro0W81=2vCQ<-1D1?yw(ZYSt9-fm#=9}o?vf5i}|I@WTcR5PS
zDmEsg0DG}gxZ5ry`RFfEJX+Zt+{K=;8C;|!l#rPAMo9l%86e70dxsiZT1IAYagxO2
zj4nj}(zyo_nZ8Rcqxl@-Xv*;L@WbCbQ!|Mau0xi~XiqFQo>x3XxFDXFMqL?f;p#(#
zmzE@f7n$jk-DU!0tyw*TS6uz1^^)RFPKwGXu;50hAKss-pL1yS^G3#A}x-PVmk0j+euZYRX*EKrW{ygEmmOvCs(z6;k;SC5N
zHG-&sgk+bpV*e*E36?Tulnml0bH!**esQHx0jXg*OZk$2(P}7edI)uew(AIIqmRe~(bU
zo}Zk>AfukEl`U3S#lrIU%g+zXpK6t70@vkgS!Lkrz7Wa$mA9S=B)#KPf2VTLcc2J~
zhA1eg2a#JZJpf7(a)?=WTepbU;JI0Xw-c5s{OW7+D&6n>%hq0lFW#R?Gawsg|d_lu%U$NvD-@$<@ZGLeedlCN#CPb%O*i
zj*q(rfmlo%N;H21f)Rp5ZGHL`e^|Csoke~aOVp}|``uA-`2&RyLk)&qo~v6g0}Ydi
z$3Y6$=;CegaPo)cu|&DS9U?PxI=JGA$?v7lik>AA$#9nk>`drDo*xie9wBZ2Qd>7f
z|8AT2ky}F6P|2y@#Rmup-#9fBhy=;Y@?Dd5qH!mk_hRU-A&!@@9SOGqOelh9rFHLV
zLYBX2PzJ4+Nf3(nQ6-*&P*d*^?5UC42Z
z9#h`UG7i%JN#U}kXXQasx;I3=?0EXJ|H9_qwkf67?Bfu4z3)!C)L3+1A5X;4$+9ei
z2vE^#)20f(G8$o12_m8M(wTKE3gbqEpaz$f<|BVw+X{-z#~pp*;pa~~?d#)M2tGEug!_`_T;N#xO!oYV3>
zgp6{Kaq#lSCL4jz)&S5$s;<>}cA#JTae`!DZL#c@guVpX(=>u-Bhh}>;N0trlI$VC
zXiI{#%09~772yJS8+f=go{eaF0R%iQt)jO5%Uw-6uMxsDkIW<`Gf6;x8*}Kh@8a!4
zl_~EG!&5#0IFwI&VyN{fv!rdkr6azC@*>p5yVuqVuf5S9{URvq#GttR$Je3;ZPQ7|
z+OY0HAg$07S^IO0fpppV<*}QpzWz$IeTMSnEr)G14LlC=4xWBHiA?{qPJx&BYxt=*
zoX-o1JRRg!2tRSVzH~H7BDqE>C$w#gJh9`h=d^9#X$ma8
zjp^#lg7U)Fal>ug<#r1m(ydQWX9b`mP&u`kLjL5N1J7{uYldPljtE5PJV*!;ERxL!
z%>|pv?!2ipcn=4&fUM|2rl1r4K-P=nRrIjN-#n)Fc%yJLh#IN=9q$_h~A&^
zeegM%J{x_kvZSqO=&o?i(#8O?J87-|af^+Y=^jbICvQSdum4503+SX~-ZnHX%K1s{
z2ivfsbVm@5_c`jai2`R_H~q1obM{wy0m>J^-Rc8hP;Bm=wLEyVHoJ)!$5RLC@hRNw
z!k>c@_fVd`ATpwpq42iv#x_egtayA1Cr&nLvQVQ;fAXKXfsF8C_cGRa`OoVa!?v&O
zhz1JOJ=-})l#M5W^0Sx6-M`GY91W#y)Ry_4W|Z0PxDk&FI`D$rD5m4?L38AdncD0T
zB+AH7mqcx)R
z?ndIoB->k^>3f<>CXzPY>IQ$;c2wW|S{;KfB&`WrPts{Bp$Q<|4y4h_l)qP$LAtVS
zYqryL?tVG0{frHJNn{rEB&3#U#_8jYLfLM)
z*}+qYBA40HDNEh6Z>5Okc~?+?*7S+d?Y1nC896TcG4?Y8Z{E?<@~-HB&)|1Dkgqw|
zfFvHY4!02F9_m^6m4VCO6bE*`@~C^fgRGl=+^lcW4LacpRLNoIuZ_k@n;&DDgebR=
zeE>7dUDnq&3v56ajHPt>M&1v%=528G$}myxo}91m1HcLNQ$@qF$G^uz9l+c%foC~8
zc#hYZaA5|Aw&*`=pYOGjIKMJ_z$bi4Q+KRG&vPs!o76&tcvTz-gV|KynTe{L?5ZLG
z4&epyvGJSxrFBwt3K$r4II*XmGaUYL*DEWWA-3|YOL@;c_6{Tvx{glU^z*vi&fvj#y6OJd46=%Lu&Q{|+HSMbGOQ`|0#u#1QHD
zxYgUb(BeCkgOXBw;|C=Wg&l+B|6Y__f35+
z@s5Lpx#G@UXKFWU>amQ|I8VRyh(6zSRan3NlcLT6Kc*Pc-b;UxK$71ok$pz`A
zb1DIYQDiOyx3xd?*%lAju3dlz4p&>*G~udl?870hTUtx1Vz3${Q^
zf5Svjv~E8YxO!
z!DEA(3;Fw0y!@?Ez;c%GzWcLgBR*dL_~%UgcnqUh2QAV!7A#25>1>un^yvpo|L$h2
z+SE1%`#y3`e@8FyVPu81F1%39|C(ox-jX7hnsoZ;$ouR@`IqZkL2rDBjAI192|f@sqCB3)7p8;t!0FelwisTLEpTD>=b5F{U69K4%dy5l
z9U@V*afi2E@MJkbqo!}49%LJn9*Qosv{jPdU&(^nF&1{l89w%@tV3;uVaPMS>YF$R
zeZREXxYz7O>)qa>yvT>Ime`AUR-O2OhLfI5B(E;J}9#H8C~
ze}#Ns@b|l%!PpV;PfN>vzmIChqSNzI>9pozYPdda(@hrt_%ZHQnmKbL$=`3|9t?&T
zBHH?OXurr3GBOV#$C;S+N%!xY*KI6!Q|ff3V0ih`0M)U{+dC8lExHy_!PZ)`6gkmw`HEvO|si;_fY#JL&IxlY-@gMu}5A%e-v@W{k`!w*ehE(V|QSZ
z7el^RT5$-qVb!BF`odaVBGOhrYna=qsr>~xjylkqnD1OGXGCcE(OD|#3pi%Cqju%QJRPtk!m+$I|R
z?6vHS%K8Lg4)=@74?KJsm$u7x6n+}c=RSyIoBs{9Ta7L5Dq*x)9*+7WcvY^jwV7AF
zhyjG~RsP{5U+p9^o-i4K@TUu)7;m`!g2fPYOuddOrhRlBBU
zSfwK57WJWuftTf-D+3O>)MPRIGx-dRW39Vj1(=(BEkaVR0@zBNjq{VT_RVv;I5%KJ
zuVh^$Hn$?==3|4G^%YsINwbvPzc)V>6O=qR?pa`e<${p0>3IrY8Dqas{Sar^1~xMO
zcT>%mt)l<-t!}d;h%*oZHQsTClxul_DjrlhUENe&l~D0R+K2{sgfE{^0;CT$3qr19
zj7i^Q(6Mo@|A@I6hwXK3C+z}%jKg3C1OzRJ=8!7K5BOM{Yg5yfO=JXEXa7D%
z2Fx4i^s9TdmM+e-4S(yD{U2kSS8o@WwI`suSZw$*?&uT8-vdpEwmJ?&2@U2mlEg^K
zJ>+NB7NCJY7E4=ESF5tJNg?
z>oII)_glGJX(jryOVDfle9^yL7t6-F;wt~SyqUCWvY0UMV~=MnEY{D98)Une^?KL)
zRoef}0=(r3bb_m+o`ENN92MA##=aJ_-9sUg*Q
zqgnq!7q*HAHwN0Ua+O6hhxs6c_xk!i#8v8vKm?{+FhhC51HC_PB9#LuS#L{`Q;!5T
z1@{>wYW(j06zr`fHQqb;_P1U&msyP}h-Hz0XA0a_?S~~;JPx4b2SNp~oaLq`HS>CF
z4ET}hPZ;eP*Iuyg^F=V0YN!vMsUS!u&
zFe()-te|MK-uL3|?}N2Rbx}hgqubtDX$oC8RgCBp_zTl|fDkGwiBgr7mqLZ
z$h*mEY#I9fOa|n8f2ieUzm~=ZGspsVDec~UwN}&wC-BA-+S=yCCZ_FXO4{~nv8?M2
zaJ-skgZvgJ(-G=iP3vrpJl3U8j${^>2M;mhN+y!F+|ZnbD)~JUl5G%zbdUUg
zm@HN7_C3p8qXnJ_n`8EwmYF}TmdtH1g#7!39WG!W2dL$UFei4k(Zqh3=}e(HEvb9L(@!79Ec7n|?+U^ZKuuv6H4o?)n44(gvSbX2Pp)4elwcaR
z4?(OL8Pp}|>7pwe(FBi0)w>GOK;$CH;7Z-wcQa-0QB#Cklp`Ie?7ZhLX}MPV>%3$C
zI>
c7V?#{yC#>_g1m<{`
zk1+8A)Nld+t!2rpmDKJMR~z`^8iHl7#m-ZuUj;@xb_|wMAgm#j;Sg0dm~a#f&7nEf#6UHm^kyoo$QGIv6Z(
z*IX~)tMAP=N=(7aA!%K4{FAb6q7#*G&nqr&C8$%q+CMK2ZL<76{W@nl
z3Om*87pkJN7>iJD6I8b8I-;(dwxuGAoxNdoDha#*^}%_cD|ljFVq^$5m5$@ZwqHl9fFxc5{1og8DnwU?L
zoR&muEFmhyQMX#@@%DF!+Rh=&*aSs|3t+zvJ)=AN^Mtq6R8v*ObEIv?V`
z8UQV(lv^Fl7Dn6Poq3x?W7|E>G{@U>C`%cWz6CL^zz`f{!VzUjyrZ*&pNEGQ!O1^;
zbs0)S)XHO(mP&Gqv@fUGzJpI5m=&oReKM>|tQ(!4o>rit)`)+#*05e`T3pC?n@gNp
z`?oQ~dEyJva2LI$8JqvXNG~*poWDv7>-oUpkMqaDe4wmZWV)j$nCp5?UqW&m6Xkf*
zigpivL_PDi`FaA$xe9`u2iwSjlm46quC=6jtikmGC*!+
zxwt?256N(4`|uiOvpW5%M`jMgr3km2nb^J?;Lc6Dx;*-$
zqt;q(GY6eI(U3v;^NpiWdW(~v^LNjr)#9BT8i8t$VvwQI;?dO0bo5H0cJ7pIicMl#
zT$##8r!3l*R+_C$|K>CCz*(H@yLzrR{vA)#oe4wG3(zCqkN-x$RHgKIbMP{k7b
z5$opoYi*9o2gj}C5;^uHpmm~&8>M24P!JV}A!OBM{h#SH1S>igyyH-(HV=|$!XrSM
z`llIjAaU?a6CZZ!IOLc{X7V;QMd;w*V7`If^AP^;?FM*djMTSS)L
zc;TVEUWMq)^=RKXO`k+3D&jh7XMC2mWyt9lyq&0(Lua+#EEq?85OB#o^L}@E!6G*d
z$HQ>k-W=oEsV}`9SoCV{SJt@jxS3lsWSmDlj|%l*dhVSAb;Dif`yC}h{+1u~7ff!O
zlDD37m~kw5Vz_>!J%Qb!j-#r47Fm6S1GC6Lp%7ySmt%VD2GO*zZGUnmw)+%lrMcA-
zpAa?E%Nl|vSlT?l;8pqg`RA*!F|=_auvZ_^7p}J&DH_TQ-J=}1hIz@0(mz=jo)6k7(kvW0hf
zhH_jB(q!A4m7y7>bfq0095X(OBI!?BD=Xc+se7HN>{Ii(j+Q_`?~=|wHFHI<(DF<(
zxTdkOv5b|(?j0{^Fv!7nyF|;IqEsJl2U-UQRwE?{wT<}k59cSINuYAZz}61BCkq!s
zFWEl!;N(N;c^_sG)sdFsQtD$-AKs0NgPflnKHFIaP2mQ!XmE*%6BMQU$L~M}bb#@|
zRHGP1bXER+O$l-!Iw3Y&p7PahCV9;=^OZR5=Bt7zp>^BI3*5WM^_e@S}Wdm>zC3L=#|
zmP@{F_|S*ql-;H4Szon08}GwIC$YL`y<;oQP+7V*0tf?-l{*SWuVCUH^kjPd3U_JK
ze=P9M^wWiW-5qgqL-1$uHLht35<~{!yy#}v;J@|_Dqfk)@KWXoLK3?0Ap<;-K5i)3
zcf*Ad{tjLmrN6*)|Crc|DwOEgM&(c@@;+fdi!d{83ep8V?>_L?jmV1H1|uM5VK|qY
zoh{l<{ZzGY4tf!zGX>mHf9j8>911d2hvd&4EeAiTAxgx@!IF^DYTJ>8DX_z)IM71I
zES#AFE~%_$#z&~7Cp(p
z6CgA12IKylhAGCE?PP3zvNEw-t%OSn5DUagu&(;6c+mPqBcB;B&J+GmHFnDg7(%=A
zfQ(w(9*wDP58clY)yR!rQyX=sq)y?1O8XqFjDq<{$7x&d2X5k|Uzbuz?7fz%8?4^%
zj5f0_x&a2k7B_?sXl|T2*-XzqcoK!7P*j7P
zmfr?N9zge&fr+>gb~Q=k`_yqlwvFXBDzG)%XZ{YG9+R?0CqMycHRrtkA&RdTvi;i1-7+2PZSbap!R;+_#M$0{)xJYI=dQ6s?C$b#
zK}F?(s?o?E>swV%PtT;B4i%tT5+$V02$vsvLd>Bi?6UD(1?uXZlDhx-5{;3%WbqL}
zCSD46Uz800*t>8Wl9TYYUgOHQmqu+oA46lt+Plu$CcF#%V2hln
zOZRVs!neAU;lR9HL1RP1pQwyT^y>)^clV2ah7Toh;fETd3D$ON;nhcH*`Z6n{@Z7d
zqLgEn`L1Z}K(HvRieeIb^Rw1^8F+gZhwiEGIuM7WIn{)-J+$6*2?iG02qL)eQhRr1
zDIE6<3Nt`mUw!(qm;J>x^A7ZlEOc-BF;`3T)pI<&-$Ut7lJ)W@SM9Ns%3d;+#rz3x
z((epzu(z)DSI?{OK2{@-cOKi7;C(lq`@f8;7slXQ0fNk6i#_d9DE|#vWhEFTzuyi{
z^b+=yAxh|+UI0M=Mg+==tLe!m6=E~O`nnh@-ykl5S;rwQp>7tP*Y*$n*MUi$dY{)Mt~hq@-)?!g#+^iuN7g7iyb7JnZP`N`d;X|
zJRLPK)x5|;`_t}aD?j|X^2}{Wo_zyqK`y}i>hTnB9d+utAU?cOJ0EZ^Q{i@)giil+
zyg*DnQd(Nd)-?!Qx)1fewjJUCJa|)Lr~H8zo(A){RCHl*^-;uQvgst)tGJ1J(#LxV
zNQDD_zR0T`+AHfL<+abFgna4e_6acnRfUGwDZ*K#L5=({*`ch^OcWY(y_Jo|kNkV^
z6(2?pv4ReSl1rW{&d=3H{a3=w|G?h60o@C1#qS#YM#h
zcbr;>x}FDk)aQgY-nkmY0v@bIHB$5UBQYWiB{Y^87kZg#t8mQA094CuYF0U)15dbJ
z&R#jzf{}L4@Y|p+IrOXlC&HA%)ywr`Q`70&-3+_*1)jl?KLsi(DMH!RD38uXRA5z&
zjlMM#DTT7d)wL4e`9Eo;=9>cd^<3Sj!KW6l4_g#NDKVv)rvskjzu~ByCrR{JADDg%
z?^A}|by$2k&%G$27stMecC#vB|$-|cgj0`MU
zmpG9RX62_*U^UZ^Z4myF?7*A&8z^)ytynV*-NaxPbrhT7R2Xh*F1;SIgWQnS6TnUh
zDTOYzR_uR!&)z$vvRH|eWVr=Q28e;FC-2wo$d0XKr59K8lF}s#RqSikqcQvK=Xv(@
zH=f788gP6AYTcx{TKtIm)Ev?7eUBRxi_UPWb9Z;=yS3p4%v|URbSH-e?=P25cw6uJ
z&&l5?W%lg`n5V#)G}<9P)#n=f5MluzOU!Qb7De9Wp?BCEX!l+E1T1%Oaj~oiMg--q
zc(~V{j1P`~=oG~?Ng1&XTxf6)MpUhgr~)ojAJd_vtFbab!jfJBzYV9gVTefTwucN#>wh6xh6sVhpRw&4mX|q^tqh
z{5kuFus$r{O8#R)@c#O-KgMeDuL>q6ts!UIO$>l^)O~%=1uyFNQ(WYX#F6>pXud%26l=LZdb%*pX$tHCOIAfAYR_1A6uyt-hDYh3MS;R
zd4ik33jmx&0GLKDaBcF(b}sBy%f#dw;ModPJV>+h=rfLV&Nhb5PrLZxsi}s->LdRc
z1NJvM*r||^zGpSFw`2^P_lo*$=@Kiiz?wR4Glb~48MxtZHhO&WDX6g5$l;jZz@WGq
zK;NQFM))5{m@hDUv-B%${R{f}u3H5kwT$j0U@hh7CT6{h05(8B6c6D27WzEAs^13n
zNuhfRsQ$Tdz$Uqvu^Xz*<;1HXcMlJKhMFog<}SeV$RqCO)S3DQ$k>b`zK(i(X&D({
zLL;n?BUwN(TF7yP<=(u>p&EihMiSta>2Y_m26;k4Y;iSn&!9dhb_zvCGT3nCs|@f<
znkGVKL`FeB?3EbxsjOsX4*;<+gX-?=j7XFFv@CdfOV7yDeeEVCJ?^;TRG>|YI7rf|2cQ;E|d$q&l7VQTzP-v8E2&V%;;G&{)#e&IFsZ
zWXmc+2O+6z$u-vdlGOG5z7~dt0n#|&3PqRz
z6)&$^{Kg#_T}BQyS62i}&sAyhZ}pKbwbVxaa%P${bRnd-HU#d|A*0Ax5jS3`@eK2kA&tgb;6f<
zibOTWug5IW!ut=Q!uK^so2RFyS}Zp`zYSTxDQPamdS4}M)t49EqYQ41LVspOHIBCz
z>OcIWJ3#gDZAO|tkLuGcCK-BdU2=LM(eCHFp`7%~%=+*XtWR=eEUy_9Qh_u$QSsH;wD
z{_^oseSk)^X>i{&toBD<0V*)OU#MMW>&F$y930GKt69EDiCasn7GGBt;?x*DEt2DE
zOaMT+5_`&&fi7lkB0VPAUM7dzJ>Zd}KKODI_hgm=+{b(Hq*%x7yW>EINuw?z?P6Yu
z_8yB~hWVz1i+nQ-|Q7@FwvChEsz*0nQ^uP~QY`L$_F1s~fOA{p&c5QpV
z>|*_}9(^H!bk1pDcdq$ayhE;fF++P)@Tgms7g}^W?>(as;IwfO-8bY~1a;LosdHKF
zOROV64dlPL1aM!tC&UAZq9xk0ZivCnyV!4lz0qTCWTc{yaG8pePBXWxC3hbEEM!|U
zQ6Ih*A@pawoq6#&=zU)}=IdtFhG%iL-D!{X2lf;>i?eP{D3#hHw(mgE*$QOOIy3B6
zmZy+E0~KyM{0Sp_)gGP-H}OcI>k%RKvBSWD+WQtLo_&(v
zYhzGSEqv`GTK4^{3|-tQC_682DGfWd+s?d<#ho^i(y_cii$+=atmc(xwUc%y$3cbV
zHAahbD>>Enawnud%L52dkiB&9VxDS~ahG*NWwCjJF)g85yg0cK{rad9ms;HQj8c2_
zjlF#emjhD`HZw^slfCy7bgu#3#A>z^b1N>dYk<7NL-h@8FQ$6?3{Du|pmBA1popHD
zn!3YF8*vdgr*?+uNPoRhjj9?RfzAfx0n4%wbbqX#e&Y#jb8cBu1_sJ`3G*
z$4cY@PD%#dv(S1Pv+e{HVe)~-mjw7*c+EE!6-CyDT&3|S((PHpXrW2p)5D3$)M#`u
zKh#*&XLDs`g6G29&TiQRD%@@&)6MYVkme`5T4Gc}N}&J){75k1$L+XAl|>ZbYL%fw
zba8aS)Vw@UHWLSe;ETOK4^E3aXhOzuy%Y6?m*JQ*CEhn)zAOI%H-xhdUf3fpsQBc$
z)Z)ddy`|LR{Z(UFPAPg-UFPT>h-lBg_d7BB@W~__rj2#N4}_rW-YeGCQ1tGJHz0XPsmWq
zeG+|Uu&GNk2BtoBKrBp+QRVqc0Cxk6d4?2BU=}>I$uxj}8$jb}V+bxlX(8zsx%>k9
zb!egj&I|c!m+Adynw6DR{UrRicqP`^xdi_&V#_@o15{BN07wEolOnWZB9O2HWrLb#
zQSfLRnmHwb^?WEwh_C`w2p~iP9lYNFJ4F>GL`U8ORIDyqy;7VnfX-(CA5`7c!LSNj
z>KtVZ#0?HpQ&ZF<*F}uWw>}k(?CNR{C4~<;OmO)cY$-Q11L}HN_x5)St2{tIg~}oj
zD`NQhr~#A@{jD&&QTwy5JBm>xw)*J0LkWL6nI$^y<3?9Ds!z-DgJ0Jl0f)_$?&Ri@
z>Q-)p_l2H7L7e?#S9^iM@_qj5yI)U;=0-cWTX9*;Hcx&bg|gV`Dk-
z8V(NYIYN6nO6uz0B|fklf4xdtsEwx?Dz)R;P0<+2?qmhPHfZ9MqlJ1RFmLTsF9#Uh
z(A7uU&Ji%h9r&P^a!>!!CZuv#y($-OSLvLd#*7
zTA)7g07?qUqunp60Y_7e&9*c@L}zot=I1#I88&p*FYHrG9ZNQAu=&s*0?;95c&1o)
z)b8@LT?3Cg|CKV+WkxuqTKwX2_29f7pnlZlKT^uDt@u@!$e)ft2F4XKo5ik%@eo2&;158p*31mU6If;~c8(8f7>Nn(
zPDTNxZ_8xBEUx;b&E(wRD%GE3WAbUy-xqb*T$eK=pA+ep1#or_@K*X)1!&A0AXod9
z^utT8&Upjs>^@0Lm>7?zDq>yR+LUcef(IbYmS`3v9%kYYd%U!`
z)NTfBXh#JCm+{h@Akjzv4%_A{J$^ji~uL%N+oL=BeD1TSR>#+!_p
zoClOsxkO<014zd_R2Ek|g_t;_6`zqO{LbQ61C5C{+Zhj$6Gw(6EtS3;yCRLNW^Ra)Jp7&lbwZrjr}tC
zD@!B{een#)w6`b2^#??l2?!`ET2NlZd^8x@Kc9v+B;@(MMvAgad7>
ztdQR&*vnRHXfLtj-N}_vLT(g2Z=Hd^UOIi#f6@8d-yxn|wYIjlY~x(QoC{i}UytzP
zgVu&)f_^1Fk?hwTP*Ad+sZ!`-80CP{1Bv5;)*U@juU~(j)_+E9*2`&(YFuotlw4f1
z7ChKFLxwv9&sXN=KJaP%I+Cl3lF%5%S0=Kx`g1}J!!f-&ue-#$W%;1%J%i>h@$6E?
zwN&l)sRE9s08zn{5Y33{_Nte~q_a5_3n82_S
z-NjQ^eF&hlKyuZTO&^)p8FOKR|5@AGvH*_N1M&c%`l7>+pwdSr$c-b=on{w%M3
zx=Y?$jC=(GMIO+0!q;nGJc@G~GI0R@x$FFaBC^B~2+gL9@Gai-X$inPewl>N-MHIY
zst}40VUR&?)LyhjubL>lB6Uz?gd5O}`^=4AA7XT3V=`QC>M`-Hs6O(7_s<0&df#-J
zegJyCDMdhN6M0KF-V-JCfgH8(jdIT77(L#tA7QARYm2q*4aeAqYJ%wjz6Dmew9>ds
z71WL@U%>lKu~Vsod_2&|6UrpJa7=ECt(jk3r&CkiADnqdWJ;Fgnzp47&*fmr2C+~T
zkSv}CJX?5snI)88WHMT0S+vRnkO8prokE|I+PI;)8D9n!eg=ye=wCQ`UyW2f;%5l>
zCBGqir|F2MxSEOqemCy1LkdA_7igQD0Jy`qj;%7}Z
zlD-T&mduXFiu$&`-xh%Pe-_5;QO0treRbmB8>6t?Pd6y~zLj)J?B(aBgT{p57^QA^D;
zR95~hS9SwB3}1gqYs!x*mkM@Kc;anJgvH3={m83wm+1EZPg%Pg)YUqko%!kwAaSBT
z46t>eKm0b7GG78{a5}O>lSpIVlC1Io_)33ULPxBzWgcjoy!^4h`IydAeFAum=uYkg
z(vYge@$~dK9=ldpOB)jeV>dlC}ZCvjO?TAYl_5RFe9?d
zGTE{ZipoUR(O4$?J>B~k-1GB!J?A{z=Xo!m&p9WxQ{IcV-@DjQSB|h$Bgk(SS8S^Y
z(090-K!9_(&6PpE8`U}T>sn6$Z1C2~N*aa{Oz0O3-}bC2hzu7&^pSFSW6_cuK3DdX
zeDuoE`B`Nv0RW2eA5EgzUT#$C{R;UnH$PcU(ArZ55WHWJtOo{dKH@%bR=?-4S`@wf
z(MC67T2k=(Lw`hMCEV-@-|&uLR~>visI|p%HJRcwGrqe7bpA?2bf;PM?$peTO5;;h
zj)rwm%~x^_e*E(X0OTV3Gk*Z6T%lKeEpP|QJ|h4EIOg6!zbB8f
zvn^ixmkxaJ*VOC~d^t%Rp!hw>oWvBPtRfM7cjSUa4VF$0ohPS^_0?>>30}5pkMa`!
zr#nrh7P8bB1j4>N`1XOw8kk(*-wy=GSOB2k^QiZhIM91j$3wlyJ|i43RU!)Bw)UiL
zcOw$TLV))J%BHMDe|n3x+5fU?Ltr(aCgT@f-1sCH8o$qbFksPqGh<_2Da{&N)2}K+
z-o5+ko-`v7&>I3zvNqB!WKJ3XFXC_IHF%XA;tz=jF7F-j@@Lvpjp!KSIX2+I&Ns
z&q}@Qm7>2i+#6y0)IDd6JzXb;zxnAp3AaE^jqOe{9b8g<{OBLbxAK(9T=Ah(2k4D`
zMv8^2i1&_F&-1%&BEK$pZoI&4UyM&lNnsdM(3rA(*tP9iht7viY@dBq@PR=uahxj*
z(8$~Y5qUjqcbI#-@%x8XiGAO*_Cu<+)q<{8Rqwt&O0w1rAaIW<6fAI~9{jyCx*+;l
z)Fi9~*zn~z6e}8?e`TWg-WDyRoAs_TFnEvF^YKb9pU^pg>Sie+=6CS7t2Sbl?);?%U05H^*y5C_N&v@1QOeCK_C`iQ*+GuX992;KdJaB
z+GP%`tbgjd90J(Z&{))}xO03E$Q!G+DC#q$seYmn#MWlsyjJg{`B|C|IDTC_X_+SV
zDRN6_?SVU7S(!{g-{tcMK5;{dHlaEzd3;R75BM?x2|L3D?_QP5z@n~Lk4g8+3iS7X
z^}0Kvm8xS?znF88Sfi>J1_)Y{%uu`boHF;tol$P>pHtGGC8XREczzL2*C1?kcwZfz92Zr4p*U^utDs3HXj=wjeV)|6>4uw}*8>P4OCvc?Mq6pu
z!d=ALZ`=P4^*h9r)T>#@%lfW`GC0m
z<$n81UGb%o9Ka#9vv#}h>2+&=?isFLk>xxhm?NDXsMF;*&lrz3Wa7HPpTC~FDP}fw
z(4fw(HmwlhzFk{K#iQ4+s9DbJix&V$-QUvEQYX3W?zkU+PK3vfR`2lSIbE_HaatoM
z9|EfoGhEx;EHt717gwL|kF2Y~0a`ZYZtd$DIm9F>hq1Eus&VMLdNLsSxIX)?-lR*7
zy}Gy{wN=X5rM>)Asr+JcD5W)!uzm(W%s{Pc2?jzWh+1KR4e`$k0iI=OV{1E|;ned-
ze&nSmw)GpOuL1u70P5g?Y6P$c=ef6EX_fc(4nw)Yb*A^d-vUxO<{uf1rmgPZ5mBDk
ztpI`n3);{;;4+rObK{huD3g*@;8uHdqqsKu5b#C5k(#an#!v8hiv({UA1%PrIbrrz
zjO5FQ^0{0~|G-gG|6;2j@Rhr`>+OTqUS0`g)$rWd>?9${H)7V9qfC%;rhj4
zo6s_?Uh}o;Z8I$ZriloOR)DGc!U{L1>lIA0(w&+@0lNaI{C?&5u)zffFoiZ8iv7~n
z+W-d;bXi_ugAbbZW+Q)7KLPrg@9?0_nW@<;M^8^=x%D0%5^2%*N*qxd`9H+GpGk3~
zXOgTL??$IHg2&vGtR(`<5MKah^akA`*wBCkHkhMfZAb1xZiK}`$mOckJ?C}FWA>ej
zFNpjnxG6{xeEsR^Ze}i{Z0}bAz%{_G2>Jj|)?(;8QvO$gj^Eq5ep>!7f_@sFFb4Fo
z%yXRAkL%`dCjQWPvQS4I0i0Je48(}BW^^C8KNi3D`8t#-ZX${@mDZYCe!yljFhx$~
zh3~O>luu8SQ!fK%_{nnW@xZ?f1;7um^3*ziZ`Rj%=BrZ}9hBJ4==0WH)nMIN12^Sx
zIbUWOFs>#6o`sbtEL*t?JOH3F7o$IarUxeW3P3vrX~@f6sQBYlKy{GH{${S!>v!=`
z@D1}1$*!qcVPGIDyWSXceHtDI?3wuQ_#o+kLJuHF>ri1^wfhnlXBWItC){x3
zd+~=h{a*d62A4AX(CSlD7W0Cy;^sN|-aNgJv$en9kVWV2Z}(W?qXdN#=#Ah|G?62G
z>(?m((XpeCg@>`~9IvcjEQ(>^O-H_I_J3Ek6sF#O3Qe)_`QVN|FlT$F#s}XhGF&djF0NejQsd!C7<
zQT{qOK05*~F^$mHlGj|Y(xpBEt@$o~yBg)Mm+iO0JXAEOxLcDZijS^Ft81#N9xNQ}
z>G5m(x(5}bjtg@BJb(6BVgcmW0UJWz;7Bi`(U?`chY{IWUM`PXqA)mPHl1Th^Zf&P
ziqrSj1%uN7i3S`(Kha&;e9CI-ndja1%KP<@kA*T|L?g?=xEB5YvqU23~SxLk$J1{A8
z=lv>S=CWoWBrc<7YX-4se*w^1pb_6lSu#TV4BME3(JYx2j8kpeSJI|i*MO56MRI$37wVoORM@U2{)UW@GNv*gcEqMCGV<=*AlwB`CLSE}@_P
zfFUpRgkjE5nb8OXfo(N3Y%5Eq8EHulCG^LAxxu)%#3Hn!E^w^kuF_9j8Js#B%W$kfz(I}-Hwk*VoCeY2uf*Y&aC
zg{wrT(HAY^QEEE7HXy-7Y$Xp8*?kg;!&^1WSz#U>yrs-KiJPf0e>pow9~AybN(r?W
zD~I>fZZGs{47Zg8sXr=o6pDTeE36I?NWh8|&mW3o%ww24q{lqyh&43qh>-PUhSr0M
zhXLy=#L(NyGWZ&M|-4x&BBou7_*l7L`4qpR?N+`JAqgkAg8MPA0
zczK$$i+3q;o9`+pMJdjUvKs9!mHOo8&5(ef0%I#iCRezXi-gRaVq9N7vZcLU#EQyRo0oQf(&p&m&l3Yu5J+g=eJ@G=5^%lss=7pG=Dl;_
zx>^Cw7xFq?=*#HBpoBsQ>@tS!DiOpw&jGS!+%1^+`yTwM76QWrQ8vtW%0mKNFn!rq
z*xH;P@HQNpt)-;;FU96gH%rxa`eqE1Xd?SQ@Baw8tkt3WXMQ-SEzb+uroOe0(VLsv
z3F!8Axu4`#whY~J5CUtVj|{`>b?C~9Niwk(p7g?Mao8e)socy-rx
zy14vCB|Y-28%=Ax@^WwM>-C3cYcp1S?ydex@L~3zEi7p0BW2~1&FTyMLS#a{9S?c~
zE~bGQzlp+1(Z%SNN!?t>YqnWE$X%0}k+XN~of;LP1NLdA*Oi6Y1_)%i8}COw+0zpT
z@vQ1GRH??VRLSELCf!HLtG8UfsH(nQh#8S(0NTR5aX)wjN{uBnL>wP%z*mKW!ya48
z`CA!BYNUaVxc|s+;}Ao+VtKfdKn4??V=fSVJ)>F=x8#O^Io21Vpc44kxP*}xqcyhd
zD;w4Lo26EWAe~sTRDh9u=`6;av@-Z98mBN3q^oob2cEE@qTt$DWBZ-}yFp8+#|5oq6*#LSPRXd(E>-^xh~t
z2vUyCjpOT)5tA<$g29mHkrs{d006!EP5WPJGn{X;#oEeR6(&XLyYg!)NG}k{DT{
zjC5>YY~(1@YA)>?U>?OS<7M=#6)j%0K-0+%zv-;@*vD=6!&<$&|O)hCjJ*L*R4KDF7}E+%eL1!Z&6XW($S%(!Q>W#ozN
z1%Brzf98uU8?2w+G;WHkb{o9rIqjD-8nk2KQ%|D1LyZqHef?`k&PH2(VgrXiYn&TX
zt)A0)-CfZ;M`|!u(E^xkL(TUiOLc|$vbt6oVYOmn>%s-sI1!bW1G}wYUP+KrhVji;
zpQ{NLy<{@k@zYc$AbYE*>~kHA9VOV@`>H~lAT^))#x1!7HECpGEyZCVP>C7UW1hF!
zW#|?eZeuX@9MLZ<>}W==!LN%DG_f?0D)(=M?!UyJ!&gY(mx_fA6hV?Z=I$jkFKW2w
zUh%L>;;^;izU@<2oS`m}@8{z3ljnaI?5El<&s;sKQ?y5Te)2Id-n)<%W_U4^->}W3
zrd_t8Jmbv|D$5*S<)nvmz-K>~+Q(Zq=OXeaAEeoNsLMcEH|&%=yo-NflNPh@2l|7u
zA9l;%#kAjjTL*tUJ%9MZgXabLX(xlvO{tOC5X4e)yo>O=#x1&jIuFDQ_kLLJ<6xg4
zu0+f&X&B;V988bA>{UjxdLK7jrFpJ{YF8~6ikx_zPzfuvdw!-0Vd=D*e^_3aKY`>b
zOnG%fLVuZ=KZ)AM%>OXDq|UXivoz28@Aa*Dt)Y;%LCK%dq*=)cWXnVlFGw&IE8fv7
zT_V+))-QMv?W5r337_8>(Jy>8!7cj3YQXM{s%QVzPL{>iNW2I0$EFi#nE<Ao8m$O&tw%P5Avp~@N<7GJZtVs_a0v|1eo3j4$@N!cXA
zU~RqX>xm)0W15|kj=fdSgaVCM=#id;*Nk($5ex?YEX1tmovwaU`P{AMMFeue^XWnOB5c<$f<-NdM+Qf$vU}97f))m7fZ|9qJ74d{5*^`rv9`0
zMC=Ot4XXD}3Ba>=%mys{=N0NT)R71x7OnVevfCO(T4a
z*(jd5CBE|LQRIc3#|~;3$8*zGSc`>;fh!9YV$#NC9Zlyf+)ubOx!U**PuRjmwH}rF
z9k98qeZJx%tV^~P6739aw?*0qPskQbB4+oiXc0xPd0uRo)l8SW6_t$V<>!YXkoFME
z_w3x!R0kegb7k(D&(W)qHV?2CY|38l3WP!s)C`k^b=6$|JGtqmjTwkoT~(ZD;0sYN
z&k}1J%>SERSn%PECyCVRFGMt$xN++Sp;~=6BwS`PSdHlOJCf_Qiw6vY?pNN;Ion-)
zX)@iwz@P|5P5DAW8~Q|3?;iA$NThK+a=>EFkr~p2j=kyDZ;3AX|6rK37Na8Kw^q$R
zYDt}!{lh;(V#p!k0*${%d_GN0Fe~|b@>SZ{BTYnLU@#NIbn6LU&~b6olVN^$#`c9z
zQ|9u@V5(xcgR^ItvqyYFe|qm<4Ix5dPYTLA0Vf_(r2UMHUt-DJJz}}5s;#@JJhGSq
z_Bn<$-Bo*ms`wb!LaRx{dg8kEZ~NV)rT{Sh^qQ@?0OqvsvAQ&&5VK^pD9_zFe^}l#
z#c?U!k|)L$@~>^Qu^8uiqS&xXqhs|Q0n)aNxH}6dbjFR{Y@3UNJ-BrBD;VrJr5_TE
zv`^&0^pR)3*up(Mdi(qPhmpwSb}gXg8PZ0@#Ks>m6;EoWrX9T+ZHh!{B=WE~_3dc1
z&IrAXCtEK)TX2A|gvh0bvKA#{%lRMs!d@2W+Znb+A(#?rZTI!Wx6%EcLv@ofE^FfM
z08@EIuJQlgl$|WkSCfjBV&mPdFvCpuPSslx-`<5CdKi#1<37i0I_0C7YSU#Fmq;PA
zJ)x&^8x!7&GfqB9SWh`^-$6gl2aU3giRgTH#G~b2V0a4xML&HxP9w`JT10T#+Co9=
zk1i~{B&)f2+9W-{$`zgnm`#+^7r8Zec$Zh(BAkf|NTe%kjET^D`9_wgJp53;Gs4xN
zcM6~FBy6D1{CyiBrDvkn20F{QL8O>RCJR3tr#$fCCLa$zk`8R{WE|?~o@fAr+#7e1
zw@?VIr1ju*yCaAaD7;^^WYj)Jld&hKsY`R|DEP_b(M~pmuBVc@cH4da%_8V~U)gb0
z61JRI7Xpb3%O*SaP1SRciZl-iAzT5xtM;N_@!-%BzLuAG_bt^eVEuAB3h0J0P%lx&
z7BUffnnl0wLOJ$Us+5en$wci*OsjqV+MqzE1s+Yd#J47eQo5yIm7u$MELv!$C5$?&
ze+o#s&2KF#HL}PtA^dC{Qj-VTeJX*VsKXupHUQ-*nj(bDHVdC!ygdgpxnsNg@Dz?T
zNu*P%tM7a2_qgM~p>t@598`%AI4k=w^w}`801pq(j0%C^O}CYCDF%LFzT0KZ1`RN}
z2Fg3M9lz~30SvBT*vfVohYvUXj{V9>4`LPO**ts&llN@2cBWPjGSxrN8;>bRc`uN1
zIaML2lk?ji_~y{7LI}`ZSPDn$`)%3cm7MOV6ERFYQ)4CVgaxUf`saSpV0l~$;;1j>6g8notSFPk;!u}
zXeS(yk^+0fXv@Iijmx%k+Mtf|p~LJK{kf#vkc&~u^*FdRN4hr04f}J**#>t5rMn()
zAR|CxIZ)x2r|EHH@%GN_y4lK?9tKwWeLionuMW}#U_}Y2O$`VI`ZL0&;^B!Tq1xFQ
ziClMuoYpi&-W*d1$DlSVAU;yhU1xRt@=99gUy^rm{uiM(G0Tp=w4FLq^8rItyQ?rq
z*j;KoPEqP=J}O?mk}eYycD7>_Hg@Cj`1T%0Q;*|=-MKza{QW1x8!X?Y
zQIV80wFXFg==Z^w3$%;lvj|B)slTa+95SW5J)z&@_>@K7=X_KmiXN%*+wU3egdV{)
z)G*3xYuWy1{#3*bA$M$#wNSeSymZ_(nesk=Aif43pAID9eD#q9tY8a0%qbF?jJ46aUHxcee@iizm^~Bf%MXgB|n((
z`;Ca+dPh51#Nm57LQk9XvsN6~gB^kHlsnXD-JIAri+MBwL|hEyeAIM3ZhT#Ua$JJj
z-D8o$1i5`+0}&=X@Li%^H-X9x>`}HB02y{j&9h}hhBsi}J(r}<7COJw5o(cTjW?Iw
zqCvxQwAE7FPH&WT;q&;xA2o%;g&jsRzwMh}G
z!Dqe~!Dd$}Tyc^0ydcAqmt-Rs5B{r=_KeV)h?Q2Y>%c7;iNSX#t>yA5eDzY9r|F(&
z*)Zx}a)ZTdy9b+)5$L|K8D?SVBhl}hkE&Jz?IZ@L(08^Yk#53wLxdxJp!MHzB*Qt}
zWN%pL!|3u6AK3Zn>dqO&ocD%2oY^U#tObl~9*`=tLDTgY4-59uuvGx0u*+wBN4vFca~QP85D<5k9b3DeRn1Zg-q6q
zwYEg-&&4)tp8
z>e8&1DMsBJw|V25px}az?l1zbqRO|+kQHw8ZRTd4UyC@AlF)w#r|8X6T3RaOYVpXk
zzNxwo7NOzu?)UATc3-xWBdgg&G%eyh%ttlR09DXuYA67`;NGdGH@;dVFjlghvXHJW
zMd)>=>%f_)lC&@_Pf9)>>X9#DEPsjKQyyE_Kf5K3`ET37zK*+PVWT*s+!cBDR~($V
zLvOh0RKOPAF0?-lG5tCCRq+SO4nRh!SX@n7SOU61-p&IksX!)g$jiz7xv>0|A)iMf
z_Y60{?KNk3qkv?UWfj!MUsd*C&nnLwst_$s36*3>|P@#o^9DfdSfBF(ZSPcsJe&lwIH6L|YA;vJ&>(1Up-+{yV
zv%dWE=Z_oxtp~bqh!=yc8jstQ3B&3TM;)pHW)0O`bSZrRwpnzVgl;K<(vHdx4xccJ
zUCd5MNYL_e`Jg_S)9D}{_0Qxnb`k=f-j)
zqIL;c2`rluNYfEe=aR7gLa&<)EWRM!hWJwx`x!w4m6NM?
z05K_^lq)sd)SA<@TJtvHsafgq*wH80xQShQsEz%uJoQS2QfooeA$+69Vb<
znTSKZSuY_JKUzFR++VGt@6ZP=W5V289U`Z$D+2f{>znqEG!k=Ep^l=Fis1yPLCR
zEbnsRuTYz=F%#|g|IAZ9OFHdmHqGA4e*SJWs0HQS?twpOiCC+gUK;^7X%QDecJHqt
zc$Np%y$BlY9Tk$><^KA3o8wFMvru!C!r4&u;yG-DK2?H!KNDT$%(?vJV*T6fHC-|z
z0=g?fXTNG?eGr?oSToOagOWJ
zAQPMr4*HwCsc!w&NS!GojQIEO-+G`sbhpxH))B9JR0tkofftM_6|N08Y2feA{RbZN
z)38uWP|M!<{aI#acRR=qTIT?qsUt4NuWum%T&}Ns&xx}kB$9XJFWcY+vj5=wnIb*v
zt>v%jAkFWsTG7@K?*lC=C%fswFf*H713=;IV2^9gqIW@1_-pH?6dNp|#{1bq_935P
zp~BNSo!RJ<)p*=U&EH^eo~BOhZ7wi&UZ`>o>`Ul9kwr)p}7wtKJ{g&>*;1@
z94$a1&)&{PW#lZ<*%zs^bpZYdS5KRu$dI!VTr-Z$tZ3-gG~O))Hb0=WPX#RE`6u+q
z76p^@mGimch&k8D5z&vPB-z)>A(WBrc?#E|!%L(Zm?{B?l@sghHDLGl=n@VFN`p6U^J|EeG%=se#swZ?0KUJ@e8L-byCsZ3<
zzy_2(C(ejsZgPI!TaQiYZEMMxd9*Ky$)fw4PRL7!8046IS_9nxpH{0~pH6Gh)|DQe
zjAJQ4IgYv(kNb;!zXJA!L^aZrZWcL|z9~*hP1Q;z^X8){)7WaqDLM~DVkO8+7wND)k>;3WLJNLL()jpdyvJCwlBESvhW-S8@vv
zu2YT&xIFg;FnjMTDo5zI5FkP4%x?!0{Q07O@pF_Jb-P}_FhC9#K&XL;zOF7WPt^v6-dc9B=q-Nv|G65-r}SlbSs?-RWIgb&DiBQXo^G|a
HQ}q7;A@Kf7
literal 0
HcmV?d00001
diff --git a/docs/html/topics/index.md b/docs/html/topics/index.md
index 10eb5dddaee..5f4c49f0467 100644
--- a/docs/html/topics/index.md
+++ b/docs/html/topics/index.md
@@ -14,6 +14,7 @@ authentication
caching
configuration
dependency-resolution
+more-dependency-resolution
repeatable-installs
vcs-support
```
diff --git a/docs/html/topics/more-dependency-resolution.md b/docs/html/topics/more-dependency-resolution.md
new file mode 100644
index 00000000000..e8609c986ad
--- /dev/null
+++ b/docs/html/topics/more-dependency-resolution.md
@@ -0,0 +1,160 @@
+# More on Dependency Resolution
+
+This article goes into more detail about pip's dependency resolution algorithm.
+In certain situations, pip can take a long time to determine what to install,
+and this article is intended to help readers understand what is happening
+"behind the scenes" during that process.
+
+## The dependency resolution problem
+
+The process of finding a set of packages to install, given a set of dependencies
+between them, is known to be an [NP-hard](https://en.wikipedia.org/wiki/NP-hardness)
+problem. What this means in practice is roughly that the process scales
+*extremely* badly as the size of the problem increases. So when you have a lot
+of dependencies, working out what to install will, in the worst case, take a
+very long time.
+
+The practical implication of that is that there will always be some situations
+where pip cannot determine what to install in a reasonable length of time. We
+make every effort to ensure that such situations happen rarely, but eliminating
+them altogether isn't even theoretically possible. We'll discuss what options
+yopu have if you hit a problem situation like this a little later.
+
+## Python specific issues
+
+Many algorithms for handling dependency resolution assume that you know the
+full details of the problem at the start - that is, you know all of the
+dependencies up front. Unfortunately, that is not the case for Python packages.
+With the current package index structure, dependency metadata is only available
+by downloading the package file, and extracting the data from it. And in the
+case of source distributions, the situation is even worse as the project must
+be built after being downloaded in order to determine the dependencies.
+
+Work is ongoing to try to make metadata more readily available at lower cost,
+but at the time of writing, this has not been completed.
+
+As downloading projects is a costly operation, pip cannot pre-compute the full
+dependency tree. This means that we are unable to use a number of techniques
+for solving the dependency resolution problem. In practice, we have to use a
+*backtracking algorithm*.
+
+## Dependency metadata
+
+It is worth discussing precisely what metadata is needed in order to drive the
+package resolution process. There are essentially three key pieces of
+information:
+
+* The project name
+* The release version
+* The dependencies themselves
+
+There are other pieces of data (e.g., extras, python version restrictions, wheel
+compatibility tags) which are used as well, but they do not fundamentally
+alter the process, so we will ignore them here.
+
+The most important information is the project name and version. Those two pieces
+of information identify an individual "candidate" for installation, and must
+uniquely identify such a candidate. Name and version must be available from the
+moment the candidate object is created. This is not an issue for distribution
+files (sdists and wheels) as that data is available from the filename, but for
+unpackaged source trees, pip needs to call the build backend to ask for that
+data. This is done before resolution proper starts.
+
+The dependency data is *not* requested in advance (as noted above, doing so
+would be prohibitively costly, and for a backtracking algorithm it isn't
+needed). Instead, pip requests dependency data "on demand", as the algorithm
+starts to check that particular candidate.
+
+One particular implication of the lazy fetching of dependency data is that
+often, pip *does not know* things that might be obvious to a human looking at
+the dependency tree as a whole. For example, if package A depends on version
+1.0 of package B, it's obvious to a human that there's no point in looking at
+other versions of package B. But if pip starts looking at B before it has
+considered A, it doesn't have access to A's dependency data, and so has no way
+of knowing that looking at other versions of B is wasted work. And worse still,
+pip cannot even know that there's vital information in A's dependencies.
+
+This latter point is a common theme with many cases where pip takes a long time
+to complete a resolution - there's information pip doesn't know at the point
+where it makes a "wrong" choice. Most of the heuristics added to the resolver
+to guide the algorithm are designed to guess correctly in the face of that
+lack of knowledge.
+
+## The resolver and the finder
+
+So far, we have been talking about the "resolver" as a single entity. While that
+is mostly true, the process of getting package data from an index is handled
+by another component of pip, the "finder". The finder is responsible for
+feeding candidates to the resolver, and has a key role to play in selecting
+suitable candidates.
+
+Note that the resolver is *only* relevant for packages fetched from an index.
+Candidates coming from other sources (local source directories, PEP 508
+direct URL references) do *not* go through the finder, and are merged with the
+candidates provided by the finder as part of the resolver's "provider"
+implementation.
+
+As well as determining what versions exist in the index for a given project,
+the finder selects the best distribution file to use for that candidate. This
+may be a wheel or a source distribution, and precisely what is selected is
+controlled by wheel compatibility tags, pip's options (whether to prefer binary
+or source) and metadata supplied by the index. In particular, if a file is
+marked as only being for specific Python versions, the file will be ignored by
+the finder (and the resolver may never even see that version).
+
+The finder also provides candidates for a project to the resolver in order of
+preference - the provider implements the rule that later versions are preferred
+over older versions, for example.
+
+## The resolver algorithm
+
+The resolver itself is based on a separate package, [resolvelib](https://pypi.org/project/resolvelib/).
+This implements an abstract backtracking resolution algorithm, in a way that is
+independent of the specifics of Python packages - those specifics are abstracted
+away by pip before calling the resolver.
+
+Pip's interface to resolvelib is in the form of a "provider", which is the
+interface between pip's model of packages and the resolution algorithm. The
+provider deals in "candidates" and "requirements" and implements the following
+operations:
+
+* `identify` - implements identity for candidates and requirements. It is this
+ operation that implements the rule that candidates are identified by their
+ name and version, for example.
+* `get_preference` - this provides information to the resolver to help it choose
+ which requirement to look at "next" when working through the resolution
+ process.
+* `find_matches` - given a set of constraints, determine what candidates exist
+ that satisfy them. This is essentially where the finder interacts with the
+ resolver.
+* `is_satisfied_by` - checks if a candidate satisfies a requirement. This is
+ basically the implementation of what a requirement meams.
+* `get_dependencies` - get the dependency metadata for a candidate. This is
+ the implementation of the process of getting and reading package metadata.
+
+Of these methods, the only non-trivial one is the `get_preference` method. This
+implements the heuristics used to guide the resolution, telling it which
+requirement to try to satisfy next. It's this method that is responsible for
+trying to guess which route through the dependency tree will be most productive.
+As noted above, it's doing this with limited information. See the following
+diagram
+
+
+
+When the provider is asked to choose between the red requirements (A->B and
+A->C) it doesn't know anything about the dependencies of B or C (i.e., the
+grey parts of the graph).
+
+Pip's current implementation of the provider implements `get_preference` as
+follows:
+
+* Prefer if any of the known requirements is "direct", e.g. points to an
+ explicit URL.
+* If equal, prefer if any requirement is "pinned", i.e. contains
+ operator ``===`` or ``==``.
+* If equal, calculate an approximate "depth" and resolve requirements
+ closer to the user-specified requirements first.
+* Order user-specified requirements by the order they are specified.
+* If equal, prefers "non-free" requirements, i.e. contains at least one
+ operator, such as ``>=`` or ``<``.
+* If equal, order alphabetically for consistency (helps debuggability).
From ece68dae568d0b73520a94bfdbcbfcf6c0a92b46 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Sat, 31 Jul 2021 12:30:05 +0100
Subject: [PATCH 002/730] Add new docs filetypes to manifest.in
---
MANIFEST.in | 1 +
1 file changed, 1 insertion(+)
diff --git a/MANIFEST.in b/MANIFEST.in
index f9b15403e84..266064db67e 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -24,6 +24,7 @@ exclude noxfile.py
recursive-include src/pip/_vendor *.pem
recursive-include src/pip/_vendor py.typed
recursive-include docs *.css *.py *.rst *.md
+recursive-include docs *.dot *.png
exclude src/pip/_vendor/six
exclude src/pip/_vendor/six/moves
From ebee1cecf8a27fea74c29fb9323ccca2d61bd4e3 Mon Sep 17 00:00:00 2001
From: Pieter Degroote
Date: Sun, 17 Oct 2021 19:19:52 +0200
Subject: [PATCH 003/730] Improve error message when egg-link does not match
installed location
Include the locations of the mismatched locations in the message, to
provide more context.
---
src/pip/_internal/req/req_uninstall.py | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py
index 779e93b44af..b135017907b 100644
--- a/src/pip/_internal/req/req_uninstall.py
+++ b/src/pip/_internal/req/req_uninstall.py
@@ -530,10 +530,11 @@ def from_dist(cls, dist: Distribution) -> "UninstallPathSet":
# develop egg
with open(develop_egg_link) as fh:
link_pointer = os.path.normcase(fh.readline().strip())
- assert (
- link_pointer == dist.location
- ), "Egg-link {} does not match installed location of {} (at {})".format(
- link_pointer, dist.project_name, dist.location
+ assert link_pointer == dist.location, (
+ "Egg-link located at {} and pointing to {} does not match "
+ "installed location of {} at {}".format(
+ develop_egg_link, link_pointer, dist.project_name, dist.location
+ )
)
paths_to_remove.add(develop_egg_link)
easy_install_pth = os.path.join(
From ae9c0fd8a8cf42f70534ae7ec4ae865735389eb8 Mon Sep 17 00:00:00 2001
From: Pieter Degroote
Date: Wed, 20 Oct 2021 20:24:33 +0200
Subject: [PATCH 004/730] Add news entry for improved error message
Co-authored-by: Pradyun Gedam
Co-authored-by: Tzu-ping Chung
---
news/10476.feature.rst | 1 +
1 file changed, 1 insertion(+)
create mode 100644 news/10476.feature.rst
diff --git a/news/10476.feature.rst b/news/10476.feature.rst
new file mode 100644
index 00000000000..7c2757771a7
--- /dev/null
+++ b/news/10476.feature.rst
@@ -0,0 +1 @@
+Specify egg-link location in assertion message when it does not match installed location to provide better error message for debugging.
From a57668ef12da8151a58e2438c23130a376265c37 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Mon, 11 Jul 2022 16:26:24 +0100
Subject: [PATCH 005/730] Add an option to the test suite to specify a zipapp
to test
---
tests/conftest.py | 16 +++++++++++++---
tests/lib/__init__.py | 9 ++++++++-
2 files changed, 21 insertions(+), 4 deletions(-)
diff --git a/tests/conftest.py b/tests/conftest.py
index 1cf058d7000..096ef2c898a 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -84,6 +84,12 @@ def pytest_addoption(parser: Parser) -> None:
default=None,
help="use given proxy in session network tests",
)
+ parser.addoption(
+ "--use-zipapp",
+ action="store",
+ default=None,
+ help="use given pip zipapp when running pip in tests",
+ )
def pytest_collection_modifyitems(config: Config, items: List[pytest.Function]) -> None:
@@ -487,7 +493,7 @@ def with_wheel(virtualenv: VirtualEnvironment, wheel_install: Path) -> None:
class ScriptFactory(Protocol):
def __call__(
- self, tmpdir: Path, virtualenv: Optional[VirtualEnvironment] = None
+ self, tmpdir: Path, virtualenv: Optional[VirtualEnvironment] = None, zipapp: Optional[str] = None
) -> PipTestEnvironment:
...
@@ -497,7 +503,7 @@ def script_factory(
virtualenv_factory: Callable[[Path], VirtualEnvironment], deprecated_python: bool
) -> ScriptFactory:
def factory(
- tmpdir: Path, virtualenv: Optional[VirtualEnvironment] = None
+ tmpdir: Path, virtualenv: Optional[VirtualEnvironment] = None, zipapp: Optional[str] = None,
) -> PipTestEnvironment:
if virtualenv is None:
virtualenv = virtualenv_factory(tmpdir.joinpath("venv"))
@@ -516,6 +522,8 @@ def factory(
assert_no_temp=True,
# Deprecated python versions produce an extra deprecation warning
pip_expect_warning=deprecated_python,
+ # Tell the Test Environment if we want to run pip via a zipapp
+ zipapp=zipapp,
)
return factory
@@ -523,6 +531,7 @@ def factory(
@pytest.fixture
def script(
+ request: pytest.FixtureRequest,
tmpdir: Path,
virtualenv: VirtualEnvironment,
script_factory: ScriptFactory,
@@ -533,7 +542,8 @@ def script(
test function. The returned object is a
``tests.lib.PipTestEnvironment``.
"""
- return script_factory(tmpdir.joinpath("workspace"), virtualenv)
+ zipapp = request.config.getoption("--use-zipapp")
+ return script_factory(tmpdir.joinpath("workspace"), virtualenv, zipapp)
@pytest.fixture(scope="session")
diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py
index 43624c16614..2750a552b6e 100644
--- a/tests/lib/__init__.py
+++ b/tests/lib/__init__.py
@@ -507,6 +507,7 @@ def __init__(
*args: Any,
virtualenv: VirtualEnvironment,
pip_expect_warning: bool = False,
+ zipapp: Optional[str] = None,
**kwargs: Any,
) -> None:
# Store paths related to the virtual environment
@@ -553,6 +554,9 @@ def __init__(
# (useful for Python version deprecation)
self.pip_expect_warning = pip_expect_warning
+ # The name of an (optional) zipapp to use when running pip
+ self.zipapp = zipapp
+
# Call the TestFileEnvironment __init__
super().__init__(base_path, *args, **kwargs)
@@ -698,7 +702,10 @@ def pip(
__tracebackhide__ = True
if self.pip_expect_warning:
kwargs["allow_stderr_warning"] = True
- if use_module:
+ if self.zipapp:
+ exe = "python"
+ args = (self.zipapp, ) + args
+ elif use_module:
exe = "python"
args = ("-m", "pip") + args
else:
From ef999f4c7668339a8772f75aab67c7e286673493 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Mon, 11 Jul 2022 17:18:21 +0100
Subject: [PATCH 006/730] Ignore temporary extracted copies of cacert.pem when
testing with a zipapp
---
tests/lib/__init__.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py
index 2750a552b6e..a8d74b6d44c 100644
--- a/tests/lib/__init__.py
+++ b/tests/lib/__init__.py
@@ -591,6 +591,10 @@ def __init__(
def _ignore_file(self, fn: str) -> bool:
if fn.endswith("__pycache__") or fn.endswith(".pyc"):
result = True
+ elif self.zipapp and fn.endswith("cacert.pem"):
+ # Temporary copies of cacert.pem are extracted
+ # when running from a zipapp
+ result = True
else:
result = super()._ignore_file(fn)
return result
From 9a51fc8e0c58e8d93f33141d9c1e5e154ebedd51 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Mon, 11 Jul 2022 20:01:26 +0100
Subject: [PATCH 007/730] Make the zipapp in a fixture
---
tests/conftest.py | 38 ++++++++++++++++++++++++++++++--------
1 file changed, 30 insertions(+), 8 deletions(-)
diff --git a/tests/conftest.py b/tests/conftest.py
index 096ef2c898a..0cb047625dc 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -86,9 +86,9 @@ def pytest_addoption(parser: Parser) -> None:
)
parser.addoption(
"--use-zipapp",
- action="store",
- default=None,
- help="use given pip zipapp when running pip in tests",
+ action="store_true",
+ default=False,
+ help="use a zipapp when running pip in tests",
)
@@ -493,17 +493,17 @@ def with_wheel(virtualenv: VirtualEnvironment, wheel_install: Path) -> None:
class ScriptFactory(Protocol):
def __call__(
- self, tmpdir: Path, virtualenv: Optional[VirtualEnvironment] = None, zipapp: Optional[str] = None
+ self, tmpdir: Path, virtualenv: Optional[VirtualEnvironment] = None
) -> PipTestEnvironment:
...
@pytest.fixture(scope="session")
def script_factory(
- virtualenv_factory: Callable[[Path], VirtualEnvironment], deprecated_python: bool
+ virtualenv_factory: Callable[[Path], VirtualEnvironment], deprecated_python: bool, zipapp: Optional[str]
) -> ScriptFactory:
def factory(
- tmpdir: Path, virtualenv: Optional[VirtualEnvironment] = None, zipapp: Optional[str] = None,
+ tmpdir: Path, virtualenv: Optional[VirtualEnvironment] = None,
) -> PipTestEnvironment:
if virtualenv is None:
virtualenv = virtualenv_factory(tmpdir.joinpath("venv"))
@@ -529,6 +529,29 @@ def factory(
return factory
+@pytest.fixture(scope="session")
+def zipapp(request: pytest.FixtureRequest, tmpdir_factory: pytest.TempPathFactory) -> Optional[str]:
+ """
+ If the user requested for pip to be run from a zipapp, build that zipapp
+ and return its location. If the user didn't request a zipapp, return None.
+
+ This fixture is session scoped, so the zipapp will only be created once.
+ """
+ if not request.config.getoption("--use-zipapp"):
+ return None
+
+ temp_location = tmpdir_factory.mktemp("zipapp")
+ pyz_file = temp_location / "pip.pyz"
+ # What we want to do here is `pip wheel --wheel-dir temp_location `
+ # and then build a zipapp from that wheel.
+ # TODO: Remove hard coded file
+ za = "pip-22.2.dev0.pyz"
+ import warnings
+ warnings.warn(f"Copying {za} to {pyz_file}")
+ shutil.copyfile(za, pyz_file)
+ return str(pyz_file)
+
+
@pytest.fixture
def script(
request: pytest.FixtureRequest,
@@ -542,8 +565,7 @@ def script(
test function. The returned object is a
``tests.lib.PipTestEnvironment``.
"""
- zipapp = request.config.getoption("--use-zipapp")
- return script_factory(tmpdir.joinpath("workspace"), virtualenv, zipapp)
+ return script_factory(tmpdir.joinpath("workspace"), virtualenv)
@pytest.fixture(scope="session")
From b84e5f3d9976241400e56b83b40a5c4e4c40294c Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Mon, 11 Jul 2022 23:52:44 +0100
Subject: [PATCH 008/730] Actually build the zipapp
---
tests/conftest.py | 39 ++++++++++++++++++++++++++++++++-------
1 file changed, 32 insertions(+), 7 deletions(-)
diff --git a/tests/conftest.py b/tests/conftest.py
index 0cb047625dc..aff7390f6e8 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -20,6 +20,7 @@
Union,
)
from unittest.mock import patch
+from zipfile import ZipFile
import pytest
@@ -32,6 +33,7 @@
from _pytest.config.argparsing import Parser
from setuptools.wheel import Wheel
+from pip import __file__ as pip_location
from pip._internal.cli.main import main as pip_entry_point
from pip._internal.locations import _USE_SYSCONFIG
from pip._internal.utils.temp_dir import global_tempdir_manager
@@ -529,6 +531,35 @@ def factory(
return factory
+ZIPAPP_MAIN = """\
+#!/usr/bin/env python
+
+import os
+import runpy
+import sys
+
+lib = os.path.join(os.path.dirname(__file__), "lib")
+sys.path.insert(0, lib)
+
+runpy.run_module("pip", run_name="__main__")
+"""
+
+def make_zipapp_from_pip(zipapp_name: Path) -> None:
+ pip_dir = Path(pip_location).parent
+ with zipapp_name.open("wb") as zipapp_file:
+ zipapp_file.write(b"#!/usr/bin/env python\n")
+ with ZipFile(zipapp_file, "w") as zipapp:
+ for pip_file in pip_dir.rglob("*"):
+ if pip_file.suffix == ".pyc":
+ continue
+ if pip_file.name == "__pycache__":
+ continue
+ rel_name = pip_file.relative_to(pip_dir.parent)
+ zipapp.write(pip_file, arcname=f"lib/{rel_name}")
+ zipapp.writestr("__main__.py", ZIPAPP_MAIN)
+
+
+
@pytest.fixture(scope="session")
def zipapp(request: pytest.FixtureRequest, tmpdir_factory: pytest.TempPathFactory) -> Optional[str]:
"""
@@ -542,13 +573,7 @@ def zipapp(request: pytest.FixtureRequest, tmpdir_factory: pytest.TempPathFactor
temp_location = tmpdir_factory.mktemp("zipapp")
pyz_file = temp_location / "pip.pyz"
- # What we want to do here is `pip wheel --wheel-dir temp_location `
- # and then build a zipapp from that wheel.
- # TODO: Remove hard coded file
- za = "pip-22.2.dev0.pyz"
- import warnings
- warnings.warn(f"Copying {za} to {pyz_file}")
- shutil.copyfile(za, pyz_file)
+ make_zipapp_from_pip(pyz_file)
return str(pyz_file)
From c7e7e426cb2a53127bae11492590f883db1779f4 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Tue, 12 Jul 2022 09:02:11 +0100
Subject: [PATCH 009/730] Apply black
---
tests/conftest.py | 13 +++++++++----
tests/lib/__init__.py | 2 +-
2 files changed, 10 insertions(+), 5 deletions(-)
diff --git a/tests/conftest.py b/tests/conftest.py
index aff7390f6e8..0523bdc20a3 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -502,10 +502,13 @@ def __call__(
@pytest.fixture(scope="session")
def script_factory(
- virtualenv_factory: Callable[[Path], VirtualEnvironment], deprecated_python: bool, zipapp: Optional[str]
+ virtualenv_factory: Callable[[Path], VirtualEnvironment],
+ deprecated_python: bool,
+ zipapp: Optional[str],
) -> ScriptFactory:
def factory(
- tmpdir: Path, virtualenv: Optional[VirtualEnvironment] = None,
+ tmpdir: Path,
+ virtualenv: Optional[VirtualEnvironment] = None,
) -> PipTestEnvironment:
if virtualenv is None:
virtualenv = virtualenv_factory(tmpdir.joinpath("venv"))
@@ -544,6 +547,7 @@ def factory(
runpy.run_module("pip", run_name="__main__")
"""
+
def make_zipapp_from_pip(zipapp_name: Path) -> None:
pip_dir = Path(pip_location).parent
with zipapp_name.open("wb") as zipapp_file:
@@ -559,9 +563,10 @@ def make_zipapp_from_pip(zipapp_name: Path) -> None:
zipapp.writestr("__main__.py", ZIPAPP_MAIN)
-
@pytest.fixture(scope="session")
-def zipapp(request: pytest.FixtureRequest, tmpdir_factory: pytest.TempPathFactory) -> Optional[str]:
+def zipapp(
+ request: pytest.FixtureRequest, tmpdir_factory: pytest.TempPathFactory
+) -> Optional[str]:
"""
If the user requested for pip to be run from a zipapp, build that zipapp
and return its location. If the user didn't request a zipapp, return None.
diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py
index a8d74b6d44c..be3e4c36e9a 100644
--- a/tests/lib/__init__.py
+++ b/tests/lib/__init__.py
@@ -708,7 +708,7 @@ def pip(
kwargs["allow_stderr_warning"] = True
if self.zipapp:
exe = "python"
- args = (self.zipapp, ) + args
+ args = (self.zipapp,) + args
elif use_module:
exe = "python"
args = ("-m", "pip") + args
From ea2318fbf9857834b2cc68dac960bc1536875733 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Tue, 12 Jul 2022 10:12:17 +0100
Subject: [PATCH 010/730] Minor zipapp-related fixes and skips for some tests
---
tests/functional/test_cli.py | 3 +++
tests/functional/test_completion.py | 7 ++++++-
tests/lib/test_lib.py | 4 ++++
3 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/tests/functional/test_cli.py b/tests/functional/test_cli.py
index 3e8570359bb..a1b69b72106 100644
--- a/tests/functional/test_cli.py
+++ b/tests/functional/test_cli.py
@@ -16,6 +16,9 @@
],
)
def test_entrypoints_work(entrypoint: str, script: PipTestEnvironment) -> None:
+ if script.zipapp:
+ pytest.skip("Zipapp does not include entrypoints")
+
fake_pkg = script.temp_path / "fake_pkg"
fake_pkg.mkdir()
fake_pkg.joinpath("setup.py").write_text(
diff --git a/tests/functional/test_completion.py b/tests/functional/test_completion.py
index df4afab74b8..b02cd4fa317 100644
--- a/tests/functional/test_completion.py
+++ b/tests/functional/test_completion.py
@@ -107,7 +107,12 @@ def test_completion_for_supported_shells(
Test getting completion for bash shell
"""
result = script_with_launchers.pip("completion", "--" + shell, use_module=False)
- assert completion in result.stdout, str(result.stdout)
+ actual = str(result.stdout)
+ if script_with_launchers.zipapp:
+ # The zipapp reports its name as "pip.pyz", but the expected
+ # output assumes "pip"
+ actual = actual.replace("pip.pyz", "pip")
+ assert completion in actual, actual
@pytest.fixture(scope="session")
diff --git a/tests/lib/test_lib.py b/tests/lib/test_lib.py
index 99514d5f92c..ea9baed54d3 100644
--- a/tests/lib/test_lib.py
+++ b/tests/lib/test_lib.py
@@ -41,6 +41,10 @@ def test_correct_pip_version(script: PipTestEnvironment) -> None:
"""
Check we are running proper version of pip in run_pip.
"""
+
+ if script.zipapp:
+ pytest.skip("Test relies on the pip under test being in the filesystem")
+
# output is like:
# pip PIPVERSION from PIPDIRECTORY (python PYVERSION)
result = script.pip("--version")
From f7240d8691ee99cab6a77da13a7e43c717f6eab3 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Tue, 12 Jul 2022 10:27:52 +0100
Subject: [PATCH 011/730] Add a news file
---
news/11250.feature.rst | 1 +
1 file changed, 1 insertion(+)
create mode 100644 news/11250.feature.rst
diff --git a/news/11250.feature.rst b/news/11250.feature.rst
new file mode 100644
index 00000000000..a80c54699c8
--- /dev/null
+++ b/news/11250.feature.rst
@@ -0,0 +1 @@
+Add an option to run the test suite with pip built as a zipapp.
From 81e813ac7948e9b3af7e0f3b3555405652dc7963 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Tue, 12 Jul 2022 10:28:34 +0100
Subject: [PATCH 012/730] Add testing with pip built as a zipapp to the CI
---
.github/workflows/ci.yml | 29 +++++++++++++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e467b3e50b1..439b6fabb52 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -219,6 +219,35 @@ jobs:
env:
TEMP: "R:\\Temp"
+ tests-zipapp:
+ name: tests / zipapp
+ runs-on: ubuntu-latest
+
+ needs: [pre-commit, packaging, determine-changes]
+ if: >-
+ needs.determine-changes.outputs.tests == 'true' ||
+ github.event_name != 'pull_request'
+
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-python@v2
+ with:
+ python-version: "3.10"
+
+ - name: Install Ubuntu dependencies
+ run: sudo apt-get install bzr
+
+ - run: pip install nox 'virtualenv<20' 'setuptools != 60.6.0'
+
+ # Main check
+ - name: Run integration tests
+ run: >-
+ nox -s test-3.10 --
+ -m integration
+ --verbose --numprocesses auto --showlocals
+ --durations=5
+ --use-zipapp
+
# TODO: Remove this when we add Python 3.11 to CI.
tests-importlib-metadata:
name: tests for importlib.metadata backend
From 8f0d16e267f531f6d7055746ba2dd6febbca79a0 Mon Sep 17 00:00:00 2001
From: Kai Mueller
Date: Wed, 13 Jul 2022 10:13:27 +0000
Subject: [PATCH 013/730] Clarify difference between pip-wheel and build
Closes #11235
---
docs/html/cli/pip_wheel.rst | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/docs/html/cli/pip_wheel.rst b/docs/html/cli/pip_wheel.rst
index 153d6925d4e..b78f08f0747 100644
--- a/docs/html/cli/pip_wheel.rst
+++ b/docs/html/cli/pip_wheel.rst
@@ -30,6 +30,15 @@ Description
This is now covered in :doc:`../reference/build-system/index`.
+Differences to `build`
+----------------------
+
+`build `_ is a simple tool which can among other things build
+wheels for projects using PEP 517
+``pip wheel`` can do the same but also supports projects not using PEP 517.
+In addition, it's e.g. also possible to include the dependencies of a project into the wheel.
+
+
Options
=======
From fcda0edff5e86f1f481115774a230245800b65a4 Mon Sep 17 00:00:00 2001
From: Federico
Date: Mon, 18 Jul 2022 17:32:52 +0200
Subject: [PATCH 014/730] Suggest disabling pip cache in containers
When building containers (like docker or podman) the layer system already handles the caching.
Not disabling pip's cache could result in the duplication of the size of the images.
I know this sujbject is not specially relevant to the documentation, so the comment is fairly small.
Duplication of pip's cache is a recurrent problem in many Python images
---
docs/html/topics/caching.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/html/topics/caching.md b/docs/html/topics/caching.md
index 929ac3541df..08e4acba32c 100644
--- a/docs/html/topics/caching.md
+++ b/docs/html/topics/caching.md
@@ -140,6 +140,6 @@ The {ref}`pip cache` command can be used to manage pip's cache.
pip's caching behaviour is disabled by passing the `--no-cache-dir` option.
-It is, however, recommended to **NOT** disable pip's caching. Doing so can
+It is, however, recommended to **NOT** disable pip's caching (except for building containerized appplications). Doing so can
significantly slow down pip (due to repeated operations and package builds)
and result in significantly more network usage.
From 97abdbc040d878b4962e5bc9fc2d85692a42c87e Mon Sep 17 00:00:00 2001
From: Federico
Date: Tue, 19 Jul 2022 10:26:06 +0200
Subject: [PATCH 015/730] Do not suggest caching if higher level caching
Co-authored-by: Pradyun Gedam
---
docs/html/topics/caching.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/html/topics/caching.md b/docs/html/topics/caching.md
index 08e4acba32c..2ce7d6e0044 100644
--- a/docs/html/topics/caching.md
+++ b/docs/html/topics/caching.md
@@ -140,6 +140,6 @@ The {ref}`pip cache` command can be used to manage pip's cache.
pip's caching behaviour is disabled by passing the `--no-cache-dir` option.
-It is, however, recommended to **NOT** disable pip's caching (except for building containerized appplications). Doing so can
+It is, however, recommended to **NOT** disable pip's caching unless you have caching at a higher level (eg: layered caches in container builds). Doing so can
significantly slow down pip (due to repeated operations and package builds)
and result in significantly more network usage.
From 3fda91290bb0c6c2e00556812e073f53d6f83608 Mon Sep 17 00:00:00 2001
From: Kai Mueller
Date: Tue, 19 Jul 2022 13:37:58 +0000
Subject: [PATCH 016/730] fix
---
docs/html/cli/pip_wheel.rst | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/docs/html/cli/pip_wheel.rst b/docs/html/cli/pip_wheel.rst
index b78f08f0747..82dd970aacb 100644
--- a/docs/html/cli/pip_wheel.rst
+++ b/docs/html/cli/pip_wheel.rst
@@ -34,10 +34,8 @@ Differences to `build`
----------------------
`build `_ is a simple tool which can among other things build
-wheels for projects using PEP 517
-``pip wheel`` can do the same but also supports projects not using PEP 517.
-In addition, it's e.g. also possible to include the dependencies of a project into the wheel.
-
+wheels for projects using PEP 517. It is comparable to the execution of ``pip wheel --no-deps .``.
+``pip wheel`` coveres the wheel scope of ``build`` but offers many additional features.
Options
=======
From 271ed7bb7a0b137c4f5e22ab542847607abcb2be Mon Sep 17 00:00:00 2001
From: Kai Mueller
Date: Tue, 19 Jul 2022 14:18:16 +0000
Subject: [PATCH 017/730] fix2
---
docs/html/cli/pip_wheel.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/html/cli/pip_wheel.rst b/docs/html/cli/pip_wheel.rst
index 82dd970aacb..d645f29cd2a 100644
--- a/docs/html/cli/pip_wheel.rst
+++ b/docs/html/cli/pip_wheel.rst
@@ -35,7 +35,7 @@ Differences to `build`
`build `_ is a simple tool which can among other things build
wheels for projects using PEP 517. It is comparable to the execution of ``pip wheel --no-deps .``.
-``pip wheel`` coveres the wheel scope of ``build`` but offers many additional features.
+``pip wheel`` covers the wheel scope of ``build`` but offers many additional features.
Options
=======
From 5d7a1a68c7feb75136a0fd120de54b85df105bac Mon Sep 17 00:00:00 2001
From: Klaas van Schelven
Date: Wed, 20 Jul 2022 15:55:17 +0200
Subject: [PATCH 018/730] Respect --no-index from the requirements file
See #11276
SearchScope was extended with an extra parameter to be able to pass-on the
value of no_index as we do with the other parameters. This allows us to respect
its value regardless of the order in which options are evaluated.
---
src/pip/_internal/index/collector.py | 1 +
src/pip/_internal/models/search_scope.py | 6 +++++-
src/pip/_internal/req/req_file.py | 9 ++++++---
tests/functional/test_build_env.py | 2 +-
tests/lib/__init__.py | 6 +++++-
tests/unit/resolution_resolvelib/conftest.py | 2 +-
tests/unit/test_index.py | 18 +++++++++---------
tests/unit/test_search_scope.py | 2 ++
8 files changed, 30 insertions(+), 16 deletions(-)
diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py
index 6e5dac5ad3c..0291d54f7cf 100644
--- a/src/pip/_internal/index/collector.py
+++ b/src/pip/_internal/index/collector.py
@@ -558,6 +558,7 @@ def create(
search_scope = SearchScope.create(
find_links=find_links,
index_urls=index_urls,
+ no_index=options.no_index,
)
link_collector = LinkCollector(
session=session,
diff --git a/src/pip/_internal/models/search_scope.py b/src/pip/_internal/models/search_scope.py
index e4e54c2f4c6..a64af73899d 100644
--- a/src/pip/_internal/models/search_scope.py
+++ b/src/pip/_internal/models/search_scope.py
@@ -20,13 +20,14 @@ class SearchScope:
Encapsulates the locations that pip is configured to search.
"""
- __slots__ = ["find_links", "index_urls"]
+ __slots__ = ["find_links", "index_urls", "no_index"]
@classmethod
def create(
cls,
find_links: List[str],
index_urls: List[str],
+ no_index: bool,
) -> "SearchScope":
"""
Create a SearchScope object after normalizing the `find_links`.
@@ -60,15 +61,18 @@ def create(
return cls(
find_links=built_find_links,
index_urls=index_urls,
+ no_index=no_index,
)
def __init__(
self,
find_links: List[str],
index_urls: List[str],
+ no_index: bool,
) -> None:
self.find_links = find_links
self.index_urls = index_urls
+ self.no_index = no_index
def get_formatted_locations(self) -> str:
lines = []
diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py
index 4550c72d607..06ea6f277aa 100644
--- a/src/pip/_internal/req/req_file.py
+++ b/src/pip/_internal/req/req_file.py
@@ -229,11 +229,13 @@ def handle_option_line(
if finder:
find_links = finder.find_links
index_urls = finder.index_urls
- if opts.index_url:
- index_urls = [opts.index_url]
+ no_index = finder.search_scope.no_index
if opts.no_index is True:
+ no_index = True
index_urls = []
- if opts.extra_index_urls:
+ if opts.index_url and not no_index:
+ index_urls = [opts.index_url]
+ if opts.extra_index_urls and not no_index:
index_urls.extend(opts.extra_index_urls)
if opts.find_links:
# FIXME: it would be nice to keep track of the source
@@ -253,6 +255,7 @@ def handle_option_line(
search_scope = SearchScope(
find_links=find_links,
index_urls=index_urls,
+ no_index=no_index,
)
finder.search_scope = search_scope
diff --git a/tests/functional/test_build_env.py b/tests/functional/test_build_env.py
index 6936246183c..437adb99570 100644
--- a/tests/functional/test_build_env.py
+++ b/tests/functional/test_build_env.py
@@ -41,7 +41,7 @@ def run_with_build_env(
link_collector = LinkCollector(
session=PipSession(),
- search_scope=SearchScope.create([{scratch!r}], []),
+ search_scope=SearchScope.create([{scratch!r}], [], False),
)
selection_prefs = SelectionPreferences(
allow_yanked=True,
diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py
index 8774d8bc144..d30deced1b2 100644
--- a/tests/lib/__init__.py
+++ b/tests/lib/__init__.py
@@ -87,7 +87,11 @@ def make_test_search_scope(
if index_urls is None:
index_urls = []
- return SearchScope.create(find_links=find_links, index_urls=index_urls)
+ return SearchScope.create(
+ find_links=find_links,
+ index_urls=index_urls,
+ no_index=False,
+ )
def make_test_link_collector(
diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py
index 9ef9f8c5c18..a4ee32444e2 100644
--- a/tests/unit/resolution_resolvelib/conftest.py
+++ b/tests/unit/resolution_resolvelib/conftest.py
@@ -23,7 +23,7 @@
@pytest.fixture
def finder(data: TestData) -> Iterator[PackageFinder]:
session = PipSession()
- scope = SearchScope([str(data.packages)], [])
+ scope = SearchScope([str(data.packages)], [], False)
collector = LinkCollector(session, scope)
prefs = SelectionPreferences(allow_yanked=False)
finder = PackageFinder.create(collector, prefs)
diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py
index cd3c748b7aa..78837b94e8b 100644
--- a/tests/unit/test_index.py
+++ b/tests/unit/test_index.py
@@ -593,7 +593,7 @@ def test_create__candidate_prefs(
"""
link_collector = LinkCollector(
session=PipSession(),
- search_scope=SearchScope([], []),
+ search_scope=SearchScope([], [], False),
)
selection_prefs = SelectionPreferences(
allow_yanked=True,
@@ -614,7 +614,7 @@ def test_create__link_collector(self) -> None:
"""
link_collector = LinkCollector(
session=PipSession(),
- search_scope=SearchScope([], []),
+ search_scope=SearchScope([], [], False),
)
finder = PackageFinder.create(
link_collector=link_collector,
@@ -629,7 +629,7 @@ def test_create__target_python(self) -> None:
"""
link_collector = LinkCollector(
session=PipSession(),
- search_scope=SearchScope([], []),
+ search_scope=SearchScope([], [], False),
)
target_python = TargetPython(py_version_info=(3, 7, 3))
finder = PackageFinder.create(
@@ -649,7 +649,7 @@ def test_create__target_python_none(self) -> None:
"""
link_collector = LinkCollector(
session=PipSession(),
- search_scope=SearchScope([], []),
+ search_scope=SearchScope([], [], False),
)
finder = PackageFinder.create(
link_collector=link_collector,
@@ -668,7 +668,7 @@ def test_create__allow_yanked(self, allow_yanked: bool) -> None:
"""
link_collector = LinkCollector(
session=PipSession(),
- search_scope=SearchScope([], []),
+ search_scope=SearchScope([], [], False),
)
selection_prefs = SelectionPreferences(allow_yanked=allow_yanked)
finder = PackageFinder.create(
@@ -684,7 +684,7 @@ def test_create__ignore_requires_python(self, ignore_requires_python: bool) -> N
"""
link_collector = LinkCollector(
session=PipSession(),
- search_scope=SearchScope([], []),
+ search_scope=SearchScope([], [], False),
)
selection_prefs = SelectionPreferences(
allow_yanked=True,
@@ -702,7 +702,7 @@ def test_create__format_control(self) -> None:
"""
link_collector = LinkCollector(
session=PipSession(),
- search_scope=SearchScope([], []),
+ search_scope=SearchScope([], [], False),
)
format_control = FormatControl(set(), {":all:"})
selection_prefs = SelectionPreferences(
@@ -743,7 +743,7 @@ def test_make_link_evaluator(
link_collector = LinkCollector(
session=PipSession(),
- search_scope=SearchScope([], []),
+ search_scope=SearchScope([], [], False),
)
finder = PackageFinder(
@@ -793,7 +793,7 @@ def test_make_candidate_evaluator(
)
link_collector = LinkCollector(
session=PipSession(),
- search_scope=SearchScope([], []),
+ search_scope=SearchScope([], [], False),
)
finder = PackageFinder(
link_collector=link_collector,
diff --git a/tests/unit/test_search_scope.py b/tests/unit/test_search_scope.py
index ef21c10b820..d8128341659 100644
--- a/tests/unit/test_search_scope.py
+++ b/tests/unit/test_search_scope.py
@@ -16,6 +16,7 @@ def test_get_formatted_locations_basic_auth(self) -> None:
search_scope = SearchScope(
find_links=find_links,
index_urls=index_urls,
+ no_index=False,
)
result = search_scope.get_formatted_locations()
@@ -29,6 +30,7 @@ def test_get_index_urls_locations(self) -> None:
search_scope = SearchScope(
find_links=[],
index_urls=["file://index1/", "file://index2"],
+ no_index=False,
)
req = install_req_from_line("Complex_Name")
assert req.name is not None
From 58c05735eaa773c6f5f2c344f90185131ee82f5c Mon Sep 17 00:00:00 2001
From: Klaas van Schelven
Date: Wed, 20 Jul 2022 16:01:07 +0200
Subject: [PATCH 019/730] Add news entry
---
news/11276.bugfix.rst | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 news/11276.bugfix.rst
diff --git a/news/11276.bugfix.rst b/news/11276.bugfix.rst
new file mode 100644
index 00000000000..af8f518bef4
--- /dev/null
+++ b/news/11276.bugfix.rst
@@ -0,0 +1,2 @@
+Fix ``--no-index`` when ``--index-url`` or ``--extra-index-url`` is specified
+inside a requirements file.
From 2ec509728148d4abedb59c764ea1f4d90e02cb05 Mon Sep 17 00:00:00 2001
From: Klaas van Schelven
Date: Wed, 27 Jul 2022 11:48:44 +0200
Subject: [PATCH 020/730] Add a test for 'respect --no-index'
See #11276
---
tests/unit/test_req_file.py | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py
index 8928fd1690f..1365e158415 100644
--- a/tests/unit/test_req_file.py
+++ b/tests/unit/test_req_file.py
@@ -395,6 +395,13 @@ def test_set_finder_no_index(
line_processor("--no-index", "file", 1, finder=finder)
assert finder.index_urls == []
+ def test_set_finder_no_index_is_remembered_for_later_invocations(
+ self, line_processor: LineProcessor, finder: PackageFinder
+ ) -> None:
+ line_processor("--no-index", "file", 1, finder=finder)
+ line_processor("--index-url=url", "file", 1, finder=finder)
+ assert finder.index_urls == []
+
def test_set_finder_index_url(
self, line_processor: LineProcessor, finder: PackageFinder, session: PipSession
) -> None:
From c88036882242578f464cef159ea3205c350a929a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Wed, 27 Jul 2022 19:18:15 +0200
Subject: [PATCH 021/730] Bump for development
---
src/pip/__init__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/pip/__init__.py b/src/pip/__init__.py
index 2451926bc52..a40148f008f 100644
--- a/src/pip/__init__.py
+++ b/src/pip/__init__.py
@@ -1,6 +1,6 @@
from typing import List, Optional
-__version__ = "22.2.1"
+__version__ = "22.3.dev0"
def main(args: Optional[List[str]] = None) -> int:
From 278141678e8bac4633a0589db83f13e17ebae6d7 Mon Sep 17 00:00:00 2001
From: q0w <43147888+q0w@users.noreply.github.com>
Date: Thu, 28 Jul 2022 07:36:02 +0300
Subject: [PATCH 022/730] Check if binary_executable exists
---
src/pip/_internal/utils/entrypoints.py | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/src/pip/_internal/utils/entrypoints.py b/src/pip/_internal/utils/entrypoints.py
index f292c64045b..15013693854 100644
--- a/src/pip/_internal/utils/entrypoints.py
+++ b/src/pip/_internal/utils/entrypoints.py
@@ -55,9 +55,14 @@ def get_best_invocation_for_this_pip() -> str:
if exe_are_in_PATH:
for exe_name in _EXECUTABLE_NAMES:
found_executable = shutil.which(exe_name)
- if found_executable and os.path.samefile(
- found_executable,
- os.path.join(binary_prefix, exe_name),
+ binary_executable = os.path.join(binary_prefix, exe_name)
+ if (
+ found_executable
+ and os.path.exists(binary_executable)
+ and os.path.samefile(
+ found_executable,
+ binary_executable,
+ )
):
return exe_name
From ee6c7caabdfcd0bddd9b92d05cddd8b6be7cbe10 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Thu, 28 Jul 2022 11:30:54 +0100
Subject: [PATCH 023/730] Fix test_runner_work_in_environments_with_no_pip to
work under --use-zipapp
---
tests/functional/test_pip_runner_script.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/tests/functional/test_pip_runner_script.py b/tests/functional/test_pip_runner_script.py
index 26016d45a08..f2f879b824d 100644
--- a/tests/functional/test_pip_runner_script.py
+++ b/tests/functional/test_pip_runner_script.py
@@ -12,7 +12,9 @@ def test_runner_work_in_environments_with_no_pip(
# Ensure there's no pip installed in the environment
script.pip("uninstall", "pip", "--yes", use_module=True)
- script.pip("--version", expect_error=True)
+ # We don't use script.pip to check here, as when testing a
+ # zipapp, script.pip will run pip from the zipapp.
+ script.run("python", "-c", "import pip", expect_error=True)
# The runner script should still invoke a usable pip
result = script.run("python", os.fspath(runner), "--version")
From 79cd5998aa617f772e6b905177c93cb1f55634ec Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Thu, 28 Jul 2022 12:10:50 +0100
Subject: [PATCH 024/730] Add a --python option
---
src/pip/_internal/cli/cmdoptions.py | 8 ++++++++
src/pip/_internal/cli/main_parser.py | 15 +++++++++++++++
2 files changed, 23 insertions(+)
diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py
index 47ed92779e9..84e0e783869 100644
--- a/src/pip/_internal/cli/cmdoptions.py
+++ b/src/pip/_internal/cli/cmdoptions.py
@@ -189,6 +189,13 @@ class PipOption(Option):
),
)
+python: Callable[..., Option] = partial(
+ Option,
+ "--python",
+ dest="python",
+ help="Run pip with the specified Python interpreter.",
+)
+
verbose: Callable[..., Option] = partial(
Option,
"-v",
@@ -1029,6 +1036,7 @@ def check_list_path_option(options: Values) -> None:
debug_mode,
isolated_mode,
require_virtualenv,
+ python,
verbose,
version,
quiet,
diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py
index 3666ab04ca6..8a79191c8b2 100644
--- a/src/pip/_internal/cli/main_parser.py
+++ b/src/pip/_internal/cli/main_parser.py
@@ -2,9 +2,11 @@
"""
import os
+import subprocess
import sys
from typing import List, Tuple
+from pip._internal.build_env import _get_runnable_pip
from pip._internal.cli import cmdoptions
from pip._internal.cli.parser import ConfigOptionParser, UpdatingDefaultsHelpFormatter
from pip._internal.commands import commands_dict, get_similar_commands
@@ -57,6 +59,19 @@ def parse_command(args: List[str]) -> Tuple[str, List[str]]:
# args_else: ['install', '--user', 'INITools']
general_options, args_else = parser.parse_args(args)
+ # --python
+ if general_options.python:
+ if "_PIP_RUNNING_IN_SUBPROCESS" not in os.environ:
+ pip_cmd = [
+ general_options.python,
+ _get_runnable_pip(),
+ ]
+ pip_cmd.extend(args)
+ # Block recursing indefinitely
+ os.environ["_PIP_RUNNING_IN_SUBPROCESS"] = "1"
+ proc = subprocess.run(pip_cmd)
+ sys.exit(proc.returncode)
+
# --version
if general_options.version:
sys.stdout.write(parser.version)
From 0f8243ff5e81d8f905422613ea9c0f45b120b84d Mon Sep 17 00:00:00 2001
From: q0w <43147888+q0w@users.noreply.github.com>
Date: Thu, 28 Jul 2022 14:23:38 +0300
Subject: [PATCH 025/730] Add news
---
news/11309.bugfix.rst | 1 +
1 file changed, 1 insertion(+)
create mode 100644 news/11309.bugfix.rst
diff --git a/news/11309.bugfix.rst b/news/11309.bugfix.rst
new file mode 100644
index 00000000000..f59d2516eee
--- /dev/null
+++ b/news/11309.bugfix.rst
@@ -0,0 +1 @@
+Ensure that a binary executable of pip exists when checking for a new version of pip.
From 95cf55bf185b41ce029b5349a8a8f016d6c5e8a5 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Thu, 28 Jul 2022 14:31:10 +0100
Subject: [PATCH 026/730] Add a news file
---
news/11320.feature.rst | 1 +
1 file changed, 1 insertion(+)
create mode 100644 news/11320.feature.rst
diff --git a/news/11320.feature.rst b/news/11320.feature.rst
new file mode 100644
index 00000000000..028f16c2bcf
--- /dev/null
+++ b/news/11320.feature.rst
@@ -0,0 +1 @@
+Add a ``--python`` option to specify the Python environment to be managed by pip.
From 42eae5033e33aace218186f8b76b731771268bbd Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Thu, 28 Jul 2022 15:04:35 +0100
Subject: [PATCH 027/730] More flexible handling of the --python argument
---
src/pip/_internal/cli/main_parser.py | 72 +++++++++++++++++++++++-----
1 file changed, 60 insertions(+), 12 deletions(-)
diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py
index 8a79191c8b2..967d568e22c 100644
--- a/src/pip/_internal/cli/main_parser.py
+++ b/src/pip/_internal/cli/main_parser.py
@@ -2,15 +2,17 @@
"""
import os
+import shutil
import subprocess
import sys
-from typing import List, Tuple
+from typing import List, Optional, Tuple
from pip._internal.build_env import _get_runnable_pip
from pip._internal.cli import cmdoptions
from pip._internal.cli.parser import ConfigOptionParser, UpdatingDefaultsHelpFormatter
from pip._internal.commands import commands_dict, get_similar_commands
from pip._internal.exceptions import CommandError
+from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.misc import get_pip_version, get_prog
__all__ = ["create_main_parser", "parse_command"]
@@ -47,6 +49,44 @@ def create_main_parser() -> ConfigOptionParser:
return parser
+def identify_python_interpreter(python: str) -> Optional[str]:
+ if python == "python" or python == "py":
+ # Run the active Python.
+ # We have to be very careful here, because:
+ #
+ # 1. On Unix, "python" is probably correct but there is a "py" launcher.
+ # 2. On Windows, "py" is the best option if it's present.
+ # 3. On Windows without "py", "python" might work, but it might also
+ # be the shim that launches the Windows store to allow you to install
+ # Python.
+ #
+ # We go with getting py on Windows, and if it's not present or we're
+ # on Unix, get python. We don't worry about the launcher on Unix or
+ # the installer stub on Windows.
+ py = None
+ if WINDOWS:
+ py = shutil.which("py")
+ if py is None:
+ py = shutil.which("python")
+ if py:
+ return py
+
+ # TODO: On Windows, `--python .venv/Scripts/python` won't pass the
+ # exists() check (no .exe extension supplied). But it's pretty
+ # obvious what the user intends. Should we allow this?
+ if os.path.exists(python):
+ if not os.path.isdir(python):
+ return python
+ # Might be a virtual environment
+ for exe in ("bin/python", "Scripts/python.exe"):
+ py = os.path.join(python, exe)
+ if os.path.exists(py):
+ return py
+
+ # Could not find the interpreter specified
+ return None
+
+
def parse_command(args: List[str]) -> Tuple[str, List[str]]:
parser = create_main_parser()
@@ -60,17 +100,25 @@ def parse_command(args: List[str]) -> Tuple[str, List[str]]:
general_options, args_else = parser.parse_args(args)
# --python
- if general_options.python:
- if "_PIP_RUNNING_IN_SUBPROCESS" not in os.environ:
- pip_cmd = [
- general_options.python,
- _get_runnable_pip(),
- ]
- pip_cmd.extend(args)
- # Block recursing indefinitely
- os.environ["_PIP_RUNNING_IN_SUBPROCESS"] = "1"
- proc = subprocess.run(pip_cmd)
- sys.exit(proc.returncode)
+ if general_options.python and "_PIP_RUNNING_IN_SUBPROCESS" not in os.environ:
+ # Re-invoke pip using the specified Python interpreter
+ interpreter = identify_python_interpreter(general_options.python)
+ if interpreter is None:
+ raise CommandError(
+ f"Could not locate Python interpreter {general_options.python}"
+ )
+
+ pip_cmd = [
+ interpreter,
+ _get_runnable_pip(),
+ ]
+ pip_cmd.extend(args)
+
+ # Set a flag so the child doesn't re-invoke itself, causing
+ # an infinite loop.
+ os.environ["_PIP_RUNNING_IN_SUBPROCESS"] = "1"
+ proc = subprocess.run(pip_cmd)
+ sys.exit(proc.returncode)
# --version
if general_options.version:
From 78e7ea88e98a66a5e0d8dd6574ad3323e13c1a8e Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Fri, 29 Jul 2022 09:37:29 +0100
Subject: [PATCH 028/730] Make get_runnable_pip public
---
src/pip/_internal/build_env.py | 4 ++--
src/pip/_internal/cli/main_parser.py | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py
index 6d4f6a56eb7..6213eedd14a 100644
--- a/src/pip/_internal/build_env.py
+++ b/src/pip/_internal/build_env.py
@@ -39,7 +39,7 @@ def __init__(self, path: str) -> None:
self.lib_dirs = get_prefixed_libs(path)
-def _get_runnable_pip() -> str:
+def get_runnable_pip() -> str:
"""Get a file to pass to a Python executable, to run the currently-running pip.
This is used to run a pip subprocess, for installing requirements into the build
@@ -194,7 +194,7 @@ def install_requirements(
if not requirements:
return
self._install_requirements(
- _get_runnable_pip(),
+ get_runnable_pip(),
finder,
requirements,
prefix,
diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py
index 967d568e22c..61dc42a1298 100644
--- a/src/pip/_internal/cli/main_parser.py
+++ b/src/pip/_internal/cli/main_parser.py
@@ -7,7 +7,7 @@
import sys
from typing import List, Optional, Tuple
-from pip._internal.build_env import _get_runnable_pip
+from pip._internal.build_env import get_runnable_pip
from pip._internal.cli import cmdoptions
from pip._internal.cli.parser import ConfigOptionParser, UpdatingDefaultsHelpFormatter
from pip._internal.commands import commands_dict, get_similar_commands
@@ -110,7 +110,7 @@ def parse_command(args: List[str]) -> Tuple[str, List[str]]:
pip_cmd = [
interpreter,
- _get_runnable_pip(),
+ get_runnable_pip(),
]
pip_cmd.extend(args)
From 24c22a3e5d0ad30dcb6fabf68047185496bd21d8 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Fri, 29 Jul 2022 09:44:14 +0100
Subject: [PATCH 029/730] Check the argument to --python is executable
---
src/pip/_internal/cli/main_parser.py | 21 ++++++++++++---------
1 file changed, 12 insertions(+), 9 deletions(-)
diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py
index 61dc42a1298..6502c567794 100644
--- a/src/pip/_internal/cli/main_parser.py
+++ b/src/pip/_internal/cli/main_parser.py
@@ -71,17 +71,20 @@ def identify_python_interpreter(python: str) -> Optional[str]:
if py:
return py
- # TODO: On Windows, `--python .venv/Scripts/python` won't pass the
- # exists() check (no .exe extension supplied). But it's pretty
- # obvious what the user intends. Should we allow this?
+ # If the named file exists, and is executable, use it.
+ # If it's a directory, assume it's a virtual environment and
+ # look for the environment's Python executable.
if os.path.exists(python):
- if not os.path.isdir(python):
+ # Do the directory check first because directories can be executable
+ if os.path.isdir(python):
+ # bin/python for Unix, Scripts/python.exe for Windows
+ # Try both in case of odd cases like cygwin.
+ for exe in ("bin/python", "Scripts/python.exe"):
+ py = os.path.join(python, exe)
+ if os.path.exists(py):
+ return py
+ elif os.access(python, os.X_OK):
return python
- # Might be a virtual environment
- for exe in ("bin/python", "Scripts/python.exe"):
- py = os.path.join(python, exe)
- if os.path.exists(py):
- return py
# Could not find the interpreter specified
return None
From 3ebcc7122c4867b8f62b6ace6bbd34c12aaa4ec8 Mon Sep 17 00:00:00 2001
From: Pradyun Gedam
Date: Fri, 29 Jul 2022 12:53:18 +0100
Subject: [PATCH 030/730] Add note to divert away from adding content to user
guide
This guide is being broken up, and multiple folks have now tried to add
content to it instead of adding dedicated pages for it.
This note should help direct contributors away from adding more content
on this page, to stop the bleeding and avoid regressing on the amount of
content we'll have to move out of this page later.
---
docs/html/user_guide.rst | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst
index 70a28ab9988..4fbbd9eae00 100644
--- a/docs/html/user_guide.rst
+++ b/docs/html/user_guide.rst
@@ -2,6 +2,15 @@
User Guide
==========
+.. Hello there!
+
+ If you're thinking of adding content to this page... please take a moment
+ to consider if this content can live on its own, within a topic guide or a
+ reference page.
+
+ There is active effort being put toward *reducing* the amount of content on
+ this specific page (https://github.com/pypa/pip/issues/9475) and moving it
+ into more focused single-page documents that cover that specific topic.
Running pip
===========
From dc1ea04e9210a5c7c3a9d9a89fa4e9a57b581aee Mon Sep 17 00:00:00 2001
From: Pradyun Gedam
Date: Fri, 29 Jul 2022 12:39:40 +0100
Subject: [PATCH 031/730] Add a dedicated topic page for HTTPS certificates
This makes further progress on moving content into dedeicated topic
pages, away from dumping it into `pip install`'s documentation or
as a part of the user guide.
---
docs/html/cli/pip_install.rst | 14 +----
docs/html/topics/https-certificates.md | 71 +++++++++++++++++++++++
docs/html/topics/index.md | 1 +
docs/html/user_guide.rst | 78 +-------------------------
4 files changed, 78 insertions(+), 86 deletions(-)
create mode 100644 docs/html/topics/https-certificates.md
diff --git a/docs/html/cli/pip_install.rst b/docs/html/cli/pip_install.rst
index 384d393fdd7..7c17c264a30 100644
--- a/docs/html/cli/pip_install.rst
+++ b/docs/html/cli/pip_install.rst
@@ -219,18 +219,10 @@ details) is selected.
See the :ref:`pip install Examples`.
+.. _`0-ssl certificate verification`:
+.. rubric:: SSL Certificate Verification
-.. _`SSL Certificate Verification`:
-
-SSL Certificate Verification
-----------------------------
-
-Starting with v1.3, pip provides SSL certificate verification over HTTP, to
-prevent man-in-the-middle attacks against PyPI downloads. This does not use
-the system certificate store but instead uses a bundled CA certificate
-store. The default bundled CA certificate store certificate store may be
-overridden by using ``--cert`` option or by using ``PIP_CERT``,
-``REQUESTS_CA_BUNDLE``, or ``CURL_CA_BUNDLE`` environment variables.
+This is now covered in :doc:`../topics/https-certificates`.
.. _`0-caching`:
.. rubric:: Caching
diff --git a/docs/html/topics/https-certificates.md b/docs/html/topics/https-certificates.md
new file mode 100644
index 00000000000..b42c463e6cc
--- /dev/null
+++ b/docs/html/topics/https-certificates.md
@@ -0,0 +1,71 @@
+(SSL Certificate Verification)=
+
+# HTTPS Certificates
+
+```{versionadded} 1.3
+
+```
+
+By default, pip will perform SSL certificate verification for network
+connections it makes over HTTPS. These serve to prevent man-in-the-middle
+attacks against package downloads. This does not use the system certificate
+store but, instead, uses a bundled CA certificate store from {pypi}`certifi`.
+
+## Using a specific certificate store
+
+The `--cert` option (and the corresponding `PIP_CERT` environment variable)
+allow users to specify a different certificate store/bundle for pip to use. It
+is also possible to use `REQUESTS_CA_BUNDLE` or `CURL_CA_BUNDLE` environment
+variables.
+
+## Using system certificate stores
+
+```{versionadded} 22.2
+Experimental support, behind `--use-feature=truststore`.
+```
+
+It is possible to use the system trust store, instead of the bundled certifi
+certificates for verifying HTTPS certificates. This approach will typically
+support corporate proxy certificates without additional configuration.
+
+In order to use system trust stores, you need to:
+
+- Use Python 3.10 or newer.
+- Install the {pypi}`truststore` package, in the Python environment you're
+ running pip in.
+
+ This is typically done by installing this package using a system package
+ manager or by using pip in {ref}`Hash-checking mode` for this package and
+ trusting the network using the `--trusted-host` flag.
+
+ ```{pip-cli}
+ $ python -m pip install truststore
+ [...]
+ $ python -m pip install SomePackage --use-feature=truststore
+ [...]
+ Successfully installed SomePackage
+ ```
+
+### When to use
+
+You should try using system trust stores when there is a custom certificate
+chain configured for your system that pip isn't aware of. Typically, this
+situation will manifest with an `SSLCertVerificationError` with the message
+"certificate verify failed: unable to get local issuer certificate":
+
+```{pip-cli}
+$ pip install -U SomePackage
+[...]
+ SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (\_ssl.c:997)'))) - skipping
+```
+
+This error means that OpenSSL wasn't able to find a trust anchor to verify the
+chain against. Using system trust stores instead of certifi will likely solve
+this issue.
+
+If you encounter a TLS/SSL error when using the `truststore` feature you should
+open an issue on the [truststore GitHub issue tracker] instead of pip's issue
+tracker. The maintainers of truststore will help diagnose and fix the issue.
+
+[truststore github issue tracker]:
+ https://github.com/sethmlarson/truststore/issues
diff --git a/docs/html/topics/index.md b/docs/html/topics/index.md
index 011205a111d..eb2b5f54d5b 100644
--- a/docs/html/topics/index.md
+++ b/docs/html/topics/index.md
@@ -14,6 +14,7 @@ authentication
caching
configuration
dependency-resolution
+https-certificates
local-project-installs
repeatable-installs
secure-installs
diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst
index 70a28ab9988..b31ca4f608a 100644
--- a/docs/html/user_guide.rst
+++ b/docs/html/user_guide.rst
@@ -1138,79 +1138,7 @@ announcements on the `low-traffic packaging announcements list`_ and
.. _the official Python blog: https://blog.python.org/
.. _Python Windows launcher: https://docs.python.org/3/using/windows.html#launcher
-Using system trust stores for verifying HTTPS
-=============================================
+.. _`0-using-system-trust-stores-for-verifying-https`:
+.. rubric:: Using system trust stores for verifying HTTPS
-pip 22.2 added **experimental** support for using system trust stores to verify HTTPS certificates
-instead of certifi. Using system trust stores has advantages over certifi like automatically supporting
-corporate proxy certificates without additional configuration.
-
-In order to use system trust stores you must be using Python 3.10+ and install the package `truststore`_ from PyPI.
-
-.. tab:: Unix/macOS
-
- .. code-block:: console
-
- # Requires Python 3.10 or later
- $ python --version
- Python 3.10.4
-
- # Install the 'truststore' package from PyPI
- $ python -m pip install truststore
- [...]
-
- # Use '--use-feature=truststore' flag to enable
- $ python -m pip install SomePackage --use-feature=truststore
- [...]
- Successfully installed SomePackage
-
-.. tab:: Windows
-
- .. code-block:: console
-
- # Requires Python 3.10 or later
- C:\> py --version
- Python 3.10.4
-
- # Install the 'truststore' package from PyPI
- C:\> py -m pip install truststore
- [...]
-
- # Use '--use-feature=truststore' flag to enable
- C:\> py -m pip install SomePackage --use-feature=truststore
- [...]
- Successfully installed SomePackage
-
-When to use system trust stores
--------------------------------
-
-You should try using system trust stores when there is a custom certificate chain configured for your
-system that pip isn't aware of. Typically this situation will manifest with an ``SSLCertVerificationError``
-with the message "certificate verify failed: unable to get local issuer certificate":
-
-.. code-block:: console
-
- $ python -m pip install -U SomePackage
-
- [...]
-
- Could not fetch URL https://pypi.org/simple/SomePackage/:
- There was a problem confirming the ssl certificate:
-
- [...]
-
- (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED]
- certificate verify failed: unable to get local issuer certificate (_ssl.c:997)'))) - skipping
-
-This error means that OpenSSL wasn't able to find a trust anchor to verify the chain against.
-Using system trust stores instead of certifi will likely solve this issue.
-
-Follow up
----------
-
-If you encounter a TLS/SSL error when using the ``truststore`` feature you should open an issue
-on the `truststore GitHub issue tracker`_ instead of pip's issue tracker. The maintainers of truststore
-will help diagnose and fix the issue.
-
-.. _truststore: https://truststore.readthedocs.io
-.. _truststore GitHub issue tracker: https://github.com/sethmlarson/truststore/issues
+This is now covered in :doc:`topics/https-certificates`.
From 89983e9ad923826217cf06f30f7c52f36c1b6069 Mon Sep 17 00:00:00 2001
From: Pradyun Gedam
Date: Wed, 8 Dec 2021 18:44:11 +0000
Subject: [PATCH 032/730] Use `shell=True` for opening the editor with `pip
config edit`
This makes the behavior compatible with git and other tools that invoke
the editor in this manner.
---
news/10716.feature.rst | 1 +
src/pip/_internal/commands/configuration.py | 8 +++++++-
2 files changed, 8 insertions(+), 1 deletion(-)
create mode 100644 news/10716.feature.rst
diff --git a/news/10716.feature.rst b/news/10716.feature.rst
new file mode 100644
index 00000000000..ef09e1b8f58
--- /dev/null
+++ b/news/10716.feature.rst
@@ -0,0 +1 @@
+Use ``shell=True`` for opening the editor with ``pip config edit``.
diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py
index e3837325986..84b134e490b 100644
--- a/src/pip/_internal/commands/configuration.py
+++ b/src/pip/_internal/commands/configuration.py
@@ -228,9 +228,15 @@ def open_in_editor(self, options: Values, args: List[str]) -> None:
fname = self.configuration.get_file_to_edit()
if fname is None:
raise PipError("Could not determine appropriate file.")
+ elif '"' in fname:
+ # This shouldn't happen, unless we see a username like that.
+ # If that happens, we'd appreciate a pull request fixing this.
+ raise PipError(
+ f'Can not open an editor for a file name containing "\n{fname}'
+ )
try:
- subprocess.check_call([editor, fname])
+ subprocess.check_call(f'{editor} "{fname}"', shell=True)
except FileNotFoundError as e:
if not e.filename:
e.filename = editor
From 50eb337a0f04960c33a9eb8da20877aa718f7ff6 Mon Sep 17 00:00:00 2001
From: Brett Rosen
Date: Fri, 29 Jul 2022 16:53:32 -0400
Subject: [PATCH 033/730] Ensure that removing shim in older setuptools does
not error
---
news/11314.bugfix.rst | 1 +
src/pip/_internal/locations/_distutils.py | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
create mode 100644 news/11314.bugfix.rst
diff --git a/news/11314.bugfix.rst b/news/11314.bugfix.rst
new file mode 100644
index 00000000000..02d78dc47ff
--- /dev/null
+++ b/news/11314.bugfix.rst
@@ -0,0 +1 @@
+Avoid ``AttributeError`` when removing the setuptools-provided ``_distutils_hack`` and it is missing its implementation.
diff --git a/src/pip/_internal/locations/_distutils.py b/src/pip/_internal/locations/_distutils.py
index 1b8b42606d7..fbcb04f488f 100644
--- a/src/pip/_internal/locations/_distutils.py
+++ b/src/pip/_internal/locations/_distutils.py
@@ -11,7 +11,7 @@
# rationale for why this is done within pip.
try:
__import__("_distutils_hack").remove_shim()
-except ImportError:
+except (ImportError, AttributeError):
pass
import logging
From db4751595867db1d938df7183a60dfb15fa0d708 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Fri, 29 Jul 2022 22:35:47 +0200
Subject: [PATCH 034/730] Import distutils only if needed, but sooner
---
news/11319.bugfix.rst | 1 +
src/pip/_internal/locations/__init__.py | 14 ++++++--------
2 files changed, 7 insertions(+), 8 deletions(-)
create mode 100644 news/11319.bugfix.rst
diff --git a/news/11319.bugfix.rst b/news/11319.bugfix.rst
new file mode 100644
index 00000000000..31cd2a34b0b
--- /dev/null
+++ b/news/11319.bugfix.rst
@@ -0,0 +1 @@
+Fix import error when reinstalling pip in user site.
diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py
index 3f6f0a58e16..60afe0a73b8 100644
--- a/src/pip/_internal/locations/__init__.py
+++ b/src/pip/_internal/locations/__init__.py
@@ -60,6 +60,12 @@ def _should_use_sysconfig() -> bool:
_USE_SYSCONFIG = _should_use_sysconfig()
+if not _USE_SYSCONFIG:
+ # Import distutils lazily to avoid deprecation warnings,
+ # but import it soon enough that it is in memory and available during
+ # a pip reinstall.
+ from . import _distutils
+
# Be noisy about incompatibilities if this platforms "should" be using
# sysconfig, but is explicitly opting out and using distutils instead.
if _USE_SYSCONFIG_DEFAULT and not _USE_SYSCONFIG:
@@ -241,8 +247,6 @@ def get_scheme(
if _USE_SYSCONFIG:
return new
- from . import _distutils
-
old = _distutils.get_scheme(
dist_name,
user=user,
@@ -407,8 +411,6 @@ def get_bin_prefix() -> str:
if _USE_SYSCONFIG:
return new
- from . import _distutils
-
old = _distutils.get_bin_prefix()
if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="bin_prefix"):
_log_context()
@@ -442,8 +444,6 @@ def get_purelib() -> str:
if _USE_SYSCONFIG:
return new
- from . import _distutils
-
old = _distutils.get_purelib()
if _looks_like_deb_system_dist_packages(old):
return old
@@ -488,8 +488,6 @@ def get_prefixed_libs(prefix: str) -> List[str]:
if _USE_SYSCONFIG:
return _deduplicated(new_pure, new_plat)
- from . import _distutils
-
old_pure, old_plat = _distutils.get_prefixed_libs(prefix)
old_lib_paths = _deduplicated(old_pure, old_plat)
From 01e122ed4125673c77bfd2aced75e70f6dfa2a7c Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Fri, 29 Jul 2022 10:42:34 +0100
Subject: [PATCH 035/730] Add tests
---
tests/functional/test_python_option.py | 33 ++++++++++++++++++++++++++
tests/unit/test_cmdoptions.py | 31 ++++++++++++++++++++++++
2 files changed, 64 insertions(+)
create mode 100644 tests/functional/test_python_option.py
diff --git a/tests/functional/test_python_option.py b/tests/functional/test_python_option.py
new file mode 100644
index 00000000000..4fafde2a8b2
--- /dev/null
+++ b/tests/functional/test_python_option.py
@@ -0,0 +1,33 @@
+import json
+import os
+from pathlib import Path
+from venv import EnvBuilder
+
+from tests.lib import PipTestEnvironment, TestData
+
+
+def test_python_interpreter(
+ script: PipTestEnvironment,
+ tmpdir: Path,
+ shared_data: TestData,
+) -> None:
+ env_path = os.fsdecode(tmpdir / "venv")
+ env = EnvBuilder(with_pip=False)
+ env.create(env_path)
+
+ result = script.pip("--python", env_path, "list", "--format=json")
+ assert json.loads(result.stdout) == []
+ script.pip(
+ "--python",
+ env_path,
+ "install",
+ "-f",
+ shared_data.find_links,
+ "--no-index",
+ "simplewheel==1.0",
+ )
+ result = script.pip("--python", env_path, "list", "--format=json")
+ assert json.loads(result.stdout) == [{"name": "simplewheel", "version": "1.0"}]
+ script.pip("--python", env_path, "uninstall", "simplewheel", "--yes")
+ result = script.pip("--python", env_path, "list", "--format=json")
+ assert json.loads(result.stdout) == []
diff --git a/tests/unit/test_cmdoptions.py b/tests/unit/test_cmdoptions.py
index 1e5ef995cd0..d5b4813822f 100644
--- a/tests/unit/test_cmdoptions.py
+++ b/tests/unit/test_cmdoptions.py
@@ -1,8 +1,12 @@
+import os
+from pathlib import Path
from typing import Optional, Tuple
+from venv import EnvBuilder
import pytest
from pip._internal.cli.cmdoptions import _convert_python_version
+from pip._internal.cli.main_parser import identify_python_interpreter
@pytest.mark.parametrize(
@@ -29,3 +33,30 @@ def test_convert_python_version(
) -> None:
actual = _convert_python_version(value)
assert actual == expected, f"actual: {actual!r}"
+
+
+def test_identify_python_interpreter_py(monkeypatch: pytest.MonkeyPatch) -> None:
+ def which(cmd: str) -> str:
+ assert cmd == "py" or cmd == "python"
+ return "dummy_value"
+
+ monkeypatch.setattr("shutil.which", which)
+ assert identify_python_interpreter("py") == "dummy_value"
+ assert identify_python_interpreter("python") == "dummy_value"
+
+
+def test_identify_python_interpreter_venv(tmpdir: Path) -> None:
+ env_path = tmpdir / "venv"
+ env = EnvBuilder(with_pip=False)
+ env.create(env_path)
+
+ # Passing a virtual environment returns the Python executable
+ interp = identify_python_interpreter(os.fsdecode(env_path))
+ assert interp is not None
+ assert Path(interp).exists()
+
+ # Passing an executable returns it
+ assert identify_python_interpreter(interp) == interp
+
+ # Passing a non-existent file returns None
+ assert identify_python_interpreter(str(tmpdir / "nonexistent")) is None
From b1eb91204e0673a61d9a3203a49675be3307abd2 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Fri, 29 Jul 2022 10:57:00 +0100
Subject: [PATCH 036/730] Added documentation
---
docs/html/user_guide.rst | 43 ++++++++++++++++++++++++++++++++++++++++
news/11320.feature.rst | 3 ++-
2 files changed, 45 insertions(+), 1 deletion(-)
diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst
index 70a28ab9988..1f1a8660627 100644
--- a/docs/html/user_guide.rst
+++ b/docs/html/user_guide.rst
@@ -782,6 +782,49 @@ This is now covered in :doc:`../topics/repeatable-installs`.
This is now covered in :doc:`../topics/dependency-resolution`.
+.. _`Managing a different Python interpreter`:
+
+Managing a different Python interpreter
+=======================================
+
+Occasionally, you may want to use pip to manage a Python installation other than
+the one pip is installed into. In this case, you can use the ``--python`` option
+to specify the interpreter you want to manage. This option can take one of three
+values:
+
+#. The path to a Python executable.
+#. The path to a virtual environment.
+#. Either "py" or "python", referring to the currently active Python interpreter.
+
+In all 3 cases, pip will run exactly as if it had been invoked from that Python
+environment.
+
+One example of where this might be useful is to manage a virtual environment
+that does not have pip installed.
+
+.. tab:: Unix/macOS
+
+ .. code-block:: console
+
+ $ python -m venv .venv --without-pip
+ $ python -m pip --python .venv install SomePackage
+ [...]
+ Successfully installed SomePackage
+
+.. tab:: Windows
+
+ .. code-block:: console
+
+ C:\> py -m venv .venv --without-pip
+ C:\> py -m pip --python .venv install SomePackage
+ [...]
+ Successfully installed SomePackage
+
+You could also use ``--python .venv/bin/python`` (or on Windows,
+``--python .venv\Scripts\python.exe``) if you wanted to be explicit, but the
+virtual environment name is shorter and works exactly the same.
+
+
.. _`Using pip from your program`:
Using pip from your program
diff --git a/news/11320.feature.rst b/news/11320.feature.rst
index 028f16c2bcf..843eac7c9f4 100644
--- a/news/11320.feature.rst
+++ b/news/11320.feature.rst
@@ -1 +1,2 @@
-Add a ``--python`` option to specify the Python environment to be managed by pip.
+Add a ``--python`` option to allow pip to manage Python environments other
+than the one pip is installed in.
From f86a140b124bef3c4a6beeaa5d6fff1a4cd62a18 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Fri, 29 Jul 2022 16:59:31 +0100
Subject: [PATCH 037/730] Move docs to a topic
---
docs/html/topics/index.md | 1 +
docs/html/topics/python-option.md | 38 +++++++++++++++++++++++++++
docs/html/user_guide.rst | 43 -------------------------------
3 files changed, 39 insertions(+), 43 deletions(-)
create mode 100644 docs/html/topics/python-option.md
diff --git a/docs/html/topics/index.md b/docs/html/topics/index.md
index 011205a111d..c5e4d36c95f 100644
--- a/docs/html/topics/index.md
+++ b/docs/html/topics/index.md
@@ -18,4 +18,5 @@ local-project-installs
repeatable-installs
secure-installs
vcs-support
+python-option
```
diff --git a/docs/html/topics/python-option.md b/docs/html/topics/python-option.md
new file mode 100644
index 00000000000..242784dfbe8
--- /dev/null
+++ b/docs/html/topics/python-option.md
@@ -0,0 +1,38 @@
+# Managing a different Python interpreter
+
+
+Occasionally, you may want to use pip to manage a Python installation other than
+the one pip is installed into. In this case, you can use the `--python` option
+to specify the interpreter you want to manage. This option can take one of three
+values:
+
+1. The path to a Python executable.
+2. The path to a virtual environment.
+3. Either "py" or "python", referring to the currently active Python interpreter.
+
+In all 3 cases, pip will run exactly as if it had been invoked from that Python
+environment.
+
+One example of where this might be useful is to manage a virtual environment
+that does not have pip installed.
+
+````{tab} Unix/macOS
+```{code-block} console
+$ python -m venv .venv --without-pip
+$ python -m pip --python .venv install SomePackage
+[...]
+Successfully installed SomePackage
+```
+````
+````{tab} Windows
+```{code-block} console
+C:\> py -m venv .venv --without-pip
+C:\> py -m pip --python .venv install SomePackage
+[...]
+Successfully installed SomePackage
+```
+````
+
+You could also use `--python .venv/bin/python` (or on Windows,
+`--python .venv\Scripts\python.exe`) if you wanted to be explicit, but the
+virtual environment name is shorter and works exactly the same.
diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst
index 1f1a8660627..70a28ab9988 100644
--- a/docs/html/user_guide.rst
+++ b/docs/html/user_guide.rst
@@ -782,49 +782,6 @@ This is now covered in :doc:`../topics/repeatable-installs`.
This is now covered in :doc:`../topics/dependency-resolution`.
-.. _`Managing a different Python interpreter`:
-
-Managing a different Python interpreter
-=======================================
-
-Occasionally, you may want to use pip to manage a Python installation other than
-the one pip is installed into. In this case, you can use the ``--python`` option
-to specify the interpreter you want to manage. This option can take one of three
-values:
-
-#. The path to a Python executable.
-#. The path to a virtual environment.
-#. Either "py" or "python", referring to the currently active Python interpreter.
-
-In all 3 cases, pip will run exactly as if it had been invoked from that Python
-environment.
-
-One example of where this might be useful is to manage a virtual environment
-that does not have pip installed.
-
-.. tab:: Unix/macOS
-
- .. code-block:: console
-
- $ python -m venv .venv --without-pip
- $ python -m pip --python .venv install SomePackage
- [...]
- Successfully installed SomePackage
-
-.. tab:: Windows
-
- .. code-block:: console
-
- C:\> py -m venv .venv --without-pip
- C:\> py -m pip --python .venv install SomePackage
- [...]
- Successfully installed SomePackage
-
-You could also use ``--python .venv/bin/python`` (or on Windows,
-``--python .venv\Scripts\python.exe``) if you wanted to be explicit, but the
-virtual environment name is shorter and works exactly the same.
-
-
.. _`Using pip from your program`:
Using pip from your program
From b5afdd604831f985427880537d37eb7a35addaa1 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Fri, 29 Jul 2022 12:54:13 +0100
Subject: [PATCH 038/730] Fix test to cater for packages leaked into venv
---
tests/functional/test_python_option.py | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/tests/functional/test_python_option.py b/tests/functional/test_python_option.py
index 4fafde2a8b2..8bf16d7a56b 100644
--- a/tests/functional/test_python_option.py
+++ b/tests/functional/test_python_option.py
@@ -11,12 +11,17 @@ def test_python_interpreter(
tmpdir: Path,
shared_data: TestData,
) -> None:
- env_path = os.fsdecode(tmpdir / "venv")
+ env_path = os.fspath(tmpdir / "venv")
env = EnvBuilder(with_pip=False)
env.create(env_path)
result = script.pip("--python", env_path, "list", "--format=json")
- assert json.loads(result.stdout) == []
+ before = json.loads(result.stdout)
+
+ # Ideally we would assert that before==[], but there's a problem in CI
+ # that means this isn't true. See https://github.com/pypa/pip/pull/11326
+ # for details.
+
script.pip(
"--python",
env_path,
@@ -26,8 +31,11 @@ def test_python_interpreter(
"--no-index",
"simplewheel==1.0",
)
+
result = script.pip("--python", env_path, "list", "--format=json")
- assert json.loads(result.stdout) == [{"name": "simplewheel", "version": "1.0"}]
+ installed = json.loads(result.stdout)
+ assert {"name": "simplewheel", "version": "1.0"} in installed
+
script.pip("--python", env_path, "uninstall", "simplewheel", "--yes")
result = script.pip("--python", env_path, "list", "--format=json")
- assert json.loads(result.stdout) == []
+ assert json.loads(result.stdout) == before
From 61249ed9ee1811ef2693195e21432ebba738574a Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Sat, 30 Jul 2022 16:55:48 +0100
Subject: [PATCH 039/730] Update docs/html/topics/python-option.md
Co-authored-by: Pradyun Gedam
---
docs/html/topics/python-option.md | 14 ++------------
1 file changed, 2 insertions(+), 12 deletions(-)
diff --git a/docs/html/topics/python-option.md b/docs/html/topics/python-option.md
index 242784dfbe8..877f78b3041 100644
--- a/docs/html/topics/python-option.md
+++ b/docs/html/topics/python-option.md
@@ -16,22 +16,12 @@ environment.
One example of where this might be useful is to manage a virtual environment
that does not have pip installed.
-````{tab} Unix/macOS
-```{code-block} console
+```{pip-cli}
$ python -m venv .venv --without-pip
-$ python -m pip --python .venv install SomePackage
+$ pip --python .venv install SomePackage
[...]
Successfully installed SomePackage
```
-````
-````{tab} Windows
-```{code-block} console
-C:\> py -m venv .venv --without-pip
-C:\> py -m pip --python .venv install SomePackage
-[...]
-Successfully installed SomePackage
-```
-````
You could also use `--python .venv/bin/python` (or on Windows,
`--python .venv\Scripts\python.exe`) if you wanted to be explicit, but the
From d0b5a8f75dbac35896a394ab27fff5378ee23baf Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Sat, 30 Jul 2022 16:56:09 +0100
Subject: [PATCH 040/730] Update docs/html/topics/python-option.md
Co-authored-by: Pradyun Gedam
---
docs/html/topics/python-option.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/docs/html/topics/python-option.md b/docs/html/topics/python-option.md
index 877f78b3041..435d8ed6774 100644
--- a/docs/html/topics/python-option.md
+++ b/docs/html/topics/python-option.md
@@ -1,5 +1,7 @@
# Managing a different Python interpreter
+```{versionadded} 22.3
+```
Occasionally, you may want to use pip to manage a Python installation other than
the one pip is installed into. In this case, you can use the `--python` option
From 7cc6445a7a3d456d4ed41ba514c11a02faddf0c9 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Sat, 30 Jul 2022 19:09:42 +0100
Subject: [PATCH 041/730] Add a clarifying note to the pip download docs
---
docs/html/cli/pip_download.rst | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/docs/html/cli/pip_download.rst b/docs/html/cli/pip_download.rst
index 4f15314d765..f1fe1769ee7 100644
--- a/docs/html/cli/pip_download.rst
+++ b/docs/html/cli/pip_download.rst
@@ -43,7 +43,9 @@ match the constraint of the current interpreter (but not your target one), it
is recommended to specify all of these options if you are specifying one of
them. Generic dependencies (e.g. universal wheels, or dependencies with no
platform, abi, or implementation constraints) will still match an over-
-constrained download requirement.
+constrained download requirement. If some of your dependencies are not
+available as binaries, you can build them manually for your target platform
+and let pip download know where to find them using ``--find-links``.
From b6be01aee8be13844fa054a5f8c56fa062cc2c21 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Sun, 31 Jul 2022 11:50:29 +0100
Subject: [PATCH 042/730] Catch errors from running the subprocess
---
src/pip/_internal/cli/main_parser.py | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py
index 6502c567794..06a61305d05 100644
--- a/src/pip/_internal/cli/main_parser.py
+++ b/src/pip/_internal/cli/main_parser.py
@@ -120,8 +120,13 @@ def parse_command(args: List[str]) -> Tuple[str, List[str]]:
# Set a flag so the child doesn't re-invoke itself, causing
# an infinite loop.
os.environ["_PIP_RUNNING_IN_SUBPROCESS"] = "1"
- proc = subprocess.run(pip_cmd)
- sys.exit(proc.returncode)
+ returncode = 0
+ try:
+ proc = subprocess.run(pip_cmd)
+ returncode = proc.returncode
+ except (subprocess.SubprocessError, OSError) as exc:
+ raise CommandError(f"Failed to run pip under {interpreter}: {exc}")
+ sys.exit(returncode)
# --version
if general_options.version:
From 333389133a9fc5f07280a1025a466b137386b18a Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Sun, 31 Jul 2022 12:12:49 +0100
Subject: [PATCH 043/730] Check python version in __pip-runner__.py
---
setup.py | 2 ++
src/pip/__pip-runner__.py | 16 ++++++++++++++++
2 files changed, 18 insertions(+)
diff --git a/setup.py b/setup.py
index 9b7fdeb1134..2179d34d2bf 100644
--- a/setup.py
+++ b/setup.py
@@ -81,5 +81,7 @@ def get_version(rel_path: str) -> str:
],
},
zip_safe=False,
+ # NOTE: python_requires is duplicated in __pip-runner__.py.
+ # When changing this value, please change the other copy as well.
python_requires=">=3.7",
)
diff --git a/src/pip/__pip-runner__.py b/src/pip/__pip-runner__.py
index 280e99f2f08..28e4399b054 100644
--- a/src/pip/__pip-runner__.py
+++ b/src/pip/__pip-runner__.py
@@ -12,6 +12,8 @@
from typing import Optional, Sequence, Union
PIP_SOURCES_ROOT = dirname(dirname(__file__))
+# Copied from setup.py
+PYTHON_REQUIRES = ">=3.7"
class PipImportRedirectingFinder:
@@ -30,8 +32,22 @@ def find_spec(
return spec
+def check_python_version() -> None:
+ # Import here to ensure the imports happen after the sys.meta_path change.
+ from pip._vendor.packaging.specifiers import SpecifierSet
+ from pip._vendor.packaging.version import Version
+
+ py_ver = Version("{0.major}.{0.minor}.{0.micro}".format(sys.version_info))
+ if py_ver not in SpecifierSet(PYTHON_REQUIRES):
+ raise SystemExit(
+ f"This version of pip does not support python {py_ver} "
+ f"(requires {PYTHON_REQUIRES})"
+ )
+
+
# TODO https://github.com/pypa/pip/issues/11294
sys.meta_path.insert(0, PipImportRedirectingFinder())
assert __name__ == "__main__", "Cannot run __pip-runner__.py as a non-main module"
+check_python_version()
runpy.run_module("pip", run_name="__main__", alter_sys=True)
From 4dc35b7399cee3668caf31ba200d3dcfc2bd7579 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Sun, 31 Jul 2022 16:01:57 +0100
Subject: [PATCH 044/730] Skip the executable check, as subprocess.run will
catch this
---
src/pip/_internal/cli/main_parser.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py
index 06a61305d05..548174d8dfe 100644
--- a/src/pip/_internal/cli/main_parser.py
+++ b/src/pip/_internal/cli/main_parser.py
@@ -71,11 +71,10 @@ def identify_python_interpreter(python: str) -> Optional[str]:
if py:
return py
- # If the named file exists, and is executable, use it.
+ # If the named file exists, use it.
# If it's a directory, assume it's a virtual environment and
# look for the environment's Python executable.
if os.path.exists(python):
- # Do the directory check first because directories can be executable
if os.path.isdir(python):
# bin/python for Unix, Scripts/python.exe for Windows
# Try both in case of odd cases like cygwin.
@@ -83,7 +82,7 @@ def identify_python_interpreter(python: str) -> Optional[str]:
py = os.path.join(python, exe)
if os.path.exists(py):
return py
- elif os.access(python, os.X_OK):
+ else:
return python
# Could not find the interpreter specified
From 0f559adabb704ca5b09e1984965aa34233c5022a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Sun, 31 Jul 2022 17:21:19 +0200
Subject: [PATCH 045/730] Remove TODO
---
src/pip/__pip-runner__.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/pip/__pip-runner__.py b/src/pip/__pip-runner__.py
index 280e99f2f08..14026c0d131 100644
--- a/src/pip/__pip-runner__.py
+++ b/src/pip/__pip-runner__.py
@@ -30,7 +30,6 @@ def find_spec(
return spec
-# TODO https://github.com/pypa/pip/issues/11294
sys.meta_path.insert(0, PipImportRedirectingFinder())
assert __name__ == "__main__", "Cannot run __pip-runner__.py as a non-main module"
From d5317f27785d9c623e3f5c74af16f1fb73d6fd50 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Sun, 31 Jul 2022 17:33:22 +0200
Subject: [PATCH 046/730] Revert "PipDeprecationWarning subclass
DeprecationWarning"
This reverts commit f1bc96a4a336e2b8889269aec046ac4044e4b46c.
---
news/11330.bugfix.rst | 1 +
src/pip/_internal/utils/deprecation.py | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
create mode 100644 news/11330.bugfix.rst
diff --git a/news/11330.bugfix.rst b/news/11330.bugfix.rst
new file mode 100644
index 00000000000..e03501fe5ef
--- /dev/null
+++ b/news/11330.bugfix.rst
@@ -0,0 +1 @@
+Show pip deprecation warnings by default.
diff --git a/src/pip/_internal/utils/deprecation.py b/src/pip/_internal/utils/deprecation.py
index 7964095ffde..72bd6f25a55 100644
--- a/src/pip/_internal/utils/deprecation.py
+++ b/src/pip/_internal/utils/deprecation.py
@@ -13,7 +13,7 @@
DEPRECATION_MSG_PREFIX = "DEPRECATION: "
-class PipDeprecationWarning(DeprecationWarning):
+class PipDeprecationWarning(Warning):
pass
From ebe491a82a13e6610697b22be00db363fb5ff5e3 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Mon, 1 Aug 2022 10:49:32 +0100
Subject: [PATCH 047/730] Get rid of the --python python/py shortcuts
---
src/pip/_internal/cli/main_parser.py | 23 -----------------------
tests/unit/test_cmdoptions.py | 12 +-----------
2 files changed, 1 insertion(+), 34 deletions(-)
diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py
index 548174d8dfe..5ade356b9c2 100644
--- a/src/pip/_internal/cli/main_parser.py
+++ b/src/pip/_internal/cli/main_parser.py
@@ -2,7 +2,6 @@
"""
import os
-import shutil
import subprocess
import sys
from typing import List, Optional, Tuple
@@ -12,7 +11,6 @@
from pip._internal.cli.parser import ConfigOptionParser, UpdatingDefaultsHelpFormatter
from pip._internal.commands import commands_dict, get_similar_commands
from pip._internal.exceptions import CommandError
-from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.misc import get_pip_version, get_prog
__all__ = ["create_main_parser", "parse_command"]
@@ -50,27 +48,6 @@ def create_main_parser() -> ConfigOptionParser:
def identify_python_interpreter(python: str) -> Optional[str]:
- if python == "python" or python == "py":
- # Run the active Python.
- # We have to be very careful here, because:
- #
- # 1. On Unix, "python" is probably correct but there is a "py" launcher.
- # 2. On Windows, "py" is the best option if it's present.
- # 3. On Windows without "py", "python" might work, but it might also
- # be the shim that launches the Windows store to allow you to install
- # Python.
- #
- # We go with getting py on Windows, and if it's not present or we're
- # on Unix, get python. We don't worry about the launcher on Unix or
- # the installer stub on Windows.
- py = None
- if WINDOWS:
- py = shutil.which("py")
- if py is None:
- py = shutil.which("python")
- if py:
- return py
-
# If the named file exists, use it.
# If it's a directory, assume it's a virtual environment and
# look for the environment's Python executable.
diff --git a/tests/unit/test_cmdoptions.py b/tests/unit/test_cmdoptions.py
index d5b4813822f..8c33ca8c18d 100644
--- a/tests/unit/test_cmdoptions.py
+++ b/tests/unit/test_cmdoptions.py
@@ -35,23 +35,13 @@ def test_convert_python_version(
assert actual == expected, f"actual: {actual!r}"
-def test_identify_python_interpreter_py(monkeypatch: pytest.MonkeyPatch) -> None:
- def which(cmd: str) -> str:
- assert cmd == "py" or cmd == "python"
- return "dummy_value"
-
- monkeypatch.setattr("shutil.which", which)
- assert identify_python_interpreter("py") == "dummy_value"
- assert identify_python_interpreter("python") == "dummy_value"
-
-
def test_identify_python_interpreter_venv(tmpdir: Path) -> None:
env_path = tmpdir / "venv"
env = EnvBuilder(with_pip=False)
env.create(env_path)
# Passing a virtual environment returns the Python executable
- interp = identify_python_interpreter(os.fsdecode(env_path))
+ interp = identify_python_interpreter(os.fspath(env_path))
assert interp is not None
assert Path(interp).exists()
From 6354192e2ef4ab19db5ba324dfd1ef4e2c840e07 Mon Sep 17 00:00:00 2001
From: q0w <43147888+q0w@users.noreply.github.com>
Date: Mon, 1 Aug 2022 14:28:59 +0300
Subject: [PATCH 048/730] Fix news
---
news/11309.bugfix.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/news/11309.bugfix.rst b/news/11309.bugfix.rst
index f59d2516eee..9ee54057da4 100644
--- a/news/11309.bugfix.rst
+++ b/news/11309.bugfix.rst
@@ -1 +1 @@
-Ensure that a binary executable of pip exists when checking for a new version of pip.
+Ensure that the candidate ``pip`` executable exists, when checking for a new version of pip.
From 9c22ee1ef11e216299a610fe3c3e01765ace9097 Mon Sep 17 00:00:00 2001
From: Godefroid Chapelle
Date: Tue, 2 Aug 2022 12:08:33 +0200
Subject: [PATCH 049/730] Do not parse JSON content with HTML parser
---
src/pip/_internal/index/collector.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py
index 6e5dac5ad3c..bc41737d972 100644
--- a/src/pip/_internal/index/collector.py
+++ b/src/pip/_internal/index/collector.py
@@ -345,6 +345,7 @@ def parse_links(page: "IndexContent") -> Iterable[Link]:
yanked_reason=yanked_reason,
hashes=file.get("hashes", {}),
)
+ return
parser = HTMLLinkParser(page.url)
encoding = page.encoding or "utf-8"
From bb2894d7377b91664897b1fe11dbb79c9f546136 Mon Sep 17 00:00:00 2001
From: Philippe Ombredanne
Date: Tue, 2 Aug 2022 16:14:01 +0200
Subject: [PATCH 050/730] Fix minor docstring typo
Signed-off-by: Philippe Ombredanne
---
src/pip/_internal/network/lazy_wheel.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/pip/_internal/network/lazy_wheel.py b/src/pip/_internal/network/lazy_wheel.py
index 2d1ddaa8981..854a6fa1fdc 100644
--- a/src/pip/_internal/network/lazy_wheel.py
+++ b/src/pip/_internal/network/lazy_wheel.py
@@ -23,7 +23,7 @@ class HTTPRangeRequestUnsupported(Exception):
def dist_from_wheel_url(name: str, url: str, session: PipSession) -> BaseDistribution:
"""Return a distribution object from the given wheel URL.
- This uses HTTP range requests to only fetch the potion of the wheel
+ This uses HTTP range requests to only fetch the portion of the wheel
containing metadata, just enough for the object to be constructed.
If such requests are not supported, HTTPRangeRequestUnsupported
is raised.
From 5befbe3b1a2077f9c29dcb762a45204a1a0cd484 Mon Sep 17 00:00:00 2001
From: Godefroid Chapelle
Date: Tue, 2 Aug 2022 16:59:48 +0200
Subject: [PATCH 051/730] fix forgotten rename
---
src/pip/_internal/index/collector.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py
index bc41737d972..c8e120b519e 100644
--- a/src/pip/_internal/index/collector.py
+++ b/src/pip/_internal/index/collector.py
@@ -448,7 +448,7 @@ def _get_index_content(
) -> Optional["IndexContent"]:
if session is None:
raise TypeError(
- "_get_html_page() missing 1 required keyword argument: 'session'"
+ "_get_index_content() missing 1 required keyword argument: 'session'"
)
url = link.url.split("#", 1)[0]
From c69ea02bff38d7d6c5cda41a344b2d6a71ad2c74 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Wed, 3 Aug 2022 10:12:29 +0100
Subject: [PATCH 052/730] Add a version check to __pip-runner__.py
---
setup.py | 2 ++
src/pip/__pip-runner__.py | 36 +++++++++++++++++++++++++-----------
2 files changed, 27 insertions(+), 11 deletions(-)
diff --git a/setup.py b/setup.py
index 9b7fdeb1134..2179d34d2bf 100644
--- a/setup.py
+++ b/setup.py
@@ -81,5 +81,7 @@ def get_version(rel_path: str) -> str:
],
},
zip_safe=False,
+ # NOTE: python_requires is duplicated in __pip-runner__.py.
+ # When changing this value, please change the other copy as well.
python_requires=">=3.7",
)
diff --git a/src/pip/__pip-runner__.py b/src/pip/__pip-runner__.py
index 14026c0d131..49a148a097e 100644
--- a/src/pip/__pip-runner__.py
+++ b/src/pip/__pip-runner__.py
@@ -4,24 +4,38 @@
an import statement.
"""
-import runpy
+# /!\ This version compatibility check section must be Python 2 compatible. /!\
+
import sys
-import types
-from importlib.machinery import ModuleSpec, PathFinder
-from os.path import dirname
-from typing import Optional, Sequence, Union
+
+# Copied from setup.py
+PYTHON_REQUIRES = (3, 7)
+
+
+def version_str(version): # type: ignore
+ return ".".join(str(v) for v in version)
+
+
+if sys.version_info[:2] < PYTHON_REQUIRES:
+ raise SystemExit(
+ "This version of pip does not support python {} (requires >={}).".format(
+ version_str(sys.version_info[:2]), version_str(PYTHON_REQUIRES)
+ )
+ )
+
+# From here on, we can use Python 3 features, but the syntax must remain
+# Python 2 compatible.
+
+import runpy # noqa: E402
+from importlib.machinery import PathFinder # noqa: E402
+from os.path import dirname # noqa: E402
PIP_SOURCES_ROOT = dirname(dirname(__file__))
class PipImportRedirectingFinder:
@classmethod
- def find_spec(
- self,
- fullname: str,
- path: Optional[Sequence[Union[bytes, str]]] = None,
- target: Optional[types.ModuleType] = None,
- ) -> Optional[ModuleSpec]:
+ def find_spec(self, fullname, path=None, target=None): # type: ignore
if fullname != "pip":
return None
From b8aa21b5759c55bf53f69c185b6193a19e82cd20 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Wed, 3 Aug 2022 10:05:25 +0100
Subject: [PATCH 053/730] Revert __pip-runner__.py changes
---
setup.py | 2 --
src/pip/__pip-runner__.py | 16 ----------------
2 files changed, 18 deletions(-)
diff --git a/setup.py b/setup.py
index 2179d34d2bf..9b7fdeb1134 100644
--- a/setup.py
+++ b/setup.py
@@ -81,7 +81,5 @@ def get_version(rel_path: str) -> str:
],
},
zip_safe=False,
- # NOTE: python_requires is duplicated in __pip-runner__.py.
- # When changing this value, please change the other copy as well.
python_requires=">=3.7",
)
diff --git a/src/pip/__pip-runner__.py b/src/pip/__pip-runner__.py
index 41d7fe00474..14026c0d131 100644
--- a/src/pip/__pip-runner__.py
+++ b/src/pip/__pip-runner__.py
@@ -12,8 +12,6 @@
from typing import Optional, Sequence, Union
PIP_SOURCES_ROOT = dirname(dirname(__file__))
-# Copied from setup.py
-PYTHON_REQUIRES = ">=3.7"
class PipImportRedirectingFinder:
@@ -32,21 +30,7 @@ def find_spec(
return spec
-def check_python_version() -> None:
- # Import here to ensure the imports happen after the sys.meta_path change.
- from pip._vendor.packaging.specifiers import SpecifierSet
- from pip._vendor.packaging.version import Version
-
- py_ver = Version("{0.major}.{0.minor}.{0.micro}".format(sys.version_info))
- if py_ver not in SpecifierSet(PYTHON_REQUIRES):
- raise SystemExit(
- f"This version of pip does not support python {py_ver} "
- f"(requires {PYTHON_REQUIRES})"
- )
-
-
sys.meta_path.insert(0, PipImportRedirectingFinder())
assert __name__ == "__main__", "Cannot run __pip-runner__.py as a non-main module"
-check_python_version()
runpy.run_module("pip", run_name="__main__", alter_sys=True)
From ef4fc3c516d2b0709328a893b8c1d840923d6914 Mon Sep 17 00:00:00 2001
From: kasium <15907922+kasium@users.noreply.github.com>
Date: Wed, 3 Aug 2022 11:39:59 +0200
Subject: [PATCH 054/730] Update docs/html/cli/pip_wheel.rst
Co-authored-by: Tzu-ping Chung
---
docs/html/cli/pip_wheel.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/html/cli/pip_wheel.rst b/docs/html/cli/pip_wheel.rst
index d645f29cd2a..e93fee2e620 100644
--- a/docs/html/cli/pip_wheel.rst
+++ b/docs/html/cli/pip_wheel.rst
@@ -30,8 +30,8 @@ Description
This is now covered in :doc:`../reference/build-system/index`.
-Differences to `build`
-----------------------
+Differences to ``build``
+------------------------
`build `_ is a simple tool which can among other things build
wheels for projects using PEP 517. It is comparable to the execution of ``pip wheel --no-deps .``.
From 905fa3f076d020239903730223b6eb057121c2f2 Mon Sep 17 00:00:00 2001
From: Kai Mueller
Date: Wed, 3 Aug 2022 09:53:13 +0000
Subject: [PATCH 055/730] Comments
---
docs/html/cli/pip_wheel.rst | 1 +
1 file changed, 1 insertion(+)
diff --git a/docs/html/cli/pip_wheel.rst b/docs/html/cli/pip_wheel.rst
index e93fee2e620..bfd19a0ccb1 100644
--- a/docs/html/cli/pip_wheel.rst
+++ b/docs/html/cli/pip_wheel.rst
@@ -35,6 +35,7 @@ Differences to ``build``
`build `_ is a simple tool which can among other things build
wheels for projects using PEP 517. It is comparable to the execution of ``pip wheel --no-deps .``.
+It can also build source distributions which is not possible with ``pip``.
``pip wheel`` covers the wheel scope of ``build`` but offers many additional features.
Options
From 5a770bd2d657a35f50152896a3b3691f9af5f56c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Wed, 3 Aug 2022 19:57:04 +0200
Subject: [PATCH 056/730] Handle a type error statically in collector
---
src/pip/_internal/index/collector.py | 9 +--------
1 file changed, 1 insertion(+), 8 deletions(-)
diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py
index c8e120b519e..fed018ac31d 100644
--- a/src/pip/_internal/index/collector.py
+++ b/src/pip/_internal/index/collector.py
@@ -443,14 +443,7 @@ def _make_index_content(
)
-def _get_index_content(
- link: Link, session: Optional[PipSession] = None
-) -> Optional["IndexContent"]:
- if session is None:
- raise TypeError(
- "_get_index_content() missing 1 required keyword argument: 'session'"
- )
-
+def _get_index_content(link: Link, session: PipSession) -> Optional["IndexContent"]:
url = link.url.split("#", 1)[0]
# Check for VCS schemes that do not support lookup as web pages.
From 1294804204cda0ba3dabcc991d248685424d8e6b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Wed, 3 Aug 2022 19:58:30 +0200
Subject: [PATCH 057/730] Bump for development
---
src/pip/__init__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/pip/__init__.py b/src/pip/__init__.py
index 3d4b45a0c99..a40148f008f 100644
--- a/src/pip/__init__.py
+++ b/src/pip/__init__.py
@@ -1,6 +1,6 @@
from typing import List, Optional
-__version__ = "22.2.2"
+__version__ = "22.3.dev0"
def main(args: Optional[List[str]] = None) -> int:
From 2009007cafcc3e8ddec399db2d222e6b63d1c36e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Wed, 3 Aug 2022 20:44:11 +0200
Subject: [PATCH 058/730] Remove 22.2.2 news files
---
news/11314.bugfix.rst | 1 -
news/11319.bugfix.rst | 1 -
news/11330.bugfix.rst | 1 -
3 files changed, 3 deletions(-)
delete mode 100644 news/11314.bugfix.rst
delete mode 100644 news/11319.bugfix.rst
delete mode 100644 news/11330.bugfix.rst
diff --git a/news/11314.bugfix.rst b/news/11314.bugfix.rst
deleted file mode 100644
index 02d78dc47ff..00000000000
--- a/news/11314.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Avoid ``AttributeError`` when removing the setuptools-provided ``_distutils_hack`` and it is missing its implementation.
diff --git a/news/11319.bugfix.rst b/news/11319.bugfix.rst
deleted file mode 100644
index 31cd2a34b0b..00000000000
--- a/news/11319.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fix import error when reinstalling pip in user site.
diff --git a/news/11330.bugfix.rst b/news/11330.bugfix.rst
deleted file mode 100644
index e03501fe5ef..00000000000
--- a/news/11330.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Show pip deprecation warnings by default.
From 9b638ec6dcf3b28c8c57b0e08056ee19177f52fd Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Wed, 3 Aug 2022 20:25:40 +0100
Subject: [PATCH 059/730] Update docs to match behaviour
---
docs/html/topics/python-option.md | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/docs/html/topics/python-option.md b/docs/html/topics/python-option.md
index 435d8ed6774..5ad46e7af9c 100644
--- a/docs/html/topics/python-option.md
+++ b/docs/html/topics/python-option.md
@@ -5,14 +5,13 @@
Occasionally, you may want to use pip to manage a Python installation other than
the one pip is installed into. In this case, you can use the `--python` option
-to specify the interpreter you want to manage. This option can take one of three
+to specify the interpreter you want to manage. This option can take one of two
values:
1. The path to a Python executable.
2. The path to a virtual environment.
-3. Either "py" or "python", referring to the currently active Python interpreter.
-In all 3 cases, pip will run exactly as if it had been invoked from that Python
+In both cases, pip will run exactly as if it had been invoked from that Python
environment.
One example of where this might be useful is to manage a virtual environment
From de49b52ec2522b78a93e5a2f77e59707dca6b22f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Thu, 4 Aug 2022 09:04:03 +0200
Subject: [PATCH 060/730] _get_index_content's session arg must be a kw arg
Co-authored-by: Pradyun Gedam
---
src/pip/_internal/index/collector.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py
index fed018ac31d..3b96749db96 100644
--- a/src/pip/_internal/index/collector.py
+++ b/src/pip/_internal/index/collector.py
@@ -443,7 +443,7 @@ def _make_index_content(
)
-def _get_index_content(link: Link, session: PipSession) -> Optional["IndexContent"]:
+def _get_index_content(link: Link, *, session: PipSession) -> Optional["IndexContent"]:
url = link.url.split("#", 1)[0]
# Check for VCS schemes that do not support lookup as web pages.
From b423c07ff49a8fb4089421d16c203549ba010b76 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Sun, 29 Aug 2021 16:56:18 +0200
Subject: [PATCH 061/730] Detected indented ERROR and WARNING messages in tests
---
tests/lib/__init__.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py
index 79b240eeb24..c753768c939 100644
--- a/tests/lib/__init__.py
+++ b/tests/lib/__init__.py
@@ -447,6 +447,7 @@ def _check_stderr(
lines = stderr.splitlines()
for line in lines:
+ line = line.lstrip()
# First check for logging errors, which we don't allow during
# tests even if allow_stderr_error=True (since a logging error
# would signal a bug in pip's code).
From 58d8dc28cb841317453b7ce62680d7cf15761866 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Tue, 2 Aug 2022 12:19:13 +0200
Subject: [PATCH 062/730] Do not fail tests on our own deprecation warnings
---
tests/lib/__init__.py | 3 +--
tests/lib/test_lib.py | 2 --
2 files changed, 1 insertion(+), 4 deletions(-)
diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py
index c753768c939..1dfaea7e0f2 100644
--- a/tests/lib/__init__.py
+++ b/tests/lib/__init__.py
@@ -39,7 +39,6 @@
from pip._internal.models.selection_prefs import SelectionPreferences
from pip._internal.models.target_python import TargetPython
from pip._internal.network.session import PipSession
-from pip._internal.utils.deprecation import DEPRECATION_MSG_PREFIX
from tests.lib.venv import VirtualEnvironment
from tests.lib.wheel import make_wheel
@@ -474,7 +473,7 @@ def _check_stderr(
if allow_stderr_warning:
continue
- if line.startswith("WARNING: ") or line.startswith(DEPRECATION_MSG_PREFIX):
+ if line.startswith("WARNING: "):
reason = (
"stderr has an unexpected warning "
"(pass allow_stderr_warning=True to permit this)"
diff --git a/tests/lib/test_lib.py b/tests/lib/test_lib.py
index ea9baed54d3..a541a0a204d 100644
--- a/tests/lib/test_lib.py
+++ b/tests/lib/test_lib.py
@@ -150,7 +150,6 @@ def test_run__allow_stderr_warning(self, script: PipTestEnvironment) -> None:
@pytest.mark.parametrize(
"prefix",
(
- "DEPRECATION",
"WARNING",
"ERROR",
),
@@ -167,7 +166,6 @@ def test_run__allow_stderr_error(
@pytest.mark.parametrize(
"prefix, expected_start",
(
- ("DEPRECATION", "stderr has an unexpected warning"),
("WARNING", "stderr has an unexpected warning"),
("ERROR", "stderr has an unexpected error"),
),
From 1800635e4c12f677cb063576c472f335d22662d5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Tue, 2 Aug 2022 14:35:32 +0200
Subject: [PATCH 063/730] Fix tests with indented errors and warning
---
tests/functional/test_download.py | 20 ++++++++++++++++--
tests/functional/test_install.py | 27 +++++++++++++++++++-----
tests/functional/test_install_vcs_git.py | 22 ++++++++++++++-----
tests/functional/test_uninstall.py | 2 +-
4 files changed, 58 insertions(+), 13 deletions(-)
diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py
index 93718ca42fe..89318b74553 100644
--- a/tests/functional/test_download.py
+++ b/tests/functional/test_download.py
@@ -1116,9 +1116,17 @@ def test_download_file_url_existing_bad_download(
simple_pkg_bytes = simple_pkg.read_bytes()
url = f"{simple_pkg.as_uri()}#sha256={sha256(simple_pkg_bytes).hexdigest()}"
- shared_script.pip("download", "-d", str(download_dir), url)
+ result = shared_script.pip(
+ "download",
+ "-d",
+ str(download_dir),
+ url,
+ allow_stderr_warning=True, # bad hash
+ )
assert simple_pkg_bytes == downloaded_path.read_bytes()
+ assert "WARNING: Previously-downloaded file" in result.stderr
+ assert "has bad hash. Re-downloading." in result.stderr
def test_download_http_url_bad_hash(
@@ -1144,9 +1152,17 @@ def test_download_http_url_bad_hash(
base_address = f"http://{mock_server.host}:{mock_server.port}"
url = f"{base_address}/simple-1.0.tar.gz#sha256={digest}"
- shared_script.pip("download", "-d", str(download_dir), url)
+ result = shared_script.pip(
+ "download",
+ "-d",
+ str(download_dir),
+ url,
+ allow_stderr_warning=True, # bad hash
+ )
assert simple_pkg_bytes == downloaded_path.read_bytes()
+ assert "WARNING: Previously-downloaded file" in result.stderr
+ assert "has bad hash. Re-downloading." in result.stderr
mock_server.stop()
requests = mock_server.get_requests()
diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py
index e74477fe299..bc9ca9eaf96 100644
--- a/tests/functional/test_install.py
+++ b/tests/functional/test_install.py
@@ -183,7 +183,16 @@ def test_pep518_with_user_pip(
non-isolated environment, and break pip in the system site-packages,
so that isolated uses of pip will fail.
"""
- script.pip("install", "--ignore-installed", "-f", common_wheels, "--user", pip_src)
+ script.pip(
+ "install",
+ "--ignore-installed",
+ "-f",
+ common_wheels,
+ "--user",
+ pip_src,
+ # WARNING: The scripts pip, pip3, ... are installed in ... which is not on PATH
+ allow_stderr_warning=True,
+ )
system_pip_dir = script.site_packages_path / "pip"
assert not system_pip_dir.exists()
system_pip_dir.mkdir()
@@ -1542,13 +1551,16 @@ def test_install_topological_sort(script: PipTestEnvironment, data: TestData) ->
@pytest.mark.usefixtures("with_wheel")
def test_install_wheel_broken(script: PipTestEnvironment) -> None:
- res = script.pip_install_local("wheelbroken", expect_stderr=True)
+ res = script.pip_install_local("wheelbroken", allow_stderr_error=True)
+ assert "ERROR: Failed building wheel for wheelbroken" in res.stderr
+ # Fallback to setup.py install (https://github.com/pypa/pip/issues/8368)
assert "Successfully installed wheelbroken-0.1" in str(res), str(res)
@pytest.mark.usefixtures("with_wheel")
def test_cleanup_after_failed_wheel(script: PipTestEnvironment) -> None:
- res = script.pip_install_local("wheelbrokenafter", expect_stderr=True)
+ res = script.pip_install_local("wheelbrokenafter", allow_stderr_error=True)
+ assert "ERROR: Failed building wheel for wheelbrokenafter" in res.stderr
# One of the effects of not cleaning up is broken scripts:
script_py = script.bin_path / "script.py"
assert script_py.exists(), script_py
@@ -1577,7 +1589,12 @@ def test_install_builds_wheels(script: PipTestEnvironment, data: TestData) -> No
# vcs coverage.
to_install = data.packages.joinpath("requires_wheelbroken_upper")
res = script.pip(
- "install", "--no-index", "-f", data.find_links, to_install, expect_stderr=True
+ "install",
+ "--no-index",
+ "-f",
+ data.find_links,
+ to_install,
+ allow_stderr_error=True, # error building wheelbroken
)
expected = (
"Successfully installed requires-wheelbroken-upper-0"
@@ -1620,7 +1637,7 @@ def test_install_no_binary_disables_building_wheels(
"-f",
data.find_links,
to_install,
- expect_stderr=True,
+ allow_stderr_error=True, # error building wheelbroken
)
expected = (
"Successfully installed requires-wheelbroken-upper-0"
diff --git a/tests/functional/test_install_vcs_git.py b/tests/functional/test_install_vcs_git.py
index 4cdf71551b8..2171d3162b3 100644
--- a/tests/functional/test_install_vcs_git.py
+++ b/tests/functional/test_install_vcs_git.py
@@ -94,7 +94,7 @@ def _install_version_pkg_only(
script: PipTestEnvironment,
path: Path,
rev: Optional[str] = None,
- expect_stderr: bool = False,
+ allow_stderr_warning: bool = False,
) -> None:
"""
Install the version_pkg package in editable mode (without returning
@@ -106,14 +106,16 @@ def _install_version_pkg_only(
rev: an optional revision to install like a branch name or tag.
"""
version_pkg_url = _make_version_pkg_url(path, rev=rev)
- script.pip("install", "-e", version_pkg_url, expect_stderr=expect_stderr)
+ script.pip(
+ "install", "-e", version_pkg_url, allow_stderr_warning=allow_stderr_warning
+ )
def _install_version_pkg(
script: PipTestEnvironment,
path: Path,
rev: Optional[str] = None,
- expect_stderr: bool = False,
+ allow_stderr_warning: bool = False,
) -> str:
"""
Install the version_pkg package in editable mode, and return the version
@@ -128,7 +130,7 @@ def _install_version_pkg(
script,
path,
rev=rev,
- expect_stderr=expect_stderr,
+ allow_stderr_warning=allow_stderr_warning,
)
result = script.run("version_pkg")
version = result.stdout.strip()
@@ -227,7 +229,13 @@ def test_git_with_short_sha1_revisions(script: PipTestEnvironment) -> None:
"HEAD~1",
cwd=version_pkg_path,
).stdout.strip()[:7]
- version = _install_version_pkg(script, version_pkg_path, rev=sha1)
+ version = _install_version_pkg(
+ script,
+ version_pkg_path,
+ rev=sha1,
+ # WARNING: Did not find branch or tag ..., assuming revision or ref.
+ allow_stderr_warning=True,
+ )
assert "0.1" == version
@@ -273,6 +281,8 @@ def test_git_install_ref(script: PipTestEnvironment) -> None:
script,
version_pkg_path,
rev="refs/foo/bar",
+ # WARNING: Did not find branch or tag ..., assuming revision or ref.
+ allow_stderr_warning=True,
)
assert "0.1" == version
@@ -294,6 +304,8 @@ def test_git_install_then_install_ref(script: PipTestEnvironment) -> None:
script,
version_pkg_path,
rev="refs/foo/bar",
+ # WARNING: Did not find branch or tag ..., assuming revision or ref.
+ allow_stderr_warning=True,
)
assert "0.1" == version
diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py
index 2933a5ef99c..b0e12f6af59 100644
--- a/tests/functional/test_uninstall.py
+++ b/tests/functional/test_uninstall.py
@@ -684,7 +684,7 @@ def test_uninstall_editable_and_pip_install_easy_install_remove(
os.remove(pip_test_fspkg_pth)
# Uninstall will fail with given warning
- uninstall = script.pip("uninstall", "FSPkg", "-y")
+ uninstall = script.pip("uninstall", "FSPkg", "-y", allow_stderr_warning=True)
assert "Cannot remove entries from nonexistent file" in uninstall.stderr
assert (
From 6817fbfb1fd389ad61009f0199db5670b146c8d3 Mon Sep 17 00:00:00 2001
From: Tzu-ping Chung
Date: Sat, 6 Aug 2022 06:18:59 +0800
Subject: [PATCH 064/730] Skip dist if metadata does not have a valid name
---
news/11352.bugfix.rst | 2 ++
src/pip/_internal/metadata/importlib/_compat.py | 14 +++++++++++++-
src/pip/_internal/metadata/importlib/_envs.py | 14 +++++++++++---
3 files changed, 26 insertions(+), 4 deletions(-)
create mode 100644 news/11352.bugfix.rst
diff --git a/news/11352.bugfix.rst b/news/11352.bugfix.rst
new file mode 100644
index 00000000000..78016c912ef
--- /dev/null
+++ b/news/11352.bugfix.rst
@@ -0,0 +1,2 @@
+Ignore distributions with invalid ``Name`` in metadata instead of crashing, when
+using the ``importlib.metadata`` backend.
diff --git a/src/pip/_internal/metadata/importlib/_compat.py b/src/pip/_internal/metadata/importlib/_compat.py
index e0879807ab9..593bff23ede 100644
--- a/src/pip/_internal/metadata/importlib/_compat.py
+++ b/src/pip/_internal/metadata/importlib/_compat.py
@@ -2,6 +2,15 @@
from typing import Any, Optional, Protocol, cast
+class BadMetadata(ValueError):
+ def __init__(self, dist: importlib.metadata.Distribution, *, reason: str) -> None:
+ self.dist = dist
+ self.reason = reason
+
+ def __str__(self) -> str:
+ return f"Bad metadata in {self.dist} ({self.reason})"
+
+
class BasePath(Protocol):
"""A protocol that various path objects conform.
@@ -40,4 +49,7 @@ def get_dist_name(dist: importlib.metadata.Distribution) -> str:
The ``name`` attribute is only available in Python 3.10 or later. We are
targeting exactly that, but Mypy does not know this.
"""
- return cast(Any, dist).name
+ name = cast(Any, dist).name
+ if not isinstance(name, str):
+ raise BadMetadata(dist, reason="invalid metadata entry 'name'")
+ return name
diff --git a/src/pip/_internal/metadata/importlib/_envs.py b/src/pip/_internal/metadata/importlib/_envs.py
index d5fcfdbfef2..cbec59e2c6d 100644
--- a/src/pip/_internal/metadata/importlib/_envs.py
+++ b/src/pip/_internal/metadata/importlib/_envs.py
@@ -1,5 +1,6 @@
import functools
import importlib.metadata
+import logging
import os
import pathlib
import sys
@@ -14,9 +15,11 @@
from pip._internal.utils.deprecation import deprecated
from pip._internal.utils.filetypes import WHEEL_EXTENSION
-from ._compat import BasePath, get_dist_name, get_info_location
+from ._compat import BadMetadata, BasePath, get_dist_name, get_info_location
from ._dists import Distribution
+logger = logging.getLogger(__name__)
+
def _looks_like_wheel(location: str) -> bool:
if not location.endswith(WHEEL_EXTENSION):
@@ -56,11 +59,16 @@ def _find_impl(self, location: str) -> Iterator[FoundResult]:
# To know exactly where we find a distribution, we have to feed in the
# paths one by one, instead of dumping the list to importlib.metadata.
for dist in importlib.metadata.distributions(path=[location]):
- normalized_name = canonicalize_name(get_dist_name(dist))
+ info_location = get_info_location(dist)
+ try:
+ raw_name = get_dist_name(dist)
+ except BadMetadata as e:
+ logger.warning("Skipping %s due to %s", info_location, e.reason)
+ continue
+ normalized_name = canonicalize_name(raw_name)
if normalized_name in self._found_names:
continue
self._found_names.add(normalized_name)
- info_location = get_info_location(dist)
yield dist, info_location
def find(self, location: str) -> Iterator[BaseDistribution]:
From 27878a52af77c9e677ffaf63d64959514d1442e4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Sat, 6 Aug 2022 19:26:58 +0200
Subject: [PATCH 065/730] Refactor legacy_install_reason
---
src/pip/_internal/commands/install.py | 3 +-
src/pip/_internal/req/req_install.py | 22 +++++---------
src/pip/_internal/utils/deprecation.py | 41 ++++++++++++++++++++++++++
3 files changed, 51 insertions(+), 15 deletions(-)
diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py
index 29907645c81..65d6362d77c 100644
--- a/src/pip/_internal/commands/install.py
+++ b/src/pip/_internal/commands/install.py
@@ -29,6 +29,7 @@
from pip._internal.req import install_given_reqs
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.compat import WINDOWS
+from pip._internal.utils.deprecation import LegacyInstallReasonFailedBdistWheel
from pip._internal.utils.distutils_args import parse_distutils_args
from pip._internal.utils.filesystem import test_writable_dir
from pip._internal.utils.logging import getLogger
@@ -440,7 +441,7 @@ def run(self, options: Values, args: List[str]) -> int:
# those.
for r in build_failures:
if not r.use_pep517:
- r.legacy_install_reason = 8368
+ r.legacy_install_reason = LegacyInstallReasonFailedBdistWheel
to_install = resolver.get_installation_order(requirement_set)
diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py
index a1e376c893a..2b9cd992e7f 100644
--- a/src/pip/_internal/req/req_install.py
+++ b/src/pip/_internal/req/req_install.py
@@ -42,7 +42,7 @@
from pip._internal.operations.install.wheel import install_wheel
from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path
from pip._internal.req.req_uninstall import UninstallPathSet
-from pip._internal.utils.deprecation import deprecated
+from pip._internal.utils.deprecation import LegacyInstallReason, deprecated
from pip._internal.utils.direct_url_helpers import (
direct_url_for_editable,
direct_url_from_link,
@@ -96,7 +96,7 @@ def __init__(
self.constraint = constraint
self.editable = editable
self.permit_editable_wheels = permit_editable_wheels
- self.legacy_install_reason: Optional[int] = None
+ self.legacy_install_reason: Optional[LegacyInstallReason] = None
# source_dir is the local directory where the linked requirement is
# located, or unpacked. In case unpacking is needed, creating and
@@ -836,18 +836,12 @@ def install(
self.install_succeeded = success
- if success and self.legacy_install_reason == 8368:
- deprecated(
- reason=(
- "{} was installed using the legacy 'setup.py install' "
- "method, because a wheel could not be built for it.".format(
- self.name
- )
- ),
- replacement="to fix the wheel build issue reported above",
- gone_in=None,
- issue=8368,
- )
+ if (
+ success
+ and self.legacy_install_reason is not None
+ and self.legacy_install_reason.emit_after_success
+ ):
+ self.legacy_install_reason.emit_deprecation(self.name)
def check_invalid_constraint_type(req: InstallRequirement) -> str:
diff --git a/src/pip/_internal/utils/deprecation.py b/src/pip/_internal/utils/deprecation.py
index 72bd6f25a55..a15c52197b9 100644
--- a/src/pip/_internal/utils/deprecation.py
+++ b/src/pip/_internal/utils/deprecation.py
@@ -118,3 +118,44 @@ def deprecated(
raise PipDeprecationWarning(message)
warnings.warn(message, category=PipDeprecationWarning, stacklevel=2)
+
+
+class LegacyInstallReason:
+ def __init__(
+ self,
+ reason: str,
+ replacement: Optional[str],
+ gone_in: Optional[str],
+ feature_flag: Optional[str] = None,
+ issue: Optional[int] = None,
+ emit_after_success: bool = False,
+ emit_before_install: bool = False,
+ ):
+ self._reason = reason
+ self._replacement = replacement
+ self._gone_in = gone_in
+ self._feature_flag = feature_flag
+ self._issue = issue
+ self.emit_after_success = emit_after_success
+ self.emit_before_install = emit_before_install
+
+ def emit_deprecation(self, name: str) -> None:
+ deprecated(
+ reason=self._reason.format(name=name),
+ replacement=self._replacement,
+ gone_in=self._gone_in,
+ feature_flag=self._feature_flag,
+ issue=self._issue,
+ )
+
+
+LegacyInstallReasonFailedBdistWheel = LegacyInstallReason(
+ reason=(
+ "{name} was installed using the legacy 'setup.py install' "
+ "method, because a wheel could not be built for it."
+ ),
+ replacement="to fix the wheel build issue reported above",
+ gone_in=None,
+ issue=8368,
+ emit_after_success=True,
+)
From 0326b33a6df086c0d83e46ac0856dd1c7b6fe51e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Mon, 1 Aug 2022 17:06:55 +0200
Subject: [PATCH 066/730] Add missing with_wheel fixture
---
tests/functional/test_install_index.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/tests/functional/test_install_index.py b/tests/functional/test_install_index.py
index 71c0b3e6c60..c1f0ecbd7c6 100644
--- a/tests/functional/test_install_index.py
+++ b/tests/functional/test_install_index.py
@@ -23,6 +23,7 @@ def test_find_links_relative_path(script: PipTestEnvironment, data: TestData) ->
result.did_create(initools_folder)
+@pytest.mark.usefixtures("with_wheel")
def test_find_links_no_doctype(script: PipTestEnvironment, data: TestData) -> None:
shutil.copy(data.packages / "simple-1.0.tar.gz", script.scratch_path)
html = script.scratch_path.joinpath("index.html")
From afe136c42b3c374567acdfbe097da0376557421a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Tue, 2 Aug 2022 12:04:00 +0200
Subject: [PATCH 067/730] Add test for issue 8559 deprecation
---
tests/functional/test_install.py | 30 ++++++++++++++++++++++++++++++
1 file changed, 30 insertions(+)
diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py
index e74477fe299..740cd2f97a6 100644
--- a/tests/functional/test_install.py
+++ b/tests/functional/test_install.py
@@ -12,6 +12,7 @@
from pip._internal.cli.status_codes import ERROR, SUCCESS
from pip._internal.models.index import PyPI, TestPyPI
+from pip._internal.utils.deprecation import DEPRECATION_MSG_PREFIX
from pip._internal.utils.misc import rmtree
from tests.conftest import CertFactory
from tests.lib import (
@@ -2286,3 +2287,32 @@ def test_install_dry_run(script: PipTestEnvironment, data: TestData) -> None:
)
assert "Would install simple-3.0" in result.stdout
assert "Successfully installed" not in result.stdout
+
+
+def test_install_8559_missing_wheel_package(
+ script: PipTestEnvironment, shared_data: TestData
+) -> None:
+ result = script.pip(
+ "install",
+ "--find-links",
+ shared_data.find_links,
+ "simple",
+ allow_stderr_warning=True,
+ )
+ assert DEPRECATION_MSG_PREFIX in result.stderr
+ assert "'wheel' package is not installed" in result.stderr
+ assert "using the legacy 'setup.py install' method" in result.stderr
+
+
+@pytest.mark.usefixtures("with_wheel")
+def test_install_8559_wheel_package_present(
+ script: PipTestEnvironment, shared_data: TestData
+) -> None:
+ result = script.pip(
+ "install",
+ "--find-links",
+ shared_data.find_links,
+ "simple",
+ allow_stderr_warning=False,
+ )
+ assert DEPRECATION_MSG_PREFIX not in result.stderr
From ae802e3e66e8a921141872fcd7b0cba9522cf5aa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Sun, 31 Jul 2022 17:16:26 +0200
Subject: [PATCH 068/730] Deprecate setup.py install fallback when wheel
package is absent
---
news/8559.removal.rst | 2 ++
src/pip/_internal/req/req_install.py | 5 +++++
src/pip/_internal/utils/deprecation.py | 14 ++++++++++++++
src/pip/_internal/wheel_builder.py | 7 ++-----
4 files changed, 23 insertions(+), 5 deletions(-)
create mode 100644 news/8559.removal.rst
diff --git a/news/8559.removal.rst b/news/8559.removal.rst
new file mode 100644
index 00000000000..aa9f814120d
--- /dev/null
+++ b/news/8559.removal.rst
@@ -0,0 +1,2 @@
+Deprecate installation with 'setup.py install' when the 'wheel' package is absent for
+source distributions without 'pyproject.toml'.
diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py
index 2b9cd992e7f..88d481dfe5c 100644
--- a/src/pip/_internal/req/req_install.py
+++ b/src/pip/_internal/req/req_install.py
@@ -811,6 +811,11 @@ def install(
install_options = list(install_options) + self.install_options
try:
+ if (
+ self.legacy_install_reason is not None
+ and self.legacy_install_reason.emit_before_install
+ ):
+ self.legacy_install_reason.emit_deprecation(self.name)
success = install_legacy(
install_options=install_options,
global_options=global_options,
diff --git a/src/pip/_internal/utils/deprecation.py b/src/pip/_internal/utils/deprecation.py
index a15c52197b9..7c7ace6ff4c 100644
--- a/src/pip/_internal/utils/deprecation.py
+++ b/src/pip/_internal/utils/deprecation.py
@@ -159,3 +159,17 @@ def emit_deprecation(self, name: str) -> None:
issue=8368,
emit_after_success=True,
)
+
+
+LegacyInstallReasonMissingWheelPackage = LegacyInstallReason(
+ reason=(
+ "{name} is being installed using the legacy "
+ "'setup.py install' method, because it does not have a "
+ "'pyproject.toml' and the 'wheel' package "
+ "is not installed."
+ ),
+ replacement="to enable the '--use-pep517' option",
+ gone_in=None,
+ issue=8559,
+ emit_before_install=True,
+)
diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py
index 77a17ff0f15..d2a7146edb7 100644
--- a/src/pip/_internal/wheel_builder.py
+++ b/src/pip/_internal/wheel_builder.py
@@ -19,6 +19,7 @@
from pip._internal.operations.build.wheel_editable import build_wheel_editable
from pip._internal.operations.build.wheel_legacy import build_wheel_legacy
from pip._internal.req.req_install import InstallRequirement
+from pip._internal.utils.deprecation import LegacyInstallReasonMissingWheelPackage
from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import ensure_dir, hash_file, is_wheel_installed
from pip._internal.utils.setuptools_build import make_setuptools_clean_args
@@ -86,11 +87,7 @@ def _should_build(
if not is_wheel_installed():
# we don't build legacy requirements if wheel is not installed
- logger.info(
- "Using legacy 'setup.py install' for %s, "
- "since package 'wheel' is not installed.",
- req.name,
- )
+ req.legacy_install_reason = LegacyInstallReasonMissingWheelPackage
return False
return True
From 77da6ae52c4a948ec75956ab39f8cc7955cd8f9d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Sun, 7 Aug 2022 11:39:56 +0200
Subject: [PATCH 069/730] Mention --quiet in --report option help
---
news/11357.doc.rst | 1 +
src/pip/_internal/commands/install.py | 4 +++-
2 files changed, 4 insertions(+), 1 deletion(-)
create mode 100644 news/11357.doc.rst
diff --git a/news/11357.doc.rst b/news/11357.doc.rst
new file mode 100644
index 00000000000..887928a086e
--- /dev/null
+++ b/news/11357.doc.rst
@@ -0,0 +1 @@
+Mention that --quiet must be used when writing the installation report to stdout.
diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py
index 29907645c81..e4b90bde801 100644
--- a/src/pip/_internal/commands/install.py
+++ b/src/pip/_internal/commands/install.py
@@ -263,7 +263,9 @@ def add_options(self) -> None:
"the provided requirements. "
"Can be used in combination with --dry-run and --ignore-installed "
"to 'resolve' the requirements. "
- "When - is used as file name it writes to stdout."
+ "When - is used as file name it writes to stdout. "
+ "When writing to stdout, please combine with the --quiet option "
+ "to avoid mixing pip logging output with JSON output."
),
)
From be718ff59ec093f22cc91eb710c53a7553b58ae3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Fri, 12 Aug 2022 12:57:39 +0200
Subject: [PATCH 070/730] Fix tests that relied on setuptools not supporting
PEP 660
---
tests/functional/test_install.py | 17 ++++++++++++++---
1 file changed, 14 insertions(+), 3 deletions(-)
diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py
index 340d7205df0..f4f8d4efb0c 100644
--- a/tests/functional/test_install.py
+++ b/tests/functional/test_install.py
@@ -700,17 +700,27 @@ def test_editable_install__local_dir_no_setup_py(
)
+@pytest.mark.network
def test_editable_install__local_dir_no_setup_py_with_pyproject(
script: PipTestEnvironment,
) -> None:
"""
Test installing in editable mode from a local directory with no setup.py
- but that does have pyproject.toml.
+ but that does have pyproject.toml with a build backend that does not support
+ the build_editable hook.
"""
local_dir = script.scratch_path.joinpath("temp")
local_dir.mkdir()
pyproject_path = local_dir.joinpath("pyproject.toml")
- pyproject_path.write_text("")
+ pyproject_path.write_text(
+ textwrap.dedent(
+ """
+ [build-system]
+ requires = ["setuptools<64"]
+ build-backend = "setuptools.build_meta"
+ """
+ )
+ )
result = script.pip("install", "-e", local_dir, expect_error=True)
assert not result.files_created
@@ -1253,13 +1263,14 @@ def test_install_editable_with_prefix_setup_py(script: PipTestEnvironment) -> No
_test_install_editable_with_prefix(script, {"setup.py": setup_py})
+@pytest.mark.network
def test_install_editable_with_prefix_setup_cfg(script: PipTestEnvironment) -> None:
setup_cfg = """[metadata]
name = pkga
version = 0.1
"""
pyproject_toml = """[build-system]
-requires = ["setuptools", "wheel"]
+requires = ["setuptools<64", "wheel"]
build-backend = "setuptools.build_meta"
"""
_test_install_editable_with_prefix(
From df8a5011b6f4c35902d48f9c9940980f4d2a6ddf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Sun, 7 Aug 2022 15:57:52 +0200
Subject: [PATCH 071/730] Simplify should_build
---
src/pip/_internal/wheel_builder.py | 11 ++++-------
1 file changed, 4 insertions(+), 7 deletions(-)
diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py
index d2a7146edb7..a166146621b 100644
--- a/src/pip/_internal/wheel_builder.py
+++ b/src/pip/_internal/wheel_builder.py
@@ -5,7 +5,7 @@
import os.path
import re
import shutil
-from typing import Any, Callable, Iterable, List, Optional, Tuple
+from typing import Callable, Iterable, List, Optional, Tuple
from pip._vendor.packaging.utils import canonicalize_name, canonicalize_version
from pip._vendor.packaging.version import InvalidVersion, Version
@@ -47,7 +47,7 @@ def _contains_egg_info(s: str) -> bool:
def _should_build(
req: InstallRequirement,
need_wheel: bool,
- check_binary_allowed: BinaryAllowedPredicate,
+ check_binary_allowed: Optional[BinaryAllowedPredicate] = None,
) -> bool:
"""Return whether an InstallRequirement should be built into a wheel."""
if req.constraint:
@@ -78,6 +78,7 @@ def _should_build(
if req.use_pep517:
return True
+ assert check_binary_allowed is not None
if not check_binary_allowed(req):
logger.info(
"Skipping wheel build for %s, due to binaries being disabled for it.",
@@ -96,7 +97,7 @@ def _should_build(
def should_build_for_wheel_command(
req: InstallRequirement,
) -> bool:
- return _should_build(req, need_wheel=True, check_binary_allowed=_always_true)
+ return _should_build(req, need_wheel=True)
def should_build_for_install_command(
@@ -156,10 +157,6 @@ def _get_cache_dir(
return cache_dir
-def _always_true(_: Any) -> bool:
- return True
-
-
def _verify_one(req: InstallRequirement, wheel_path: str) -> None:
canonical_name = canonicalize_name(req.name or "")
w = Wheel(os.path.basename(wheel_path))
From d8e2d6605ab4bba341f80519cada310556204abe Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Sun, 7 Aug 2022 16:09:01 +0200
Subject: [PATCH 072/730] Rename BinaryAllowedPredicate
It really is a BdistWheelAllowedPredicate and
this will make it easier to reason when --no-binary
does not imply setup.py install anymore.
---
src/pip/_internal/commands/install.py | 12 ++++++++----
src/pip/_internal/wheel_builder.py | 12 ++++++------
tests/unit/test_wheel_builder.py | 10 +++++-----
3 files changed, 19 insertions(+), 15 deletions(-)
diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py
index 91fe3b3b658..dcf5ce8c617 100644
--- a/src/pip/_internal/commands/install.py
+++ b/src/pip/_internal/commands/install.py
@@ -45,7 +45,7 @@
virtualenv_no_global,
)
from pip._internal.wheel_builder import (
- BinaryAllowedPredicate,
+ BdistWheelAllowedPredicate,
build,
should_build_for_install_command,
)
@@ -53,7 +53,9 @@
logger = getLogger(__name__)
-def get_check_binary_allowed(format_control: FormatControl) -> BinaryAllowedPredicate:
+def get_check_bdist_wheel_allowed(
+ format_control: FormatControl,
+) -> BdistWheelAllowedPredicate:
def check_binary_allowed(req: InstallRequirement) -> bool:
canonical_name = canonicalize_name(req.name or "")
allowed_formats = format_control.get_allowed_formats(canonical_name)
@@ -409,12 +411,14 @@ def run(self, options: Values, args: List[str]) -> int:
modifying_pip = pip_req.satisfied_by is None
protect_pip_from_modification_on_windows(modifying_pip=modifying_pip)
- check_binary_allowed = get_check_binary_allowed(finder.format_control)
+ check_bdist_wheel_allowed = get_check_bdist_wheel_allowed(
+ finder.format_control
+ )
reqs_to_build = [
r
for r in requirement_set.requirements.values()
- if should_build_for_install_command(r, check_binary_allowed)
+ if should_build_for_install_command(r, check_bdist_wheel_allowed)
]
_, build_failures = build(
diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py
index a166146621b..60db28e92c3 100644
--- a/src/pip/_internal/wheel_builder.py
+++ b/src/pip/_internal/wheel_builder.py
@@ -32,7 +32,7 @@
_egg_info_re = re.compile(r"([a-z0-9_.]+)-([a-z0-9_.!+-]+)", re.IGNORECASE)
-BinaryAllowedPredicate = Callable[[InstallRequirement], bool]
+BdistWheelAllowedPredicate = Callable[[InstallRequirement], bool]
BuildResult = Tuple[List[InstallRequirement], List[InstallRequirement]]
@@ -47,7 +47,7 @@ def _contains_egg_info(s: str) -> bool:
def _should_build(
req: InstallRequirement,
need_wheel: bool,
- check_binary_allowed: Optional[BinaryAllowedPredicate] = None,
+ check_bdist_wheel: Optional[BdistWheelAllowedPredicate] = None,
) -> bool:
"""Return whether an InstallRequirement should be built into a wheel."""
if req.constraint:
@@ -78,8 +78,8 @@ def _should_build(
if req.use_pep517:
return True
- assert check_binary_allowed is not None
- if not check_binary_allowed(req):
+ assert check_bdist_wheel is not None
+ if not check_bdist_wheel(req):
logger.info(
"Skipping wheel build for %s, due to binaries being disabled for it.",
req.name,
@@ -102,10 +102,10 @@ def should_build_for_wheel_command(
def should_build_for_install_command(
req: InstallRequirement,
- check_binary_allowed: BinaryAllowedPredicate,
+ check_bdist_wheel_allowed: BdistWheelAllowedPredicate,
) -> bool:
return _should_build(
- req, need_wheel=False, check_binary_allowed=check_binary_allowed
+ req, need_wheel=False, check_bdist_wheel=check_bdist_wheel_allowed
)
diff --git a/tests/unit/test_wheel_builder.py b/tests/unit/test_wheel_builder.py
index 2329899608a..5444056e790 100644
--- a/tests/unit/test_wheel_builder.py
+++ b/tests/unit/test_wheel_builder.py
@@ -58,7 +58,7 @@ def supports_pyproject_editable(self) -> bool:
@pytest.mark.parametrize(
- "req, disallow_binaries, expected",
+ "req, disallow_bdist_wheel, expected",
[
# When binaries are allowed, we build.
(ReqMock(use_pep517=True), False, True),
@@ -110,11 +110,11 @@ def supports_pyproject_editable(self) -> bool:
],
)
def test_should_build_for_install_command(
- req: ReqMock, disallow_binaries: bool, expected: bool
+ req: ReqMock, disallow_bdist_wheel: bool, expected: bool
) -> None:
should_build = wheel_builder.should_build_for_install_command(
cast(InstallRequirement, req),
- check_binary_allowed=lambda req: not disallow_binaries,
+ check_bdist_wheel_allowed=lambda req: not disallow_bdist_wheel,
)
assert should_build is expected
@@ -144,7 +144,7 @@ def test_should_build_legacy_wheel_not_installed(is_wheel_installed: mock.Mock)
legacy_req = ReqMock(use_pep517=False)
should_build = wheel_builder.should_build_for_install_command(
cast(InstallRequirement, legacy_req),
- check_binary_allowed=lambda req: True,
+ check_bdist_wheel_allowed=lambda req: True,
)
assert not should_build
@@ -155,7 +155,7 @@ def test_should_build_legacy_wheel_installed(is_wheel_installed: mock.Mock) -> N
legacy_req = ReqMock(use_pep517=False)
should_build = wheel_builder.should_build_for_install_command(
cast(InstallRequirement, legacy_req),
- check_binary_allowed=lambda req: True,
+ check_bdist_wheel_allowed=lambda req: True,
)
assert should_build
From b9ec5ddc297259cc81dfcb6c590da5700554ae8b Mon Sep 17 00:00:00 2001
From: hauntsaninja
Date: Fri, 12 Aug 2022 18:44:48 -0700
Subject: [PATCH 073/730] Use --no-implicit-optional for type checking
This makes type checking PEP 484 compliant (as of 2018).
mypy will change its defaults soon.
See:
https://github.com/python/mypy/issues/9091
https://github.com/python/mypy/pull/13401
---
src/pip/_internal/cli/spinners.py | 4 ++--
src/pip/_internal/exceptions.py | 5 ++++-
src/pip/_internal/locations/_distutils.py | 6 +++---
src/pip/_internal/operations/install/wheel.py | 4 +++-
src/pip/_internal/utils/hashes.py | 2 +-
src/pip/_internal/utils/setuptools_build.py | 2 +-
src/pip/_internal/vcs/subversion.py | 2 +-
7 files changed, 15 insertions(+), 10 deletions(-)
diff --git a/src/pip/_internal/cli/spinners.py b/src/pip/_internal/cli/spinners.py
index a50e6adf263..cf2b976f377 100644
--- a/src/pip/_internal/cli/spinners.py
+++ b/src/pip/_internal/cli/spinners.py
@@ -3,7 +3,7 @@
import logging
import sys
import time
-from typing import IO, Generator
+from typing import IO, Generator, Optional
from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.logging import get_indentation
@@ -23,7 +23,7 @@ class InteractiveSpinner(SpinnerInterface):
def __init__(
self,
message: str,
- file: IO[str] = None,
+ file: Optional[IO[str]] = None,
spin_chars: str = "-\\|/",
# Empirically, 8 updates/second looks nice
min_update_interval_seconds: float = 0.125,
diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py
index 97b9612a187..377cde52521 100644
--- a/src/pip/_internal/exceptions.py
+++ b/src/pip/_internal/exceptions.py
@@ -288,7 +288,10 @@ class NetworkConnectionError(PipError):
"""HTTP connection error"""
def __init__(
- self, error_msg: str, response: Response = None, request: Request = None
+ self,
+ error_msg: str,
+ response: Optional[Response] = None,
+ request: Optional[Request] = None,
) -> None:
"""
Initialize NetworkConnectionError with `request` and `response`
diff --git a/src/pip/_internal/locations/_distutils.py b/src/pip/_internal/locations/_distutils.py
index fbcb04f488f..c7712f016f5 100644
--- a/src/pip/_internal/locations/_distutils.py
+++ b/src/pip/_internal/locations/_distutils.py
@@ -35,10 +35,10 @@
def distutils_scheme(
dist_name: str,
user: bool = False,
- home: str = None,
- root: str = None,
+ home: Optional[str] = None,
+ root: Optional[str] = None,
isolated: bool = False,
- prefix: str = None,
+ prefix: Optional[str] = None,
*,
ignore_config_files: bool = False,
) -> Dict[str, str]:
diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py
index 1af8978d409..1650d59a374 100644
--- a/src/pip/_internal/operations/install/wheel.py
+++ b/src/pip/_internal/operations/install/wheel.py
@@ -420,7 +420,9 @@ def _raise_for_invalid_entrypoint(specification: str) -> None:
class PipScriptMaker(ScriptMaker):
- def make(self, specification: str, options: Dict[str, Any] = None) -> List[str]:
+ def make(
+ self, specification: str, options: Optional[Dict[str, Any]] = None
+ ) -> List[str]:
_raise_for_invalid_entrypoint(specification)
return super().make(specification, options)
diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py
index 0c1af327cc2..e79cfdb8c20 100644
--- a/src/pip/_internal/utils/hashes.py
+++ b/src/pip/_internal/utils/hashes.py
@@ -28,7 +28,7 @@ class Hashes:
"""
- def __init__(self, hashes: Dict[str, List[str]] = None) -> None:
+ def __init__(self, hashes: Optional[Dict[str, List[str]]] = None) -> None:
"""
:param hashes: A dict of algorithm names pointing to lists of allowed
hex digests
diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py
index f460c4003f3..01ef4a4ca59 100644
--- a/src/pip/_internal/utils/setuptools_build.py
+++ b/src/pip/_internal/utils/setuptools_build.py
@@ -48,7 +48,7 @@
def make_setuptools_shim_args(
setup_py_path: str,
- global_options: Sequence[str] = None,
+ global_options: Optional[Sequence[str]] = None,
no_user_config: bool = False,
unbuffered_output: bool = False,
) -> List[str]:
diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py
index 89c8754ce09..2cd6f0ae9d2 100644
--- a/src/pip/_internal/vcs/subversion.py
+++ b/src/pip/_internal/vcs/subversion.py
@@ -184,7 +184,7 @@ def is_commit_id_equal(cls, dest: str, name: Optional[str]) -> bool:
"""Always assume the versions don't match"""
return False
- def __init__(self, use_interactive: bool = None) -> None:
+ def __init__(self, use_interactive: Optional[bool] = None) -> None:
if use_interactive is None:
use_interactive = is_console_interactive()
self.use_interactive = use_interactive
From 4d13842ec6e246e90718c371b74503cee8d061ad Mon Sep 17 00:00:00 2001
From: hauntsaninja
Date: Fri, 12 Aug 2022 18:48:15 -0700
Subject: [PATCH 074/730] fixups
---
setup.cfg | 1 +
src/pip/_internal/utils/hashes.py | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/setup.cfg b/setup.cfg
index bdc224e6dd6..dae2f21b10d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -40,6 +40,7 @@ ignore_missing_imports = True
disallow_untyped_defs = True
disallow_any_generics = True
warn_unused_ignores = True
+no_implicit_optional = True
[mypy-pip._vendor.*]
ignore_errors = True
diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py
index e79cfdb8c20..76727306a4c 100644
--- a/src/pip/_internal/utils/hashes.py
+++ b/src/pip/_internal/utils/hashes.py
@@ -1,5 +1,5 @@
import hashlib
-from typing import TYPE_CHECKING, BinaryIO, Dict, Iterable, List
+from typing import TYPE_CHECKING, BinaryIO, Dict, Iterable, List, Optional
from pip._internal.exceptions import HashMismatch, HashMissing, InstallationError
from pip._internal.utils.misc import read_chunks
From c0b86d338a69741fc791bd94575f8cfdecfc1c3e Mon Sep 17 00:00:00 2001
From: hauntsaninja
Date: Fri, 12 Aug 2022 18:51:47 -0700
Subject: [PATCH 075/730] no news today
---
news/5580954E-E089-4CDB-857A-868BA1F7435D.trivial.rst | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 news/5580954E-E089-4CDB-857A-868BA1F7435D.trivial.rst
diff --git a/news/5580954E-E089-4CDB-857A-868BA1F7435D.trivial.rst b/news/5580954E-E089-4CDB-857A-868BA1F7435D.trivial.rst
new file mode 100644
index 00000000000..e69de29bb2d
From edbfeae9fbd4618256091bceabb494749cfc2c94 Mon Sep 17 00:00:00 2001
From: hauntsaninja
Date: Fri, 12 Aug 2022 19:01:26 -0700
Subject: [PATCH 076/730] fix tests
---
tests/lib/__init__.py | 2 +-
tests/unit/test_req_file.py | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py
index 1dfaea7e0f2..1436f7a42c7 100644
--- a/tests/lib/__init__.py
+++ b/tests/lib/__init__.py
@@ -1168,7 +1168,7 @@ def create_basic_wheel_for_package(
name: str,
version: str,
depends: Optional[List[str]] = None,
- extras: Dict[str, List[str]] = None,
+ extras: Optional[Dict[str, List[str]]] = None,
requires_python: Optional[str] = None,
extra_files: Optional[Dict[str, Union[bytes, str]]] = None,
) -> pathlib.Path:
diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py
index 8928fd1690f..ef575f601d2 100644
--- a/tests/unit/test_req_file.py
+++ b/tests/unit/test_req_file.py
@@ -60,8 +60,8 @@ def options(session: PipSession) -> mock.Mock:
def parse_reqfile(
filename: Union[Path, str],
session: PipSession,
- finder: PackageFinder = None,
- options: Values = None,
+ finder: Optional[PackageFinder] = None,
+ options: Optional[Values] = None,
constraint: bool = False,
isolated: bool = False,
) -> Iterator[InstallRequirement]:
From 1413fae8eb15ba38751173cdd29258c4f8f23790 Mon Sep 17 00:00:00 2001
From: Kai Mueller
Date: Sun, 14 Aug 2022 19:26:08 +0000
Subject: [PATCH 077/730] Add news
---
news/11254.trivial.rst | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 news/11254.trivial.rst
diff --git a/news/11254.trivial.rst b/news/11254.trivial.rst
new file mode 100644
index 00000000000..e69de29bb2d
From 26b66a830fd9322dcc826fee2f1924670ea6c976 Mon Sep 17 00:00:00 2001
From: Tzu-ping Chung
Date: Wed, 17 Aug 2022 06:41:27 +0800
Subject: [PATCH 078/730] Decrease timeout to make test less flaky
---
tests/functional/test_requests.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/functional/test_requests.py b/tests/functional/test_requests.py
index 0fbc4ae0e36..66050c518e5 100644
--- a/tests/functional/test_requests.py
+++ b/tests/functional/test_requests.py
@@ -7,7 +7,7 @@
def test_timeout(script: PipTestEnvironment) -> None:
result = script.pip(
"--timeout",
- "0.0001",
+ "0.00001",
"install",
"-vvv",
"INITools",
From 5ec3f37bc87ea129790c9e9408d392343ef72161 Mon Sep 17 00:00:00 2001
From: Tzu-ping Chung
Date: Wed, 17 Aug 2022 15:38:15 +0800
Subject: [PATCH 079/730] Don't retry to 'improve' possibility of failure
---
tests/functional/test_requests.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/tests/functional/test_requests.py b/tests/functional/test_requests.py
index 66050c518e5..622b150aa83 100644
--- a/tests/functional/test_requests.py
+++ b/tests/functional/test_requests.py
@@ -6,6 +6,8 @@
@pytest.mark.network
def test_timeout(script: PipTestEnvironment) -> None:
result = script.pip(
+ "--retries",
+ "1",
"--timeout",
"0.00001",
"install",
From 72ce3ba0fe9b7ff740dc29a87d0863d9416b3cf8 Mon Sep 17 00:00:00 2001
From: Diego Ramirez
Date: Thu, 18 Aug 2022 11:02:37 -0500
Subject: [PATCH 080/730] Delete the "good first issue" template
Seems like we're not using this template, and other users are using it as "good first reporters".
---
.github/ISSUE_TEMPLATE/~good-first-issue.yml | 38 --------------------
1 file changed, 38 deletions(-)
delete mode 100644 .github/ISSUE_TEMPLATE/~good-first-issue.yml
diff --git a/.github/ISSUE_TEMPLATE/~good-first-issue.yml b/.github/ISSUE_TEMPLATE/~good-first-issue.yml
deleted file mode 100644
index 81e206a35f9..00000000000
--- a/.github/ISSUE_TEMPLATE/~good-first-issue.yml
+++ /dev/null
@@ -1,38 +0,0 @@
-name: Good first issue
-description: If you're a pip maintainer, use this to create a "good first issue" for new contributors.
-labels: "good first issue"
-
-body:
- - type: textarea
- attributes:
- label: Description
- description: >-
- A clear and concise description of what the task is.
- validations:
- required: true
-
- - type: textarea
- attributes:
- label: What needs to be done
- description: >-
- Describe what the contributor would need to do, describing the change.
- See https://github.com/pypa/pip/issues/7661 for example.
- validations:
- required: true
-
- - type: textarea
- attributes:
- label: Guidance for potential contributors
- description: >-
- Usually, you don't have to modify the content here.
- value: >-
- This issue is a good starting point for first time contributors -- the
- process of fixing this should be a good introduction to pip's
- development workflow. If there is not a corresponding pull request for
- this issue, it is up for grabs. For directions for getting set up, see our
- [Getting Started Guide](https://pip.pypa.io/en/latest/development/getting-started/).
- If you are working on this issue and have questions, feel free to ask
- them here. If you've contributed code to pip before, we encourage you to
- pick up an issue without this label.
- validations:
- required: true
From 7e1bb71b050ce817c4a535cca999313ec4a550f1 Mon Sep 17 00:00:00 2001
From: Pradyun Gedam
Date: Fri, 26 Aug 2022 13:44:15 +0100
Subject: [PATCH 081/730] Mention pip config on the page about pip's
configuration
This is relevant to the topic page and should make this command more
visible.
---
docs/html/topics/configuration.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/docs/html/topics/configuration.md b/docs/html/topics/configuration.md
index 9b240ec7902..e4aafcd2b98 100644
--- a/docs/html/topics/configuration.md
+++ b/docs/html/topics/configuration.md
@@ -11,6 +11,10 @@ pip allows a user to change its behaviour via 3 mechanisms:
This page explains how the configuration files and environment variables work,
and how they are related to pip's various command line options.
+```{seealso}
+{doc}`../cli/pip_config` command, which helps manage pip's configuration.
+```
+
(config-file)=
## Configuration Files
From 254e668eef34ca21005634a2bdba9d9a74deaa26 Mon Sep 17 00:00:00 2001
From: M00nL1ght <69127692+SCH227@users.noreply.github.com>
Date: Tue, 30 Aug 2022 05:51:29 +0300
Subject: [PATCH 082/730] Fix vulnerable regex
Implement exclusive RE searches to avoid backtracking
---
src/pip/_internal/models/wheel.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/pip/_internal/models/wheel.py b/src/pip/_internal/models/wheel.py
index 35c70375539..a5dc12bdd63 100644
--- a/src/pip/_internal/models/wheel.py
+++ b/src/pip/_internal/models/wheel.py
@@ -13,8 +13,8 @@ class Wheel:
"""A wheel file"""
wheel_file_re = re.compile(
- r"""^(?P(?P.+?)-(?P.*?))
- ((-(?P\d[^-]*?))?-(?P.+?)-(?P.+?)-(?P.+?)
+ r"""^(?P(?P[^\s-]+?)-(?P[^\s-]*?))
+ ((-(?P\d[^-]*?))?-(?P[^\s-]+?)-(?P[^\s-]+?)-(?P[^\s-]+?)
\.whl|\.dist-info)$""",
re.VERBOSE,
)
From 321018fb930e95c85fc218e3ecf1ea1436309fc6 Mon Sep 17 00:00:00 2001
From: M00nL1ght <69127692+SCH227@users.noreply.github.com>
Date: Tue, 30 Aug 2022 06:04:32 +0300
Subject: [PATCH 083/730] Create 11418.bugfix.rst
---
news/11418.bugfix.rst | 1 +
1 file changed, 1 insertion(+)
create mode 100644 news/11418.bugfix.rst
diff --git a/news/11418.bugfix.rst b/news/11418.bugfix.rst
new file mode 100644
index 00000000000..df32a0d0bc3
--- /dev/null
+++ b/news/11418.bugfix.rst
@@ -0,0 +1 @@
+Patch non-exploitable ReDoS vulnerability in wheel_file regex
From 7485260b4e741ddf7b0fbcf5efe54feac768321e Mon Sep 17 00:00:00 2001
From: Pradyun Gedam
Date: Fri, 2 Sep 2022 21:42:08 +0100
Subject: [PATCH 084/730] Update bug-report.yml
Drop the "render", because our users are generally smart and GitHub is not.
---
.github/ISSUE_TEMPLATE/bug-report.yml | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
index 51a290f50c7..e28e5408208 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.yml
+++ b/.github/ISSUE_TEMPLATE/bug-report.yml
@@ -60,14 +60,12 @@ body:
label: Output
description: >-
Provide the output of the steps above, including the commands
- themselves and pip's output/traceback etc. If you're familiar with
- Markdown, DO NOT add backticks. They're added automatically.
+ themselves and pip's output/traceback etc.
If you want to present output from multiple commands, please prefix
the line containing the command with `$ `. Please also ensure that
the "How to reproduce" section contains matching instructions for
reproducing this.
- render: shell
- type: checkboxes
attributes:
From 8856b5900e6f902ca2e72f0e625591dc9d75c0d3 Mon Sep 17 00:00:00 2001
From: Tzu-ping Chung
Date: Thu, 8 Sep 2022 10:24:04 +0800
Subject: [PATCH 085/730] Further attempt to stablize timeout test
---
tests/functional/test_requests.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/functional/test_requests.py b/tests/functional/test_requests.py
index 622b150aa83..2ef121fedcb 100644
--- a/tests/functional/test_requests.py
+++ b/tests/functional/test_requests.py
@@ -7,7 +7,7 @@
def test_timeout(script: PipTestEnvironment) -> None:
result = script.pip(
"--retries",
- "1",
+ "0",
"--timeout",
"0.00001",
"install",
From bad03ef931d9b3ff4f9e75f35f9c41f45839e2a1 Mon Sep 17 00:00:00 2001
From: Danny McClanahan <1305167+cosmicexplorer@users.noreply.github.com>
Date: Sat, 10 Sep 2022 10:28:57 +0000
Subject: [PATCH 086/730] Use data-dist-info-metadata (PEP 658) to decouple
resolution from downloading (#11111)
Co-authored-by: Tzu-ping Chung
---
news/11111.feature.rst | 1 +
src/pip/_internal/exceptions.py | 11 +-
src/pip/_internal/index/collector.py | 120 +-----
src/pip/_internal/metadata/__init__.py | 22 ++
src/pip/_internal/metadata/base.py | 18 +
.../_internal/metadata/importlib/_dists.py | 18 +
src/pip/_internal/metadata/pkg_resources.py | 23 +-
src/pip/_internal/models/link.py | 247 ++++++++++--
src/pip/_internal/operations/prepare.py | 73 +++-
tests/functional/test_download.py | 355 +++++++++++++++++-
tests/functional/test_new_resolver.py | 2 +-
tests/lib/server.py | 8 -
.../metadata/test_metadata_pkg_resources.py | 6 +-
tests/unit/test_collector.py | 110 +++++-
14 files changed, 834 insertions(+), 180 deletions(-)
create mode 100644 news/11111.feature.rst
diff --git a/news/11111.feature.rst b/news/11111.feature.rst
new file mode 100644
index 00000000000..39cb4b35c12
--- /dev/null
+++ b/news/11111.feature.rst
@@ -0,0 +1 @@
+Use the ``data-dist-info-metadata`` attribute from :pep:`658` to resolve distribution metadata without downloading the dist yet.
diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py
index 377cde52521..2ab1f591f12 100644
--- a/src/pip/_internal/exceptions.py
+++ b/src/pip/_internal/exceptions.py
@@ -335,8 +335,8 @@ class MetadataInconsistent(InstallationError):
"""Built metadata contains inconsistent information.
This is raised when the metadata contains values (e.g. name and version)
- that do not match the information previously obtained from sdist filename
- or user-supplied ``#egg=`` value.
+ that do not match the information previously obtained from sdist filename,
+ user-supplied ``#egg=`` value, or an install requirement name.
"""
def __init__(
@@ -348,11 +348,10 @@ def __init__(
self.m_val = m_val
def __str__(self) -> str:
- template = (
- "Requested {} has inconsistent {}: "
- "filename has {!r}, but metadata has {!r}"
+ return (
+ f"Requested {self.ireq} has inconsistent {self.field}: "
+ f"expected {self.f_val!r}, but metadata has {self.m_val!r}"
)
- return template.format(self.ireq, self.field, self.f_val, self.m_val)
class LegacyInstallFailure(DiagnosticPipError):
diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py
index f4e6e221f5d..0120610c758 100644
--- a/src/pip/_internal/index/collector.py
+++ b/src/pip/_internal/index/collector.py
@@ -9,10 +9,8 @@
import json
import logging
import os
-import re
import urllib.parse
import urllib.request
-import xml.etree.ElementTree
from html.parser import HTMLParser
from optparse import Values
from typing import (
@@ -39,7 +37,7 @@
from pip._internal.network.session import PipSession
from pip._internal.network.utils import raise_for_status
from pip._internal.utils.filetypes import is_archive_file
-from pip._internal.utils.misc import pairwise, redact_auth_from_url
+from pip._internal.utils.misc import redact_auth_from_url
from pip._internal.vcs import vcs
from .sources import CandidatesFromPage, LinkSource, build_source
@@ -51,7 +49,6 @@
logger = logging.getLogger(__name__)
-HTMLElement = xml.etree.ElementTree.Element
ResponseHeaders = MutableMapping[str, str]
@@ -191,94 +188,6 @@ def _get_encoding_from_headers(headers: ResponseHeaders) -> Optional[str]:
return None
-def _clean_url_path_part(part: str) -> str:
- """
- Clean a "part" of a URL path (i.e. after splitting on "@" characters).
- """
- # We unquote prior to quoting to make sure nothing is double quoted.
- return urllib.parse.quote(urllib.parse.unquote(part))
-
-
-def _clean_file_url_path(part: str) -> str:
- """
- Clean the first part of a URL path that corresponds to a local
- filesystem path (i.e. the first part after splitting on "@" characters).
- """
- # We unquote prior to quoting to make sure nothing is double quoted.
- # Also, on Windows the path part might contain a drive letter which
- # should not be quoted. On Linux where drive letters do not
- # exist, the colon should be quoted. We rely on urllib.request
- # to do the right thing here.
- return urllib.request.pathname2url(urllib.request.url2pathname(part))
-
-
-# percent-encoded: /
-_reserved_chars_re = re.compile("(@|%2F)", re.IGNORECASE)
-
-
-def _clean_url_path(path: str, is_local_path: bool) -> str:
- """
- Clean the path portion of a URL.
- """
- if is_local_path:
- clean_func = _clean_file_url_path
- else:
- clean_func = _clean_url_path_part
-
- # Split on the reserved characters prior to cleaning so that
- # revision strings in VCS URLs are properly preserved.
- parts = _reserved_chars_re.split(path)
-
- cleaned_parts = []
- for to_clean, reserved in pairwise(itertools.chain(parts, [""])):
- cleaned_parts.append(clean_func(to_clean))
- # Normalize %xx escapes (e.g. %2f -> %2F)
- cleaned_parts.append(reserved.upper())
-
- return "".join(cleaned_parts)
-
-
-def _clean_link(url: str) -> str:
- """
- Make sure a link is fully quoted.
- For example, if ' ' occurs in the URL, it will be replaced with "%20",
- and without double-quoting other characters.
- """
- # Split the URL into parts according to the general structure
- # `scheme://netloc/path;parameters?query#fragment`.
- result = urllib.parse.urlparse(url)
- # If the netloc is empty, then the URL refers to a local filesystem path.
- is_local_path = not result.netloc
- path = _clean_url_path(result.path, is_local_path=is_local_path)
- return urllib.parse.urlunparse(result._replace(path=path))
-
-
-def _create_link_from_element(
- element_attribs: Dict[str, Optional[str]],
- page_url: str,
- base_url: str,
-) -> Optional[Link]:
- """
- Convert an anchor element's attributes in a simple repository page to a Link.
- """
- href = element_attribs.get("href")
- if not href:
- return None
-
- url = _clean_link(urllib.parse.urljoin(base_url, href))
- pyrequire = element_attribs.get("data-requires-python")
- yanked_reason = element_attribs.get("data-yanked")
-
- link = Link(
- url,
- comes_from=page_url,
- requires_python=pyrequire,
- yanked_reason=yanked_reason,
- )
-
- return link
-
-
class CacheablePageContent:
def __init__(self, page: "IndexContent") -> None:
assert page.cache_link_parsing
@@ -326,25 +235,10 @@ def parse_links(page: "IndexContent") -> Iterable[Link]:
if content_type_l.startswith("application/vnd.pypi.simple.v1+json"):
data = json.loads(page.content)
for file in data.get("files", []):
- file_url = file.get("url")
- if file_url is None:
+ link = Link.from_json(file, page.url)
+ if link is None:
continue
-
- # The Link.yanked_reason expects an empty string instead of a boolean.
- yanked_reason = file.get("yanked")
- if yanked_reason and not isinstance(yanked_reason, str):
- yanked_reason = ""
- # The Link.yanked_reason expects None instead of False
- elif not yanked_reason:
- yanked_reason = None
-
- yield Link(
- _clean_link(urllib.parse.urljoin(page.url, file_url)),
- comes_from=page.url,
- requires_python=file.get("requires-python"),
- yanked_reason=yanked_reason,
- hashes=file.get("hashes", {}),
- )
+ yield link
return
parser = HTMLLinkParser(page.url)
@@ -354,11 +248,7 @@ def parse_links(page: "IndexContent") -> Iterable[Link]:
url = page.url
base_url = parser.base_url or url
for anchor in parser.anchors:
- link = _create_link_from_element(
- anchor,
- page_url=url,
- base_url=base_url,
- )
+ link = Link.from_element(anchor, page_url=url, base_url=base_url)
if link is None:
continue
yield link
diff --git a/src/pip/_internal/metadata/__init__.py b/src/pip/_internal/metadata/__init__.py
index 8cd0fda6851..9f73ca7105f 100644
--- a/src/pip/_internal/metadata/__init__.py
+++ b/src/pip/_internal/metadata/__init__.py
@@ -103,3 +103,25 @@ def get_wheel_distribution(wheel: Wheel, canonical_name: str) -> BaseDistributio
:param canonical_name: Normalized project name of the given wheel.
"""
return select_backend().Distribution.from_wheel(wheel, canonical_name)
+
+
+def get_metadata_distribution(
+ metadata_contents: bytes,
+ filename: str,
+ canonical_name: str,
+) -> BaseDistribution:
+ """Get the dist representation of the specified METADATA file contents.
+
+ This returns a Distribution instance from the chosen backend sourced from the data
+ in `metadata_contents`.
+
+ :param metadata_contents: Contents of a METADATA file within a dist, or one served
+ via PEP 658.
+ :param filename: Filename for the dist this metadata represents.
+ :param canonical_name: Normalized project name of the given dist.
+ """
+ return select_backend().Distribution.from_metadata_file_contents(
+ metadata_contents,
+ filename,
+ canonical_name,
+ )
diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py
index 151fd6d009e..cafb79fb3dc 100644
--- a/src/pip/_internal/metadata/base.py
+++ b/src/pip/_internal/metadata/base.py
@@ -113,6 +113,24 @@ def from_directory(cls, directory: str) -> "BaseDistribution":
"""
raise NotImplementedError()
+ @classmethod
+ def from_metadata_file_contents(
+ cls,
+ metadata_contents: bytes,
+ filename: str,
+ project_name: str,
+ ) -> "BaseDistribution":
+ """Load the distribution from the contents of a METADATA file.
+
+ This is used to implement PEP 658 by generating a "shallow" dist object that can
+ be used for resolution without downloading or building the actual dist yet.
+
+ :param metadata_contents: The contents of a METADATA file.
+ :param filename: File name for the dist with this metadata.
+ :param project_name: Name of the project this dist represents.
+ """
+ raise NotImplementedError()
+
@classmethod
def from_wheel(cls, wheel: "Wheel", name: str) -> "BaseDistribution":
"""Load the distribution from a given wheel.
diff --git a/src/pip/_internal/metadata/importlib/_dists.py b/src/pip/_internal/metadata/importlib/_dists.py
index fbf9a93218a..65c043c87ef 100644
--- a/src/pip/_internal/metadata/importlib/_dists.py
+++ b/src/pip/_internal/metadata/importlib/_dists.py
@@ -28,6 +28,7 @@
)
from pip._internal.utils.misc import normalize_path
from pip._internal.utils.packaging import safe_extra
+from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.utils.wheel import parse_wheel, read_wheel_metadata_file
from ._compat import BasePath, get_dist_name
@@ -109,6 +110,23 @@ def from_directory(cls, directory: str) -> BaseDistribution:
dist = importlib.metadata.Distribution.at(info_location)
return cls(dist, info_location, info_location.parent)
+ @classmethod
+ def from_metadata_file_contents(
+ cls,
+ metadata_contents: bytes,
+ filename: str,
+ project_name: str,
+ ) -> BaseDistribution:
+ # Generate temp dir to contain the metadata file, and write the file contents.
+ temp_dir = pathlib.Path(
+ TempDirectory(kind="metadata", globally_managed=True).path
+ )
+ metadata_path = temp_dir / "METADATA"
+ metadata_path.write_bytes(metadata_contents)
+ # Construct dist pointing to the newly created directory.
+ dist = importlib.metadata.Distribution.at(metadata_path.parent)
+ return cls(dist, metadata_path.parent, None)
+
@classmethod
def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
try:
diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py
index bf79ba139c0..f330ef12a2c 100644
--- a/src/pip/_internal/metadata/pkg_resources.py
+++ b/src/pip/_internal/metadata/pkg_resources.py
@@ -33,7 +33,7 @@ class EntryPoint(NamedTuple):
group: str
-class WheelMetadata:
+class InMemoryMetadata:
"""IMetadataProvider that reads metadata files from a dictionary.
This also maps metadata decoding exceptions to our internal exception type.
@@ -92,12 +92,29 @@ def from_directory(cls, directory: str) -> BaseDistribution:
dist = dist_cls(base_dir, project_name=dist_name, metadata=metadata)
return cls(dist)
+ @classmethod
+ def from_metadata_file_contents(
+ cls,
+ metadata_contents: bytes,
+ filename: str,
+ project_name: str,
+ ) -> BaseDistribution:
+ metadata_dict = {
+ "METADATA": metadata_contents,
+ }
+ dist = pkg_resources.DistInfoDistribution(
+ location=filename,
+ metadata=InMemoryMetadata(metadata_dict, filename),
+ project_name=project_name,
+ )
+ return cls(dist)
+
@classmethod
def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
try:
with wheel.as_zipfile() as zf:
info_dir, _ = parse_wheel(zf, name)
- metadata_text = {
+ metadata_dict = {
path.split("/", 1)[-1]: read_wheel_metadata_file(zf, path)
for path in zf.namelist()
if path.startswith(f"{info_dir}/")
@@ -108,7 +125,7 @@ def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
raise UnsupportedWheel(f"{name} has an invalid wheel, {e}")
dist = pkg_resources.DistInfoDistribution(
location=wheel.location,
- metadata=WheelMetadata(metadata_text, wheel.location),
+ metadata=InMemoryMetadata(metadata_dict, wheel.location),
project_name=name,
)
return cls(dist)
diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py
index 8fd1c3d9960..c792d128bcf 100644
--- a/src/pip/_internal/models/link.py
+++ b/src/pip/_internal/models/link.py
@@ -1,11 +1,14 @@
import functools
+import itertools
import logging
import os
import posixpath
import re
import urllib.parse
+from dataclasses import dataclass
from typing import (
TYPE_CHECKING,
+ Any,
Dict,
List,
Mapping,
@@ -18,6 +21,7 @@
from pip._internal.utils.filetypes import WHEEL_EXTENSION
from pip._internal.utils.hashes import Hashes
from pip._internal.utils.misc import (
+ pairwise,
redact_auth_from_url,
split_auth_from_netloc,
splitext,
@@ -36,6 +40,119 @@
_SUPPORTED_HASHES = ("sha512", "sha384", "sha256", "sha224", "sha1", "md5")
+@dataclass(frozen=True)
+class LinkHash:
+ """Links to content may have embedded hash values. This class parses those.
+
+ `name` must be any member of `_SUPPORTED_HASHES`.
+
+ This class can be converted to and from `ArchiveInfo`. While ArchiveInfo intends to
+ be JSON-serializable to conform to PEP 610, this class contains the logic for
+ parsing a hash name and value for correctness, and then checking whether that hash
+ conforms to a schema with `.is_hash_allowed()`."""
+
+ name: str
+ value: str
+
+ _hash_re = re.compile(
+ # NB: we do not validate that the second group (.*) is a valid hex
+ # digest. Instead, we simply keep that string in this class, and then check it
+ # against Hashes when hash-checking is needed. This is easier to debug than
+ # proactively discarding an invalid hex digest, as we handle incorrect hashes
+ # and malformed hashes in the same place.
+ r"({choices})=(.*)".format(
+ choices="|".join(re.escape(hash_name) for hash_name in _SUPPORTED_HASHES)
+ ),
+ )
+
+ def __post_init__(self) -> None:
+ assert self._hash_re.match(f"{self.name}={self.value}")
+
+ @classmethod
+ @functools.lru_cache(maxsize=None)
+ def split_hash_name_and_value(cls, url: str) -> Optional["LinkHash"]:
+ """Search a string for a checksum algorithm name and encoded output value."""
+ match = cls._hash_re.search(url)
+ if match is None:
+ return None
+ name, value = match.groups()
+ return cls(name=name, value=value)
+
+ def as_hashes(self) -> Hashes:
+ """Return a Hashes instance which checks only for the current hash."""
+ return Hashes({self.name: [self.value]})
+
+ def is_hash_allowed(self, hashes: Optional[Hashes]) -> bool:
+ """
+ Return True if the current hash is allowed by `hashes`.
+ """
+ if hashes is None:
+ return False
+ return hashes.is_hash_allowed(self.name, hex_digest=self.value)
+
+
+def _clean_url_path_part(part: str) -> str:
+ """
+ Clean a "part" of a URL path (i.e. after splitting on "@" characters).
+ """
+ # We unquote prior to quoting to make sure nothing is double quoted.
+ return urllib.parse.quote(urllib.parse.unquote(part))
+
+
+def _clean_file_url_path(part: str) -> str:
+ """
+ Clean the first part of a URL path that corresponds to a local
+ filesystem path (i.e. the first part after splitting on "@" characters).
+ """
+ # We unquote prior to quoting to make sure nothing is double quoted.
+ # Also, on Windows the path part might contain a drive letter which
+ # should not be quoted. On Linux where drive letters do not
+ # exist, the colon should be quoted. We rely on urllib.request
+ # to do the right thing here.
+ return urllib.request.pathname2url(urllib.request.url2pathname(part))
+
+
+# percent-encoded: /
+_reserved_chars_re = re.compile("(@|%2F)", re.IGNORECASE)
+
+
+def _clean_url_path(path: str, is_local_path: bool) -> str:
+ """
+ Clean the path portion of a URL.
+ """
+ if is_local_path:
+ clean_func = _clean_file_url_path
+ else:
+ clean_func = _clean_url_path_part
+
+ # Split on the reserved characters prior to cleaning so that
+ # revision strings in VCS URLs are properly preserved.
+ parts = _reserved_chars_re.split(path)
+
+ cleaned_parts = []
+ for to_clean, reserved in pairwise(itertools.chain(parts, [""])):
+ cleaned_parts.append(clean_func(to_clean))
+ # Normalize %xx escapes (e.g. %2f -> %2F)
+ cleaned_parts.append(reserved.upper())
+
+ return "".join(cleaned_parts)
+
+
+def _ensure_quoted_url(url: str) -> str:
+ """
+ Make sure a link is fully quoted.
+ For example, if ' ' occurs in the URL, it will be replaced with "%20",
+ and without double-quoting other characters.
+ """
+ # Split the URL into parts according to the general structure
+ # `scheme://netloc/path;parameters?query#fragment`.
+ result = urllib.parse.urlparse(url)
+ # If the netloc is empty, then the URL refers to a local filesystem path.
+ is_local_path = not result.netloc
+ path = _clean_url_path(result.path, is_local_path=is_local_path)
+ return urllib.parse.urlunparse(result._replace(path=path))
+
+
class Link(KeyBasedCompareMixin):
"""Represents a parsed link from a Package Index's simple URL"""
@@ -46,6 +163,8 @@ class Link(KeyBasedCompareMixin):
"comes_from",
"requires_python",
"yanked_reason",
+ "dist_info_metadata",
+ "link_hash",
"cache_link_parsing",
]
@@ -55,6 +174,8 @@ def __init__(
comes_from: Optional[Union[str, "IndexContent"]] = None,
requires_python: Optional[str] = None,
yanked_reason: Optional[str] = None,
+ dist_info_metadata: Optional[str] = None,
+ link_hash: Optional[LinkHash] = None,
cache_link_parsing: bool = True,
hashes: Optional[Mapping[str, str]] = None,
) -> None:
@@ -72,6 +193,14 @@ def __init__(
a simple repository HTML link. If the file has been yanked but
no reason was provided, this should be the empty string. See
PEP 592 for more information and the specification.
+ :param dist_info_metadata: the metadata attached to the file, or None if no such
+ metadata is provided. This is the value of the "data-dist-info-metadata"
+ attribute, if present, in a simple repository HTML link. This may be parsed
+ into its own `Link` by `self.metadata_link()`. See PEP 658 for more
+ information and the specification.
+ :param link_hash: a checksum for the content the link points to. If not
+ provided, this will be extracted from the link URL, if the URL has
+ any checksum.
:param cache_link_parsing: A flag that is used elsewhere to determine
whether resources retrieved from this link
should be cached. PyPI index urls should
@@ -94,11 +223,75 @@ def __init__(
self.comes_from = comes_from
self.requires_python = requires_python if requires_python else None
self.yanked_reason = yanked_reason
+ self.dist_info_metadata = dist_info_metadata
+ self.link_hash = link_hash or LinkHash.split_hash_name_and_value(self._url)
super().__init__(key=url, defining_class=Link)
self.cache_link_parsing = cache_link_parsing
+ @classmethod
+ def from_json(
+ cls,
+ file_data: Dict[str, Any],
+ page_url: str,
+ ) -> Optional["Link"]:
+ """
+ Convert an pypi json document from a simple repository page into a Link.
+ """
+ file_url = file_data.get("url")
+ if file_url is None:
+ return None
+
+ url = _ensure_quoted_url(urllib.parse.urljoin(page_url, file_url))
+ pyrequire = file_data.get("requires-python")
+ yanked_reason = file_data.get("yanked")
+ dist_info_metadata = file_data.get("dist-info-metadata")
+ hashes = file_data.get("hashes", {})
+
+ # The Link.yanked_reason expects an empty string instead of a boolean.
+ if yanked_reason and not isinstance(yanked_reason, str):
+ yanked_reason = ""
+ # The Link.yanked_reason expects None instead of False.
+ elif not yanked_reason:
+ yanked_reason = None
+
+ return cls(
+ url,
+ comes_from=page_url,
+ requires_python=pyrequire,
+ yanked_reason=yanked_reason,
+ hashes=hashes,
+ dist_info_metadata=dist_info_metadata,
+ )
+
+ @classmethod
+ def from_element(
+ cls,
+ anchor_attribs: Dict[str, Optional[str]],
+ page_url: str,
+ base_url: str,
+ ) -> Optional["Link"]:
+ """
+ Convert an anchor element's attributes in a simple repository page to a Link.
+ """
+ href = anchor_attribs.get("href")
+ if not href:
+ return None
+
+ url = _ensure_quoted_url(urllib.parse.urljoin(base_url, href))
+ pyrequire = anchor_attribs.get("data-requires-python")
+ yanked_reason = anchor_attribs.get("data-yanked")
+ dist_info_metadata = anchor_attribs.get("data-dist-info-metadata")
+
+ return cls(
+ url,
+ comes_from=page_url,
+ requires_python=pyrequire,
+ yanked_reason=yanked_reason,
+ dist_info_metadata=dist_info_metadata,
+ )
+
def __str__(self) -> str:
if self.requires_python:
rp = f" (requires-python:{self.requires_python})"
@@ -181,32 +374,36 @@ def subdirectory_fragment(self) -> Optional[str]:
return None
return match.group(1)
- _hash_re = re.compile(
- r"({choices})=([a-f0-9]+)".format(choices="|".join(_SUPPORTED_HASHES))
- )
+ def metadata_link(self) -> Optional["Link"]:
+ """Implementation of PEP 658 parsing."""
+ # Note that Link.from_element() parsing the "data-dist-info-metadata" attribute
+ # from an HTML anchor tag is typically how the Link.dist_info_metadata attribute
+ # gets set.
+ if self.dist_info_metadata is None:
+ return None
+ metadata_url = f"{self.url_without_fragment}.metadata"
+ link_hash: Optional[LinkHash] = None
+ # If data-dist-info-metadata="true" is set, then the metadata file exists,
+ # but there is no information about its checksum or anything else.
+ if self.dist_info_metadata != "true":
+ link_hash = LinkHash.split_hash_name_and_value(self.dist_info_metadata)
+ return Link(metadata_url, link_hash=link_hash)
+
+ def as_hashes(self) -> Optional[Hashes]:
+ if self.link_hash is not None:
+ return self.link_hash.as_hashes()
+ return None
@property
def hash(self) -> Optional[str]:
- for hashname in _SUPPORTED_HASHES:
- if hashname in self._hashes:
- return self._hashes[hashname]
-
- match = self._hash_re.search(self._url)
- if match:
- return match.group(2)
-
+ if self.link_hash is not None:
+ return self.link_hash.value
return None
@property
def hash_name(self) -> Optional[str]:
- for hashname in _SUPPORTED_HASHES:
- if hashname in self._hashes:
- return hashname
-
- match = self._hash_re.search(self._url)
- if match:
- return match.group(1)
-
+ if self.link_hash is not None:
+ return self.link_hash.name
return None
@property
@@ -236,19 +433,15 @@ def is_yanked(self) -> bool:
@property
def has_hash(self) -> bool:
- return self.hash_name is not None
+ return self.link_hash is not None
def is_hash_allowed(self, hashes: Optional[Hashes]) -> bool:
"""
- Return True if the link has a hash and it is allowed.
+ Return True if the link has a hash and it is allowed by `hashes`.
"""
- if hashes is None or not self.has_hash:
+ if self.link_hash is None:
return False
- # Assert non-None so mypy knows self.hash_name and self.hash are str.
- assert self.hash_name is not None
- assert self.hash is not None
-
- return hashes.is_hash_allowed(self.hash_name, hex_digest=self.hash)
+ return self.link_hash.is_hash_allowed(hashes)
class _CleanResult(NamedTuple):
diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py
index 80723fffe47..4bf414cb005 100644
--- a/src/pip/_internal/operations/prepare.py
+++ b/src/pip/_internal/operations/prepare.py
@@ -19,12 +19,13 @@
HashMismatch,
HashUnpinned,
InstallationError,
+ MetadataInconsistent,
NetworkConnectionError,
PreviousBuildDirError,
VcsHashUnsupported,
)
from pip._internal.index.package_finder import PackageFinder
-from pip._internal.metadata import BaseDistribution
+from pip._internal.metadata import BaseDistribution, get_metadata_distribution
from pip._internal.models.direct_url import ArchiveInfo
from pip._internal.models.link import Link
from pip._internal.models.wheel import Wheel
@@ -346,19 +347,72 @@ def _get_linked_req_hashes(self, req: InstallRequirement) -> Hashes:
# showing the user what the hash should be.
return req.hashes(trust_internet=False) or MissingHashes()
+ def _fetch_metadata_only(
+ self,
+ req: InstallRequirement,
+ ) -> Optional[BaseDistribution]:
+ if self.require_hashes:
+ logger.debug(
+ "Metadata-only fetching is not used as hash checking is required",
+ )
+ return None
+ # Try PEP 658 metadata first, then fall back to lazy wheel if unavailable.
+ return self._fetch_metadata_using_link_data_attr(
+ req
+ ) or self._fetch_metadata_using_lazy_wheel(req.link)
+
+ def _fetch_metadata_using_link_data_attr(
+ self,
+ req: InstallRequirement,
+ ) -> Optional[BaseDistribution]:
+ """Fetch metadata from the data-dist-info-metadata attribute, if possible."""
+ # (1) Get the link to the metadata file, if provided by the backend.
+ metadata_link = req.link.metadata_link()
+ if metadata_link is None:
+ return None
+ assert req.req is not None
+ logger.info(
+ "Obtaining dependency information for %s from %s",
+ req.req,
+ metadata_link,
+ )
+ # (2) Download the contents of the METADATA file, separate from the dist itself.
+ metadata_file = get_http_url(
+ metadata_link,
+ self._download,
+ hashes=metadata_link.as_hashes(),
+ )
+ with open(metadata_file.path, "rb") as f:
+ metadata_contents = f.read()
+ # (3) Generate a dist just from those file contents.
+ metadata_dist = get_metadata_distribution(
+ metadata_contents,
+ req.link.filename,
+ req.req.name,
+ )
+ # (4) Ensure the Name: field from the METADATA file matches the name from the
+ # install requirement.
+ #
+ # NB: raw_name will fall back to the name from the install requirement if
+ # the Name: field is not present, but it's noted in the raw_name docstring
+ # that that should NEVER happen anyway.
+ if metadata_dist.raw_name != req.req.name:
+ raise MetadataInconsistent(
+ req, "Name", req.req.name, metadata_dist.raw_name
+ )
+ return metadata_dist
+
def _fetch_metadata_using_lazy_wheel(
self,
link: Link,
) -> Optional[BaseDistribution]:
"""Fetch metadata using lazy wheel, if possible."""
+ # --use-feature=fast-deps must be provided.
if not self.use_lazy_wheel:
return None
- if self.require_hashes:
- logger.debug("Lazy wheel is not used as hash checking is required")
- return None
if link.is_file or not link.is_wheel:
logger.debug(
- "Lazy wheel is not used as %r does not points to a remote wheel",
+ "Lazy wheel is not used as %r does not point to a remote wheel",
link,
)
return None
@@ -414,13 +468,12 @@ def prepare_linked_requirement(
) -> BaseDistribution:
"""Prepare a requirement to be obtained from req.link."""
assert req.link
- link = req.link
self._log_preparing_link(req)
with indent_log():
# Check if the relevant file is already available
# in the download directory
file_path = None
- if self.download_dir is not None and link.is_wheel:
+ if self.download_dir is not None and req.link.is_wheel:
hashes = self._get_linked_req_hashes(req)
file_path = _check_download_dir(req.link, self.download_dir, hashes)
@@ -429,10 +482,10 @@ def prepare_linked_requirement(
self._downloaded[req.link.url] = file_path
else:
# The file is not available, attempt to fetch only metadata
- wheel_dist = self._fetch_metadata_using_lazy_wheel(link)
- if wheel_dist is not None:
+ metadata_dist = self._fetch_metadata_only(req)
+ if metadata_dist is not None:
req.needs_more_preparation = True
- return wheel_dist
+ return metadata_dist
# None of the optimizations worked, fully prepare the requirement
return self._prepare_linked_requirement(req, parallel_builds)
diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py
index 89318b74553..ede2213aa70 100644
--- a/tests/functional/test_download.py
+++ b/tests/functional/test_download.py
@@ -1,17 +1,24 @@
import os
+import re
import shutil
import textwrap
+import uuid
+from dataclasses import dataclass
+from enum import Enum
from hashlib import sha256
from pathlib import Path
-from typing import List
+from textwrap import dedent
+from typing import Callable, Dict, List, Tuple
import pytest
from pip._internal.cli.status_codes import ERROR
+from pip._internal.utils.urls import path_to_url
from tests.conftest import MockServer, ScriptFactory
from tests.lib import (
PipTestEnvironment,
TestData,
+ TestPipResult,
create_basic_sdist_for_package,
create_really_basic_wheel,
)
@@ -1230,3 +1237,349 @@ def test_download_use_pep517_propagation(
downloads = os.listdir(download_dir)
assert len(downloads) == 2
+
+
+class MetadataKind(Enum):
+ """All the types of values we might be provided for the data-dist-info-metadata
+ attribute from PEP 658."""
+
+ # Valid: will read metadata from the dist instead.
+ No = "none"
+ # Valid: will read the .metadata file, but won't check its hash.
+ Unhashed = "unhashed"
+ # Valid: will read the .metadata file and check its hash matches.
+ Sha256 = "sha256"
+ # Invalid: will error out after checking the hash.
+ WrongHash = "wrong-hash"
+ # Invalid: will error out after failing to fetch the .metadata file.
+ NoFile = "no-file"
+
+
+@dataclass(frozen=True)
+class Package:
+ """Mock package structure used to generate a PyPI repository.
+
+ Package name and version should correspond to sdists (.tar.gz files) in our test
+ data."""
+
+ name: str
+ version: str
+ filename: str
+ metadata: MetadataKind
+ # This will override any dependencies specified in the actual dist's METADATA.
+ requires_dist: Tuple[str, ...] = ()
+
+ def metadata_filename(self) -> str:
+ """This is specified by PEP 658."""
+ return f"{self.filename}.metadata"
+
+ def generate_additional_tag(self) -> str:
+ """This gets injected into the tag in the generated PyPI index page for this
+ package."""
+ if self.metadata == MetadataKind.No:
+ return ""
+ if self.metadata in [MetadataKind.Unhashed, MetadataKind.NoFile]:
+ return 'data-dist-info-metadata="true"'
+ if self.metadata == MetadataKind.WrongHash:
+ return 'data-dist-info-metadata="sha256=WRONG-HASH"'
+ assert self.metadata == MetadataKind.Sha256
+ checksum = sha256(self.generate_metadata()).hexdigest()
+ return f'data-dist-info-metadata="sha256={checksum}"'
+
+ def requires_str(self) -> str:
+ if not self.requires_dist:
+ return ""
+ joined = " and ".join(self.requires_dist)
+ return f"Requires-Dist: {joined}"
+
+ def generate_metadata(self) -> bytes:
+ """This is written to `self.metadata_filename()` and will override the actual
+ dist's METADATA, unless `self.metadata == MetadataKind.NoFile`."""
+ return dedent(
+ f"""\
+ Metadata-Version: 2.1
+ Name: {self.name}
+ Version: {self.version}
+ {self.requires_str()}
+ """
+ ).encode("utf-8")
+
+
+@pytest.fixture(scope="function")
+def write_index_html_content(tmpdir: Path) -> Callable[[str], Path]:
+ """Generate a PyPI package index.html within a temporary local directory."""
+ html_dir = tmpdir / "index_html_content"
+ html_dir.mkdir()
+
+ def generate_index_html_subdir(index_html: str) -> Path:
+ """Create a new subdirectory after a UUID and write an index.html."""
+ new_subdir = html_dir / uuid.uuid4().hex
+ new_subdir.mkdir()
+
+ with open(new_subdir / "index.html", "w") as f:
+ f.write(index_html)
+
+ return new_subdir
+
+ return generate_index_html_subdir
+
+
+@pytest.fixture(scope="function")
+def html_index_for_packages(
+ shared_data: TestData,
+ write_index_html_content: Callable[[str], Path],
+) -> Callable[..., Path]:
+ """Generate a PyPI HTML package index within a local directory pointing to
+ blank data."""
+
+ def generate_html_index_for_packages(packages: Dict[str, List[Package]]) -> Path:
+ """
+ Produce a PyPI directory structure pointing to the specified packages.
+ """
+ # (1) Generate the content for a PyPI index.html.
+ pkg_links = "\n".join(
+ f' {pkg}' for pkg in packages.keys()
+ )
+ index_html = f"""\
+
+
+
+
+ Simple index
+
+
+{pkg_links}
+
+"""
+ # (2) Generate the index.html in a new subdirectory of the temp directory.
+ index_html_subdir = write_index_html_content(index_html)
+
+ # (3) Generate subdirectories for individual packages, each with their own
+ # index.html.
+ for pkg, links in packages.items():
+ pkg_subdir = index_html_subdir / pkg
+ pkg_subdir.mkdir()
+
+ download_links: List[str] = []
+ for package_link in links:
+ # (3.1) Generate the tag which pip can crawl pointing to this
+ # specific package version.
+ download_links.append(
+ f' {package_link.filename}
' # noqa: E501
+ )
+ # (3.2) Copy over the corresponding file in `shared_data.packages`.
+ shutil.copy(
+ shared_data.packages / package_link.filename,
+ pkg_subdir / package_link.filename,
+ )
+ # (3.3) Write a metadata file, if applicable.
+ if package_link.metadata != MetadataKind.NoFile:
+ with open(pkg_subdir / package_link.metadata_filename(), "wb") as f:
+ f.write(package_link.generate_metadata())
+
+ # (3.4) After collating all the download links and copying over the files,
+ # write an index.html with the generated download links for each
+ # copied file for this specific package name.
+ download_links_str = "\n".join(download_links)
+ pkg_index_content = f"""\
+
+
+
+
+ Links for {pkg}
+
+
+ Links for {pkg}
+{download_links_str}
+
+"""
+ with open(pkg_subdir / "index.html", "w") as f:
+ f.write(pkg_index_content)
+
+ return index_html_subdir
+
+ return generate_html_index_for_packages
+
+
+@pytest.fixture(scope="function")
+def download_generated_html_index(
+ script: PipTestEnvironment,
+ html_index_for_packages: Callable[[Dict[str, List[Package]]], Path],
+ tmpdir: Path,
+) -> Callable[..., Tuple[TestPipResult, Path]]:
+ """Execute `pip download` against a generated PyPI index."""
+ download_dir = tmpdir / "download_dir"
+
+ def run_for_generated_index(
+ packages: Dict[str, List[Package]],
+ args: List[str],
+ allow_error: bool = False,
+ ) -> Tuple[TestPipResult, Path]:
+ """
+ Produce a PyPI directory structure pointing to the specified packages, then
+ execute `pip download -i ...` pointing to our generated index.
+ """
+ index_dir = html_index_for_packages(packages)
+ pip_args = [
+ "download",
+ "-d",
+ str(download_dir),
+ "-i",
+ path_to_url(str(index_dir)),
+ *args,
+ ]
+ result = script.pip(*pip_args, allow_error=allow_error)
+ return (result, download_dir)
+
+ return run_for_generated_index
+
+
+# The package database we generate for testing PEP 658 support.
+_simple_packages: Dict[str, List[Package]] = {
+ "simple": [
+ Package("simple", "1.0", "simple-1.0.tar.gz", MetadataKind.Sha256),
+ Package("simple", "2.0", "simple-2.0.tar.gz", MetadataKind.No),
+ # This will raise a hashing error.
+ Package("simple", "3.0", "simple-3.0.tar.gz", MetadataKind.WrongHash),
+ ],
+ "simple2": [
+ # Override the dependencies here in order to force pip to download
+ # simple-1.0.tar.gz as well.
+ Package(
+ "simple2",
+ "1.0",
+ "simple2-1.0.tar.gz",
+ MetadataKind.Unhashed,
+ ("simple==1.0",),
+ ),
+ # This will raise an error when pip attempts to fetch the metadata file.
+ Package("simple2", "2.0", "simple2-2.0.tar.gz", MetadataKind.NoFile),
+ ],
+ "colander": [
+ # Ensure we can read the dependencies from a metadata file within a wheel
+ # *without* PEP 658 metadata.
+ Package(
+ "colander", "0.9.9", "colander-0.9.9-py2.py3-none-any.whl", MetadataKind.No
+ ),
+ ],
+ "compilewheel": [
+ # Ensure we can override the dependencies of a wheel file by injecting PEP
+ # 658 metadata.
+ Package(
+ "compilewheel",
+ "1.0",
+ "compilewheel-1.0-py2.py3-none-any.whl",
+ MetadataKind.Unhashed,
+ ("simple==1.0",),
+ ),
+ ],
+ "has-script": [
+ # Ensure we check PEP 658 metadata hashing errors for wheel files.
+ Package(
+ "has-script",
+ "1.0",
+ "has.script-1.0-py2.py3-none-any.whl",
+ MetadataKind.WrongHash,
+ ),
+ ],
+ "translationstring": [
+ Package(
+ "translationstring", "1.1", "translationstring-1.1.tar.gz", MetadataKind.No
+ ),
+ ],
+ "priority": [
+ # Ensure we check for a missing metadata file for wheels.
+ Package(
+ "priority", "1.0", "priority-1.0-py2.py3-none-any.whl", MetadataKind.NoFile
+ ),
+ ],
+}
+
+
+@pytest.mark.parametrize(
+ "requirement_to_download, expected_outputs",
+ [
+ ("simple2==1.0", ["simple-1.0.tar.gz", "simple2-1.0.tar.gz"]),
+ ("simple==2.0", ["simple-2.0.tar.gz"]),
+ (
+ "colander",
+ ["colander-0.9.9-py2.py3-none-any.whl", "translationstring-1.1.tar.gz"],
+ ),
+ (
+ "compilewheel",
+ ["compilewheel-1.0-py2.py3-none-any.whl", "simple-1.0.tar.gz"],
+ ),
+ ],
+)
+def test_download_metadata(
+ download_generated_html_index: Callable[..., Tuple[TestPipResult, Path]],
+ requirement_to_download: str,
+ expected_outputs: List[str],
+) -> None:
+ """Verify that if a data-dist-info-metadata attribute is present, then it is used
+ instead of the actual dist's METADATA."""
+ _, download_dir = download_generated_html_index(
+ _simple_packages,
+ [requirement_to_download],
+ )
+ assert sorted(os.listdir(download_dir)) == expected_outputs
+
+
+@pytest.mark.parametrize(
+ "requirement_to_download, real_hash",
+ [
+ (
+ "simple==3.0",
+ "95e0f200b6302989bcf2cead9465cf229168295ea330ca30d1ffeab5c0fed996",
+ ),
+ (
+ "has-script",
+ "16ba92d7f6f992f6de5ecb7d58c914675cf21f57f8e674fb29dcb4f4c9507e5b",
+ ),
+ ],
+)
+def test_incorrect_metadata_hash(
+ download_generated_html_index: Callable[..., Tuple[TestPipResult, Path]],
+ requirement_to_download: str,
+ real_hash: str,
+) -> None:
+ """Verify that if a hash for data-dist-info-metadata is provided, it must match the
+ actual hash of the metadata file."""
+ result, _ = download_generated_html_index(
+ _simple_packages,
+ [requirement_to_download],
+ allow_error=True,
+ )
+ assert result.returncode != 0
+ expected_msg = f"""\
+ Expected sha256 WRONG-HASH
+ Got {real_hash}"""
+ assert expected_msg in result.stderr
+
+
+@pytest.mark.parametrize(
+ "requirement_to_download, expected_url",
+ [
+ ("simple2==2.0", "simple2-2.0.tar.gz.metadata"),
+ ("priority", "priority-1.0-py2.py3-none-any.whl.metadata"),
+ ],
+)
+def test_metadata_not_found(
+ download_generated_html_index: Callable[..., Tuple[TestPipResult, Path]],
+ requirement_to_download: str,
+ expected_url: str,
+) -> None:
+ """Verify that if a data-dist-info-metadata attribute is provided, that pip will
+ fetch the .metadata file at the location specified by PEP 658, and error
+ if unavailable."""
+ result, _ = download_generated_html_index(
+ _simple_packages,
+ [requirement_to_download],
+ allow_error=True,
+ )
+ assert result.returncode != 0
+ expected_re = re.escape(expected_url)
+ pattern = re.compile(
+ f"ERROR: 404 Client Error: FileNotFoundError for url:.*{expected_re}"
+ )
+ assert pattern.search(result.stderr), (pattern, result.stderr)
diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py
index efcae29289f..fc52ab9c8d8 100644
--- a/tests/functional/test_new_resolver.py
+++ b/tests/functional/test_new_resolver.py
@@ -1363,7 +1363,7 @@ def test_new_resolver_skip_inconsistent_metadata(script: PipTestEnvironment) ->
)
assert (
- " inconsistent version: filename has '3', but metadata has '2'"
+ " inconsistent version: expected '3', but metadata has '2'"
) in result.stdout, str(result)
script.assert_installed(a="1")
diff --git a/tests/lib/server.py b/tests/lib/server.py
index 4b5add345d3..4cc18452cb5 100644
--- a/tests/lib/server.py
+++ b/tests/lib/server.py
@@ -150,14 +150,6 @@ def html5_page(text: str) -> str:
)
-def index_page(spec: Dict[str, str]) -> "WSGIApplication":
- def link(name: str, value: str) -> str:
- return '{}'.format(value, name)
-
- links = "".join(link(*kv) for kv in spec.items())
- return text_html_response(html5_page(links))
-
-
def package_page(spec: Dict[str, str]) -> "WSGIApplication":
def link(name: str, value: str) -> str:
return '{}'.format(value, name)
diff --git a/tests/unit/metadata/test_metadata_pkg_resources.py b/tests/unit/metadata/test_metadata_pkg_resources.py
index 6bb67156c9f..ab1a56107f4 100644
--- a/tests/unit/metadata/test_metadata_pkg_resources.py
+++ b/tests/unit/metadata/test_metadata_pkg_resources.py
@@ -11,7 +11,7 @@
from pip._internal.metadata.pkg_resources import (
Distribution,
Environment,
- WheelMetadata,
+ InMemoryMetadata,
)
pkg_resources = pytest.importorskip("pip._vendor.pkg_resources")
@@ -99,7 +99,7 @@ def test_wheel_metadata_works() -> None:
dist = Distribution(
pkg_resources.DistInfoDistribution(
location="",
- metadata=WheelMetadata({"METADATA": metadata.as_bytes()}, ""),
+ metadata=InMemoryMetadata({"METADATA": metadata.as_bytes()}, ""),
project_name=name,
),
)
@@ -116,7 +116,7 @@ def test_wheel_metadata_works() -> None:
def test_wheel_metadata_throws_on_bad_unicode() -> None:
- metadata = WheelMetadata({"METADATA": b"\xff"}, "")
+ metadata = InMemoryMetadata({"METADATA": b"\xff"}, "")
with pytest.raises(UnsupportedWheel) as e:
metadata.get_metadata("METADATA")
diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py
index 3afc5210dc7..55676a4fc5c 100644
--- a/tests/unit/test_collector.py
+++ b/tests/unit/test_collector.py
@@ -11,13 +11,12 @@
import pytest
from pip._vendor import requests
+from pip._vendor.packaging.requirements import Requirement
from pip._internal.exceptions import NetworkConnectionError
from pip._internal.index.collector import (
IndexContent,
LinkCollector,
- _clean_link,
- _clean_url_path,
_get_index_content,
_get_simple_response,
_make_index_content,
@@ -28,7 +27,12 @@
from pip._internal.index.sources import _FlatDirectorySource, _IndexDirectorySource
from pip._internal.models.candidate import InstallationCandidate
from pip._internal.models.index import PyPI
-from pip._internal.models.link import Link
+from pip._internal.models.link import (
+ Link,
+ LinkHash,
+ _clean_url_path,
+ _ensure_quoted_url,
+)
from pip._internal.network.session import PipSession
from tests.lib import TestData, make_test_link_collector
@@ -402,13 +406,13 @@ def test_clean_url_path_with_local_path(path: str, expected: str) -> None:
),
],
)
-def test_clean_link(url: str, clean_url: str) -> None:
- assert _clean_link(url) == clean_url
+def test_ensure_quoted_url(url: str, clean_url: str) -> None:
+ assert _ensure_quoted_url(url) == clean_url
def _test_parse_links_data_attribute(
anchor_html: str, attr: str, expected: Optional[str]
-) -> None:
+) -> Link:
html = (
""
''
@@ -427,6 +431,7 @@ def _test_parse_links_data_attribute(
(link,) = links
actual = getattr(link, attr)
assert actual == expected
+ return link
@pytest.mark.parametrize(
@@ -454,6 +459,12 @@ def test_parse_links__requires_python(
_test_parse_links_data_attribute(anchor_html, "requires_python", expected)
+# TODO: this test generates its own examples to validate the json client implementation
+# instead of sharing those examples with the html client testing. We expect this won't
+# hide any bugs because operations like resolving PEP 658 metadata should use the same
+# code for both types of indices, but it might be nice to explicitly have all our tests
+# in test_download.py execute over both html and json indices with
+# a pytest.mark.parameterize decorator to ensure nothing slips through the cracks.
def test_parse_links_json() -> None:
json_bytes = json.dumps(
{
@@ -474,6 +485,14 @@ def test_parse_links_json() -> None:
"requires-python": ">=3.7",
"dist-info-metadata": False,
},
+ # Same as above, but parsing dist-info-metadata.
+ {
+ "filename": "holygrail-1.0-py3-none-any.whl",
+ "url": "/files/holygrail-1.0-py3-none-any.whl",
+ "hashes": {"sha256": "sha256 hash", "blake2b": "blake2b hash"},
+ "requires-python": ">=3.7",
+ "dist-info-metadata": "sha512=aabdd41",
+ },
],
}
).encode("utf8")
@@ -502,8 +521,25 @@ def test_parse_links_json() -> None:
yanked_reason=None,
hashes={"sha256": "sha256 hash", "blake2b": "blake2b hash"},
),
+ Link(
+ "https://example.com/files/holygrail-1.0-py3-none-any.whl",
+ comes_from=page.url,
+ requires_python=">=3.7",
+ yanked_reason=None,
+ hashes={"sha256": "sha256 hash", "blake2b": "blake2b hash"},
+ dist_info_metadata="sha512=aabdd41",
+ ),
]
+ # Ensure the metadata info can be parsed into the correct link.
+ metadata_link = links[2].metadata_link()
+ assert metadata_link is not None
+ assert (
+ metadata_link.url
+ == "https://example.com/files/holygrail-1.0-py3-none-any.whl.metadata"
+ )
+ assert metadata_link.link_hash == LinkHash("sha512", "aabdd41")
+
@pytest.mark.parametrize(
"anchor_html, expected",
@@ -534,6 +570,48 @@ def test_parse_links__yanked_reason(anchor_html: str, expected: Optional[str]) -
_test_parse_links_data_attribute(anchor_html, "yanked_reason", expected)
+# Requirement objects do not == each other unless they point to the same instance!
+_pkg1_requirement = Requirement("pkg1==1.0")
+
+
+@pytest.mark.parametrize(
+ "anchor_html, expected, link_hash",
+ [
+ # Test not present.
+ (
+ '',
+ None,
+ None,
+ ),
+ # Test with value "true".
+ (
+ '',
+ "true",
+ None,
+ ),
+ # Test with a provided hash value.
+ (
+ '', # noqa: E501
+ "sha256=aa113592bbe",
+ None,
+ ),
+ # Test with a provided hash value for both the requirement as well as metadata.
+ (
+ '', # noqa: E501
+ "sha256=aa113592bbe",
+ LinkHash("sha512", "abc132409cb"),
+ ),
+ ],
+)
+def test_parse_links__dist_info_metadata(
+ anchor_html: str,
+ expected: Optional[str],
+ link_hash: Optional[LinkHash],
+) -> None:
+ link = _test_parse_links_data_attribute(anchor_html, "dist_info_metadata", expected)
+ assert link.link_hash == link_hash
+
+
def test_parse_links_caches_same_page_by_url() -> None:
html = (
""
@@ -963,3 +1041,23 @@ def expand_path(path: str) -> str:
expected_temp2_dir = os.path.normcase(temp2_dir)
assert search_scope.find_links == ["~/temp1", expected_temp2_dir]
assert search_scope.index_urls == ["default_url"]
+
+
+@pytest.mark.parametrize(
+ "url, result",
+ [
+ (
+ "https://pypi.org/pip-18.0.tar.gz#sha256=aa113592bbe",
+ LinkHash("sha256", "aa113592bbe"),
+ ),
+ (
+ "https://pypi.org/pip-18.0.tar.gz#md5=aa113592bbe",
+ LinkHash("md5", "aa113592bbe"),
+ ),
+ ("https://pypi.org/pip-18.0.tar.gz", None),
+ # We don't recognize the "sha500" algorithm, so we discard it.
+ ("https://pypi.org/pip-18.0.tar.gz#sha500=aa113592bbe", None),
+ ],
+)
+def test_link_hash_parsing(url: str, result: Optional[LinkHash]) -> None:
+ assert LinkHash.split_hash_name_and_value(url) == result
From 08eb492641fecfaf0cdfbed5581bd405166a3816 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Sun, 7 Aug 2022 17:39:33 +0200
Subject: [PATCH 087/730] Deprecate --no-binary disabling the wheel cache
---
news/11454.removal.rst | 1 +
src/pip/_internal/cache.py | 6 +++++-
src/pip/_internal/cli/cmdoptions.py | 7 ++++++-
src/pip/_internal/commands/install.py | 26 +++++++++++++++++++++++---
src/pip/_internal/commands/wheel.py | 20 ++++++++++++++++++++
5 files changed, 55 insertions(+), 5 deletions(-)
create mode 100644 news/11454.removal.rst
diff --git a/news/11454.removal.rst b/news/11454.removal.rst
new file mode 100644
index 00000000000..14c4dc73ac7
--- /dev/null
+++ b/news/11454.removal.rst
@@ -0,0 +1 @@
+Deprecate ```--no-binary`` disabling the wheel cache.
diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py
index e51edd5157e..c53b7f023a1 100644
--- a/src/pip/_internal/cache.py
+++ b/src/pip/_internal/cache.py
@@ -221,7 +221,11 @@ class WheelCache(Cache):
when a certain link is not found in the simple wheel cache first.
"""
- def __init__(self, cache_dir: str, format_control: FormatControl) -> None:
+ def __init__(
+ self, cache_dir: str, format_control: Optional[FormatControl] = None
+ ) -> None:
+ if format_control is None:
+ format_control = FormatControl()
super().__init__(cache_dir, format_control, {"binary"})
self._wheel_cache = SimpleWheelCache(cache_dir, format_control)
self._ephem_cache = EphemWheelCache(format_control)
diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py
index 84e0e783869..f0950332115 100644
--- a/src/pip/_internal/cli/cmdoptions.py
+++ b/src/pip/_internal/cli/cmdoptions.py
@@ -1007,7 +1007,12 @@ def check_list_path_option(options: Values) -> None:
metavar="feature",
action="append",
default=[],
- choices=["2020-resolver", "fast-deps", "truststore"],
+ choices=[
+ "2020-resolver",
+ "fast-deps",
+ "truststore",
+ "no-binary-enable-wheel-cache",
+ ],
help="Enable new functionality, that may be backward incompatible.",
)
diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py
index dcf5ce8c617..b37303caade 100644
--- a/src/pip/_internal/commands/install.py
+++ b/src/pip/_internal/commands/install.py
@@ -29,7 +29,10 @@
from pip._internal.req import install_given_reqs
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.compat import WINDOWS
-from pip._internal.utils.deprecation import LegacyInstallReasonFailedBdistWheel
+from pip._internal.utils.deprecation import (
+ LegacyInstallReasonFailedBdistWheel,
+ deprecated,
+)
from pip._internal.utils.distutils_args import parse_distutils_args
from pip._internal.utils.filesystem import test_writable_dir
from pip._internal.utils.logging import getLogger
@@ -326,8 +329,6 @@ def run(self, options: Values, args: List[str]) -> int:
target_python=target_python,
ignore_requires_python=options.ignore_requires_python,
)
- wheel_cache = WheelCache(options.cache_dir, options.format_control)
-
build_tracker = self.enter_context(get_build_tracker())
directory = TempDirectory(
@@ -339,6 +340,25 @@ def run(self, options: Values, args: List[str]) -> int:
try:
reqs = self.get_requirements(args, options, finder, session)
+ if "no-binary-enable-wheel-cache" in options.features_enabled:
+ # TODO: remove format_control from WheelCache when the deprecation cycle
+ # is over
+ wheel_cache = WheelCache(options.cache_dir)
+ else:
+ if options.format_control.no_binary:
+ deprecated(
+ reason=(
+ "--no-binary currently disables reading from "
+ "the cache of locally built wheels. In the future "
+ "--no-binary will not influence the wheel cache."
+ ),
+ replacement="to use the --no-cache-dir option",
+ feature_flag="no-binary-enable-wheel-cache",
+ issue=11453,
+ gone_in="23.1",
+ )
+ wheel_cache = WheelCache(options.cache_dir, options.format_control)
+
# Only when installing is it permitted to use PEP 660.
# In other circumstances (pip wheel, pip download) we generate
# regular (i.e. non editable) metadata and wheels.
diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py
index 9dd6c82f210..5ddb3bd6ceb 100644
--- a/src/pip/_internal/commands/wheel.py
+++ b/src/pip/_internal/commands/wheel.py
@@ -11,6 +11,7 @@
from pip._internal.exceptions import CommandError
from pip._internal.operations.build.build_tracker import get_build_tracker
from pip._internal.req.req_install import InstallRequirement
+from pip._internal.utils.deprecation import deprecated
from pip._internal.utils.misc import ensure_dir, normalize_path
from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.wheel_builder import build, should_build_for_wheel_command
@@ -120,6 +121,25 @@ def run(self, options: Values, args: List[str]) -> int:
reqs = self.get_requirements(args, options, finder, session)
+ if "no-binary-enable-wheel-cache" in options.features_enabled:
+ # TODO: remove format_control from WheelCache when the deprecation cycle
+ # is over
+ wheel_cache = WheelCache(options.cache_dir)
+ else:
+ if options.format_control.no_binary:
+ deprecated(
+ reason=(
+ "--no-binary currently disables reading from "
+ "the cache of locally built wheels. In the future "
+ "--no-binary will not influence the wheel cache."
+ ),
+ replacement="to use the --no-cache-dir option",
+ feature_flag="no-binary-enable-wheel-cache",
+ issue=11453,
+ gone_in="23.1",
+ )
+ wheel_cache = WheelCache(options.cache_dir, options.format_control)
+
preparer = self.make_requirement_preparer(
temp_build_dir=directory,
options=options,
From f39d38668add998395414064449f7fd0a68bd650 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Sun, 7 Aug 2022 16:21:20 +0200
Subject: [PATCH 088/730] Deprecate --no-binary implying setup.py install
---
news/11452.removal.rst | 2 ++
src/pip/_internal/utils/deprecation.py | 17 +++++++++++++++--
src/pip/_internal/wheel_builder.py | 10 +++++-----
3 files changed, 22 insertions(+), 7 deletions(-)
create mode 100644 news/11452.removal.rst
diff --git a/news/11452.removal.rst b/news/11452.removal.rst
new file mode 100644
index 00000000000..ac29324abc8
--- /dev/null
+++ b/news/11452.removal.rst
@@ -0,0 +1,2 @@
+Deprecate installation with 'setup.py install' when no-binary is enabled for
+source distributions without 'pyproject.toml'.
diff --git a/src/pip/_internal/utils/deprecation.py b/src/pip/_internal/utils/deprecation.py
index 7c7ace6ff4c..a7acf07bb3a 100644
--- a/src/pip/_internal/utils/deprecation.py
+++ b/src/pip/_internal/utils/deprecation.py
@@ -124,8 +124,8 @@ class LegacyInstallReason:
def __init__(
self,
reason: str,
- replacement: Optional[str],
- gone_in: Optional[str],
+ replacement: Optional[str] = None,
+ gone_in: Optional[str] = None,
feature_flag: Optional[str] = None,
issue: Optional[int] = None,
emit_after_success: bool = False,
@@ -173,3 +173,16 @@ def emit_deprecation(self, name: str) -> None:
issue=8559,
emit_before_install=True,
)
+
+LegacyInstallReasonNoBinaryForcesSetuptoolsInstall = LegacyInstallReason(
+ reason=(
+ "{name} is being installed using the legacy "
+ "'setup.py install' method, because the '--no-binary' option was enabled "
+ "for it and this currently disables local wheel building for projects that "
+ "don't have a 'pyproject.toml' file."
+ ),
+ replacement="to enable the '--use-pep517' option",
+ gone_in="23.1",
+ issue=11451,
+ emit_before_install=True,
+)
diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py
index 60db28e92c3..6f359421d80 100644
--- a/src/pip/_internal/wheel_builder.py
+++ b/src/pip/_internal/wheel_builder.py
@@ -19,7 +19,10 @@
from pip._internal.operations.build.wheel_editable import build_wheel_editable
from pip._internal.operations.build.wheel_legacy import build_wheel_legacy
from pip._internal.req.req_install import InstallRequirement
-from pip._internal.utils.deprecation import LegacyInstallReasonMissingWheelPackage
+from pip._internal.utils.deprecation import (
+ LegacyInstallReasonMissingWheelPackage,
+ LegacyInstallReasonNoBinaryForcesSetuptoolsInstall,
+)
from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import ensure_dir, hash_file, is_wheel_installed
from pip._internal.utils.setuptools_build import make_setuptools_clean_args
@@ -80,10 +83,7 @@ def _should_build(
assert check_bdist_wheel is not None
if not check_bdist_wheel(req):
- logger.info(
- "Skipping wheel build for %s, due to binaries being disabled for it.",
- req.name,
- )
+ req.legacy_install_reason = LegacyInstallReasonNoBinaryForcesSetuptoolsInstall
return False
if not is_wheel_installed():
From 24c8ebc85e0ab8097a879ad274e2b21e42349788 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Sat, 17 Sep 2022 15:43:40 +0200
Subject: [PATCH 089/730] Set deprecation deadlines
---
src/pip/_internal/utils/deprecation.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/pip/_internal/utils/deprecation.py b/src/pip/_internal/utils/deprecation.py
index 7c7ace6ff4c..51de0a5bde7 100644
--- a/src/pip/_internal/utils/deprecation.py
+++ b/src/pip/_internal/utils/deprecation.py
@@ -155,7 +155,7 @@ def emit_deprecation(self, name: str) -> None:
"method, because a wheel could not be built for it."
),
replacement="to fix the wheel build issue reported above",
- gone_in=None,
+ gone_in="23.1",
issue=8368,
emit_after_success=True,
)
@@ -169,7 +169,7 @@ def emit_deprecation(self, name: str) -> None:
"is not installed."
),
replacement="to enable the '--use-pep517' option",
- gone_in=None,
+ gone_in="23.1",
issue=8559,
emit_before_install=True,
)
From 857df9059d24ec43f088da88b957665bd3b05d32 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=