././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8600292 jeepney-0.9.0/LICENSE0000644000000000000000000000207114760131573011121 0ustar00The MIT License (MIT) Copyright (c) 2017 Thomas Kluyver Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8600292 jeepney-0.9.0/README.rst0000644000000000000000000000056114760131573011605 0ustar00Jeepney is a pure Python implementation of D-Bus messaging. It has an `I/O-free `__ core, and integration modules for different event loops. D-Bus is an inter-process communication system, mainly used in Linux. To install Jeepney:: pip install jeepney `Jeepney docs on Readthedocs `__ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8600292 jeepney-0.9.0/docs/Makefile0000644000000000000000000001701414760131573012507 0ustar00# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " epub3 to make an epub3" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" @echo " dummy to check syntax errors of document sources" .PHONY: clean clean: rm -rf $(BUILDDIR)/* .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: pickle pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." .PHONY: qthelp qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Jeepney.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Jeepney.qhc" .PHONY: applehelp applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." .PHONY: devhelp devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Jeepney" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Jeepney" @echo "# devhelp" .PHONY: epub epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .PHONY: epub3 epub3: $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 @echo @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." .PHONY: latex latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." .PHONY: latexpdf latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: latexpdfja latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: text text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." .PHONY: man man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." .PHONY: texinfo texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." .PHONY: info info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." .PHONY: gettext gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." .PHONY: changes changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." .PHONY: linkcheck linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." .PHONY: doctest doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." .PHONY: coverage coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." .PHONY: xml xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." .PHONY: pseudoxml pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." .PHONY: dummy dummy: $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy @echo @echo "Build finished. Dummy builder generates no files." .PHONY: livehtml livehtml: sphinx-autobuild $(ALLSPHINXOPTS) "$(BUILDDIR)" $(O) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8610291 jeepney-0.9.0/docs/_static/desktop-notification.png0000644000000000000000000012260714760131573017345 0ustar00PNG  IHDRSbMKsBIT|dtEXtSoftwaregnome-screenshot>*tEXtCreation TimeSun 17 Oct 2021 17:28:48 BSTc IDATxy]E]U{s0"2C $a~  ڴhEEpƠҠ=6cdDpBe1@d{ήIH.$az\r9{׮]k=RWB VL *(((((((X2UPPPPPPP (d`PTAAAAAAA*U@RW`;v,y߰h"^:,5rMc v{,sGydC_svtZQG{[oͼy;7#<2z3Xx1=;3G}4&Mb̘1̚5SO=;#n9ꨣx;ɆnO<_gsn/((xWq<3r-lns9k䚻;ӧOgm{?/~mL2o}cر?`v+fb6Ca}cبۿqAq/S=lw ,{ac=>x ='x"}{{+k?MUUviAAaœ9sŒ3nj1zƍ wyg={vw̙=ܕ/9̙3'tAÜ9sgD$wqaΜ9w]h+i̙v=x`w}~s[ZV8sÜ9sܾ?omfmœ9s\𒏭S~ϪTA+n!v?{:Ü9s w'x"sa};_*'t]w7x#ԧ{Eq+t}bCa޼y\q|n9#Odhh_י5k{o}+`̙瞜vitA?g+jo駟Ιgo \vXgur/^0aBnm6|QTA+3gdhhߞ7p lVϻC=}ه9s:p1ǰ{//|&?1c0c ^M7ݔV]wO?կb-qGUUe``ŋsWpO?}T[;w.s3g'\y'OF;ϱ}~rʨE .drG~]wݕT?k-{v̘1cǼ`~ܹZ6pCx8j+f̘1jsgpp>9ϛ4ip>>=mݘ6mЇ1crIeAA+ 2UP u];~azk\}~i=KcN>Kw&O̗%9sp1Ǭ/ *(x!UW]q-kޘ?֫sK>?1?!n7^{ٳ7nڳ:\3f ?ϸ ƸgfmƷm{{/~ qEw=ܓM7EiSAAA!Sm~| B`-/b`meԩ̜93g>q/ /j;6wG7ٳgx;Iôk֬=}k4io|yk^(:36m\p_F?qYd>TA+o~937o<lv?dxxYf1yd."͛Ǟ{J|;)Sp-SNa…zXN=TNʞ{N6h#b yOsロoSrs5װV[vms= _W^y%~JIh}DZKGk\ւ 8S馛;w.oy[+馛4il W_}u֊2Q,S|gaҤIl&q| .g̙l& p'5LG#|g>fuf\ve+-󩧞;>(SL{}c\xᅬqJ /6xc;06|=^zӦMRxs!p!ַg}#8Z->`ƌg'>ֻ ANTAAAAAAA*U@!SB VL *(((((((X2UPPPPPPP (d`PTAAAAAAA*U@_@)H8#@ ր1n !DF"Hz\ Bdž@A?<1{AZc-!w!} LЯmG'񼀫[T0բ&ԞAS^XsǶ,vÝx`A #p.v;B XLK#WX[ђvq3B_ULǑ0"t@ 3` " îfQ;B`+1R @0A :@9m BcAǐXzL%qƀ p0CADlIǒ^#3P{h}P;!o-c+akj#a#C!muv%\@@B|b{*c xID \g<=\J8!;s[:U 䱇ޏV۪T H|=vSNB 5&GV6?xp+[ꇠm#HgC=uC ^$Dkc Z+i^#}btl{-F^ڊB5)-KggP奇6=&u>އص Z{1!w5BeSƶ,FFp}}Kmpx:5{L_{:Cw;]|ǂ`[Uht,Op:W{XԱ*Cstݚ\lA6 C. &x9Zؾb-p<ݮ]YJג86Bk?wk\"*p*:EH#-6/J`<' z&*B,]O'"} ΤHo> Refdӛkta>W/0XT2.+#0`k\by$ESIJ\BPrNtU ```[F'0JVV"ei[Cׄ w"@` x}}?k+ .{PM7zwK!U~t3L&qtH3 %ғ HʁTq!se 6c @M%7LsJNz/wfcL\⃏Xtqk┺cA b}!=qުD6CQtc^PE<..6<ƽs=s5sck2Q JrEt^i:7Źg~΄6-d"-S.!ވ|$:/ZЙi}y|E%J)2HÇӋRqqHe:6Zj;5uAcEmIoH߮=j1Fm=m>"} ɉ+L:2@+ih}`ϰ*kPW]cVxBmwJPv`&1l!&ƚfi jTL+3<B0ԑqz$^@7"N%!IN:YfP?Q+hH| 8וy>wB:;0ɖs#iO@ dBM 5iaS䗛}B:}Xc&g}&$<'eߴe#-$}AISf"!4Ʊ z1Ԥ_켔&c6":B|AQR8 ⩋1&OĤ)ޣ4 EK0"Xhys.G5x"ًFfdGXas8{Oj{N4]LZsʹUKWt̥XHPCjlȯ!Dݼħ{HbCBϲs!^P*1aBuIk_~B-Mj,q #kkqPH-w9\-p ԡֺZS Xԃ5VK}d%x|>WI/Beb]L$݉-:/V_<ԣ(I;["s҆fnЎֳBX5I9xu-GF&Ɓ O46f4Htro@'DKtYLizV}v4fl~ Χ@=T8'ĥY~ɚCUkb.J֍$mXSij I̪ ;BTjݚNPׂq(kl,Ю,Z:0 frFJN.nZԝr RGD BVIZdC3c `D'|"I5͢^r|~O~a[VqD2BtkD+ 8z%@8 yqϽia*, ѤWttiӻZɊx՗T%NMO?Grϋ4|- ڢoV ̈i$R.W'QIL:7x%[ѕAPvNV.\ı,f!Zcཎ}u#UX}jDxkuz kvїEu^l -\\:G&ݤbl!`:5+=YoIpPyhc3⧁K0y=F>Ҽ$D"!jVmR׵z Ӎ[)AZjC!H @SX22i?{5NbՊu$j%Jl_[+h%ۘ5#!j,b e!pN/?֕ՁAh4/74Y3 cG5aР(mK%398-80OpcU4|g%?A=o.~BBݥŇf8mcvTj# &6OH#Huk]6F 6D,V0U5~I?D`M~ntЪ}s͠2/w$=%5fQnx4һbtQDwb~&2BZ=C{'A REje[XtEm[-d؆P;A>v׬Ӿ4g%Z7.*3֪RgīU'z|&:i6,C>ڒZ)Rj:!iG=ޢb0Piwч,FE@=VӋ,]9TM>8&TxђfyK -kNw Ck7 ^0>f0ìAT1%xFry$ c d1X;]14^N%]2ȽZ-teKRRMUngOAUDxUoS0 jC1Hê)z 5rC,Rܜ?~|BQUǏgt;myBz605;Vͨhݠz+3@pui9" GrCh~mv 2&j@J֞U2{ɂ9 g)F&x!e_I N;zvExMn{-HB "7JDmjGuneT2='`Jq"tk,!ƛJƩuC @.Ez>" !Ly г<H3anwK@CR!KqW*?H06zcbD◶8#^~؁B ^Ax蛸ftSY&IIczBAD T*9CP'8ҖJwk^S/AliH!Q+o_&/IG_ʡpוdɳ1Re@XNbz7Aº!̝Tb 8=.ǰ8 &cNȜJ{>V<]q[O ẁzİDu11ELi4JtMVmtD\KT )Xq@7fx#qJ9کrtWvtM__*ʂ VydM7⽞-!^O *&OC0$79cOnH\lLg Z+q.ƙj,]#I:=A4\\/XJy ȩ,>eb*(((((xj1I|Gi'e"ntRFBZ,D:%zR4D4R?FϒC@%1ͦqbӳ;1@D}$T11GHLR5 ,2ْBTAAAAA+"B,:HDAD{uPS݀vB0u]H{0Ҧ7C>@I~$XibǟUeE$w }ae͆z`"1M>=f0*ЈA!4[TAAAAA+"Bp]<`?DPʌ2D2LX1ab&ɸC/2E`)*@ߚIZQ + cΔqZ"Fã u/Ԍtڷ瘪Eg OeER+-Fu { IDAT ^0#5LB|HTNBB7`ϛ$)=Bd#l89HhcbTu1E[W*';4FIt|JEBsQo%1Bi2(%իѩMP7% ^=l%jC-%k.}ЛFSP@CN,1ꥉ- Np&UP5OpeDTB%nch2?O94b0PUnaK-Sg륎G}_PPPPPP@FIfO'kh@Y IC_1~$7bð!0Okh"X>,!$]1&};hmDQ4n+,Tyo zjVJrIZB ^UH(z4JBvZFDqezIL-WeN)]Ab FBBc f~*zH顉ywO ;GJjJA79xSY*~3ZG^ii_B5ճ* Rl qW`FI4Us0q1fuɿMW%SWQ Ӕ`B͠;@i%딱Vg P͖|5Li 02UPPPPPjA A S0ΤN UJeȈ4| >|wڥ݂=P(bJO$K \M5"!iqe"yJM$;$F=oVn{Lzڼ'1Hf4ℑ;1^ɫzNO%pNu=Ս+ + '3f<=lƎᇿŬjAAAAJи[J ǏJxD'˒oIzތ.]t{QHTg.uUR'162:I"} c&[ lhk} !*ea9s Z_}`\n'2qDLمꪫ~O?*x W?dH}yti:knRk~0Ώ gR߸\Xcb^|IE`PP{M\+2^dJl"޻% #92ʉQ}6WQgCMvi"'}H|eر:j1mT3kL V+n륮FAAAAADF %S^SgJdI@43l*DkVHA3CIcʗĪFX*(0*>F0{O,1xo"JaYc DFFdi5sQ"SES2u1f/s?D__?0m4Ǝ஻'|c v({&L?O~rrJ=ԯ[Mժ8shş7ocǎG?wy'_Iy'~OM_|G2e.z/24Hv!h$Уi2\hN`Hb )PWFS E[e؜Oe7 %!|PjcHr%KAцga##lj:,D.&  qbTM*#@[SV7M|G/>_w` i׿>+"|Cd\  E5ve9dg}>}:̱"uԇ9[c>'r]wܫ-ܒmنヌmٖZ w܁_7lݶ]wB`ࡇr챟a)̙g1MucK]2UPPPrA4S (1]]DMuC/H9zdM)MG#Qo΃:i:ʀ8UTjlBpnĪ̥1IUQl2Ŵ01hy%WhTGXhF4n߫'r1Yᗿ2zGyGy8Rp1d򵺱o\oxæL 7p#_Woދ6ڈG}tO0+> 'ַq!R2uM7smvO?ZkN;įv[nu=q̞=k-}}}#3u͛1|=?̧GW,S//$,&\iN- n D&̔d +۳OJ;QăQ28e:*N-Wb|иN*eeDVPetDK |y$h>ORv@;W+={Ü|)̚u !pe裏/~!qjeogoom]//qq+ׂ`[nnpusq׽n?̝;7[vqz6hCviG1l$yx N2-Y2ȃ>8~ⵯ}-v{.kɒ%W,S/?h6(2ޝgzI bj;k2Oȉb%_Әv㕇,4V$Ԉ"V2ihrM1Ih) tA,:7bIeHqԜZ(JU>v0 ѯ:0qD-#>;sVJz1s{wGʟ>?Y#a7q-mY`!7p#gyֈ.\ I0]3J7puw[o͖[nO~r>믿> 'Of-/g]uLh?0)((((x!/{)iRāszOܧ,]Z.V)ƨ'"p|=kWPƣ#0œZF_RHT4@W&ֻ.c FD 4yRe#?kX o^+Lqۈ/ ow9_~K#twm9ǤIvyꩧs^ji~衇6mZ&8 "/_4nFswr7pny]/=0MJa-gZoYFUvAAAA{_PnCfD*GJ [?f8 `jWHB*KDr|㜋K@,A SB0ٜfzLg<1+$(RPΐG݄`zEd" W2Rt@{ixկ~=ⳝwyB3_Ǐk_ s11׿.`u}{/cƌ[h 934G}N3eK~:¢0{,XSp]wtxg9L/{zυ޺ϛ$w97ޘT=3}GFOs;! ^b$W*D\n>YBωjuhj 4?{τ?ɀc,4oQ"7:] C~xWkdrPlAIF0FttA١$mU2(K0os\/I_0s/x_pYg}n;-؂ŋg ηm~iַ馯f?}}}L w}7'7OC+c;v, ,]V{$"ph[sr{7|KcVzaK_ke„ r_n}ӟ}Uk)@s1<OP SI4dA#٢4P[wcYk*5]v ]x}!P|6c1V8L:5Uk~ ɑݍ#HOqw_Jj ܃|eGY`u]wEfwa{`4 K0~T^,Y=cǎs188HUU;u]DSfWu0Fm#^wu>Y? Q5v;:NgVx=E!"T]-qN˶miZS^PPPPga?z!VIB. kS)8!w=D p(MB3U3ي  GTJ^kFe|d:0,DUbȽ/-&6,Zʲj5qAv a֬;ax8⸥5HMW\ v =,I}}}ˈ6|3no/YZYDdT~̘1yi첫.P)!rQ.$"h<MB!gFЭ Avhc2Y bcM}P kAՀ FMy|V8F=|Īj M[{]=,0`Y a*[[` ĉ$"?wPR쳋)kĉ#~gWO%_Xx W_}K]WzyDyBf&J)cSDՄ,1|ƝY3hXʅJ-ywfO| r 57R(.O3;U$&Fym$B{yU/YClo*)hgHL,hD>/?v1fK```%K0k֬e[?ey`֬Y{w?~(((((߇L[. (@) PWpAW=| Dy%5ܪ<{ޢgMH]z).UTiYlw@0b+Tm=4A5SlGS\{Y2;#RbdIeBSW4R~NAVn}n_u5wq>g]̙3UcW1,(((((XK5gqݥhݑgB"Q Q5DbDB TU6[zbq˦2V/"B'ioBUf-YNb$AUcu e%1W_3B۴foXt]nvnvz<yVO% ^HnDe$Rx^,`<3]4zpSٸ13-L ē]H0c0>LRvs:u9)2nQvՓ'`8Q^RBԵ1'&Gŋҟo?ied<]rϖƤ`Y-">D}idE^B#YAA9Crh [v*K@z6`Q-"?1d1#BnaZbL+%8WgB}$dl¶,"Be-coj*QG 58%5aAX<p7j8X6N:;{q.觫j$3ɃeojD4Fh@9+*cv:ƆҭyM(&'Z=AOpZ]kkc[V .<--9bltYciZQEvӵZiت¶\ !']]Xd _kZ|GuM9g7|Djxx|+aWPPPPPP1r˛4y0Im MkMR`w,u-H nz9|c뮣;ujQ 1"ÝhrFhKeXiK6c lM k yԪ8cG?vc2n7m'!l XF.9S l7ql#>OX͵+(((((x#% DsGMV#%\hyy%M>0MfCL?.fhMƇ:`[HDo\uj: b UAAz'|jYZTm {BlL16"j` t?znrBri"/ĉ<'%<\u5\sC2t҉x\qů׵_y1}+kI;&wb]Xh1gu6S1{/JV'מs[y>xc쳿֣*Ir;IF%)ChF@0O&^&D&*Mc>d>n Z&Ut &QU 6q!FZ뢅Ik )TEdْI [;"x]VTJ-YbWS> SO=?|5VO<4|KѪscN(#1UMdN@PᝧV_`h!c,%I>n3t]ύdRd  ucES՘XYkx"cm;E?"!7->2a„7|oѺ=P[4E_/kf~+xikK 6xn&N݅7ވ3/.ڏwߕϜ93cy<|6m W]u-3f7}a=MoڝsK_7w_c˾^{1<,X>MV֎3>=ӧ; IDATϕWwߕM7}=gu6wQ]`ec`e߯ḽVT?|$|?|ַu VbLl+И 1g.@Mã9$J<)Nfmp݇%ղPBjcȱ*(U"tC꺦ۭiD>U[hNzD(m!N5^0DZLUa ֩!k`;߫&Ϲ~'ϻ}(Gu$ӧD]#\O|?>k_1c,r^]̝;J}] ?D.wm~Zkg?ġ3K.}y x;΃>W_KUU[ows.O? L~ O"NJ꿼^QtЁl$&Cs챟⤓~"׼f"_gjq9Yp!Ǐ笳2ߗSo_Wv[.|vv4uV؎4^G3>VԞ?O~Ă F};F}1ZQ_#{1d;lo~sWPHn'dKf$xR5$[}E'W_o gkyRB#2J"b$Pc-U}K wݡ[]{jG.'.C5y|aյf:Q^ؔD9&K/d?/7Wcˮ`^p}uZ˝w`x衇WZ9wbѢE\~/?I^MFs׭c{pŗR5CCC\td@-=>L<]ww= 1z\su<<pdgY`!W]u [lوs~C344y]ȳ>ԩ,+\RUy\hiXk10n8);%N ,s1؟s=8CCr˭ysƊt4uXY;Vmnu_xXV6F;FV6枫VVwM__nzƏ&lRB$4֡ *ڮ$?ŀj5H">و4ɸ峻09חz_%c4#=m4`@.2zb x%E"Bek(LBWע1heg: FZ.|j`Ν=Y\suOWZ9oe)lf?Z+S 6c gƌ?_>YgPv>z<|ƲdZeHAjqjFS\2aXkyG:?몘\zs\b}5'B $DXbkX3]}QB!i7+_:[m%>{}G>N8tӭ0S|~A]x1_)U:ƍ"OCϮB=RK2 /"c4_Y}R]BDhZ#\]˽ E;^H.Ҏ5{]xU#є-!g`'s7"U‡, Wh~,V16 8( El}]kJG;%[E\\F*$MuSF4k+\̔|L\Yu:A6V:et;mÞ1\k;/>옘1BRI(!&4ďJ*?"U" JTH"R&R#FEIj%4`E&@M `ssx1{{}t|zךk113T#eD!9V1c"Ӗokk__|ZS5? k /2// /<[?!<}_5=z\2o~۾]ɼW5ַ~_}BW~_#/yo}+>_Z?V1WPy=>x5ky+\}қyfw:>mc돯HeI7eVgIEk›@ 6S*wa_`Tj>]a$`p(θrqY<e^'>`?1;>(`G;lYFTs r ?'=oMOr#|~ ^zMx_//kV'=>_ux^?//53wΏ᛾7q?g ?#?w_ -~߀w׽o?[OSG>K+:jGş[x_oo?a?_uoǛ;q:}bތKxW[s6wO_]2qR! z~5+dj 5 @|'eK~Uf{!$T.ǔDqϥ LH`Nnۆa|rND$n6R.᭘lah?#"R?% !k5om+o㸽/WT'~W??⛾3%OG|wIMc? :N'`6C'''>.{^|7q o/[߿wg|G/W03Ϳ?[O;?E ?_˾W_m໿?G7><׻}{xTO~W}%ZG?z5?䘁&fL2a}C?mxw}x'8?  ;k1(%-[ThBl;6a?_+oF|>cQO<(ŗ_~wwwh=I y|;ϫco51ƪL<~x]^xay眸ǣG}7wy]Мv6o^mcuG]Lɓ'8NL<<<,3֜>=sf{7ǟzXݶr^_k=|қ=/X]u|2LJ>!u߉G RQ%HSPt񝆨H%aPѣd<\JSy2Ala)`ĊvӁ}"a 3sf@k?e11! : 5س/\n̄T- 84`$F!R7qkOsULu`|3dGk3XQs?777ZMwk~Ͼwך繇<ޏ^kW[ss߯uʯ{ku|fFqW4H;9< _?afRx)&2x3~=lKˁmvK~ѩ;DRX $7 w~jq>/.ZaPp)H, .LnN *Xݥ$u\u\u_w_u|b7pA*A[4c4<"|iY֐qWP)S/ђZ1&S}7"asP }h';S~f2`.73&ܠF`Q9ąbo73&~In/ΐ !aƚ+븎~ku|u,+/!/-s08@`D*3(RDFBH3x3L [㵛#G G`͝'y9) lB;wM_ض 恈Wt@]15yF7UһJLTߝ&q=󽟴q3>k3;ѣkd:>3C] U/ 5 f$.4 'H7Z m 3Gtm ~ eR-.]JqM@fPi "Fo=N85d d0FPxDRz&YyRku|Fǫq:>Su\gl\lzsu>շ73c.M+J\I.Hg <&^T8@ 8 )qr/fѪD6GOPRq190HXrktqS"Y[ Kdh&NS"^S/@#[Tu\u\u\ud\w O HSB4 " D"Gtro: A(iXN 5Cu*q8ֈm"֜sКcBx&MUSQ"VqDm=4B}D@ׯtCW2 yZb#s`Ki42 0$NCHs~pzg̼͵n\ʢKK$GfIDrI 8H(_])q4>$*<,$fi*(&TGLrqFSTƭsP]?2#UMtM 7N]da & H9`:::tDpzW{HJbwRO` PϘ"ת&>]en[s$ cꋛ.8⌇aJa󐁌#@ȵ^`@'қ??qqqw1,^)*3eΔ_5_ xLZ&unGQɼjoWHVQL&I9%YXs2,T@%wp!c`FG(UE"']0_12rET9 -m[t븎븎븎Ͼ;ֶ%!f+h3988'! (51f0I $;O[{Є@9.6'2G X0'+瘒6[ 65PFY. J ’@P\L~;w/^u\u\u\gc`[l{жA1Z$UǤ9#};Z,W Te`r7jP9+cf)rN vvĮ-aov9>*gZ|ARO/ByZ% ~{鮣 -o\:::bd&?~ӗ| l;-NFi$fI`ݐ͑zVЊ}D~њM6y w8ܰm [g$= xw~;~ӟ{?|_aa~T븎븎븎x6 >Kos?ݢnwU43F@5a"(V@s44KL5 FO J"9_;&58pyFzSqJ\oMŸ34xkh@[o$Wt,QApz: wO1>{ &F{C@cZdPxP JfDE e{s(Zol$9'ۭ>FgͪSߞev?P4O2&3lR~M ^ED}Ί1$EHdvl-Ȭ*q6)ݜT9$IEܪ6 F%l> }I0Ȯ9[b{KkaXoqNC=rLÂn5~UYmvYMih'Zwym .iϬX p=98UB=1guko#gbFX[JqzQj]st-)|Bhvǩ;F3 f\6 Z`S130&sqXk,a9e{`f2\.ZwIS@&TgwCw\3GgRPmAFq?{,d˲Z3kJծow7VPL1Ovp>ϵN3cHH@Z,D_OUbPGm"Zhج3XY 2;t UλԾ7}qhTwe.WV`8s} 4%RE#.3#VPn@K ۣawrhHY$9,8ɡyDٳWDI'ʺ1ڑFz?X7Xk 2D]2M81L0xoLˋL1x؛/!eJ ~&f Bۆow/áZ vɶj#T9 VlBc7MfndJ~4Ӻ IGuf  dv+{f9K0z+b-ّf*B"TŸL1߁"&b22uݒǝ+"RGm )*PJq2iy1Y1c0(9>' \~K(ЈFuV2?w>< GԆ1[RgrykY/NWMdoө7]FqEȷ{8b,NX3C(\ӥQB a~ou5<*<1'}*Z9 @c^Ls00q Q)z8HpV]r`Opy]\8V)S_"bTF|<ڊ ?Љ,n[~fuW HXy Kl3<!c 3/*zq_N%8u;q@sv3%^~ׯߖ:CKMS6\.,^dHqn8Oq&sE@#S[4E 53uEԜ*r`&Եr?JӇ@",e^Qwl3X2 6sٹJm5fӜT.\C6:-isS }Oˮռ jy58RMSPXތ䋸38C|.Z:Sc΀)F 43S;x0[QA\tE2!Jy^HLa[S=\^\\vj}@*}gΐxvp+dJi 7; ,HUQ]e;~^sZˬA?_f ɠ;5ϵBWDF%M[s<=ȳ>Ѭh6̙Ϝ'yI0*.P(C2s.@.ɑbeZ3hL kc"\KhM/9QtkeB[kOz-8:/ЦrUvqzHfD)'-QyK1g ڿxBE hk}LwݿkT9զ?Ù̩H< `D N̳Aq:,U1f.ya>:\?‚r!CE}liC{aiK,{j& 8';)O ;'nNf#JGUԞK@롢ߙߎԭ^jhD*" s~ S *w"$W1 RDMS/ o@i ~HiCVa-&k8r@赢fH?l}ZOU1ZCOHNUoUL-(++uS2#eM^K=sujwfW:CrFF֙3 U3 g(c!YwlfJ'V$kEるxi&cCD}~W^ULGp'y 5@e':fbDr@sޭ Wb~"|_E!㗙hdc?~  p ys3ȩ{RH{nBXY`_ð,Dvϕ>q0[xe2 @7}3CS7lDQЮ.d~:ր9`ʺ*6_E6Oa~z/j+4MH°G/wDhxg0tZX$" @Q"]P^qˮ!˸6ᑂ8 L]X3rlCL`nn9!k޿m}8ΕӹO{[{x=qnsZ{B 0ykي~#)`j(t \DIPx\,i. bsIj#Vu_ J)u%A P6Ϊ#gB#" ct4@ynh OB>Q䋽5W|Y!GtgcV۱cXAR\dnU)S 0+̀N[!@Wk˺DxW ׎ I5y#=%Jzc4;4's'.#(0=7vw+3q#>B?mW] 櫚{peKH9gMsq<%5 B4iik-]FJ_4 +7ڪ]EKuϕT&w|)WOhL49ya(jQUVJ)[Q|XNhU&f`Ԧ@L~8{k2)`DJ ZS)]rŊ}-EϕޏdCW|ܙڛz7FLى&QH >)ZhE-`nB4trVr `2'Y7Y;!ɐ/3~Wn6^먭(Yg*PR%e`\H_R<ցdG!庆.WUcS a +PiƈLpݘB[8Fw|&b7g,T M5mC3_Q I73Xz_ L!MٔFƀcT!r1N~_i)tz` C@͡3U{ӆ º,JeAɒJY(Byvu0n%m1C$Jvؽ; ȺT`}~-Tȁe\F#QVX憭3m`uc1dT1TC#0 3߼ < nx׾LnàZ§Is<-.u4Ê"~Y8cbk$MР;0.c r4sؓg}MFnN { $07O)9C8Xi!yPCGӹ+eV5m\Ԍy,7[M"~F>u [u/) GbuGkJàLF- ,PSY9Xkihlh"[_HFgZ 3I_h24PɀLf ׄ'.bD$T,%D; JaŽ.,' +LJʈWQC0; hָ9ћ#R@!Ev'Gz9cg .{AdaC.0QC"}BI _͎ ed׏hַC%ªLggȾLzsNFs:pvtX x*43{hn8lEu ͭaW<#JB0x.q'O6F'_^骱O\۩/Cw/EѲ=RċHwF˶FO *f8mJ}s4܆O2#ĻB967e0&^OpdgE]9w K0W3^C06m&scjgMg9VkX4vmS6Sb^"VSpL]ϣÀ NE fhJI,Ɣ朹[ PF,[xLQ[m:;&{4f8#wߴ_`J~1̂V(E<]_fh*{?"dN e0цC=BzNy2]ܔXVnP^oji6T;LMF6cAg6{qpPu$0֜"kҦOU5/03:sֲ fPDȑC9Y\@N;vY:gZE#j62tUUƚ* 䪔11Je-~v0je*&ͱ.'<(?xy #ł6Xs64Idbȩ0$XqV*0{y*xZ0{(9CѪÃ:l.jc1Z!eTlb $i)#/vOa 3jf$\R 0^ztUy?' #&y17%K_X IDATaZ*HbܗCꋀiqwg`Zj$^7`KEbb"1COT+@QΦr T `E驑`,: <ӝIC;%2Zo 1!j"/>kUi)Yo􂈍Q^hx脈؞rF*ܞ g*inh[H!+bW>\2Ctw(0E[S]{m", 12n3 Uhp㎞'JyV =q3h_}c65qV)i3q1:" #c&;4rVY٭g `|U,t,Z\i 6g֌ `'&a30lhR$ژk mfNJc=/*& Zw~( #.WnUE)yL5-ǜ{452]1/mkqB,IlP$ @Mtt B$wZqZ\3YZ 1WbJJܘz[2rP zrx4˷V`@O>c;h&E|J]T5=df05_*zKV2[ <<ݝZ\ qoN5"Fc۔čmHg|UL9Q]`DLdAATJV6*fҦȄvvs` jmՁ58"20T wzlGރ[twjl hsM:(wV8zd>ZWP޺ERȀg"%ΉF}a86unn'"1WE UG8XX=r+sI3V He1 'CI~T2qmFVhw`Ŏ(ycœ2`L5M`1rG⢕GJn7ʳa , Jk*=gHOQN!p[ N7A}#xlZ@ESxF+3^#7j8B OSӃ瑨Xb KB/OE[sn:J;NkRԳC 7Ϲ^\$Μ| {Lݓ`C!_ERPWu 61H0 BJ-`cM8MַT24G6AP&ѽB&̫XLEq$xY!б 6yg /?ATĆ*W& pʊ1&.rB P N}AyF*ZZG3D?Uʞ]Hg[36X?|rN73@Û1 oXX! ]ˈIu,'Fi74,D Ev~E\F.f+,0RCXpȰ|<z]LerM8`@kU(B~Ym7fIX]KTލwѾNw}9)W͋וY=F=3pyvcp:,̔qw 1 oh!#u#c0ØxOuga[Z C#H…Iѡ':0ē 'Dg)zB\D*CQf*CͰvsE^ ÝXYӻ7#ٻDit{S 3$hU[ K*Y*p7GދùWJ1.E,+M ?B熡 \!D@v򮚪 9PgG|(*2s _Cu:x7n f3&Rc*܊2wu#B6(؀Z$YZQY \%' \@n9mgu,{*-+"'. 2`O5kZ]@qC_/хpSHzZ˅'\!2p88"ԊpQTs{lcK H)lwcw(t`y)qB0oTG-fx!/ޑސcy@ j`7LfM 0"m!3]aj 0ڃ(j*UQ:v<<H;}kY$2kW@pHC &+OyQN8m ,\'2<}F`>0a%X ]!;׭7(:Ms31v`?!;#`$Gy(2zֹ]bćJ'E*?="1w^X x, s Ů=3(NòCvς,+[H΁rک30] @W^嬭赮]*:!Zn,I*|*l{D=Cfe'ˮa8u?q貇4Rr֍{85Dklj;<K`.zgW=΁7Ovl6LU-z3㜡CUň<:KI6WY{&bkjY4h|ݶԆ%" /gLڅe H+Í<zxakc8_m-@Z*TiWS]k Nן!mεEZJ:C 6@-m\L7,Ҙׁ@N "ZHNűPQ:cCuno +I༯)ņv25#{/:Oz lEާC/B(rΉG`XpqOLMPޘuzR$8{߽j)%0e^:#՚`Rݵmϯ YW7\3׾Bф2%]Q ".zj Q<(M]J3P5~\d߫=Dğg ~o,$^MRy4TU~r+m-3;*Wt'rmYCp@~ęDKRɩarծ3yvϡ6f7ʘ:õn`ĩ3{C jLšH4h$[2)uKLy> %W@lx =ȸ!أh^ϳ!pk9g"oaElEjL$-K 9x)RJRV|RHI:/aP$6iֳr-s vCkT:urtaᕰ%I<%ꇇt9ҟ؝hF)6DᏑ `A&ú? ԧN%WghzE`"qtYQ89(T=5QPhՎ޻kNZ< aAGEypÔBnW֌:?"S{;g!sx=43¦${&5SkH\f^ܔ[1Q꾗J}mܫ(["$+O(&3v:GUN@~&p4ad W/|dn[TB䋄"6mTdP_ׅx`1[F3r@L,9ljyц I݁NGWb}G8i.橽z{U`VD, om%q) J[q(s6RycGF/l?ٰ6;-Ǎӯ^tm UV 1)N #~E%=WlG~FF:hF"L#uOzt0pX̀ZC,yt2Pעt;];^sdeS ;8yN*56i ? 6L 65b] E [ϱrtY9u5;UgE[,)^@m|pJr@j4o/B4 ׋Lȵ2̩~ꋝxK @j&9=Y^ysr&K!+E"R9PsU:i@5g*S#*"Q?H 62q6](vp(*Rzl .PhMTc >n|#mZtK&1,Y dyd_:ށ4<ak%YD#eIߡgRzh௢o6;PsJ1".@1 ke$N Ac8/ v g<ႌWͣJ({b=9>?qv͔mV,9o\bTG棣.\$\5z^}BFsc&RqHL:wTp-'͸7 _ѱyФʈ^ UKAvoR[ 5>Du !2B-]i]U.=l1{%VB!箛u7$](NKSRhX{KR1#`0FAkԇr4鿃+@\SKAsJ!xm4V_ﵾ!* &Y0)< Af,AŁ/et:Fu1ΣBҤvUie`29g @1EEXw)u:#LMmh g3^ O C"^% elm m.9;?7 GPe07@ݻk:ڬihPd5ZН]4ɇƢ-#f#{Wihlhp35-wem]_+ծ‚֬EE6:uOm# + whu_x7?" zojZcY`pA"p&Ӯm"OUZ-xM.:IN(]튄g\sS[pѪ瀌-£&* DhSv{z[z,|ʲGđ(zE}_j<90@1``j8 TǧTӣT@]; \+"XHK77E=B!0nwJ޿êQ\q G؜"D_1 Aׅ7!:`mpBXÈ]4Lș xto>d/)KBN$p8LnqJIN, nV&}\}@]q#f2w*;7DNґо?Rf6>EL.ON낇~r8ACDQT9'Q=TiSt" ;[t}_ |3b%#=P|VxxxR64$C"20au3vEgz-}sB8].-$%S0n6v_Vh|uKi7jCBFv lOE`ip'DE%?|~K"pŁ3Z"?Teb4U~ H@S<@H|[S=@uysʽey=Vm\0hE{4(` ptO6v,eVeKi(G( +/~*Ʃ;sȯgVjD")_vlzhA }iqNږ~jOr@}#IshD`~/ԽS*tAN }|x.)1;hoK54CIENDB`././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8610291 jeepney-0.9.0/docs/api/asyncio.rst0000644000000000000000000000115014760131573014011 0ustar00Asyncio integration =================== This supports D-Bus in applications built with `asyncio `_. See :ref:`connections_and_routers` for more about the two interfaces. .. module:: jeepney.io.asyncio .. autofunction:: open_dbus_router .. autoclass:: DBusRouter .. automethod:: send .. automethod:: send_and_get_reply .. automethod:: filter .. autoclass:: Proxy .. seealso:: :ref:`msggen_proxies` .. autofunction:: open_dbus_connection .. autoclass:: DBusConnection .. automethod:: send .. automethod:: receive .. automethod:: close ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8610291 jeepney-0.9.0/docs/api/auth.rst0000644000000000000000000000321114760131573013305 0ustar00Authentication ============== .. note:: If you use any of Jeepney's I/O integrations, authentication is built in. You only need these functions if you're working outside that. If you are setting up a socket for D-Bus, you will need to do `SASL `_ authentication before starting to send and receive D-Bus messages. This text based protocol is completely different to D-Bus itself. Only a small fraction of SASL is implemented here, primarily what Jeepney's integration layer uses. If you're doing something different, you may need to implement other messages yourself. .. module:: jeepney.auth .. autofunction:: make_auth_external .. autofunction:: make_auth_anonymous .. data:: BEGIN Send this just before switching to the D-Bus protocol. .. autoclass:: Authenticator .. versionchanged:: 0.7 This class was renamed from ``SASLParser`` and substantially changed. .. attribute:: authenticated Initially False, changes to True when authentication has succeeded. .. attribute:: error ``None``, or the raw bytes of an error message if authentication failed. .. automethod:: data_to_send .. automethod:: feed .. autoexception:: AuthenticationError .. autoexception:: FDNegotiationError Typical flow ------------ 1. Send the data from :meth:`Authenticator.data_to_send` (or ``for req_data in authenticator``). 2. Receive data from the server, pass to :meth:`Authenticator.feed`. 3. Repeat 1 & 2 until :attr:`Authenticator.authenticated` is True, or the for loop exits. 4. Send :data:`BEGIN`. 5. Start sending & receiving D-Bus messages. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8610291 jeepney-0.9.0/docs/api/blocking.rst0000644000000000000000000000134614760131573014143 0ustar00Blocking I/O ============ This is a good option for simple scripts, where you don't need to do anything else while waiting for a D-Bus reply. If you will use D-Bus for multiple threads, or you want a nicer way to wait for signals, see :doc:`threading`. .. module:: jeepney.io.blocking .. autofunction:: open_dbus_connection .. autoclass:: DBusConnection .. automethod:: send .. automethod:: receive .. automethod:: send_and_get_reply .. automethod:: recv_messages .. automethod:: filter .. automethod:: recv_until_filtered .. automethod:: close Using ``with open_dbus_connection()`` will also close the connection on exiting the block. .. autoclass:: Proxy .. seealso:: :ref:`msggen_proxies` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8610291 jeepney-0.9.0/docs/api/common_msgs.rst0000644000000000000000000000067114760131573014674 0ustar00Common messages =============== .. currentmodule:: jeepney These classes are *message generators*. Wrap them in a :ref:`Proxy ` class to actually send the messages as well. .. autoclass:: Properties :members: .. autoclass:: Introspectable :members: .. autoclass:: DBus :members: :undoc-members: There is a ready-made instance of this at ``jeepney.message_bus``. .. autoclass:: Monitoring :members: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8610291 jeepney-0.9.0/docs/api/core.rst0000644000000000000000000000755314760131573013311 0ustar00Core API ======== .. module:: jeepney Message constructors -------------------- .. autofunction:: new_method_call .. autofunction:: new_method_return .. autofunction:: new_error .. autofunction:: new_signal .. autoclass:: DBusAddress .. autoclass:: MessageGenerator .. seealso:: :doc:`/bindgen` Parsing ------- .. autoclass:: Parser .. automethod:: add_data .. automethod:: get_next_message Message objects --------------- .. autoclass:: Message .. attribute:: header A :class:`Header` object .. attribute:: body A tuple of the data in this message. The number and types of the elements depend on the message's signature: =========== ========== =========== D-Bus type D-Bus code Python type =========== ========== =========== BYTE ``y`` int BOOLEAN ``b`` bool INT16 ``n`` int UINT16 ``q`` int INT32 ``i`` int UINT32 ``u`` int INT64 ``x`` int UINT64 ``t`` int DOUBLE ``d`` float STRING ``s`` str OBJECT_PATH ``o`` str SIGNATURE ``g`` str ARRAY ``a`` list STRUCT ``()`` tuple VARIANT ``v`` 2-tuple ``(signature, value)`` DICT_ENTRY ``{}`` dict (for array of dict entries) UNIX_FD ``h`` See :ref:`send_recv_fds` =========== ========== =========== .. automethod:: serialise .. autoclass:: Header .. attribute:: endianness :class:`Endianness` object, affecting message serialisation. .. attribute:: message_type :class:`MessageType` object. .. attribute:: flags :class:`MessageFlag` object. .. attribute:: protocol_version Currently always 1. .. attribute:: body_length The length of the raw message body in bytes. .. attribute:: serial Sender's serial number for this message. This is not necessarily set for outgoing messages - see :meth:`Message.serialise`. .. attribute:: fields Mapping of :class:`HeaderFields` values to the relevant Python objects. Exceptions ---------- .. autoexception:: SizeLimitError .. autoexception:: DBusErrorResponse .. attribute:: name The error name from the remote end. .. attribute:: body Any data fields contained in the error message. Enums & Flags ------------- .. class:: Endianness .. autoattribute:: little .. autoattribute:: big .. class:: HeaderFields .. autoattribute:: path .. autoattribute:: interface .. autoattribute:: member .. autoattribute:: error_name .. autoattribute:: reply_serial .. autoattribute:: destination .. autoattribute:: sender .. autoattribute:: signature .. autoattribute:: unix_fds .. class:: MessageFlag .. autoattribute:: no_reply_expected On a method call message, indicates that a reply should not be sent. .. autoattribute:: no_auto_start D-Bus includes a mechanism to start a service on demand to handle messages. If this flag is set, it will avoid that, only handling the message if the target is already running. .. autoattribute:: allow_interactive_authorization Signals that the recipient may prompt the user for elevated privileges to handle the request. The D-Bus specification has more details. .. class:: MessageType .. autoattribute:: method_call .. autoattribute:: method_return .. autoattribute:: error .. autoattribute:: signal Matching messages ----------------- .. autoclass:: MatchRule MatchRule objects are used both for filtering messages internally, and for setting up subscriptions in the message bus. .. automethod:: add_arg_condition .. automethod:: matches .. automethod:: serialise ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8610291 jeepney-0.9.0/docs/api/fds.rst0000644000000000000000000000155014760131573013124 0ustar00File descriptor support ======================= .. currentmodule:: jeepney .. autoclass:: FileDescriptor .. automethod:: to_file .. note:: If the descriptor does not refer to a regular file, or it doesn't have the right access mode, you may get strange behaviour or errors while using it. You can use :func:`os.stat` and the :mod:`stat` module to check the type of object the descriptor refers to, and :func:`fcntl.fcntl` to check the access mode, e.g.:: stat.S_ISREG(os.stat(fd.fileno()).st_mode) # Regular file? status_flags = fcntl.fcntl(fd, fcntl.F_GETFL) (status_flags & os.O_ACCMODE) == os.O_RDONLY # Read-only? .. automethod:: to_socket .. automethod:: to_raw_fd .. automethod:: fileno .. automethod:: close .. autoexception:: NoFDError ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8610291 jeepney-0.9.0/docs/api/index.rst0000644000000000000000000000061614760131573013461 0ustar00API reference ============= .. toctree:: :maxdepth: 2 core common_msgs auth fds .. toctree:: :maxdepth: 2 :caption: I/O integrations blocking threading trio asyncio io_exceptions There is also a deprecated ``jeepney.io.tornado`` integration. Recent versions of Tornado are built on asyncio, so you can use the asyncio integration with Tornado applications. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.862029 jeepney-0.9.0/docs/api/io_exceptions.rst0000644000000000000000000000013614760131573015217 0ustar00I/O Exceptions ============== .. currentmodule:: jeepney.io .. autoexception:: RouterClosed ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.862029 jeepney-0.9.0/docs/api/threading.rst0000644000000000000000000000135014760131573014313 0ustar00Blocking I/O with threads ========================= This allows using a D-Bus connection from multiple threads. The router also launches a separate thread to receive incoming messages. See :ref:`connections_and_routers` for more about the two interfaces. .. module:: jeepney.io.threading .. autofunction:: open_dbus_router .. autoclass:: DBusRouter .. automethod:: send .. automethod:: send_and_get_reply .. automethod:: filter .. automethod:: close Leaving the ``with`` block will also close the router. .. autoclass:: Proxy .. seealso:: :ref:`msggen_proxies` .. autofunction:: open_dbus_connection .. autoclass:: DBusConnection .. automethod:: send .. automethod:: receive .. automethod:: close ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.862029 jeepney-0.9.0/docs/api/trio.rst0000644000000000000000000000131714760131573013326 0ustar00Trio integration ================ This supports D-Bus in applications built with `Trio `_. See :ref:`connections_and_routers` for more about the two interfaces. .. module:: jeepney.io.trio .. autofunction:: open_dbus_router .. autoclass:: DBusRouter .. automethod:: send .. automethod:: send_and_get_reply .. automethod:: filter .. automethod:: aclose Leaving the ``async with`` block will also close the router. .. autoclass:: Proxy .. seealso:: :ref:`msggen_proxies` .. autofunction:: open_dbus_connection .. autoclass:: DBusConnection .. automethod:: send .. automethod:: receive .. automethod:: router .. automethod:: aclose ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.862029 jeepney-0.9.0/docs/bindgen.rst0000644000000000000000000000327114760131573013207 0ustar00Generating D-Bus wrappers ========================= D-Bus includes a mechanism to introspect remote objects and discover the methods they define. Jeepney can use this to generate classes defining the messages to send. Use it like this:: python3 -m jeepney.bindgen --name org.freedesktop.Notifications \ --path /org/freedesktop/Notifications This command will produce the class in the example under :ref:`msggen_proxies`. You specify *name*—which D-Bus service you're talking to—and *path*—an object in that service. Jeepney will generate a wrapper for each interface that object has, except for some standard ones like the introspection interface itself. You are welcome to edit the generated code, e.g. to add docstrings or give parameters meaningful names. Names like ``arg_1`` are created when introspection doesn't provide a name. Bindgen command options ----------------------- .. program:: python -m jeepney.bindgen .. option:: -n , --name Bus name to introspect, required unless using :option:`--file`. .. option:: -p , --path Object path to introspect, required unless using :option:`--file`. Bindings will be generated for all interfaces this object exposes, except for common interfaces like 'Introspectable'. .. option:: --bus Bus to connect to, SESSION (default) or SYSTEM. .. option:: -f , --file An XML file to use as input instead of connecting to D-Bus and using introspection. The options above are ignored if this is used. .. option:: -o , --output Write the output (Python code) to the specified file. By default, a filename is chosen based on the input. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.862029 jeepney-0.9.0/docs/conf.py0000644000000000000000000002370714760131573012354 0ustar00#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Jeepney documentation build configuration file, created by # sphinx-quickstart on Mon May 29 14:26:37 2017. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys sys.path.insert(0, os.path.abspath('..')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx', 'sphinx.ext.extlinks', ] extlinks = { 'issue': ('https://gitlab.com/takluyver/jeepney/-/issues/%s', "issue #%s"), 'mr': ('https://gitlab.com/takluyver/jeepney/-/merge_requests/%s', "MR !%s"), } # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. # # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'Jeepney' copyright = '2017, Thomas Kluyver' author = 'Thomas Kluyver' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. import jeepney version = jeepney.__version__ # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # # today = '' # # Else, today_fmt is used as the format for a strftime call. # # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The reST default role (used for this markup: `text`) to use for all # documents. # # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), } # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. # " v documentation" by default. # # html_title = 'Jeepney v0.3' # A shorter title for the navigation bar. Default is the same as html_title. # # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # # html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # # html_extra_path = [] # If not None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. # # html_last_updated_fmt = None # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # # html_additional_pages = {} # If false, no module index is generated. # # html_domain_indices = True # If false, no index is generated. # # html_use_index = True # If true, the index is split into individual pages for each letter. # # html_split_index = False # If true, links to the reST sources are added to the pages. # # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' # # html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # 'ja' uses this config value. # 'zh' user can custom change `jieba` dictionary path. # # html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. # # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'Jeepneydoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'Jeepney.tex', 'Jeepney Documentation', 'Thomas Kluyver', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # # latex_use_parts = False # If true, show page references after internal links. # # latex_show_pagerefs = False # If true, show URL addresses after external links. # # latex_show_urls = False # Documents to append as an appendix to all manuals. # # latex_appendices = [] # It false, will not define \strong, \code, itleref, \crossref ... but only # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added # packages. # # latex_keep_old_macro_names = True # If false, no module index is generated. # # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'jeepney', 'Jeepney Documentation', [author], 1) ] # If true, show URL addresses after external links. # # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'Jeepney', 'Jeepney Documentation', author, 'Jeepney', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # # texinfo_appendices = [] # If false, no module index is generated. # # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # # texinfo_no_detailmenu = False ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.862029 jeepney-0.9.0/docs/dbus-background.rst0000644000000000000000000001234114760131573014651 0ustar00What is D-Bus? ============== D-Bus is a system for programs on the same computer to communicate. It's used primarily on Linux, to interact with various parts of the operating system. For example, take desktop notifications - the alerts that appear to tell you about things like new chat messages. .. figure:: _static/desktop-notification.png A desktop notification on GNOME A program that wants to display a notification sends a D-Bus message to the 'notification server', which displays an alert for a brief time and then hides it again. Different desktops, like GNOME and KDE, have different notification servers, but they handle the same messages (defined in the `desktop notification spec `_), so programs don't need to do different things for different desktops. Other things that use D-Bus include: - Retrieving passwords from the desktop's 'keyring' - Disabling the screensaver while playing a film - Special keyboard keys, like pause & skip track, working with whichever media player you use. - Opening a user's files in a sandboxed (`Flatpak `_) application. Methods & signals ----------------- D-Bus uses two types of messaging: **Method calls** go to a specific destination, which replies with either a 'method return' or an error message. In the notifications example above, the program sends a method call message to the notification server to ask it to display a notification. **Signals** are sent out to any program that subscribes to them. For example, when a desktop notification is closed, the notification server sends a signal. The application might use this to choose between updating the notification ('**2** new messages') or sending a new one. There's no reply to a signal, and the sender doesn't know if anything received it or not. Names ----- There are a lot of names in D-Bus, and they can look quite similar. For instance, displaying a desktop notification involves sending a message to the bus name ``org.freedesktop.Notifications``, for the object ``/org/freedesktop/Notifications``, with the interface ``org.freedesktop.Notifications``. What do those all mean? - The bus name (``.`` separated) is which program you're talking to. - The object name (``/`` separated) is which thing inside that program you want to use, e.g. which password in the keyring. - The interface name (``.`` separated) is which set of methods and signals you are using. Most objects have one main interface plus a few standard ones for things like introspection (finding what methods are available). Finally, a simple name like ``Notify`` or ``NotificationClosed`` identifies which method is being called, or which signal is being sent, from a list for that interface. The bus, object and interface names are all based on reversed domain names. The people who control https://freedesktop.org/ can define names starting with ``org.freedesktop.`` (or ``/org/freedesktop/`` for objects). There's no way to enforce this, but so long as everyone sticks to it, we don't have to worry about the same name being used for different things. Message buses ------------- Applications using D-Bus connect to a *message bus*, a small program which is always running. The bus takes care of delivering messages to other applications. There are normally two buses you need to know about. Each logged-in user has their own **session bus**, handling things like desktop notifications (and the other examples above). The **system bus** is shared for all users. In particular, requests sent via the system bus can do things that would otherwise require admin (sudo) access, like unmounting a USB stick or installing new packages. (How the system decides whether to allow these actions or not is a separate topic - look up 'polkit' if you want to know about that). You can also talk to the message bus itself (using D-Bus messages, of course). This is how you subscribe to signals, or claim a bus name so other programs can send you method calls. The message bus has the name ``org.freedesktop.DBus``. .. note:: Programs *can* agree some other way to connect and send each other D-Bus messages without a message bus. This isn't very common, though. Special features ---------------- You can send a D-Bus message to a program that's not even running, and the message bus will start it and then deliver the message. This feature (*activation*) means that programs don't have to stay running just to reply to D-Bus method calls. A config file installed with the application defines its bus name and how to launch it. Because D-Bus is designed to be used between programs on the same computer, it can do things that are impossible over the network. D-Bus messages can include 'file descriptors', handles for things like open files, pipes and sockets. This can be used to selectively give a program access to something that would normally be off limits. See :ref:`send_recv_fds` for how to use this from Jeepney. .. seealso:: `Introduction to D-Bus (freedesktop.org) `_ `Introduction to D-Bus (KDE) `_ `D-Bus overview (txdbus) `_ ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.862029 jeepney-0.9.0/docs/design.rst0000644000000000000000000000730414760131573013053 0ustar00Design & Limitations ==================== There are two parts to Jeepney: The **core** is all about creating D-Bus messages, serialising them to bytes, and deserialising bytes into :class:`.Message` objects. It aims to be a complete & reliable implementation of the D-Bus wire protocol. It follows the idea of `"Sans-I/O" `_, implementing the D-Bus protocol independent of any means of sending or receiving the data. The second part is **I/O integration**. This supports the typical use case for D-Bus - connecting to a message bus on a Unix socket - with various I/O frameworks. There is one integration module for each framework, and they provide similar interfaces (:ref:`connections_and_routers`), but differ as much as necessary to fit in with the different frameworks - e.g. the Trio integration uses channels where the asyncio integration uses queues. Jeepney also allows for a similar split in code using it. If you want to wrap the desktop notifications service, for instance, you can write (or generate) a :ref:`message generator ` class for it. The same message generator class can then be wrapped in a *proxy* for any of Jeepney's I/O integrations. Non-goals --------- Jeepney does not (currently) aim for: - Very high performance. Parsing binary messages in pure Python code is not the fastest way to do it, but for many use cases of D-Bus it's more than fast enough. - Supporting all possible D-Bus transports. The I/O integration layer only works with Unix sockets, the most common way to use D-Bus. If you need to use another transport, you can still use :meth:`.Message.serialise` and :class:`.Parser`, and deal with sending & receiving data yourself. - Supporting all authentication options. The :doc:`auth module ` only provides what the I/O integration layer uses. - High-level server APIs. Jeepney's API for D-Bus servers is on a low-level, sending and receiving messages, not registering handler methods. See `dbus-objects `_ for a server API built on top of Jeepney. - 'Magic' introspection. Some D-Bus libraries use introspection at runtime to discover available methods, but Jeepney does not. Instead, it uses introspection during development to write message generators (:doc:`bindgen`). Alternatives ------------ * GTK applications can use `Gio.DBusConnection `_ or a higher-level wrapper like `dasbus `_ or `pydbus `_. There are also GObject wrappers for specific D-Bus services, e.g. `secret storage `__ and `desktop notifications `__. * PyQt applications can use the `Qt D-Bus module `_. This has been available `in PyQt `_ for many years, and `in PySide `_ from version 6.2 (released in 2021). * `DBussy `_ works with asyncio. It is a Python binding to the libdbus reference implementation in C, whereas Jeepney reimplements the D-Bus protocol in Python. * `dbus-python `_ is the original Python binding to libdbus. It is very complete and well tested, but may be trickier to install and to integrate with event loops and async frameworks. .. seealso:: `D-Bus Python bindings on the Freedesktop wiki `_ ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.862029 jeepney-0.9.0/docs/index.rst0000644000000000000000000000253114760131573012706 0ustar00Jeepney |version| ================= Jeepney is a pure Python interface to D-Bus, a protocol for interprocess communication on desktop Linux (mostly). See :doc:`dbus-background` for more background on what it can do. The core of Jeepney is `I/O free `__, and the ``jeepney.io`` package contains bindings for different event loops to handle I/O. Jeepney tries to be *non-magical*, so you may have to write a bit more code than with other interfaces such as `dbus-python `_ or `pydbus `_. Jeepney doesn't rely on libdbus or other compiled libraries, so it's easy to install with Python tools like ``pip``:: pip install jeepney For most use cases, the D-Bus daemon needs to be running on your computer; this is a standard part of most modern Linux desktops. Contents: .. toctree:: :maxdepth: 2 integrate messages bindgen api/index design dbus-background release-notes .. seealso:: `D-Feet `__ App for exploring available D-Bus services on your machine. `D-Bus Specification `__ Technical details about the D-Bus protocol. Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.862029 jeepney-0.9.0/docs/integrate.rst0000644000000000000000000001022314760131573013556 0ustar00Connecting to DBus and sending messages ======================================= Jeepney can be used with several different frameworks: - Blocking I/O - Multi-threading with the `threading `_ module - `Trio `_ - `asyncio `_ For each of these, there is a module in ``jeepney.io`` providing the integration layer. Here's an example of sending a desktop notification, using blocking I/O: .. literalinclude:: /../examples/blocking_notify.py And here is the same thing using asyncio: .. literalinclude:: /../examples/aio_notify_noproxy.py See the `examples folder `_ in Jeepney's source repository for more examples. .. _connections_and_routers: Connections and Routers ----------------------- Each integration (except blocking I/O) can create *connections* and *routers*. **Routers** are useful for calling methods in other processes. Routers let you send a request and wait for a reply, using a :ref:`proxy ` or with ``router.send_and_get_reply()``. You can also filter incoming messages into queues, e.g. to wait for a specific signal. But if messages arrive faster than they are processed, these queues fill up, and messages may be dropped. **Connections** are simpler: they let you send and receive messages, but ``conn.receive()`` will give you the next message read, whatever that is. You'd use this to write a server which responds to incoming messages. A connection will never discard an incoming message. .. note:: For blocking, single-threaded I/O, the connection doubles as a router. Incoming messages while you're waiting for a reply will be filtered, and you can also filter the next message by calling ``conn.recv_messages()``. Routers for the other integrations receive messages in a background task. .. _msggen_proxies: Message generators and proxies ------------------------------ If you're calling a number of different methods, you can make a *message generator* class containing their definitions. Jeepney includes a tool to generate these classes automatically—see :doc:`bindgen`. Message generators define how to construct messages. *Proxies* are wrappers around message generators which send a message and get the reply back. Let's rewrite the example above to use a message generator and a proxy: .. literalinclude:: /../examples/aio_notify.py This is more code for the simple use case here, but in a larger application collecting the message definitions together like this could make it clearer. .. _send_recv_fds: Sending & receiving file descriptors ------------------------------------ .. versionadded:: 0.7 D-Bus allows sending file descriptors - references to open files, sockets, etc. To use this, use the blocking, multi-threading or Trio integration and enable it (``enable_fds=True``) when connecting to D-Bus. If you enable FD support but the message bus can't or won't support it, :exc:`.FDNegotiationError` will be raised. To send a file descriptor, pass any object with a ``.fileno()`` method, such as an open file or socket, or a suitable integer. The file descriptor must not be closed before the message is sent. A received file descriptor will be returned as a :class:`.FileDescriptor` object to help avoid leaking FDs. This can easily be converted to a file object (:meth:`~.FileDescriptor.to_file`), a socket (:meth:`~.FileDescriptor.to_socket`) or a plain integer (:meth:`~.FileDescriptor.to_raw_fd`). .. code-block:: python # Send a file descriptor for a temp file (normally not visible in any folder) with TemporaryFile() as tf: msg = new_method_call(server, 'write_data', 'h', (tf,)) await router.send_and_get_reply(msg) # Receive a file descriptor, use it as a writable file msg = await conn.receive() fd, = msg.body with fd.to_file('w') as f: f.write(f'Timestamp: {datetime.now()}') The snippets above are based on the Trio integration. See the `examples directory `__ in the Jeepney repository for complete, working examples. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.862029 jeepney-0.9.0/docs/messages.rst0000644000000000000000000000457014760131573013413 0ustar00Making and parsing messages =========================== The core of Jeepney is code to build, serialise and deserialise D-Bus messages. .. currentmodule:: jeepney Making messages --------------- D-Bus has four message types. Three, *method call*, *method return* and *error*, are used in a request-reply pattern. The fourth, *signal*, is a broadcast message with no reply. - *Method call* messages are most conveniently made with a message generator class, which can be :doc:`autogenerated `. One layer down from this is :func:`new_method_call`, which takes a :class:`DBusAddress` object. - *Method return* and *error* messages are made with :func:`new_method_return` and :func:`new_error`, passing the method call message which they are replying to. - *signal* messages are made with :func:`new_signal`, which takes a :class:`DBusAddress` representing the sender. All of these return a :class:`Message` object. :meth:`Message.serialise` converts it to bytes, but none of these core methods ever send a message. See the :doc:`integration layer ` for that. Signatures ~~~~~~~~~~ D-Bus is strongly typed, and every message has a *signature* describing the body data. These are strings using characters such as ``i`` for a signed 32-bit integer. See the `DBus specification `_ for the full list. Jeepney does not try to guess or discover the signature when you build a message: your code must explicitly specify a signature for every message. However, Jeepney can help you write this code: see :doc:`bindgen`. D-Bus types are converted to and from native Python objects as follows: * All the D-Bus integer types are represented as Python :class:`int`, including *BYTE* when it's not in an array. * *BOOLEAN* is :class:`bool`. * *DOUBLE* is :class:`float`. * *STRING*, *OBJECT_PATH* and *SIGNATURE* are all :class:`str`. * *ARRAY* is :class:`list`, except that an array of *BYTE* is a :class:`bytes` object, and an array of *DICT_ENTRY* is a :class:`dict`. * *STRUCT* is :class:`tuple`. * *VARIANT* is a 2-tuple ``(signature, data)``. E.g. to put a string into a variant field, you would pass the data ``("s", "my string")``. * *UNIX_FD* are converted from objects with a ``.fileno()`` method or plain integers, and converted to :class:`.FileDescriptor` objects. See :ref:`send_recv_fds` for more details. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.862029 jeepney-0.9.0/docs/release-notes.rst0000644000000000000000000001205714760131573014351 0ustar00Release notes ============= 0.9 --- 2025-02-27 * Fixed subscribing to messages on the message bus with a ``path_namespace`` parameter (:mr:`38`) * Fixed authentication on (some?) BSDs, using SCM_CREDS (:mr:`33`), for all integrations except for asyncio (which does not expose ``sendmsg``). * :class:`~.DBusAddress` and message generators will now raise :exc:`ValueError` if given invalid D-Bus names - bus names, object paths, or interface names (:mr:`36`). Previously these could easily be sent in messages, resulting in the bus closing the connection. * Bindings can now be :doc:`generated ` from D-Bus XML in a file with the new :option:`--file` option (:mr:`34`). * The ``async_timeout`` package is no longer required for running the tests on Python 3.11 or above (:mr:`39`). Breaking changes ~~~~~~~~~~~~~~~~ * Removed the deprecated ``connection.router`` API in the blocking IO integration (:mr:`40`). * Removed the deprecated ``unwrap`` parameter from ``send_and_get_reply`` in the blocking integration (:mr:`37`). 0.8 --- 2022-04-03 * Removed ``jeepney.integrate`` APIs, which were deprecated in 0.7. Use ``jeepney.io`` instead (see :doc:`integrate`). * Removed deprecated ``jeepney.io.tornado`` API. Tornado now uses the asyncio event loop, so you can use it along with ``jeepney.io.asyncio``. * Deprecated ``conn.router`` attribute in the :doc:`api/blocking` integration. Use :ref:`proxies ` or :meth:`~.blocking.DBusConnection.send_and_get_reply` to find replies to method calls, and :meth:`~.blocking.DBusConnection.filter` for other routing. * Added docs page with background on D-Bus (:doc:`dbus-background`). 0.7.1 ----- 2021-07-28 * Add ``async with`` support to :class:`~.asyncio.DBusConnection` in the asyncio integration. * Fix calling :meth:`~.asyncio.DBusConnection.receive` immediately after opening a connection in the asyncio integration. Thanks to Aleksandr Mezin for these changes. 0.7 --- 2021-07-21 * Support for :ref:`sending and receiving file descriptors `. This is available with the blocking, threading and trio integration layers. * Deprecated older integration APIs, in favour of new APIs introduced in 0.5. * Fixed passing a deque in to :meth:`~.blocking.DBusConnection.filter` in the blocking integration API. 0.6 --- 2020-11-19 * New method :meth:`~.blocking.DBusConnection.recv_until_filtered` in the blocking I/O integration to receive messages until one is filtered into a queue. * More efficient buffering of received data waiting to be parsed into D-Bus messages. 0.5 --- 2020-11-10 * New common scheme for I/O integration - see :ref:`connections_and_routers`. * This is designed for tasks to wait for messages and then act on them, rather than triggering callbacks. This is based on ideas from 'structured concurrency', which also informs the design of Trio. See `this blog post by Nathaniel Smith `_ for more background. * There are new integrations for :doc:`Trio ` and :doc:`threading `. * The old integration interfaces should still work for now, but they will be deprecated and eventually removed. * :meth:`.Message.serialise` accepts a serial number, to serialise outgoing messages without modifying the message object. * Improved documentation, including :doc:`API docs `. 0.4.3 ----- 2020-03-04 * The blocking integration now throws ``ConnectionResetError`` on all systems when the connection was closed from the other end. It would previously hang on some systems. 0.4.2 ----- 2020-01-03 * The blocking ``DBusConnection`` integration class now has a ``.close()`` method, and can be used as a context manager:: from jeepney.integrate.blocking import connect_and_authenticate with connect_and_authenticate() as connection: ... 0.4.1 ----- 2019-08-11 * Avoid using :class:`asyncio.Future` for the blocking integration. * Set the 'destination' field on method return and error messages to the 'sender' from the parent message. Thanks to Oscar Caballero and Thomas Grainger for contributing to this release. 0.4 --- 2018-09-24 * Authentication failures now raise a new :exc:`AuthenticationError` subclass of :exc:`ValueError`, so that they can be caught specifically. * Fixed logic error when authentication is rejected. * Use *effective* user ID for authentication instead of *real* user ID. In typical use cases these are the same, but where they differ, effective uid seems to be the relevant one. * The 64 MiB size limit for an array is now checked when serialising it. * New function :func:`jeepney.auth.make_auth_anonymous` to prepare an anonymous authentication message. This is not used by the wrappers in Jeepney at the moment, but may be useful for third party code in some situations. * New examples for subscribing to D-Bus signals, with blocking I/O and with asyncio. * Various improvements to documentation. Thanks to Jane Soko and Gitlab user xiretza for contributing to this release. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.862029 jeepney-0.9.0/docs/requirements.txt0000644000000000000000000000001514760131573014324 0ustar00trio outcome ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8630292 jeepney-0.9.0/examples/aio_hello.py0000644000000000000000000000032014760131573014232 0ustar00import asyncio from jeepney.io.asyncio import open_dbus_connection async def hello(): conn = await open_dbus_connection(bus='SESSION') print('My ID is:', conn.unique_name) asyncio.run(hello()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8630292 jeepney-0.9.0/examples/aio_notify.py0000644000000000000000000000354614760131573014454 0ustar00"""Send a desktop notification See also aio_notify_noproxy.py, which does the same with lower-level APIs """ import asyncio from jeepney import MessageGenerator, new_method_call from jeepney.io.asyncio import open_dbus_router, Proxy # ---- Message generator, created by jeepney.bindgen ---- class Notifications(MessageGenerator): interface = 'org.freedesktop.Notifications' def __init__(self, object_path='/org/freedesktop/Notifications', bus_name='org.freedesktop.Notifications'): super().__init__(object_path=object_path, bus_name=bus_name) def Notify(self, arg_0, arg_1, arg_2, arg_3, arg_4, arg_5, arg_6, arg_7): return new_method_call(self, 'Notify', 'susssasa{sv}i', (arg_0, arg_1, arg_2, arg_3, arg_4, arg_5, arg_6, arg_7)) def CloseNotification(self, arg_0): return new_method_call(self, 'CloseNotification', 'u', (arg_0,)) def GetCapabilities(self): return new_method_call(self, 'GetCapabilities') def GetServerInformation(self): return new_method_call(self, 'GetServerInformation') # ---- End auto generated code ---- async def send_notification(): async with open_dbus_router() as router: proxy = Proxy(Notifications(), router) resp = await proxy.Notify('jeepney_test', # App name 0, # Not replacing any previous notification '', # Icon 'Hello, world!', # Summary 'This is an example notification from Jeepney', [], {}, # Actions, hints -1, # expire_timeout (-1 = default) ) print('Notification ID:', resp[0]) if __name__ == '__main__': asyncio.run(send_notification()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8630292 jeepney-0.9.0/examples/aio_notify_noproxy.py0000644000000000000000000000236214760131573016245 0ustar00"""Send a desktop notification See also aio_notify.py, which does the same with the higher-level Proxy API. """ import asyncio from jeepney import DBusAddress, new_method_call from jeepney.io.asyncio import open_dbus_router notifications = DBusAddress('/org/freedesktop/Notifications', bus_name='org.freedesktop.Notifications', interface='org.freedesktop.Notifications') async def send_notification(): msg = new_method_call(notifications, 'Notify', 'susssasa{sv}i', ('jeepney_test', # App name 0, # Not replacing any previous notification '', # Icon 'Hello, world!', # Summary 'This is an example notification from Jeepney', [], {}, # Actions, hints -1, # expire_timeout (-1 = default) )) # Send the message and await the reply async with open_dbus_router() as router: reply = await router.send_and_get_reply(msg) print('Notification ID:', reply.body[0]) loop = asyncio.get_event_loop() loop.run_until_complete(send_notification()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8630292 jeepney-0.9.0/examples/aio_secretstorage.py0000644000000000000000000000223214760131573016005 0ustar00"""Example accessing the SecretStorage DBus interface with asyncio APIs https://freedesktop.org/wiki/Specifications/secret-storage-spec/secrets-api-0.1.html#ref-dbus-api """ import asyncio from jeepney import new_method_call, DBusAddress, Properties from jeepney.io.asyncio import open_dbus_router secrets = DBusAddress('/org/freedesktop/secrets', bus_name= 'org.freedesktop.secrets', interface='org.freedesktop.Secret.Service') login_keyring = DBusAddress('/org/freedesktop/secrets/collection/login', bus_name= 'org.freedesktop.secrets', interface='org.freedesktop.Secret.Collection') search_msg = new_method_call( login_keyring, 'SearchItems', 'a{ss}', ({'user': 'tk2e15',},) ) async def query_secrets(): async with open_dbus_router() as router: get_collections_msg = Properties(secrets).get('Collections') resp = await router.send_and_get_reply(get_collections_msg) print('Collections:', resp.body[0][1]) resp = await router.send_and_get_reply(search_msg) print('Search res:', resp.body) asyncio.run(query_secrets()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8630292 jeepney-0.9.0/examples/aio_subscribe.py0000644000000000000000000000416314760131573015121 0ustar00""" This example simulates two clients interacting with the message bus, more or less independently. Client 1 Any app on the bus (here, called *service*). It asks the bus for sole custody of its preferred name, a `well-known bus name`_ that it wants others to recognize. Client 2 An interested party (here, called *watcher*). It asks the bus to emit a signal (send it an update) whenever *service*'s well-known bus name changes hands. It uses Jeepney's ``router.filter()`` to get a message whenever this happens. .. _well-known bus name: https://dbus.freedesktop.org/doc /dbus-specification.html#message-protocol-names """ import asyncio from jeepney.io.asyncio import open_dbus_router, Proxy from jeepney.bus_messages import message_bus, MatchRule well_known_bus_name = "io.readthedocs.jeepney.aio_subscribe_example" async def watcher(ready: asyncio.Event): # Create a "signal-selection" match rule match_rule = MatchRule( type="signal", sender=message_bus.bus_name, interface=message_bus.interface, member="NameOwnerChanged", path=message_bus.object_path, ) # Condition: arg number 0 must match the bus name (try changing either) match_rule.add_arg_condition(0, well_known_bus_name) async with open_dbus_router() as router: await Proxy(message_bus, router).AddMatch(match_rule) with router.filter(match_rule) as q: print("[watcher] subscribed to NameOwnerChanged signal") ready.set() msg = await q.get() print("[watcher] match hit:", msg.body) async def main(): watcher_ready = asyncio.Event() watcher_task = asyncio.create_task(watcher(watcher_ready)) await watcher_ready.wait() async with open_dbus_router() as router: print("[service] calling 'RequestName'") resp = await Proxy(message_bus, router).RequestName(well_known_bus_name, 4) print("[service] reply:", (None, "primary owner", "in queue", "exists", "already owned")[resp[0]]) await watcher_task if __name__ == "__main__": asyncio.run(main()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8630292 jeepney-0.9.0/examples/blocking_choose_file.py0000644000000000000000000000250114760131573016431 0ustar00from jeepney import DBusAddress, new_method_call from jeepney.bus_messages import message_bus, MatchRule from jeepney.io.blocking import open_dbus_connection, Proxy portal = DBusAddress( object_path='/org/freedesktop/portal/desktop', bus_name='org.freedesktop.portal.Desktop', ) filechooser = portal.with_interface('org.freedesktop.portal.FileChooser') conn = open_dbus_connection() token = 'file_choice_1' sender_name = conn.unique_name[1:].replace('.', '_') handle = f"/org/freedesktop/portal/desktop/request/{sender_name}/{token}" response_rule = MatchRule( type='signal', interface='org.freedesktop.portal.Request', path=handle ) Proxy(message_bus, conn).AddMatch(response_rule) with conn.filter(response_rule) as responses: # https://flatpak.github.io/xdg-desktop-portal/portal-docs.html#gdbus-method-org-freedesktop-portal-FileChooser.OpenFile req = new_method_call(filechooser, 'OpenFile', 'ssa{sv}', ( # Parent window, title, options '', 'Pick a file', {'handle_token': ('s', token)} )) conn.send_and_get_reply(req) # responses is a deque - process messages until one is filtered into it response_msg = conn.recv_until_filtered(responses) response, results = response_msg.body if response == 0: # print(results) print("Chose file:", results['uris'][1][0]) conn.close() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8630292 jeepney-0.9.0/examples/blocking_notify.py0000644000000000000000000000213714760131573015467 0ustar00from jeepney import DBusAddress, new_method_call from jeepney.io.blocking import open_dbus_connection notifications = DBusAddress('/org/freedesktop/Notifications', bus_name='org.freedesktop.Notifications', interface='org.freedesktop.Notifications') connection = open_dbus_connection(bus='SESSION') # Construct a new D-Bus message. new_method_call takes the address, the # method name, the signature string, and a tuple of arguments. msg = new_method_call(notifications, 'Notify', 'susssasa{sv}i', ('jeepney_test', # App name 0, # Not replacing any previous notification '', # Icon 'Hello, world!', # Summary 'This is an example notification from Jeepney', [], {}, # Actions, hints -1, # expire_timeout (-1 = default) )) # Send the message and wait for the reply reply = connection.send_and_get_reply(msg) print('Notification ID:', reply.body[0]) connection.close() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8630292 jeepney-0.9.0/examples/blocking_pick_colour.py0000644000000000000000000000437214760131573016473 0ustar00"""Pick a colour on your screen, show its details in the terminal This probably only works on Gnome desktops, as it uses a Gnome D-Bus interface. This is just an example, so broad support is less important than simplicity. """ from jeepney.io.blocking import open_dbus_connection, Proxy from jeepney.wrappers import MessageGenerator, new_method_call # This class was auto-generated with: # python3 -m jeepney.bindgen --name org.gnome.Shell.Screenshot --path /org/gnome/Shell/Screenshot class Screenshot(MessageGenerator): interface = 'org.gnome.Shell.Screenshot' def __init__(self, object_path='/org/gnome/Shell/Screenshot', bus_name='org.gnome.Shell.Screenshot'): super().__init__(object_path=object_path, bus_name=bus_name) def Screenshot(self, include_cursor, flash, filename): return new_method_call(self, 'Screenshot', 'bbs', (include_cursor, flash, filename)) def ScreenshotWindow(self, include_frame, include_cursor, flash, filename): return new_method_call(self, 'ScreenshotWindow', 'bbbs', (include_frame, include_cursor, flash, filename)) def ScreenshotArea(self, x, y, width, height, flash, filename): return new_method_call(self, 'ScreenshotArea', 'iiiibs', (x, y, width, height, flash, filename)) def PickColor(self): return new_method_call(self, 'PickColor') def FlashArea(self, x, y, width, height): return new_method_call(self, 'FlashArea', 'iiii', (x, y, width, height)) def SelectArea(self): return new_method_call(self, 'SelectArea') with open_dbus_connection() as conn: screenshot = Proxy(Screenshot(), conn) res_dict, = screenshot.PickColor() # We get RGB as floats (0-1), and convert to integers (0-255) rf, gf, bf = res_dict['color'][1] ri, gi, bi = round(rf * 255), round(gf * 255), round(bf * 255) print(f"RGB (0-1) : ({rf:.5f}, {gf:.5f}, {bf:.5f})") print(f"RGB (0-255): {ri, gi, bi}") print(f"Hex code : #{ri:02X}{gi:02X}{bi:02X}") print(f"Preview : \x1b[48;2;{ri};{gi};{bi}m \x1b[0m") # The line above assumes our terminal supports 24-bit colour: # https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8630292 jeepney-0.9.0/examples/blocking_recv_fd.py0000644000000000000000000000301214760131573015560 0ustar00"""Demonstrate receiving a file descriptor Start this, and then run one of the _send_fd.py scripts to send requests. """ from datetime import datetime from jeepney import MessageType, HeaderFields, new_method_return, new_error from jeepney.bus_messages import message_bus from jeepney.io.blocking import open_dbus_connection SERVER_NAME = "io.gitlab.takluyver.jeepney.examples.FDWriter" with open_dbus_connection(enable_fds=True) as connection: print("My unique name is:", connection.unique_name) # Request an additional name on the message bus rep = connection.send_and_get_reply(message_bus.RequestName(SERVER_NAME)) if rep.body[0] == 1: # DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER print("Got name", SERVER_NAME) while True: msg = connection.receive() if msg.header.message_type != MessageType.method_call: print("Received non-method-call message:", msg) method = msg.header.fields[HeaderFields.member] print(f"Message {msg.header.serial} calls {method}") if method == 'write_data': # body contains a FileDescriptor object, which we can convert to a file: fd, = msg.body with fd.to_file('w') as f: f.write(f'Timestamp: {datetime.now()}') # Leaving the with block will close the fd in this process rep = new_method_return(msg, '') # Empty reply to say we're done else: rep = new_error(msg, SERVER_NAME + '.Error.NoMethod') connection.send_message(rep) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8630292 jeepney-0.9.0/examples/blocking_secretstorage.py0000644000000000000000000000257414760131573017036 0ustar00"""Example accessing the SecretStorage DBus interface with blocking APIs https://freedesktop.org/wiki/Specifications/secret-storage-spec/secrets-api-0.1.html#ref-dbus-api """ from jeepney import new_method_call, DBusAddress, Properties from jeepney.io.blocking import open_dbus_connection secrets = DBusAddress('/org/freedesktop/secrets', bus_name= 'org.freedesktop.secrets', interface='org.freedesktop.Secret.Service') login_keyring = DBusAddress('/org/freedesktop/secrets/collection/login', bus_name= 'org.freedesktop.secrets', interface='org.freedesktop.Secret.Collection') list_items = new_method_call(login_keyring, 'SearchItems', 'a{ss}', ([],)) conn = open_dbus_connection(bus='SESSION') resp = conn.send_and_get_reply(Properties(secrets).get('Collections')) print('Collections:', resp.body[0][1]) print('\nItems in login collection:') all_items = conn.send_and_get_reply(list_items).body[0] for obj_path in all_items: item = DBusAddress(obj_path, 'org.freedesktop.secrets', interface='org.freedesktop.Secret.Item') props_resp = conn.send_and_get_reply(Properties(item).get_all()) props = dict(props_resp.body[0]) state = '(locked)' if props['Locked'][1] else '' print(props['Label'][1], state) conn.close() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8640292 jeepney-0.9.0/examples/blocking_send_fd.py0000644000000000000000000000215014760131573015554 0ustar00"""Demonstrate sending a file descriptor Start one of the _recv_fd.py scripts first, then run this. Make a writable temporary file, send its file descriptor. The other process writes into it before replying. This process can read what was written. This works even though the temp file may never have a name on the filesystem for the other process to open it. """ from tempfile import TemporaryFile from jeepney import DBusAddress, new_method_call from jeepney.io.blocking import open_dbus_connection server = DBusAddress( "/io/gitlab/takluyver/jeepney/examples/FDWriter", bus_name="io.gitlab.takluyver.jeepney.examples.FDWriter", ) with open_dbus_connection(enable_fds=True) as connection: with TemporaryFile() as tf: # TemporaryFile() has a .fileno() method, so it can be passed as the # data for a file descriptor (h for handle). msg = new_method_call(server, 'write_data', 'h', (tf,)) print("Sending:", tf) reply = connection.send_and_get_reply(msg) # Retrieve what the other process has written tf.seek(0) print("File contents:", tf.read()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8640292 jeepney-0.9.0/examples/blocking_server.py0000644000000000000000000000356314760131573015471 0ustar00"""A simple server built on Jeepney's blocking integration. Start this, and then run blocking_server_client.py to send requests to it. """ import time from jeepney import MessageType, HeaderFields, new_method_return, new_error from jeepney.bus_messages import message_bus from jeepney.io.blocking import open_dbus_connection SERVER_NAME = "io.gitlab.takluyver.jeepney.examples.Server" def slow_double(n): time.sleep(5) return n * 2 def double(n): return n * 2 def divide(n, d): return n / d with open_dbus_connection() as connection: print("My unique name is:", connection.unique_name) # Request an additional name on the message bus rep = connection.send_and_get_reply(message_bus.RequestName(SERVER_NAME)) if rep.body[0] == 1: # DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER print("Got name", SERVER_NAME) while True: msg = connection.receive() if msg.header.message_type != MessageType.method_call: print("Received non-method-call message:", msg) method = msg.header.fields[HeaderFields.member] print(f"Message {msg.header.serial} calls {method}") # Dispatch to different methods if method == 'slow_double': res = slow_double(msg.body[0]) print(f"Delayed reply to {msg.header.serial}: {res}") rep = new_method_return(msg, 'i', (res,)) elif method == 'double': res = double(msg.body[0]) rep = new_method_return(msg, 'i', (res,)) elif method == 'divide': try: res = divide(*msg.body) except ZeroDivisionError: rep = new_error(msg, SERVER_NAME + ".Error.DivideByZero") else: rep = new_method_return(msg, 'd', (res,)) else: rep = new_error(msg, SERVER_NAME + '.Error.NoMethod') connection.send_message(rep) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8640292 jeepney-0.9.0/examples/blocking_server_client.py0000644000000000000000000000150214760131573017016 0ustar00"""Simple client for the server in blocking_server.py """ import random from time import sleep from jeepney import DBusAddress, new_method_call from jeepney.io.blocking import open_dbus_connection server = DBusAddress( "/io/gitlab/takluyver/jeepney/examples/Server", bus_name="io.gitlab.takluyver.jeepney.examples.Server", ) with open_dbus_connection() as connection: for i in range(10): n = random.randint(0, 5) # Construct a new D-Bus message. new_method_call takes the address, the # method name, the signature string, and a tuple of arguments. msg = new_method_call(server, 'double', 'i', (n,)) print("Request:", n) # Send the message and wait for the reply reply = connection.send_and_get_reply(msg) print('Result:', reply.body[0]) sleep(1) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8640292 jeepney-0.9.0/examples/blocking_subscribe.py0000644000000000000000000000341514760131573016140 0ustar00""" Example of subscribing to a D-Bus signal using blocking I/O. This subscribes to the signal for a desktop notification being closed. To try it, start this script, then trigger a desktop notification, and close it somehow to trigger the signal. Use Ctrl-C to stop the script. This example relies on the ``org.freedesktop.Notifications.NotificationClosed`` signal; some desktops may not support it. See the notification spec for more details: https://people.gnome.org/~mccann/docs/notification-spec/notification-spec-latest.html Match rules are defined in the D-Bus specification: https://dbus.freedesktop.org/doc/dbus-specification.html#message-bus-routing-match-rules """ from collections import deque from jeepney.bus_messages import MatchRule, message_bus from jeepney.io.blocking import open_dbus_connection, Proxy from jeepney.wrappers import DBusAddress noti = DBusAddress('/org/freedesktop/Notifications', bus_name='org.freedesktop.Notifications', interface='org.freedesktop.Notifications') connection = open_dbus_connection(bus="SESSION") match_rule = MatchRule( type="signal", interface=noti.interface, member="NotificationClosed", path=noti.object_path, ) # Tell the session bus to pass us matching signal messages: bus_proxy = Proxy(message_bus, connection) print("Match added?", bus_proxy.AddMatch(match_rule) == ()) print("Trigger a desktop notification (e.g. with notify-send) and then close it") with connection.filter(match_rule) as queue: signal_msg = connection.recv_until_filtered(queue) reasons = {1: 'expiry', 2: 'dismissal', 3: 'dbus', '4': 'undefined'} nid, reason_no = signal_msg.body reason = reasons.get(reason_no, 'unknown') print('Notification {} closed by: {}'.format(nid, reason)) connection.close() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8640292 jeepney-0.9.0/examples/evil_client.py0000644000000000000000000000250014760131573014576 0ustar00"""Do not copy this example! This is a deliberately broken example to test how D-Bus behaves when messages are sent faster than they are handled. See other files in this folder for useful examples of sending D-Bus messages nicely. """ import threading import time from jeepney import DBusAddress, new_method_call from jeepney.io.blocking import open_dbus_connection server = DBusAddress( "/io/gitlab/takluyver/jeepney/examples/Server", bus_name="io.gitlab.takluyver.jeepney.examples.Server", ) connection = open_dbus_connection(bus='SESSION') sending = True def sender(): i = 0 while sending: i += 1 n = i #random.randint(0, 5) # Construct a new D-Bus message. new_method_call takes the address, the # method name, the signature string, and a tuple of arguments. msg = new_method_call(server, 'slow_double', 'is', (n, 'a' * 1024 * 300)) print(f"Request {i}: {n}") connection.send(msg) time.sleep(0.1) send_thread = threading.Thread(target=sender) send_thread.start() def receiver(): while True: msg = connection.receive() print('Result:', msg.body[0]) # recv_thread = threading.Thread(target=receiver) # recv_thread.start() try: receiver() except KeyboardInterrupt: pass sending = False send_thread.join() connection.close() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8640292 jeepney-0.9.0/examples/threading_recv_fd.py0000644000000000000000000000372714760131573015752 0ustar00"""Demonstrate receiving a file descriptor Start this, and then run one of the _send_fd.py scripts to send requests. """ from datetime import datetime import signal from threading import Thread from jeepney import MessageType, HeaderFields, new_method_return, new_error from jeepney.bus_messages import message_bus from jeepney.io.threading import ( open_dbus_connection, DBusRouter, Proxy, ReceiveStopped, ) SERVER_NAME = "io.gitlab.takluyver.jeepney.examples.FDWriter" def serve(conn, i): while True: try: msg = conn.receive() except ReceiveStopped: return if msg.header.message_type != MessageType.method_call: print("Received non-method-call message:", msg) method = msg.header.fields[HeaderFields.member] print(f"Thread {i}: Message {msg.header.serial} calls {method}") if method == 'write_data': # body contains a FileDescriptor object, which we can convert to a file: fd, = msg.body with fd.to_file('w') as f: f.write(f'Timestamp: {datetime.now()}, server thread {i}') # Leaving the with block will close the fd in this process rep = new_method_return(msg, '') # Empty reply to say we're done else: rep = new_error(msg, SERVER_NAME + '.Error.NoMethod') conn.send(rep) with open_dbus_connection(enable_fds=True) as conn: # Request an additional name on the message bus with DBusRouter(conn) as router: bus_proxy = Proxy(message_bus, router, timeout=10) if bus_proxy.RequestName(SERVER_NAME) == (1,): # 1 == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER print("Got name", SERVER_NAME) threads = [Thread(target=serve, args=(conn, i)) for i in range(4)] for t in threads: t.start() try: signal.pause() # Wait for Ctrl-C except KeyboardInterrupt: pass conn.interrupt() for t in threads: t.join() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8640292 jeepney-0.9.0/examples/threading_send_fd.py0000644000000000000000000000275414760131573015743 0ustar00"""Demonstrate sending a file descriptor Start one of the _recv_fd.py scripts first, then run this. Make a writable temporary file, send its file descriptor. The other process writes into it before replying. This process can read what was written. This works even though the temp file may never have a name on the filesystem for the other process to open it. """ from tempfile import TemporaryFile from threading import Thread import time from jeepney import DBusAddress, new_method_call from jeepney.io.threading import open_dbus_router server = DBusAddress( "/io/gitlab/takluyver/jeepney/examples/FDWriter", bus_name="io.gitlab.takluyver.jeepney.examples.FDWriter", ) def requests(router, i): for _ in range(4): with TemporaryFile() as tf: # Construct a new D-Bus message. new_method_call takes the address, the # method name, the signature string, and a tuple of arguments. msg = new_method_call(server, 'write_data', 'h', (tf,)) print(f"Client thread {i} sending", tf) # Send the message and wait for the reply router.send_and_get_reply(msg) # Retrieve what the other process has written tf.seek(0) print(f"Client thread {i} reads:", tf.read()) time.sleep(0.5) with open_dbus_router(enable_fds=True) as router: threads = [Thread(target=requests, args=(router, i)) for i in range(5)] for t in threads: t.start() for t in threads: t.join() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8640292 jeepney-0.9.0/examples/threading_server.py0000644000000000000000000000465714760131573015653 0ustar00"""A simple server built on Jeepney's blocking integration. Start this, and then run blocking_server_client.py to send requests to it. """ import signal from threading import Thread import time from jeepney import MessageType, HeaderFields, new_method_return, new_error from jeepney.bus_messages import message_bus from jeepney.io.threading import ( open_dbus_connection, DBusRouter, Proxy, ReceiveStopped, ) SERVER_NAME = "io.gitlab.takluyver.jeepney.examples.Server" def slow_double(n): # Simulate something slow which releases the GIL so other threads can run time.sleep(5) return n * 2 def double(n): return n * 2 def divide(n, d): return n / d def serve(conn, i): while True: try: msg = conn.receive() except ReceiveStopped: return if msg.header.message_type != MessageType.method_call: print("Received non-method-call message:", msg) method = msg.header.fields[HeaderFields.member] print(f"Thread {i}: Message {msg.header.serial} calls {method}") # Dispatch to different methods if method == 'slow_double': res = slow_double(msg.body[0]) print(f"Delayed reply to {msg.header.serial}: {res}") rep = new_method_return(msg, 'i', (res,)) elif method == 'double': res = double(msg.body[0]) rep = new_method_return(msg, 'i', (res,)) elif method == 'divide': try: res = divide(*msg.body) except ZeroDivisionError: rep = new_error(msg, SERVER_NAME + ".Error.DivideByZero") else: rep = new_method_return(msg, 'd', (res,)) else: rep = new_error(msg, SERVER_NAME + '.Error.NoMethod') conn.send(rep) with open_dbus_connection() as conn: print("My unique name is:", conn.unique_name) # Request an additional name on the message bus with DBusRouter(conn) as router: bus_proxy = Proxy(message_bus, router, timeout=10) if bus_proxy.RequestName(SERVER_NAME) == (1,): # 1 == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER print("Got name", SERVER_NAME) threads = [Thread(target=serve, args=(conn, i)) for i in range(4)] for t in threads: t.start() try: signal.pause() # Wait for Ctrl-C except KeyboardInterrupt: pass conn.interrupt() for t in threads: t.join() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8640292 jeepney-0.9.0/examples/threading_server_client.py0000644000000000000000000000205214760131573017174 0ustar00"""Make overlapping requests from several threads. Use this alongside one of the _server.py examples """ import random from threading import Thread from jeepney import DBusAddress, new_method_call from jeepney.io.threading import open_dbus_router server = DBusAddress( "/io/gitlab/takluyver/jeepney/examples/Server", bus_name="io.gitlab.takluyver.jeepney.examples.Server", ) def requests(router, i): for _ in range(5): n = random.randint(0, 5) # Construct a new D-Bus message. new_method_call takes the address, the # method name, the signature string, and a tuple of arguments. msg = new_method_call(server, 'slow_double', 'i', (n,)) print(f"Thread {i} request:", n) # Send the message and wait for the reply reply = router.send_and_get_reply(msg) print(f'Thread {i} result:', reply.body[0]) with open_dbus_router() as router: threads = [Thread(target=requests, args=(router, i)) for i in range(6)] for t in threads: t.start() for t in threads: t.join() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8640292 jeepney-0.9.0/examples/trio_choose_file.py0000644000000000000000000000276714760131573015634 0ustar00import trio from jeepney import DBusAddress, new_method_call from jeepney.bus_messages import message_bus, MatchRule from jeepney.io.trio import open_dbus_router, Proxy portal = DBusAddress( object_path='/org/freedesktop/portal/desktop', bus_name='org.freedesktop.portal.Desktop', ) filechooser = portal.with_interface('org.freedesktop.portal.FileChooser') token = 'file_choice_1' async def choose_file(): async with open_dbus_router() as router: sender_name = router.unique_name[1:].replace('.', '_') handle = f"/org/freedesktop/portal/desktop/request/{sender_name}/{token}" response_rule = MatchRule( type='signal', interface='org.freedesktop.portal.Request', path=handle ) await Proxy(message_bus, router).AddMatch(response_rule) async with router.filter(response_rule) as responses: # https://flatpak.github.io/xdg-desktop-portal/portal-docs.html#gdbus-method-org-freedesktop-portal-FileChooser.OpenFile req = new_method_call(filechooser, 'OpenFile', 'ssa{sv}', ( # Parent window, title, options '', 'Pick a file', {'handle_token': ('s', token)} )) await router.send_and_get_reply(req) response_signal_msg = await responses.receive() response, results = response_signal_msg.body if response == 0: # print(results) print("Chose file:", results['uris'][1][0]) if __name__ == '__main__': trio.run(choose_file) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8640292 jeepney-0.9.0/examples/trio_notify.py0000644000000000000000000000345514760131573014660 0ustar00import sys sys.excepthook = sys.__excepthook__ import trio from jeepney import MessageGenerator, new_method_call from jeepney.io.trio import open_dbus_router, Proxy # ---- Message generator, created by jeepney.bindgen ---- class Notifications(MessageGenerator): interface = 'org.freedesktop.Notifications' def __init__(self, object_path='/org/freedesktop/Notifications', bus_name='org.freedesktop.Notifications'): super().__init__(object_path=object_path, bus_name=bus_name) def Notify(self, arg_0, arg_1, arg_2, arg_3, arg_4, arg_5, arg_6, arg_7): return new_method_call(self, 'Notify', 'susssasa{sv}i', (arg_0, arg_1, arg_2, arg_3, arg_4, arg_5, arg_6, arg_7)) def CloseNotification(self, arg_0): return new_method_call(self, 'CloseNotification', 'u', (arg_0,)) def GetCapabilities(self): return new_method_call(self, 'GetCapabilities') def GetServerInformation(self): return new_method_call(self, 'GetServerInformation') # ---- End auto generated code ---- async def send_notification(): async with open_dbus_router(bus='SESSION') as req: proxy = Proxy(Notifications(), req) resp = await proxy.Notify('jeepney_test', # App name 0, # Not replacing any previous notification '', # Icon 'Hello, world!', # Summary 'This is an example notification from Jeepney & Trio', [], {}, # Actions, hints -1, # expire_timeout (-1 = default) ) print('Notification ID:', resp[0]) if __name__ == '__main__': trio.run(send_notification) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8640292 jeepney-0.9.0/examples/trio_recv_fd.py0000644000000000000000000000351414760131573014754 0ustar00"""Demonstrate receiving a file descriptor Start this, and then run one of the _send_fd.py scripts to send requests. """ from datetime import datetime import trio from jeepney import MessageType, HeaderFields, new_method_return, new_error from jeepney.bus_messages import message_bus from jeepney.io.trio import ( open_dbus_connection, Proxy, ) SERVER_NAME = "io.gitlab.takluyver.jeepney.examples.FDWriter" async def serve(conn, i): while True: msg = await conn.receive() if msg.header.message_type != MessageType.method_call: print("Received non-method-call message:", msg) continue method = msg.header.fields[HeaderFields.member] print(f"Task {i}: Message {msg.header.serial} calls {method}") if method == 'write_data': # body contains a FileDescriptor object, which we can convert to a file: fd, = msg.body with fd.to_file('w') as f: f.write(f'Timestamp: {datetime.now()}, server task {i}') # Leaving the with block will close the fd in this process rep = new_method_return(msg, '') # Empty reply to say we're done else: rep = new_error(msg, SERVER_NAME + '.Error.NoMethod') await conn.send(rep) async def main(): conn = await open_dbus_connection(enable_fds=True) # Request an additional name on the message bus async with conn.router() as router: bus_proxy = Proxy(message_bus, router) with trio.fail_after(2): reply, = await bus_proxy.RequestName(SERVER_NAME) if reply == 1: # 1 == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER print("Got name", SERVER_NAME) async with trio.open_nursery() as nursery: for i in range(4): nursery.start_soon(serve, conn, i) trio.run(main) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8640292 jeepney-0.9.0/examples/trio_send_fd.py0000644000000000000000000000276314760131573014753 0ustar00"""Demonstrate sending a file descriptor Start one of the _recv_fd.py scripts first, then run this. Make a writable temporary file, send its file descriptor. The other process writes into it before replying. This process can read what was written. This works even though the temp file may never have a name on the filesystem for the other process to open it. """ from tempfile import TemporaryFile import trio from jeepney import DBusAddress, new_method_call from jeepney.io.trio import open_dbus_router server = DBusAddress( "/io/gitlab/takluyver/jeepney/examples/FDWriter", bus_name="io.gitlab.takluyver.jeepney.examples.FDWriter", ) async def requests(router, i): for _ in range(4): with TemporaryFile() as tf: # Construct a new D-Bus message. new_method_call takes the address, the # method name, the signature string, and a tuple of arguments. msg = new_method_call(server, 'write_data', 'h', (tf,)) print(f"Client task {i} sending", tf) # Send the message and wait for the reply await router.send_and_get_reply(msg) # Retrieve what the other process has written tf.seek(0) print(f"Client task {i} reads:", tf.read()) await trio.sleep(0.5) async def main(): async with open_dbus_router(enable_fds=True) as router: async with trio.open_nursery() as nursery: for i in range(5): nursery.start_soon(requests, router, i) trio.run(main) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.865029 jeepney-0.9.0/jeepney/__init__.py0000644000000000000000000000063014760131573013663 0ustar00"""Low-level, pure Python DBus protocol wrapper. """ from .auth import AuthenticationError, FDNegotiationError from .low_level import ( Endianness, Header, HeaderFields, Message, MessageFlag, MessageType, Parser, SizeLimitError, ) from .bus import find_session_bus, find_system_bus from .bus_messages import * from .fds import FileDescriptor, NoFDError from .wrappers import * __version__ = '0.9.0' ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.865029 jeepney-0.9.0/jeepney/auth.py0000644000000000000000000001244114760131573013070 0ustar00from binascii import hexlify from enum import Enum import os from typing import Optional def make_auth_external() -> bytes: """Prepare an AUTH command line with the current effective user ID. This is the preferred authentication method for typical D-Bus connections over a Unix domain socket. """ hex_uid = hexlify(str(os.geteuid()).encode('ascii')) return b'AUTH EXTERNAL %b\r\n' % hex_uid def make_auth_anonymous() -> bytes: """Format an AUTH command line for the ANONYMOUS mechanism Jeepney's higher-level wrappers don't currently use this mechanism, but third-party code may choose to. See for details. """ from . import __version__ trace = hexlify(('Jeepney %s' % __version__).encode('ascii')) return b'AUTH ANONYMOUS %s\r\n' % trace BEGIN = b'BEGIN\r\n' NEGOTIATE_UNIX_FD = b'NEGOTIATE_UNIX_FD\r\n' class ClientState(Enum): # States from the D-Bus spec (plus 'Success'). Not all used in Jeepney. WaitingForData = 1 WaitingForOk = 2 WaitingForReject = 3 WaitingForAgreeUnixFD = 4 Success = 5 class AuthenticationError(ValueError): """Raised when DBus authentication fails""" def __init__(self, data, msg="Authentication failed"): self.msg = msg self.data = data def __str__(self): return f"{self.msg}. Bus sent: {self.data!r}" class FDNegotiationError(AuthenticationError): """Raised when file descriptor support is requested but not available""" def __init__(self, data): super().__init__(data, msg="File descriptor support not available") class Authenticator: """Process data for the SASL authentication conversation If enable_fds is True, this includes negotiating support for passing file descriptors. If inc_null_byte is True, sends the '\0' byte at the beginning of the negotiations, which was the past behavior, but which prevents sending the SCM_CREDS ancillary data over the socket, breaking authentication on *BSD; the caller should rather send that null byte and ancillary data and pass inc_null_byte=False to prevent it being done here. """ def __init__(self, enable_fds=False, inc_null_byte=True): self.enable_fds = enable_fds self.buffer = bytearray() if inc_null_byte: self._to_send = b'\0' + make_auth_external() else: self._to_send = make_auth_external() self.state = ClientState.WaitingForOk self.error = None @property def authenticated(self): return self.state is ClientState.Success def __iter__(self): return iter(self.data_to_send, None) def data_to_send(self) -> Optional[bytes]: """Get a line of data to send to the server The data returned should be sent before waiting to receive data. Returns empty bytes if waiting for more data from the server, and None if authentication is finished (success or error). Iterating over the Authenticator object will also yield these lines; :meth:`feed` should be called with received data inside the loop. """ if self.authenticated or self.error: return None self._to_send, to_send = b'', self._to_send return to_send def process_line(self, line): if self.state is ClientState.WaitingForOk: if line.startswith(b'OK '): if self.enable_fds: return NEGOTIATE_UNIX_FD, ClientState.WaitingForAgreeUnixFD else: return BEGIN, ClientState.Success # We only support EXTERNAL authentication, but if we allow others, # 'REJECTED ' would tell us to try another one. elif self.state is ClientState.WaitingForAgreeUnixFD: if line.startswith(b'AGREE_UNIX_FD'): return BEGIN, ClientState.Success # The protocol allows us to continue if FD passing is rejected, # but Jeepney assumes that if you enable FD support you need it, # so we fail rather self.error = line raise FDNegotiationError(line) self.error = line raise AuthenticationError(line) def feed(self, data: bytes): """Process received data Raises AuthenticationError if the incoming data is not as expected for successful authentication. The connection should then be abandoned. """ self.buffer += data if b'\r\n' in self.buffer: line, self.buffer = self.buffer.split(b'\r\n', 1) if self.buffer: # We only expect one line before we reply raise AuthenticationError(self.buffer, "Unexpected data received") self._to_send, self.state = self.process_line(line) # Avoid consuming lots of memory if the server is not sending what we # expect. There doesn't appear to be a specified maximum line length, # but 8192 bytes leaves a sizeable margin over all the examples in the # spec (all < 100 bytes per line). elif len(self.buffer) > 8192: raise AuthenticationError( self.buffer, "Too much data received without line ending" ) # Old name (behaviour on errors has changed, but should work for standard case) SASLParser = Authenticator ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.865029 jeepney-0.9.0/jeepney/bindgen.py0000644000000000000000000001370514760131573013541 0ustar00"""Generate a wrapper class from DBus introspection data""" import argparse import os.path import sys import xml.etree.ElementTree as ET from textwrap import indent from jeepney.wrappers import Introspectable from jeepney.io.blocking import open_dbus_connection, Proxy from jeepney import __version__ class Method: def __init__(self, xml_node): self.name = xml_node.attrib['name'] self.in_args = [] self.signature = [] for arg in xml_node.findall("arg[@direction='in']"): try: name = arg.attrib['name'] except KeyError: name = 'arg{}'.format(len(self.in_args)) self.in_args.append(name) self.signature.append(arg.attrib['type']) def _make_code_noargs(self): return ("def {name}(self):\n" " return new_method_call(self, '{name}')\n").format( name=self.name) def make_code(self): if not self.in_args: return self._make_code_noargs() args = ', '.join(self.in_args) signature = ''.join(self.signature) tuple = ('({},)' if len(self.in_args) == 1 else '({})').format(args) return ("def {name}(self, {args}):\n" " return new_method_call(self, '{name}', '{signature}',\n" " {tuple})\n").format( name=self.name, args=args, signature=signature, tuple=tuple ) INTERFACE_CLASS_TEMPLATE = """ class {cls_name}(MessageGenerator): interface = {interface!r} def __init__(self, object_path{path_default}, bus_name{name_default}): super().__init__(object_path=object_path, bus_name=bus_name) """ class Interface: def __init__(self, xml_node, path, bus_name): self.name = xml_node.attrib['name'] self.path = path self.bus_name = bus_name self.methods = [Method(node) for node in xml_node.findall('method')] def make_code(self): cls_name = self.name.split('.')[-1] chunks = [INTERFACE_CLASS_TEMPLATE.format( cls_name=cls_name, interface=self.name, path_default='' if self.path is None else f'={self.path!r}', name_default='' if self.bus_name is None else f'={self.bus_name!r}' )] for method in self.methods: chunks.append(indent(method.make_code(), ' ' * 4)) return '\n'.join(chunks) MODULE_TEMPLATE = '''\ """Auto-generated DBus bindings Generated by jeepney version {version} Object path: {path} Bus name : {bus_name} """ from jeepney.wrappers import MessageGenerator, new_method_call ''' # Jeepney already includes bindings for these common interfaces IGNORE_INTERFACES = { 'org.freedesktop.DBus.Introspectable', 'org.freedesktop.DBus.Properties', 'org.freedesktop.DBus.Peer', } def code_from_xml(xml, path, bus_name, fh): if isinstance(fh, (bytes, str)): with open(fh, 'w') as f: return code_from_xml(xml, path, bus_name, f) root = ET.fromstring(xml) fh.write(MODULE_TEMPLATE.format(version=__version__, path=path, bus_name=bus_name)) i = 0 for interface_node in root.findall('interface'): if interface_node.attrib['name'] in IGNORE_INTERFACES: continue fh.write(Interface(interface_node, path, bus_name).make_code()) i += 1 return i def generate_from_introspection(path, name, output_file, bus='SESSION'): # Many D-Bus services have a main object at a predictable name, e.g. # org.freedesktop.Notifications -> /org/freedesktop/Notifications if not path: path = '/' + name.replace('.', '/') conn = open_dbus_connection(bus) introspectable = Proxy(Introspectable(path, name), conn) xml, = introspectable.Introspect() # print(xml) n_interfaces = code_from_xml(xml, path, name, output_file) print("Written {} interface wrappers to {}".format(n_interfaces, output_file)) def generate_from_file(input_file, path, name, output_file): with open(input_file, encoding='utf-8') as f: xml = f.read() n_interfaces = code_from_xml(xml, path, name, output_file) print("Written {} interface wrappers to {}".format(n_interfaces, output_file)) def main(): ap = argparse.ArgumentParser( description="Generate a simple wrapper module to call D-Bus methods.", epilog="If you don't use --file, this will connect to D-Bus and introspect the " "given name and path. --name and --path can also be used with --file, " "to give defaults for the generated class." ) ap.add_argument('-n', '--name', help='Bus name to introspect, required unless using file') ap.add_argument('-p', '--path', help='Object path to introspect. If not specified, a path matching ' 'the name will be used, e.g. /org/freedesktop/Notifications for org.freedesktop.Notifications') ap.add_argument('--bus', default='SESSION', help='Bus to connect to for introspection (SESSION/SYSTEM), default SESSION') ap.add_argument('-f', '--file', help='XML file to use instead of D-Bus introspection') ap.add_argument('-o', '--output', help='Output filename') args = ap.parse_args() if not (args.file or args.name): sys.exit("Either --name or --file is required") # If no --output, guess a (hopefully) reasonable name. if args.output: output = args.output elif args.file: output = os.path.splitext(os.path.basename(args.file))[0] + '.py' elif args.path and len(args.path) > 1: output = args.path[1:].replace('/', '_') + '.py' else: # e.g. path is '/' output = args.name.replace('.', '_') + '.py' if args.file: generate_from_file(args.file, args.path, args.name, output) else: generate_from_introspection(args.path, args.name, output, args.bus) if __name__ == '__main__': main() ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.865029 jeepney-0.9.0/jeepney/bus.py0000644000000000000000000000343114760131573012717 0ustar00import os import re _escape_pat = re.compile(r'%([0-9A-Fa-f]{2})') def unescape(v): def repl(match): n = int(match.group(1), base=16) return chr(n) return _escape_pat.sub(repl, v) def parse_addresses(s): for addr in s.split(';'): transport, info = addr.split(':', 1) kv = {} for x in info.split(','): k, v = x.split('=', 1) kv[k] = unescape(v) yield (transport, kv) SUPPORTED_TRANSPORTS = ('unix',) def get_connectable_addresses(addr): unsupported_transports = set() found = False for transport, kv in parse_addresses(addr): if transport not in SUPPORTED_TRANSPORTS: unsupported_transports.add(transport) elif transport == 'unix': if 'abstract' in kv: yield '\0' + kv['abstract'] found = True elif 'path' in kv: yield kv['path'] found = True if not found: raise RuntimeError("DBus transports ({}) not supported. Supported: {}" .format(unsupported_transports, SUPPORTED_TRANSPORTS)) def find_session_bus(): addr = os.environ['DBUS_SESSION_BUS_ADDRESS'] return next(get_connectable_addresses(addr)) # TODO: fallbacks to X, filesystem def find_system_bus(): addr = os.environ.get('DBUS_SYSTEM_BUS_ADDRESS', '') \ or 'unix:path=/var/run/dbus/system_bus_socket' return next(get_connectable_addresses(addr)) def get_bus(addr): if addr == 'SESSION': return find_session_bus() elif addr == 'SYSTEM': return find_system_bus() else: return next(get_connectable_addresses(addr)) if __name__ == '__main__': print('System bus at:', find_system_bus()) print('Session bus at:', find_session_bus()) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.865029 jeepney-0.9.0/jeepney/bus_messages.py0000644000000000000000000002005714760131573014611 0ustar00"""Messages for talking to the DBus daemon itself Generated by jeepney.bindgen and modified by hand. """ from .low_level import Message, MessageType, HeaderFields from .wrappers import MessageGenerator, new_method_call __all__ = [ 'DBusNameFlags', 'DBus', 'message_bus', 'Monitoring', 'Stats', 'MatchRule', ] class DBusNameFlags: allow_replacement = 1 replace_existing = 2 do_not_queue = 4 class DBus(MessageGenerator): """Messages to talk to the message bus """ interface = 'org.freedesktop.DBus' def __init__(self, object_path='/org/freedesktop/DBus', bus_name='org.freedesktop.DBus'): super().__init__(object_path=object_path, bus_name=bus_name) def Hello(self): return new_method_call(self, 'Hello') def RequestName(self, name, flags=0): return new_method_call(self, 'RequestName', 'su', (name, flags)) def ReleaseName(self, name): return new_method_call(self, 'ReleaseName', 's', (name,)) def StartServiceByName(self, name): return new_method_call(self, 'StartServiceByName', 'su', (name, 0)) def UpdateActivationEnvironment(self, env): return new_method_call(self, 'UpdateActivationEnvironment', 'a{ss}', (env,)) def NameHasOwner(self, name): return new_method_call(self, 'NameHasOwner', 's', (name,)) def ListNames(self): return new_method_call(self, 'ListNames') def ListActivatableNames(self): return new_method_call(self, 'ListActivatableNames') def AddMatch(self, rule): """*rule* can be a str or a :class:`MatchRule` instance""" if isinstance(rule, MatchRule): rule = rule.serialise() return new_method_call(self, 'AddMatch', 's', (rule,)) def RemoveMatch(self, rule): if isinstance(rule, MatchRule): rule = rule.serialise() return new_method_call(self, 'RemoveMatch', 's', (rule,)) def GetNameOwner(self, name): return new_method_call(self, 'GetNameOwner', 's', (name,)) def ListQueuedOwners(self, name): return new_method_call(self, 'ListQueuedOwners', 's', (name,)) def GetConnectionUnixUser(self, name): return new_method_call(self, 'GetConnectionUnixUser', 's', (name,)) def GetConnectionUnixProcessID(self, name): return new_method_call(self, 'GetConnectionUnixProcessID', 's', (name,)) def GetAdtAuditSessionData(self, name): return new_method_call(self, 'GetAdtAuditSessionData', 's', (name,)) def GetConnectionSELinuxSecurityContext(self, name): return new_method_call(self, 'GetConnectionSELinuxSecurityContext', 's', (name,)) def ReloadConfig(self): return new_method_call(self, 'ReloadConfig') def GetId(self): return new_method_call(self, 'GetId') def GetConnectionCredentials(self, name): return new_method_call(self, 'GetConnectionCredentials', 's', (name,)) message_bus = DBus() class Monitoring(MessageGenerator): interface = 'org.freedesktop.DBus.Monitoring' def __init__(self, object_path='/org/freedesktop/DBus', bus_name='org.freedesktop.DBus'): super().__init__(object_path=object_path, bus_name=bus_name) def BecomeMonitor(self, rules): """Convert this connection to a monitor connection (advanced)""" return new_method_call(self, 'BecomeMonitor', 'asu', (rules, 0)) class Stats(MessageGenerator): interface = 'org.freedesktop.DBus.Debug.Stats' def __init__(self, object_path='/org/freedesktop/DBus', bus_name='org.freedesktop.DBus'): super().__init__(object_path=object_path, bus_name=bus_name) def GetStats(self): return new_method_call(self, 'GetStats') def GetConnectionStats(self, arg0): return new_method_call(self, 'GetConnectionStats', 's', (arg0,)) def GetAllMatchRules(self): return new_method_call(self, 'GetAllMatchRules') class MatchRule: """Construct a match rule to subscribe to DBus messages. e.g.:: mr = MatchRule( interface='org.freedesktop.DBus', member='NameOwnerChanged', type='signal' ) msg = message_bus.AddMatch(mr) # Send this message to subscribe to the signal """ def __init__(self, *, type=None, sender=None, interface=None, member=None, path=None, path_namespace=None, destination=None, eavesdrop=False): if isinstance(type, str): type = MessageType[type] self.message_type = type fields = { 'sender': sender, 'interface': interface, 'member': member, 'path': path, 'destination': destination, } self.header_fields = { k: v for (k, v) in fields.items() if (v is not None) } self.path_namespace = path_namespace self.eavesdrop = eavesdrop self.arg_conditions = {} def add_arg_condition(self, argno: int, value: str, kind='string'): """Add a condition for a particular argument argno: int, 0-63 kind: 'string', 'path', 'namespace' """ if kind not in {'string', 'path', 'namespace'}: raise ValueError("kind={!r}".format(kind)) if kind == 'namespace' and argno != 0: raise ValueError("argno must be 0 for kind='namespace'") self.arg_conditions[argno] = (value, kind) def serialise(self) -> str: """Convert to a string to use in an AddMatch call to the message bus""" pairs = list(self.header_fields.items()) if self.message_type: pairs.append(('type', self.message_type.name)) if self.path_namespace: pairs.append(('path_namespace', self.path_namespace)) if self.eavesdrop: pairs.append(('eavesdrop', 'true')) for argno, (val, kind) in self.arg_conditions.items(): if kind == 'string': kind = '' pairs.append((f'arg{argno}{kind}', val)) # Quoting rules: single quotes ('') needed if the value contains a comma. # A literal ' can only be represented outside single quotes, by # backslash-escaping it. No escaping inside the quotes. # The simplest way to handle this is to use '' around every value, and # use '\'' (end quote, escaped ', restart quote) for literal ' . return ','.join( "{}='{}'".format(k, v.replace("'", r"'\''")) for (k, v) in pairs ) def matches(self, msg: Message) -> bool: """Returns True if msg matches this rule""" h = msg.header if (self.message_type is not None) and h.message_type != self.message_type: return False for field, expected in self.header_fields.items(): if h.fields.get(HeaderFields[field], None) != expected: return False if self.path_namespace is not None: path = h.fields.get(HeaderFields.path, '\0') path_ns = self.path_namespace.rstrip('/') if not ((path == path_ns) or path.startswith(path_ns + '/')): return False for argno, (expected, kind) in self.arg_conditions.items(): if argno >= len(msg.body): return False arg = msg.body[argno] if not isinstance(arg, str): return False if kind == 'string': if arg != expected: return False elif kind == 'path': if not ( (arg == expected) or (expected.endswith('/') and arg.startswith(expected)) or (arg.endswith('/') and expected.startswith(arg)) ): return False elif kind == 'namespace': if not ( (arg == expected) or arg.startswith(expected + '.') ): return False return True ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.865029 jeepney-0.9.0/jeepney/fds.py0000644000000000000000000001170014760131573012700 0ustar00import array import os import socket from warnings import warn class NoFDError(RuntimeError): """Raised by :class:`FileDescriptor` methods if it was already closed/converted """ pass class FileDescriptor: """A file descriptor received in a D-Bus message This wrapper helps ensure that the file descriptor is closed exactly once. If you don't explicitly convert or close the FileDescriptor object, it will close its file descriptor when it goes out of scope, and emit a ResourceWarning. """ __slots__ = ('_fd',) _CLOSED = -1 _CONVERTED = -2 def __init__(self, fd): self._fd = fd def __repr__(self): detail = self._fd if self._fd == self._CLOSED: detail = 'closed' elif self._fd == self._CONVERTED: detail = 'converted' return f"" def close(self): """Close the file descriptor This can safely be called multiple times, but will raise RuntimeError if called after converting it with one of the ``to_*`` methods. This object can also be used in a ``with`` block, to close it on leaving the block. """ if self._fd == self._CLOSED: pass elif self._fd == self._CONVERTED: raise NoFDError("Can't close FileDescriptor after converting it") else: self._fd, fd = self._CLOSED, self._fd os.close(fd) def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() def __del__(self): if self._fd >= 0: warn( f'FileDescriptor ({self._fd}) was neither closed nor converted', ResourceWarning, stacklevel=2, source=self ) self.close() def _check(self): if self._fd < 0: detail = 'closed' if self._fd == self._CLOSED else 'converted' raise NoFDError(f'FileDescriptor object was already {detail}') def fileno(self): """Get the integer file descriptor This does not change the state of the :class:`FileDescriptor` object, unlike the ``to_*`` methods. """ self._check() return self._fd def to_raw_fd(self): """Convert to the low-level integer file descriptor:: raw_fd = fd.to_raw_fd() os.write(raw_fd, b'xyz') os.close(raw_fd) The :class:`FileDescriptor` can't be used after calling this. The caller is responsible for closing the file descriptor. """ self._check() self._fd, fd = self._CONVERTED, self._fd return fd def to_file(self, mode, buffering=-1, encoding=None, errors=None, newline=None): """Convert to a Python file object:: with fd.to_file('w') as f: f.write('xyz') The arguments are the same as for the builtin :func:`open` function. The :class:`FileDescriptor` can't be used after calling this. Closing the file object will also close the file descriptor. """ self._check() f = open( self._fd, mode, buffering=buffering, encoding=encoding, errors=errors, newline=newline ) self._fd = self._CONVERTED return f def to_socket(self): """Convert to a socket object This returns a standard library :func:`socket.socket` object:: with fd.to_socket() as sock: b = sock.sendall(b'xyz') The wrapper object can't be used after calling this. Closing the socket object will also close the file descriptor. """ from socket import socket self._check() s = socket(fileno=self._fd) self._fd = self._CONVERTED return s @classmethod def from_ancdata(cls, ancdata) -> ['FileDescriptor']: """Make a list of FileDescriptor from received file descriptors ancdata is a list of ancillary data tuples as returned by socket.recvmsg() """ fds = array.array("i") # Array of ints for cmsg_level, cmsg_type, data in ancdata: if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS: # Append data, ignoring any truncated integers at the end. fds.frombytes(data[:len(data) - (len(data) % fds.itemsize)]) return [cls(i) for i in fds] _fds_buf_size_cache = None def fds_buf_size(): # If there may be file descriptors, we try to read 1 message at a time. # The reference implementation of D-Bus defaults to allowing 16 FDs per # message, and the Linux kernel currently allows 253 FDs per sendmsg() # call. So hopefully allowing 256 FDs per recvmsg() will always suffice. global _fds_buf_size_cache if _fds_buf_size_cache is None: maxfds = 256 fd_size = array.array('i').itemsize _fds_buf_size_cache = socket.CMSG_SPACE(maxfds * fd_size) return _fds_buf_size_cache ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.865029 jeepney-0.9.0/jeepney/io/__init__.py0000644000000000000000000000004114760131573014266 0ustar00from .common import RouterClosed ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.865029 jeepney-0.9.0/jeepney/io/asyncio.py0000644000000000000000000001670614760131573014213 0ustar00import asyncio import contextlib from itertools import count from typing import Optional from jeepney.auth import Authenticator, BEGIN from jeepney.bus import get_bus from jeepney import Message, MessageType, Parser from jeepney.wrappers import ProxyBase, unwrap_msg from jeepney.bus_messages import message_bus from .common import ( MessageFilters, FilterHandle, ReplyMatcher, RouterClosed, check_replyable, ) class DBusConnection: """A plain D-Bus connection with no matching of replies. This doesn't run any separate tasks: sending and receiving are done in the task that calls those methods. It's suitable for implementing servers: several worker tasks can receive requests and send replies. For a typical client pattern, see :class:`DBusRouter`. """ def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter): self.reader = reader self.writer = writer self.parser = Parser() self.outgoing_serial = count(start=1) self.unique_name = None self.send_lock = asyncio.Lock() async def send(self, message: Message, *, serial=None): """Serialise and send a :class:`~.Message` object""" async with self.send_lock: if serial is None: serial = next(self.outgoing_serial) self.writer.write(message.serialise(serial)) await self.writer.drain() async def receive(self) -> Message: """Return the next available message from the connection""" while True: msg = self.parser.get_next_message() if msg is not None: return msg b = await self.reader.read(4096) if not b: raise EOFError self.parser.add_data(b) async def close(self): """Close the D-Bus connection""" self.writer.close() await self.writer.wait_closed() async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_val, exc_tb): await self.close() async def open_dbus_connection(bus='SESSION'): """Open a plain D-Bus connection :return: :class:`DBusConnection` """ bus_addr = get_bus(bus) reader, writer = await asyncio.open_unix_connection(bus_addr) # Authentication flow authr = Authenticator() for req_data in authr: writer.write(req_data) await writer.drain() b = await reader.read(1024) if not b: raise EOFError("Socket closed before authentication") authr.feed(b) writer.write(BEGIN) await writer.drain() # Authentication finished conn = DBusConnection(reader, writer) # Say *Hello* to the message bus - this must be the first message, and the # reply gives us our unique name. async with DBusRouter(conn) as router: reply_body = await asyncio.wait_for(Proxy(message_bus, router).Hello(), 10) conn.unique_name = reply_body[0] return conn class DBusRouter: """A 'client' D-Bus connection which can wait for a specific reply. This runs a background receiver task, and makes it possible to send a request and wait for the relevant reply. """ _nursery_mgr = None _send_cancel_scope = None _rcv_cancel_scope = None def __init__(self, conn: DBusConnection): self._conn = conn self._replies = ReplyMatcher() self._filters = MessageFilters() self._rcv_task = asyncio.create_task(self._receiver()) @property def unique_name(self): return self._conn.unique_name async def send(self, message, *, serial=None): """Send a message, don't wait for a reply""" await self._conn.send(message, serial=serial) async def send_and_get_reply(self, message) -> Message: """Send a method call message and wait for the reply Returns the reply message (method return or error message type). """ check_replyable(message) if self._rcv_task.done(): raise RouterClosed("This DBusRouter has stopped") serial = next(self._conn.outgoing_serial) with self._replies.catch(serial, asyncio.Future()) as reply_fut: await self.send(message, serial=serial) return (await reply_fut) def filter(self, rule, *, queue: Optional[asyncio.Queue] =None, bufsize=1): """Create a filter for incoming messages Usage:: with router.filter(rule) as queue: matching_msg = await queue.get() :param MatchRule rule: Catch messages matching this rule :param asyncio.Queue queue: Send matching messages here :param int bufsize: If no queue is passed in, create one with this size """ return FilterHandle(self._filters, rule, queue or asyncio.Queue(bufsize)) async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_val, exc_tb): if self._rcv_task.done(): self._rcv_task.result() # Throw exception if receive task failed else: self._rcv_task.cancel() with contextlib.suppress(asyncio.CancelledError): await self._rcv_task return False # Code to run in receiver task ------------------------------------ def _dispatch(self, msg: Message): """Handle one received message""" if self._replies.dispatch(msg): return for filter in list(self._filters.matches(msg)): try: filter.queue.put_nowait(msg) except asyncio.QueueFull: pass async def _receiver(self): """Receiver loop - runs in a separate task""" try: while True: msg = await self._conn.receive() self._dispatch(msg) finally: # Send errors to any tasks still waiting for a message. self._replies.drop_all() class open_dbus_router: """Open a D-Bus 'router' to send and receive messages Use as an async context manager:: async with open_dbus_router() as router: ... """ conn = None req_ctx = None def __init__(self, bus='SESSION'): self.bus = bus async def __aenter__(self): self.conn = await open_dbus_connection(self.bus) self.req_ctx = DBusRouter(self.conn) return await self.req_ctx.__aenter__() async def __aexit__(self, exc_type, exc_val, exc_tb): await self.req_ctx.__aexit__(exc_type, exc_val, exc_tb) await self.conn.close() class Proxy(ProxyBase): """An asyncio proxy for calling D-Bus methods You can call methods on the proxy object, such as ``await bus_proxy.Hello()`` to make a method call over D-Bus and wait for a reply. It will either return a tuple of returned data, or raise :exc:`.DBusErrorResponse`. The methods available are defined by the message generator you wrap. :param msggen: A message generator object. :param ~asyncio.DBusRouter router: Router to send and receive messages. """ def __init__(self, msggen, router): super().__init__(msggen) self._router = router def __repr__(self): return 'Proxy({}, {})'.format(self._msggen, self._router) def _method_call(self, make_msg): async def inner(*args, **kwargs): msg = make_msg(*args, **kwargs) assert msg.header.message_type is MessageType.method_call reply = await self._router.send_and_get_reply(msg) return unwrap_msg(reply) return inner ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.866029 jeepney-0.9.0/jeepney/io/blocking.py0000644000000000000000000002724414760131573014335 0ustar00"""Synchronous IO wrappers around jeepney """ import array from collections import deque from errno import ECONNRESET import functools from itertools import count import os from selectors import DefaultSelector, EVENT_READ import socket import time from typing import Optional from jeepney import Parser, Message, MessageType, HeaderFields from jeepney.auth import Authenticator, BEGIN from jeepney.bus import get_bus from jeepney.fds import FileDescriptor, fds_buf_size from jeepney.wrappers import ProxyBase, unwrap_msg from jeepney.bus_messages import message_bus from .common import MessageFilters, FilterHandle, check_replyable __all__ = [ 'open_dbus_connection', 'DBusConnection', 'Proxy', ] class _Future: def __init__(self): self._result = None def done(self): return bool(self._result) def set_exception(self, exception): self._result = (False, exception) def set_result(self, result): self._result = (True, result) def result(self): success, value = self._result if success: return value raise value def timeout_to_deadline(timeout): if timeout is not None: return time.monotonic() + timeout return None def deadline_to_timeout(deadline): if deadline is not None: return max(deadline - time.monotonic(), 0.) return None class DBusConnectionBase: """Connection machinery shared by this module and threading""" def __init__(self, sock: socket.socket, enable_fds=False): self.sock = sock self.enable_fds = enable_fds self.parser = Parser() self.outgoing_serial = count(start=1) self.selector = DefaultSelector() self.select_key = self.selector.register(sock, EVENT_READ) self.unique_name = None def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() return False def _serialise(self, message: Message, serial) -> (bytes, Optional[array.array]): if serial is None: serial = next(self.outgoing_serial) fds = array.array('i') if self.enable_fds else None data = message.serialise(serial=serial, fds=fds) return data, fds def _send_with_fds(self, data, fds): bytes_sent = self.sock.sendmsg( [data], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fds)] ) # If sendmsg succeeds, I think ancillary data has been sent atomically? # So now we just need to send any leftover normal data. if bytes_sent < len(data): self.sock.sendall(data[bytes_sent:]) def _receive(self, deadline): while True: msg = self.parser.get_next_message() if msg is not None: return msg b, fds = self._read_some_data(timeout=deadline_to_timeout(deadline)) self.parser.add_data(b, fds=fds) def _read_some_data(self, timeout=None): for key, ev in self.selector.select(timeout): if key == self.select_key: if self.enable_fds: return self._read_with_fds() else: return unwrap_read(self.sock.recv(4096)), [] raise TimeoutError def _read_with_fds(self): nbytes = self.parser.bytes_desired() data, ancdata, flags, _ = self.sock.recvmsg(nbytes, fds_buf_size()) if flags & getattr(socket, 'MSG_CTRUNC', 0): self.close() raise RuntimeError("Unable to receive all file descriptors") return unwrap_read(data), FileDescriptor.from_ancdata(ancdata) def close(self): """Close the connection""" self.selector.close() self.sock.close() class DBusConnection(DBusConnectionBase): def __init__(self, sock: socket.socket, enable_fds=False): super().__init__(sock, enable_fds) # Message routing machinery self._filters = MessageFilters() # Say Hello, get our unique name self.bus_proxy = Proxy(message_bus, self) hello_reply = self.bus_proxy.Hello() self.unique_name = hello_reply[0] def send(self, message: Message, serial=None): """Serialise and send a :class:`~.Message` object""" data, fds = self._serialise(message, serial) if fds: self._send_with_fds(data, fds) else: self.sock.sendall(data) send_message = send # Backwards compatibility def receive(self, *, timeout=None) -> Message: """Return the next available message from the connection If the data is ready, this will return immediately, even if timeout<=0. Otherwise, it will wait for up to timeout seconds, or indefinitely if timeout is None. If no message comes in time, it raises TimeoutError. """ return self._receive(timeout_to_deadline(timeout)) def recv_messages(self, *, timeout=None): """Receive one message and apply filters See :meth:`filter`. Returns nothing. """ msg = self.receive(timeout=timeout) for filter in self._filters.matches(msg): filter.queue.append(msg) def send_and_get_reply(self, message, *, timeout=None): """Send a message, wait for the reply and return it Filters are applied to other messages received before the reply - see :meth:`add_filter`. """ check_replyable(message) deadline = timeout_to_deadline(timeout) serial = next(self.outgoing_serial) self.send_message(message, serial=serial) while True: msg_in = self.receive(timeout=deadline_to_timeout(deadline)) reply_to = msg_in.header.fields.get(HeaderFields.reply_serial, -1) if reply_to == serial: return msg_in # Not the reply for filter in self._filters.matches(msg_in): filter.queue.append(msg_in) def filter(self, rule, *, queue: Optional[deque] =None, bufsize=1): """Create a filter for incoming messages Usage:: with conn.filter(rule) as matches: # matches is a deque containing matched messages matching_msg = conn.recv_until_filtered(matches) :param jeepney.MatchRule rule: Catch messages matching this rule :param collections.deque queue: Matched messages will be added to this :param int bufsize: If no deque is passed in, create one with this size """ if queue is None: queue = deque(maxlen=bufsize) return FilterHandle(self._filters, rule, queue) def recv_until_filtered(self, queue, *, timeout=None) -> Message: """Process incoming messages until one is filtered into queue Pops the message from queue and returns it, or raises TimeoutError if the optional timeout expires. Without a timeout, this is equivalent to:: while len(queue) == 0: conn.recv_messages() return queue.popleft() In the other I/O modules, there is no need for this, because messages are placed in queues by a separate task. :param collections.deque queue: A deque connected by :meth:`filter` :param float timeout: Maximum time to wait in seconds """ deadline = timeout_to_deadline(timeout) while len(queue) == 0: self.recv_messages(timeout=deadline_to_timeout(deadline)) return queue.popleft() class Proxy(ProxyBase): """A blocking proxy for calling D-Bus methods You can call methods on the proxy object, such as ``bus_proxy.Hello()`` to make a method call over D-Bus and wait for a reply. It will either return a tuple of returned data, or raise :exc:`.DBusErrorResponse`. The methods available are defined by the message generator you wrap. You can set a time limit on a call by passing ``_timeout=`` in the method call, or set a default when creating the proxy. The ``_timeout`` argument is not passed to the message generator. All timeouts are in seconds, and :exc:`TimeoutErrror` is raised if it expires before a reply arrives. :param msggen: A message generator object :param ~blocking.DBusConnection connection: Connection to send and receive messages :param float timeout: Default seconds to wait for a reply, or None for no limit """ def __init__(self, msggen, connection, *, timeout=None): super().__init__(msggen) self._connection = connection self._timeout = timeout def __repr__(self): extra = '' if (self._timeout is None) else f', timeout={self._timeout}' return f"Proxy({self._msggen}, {self._connection}{extra})" def _method_call(self, make_msg): @functools.wraps(make_msg) def inner(*args, **kwargs): timeout = kwargs.pop('_timeout', self._timeout) msg = make_msg(*args, **kwargs) assert msg.header.message_type is MessageType.method_call return unwrap_msg(self._connection.send_and_get_reply( msg, timeout=timeout )) return inner def unwrap_read(b): """Raise ConnectionResetError from an empty read. Sometimes the socket raises an error itself, sometimes it gives no data. I haven't worked out when it behaves each way. """ if not b: raise ConnectionResetError(ECONNRESET, os.strerror(ECONNRESET)) return b def prep_socket(addr, enable_fds=False, timeout=2.0) -> socket.socket: """Create a socket and authenticate ready to send D-Bus messages""" sock = socket.socket(family=socket.AF_UNIX) # To impose the overall auth timeout, we'll update the timeout on the socket # before each send/receive. This is ugly, but we can't use the socket for # anything else until this has succeeded, so this should be safe. deadline = timeout_to_deadline(timeout) def with_sock_deadline(meth, *args): sock.settimeout(deadline_to_timeout(deadline)) return meth(*args) try: with_sock_deadline(sock.connect, addr) authr = Authenticator(enable_fds=enable_fds, inc_null_byte=False) if hasattr(socket, 'SCM_CREDS'): # BSD: send credentials message to authenticate (kernel fills in data) sock.sendmsg([b'\0'], [(socket.SOL_SOCKET, socket.SCM_CREDS, bytes(512))]) else: # Linux: no ancillary data needed, bus checks with SO_PEERCRED sock.send(b'\0') for req_data in authr: with_sock_deadline(sock.sendall, req_data) authr.feed(unwrap_read(with_sock_deadline(sock.recv, 1024))) with_sock_deadline(sock.sendall, BEGIN) except socket.timeout as e: sock.close() raise TimeoutError(f"Did not authenticate in {timeout} seconds") from e except: sock.close() raise sock.settimeout(None) # Put the socket back in blocking mode return sock def open_dbus_connection( bus='SESSION', enable_fds=False, auth_timeout=1., ) -> DBusConnection: """Connect to a D-Bus message bus Pass ``enable_fds=True`` to allow sending & receiving file descriptors. An error will be raised if the bus does not allow this. For simplicity, it's advisable to leave this disabled unless you need it. D-Bus has an authentication step before sending or receiving messages. This takes < 1 ms in normal operation, but there is a timeout so that client code won't get stuck if the server doesn't reply. *auth_timeout* configures this timeout in seconds. """ bus_addr = get_bus(bus) sock = prep_socket(bus_addr, enable_fds, timeout=auth_timeout) conn = DBusConnection(sock, enable_fds) return conn if __name__ == '__main__': conn = open_dbus_connection() print("Unique name:", conn.unique_name) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.866029 jeepney-0.9.0/jeepney/io/common.py0000644000000000000000000000521014760131573014022 0ustar00from contextlib import contextmanager from itertools import count from jeepney import HeaderFields, Message, MessageFlag, MessageType class MessageFilters: def __init__(self): self.filters = {} self.filter_ids = count() def matches(self, message): for handle in self.filters.values(): if handle.rule.matches(message): yield handle class FilterHandle: def __init__(self, filters: MessageFilters, rule, queue): self._filters = filters self._filter_id = next(filters.filter_ids) self.rule = rule self.queue = queue self._filters.filters[self._filter_id] = self def close(self): del self._filters.filters[self._filter_id] def __enter__(self): return self.queue def __exit__(self, exc_type, exc_val, exc_tb): self.close() return False class ReplyMatcher: def __init__(self): self._futures = {} @contextmanager def catch(self, serial, future): """Context manager to capture a reply for the given serial number""" self._futures[serial] = future try: yield future finally: del self._futures[serial] def dispatch(self, msg): """Dispatch an incoming message which may be a reply Returns True if a task was waiting for it, otherwise False. """ rep_serial = msg.header.fields.get(HeaderFields.reply_serial, -1) if rep_serial in self._futures: self._futures[rep_serial].set_result(msg) return True else: return False def drop_all(self, exc: Exception = None): """Throw an error in any task still waiting for a reply""" if exc is None: exc = RouterClosed("D-Bus router closed before reply arrived") futures, self._futures = self._futures, {} for fut in futures.values(): fut.set_exception(exc) class RouterClosed(Exception): """Raised in tasks waiting for a reply when the router is closed This will also be raised if the receiver task crashes, so tasks are not stuck waiting for a reply that can never come. The router object will not be usable after this is raised. """ pass def check_replyable(msg: Message): """Raise an error if we wouldn't expect a reply for msg""" if msg.header.message_type != MessageType.method_call: raise TypeError("Only method call messages have replies " f"(not {msg.header.message_type})") if MessageFlag.no_reply_expected & msg.header.flags: raise ValueError("This message has the no_reply_expected flag set") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.9000287 jeepney-0.9.0/jeepney/io/tests/__init__.py0000644000000000000000000000000014760131573015423 0ustar00././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.866029 jeepney-0.9.0/jeepney/io/tests/conftest.py0000644000000000000000000000525214760131573015527 0ustar00from tempfile import TemporaryFile import threading import pytest from jeepney import ( DBusAddress, HeaderFields, message_bus, MessageType, new_error, new_method_return, ) from jeepney.io.threading import open_dbus_connection, DBusRouter, Proxy @pytest.fixture() def respond_with_fd(): name = "io.gitlab.takluyver.jeepney.tests.respond_with_fd" addr = DBusAddress(bus_name=name, object_path='/') with open_dbus_connection(bus='SESSION', enable_fds=True) as conn: with DBusRouter(conn) as router: status, = Proxy(message_bus, router).RequestName(name) assert status == 1 # DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER def _reply_once(): while True: msg = conn.receive() if msg.header.message_type is MessageType.method_call: if msg.header.fields[HeaderFields.member] == 'GetFD': with TemporaryFile('w+') as tf: tf.write('readme') tf.seek(0) rep = new_method_return(msg, 'h', (tf,)) conn.send(rep) return else: conn.send(new_error(msg, 'NoMethod')) reply_thread = threading.Thread(target=_reply_once, daemon=True) reply_thread.start() yield addr reply_thread.join() @pytest.fixture() def read_from_fd(): name = "io.gitlab.takluyver.jeepney.tests.read_from_fd" addr = DBusAddress(bus_name=name, object_path='/') with open_dbus_connection(bus='SESSION', enable_fds=True) as conn: with DBusRouter(conn) as router: status, = Proxy(message_bus, router).RequestName(name) assert status == 1 # DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER def _reply_once(): while True: msg = conn.receive() if msg.header.message_type is MessageType.method_call: if msg.header.fields[HeaderFields.member] == 'ReadFD': with msg.body[0].to_file('rb') as f: f.seek(0) b = f.read() conn.send(new_method_return(msg, 'ay', (b,))) return else: conn.send(new_error(msg, 'NoMethod')) reply_thread = threading.Thread(target=_reply_once, daemon=True) reply_thread.start() yield addr reply_thread.join() @pytest.fixture() def temp_file_and_contents(): data = b'abc123' with TemporaryFile('w+b') as tf: tf.write(data) tf.flush() tf.seek(0) yield tf, data ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.866029 jeepney-0.9.0/jeepney/io/tests/test_asyncio.py0000644000000000000000000000524014760131573016403 0ustar00import asyncio import sys if sys.version_info >= (3, 11): from asyncio import timeout else: from async_timeout import timeout import pytest import pytest_asyncio from jeepney import DBusAddress, new_method_call from jeepney.bus_messages import message_bus, MatchRule from jeepney.io.asyncio import ( open_dbus_connection, open_dbus_router, Proxy ) from .utils import have_session_bus pytestmark = [ pytest.mark.asyncio, pytest.mark.skipif( not have_session_bus, reason="Tests require DBus session bus" ), ] bus_peer = DBusAddress( bus_name='org.freedesktop.DBus', object_path='/org/freedesktop/DBus', interface='org.freedesktop.DBus.Peer' ) @pytest_asyncio.fixture() async def connection(): async with (await open_dbus_connection(bus='SESSION')) as conn: yield conn async def test_connect(connection): assert connection.unique_name.startswith(':') @pytest_asyncio.fixture() async def router(): async with open_dbus_router(bus='SESSION') as router: yield router async def test_send_and_get_reply(router): ping_call = new_method_call(bus_peer, 'Ping') reply = await asyncio.wait_for( router.send_and_get_reply(ping_call), timeout=5 ) assert reply.body == () async def test_proxy(router): proxy = Proxy(message_bus, router) name = "io.gitlab.takluyver.jeepney.examples.Server" res = await proxy.RequestName(name) assert res in {(1,), (2,)} # 1: got the name, 2: queued has_owner, = await proxy.NameHasOwner(name) assert has_owner is True async def test_filter(router): bus = Proxy(message_bus, router) name = "io.gitlab.takluyver.jeepney.tests.asyncio_test_filter" match_rule = MatchRule( type="signal", sender=message_bus.bus_name, interface=message_bus.interface, member="NameOwnerChanged", path=message_bus.object_path, ) match_rule.add_arg_condition(0, name) # Ask the message bus to subscribe us to this signal await bus.AddMatch(match_rule) with router.filter(match_rule) as queue: res, = await bus.RequestName(name) assert res == 1 # 1: got the name signal_msg = await asyncio.wait_for(queue.get(), timeout=2.0) assert signal_msg.body == (name, '', router.unique_name) async def test_recv_after_connect(): # Can't use here: # 1. 'connection' fixture # 2. asyncio.wait_for() # If (1) and/or (2) is used, the error won't be triggered. conn = await open_dbus_connection(bus='SESSION') try: with pytest.raises(asyncio.TimeoutError): async with timeout(0): await conn.receive() finally: await conn.close() ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.866029 jeepney-0.9.0/jeepney/io/tests/test_blocking.py0000644000000000000000000000536414760131573016535 0ustar00import pytest from jeepney import new_method_call, MessageType, DBusAddress from jeepney.bus_messages import message_bus, MatchRule from jeepney.io.blocking import open_dbus_connection, Proxy from .utils import have_session_bus pytestmark = pytest.mark.skipif( not have_session_bus, reason="Tests require DBus session bus" ) @pytest.fixture def session_conn(): with open_dbus_connection(bus='SESSION') as conn: yield conn def test_connect(session_conn): assert session_conn.unique_name.startswith(':') bus_peer = DBusAddress( bus_name='org.freedesktop.DBus', object_path='/org/freedesktop/DBus', interface='org.freedesktop.DBus.Peer' ) def test_send_and_get_reply(session_conn): ping_call = new_method_call(bus_peer, 'Ping') reply = session_conn.send_and_get_reply(ping_call, timeout=5) assert reply.header.message_type == MessageType.method_return assert reply.body == () def test_proxy(session_conn): proxy = Proxy(message_bus, session_conn, timeout=5) name = "io.gitlab.takluyver.jeepney.examples.Server" res = proxy.RequestName(name) assert res in {(1,), (2,)} # 1: got the name, 2: queued has_owner, = proxy.NameHasOwner(name, _timeout=3) assert has_owner is True def test_filter(session_conn): bus = Proxy(message_bus, session_conn) name = "io.gitlab.takluyver.jeepney.tests.blocking_test_filter" match_rule = MatchRule( type="signal", sender=message_bus.bus_name, interface=message_bus.interface, member="NameOwnerChanged", path=message_bus.object_path, ) match_rule.add_arg_condition(0, name) # Ask the message bus to subscribe us to this signal bus.AddMatch(match_rule) with session_conn.filter(match_rule) as matches: res, = bus.RequestName(name) assert res == 1 # 1: got the name signal_msg = session_conn.recv_until_filtered(matches, timeout=2) assert signal_msg.body == (name, '', session_conn.unique_name) def test_recv_fd(respond_with_fd): getfd_call = new_method_call(respond_with_fd, 'GetFD') with open_dbus_connection(bus='SESSION', enable_fds=True) as conn: reply = conn.send_and_get_reply(getfd_call, timeout=5) assert reply.header.message_type is MessageType.method_return with reply.body[0].to_file('w+') as f: assert f.read() == 'readme' def test_send_fd(temp_file_and_contents, read_from_fd): temp_file, data = temp_file_and_contents readfd_call = new_method_call(read_from_fd, 'ReadFD', 'h', (temp_file,)) with open_dbus_connection(bus='SESSION', enable_fds=True) as conn: reply = conn.send_and_get_reply(readfd_call, timeout=5) assert reply.header.message_type is MessageType.method_return assert reply.body[0] == data ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.866029 jeepney-0.9.0/jeepney/io/tests/test_threading.py0000644000000000000000000000521314760131573016703 0ustar00import pytest from jeepney import new_method_call, MessageType, DBusAddress from jeepney.bus_messages import message_bus, MatchRule from jeepney.io.threading import open_dbus_router, Proxy from .utils import have_session_bus pytestmark = pytest.mark.skipif( not have_session_bus, reason="Tests require DBus session bus" ) @pytest.fixture def router(): with open_dbus_router(bus='SESSION') as conn: yield conn def test_connect(router): assert router.unique_name.startswith(':') bus_peer = DBusAddress( bus_name='org.freedesktop.DBus', object_path='/org/freedesktop/DBus', interface='org.freedesktop.DBus.Peer' ) def test_send_and_get_reply(router): ping_call = new_method_call(bus_peer, 'Ping') reply = router.send_and_get_reply(ping_call, timeout=5) assert reply.header.message_type == MessageType.method_return assert reply.body == () def test_proxy(router): proxy = Proxy(message_bus, router, timeout=5) name = "io.gitlab.takluyver.jeepney.examples.Server" res = proxy.RequestName(name) assert res in {(1,), (2,)} # 1: got the name, 2: queued has_owner, = proxy.NameHasOwner(name, _timeout=3) assert has_owner is True def test_filter(router): bus = Proxy(message_bus, router) name = "io.gitlab.takluyver.jeepney.tests.threading_test_filter" match_rule = MatchRule( type="signal", sender=message_bus.bus_name, interface=message_bus.interface, member="NameOwnerChanged", path=message_bus.object_path, ) match_rule.add_arg_condition(0, name) # Ask the message bus to subscribe us to this signal bus.AddMatch(match_rule) with router.filter(match_rule) as queue: res, = bus.RequestName(name) assert res == 1 # 1: got the name signal_msg = queue.get(timeout=2.0) assert signal_msg.body == (name, '', router.unique_name) def test_recv_fd(respond_with_fd): getfd_call = new_method_call(respond_with_fd, 'GetFD') with open_dbus_router(bus='SESSION', enable_fds=True) as router: reply = router.send_and_get_reply(getfd_call, timeout=5) assert reply.header.message_type is MessageType.method_return with reply.body[0].to_file('w+') as f: assert f.read() == 'readme' def test_send_fd(temp_file_and_contents, read_from_fd): temp_file, data = temp_file_and_contents readfd_call = new_method_call(read_from_fd, 'ReadFD', 'h', (temp_file,)) with open_dbus_router(bus='SESSION', enable_fds=True) as router: reply = router.send_and_get_reply(readfd_call, timeout=5) assert reply.header.message_type is MessageType.method_return assert reply.body[0] == data ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.866029 jeepney-0.9.0/jeepney/io/tests/test_trio.py0000644000000000000000000000746414760131573015725 0ustar00import trio import pytest from jeepney import DBusAddress, DBusErrorResponse, MessageType, new_method_call from jeepney.bus_messages import message_bus, MatchRule from jeepney.io.trio import ( open_dbus_connection, open_dbus_router, Proxy, ) from .utils import have_session_bus pytestmark = [ pytest.mark.trio, pytest.mark.skipif( not have_session_bus, reason="Tests require DBus session bus" ), ] # Can't use any async fixtures here, because pytest-asyncio tries to handle # all of them: https://github.com/pytest-dev/pytest-asyncio/issues/124 async def test_connect(): conn = await open_dbus_connection(bus='SESSION') async with conn: assert conn.unique_name.startswith(':') bus_peer = DBusAddress( bus_name='org.freedesktop.DBus', object_path='/org/freedesktop/DBus', interface='org.freedesktop.DBus.Peer' ) async def test_send_and_get_reply(): ping_call = new_method_call(bus_peer, 'Ping') async with open_dbus_router(bus='SESSION') as req: with trio.fail_after(5): reply = await req.send_and_get_reply(ping_call) assert reply.header.message_type == MessageType.method_return assert reply.body == () async def test_send_and_get_reply_error(): ping_call = new_method_call(bus_peer, 'Snart') # No such method async with open_dbus_router(bus='SESSION') as req: with trio.fail_after(5): reply = await req.send_and_get_reply(ping_call) assert reply.header.message_type == MessageType.error async def test_proxy(): async with open_dbus_router(bus='SESSION') as req: proxy = Proxy(message_bus, req) name = "io.gitlab.takluyver.jeepney.examples.Server" res = await proxy.RequestName(name) assert res in {(1,), (2,)} # 1: got the name, 2: queued has_owner, = await proxy.NameHasOwner(name) assert has_owner is True async def test_proxy_error(): async with open_dbus_router(bus='SESSION') as req: proxy = Proxy(message_bus, req) with pytest.raises(DBusErrorResponse): await proxy.RequestName(":123") # Invalid name async def test_filter(): name = "io.gitlab.takluyver.jeepney.tests.trio_test_filter" async with open_dbus_router(bus='SESSION') as router: bus = Proxy(message_bus, router) match_rule = MatchRule( type="signal", sender=message_bus.bus_name, interface=message_bus.interface, member="NameOwnerChanged", path=message_bus.object_path, ) match_rule.add_arg_condition(0, name) # Ask the message bus to subscribe us to this signal await bus.AddMatch(match_rule) async with router.filter(match_rule) as chan: res, = await bus.RequestName(name) assert res == 1 # 1: got the name with trio.fail_after(2.0): signal_msg = await chan.receive() assert signal_msg.body == (name, '', router.unique_name) async def test_recv_fd(respond_with_fd): getfd_call = new_method_call(respond_with_fd, 'GetFD') with trio.fail_after(5): async with open_dbus_router(bus='SESSION', enable_fds=True) as router: reply = await router.send_and_get_reply(getfd_call) assert reply.header.message_type is MessageType.method_return with reply.body[0].to_file('w+') as f: assert f.read() == 'readme' async def test_send_fd(temp_file_and_contents, read_from_fd): temp_file, data = temp_file_and_contents readfd_call = new_method_call(read_from_fd, 'ReadFD', 'h', (temp_file,)) with trio.fail_after(5): async with open_dbus_router(bus='SESSION', enable_fds=True) as router: reply = await router.send_and_get_reply(readfd_call) assert reply.header.message_type is MessageType.method_return assert reply.body[0] == data ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.866029 jeepney-0.9.0/jeepney/io/tests/utils.py0000644000000000000000000000011714760131573015035 0ustar00import os have_session_bus = bool(os.environ.get('DBUS_SESSION_BUS_ADDRESS')) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.866029 jeepney-0.9.0/jeepney/io/threading.py0000644000000000000000000002225714760131573014511 0ustar00"""Synchronous IO wrappers with thread safety """ from concurrent.futures import Future from contextlib import contextmanager import functools import os from selectors import EVENT_READ import socket from queue import Queue, Full as QueueFull from threading import Lock, Thread from typing import Optional from jeepney import Message, MessageType from jeepney.bus import get_bus from jeepney.bus_messages import message_bus from jeepney.wrappers import ProxyBase, unwrap_msg from .blocking import ( unwrap_read, prep_socket, DBusConnectionBase, timeout_to_deadline, ) from .common import ( MessageFilters, FilterHandle, ReplyMatcher, RouterClosed, check_replyable, ) __all__ = [ 'open_dbus_connection', 'open_dbus_router', 'DBusConnection', 'DBusRouter', 'Proxy', 'ReceiveStopped', ] class ReceiveStopped(Exception): pass class DBusConnection(DBusConnectionBase): def __init__(self, sock: socket.socket, enable_fds=False): super().__init__(sock, enable_fds=enable_fds) self._stop_r, self._stop_w = os.pipe() self.stop_key = self.selector.register(self._stop_r, EVENT_READ) self.send_lock = Lock() self.rcv_lock = Lock() def send(self, message: Message, serial=None): """Serialise and send a :class:`~.Message` object""" data, fds = self._serialise(message, serial) with self.send_lock: if fds: self._send_with_fds(data, fds) else: self.sock.sendall(data) def receive(self, *, timeout=None) -> Message: """Return the next available message from the connection If the data is ready, this will return immediately, even if timeout<=0. Otherwise, it will wait for up to timeout seconds, or indefinitely if timeout is None. If no message comes in time, it raises TimeoutError. If the connection is closed from another thread, this will raise ReceiveStopped. """ deadline = timeout_to_deadline(timeout) if not self.rcv_lock.acquire(timeout=(timeout or -1)): raise TimeoutError(f"Did not get receive lock in {timeout} seconds") try: return self._receive(deadline) finally: self.rcv_lock.release() def _read_some_data(self, timeout=None): # Wait for data or a signal on the stop pipe for key, ev in self.selector.select(timeout): if key == self.select_key: if self.enable_fds: return self._read_with_fds() else: return unwrap_read(self.sock.recv(4096)), [] elif key == self.stop_key: raise ReceiveStopped("DBus receive stopped from another thread") raise TimeoutError def interrupt(self): """Make any threads waiting for a message raise ReceiveStopped""" os.write(self._stop_w, b'a') def reset_interrupt(self): """Allow calls to .receive() again after .interrupt() To avoid race conditions, you should typically wait for threads to respond (e.g. by joining them) between interrupting and resetting. """ # Clear any data on the stop pipe while (self.stop_key, EVENT_READ) in self.selector.select(timeout=0): os.read(self._stop_r, 1024) def close(self): """Close the connection""" self.interrupt() super().close() def open_dbus_connection(bus='SESSION', enable_fds=False, auth_timeout=1.): """Open a plain D-Bus connection D-Bus has an authentication step before sending or receiving messages. This takes < 1 ms in normal operation, but there is a timeout so that client code won't get stuck if the server doesn't reply. *auth_timeout* configures this timeout in seconds. :return: :class:`DBusConnection` """ bus_addr = get_bus(bus) sock = prep_socket(bus_addr, enable_fds, timeout=auth_timeout) conn = DBusConnection(sock, enable_fds) with DBusRouter(conn) as router: reply_body = Proxy(message_bus, router, timeout=10).Hello() conn.unique_name = reply_body[0] return conn class DBusRouter: """A client D-Bus connection which can wait for replies. This runs a separate receiver thread and dispatches received messages. It's possible to wrap a :class:`DBusConnection` in a router temporarily. Using the connection directly while it is wrapped is not supported, but you can use it again after the router is closed. """ def __init__(self, conn: DBusConnection): self.conn = conn self._replies = ReplyMatcher() self._filters = MessageFilters() self._rcv_thread = Thread(target=self._receiver, daemon=True) self._rcv_thread.start() @property def unique_name(self): return self.conn.unique_name def send(self, message, *, serial=None): """Serialise and send a :class:`~.Message` object""" self.conn.send(message, serial=serial) def send_and_get_reply(self, msg: Message, *, timeout=None) -> Message: """Send a method call message, wait for and return a reply""" check_replyable(msg) if not self._rcv_thread.is_alive(): raise RouterClosed("This D-Bus router has stopped") serial = next(self.conn.outgoing_serial) with self._replies.catch(serial, Future()) as reply_fut: self.conn.send(msg, serial=serial) return reply_fut.result(timeout=timeout) def close(self): """Close this router This does not close the underlying connection. """ self.conn.interrupt() self._rcv_thread.join(timeout=10) self.conn.reset_interrupt() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() return False def filter(self, rule, *, queue: Optional[Queue] =None, bufsize=1): """Create a filter for incoming messages Usage:: with router.filter(rule) as queue: matching_msg = queue.get() :param jeepney.MatchRule rule: Catch messages matching this rule :param queue.Queue queue: Matched messages will be added to this :param int bufsize: If no queue is passed in, create one with this size """ return FilterHandle(self._filters, rule, queue or Queue(maxsize=bufsize)) # Code to run in receiver thread ------------------------------------ def _dispatch(self, msg: Message): if self._replies.dispatch(msg): return for filter in self._filters.matches(msg): try: filter.queue.put_nowait(msg) except QueueFull: pass def _receiver(self): try: while True: msg = self.conn.receive() self._dispatch(msg) except ReceiveStopped: pass finally: # Send errors to any tasks still waiting for a message. self._replies.drop_all() class Proxy(ProxyBase): """A blocking proxy for calling D-Bus methods via a :class:`DBusRouter`. You can call methods on the proxy object, such as ``bus_proxy.Hello()`` to make a method call over D-Bus and wait for a reply. It will either return a tuple of returned data, or raise :exc:`.DBusErrorResponse`. The methods available are defined by the message generator you wrap. You can set a time limit on a call by passing ``_timeout=`` in the method call, or set a default when creating the proxy. The ``_timeout`` argument is not passed to the message generator. All timeouts are in seconds, and :exc:`TimeoutErrror` is raised if it expires before a reply arrives. :param msggen: A message generator object :param ~threading.DBusRouter router: Router to send and receive messages :param float timeout: Default seconds to wait for a reply, or None for no limit """ def __init__(self, msggen, router, *, timeout=None): super().__init__(msggen) self._router = router self._timeout = timeout def __repr__(self): extra = '' if (self._timeout is None) else f', timeout={self._timeout}' return f"Proxy({self._msggen}, {self._router}{extra})" def _method_call(self, make_msg): @functools.wraps(make_msg) def inner(*args, **kwargs): timeout = kwargs.pop('_timeout', self._timeout) msg = make_msg(*args, **kwargs) assert msg.header.message_type is MessageType.method_call reply = self._router.send_and_get_reply(msg, timeout=timeout) return unwrap_msg(reply) return inner @contextmanager def open_dbus_router(bus='SESSION', enable_fds=False): """Open a D-Bus 'router' to send and receive messages. Use as a context manager:: with open_dbus_router() as router: ... On leaving the ``with`` block, the connection will be closed. :param str bus: 'SESSION' or 'SYSTEM' or a supported address. :param bool enable_fds: Whether to enable passing file descriptors. :return: :class:`DBusRouter` """ with open_dbus_connection(bus=bus, enable_fds=enable_fds) as conn: with DBusRouter(conn) as router: yield router ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740682106.866029 jeepney-0.9.0/jeepney/io/trio.py0000644000000000000000000003536014760131573013520 0ustar00import array import errno import logging import socket from contextlib import asynccontextmanager, contextmanager from itertools import count from typing import Optional from outcome import Value, Error import trio from trio.abc import Channel from jeepney.auth import Authenticator, BEGIN from jeepney.bus import get_bus from jeepney.fds import FileDescriptor, fds_buf_size from jeepney.low_level import Parser, MessageType, Message from jeepney.wrappers import ProxyBase, unwrap_msg from jeepney.bus_messages import message_bus from .common import ( MessageFilters, FilterHandle, ReplyMatcher, RouterClosed, check_replyable, ) log = logging.getLogger(__name__) __all__ = [ 'open_dbus_connection', 'open_dbus_router', 'Proxy', ] # The function below is copied from trio, which is under the MIT license: # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. @contextmanager def _translate_socket_errors_to_stream_errors(): try: yield except OSError as exc: if exc.errno in {errno.EBADF, errno.ENOTSOCK}: # EBADF on Unix, ENOTSOCK on Windows raise trio.ClosedResourceError("this socket was already closed") from None else: raise trio.BrokenResourceError( "socket connection broken: {}".format(exc) ) from exc class DBusConnection(Channel): """A plain D-Bus connection with no matching of replies. This doesn't run any separate tasks: sending and receiving are done in the task that calls those methods. It's suitable for implementing servers: several worker tasks can receive requests and send replies. For a typical client pattern, see :class:`DBusRouter`. Implements trio's channel interface for Message objects. """ def __init__(self, socket, enable_fds=False): self.socket = socket self.enable_fds = enable_fds self.parser = Parser() self.outgoing_serial = count(start=1) self.unique_name = None self.send_lock = trio.Lock() self.recv_lock = trio.Lock() self._leftover_to_send = None # type: Optional[memoryview] async def send(self, message: Message, *, serial=None): """Serialise and send a :class:`~.Message` object""" async with self.send_lock: if serial is None: serial = next(self.outgoing_serial) fds = array.array('i') if self.enable_fds else None data = message.serialise(serial, fds=fds) await self._send_data(data, fds) # _send_data is copied & modified from trio's SocketStream.send_all() . # See above for the MIT license. async def _send_data(self, data: bytes, fds): if self.socket.did_shutdown_SHUT_WR: raise trio.ClosedResourceError("can't send data after sending EOF") with _translate_socket_errors_to_stream_errors(): if self._leftover_to_send: # A previous message was partly sent - finish sending it now. await self._send_remainder(self._leftover_to_send) with memoryview(data) as data: if fds: sent = await self.socket.sendmsg([data], [( trio.socket.SOL_SOCKET, trio.socket.SCM_RIGHTS, fds )]) else: sent = await self.socket.send(data) await self._send_remainder(data, sent) async def _send_remainder(self, data: memoryview, already_sent=0): try: while already_sent < len(data): with data[already_sent:] as remaining: sent = await self.socket.send(remaining) already_sent += sent self._leftover_to_send = None except trio.Cancelled: # Sending cancelled mid-message. Keep track of the remaining data # so it can be sent before the next message, otherwise the next # message won't be recognised. self._leftover_to_send = data[already_sent:] raise async def receive(self) -> Message: """Return the next available message from the connection""" async with self.recv_lock: while True: msg = self.parser.get_next_message() if msg is not None: return msg # Once data is read, it must be given to the parser with no # checkpoints (where the task could be cancelled). b, fds = await self._read_data() if not b: raise trio.EndOfChannel("Socket closed at the other end") self.parser.add_data(b, fds) async def _read_data(self): if self.enable_fds: nbytes = self.parser.bytes_desired() with _translate_socket_errors_to_stream_errors(): data, ancdata, flags, _ = await self.socket.recvmsg( nbytes, fds_buf_size() ) if flags & getattr(trio.socket, 'MSG_CTRUNC', 0): self._close() raise RuntimeError("Unable to receive all file descriptors") return data, FileDescriptor.from_ancdata(ancdata) else: # not self.enable_fds with _translate_socket_errors_to_stream_errors(): data = await self.socket.recv(4096) return data, [] def _close(self): self.socket.close() self._leftover_to_send = None # Our closing is currently sync, but AsyncResource objects must have aclose async def aclose(self): """Close the D-Bus connection""" self._close() @asynccontextmanager async def router(self): """Temporarily wrap this connection as a :class:`DBusRouter` To be used like:: async with conn.router() as req: reply = await req.send_and_get_reply(msg) While the router is running, you shouldn't use :meth:`receive`. Once the router is closed, you can use the plain connection again. """ async with trio.open_nursery() as nursery: router = DBusRouter(self) await router.start(nursery) try: yield router finally: await router.aclose() async def open_dbus_connection(bus='SESSION', *, enable_fds=False) -> DBusConnection: """Open a plain D-Bus connection :return: :class:`DBusConnection` """ bus_addr = get_bus(bus) sock : trio.SocketStream = await trio.open_unix_socket(bus_addr) # Authentication authr = Authenticator(enable_fds=enable_fds, inc_null_byte=False) if hasattr(socket, 'SCM_CREDS'): # BSD: send credentials message to authenticate (kernel fills in data) await sock.socket.sendmsg( [b'\0'], [(socket.SOL_SOCKET, socket.SCM_CREDS, bytes(512))] ) else: # Linux: no ancillary data needed, bus checks with SO_PEERCRED await sock.send_all(b'\0') for req_data in authr: await sock.send_all(req_data) authr.feed(await sock.receive_some()) await sock.send_all(BEGIN) conn = DBusConnection(sock.socket, enable_fds=enable_fds) # Say *Hello* to the message bus - this must be the first message, and the # reply gives us our unique name. async with conn.router() as router: reply = await router.send_and_get_reply(message_bus.Hello()) conn.unique_name = reply.body[0] return conn class TrioFilterHandle(FilterHandle): def __init__(self, filters: MessageFilters, rule, send_chn, recv_chn): super().__init__(filters, rule, recv_chn) self.send_channel = send_chn @property def receive_channel(self): return self.queue async def aclose(self): self.close() await self.send_channel.aclose() async def __aenter__(self): return self.queue async def __aexit__(self, exc_type, exc_val, exc_tb): await self.aclose() class Future: """A very simple Future for trio based on `trio.Event`.""" def __init__(self): self._outcome = None self._event = trio.Event() def set_result(self, result): self._outcome = Value(result) self._event.set() def set_exception(self, exc): self._outcome = Error(exc) self._event.set() async def get(self): await self._event.wait() return self._outcome.unwrap() class DBusRouter: """A client D-Bus connection which can wait for replies. This runs a separate receiver task and dispatches received messages. """ _nursery_mgr = None _rcv_cancel_scope = None def __init__(self, conn: DBusConnection): self._conn = conn self._replies = ReplyMatcher() self._filters = MessageFilters() @property def unique_name(self): return self._conn.unique_name async def send(self, message, *, serial=None): """Send a message, don't wait for a reply """ await self._conn.send(message, serial=serial) async def send_and_get_reply(self, message) -> Message: """Send a method call message and wait for the reply Returns the reply message (method return or error message type). """ check_replyable(message) if self._rcv_cancel_scope is None: raise RouterClosed("This DBusRouter has stopped") serial = next(self._conn.outgoing_serial) with self._replies.catch(serial, Future()) as reply_fut: await self.send(message, serial=serial) return (await reply_fut.get()) def filter(self, rule, *, channel: Optional[trio.MemorySendChannel]=None, bufsize=1): """Create a filter for incoming messages Usage:: async with router.filter(rule) as receive_channel: matching_msg = await receive_channel.receive() # OR: send_chan, recv_chan = trio.open_memory_channel(1) async with router.filter(rule, channel=send_chan): matching_msg = await recv_chan.receive() If the channel fills up, The sending end of the channel is closed when leaving the ``async with`` block, whether or not it was passed in. :param jeepney.MatchRule rule: Catch messages matching this rule :param trio.MemorySendChannel channel: Send matching messages here :param int bufsize: If no channel is passed in, create one with this size """ if channel is None: channel, recv_channel = trio.open_memory_channel(bufsize) else: recv_channel = None return TrioFilterHandle(self._filters, rule, channel, recv_channel) # Task management ------------------------------------------- async def start(self, nursery: trio.Nursery): if self._rcv_cancel_scope is not None: raise RuntimeError("DBusRouter receiver task is already running") self._rcv_cancel_scope = await nursery.start(self._receiver) async def aclose(self): """Stop the sender & receiver tasks""" # It doesn't matter if we receive a partial message - the connection # should ensure that whatever is received is fed to the parser. if self._rcv_cancel_scope is not None: self._rcv_cancel_scope.cancel() self._rcv_cancel_scope = None # Ensure trio checkpoint await trio.sleep(0) # Code to run in receiver task ------------------------------------ def _dispatch(self, msg: Message): """Handle one received message""" if self._replies.dispatch(msg): return for filter in self._filters.matches(msg): try: filter.send_channel.send_nowait(msg) except trio.WouldBlock: pass async def _receiver(self, task_status=trio.TASK_STATUS_IGNORED): """Receiver loop - runs in a separate task""" with trio.CancelScope() as cscope: self.is_running = True task_status.started(cscope) try: while True: msg = await self._conn.receive() self._dispatch(msg) finally: self.is_running = False # Send errors to any tasks still waiting for a message. self._replies.drop_all() # Closing a memory channel can't block, but it only has an # async close method, so we need to shield it from cancellation. with trio.move_on_after(3) as cleanup_scope: for filter in self._filters.filters.values(): cleanup_scope.shield = True await filter.send_channel.aclose() class Proxy(ProxyBase): """A trio proxy for calling D-Bus methods You can call methods on the proxy object, such as ``await bus_proxy.Hello()`` to make a method call over D-Bus and wait for a reply. It will either return a tuple of returned data, or raise :exc:`.DBusErrorResponse`. The methods available are defined by the message generator you wrap. :param msggen: A message generator object. :param ~trio.DBusRouter router: Router to send and receive messages. """ def __init__(self, msggen, router): super().__init__(msggen) if not isinstance(router, DBusRouter): raise TypeError("Proxy can only be used with DBusRequester") self._router = router def _method_call(self, make_msg): async def inner(*args, **kwargs): msg = make_msg(*args, **kwargs) assert msg.header.message_type is MessageType.method_call reply = await self._router.send_and_get_reply(msg) return unwrap_msg(reply) return inner @asynccontextmanager async def open_dbus_router(bus='SESSION', *, enable_fds=False): """Open a D-Bus 'router' to send and receive messages. Use as an async context manager:: async with open_dbus_router() as req: ... :param str bus: 'SESSION' or 'SYSTEM' or a supported address. :return: :class:`DBusRouter` This is a shortcut for:: conn = await open_dbus_connection() async with conn: async with conn.router() as req: ... """ conn = await open_dbus_connection(bus, enable_fds=enable_fds) async with conn: async with conn.router() as rtr: yield rtr ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8670292 jeepney-0.9.0/jeepney/low_level.py0000644000000000000000000004675514760131573014136 0ustar00import string import struct from collections import deque from enum import Enum, IntEnum, IntFlag from typing import Optional class SizeLimitError(ValueError): """Raised when trying to (de-)serialise data exceeding D-Bus' size limit. This is currently only implemented for arrays, where the maximum size is 64 MiB. """ pass class Endianness(Enum): little = 1 big = 2 def struct_code(self): return '<' if (self is Endianness.little) else '>' def dbus_code(self): return b'l' if (self is Endianness.little) else b'B' endian_map = {b'l': Endianness.little, b'B': Endianness.big} class MessageType(Enum): method_call = 1 method_return = 2 error = 3 signal = 4 class MessageFlag(IntFlag): no_reply_expected = 1 no_auto_start = 2 allow_interactive_authorization = 4 class HeaderFields(IntEnum): path = 1 interface = 2 member = 3 error_name = 4 reply_serial = 5 destination = 6 sender = 7 signature = 8 unix_fds = 9 def padding(pos, step): pad = step - (pos % step) if pad == step: return 0 return pad class FixedType: def __init__(self, size, struct_code): self.size = self.alignment = size self.struct_code = struct_code def parse_data(self, buf, pos, endianness, fds=()): pos += padding(pos, self.alignment) code = endianness.struct_code() + self.struct_code val = struct.unpack(code, buf[pos:pos + self.size])[0] return val, pos + self.size def serialise(self, data, pos, endianness, fds=None): pad = b'\0' * padding(pos, self.alignment) code = endianness.struct_code() + self.struct_code return pad + struct.pack(code, data) def __repr__(self): return 'FixedType({!r}, {!r})'.format(self.size, self.struct_code) def __eq__(self, other): return (type(other) is FixedType) and (self.size == other.size) \ and (self.struct_code == other.struct_code) class Boolean(FixedType): def __init__(self): super().__init__(4, 'I') # D-Bus booleans take 4 bytes def parse_data(self, buf, pos, endianness, fds=()): val, new_pos = super().parse_data(buf, pos, endianness) return bool(val), new_pos def __repr__(self): return 'Boolean()' def __eq__(self, other): return type(other) is Boolean class FileDescriptor(FixedType): def __init__(self): super().__init__(4, 'I') def parse_data(self, buf, pos, endianness, fds=()): idx, new_pos = super().parse_data(buf, pos, endianness) return fds[idx], new_pos def serialise(self, data, pos, endianness, fds=None): if fds is None: raise RuntimeError("Sending FDs is not supported or not enabled") if hasattr(data, 'fileno'): data = data.fileno() if isinstance(data, bool) or not isinstance(data, int): raise TypeError("Cannot use {data!r} as file descriptor. Expected " "an int or an object with fileno() method") if data < 0: raise ValueError(f"File descriptor can't be negative ({data})") fds.append(data) return super().serialise(len(fds) - 1, pos, endianness) def __repr__(self): return 'FileDescriptor()' def __eq__(self, other): return type(other) is FileDescriptor simple_types = { 'y': FixedType(1, 'B'), # unsigned 8 bit 'n': FixedType(2, 'h'), # signed 16 bit 'q': FixedType(2, 'H'), # unsigned 16 bit 'b': Boolean(), # bool (32-bit) 'i': FixedType(4, 'i'), # signed 32-bit 'u': FixedType(4, 'I'), # unsigned 32-bit 'x': FixedType(8, 'q'), # signed 64-bit 't': FixedType(8, 'Q'), # unsigned 64-bit 'd': FixedType(8, 'd'), # double 'h': FileDescriptor(), # file descriptor (uint32 index in a separate list) } class StringType: def __init__(self, length_type): self.length_type = length_type @property def alignment(self): return self.length_type.size def parse_data(self, buf, pos, endianness, fds=()): length, pos = self.length_type.parse_data(buf, pos, endianness) end = pos + length val = buf[pos:end].decode('utf-8') assert buf[end:end + 1] == b'\0' return val, end + 1 def check_data(self, data): if not isinstance(data, str): raise TypeError("Expected str, not {!r}".format(data)) def serialise(self, data, pos, endianness, fds=None): self.check_data(data) encoded = data.encode('utf-8') len_data = self.length_type.serialise(len(encoded), pos, endianness) return len_data + encoded + b'\0' def __repr__(self): return 'StringType({!r})'.format(self.length_type) def __eq__(self, other): return (type(other) is StringType) \ and (self.length_type == other.length_type) class ObjectPathType(StringType): def __init__(self): super().__init__(simple_types['u']) def check_data(self, data): super().check_data(data) if not data.startswith('/'): raise ValueError(f"Object path ({data!r}) must start with /") if data.endswith('/') and len(data) > 1: raise ValueError(f"Object path ({data!r}) cannot end with /") if '//' in data: raise ValueError(f"Object path ({data!r}) cannot contain double /") valid_chars = string.ascii_letters + string.digits + '/_' if any(c not in valid_chars for c in data): raise ValueError( f"Object path ({data!r}) can only contain A-Z, a-z, 0-9, / and _" ) simple_types.update({ 's': StringType(simple_types['u']), # String 'o': ObjectPathType(), # Object path 'g': StringType(simple_types['y']), # Signature }) class Struct: alignment = 8 def __init__(self, fields): if any(isinstance(f, DictEntry) for f in fields): raise TypeError("Found dict entry outside array") self.fields = fields def parse_data(self, buf, pos, endianness, fds=()): pos += padding(pos, 8) res = [] for field in self.fields: v, pos = field.parse_data(buf, pos, endianness, fds=fds) res.append(v) return tuple(res), pos def serialise(self, data, pos, endianness, fds=None): if not isinstance(data, tuple): raise TypeError("Expected tuple, not {!r}".format(data)) if len(data) != len(self.fields): raise ValueError("{} entries for {} fields".format( len(data), len(self.fields) )) pad = b'\0' * padding(pos, self.alignment) pos += len(pad) res_pieces = [] for item, field in zip(data, self.fields): res_pieces.append(field.serialise(item, pos, endianness, fds=fds)) pos += len(res_pieces[-1]) return pad + b''.join(res_pieces) def __repr__(self): return "{}({!r})".format(type(self).__name__, self.fields) def __eq__(self, other): return (type(other) is type(self)) and (self.fields == other.fields) class DictEntry(Struct): def __init__(self, fields): if len(fields) != 2: raise TypeError( "Dict entry must have 2 fields, not %d" % len(fields)) if not isinstance(fields[0], (FixedType, StringType)): raise TypeError( "First field in dict entry must be simple type, not {}" .format(type(fields[0]))) super().__init__(fields) class Array: alignment = 4 length_type = FixedType(4, 'I') def __init__(self, elt_type): self.elt_type = elt_type def parse_data(self, buf, pos, endianness, fds=()): # print('Array start', pos) length, pos = self.length_type.parse_data(buf, pos, endianness) pos += padding(pos, self.elt_type.alignment) end = pos + length if self.elt_type == simple_types['y']: # Array of bytes return buf[pos:end], end res = [] while pos < end: # print('Array elem', pos) v, pos = self.elt_type.parse_data(buf, pos, endianness, fds=fds) res.append(v) if isinstance(self.elt_type, DictEntry): # Convert list of 2-tuples to dict res = dict(res) return res, pos def serialise(self, data, pos, endianness, fds=None): data_is_bytes = False if isinstance(self.elt_type, DictEntry) and isinstance(data, dict): data = data.items() elif (self.elt_type == simple_types['y']) and isinstance(data, bytes): data_is_bytes = True elif not isinstance(data, list): raise TypeError("Not suitable for array: {!r}".format(data)) # Fail fast if we know in advance that the data is too big: if isinstance(self.elt_type, FixedType): if (self.elt_type.size * len(data)) > 2**26: raise SizeLimitError("Array size exceeds 64 MiB limit") pad1 = padding(pos, self.alignment) pos_after_length = pos + pad1 + 4 pad2 = padding(pos_after_length, self.elt_type.alignment) if data_is_bytes: buf = data else: data_pos = pos_after_length + pad2 limit_pos = data_pos + 2 ** 26 chunks = [] for item in data: chunks.append(self.elt_type.serialise( item, data_pos, endianness, fds=fds )) data_pos += len(chunks[-1]) if data_pos > limit_pos: raise SizeLimitError("Array size exceeds 64 MiB limit") buf = b''.join(chunks) len_data = self.length_type.serialise(len(buf), pos+pad1, endianness) # print('Array ser: pad1={!r}, len_data={!r}, pad2={!r}, buf={!r}'.format( # pad1, len_data, pad2, buf)) return (b'\0' * pad1) + len_data + (b'\0' * pad2) + buf def __repr__(self): return 'Array({!r})'.format(self.elt_type) def __eq__(self, other): return (type(other) is Array) and (self.elt_type == other.elt_type) class Variant: alignment = 1 def parse_data(self, buf, pos, endianness, fds=()): # print('variant', pos) sig, pos = simple_types['g'].parse_data(buf, pos, endianness) # print('variant sig:', repr(sig), pos) valtype = parse_signature(list(sig)) val, pos = valtype.parse_data(buf, pos, endianness, fds=fds) # print('variant done', (sig, val), pos) return (sig, val), pos def serialise(self, data, pos, endianness, fds=None): sig, data = data valtype = parse_signature(list(sig)) sig_buf = simple_types['g'].serialise(sig, pos, endianness) return sig_buf + valtype.serialise( data, pos + len(sig_buf), endianness, fds=fds ) def __repr__(self): return 'Variant()' def __eq__(self, other): return type(other) is Variant def parse_signature(sig): """Parse a symbolic signature into objects. """ # Based on http://norvig.com/lispy.html token = sig.pop(0) if token == 'a': return Array(parse_signature(sig)) if token == 'v': return Variant() elif token == '(': fields = [] while sig[0] != ')': fields.append(parse_signature(sig)) sig.pop(0) # ) return Struct(fields) elif token == '{': de = [] while sig[0] != '}': de.append(parse_signature(sig)) sig.pop(0) # } return DictEntry(de) elif token in ')}': raise ValueError('Unexpected end of struct') else: return simple_types[token] def calc_msg_size(buf): endian, = struct.unpack('c', buf[:1]) endian = endian_map[endian] body_length, = struct.unpack(endian.struct_code() + 'I', buf[4:8]) fields_array_len, = struct.unpack(endian.struct_code() + 'I', buf[12:16]) header_len = 16 + fields_array_len return header_len + padding(header_len, 8) + body_length _header_fields_type = Array(Struct([simple_types['y'], Variant()])) def parse_header_fields(buf, endianness): l, pos = _header_fields_type.parse_data(buf, 12, endianness) return {HeaderFields(k): v[1] for (k, v) in l}, pos header_field_codes = { 1: 'o', 2: 's', 3: 's', 4: 's', 5: 'u', 6: 's', 7: 's', 8: 'g', 9: 'u', } def serialise_header_fields(d, endianness): l = [(i.value, (header_field_codes[i], v)) for (i, v) in sorted(d.items())] return _header_fields_type.serialise(l, 12, endianness) class Header: def __init__(self, endianness, message_type, flags, protocol_version, body_length, serial, fields): """A D-Bus message header It's not normally necessary to construct this directly: use higher level functions and methods instead. """ self.endianness = endianness self.message_type = MessageType(message_type) self.flags = MessageFlag(flags) self.protocol_version = protocol_version self.body_length = body_length self.serial = serial self.fields = fields def __repr__(self): return 'Header({!r}, {!r}, {!r}, {!r}, {!r}, {!r}, fields={!r})'.format( self.endianness, self.message_type, self.flags, self.protocol_version, self.body_length, self.serial, self.fields) def serialise(self, serial=None): s = self.endianness.struct_code() + 'cBBBII' if serial is None: serial = self.serial return struct.pack(s, self.endianness.dbus_code(), self.message_type.value, self.flags, self.protocol_version, self.body_length, serial) \ + serialise_header_fields(self.fields, self.endianness) @classmethod def from_buffer(cls, buf): endian, msgtype, flags, pv = struct.unpack('cBBB', buf[:4]) endian = endian_map[endian] bodylen, serial = struct.unpack(endian.struct_code() + 'II', buf[4:12]) fields, pos = parse_header_fields(buf, endian) return cls(endian, msgtype, flags, pv, bodylen, serial, fields), pos class Message: """Object representing a DBus message. It's not normally necessary to construct this directly: use higher level functions and methods instead. """ def __init__(self, header, body): self.header = header self.body = body def __repr__(self): return "{}({!r}, {!r})".format(type(self).__name__, self.header, self.body) @classmethod def from_buffer(cls, buf: bytes, fds=()) -> 'Message': header, pos = Header.from_buffer(buf) n_fds = header.fields.get(HeaderFields.unix_fds, 0) if n_fds > len(fds): raise ValueError( f"Message expects {n_fds} FDs, but only {len(fds)} were received" ) fds = fds[:n_fds] body = () if HeaderFields.signature in header.fields: sig = header.fields[HeaderFields.signature] body_type = parse_signature(list('(%s)' % sig)) body = body_type.parse_data(buf, pos, header.endianness, fds=fds)[0] return Message(header, body) def serialise(self, serial=None, fds=None) -> bytes: """Convert this message to bytes. Specifying *serial* overrides the ``msg.header.serial`` field, so a connection can use its own serial number without modifying the message. If file-descriptor support is in use, *fds* should be a :class:`array.array` object with type ``'i'``. Any file descriptors in the message will be added to the array. If the message contains FDs, it can't be serialised without this array. """ endian = self.header.endianness if HeaderFields.signature in self.header.fields: sig = self.header.fields[HeaderFields.signature] body_type = parse_signature(list('(%s)' % sig)) body_buf = body_type.serialise(self.body, 0, endian, fds=fds) else: body_buf = b'' self.header.body_length = len(body_buf) if fds: self.header.fields[HeaderFields.unix_fds] = len(fds) header_buf = self.header.serialise(serial=serial) pad = b'\0' * padding(len(header_buf), 8) return header_buf + pad + body_buf class Parser: """Parse DBus messages from a stream of incoming data. """ def __init__(self): self.buf = BufferPipe() self.fds = [] self.next_msg_size = None def add_data(self, data: bytes, fds=()): """Provide newly received data to the parser""" self.buf.write(data) self.fds.extend(fds) def feed(self, data): """Feed the parser newly read data. Returns a list of messages completed by the new data. """ self.add_data(data) return list(iter(self.get_next_message, None)) def bytes_desired(self): """How many bytes can be received without going beyond the next message? This is only used with file-descriptor passing, so we don't get too many FDs in a single recvmsg call. """ got = self.buf.bytes_buffered if got < 16: # The first 16 bytes tell us the message size return 16 - got if self.next_msg_size is None: self.next_msg_size = calc_msg_size(self.buf.peek(16)) return self.next_msg_size - got def get_next_message(self) -> Optional[Message]: """Parse one message, if there is enough data. Returns None if it doesn't have a complete message. """ if self.next_msg_size is None: if self.buf.bytes_buffered >= 16: self.next_msg_size = calc_msg_size(self.buf.peek(16)) nms = self.next_msg_size if (nms is not None) and self.buf.bytes_buffered >= nms: raw_msg = self.buf.read(nms) msg = Message.from_buffer(raw_msg, fds=self.fds) self.next_msg_size = None fds_consumed = msg.header.fields.get(HeaderFields.unix_fds, 0) self.fds = self.fds[fds_consumed:] return msg class BufferPipe: """A place to store received data until we can parse a complete message The main difference from io.BytesIO is that read & write operate at opposite ends, like a pipe. """ def __init__(self): self.chunks = deque() self.bytes_buffered = 0 def write(self, b: bytes): self.chunks.append(b) self.bytes_buffered += len(b) def _peek_iter(self, nbytes: int): assert nbytes <= self.bytes_buffered for chunk in self.chunks: chunk = chunk[:nbytes] nbytes -= len(chunk) yield chunk if nbytes <= 0: break def peek(self, nbytes: int) -> bytes: """Get exactly nbytes bytes from the front without removing them""" return b''.join(self._peek_iter(nbytes)) def _read_iter(self, nbytes: int): assert nbytes <= self.bytes_buffered while True: chunk = self.chunks.popleft() self.bytes_buffered -= len(chunk) if nbytes <= len(chunk): break nbytes -= len(chunk) yield chunk # Final chunk chunk, rem = chunk[:nbytes], chunk[nbytes:] if rem: self.chunks.appendleft(rem) self.bytes_buffered += len(rem) yield chunk def read(self, nbytes: int) -> bytes: """Take & return exactly nbytes bytes from the front""" return b''.join(self._read_iter(nbytes)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.9000287 jeepney-0.9.0/jeepney/tests/__init__.py0000644000000000000000000000000014760131573015014 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8670292 jeepney-0.9.0/jeepney/tests/secrets_introspect.xml0000644000000000000000000001073714760131573017371 0ustar00 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8670292 jeepney-0.9.0/jeepney/tests/test_auth.py0000644000000000000000000000114314760131573015266 0ustar00import pytest from jeepney import auth def test_make_auth_external(): b = auth.make_auth_external() assert b.startswith(b'AUTH EXTERNAL') def test_make_auth_anonymous(): b = auth.make_auth_anonymous() assert b.startswith(b'AUTH ANONYMOUS') def test_parser(): p = auth.SASLParser() p.feed(b'OK 728d62bc2eb394') assert not p.authenticated p.feed(b'1ebbb0b42958b1e0d6\r\n') assert p.authenticated def test_parser_rejected(): p = auth.SASLParser() with pytest.raises(auth.AuthenticationError): p.feed(b'REJECTED EXTERNAL\r\n') assert not p.authenticated ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8670292 jeepney-0.9.0/jeepney/tests/test_bindgen.py0000644000000000000000000000211214760131573015730 0ustar00from io import StringIO import os.path from jeepney.low_level import MessageType, HeaderFields from jeepney.bindgen import code_from_xml sample_file = os.path.join(os.path.dirname(__file__), 'secrets_introspect.xml') def test_bindgen(): with open(sample_file) as f: xml = f.read() sio = StringIO() n_interfaces = code_from_xml(xml, path='/org/freedesktop/secrets', bus_name='org.freedesktop.secrets', fh=sio) # 5 interfaces defined, but we ignore Properties, Introspectable, Peer assert n_interfaces == 2 # Run the generated code, defining the message generator classes. binding_ns = {} exec(sio.getvalue(), binding_ns) Service = binding_ns['Service'] # Check basic functionality of the Service class assert Service.interface == 'org.freedesktop.Secret.Service' msg = Service().SearchItems({"service": "foo", "user": "bar"}) assert msg.header.message_type is MessageType.method_call assert msg.header.fields[HeaderFields.destination] == 'org.freedesktop.secrets' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8670292 jeepney-0.9.0/jeepney/tests/test_bus.py0000644000000000000000000000151714760131573015123 0ustar00import pytest from testpath import modified_env from jeepney import bus def test_get_connectable_addresses(): a = list(bus.get_connectable_addresses('unix:path=/run/user/1000/bus')) assert a == ['/run/user/1000/bus'] a = list(bus.get_connectable_addresses('unix:abstract=/tmp/foo')) assert a == ['\0/tmp/foo'] with pytest.raises(RuntimeError): list(bus.get_connectable_addresses('unix:tmpdir=/tmp')) def test_get_bus(): with modified_env({ 'DBUS_SESSION_BUS_ADDRESS':'unix:path=/run/user/1000/bus', 'DBUS_SYSTEM_BUS_ADDRESS': 'unix:path=/var/run/dbus/system_bus_socket' }): assert bus.get_bus('SESSION') == '/run/user/1000/bus' assert bus.get_bus('SYSTEM') == '/var/run/dbus/system_bus_socket' assert bus.get_bus('unix:path=/run/user/1002/bus') == '/run/user/1002/bus' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8670292 jeepney-0.9.0/jeepney/tests/test_bus_messages.py0000644000000000000000000000641214760131573017011 0ustar00from jeepney import DBusAddress, new_signal, new_method_call from jeepney.bus_messages import MatchRule, message_bus portal = DBusAddress( object_path='/org/freedesktop/portal/desktop', bus_name='org.freedesktop.portal.Desktop', ) portal_req_iface = portal.with_interface('org.freedesktop.portal.Request') def test_match_rule_simple(): rule = MatchRule( type='signal', interface='org.freedesktop.portal.Request', ) assert rule.matches(new_signal(portal_req_iface, 'Response')) # Wrong message type assert not rule.matches(new_method_call(portal_req_iface, 'Boo')) # Wrong interface assert not rule.matches(new_signal( portal.with_interface('org.freedesktop.portal.FileChooser'), 'Response' )) def test_match_rule_path_namespace(): assert MatchRule(path_namespace='/org/freedesktop/portal').matches( new_signal(portal_req_iface, 'Response') ) assert "/freedesktop/" in ( MatchRule(path_namespace='/org/freedesktop/portal').serialise() ) # Prefix but not a parent in the path hierarchy assert not MatchRule(path_namespace='/org/freedesktop/por').matches( new_signal(portal_req_iface, 'Response') ) def test_match_rule_arg(): rule = MatchRule(type='method_call') rule.add_arg_condition(0, 'foo') assert rule.matches(new_method_call( portal_req_iface, 'Boo', signature='s', body=('foo',) )) assert not rule.matches(new_method_call( portal_req_iface, 'Boo', signature='s', body=('foobar',) )) # No such argument assert not rule.matches(new_method_call(portal_req_iface, 'Boo')) def test_match_rule_arg_path(): rule = MatchRule(type='method_call') rule.add_arg_condition(0, '/aa/bb/', kind='path') # Exact match assert rule.matches(new_method_call( portal_req_iface, 'Boo', signature='s', body=('/aa/bb/',) )) # Match a prefix assert rule.matches(new_method_call( portal_req_iface, 'Boo', signature='s', body=('/aa/bb/cc',) )) # Argument is a prefix, ending with / assert rule.matches(new_method_call( portal_req_iface, 'Boo', signature='s', body=('/aa/',) )) # Argument is a prefix, but NOT ending with / assert not rule.matches(new_method_call( portal_req_iface, 'Boo', signature='s', body=('/aa',) )) assert not rule.matches(new_method_call( portal_req_iface, 'Boo', signature='s', body=('/aa/bb',) )) # Not a string assert not rule.matches(new_method_call( portal_req_iface, 'Boo', signature='u', body=(12,) )) def test_match_rule_arg_namespace(): rule = MatchRule(member='NameOwnerChanged') rule.add_arg_condition(0, 'com.example.backend1', kind='namespace') # Exact match assert rule.matches(new_signal( message_bus, 'NameOwnerChanged', 's', ('com.example.backend1',) )) # Parent of the name assert rule.matches(new_signal( message_bus, 'NameOwnerChanged', 's', ('com.example.backend1.foo.bar',) )) # Prefix but not a parent in the namespace assert not rule.matches(new_signal( message_bus, 'NameOwnerChanged', 's', ('com.example.backend12',) )) # Not a string assert not rule.matches(new_signal( message_bus, 'NameOwnerChanged', 'u', (1,) )) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8670292 jeepney-0.9.0/jeepney/tests/test_fds.py0000644000000000000000000000343514760131573015107 0ustar00import errno import os import socket import pytest from jeepney import FileDescriptor, NoFDError def assert_not_fd(fd: int): """Check that the given number is not open as a file descriptor""" with pytest.raises(OSError) as exc_info: os.stat(fd) assert exc_info.value.errno == errno.EBADF def test_close(tmp_path): fd = os.open(tmp_path / 'a', os.O_CREAT | os.O_RDWR) with FileDescriptor(fd) as wfd: assert wfd.fileno() == fd # Leaving the with block is equivalent to calling .close() assert 'closed' in repr(wfd) with pytest.raises(NoFDError): wfd.fileno() assert_not_fd(fd) def test_to_raw_fd(tmp_path): fd = os.open(tmp_path / 'a', os.O_CREAT) wfd = FileDescriptor(fd) assert wfd.fileno() == fd assert wfd.to_raw_fd() == fd try: assert 'converted' in repr(wfd) with pytest.raises(NoFDError): wfd.fileno() finally: os.close(fd) def test_to_file(tmp_path): fd = os.open(tmp_path / 'a', os.O_CREAT | os.O_RDWR) wfd = FileDescriptor(fd) with wfd.to_file('w') as f: assert f.write('abc') assert 'converted' in repr(wfd) with pytest.raises(NoFDError): wfd.fileno() assert_not_fd(fd) # Check FD was closed by file object assert (tmp_path / 'a').read_text() == 'abc' def test_to_socket(): s1, s2 = socket.socketpair() try: s1.sendall(b'abcd') sfd = s2.detach() wfd = FileDescriptor(sfd) with wfd.to_socket() as sock: b = sock.recv(16) assert b and b'abcd'.startswith(b) assert 'converted' in repr(wfd) with pytest.raises(NoFDError): wfd.fileno() assert_not_fd(sfd) # Check FD was closed by socket object finally: s1.close() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8670292 jeepney-0.9.0/jeepney/tests/test_low_level.py0000644000000000000000000000567014760131573016326 0ustar00import pytest from jeepney.low_level import * HELLO_METHOD_CALL = ( b'l\x01\x00\x01\x00\x00\x00\x00\x01\x00\x00\x00m\x00\x00\x00\x01\x01o\x00\x15' b'\x00\x00\x00/org/freedesktop/DBus\x00\x00\x00\x02\x01s\x00\x14\x00\x00\x00' b'org.freedesktop.DBus\x00\x00\x00\x00\x03\x01s\x00\x05\x00\x00\x00Hello\x00' b'\x00\x00\x06\x01s\x00\x14\x00\x00\x00org.freedesktop.DBus\x00\x00\x00\x00') def test_parser_simple(): msg = Parser().feed(HELLO_METHOD_CALL)[0] assert msg.header.fields[HeaderFields.member] == 'Hello' def chunks(src, size): pos = 0 while pos < len(src): end = pos + size yield src[pos:end] pos = end def test_parser_chunks(): p = Parser() chunked = list(chunks(HELLO_METHOD_CALL, 16)) for c in chunked[:-1]: assert p.feed(c) == [] msg = p.feed(chunked[-1])[0] assert msg.header.fields[HeaderFields.member] == 'Hello' def test_multiple(): msgs = Parser().feed(HELLO_METHOD_CALL * 6) assert len(msgs) == 6 for msg in msgs: assert msg.header.fields[HeaderFields.member] == 'Hello' def test_roundtrip(): msg = Parser().feed(HELLO_METHOD_CALL)[0] assert msg.serialise() == HELLO_METHOD_CALL def test_serialise_dict(): data = { 'a': 'b', 'de': 'f', } string_type = simple_types['s'] sig = Array(DictEntry([string_type, string_type])) print(sig.serialise(data, 0, Endianness.little)) assert sig.serialise(data, 0, Endianness.little) == ( b'\x1e\0\0\0' + # Length b'\0\0\0\0' + # Padding b'\x01\0\0\0a\0\0\0' + b'\x01\0\0\0b\0\0\0' + b'\x02\0\0\0de\0\0' + b'\x01\0\0\0f\0' ) def test_parse_signature(): sig = parse_signature(list('(a{sv}(oayays)b)')) print(sig) assert sig == Struct([ Array(DictEntry([simple_types['s'], Variant()])), Struct([ simple_types['o'], Array(simple_types['y']), Array(simple_types['y']), simple_types['s'] ]), simple_types['b'], ]) class fake_list(list): def __init__(self, n): super().__init__() self._n = n def __len__(self): return self._n def __iter__(self): return iter(range(self._n)) def test_array_limit(): # The spec limits arrays to 64 MiB a = Array(FixedType(8, 'Q')) # 'at' - array of uint64 a.serialise(fake_list(100), 0, Endianness.little) with pytest.raises(SizeLimitError): a.serialise(fake_list(2**23 + 1), 0, Endianness.little) def test_bad_object_path(): with pytest.raises(ValueError): ObjectPathType().check_data('org/freedesktop/DBus') with pytest.raises(ValueError): ObjectPathType().check_data('/org/freedesktop/DBus/') with pytest.raises(ValueError): ObjectPathType().check_data('/org//freedesktop/DBus') with pytest.raises(ValueError): ObjectPathType().check_data('/org/freedesktop/DBüs') # Non-ASCII character ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8670292 jeepney-0.9.0/jeepney/tests/test_wrappers.py0000644000000000000000000000423214760131573016172 0ustar00import pytest from jeepney.wrappers import * def test_bad_bus_name(): obj = '/com/example/foo' DBusAddress(obj, 'com.example.a') # Valid (well known name) DBusAddress(obj, 'com.example.a-b') # Valid but discouraged DBusAddress(obj, ':1.13') # Valid (unique name) with pytest.raises(ValueError, match='too long'): DBusAddress(obj, 'com.example.' + ('a' * 256)) with pytest.raises(ValueError): DBusAddress(obj, '.com.example.a') with pytest.raises(ValueError): DBusAddress(obj, 'com..example.a') with pytest.raises(ValueError): DBusAddress(obj, 'com.2example.a') with pytest.raises(ValueError): DBusAddress(obj, 'cöm.example.a') # Non-ASCII character with pytest.raises(ValueError): DBusAddress(obj, 'com') def test_bad_interface(): obj = '/com/example/foo' busname = 'com.example.foo' DBusAddress(obj, 'com.example.a', 'com.example.a_b') # Valid with pytest.raises(ValueError, match='too long'): DBusAddress(obj, 'com.example.a', 'com.example.' + ('a' * 256)) with pytest.raises(ValueError): DBusAddress(obj, 'com.example.a', 'com.example.a-b') # No hyphens with pytest.raises(ValueError): DBusAddress(obj, busname, '.com.example.a') with pytest.raises(ValueError): DBusAddress(obj, busname, 'com..example.a') with pytest.raises(ValueError): DBusAddress(obj, busname, 'com.2example.a') with pytest.raises(ValueError): DBusAddress(obj, busname, 'cöm.example.a') # Non-ASCII character with pytest.raises(ValueError): DBusAddress(obj, busname, 'com') def test_bad_member_name(): addr = DBusAddress( '/org/freedesktop/DBus', bus_name='org.freedesktop.DBus', interface='org.freedesktop.DBus', ) new_method_call(addr, 'Hello') with pytest.raises(ValueError, match='too long'): new_method_call(addr, 'Hell' + ('o' * 256)) with pytest.raises(ValueError): new_method_call(addr, 'org.Hello') with pytest.raises(ValueError): new_method_call(addr, '9Hello') with pytest.raises(ValueError): new_method_call(addr, '') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8670292 jeepney-0.9.0/jeepney/wrappers.py0000644000000000000000000002260514760131573013775 0ustar00import re from typing import Union from warnings import warn from .low_level import * __all__ = [ 'DBusAddress', 'new_method_call', 'new_method_return', 'new_error', 'new_signal', 'MessageGenerator', 'Properties', 'Introspectable', 'DBusErrorResponse', ] bus_name_pat = re.compile( r'([A-Za-z_-][A-Za-z0-9_-]*(\.[A-Za-z_-][A-Za-z0-9_-]*)+' # Well known name r'|:[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+))$', # Unique name ) def check_bus_name(name): if len(name) > 255: abbr = name[:8] + '...' raise ValueError(f"Bus name ({abbr!r}) is too long (> 255 characters)") if not bus_name_pat.match(name): raise ValueError(f"Bus name ({name!r}) is not valid") interface_pat = re.compile(r'[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)+$') def check_interface(name): if len(name) > 255: abbr = name[:8] + '...' raise ValueError(f"Interface name ({abbr!r}) is too long (> 255 characters)") if not interface_pat.match(name): raise ValueError(f"Interface name ({name!r}) is not valid") member_name_pat = re.compile(r'[A-Za-z_][A-Za-z0-9_]*$') def check_member_name(name): if len(name) > 255: abbr = name[:8] + '...' raise ValueError(f"Member name ({abbr!r}) is too long (> 255 characters)") if not member_name_pat.match(name): raise ValueError(f"Member name ({name!r} is not valid") class DBusAddress: """This identifies the object and interface a message is for. e.g. messages to display desktop notifications would have this address:: DBusAddress('/org/freedesktop/Notifications', bus_name='org.freedesktop.Notifications', interface='org.freedesktop.Notifications') """ def __init__(self, object_path, bus_name=None, interface=None): ObjectPathType().check_data(object_path) self.object_path = object_path if bus_name is not None: check_bus_name(bus_name) self.bus_name = bus_name if interface is not None: check_interface(interface) self.interface = interface def __repr__(self): return '{}({!r}, bus_name={!r}, interface={!r})'.format(type(self).__name__, self.object_path, self.bus_name, self.interface) def with_interface(self, interface): check_interface(interface) return type(self)(self.object_path, self.bus_name, interface) class DBusObject(DBusAddress): def __init__(self, object_path, bus_name=None, interface=None): super().__init__(object_path, bus_name, interface) warn('Deprecated alias, use DBusAddress instead', stacklevel=2) def new_header(msg_type): return Header(Endianness.little, msg_type, flags=0, protocol_version=1, body_length=-1, serial=-1, fields={}) def new_method_call(remote_obj, method, signature=None, body=()): """Construct a new method call message This is a relatively low-level method. In many cases, this will be called from a :class:`MessageGenerator` subclass which provides a more convenient API. :param DBusAddress remote_obj: The object to call a method on :param str method: The name of the method to call :param str signature: The DBus signature of the body data :param tuple body: Body data (i.e. method parameters) """ check_member_name(method) header = new_header(MessageType.method_call) header.fields[HeaderFields.path] = remote_obj.object_path if remote_obj.bus_name is None: raise ValueError("remote_obj.bus_name cannot be None for method calls") header.fields[HeaderFields.destination] = remote_obj.bus_name if remote_obj.interface is not None: header.fields[HeaderFields.interface] = remote_obj.interface header.fields[HeaderFields.member] = method if signature is not None: header.fields[HeaderFields.signature] = signature return Message(header, body) def new_method_return(parent_msg, signature=None, body=()): """Construct a new response message :param Message parent_msg: The method call this is a reply to :param str signature: The DBus signature of the body data :param tuple body: Body data """ header = new_header(MessageType.method_return) header.fields[HeaderFields.reply_serial] = parent_msg.header.serial sender = parent_msg.header.fields.get(HeaderFields.sender, None) if sender is not None: header.fields[HeaderFields.destination] = sender if signature is not None: header.fields[HeaderFields.signature] = signature return Message(header, body) def new_error(parent_msg, error_name, signature=None, body=()): """Construct a new error response message :param Message parent_msg: The method call this is a reply to :param str error_name: The name of the error :param str signature: The DBus signature of the body data :param tuple body: Body data """ header = new_header(MessageType.error) header.fields[HeaderFields.reply_serial] = parent_msg.header.serial header.fields[HeaderFields.error_name] = error_name sender = parent_msg.header.fields.get(HeaderFields.sender, None) if sender is not None: header.fields[HeaderFields.destination] = sender if signature is not None: header.fields[HeaderFields.signature] = signature return Message(header, body) def new_signal(emitter, signal, signature=None, body=()): """Construct a new signal message :param DBusAddress emitter: The object sending the signal :param str signal: The name of the signal :param str signature: The DBus signature of the body data :param tuple body: Body data """ check_member_name(signal) header = new_header(MessageType.signal) header.fields[HeaderFields.path] = emitter.object_path if emitter.interface is None: raise ValueError("emitter.interface cannot be None for signals") header.fields[HeaderFields.interface] = emitter.interface header.fields[HeaderFields.member] = signal if signature is not None: header.fields[HeaderFields.signature] = signature return Message(header, body) class MessageGenerator: """Subclass this to define the methods available on a DBus interface. jeepney.bindgen can automatically create subclasses using introspection. """ interface: Optional[str] = None def __init__(self, object_path, bus_name): ObjectPathType().check_data(object_path) check_bus_name(bus_name) if self.interface is not None: check_interface(self.interface) self.object_path = object_path self.bus_name = bus_name def __repr__(self): return "{}({!r}, bus_name={!r})".format(type(self).__name__, self.object_path, self.bus_name) class ProxyBase: """A proxy is an IO-aware wrapper around a MessageGenerator Calling methods on a proxy object will send a message and wait for the reply. This is a base class for proxy implementations in jeepney.io. """ def __init__(self, msggen): self._msggen = msggen def __getattr__(self, item): if item.startswith('__'): raise AttributeError(item) make_msg = getattr(self._msggen, item, None) if callable(make_msg): return self._method_call(make_msg) raise AttributeError(item) def _method_call(self, make_msg): raise NotImplementedError("Needs to be implemented in subclass") class Properties: """Build messages for accessing object properties If a D-Bus object has multiple interfaces, each interface has its own set of properties. This uses the standard DBus interface ``org.freedesktop.DBus.Properties`` """ def __init__(self, obj: Union[DBusAddress, MessageGenerator]): self.obj = obj self.props_if = DBusAddress(obj.object_path, bus_name=obj.bus_name, interface='org.freedesktop.DBus.Properties') def get(self, name): """Get the value of the property *name*""" return new_method_call(self.props_if, 'Get', 'ss', (self.obj.interface, name)) def get_all(self): """Get all property values for this interface""" return new_method_call(self.props_if, 'GetAll', 's', (self.obj.interface,)) def set(self, name, signature, value): """Set the property *name* to *value* (with appropriate signature)""" return new_method_call(self.props_if, 'Set', 'ssv', (self.obj.interface, name, (signature, value))) class Introspectable(MessageGenerator): interface = 'org.freedesktop.DBus.Introspectable' def Introspect(self): """Request D-Bus introspection XML for a remote object""" return new_method_call(self, 'Introspect') class DBusErrorResponse(Exception): """Raised by proxy method calls when the reply is an error message""" def __init__(self, msg): self.name = msg.header.fields.get(HeaderFields.error_name) self.data = msg.body def __str__(self): return '[{}] {}'.format(self.name, self.data) def unwrap_msg(msg: Message): """Get the body of a message, raising DBusErrorResponse for error messages This is to be used with replies to method_call messages, which may be method_return or error. """ if msg.header.message_type == MessageType.error: raise DBusErrorResponse(msg) return msg.body ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8670292 jeepney-0.9.0/pyproject.toml0000644000000000000000000000150214760131573013026 0ustar00[build-system] requires = ["flit_core >=3.11,<4"] build-backend = "flit_core.buildapi" [project] name = "jeepney" authors = [ {name = "Thomas Kluyver", email = "thomas@kluyver.me.uk"}, ] readme = "README.rst" requires-python = ">=3.7" license = "MIT" license-files = ["LICENSE"] classifiers = [ "Programming Language :: Python :: 3", "Topic :: Desktop Environment" ] dynamic = ["version", "description"] [project.optional-dependencies] test = [ "pytest", "pytest-trio", "pytest-asyncio >=0.17", "testpath", "trio", "async-timeout; python_version < '3.11'", ] trio = [ "trio", ] [project.urls] Documentation = "https://jeepney.readthedocs.io/en/latest/" Source = "https://gitlab.com/takluyver/jeepney" [tool.flit.sdist] include = ["docs", "examples", "pytest.ini"] exclude = ["docs/_build"] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740682106.8670292 jeepney-0.9.0/pytest.ini0000644000000000000000000000003714760131573012145 0ustar00[pytest] asyncio_mode = strict jeepney-0.9.0/PKG-INFO0000644000000000000000000000231600000000000011145 0ustar00Metadata-Version: 2.4 Name: jeepney Version: 0.9.0 Summary: Low-level, pure Python DBus protocol wrapper. Author-email: Thomas Kluyver Requires-Python: >=3.7 Description-Content-Type: text/x-rst License-Expression: MIT Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Desktop Environment License-File: LICENSE Requires-Dist: pytest ; extra == "test" Requires-Dist: pytest-trio ; extra == "test" Requires-Dist: pytest-asyncio >=0.17 ; extra == "test" Requires-Dist: testpath ; extra == "test" Requires-Dist: trio ; extra == "test" Requires-Dist: async-timeout ; extra == "test" and ( python_version < '3.11') Requires-Dist: trio ; extra == "trio" Project-URL: Documentation, https://jeepney.readthedocs.io/en/latest/ Project-URL: Source, https://gitlab.com/takluyver/jeepney Provides-Extra: test Provides-Extra: trio Jeepney is a pure Python implementation of D-Bus messaging. It has an `I/O-free `__ core, and integration modules for different event loops. D-Bus is an inter-process communication system, mainly used in Linux. To install Jeepney:: pip install jeepney `Jeepney docs on Readthedocs `__