PNG  IHDRX cHRMz&u0`:pQ<bKGD pHYsodtIME MeqIDATxw]Wug^Qd˶ 6`!N:!@xI~)%7%@Bh&`lnjVF29gΨ4E$|>cɚ{gk= %,a KX%,a KX%,a KX%,a KX%,a KX%,a KX%, b` ǟzeאfp]<!SJmɤY޲ڿ,%c ~ع9VH.!Ͳz&QynֺTkRR.BLHi٪:l;@(!MԴ=žI,:o&N'Kù\vRmJ雵֫AWic H@" !: Cé||]k-Ha oݜ:y F())u]aG7*JV@J415p=sZH!=!DRʯvɱh~V\}v/GKY$n]"X"}t@ xS76^[bw4dsce)2dU0 CkMa-U5tvLƀ~mlMwfGE/-]7XAƟ`׮g ewxwC4\[~7@O-Q( a*XGƒ{ ՟}$_y3tĐƤatgvێi|K=uVyrŲlLӪuܿzwk$m87k( `múcE)"@rK( z4$D; 2kW=Xb$V[Ru819קR~qloѱDyįݎ*mxw]y5e4K@ЃI0A D@"BDk_)N\8͜9dz"fK0zɿvM /.:2O{ Nb=M=7>??Zuo32 DLD@D| &+֎C #B8ַ`bOb $D#ͮҪtx]%`ES`Ru[=¾!@Od37LJ0!OIR4m]GZRJu$‡c=%~s@6SKy?CeIh:[vR@Lh | (BhAMy=݃  G"'wzn޺~8ԽSh ~T*A:xR[ܹ?X[uKL_=fDȊ؂p0}7=D$Ekq!/t.*2ʼnDbŞ}DijYaȲ(""6HA;:LzxQ‘(SQQ}*PL*fc\s `/d'QXW, e`#kPGZuŞuO{{wm[&NBTiiI0bukcA9<4@SӊH*؎4U/'2U5.(9JuDfrޱtycU%j(:RUbArLֺN)udA':uGQN"-"Is.*+k@ `Ojs@yU/ H:l;@yyTn}_yw!VkRJ4P)~y#)r,D =ě"Q]ci'%HI4ZL0"MJy 8A{ aN<8D"1#IJi >XjX֔#@>-{vN!8tRݻ^)N_╗FJEk]CT՟ YP:_|H1@ CBk]yKYp|og?*dGvzنzӴzjֺNkC~AbZƷ`.H)=!QͷVTT(| u78y֮}|[8-Vjp%2JPk[}ԉaH8Wpqhwr:vWª<}l77_~{s۴V+RCģ%WRZ\AqHifɤL36: #F:p]Bq/z{0CU6ݳEv_^k7'>sq*+kH%a`0ԣisqにtү04gVgW΂iJiS'3w.w}l6MC2uԯ|>JF5`fV5m`Y**Db1FKNttu]4ccsQNnex/87+}xaUW9y>ͯ骵G{䩓Գ3+vU}~jJ.NFRD7<aJDB1#ҳgSb,+CS?/ VG J?|?,2#M9}B)MiE+G`-wo߫V`fio(}S^4e~V4bHOYb"b#E)dda:'?}׮4繏`{7Z"uny-?ǹ;0MKx{:_pÚmFמ:F " .LFQLG)Q8qN q¯¯3wOvxDb\. BKD9_NN &L:4D{mm o^tֽ:q!ƥ}K+<"m78N< ywsard5+вz~mnG)=}lYݧNj'QJS{S :UYS-952?&O-:W}(!6Mk4+>A>j+i|<<|;ر^߉=HE|V#F)Emm#}/"y GII웻Jі94+v뾧xu~5C95~ūH>c@덉pʃ1/4-A2G%7>m;–Y,cyyaln" ?ƻ!ʪ<{~h~i y.zZB̃/,雋SiC/JFMmBH&&FAbϓO^tubbb_hZ{_QZ-sύodFgO(6]TJA˯#`۶ɟ( %$&+V'~hiYy>922 Wp74Zkq+Ovn錄c>8~GqܲcWꂎz@"1A.}T)uiW4="jJ2W7mU/N0gcqܗOO}?9/wìXžΏ0 >֩(V^Rh32!Hj5`;O28؇2#ݕf3 ?sJd8NJ@7O0 b־?lldщ̡&|9C.8RTWwxWy46ah嘦mh٤&l zCy!PY?: CJyв]dm4ǜҐR޻RլhX{FƯanшQI@x' ao(kUUuxW_Ñ줮[w8 FRJ(8˼)_mQ _!RJhm=!cVmm ?sFOnll6Qk}alY}; "baӌ~M0w,Ggw2W:G/k2%R,_=u`WU R.9T"v,<\Ik޽/2110Ӿxc0gyC&Ny޽JҢrV6N ``یeA16"J³+Rj*;BϜkZPJaÍ<Jyw:NP8/D$ 011z֊Ⱳ3ι֘k1V_"h!JPIΣ'ɜ* aEAd:ݺ>y<}Lp&PlRfTb1]o .2EW\ͮ]38؋rTJsǏP@芎sF\> P^+dYJLbJ C-xϐn> ι$nj,;Ǖa FU *择|h ~izť3ᤓ`K'-f tL7JK+vf2)V'-sFuB4i+m+@My=O҈0"|Yxoj,3]:cо3 $#uŘ%Y"y죯LebqtҢVzq¼X)~>4L׶m~[1_k?kxֺQ`\ |ٛY4Ѯr!)N9{56(iNq}O()Em]=F&u?$HypWUeB\k]JɩSع9 Zqg4ZĊo oMcjZBU]B\TUd34ݝ~:7ڶSUsB0Z3srx 7`:5xcx !qZA!;%͚7&P H<WL!džOb5kF)xor^aujƍ7 Ǡ8/p^(L>ὴ-B,{ۇWzֺ^k]3\EE@7>lYBȝR.oHnXO/}sB|.i@ɥDB4tcm,@ӣgdtJ!lH$_vN166L__'Z)y&kH;:,Y7=J 9cG) V\hjiE;gya~%ks_nC~Er er)muuMg2;֫R)Md) ,¶ 2-wr#F7<-BBn~_(o=KO㭇[Xv eN_SMgSҐ BS헃D%g_N:/pe -wkG*9yYSZS.9cREL !k}<4_Xs#FmҶ:7R$i,fi!~' # !6/S6y@kZkZcX)%5V4P]VGYq%H1!;e1MV<!ϐHO021Dp= HMs~~a)ަu7G^];git!Frl]H/L$=AeUvZE4P\.,xi {-~p?2b#amXAHq)MWǾI_r`S Hz&|{ +ʖ_= (YS(_g0a03M`I&'9vl?MM+m~}*xT۲(fY*V4x@29s{DaY"toGNTO+xCAO~4Ϳ;p`Ѫ:>Ҵ7K 3}+0 387x\)a"/E>qpWB=1 ¨"MP(\xp߫́A3+J] n[ʼnӼaTbZUWb={~2ooKױӰp(CS\S筐R*JغV&&"FA}J>G֐p1ٸbk7 ŘH$JoN <8s^yk_[;gy-;߉DV{c B yce% aJhDȶ 2IdйIB/^n0tNtџdcKj4϶v~- CBcgqx9= PJ) dMsjpYB] GD4RDWX +h{y`,3ꊕ$`zj*N^TP4L:Iz9~6s) Ga:?y*J~?OrMwP\](21sZUD ?ܟQ5Q%ggW6QdO+\@ ̪X'GxN @'4=ˋ+*VwN ne_|(/BDfj5(Dq<*tNt1х!MV.C0 32b#?n0pzj#!38}޴o1KovCJ`8ŗ_"]] rDUy޲@ Ȗ-;xџ'^Y`zEd?0„ DAL18IS]VGq\4o !swV7ˣι%4FѮ~}6)OgS[~Q vcYbL!wG3 7띸*E Pql8=jT\꘿I(z<[6OrR8ºC~ډ]=rNl[g|v TMTղb-o}OrP^Q]<98S¤!k)G(Vkwyqyr޽Nv`N/e p/~NAOk \I:G6]4+K;j$R:Mi #*[AȚT,ʰ,;N{HZTGMoּy) ]%dHء9Պ䠬|<45,\=[bƟ8QXeB3- &dҩ^{>/86bXmZ]]yޚN[(WAHL$YAgDKp=5GHjU&99v簪C0vygln*P)9^͞}lMuiH!̍#DoRBn9l@ xA/_v=ȺT{7Yt2N"4!YN`ae >Q<XMydEB`VU}u]嫇.%e^ánE87Mu\t`cP=AD/G)sI"@MP;)]%fH9'FNsj1pVhY&9=0pfuJ&gޤx+k:!r˭wkl03׼Ku C &ѓYt{.O.zҏ z}/tf_wEp2gvX)GN#I ݭ߽v/ .& и(ZF{e"=V!{zW`, ]+LGz"(UJp|j( #V4, 8B 0 9OkRrlɱl94)'VH9=9W|>PS['G(*I1==C<5"Pg+x'K5EMd؞Af8lG ?D FtoB[je?{k3zQ vZ;%Ɠ,]E>KZ+T/ EJxOZ1i #T<@ I}q9/t'zi(EMqw`mYkU6;[t4DPeckeM;H}_g pMww}k6#H㶏+b8雡Sxp)&C $@'b,fPߑt$RbJ'vznuS ~8='72_`{q纶|Q)Xk}cPz9p7O:'|G~8wx(a 0QCko|0ASD>Ip=4Q, d|F8RcU"/KM opKle M3#i0c%<7׿p&pZq[TR"BpqauIp$ 8~Ĩ!8Սx\ւdT>>Z40ks7 z2IQ}ItԀ<-%S⍤};zIb$I 5K}Q͙D8UguWE$Jh )cu4N tZl+[]M4k8֦Zeq֮M7uIqG 1==tLtR,ƜSrHYt&QP윯Lg' I,3@P'}'R˪e/%-Auv·ñ\> vDJzlӾNv5:|K/Jb6KI9)Zh*ZAi`?S {aiVDԲuy5W7pWeQJk֤#5&V<̺@/GH?^τZL|IJNvI:'P=Ϛt"¨=cud S Q.Ki0 !cJy;LJR;G{BJy޺[^8fK6)=yʊ+(k|&xQ2`L?Ȓ2@Mf 0C`6-%pKpm')c$׻K5[J*U[/#hH!6acB JA _|uMvDyk y)6OPYjœ50VT K}cǻP[ $:]4MEA.y)|B)cf-A?(e|lɉ#P9V)[9t.EiQPDѠ3ϴ;E:+Օ t ȥ~|_N2,ZJLt4! %ա]u {+=p.GhNcŞQI?Nd'yeh n7zi1DB)1S | S#ًZs2|Ɛy$F SxeX{7Vl.Src3E℃Q>b6G ўYCmtկ~=K0f(=LrAS GN'ɹ9<\!a`)֕y[uՍ[09` 9 +57ts6}b4{oqd+J5fa/,97J#6yν99mRWxJyѡyu_TJc`~W>l^q#Ts#2"nD1%fS)FU w{ܯ R{ ˎ󅃏џDsZSQS;LV;7 Od1&1n$ N /.q3~eNɪ]E#oM~}v֯FڦwyZ=<<>Xo稯lfMFV6p02|*=tV!c~]fa5Y^Q_WN|Vs 0ҘދU97OI'N2'8N֭fgg-}V%y]U4 峧p*91#9U kCac_AFңĪy뚇Y_AiuYyTTYЗ-(!JFLt›17uTozc. S;7A&&<ԋ5y;Ro+:' *eYJkWR[@F %SHWP 72k4 qLd'J "zB6{AC0ƁA6U.'F3:Ȅ(9ΜL;D]m8ڥ9}dU "v!;*13Rg^fJyShyy5auA?ɩGHRjo^]׽S)Fm\toy 4WQS@mE#%5ʈfFYDX ~D5Ϡ9tE9So_aU4?Ѽm%&c{n>.KW1Tlb}:j uGi(JgcYj0qn+>) %\!4{LaJso d||u//P_y7iRJ߬nHOy) l+@$($VFIQ9%EeKʈU. ia&FY̒mZ=)+qqoQn >L!qCiDB;Y<%} OgBxB!ØuG)WG9y(Ą{_yesuZmZZey'Wg#C~1Cev@0D $a@˲(.._GimA:uyw֬%;@!JkQVM_Ow:P.s\)ot- ˹"`B,e CRtaEUP<0'}r3[>?G8xU~Nqu;Wm8\RIkբ^5@k+5(By'L&'gBJ3ݶ!/㮻w҅ yqPWUg<e"Qy*167΃sJ\oz]T*UQ<\FԎ`HaNmڜ6DysCask8wP8y9``GJ9lF\G g's Nn͵MLN֪u$| /|7=]O)6s !ĴAKh]q_ap $HH'\1jB^s\|- W1:=6lJBqjY^LsPk""`]w)󭃈,(HC ?䔨Y$Sʣ{4Z+0NvQkhol6C.婧/u]FwiVjZka&%6\F*Ny#8O,22+|Db~d ~Çwc N:FuuCe&oZ(l;@ee-+Wn`44AMK➝2BRՈt7g*1gph9N) *"TF*R(#'88pm=}X]u[i7bEc|\~EMn}P瘊J)K.0i1M6=7'_\kaZ(Th{K*GJyytw"IO-PWJk)..axӝ47"89Cc7ĐBiZx 7m!fy|ϿF9CbȩV 9V-՛^pV̌ɄS#Bv4-@]Vxt-Z, &ֺ*diؠ2^VXbs֔Ìl.jQ]Y[47gj=幽ex)A0ip׳ W2[ᎇhuE^~q흙L} #-b۸oFJ_QP3r6jr+"nfzRJTUqoaۍ /$d8Mx'ݓ= OՃ| )$2mcM*cЙj}f };n YG w0Ia!1Q.oYfr]DyISaP}"dIӗթO67jqR ҊƐƈaɤGG|h;t]䗖oSv|iZqX)oalv;۩meEJ\!8=$4QU4Xo&VEĊ YS^E#d,yX_> ۘ-e\ "Wa6uLĜZi`aD9.% w~mB(02G[6y.773a7 /=o7D)$Z 66 $bY^\CuP. (x'"J60׿Y:Oi;F{w佩b+\Yi`TDWa~|VH)8q/=9!g߆2Y)?ND)%?Ǐ`k/sn:;O299yB=a[Ng 3˲N}vLNy;*?x?~L&=xyӴ~}q{qE*IQ^^ͧvü{Huu=R|>JyUlZV, B~/YF!Y\u_ݼF{_C)LD]m {H 0ihhadd nUkf3oٺCvE\)QJi+֥@tDJkB$1!Đr0XQ|q?d2) Ӣ_}qv-< FŊ߫%roppVBwü~JidY4:}L6M7f٬F "?71<2#?Jyy4뷢<_a7_=Q E=S1И/9{+93֮E{ǂw{))?maÆm(uLE#lïZ  ~d];+]h j?!|$F}*"4(v'8s<ŏUkm7^7no1w2ؗ}TrͿEk>p'8OB7d7R(A 9.*Mi^ͳ; eeUwS+C)uO@ =Sy]` }l8^ZzRXj[^iUɺ$tj))<sbDJfg=Pk_{xaKo1:-uyG0M ԃ\0Lvuy'ȱc2Ji AdyVgVh!{]/&}}ċJ#%d !+87<;qN޼Nفl|1N:8ya  8}k¾+-$4FiZYÔXk*I&'@iI99)HSh4+2G:tGhS^繿 Kتm0 вDk}֚+QT4;sC}rՅE,8CX-e~>G&'9xpW,%Fh,Ry56Y–hW-(v_,? ; qrBk4-V7HQ;ˇ^Gv1JVV%,ik;D_W!))+BoS4QsTM;gt+ndS-~:11Sgv!0qRVh!"Ȋ(̦Yl.]PQWgٳE'`%W1{ndΗBk|Ž7ʒR~,lnoa&:ü$ 3<a[CBݮwt"o\ePJ=Hz"_c^Z.#ˆ*x z̝grY]tdkP*:97YľXyBkD4N.C_[;F9`8& !AMO c `@BA& Ost\-\NX+Xp < !bj3C&QL+*&kAQ=04}cC!9~820G'PC9xa!w&bo_1 Sw"ܱ V )Yl3+ס2KoXOx]"`^WOy :3GO0g;%Yv㐫(R/r (s } u B &FeYZh0y> =2<Ϟc/ -u= c&׭,.0"g"7 6T!vl#sc>{u/Oh Bᾈ)۴74]x7 gMӒ"d]U)}" v4co[ ɡs 5Gg=XR14?5A}D "b{0$L .\4y{_fe:kVS\\O]c^W52LSBDM! C3Dhr̦RtArx4&agaN3Cf<Ԉp4~ B'"1@.b_/xQ} _߃҉/gٓ2Qkqp0շpZ2fԫYz< 4L.Cyυι1t@鎫Fe sYfsF}^ V}N<_`p)alٶ "(XEAVZ<)2},:Ir*#m_YӼ R%a||EƼIJ,,+f"96r/}0jE/)s)cjW#w'Sʯ5<66lj$a~3Kʛy 2:cZ:Yh))+a߭K::N,Q F'qB]={.]h85C9cr=}*rk?vwV렵ٸW Rs%}rNAkDv|uFLBkWY YkX מ|)1!$#3%y?pF<@<Rr0}: }\J [5FRxY<9"SQdE(Q*Qʻ)q1E0B_O24[U'],lOb ]~WjHޏTQ5Syu wq)xnw8~)c 쫬gٲߠ H% k5dƝk> kEj,0% b"vi2Wس_CuK)K{n|>t{P1򨾜j>'kEkƗBg*H%'_aY6Bn!TL&ɌOb{c`'d^{t\i^[uɐ[}q0lM˕G:‚4kb祔c^:?bpg… +37stH:0}en6x˟%/<]BL&* 5&fK9Mq)/iyqtA%kUe[ڛKN]Ě^,"`/ s[EQQm?|XJ߅92m]G.E΃ח U*Cn.j_)Tѧj̿30ڇ!A0=͜ar I3$C^-9#|pk!)?7.x9 @OO;WƝZBFU keZ75F6Tc6"ZȚs2y/1 ʵ:u4xa`C>6Rb/Yм)^=+~uRd`/|_8xbB0?Ft||Z\##|K 0>>zxv8۴吅q 8ĥ)"6>~\8:qM}#͚'ĉ#p\׶ l#bA?)|g g9|8jP(cr,BwV (WliVxxᡁ@0Okn;ɥh$_ckCgriv}>=wGzβ KkBɛ[˪ !J)h&k2%07δt}!d<9;I&0wV/ v 0<H}L&8ob%Hi|޶o&h1L|u֦y~󛱢8fٲUsւ)0oiFx2}X[zVYr_;N(w]_4B@OanC?gĦx>мgx>ΛToZoOMp>40>V Oy V9iq!4 LN,ˢu{jsz]|"R޻&'ƚ{53ўFu(<٪9:΋]B;)B>1::8;~)Yt|0(pw2N%&X,URBK)3\zz&}ax4;ǟ(tLNg{N|Ǽ\G#C9g$^\}p?556]/RP.90 k,U8/u776s ʪ_01چ|\N 0VV*3H鴃J7iI!wG_^ypl}r*jɤSR 5QN@ iZ#1ٰy;_\3\BQQ x:WJv츟ٯ$"@6 S#qe딇(/P( Dy~TOϻ<4:-+F`0||;Xl-"uw$Цi󼕝mKʩorz"mϺ$F:~E'ҐvD\y?Rr8_He@ e~O,T.(ފR*cY^m|cVR[8 JҡSm!ΆԨb)RHG{?MpqrmN>߶Y)\p,d#xۆWY*,l6]v0h15M˙MS8+EdI='LBJIH7_9{Caз*Lq,dt >+~ّeʏ?xԕ4bBAŚjﵫ!'\Ը$WNvKO}ӽmSşذqsOy?\[,d@'73'j%kOe`1.g2"e =YIzS2|zŐƄa\U,dP;jhhhaxǶ?КZ՚.q SE+XrbOu%\GتX(H,N^~]JyEZQKceTQ]VGYqnah;y$cQahT&QPZ*iZ8UQQM.qo/T\7X"u?Mttl2Xq(IoW{R^ ux*SYJ! 4S.Jy~ BROS[V|žKNɛP(L6V^|cR7i7nZW1Fd@ Ara{詑|(T*dN]Ko?s=@ |_EvF]׍kR)eBJc" MUUbY6`~V޴dJKß&~'d3i WWWWWW
Current Directory: /opt/imh-python/lib/python3.9/site-packages/pylint/checkers
Viewing File: /opt/imh-python/lib/python3.9/site-packages/pylint/checkers/imports.py
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Imports checkers for Python code.""" from __future__ import annotations import collections import copy import os import sys from collections import defaultdict from collections.abc import ItemsView, Sequence from functools import cached_property from typing import TYPE_CHECKING, Any, Union import astroid from astroid import nodes from astroid.nodes._base_nodes import ImportNode from pylint.checkers import BaseChecker, DeprecatedMixin from pylint.checkers.utils import ( get_import_name, in_type_checking_block, is_from_fallback_block, is_module_ignored, is_sys_guard, node_ignores_exception, ) from pylint.constants import MAX_NUMBER_OF_IMPORT_SHOWN from pylint.exceptions import EmptyReportError from pylint.graph import DotBackend, get_cycles from pylint.interfaces import HIGH from pylint.reporters.ureports.nodes import Paragraph, Section, VerbatimText from pylint.typing import MessageDefinitionTuple from pylint.utils import IsortDriver from pylint.utils.linterstats import LinterStats if TYPE_CHECKING: from pylint.lint import PyLinter # The dictionary with Any should actually be a _ImportTree again # but mypy doesn't support recursive types yet _ImportTree = dict[str, Union[list[dict[str, Any]], list[str]]] DEPRECATED_MODULES = { (0, 0, 0): {"tkinter.tix", "fpectl"}, (3, 3, 0): {"xml.etree.cElementTree"}, (3, 4, 0): {"imp"}, (3, 5, 0): {"formatter"}, (3, 6, 0): {"asynchat", "asyncore", "smtpd"}, (3, 7, 0): {"macpath"}, (3, 9, 0): {"lib2to3", "parser", "symbol", "binhex"}, (3, 10, 0): {"distutils", "typing.io", "typing.re"}, (3, 11, 0): { "aifc", "audioop", "cgi", "cgitb", "chunk", "crypt", "imghdr", "msilib", "mailcap", "nis", "nntplib", "ossaudiodev", "pipes", "sndhdr", "spwd", "sunau", "sre_compile", "sre_constants", "sre_parse", "telnetlib", "uu", "xdrlib", }, } def _get_first_import( node: ImportNode, context: nodes.LocalsDictNodeNG, name: str, base: str | None, level: int | None, alias: str | None, ) -> tuple[nodes.Import | nodes.ImportFrom | None, str | None]: """Return the node where [base.]<name> is imported or None if not found.""" fullname = f"{base}.{name}" if base else name first = None found = False msg = "reimported" for first in context.body: if first is node: continue if first.scope() is node.scope() and first.fromlineno > node.fromlineno: continue if isinstance(first, nodes.Import): if any(fullname == iname[0] for iname in first.names): found = True break for imported_name, imported_alias in first.names: if not imported_alias and imported_name == alias: found = True msg = "shadowed-import" break if found: break elif isinstance(first, nodes.ImportFrom): if level == first.level: for imported_name, imported_alias in first.names: if fullname == f"{first.modname}.{imported_name}": found = True break if ( name != "*" and name == imported_name and not (alias or imported_alias) ): found = True break if not imported_alias and imported_name == alias: found = True msg = "shadowed-import" break if found: break if found and not astroid.are_exclusive(first, node): return first, msg return None, None def _ignore_import_failure( node: ImportNode, modname: str, ignored_modules: Sequence[str], ) -> bool: if is_module_ignored(modname, ignored_modules): return True # Ignore import failure if part of guarded import block # I.e. `sys.version_info` or `typing.TYPE_CHECKING` if in_type_checking_block(node): return True if isinstance(node.parent, nodes.If) and is_sys_guard(node.parent): return True return node_ignores_exception(node, ImportError) # utilities to represents import dependencies as tree and dot graph ########### def _make_tree_defs(mod_files_list: ItemsView[str, set[str]]) -> _ImportTree: """Get a list of 2-uple (module, list_of_files_which_import_this_module), it will return a dictionary to represent this as a tree. """ tree_defs: _ImportTree = {} for mod, files in mod_files_list: node: list[_ImportTree | list[str]] = [tree_defs, []] for prefix in mod.split("."): assert isinstance(node[0], dict) node = node[0].setdefault(prefix, ({}, [])) # type: ignore[arg-type,assignment] assert isinstance(node[1], list) node[1].extend(files) return tree_defs def _repr_tree_defs(data: _ImportTree, indent_str: str | None = None) -> str: """Return a string which represents imports as a tree.""" lines = [] nodes_items = data.items() for i, (mod, (sub, files)) in enumerate(sorted(nodes_items, key=lambda x: x[0])): files_list = "" if not files else f"({','.join(sorted(files))})" if indent_str is None: lines.append(f"{mod} {files_list}") sub_indent_str = " " else: lines.append(rf"{indent_str}\-{mod} {files_list}") if i == len(nodes_items) - 1: sub_indent_str = f"{indent_str} " else: sub_indent_str = f"{indent_str}| " if sub and isinstance(sub, dict): lines.append(_repr_tree_defs(sub, sub_indent_str)) return "\n".join(lines) def _dependencies_graph(filename: str, dep_info: dict[str, set[str]]) -> str: """Write dependencies as a dot (graphviz) file.""" done = {} printer = DotBackend(os.path.splitext(os.path.basename(filename))[0], rankdir="LR") printer.emit('URL="." node[shape="box"]') for modname, dependencies in sorted(dep_info.items()): sorted_dependencies = sorted(dependencies) done[modname] = 1 printer.emit_node(modname) for depmodname in sorted_dependencies: if depmodname not in done: done[depmodname] = 1 printer.emit_node(depmodname) for depmodname, dependencies in sorted(dep_info.items()): for modname in sorted(dependencies): printer.emit_edge(modname, depmodname) return printer.generate(filename) def _make_graph( filename: str, dep_info: dict[str, set[str]], sect: Section, gtype: str ) -> None: """Generate a dependencies graph and add some information about it in the report's section. """ outputfile = _dependencies_graph(filename, dep_info) sect.append(Paragraph((f"{gtype}imports graph has been written to {outputfile}",))) # the import checker itself ################################################### MSGS: dict[str, MessageDefinitionTuple] = { "E0401": ( "Unable to import %s", "import-error", "Used when pylint has been unable to import a module.", {"old_names": [("F0401", "old-import-error")]}, ), "E0402": ( "Attempted relative import beyond top-level package", "relative-beyond-top-level", "Used when a relative import tries to access too many levels " "in the current package.", ), "R0401": ( "Cyclic import (%s)", "cyclic-import", "Used when a cyclic import between two or more modules is detected.", ), "R0402": ( "Use 'from %s import %s' instead", "consider-using-from-import", "Emitted when a submodule of a package is imported and " "aliased with the same name, " "e.g., instead of ``import concurrent.futures as futures`` use " "``from concurrent import futures``.", ), "W0401": ( "Wildcard import %s", "wildcard-import", "Used when `from module import *` is detected.", ), "W0404": ( "Reimport %r (imported line %s)", "reimported", "Used when a module is imported more than once.", ), "W0406": ( "Module import itself", "import-self", "Used when a module is importing itself.", ), "W0407": ( "Prefer importing %r instead of %r", "preferred-module", "Used when a module imported has a preferred replacement module.", ), "W0410": ( "__future__ import is not the first non docstring statement", "misplaced-future", "Python 2.5 and greater require __future__ import to be the " "first non docstring statement in the module.", ), "C0410": ( "Multiple imports on one line (%s)", "multiple-imports", "Used when import statement importing multiple modules is detected.", ), "C0411": ( "%s should be placed before %s", "wrong-import-order", "Used when PEP8 import order is not respected (standard imports " "first, then third-party libraries, then local imports).", ), "C0412": ( "Imports from package %s are not grouped", "ungrouped-imports", "Used when imports are not grouped by packages.", ), "C0413": ( 'Import "%s" should be placed at the top of the module', "wrong-import-position", "Used when code and imports are mixed.", ), "C0414": ( "Import alias does not rename original package", "useless-import-alias", "Used when an import alias is same as original package, " "e.g., using import numpy as numpy instead of import numpy as np.", ), "C0415": ( "Import outside toplevel (%s)", "import-outside-toplevel", "Used when an import statement is used anywhere other than the module " "toplevel. Move this import to the top of the file.", ), "W0416": ( "Shadowed %r (imported line %s)", "shadowed-import", "Used when a module is aliased with a name that shadows another import.", ), } DEFAULT_STANDARD_LIBRARY = () DEFAULT_KNOWN_THIRD_PARTY = ("enchant",) DEFAULT_PREFERRED_MODULES = () class ImportsChecker(DeprecatedMixin, BaseChecker): """BaseChecker for import statements. Checks for * external modules dependencies * relative / wildcard imports * cyclic imports * uses of deprecated modules * uses of modules instead of preferred modules """ name = "imports" msgs = {**DeprecatedMixin.DEPRECATED_MODULE_MESSAGE, **MSGS} default_deprecated_modules = () options = ( ( "deprecated-modules", { "default": default_deprecated_modules, "type": "csv", "metavar": "<modules>", "help": "Deprecated modules which should not be used," " separated by a comma.", }, ), ( "preferred-modules", { "default": DEFAULT_PREFERRED_MODULES, "type": "csv", "metavar": "<module:preferred-module>", "help": "Couples of modules and preferred modules," " separated by a comma.", }, ), ( "import-graph", { "default": "", "type": "path", "metavar": "<file.gv>", "help": "Output a graph (.gv or any supported image format) of" " all (i.e. internal and external) dependencies to the given file" " (report RP0402 must not be disabled).", }, ), ( "ext-import-graph", { "default": "", "type": "path", "metavar": "<file.gv>", "help": "Output a graph (.gv or any supported image format)" " of external dependencies to the given file" " (report RP0402 must not be disabled).", }, ), ( "int-import-graph", { "default": "", "type": "path", "metavar": "<file.gv>", "help": "Output a graph (.gv or any supported image format)" " of internal dependencies to the given file" " (report RP0402 must not be disabled).", }, ), ( "known-standard-library", { "default": DEFAULT_STANDARD_LIBRARY, "type": "csv", "metavar": "<modules>", "help": "Force import order to recognize a module as part of " "the standard compatibility libraries.", }, ), ( "known-third-party", { "default": DEFAULT_KNOWN_THIRD_PARTY, "type": "csv", "metavar": "<modules>", "help": "Force import order to recognize a module as part of " "a third party library.", }, ), ( "allow-any-import-level", { "default": (), "type": "csv", "metavar": "<modules>", "help": ( "List of modules that can be imported at any level, not just " "the top level one." ), }, ), ( "allow-wildcard-with-all", { "default": False, "type": "yn", "metavar": "<y or n>", "help": "Allow wildcard imports from modules that define __all__.", }, ), ( "allow-reexport-from-package", { "default": False, "type": "yn", "metavar": "<y or n>", "help": "Allow explicit reexports by alias from a package __init__.", }, ), ) def __init__(self, linter: PyLinter) -> None: BaseChecker.__init__(self, linter) self.import_graph: defaultdict[str, set[str]] = defaultdict(set) self._imports_stack: list[tuple[ImportNode, str]] = [] self._first_non_import_node = None self._module_pkg: dict[Any, Any] = ( {} ) # mapping of modules to the pkg they belong in self._allow_any_import_level: set[Any] = set() self.reports = ( ("RP0401", "External dependencies", self._report_external_dependencies), ("RP0402", "Modules dependencies graph", self._report_dependencies_graph), ) self._excluded_edges: defaultdict[str, set[str]] = defaultdict(set) def open(self) -> None: """Called before visiting project (i.e set of modules).""" self.linter.stats.dependencies = {} self.linter.stats = self.linter.stats self.import_graph = defaultdict(set) self._module_pkg = {} # mapping of modules to the pkg they belong in self._current_module_package = False self._ignored_modules: Sequence[str] = self.linter.config.ignored_modules # Build a mapping {'module': 'preferred-module'} self.preferred_modules = dict( module.split(":") for module in self.linter.config.preferred_modules if ":" in module ) self._allow_any_import_level = set(self.linter.config.allow_any_import_level) self._allow_reexport_package = self.linter.config.allow_reexport_from_package def _import_graph_without_ignored_edges(self) -> defaultdict[str, set[str]]: filtered_graph = copy.deepcopy(self.import_graph) for node in filtered_graph: filtered_graph[node].difference_update(self._excluded_edges[node]) return filtered_graph def close(self) -> None: """Called before visiting project (i.e set of modules).""" if self.linter.is_message_enabled("cyclic-import"): graph = self._import_graph_without_ignored_edges() vertices = list(graph) for cycle in get_cycles(graph, vertices=vertices): self.add_message("cyclic-import", args=" -> ".join(cycle)) def get_map_data( self, ) -> tuple[defaultdict[str, set[str]], defaultdict[str, set[str]]]: if self.linter.is_message_enabled("cyclic-import"): return (self.import_graph, self._excluded_edges) return (defaultdict(set), defaultdict(set)) def reduce_map_data( self, linter: PyLinter, data: list[tuple[defaultdict[str, set[str]], defaultdict[str, set[str]]]], ) -> None: if self.linter.is_message_enabled("cyclic-import"): self.import_graph = defaultdict(set) self._excluded_edges = defaultdict(set) for to_update in data: graph, excluded_edges = to_update self.import_graph.update(graph) self._excluded_edges.update(excluded_edges) self.close() def deprecated_modules(self) -> set[str]: """Callback returning the deprecated modules.""" # First get the modules the user indicated all_deprecated_modules = set(self.linter.config.deprecated_modules) # Now get the hard-coded ones from the stdlib for since_vers, mod_set in DEPRECATED_MODULES.items(): if since_vers <= sys.version_info: all_deprecated_modules = all_deprecated_modules.union(mod_set) return all_deprecated_modules def visit_module(self, node: nodes.Module) -> None: """Store if current module is a package, i.e. an __init__ file.""" self._current_module_package = node.package def visit_import(self, node: nodes.Import) -> None: """Triggered when an import statement is seen.""" self._check_reimport(node) self._check_import_as_rename(node) self._check_toplevel(node) names = [name for name, _ in node.names] if len(names) >= 2: self.add_message("multiple-imports", args=", ".join(names), node=node) for name in names: self.check_deprecated_module(node, name) self._check_preferred_module(node, name) imported_module = self._get_imported_module(node, name) if isinstance(node.parent, nodes.Module): # Allow imports nested self._check_position(node) if isinstance(node.scope(), nodes.Module): self._record_import(node, imported_module) if imported_module is None: continue self._add_imported_module(node, imported_module.name) def visit_importfrom(self, node: nodes.ImportFrom) -> None: """Triggered when a from statement is seen.""" basename = node.modname imported_module = self._get_imported_module(node, basename) absolute_name = get_import_name(node, basename) self._check_import_as_rename(node) self._check_misplaced_future(node) self.check_deprecated_module(node, absolute_name) self._check_preferred_module(node, basename) self._check_wildcard_imports(node, imported_module) self._check_same_line_imports(node) self._check_reimport(node, basename=basename, level=node.level) self._check_toplevel(node) if isinstance(node.parent, nodes.Module): # Allow imports nested self._check_position(node) if isinstance(node.scope(), nodes.Module): self._record_import(node, imported_module) if imported_module is None: return for name, _ in node.names: if name != "*": self._add_imported_module(node, f"{imported_module.name}.{name}") else: self._add_imported_module(node, imported_module.name) def leave_module(self, node: nodes.Module) -> None: # Check imports are grouped by category (standard, 3rd party, local) std_imports, ext_imports, loc_imports = self._check_imports_order(node) # Check that imports are grouped by package within a given category met_import: set[str] = set() # set for 'import x' style met_from: set[str] = set() # set for 'from x import y' style current_package = None for import_node, import_name in std_imports + ext_imports + loc_imports: met = met_from if isinstance(import_node, nodes.ImportFrom) else met_import package, _, _ = import_name.partition(".") if ( current_package and current_package != package and package in met and not in_type_checking_block(import_node) and not ( isinstance(import_node.parent, nodes.If) and is_sys_guard(import_node.parent) ) ): self.add_message("ungrouped-imports", node=import_node, args=package) current_package = package if not self.linter.is_message_enabled( "ungrouped-imports", import_node.fromlineno ): continue met.add(package) self._imports_stack = [] self._first_non_import_node = None def compute_first_non_import_node( self, node: ( nodes.If | nodes.Expr | nodes.Comprehension | nodes.IfExp | nodes.Assign | nodes.AssignAttr | nodes.Try ), ) -> None: # if the node does not contain an import instruction, and if it is the # first node of the module, keep a track of it (all the import positions # of the module will be compared to the position of this first # instruction) if self._first_non_import_node: return if not isinstance(node.parent, nodes.Module): return if isinstance(node, nodes.Try) and any( node.nodes_of_class((nodes.Import, nodes.ImportFrom)) ): return if isinstance(node, nodes.Assign): # Add compatibility for module level dunder names # https://www.python.org/dev/peps/pep-0008/#module-level-dunder-names valid_targets = [ isinstance(target, nodes.AssignName) and target.name.startswith("__") and target.name.endswith("__") for target in node.targets ] if all(valid_targets): return self._first_non_import_node = node visit_try = visit_assignattr = visit_assign = visit_ifexp = visit_comprehension = ( visit_expr ) = visit_if = compute_first_non_import_node def visit_functiondef( self, node: nodes.FunctionDef | nodes.While | nodes.For | nodes.ClassDef ) -> None: # If it is the first non import instruction of the module, record it. if self._first_non_import_node: return # Check if the node belongs to an `If` or a `Try` block. If they # contain imports, skip recording this node. if not isinstance(node.parent.scope(), nodes.Module): return root = node while not isinstance(root.parent, nodes.Module): root = root.parent if isinstance(root, (nodes.If, nodes.Try)): if any(root.nodes_of_class((nodes.Import, nodes.ImportFrom))): return self._first_non_import_node = node visit_classdef = visit_for = visit_while = visit_functiondef def _check_misplaced_future(self, node: nodes.ImportFrom) -> None: basename = node.modname if basename == "__future__": # check if this is the first non-docstring statement in the module prev = node.previous_sibling() if prev: # consecutive future statements are possible if not ( isinstance(prev, nodes.ImportFrom) and prev.modname == "__future__" ): self.add_message("misplaced-future", node=node) def _check_same_line_imports(self, node: nodes.ImportFrom) -> None: # Detect duplicate imports on the same line. names = (name for name, _ in node.names) counter = collections.Counter(names) for name, count in counter.items(): if count > 1: self.add_message("reimported", node=node, args=(name, node.fromlineno)) def _check_position(self, node: ImportNode) -> None: """Check `node` import or importfrom node position is correct. Send a message if `node` comes before another instruction """ # if a first non-import instruction has already been encountered, # it means the import comes after it and therefore is not well placed if self._first_non_import_node: if self.linter.is_message_enabled( "wrong-import-position", self._first_non_import_node.fromlineno ): self.add_message( "wrong-import-position", node=node, args=node.as_string() ) else: self.linter.add_ignored_message( "wrong-import-position", node.fromlineno, node ) def _record_import( self, node: ImportNode, importedmodnode: nodes.Module | None, ) -> None: """Record the package `node` imports from.""" if isinstance(node, nodes.ImportFrom): importedname = node.modname else: importedname = importedmodnode.name if importedmodnode else None if not importedname: importedname = node.names[0][0].split(".")[0] if isinstance(node, nodes.ImportFrom) and (node.level or 0) >= 1: # We need the importedname with first point to detect local package # Example of node: # 'from .my_package1 import MyClass1' # the output should be '.my_package1' instead of 'my_package1' # Example of node: # 'from . import my_package2' # the output should be '.my_package2' instead of '{pyfile}' importedname = "." + importedname self._imports_stack.append((node, importedname)) @staticmethod def _is_fallback_import( node: ImportNode, imports: list[tuple[ImportNode, str]] ) -> bool: imports = [import_node for (import_node, _) in imports] return any(astroid.are_exclusive(import_node, node) for import_node in imports) # pylint: disable = too-many-statements def _check_imports_order(self, _module_node: nodes.Module) -> tuple[ list[tuple[ImportNode, str]], list[tuple[ImportNode, str]], list[tuple[ImportNode, str]], ]: """Checks imports of module `node` are grouped by category. Imports must follow this order: standard, 3rd party, local """ std_imports: list[tuple[ImportNode, str]] = [] third_party_imports: list[tuple[ImportNode, str]] = [] first_party_imports: list[tuple[ImportNode, str]] = [] # need of a list that holds third or first party ordered import external_imports: list[tuple[ImportNode, str]] = [] local_imports: list[tuple[ImportNode, str]] = [] third_party_not_ignored: list[tuple[ImportNode, str]] = [] first_party_not_ignored: list[tuple[ImportNode, str]] = [] local_not_ignored: list[tuple[ImportNode, str]] = [] isort_driver = IsortDriver(self.linter.config) for node, modname in self._imports_stack: if modname.startswith("."): package = "." + modname.split(".")[1] else: package = modname.split(".")[0] nested = not isinstance(node.parent, nodes.Module) ignore_for_import_order = not self.linter.is_message_enabled( "wrong-import-order", node.fromlineno ) import_category = isort_driver.place_module(package) node_and_package_import = (node, package) if import_category in {"FUTURE", "STDLIB"}: std_imports.append(node_and_package_import) wrong_import = ( third_party_not_ignored or first_party_not_ignored or local_not_ignored ) if self._is_fallback_import(node, wrong_import): continue if wrong_import and not nested: self.add_message( "wrong-import-order", node=node, args=( ## TODO - this isn't right for multiple on the same line... f'standard import "{self._get_full_import_name((node, package))}"', self._get_out_of_order_string( third_party_not_ignored, first_party_not_ignored, local_not_ignored, ), ), ) elif import_category == "THIRDPARTY": third_party_imports.append(node_and_package_import) external_imports.append(node_and_package_import) if not nested: if not ignore_for_import_order: third_party_not_ignored.append(node_and_package_import) else: self.linter.add_ignored_message( "wrong-import-order", node.fromlineno, node ) wrong_import = first_party_not_ignored or local_not_ignored if wrong_import and not nested: self.add_message( "wrong-import-order", node=node, args=( f'third party import "{self._get_full_import_name((node, package))}"', self._get_out_of_order_string( None, first_party_not_ignored, local_not_ignored ), ), ) elif import_category == "FIRSTPARTY": first_party_imports.append(node_and_package_import) external_imports.append(node_and_package_import) if not nested: if not ignore_for_import_order: first_party_not_ignored.append(node_and_package_import) else: self.linter.add_ignored_message( "wrong-import-order", node.fromlineno, node ) wrong_import = local_not_ignored if wrong_import and not nested: self.add_message( "wrong-import-order", node=node, args=( f'first party import "{self._get_full_import_name((node, package))}"', self._get_out_of_order_string( None, None, local_not_ignored ), ), ) elif import_category == "LOCALFOLDER": local_imports.append((node, package)) if not nested: if not ignore_for_import_order: local_not_ignored.append((node, package)) else: self.linter.add_ignored_message( "wrong-import-order", node.fromlineno, node ) return std_imports, external_imports, local_imports def _get_out_of_order_string( self, third_party_imports: list[tuple[ImportNode, str]] | None, first_party_imports: list[tuple[ImportNode, str]] | None, local_imports: list[tuple[ImportNode, str]] | None, ) -> str: # construct the string listing out of order imports used in the message # for wrong-import-order if third_party_imports: plural = "s" if len(third_party_imports) > 1 else "" if len(third_party_imports) > MAX_NUMBER_OF_IMPORT_SHOWN: imports_list = ( ", ".join( [ f'"{self._get_full_import_name(tpi)}"' for tpi in third_party_imports[ : int(MAX_NUMBER_OF_IMPORT_SHOWN // 2) ] ] ) + " (...) " + ", ".join( [ f'"{self._get_full_import_name(tpi)}"' for tpi in third_party_imports[ int(-MAX_NUMBER_OF_IMPORT_SHOWN // 2) : ] ] ) ) else: imports_list = ", ".join( [ f'"{self._get_full_import_name(tpi)}"' for tpi in third_party_imports ] ) third_party = f"third party import{plural} {imports_list}" else: third_party = "" if first_party_imports: plural = "s" if len(first_party_imports) > 1 else "" if len(first_party_imports) > MAX_NUMBER_OF_IMPORT_SHOWN: imports_list = ( ", ".join( [ f'"{self._get_full_import_name(tpi)}"' for tpi in first_party_imports[ : int(MAX_NUMBER_OF_IMPORT_SHOWN // 2) ] ] ) + " (...) " + ", ".join( [ f'"{self._get_full_import_name(tpi)}"' for tpi in first_party_imports[ int(-MAX_NUMBER_OF_IMPORT_SHOWN // 2) : ] ] ) ) else: imports_list = ", ".join( [ f'"{self._get_full_import_name(fpi)}"' for fpi in first_party_imports ] ) first_party = f"first party import{plural} {imports_list}" else: first_party = "" if local_imports: plural = "s" if len(local_imports) > 1 else "" if len(local_imports) > MAX_NUMBER_OF_IMPORT_SHOWN: imports_list = ( ", ".join( [ f'"{self._get_full_import_name(tpi)}"' for tpi in local_imports[ : int(MAX_NUMBER_OF_IMPORT_SHOWN // 2) ] ] ) + " (...) " + ", ".join( [ f'"{self._get_full_import_name(tpi)}"' for tpi in local_imports[ int(-MAX_NUMBER_OF_IMPORT_SHOWN // 2) : ] ] ) ) else: imports_list = ", ".join( [f'"{self._get_full_import_name(li)}"' for li in local_imports] ) local = f"local import{plural} {imports_list}" else: local = "" delimiter_third_party = ( ( ", " if (first_party and local) else (" and " if (first_party or local) else "") ) if third_party else "" ) delimiter_first_party1 = ( (", " if (third_party and local) else " ") if first_party else "" ) delimiter_first_party2 = ("and " if local else "") if first_party else "" delimiter_first_party = f"{delimiter_first_party1}{delimiter_first_party2}" msg = ( f"{third_party}{delimiter_third_party}" f"{first_party}{delimiter_first_party}" f'{local if local else ""}' ) return msg def _get_full_import_name(self, importNode: ImportNode) -> str: # construct a more descriptive name of the import # for: import X, this returns X # for: import X.Y this returns X.Y # for: from X import Y, this returns X.Y try: # this will only succeed for ImportFrom nodes, which in themselves # contain the information needed to reconstruct the package return f"{importNode[0].modname}.{importNode[0].names[0][0]}" except AttributeError: # in all other cases, the import will either be X or X.Y node: str = importNode[0].names[0][0] package: str = importNode[1] if node.split(".")[0] == package: # this is sufficient with one import per line, since package = X # and node = X.Y or X return node # when there is a node that contains multiple imports, the "current" # import being analyzed is specified by package (node is the first # import on the line and therefore != package in this case) return package def _get_imported_module( self, importnode: ImportNode, modname: str ) -> nodes.Module | None: try: return importnode.do_import_module(modname) except astroid.TooManyLevelsError: if _ignore_import_failure(importnode, modname, self._ignored_modules): return None self.add_message("relative-beyond-top-level", node=importnode) except astroid.AstroidSyntaxError as exc: message = f"Cannot import {modname!r} due to '{exc.error}'" self.add_message( "syntax-error", line=importnode.lineno, args=message, confidence=HIGH ) except astroid.AstroidBuildingError: if not self.linter.is_message_enabled("import-error"): return None if _ignore_import_failure(importnode, modname, self._ignored_modules): return None if ( not self.linter.config.analyse_fallback_blocks and is_from_fallback_block(importnode) ): return None dotted_modname = get_import_name(importnode, modname) self.add_message("import-error", args=repr(dotted_modname), node=importnode) except Exception as e: # pragma: no cover raise astroid.AstroidError from e return None def _add_imported_module(self, node: ImportNode, importedmodname: str) -> None: """Notify an imported module, used to analyze dependencies.""" module_file = node.root().file context_name = node.root().name base = os.path.splitext(os.path.basename(module_file))[0] try: if isinstance(node, nodes.ImportFrom) and node.level: importedmodname = astroid.modutils.get_module_part( importedmodname, module_file ) else: importedmodname = astroid.modutils.get_module_part(importedmodname) except ImportError: pass if context_name == importedmodname: self.add_message("import-self", node=node) elif not astroid.modutils.is_stdlib_module(importedmodname): # if this is not a package __init__ module if base != "__init__" and context_name not in self._module_pkg: # record the module's parent, or the module itself if this is # a top level module, as the package it belongs to self._module_pkg[context_name] = context_name.rsplit(".", 1)[0] # handle dependencies dependencies_stat: dict[str, set[str]] = self.linter.stats.dependencies importedmodnames = dependencies_stat.setdefault(importedmodname, set()) if context_name not in importedmodnames: importedmodnames.add(context_name) # update import graph self.import_graph[context_name].add(importedmodname) if not self.linter.is_message_enabled( "cyclic-import", line=node.lineno ) or in_type_checking_block(node): self._excluded_edges[context_name].add(importedmodname) def _check_preferred_module(self, node: ImportNode, mod_path: str) -> None: """Check if the module has a preferred replacement.""" mod_compare = [mod_path] # build a comparison list of possible names using importfrom if isinstance(node, astroid.nodes.node_classes.ImportFrom): mod_compare = [f"{node.modname}.{name[0]}" for name in node.names] # find whether there are matches with the import vs preferred_modules keys matches = [ k for k in self.preferred_modules for mod in mod_compare # exact match if k == mod # checks for base module matches or k in mod.split(".")[0] ] # if we have matches, add message if matches: self.add_message( "preferred-module", node=node, args=(self.preferred_modules[matches[0]], matches[0]), ) def _check_import_as_rename(self, node: ImportNode) -> None: names = node.names for name in names: if not all(name): return splitted_packages = name[0].rsplit(".", maxsplit=1) import_name = splitted_packages[-1] aliased_name = name[1] if import_name != aliased_name: continue if len(splitted_packages) == 1 and ( self._allow_reexport_package is False or self._current_module_package is False ): self.add_message("useless-import-alias", node=node, confidence=HIGH) elif len(splitted_packages) == 2: self.add_message( "consider-using-from-import", node=node, args=(splitted_packages[0], import_name), ) def _check_reimport( self, node: ImportNode, basename: str | None = None, level: int | None = None, ) -> None: """Check if a module with the same name is already imported or aliased.""" if not self.linter.is_message_enabled( "reimported" ) and not self.linter.is_message_enabled("shadowed-import"): return frame = node.frame() root = node.root() contexts = [(frame, level)] if root is not frame: contexts.append((root, None)) for known_context, known_level in contexts: for name, alias in node.names: first, msg = _get_first_import( node, known_context, name, basename, known_level, alias ) if first is not None and msg is not None: name = name if msg == "reimported" else alias self.add_message( msg, node=node, args=(name, first.fromlineno), confidence=HIGH ) def _report_external_dependencies( self, sect: Section, _: LinterStats, _dummy: LinterStats | None ) -> None: """Return a verbatim layout for displaying dependencies.""" dep_info = _make_tree_defs(self._external_dependencies_info.items()) if not dep_info: raise EmptyReportError() tree_str = _repr_tree_defs(dep_info) sect.append(VerbatimText(tree_str)) def _report_dependencies_graph( self, sect: Section, _: LinterStats, _dummy: LinterStats | None ) -> None: """Write dependencies as a dot (graphviz) file.""" dep_info = self.linter.stats.dependencies if not dep_info or not ( self.linter.config.import_graph or self.linter.config.ext_import_graph or self.linter.config.int_import_graph ): raise EmptyReportError() filename = self.linter.config.import_graph if filename: _make_graph(filename, dep_info, sect, "") filename = self.linter.config.ext_import_graph if filename: _make_graph(filename, self._external_dependencies_info, sect, "external ") filename = self.linter.config.int_import_graph if filename: _make_graph(filename, self._internal_dependencies_info, sect, "internal ") def _filter_dependencies_graph(self, internal: bool) -> defaultdict[str, set[str]]: """Build the internal or the external dependency graph.""" graph: defaultdict[str, set[str]] = defaultdict(set) for importee, importers in self.linter.stats.dependencies.items(): for importer in importers: package = self._module_pkg.get(importer, importer) is_inside = importee.startswith(package) if is_inside and internal or not is_inside and not internal: graph[importee].add(importer) return graph @cached_property def _external_dependencies_info(self) -> defaultdict[str, set[str]]: """Return cached external dependencies information or build and cache them. """ return self._filter_dependencies_graph(internal=False) @cached_property def _internal_dependencies_info(self) -> defaultdict[str, set[str]]: """Return cached internal dependencies information or build and cache them. """ return self._filter_dependencies_graph(internal=True) def _check_wildcard_imports( self, node: nodes.ImportFrom, imported_module: nodes.Module | None ) -> None: if node.root().package: # Skip the check if in __init__.py issue #2026 return wildcard_import_is_allowed = self._wildcard_import_is_allowed(imported_module) for name, _ in node.names: if name == "*" and not wildcard_import_is_allowed: self.add_message("wildcard-import", args=node.modname, node=node) def _wildcard_import_is_allowed(self, imported_module: nodes.Module | None) -> bool: return ( self.linter.config.allow_wildcard_with_all and imported_module is not None and "__all__" in imported_module.locals ) def _check_toplevel(self, node: ImportNode) -> None: """Check whether the import is made outside the module toplevel.""" # If the scope of the import is a module, then obviously it is # not outside the module toplevel. if isinstance(node.scope(), nodes.Module): return module_names = [ ( f"{node.modname}.{name[0]}" if isinstance(node, nodes.ImportFrom) else name[0] ) for name in node.names ] # Get the full names of all the imports that are only allowed at the module level scoped_imports = [ name for name in module_names if name not in self._allow_any_import_level ] if scoped_imports: self.add_message( "import-outside-toplevel", args=", ".join(scoped_imports), node=node ) def register(linter: PyLinter) -> None: linter.register_checker(ImportsChecker(linter))