pax_global_header00006660000000000000000000000064147541251540014522gustar00rootroot0000000000000052 comment=7f15b5c2ed2fde189d87880dab83bc3d4075862b pyotherside-1.6.2/000077500000000000000000000000001475412515400140675ustar00rootroot00000000000000pyotherside-1.6.2/.github/000077500000000000000000000000001475412515400154275ustar00rootroot00000000000000pyotherside-1.6.2/.github/FUNDING.yml000066400000000000000000000002121475412515400172370ustar00rootroot00000000000000# These are supported funding model platforms github: thp ko_fi: thpx86 custom: https://www.amazon.de/gp/registry/wishlist/2PD2MYGHE6857 pyotherside-1.6.2/.github/workflows/000077500000000000000000000000001475412515400174645ustar00rootroot00000000000000pyotherside-1.6.2/.github/workflows/build.yaml000066400000000000000000000015431475412515400214520ustar00rootroot00000000000000name: Build and test on: push: branches: - master pull_request: branches: - master jobs: build: strategy: fail-fast: false matrix: os: - ubuntu-latest - macos-latest runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - name: Install dependencies (Ubuntu) if: matrix.os == 'ubuntu-latest' run: | sudo apt install -y \ python3-dev \ qt5-qmake qtbase5-dev qtdeclarative5-dev libqt5svg5-dev - name: Install dependencies (macOS) if: matrix.os == 'macos-latest' run: | brew install qt@5 python@3.12 echo PATH=/opt/homebrew/opt/qt@5/bin:/usr/local/opt/qt@5/bin:$PATH >> ${GITHUB_ENV} - run: qmake - run: make - run: env QT_QPA_PLATFORM=offscreen tests/tests pyotherside-1.6.2/.github/workflows/docs.yaml000066400000000000000000000004521475412515400213010ustar00rootroot00000000000000name: Documentation on: push: branches: - master pull_request: branches: - master jobs: docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - run: sudo apt install -y python3-sphinx python3-sphinx-rtd-theme - run: make -C docs html pyotherside-1.6.2/.gitignore000066400000000000000000000004531475412515400160610ustar00rootroot00000000000000*.dylib *.so *.o /.qmake.stash /Makefile /src/Makefile /src/moc_*.cpp /src/moc_predefs.h /src/qrc_qrc_importer.cpp /tests/Makefile /tests/moc_*.cpp /tests/moc_predefs.h /tests/tests /qtquicktests/Makefile /qtquicktests/qtquicktests /qtquicktests/target_wrapper.sh /examples/__pycache__ /docs/_build pyotherside-1.6.2/.readthedocs.yaml000066400000000000000000000002441475412515400173160ustar00rootroot00000000000000version: 2 build: os: ubuntu-22.04 tools: python: "3.12" python: install: - requirements: docs/requirements.txt sphinx: configuration: docs/conf.py pyotherside-1.6.2/LICENSE000066400000000000000000000014741475412515400151020ustar00rootroot00000000000000PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 ISC License Copyright (c) 2011, 2013-2025, Thomas Perl Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. pyotherside-1.6.2/README.md000066400000000000000000000034141475412515400153500ustar00rootroot00000000000000PyOtherSide: Python 3 QML Plugin for Qt 5 and Qt 6 ================================================== [![Build and test](https://github.com/thp/pyotherside/actions/workflows/build.yaml/badge.svg)](https://github.com/thp/pyotherside/actions/workflows/build.yaml) [![Documentation](https://readthedocs.org/projects/pyotherside/badge/?version=latest&style=flat)](https://pyotherside.readthedocs.io/en/latest/) A Qt plugin providing access to a Python 3 interpreter from QML for creating asynchronous mobile and Desktop UIs with Python. Requirements ------------ * Qt 5.1.0 or newer (Qt 6.x also supported) * Python 3.8.0 or newer Building -------- To build and install the QML plugin: ``` qmake # use "qmake6" for Qt 6 make make install ``` To build against a specific Python version, use: ``` qmake PYTHON_CONFIG=python3.8-config # use "qmake6" for Qt 6 make make install ``` To manually update the qmltypes file on x64 Linux (TODO: make this automated): ``` qmake # use "qmake6" for Qt 6 make make INSTALL_ROOT=$(pwd)/tmp/ QML2_IMPORT_PATH=$(pwd)/tmp/usr/lib/x86_64-linux-gnu/qt5/qml \ make -C src qmltypes ``` Unit Testing ------------ To run the included unit tests after building, use: ``` ./tests/tests ``` Static Linking -------------- If you want to link PyOtherSide statically against Python 3, you can include the Python Standard Library in PyOtherSide as Qt Resource and have it extracted automatically on load, for this, zip up the Standard Library and place the .zip file as "pythonlib.zip" into src/ before running qmake. More information ---------------- - Project page: https://thp.io/2011/pyotherside/ - Git repo: http://github.com/thp/pyotherside/ - Bug tracker: https://github.com/thp/pyotherside/issues - Documentation: http://pyotherside.readthedocs.org/ pyotherside-1.6.2/docs/000077500000000000000000000000001475412515400150175ustar00rootroot00000000000000pyotherside-1.6.2/docs/Makefile000066400000000000000000000127201475412515400164610ustar00rootroot00000000000000# 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 clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 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 " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @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 " 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 " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." 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." 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/PyOtherSide.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PyOtherSide.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/PyOtherSide" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PyOtherSide" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 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)." 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." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 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)." 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." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." 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." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." pyotherside-1.6.2/docs/conf.py000066400000000000000000000171571475412515400163310ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # PyOtherSide documentation build configuration file, created by # sphinx-quickstart on Wed Feb 5 22:01:34 2014. # # 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. import sys, os # 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. #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_rtd_theme'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. 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 = u'PyOtherSide' copyright = u'2014-2025 Thomas Perl' # 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. version = '1.6' # The full version, including alpha/beta/rc tags. release = '1.6.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #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. exclude_patterns = ['_build'] # 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 = [] # -- 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. If None, it defaults to # " v documentation". #html_title = None # 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 (within the static path) to use as 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'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # 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 # Output file base name for HTML help builder. htmlhelp_basename = 'PyOtherSidedoc' # -- 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': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'PyOtherSide.tex', u'PyOtherSide Documentation', u'Thomas Perl', '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 = [] # 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 = [ ('index', 'pyotherside', u'PyOtherSide Documentation', [u'Thomas Perl'], 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 = [ ('index', 'PyOtherSide', u'PyOtherSide Documentation', u'Thomas Perl', 'PyOtherSide', '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' pyotherside-1.6.2/docs/images/000077500000000000000000000000001475412515400162645ustar00rootroot00000000000000pyotherside-1.6.2/docs/images/image_provider_example.png000066400000000000000000002165611475412515400235140ustar00rootroot00000000000000PNG  IHDR,,"sRGB pHYs  tIME W IDATxڴgw$ו-=P,%Lt[=Nt?֕(Jd{r>7ύHV-Hdĉc9mȋ 729dx26O2ΐe0w3_22ÇS3lexd^~>ÿdU3 Û 739f28 e txÅ 2,d/3e g3f4å dq d&3g ?n "õ >dXu?3&g3t鯻 S.dP˰~2?23C3LfȰqU >p&HZ] 2p*Co 2 p+ÿfMd8a 3p]e'_dx{'5ï3|a:C-J=ÿd]g8a(AWW/g2&2te ÷> ~ 7= [ߐߐ[gN7 W>tЦp7-3~.# Q#Dh'Gġ9ͣGuF<':m.pt$:C:%:9mD84G:ѩ~;=b!2ˣP#*D8 mb&.Ny:tuh#Hl+1L31Hԉ#bxM<$q'b"*qx~A7=e;N  1O#EbJ't}1B Aw^k&8i':.!^[ EQ8!#mJ|yIvE5s:nE1#F~)wyy#=cQZ*#C vŀ̈́1Jm #p5}yH'ԦY='KtU"=2i6&ҚI!қLcRMAҚb8dUbtSGlKq"&!"#ڈbxNp'>'> N+[v),1N!`bCa(vyQAة/<پ(䊚GKRMToRّOKr 4&E1L6<\̈́X&b2aіAhl4DZgi"i#M]i 7jbM1DM5&Ц%-Glub8KXGX}K81DE,O{ub8OÊ>1Lu.qVW!K qxk ^ u!Uʺ9|{L e3[1Nd#e`'VbŸWJV60k avx GUf H Ƙ BT=T1fELiw ?lU Wi_a:ފ\1Ub8p"m8C5Kd:'4TùIl<9t~:t(R 3?&aN(jᖉ~ж-.K'S5[h L}bX q8M2~{,_xBWIZ! QtT +B!iLonnh †T86b=2GG Qzr :l lwiwvFVGqyhtPv/+1L Be!X#Ӛv݂«V Np<8~5NM95~(E?F)N9-mo^xQ /2:uGo!)c̞S8 .9#јi|b^V=J#-}QgnZ8v;nf{ǣoxDxF ~3 ǁ PKˊĒ|H/DBHͼ $6: ҫ"<\rZam aN@ /߆O+ Gor\6&m~iq7cVJ`^Ҷ=8֥>.Wn1^FeޡqM_co0&ď J{Vٿ-^)[?q­`[_撇"N|s-w9ޮ!qObYB R9_=M6P*"Z] K-?ڎI~m`nioPcChՆZK'\9,ێj ʞB]hu 5[!^%E-9p5 }yCBi>Ĩ0-@c*෇i-}QG:eqT*"ZC_i;I F'TuJ-AGy[;vעf!1U41p GյPfEbX&V-ˣaH}vh:UkڡfBcO-k11fǍמ0+ ?I[-6y ͗ ÜG%{B҄rC0k3hyPTC]`.07 Xw ]1H# ⅈ2]"ֈ' <%^2BD)@(7V]%^]di -X;oY-PQm>njma89R=n(b%*ЮV~?M2WRܣ~Ax2GB:j[(:!d?G?D=*ҦmIt tؖ `qm: ] 65zbN,ĺTW!LjY񂸡KĴ.ψ;MKɦOrX'Eb-v92R0]1╘K;e"$pߒ [ wZZI~$۹a֒J}R- *3E&6\1۴HWmՑkAhC* *P+j{oɈ =Z0}CH{ (q*ߤv~/Z6#U~Nz ".g w5x1IDhwAWPPlyl8Ggu)NS+P"VYDR"=sJ֑v(iP-RJi"=Y^N5%Ҋ+Jȸ]c:B65` j)/GOPaz;MeUCe.Nb&̤^]UEbJF qxx8Cv^}J<$gybJ5#[ @!Ĩ)Vە6 O Pː'Ѳ_ALV\QZP7:^moH;[R7 j!'n21{b1"fI8K#eZdVTjmc5ɚF;;;hnzlHb}՜RDK THĄǼ9+1*v,'azQbxĶǡˣsb"f)Јw5sy<'UNXSAH%abPH[nHX+-Ed9?/<*)88L;I-qJL(g=F2Ķ؞OI8!M1 jh)W5 jm"=\npW WO=mRxZ6H trnBJX- H #,=b(Wk9bZ9j]bfg'ӡ{.172{! E%bXӄf&cĄ`Eb]X Ɖqx,n@׸Tr J\2D:9S9m=捀[!%A|BG!&> exD|A̘_ }!_Xꚾ'kX]I;z^D1ϞƩS%ĆzseJV43Gf,6h@Z6=LxBT=:\e_ ̳,C2KG#wfىB"{ )<\3#Xgfu+rS fY.gxǂsKwk#s%f#0ӆY'5[2DkX^J8R[fOuIȸEuy9l }1T#l 11Ct*V?59R1Wq-?,DY36vOͶx츦e=OX(KY a-3ObpC8~r[ebYuc&l/兴!iG#) P/Yty D(ۄ@zir]*C"!ʆJ(š-8=랩6͌W PޚǶ8[uJ (g\ ~b8E"^JEnWO, 03pIAL56A|B|npXq#mS/QĜpˡؗ۴XqBC oCgI,k}' zKSh0Wpda46M.H n`kݼ4ݘH8R  `!,=/N) ^dsKQlfY.8+J{7 ׏)8Up^!^ J]8L{t #haxKLnBLkMx&Lo4ZR:2Z7iOĔZ}bxibuBQ3yBÌ*[ ^OL.X@ duS21[4D٥Dz5;fǮEJk #i&,xI7JIKW<VL&̈c[u.N fOZ:.{t<W[G-N`>W"+|gωs9a;!o{\wFB3)eCbX ib2Ǟ&^!&MZ\tMr?].YL!c ($=z]ðWb10&H$u͇`)vm-ySi˩O'=vva T4ZR(m*k7rn3;Nz \ NWy?q]Õm %bّƂS  @ u3sީ]+sⱦ/Ɂ˩)ĄԃWA8K!B{Sf\}J,}41OsĬ v&qL\ .n4cط 91<&qƲq3Y'lbuԸ0?5wS뎆:ćx۪f :RK`-:b>qfiZV qY{lL6vIN+3nN'o䝴\a"q#ޚzօz :<2VgIN ̣q{Tw[.$úb%nψČ9Ǵk"zI]/"L;Gё~bTCWě3uǸzLj81bv*> #VWlWƆǂWQRL)U"S%:<+7jT.ÓHPVd;OOytXX:thq קPZ=H=]sgBT>0Eqv&1Ô=) z`6(JIS3PpVt*K"[w[T'~|fS.bzO(ȜުCipQ@PZJjOM 7[4}jz rI By+a$KW+V҂8Mf^GAw *8c)H}T({5 7E\C]qtoa Qm%hE%ÜꥺاFT&Ìd++H", KP'="_~Nc}ⴠ6ҼdNvMWF0ipC5(fvD!]C1C.?d[8Y  e'{ʄ=&Ƭ]q  g2 麴iq%kcM0  (Bg5F5,}İǘ ESpvݒ@p]>'ikS_u aM=]͎0xK!n){ڿiP7bvxD<&njj#9z k#koEωI 4q8E?Һ*%3ħć9b0TWlM3=l=]M[PLcbc:0833 /{*V=޸fڧ3ՅXmZ=I)VO.aZ&vŮ"k)b^M5'R4j6MFn-[f| ׊M';r|ֶ"SN3vX)`*ƒ *w9@u)GtQ#%@#fhad7le݌pC_\[Dқ#'EѪ:1IϴR6$Y c<10:cwhis(3b?#։VőX7O5"/h&4.EEm"ʼnO-݉A؄>k!o}b N) k4g_xn)7%RWRH[Q:HzL&zUqC0ÒHDǰ yKM7!k YvZS;4ڈo%vS`H|׿x㰠qCd2:x:͌ғo$3_\;/[h.uYqֻǦk!8gEvMn݆q?4ۊ+]XjҏSx䰕>RKj)V#隤Oi:_2Wf + Bipu_ݝ~%m-FRgܻ؄G[MO>)aY^&TU4C<ӄf*%yFuTvJ0C!K8^%(lv;5-0 N۠fˊ MuLz+ xO;>)&^&>!>%>&3Cf)qA\%F tNCxL#?hԤ'/-8Lix%gH\U0bhY)ڔ%}Ր'ئLd\JYq4aD gI_30>z|gQ["u,8[ǔSp.nĦ:'-q3yd./{qߔGd nF 1bl8m/u9:%Vg=XBE{W"kF=q91^Cv{7O9W.OcN(%ωibtb:dLLmCf#sGG="I;G5BDB5Vapo(Q?5b\{dt84E,V+"m;qmZbiE|N8; zDwM3ܡR_4ZV[^ǒúK༮sE׻C)bS3͢˺cd8' [p"uuV 6҅pl 50R!480+Y^PJ?89.{4QЃ%.*521H|ĦSF,D dSVE'K61 ec&M{5*zPXd boėLhxY՟jb-96XVvUf+ 3Ψz;j pZLE}AU䰡؈gp|d8ekz˯Ĵ.CݐR#<ƴ1f84.fv2B N/ėćǡոfj|bي!G)9&P=#v։0L88erC\oM0qhğUY}u؍EqE y[ѺrH@HTDLzmN*0GrX$~.}-rD)tR8G-M'"H|JpI>c&u38T,04Ӓ1c) {BƂ["8>-8n(8].;A )9%yF&:'yz,|Za<0(>K/G0KW=<szbE"5oδ17'f6dA#zEyyLf_gў,X.Xfi{1SWecFg!!Y=&.ǔ˫8b xH<tC6Y@v{K!=F\jPU bV(:v!k1J7 %u']U .O21k5|b DCbX~fTU[V-hA7F4LHf +ik1QUb4Bb9cMmxWyǴ v_vz&"^xusiS;l@І}oQj4,xwDkP?&2,ںٽnkm)gf,peF?OeT}0F\sqhqp:B|l%#jgSE6tw/~kCMP4OSJjGj<S_dLI}bRZ. *cj\ϠN2g{\yMøRPf\81`:_{t9*Va$F:XIQw6l=h7>WTbz1V ^ukNoX2g`wᰴgDžEqbmyaD+ qU*LM~k@$(65Ϗ[WB1li6 IkB]bҸ6ıc*VZHq0QrIʌ,TDfX1CxcuUUg5ԯd BNX:yZci]Nx6$1Fӗ4'Ct NqA`}vԚAΘ_(⢉@(C]j;f5y ZLnfR8}%F! FtA})_<Gފiax3F@П.BuW4DL;c6| 3MO^N0#."&/h`kQUmhS#GhVTږ>oUіX%Z(8_^Զ{[n4/Լwk&N=L_TAc IDATӐ<焹!f=]^u(nTۮt[D:NlV0M\^ZaDXbA ~NotJ2nx,M E >|(sunyV<\^k(̼wSTOQ-xFC-㪙h8QqM~aqN㊡x%iSuA= rGOduolQϣ.) LI"&2Oǎ 0U.PR+z~EJw&{!d셂s$gZc(,ШxKFpVV10OwSBaD@I1o tHмUWZcoC^CC|b27uu](cfZ09HjfKfv?l4O NM>@^*r`N cfHZ g3C3ӛ cOe}/K:TDs+O07z*)n/x Ԗ ZSKPG'o9Y5 mR2c&YVQ20Z^L6?:Y#5eU.f-mZq,t1ЪcML4~7/Z3)%fhUB m41pl)7̦&Uf-7ԊyS_4YMgƂ3bs2V4.rې=F\>Y $I"FVmbcai'Eg hlJ n[fL& z.+F>kK-݅Vpy (dmzA#0R7\NΊ73] ]vDF 9L\$<>s9Ga`\%~*VE%~M;/3TgLB^mw%Jǚ+m,2Ψ* xXj4 h1fP$g͚ۂa2#iL&̮q#l]=5`|K4ZQɤN5/bɜѽ~[ٹW PAM ~چB-u&p^j0B| ۋYzP6ziP'=%>WaYxXU2x|GUOH2&V|U( .:ˆVڲؠJj& Ԑ> jYpZdJqHiܣyHCw#7J]),c%/Xn{l3M<>cox pߤ$=~ sEp630zh,ķD"\2Y06W^UE XoC@V4 s<$.݌:<]^) o@_xM>Y2,q+ڨ)8RHiM2$ϧܵdV k(+$UfeYJk<31ˌN*7RfJMA Vӗ;)DG+fq_lSCqMo"iDH'$qaEt'T$5lâ[8}er0reVqh7ti"pE$ Qbc5w*JOLEYKӰUF=ß y|iCp8g<(uʸCŐx=]"P\&>~8dn+5 c*qݎ Kb`x _efXtc'}L5 #[%FB|)ʤ.k[*5D1clr'xҺ鸹a0,0H}DIRdC-⏒1jeQ&.`Z$㶬-FUwD~3:59_׆=SF>j캵U(>F`t-Ti9]魸l4/9(쬥;oo4K HpBlzsFrΪϫ홷EUS4Vs9L) h,"h"bsxo~5ldHEl[ ~O|0ƾ)0t|C ?]^ݱ Nh'Z<\si2ݾE.ty^b3V9q.6ԗ3_;2vc04Eg tq]s?Ej̖MMI2N̛US-|t"B},[&Ɛu >^ +3♼"sb{tq#n[H5koa?y]ifX9{+=ioĻ;fڵQ~df1S#YLJ>rs`gR12_ aS.[7"o`U3C|lҠ!~Y6\H!OfP%l'5V  gR+3\ ^J@y]̧U6RIM7<\zReWFEl%BNayk㚂Uf R[m8q13JcK_ LTk]QjIҊba.KOub1o3isM)TXc+_8|u,,+qV@"?4&xƠG^x3K Kw,-/RR=K,^p4/%D+ o=n*8lڌ=1A#Om߰zô{@1Ìv3׉ePMI-֩C\8alQqSwqv}x<S\uv:}iL7 vI6үDW$戟1m}R7r祡 ąlS S1{?%8O~=TuʸAܔ1B2(+f3(?$O %lYXjp< oUN1qqߪSyq{ԉf;B*mKJ\w OO7^.)_3^W߂:{7h]rm «Y [lwTs$Ba|ZS kz J|pZ(y=s4)l3Q6N&1atqq|/:5ysJMe^$ K#|X;u{N#As: ϋ_\|5y s,?pNQ:Vزhta_6bcJ*gL_\c>L_"jg$,Y1 މq-[' SdOkE%7J Ҧϥ k "X]5qWH0Y1bVM_F*{vW1.Ʉo,},;0FF78 ؽ@idz\,s N 6;v <:w6c%u&[" J*n^ϞfUw#_7l^axI$HVQjx\sM5b$M@@zt9o"%Hkzzt|n@o =־2c^yY2r:m42kZtL[;vEp&_WYoDs9&/nһ3pz̺&182']ၗUF":A+mW 4;rHwݙ5Zi=_iW̎{݁y?sns潈oDZud'劓aU g\W9f`a-(%oOpc1W,0 pbdW5m<ƁsHsyH^3?}8mX%#0N> IF.6CK &ų({`ax?ħYSb] ;%&26ʼxGeijV> 9ϲ_e@eVs2Ͱu?؞-,s ,kpe Eo;O bʟU>QFx VuplmkVһ*X6C4^[5m<5ЎyX&3~ hqoӗV?/}fRA ɀVa\t1o!3L$N*%iJ' "8o= r[*OTBCS] gihk4t]͈*^owuxv2ltb L\kQ9 ԋ e~^\YT6U9sDcW]Q ߳8"h*U}jbۧdv'hHM҃[2J U.3m cncP*9ߎBD"|CU$EַbRNIHbfȶaa??]F10HQiY2auS*TMx01'$MJ z3;Q{y!L;af: 3Ӄ13L+{lZ0VST 1wNX1YBi$0 +S\OUe ű񜸩]PL؊qc" Mr$13+2AɌ~7!M^]qZpЗӝE=+0[n$1WK+Ӵ}\NN^\fJ/֓agRw#b·6:}0 h4!Ŭ^v]i%c?X؄hXsmE字.|֊y-P-V`tŽ%t?qڇ+V7ZM a4F2*JiS%H)`^n;RيauO寄!*sÚ6Lsg-w jvo$gnus#1\̤h]Ns)"Xd(o>?ZӺ \}/Sv-ѓasx7i!Fob8k/?XaפV:piVYA'O E;81156W ~Fut MR:}4A\. zViջ*`MʆL}/s)6IϾg6Ne - hVIÕ-&]JM,F^v]\͞lNge~V'X'oUwVϨ{tgΰ"4=xs3jlfA y v^Wؒ&I-I)Жa=~UA{fVr WE t*V/|+WH Ϸj+pM;Ӌ4YAIXf5/訜x- +Sϫ0fd [VMqX,3T"w 'Ut";ZC1xݘ孫\&I;3r՘t>T9FDe)Tlu_L޿>fXˑ,\VIpjbT$cHk̨ mtbqV(\(nukA &i<:1f%oI6_5[P/"}*75m̙mg9C x9g%p$j^:\Nڥ?c#h; i u3Gop<;]-kǕɠA;-jc&]X:iu)O܆,E<2Z/_l IDAT.,hvc|UEl n'Ukژ]/34wc+/^޹w={dڭK+}MH?T9D{{mIȜG@® zrQۋ(s.ͳbKq%6vf -Phx)'U/5[mkjeݕ/[ut,Δ֙>zrEi!+@?/Ȃ jpez1K Ϫ\S=3fe'7/6݅?m~:IVgEq2mpʷ SMf9r r7\+kT\lKW|lΤh.q0|84O)dGq-ڨN FG8?SX g`?n"vf(2U6cxZe[;7XdF7S*x5q5=ۿCL-֧ ?umߤK*5j^aNT~|o`cM3RBo/p \`0hV* itմ'*wLEϛKz/u|2)t$!I'P##ZTסNˬ˥3| T|O nrrWb!d*lGSlmNx!ml)˔+}37VP4fˆ+u>€a*2eE;?cB8>2/]t =9\rI/_—3aH]*^]1_Κb+E^]!ވLiTNV?fAs $BJgUfSŤHR, $ӚY<Ո4kXT4 Ug8v5 H^TB@I[:= 6:g= !1Ð8 f3=ԗCԤ0I<| EtEʷqk{Aڦq;Ps5//{?oϿO ttPJ8椌*h*]+5W•~O —?/S@x{TP|ptR0q&m3ՕRaذ X>!aJ~WH]5`'nz撙x/V筀ڢ@l;rvlX3ˁv̴ŭLf^W_|ov֙ iߨ̕ʢi _zy u ?#ԝB^.BJg [XG`|eU1Iw*ϼs)cj[DfLzpE2{͝&EUNxls9YfCxK1ոA&HJU~Kq!vUƗL f,O:\21J5ᛉ> LvJ8zDh[W.`GZec}cQ.O`&_x8mDžWK 4& Zٷ@c='e r7gqɤ (n|Ś61 @MBޕQAӗNyq=NL {u؅ 9c0tyq5f?NQd7 EK+U}X1(eXxqE zoBݞ§se{rH咘jOΌQÚZbS^6RFl=1ʺʴʷe$2Maea;E%Fk B6bPKsNPt TA-}I>V*Ø`1.xx]KRbM]y W>:cO\d{K 2) dƆͰImCD{;nŵVK~jα6p'kJ!Wx¾qY0xuՕ/Xo,l='Ƹ=t.Exdˈ+Kcl)Οont+0'|O5l1sί *LBϪt)/|31^KiL<);@wq<ʮ3Pdb|oyB ԽɪyE hl퀇ƌICL AS<\ Ո>mK ѐ;]m[h2 HyVfTbF[!@c;+`ENyji~ܼMY&T>/㝜Ӟ Ο!]A%#tpRQCI\xk 7p }M+Ԏh*_UUb$e&j@ (֬ vEg 85pD3."?o޲ʏ(҅2"Œ J{pQ:iC>zȿ`QmWRwi`wRX%}/ݮއαOj_ }:2JbgL7fxH>왶C5/.ZG0]Qz^w{/Np_'lrϼT9I˯0N邩*m^\ÿAc`1TatSOF2ܓaxvX厗N0erM(gR $?dR<ˍL t 񅛫."X^mX$8IR0n5*[xvEnqM呖I c(>{e`h}4w"MK|D"Kl]'F+z:3\U !uzF]*{!Ee0'4id`Ny]ͪf &f*6a+.!?t.jڸO> "d7D8I iRS2 F/ƙ6c/\TI+[1`",cVӺ8 ziwF?@RČʜ=0,A:bniF@Xe]e/fO$됯-*-0 U\WΨ|e:ӗ\Z\ u֊{*S/aJj]P믫|ք,Vx+A+IVu^/_1Cmg 5/{<]2&1rUTִq;u҄h1X ,+*L2I%om~!).?[is^XXS<>To@*ujro1>;ғexGbL?`QbaK۶A]*DW}T)˭)w@e\叙BmWbj P10[nDŽPİmn0~|~lqfytqmfv'bi_'=-/k°G*?ȵ #OL*;嗷^lX3G"qԉ|ͯBi sSS5g>:v `p{[%pì?N <$ֶ |)*i Rq"e05VY̸RR!z˜s{z%;A`+v*`q^[#~RXVGV[5ڄb==& # Zx[FMpf3Iea"Gg 6?Vb5$AbkJ eMܦX)q`M(k?N:Et| hY2QoyKMh1)aS3J,xkiOU5_=m`|*R9P߱3}ϳrB{`-OKs^jç@B=$"CWc0&H媗wNFv|ֶِU26W'$Wm5frX":q۹uW.ŝR?5?yԥ.q"'; kM3a_,1bY)g\6ٳjU'1;&_w~ㄵ/W\* N#4]B E ^(*>M#i?0/KDz H'Z/.5s^]g-!|'y ].](>.MEr **uf#a^/uWvn> uC/.IǠ ,^.892a8^|AeXq^}N]opcLm/T̸3jژ"#h^OH|X᳥\0ɤ['Z`/ w@ vRE$8{h,6,M UHc%f+Hxe:1loRN6s֓7c]HȬ3CrX{xb8A\7I ÛwTr +Gk1";#^<9rFe\4َG|3U &Iꃪx#$%j0&;*Eb#h73)j08b!Wo\=C)J{9tmpP^>u(ִa>#]m5je6L"E$4^-t/YvnekPzO`'aԈk1K{'LS2IpRu,P/Jj&1?e@[iivܭ2EWd;@U{ZT<|e5^QۦAЮ"]LtDmajD4"rO&-_vcM09INAـhƤ][\^?ڧa ?R}!.eV\f .f{wK s8e !1/SP(sQlpe۔bƿ0Ͷ`I^]yg/˥]Wb4ƘOU{yN|ͽ!? {x PQ/垸%/k.NM'492b+>sGC?f1K^ =gf@rɤ`MG8FnMNN05Q$Oay:pu MZO[Nʂ#h~*7E kUa)YQk03Xs}ʱANuf5iL}3ڦI5"_ً!/Mڂg?QO{h\ 7Tb?brc BiTl[~{;06VH _`3cY&ng5&mNTN@ex^i 2\ b\2z2b[4^¡2J3Ӥ8u!0>[j~0 +2UbQ%5u~GӰu@2!833y[;,x:^:,!W=qDH\2G^UPm\^ZT8U/g:RɘSd4\vW?e7ۡ#Q>{sek\P9{J)xXfeYV5؂ gN5?s%t?-, FOѸ;Q f]L N]մqK+1""XmzpdgCtÈMғ(51QH(ٵ:!bs`'Cp4 IDAT~ˈ+6 k? )bATϨL5)ذGk^"K*g1s=a3m԰>uPeˀ+I{DIA\=7#X<ܕ*T>!p҅X]=e[t֞lqkSl3 jDxGӻXu%p.Vܥ<~zx&$+(.14eEd9d[5m,\&tCy+fÌ ~a d$q8q&[@dV8do!=p˜$aa"|.'㑠:h rnM2pnfM MUZͰ:;cƯnybuf/.yP[^|F$7vCL<V~F34^p^ϜTNcoxC'ŰEpnvrMxlK'uf31D| [xqNSIj"hjD(co/2u2(ToR}Zҥ9Oo#d$A _xݍ϶i&ie:5L[ʔeddח3&߯'* Jc,MTU o7AgTe#[ºro' >_ʰn\P ΫM*6> l/W\o~|d(kj׍AԜc_)]^P\2mM$w_Œ`o#<1,zytΛư0:H 1y)4d, 0ӎ[%CĀJκh[~iTު̨&+ߤ1W; n s{GP..51WLඳP5H<"\7/+0}h.|9yh+*UĤ覮V|bG+UY$~_+-qe .ё1|}/7]a#*?B?Kq.)$~ѹ.>1R'af-ڧUz#~c-=ųic"/])EtNC]+I{xziw$c4)Y!媋u,̛^bZ厗;yI#ivŢ&+We^n)DpPu0&F : 2LKV0*fh\c/\iI?7΄g+ & ގq/LZ3$neY$/<|9?NϢjCWy kG<2+W|lUG̈́,]AX kN}qd1N` (IT;7]ЖR]*GNG|܎-(cK7(c<&FJL+U ##ڔ򔰞Pg*3pKeBwIbՊF8Kt =~?7vAC%{R_h0N,NOpla +}ˌaN[)$y\"[c 5WS*g&Y@0;M#P/5XbIp+fvZC/(YaM$AcL{s!\201OPٳ6eݕIhQTd:i'UAbZMd^ H)Y_;/NT> LBWqk}Nɹ6|;]D_|unᾧK @@-eT@/\=Cr%+߆e) Sh'Tfk0I$ u)\6.O!'%ɀ U4M6T~;dPϥP##km= Kf,y|&t )[%PY:WZwlEWGYgݘ6U.wR/vq|ɝۨʇ=P}Yʗt \415+:VH \|qaM-劷˶+$z\݉D8g t:Lfm  xiseZ7Zi$,;!8?Cr?FѤ~L% *{B*2lO}R"DO>ZQ<ҶvPŹwUfH ̢i(g֙ZL}KB.Z3Vy+?unא{zk҅k$~טSOUKc1K*cp\YL0TyRs{rOF ~mݨF -]ȧа 'zUM;UVBSo;$NYAEs5uI@W@Ē>d^z]Y'X'i?W`墓s ØYLX 1%l\◬As3™L_>j(F$Kq4b[PW+NұC yOwԂJ2Lby$ekQFL`"VoҋSގUJ/$^xu&I0uJ]eZ:3‡oeEZaF_7и# 6Mfs9KWK\_QIÆ9yS 24^$Bu#uh.* ml]*bb˄+ZZ`m, ̀.WJt:]2f1;ҏ|'V/p2~(C?$_+OgˑKmkQ-%&%֙,:JET&!'.iWir*pF Z1ǀ: dŦ ZJW^ƅ7Ba,].=DWV`qMEb4<6kzy2aK, !3ء 84av`chRz4=8kJMe쓊Dej5!VHkiT0r1:Sg̓PSWu(Z׼.|-1 &|A[cp_8yx;v0y@w.>E@: 4|R =F96^aΤNr.e> ڜ6۱\:˘/! R`7\23^&\^c _{y kY@ Lh4nz΄77:;:&EM1ߑGp)β׮{1/v27"[^V\1;U|Obҵ/O]I ҆C߲v} rǀ|ƐBi+bT+Rgl^|9_% c-ũS65Aڰ, vцf[,pT~&t~(.{LM|rye.; iς^%W雩r&xGtm^kLL4,S彵V,S1"$u|Mcgn!unwR^Ao/bbn[^/[*<'1@mYִ%")b$qm˙d҈Y弋au ;ʹ dNÛ*7q,|hTw>|E* QVֶY.!zAiCd[pSӎ4/.mJB2eC9^h4K+nږ]Vy\外ߜ)WԹf0Oa: 6W2uk\xH .;G^R0Ϸmk"=2g]jG82Vӆ&E$-eЕV#c&RDI^:\y *e7:o ]p./3NTsY}lM&*yv3%?gTc gC'!qON>7y/\alWUUY|܋" ;]RTx4laP7UL٧R;>NRvQ\eD /ȿ6MA+65tjw!x7D!ymLIMPUc6|PSJC`iҺa)rugs.ׁecy"l s@eD*i#i˻Ъ@OGף f!e=O-U#i~u&@T&T>Y= `3-d]GpR.{ncVٹ DX[l3U/~ IJ]lčUÙzA>ߧF+W7*c\ͯXTKKijW11"vʥ8%D Ac*7(8wJV0,Ky@Gv/ WH9/^rpDeˠ+Ud5vK:lh5yiq]Uţˮ+c^! ʘQ6qBXeZ^yy C,d3Lx++GCHOP/_3̾Q?E~7^aE=[bٮi yk7lpAxg/$8ޤNj=h ~ +oa0XӵO ~JO?aH&)֎AE(Re u VzE [oY<܈Εݡi( uVcwUUT~G8ێ؇%6_=C%3*s*7[u?fh `{*TAkWt z9銝3:J+^F\6m!]|yy՚6dg;j6Y[2J:-O}H랣nXStOLV;ņ5tR–b°x7(q2)6ʴm e%nAۈM3[ٙi IDATxID ְ eC2llMreAlaZSrF勜|A6MwTT\ͧ&#:z3k$]\'fIpj&3s* Ri*)a^4vGkD#VZC2yxt~/#8?23*1,g~W,:^<#uZeˤ+&Q;a]1ق_[Q{sgUq+SbtK1)HktPD:,&qY.m/3Niϲ*+{uyiw3aMN^]TƝ Nc^]{B*y/|m.:Tlbr Я/fXWsr(%Ca,/o?쫪l^tRuK?)I %35־bb=W9R'vjeL?$7~^hy~c&˰]l Axiuf3Kx2ML +?7ONzQ}KxGM<rOoˡ,S prqj4nϩQY yB FH,`mdS Pǵ?os˕ h]Y Rgm˸+}Ms?$82 mwge/FyKWYh6(ӎA+5W$-h_qoYkVJ]U~M?mS?Ž~֙ zy)ZxѦeՕn WR97'(˞+aoe {^\D +Sgcv{=0~ bq>^ş7dެK0wkTaT$h kXQI` ac% HEWrNNy &,т U:~(cQ$P95&|&[k.aj|u\N!wn{1K[UŨ XRwAοҭ BՅl~BӅ6zai疟3`uKf PVN:⽼sNMqu\2IpA79G^: l<%O|c|zBleTƞI}W#eZ;ZYV-W:1rf;^{sȹvG̼nޡʷwұ-Zws2N2 Tlxf77]ԅƎ- ʓv['i;uLR h/醙/,R#i͞wv?V/G8|2xΟQ+#C^z\a:-;PٹӉS9I^]=pO@] z0kn ux4UzS[84%_c)%@̰8t!80F!LHz:il3EpXaANQ&qXمWIm'䘩ħVoS[g7JLx̅O&yHY~ӗgZN@ Գ_yg^tn{f }O["j \!]l*G[UéxpZaM}2z]1@R&Exiw);2]R܁]2=^8?JMfr2JEPaM(#dD2Hʰ!$dzsWQwtϿjG ?rnގN&_۳T10du7g72 3SgJc+T\ _xYwBipόx>`5/KܘtIHWc]/ם\R2Hԉ2PfHQ$xp, ! Kq`{j!smd:7][p  `'p[< Ɇ5ǽKׅz^|&&A^d4nAbXg[͆(qA厙 mrؓ ^-T%(YՎ VΦ78ymN_0yG|eo}DO75ldֹ=."z-ua *@  +PrMMb]^' ]c;@yfI !8?µ^]t N/.p̣1#Y~['pEųe&_Xw%[~!<X^N!Id;m*}F* U:-p| 12"g^x> f~)g BV7>Obv?i)-qKk𽦩$5]{w)w]>֞]kt x;â$#8ߋ#nQty.{yJ# EhI:zUKei5L~$UDWb/v,:׼߾̧XoqgS?)Psű01U2eE=Y=)K#;xM帗O\1ajpϴ,5и5mer4,ѷFHCn&_UyrRrytu5gs{_K1DO8L48 G*ߩ|Q)bR] t[`kptm/vkt`tוM/ݴta%Sb b+k_e/^e8(^\9K^vӈX^6&]jzr[e+;yvRc ܤHAeG=W"V`U㷞x3S1J MbmV32ו6ҕ 6/ULp=+p E/ӮRZ]F IK+ ʞz@ X R R3|mv7]:ahRܠ(C/vaSeb}*6ʄ4Dʼn)ez-ȭ~ɛ6zO|;m]$ӽ" }sSza$603#*%^W]n`"q%s+JF(;/ ٚp \K$G cr+z! y /Cc5m fEhRDIdzt| k1nƒš! *FwΘhnz㙗_te6dprʩաYZ9Z$aly~6JL=dz0fvʦ4IBVyZb.W+!t̟ĤI츈{**S{\63AFωOF;c\ͪS6_*l *n_gMq\S?7lwtOI<wnŝ枹O~?cv`-eB{F5m:{cj!TNxsta{6~gq$E2faam4O'44.!M1ݬX20l20n(38!0SM䙝?ɯ'*J DOODڛ\Ϛ춼9"؍[1`L۫*f7381ť1ע2eloJ0b(cE3K}.=?6v}Q[cj~QT(ǕPLw7lb5K -ŤA[bXK^]_öf i]/Nf㲗'*OUN&/{ShC|]1+^V]I ҪJ{Y3Zg:0ƶf^ĥ uO]4^lⲷ] 2@?kxF֙C<eˊKKc <34K/xqv{uyüsʉ?eԕ-]%0P=6+TxxUbM%LC,2wE8Nm q~`?wq[NF90PMgFɲ|ג,e[ɚfA9Ч޵9Ou['̐0IYFi{YUeTe^Z|vu\2;@+ۅ Cd^1ә4wY>kt5%iF HqbeVݡ MGR~>}*Li8A<ܗ_"`n⧀͟&H 3oX@0Ora-]9ުps&ӗz, ^$ED1V ~8b=k?E_B(QYiW(?S(WF\ʰxX-CCۇqhluX8freu1 2ML֎:h=3^B(Vbdez3S !H۳Zow4ln0} F.Og4s~?3=1lAJ5u~ qB$_̦UZFp~i׿m#uv/n{&#5 B އ{cl`1 +2|$b)07CfF$唺շ%3) >I+TYRV^Ͱa/lsܤkLj:zi$H5OUW :dc i6#z֔F/aWI"Z+1$3l;vRTh!8H71-yS>ի x&Ɔc;j1#`P̙uM 鱤V|O\qt3O6^VF㕯;ha*96]pK{"Ϳ2X d">* tYpj~ b:{(d֙2Y ,ń5TXd~26vtj֙\RZ=C;-2}b3Inqn+[놏2ki@5tl~̍80OQ+CK NzAL&L$!0tk;)El}jO\:EQ׻_l546s;B]{&&q5dQ| W4>2|76=m:?f}%3 I׷-~SI}Ca-Ju5W\RAuƛkO+p}+ !4ʌE[ #i2jie{P{L{cۃ!JlH8^kﵭiU,O42f< 8IifW8Hvdou+Of|1 Q - ̦MQZ9/,{L״x-*GлR}ܞj/dZ 2t\ ;4 iõK12|ǡR3 :_23 t5CDIeW0,kpZYKlhYFLP@> $K#kIرKNtWP$)Ϙ`e0fjPS'VPxHky ӭJ")]o{k/yz GG5 cuv9ꅧ'kL?Q P3/xz G:s+N,5v?SqK~x4RGy/FfH+I3C`ĿKq"0;%zIR(2Llx:/ZܓF;Q,f& K3]& E2)J+w:kkreFtDyMCйwZiS0lD^,?$_lbY죛Lw4@<˹gc1[L~ŷ>s0wҀV̀ TT5*%,;8)Ǒcq2L)fn;)0Jr)(Hg4ڭn)=^&35Obyse)aI żvӂK#Gc)׾azGm2]pkB3|e`w /=8?XҞph]V&LxG"^JqV. _Ot5l8}P;H:0E;sN4‹yI`SoYRڞEd4{fJM4R*"J)ӨKJ5= OEjNGAjڹqK!fMtq㡑7v4=E65s GW@<\fcEcv-gZɉpܘ;L? 0Vbm0p\:."SSh.蒙28@ٞT>:0,,٨`0%JSSe%=SMd=( Hk!': mkQbẖ4Q143YA?G O3.e ]Fmi^2{z d)xꈵ XP\QW N&SS{tiֵYCnoX2WONE/2}4XtlC]KP'Fnh%p'KX+*PfxQ->e#zt{D7i-cnA`X*H@4Ã1Mf=͸b9 -O8͏mE~? g\[?)\t=.X5LOkfK VLu N'X#4sBJ^h[FZfJF5(S19.s>9וvfNvPY I ]IigFK@Iuqw_j\QP~LL7eCnwAfnjp g=߅)I=iѾ_c2|Buxj7=ͺZ-/0=dPYahYn {A7¸&FZ(t]cA:nkAr;Қ@r.dWbP S)sa3,dǚs֙syZie|T{]q\{tʴIC ={?e)vމ W $̾^& 0}|OS}{fS=B[?JveW ~iCӥ%0,i g5<u%Ͱx.a/X9lMW#k]>1lDLg2|osd\l_ڪao-? XF8 pZKǙ1 hM'R~GgLO4EvϼKЀ\ӫ]0z`wӦk>L7kc'!6( .K)^tݩ>2b4 х^0}%a/X:f.,~sӖk!mkIHyҎPz(c=4;woqOEyu)iO l=i'+S^WwbjŊk3JXN} "ʹ8z!WKx4KO _{LUѧ"sӹ+"GjpS*zZ!Hp[:V숵Rk k} &FVEyGe+77a/hN38١;s4>LDK9?geJx40Nv`^lRZ4."&= Q gi]`h Nbfӎl+=mgDrS`^{V56C~+0fz_37*ܚTL)wO\GE5od<-; nK$) F'L߈ jSK␪`NR6$miU&+ +WcݑzT{27=0KP8Wɺ΄B4iZgkRp~^̵ϐLL?Iuۧ[LVܾ4R~3$-ИS3_sVI:,( .mnLapGlP,kLOdpIz5}tG'cF~mHa%ccV2d"鬌|遣mwIIl 鄉=S|'f,B'Ot*R ؚKl 6ov~sb8k6^]l`?!zӹ+j{вX~Ϙi3a4 g0fwTR+faxa6s'L<hb'.$GvM$qK!b,smZd^ʡ"d.޵ sú e02L\9S۳f)XS.dn"7&m?L#,ӀKvRג`dX zq*#G(-EIQFo!y1c\nw:^-c @[Oo]{_e!:HT$J3m6\,"W~:+W]9S ղaiHC{:.MG.7QGYLHkaV$L[SK1UC{4"uv 6Ӱ+H p:t%XD G3 1a "~J6:,~O(cC 0!kQd6$;j2 ;蘅QKΔKXOL\ r5f_wreK\488_dI bd'OSN4G` TŴtba[2YheD2e=u$L(O=ǞΜ_ۚRfueVv%m} Kמh4b6s2zK=Yxtт\ϙn3vC)@(Eqp^ÔbXXȰHg)u؀Qðx%shSńNAP]9ql7+LNP^LH$|-EKvlo}Z2X{[Žznz4d39PrD߇k:_C11Hޘd(>Qw ) #_5>1}*5Pųۑ 6z#ITsmA)d&yR bS_2 8:BX 2FvE$kl,d3C]ޅiusG{L-jURoaLyJp.\Y9 ~r`GS PafS6=,ZR{F?i5zN3M{t)ɲZXSHXf"y䖤Q_ o<=s+}B!U涧eWP+!s­`q4]DLL°zxGcu1LH H- vP~BmcFl X[1ֳ'dKI杲ZE2}+י<-a1(OC7 ?yT#}g2&F`p> jtQ<1;>uyb!͜ "Pw1@e\4O5uVQ0vTQuO\#Y JMm#8z %am?4?3߁h!"bWy4K觸/9`Wu-3{~<ǰ6#/a`qD0"NT,i# { 'zd^нUzXy:eZ%sBou4ca:e*%ozJmRڙ9_j w&؏9j3X{] BIU>C?Ct3@"M`z0oi[&`C kOk6 : ؚ]S!a]88ELa/]2Ӛ6 tE2'Ϋ(3ʈ2v ;dtxQ5je.?dgg:PgPJ]9Y|3#:c 5t47 JӿJah EgWxt_k͗ 1]x6s­/!:nC04vK}RY5OFQЃuƟb2F;Ăa!9Ou>C%ӦM \)b JtC{ݥFK)O88:u$\ {&0wX2/k+B&l}tA ?Kð*ۧSOP`&0w)= ;9ut45DEz v`]EA4mw10Jxe.Y` S| $#B2lk6*RWSLƌ40U EDƝ#WtS5d H%L3L?O0Wت pUV:7 ɥHص+xGtQީUuKҎI( 6q4JwkCFJp2;&0ɠ2qcb=+L_0յg-1h2ɼВD4B[v@T J}Y>a6S~lFӜNGyhIi :׎>У_Ӊfoτ_ <Ջ >Jw:7/]2kk%$Ӕ)@|j !!x}pE̦޿S"X{DI]ʰTCp0!08o(o'3 N4wո| =9?15^rU6 H#'&HcN cg9څ$ ГJ./Ou628j&)Ӊ+֩2̖gi -`& bӮSε g iG%c3pD|_l~C3>CӤk/0 觘}iå %jkqJ0&2,椄MNܥ%]01.o $xƩMd059; 碗PGX6 *ld_o條ɾ ZƖbbaK m^Z{q;#`zw(d3C&;9[-7&iW9cUv%1 0} 7c^ 0?nw;t­!Ou'ep PK2tcqc%eK\K\xvן+ńa fdp~ ^|Cxl{t*w)<apak[εBZ`,F*S.hb1F?@SvZYۗrnWQ^~~BB暧W0u z0~X{uGLM;2ggekLn]ȶI0οѝ\M1u]{Gqx$.5[/ ^2돡Q&J(jWG|2{L[B"yZε&,>ʅ9eu&e4SK3IN6e"g.(G~[ii Oʖ_F,?>f39 ?@עxxiJΟonm1yBL RS7OMW 8Xe#NfNz\{>]ӬSh^NrC;PmÒ 2_3 mR@uh0+HIVg㴌tLRZ:@wu&ehuSS^tl'1IM0-Ix&Ӣ%2Ʈyz LP4 9|Oܘ HKz`L_BraDȰA+Vĩyd cAڧ툑} X3,-iR@N&Q/ud)ORgs~,Ojl 3 {^M[g _1Uқ^ٌ"V1@H0'/3y0MHF7V[߆i%oqظ ,Bz|l~`K"ͯzZuLu]`_XPwQKV%i[2JM&vd,e<.sXFjv/|\RZX̌Yش&մ(mpOG?i}6& È )+=K}ۭSXxi%k? 7Df.ǒ3b)nffZ"c`NLK'Th%P?3ε¿#?2Z]Ԗ"-բdf5C6'96Ck;: -Дg֙mD1M1ҁMI@zK'O?\صy2!w#~LcLv|rcwI#aXdztӂ+@vJb4.ӄ+ә;tbBɦ+>$,ȇLS:ˌ2yPd bܝj:jFHFG3@ȕs wZXR֛B zc+ FNԊX~!7} O ./k"E䴧 Wu(? !SJGxA[ky#=@ ȍ~u@EF&<hɭ}}[D/*(3(lյQO- 8fEɍrZ m E~̰q̴ YnEZlJ}aVaO&1[{5K;֡1~)fZd(ĩX~RƖ|bMax/=S/@\ڵ3ư7i`X| 'uUӜ*C)HK*‚d^{qŴarPiĥ5laRO.ct*<hB.RvHe{s%Ч8BCFCk9qOLǵ KV5&&:=,[Ӷ7A8._Ϡx#iqOC=^Kڼdn?g^[?u>Hap0Ox?c*+ }Wa/({ PqD4kAzgFS|45ac"MkxA̅<)!.a:%wris0SHv)Ȕ{kMsi|bPk{: Mi]\;o#dXZB*B#%w ߡ6DT'^ztEca[@>=1YyƢ Xa鱣?:mܣpWLAeb.71kZ<ʹRpw&)B;+=a-iuRζFFO2hFOMfӑ3CjŦ31*JT8ЉИ\mOonLq꺙=" ON>XkaYڽ$8Xenbiznp~xwLB~bb5 2_3Tx+R5= .KG1E}mfIiV)#44#ZGLb[;;}o>.=al+Z< ! 'Lm0^nx6o4KaXk^M1zwE)6̘g]>aV3I؋!@:"ULwĹ6M_`hG+Gz"[{.\c&&I]%4Wک g{ڶ!GLLJp2Ry"W| W@eiӴS 7vM2oc==u̴tCMܵ00͉xxknU딸db"NQ1:_iŜ!eѽpO2_idI#VǚDZm S&QCDu^U;gN8쒝p֟+<֙x aKk8z`16i8|ddpAgjT/ C2 'K6E!2Yh-k,seh9ZY@ERo0tELV#U]R]Le+h4GƜ` mCV 4t=9}:/f6; M\14_UҿI= ­Ckڰ6)iJqɐM°x" [#N L2wFrnQF&i QӢvL(Z Zh3jW\vU+ct|٘4K|:lӀ2t _Ci֙o0M1]+B7Q7r!>r'ߡv?5zc $ k2u°س)JQzGte4|Z-4a>מn;.Qu`{|CR\;LwG匊a/YM}魧uF&Ff 'DL]eXOvЎ@sh?'N"jyfeza0iuo7׆q֌TmpO{cW=Rgz&TON)Ϳ= gcP~Ĵ1\Z{bK1M1>A%a p5!Mq`7To{ڰ|4E7s@vў t!na1cZa) $$uJ 1ATʂ:-:D2UNjT_7%뼼ί.,蛽{&{mRgGR1ݠn%0Q(SL7u&4`ol>2(ӧ4  ~+?)/>az­Oa`߄gg=M"{6._R mJGYڔqtI K״׵N y7ِg)ӑdfӗ._yl, (TH,B0u;bafϙk94nyXZC[:3s .iuPYE#z`n*[BHXv49hc]T/^2=,BhsGoɶ/{~c;F_˖q ҮʻŲwLmƼp2(ii(I5v6thr^sQMB;4Wl^:!>1JXoҨp2EG>tõXts&j̜a/ie"Q&|Q,LA4jlA ` ߡ?p{ =5}c/C0U6igiֳ]5&@> 0 7qճ('#Ƶ7}Pyִ5[VpBW@^Zs̴$PSյ["~(𾩽 Y0M0r;7&9 •#_!?\h Rq~ w$_2л_2 &_Tu$s8I k"iѥJ3&@T jNRl!aEŭ'F SӅS5\Z$βDmt[XKBBz~Stţ0Q&/HKv&Ԍ~UYO$ja``)1LcFa-ǐZiiZt3Q<9z8SL?8XcjZAWk| 3} EtJ'sa/aoOaP籯5i)ii]~Cؕ1 ӹ.)s5,,)$޴ePibj@ݢ7˃g:XgځM6*n1yO<3I5d %+ߧv7<};'c(C$ܞ~ԊeCk۞# >ļVos h弣Q_f+bFo |}Iuq V> ~NfXɐaM6ЮsEeIb„6:yt:$|F)mt6Ķ-_P_[3!=&]15ʔeֶk]fO!B^ֳvaD.7v\l=VUZYI>1Gtlɓd& ٔ:UbP~L3L?`q}qClZѩ隤V,TAӱ##GwtVgX5#F貨C\?(Yhی{5Ğtiܓ&OW#*vbfE/U.9m9zE$hDN3]9iREIs#f)!ݎw[md6%Ӟ:DO 52͈mm:akqpZ//L7 6r0Ӎ >GLߊNrt Ԙ<5\[6)`@B_k[Oo\[X&>7aKLqa{LF7/O]~8{5P/!uGKn}$|'f,A؉%V~[?#NU4BQ9W)el+H4 %<>Lo(ӟBez6 heNn\KVI.+7 M(AmI 7@ ZZgM'$ǼS]ٵИgL/~ ) v&'|U2~veW51S- E3[Y/W11D -G_3=gRINȀf_ ;d_ gV10e#.1zviY^z$=wXnYaL!ؘ=1M0][]:$\iÑ:oӔࠔ#k 4>LL7 {q3fh0j"m6'H]Әg(7 ~x$8@֐a.ӼLjeXGLk) j%9_ -Xg?8꜊퐗]\m;5q )n}M _fΰbXue[9g~3q?:MH9SSv2, SAn)lT|Xk-q 9Ö]J(IoDoLRFi!ixɧ#Cʹ&1qI ZQڶ@#tt.LM_Yځ6Xh6@sN:jE[gk$\a\FZ6/)j0$`ٍ\F@楝0M0-2}= ¶`u\a=饣LD̆ Q6v>w;于0yŴ4:#.WWVJ<-¹]ns';LNK(rR2 PmS%T~#u8՞\b+N螙OWYdXxzfX,TxH5{@a/YnOe]1ӗqWޘ VgQ?LR+ipBмt Fgw4Ӟf] Cha O0,^0 ~WQ?՘I.SoI G]ǩ#ߑ&P_uCBc5 \K?fgCBz}0wbL=mc.IJ9$)[/~OzKiq.oPZ|oCc酧Fn{e8 `COZ?qd7pGkvlvK>aL ϙ F]f騜(Syʣ-)M4(Ǐ(lzM ud=d w/y5kI 46)G'!g ^Fi03}u=}$4&~LsLh\H&P>=Qz[ NӘ+K°Bc26 "4KJQnw#x HNL62ʄSmDO-=V5P<̢SO')kRkc6ֳvÙ ,)~a;pML,zTdC˂j=ӝzp_nk/fNz:WdCc^0%d&\*Gq~!a;dֿ*ɠwkb)==k-@ZKðH%0(}i/M/#P${J@|J>5ֿ3i rQTF[hVFy^_p+M%~6TQ0}FQ:/f~n [#g1εل'cz4 <mt =,2;L;L]9S/XεfX8438_FȄ1ɆM|3zJcNL"KF7B*# 4}zp,=abr:Mbk2M1(<6\]2ZԖ3t ؍G\G6WL7_Is]a"IENDB`pyotherside-1.6.2/docs/images/pyfbo_example.png000066400000000000000000001023651475412515400216330ustar00rootroot00000000000000PNG  IHDRݡ pHYs  tIME1'NN5P IDATxgцgӝ%e! !$r&``d?l08lL0h@ lI6Lw}?f^g{;v٪j裏 0[[[}nkҾ%b`X,b1X,bX,bX ,b`X,b1X,bX ,bX,bU"p>[g bKbX ,bX,b1X,bX,bX ,b`X,b1X,bX ,'epn{KlaX,v Y,bX,bX,b`X,bX,bX ,b`X,b1X,b s~oi-,.!b1X,bX,bX ,b`X,b1X,bX ,bX,b1X,Vw}n-b%dX,b`X,bX,bX ,b`X~"os4jԨ  a`XݬI&M``X=Vb1X,bbJpErU~bbLqbb`XL%b1X ,o,Tb1XX ,J,b1XX ,Sb`ח?zS @ $-")b1j$5_"E{CNjQi"`EJ5l9U0I%g_.Qaˤd~Trza :""Nv Y}Iw- $ P 3DW FfsX-u^k&ay,3 DP=C$V")е%5sqXUGQ(?. ,V9Sd7 GD"Z@@`+A$busW[^1XeQ&rM4{Hߤ"! D¡@T9ӌbuo*F:{m CՉ[AuQoB& tBziDGc(2ahL*`Ъȯ 4/ɵ,Z z3o|{,r*Uya8QOX񎚴cXThD'.mPf Do xCD,fd1VaV*tf?S)#Z@ffv%ؠ$ ʌ|ND4'.}. ~!o{J'twPO8XF E]L$69G`T5(;sLB~ ҅vXfϛWN(AX[ِhTnakDa"XdLF d{=J?.f9 HJx|B; @FHv ~PAY1τ|K 49[d3hKT|q5AiS[ ( b "VD9 ~M+D!R" ;DNK16D!D" m52&M4lwƖD O > 4]VUPgꛞ`V8A(6.sCkA"(/gTx:>vQ(l*ayERC2+ tօ{58'"PI廙-ֶrV ",oh 2@\XDo^|q+!v +lΌT@u˃@ȕŞs0-JC!/xHl; @ ZWZQ#H *Ko&d$Cx@FR.#`vqGa%0ض*E+Q @x2Q9&[B&%cЀ7a-׮@d"*c]:'6(z14L %O"K;DMb $2X}ƶ |^0Q.VزQ58B=K1[n0*Rd+Kǻ(b{ Mt HqE IT˜cۢWng^} h PU%Ub(^#@H#bZJB$2>D$-w-EyP y⩅׷m%k3b跭PyCdrN!"nW+L͠ \u3%hh)ϛ2~% İEzrֿ~ "tvD)P rL v̈́6%:Ygei+(Kh8Kb)/bAalC"W;hq]^K`ΠDXh8A(Q_wbB Dd$ӁQy"!c)IeD*`..*p-!d: 9|곴@a\Mj[I+QXFb\Hu:ܵg$1k5Aa-6Yz~ʯCQdHgg˨WFF 3I5{V[b ; e`/+Z%)1Rar($Hk"]0KPX iQ~+!djEE(E{X} c ȮӁ*:[c232*0b>G2;B:Z\G\B]Ǧ0LU?~,(3 1LRaOToB1JmT '7`23{V߃:/ȊǶD='4"E'V 10 i*"󊈈{! ef6X/QFiAő1f%*=@úLPU')_5*DFn|6(֡!B\DȰ2X̦:3"0*+,{EˉgV6P=,VaiS:YeڑBD{5q:~PCFl,/-_( V +zXF8-ջT;SAJ{u頔'ZF.ΉHW2T'1EfXoRy\Ra˱J[F>iT%U!l9/_k|yVw) }J,.CwН#*s0w7۽ʏnH{i,fO/ox6uc)z(ks1δp;Qo]gg1XƆU> JX4T_Aګk@@P%+*S 0U&CSa~2XPV= -#[_&Q8u-Z3+_fЖQ1 &V顽*[b P~DX 7!^FyW K(P Vw+J[C( .֖&S9JRvk@:U|2vX\ߪ?2e9UNII(,zs/O-FVǒh 9*ְzX x5hVPYX .R)&t-_#1DRf*c|b`E`aXa5Ӆ6j$L;ա#+Pn2lQ/! tb`Ml'D0peazIXՌ2jW4:H,iq~)(H(5?dK{1V{+MTvly ^H)E).B Ğ .cU/GX}I ._J=aϭ" Y oydDK@ D^ٝD;.Vܨm=m>cx}L]!V3d T\D.[R ](yEsQ<ʸ)~NX APj)*U"XrسoN{G55X58IVmj~ `+b8VXo1rֿ_g {1k:/7 X ^i2` g3v@ iX|XoT̪@Hw40b` NU4J ;|^yGʼ]VOneL-VuE{~/.lݠ;g35jhe㪾gTe`{Na7-T ![]rskGX]V}`Mf`MBՐ2Uo ,F[tWWޘ)㩳;Y*uٱX ^ЯJ׽;NuCd`n=7 {2{W >Y4W-J`q}ڶ_壏_ +bDmeT7ݴrֿ.a_FW*B⇌]BVX.aFLUsXஂ+q XVVYX}2,V]İXVMA/VvPVGl.8W'Kƕ5bǰzKؕ0u.]N灁_Fsb`:L< }:VXM3VS׻u78ԏbXԁUVM*asS;;7Z=]|,.(bu ׽BdkR]p[նZUC{E%ݡrֿa"vɁ]BVWD20Xu ]~w%d1zJtj2N,``J ،zJ.-,\^TlwOjw8ǢK_j ա;mn9Տ,/,77]X JLW;/g3koC ]xs2#VJo@nEmF NIgr1mwnWSYX &6u!@ɉ4Vq$T\[ '8Y=e _ض]͏G-szVS'd @_Du.a7[նYjai"U;&:Ctpf/g/z|e:UX{Q=^+ә|)U4-v a2:weCj{ EpzKHDT@f&}w,Vߔ;Y4 XTS(tqNn(rl HWVMTnLU+*ȜTvʈ$.L[cjUO(Cgi{5}IhDJ{J2{))"=^2+Ē__lRD fF:H-!#)a`¨k NvsN cF~fW@tX$rb,V&͐ʍ|#V~?j6޵ .2˟K;d&q+VB/VU z-Bv('VFU4S?n4uy ck+OpJv$o,,iD+:/eP#uM+][6r`WX s hQfDⲃlTʪ] ʳX I%R*;k(NQV-jΟ:Bkeۙd)V<2ݕg\;*M]d2X"R#d+>:*[)ƈ֡Lgڀ4vv8m|K"o:z.C2~i3@L"@4b}κ?JZYE+j ӋVm?1<6a ;Rw*< LUdHmiO8VTbfoJ+[zg LJa,Pfʈ%:݄ꎛI' Rgڴ{y20Wqj'J7|I yi,C\]uD[YtCU5Qv[v/V ]E'+J"Og%v6.eךwKDmڙNsp&mZMgA|ao՟U3g,LK7L"]ڔfVLʚc\8]>CԎrȶ!-b_tҖx]c*c͕,3}** Q'v$"wx+X}rMBWw"BWaeȢ% 4 Qt4≮cPW}7(i2YU|Peb`W{~"!6Fr2ML]R@WgRgd|);*lmudR!*éTG/Bk{eqpdF<V)YScT/Vv&)@ %V)DSh W(Ji,Wl+iFaeXe[նR}"PJaBYO07,;5iYB_%ی@f[[TV6%3;N6Vʶ!CP#)wrVg@w;VpJԍrֿR B!TL:,]j8ܻ$V2Rd[XFRʲ xRڼ iɴ*Y ^c`k{iU /mZNJ4dLr,C"m2t Ttt/nD@Vrs+7沥. ҩlBiXyRRfNL'+ -{7Lf;Q;e hK1XS0嚑c(yU*:$*Tj#ta$!vm[Tҥ*M۶J3cZXu X1j)wl]GWʙckfEthX6'ƨS0 u"t !̍މvrrPVIC[eYVf .SCx OT2-bmmx,_d:;et& D?Ϊޏl3" U.d2!H{U*ZГE'XSܓIÂe Y f,ǶvT*#Qh>]>0O x Qڠx{DKwLJ?΄h%T:nG3L&!2XTy8A+thNڢC ";9i )ȷPʻx(0 1hej&9Uav.h8PkNl[Y҅j \:GBUjƆl , dF wd+ze)ekwx Vv~a6MmȋD 5[U *`[bDf,RP ,ؼ=Umۯ@:GPcEhR2 (:e4" ] v2-#H% 6x:!YɤGʼn@(*c覵FT*nEb ib[tcYm%<}F];Ju9*e;N{, m+gwld ,R;wAHJ"J=ӲpAA¬|ȵb3 J!WTKx'PRVBֶ*ntY 'B3l:r9)Yx`ךd1g5sWmJ$D!=iKw<˴F =,hkI)4^n>!%߃zNbDv V]*[vfƶmT*"DrG߉׍f`Q#*`y;K,R2. uM*iJ8-F]9D:j^PV&˲aZ+e2֮XB*&YZ!::dBAEL36ou]2@Dd֌@&"m fw*o߲lҤmU8DN[3M.|C[.'c?KFI(1_G'T.;2Jk!{U)"eʆ^ޟz5 s8$D@TvlNEh33"ږ]8)3r&+EHh.<Y).,4C K+KCZY0YB&"Yv(#139"gV^ \#M>;DEDDw$x63VX0V'eY+4w'?3&ڼJj(o¸p;*i玑l !cF*D,'PB+?@ r8gs 9/3-ֶ `Z$C3QT-rէ}CW*Nj1ѠY,oj ;Hi;i\u>/r?R)E fyvilV_ ]uµ i$O$0miD ZPgrV>"C4Ed*6ǂzw*t6!ה)+cc;ٖ2Mڔɨ"/jW ¬|CACoKaDlKQo0OLz@Y[ɁJVA-,nϟ^A61w]9\,%(c'6e#?N?,iD)=t$V+e{LHD%)T(Έ;ӚHl:Tmm[*0s77iXJ:I Agbbo62+ 6KTfAW!""PD*&r7\xV,Vv-SbO5dR9{iVmR[I$\DcUU* 8E:5F p"i\<6]A'A-_(D MnHN{(e&\A:]/,SyV[b ǛZ)<>D;De*@@2!JQ;oy# P +&Z>v惦0&K%uc1J,S g~Yv0(#FS*2ɕE%J} ucX"n-F,T!"P@@{t s ZQqe&ʺXƕږ [lK[E=A]1X*w&)0<Q$<ʹl ǬAT3wR@ O#T@(ӤiiO<`>`ҥq &2WwT+AȜ̙@^:*sTF<QEzwy}]wu`'O2e{zx≃>C6mZ?ւ ^ٹ>` nh<&MT9 NdXM:ok޼y{VZu!tx'xsnXzk+ۻrrW*d;`qDL,8VN.@h ;v-aP-QĤʺ7lv|B: nY(ۋمHyt/5gkBɹ\]k7I+""UtKtRVu8W#Y89yrr~(.dE$Nh3BXCs],VoW 4 (QBo@ ^1+< qЪsL(^j24VzPT;c,:mU8>(" Pޗ9blC,KcZ"hIJ 6Xi@QbzQ pa;FX,Y!Ց5Jv3Sv-~WVQՔjBYzeo)Wa^=pxkOy }'~W}b'4ÐҋxϷn|g*/lJ)7C*OYXx6 j#-gn`gtkOn{Yiӻ_sNC,|wj=yd-ͼ/z'Z7~K^t;o2x;7KV%->~M+0zߟ,1#֟& z{[z>Qu S Aʷwwߝ\W"I%k->{z"jmCW^v UrŲLlؐf=c7dIM:8vʴi8v,bY/V_}Er 8);O/~፟71аۖ}pq2֞zNOkzkW ~6&ң {gӚz:丗g)S>,\?}tww^ i qR d o VP1nL:ꞿU3I~#iA.7|εOmlka ɀcR-zꝆyо؆GxݜfP+߾{wMK5t'o _]uřM#9i(^VAW;w[%bV$njeǥ3̪sڋY+ED4kD"<K9㛆4~cjW;hӱÛFMg+򉗿vA>eĝy'㴷kraÆ k>~YN|z+lٳfm-s_hom7\kDKsssˤC̯pÇmhh~~=60r}mƇ?-7n:lG2rx#~&ojll0f^|KbÛf/'AȾ/Lz&HZcz#/8ѡ`|:}w Gٳ˟"7?ӥG̭gn;g}~v3w_[Ro}Ø~c;y)]sko>v=G8{3'>~Yw>X.a{}i{L{/gǿq!{\p-3kמ|ᡇ7\=w7廟Ϝ~w-~w2q`?yH}V}죝Y6]?>㛯١Y,Zo(,z@vm=RAzs3v;ġW>z+ 8aFn]lh{{޹7?Nތi\p9Z"^3aϛW}4x3Z#1{4]%Ơkv.+ut&CV':)h?6s׭Ɏ5>>\A൯GrӇE Z̅?}ӶM +k- ?[y.#/떛ϸ|ăܩ7_ӈ'/ )"֟as`w_uY7}F׾wܳ7B+;{!_>~Q7S7=~WٰϮm_}S^O6o䉛Nx~F1\ >{[fr 1mƒ7uEa{rVp7~YWSwo`~Գ79I[ꊷ'?IySp…ki ROw_L:'.ݼR8*mI[<{;Ϳ|M{3&f,sm͟ox`fvq"o\psPkfet;̘^&oyFm{ġM.}o`L@q$h~oe=fRc ?7oy`W5@i0|sNe?Ҍ9׵ʕ)Ao9ݡ߽dϺʟ܈ɤswϼ[IȈ gllҴx1f?#oҖg9{@S|W~u'F6jUGbƦ?N9p:9=7/<Ҋo}j;buQr9 IDATKlh:tG=fN{A[J|7ؿX MIi֯*^ΠnщeI?vOozrǍ;Tvj/wمmuzA'N:f Hb]$cO]/luh]]+ת5i/Odl1~-E7\'+[~|"f #Ѻ?;[L-jxvڈJ&N_c{ly>ɔ S1lp¦5~}*ھx9l aJӋ%bZh˴)1rfӛrO"zEߞ=W]vϷo#,%Յ\ )g,Sx]<צi6F9FnzRRlַ۫.od_> #:&ԭz7B40(L:~ÍN50q&ovCO|~i_gHhѣ-m'ǂNawo1M)08[$%Zԁ,ˆu&O֌Nٶ6]prZ/J&v#SoT'cimY GN2);)Ӣ#L^#?p=8wjAcIm8fc߇\6nB[,- 8lfk?mV/4ul&l[s[9gsǞ؏=;^;mr''^^O>tΔ'߼/;0"HR3qx#>vBXڭ+=}741yz!4yuJ|#vWk-dB 1k 5em :|щZv<5zPhm6͵7p)]>iR{5NhDϔGӳS_[Q3Fy[f2/FMnlsyVM:;[?7\b_OwyEST61! cS?hԓx˛-_{{U  kG,Z|6M=ͬ]p /|d%_%:.iSr}i<KW,\U}~wT41zM#_%L*:=Zp7_~ka{GM_ruϟW?w+V,Nm?]'WiVpv슒'6=ip.I43bXtb5rԩS'x N.`fyywŲUkVw~1cu W-}M}uV>}ͲU/ 췉zny+WNa_ Zf?|ղ~3Z4[qo^iV0j[3o|jWfֶs6Wo?]r%_C[XR|U7=v:| B =xyƺag/?w3@0`wY~/?YDZ(;/wnr'0C4r@$;" wd|p'\ Q"D68]nKk+_<=:h)"~+^&"+9DIG|ꖮaº-O o?wJ)Pflt21uZ"bH#[H,i[L: ro/?nr@9/iWP8犿͇8ۋ?z1%Px⛜WK:WA6YBDHv˩$ cz'WG/Hl`@ްⒹ]pI 6t轢<1:nbß\{Yo7@ĔzU׵o>>@DX\Fw=xk s.[ 3k1^2'M=/?H899%$V!sb̾_ s!"un4bsz7a…[JLi+c̽qT۲EKV![%dQc͒EKZuЦơc.Nƛw<×ߺl죪S-^eʺC N8 iR"6-xw,ʴ.Y,29RUӉ!NG&u%kTaxU26D kQuC.R+.]!C$"HΤy{81Qi3II'AٯdoWX1c[/zc1mG\x64i`ĹijeZd%,nqnj6 &mW[/>"eVf~h:JV˓*:`T^bꔎ jhV_>|tͽN qV,]CI$e eܴm$O K')>w7)"UӧO =e.2]7 fad`A@S]a# P#h,0X^?dCF3dTy싷o1QA-c=nt`˸-Oe}k5"d1at@cł S^V/E3^B+- ץEEQ7lx0^нf2j:ȨR>vgpT7?ݎby%<,{};&U>]_-U|.S2U~nTʟ^ca-~׷<3,3Wh0z`W_OťU7%^gKWeɏ }'U5uJGm.?s|SWX'mY׽߾5O O{:oW2g>x|˳S˿f+|k Ǐ]wg?|hT'Niwdi[MvyĚ7~w eVeMlUWˍ 9yi ut ;n~ϻcgћTY8fyz =-A/q&`V*^@'DYN E([{B#RSzr6u";,v` {/7_#9aȡƢs ڏJ|U /يm+M+W;i W~tjFs¾z "| * jL,(*JD: ۟]m`u]yv4y'3fs ƍhnjjj=yjg?FG45 6yمtۿjm.{)kE+߾FL7Ґz'jinijl^{(&Vp֓&Mޖm%x7O@}ϫ~aC _o38|Y?_}Æ mz{Wm70aذv<K?i 6`I3}ɉDfܴ- ם} Ջ_j0b9<}薾tinnji9i;|=72v]?v͍-k<8_.{ ojh6ifo9ah'>qɫ7#^wc`}{`ݑM O?6]?`dScca69wGk1y\KKsSSIw\Wzjnlh1iS8s1͍ MwoN󰆆1}W;l}:aTKssSc˸KPX]?vKCC{߲,z5̟,{M'sǧ mm:7o6z^ܸ~㺃#n֬ٳqσOa]l8iҤqӇ Z5wƘa M6ǿrΰݻOcuG onnn53z8}lð~?sGwƆa 6=yx?qZyfVZXMsm6x|Ӟ?crDa?闻Тwwx?k|vC Ku}KcsW|Ɂ7|V>}.}ݪ~бQXɻ{ٷ]U&_?tn?}u){3NWf a7޷Xc3?i O>woqAƾtU/}[OwnO?K[1{hA녱>gGWu/:K7~31ռyu-c'o噻^>ùw9;/~宋=a_2{ GcNqagǝt_g{EҴkf6eEJT@9`939cʝ3 Tq@83ߏda1\z痸3U]OO؋e5U)՗}n3ɕ! [e>ϿVfCՋΗ,{\c 4K Og|(AL4D5dv{e:u\mx"Ã"OD:7?ܼtƇoZAW ^fDj6iU9je+ߞXqX_`h$.8,oTBU)TiD`WfuMXvN|zQ2pkQ߮U6-P@!8/'{'6c\os&8kVM.y$.1?ԥX7ȊE?1v݃)ҏjTzYnTmْxHmdZRYSնs/9s"WvO3ߣ1ݏk&b>HIaZ:Ys* ys`zaM=O`'+*#c'2nf$:$~ b܉v&y6,G*+?|ih -t1[k*tuµ?*?ݾ+ xgB(NVJչ7Ϧ,J]yzcUO˳g|J 3'NOz`qٲYٹZIJ(ӜMɗHy/d x\\!WT36?y*#(U'ڊp1* meK>8vE^߷nczJi▴"ѻKK,͉҈(=)Yqq[Or&&jPQӎAˈv&׎X".xy% ij%sR2rqG赟Jz'ҪC&%fXf1zvk94 EHZXw9*QA˹1?:2@=a =OO[KZ˦y[EP{㧯pVfhT0@/h^7&ޘX5'bg/AH7 IDAT4hW ZcAP@Hsu`~YQ Q+vPAz1n3ۧwlW/Ez 5# A(B"Z:*>$/C sα4p@7f~?LWM}loYEQ}C Eo@EPc(5?_m#6=fceϦ;Qwq1j7b4F;(Mu1 'Q!uV-e!K8/PT+>taȡS%C/lW:IIqN+ŋzvbI৬ %rchlla`8!So6аXJ[U-,8p_wV4I,S 3CF3jYT15*BZ˅Ee` 072D` QI i\π+Udm^8R!YDZZ9XՒĕJИUB՞($yķ'T %^g%!Y& !qJ ^]R#tRՕBiRC.f)-.,.#8<c_ @drܚeܨO$iV^;K 2ëQyCQ;o{Ћ(עZiP\I *ԉQ7ܸ6R$R ,z]ĕ*5Bcњ/Qz՟UKZBPz}FFeiQ8| q5T_P)dm*GRUVWTF0>뛠^U CiZ$i55N"}Ng27۵P %8ԘMCHBRh<\")U %2uÈxTX,U1f1 eHRA2Ǖ~*,Q3 S*.Eci3g__/Z322ёY\+obqtƦl-n8>FGpu-lkx.kT5c0tS;:DJuGL eX|)ЦC1HbCCZ ¬VJMk θl~ѹN >͝x0pyzi(u"247gm0/`ҙ:-az&_n->:u^73s{])[tV_4#SսQgA|kC]Mo[amAE8<!¿je,\t¬-~r7odiC_2)r쎸ԷjezT;,NO7Q)aӝb/@¿%{ޓXtY*o9/:dʴ=9oSbUpc˧NOwѤTC@O_" Vuvg5 å?O]RNx "G~W7.-WP}hk=+*ߧ;r*iCQSފ;B:ޗ"Ow-3OZt=~Gֈa7Ο=Ju%<1ms$DRvm>S}@6·]򯶧0OTt_~F_ײoiy|Dߣ}/_kW>_ Tc|{Ж޶Z(s=ϊpcNڹqÂ#˗dAJH41fP$_[5~C.WJ4}f ^kU~|WܪvI}.YkaI{>ntVӕϢLo)Y=:ϩifG\xVV<(ʊ_l++nj Xmz7?>sB{W;cT.P3aVL{{X"O޳j瘃jo80LEnwU)N$4@W5r:A];"."}BQҋ^pqJTϭ37G.j}~j4҉9&e 2sfhh yHQ&8YAH-N6$ $&NjꢁS8E___8N"n_]+5ȕVCA8ݴH'UL9ؘؙb욽GBtgm[soi|]V9f_K K߶,dޮom+:opY;ExmKNÂ盚ZJ5Όo318M~`y\o8|r磃ٙ I]wonmb51w=Rf,m0+<Ӄ\ۯٳ m\v~[{~ Lu13LLs쳗łL욄Do̐i\4\`&05sJy͉uQNz}nbGj ~k/hEjѮW)-*Q@SavMx+@ Oڱ-vgk%дٲuM5-Hz||YWKdl]Hպq^u*B&d2B]cEUJEeEIS+*k[@65=ókF4q&GqAڵ'FSWP8eg߸ɽ,4r6?;tl87;&N9iWC_=f;عㄍG\pz%g&yTϊ?t+cn[ +WǏwyyHP<ܧ[%ݳ0@dCNsl=wOŬVj~eeNuO0AVû29~z>@ІoG8|W;;w8|lUO` }o3~(T򺄇60ѴSFm ^uz<<@qGN.{䳽{ z,<y9;qB".Kӎ{ ת|3kޘs^NoN?ܽDɏz9B u |PnIjJMxFT^6ܺ#"+_(tX['zP(LJ9LyڒE1Hч|Ԏ ik[Ԧt0 2Bfv$/kT)eW).hNbb JU%J%܎sA֏먙3vZ'Ƴ֓0f,X,Zsz7^ zri1iA T{Cr./)U!~,4 'Bggq⺯ԫW\Ar<.x| ?eANFfjDIJ*gXVNV @I>5Wf{fxsVocgLу[]x'O6Y̓vz159\[dJ~h7 "<ǬGE,uƪkB4c^iYW[j11 ;y;a!m &4k$. <)6mrqs;7LyV(g 8VYRrX(B~I tC#m^ Ŷ5rInw=cLux[i<'ώ؄BZҥQAMkdBwE  eA$*d[z=݉#>42 tCs;hҤuINˍǮuw4>}F÷srn -\-?/8(`};{X6=1 L@Y6] ڧWtRdXl: 'k;ҥ~0@ʗJבtM*wm+v!4kS u^SEo1~԰h'gIY܎ 4c~V 瞆i1\G.aG'*M;݆ gIW1ic[/u‡!7.1oċJ"5QP$]@[|YC+M=qnq:$ ˘P]uc4?^~YIyS3< }cc/ИHn#Gƌ~u> X̯Ya]u2g8:@#,\-;'T=94uj)@Z 2}'㏋Ch :!ux HRyHW je.mzڄ)]p|ykAtmTم7j97|nCb@+3+e3/s#5w>afΎFҧSkPbZqNhMCPcR/exI]^%|ۙc]>;|=IG{LZݥ/kOLl7Nx̨3_˫h"g>8Օ >帆 4'dF-7Ǝ%O[[CZ-v;[<ЪtGĐVWP.y̲ϸ=? džaҳ?d:oR.9 ؆&66dБqnm,dEaQ'"D!$D ޻ݿ@_Z;|M XF`qC?%)_~D2a~ާrRܯ{^m>UTqbw{MX6ԭA]x/kaL^Q3t/keƑ\*do6DL_, LPD5IA3"+&O?CUL[vym}IᇜkןaADTԔŷ0ArӯfJf>d 9EXZ,Q)D2y`h(nE6B鱙vm`"EO? CkH%^6jeYtSErRRQˡM ZuMD* j"Kݰ?%6!@Դլ#}m>)Ӂ 88ݶqxءY;[eQg8Ϊf@k:mw%7Wb&i*vքQvVM0y%Ӏ"gNݶAŃwJ`$Q#ωA ivHPq!( Ek1"c2i<#p *3i(`ZZX !%ZLiZ!SUl$!PEj1؀E<ā`j1P. KLj5Tqʆ' N ?ޕ 57|2IADI38X- iA~q &z4,z8ظMrNTV,cD{nU4 %4&P s".H4C9|yogD kaa"S(X\riP*G؆\!GX%˛u;)FxDI723qh()Vo@CB'V#ψ]e6uD0IUj b$GL0.6 Ej1X=0DThM#!sIXbjO]k++e c96@*$r SW_@+-]Uj*{qdi7~>`˃^?(_$iOL_rk {dІ&W9/ yx5/C<:vN )"AEQ`Pϔ/*g^)QиA3Nl@epw7(6tn?׾R~tvEu 4wB5ϒ_-?0F1aՃsbVۇ9)EɧSkϰ;EG bEјQ@*3svJZ&I4Nb:{5o"dIDAT7wj08 ~7ʃ4A{.8TF#ju<I-*7 (P3 NM[ͺZV.3^W3kocuHC#ý,My<oC[ƶux6[3(Q@Ͱ(ğ k&-Ga ;ަ(a[3 ny;i#F^dJfk I?֨#gmi3w>w)Q@9Qӎn/Jܴx6IO>z1GXd8½K߆ՃMdi7&* ! TO=B)*A9Ckc%|V[hjΟా?HɧS)Ak@¿â@(P@Z3S)|jEjIH â@(P@rX(P@9, (P (E â@(P@rX(P@9, (P (E â@e- zIENDB`pyotherside-1.6.2/docs/index.rst000066400000000000000000001263461475412515400166740ustar00rootroot00000000000000PyOtherSide Developer Guide =========================== *PyOtherSide* is a QML Plugin for Qt 5 and Qt 6 that provides access to a Python 3 interpreter from QML. It was designed with mobile devices in mind, where high-framerate touch interfaces are common, and where the user usually interfaces only with one application at a time via a touchscreen. As such, it is important to never block the UI thread, so that the user can always continue to use the interface, even when the backend is processing, downloading or calculating something in the background. At its core, PyOtherSide is basically a simple layer that converts Qt (QML) objects to Python objects and vice versa, with focus on asynchronous events and continuation-passing style function calls. Qt 6 Support ============ .. versionadded:: 1.6.0 PyOtherSide now supports Qt 6 while retaining source compatibility with Qt 5. The following restrictions currently apply when using Qt 6: * ``PyGLArea`` is currently broken with Qt 6, use ``PyFBO`` instead. QML API ======= This section describes the QML API exposed by the *PyOtherSide* QML Plugin. Import Versions --------------- The current QML API version of PyOtherSide is 1.5. When new features are introduced, or behavior is changed, the API version will be bumped and documented here. io.thp.pyotherside 1.0 `````````````````````` * Initial API release. io.thp.pyotherside 1.2 `````````````````````` * :func:`importModule` now behaves like the ``import`` statement in Python for names with dots. This means that ``importModule('x.y.z', ...)`` now works like ``import x.y.z`` in Python. * If a JavaScript exception occurs in the callback passed to :func:`importModule` or :func:`call`, the signal :func:`error` is emitted with the exception information (filename, line, message) as ``traceback``. io.thp.pyotherside 1.3 `````````````````````` * :func:`addImportPath` now also accepts ``qrc:/`` URLs. This is useful if your Python files are embedded as Qt Resources, relative to your QML files (use :func:`Qt.resolvedUrl` from the QML file). io.thp.pyotherside 1.4 `````````````````````` * Added :func:`getattr` * :func:`call` and :func:`call_sync` now accept a Python callable object for the first parameter (previously, only strings were supported) * If :func:`error` doesn't have a handler defined, error messages will be printed to the console as warnings io.thp.pyotherside 1.5 `````````````````````` * Added ``PyGLArea`` and ``PyFBO`` for OpenGL rendering, see `OpenGL rendering in Python`_ * Added :func:`importNames` and :func:`importNames_sync` to mirror Python's ``from foo import bar, baz`` import mechanism QML ``Python`` Element ---------------------- The ``Python`` element exposes a Python interpreter in a QML file. In PyOtherSide 1.0, if multiple Python elements are instantiated, they will share the same underlying Python interpreter, so Python module-global state will be shared between all Python elements. To use the ``Python`` element in a QML file, you have to import the plugin using: .. code-block:: javascript import io.thp.pyotherside 1.5 Signals ``````` .. function:: received(var data) Default event handler for :func:`pyotherside.send` if no other event handler was set. .. function:: error(string traceback) Error handler for errors from Python. .. versionchanged:: 1.4.0 If the error signal is not connected, PyOtherSide will print the error as QWarning on the console (previously, error messages were only shown if the signal was connected and printed there). To avoid printing the error, just define a no-op handler. Methods ``````` To configure event handlers for events from Python, you can use the :func:`setHandler` method: .. function:: setHandler(string event, callable callback) Set the handler for events sent with :func:`pyotherside.send`. Importing modules is then done by optionally adding an import path and then importing the module asynchronously: .. function:: addImportPath(string path) Add a path to Python's ``sys.path``. .. versionchanged:: 1.1.0 :func:`addImportPath` will automatically strip a leading ``file://`` from the path, so you can use :func:`Qt.resolvedUrl()` without having to manually strip the leading ``file://`` in QML. .. versionchanged:: 1.3.0 Starting with QML API version 1.3 (``import io.thp.pyotherside 1.3``), :func:`addImportPath` now also accepts ``qrc:/`` URLs. The first time a ``qrc:/`` path is added, a new import handler will be installed, which will enable Python to transparently import modules from it. .. function:: importModule(string name, function callback(success) {}) Import a Python module. .. versionchanged:: 1.2.0 Previously, this function didn't work correctly for importing modules with dots in their name. Starting with the API version 1.2 (``import io.thp.pyotherside 1.2``), this behavior is now fixed, and ``importModule('x.y.z', ...)`` behaves like ``import x.y.z``. .. versionchanged:: 1.2.0 If a JavaScript exception occurs in the callback, the :func:`error` signal is emitted with ``traceback`` containing the exception info (QML API version 1.2 and newer). .. function:: importNames(string module, array object_names, function callback(success) {}) Import a list of names from a given modules, like Python's ``from foo import bar, baz`` syntax -- the equivalent call would be ``importNames('module', ['bar', 'baz'], ...);`` .. versionadded:: 1.5.0 Once modules are imported, Python function can be called on the imported modules using: .. function:: call(var func, args=[], function callback(result) {}) Call the Python function ``func`` with ``args`` asynchronously. If ``args`` is omitted, ``func`` will be called without arguments. If ``callback`` is a callable, it will be called with the Python function result as single argument when the call has succeeded. .. versionchanged:: 1.2.0 If a JavaScript exception occurs in the callback, the :func:`error` signal is emitted with ``traceback`` containing the exception info (QML API version 1.2 and newer). .. versionchanged:: 1.4.0 ``func`` can also be a Python callable object, not just a string. Attributes on Python objects can be accessed using :func:`getattr`: .. function:: getattr(obj, string attr) -> var Get the attribute ``attr`` of the Python object ``obj``. .. versionadded:: 1.4.0 For some of these methods, there also exist synchronous variants, but it is highly recommended to use the asynchronous variants instead to avoid blocking the QML UI thread: .. function:: evaluate(string expr) -> var Evaluate a Python expression synchronously. .. function:: importModule_sync(string name) -> bool Import a Python module. Returns ``true`` on success, ``false`` otherwise. .. function:: importNames_sync(string module, array names) -> bool Import names from a Python modules. Returns ``true`` on success, ``false`` otherwise. .. function:: call_sync(var func, var args=[]) -> var Call a Python function. Returns the return value of the Python function. .. versionchanged:: 1.4.0 ``func`` can also be a Python callable object, not just a string. The following functions allow access to the version of the running PyOtherSide plugin and Python interpreter. .. function:: pluginVersion() -> string Get the version of the PyOtherSide plugin that is currently used. .. note:: This is not necessarily the same as the QML API version currently in use. The QML API version is decided by the QML import statement, so even if :func:`pluginVersion` returns 1.2.0, if the plugin has been imported as ``import io.thp.pyotherside 1.0``, the API version used would be 1.0. .. versionadded:: 1.1.0 .. function:: pythonVersion() -> string Get the version of the Python interpreter that is currently used. .. versionadded:: 1.1.0 .. versionchanged:: 1.5.0 Previously, :func:`pythonVersion` returned the compile-time version of Python against which PyOtherSide was built. Starting with version 1.5.0, the run-time version of Python is returned (e.g. PyOtherSide compiled against Python 3.4.0 and running with Python 3.4.1 returned "3.4.0" before, but returns "3.4.1" in PyOtherSide after and including 1.5.0). QML ``PyGLArea`` Element ------------------------ .. versionadded:: 1.5.0 The PyGLArea allows rendering arbitrary OpenGL content from Python into the QML scene. Properties `````````` .. function:: PyObject renderer Python object that implements the IRenderer interface, see `OpenGL rendering in Python`_ for details. .. function:: bool before ``true`` to render before (= below) the rest of the QML scene, ``false`` to render after (= above) the rest of the QML scene. Default: ``true`` QML ``PyFBO`` Element --------------------- .. versionadded:: 1.5.0 The PyFBO allows offscreen rendering of arbitrary OpenGL content from Python into the QML scene. Properties `````````` .. function:: PyObject renderer Python object that implements the IRenderer interface, see `OpenGL rendering in Python`_ for details Python API ========== PyOtherSide uses a normal Python 3.x interpreter for running your Python code. The ``pyotherside`` module -------------------------- When a module is imported in PyOtherSide, it will have access to a special module called :mod:`pyotherside` in addition to all Python Standard Library modules and Python modules in ``sys.path``: .. code-block:: python import pyotherside The module can be used to send events asynchronously (even from different threads) to the QML layer, register a callback for doing clean-ups at application exit and integrate with other QML-specific features of PyOtherSide. Methods ``````` .. function:: pyotherside.send(event, \*args) Send an asynchronous event with name ``event`` with optional arguments ``args`` to QML. .. function:: pyotherside.atexit(callback) Register a ``callback`` to be called when the application is closing. .. function:: pyotherside.set_image_provider(provider) Set the QML `image provider`_ (``image://python/``). .. versionadded:: 1.1.0 .. function:: pyotherside.qrc_is_file(filename) Check if ``filename`` is an existing file in the `Qt Resource System`_. :returns: ``True`` if ``filename`` is a file, ``False`` otherwise. .. versionadded:: 1.3.0 .. function:: pyotherside.qrc_is_dir(dirname) Check if ``dirname`` is an existing directory in the `Qt Resource System`_. :returns: ``True`` if ``dirname`` is a directory, ``False`` otherwise. .. versionadded:: 1.3.0 .. function:: pyotherside.qrc_get_file_contents(filename) Get the file contents of a file in the `Qt Resource System`_. :raise ValueError: If ``filename`` does not denote a valid file. :returns: The file contents as Python ``bytearray`` object. .. versionadded:: 1.3.0 .. function:: pyotherside.qrc_list_dir(dirname) Get the entry list of a directory in the `Qt Resource System`_. :raise ValueError: If ``dirname`` does not denote a valid directory. :returns: The directory entries as list of strings. .. versionadded:: 1.3.0 .. _Qt Resource System: http://qt-project.org/doc/qt-5/resources.html .. _constants: Constants ````````` .. versionadded:: 1.1.0 These constants are used in the return value of a `image provider`_ function: **pyotherside.format_mono** Mono pixel format (``QImage::Format_Mono``). **pyotherside.format_mono_lsb** Mono pixel format, LSB alignment (``QImage::Format_MonoLSB``). **pyotherside.format_rgb32** 32-bit RGB format (``QImage::Format_RGB32``). **pyotherside.format_argb32** 32-bit ARGB format (``QImage::Format_ARGB32``). **pyotherside.format_rgb16** 16-bit RGB format (``QImage::Format_RGB16``). **pyotherside.format_rgb666** 18bpp RGB666 format (``QImage::Format_RGB666``). **pyotherside.format_rgb555** 15bpp RGB555 format (``QImage::Format_RGB555``). **pyotherside.format_rgb888** 24-bit RGB format (``QImage::Format_RGB888``). **pyotherside.format_rgb444** 12bpp RGB format (``QImage::Format_RGB444``). **pyotherside.format_data** Encoded image file data (e.g. PNG/JPEG data). .. versionadded:: 1.3.0 The following constants have been added in PyOtherSide 1.3: **pyotherside.version** Version of PyOtherSide as string. .. versionadded:: 1.5.0 The following constants have been added in PyOtherSide 1.5: **pyotherside.format_svg_data** SVG image XML data Data Type Mapping ================= PyOtherSide will automatically convert Python data types to Qt data types (which in turn will be converted to QML data types by the QML engine). The following data types are supported and can be used to pass data between Python and QML (and vice versa): +--------------------+----------------+-----------------------------+ | Python | QML | Remarks | +====================+================+=============================+ | bool | bool | | +--------------------+----------------+-----------------------------+ | int | int | | +--------------------+----------------+-----------------------------+ | float | double | | +--------------------+----------------+-----------------------------+ | str | string | | +--------------------+----------------+-----------------------------+ | list | JS Array | JS Arrays are always | | | | converted to Python lists. | +--------------------+----------------+-----------------------------+ | tuple | JS Array | | +--------------------+----------------+-----------------------------+ | dict | JS Object | Keys must be strings | +--------------------+----------------+-----------------------------+ | datetime.date | QML date | since PyOtherSide 1.2.0 | +--------------------+----------------+-----------------------------+ | datetime.time | QML time | since PyOtherSide 1.2.0 | +--------------------+----------------+-----------------------------+ | datetime.datetime | JS Date | since PyOtherSide 1.2.0 | +--------------------+----------------+-----------------------------+ | set | JS Array | since PyOtherSide 1.3.0 | +--------------------+----------------+-----------------------------+ | iterable | JS Array | since PyOtherSide 1.3.0 | +--------------------+----------------+-----------------------------+ | object | (opaque) | since PyOtherSide 1.4.0 | +--------------------+----------------+-----------------------------+ | pyotherside.QObject| QObject | since PyOtherSide 1.4.0 | +--------------------+----------------+-----------------------------+ | bytes | JS ArrayBuffer | since PyOtherSide 1.5.6; | | | | requires Qt 5.8; the C++ | | | | data type is QByteArray | +--------------------+----------------+-----------------------------+ Trying to pass in other types than the ones listed here is undefined behavior and will usually result in an error. .. _image provider: Image Provider ============== .. versionadded:: 1.1.0 A QML Image Provider can be registered from Python to load image data (e.g. map tiles, diagrams, graphs or generated images) in QML ``Image`` elements without resorting to saving/loading files. An image provider has the following argument list and return values: .. code-block:: python def image_provider(image_id, requested_size): ... return bytearray(pixels), (width, height), format The parameters to the image provider functions are: **image_id** The ID of the image URL (``image://python/``). **requested_size** The source size of the QML ``Image`` as tuple: ``(width, height)``. ``(-1, -1)`` if the source size is not set. The image provider must return a tuple ``(data, size, format)``: **data** A ``bytearray`` object containing the pixel data for the given size and the given format. **size** A tuple ``(width, height)`` describing the size of the pixel data in pixels. **format** The pixel format of ``data`` (see `constants`_), ``pyotherside.format_data`` if ``data`` contains an encoded (PNG/JPEG) image instead of raw pixel data or ``pyotherside.format_svg_data`` if ``data`` contains SVG image XML data. In order to register the image provider with PyOtherSide for use as provider for ``image://python/`` URLs, the image provider function needs to be passed to PyOtherSide: .. code-block:: python import pyotherside def image_provider(image_id, requested_size): ... pyotherside.set_image_provider(image_provider) Because Python modules are usually imported asynchronously, the image provider will only be registered once the module registering the image provider is successfully imported. You have to make sure that setting the ``source`` property on a QML ``Image`` element only happens *after* the image provider has been set (e.g. by setting the ``source`` property in the callback function passed to :func:`importModule`). .. _qt resource access: Qt Resource Access ================== .. versionadded:: 1.3.0 If you are using PyOtherSide in combination with an application binary compiled from C++ code with Qt Resources (see `Qt Resource System`_), you can inspect and access the resources from Python. This example demonstrates the API by walking the whole resource tree, printing out directory names and file sizes: .. code-block:: python import pyotherside import os.path def walk(root): for entry in pyotherside.qrc_list_dir(root): name = os.path.join(root, entry) if pyotherside.qrc_is_dir(name): print('Directory:', name) walk(name) else: data = pyotherside.qrc_get_file_contents(name) print('File:', name, 'has', len(data), 'bytes') walk('/') Importing Python modules from Qt Resources also works starting with QML API 1.3 using :func:`Qt.resolvedUrl` from within a QML file in Qt Resources. As an alternative, ``addImportPath('qrc:/')`` will add the root directory of the Qt Resources to Python's module search path. .. _qobjects in python: Accessing QObjects from Python ============================== .. versionadded:: 1.4.0 Since version 1.4, PyOtherSide allows passing QObjects from QML to Python, and accessing (setting / getting) properties and calling slots and dynamic methods. References to QObjects passed to Python can be passed back to QML transparently: .. code-block:: python # Assume func will be called with a QObject as sole argument def func(qobject): # Getting properties print(qobject.x) # Setting properties qobject.x = 123 # Calling slots and dynamic functions print(qobject.someFunction(123, 'b')) # Returning a QObject reference to the caller return qobject It is possible to store a reference (bound method) to a method of a QObject. Such references cannot be passed to QML, and can only be used in Python for the lifetime of the QObject. If you need to pass such a bound method to QML, you can wrap it into a Python object (or even just a lambda) and pass that instead: .. code-block:: python def func(qobject): # Can store a reference to a bound method bound_method = qobject.someFunction # Calling the bound method bound_method(123, 'b') # If you need to return the bound method, you must wrap it # in a lambda (or any other Python object), the bound method # cannot be returned as-is for now return lambda a, b: bound_method(a, b) It's not possible to instantiate new QObjects from within Python, and it's not possible to subclass QObject from within Python. Also, be aware that a reference to a QObject in Python will become invalid when the QObject is deleted (there's no way for PyOtherSide to prevent referenced QObjects from being deleted, but PyOtherSide tries hard to detect the deletion of objects and give meaningful error messages in case the reference is accessed). Calling signals of QML objects ------------------------------ .. versionadded:: 1.5.4 Calling (emitting) signals of QML objects is supported since PyOtherSide 1.5.4. However, as signals do not have a return value as such, the return value is either just `true` or `false`, depending on whether the call worked or not. OpenGL rendering in Python ========================== .. versionadded:: 1.5.0 You can render directly to a QML application's OpenGL context in your Python code (i.e. via PyOpenGL or vispy.gloo) by using a ``PyGLArea`` or ``PyFBO`` item. The ``IRenderer`` interface that needs to be implemented in Python and set as the ``renderer`` property of ``PyGLArea`` or ``PyFBO`` needs to provide the following functions: .. function:: IRenderer.init() Initialize OpenGL resources required for rendering. This method is optional. .. function:: IRenderer.reshape(x, y, width, height) Called when the geometry has changed. ``(x, y)`` is the position of the bottom left corner of the area, in window coordinates, e.g. (0, 0) is the bottom left corner of the window. .. function:: IRenderer.render() Render to the OpenGL context. It is the renderer's responsibility to unbind any used resources to leave the context in a clean state. .. function:: IRenderer.cleanup() Free any resources allocated by :func:`IRenderer.init`. This method is optional. See `Rendering with PyOpenGL`_ for an example implementation. Note that you might to use a recent version of PyOpenGL (>= 3.1.0) for some of the examples to work, earlier versions had problems. If your distribution does not provide new versions, you can install the most recent version of PyOpenGL to your ``$HOME`` using: .. code-block:: shell pip3 install --user --upgrade PyOpenGL PyOpenGL_accelerate Cookbook ======== This section contains code examples and best practices for combining Python and QML. Importing modules and calling functions asynchronously ------------------------------------------------------ In this example, we import the Python Standard Library module ``os`` and - when the module is imported - call the :func:`os.getcwd` function on it. The result of the :func:`os.getcwd` function is then printed to the console and :func:`os.chdir` is called with a single argument (``'/'``) - again, after the :func:`os.chdir` function has returned, a message will be printed. In this example, importing modules and calling functions are both done in an asynchronous way - the QML/GUI thread will not block while these functions execute. In fact, the ``Component.onCompleted`` code block will probably finish before the :mod:`os` module has been imported in Python. .. code-block:: javascript Python { Component.onCompleted: { importModule('os', function() { call('os.getcwd', [], function (result) { console.log('Working directory: ' + result); call('os.chdir', ['/'], function (result) { console.log('Working directory changed.'); });); }); }); } } While this `continuation-passing style`_ might look a like a little pyramid due all the nesting and indentation at first, it makes sure your application's UI is always responsive. The user will be able to interact with the GUI (e.g. scroll and move around in the UI) while the Python code can process requests. .. _Continuation-passing style: https://en.wikipedia.org/wiki/Continuation-passing_style To avoid what's called `callback hell`_ in JavaScript, you can pull out the anonymous functions you give as callbacks, give them names and pass them to the API functions via name, e.g. the above example would turn into a shallow structure (of course, in this example, splitting everything out does not make too much sense, as the functions are very simple to begin with, but it's here to demonstrate how splitting a callback hell pyramid basically works): .. _callback hell: http://callbackhell.com/ .. code-block:: javascript Python { Component.onCompleted: { function changedCwd(result) { console.log('Working directory changed.'); } function gotCwd(result) { console.log('Working directory: ' + result); call('os.chdir', ['/'], changedCwd); } function withOs() { call('os.getcwd', [], gotCwd); } importModule('os', withOs); } } Evaluating Python expressions in QML ```````````````````````````````````` The :func:`evaluate` method on the ``Python`` object can be used to evaluate a simple Python expression and return its result as JavaScript object: .. code-block:: javascript Python { Component.onCompleted: { console.log('Squares: ' + evaluate('[x for x in range(10)]')); } } Evaluating expressions is done synchronously, so make sure you only use it for expressions that are not long-running calculations / operations. Error handling in QML --------------------- If an error happens in Python while calling functions, the traceback of the error (or an error message in case the error happens in the PyOtherSide layer) will be sent with the :func:`error` signal of the ``Python`` element. During early development, it's probably enough to just log the error to the console: .. code-block:: javascript Python { // ... onError: console.log('Error: ' + traceback) } Once your application grows, it might make sense to maybe show the error to the user in a dialog box, message or notification in addition to or instead of using :func:`console.log()` to print the error. Handling asynchronous events from Python in QML ----------------------------------------------- Your Python code can send asynchronous events with optional data to the QML layer using the :func:`pyotherside.send` function. You can call this function from functions called from QML, but also from anywhere else - including threads that you created in Python. The first parameter is mandatory, and must be a string that identifies the event. Additional parameters are optional and can be of any data type that PyOtherSide supports: .. code-block:: python import pyotherside pyotherside.send('new-entries', 100, 123) If you do not add a special handler on the ``Python`` object, such events would be handled by the :func:`received` signal handler in QML - its ``data`` parameter contains the event name and all arguments in a list: .. code-block:: javascript Python { // .. onReceived: console.log('Event: ' + data) } Usually, you want to install a handler for such events. If you have e.g. the ``'new-entries'`` event like shown above (with two numeric parameters that we will call ``first`` and ``last`` for this example), you might want to define a simple handler function that will process this event: .. code-block:: javascript Python { // .. Component.onCompleted: { setHandler('new-entries', function (first, last) { console.log('New entries from ' + first + ' to ' + last); }); } } Once a handler for a given event is defined, the :func:`received` signal will not be emitted anymore. If you need to unset a handler for a given event, you can use ``setHandler('event', undefined)`` to do so. In some cases, it might be useful to not install a handler function directly, but turn the :func:`pyotherside.send` call into a new signal on the ``Python`` object. As there is no easy way for PyOtherSide to determine the names of the arguments of the event, you have to define and hook up these signals manually. The upside of having to define the signals this way is that all signals will be nicely documented in your QML file for future reference: .. code-block:: javascript Python { signal updated() signal newEntries(int first, int last) signal entryRenamed(int index, string name) Component.onCompleted: { setHandler('updated', updated); setHandler('new-entries', newEntries); setHandler('entry-renamed', entryRenamed); } } With this setup, you can now emit these signals from the ``Python`` object by using :func:`pyotherside.send` in your Python code: .. code-block:: python pyotherside.send('updated') pyotherside.send('new-entries', 20, 30) pyotherside.send('entry-renamed', 11, 'Hello World') Loading ``ListModel`` data from Python -------------------------------------- Most of the time a PyOtherSide QML application will display some data stored somewhere and retrieved or generated with Python. The easiest way to do this is to return a list-of-dicts in your Python function: **listmodel.py** .. code-block:: python def get_data(): return [ {'name': 'Alpha', 'team': 'red'}, {'name': 'Beta', 'team': 'blue'}, {'name': 'Gamma', 'team': 'green'}, {'name': 'Delta', 'team': 'yellow'}, {'name': 'Epsilon', 'team': 'orange'}, ] Of course, the function could do other things (such as doing web requests, querying databases, etc..) - as long as it returns a list-of-dicts, it will be fine (if you are using a generator that yields dicts, just wrap the generator with :func:`list`). Using this function from QML is straightforward: **listmodel.qml** .. code-block:: javascript import QtQuick 2.0 import io.thp.pyotherside 1.5 Rectangle { color: 'black' width: 400 height: 400 ListView { anchors.fill: parent model: ListModel { id: listModel } delegate: Text { // Both "name" and "team" are taken from the model text: name color: team } } Python { id: py Component.onCompleted: { // Add the directory of this .qml file to the search path addImportPath(Qt.resolvedUrl('.')); // Import the main module and load the data importModule('listmodel', function () { py.call('listmodel.get_data', [], function(result) { // Load the received data into the list model for (var i=0; i 32 bits on platforms where ``sizeof(long)`` is 4 bytes (issue #86) Version 1.5.1 (2017-03-17) -------------------------- * Fix :func:`call_sync` when used with parameters (fix by Robie Basak; issue #49) Version 1.5.0 (2016-06-14) -------------------------- * Support for `OpenGL rendering in Python`_ using PyOpenGL >= 3.1.0 * New QML components: ``PyGLArea``, ``PyFBO`` * :func:`pythonVersion` now returns the runtime Python version * Add the library to ``PYTHONPATH`` for standard library appended as .zip (except on Windows) * Call ``PyDateTime_IMPORT`` as often as necessary (Fixes #46) * Added ``pyotherside.format_svg_data`` for using SVG data in the image provider * Handle converting ``QVariantHash`` to Python ``dict`` type * Added ``.qmltypes`` file to provide metadata information for Qt Creator * New functions :func:`importNames` and :func:`importNames_sync` for from-imports Version 1.4.0 (2015-02-19) -------------------------- * Support for passing Python objects to QML and keeping references there * Add :func:`getattr` to get an attribute from a Python object * :func:`call` and :func:`call_sync` now also accept a Python callable as first argument * Support for `Accessing QObjects from Python`_ (properties and slots) * Print error messages to the console if :func:`error` doesn't have any handlers connected Version 1.3.0 (2014-07-24) -------------------------- * Access to the `Qt Resource System`_ from Python (see `Qt Resource Access`_). * QML API 1.3: Import from Qt Resources (:func:`addImportPath` with ``qrc:/``). * Add ``pyotherside.version`` constant to access version from Python as string. * Support for building on Windows, build instructions for Windows builds. * New data type conversions: Python ``set`` and iterable types (e.g. generator expressions and generators) are converted to JS ``Array``. Version 1.2.0 (2014-02-16) -------------------------- * Introduced versioned QML imports for API change. * QML API 1.2: Change :func:`importModule` behavior for imports with dots. * QML API 1.2: Emit :func:`error` when JavaScript callbacks passed to :func:`importModule` and :func:`call` throw an exception. * New data type conversions: Python ``datetime.date``, ``datetime.time`` and ``datetime.datetime`` are converted to QML ``date``, ``time`` and JS ``Date`` types, respectively. Version 1.1.0 (2014-02-06) -------------------------- * Add support for Python-based image providers (see `Image Provider`_). * Fix threading crashes and aborts due to assertions. * :func:`addImportPath` will automatically strip a leading ``file://``. * Added :func:`pluginVersion` and :func:`pythonVersion` for runtime version detection. Version 1.0.0 (2013-08-08) -------------------------- * Initial QML plugin release. Version 0.0.1 (2013-05-17) -------------------------- * Proof-of-concept (based on a prototype from May 2011). pyotherside-1.6.2/docs/requirements.txt000066400000000000000000000000301475412515400202740ustar00rootroot00000000000000sphinx-rtd-theme==1.3.0 pyotherside-1.6.2/examples/000077500000000000000000000000001475412515400157055ustar00rootroot00000000000000pyotherside-1.6.2/examples/atexit_example.py000066400000000000000000000004161475412515400212710ustar00rootroot00000000000000# This examples shows how to do cleanups and other things when # the application exists by using pyside.atexit(). import pyotherside def called_when_exiting(): print('Now exiting the application...') pyotherside.atexit(called_when_exiting) print('python loaded') pyotherside-1.6.2/examples/atexit_example.qml000066400000000000000000000006271475412515400214360ustar00rootroot00000000000000import QtQuick 2.0 import io.thp.pyotherside 1.0 Rectangle { width: 200 height: 200 color: 'red' Python { Component.onCompleted: { // Add the directory of this .qml file to the search path addImportPath(Qt.resolvedUrl('.')); importModule_sync('atexit_example'); } onError: console.log('error in python: ' + traceback); } } pyotherside-1.6.2/examples/events_example.py000066400000000000000000000010421475412515400212730ustar00rootroot00000000000000# This example demonstrates the use of pyotherside.send() to send events to Qt. import pyotherside import threading import time print('Using PyOtherSide version', pyotherside.version) COLORS = ['red', 'green', 'blue'] def thread_func(): i = 0 while True: pyotherside.send('append', 'Next Number: ', i) if i % 2 == 0: color = COLORS[int((i / 2) % len(COLORS))] pyotherside.send('color', color) i += 1 time.sleep(1) thread = threading.Thread(target=thread_func) thread.start() pyotherside-1.6.2/examples/events_example.qml000066400000000000000000000014761475412515400214470ustar00rootroot00000000000000import QtQuick 2.0 import io.thp.pyotherside 1.0 Rectangle { width: 400 height: width Python { Component.onCompleted: { // Add the directory of this .qml file to the search path addImportPath(Qt.resolvedUrl('.')); setHandler('append', function (message, number) { output.text += '\n' + message + number; }); setHandler('color', function(color) { output.color = color; }); // Import the main module importModule('events_example', function () { console.log('Event example module is now imported'); }); } onReceived: console.log('Unhandled event: ' + data) } Text { id: output anchors.centerIn: parent } } pyotherside-1.6.2/examples/helloworld.qml000066400000000000000000000034451475412515400206010ustar00rootroot00000000000000import QtQuick 2.0 import io.thp.pyotherside 1.4 Rectangle { width: 200 height: 200 color: 'blue' ListView { id: listView anchors.fill: parent delegate: Text { color: 'white'; text: modelData } } Python { id: python Component.onCompleted: { // Print version of plugin and Python interpreter console.log('PyOtherSide version: ' + pluginVersion()); console.log('Python version: ' + pythonVersion()); // Asynchronous module importing importModule('os', function() { console.log('Python module "os" is now imported'); // Asynchronous function calls call('os.listdir', [], function(result) { console.log('dir listing: ' + result); listView.model = result; }); // Synchronous calls - avoid these, they block the UI call_sync('os.chdir', ['/']); console.log('files in /: ' + call_sync('os.listdir', ['.'])); }); // sychronous imports and calls - again, avoid! importModule_sync('pyotherside'); call_sync('pyotherside.send', ['hello world!', 123]); // error handling importModule_sync('thismoduledoesnotexisthopefully'); evaluate('[ 123 [.syntax234-error!'); } onError: { // when an exception is raised, this error handler will be called console.log('python error: ' + traceback); } onReceived: { // asychronous messages from Python arrive here // in Python, this can be accomplished via pyotherside.send() console.log('got message from python: ' + data); } } } pyotherside-1.6.2/examples/image_loader.py000066400000000000000000000035011475412515400206660ustar00rootroot00000000000000# # PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 # Copyright (c) 2011, 2013, Thomas Perl # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND # FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR # OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # PERFORMANCE OF THIS SOFTWARE. # import pyotherside import os import random from PIL import Image from PIL import ImageDraw def render(image_id, requested_size): # width and height will be -1 if not set in QML if requested_size == (-1, -1): requested_size = (300, 300) img = Image.new('RGBA', requested_size) filename = os.path.join(os.path.dirname(__file__), image_id) logo = Image.open(filename) for x in range(100): img.paste(logo, (random.randint(0, requested_size[0]), random.randint(0, requested_size[1]))) draw = ImageDraw.Draw(img) for x in range(100): draw.text((random.randint(0, requested_size[0]), random.randint(0, requested_size[1])), 'PyOtherSide PIL Test', fill=(random.randint(10, 255), random.randint(10, 255), random.randint(10, 255))) del draw b, g, r, a = img.split() img = Image.merge("RGBA", (r, g, b, a)) return bytearray(img.tobytes()), img.size, pyotherside.format_argb32 pyotherside.set_image_provider(render) pyotherside-1.6.2/examples/image_loader.qml000066400000000000000000000024771475412515400210420ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2011, 2013, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ import QtQuick 2.0 import io.thp.pyotherside 1.0 Image { id: image width: 300 height: 300 Python { Component.onCompleted: { // Add the directory of this .qml file to the search path addImportPath(Qt.resolvedUrl('.')); importModule('image_loader', function () { image.source = 'image://python/pyotherside.png'; }); } onError: console.log('Python error: ' + traceback) } } pyotherside-1.6.2/examples/imageprovider_data.py000066400000000000000000000054071475412515400221130ustar00rootroot00000000000000# # PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 # Copyright (c) 2011, 2013, Thomas Perl # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND # FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR # OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # PERFORMANCE OF THIS SOFTWARE. # import pyotherside import os import base64 # base64-encoded image data (PNG) IMAGE_DATA = b""" iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAABRFBMVEX////C0d5+pcZklb1JhbZD g7ZDf7JQhbBkkbV9ob/X3uOdutFDhbs4gr44fLU3ea83dqtkjrI4f7o3c6VCc6VJiLtEicDN3+73 9/+CrdFwlrWbv9329fU8cp6Cr9Q2bZybvdo3apRakb/v7+//7WD/5ln44W5/qMr/617/41T51ls4 hcP/20z12oL7zkewx9n/1UTx5cNyoMVQfKH16Mb/4FL/0ED12Y1Sjb341Wj4zVd/nrjx7Nr/zDv7 yED71U/8xTmAo8DG1OD/2En/xTJDeafc4ef145j64mL7wDf15qb/vCn5wkSApsb3zXTz8vJCfa76 vkCQr8n/tSH22Jf5wEmhus9Jf6z/zVf/4Jf/1nv/9+X/03b9vi7/+/L/1oP33pL/z2r/7cj/5Kz/ uC330oT4x2L236D/tTH31Hvr6urm5uaTuO3WAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAAASAAAAEgA RslrPgAAAnBJREFUaN7tlltz0kAYQBcVRcsmbdDWKooppDZNRFRAUWhtoViKFjXWe73f/f/v7iVZ toG8ZD9mdNzz2plzyJdvt0FIo9FoNBrNv0bm2PET2ZOEU7nTZ+D1c3mMsWEYpmnOExbA/RhblmXQ AtEXCoWzwIFzeHHp/DJ7ANMsUC7ABvL4IkLFcEAscAk2gPFlhJYM4S+VStABvHwlKwZUAg+wF3zE Dx0QGxr5gQOCXBSwgYSZFXrCcHxAtm2XKxXHcVYV/VcxTvDb5bU1x3XddSX/nOd59Ahb0gkQAYf6 ff+aSmCR+6UrQvjtssv8flXBf91LHBALEL1fq91IH7jpefIdKhaIcqveaFB9rXk7feAOThyQnWn5 3N+8mz6QTx7QvXbo73Q20gcsa/KK4PPJtDepn+g7nfvpA/Lv39ouj6m3ut0u9/d6PaUnMHhhp/9g t8Kh68/XJ/QrBLJiQP1VoXca0fqE/kH6wErk39qb4m9y//Bh+sCj6AT0d8fjccX69Jh/uJ8+sB2e gPmRE/NH4yH+YTt9AD0ON3SU4Cf64RMFP3rKP7N4YHJ9qD94phJAOX5FjKauD/MfKPkRev5ih4DI P5fQX61WNxgDhsIbPoIYfwtIGMedWB+F4zU9EH+9wAE6/uj27L189Zq8XtiAL6/PG4TeBoewgU12 O4fXwzuE3gfAI/og3z4fP33+EnyFDaB16fYJCN+A/Qh9r0anl+h/qN0PCdR/HgwO6e22r/AlpPlr KY4mmIU8/OoCjRSLsloGpiHrf42REwC/X+h/c0QCojDjJ5j9O2CJ4pQlJdA/qOulSgwwt0aj0Wg0 Gs1/wR/yOd+mI4x7LgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxMy0xMC0wNFQwMzo1OTozNCswMDow MLgfTfgAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTMtMTAtMDRUMDM6NTk6MzQrMDA6MDDJQvVEAAAA AElFTkSuQmCC """ def load_data(image_id, requested_size): # If you return data in the format "pyotherside.format_data", the size is # ignored; the data is interpreted as image file data (e.g. PNG, jpeg, ..) return bytearray(base64.b64decode(IMAGE_DATA)), (-1, -1), pyotherside.format_data pyotherside.set_image_provider(load_data) pyotherside-1.6.2/examples/imageprovider_data.qml000066400000000000000000000025031475412515400222460ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2011, 2013, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ import QtQuick 2.0 import io.thp.pyotherside 1.0 Image { id: image width: 300 height: 300 Python { Component.onCompleted: { // Add the directory of this .qml file to the search path addImportPath(Qt.resolvedUrl('.')); importModule('imageprovider_data', function () { image.source = 'image://python/get-some-data'; }); } onError: console.log('Python error: ' + traceback) } } pyotherside-1.6.2/examples/imageprovider_example.py000066400000000000000000000031731475412515400226330ustar00rootroot00000000000000# # PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 # Copyright (c) 2011, 2013, Thomas Perl # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND # FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR # OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # PERFORMANCE OF THIS SOFTWARE. # import pyotherside import math def render(image_id, requested_size): print('image_id: "{image_id}", size: {requested_size}'.format(**locals())) # width and height will be -1 if not set in QML if requested_size == (-1, -1): requested_size = (300, 300) width, height = requested_size # center for circle cx, cy = width/2, 10 pixels = [] for y in range(height): for x in range(width): pixels.extend(reversed([ 255, # alpha int(10 + 10 * ((x - y * 0.5) % 20)), # red 20 + 10 * (y % 20), # green int(255 * abs(math.sin(0.3*math.sqrt((cx-x)**2 + (cy-y)**2)))) # blue ])) return bytearray(pixels), (width, height), pyotherside.format_argb32 pyotherside.set_image_provider(render) pyotherside-1.6.2/examples/imageprovider_example.qml000066400000000000000000000025211475412515400227700ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2011, 2013, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ import QtQuick 2.0 import io.thp.pyotherside 1.0 Image { id: image width: 300 height: 300 Python { Component.onCompleted: { // Add the directory of this .qml file to the search path addImportPath(Qt.resolvedUrl('.')); importModule('imageprovider_example', function () { image.source = 'image://python/image-id-passed-from-qml'; }); } onError: console.log('Python error: ' + traceback) } } pyotherside-1.6.2/examples/imageprovider_svg_data.py000066400000000000000000000025471475412515400227740ustar00rootroot00000000000000# # PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 # Copyright (c) 2011, 2013, Thomas Perl # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND # FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR # OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # PERFORMANCE OF THIS SOFTWARE. # import pyotherside import os HERE = os.path.dirname(__file__) or '.' def load_data(image_id, requested_size): # If you return data in the format "pyotherside.format_svg_data" requested_size # is is used as the target size when rendering the SVG image # We use the image id to get name of the SVG file to render with open(os.path.join(HERE, image_id), 'rb') as f: svg_image_data = f.read() return bytearray(svg_image_data), requested_size, pyotherside.format_svg_data pyotherside.set_image_provider(load_data) pyotherside-1.6.2/examples/imageprovider_svg_data.qml000066400000000000000000000025761475412515400231370ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2011, 2013, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ import QtQuick 2.0 import io.thp.pyotherside 1.0 Image { id: image width: 300 height: 300 sourceSize.width: 300 sourceSize.height: 300 Python { Component.onCompleted: { // Add the directory of this .qml file to the search path addImportPath(Qt.resolvedUrl('.')); importModule('imageprovider_svg_data', function () { image.source = 'image://python/python_logo.svg'; }); } onError: console.log('Python error: ' + traceback) } } pyotherside-1.6.2/examples/listmodel.py000066400000000000000000000004711475412515400202550ustar00rootroot00000000000000# Example how to fill a list model with data from Python. def get_data(): return [ {'name': 'Alpha', 'team': 'red'}, {'name': 'Beta', 'team': 'blue'}, {'name': 'Gamma', 'team': 'green'}, {'name': 'Delta', 'team': 'yellow'}, {'name': 'Epsilon', 'team': 'orange'}, ] pyotherside-1.6.2/examples/listmodel.qml000066400000000000000000000017311475412515400204160ustar00rootroot00000000000000import QtQuick 2.0 import io.thp.pyotherside 1.0 Rectangle { color: 'black' width: 400 height: 400 ListView { anchors.fill: parent model: ListModel { id: listModel } delegate: Text { // Both "name" and "team" are taken from the model text: name color: team } } Python { id: py Component.onCompleted: { // Add the directory of this .qml file to the search path addImportPath(Qt.resolvedUrl('.')); // Import the main module and load the data importModule('listmodel', function () { py.call('listmodel.get_data', [], function(result) { // Load the received data into the list model for (var i=0; i # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND # FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR # OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # PERFORMANCE OF THIS SOFTWARE. # import pyotherside import random COLORS = [(255, 255, 255, 255), (255, 0, 0, 0)] for i in range(30): COLORS.insert(1, ( 255, # alpha random.randint(30, 255), # red random.randint(30, 255), # green random.randint(30, 255), # blue )) # Of course, in production code, you don't want to calculate your mandelbrot # set in CPython for performance reasons. This is just an example of what is # possible with the image provider support in PyOtherSide. def mandelbrot(x, y, steps): a = b = 0 for i in range(steps): a, b = a**2 - b**2 + x, 2*a*b + y if a**2 + b**2 >= 4: break return i def render_mandelbrot(image_id, requested_size): # when the url is: "image://python/xmin/xmax/ymin/ymax" # the image_id is: "xmin/xmax/ymin/ymax" parts = [float(x) for x in image_id.split('/')] x_range = parts[0:2] y_range = parts[2:4] # width and height will be -1 if not set in QML if requested_size == (-1, -1): requested_size = (200, 200) width, height = requested_size pixels = [] for y in range(height): yy = y_range[0] + (y_range[1] - y_range[0]) * y / height for x in range(width): xx = x_range[0] + (x_range[1] - x_range[0]) * x / width pixels.extend(reversed(COLORS[mandelbrot(xx, yy, len(COLORS))])) return bytearray(pixels), (width, height), pyotherside.format_argb32 pyotherside.set_image_provider(render_mandelbrot) pyotherside-1.6.2/examples/mandelbrot.qml000066400000000000000000000070511475412515400205520ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2011, 2013, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ import QtQuick 2.0 import io.thp.pyotherside 1.0 Image { id: image width: 300 height: 300 property real zoomFactor: 1.2 property real zoom: 2.5 property real xCenter: -0.5 property real yCenter: 0.0 property real xRange: zoom property real yRange: zoom * height / width property real mLeft: xCenter - xRange / 2 property real mRight: xCenter + xRange / 2 property real mTop: yCenter - yRange / 2 property real mBottom: yCenter + yRange / 2 onXRangeChanged: updateImageTimer.start() onYRangeChanged: updateImageTimer.start() onXCenterChanged: updateImageTimer.start() onYCenterChanged: updateImageTimer.start() sourceSize { width: image.width / 4 height: image.height / 4 } MouseArea { anchors.fill: parent property real lastPosX property real lastPosY onPressed: { lastPosX = mouse.x; lastPosY = mouse.y; } onPositionChanged: { var diffX = (mouse.x - lastPosX); var diffY = (mouse.y - lastPosY); image.xCenter -= diffX / image.width * image.xRange; image.yCenter -= diffY / image.height * image.yRange; lastPosX = mouse.x; lastPosY = mouse.y; } } Column { spacing: 10 anchors { right: parent.right top: parent.top margins: 10 } Rectangle { width: 30; height: 30 color: '#aaffffff' Text { anchors.centerIn: parent text: '-' } MouseArea { anchors.fill: parent onClicked: image.zoom *= image.zoomFactor } } Rectangle { width: 30; height: 30 color: '#aaffffff' Text { anchors.centerIn: parent text: '+' } MouseArea { anchors.fill: parent onClicked: image.zoom /= image.zoomFactor } } } Python { id: py Component.onCompleted: { // Add the directory of this .qml file to the search path py.addImportPath(Qt.resolvedUrl('.')); py.importModule('mandelbrot', function () { // Do the first image update once the module is loaded updateImageTimer.start(); }); } onError: console.log('Python error: ' + traceback) } Timer { id: updateImageTimer interval: 100 onTriggered: image.source = 'image://python/' + image.mLeft + '/' + image.mRight + '/' + image.mTop + '/' + image.mBottom } } pyotherside-1.6.2/examples/notes_example.py000066400000000000000000000021601475412515400211210ustar00rootroot00000000000000# A very simple notetaking application that uses Python to load # and save a string in a text file in the user's home directory. import os import threading import time import pyotherside class Notes: def __init__(self): self.filename = os.path.expanduser('~/pyotherside_notes.txt') self.thread = None self.new_text = self.get_contents() def save_now(self): print('Saving file right away at exit') self._update_file(now=True) def _update_file(self, now=False): if not now: time.sleep(3) print('Saving file now') open(self.filename, 'w').write(self.new_text) self.thread = None def get_contents(self): if os.path.exists(self.filename): return open(self.filename).read() else: return '' def set_contents(self, text): self.new_text = text if self.thread is None: print('Scheduling saving of file') self.thread = threading.Thread(target=self._update_file) self.thread.start() notes = Notes() pyotherside.atexit(notes.save_now) pyotherside-1.6.2/examples/notes_example.qml000066400000000000000000000016511475412515400212660ustar00rootroot00000000000000import QtQuick 2.0 import io.thp.pyotherside 1.0 Rectangle { width: 300 height: 200 TextInput { id: ti anchors.fill: parent onTextChanged: { py.call('notes_example.notes.set_contents', [text], function() { console.log('Changes sent to Python'); }); } Python { id: py Component.onCompleted: { // Add the directory of this .qml file to the search path addImportPath(Qt.resolvedUrl('.')); importModule('notes_example', function () { console.log('imported python module'); call('notes_example.notes.get_contents', [], function(result) { console.log('got contents from Python: ' + result); ti.text = result; }); }); } } } } pyotherside-1.6.2/examples/plotorama.py000066400000000000000000000025541475412515400202630ustar00rootroot00000000000000# # PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 # Copyright (c) 2011, 2013, Thomas Perl # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND # FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR # OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # PERFORMANCE OF THIS SOFTWARE. # import matplotlib.pyplot as plt import numpy as np import io import pyotherside def plot_provider(image_id, requested_size): plt.figure() x = np.arange(0.0, 5.0, 0.1) plt.plot(np.sin(x)) for dataset in image_id.split(';'): plt.plot([float(x) for x in dataset.split(',') if x]) plt.title("Dataset from QML") buf = io.BytesIO() plt.savefig(buf, format='png') plt.close() buf.seek(0) return bytearray(buf.read()), requested_size, pyotherside.format_data pyotherside.set_image_provider(plot_provider) pyotherside-1.6.2/examples/plotorama.qml000066400000000000000000000030351475412515400204170ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2011, 2013, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ import QtQuick 2.0 import io.thp.pyotherside 1.0 Image { id: image width: 300 height: 300 Python { Component.onCompleted: { // Add the directory of this .qml file to the search path addImportPath(Qt.resolvedUrl('.')); importModule('plotorama', function () { input.text = '4.3,5.5,3.3,2.2,1.4,1.3,1.2,0.7,0.2'; }); } onError: console.log('Python error: ' + traceback) } TextInput { id: input anchors { top: parent.top left: parent.left right: parent.right } onTextChanged: image.source = 'image://python/' + text } } pyotherside-1.6.2/examples/pyfbo.qml000066400000000000000000000140051475412515400175370ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). ** Contact: http://www.qt-project.org/legal ** ** This file is part of the examples of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:BSD$ ** You may use this file under the terms of the BSD license as follows: ** ** "Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are ** met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in ** the documentation and/or other materials provided with the ** distribution. ** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names ** of its contributors may be used to endorse or promote products derived ** from this software without specific prior written permission. ** ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ** ** $QT_END_LICENSE$ ** ****************************************************************************/ import QtQuick 2.0 import io.thp.pyotherside 1.5 Item { width: 400 height: 400 // The checkers background ShaderEffect { id: tileBackground anchors.fill: parent property real tileSize: 16 property color color1: Qt.rgba(0.9, 0.9, 0.9, 1); property color color2: Qt.rgba(0.85, 0.85, 0.85, 1); property size pixelSize: Qt.size(width / tileSize, height / tileSize); fragmentShader: " uniform lowp vec4 color1; uniform lowp vec4 color2; uniform highp vec2 pixelSize; varying highp vec2 qt_TexCoord0; void main() { highp vec2 tc = sign(sin(3.14152 * qt_TexCoord0 * pixelSize)); if (tc.x != tc.y) gl_FragColor = color1; else gl_FragColor = color2; } " } PyFBO { id: fbo anchors.fill: parent anchors.margins: 10 property var t: 0 SequentialAnimation on t { NumberAnimation { to: 1; duration: 2500; easing.type: Easing.InQuad } NumberAnimation { to: 0; duration: 2500; easing.type: Easing.OutQuad } loops: Animation.Infinite running: true } // The transform is just to show something interesting.. transform: [ Rotation { id: rotation; axis.x: 0; axis.z: 0; axis.y: 1; angle: 0; origin.x: fbo.width / 2; origin.y: fbo.height / 2; }, Translate { id: txOut; x: -fbo.width / 2; y: -fbo.height / 2 }, Scale { id: scale; }, Translate { id: txIn; x: fbo.width / 2; y: fbo.height / 2 } ] onTChanged: { if (renderer) { py.call(py.getattr(renderer, 'set_t'), [t], update); } } } // Just to show something interesting SequentialAnimation { PauseAnimation { duration: 5000 } ParallelAnimation { NumberAnimation { target: scale; property: "xScale"; to: 0.6; duration: 1000; easing.type: Easing.InOutBack } NumberAnimation { target: scale; property: "yScale"; to: 0.6; duration: 1000; easing.type: Easing.InOutBack } } NumberAnimation { target: rotation; property: "angle"; to: 80; duration: 1000; easing.type: Easing.InOutCubic } NumberAnimation { target: rotation; property: "angle"; to: -80; duration: 1000; easing.type: Easing.InOutCubic } NumberAnimation { target: rotation; property: "angle"; to: 0; duration: 1000; easing.type: Easing.InOutCubic } NumberAnimation { target: fbo; property: "opacity"; to: 0.5; duration: 1000; easing.type: Easing.InOutCubic } PauseAnimation { duration: 1000 } NumberAnimation { target: fbo; property: "opacity"; to: 0.8; duration: 1000; easing.type: Easing.InOutCubic } ParallelAnimation { NumberAnimation { target: scale; property: "xScale"; to: 1; duration: 1000; easing.type: Easing.InOutBack } NumberAnimation { target: scale; property: "yScale"; to: 1; duration: 1000; easing.type: Easing.InOutBack } } running: true loops: Animation.Infinite } Rectangle { id: labelFrame anchors.margins: -10 radius: 5 color: "white" border.color: "black" opacity: 0.8 anchors.fill: label } Text { id: label anchors.bottom: fbo.bottom anchors.left: fbo.left anchors.right: fbo.right anchors.margins: 20 wrapMode: Text.WordWrap text: "The squircle is an FBO, rendered by the application on the scene graph rendering thread. The FBO is managed and displayed using a PyFBO item." } Python { id: py Component.onCompleted: { addImportPath(Qt.resolvedUrl('.')); importModule('renderer', function () { call('renderer.Renderer', [], function (renderer) { fbo.renderer = renderer; }); }); } onError: console.log(traceback); } } pyotherside-1.6.2/examples/pyglarea.qml000066400000000000000000000030041475412515400202210ustar00rootroot00000000000000import QtQuick 2.0 import io.thp.pyotherside 1.5 Item { width: 320 height: 480 PyGLArea { id: glArea anchors.fill: parent property var t: 0 SequentialAnimation on t { NumberAnimation { to: 1; duration: 2500; easing.type: Easing.InQuad } NumberAnimation { to: 0; duration: 2500; easing.type: Easing.OutQuad } loops: Animation.Infinite running: true } onTChanged: { if (renderer) { py.call(py.getattr(renderer, 'set_t'), [t], update); } } } Rectangle { color: Qt.rgba(1, 1, 1, 0.7) radius: 10 border.width: 1 border.color: "white" anchors.fill: label anchors.margins: -10 } Text { id: label color: "black" wrapMode: Text.WordWrap text: "The background here is a squircle rendered with raw OpenGL using a PyGLArea. This text label and its border is rendered using QML" anchors.right: parent.right anchors.left: parent.left anchors.bottom: parent.bottom anchors.margins: 20 } Python { id: py Component.onCompleted: { addImportPath(Qt.resolvedUrl('.')); importModule('renderer', function () { call('renderer.Renderer', [], function (renderer) { glArea.renderer = renderer; }); }); } onError: console.log(traceback); } } pyotherside-1.6.2/examples/pyotherside.png000066400000000000000000000046251475412515400207610ustar00rootroot00000000000000PNG  IHDR?XB/=sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< IDATx{p\u?ޛ&i҇M XR(k(”ͮKXpQ(è((ph A^fȈ c%ݹyqZk (m4fI{춓M3;"xq88I~$+5yF,YR2l)TV#n=}}D"q B@~F{"xP^& fZ=Eo&hX- ( L^D$ߊY d2>'ȉ|$9  ئMm@>׈:S a !` rRFkP\s-3 f8PxBζl1 YNqh6fEypSSiԷ‹Fg(zo ⶉf`:0 B,흓H$~ܞC,i#+b-V/)DFE#(0϶Yx"#h4Z-@I0k0;cXԵ@/ x$_GÓ|]]BAulO[ZZ(2rq$6WI RYYN`h4Z6Uu^ }e4ڵkf2;J==޿ymM0*/HG9067:NX,Gy<+%H$\%/"B U=Lpq'/(E"]?A.IGQMI?W_ۅЋ0M(8^-sPM.]a<NKvBմC7?!`3ORFu +.ꭡdpZPP`Ӓ#\2.%J Evv $erФ@iEZ̟v~lKeu>~miH$1HͽgbG;6NaP˱f57N7=gXnD poE2Rx2Syp\zoI!52='s gO_l[d[%厹qbmgɴ"g!i\V촡A[}o$\!0kbs'9w5=yqWߛ7LX,?PFuuu 16fP"\1sS̛-[z-pဨ@W6p_wWWWƍ@(kI"Z? R&im /w:}0QS4t3И~nYyW^衰3o+^Cc=t{!'q]_)'oϼkhlex m 1 0x m?E$z/ ]1QbOQ-OL @ :7,1*hZ{GVD|M7ŊIENDB`pyotherside-1.6.2/examples/python_logo.svg000066400000000000000000000125751475412515400210010ustar00rootroot00000000000000 image/svg+xml pyotherside-1.6.2/examples/qobject_reference.py000066400000000000000000000034551475412515400217330ustar00rootroot00000000000000import threading import time import pyotherside # If you try to instantiate a QObject, it's unbound unbound = pyotherside.QObject() print(unbound) try: unbound.a = 1 except Exception as e: print('Got exception:', e) def do_something(bar): while True: print('got: ', bar.dynamicFunction(1, 2, 3)) time.sleep(1) def foo(bar, py): # Printing the objects will give some info on the # QObject class and memory address print('got:', bar, py) # Ok, this is pretty wicked - we can now call into # the PyOtherSide QML element from within Python # (not that it's a good idea to do this, mind you..) print(py.evaluate) print(py.evaluate('3*3')) try: bar.i_am_pretty_sure_this_attr_does_not_exist = 147 except Exception as e: print('Got exception (as expected):', e) try: bar.x = 'i dont think i can set this to a string' except Exception as e: print('Got exception (as expected):', e) # This doesn't work yet, because we can't convert a bound # member function to a Qt/QML type yet (fallback to None) try: bar.dynamicFunction(bar.dynamicFunction, 2, 3) except Exception as e: print('Got exception (as expected):', e) # Property access works just like expected print(bar.x, bar.color, bar.scale) bar.x *= 3 # Printing a member function gives a bound method print(bar.dynamicFunction) # Calling a member function is just as easy result = bar.dynamicFunction(1, 2, 3) print('result:', result) try: bar.dynamicFunction(1, 2, 3, unexpected=123) except Exception as e: print('Got exception (as expected):', e) threading.Thread(target=do_something, args=[bar]).start() # Returning QObject references from Python also works return bar pyotherside-1.6.2/examples/qobject_reference.qml000066400000000000000000000015101475412515400220620ustar00rootroot00000000000000import QtQuick 2.0 import io.thp.pyotherside 1.4 Rectangle { id: root width: 400 height: 400 Rectangle { id: foo x: 123 y: 123 width: 20 height: 20 color: 'blue' function dynamicFunction(a, b, c) { console.log('In QML, dynamicFunction got: ' + a + ', ' + b + ', ' + c); rotation += 4; return 'hello'; } } Python { id: py Component.onCompleted: { addImportPath(Qt.resolvedUrl('.')); importModule('qobject_reference', function () { call('qobject_reference.foo', [foo, py], function (result) { console.log('In QML, got result: ' + result); result.color = 'green'; }); }); } } } pyotherside-1.6.2/examples/qrc/000077500000000000000000000000001475412515400164725ustar00rootroot00000000000000pyotherside-1.6.2/examples/qrc/data/000077500000000000000000000000001475412515400174035ustar00rootroot00000000000000pyotherside-1.6.2/examples/qrc/data/below/000077500000000000000000000000001475412515400205135ustar00rootroot00000000000000pyotherside-1.6.2/examples/qrc/data/below/qrc_example_below.py000066400000000000000000000001741475412515400245570ustar00rootroot00000000000000import sys import pyotherside print('Hello from below!') print('sys.path =', sys.path) print('pyotherside =', pyotherside) pyotherside-1.6.2/examples/qrc/data/qrc_example.py000066400000000000000000000025131475412515400222560ustar00rootroot00000000000000import pyotherside import os.path import sys print('Hello from module!') print(sys.path) print('file exists?', pyotherside.qrc_is_file('qrc_example.qml')) print('file exists?', pyotherside.qrc_is_file('qrc_example.qml.nonexistent')) print('dir exists?', pyotherside.qrc_is_dir('/')) print('dir exists?', pyotherside.qrc_is_dir('/nonexistent')) print('='*30) def walk(root): for entry in pyotherside.qrc_list_dir(root): name = os.path.join(root, entry) if pyotherside.qrc_is_dir(name): walk(name) else: print(name, '=', len(pyotherside.qrc_get_file_contents(name)), 'bytes') walk('/') print('='*30) print(pyotherside.qrc_get_file_contents('qrc_example.py').decode('utf-8')) print('='*30) try: print('dir exists with number', pyotherside.qrc_is_dir(123)) except Exception as e: print('got exception (as expected):', e) try: print('file exists with none', pyotherside.qrc_is_file(None)) except Exception as e: print('got exception (as expected):', e) try: print('dir entries with invalid', pyotherside.qrc_list_dir('/nonexistent')) except Exception as e: print('got exception (as expected):', e) try: print('file contents with invalid', pyotherside.qrc_get_file_contents('/qrc_example.qml.nonexistent')) except Exception as e: print('got exception (as expected):', e) pyotherside-1.6.2/examples/qrc/data/qrc_example.qml000066400000000000000000000010501475412515400224120ustar00rootroot00000000000000import QtQuick 2.0 import io.thp.pyotherside 1.3 Rectangle { width: 100 height: 100 Python { Component.onCompleted: { addImportPath(Qt.resolvedUrl('.')); importModule('qrc_example', function (success) { console.log('module imported: ' + success); addImportPath(Qt.resolvedUrl('below')); importModule('qrc_example_below', function (success) { console.log('also imported: ' + success); }); }); } } } pyotherside-1.6.2/examples/qrc/data/qrc_example.qrc000066400000000000000000000002641475412515400224140ustar00rootroot00000000000000 qrc_example.qml qrc_example.py below/qrc_example_below.py pyotherside-1.6.2/examples/qrc/qrc_example.cpp000066400000000000000000000003731475412515400215010ustar00rootroot00000000000000#include #include #include int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QQuickView view; view.setSource(QUrl("qrc:/qrc_example.qml")); view.show(); return app.exec(); } pyotherside-1.6.2/examples/qrc/qrc_example.pro000066400000000000000000000002251475412515400215130ustar00rootroot00000000000000TARGET = qrc_example TEMPLATE = app DEPENDPATH += . INCLUDEPATH += . QT += qml quick SOURCES += qrc_example.cpp RESOURCES += data/qrc_example.qrc pyotherside-1.6.2/examples/renderer.py000066400000000000000000000042221475412515400200650ustar00rootroot00000000000000import numpy from OpenGL.GL import * from OpenGL.GL.shaders import compileShader, compileProgram VERTEX_SHADER = """#version 120 attribute vec4 vertices; varying vec2 coords; void main() { gl_Position = vertices; coords = vertices.xy; } """ FRAGMENT_SHADER = """#version 120 uniform float t; varying vec2 coords; void main() { float i = 1. - (pow(abs(coords.x), 4.) + pow(abs(coords.y), 4.)); i = smoothstep(t - 0.8, t + 0.8, i); i = floor(i * 20.) / 20.; gl_FragColor = vec4(coords * .5 + .5, i, i); } """ class Renderer(object): def __init__(self): self.t = 0.0 self.values = numpy.array([ -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0 ], dtype=numpy.float32) def set_t(self, t): self.t = t def init(self): self.vertexbuffer = glGenBuffers(1) vertex_shader = compileShader(VERTEX_SHADER, GL_VERTEX_SHADER) fragment_shader = compileShader(FRAGMENT_SHADER, GL_FRAGMENT_SHADER) self.program = compileProgram(vertex_shader, fragment_shader) self.vertices_attr = glGetAttribLocation(self.program, b'vertices') self.t_attr = glGetUniformLocation(self.program, b't') def reshape(self, x, y, width, height): glViewport(x, y, width, height) def render(self): glUseProgram(self.program) try: glDisable(GL_DEPTH_TEST) glClearColor(0, 0, 0, 1) glClear(GL_COLOR_BUFFER_BIT) glEnable(GL_BLEND) glBlendFunc(GL_SRC_ALPHA, GL_ONE) glBindBuffer(GL_ARRAY_BUFFER, self.vertexbuffer) glEnableVertexAttribArray(self.vertices_attr) glBufferData(GL_ARRAY_BUFFER, self.values, GL_STATIC_DRAW) glVertexAttribPointer(self.vertices_attr, 2, GL_FLOAT, GL_FALSE, 0, None) glUniform1f(self.t_attr, self.t) glDrawArrays(GL_TRIANGLE_STRIP, 0, 4) finally: glDisableVertexAttribArray(0) glBindBuffer(GL_ARRAY_BUFFER, 0) glUseProgram(0) def cleanup(self): glDeleteProgram(self.program) glDeleteBuffers(1, [self.vertexbuffer]) pyotherside-1.6.2/pyotherside.pri000066400000000000000000000000521475412515400171370ustar00rootroot00000000000000PROJECTNAME = pyotherside VERSION = 1.6.2 pyotherside-1.6.2/pyotherside.pro000066400000000000000000000012471475412515400171540ustar00rootroot00000000000000TEMPLATE = subdirs SUBDIRS += src tests qtquicktests tests.depends = src include(pyotherside.pri) !win32 { # The make used in the Qt MSVC toolchain does not support $^, but # as we are not going to do source builds on Windows, just make # the source release (sdist) target depend on anything but win32. tar.target = $${PROJECTNAME}-$${VERSION}.tar tar.commands = git archive --format=tar --prefix=$${PROJECTNAME}-$${VERSION}/ --output=$@ $${VERSION} targz.target = $${PROJECTNAME}-$${VERSION}.tar.gz targz.depends = tar targz.commands = gzip $^ sdist.target = sdist sdist.depends = targz QMAKE_EXTRA_TARGETS += tar targz sdist } pyotherside-1.6.2/python-config-wrapper000077500000000000000000000013731475412515400202630ustar00rootroot00000000000000#!/bin/sh # python3-config wrapper script to support Python 3.8 # Works around https://bugs.python.org/issue36721 set -e usage() { echo "Usage: $0 [python3-config] (--libs|--includes)" exit 1 } if [ $# -ne 2 ]; then usage fi PYTHON_CONFIG="$1" shift WHICH_FLAG="$1" shift case "$WHICH_FLAG" in --libs) # Python 3.8 needs --embed, but previous versions do not have it: # https://github.com/python/cpython/pull/13500 if "$PYTHON_CONFIG" --ldflags --libs --embed >/dev/null 2>&1; then "$PYTHON_CONFIG" --ldflags --libs --embed else "$PYTHON_CONFIG" --ldflags --libs fi ;; --includes) "$PYTHON_CONFIG" --includes ;; *) usage ;; esac pyotherside-1.6.2/python.pri000066400000000000000000000004071475412515400161250ustar00rootroot00000000000000isEmpty(PYTHON_CONFIG) { PYTHON_CONFIG = python3-config } message(PYTHON_CONFIG = $$PYTHON_CONFIG) QMAKE_LIBS += $$system($$PWD/python-config-wrapper $$PYTHON_CONFIG --libs) QMAKE_CXXFLAGS += $$system($$PWD/python-config-wrapper $$PYTHON_CONFIG --includes) pyotherside-1.6.2/qtquicktests/000077500000000000000000000000001475412515400166335ustar00rootroot00000000000000pyotherside-1.6.2/qtquicktests/PythonRectangle.qml000066400000000000000000000005301475412515400224520ustar00rootroot00000000000000import QtQuick 2.3 import io.thp.pyotherside 1.5 Rectangle { property bool ready: false; property alias py: py; Python { id: py Component.onCompleted: { addImportPath(Qt.resolvedUrl('.')); importModule('test_functions', function() { ready = true; }); } } } pyotherside-1.6.2/qtquicktests/qtquicktests.cpp000066400000000000000000000001011475412515400220730ustar00rootroot00000000000000#include QUICK_TEST_MAIN(qtquicktests) pyotherside-1.6.2/qtquicktests/qtquicktests.pro000066400000000000000000000001371475412515400221220ustar00rootroot00000000000000TEMPLATE = app TARGET = qtquicktests CONFIG += warn_on qmltestcase SOURCES += qtquicktests.cpp pyotherside-1.6.2/qtquicktests/run000077500000000000000000000000571475412515400173670ustar00rootroot00000000000000#!/bin/sh exec ./qtquicktests -plugins ../src pyotherside-1.6.2/qtquicktests/test_functions.py000066400000000000000000000002231475412515400222510ustar00rootroot00000000000000def function_that_takes_one_parameter(parameter): '''For tst_model_add_one:call_sync_with_parameters''' assert parameter == 1 return 1 pyotherside-1.6.2/qtquicktests/tst_bytes.py000066400000000000000000000004771475412515400212350ustar00rootroot00000000000000import struct def get_bytes(): return struct.pack(' * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #ifndef PYOTHERSIDE_CONVERTER_H #define PYOTHERSIDE_CONVERTER_H #include "pyobject_ref.h" #include "qobject_ref.h" struct ConverterDate { ConverterDate(int y, int m, int d) : y(y), m(m), d(d) { } int y, m, d; }; struct ConverterTime { ConverterTime(int h, int m, int s, int ms) : h(h), m(m), s(s), ms(ms) { } int h, m, s, ms; }; struct ConverterDateTime : public ConverterDate { ConverterDateTime(int y, int m, int d, int h, int mm, int s, int ms) : ConverterDate(y, m, d) , time(h, mm, s, ms) { } ConverterTime time; }; template class ListBuilder { public: ListBuilder() {} virtual ~ListBuilder() {} virtual void append(V) = 0; virtual V value() = 0; }; template class DictBuilder { public: DictBuilder() {} virtual ~DictBuilder() {} virtual void set(V, V) = 0; virtual V value() = 0; }; template class ListIterator { public: ListIterator() {} virtual ~ListIterator() {} virtual bool next(V*) = 0; }; template class DictIterator { public: DictIterator() {} virtual ~DictIterator() {} virtual bool next(V*, V*) = 0; }; template class Converter { public: Converter() {} virtual ~Converter() {} enum Type { NONE = 0, INTEGER, FLOATING, BOOLEAN, STRING, BYTES, LIST, DICT, DATE, TIME, DATETIME, PYOBJECT, QOBJECT, }; virtual enum Type type(const V&) = 0; virtual long long integer(V&) = 0; virtual double floating(V&) = 0; virtual bool boolean(V&) = 0; virtual const char *string(V&) = 0; virtual QByteArray bytes(V&) = 0; virtual ListIterator *list(V&) = 0; virtual DictIterator *dict(V&) = 0; virtual ConverterDate date(V&) = 0; virtual ConverterTime time(V&) = 0; virtual ConverterDateTime dateTime(V&) = 0; virtual PyObjectRef pyObject(V&) = 0; virtual QObjectRef qObject(V&) = 0; virtual V fromInteger(long long v) = 0; virtual V fromFloating(double v) = 0; virtual V fromBoolean(bool v) = 0; virtual V fromString(const char *v) = 0; virtual V fromBytes(const QByteArray &v) = 0; virtual V fromDate(ConverterDate date) = 0; virtual V fromTime(ConverterTime time) = 0; virtual V fromDateTime(ConverterDateTime dateTime) = 0; virtual V fromPyObject(const PyObjectRef &pyobj) = 0; virtual V fromQObject(const QObjectRef &qobj) = 0; virtual ListBuilder *newList() = 0; virtual DictBuilder *newDict() = 0; virtual V none() = 0; }; template T convert(F from) { FC fconv; TC tconv; switch (fconv.type(from)) { case FC::NONE: return tconv.none(); case FC::INTEGER: return tconv.fromInteger(fconv.integer(from)); case FC::FLOATING: return tconv.fromFloating(fconv.floating(from)); case FC::BOOLEAN: return tconv.fromBoolean(fconv.boolean(from)); case FC::STRING: return tconv.fromString(fconv.string(from)); case FC::BYTES: return tconv.fromBytes(fconv.bytes(from)); case FC::LIST: { ListBuilder *listBuilder = tconv.newList(); F listValue; ListIterator *listIterator = fconv.list(from); while (listIterator->next(&listValue)) { listBuilder->append(convert(listValue)); } delete listIterator; T listResult = listBuilder->value(); delete listBuilder; return listResult; } case FC::DICT: { DictBuilder *dictBuilder = tconv.newDict(); DictIterator *dictIterator = fconv.dict(from); FC keyConvFrom; TC keyConvTo; F dictKey; F dictValue; while (dictIterator->next(&dictKey, &dictValue)) { dictBuilder->set(keyConvTo.fromString(keyConvFrom.string(dictKey)), convert(dictValue)); } delete dictIterator; T dictResult = dictBuilder->value(); delete dictBuilder; return dictResult; } case FC::DATE: return tconv.fromDate(fconv.date(from)); case FC::TIME: return tconv.fromTime(fconv.time(from)); case FC::DATETIME: return tconv.fromDateTime(fconv.dateTime(from)); case FC::PYOBJECT: return tconv.fromPyObject(fconv.pyObject(from)); case FC::QOBJECT: return tconv.fromQObject(fconv.qObject(from)); } return tconv.none(); } #endif /* PYOTHERSIDE_CONVERTER_H */ pyotherside-1.6.2/src/ensure_gil_state.h000066400000000000000000000022741475412515400203700ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2014, Felix Krull * Copyright (c) 2014, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #include "python_wrap.h" class EnsureGILState { public: EnsureGILState() : gil_state(PyGILState_Ensure()) { } ~EnsureGILState() { PyGILState_Release(gil_state); } private: PyGILState_STATE gil_state; }; #define ENSURE_GIL_STATE EnsureGILState _ensure; Q_UNUSED(_ensure) pyotherside-1.6.2/src/global_libpython_loader.cpp000066400000000000000000000041321475412515400222400ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2013, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #include "global_libpython_loader.h" namespace GlobalLibPythonLoader { #if defined(__linux__) && !defined(ANDROID) #define _GNU_SOURCE #include #include #include #include static int load_python_globally_callback(struct dl_phdr_info *info, size_t size, void *data) { int major, minor; const char *basename = strrchr(info->dlpi_name, '/'); int *success = (int *)data; if (basename != NULL) { if (sscanf(basename, "/libpython%d.%d.so", &major, &minor) != 2) { if (sscanf(basename, "/libpython%d.%dm.so", &major, &minor) != 2) { return 0; } } void *pylib = dlopen(info->dlpi_name, RTLD_GLOBAL | RTLD_NOW); if (pylib != NULL) { *success = 1; } else { fprintf(stderr, "Could not load python library '%s': %s\n", info->dlpi_name, dlerror()); } } return 0; } bool loadPythonGlobally() { int success = 0; dl_iterate_phdr(load_python_globally_callback, &success); return success; } #else /* __linux__ */ bool loadPythonGlobally() { /* On non-Linux systems, no need to load globally */ return true; } #endif /* __linux__ */ }; /* namespace GlobalLibPythonLoader */ pyotherside-1.6.2/src/global_libpython_loader.h000066400000000000000000000020531475412515400217050ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2013, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #ifndef PYOTHERSIDE_GLOBAL_LIBPYTHON_LOADER_H #define PYOTHERSIDE_GLOBAL_LIBPYTHON_LOADER_H namespace GlobalLibPythonLoader { bool loadPythonGlobally(); }; #endif /* PYOTHERSIDE_GLOBAL_LIBPYTHON_LOADER_H */ pyotherside-1.6.2/src/pyfbo.cpp000066400000000000000000000053501475412515400165040ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2014, Dennis Tomas * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #include "pyfbo.h" #include #include class PyFboRenderer : public QQuickFramebufferObject::Renderer { public: PyFboRenderer() : m_renderer(0) , m_size(0, 0) { } ~PyFboRenderer() { if (m_renderer) { delete m_renderer; m_renderer = 0; } } void render() { if (m_renderer) m_renderer->render(); } void synchronize(QQuickFramebufferObject *item) { PyFbo *pyFbo = static_cast(item); if (pyFbo->renderer() != m_rendererRef) { // The renderer has changed. if (m_renderer) { m_renderer->cleanup(); delete m_renderer; m_renderer = 0; } m_rendererRef = pyFbo->renderer(); if (!m_rendererRef.isNull()) { m_renderer = new PyGLRenderer(m_rendererRef); m_renderer->init(); m_sizeChanged = true; } } if (m_renderer && m_sizeChanged) { // The size has changed. m_renderer->reshape(QRect(QPoint(0, 0), m_size)); m_sizeChanged = false; update(); } } QOpenGLFramebufferObject *createFramebufferObject(const QSize &size) { m_size = size; m_sizeChanged = true; QOpenGLFramebufferObjectFormat format; // TODO: get the FBO format from the PyGLRenderer. return new QOpenGLFramebufferObject(size, format); } private: QVariant m_rendererRef; PyGLRenderer *m_renderer; QSize m_size; bool m_sizeChanged; }; void PyFbo::setRenderer(QVariant rendererRef) { if (rendererRef == m_rendererRef) return; m_rendererRef = rendererRef; update(); } QQuickFramebufferObject::Renderer *PyFbo::createRenderer() const { return new PyFboRenderer(); } pyotherside-1.6.2/src/pyfbo.h000066400000000000000000000024321475412515400161470ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2014, Dennis Tomas * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #ifndef PYFBO_H #define PYFBO_H #include "pyglrenderer.h" #include #include class PyFbo : public QQuickFramebufferObject { Q_OBJECT Q_PROPERTY(QVariant renderer READ renderer WRITE setRenderer) public: Renderer *createRenderer() const; QVariant renderer() const { return m_rendererRef; }; void setRenderer(QVariant rendererRef); private: QVariant m_rendererRef; }; #endif pyotherside-1.6.2/src/pyglarea.cpp000066400000000000000000000107771475412515400172020ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2014, Dennis Tomas * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #include "qpython_priv.h" #include "pyglarea.h" #include #include #include #include #include #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) # include #endif PyGLArea::PyGLArea() : m_before(true) , m_renderer(0) , m_rendererChanged(false) , m_beforeChanged(true) { connect(this, SIGNAL(windowChanged(QQuickWindow*)), this, SLOT(handleWindowChanged(QQuickWindow*))); } PyGLArea::~PyGLArea() { if (m_renderer) { delete m_renderer; m_renderer = 0; } } void PyGLArea::setRenderer(QVariant renderer) { if (renderer == m_pyRenderer) return; m_pyRenderer = renderer; // Defer creating the PyGLRenderer until sync() is called, // when we have an OpenGL context. m_rendererChanged = true; update(); } void PyGLArea::setBefore(bool before) { if (before == m_before) return; m_before = before; m_beforeChanged = true; update(); } void PyGLArea::handleWindowChanged(QQuickWindow *win) { if (win) { connect(win, SIGNAL(beforeSynchronizing()), this, SLOT(sync()), Qt::DirectConnection); connect(win, SIGNAL(sceneGraphInvalidated()), this, SLOT(cleanup()), Qt::DirectConnection); } } void PyGLArea::update() { if (window()) window()->update(); } void PyGLArea::sync() { if (m_beforeChanged) { disconnect(window(), SIGNAL(beforeRendering()), this, SLOT(render())); disconnect(window(), SIGNAL(afterRendering()), this, SLOT(render())); if (m_before) { #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // If we allow QML to do the clearing, they would clear what we paint // and nothing would show. window()->setClearBeforeRendering(false); connect(window(), SIGNAL(beforeRendering()), this, SLOT(render()), Qt::DirectConnection); #else // For Qt 6, we don't support rendering as underlay; see also: // https://doc.qt.io/qt-6/qquickwindow.html#integration-with-accelerated-3d-graphics-apis // See also (search for "setClearBeforeRendering" on that page): // https://doc.qt.io/qt-6/quick-changes-qt6.html qWarning() << "PyGLArea doesn't work properly in Qt 6 yet, please use PyFBO instead."; connect(window(), SIGNAL(beforeRendering()), this, SLOT(render()), Qt::DirectConnection); #endif } else { #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) window()->setClearBeforeRendering(true); #endif connect(window(), SIGNAL(afterRendering()), this, SLOT(render()), Qt::DirectConnection); } m_beforeChanged = false; } if (m_rendererChanged) { if (m_renderer) { m_renderer->cleanup(); delete m_renderer; m_renderer = 0; } if (!m_pyRenderer.isNull()) { m_renderer = new PyGLRenderer(m_pyRenderer); m_renderer->init(); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) window()->resetOpenGLState(); #else QQuickOpenGLUtils::resetOpenGLState(); #endif } m_rendererChanged = false; } } void PyGLArea::render() { if (!m_renderer) return; QPointF pos = mapToScene(QPointF(.0, .0)); m_renderer->reshape( QRect( (long)pos.x(), (long)(window()->height() - this->height() - pos.y()), this->width(), this->height() ) ); m_renderer->render(); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) window()->resetOpenGLState(); #else QQuickOpenGLUtils::resetOpenGLState(); #endif } void PyGLArea::cleanup() { if (m_renderer) m_renderer->cleanup(); } pyotherside-1.6.2/src/pyglarea.h000066400000000000000000000034011475412515400166310ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2014, Dennis Tomas * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #ifndef PYOTHERSIDE_PYGLAREA_H #define PYOTHERSIDE_PYGLAREA_H #include "python_wrap.h" #include #include #include #include "pyobject_ref.h" #include "pyglrenderer.h" class PyGLArea : public QQuickItem { Q_OBJECT Q_PROPERTY(QVariant renderer READ renderer WRITE setRenderer) Q_PROPERTY(bool before READ before WRITE setBefore) public: PyGLArea(); ~PyGLArea(); QVariant renderer() const { return m_pyRenderer; }; bool before() { return m_before; }; void setRenderer(QVariant renderer); void setBefore(bool before); public slots: void sync(); void update(); private slots: void handleWindowChanged(QQuickWindow *win); void render(); void cleanup(); private: QVariant m_pyRenderer; bool m_before; PyGLRenderer *m_renderer; bool m_rendererChanged; bool m_beforeChanged; }; #endif /* PYOTHERSIDE_PYGLAREA_H */ pyotherside-1.6.2/src/pyglrenderer.cpp000066400000000000000000000107501475412515400200670ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2014, Dennis Tomas * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #include "qpython_priv.h" #include "converter.h" #include "pyobject_ref.h" #include "pyglrenderer.h" #include "ensure_gil_state.h" #include #include PyGLRenderer::PyGLRenderer(QVariant pyRenderer) : m_pyRendererObject(0) , m_initMethod(0) , m_reshapeMethod(0) , m_renderMethod(0) , m_cleanupMethod(0) , m_initialized(false) { ENSURE_GIL_STATE; if (pyRenderer.userType() != qMetaTypeId()) { qWarning() << "Renderer must be of type PyObjectRef (got " << pyRenderer << ")."; return; } m_pyRendererObject = pyRenderer.value().newRef(); if (PyObject_HasAttrString(m_pyRendererObject, "render")) { m_renderMethod = PyObject_GetAttrString(m_pyRendererObject, "render"); if (!m_renderMethod) { qWarning() << "Error getting render method of renderer."; PyErr_PrintEx(0); } } else { qWarning() << "Renderer has no render method."; } if (PyObject_HasAttrString(m_pyRendererObject, "init")) { m_initMethod = PyObject_GetAttrString(m_pyRendererObject, "init"); if (!m_initMethod) { qWarning() << "Error getting init method of renderer."; PyErr_PrintEx(0); } } if (PyObject_HasAttrString(m_pyRendererObject, "reshape")) { m_reshapeMethod = PyObject_GetAttrString(m_pyRendererObject, "reshape"); if (!m_reshapeMethod) { qWarning() << "Error getting reshape method of renderer."; PyErr_PrintEx(0); } } if (PyObject_HasAttrString(m_pyRendererObject, "cleanup")) { m_cleanupMethod = PyObject_GetAttrString(m_pyRendererObject, "cleanup"); if (!m_cleanupMethod) { qWarning() << "Error getting cleanup method of renderer."; PyErr_PrintEx(0); } } } PyGLRenderer::~PyGLRenderer() { ENSURE_GIL_STATE; Py_CLEAR(m_initMethod); Py_CLEAR(m_reshapeMethod); Py_CLEAR(m_renderMethod); Py_CLEAR(m_cleanupMethod); Py_CLEAR(m_pyRendererObject); } void PyGLRenderer::init() { if (m_initialized || !m_initMethod) return; ENSURE_GIL_STATE; PyObject *args = PyTuple_New(0); PyObject *o = PyObject_Call(m_initMethod, args, NULL); if (o) Py_DECREF(o); else PyErr_PrintEx(0); Py_DECREF(args); m_initialized = true; } void PyGLRenderer::reshape(QRect geometry) { if (!m_initialized || !m_reshapeMethod) return; ENSURE_GIL_STATE; // Call the reshape callback with arguments x, y, width, height. // These are the boundaries in which the callback should render, // though it may choose to ignore them and simply paint anywhere over // (or below) the QML scene. // (x, y) is the bottom left corner. // (x + width, y + height) is the top right corner. PyObject *args = Py_BuildValue( "llll", geometry.x(), geometry.y(), geometry.width(), geometry.height() ); PyObject *o = PyObject_Call(m_reshapeMethod, args, NULL); Py_DECREF(args); if (o) Py_DECREF(o); else PyErr_PrintEx(0); } void PyGLRenderer::render() { if (!m_initialized || !m_renderMethod) return; ENSURE_GIL_STATE; PyObject *args = PyTuple_New(0); PyObject *o = PyObject_Call(m_renderMethod, args, NULL); Py_DECREF(args); if (o) Py_DECREF(o); else PyErr_PrintEx(0); } void PyGLRenderer::cleanup() { if (!m_initialized || !m_cleanupMethod) return; ENSURE_GIL_STATE; PyObject *args = PyTuple_New(0); PyObject *o = PyObject_Call(m_cleanupMethod, args, NULL); if (o) Py_DECREF(o); else PyErr_PrintEx(0); m_initialized = false; Py_DECREF(args); } pyotherside-1.6.2/src/pyglrenderer.h000066400000000000000000000026231475412515400175340ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2014, Dennis Tomas * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #ifndef PYOTHERSIDE_PYGLRENDERER_H #define PYOTHERSIDE_PYGLRENDERER_H #include "python_wrap.h" #include #include #include class PyGLRenderer { public: PyGLRenderer(QVariant pyRenderer); ~PyGLRenderer(); void init(); void reshape(QRect geometry); void render(); void cleanup(); private: PyObject *m_pyRendererObject; PyObject *m_initMethod; PyObject *m_reshapeMethod; PyObject *m_renderMethod; PyObject *m_cleanupMethod; bool m_initialized; }; #endif /* PYOTHERSIDE_PYGLRENDERER_H */ pyotherside-1.6.2/src/pyobject_converter.h000066400000000000000000000203311475412515400207340ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2011, 2013-2025, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #ifndef PYOTHERSIDE_PYOBJECT_CONVERTER_H #define PYOTHERSIDE_PYOBJECT_CONVERTER_H #include "converter.h" #include "pyqobject.h" #include "python_wrap.h" #include "datetime.h" #include class PyObjectListBuilder : public ListBuilder { public: PyObjectListBuilder() : list(PyList_New(0)) {} virtual ~PyObjectListBuilder() {} virtual void append(PyObject *o) { PyList_Append(list, o); Py_DECREF(o); } virtual PyObject * value() { return list; } private: PyObject *list; }; class PyObjectDictBuilder : public DictBuilder { public: PyObjectDictBuilder() : dict(PyDict_New()) {} virtual ~PyObjectDictBuilder() {} virtual void set(PyObject *key, PyObject *value) { PyDict_SetItem(dict, key, value); Py_DECREF(value); } virtual PyObject * value() { return dict; } private: PyObject *dict; }; class PyObjectListIterator : public ListIterator { public: PyObjectListIterator(PyObject *&v) : list(v) , iter(PyObject_GetIter(list)) , ref(NULL) { if (iter == NULL) { // TODO: Handle error } } virtual ~PyObjectListIterator() { Py_XDECREF(ref); Py_XDECREF(iter); if (PyErr_Occurred()) { // TODO: Handle error } } virtual bool next(PyObject **v) { if (!iter) { return false; } Py_XDECREF(ref); ref = PyIter_Next(iter); if (ref) { *v = ref; return true; } return false; } private: PyObject *list; PyObject *iter; PyObject *ref; }; class PyObjectDictIterator : public DictIterator { public: PyObjectDictIterator(PyObject *&v) : dict(v), pos(0) {} virtual ~PyObjectDictIterator() {} virtual bool next(PyObject **key, PyObject **value) { return PyDict_Next(dict, &pos, key, value); } private: PyObject *dict; Py_ssize_t pos; }; class PyObjectConverter : public Converter { public: PyObjectConverter() { if (!PyDateTimeAPI) { PyDateTime_IMPORT; } } virtual ~PyObjectConverter() { } virtual enum Type type(PyObject * const & o) { if (PyObject_TypeCheck(o, &pyotherside_QObjectType)) { return QOBJECT; } else if (PyObject_TypeCheck(o, &pyotherside_QObjectMethodType)) { qWarning("Cannot convert QObjectMethod yet - falling back to None"); // TODO: Implement passing QObjectMethod references around return NONE; } else if (PyBool_Check(o)) { return BOOLEAN; } else if (PyLong_Check(o)) { return INTEGER; } else if (PyFloat_Check(o)) { return FLOATING; } else if (PyUnicode_Check(o)) { return STRING; } else if (PyBytes_Check(o)) { return BYTES; } else if (PyDateTime_Check(o)) { // Need to check PyDateTime before PyDate, because // it is a subclass of PyDate. return DATETIME; } else if (PyDate_Check(o)) { return DATE; } else if (PyTime_Check(o)) { return TIME; } else if (PyList_Check(o) || PyTuple_Check(o) || PySet_Check(o) || PyIter_Check(o)) { return LIST; } else if (PyDict_Check(o)) { return DICT; } else if (o == Py_None) { return NONE; } else { return PYOBJECT; } } virtual long long integer(PyObject *&o) { return PyLong_AsLongLong(o); } virtual double floating(PyObject *&o) { return PyFloat_AsDouble(o); } virtual bool boolean(PyObject *&o) { return (o == Py_True); } virtual const char *string(PyObject *&o) { return PyUnicode_AsUTF8(o); } virtual QByteArray bytes(PyObject *&o) { return QByteArray(PyBytes_AsString(o), PyBytes_Size(o)); } virtual ListIterator *list(PyObject *&o) { return new PyObjectListIterator(o); } virtual DictIterator *dict(PyObject *&o) { return new PyObjectDictIterator(o);; } virtual ConverterDate date(PyObject *&o) { return ConverterDate(PyDateTime_GET_YEAR(o), PyDateTime_GET_MONTH(o), PyDateTime_GET_DAY(o)); } virtual ConverterTime time(PyObject *&o) { return ConverterTime(PyDateTime_TIME_GET_HOUR(o), PyDateTime_TIME_GET_MINUTE(o), PyDateTime_TIME_GET_SECOND(o), PyDateTime_TIME_GET_MICROSECOND(o) / 1000); } virtual ConverterDateTime dateTime(PyObject *&o) { return ConverterDateTime(PyDateTime_GET_YEAR(o), PyDateTime_GET_MONTH(o), PyDateTime_GET_DAY(o), PyDateTime_DATE_GET_HOUR(o), PyDateTime_DATE_GET_MINUTE(o), PyDateTime_DATE_GET_SECOND(o), PyDateTime_DATE_GET_MICROSECOND(o) / 1000); } virtual PyObjectRef pyObject(PyObject *&o) { return PyObjectRef(o); } virtual QObjectRef qObject(PyObject *&o) { if (PyObject_TypeCheck(o, &pyotherside_QObjectType)) { pyotherside_QObject *result = reinterpret_cast(o); return QObjectRef(*(result->m_qobject_ref)); } return QObjectRef(); } virtual PyObject * fromInteger(long long v) { return PyLong_FromLong((long)v); } virtual PyObject * fromFloating(double v) { return PyFloat_FromDouble(v); } virtual PyObject * fromBoolean(bool v) { return PyBool_FromLong((long)v); } virtual PyObject * fromString(const char *v) { return PyUnicode_FromString(v); } virtual PyObject * fromBytes(const QByteArray &v) { return PyBytes_FromStringAndSize(v.constData(), v.size()); } virtual PyObject * fromDate(ConverterDate v) { return PyDate_FromDate(v.y, v.m, v.d); } virtual PyObject * fromTime(ConverterTime v) { return PyTime_FromTime(v.h, v.m, v.s, 1000 * v.ms); } virtual PyObject * fromDateTime(ConverterDateTime v) { return PyDateTime_FromDateAndTime(v.y, v.m, v.d, v.time.h, v.time.m, v.time.s, v.time.ms * 1000); } virtual PyObject * fromPyObject(const PyObjectRef &pyobj) { return pyobj.newRef(); } virtual PyObject * fromQObject(const QObjectRef &qobj) { pyotherside_QObject *result = PyObject_New(pyotherside_QObject, &pyotherside_QObjectType); result->m_qobject_ref = new QObjectRef(qobj); return reinterpret_cast(result); } virtual ListBuilder *newList() { return new PyObjectListBuilder(); } virtual DictBuilder *newDict() { return new PyObjectDictBuilder(); } virtual PyObject * none() { Py_RETURN_NONE; } }; #endif /* PYOTHERSIDE_PYOBJECT_CONVERTER_H */ pyotherside-1.6.2/src/pyobject_ref.cpp000066400000000000000000000041611475412515400200370ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2014, Felix Krull * Copyright (c) 2014, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #include "pyobject_ref.h" #include "ensure_gil_state.h" PyObjectRef::PyObjectRef(PyObject *obj, bool consume) : pyobject(obj) { if (pyobject && !consume) { ENSURE_GIL_STATE; Py_INCREF(pyobject); } } PyObjectRef::PyObjectRef(const PyObjectRef &other) : pyobject(other.pyobject) { if (pyobject) { ENSURE_GIL_STATE; Py_INCREF(pyobject); } } PyObjectRef::~PyObjectRef() { if (pyobject) { ENSURE_GIL_STATE; Py_CLEAR(pyobject); } } PyObjectRef & PyObjectRef::operator=(const PyObjectRef &other) { if (this != &other) { if (pyobject || other.pyobject) { ENSURE_GIL_STATE; if (pyobject) { Py_CLEAR(pyobject); } if (other.pyobject) { pyobject = other.pyobject; Py_INCREF(pyobject); } } } return *this; } bool PyObjectRef::operator==(const PyObjectRef &other) { return pyobject == other.pyobject; } PyObject * PyObjectRef::newRef() const { if (pyobject) { ENSURE_GIL_STATE; Py_INCREF(pyobject); } return pyobject; } PyObject * PyObjectRef::borrow() const { // Borrowed reference return pyobject; } pyotherside-1.6.2/src/pyobject_ref.h000066400000000000000000000030241475412515400175010ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2014, Felix Krull * Copyright (c) 2014, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #ifndef PYOTHERSIDE_PYOBJECT_REF_H #define PYOTHERSIDE_PYOBJECT_REF_H #include "python_wrap.h" #include class PyObjectRef { public: explicit PyObjectRef(PyObject *obj=0, bool consume=false); PyObjectRef(const PyObjectRef &other); virtual ~PyObjectRef(); PyObjectRef &operator=(const PyObjectRef &other); bool operator==(const PyObjectRef &other); PyObject *newRef() const; PyObject *borrow() const; operator bool() const { return (pyobject != 0); } private: PyObject *pyobject; }; Q_DECLARE_METATYPE(PyObjectRef) #endif // PYOTHERSIDE_PYOBJECT_REF_H pyotherside-1.6.2/src/pyotherside.qmltypes000066400000000000000000000265641475412515400210320ustar00rootroot00000000000000import QtQuick.tooling 1.2 // This file describes the plugin-supplied types contained in the library. // It is used for QML tooling purposes only. // // This file was auto-generated by: // 'qmlplugindump -nonrelocatable io.thp.pyotherside 1.5' Module { dependencies: [] Component { name: "PyFbo" defaultProperty: "data" prototype: "QQuickFramebufferObject" exports: ["io.thp.pyotherside/PyFBO 1.5"] exportMetaObjectRevisions: [0] Property { name: "renderer"; type: "QVariant" } } Component { name: "PyGLArea" defaultProperty: "data" prototype: "QQuickItem" exports: ["io.thp.pyotherside/PyGLArea 1.5"] exportMetaObjectRevisions: [0] Property { name: "renderer"; type: "QVariant" } Property { name: "before"; type: "bool" } Method { name: "sync" } Method { name: "update" } } Component { name: "QPython" prototype: "QObject" Signal { name: "received" Parameter { name: "data"; type: "QVariant" } } Signal { name: "error" Parameter { name: "traceback"; type: "string" } } Signal { name: "process" Parameter { name: "func"; type: "QVariant" } Parameter { name: "unboxed_args"; type: "QVariant" } Parameter { name: "callback"; type: "QJSValue"; isPointer: true } } Signal { name: "import" Parameter { name: "name"; type: "string" } Parameter { name: "callback"; type: "QJSValue"; isPointer: true } } Signal { name: "import_names" Parameter { name: "name"; type: "string" } Parameter { name: "args"; type: "QVariant" } Parameter { name: "callback"; type: "QJSValue"; isPointer: true } } Method { name: "addImportPath" Parameter { name: "path"; type: "string" } } Method { name: "setHandler" Parameter { name: "event"; type: "string" } Parameter { name: "callback"; type: "QJSValue" } } Method { name: "evaluate" type: "QVariant" Parameter { name: "expr"; type: "string" } } Method { name: "importNames" Parameter { name: "name"; type: "string" } Parameter { name: "args"; type: "QVariant" } Parameter { name: "callback"; type: "QJSValue" } } Method { name: "importNames_sync" type: "bool" Parameter { name: "name"; type: "string" } Parameter { name: "args"; type: "QVariant" } } Method { name: "importModule" Parameter { name: "name"; type: "string" } Parameter { name: "callback"; type: "QJSValue" } } Method { name: "importModule_sync" type: "bool" Parameter { name: "name"; type: "string" } } Method { name: "call" Parameter { name: "func"; type: "QVariant" } Parameter { name: "args"; type: "QVariant" } Parameter { name: "callback"; type: "QJSValue" } } Method { name: "call" Parameter { name: "func"; type: "QVariant" } Parameter { name: "args"; type: "QVariant" } } Method { name: "call" Parameter { name: "func"; type: "QVariant" } } Method { name: "call_sync" type: "QVariant" Parameter { name: "func"; type: "QVariant" } Parameter { name: "boxed_args"; type: "QVariant" } } Method { name: "call_sync" type: "QVariant" Parameter { name: "func"; type: "QVariant" } } Method { name: "getattr" type: "QVariant" Parameter { name: "obj"; type: "QVariant" } Parameter { name: "attr"; type: "string" } } Method { name: "pluginVersion"; type: "string" } Method { name: "pythonVersion"; type: "string" } } Component { name: "QPython10" prototype: "QPython" exports: ["io.thp.pyotherside/Python 1.0"] exportMetaObjectRevisions: [0] } Component { name: "QPython12" prototype: "QPython" exports: ["io.thp.pyotherside/Python 1.2"] exportMetaObjectRevisions: [0] } Component { name: "QPython13" prototype: "QPython" exports: ["io.thp.pyotherside/Python 1.3"] exportMetaObjectRevisions: [0] } Component { name: "QPython14" prototype: "QPython" exports: ["io.thp.pyotherside/Python 1.4"] exportMetaObjectRevisions: [0] } Component { name: "QPython15" prototype: "QPython" exports: ["io.thp.pyotherside/Python 1.5"] exportMetaObjectRevisions: [0] } Component { name: "QQuickFramebufferObject" defaultProperty: "data" prototype: "QQuickItem" Property { name: "textureFollowsItemSize"; type: "bool" } Property { name: "mirrorVertically"; type: "bool" } Signal { name: "textureFollowsItemSizeChanged" Parameter { type: "bool" } } Signal { name: "mirrorVerticallyChanged" Parameter { type: "bool" } } } Component { name: "QQuickItem" defaultProperty: "data" prototype: "QObject" Enum { name: "TransformOrigin" values: { "TopLeft": 0, "Top": 1, "TopRight": 2, "Left": 3, "Center": 4, "Right": 5, "BottomLeft": 6, "Bottom": 7, "BottomRight": 8 } } Property { name: "parent"; type: "QQuickItem"; isPointer: true } Property { name: "data"; type: "QObject"; isList: true; isReadonly: true } Property { name: "resources"; type: "QObject"; isList: true; isReadonly: true } Property { name: "children"; type: "QQuickItem"; isList: true; isReadonly: true } Property { name: "x"; type: "float" } Property { name: "y"; type: "float" } Property { name: "z"; type: "float" } Property { name: "width"; type: "float" } Property { name: "height"; type: "float" } Property { name: "opacity"; type: "float" } Property { name: "enabled"; type: "bool" } Property { name: "visible"; type: "bool" } Property { name: "visibleChildren"; type: "QQuickItem"; isList: true; isReadonly: true } Property { name: "states"; type: "QQuickState"; isList: true; isReadonly: true } Property { name: "transitions"; type: "QQuickTransition"; isList: true; isReadonly: true } Property { name: "state"; type: "string" } Property { name: "childrenRect"; type: "QRectF"; isReadonly: true } Property { name: "anchors"; type: "QQuickAnchors"; isReadonly: true; isPointer: true } Property { name: "left"; type: "QQuickAnchorLine"; isReadonly: true } Property { name: "right"; type: "QQuickAnchorLine"; isReadonly: true } Property { name: "horizontalCenter"; type: "QQuickAnchorLine"; isReadonly: true } Property { name: "top"; type: "QQuickAnchorLine"; isReadonly: true } Property { name: "bottom"; type: "QQuickAnchorLine"; isReadonly: true } Property { name: "verticalCenter"; type: "QQuickAnchorLine"; isReadonly: true } Property { name: "baseline"; type: "QQuickAnchorLine"; isReadonly: true } Property { name: "baselineOffset"; type: "float" } Property { name: "clip"; type: "bool" } Property { name: "focus"; type: "bool" } Property { name: "activeFocus"; type: "bool"; isReadonly: true } Property { name: "activeFocusOnTab"; revision: 1; type: "bool" } Property { name: "rotation"; type: "float" } Property { name: "scale"; type: "float" } Property { name: "transformOrigin"; type: "TransformOrigin" } Property { name: "transformOriginPoint"; type: "QPointF"; isReadonly: true } Property { name: "transform"; type: "QQuickTransform"; isList: true; isReadonly: true } Property { name: "smooth"; type: "bool" } Property { name: "antialiasing"; type: "bool" } Property { name: "implicitWidth"; type: "float" } Property { name: "implicitHeight"; type: "float" } Property { name: "layer"; type: "QQuickItemLayer"; isReadonly: true; isPointer: true } Signal { name: "childrenRectChanged" Parameter { type: "QRectF" } } Signal { name: "baselineOffsetChanged" Parameter { type: "float" } } Signal { name: "stateChanged" Parameter { type: "string" } } Signal { name: "focusChanged" Parameter { type: "bool" } } Signal { name: "activeFocusChanged" Parameter { type: "bool" } } Signal { name: "activeFocusOnTabChanged" revision: 1 Parameter { type: "bool" } } Signal { name: "parentChanged" Parameter { type: "QQuickItem"; isPointer: true } } Signal { name: "transformOriginChanged" Parameter { type: "TransformOrigin" } } Signal { name: "smoothChanged" Parameter { type: "bool" } } Signal { name: "antialiasingChanged" Parameter { type: "bool" } } Signal { name: "clipChanged" Parameter { type: "bool" } } Signal { name: "windowChanged" revision: 1 Parameter { name: "window"; type: "QQuickWindow"; isPointer: true } } Method { name: "update" } Method { name: "grabToImage" revision: 2 type: "bool" Parameter { name: "callback"; type: "QJSValue" } Parameter { name: "targetSize"; type: "QSize" } } Method { name: "grabToImage" revision: 2 type: "bool" Parameter { name: "callback"; type: "QJSValue" } } Method { name: "contains" type: "bool" Parameter { name: "point"; type: "QPointF" } } Method { name: "mapFromItem" Parameter { type: "QQmlV4Function"; isPointer: true } } Method { name: "mapToItem" Parameter { type: "QQmlV4Function"; isPointer: true } } Method { name: "forceActiveFocus" } Method { name: "forceActiveFocus" Parameter { name: "reason"; type: "Qt::FocusReason" } } Method { name: "nextItemInFocusChain" revision: 1 type: "QQuickItem*" Parameter { name: "forward"; type: "bool" } } Method { name: "nextItemInFocusChain"; revision: 1; type: "QQuickItem*" } Method { name: "childAt" type: "QQuickItem*" Parameter { name: "x"; type: "float" } Parameter { name: "y"; type: "float" } } } } pyotherside-1.6.2/src/pyotherside_plugin.cpp000066400000000000000000000051561475412515400213060ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2011, 2013-2025, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #include "qpython_priv.h" #include "qpython.h" #include "pyglarea.h" #include "pyfbo.h" #include "qpython_imageprovider.h" #include "global_libpython_loader.h" #include "pythonlib_loader.h" #include "pyotherside_plugin.h" static void pyotherside_atexit() { QPythonPriv::closing(); } PyOtherSideExtensionPlugin::PyOtherSideExtensionPlugin() { atexit(pyotherside_atexit); } PyOtherSideExtensionPlugin::~PyOtherSideExtensionPlugin() { } void PyOtherSideExtensionPlugin::initializeEngine(QQmlEngine *engine, const char *uri) { Q_ASSERT(QString(PYOTHERSIDE_PLUGIN_ID) == uri); // In some Linux distributions, the plugin (and subsequently libpython) // isn't loaded with the RTLD_GLOBAL flag, so symbols in libpython that // are needed by shared Python modules won't be resolved unless we also // load libpython again RTLD_GLOBAL again. We do this here. GlobalLibPythonLoader::loadPythonGlobally(); // Extract and load embedded Python Standard Library, if necessary PythonLibLoader::extractPythonLibrary(); engine->addImageProvider(PYOTHERSIDE_IMAGEPROVIDER_ID, new QPythonImageProvider); } void PyOtherSideExtensionPlugin::registerTypes(const char *uri) { Q_ASSERT(QString(PYOTHERSIDE_PLUGIN_ID) == uri); qmlRegisterType(uri, 1, 0, PYOTHERSIDE_QPYTHON_NAME); // There is no PyOtherSide 1.1 import, as it's the same as 1.0 qmlRegisterType(uri, 1, 2, PYOTHERSIDE_QPYTHON_NAME); qmlRegisterType(uri, 1, 3, PYOTHERSIDE_QPYTHON_NAME); qmlRegisterType(uri, 1, 4, PYOTHERSIDE_QPYTHON_NAME); qmlRegisterType(uri, 1, 5, PYOTHERSIDE_QPYTHON_NAME); qmlRegisterType(uri, 1, 5, PYOTHERSIDE_QPYGLAREA_NAME); qmlRegisterType(uri, 1, 5, PYOTHERSIDE_PYFBO_NAME); } pyotherside-1.6.2/src/pyotherside_plugin.h000066400000000000000000000030741475412515400207500ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2011, 2013-2025, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #ifndef PYOTHERSIDE_PLUGIN_H #define PYOTHERSIDE_PLUGIN_H #include #include #define PYOTHERSIDE_PLUGIN_ID "io.thp.pyotherside" #define PYOTHERSIDE_IMAGEPROVIDER_ID "python" #define PYOTHERSIDE_QPYTHON_NAME "Python" #define PYOTHERSIDE_QPYGLAREA_NAME "PyGLArea" #define PYOTHERSIDE_PYFBO_NAME "PyFBO" class Q_DECL_EXPORT PyOtherSideExtensionPlugin : public QQmlExtensionPlugin { Q_OBJECT Q_PLUGIN_METADATA(IID PYOTHERSIDE_PLUGIN_ID) public: PyOtherSideExtensionPlugin(); ~PyOtherSideExtensionPlugin(); virtual void initializeEngine(QQmlEngine *engine, const char *uri); virtual void registerTypes(const char *uri); }; #endif /* PYOTHERSIDE_PLUGIN_H */ pyotherside-1.6.2/src/pyqobject.h000066400000000000000000000024201475412515400170250ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2014, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #ifndef PYOTHERSIDE_PYQOBJECT_H #define PYOTHERSIDE_PYQOBJECT_H #include "python_wrap.h" #include "qobject_ref.h" typedef struct { PyObject_HEAD QObjectRef *m_qobject_ref; } pyotherside_QObject; typedef struct { PyObject_HEAD QObjectMethodRef *m_method_ref; } pyotherside_QObjectMethod; extern PyTypeObject pyotherside_QObjectType; extern PyTypeObject pyotherside_QObjectMethodType; #endif /* PYOTHERSIDE_PYQOBJECT_H */ pyotherside-1.6.2/src/python_wrap.h000066400000000000000000000001461475412515400174020ustar00rootroot00000000000000#pragma once #pragma push_macro("slots") #undef slots #include "Python.h" #pragma pop_macro("slots") pyotherside-1.6.2/src/pythonlib_loader.cpp000066400000000000000000000065211475412515400207240ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2014, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #include "pythonlib_loader.h" #include #include #include #if defined(HAVE_DLADDR) # include #endif namespace PythonLibLoader { static void prependPythonPath(const QString &path) { QString pythonpath(path + ":" + QString::fromUtf8(qgetenv("PYTHONPATH"))); QByteArray pythonpath_utf8 = pythonpath.toUtf8(); qputenv("PYTHONPATH", pythonpath_utf8.constData()); } #if defined(PYTHONLIB_LOADER_HAVE_PYTHONLIB_ZIP) bool extractPythonLibrary() { QString source(":/io/thp/pyotherside/pythonlib.zip"); QString destdir(QStandardPaths::writableLocation(QStandardPaths::TempLocation)); QString destination(QDir(destdir).filePath("pythonlib.zip")); prependPythonPath(destination); if (QFile::exists(destination)) { return true; } return QFile::copy(source, destination); } #else /* PYTHONLIB_LOADER_HAVE_PYTHONLIB_ZIP */ bool extractPythonLibrary() { #if defined(HAVE_DLADDR) // Add the library into the path in case it has a .zip file appended Dl_info info; memset(&info, 0, sizeof(info)); int res = dladdr((void *)&extractPythonLibrary, &info); if (!res) { qWarning() << "Could not determine library path"; return false; } QString fname = QString::fromUtf8(info.dli_fname); // On Android, dladdr() returns only the basename of the file, so we go // hunt for the full path in /proc/self/maps, where the shared library is // mapped (TODO: We could parse the address range and compare that, too) if (!fname.startsWith("/")) { QFile mapsf("/proc/self/maps"); if (mapsf.exists()) { mapsf.open(QIODevice::ReadOnly); QTextStream maps(&mapsf); QString line; while (!(line = maps.readLine()).isNull()) { #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // Qt::SkipEmptyParts was introduced in Qt 5.14 according to: // https://doc.qt.io/qt-5/qt.html#SplitBehaviorFlags-enum QString filename = line.split(' ', QString::SkipEmptyParts).last(); #else QString filename = line.split(' ', Qt::SkipEmptyParts).last(); #endif if (filename.endsWith("/" + fname)) { fname = filename; qDebug() << "Resolved full path:" << fname; break; } } } } prependPythonPath(fname); #endif return true; } #endif /* PYTHONLIB_LOADER_HAVE_PYTHONLIB_ZIP */ }; /* namespace PythonLibLoader */ pyotherside-1.6.2/src/pythonlib_loader.h000066400000000000000000000020221475412515400203610ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2014, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #ifndef PYOTHERSIDE_PYTHONLIB_LOADER_H #define PYOTHERSIDE_PYTHONLIB_LOADER_H namespace PythonLibLoader { bool extractPythonLibrary(); }; #endif /* PYOTHERSIDE_PYTHONLIB_LOADER_H */ pyotherside-1.6.2/src/pythonlib_loader.qrc000066400000000000000000000002041475412515400207170ustar00rootroot00000000000000 pythonlib.zip pyotherside-1.6.2/src/qml_python_bridge.h000066400000000000000000000024771475412515400205470ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2011, 2013-2025, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #ifndef PYOTHERSIDE_QML_PYTHON_BRIDGE_H #define PYOTHERSIDE_QML_PYTHON_BRIDGE_H #include "pyobject_converter.h" #include "qvariant_converter.h" inline PyObject * convertQVariantToPyObject(QVariant v) { return convert(v); } inline QVariant convertPyObjectToQVariant(PyObject *o) { return convert(o); } #endif /* PYOTHERSIDE_QML_PYTHON_BRIDGE_H */ pyotherside-1.6.2/src/qmldir000066400000000000000000000001211475412515400160630ustar00rootroot00000000000000module io.thp.pyotherside plugin pyothersideplugin typeinfo pyotherside.qmltypes pyotherside-1.6.2/src/qobject_ref.cpp000066400000000000000000000043701475412515400176510ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2014, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #include "qobject_ref.h" QObjectRef::QObjectRef(QObject *obj) : qobject(obj) { if (qobject) { QObject::connect(qobject, SIGNAL(destroyed(QObject *)), this, SLOT(handleDestroyed(QObject *))); } } QObjectRef::QObjectRef(const QObjectRef &other) : qobject(other.qobject) { if (qobject) { QObject::connect(qobject, SIGNAL(destroyed(QObject *)), this, SLOT(handleDestroyed(QObject *))); } } QObjectRef::~QObjectRef() { if (qobject) { QObject::disconnect(qobject, SIGNAL(destroyed(QObject *)), this, SLOT(handleDestroyed(QObject *))); } } QObjectRef & QObjectRef::operator=(const QObjectRef &other) { if (this != &other) { if (qobject != other.qobject) { if (qobject) { QObject::disconnect(qobject, SIGNAL(destroyed(QObject *)), this, SLOT(handleDestroyed(QObject *))); } if (other.qobject) { qobject = other.qobject; QObject::connect(qobject, SIGNAL(destroyed(QObject *)), this, SLOT(handleDestroyed(QObject *))); } } } return *this; } void QObjectRef::handleDestroyed(QObject *obj) { if (obj == qobject) { // Have to remove internal reference, as qobject is destroyed qobject = NULL; } } pyotherside-1.6.2/src/qobject_ref.h000066400000000000000000000032741475412515400173200ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2014, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #ifndef PYOTHERSIDE_QOBJECT_REF_H #define PYOTHERSIDE_QOBJECT_REF_H #include class QObjectRef : public QObject { Q_OBJECT public: explicit QObjectRef(QObject *obj=0); virtual ~QObjectRef(); QObjectRef(const QObjectRef &other); QObjectRef &operator=(const QObjectRef &other); QObject *value() const { return qobject; } operator bool() const { return (qobject != 0); } private slots: void handleDestroyed(QObject *obj); private: QObject *qobject; }; class QObjectMethodRef { public: QObjectMethodRef(const QObjectRef &object, const QString &method) : m_object(object) , m_method(method) { } const QObjectRef &object() { return m_object; } const QString &method() { return m_method; } private: QObjectRef m_object; QString m_method; }; #endif // PYOTHERSIDE_QOBJECT_REF_H pyotherside-1.6.2/src/qpython.cpp000066400000000000000000000361611475412515400170730ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2011, 2013-2025, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #include "qml_python_bridge.h" #include "qpython.h" #include "qpython_priv.h" #include "qpython_worker.h" #include "ensure_gil_state.h" #include #include #include #define SINCE_API_VERSION(smaj, smin) \ ((api_version_major > smaj) || (api_version_major == smaj && api_version_minor >= smin)) #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) # define GET_JS_ENGINE(obj) ((obj).engine()) #else # define GET_JS_ENGINE(obj) (qjsEngine(this)) #endif QPythonPriv * QPython::priv = NULL; QPython::QPython(QObject *parent, int api_version_major, int api_version_minor) : QObject(parent) , worker(new QPythonWorker(this)) , thread() , handlers() , api_version_major(api_version_major) , api_version_minor(api_version_minor) , error_connections(0) { if (priv == NULL) { priv = new QPythonPriv; } worker->moveToThread(&thread); QObject::connect(priv, SIGNAL(receive(QVariant)), this, SLOT(receive(QVariant))); QObject::connect(this, SIGNAL(process(QVariant,QVariant,QJSValue *)), worker, SLOT(process(QVariant,QVariant,QJSValue *))); QObject::connect(worker, SIGNAL(finished(QVariant,QJSValue *)), this, SLOT(finished(QVariant,QJSValue *))); QObject::connect(this, SIGNAL(import(QString,QJSValue *)), worker, SLOT(import(QString,QJSValue *))); QObject::connect(this, SIGNAL(import_names(QString, QVariant, QJSValue *)), worker, SLOT(import_names(QString, QVariant, QJSValue *))); QObject::connect(worker, SIGNAL(imported(bool,QJSValue *)), this, SLOT(imported(bool,QJSValue *))); thread.setObjectName("QPythonWorker"); thread.start(); } QPython::~QPython() { thread.quit(); thread.wait(); delete worker; } void QPython::connectNotify(const QMetaMethod &signal) { if (signal == QMetaMethod::fromSignal(&QPython::error)) { error_connections++; } } void QPython::disconnectNotify(const QMetaMethod &signal) { if (signal == QMetaMethod::fromSignal(&QPython::error)) { error_connections--; } } void QPython::addImportPath(QString path) { ENSURE_GIL_STATE; // Strip leading "file://" (for use with Qt.resolvedUrl()) if (path.startsWith("file://")) { #ifdef WIN32 // On Windows, path would be "file:///C:\...", so strip 8 chars to get // a Windows-compatible absolute filename to be used as import path path = path.mid(8); #else path = path.mid(7); #endif } if (SINCE_API_VERSION(1, 3) && path.startsWith("qrc:")) { const char *module = "pyotherside.qrc_importer"; QString filename = "/io/thp/pyotherside/qrc_importer.py"; QString errorMessage = priv->importFromQRC(module, filename); if (!errorMessage.isNull()) { emitError(errorMessage); } } QByteArray utf8bytes = path.toUtf8(); PyObject *sys_path = PySys_GetObject((char*)"path"); PyObjectRef cwd(PyUnicode_FromString(utf8bytes.constData()), true); PyList_Insert(sys_path, 0, cwd.borrow()); } void QPython::importNames(QString name, QVariant args, QJSValue callback) { QJSValue *cb = 0; if (!callback.isNull() && !callback.isUndefined() && callback.isCallable()) { cb = new QJSValue(callback); } emit import_names(name, args, cb); } bool QPython::importNames_sync(QString module_name, QVariant args) { // The plan is to "from module_name import a, b, c". And args is the list with a, b, c. // The module_name can be a packaged module "x.y.z" -- "from x.y.z import a, b, c". // Thus: // - import the module, given by module_name, // - get the objects from the module, given by names in args, // - put the objects into globals of priv QByteArray utf8bytes = module_name.toUtf8(); const char *moduleName = utf8bytes.constData(); ENSURE_GIL_STATE; // PyOtherSide API 1.2 behavior: "import x.y.z" -- where the module 'z' is needed PyObjectRef module = PyObjectRef(PyImport_ImportModule(moduleName), true); if (!module) { emitError(QString("Cannot import module: %1 (%2)").arg(module_name).arg(priv->formatExc())); return false; } // at this point the module with the target objects is in PyObjectRef module, // it should be well imported in Python // Get the names of functions/objects to import QVariantList vl = args.toList(); QString obj_name; // object name to import PyObjectRef result; // the object, obtained from globals_temp // for each object name try to get it from the module // - on success put it into priv.globals // - on failure emit the error and continue for (QVariantList::const_iterator obj = vl.begin(); obj != vl.end(); ++obj) { obj_name = obj->toString(); utf8bytes = obj_name.toUtf8(); PyObject *res = PyObject_GetAttrString(module.borrow(), utf8bytes); result = PyObjectRef(res, true); if (!result) { emitError(QString("Object '%1' is not found in '%2': (%3)").arg(obj_name).arg(module_name).arg(priv->formatExc())); continue; } PyDict_SetItemString(priv->globals.borrow(), utf8bytes.constData(), result.borrow()); } return true; } void QPython::importModule(QString name, QJSValue callback) { QJSValue *cb = 0; if (!callback.isNull() && !callback.isUndefined() && callback.isCallable()) { cb = new QJSValue(callback); } emit import(name, cb); } bool QPython::importModule_sync(QString name) { // Lesson learned: name.toUtf8().constData() doesn't work, as the // temporary QByteArray will be destroyed after constData() has // returned, so we need to save the toUtf8() result in a local // variable that doesn't get destroyed until the function returns. QByteArray utf8bytes = name.toUtf8(); const char *moduleName = utf8bytes.constData(); ENSURE_GIL_STATE; bool use_api_10 = (api_version_major == 1 && api_version_minor == 0); PyObjectRef module; if (use_api_10) { // PyOtherSide API 1.0 behavior (star import) module = PyObjectRef(PyImport_ImportModule(moduleName), true); } else { // PyOtherSide API 1.2 behavior: "import x.y.z" PyObjectRef fromList(PyList_New(0), true); module = PyObjectRef(PyImport_ImportModuleEx(const_cast(moduleName), NULL, NULL, fromList.borrow()), true); } if (!module) { emitError(QString("Cannot import module: %1 (%2)").arg(name).arg(priv->formatExc())); return false; } if (!use_api_10) { // PyOtherSide API 1.2 behavior: "import x.y.z" // If "x.y.z" is imported, we need to set "x" in globals if (name.indexOf('.') != -1) { name = name.mid(0, name.indexOf('.')); utf8bytes = name.toUtf8(); moduleName = utf8bytes.constData(); } } PyDict_SetItemString(priv->globals.borrow(), moduleName, module.borrow()); return true; } void QPython::receive(QVariant variant) { QVariantList list = variant.toList(); QString event = list[0].toString(); if (handlers.contains(event)) { QJSValue callback = handlers[event]; QJSValueList args; for (int i=1; itoScriptValue(list[i]); } QJSValue result = callback.call(args); if (result.isError()) { // Ideally we would throw the error back to Python (so that the // pyotherside.send() method fails, as this is where the call // originated). We can't do this, because the pyotherside.send() // call is asynchronous (it returns before we call into JS), so do // the next best thing and report the error to our error handler in // QML instead. emitError("pyotherside.send() failed handler: " + result.property("fileName").toString() + ":" + result.property("lineNumber").toString() + ": " + result.toString()); } } else { // Default action emit received(variant); } } void QPython::setHandler(QString event, QJSValue callback) { if (!callback.isCallable() || callback.isNull() || callback.isUndefined()) { handlers.remove(event); } else { handlers[event] = callback; } } QVariant QPython::evaluate(QString expr) { ENSURE_GIL_STATE; PyObjectRef o(priv->eval(expr), true); if (!o) { emitError(QString("Cannot evaluate '%1' (%2)").arg(expr).arg(priv->formatExc())); return QVariant(); } return convertPyObjectToQVariant(o.borrow()); } QVariantList QPython::unboxArgList(QVariant &args) { // Unbox QJSValue from QVariant QVariantList vl = args.toList(); for (int i = 0, c = vl.count(); i < c; ++i) { QVariant &v = vl[i]; if (v.userType() == qMetaTypeId()) { // TODO: Support boxing a QJSValue as reference in Python v = v.value().toVariant(); } } return vl; } void QPython::call(QVariant func, QVariant boxed_args, QJSValue callback) { QJSValue *cb = 0; if (!callback.isNull() && !callback.isUndefined() && callback.isCallable()) { cb = new QJSValue(callback); } // Unbox QJSValue from QVariant, since QJSValue::toVariant() can cause calls into // QML engine and we don't want that to happen from non-GUI thread QVariantList unboxed_args = unboxArgList(boxed_args); emit process(func, unboxed_args, cb); } QVariant QPython::call_sync(QVariant func, QVariant boxed_args) { return call_internal(func, boxed_args, true); } QVariant QPython::call_internal(QVariant func, QVariant args, bool unbox) { ENSURE_GIL_STATE; PyObjectRef callable; QString name; if (SINCE_API_VERSION(1, 4)) { if (static_cast(func.type()) == QMetaType::QString) { // Using version >= 1.4, but func is a string callable = PyObjectRef(priv->eval(func.toString()), true); name = func.toString(); } else { // Try to interpret "func" as a Python object callable = PyObjectRef(convertQVariantToPyObject(func), true); PyObjectRef repr = PyObjectRef(PyObject_Repr(callable.borrow()), true); name = convertPyObjectToQVariant(repr.borrow()).toString(); } } else { // Versions before 1.4 only support func as a string callable = PyObjectRef(priv->eval(func.toString()), true); name = func.toString(); } if (!callable) { emitError(QString("Function not found: '%1' (%2)").arg(name).arg(priv->formatExc())); return QVariant(); } // Unbox QJSValue from QVariant if requested. QPython::call may have done // this already, but call_sync is also exposed directly, so it does not // happen in this case otherwise QVariant args_unboxed; if (unbox) { args_unboxed = unboxArgList(args); } else { args_unboxed = args; } QVariant v; QString errorMessage = priv->call(callable.borrow(), name, args_unboxed, &v); if (!errorMessage.isNull()) { emitError(errorMessage); } return v; } QVariant QPython::getattr(QVariant obj, QString attr) { if (!SINCE_API_VERSION(1, 4)) { emitError(QString("Import PyOtherSide 1.4 or newer to use getattr()")); return QVariant(); } ENSURE_GIL_STATE; PyObjectRef pyobj(convertQVariantToPyObject(obj), true); if (!pyobj) { emitError(QString("Failed to convert %1 to python object: '%1' (%2)").arg(obj.toString()).arg(priv->formatExc())); return QVariant(); } QByteArray byteArray = attr.toUtf8(); const char *attrStr = byteArray.data(); PyObjectRef o(PyObject_GetAttrString(pyobj.borrow(), attrStr), true); if (!o) { emitError(QString("Attribute not found: '%1' (%2)").arg(attr).arg(priv->formatExc())); return QVariant(); } return convertPyObjectToQVariant(o.borrow()); } void QPython::finished(QVariant result, QJSValue *callback) { QJSValueList args; QJSValue v = GET_JS_ENGINE(*callback)->toScriptValue(result); args << v; QJSValue callbackResult = callback->call(args); if (SINCE_API_VERSION(1, 2)) { if (callbackResult.isError()) { emitError(callbackResult.property("fileName").toString() + ":" + callbackResult.property("lineNumber").toString() + ": " + callbackResult.toString()); } } delete callback; } void QPython::imported(bool result, QJSValue *callback) { QJSValueList args; QJSValue v = GET_JS_ENGINE(*callback)->toScriptValue(QVariant(result)); args << v; QJSValue callbackResult = callback->call(args); if (SINCE_API_VERSION(1, 2)) { if (callbackResult.isError()) { emitError(callbackResult.property("fileName").toString() + ":" + callbackResult.property("lineNumber").toString() + ": " + callbackResult.toString()); } } delete callback; } QString QPython::pluginVersion() { return QString(PYOTHERSIDE_VERSION); } QString QPython::pythonVersion() { if (SINCE_API_VERSION(1, 5)) { ENSURE_GIL_STATE; PyObjectRef version_info(PySys_GetObject("version_info")); if (version_info && PyTuple_Check(version_info.borrow()) && PyTuple_Size(version_info.borrow()) >= 3) { QStringList parts; for (int i=0; i<3; i++) { PyObjectRef part(PyTuple_GetItem(version_info.borrow(), i)); parts << convertPyObjectToQVariant(part.borrow()).toString(); } return parts.join('.'); } // Fallback to the compile-time version below qWarning("Could not determine runtime Python version"); } return QString(PY_VERSION); } void QPython::emitError(const QString &message) { if (error_connections) { emit error(message); } else { // We should only print the error if SINCE_API_VERSION(1, 4), but as // the error messages are useful for debugging (especially if users // don't import the latest API version), we do it unconditionally qWarning("Unhandled PyOtherSide error: %s", message.toUtf8().constData()); } } pyotherside-1.6.2/src/qpython.h000066400000000000000000000337321475412515400165410ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2011, 2013-2025, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #ifndef PYOTHERSIDE_QPYTHON_H #define PYOTHERSIDE_QPYTHON_H #include "python_wrap.h" #include #include #include #include #include #include #include class QPython; class QPythonPriv; class QPythonWorker; class QPython : public QObject { Q_OBJECT public: /** * \brief Create a new Python instance * * A new Python instance can be created in QML as follows: * * \code * import io.thp.pyotherside 1.0 * * Python { * id: py * } * \endcode * * \arg parent The parent QObject * \arg api_version_major Major API version (used internally) * \arg api_version_minor Minor API version (used internally) **/ QPython(QObject *parent, int api_version_major, int api_version_minor); virtual ~QPython(); /** * \brief Add a local filesystem path to Python's sys.path. * * The import path will be added synchronously. After the call * returns, the new import path will already be in effect. * * \arg path Directory that will be added to the search path **/ Q_INVOKABLE void addImportPath(QString path); /** * \brief Add a handler for events sent with \c pyotherside.send() * * The provided \a callback will be called whenever the first argument * to \c pyotherside.send() matches the \a event argument. * * For example, when the Python code contains a call like this: * * \code * import pyotherside * * pyotherside.send('new-entries', 100, 123) * \endcode * * The event can be captured in QML like this: * * \code * Python { * id: py * * Component.onCompleted: { * py.setHandler('new-entries', function(first, last) { * console.log('New entries from ' + first + ' to ' + last); * }); * } * } * \endcode * * All events for which no handler was set will be sent to the * received() signal. * * If a handler for that specific \a event already exist, the * new \a callback will replace the old one. * * \arg event The event name (first argument to pyotherside.send()) * \arg callback The JS callback to be called when the event occurs **/ Q_INVOKABLE void setHandler(QString event, QJSValue callback); /** * \brief Evaluate a Python expression synchronously * * Evaluate the string \a expr as Python expression and return the * result as Qt data type. The evaluation happens synchronously. * * \code * Python { * Component.onCompleted: { * console.log('Squares: ' + evaluate('[x for x in range(10)]')); * } * } * \endcode * * \arg expr Python expression to be evaluated * \result The result of the expression as Qt data type **/ Q_INVOKABLE QVariant evaluate(QString expr); /** * \brief Asynchronously import objects from Python module * * Imports objects, given by list of names, from Python module asynchronously. * The function will return immediately. If the module is successfully imported, * the supplied \a callback will be called. Only then will the * imported module be available: * * \code * Python { * Component.onCompleted: { * importNames('os', ['path'], function (success) { * if (success) { * // You can use the "path" submodule here * } else { * console.log('Importing failed') * } * }); * } * } * \endcode * * If an error occurs while trying to import, the signal error() * will be emitted with detailed information about the error. * * \arg name The name of the Python module to import from * \arg args The name of Python objects to import from the module * \arg callback The JS callback to be called when the module is * successfully imported **/ Q_INVOKABLE void importNames(QString name, QVariant args, QJSValue callback); /** * \brief Synchronously import objects from Python module * * Imports objects, given by list of names, from Python module synchronously. * This function will block until the objects are imported and available. * In general, you should use importNames() instead of this function * to avoid blocking the QML UI thread. Example use: * * \code * Python { * Component.onCompleted: { * var success = importNames_sync('os', ['path']); * if (success) { * // You can use the "path" submodule here * } * } * } * \endcode * * \arg name The name of the Python module to import from * \arg args The name of Python objects to import from the module * \result \c true if the import was successful, \c false otherwise **/ Q_INVOKABLE bool importNames_sync(QString name, QVariant args); /** * \brief Asynchronously import a Python module * * Imports a Python module by name asynchronously. The function * will return immediately. If the module is successfully imported, * the supplied \a callback will be called. Only then will the * imported module be available: * * \code * Python { * Component.onCompleted: { * importModule('os', function() { * // You can use the "os" module here * }); * } * } * \endcode * * If an error occurs while trying to import, the signal error() * will be emitted with detailed information about the error. * * \arg name The name of the Python module to import * \arg callback The JS callback to be called when the module is * successfully imported **/ Q_INVOKABLE void importModule(QString name, QJSValue callback); /** * \brief Synchronously import a Python module * * Imports a Python module by name synchronously. This function * will block until the module is imported and available. In * general, you should use importModule() instead of this function * to avoid blocking the QML UI thread. Example use: * * \code * Python { * Component.onCompleted: { * var success = importModule_sync('os'); * if (success) { * // You can use the "os" module here * } * } * } * \endcode * * \arg name The name of the Python module to import * \result \c true if the import was successful, \c false otherwise **/ Q_INVOKABLE bool importModule_sync(QString name); /** * \brief Asynchronously call a Python function * * Call a Python function asynchronously and call back into QML * when the result is available: * * \code * Python { * Component.onCompleted: { * importModule('os', function() { * call('os.getcwd', [], function (result) { * console.log('Working directory: ' + result); * call('os.chdir', ['/'], function (result) { * console.log('Working directory changed.'); * });); * }); * }); * } * } * \endcode * * \arg func The Python function to call (string or Python callable) * \arg args A list of arguments, or \c [] for no arguments * \arg callback A callback that receives the function call result **/ Q_INVOKABLE void call(QVariant func, QVariant args=QVariantList(), QJSValue callback=QJSValue()); /** * \brief Synchronously call a Python function * * This is the synchronous variant of call(). In general, you should * use call() instead of this function to avoid blocking the QML UI * thread. Example usage: * * \code * Python { * Component.onCompleted: { * importModule_sync('os'); * var cwd = call_sync('os.getcwd', []); * console.log('Working directory: ' + cwd); * call_sync('os.chdir', ['/']); * console.log('Working directory changed.'); * } * } * \endcode * * \arg func The Python function to call (string or Python callable) * \arg args A list of arguments, or \c [] for no arguments * \result The return value of the Python call as Qt data type **/ Q_INVOKABLE QVariant call_sync(QVariant func, QVariant boxed_args=QVariantList()); QVariant call_internal(QVariant func, QVariant boxed_args=QVariantList(), bool unbox=true); /** * \brief Get an attribute value of a Python object synchronously * * \code * Python { * Component.onCompleted: { * importModule('datetime', function() { * call('datetime.datetime.now', [], function(dt) { * console.log('Year: ' + getattr(dt, 'year')); * }); * }); * } * } * \endcode * * \arg obj The Python object * \arg attr The attribute to get * \result The attribute value **/ Q_INVOKABLE QVariant getattr(QVariant obj, QString attr); /** * \brief Get the PyOtherSide version * * \result The running version of PyOtherSide **/ Q_INVOKABLE QString pluginVersion(); /** * \brief Get the Python versino * * \result The running versino of Python **/ Q_INVOKABLE QString pythonVersion(); signals: /** * \brief Default event handler for \c pyotherside.send() * * This signal will be emitted for all events from Python for * which no specific handler (see setHandler()) is configured. * * \arg data The argument list of \c pyotherside.send() **/ void received(QVariant data); /** * \brief Error handler for errors from Python * * This signal will be emitted when an error happens in the * Python interpreter that isn't caught. For example, errors * in evaluate(), importModule() and call() will be reported * with this signal. * * \arg traceback A string describing the error **/ void error(QString traceback); /* For internal use only */ void process(QVariant func, QVariant unboxed_args, QJSValue *callback); void import(QString name, QJSValue *callback); void import_names(QString name, QVariant args, QJSValue *callback); private slots: void receive(QVariant data); void finished(QVariant result, QJSValue *callback); void imported(bool result, QJSValue *callback); void connectNotify(const QMetaMethod &signal); void disconnectNotify(const QMetaMethod &signal); private: QVariantList unboxArgList(QVariant &args); static QPythonPriv *priv; QPythonWorker *worker; QThread thread; QMap handlers; int api_version_major; int api_version_minor; void emitError(const QString &message); int error_connections; }; class QPython10 : public QPython { Q_OBJECT public: QPython10(QObject *parent=0) : QPython(parent, 1, 0) { } }; class QPython12 : public QPython { Q_OBJECT public: QPython12(QObject *parent=0) : QPython(parent, 1, 2) { } }; class QPython13 : public QPython { Q_OBJECT public: QPython13(QObject *parent=0) : QPython(parent, 1, 3) { } }; class QPython14 : public QPython { Q_OBJECT public: QPython14(QObject *parent=0) : QPython(parent, 1, 4) { } }; class QPython15 : public QPython { Q_OBJECT public: QPython15(QObject *parent=0) : QPython(parent, 1, 5) { } }; #endif /* PYOTHERSIDE_QPYTHON_H */ pyotherside-1.6.2/src/qpython_imageprovider.cpp000066400000000000000000000216351475412515400220100ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2011, 2013-2025, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #include "qpython_priv.h" #include "qpython_imageprovider.h" #include "ensure_gil_state.h" #include #include #include QPythonImageProvider::QPythonImageProvider() : QQuickImageProvider(QQmlImageProviderBase::Image) { } QPythonImageProvider::~QPythonImageProvider() { } static void cleanup_python_qimage(void *data) { QPythonPriv *priv = QPythonPriv::instance(); ENSURE_GIL_STATE; Py_XDECREF(static_cast(data)); } QImage QPythonImageProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize) { QImage img; // Image data (and metadata) returned from Python PyObject *pixels = NULL; int width = 0, height = 0; int format = 0; // For counting the number of required bytes int bitsPerPixel = 0; size_t requiredBytes = 0; size_t actualBytes = 0; QPythonPriv *priv = QPythonPriv::instance(); if (!priv) { qWarning() << "Python component not instantiated yet"; return QImage(); } if (!priv->image_provider) { qWarning() << "No image provider set in Python code"; return QImage(); } QByteArray id_utf8 = id.toUtf8(); // Image provider implementation in Python: // // import pyotherside // // def image_provider(image_id, requested_size): // if requested_size == (-1, -1): // requested_size = 100, 200 # some sane default size // width, height = requested_size // pixels = ... // format = pyotherside.format_argb32 # or some other format // return (bytearray(pixels), (width, height), format) // // pyotherside.set_image_provider(image_provider) ENSURE_GIL_STATE; PyObjectRef args(Py_BuildValue("(N(ii))", PyUnicode_FromString(id_utf8.constData()), requestedSize.width(), requestedSize.height()), true); PyObjectRef result(PyObject_Call(priv->image_provider.borrow(), args.borrow(), NULL), true); if (!result) { qDebug() << "Error while calling the image provider"; PyErr_Print(); goto cleanup; } if (!PyArg_ParseTuple(result.borrow(), "O(ii)i", &pixels, &width, &height, &format)) { PyErr_Clear(); qDebug() << "Image provider must return (pixels, (width, height), format)"; goto cleanup; } if (!PyByteArray_Check(pixels)) { qDebug() << "Image data must be a Python bytearray()"; goto cleanup; } switch (format) { case PYOTHERSIDE_IMAGE_FORMAT_ENCODED: /* pyotherside.format_data */ break; case PYOTHERSIDE_IMAGE_FORMAT_SVG: /* pyotherside.format_svg_data */ break; case QImage::Format_Mono: case QImage::Format_MonoLSB: bitsPerPixel = 1; break; case QImage::Format_RGB32: case QImage::Format_ARGB32: bitsPerPixel = 32; break; case QImage::Format_RGB16: case QImage::Format_RGB555: case QImage::Format_RGB444: bitsPerPixel = 16; break; case QImage::Format_RGB666: case QImage::Format_RGB888: bitsPerPixel = 24; break; default: qDebug() << "Invalid format:" << format; goto cleanup; } requiredBytes = (bitsPerPixel * width * height + 7) / 8; actualBytes = PyByteArray_Size(pixels); // QImage requires scanlines to be 32-bit aligned. Scanlines from Python // are considered to be tightly packed, we have to check for alignment. // While we could re-pack the data to be aligned, we don't want to do that // for performance reasons. // If we're using 32-bit data (e.g. ARGB32), it will always be aligned. if (format >= 0 && bitsPerPixel != 32) { if ((bitsPerPixel * width) % 32 != 0) { // If actualBytes > requiredBytes, we can check if there are enough // bytes to consider the data 32-bit aligned (from Python) and avoid // the error (scanlines must be padded to multiples of 4 bytes) if ((unsigned int)(((width * bitsPerPixel / 8 + 3) / 4) * 4 * height) == actualBytes) { qDebug() << "Assuming 32-bit aligned scanlines from Python"; } else { qDebug() << "Each scanline of data must be 32-bit aligned"; goto cleanup; } } } if (format >= 0 && requiredBytes > actualBytes) { qDebug() << "Format" << (enum QImage::Format)format << "at size" << QSize(width, height) << "requires at least" << requiredBytes << "bytes of image data, got only" << actualBytes << "bytes"; goto cleanup; } if (format < 0) { switch (format) { case PYOTHERSIDE_IMAGE_FORMAT_ENCODED: { // Pixel data is actually encoded image data that we need to decode img.loadFromData((const unsigned char*)PyByteArray_AsString(pixels), PyByteArray_Size(pixels)); break; } case PYOTHERSIDE_IMAGE_FORMAT_SVG: { // Convert the Python byte array to a QByteArray QByteArray svgDataArray(PyByteArray_AsString(pixels), PyByteArray_Size(pixels)); // Load the SVG data to the SVG renderer QSvgRenderer renderer(svgDataArray); // Handle width, height or both not being set QSize defaultSize = renderer.defaultSize(); int defaultWidth = defaultSize.width(); int defaultHeight = defaultSize.height(); if (width < 0 && height < 0) { // Both Width and Height have not been set - use the defaults from the SVG data // (each SVG image has a default size) // NOTE: we get a -1,-1 requestedSize only if sourceSize is not set at all, // if either width or height is set then the other one is 0, not -1 width = defaultWidth; height = defaultHeight; } else { // At least width or height is valid if (width <= 0) { // Width is not set, use default width scaled according to height to keep // aspect ratio if (defaultHeight != 0) { // Protect from division by zero width = (float)defaultWidth*((float)height/(float)defaultHeight); } } if (height <= 0) { // Height is not set, use default height scaled according to width to keep // aspect ratio if (defaultWidth != 0) { // Protect from division by zero height = (float)defaultHeight*((float)width/(float)defaultWidth); } } } // The pixel data is actually SVG image data that we need to render at correct size // // Note: according to the QImage and QPainter documentation the optimal QImage // format for drawing is Format_ARGB32_Premultiplied img = QImage(width, height, QImage::Format_ARGB32_Premultiplied); // According to the documentation an empty QImage needs to be "flushed" before // being used with QPainter to prevent rendering artifacts from showing up img.fill(Qt::transparent); // Paints the rendered SVG to the QImage instance QPainter painter(&img); renderer.render(&painter); break; } default: qWarning() << "Unknown format" << format << "has been specified and will not be handled."; break; } } else { // Need to keep a reference to the byte array object, as it contains // the backing store data for the QImage. // Will be decref'd by cleanup_python_qimage once the QImage is gone. Py_INCREF(pixels); img = QImage((const unsigned char *)PyByteArray_AsString(pixels), width, height, (enum QImage::Format)format, cleanup_python_qimage, pixels); } cleanup: *size = img.size(); return img; } pyotherside-1.6.2/src/qpython_imageprovider.h000066400000000000000000000023601475412515400214470ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2011, 2013-2025, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #ifndef PYOTHERSIDE_QPYTHON_IMAGEPROVIDER_H #define PYOTHERSIDE_QPYTHON_IMAGEPROVIDER_H #include class QPythonImageProvider : public QQuickImageProvider { public: QPythonImageProvider(); virtual ~QPythonImageProvider(); virtual QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize); }; #endif /* PYOTHERSIDE_QPYTHON_IMAGEPROVIDER_H */ pyotherside-1.6.2/src/qpython_priv.cpp000066400000000000000000000560171475412515400201350ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2011, 2013-2025, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #include "qml_python_bridge.h" #include "qpython_priv.h" #include "ensure_gil_state.h" #include #include #include #include #include #include #include #include #include static QPythonPriv *priv = NULL; static QString qstring_from_pyobject_arg(PyObject *object) { PyObjectConverter conv; if (conv.type(object) != PyObjectConverter::STRING) { PyErr_SetString(PyExc_ValueError, "Argument must be a string"); return QString(); } return QString::fromUtf8(conv.string(object)); } PyTypeObject pyotherside_QObjectType = { PyVarObject_HEAD_INIT(NULL, 0) "pyotherside.QObject", /* tp_name */ sizeof(pyotherside_QObject), /* tp_basicsize */ 0, /* tp_itemsize */ 0, /* tp_dealloc */ 0, /* tp_print */ 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_reserved */ 0, /* tp_repr */ 0, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ 0, /* tp_hash */ 0, /* tp_call */ 0, /* tp_str */ 0, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT, /* tp_flags */ "Wrapped QObject", /* tp_doc */ }; PyTypeObject pyotherside_QObjectMethodType = { PyVarObject_HEAD_INIT(NULL, 0) "pyotherside.QObjectMethod", /* tp_name */ sizeof(pyotherside_QObjectMethod), /* tp_basicsize */ 0, /* tp_itemsize */ 0, /* tp_dealloc */ 0, /* tp_print */ 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_reserved */ 0, /* tp_repr */ 0, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ 0, /* tp_hash */ 0, /* tp_call */ 0, /* tp_str */ 0, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT, /* tp_flags */ "Bound method of wrapped QObject", /* tp_doc */ }; PyObject * pyotherside_send(PyObject *self, PyObject *args) { priv->receiveObject(args); Py_RETURN_NONE; } PyObject * pyotherside_atexit(PyObject *self, PyObject *o) { priv->atexit_callback = PyObjectRef(o); Py_RETURN_NONE; } PyObject * pyotherside_set_image_provider(PyObject *self, PyObject *o) { priv->image_provider = PyObjectRef(o); Py_RETURN_NONE; } PyObject * pyotherside_qrc_is_file(PyObject *self, PyObject *filename) { QString qfilename = qstring_from_pyobject_arg(filename); if (qfilename.isNull()) { return NULL; } if (QFile(":" + qfilename).exists()) { Py_RETURN_TRUE; } Py_RETURN_FALSE; } PyObject * pyotherside_qrc_is_dir(PyObject *self, PyObject *dirname) { QString qdirname = qstring_from_pyobject_arg(dirname); if (qdirname.isNull()) { return NULL; } if (QDir(":" + qdirname).exists()) { Py_RETURN_TRUE; } Py_RETURN_FALSE; } PyObject * pyotherside_qrc_get_file_contents(PyObject *self, PyObject *filename) { QString qfilename = qstring_from_pyobject_arg(filename); if (qfilename.isNull()) { return NULL; } QFile file(":" + qfilename); if (!file.exists() || !file.open(QIODevice::ReadOnly)) { PyErr_SetString(PyExc_ValueError, "File not found"); return NULL; } QByteArray ba = file.readAll(); return PyByteArray_FromStringAndSize(ba.constData(), ba.size()); } PyObject * pyotherside_qrc_list_dir(PyObject *self, PyObject *dirname) { QString qdirname = qstring_from_pyobject_arg(dirname); if (qdirname.isNull()) { return NULL; } QDir dir(":" + qdirname); if (!dir.exists()) { PyErr_SetString(PyExc_ValueError, "Directory not found"); return NULL; } return convertQVariantToPyObject(dir.entryList()); } void pyotherside_QObject_dealloc(pyotherside_QObject *self) { delete self->m_qobject_ref; Py_TYPE(self)->tp_free((PyObject *)self); } PyObject * pyotherside_QObject_repr(PyObject *o) { if (!PyObject_TypeCheck(o, &pyotherside_QObjectType)) { return PyErr_Format(PyExc_TypeError, "Not a pyotherside.QObject"); } pyotherside_QObject *pyqobject = reinterpret_cast(o); QObjectRef *ref = pyqobject->m_qobject_ref; if (ref) { QObject *qobject = ref->value(); const QMetaObject *metaObject = qobject->metaObject(); return PyUnicode_FromFormat("", metaObject->className(), qobject); } return PyUnicode_FromFormat(""); } PyObject * pyotherside_QObject_getattro(PyObject *o, PyObject *attr_name) { if (!PyObject_TypeCheck(o, &pyotherside_QObjectType)) { return PyErr_Format(PyExc_TypeError, "Not a pyotherside.QObject"); } if (!PyUnicode_Check(attr_name)) { return PyErr_Format(PyExc_TypeError, "attr_name must be a string"); } pyotherside_QObject *pyqobject = reinterpret_cast(o); QObjectRef *ref = pyqobject->m_qobject_ref; if (!ref) { return PyErr_Format(PyExc_ValueError, "Dangling QObject"); } QObject *qobject = ref->value(); if (!qobject) { return PyErr_Format(PyExc_ReferenceError, "Referenced QObject was deleted"); } const QMetaObject *metaObject = qobject->metaObject(); QString attrName = convertPyObjectToQVariant(attr_name).toString(); for (int i=0; ipropertyCount(); i++) { QMetaProperty property = metaObject->property(i); if (attrName == property.name()) { return convertQVariantToPyObject(property.read(qobject)); } } for (int i=0; imethodCount(); i++) { QMetaMethod method = metaObject->method(i); if (attrName == method.name()) { pyotherside_QObjectMethod *result = PyObject_New(pyotherside_QObjectMethod, &pyotherside_QObjectMethodType); result->m_method_ref = new QObjectMethodRef(*ref, attrName); return reinterpret_cast(result); } } return PyErr_Format(PyExc_AttributeError, "Not a valid attribute"); } int pyotherside_QObject_setattro(PyObject *o, PyObject *attr_name, PyObject *v) { if (!PyObject_TypeCheck(o, &pyotherside_QObjectType)) { PyErr_Format(PyExc_TypeError, "Not a pyotherside.QObject"); return -1; } if (!PyUnicode_Check(attr_name)) { PyErr_Format(PyExc_TypeError, "attr_name must be a string"); return -1; } pyotherside_QObject *pyqobject = reinterpret_cast(o); QObjectRef *ref = pyqobject->m_qobject_ref; if (!ref) { PyErr_Format(PyExc_ValueError, "Dangling QObject"); return -1; } QObject *qobject = ref->value(); if (!qobject) { PyErr_Format(PyExc_ReferenceError, "Referenced QObject was deleted"); return -1; } const QMetaObject *metaObject = qobject->metaObject(); QString attrName = convertPyObjectToQVariant(attr_name).toString(); for (int i=0; ipropertyCount(); i++) { QMetaProperty property = metaObject->property(i); if (attrName == property.name()) { QVariant variant(convertPyObjectToQVariant(v)); if (!property.write(qobject, variant)) { PyErr_Format(PyExc_AttributeError, "Could not set property %s to %s(%s)", attrName.toUtf8().constData(), variant.typeName(), variant.toString().toUtf8().constData()); return -1; } return 0; } } PyErr_Format(PyExc_AttributeError, "Property does not exist: %s", attrName.toUtf8().constData()); return -1; } void pyotherside_QObjectMethod_dealloc(pyotherside_QObjectMethod *self) { delete self->m_method_ref; Py_TYPE(self)->tp_free((PyObject *)self); } PyObject * pyotherside_QObjectMethod_repr(PyObject *o) { if (!PyObject_TypeCheck(o, &pyotherside_QObjectMethodType)) { return PyErr_Format(PyExc_TypeError, "Not a pyotherside.QObjectMethod"); } pyotherside_QObjectMethod *pyqobjectmethod = reinterpret_cast(o); QObjectMethodRef *ref = pyqobjectmethod->m_method_ref; if (!ref) { return PyUnicode_FromFormat(""); } QObjectRef oref = ref->object(); QObject *qobject = oref.value(); if (!qobject) { return PyUnicode_FromFormat("", ref->method().toUtf8().constData()); } const QMetaObject *metaObject = qobject->metaObject(); return PyUnicode_FromFormat("", ref->method().toUtf8().constData(), metaObject->className(), qobject); } PyObject * pyotherside_QObjectMethod_call(PyObject *callable_object, PyObject *args, PyObject *kw) { if (!PyObject_TypeCheck(callable_object, &pyotherside_QObjectMethodType)) { return PyErr_Format(PyExc_TypeError, "Not a pyotherside.QObjectMethod"); } if (!PyTuple_Check(args)) { return PyErr_Format(PyExc_TypeError, "Argument list not a tuple"); } if (kw) { if (!PyMapping_Check(kw)) { return PyErr_Format(PyExc_TypeError, "Keyword arguments not a mapping"); } if (PyMapping_Size(kw) > 0) { return PyErr_Format(PyExc_ValueError, "Keyword arguments not supported"); } } QList qargs = convertPyObjectToQVariant(args).toList(); pyotherside_QObjectMethod *pyqobjectmethod = reinterpret_cast(callable_object); QObjectMethodRef *ref = pyqobjectmethod->m_method_ref; if (!ref) { return PyErr_Format(PyExc_ValueError, "Dangling QObject"); } QList genericArguments; for (int j=0; jobject().value(); if (!o) { return PyErr_Format(PyExc_ReferenceError, "Referenced QObject was deleted"); } const QMetaObject *metaObject = o->metaObject(); for (int i=0; imethodCount(); i++) { QMetaMethod method = metaObject->method(i); if (method.name() == ref->method()) { if (method.methodType() == QMetaMethod::Signal) { // Signals can't be called directly, we just return true or // false depending on whether method.invoke() worked or not bool result = method.invoke(o, Qt::AutoConnection, genericArguments.value(0), genericArguments.value(1), genericArguments.value(2), genericArguments.value(3), genericArguments.value(4), genericArguments.value(5), genericArguments.value(6), genericArguments.value(7), genericArguments.value(8), genericArguments.value(9)); return convertQVariantToPyObject(result); } QVariant result; #if QT_VERSION < QT_VERSION_CHECK(6, 5, 0) QGenericReturnArgument returnArg = Q_RETURN_ARG(QVariant, result); #else /** * Starting with Qt 6.5, Q_RETURN_ARG() expands to a QMetaMethodReturnArgument, * whereas previously it returned a QGenericReturnArgument. Since we are using * the old, deprecated QMetaMethod::invoke() functions, and those take a * QGenericReturnArgument and not a QMetaMethodReturnArgument, we need to * create the QGenericReturnArgument ourselves by emulating what Q_RETURN_ARG() * does in old Qt versions before 6.5. * * See also: * https://bugreports.qt.io/browse/QTBUG-113147 * https://github.com/thp/pyotherside/issues/128 **/ QGenericReturnArgument returnArg {QT_STRINGIFY(QVariant), &result}; #endif if (method.invoke(o, Qt::DirectConnection, returnArg, genericArguments.value(0), genericArguments.value(1), genericArguments.value(2), genericArguments.value(3), genericArguments.value(4), genericArguments.value(5), genericArguments.value(6), genericArguments.value(7), genericArguments.value(8), genericArguments.value(9))) { return convertQVariantToPyObject(result); } return PyErr_Format(PyExc_RuntimeError, "QObject method call failed"); } } return PyErr_Format(PyExc_RuntimeError, "QObject method not found: %s", ref->method().toUtf8().constData()); } static PyMethodDef PyOtherSideMethods[] = { /* Introduced in PyOtherSide 1.0 */ {"send", pyotherside_send, METH_VARARGS, "Send data to Qt."}, {"atexit", pyotherside_atexit, METH_O, "Function to call on shutdown."}, /* Introduced in PyOtherSide 1.1 */ {"set_image_provider", pyotherside_set_image_provider, METH_O, "Set the QML image provider."}, /* Introduced in PyOtherSide 1.3 */ {"qrc_is_file", pyotherside_qrc_is_file, METH_O, "Check if a file exists in Qt Resources."}, {"qrc_is_dir", pyotherside_qrc_is_dir, METH_O, "Check if a directory exists in Qt Resources."}, {"qrc_get_file_contents", pyotherside_qrc_get_file_contents, METH_O, "Get file contents from a Qt Resource."}, {"qrc_list_dir", pyotherside_qrc_list_dir, METH_O, "Get directory entries from a Qt Resource."}, /* sentinel */ {NULL, NULL, 0, NULL}, }; static struct PyModuleDef PyOtherSideModule = { PyModuleDef_HEAD_INIT, "pyotherside", /* name of module */ NULL, -1, PyOtherSideMethods, }; PyMODINIT_FUNC PyOtherSide_init() { PyObject *pyotherside = PyModule_Create(&PyOtherSideModule); // Format constants for the image provider return value format // see http://qt-project.org/doc/qt-5.1/qtgui/qimage.html#Format-enum PyModule_AddIntConstant(pyotherside, "format_mono", QImage::Format_Mono); PyModule_AddIntConstant(pyotherside, "format_mono_lsb", QImage::Format_MonoLSB); PyModule_AddIntConstant(pyotherside, "format_rgb32", QImage::Format_RGB32); PyModule_AddIntConstant(pyotherside, "format_argb32", QImage::Format_ARGB32); PyModule_AddIntConstant(pyotherside, "format_rgb16", QImage::Format_RGB16); PyModule_AddIntConstant(pyotherside, "format_rgb666", QImage::Format_RGB666); PyModule_AddIntConstant(pyotherside, "format_rgb555", QImage::Format_RGB555); PyModule_AddIntConstant(pyotherside, "format_rgb888", QImage::Format_RGB888); PyModule_AddIntConstant(pyotherside, "format_rgb444", QImage::Format_RGB444); // Custom constant - pixels are to be interpreted as encoded image file data PyModule_AddIntConstant(pyotherside, "format_data", PYOTHERSIDE_IMAGE_FORMAT_ENCODED); PyModule_AddIntConstant(pyotherside, "format_svg_data", PYOTHERSIDE_IMAGE_FORMAT_SVG); // Version of PyOtherSide (new in 1.3) PyModule_AddStringConstant(pyotherside, "version", PYOTHERSIDE_VERSION); // QObject wrappers (new in 1.4) pyotherside_QObjectType.tp_new = PyType_GenericNew; pyotherside_QObjectType.tp_repr = pyotherside_QObject_repr; pyotherside_QObjectType.tp_getattro = pyotherside_QObject_getattro; pyotherside_QObjectType.tp_setattro = pyotherside_QObject_setattro; pyotherside_QObjectType.tp_dealloc = (destructor)pyotherside_QObject_dealloc; if (PyType_Ready(&pyotherside_QObjectType) < 0) { qFatal("Could not initialize QObjectType"); // Not reached return NULL; } Py_INCREF(&pyotherside_QObjectType); PyModule_AddObject(pyotherside, "QObject", (PyObject *)(&pyotherside_QObjectType)); pyotherside_QObjectMethodType.tp_new = PyType_GenericNew; pyotherside_QObjectMethodType.tp_repr = pyotherside_QObjectMethod_repr; pyotherside_QObjectMethodType.tp_call = pyotherside_QObjectMethod_call; pyotherside_QObjectMethodType.tp_dealloc = (destructor)pyotherside_QObjectMethod_dealloc; if (PyType_Ready(&pyotherside_QObjectMethodType) < 0) { qFatal("Could not initialize QObjectMethodType"); // Not reached return NULL; } Py_INCREF(&pyotherside_QObjectMethodType); PyModule_AddObject(pyotherside, "QObjectMethod", (PyObject *)(&pyotherside_QObjectMethodType)); return pyotherside; } QPythonPriv::QPythonPriv() : locals() , globals() , atexit_callback() , image_provider() , traceback_mod() , pyotherside_mod() , thread_state(NULL) { PyImport_AppendInittab("pyotherside", PyOtherSide_init); Py_InitializeEx(0); // Initialize sys.argv (https://github.com/thp/pyotherside/issues/77) int argc = 1; wchar_t **argv = (wchar_t **)malloc(argc * sizeof(wchar_t *)); argv[0] = Py_DecodeLocale("", nullptr); PySys_SetArgvEx(argc, argv, 0); PyMem_RawFree((void *)argv[0]); free(argv); locals = PyObjectRef(PyDict_New(), true); assert(locals); globals = PyObjectRef(PyDict_New(), true); assert(globals); traceback_mod = PyObjectRef(PyImport_ImportModule("traceback"), true); assert(traceback_mod); priv = this; if (PyDict_GetItemString(globals.borrow(), "__builtins__") == NULL) { PyDict_SetItemString(globals.borrow(), "__builtins__", PyEval_GetBuiltins()); } // Need to "self-import" the pyotherside module here, so that Python code // can use objects wrapped with pyotherside.QObject without crashing when // the user's Python code doesn't "import pyotherside" pyotherside_mod = PyObjectRef(PyImport_ImportModule("pyotherside"), true); assert(pyotherside_mod); // Release the GIL thread_state = PyEval_SaveThread(); } QPythonPriv::~QPythonPriv() { // Re-acquire the previously-released GIL PyEval_RestoreThread(thread_state); Py_Finalize(); } void QPythonPriv::receiveObject(PyObject *o) { emit receive(convertPyObjectToQVariant(o)); } QString QPythonPriv::formatExc() { PyObject *type = NULL; PyObject *value = NULL; PyObject *traceback = NULL; PyObject *list = NULL; PyObject *n = NULL; PyObject *s = NULL; PyErr_Fetch(&type, &value, &traceback); PyErr_NormalizeException(&type, &value, &traceback); QString message; QVariant v; if (type == NULL && value == NULL && traceback == NULL) { // No exception thrown? goto cleanup; } if (value != NULL) { // We can at least format the exception as string message = convertPyObjectToQVariant(PyObject_Str(value)).toString(); } if (type == NULL || traceback == NULL) { // Cannot get a traceback for this exception goto cleanup; } list = PyObject_CallMethod(traceback_mod.borrow(), "format_exception", "OOO", type, value, traceback); if (list == NULL) { // Could not format exception, fall back to original message PyErr_Print(); goto cleanup; } n = PyUnicode_FromString("\n"); if (n == NULL) { PyErr_Print(); goto cleanup; } s = PyUnicode_Join(n, list); if (s == NULL) { PyErr_Print(); goto cleanup; } v = convertPyObjectToQVariant(s); if (v.isValid()) { message = v.toString(); } cleanup: Py_XDECREF(s); Py_XDECREF(n); Py_XDECREF(list); Py_XDECREF(type); Py_XDECREF(value); Py_XDECREF(traceback); qDebug() << QString("PyOtherSide error: %1").arg(message); return message; } PyObject * QPythonPriv::eval(QString expr) { QByteArray utf8bytes = expr.toUtf8(); PyObject *result = PyRun_String(utf8bytes.constData(), Py_eval_input, globals.borrow(), locals.borrow()); return result; } void QPythonPriv::closing() { if (!priv) { return; } ENSURE_GIL_STATE; if (priv->atexit_callback) { PyObjectRef args(PyTuple_New(0), true); PyObjectRef result(PyObject_Call(priv->atexit_callback.borrow(), args.borrow(), NULL), true); Q_UNUSED(result); } priv->atexit_callback = PyObjectRef(); priv->image_provider = PyObjectRef(); } QPythonPriv * QPythonPriv::instance() { return priv; } QString QPythonPriv::importFromQRC(const char *module, const QString &filename) { PyObjectRef sys_modules(PySys_GetObject((char *)"modules")); if (!PyMapping_Check(sys_modules.borrow())) { return QString("sys.modules is not a mapping object"); } PyObjectRef qrc_importer(PyMapping_GetItemString(sys_modules.borrow(), (char *)module), true); if (!qrc_importer) { PyErr_Clear(); QFile qrc_importer_code(":" + filename); if (!qrc_importer_code.open(QIODevice::ReadOnly)) { return QString("Cannot load qrc importer source"); } QByteArray ba = qrc_importer_code.readAll(); QByteArray fn = QString("qrc:/" + filename).toUtf8(); PyObjectRef co(Py_CompileString(ba.constData(), fn.constData(), Py_file_input), true); if (!co) { QString result = QString("Cannot compile qrc importer: %1") .arg(formatExc()); PyErr_Clear(); return result; } qrc_importer = PyObjectRef(PyImport_ExecCodeModule((char *)module, co.borrow()), true); if (!qrc_importer) { QString result = QString("Cannot exec qrc importer: %1") .arg(formatExc()); PyErr_Clear(); return result; } } return QString(); } QString QPythonPriv::call(PyObject *callable, QString name, QVariant args, QVariant *v) { if (!PyCallable_Check(callable)) { return QString("Not a callable: %1").arg(name); } PyObjectRef argl(convertQVariantToPyObject(args), true); if (!PyList_Check(argl.borrow())) { return QString("Not a parameter list in call to %1: %2") .arg(name).arg(args.toString()); } PyObjectRef argt(PyList_AsTuple(argl.borrow()), true); PyObjectRef o(PyObject_Call(callable, argt.borrow(), NULL), true); if (!o) { return QString("Return value of PyObject call is NULL: %1").arg(priv->formatExc()); } else { if (v != NULL) { *v = convertPyObjectToQVariant(o.borrow()); } } return QString(); } pyotherside-1.6.2/src/qpython_priv.h000066400000000000000000000036671475412515400176050ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2011, 2013-2025, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #ifndef PYOTHERSIDE_QPYTHON_PRIV_H #define PYOTHERSIDE_QPYTHON_PRIV_H #include "python_wrap.h" #include "pyobject_ref.h" #include "pyqobject.h" #include #include #include enum PyOtherSideImageFormat { PYOTHERSIDE_IMAGE_FORMAT_ENCODED = -1, PYOTHERSIDE_IMAGE_FORMAT_SVG = -2, }; class QPythonPriv : public QObject { Q_OBJECT public: QPythonPriv(); ~QPythonPriv(); PyObject *eval(QString expr); QString importFromQRC(const char *module, const QString &filename); QString call(PyObject *callable, QString name, QVariant args, QVariant *v); void receiveObject(PyObject *o); static void closing(); static QPythonPriv *instance(); QString formatExc(); PyObjectRef locals; PyObjectRef globals; PyObjectRef atexit_callback; PyObjectRef image_provider; PyObjectRef traceback_mod; PyObjectRef pyotherside_mod; PyThreadState *thread_state; signals: void receive(QVariant data); }; #endif /* PYOTHERSIDE_QPYTHON_PRIV_H */ pyotherside-1.6.2/src/qpython_worker.cpp000066400000000000000000000032641475412515400204620ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2011, 2013-2025, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #include "qpython.h" #include "qpython_worker.h" QPythonWorker::QPythonWorker(QPython *qpython) : QObject() , qpython(qpython) { } QPythonWorker::~QPythonWorker() { } void QPythonWorker::process(QVariant func, QVariant unboxed_args, QJSValue *callback) { QVariant result = qpython->call_internal(func, unboxed_args, false); if (callback) { emit finished(result, callback); } } void QPythonWorker::import(QString name, QJSValue *callback) { bool result = qpython->importModule_sync(name); if (callback) { emit imported(result, callback); } } void QPythonWorker::import_names(QString name, QVariant args, QJSValue *callback) { bool result = qpython->importNames_sync(name, args); if (callback) { emit imported(result, callback); // using the same imported signal at the end } } pyotherside-1.6.2/src/qpython_worker.h000066400000000000000000000031151475412515400201220ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2011, 2013-2025, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #ifndef PYOTHERSIDE_QPYTHON_WORKER_H #define PYOTHERSIDE_QPYTHON_WORKER_H #include #include #include #include class QPython; class QPythonWorker : public QObject { Q_OBJECT public: QPythonWorker(QPython *qpython); ~QPythonWorker(); public slots: void process(QVariant func, QVariant unboxed_args, QJSValue *callback); void import(QString func, QJSValue *callback); void import_names(QString func, QVariant args, QJSValue *callback); signals: void finished(QVariant result, QJSValue *callback); void imported(bool result, QJSValue *callback); private: QPython *qpython; }; #endif /* PYOTHERSIDE_QPYTHON_WORKER_H */ pyotherside-1.6.2/src/qrc_importer.py000066400000000000000000000037451475412515400177470ustar00rootroot00000000000000# # PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 # Copyright (c) 2014, Thomas Perl # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND # FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR # OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # PERFORMANCE OF THIS SOFTWARE. # import sys from importlib import abc from importlib.util import spec_from_loader import pyotherside def get_filename(fullname): basename = fullname.replace(".", "/") for import_path in sys.path: if not import_path.startswith("qrc:"): continue for candidate in ("{}/{}.py", "{}/{}/__init__.py"): filename = candidate.format(import_path, basename) if pyotherside.qrc_is_file(filename[len("qrc:"):]): return filename class PyOtherSideQtRCLoader(abc.SourceLoader): def __init__(self, filepath): self.filepath = filepath def get_data(self, path): return pyotherside.qrc_get_file_contents(self.filepath[len("qrc:"):]) def get_filename(self, fullname): return get_filename(fullname) class PyOtherSideQtRCImporter(abc.MetaPathFinder): def find_spec(self, fullname, path, target=None): if path is None or all(x.startswith('qrc:') for x in path): fname = get_filename(fullname) if fname: return spec_from_loader(fullname, PyOtherSideQtRCLoader(fname)) return None sys.meta_path.append(PyOtherSideQtRCImporter()) pyotherside-1.6.2/src/qrc_importer.qrc000066400000000000000000000002061475412515400200710ustar00rootroot00000000000000 qrc_importer.py pyotherside-1.6.2/src/qvariant_converter.h000066400000000000000000000204051475412515400207440ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2011, 2013-2025, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #ifndef PYOTHERSIDE_QVARIANT_CONVERTER_H #define PYOTHERSIDE_QVARIANT_CONVERTER_H #include "converter.h" #include #include #include #include #include #include #include #include class QVariantListBuilder : public ListBuilder { public: QVariantListBuilder() : list() {} virtual ~QVariantListBuilder() {} virtual void append(QVariant v) { list << v; } virtual QVariant value() { return QVariant(list); } private: QVariantList list; }; class QVariantDictBuilder : public DictBuilder { public: QVariantDictBuilder() : dict() {} virtual ~QVariantDictBuilder() {} virtual void set(QVariant key, QVariant value) { dict[key.toString()] = value; } virtual QVariant value() { return QVariant(dict); } private: QMap dict; }; class QVariantListIterator : public ListIterator { public: QVariantListIterator(const QVariant &v) : list(v.toList()), pos(0) {} virtual ~QVariantListIterator() {} virtual bool next(QVariant *v) { if (pos == list.size()) { return false; } *v = list[pos]; pos++; return true; } private: QList list; int pos; }; class QVariantDictIterator : public DictIterator { public: QVariantDictIterator(const QVariant &v) : dict(v.toMap()), keys(dict.keys()), pos(0) {} virtual ~QVariantDictIterator() {} virtual bool next(QVariant *key, QVariant *value) { if (pos == keys.size()) { return false; } *key = keys[pos]; *value = dict[keys[pos]]; pos++; return true; } private: QMap dict; QList keys; int pos; }; class QVariantConverter : public Converter { public: QVariantConverter() : stringstorage() {} virtual ~QVariantConverter() {} virtual enum Type type(const QVariant &v) { if (v.canConvert()) { return QOBJECT; } QMetaType::Type t = (QMetaType::Type)v.type(); switch (t) { case QMetaType::Bool: return BOOLEAN; case QMetaType::Int: case QMetaType::LongLong: case QMetaType::UInt: case QMetaType::ULongLong: return INTEGER; case QMetaType::Double: return FLOATING; case QMetaType::QString: return STRING; case QMetaType::QByteArray: return BYTES; case QMetaType::QDate: return DATE; case QMetaType::QTime: return TIME; case QMetaType::QDateTime: return DATETIME; case QMetaType::QVariantList: case QMetaType::QStringList: return LIST; case QMetaType::QVariantMap: case QMetaType::QVariantHash: return DICT; case QMetaType::UnknownType: return NONE; default: int userType = v.userType(); if (userType == qMetaTypeId()) { return PYOBJECT; } else if (userType == qMetaTypeId()) { Q_ASSERT(QThread::currentThread() == qApp->thread()); return type(QVariant()); } else { qDebug() << "Cannot convert:" << v; return NONE; } } } virtual ListIterator *list(QVariant &v) { // XXX: Until we support boxing QJSValue objects directly in Python if (v.userType() == qMetaTypeId()) { return new QVariantListIterator(v.value().toVariant()); } return new QVariantListIterator(v); } virtual DictIterator *dict(QVariant &v) { // XXX: Until we support boxing QJSValue objects directly in Python if (v.userType() == qMetaTypeId()) { return new QVariantDictIterator(v.value().toVariant()); } return new QVariantDictIterator(v); } virtual long long integer(QVariant &v) { return v.toLongLong(); } virtual double floating(QVariant &v) { return v.toDouble(); } virtual bool boolean(QVariant &v) { return v.toBool(); } virtual ConverterDate date(QVariant &v) { QDate d = v.toDate(); return ConverterDate(d.year(), d.month(), d.day()); } virtual ConverterTime time(QVariant &v) { QTime t = v.toTime(); return ConverterTime(t.hour(), t.minute(), t.second(), t.msec()); } virtual ConverterDateTime dateTime(QVariant &v) { QDateTime dt = v.toDateTime(); QDate d = dt.date(); QTime t = dt.time(); return ConverterDateTime(d.year(), d.month(), d.day(), t.hour(), t.minute(), t.second(), t.msec()); } virtual const char *string(QVariant &v) { stringstorage = v.toString().toUtf8(); return stringstorage.constData(); } virtual QByteArray bytes(QVariant &v) { return stringstorage = v.toByteArray(); } virtual PyObjectRef pyObject(QVariant &v) { return v.value(); } virtual QObjectRef qObject(QVariant &v) { return QObjectRef(v.value()); } virtual ListBuilder *newList() { return new QVariantListBuilder; } virtual DictBuilder *newDict() { return new QVariantDictBuilder; } virtual QVariant fromInteger(long long v) { return QVariant(v); } virtual QVariant fromFloating(double v) { return QVariant(v); } virtual QVariant fromBoolean(bool v) { return QVariant(v); } virtual QVariant fromString(const char *v) { return QVariant(QString::fromUtf8(v)); } virtual QVariant fromBytes(const QByteArray &v) { return QVariant(v); } virtual QVariant fromDate(ConverterDate v) { return QVariant(QDate(v.y, v.m, v.d)); } virtual QVariant fromTime(ConverterTime v) { return QVariant(QTime(v.h, v.m, v.s, v.ms)); } virtual QVariant fromDateTime(ConverterDateTime v) { QDate d(v.y, v.m, v.d); QTime t(v.time.h, v.time.m, v.time.s, v.time.ms); return QVariant(QDateTime(d, t)); } virtual QVariant fromPyObject(const PyObjectRef &pyobj) { return QVariant::fromValue(pyobj); } virtual QVariant fromQObject(const QObjectRef &qobj) { return QVariant::fromValue(qobj.value()); } virtual QVariant none() { return QVariant(); }; private: QByteArray stringstorage; }; #endif /* PYOTHERSIDE_QVARIANT_CONVERTER_H */ pyotherside-1.6.2/src/src.pro000066400000000000000000000036161475412515400161750ustar00rootroot00000000000000TARGET = pyothersideplugin include(../pyotherside.pri) DEFINES += PYOTHERSIDE_VERSION=\\\"$${VERSION}\\\" PLUGIN_IMPORT_PATH = io/thp/pyotherside TEMPLATE = lib CONFIG += qt plugin QT += qml quick svg opengl target.path = $$[QT_INSTALL_QML]/$$PLUGIN_IMPORT_PATH INSTALLS += target qmldir.files += qmldir pyotherside.qmltypes qmldir.path += $$target.path INSTALLS += qmldir qmltypes.commands = qmlplugindump -nonrelocatable io.thp.pyotherside 1.5 > $$PWD/pyotherside.qmltypes QMAKE_EXTRA_TARGETS += qmltypes DEPENDPATH += . INCLUDEPATH += . # PyOtherSide QML Plugin SOURCES += pyotherside_plugin.cpp HEADERS += pyotherside_plugin.h # QML Image Provider SOURCES += qpython_imageprovider.cpp HEADERS += qpython_imageprovider.h # PyGLArea SOURCES += pyglarea.cpp pyglrenderer.cpp HEADERS += pyglarea.h pyglrenderer.h # PyFBO SOURCES += pyfbo.cpp HEADERS += pyfbo.h # Importer from Qt Resources RESOURCES += qrc_importer.qrc # Embedded Python Library (add pythonlib.zip if you want this) exists (pythonlib.zip) { RESOURCES += pythonlib_loader.qrc DEFINES *= PYTHONLIB_LOADER_HAVE_PYTHONLIB_ZIP } !windows { DEFINES *= HAVE_DLADDR } HEADERS += pythonlib_loader.h SOURCES += pythonlib_loader.cpp # Python QML Object SOURCES += qpython.cpp HEADERS += qpython.h SOURCES += qpython_worker.cpp HEADERS += qpython_worker.h SOURCES += qpython_priv.cpp HEADERS += qpython_priv.h # Globally Load Python hack SOURCES += global_libpython_loader.cpp HEADERS += global_libpython_loader.h # Reference-counting PyObject wrapper class SOURCES += pyobject_ref.cpp HEADERS += pyobject_ref.h # QObject wrapper class exposed to Python SOURCES += qobject_ref.cpp HEADERS += qobject_ref.h HEADERS += pyqobject.h # GIL helper HEADERS += ensure_gil_state.h # Type System Conversion Logic HEADERS += converter.h HEADERS += qvariant_converter.h HEADERS += pyobject_converter.h HEADERS += qml_python_bridge.h include(../python.pri) pyotherside-1.6.2/tests/000077500000000000000000000000001475412515400152315ustar00rootroot00000000000000pyotherside-1.6.2/tests/test_added_in_14/000077500000000000000000000000001475412515400203235ustar00rootroot00000000000000pyotherside-1.6.2/tests/test_added_in_14/test_added_in_14.qml000066400000000000000000000013121475412515400241250ustar00rootroot00000000000000import QtQuick 2.0 // Note that we import 1.3 here instead of >= 1.4 import io.thp.pyotherside 1.3 Python { Component.onCompleted: { var foo = undefined; // qml: Received error: Import PyOtherSide 1.4 or newer to use getattr() getattr(foo, 'test'); var func = eval('[]'); // qml: Received error: Function not found: '' (unexpected EOF while parsing (, line 0)) call(func, [], function (result) {}); // qml: Received error: Function not found: '' (unexpected EOF while parsing (, line 0)) var result = call_sync(func, []); Qt.quit(); } onError: { console.log('Received error: ' + traceback); } } pyotherside-1.6.2/tests/test_call_signal/000077500000000000000000000000001475412515400205405ustar00rootroot00000000000000pyotherside-1.6.2/tests/test_call_signal/TestModule.py000066400000000000000000000002371475412515400232010ustar00rootroot00000000000000import pyotherside def makeCalls(obj): print(f'result of callFunction: {obj.callFunction()}') print(f'result of callSignal: {obj.callSignal()}') pyotherside-1.6.2/tests/test_call_signal/test.qml000066400000000000000000000011261475412515400222320ustar00rootroot00000000000000import QtQuick 2.9 import io.thp.pyotherside 1.5 Item { id: obj signal callSignal() function callFunction() { print('Function Called') return 'hoho' } function signalConnection(){ print('Signal Called') } Component.onCompleted: { obj.callSignal.connect(signalConnection) } Python { Component.onCompleted: { addImportPath(Qt.resolvedUrl('.')); importModule('TestModule', function(){ call('TestModule.makeCalls', [obj]) }) } } } pyotherside-1.6.2/tests/test_callback_errors/000077500000000000000000000000001475412515400214205ustar00rootroot00000000000000pyotherside-1.6.2/tests/test_callback_errors/test_callback_errors.qml000066400000000000000000000021171475412515400263230ustar00rootroot00000000000000import QtQuick 2.0 import io.thp.pyotherside 1.2 // Test if PyOtherSide correctly reports JS errors happening in callbacks // in signal error(string traceback) for both imports and function calls. Python { property var tests: ([]) function test_next() { if (tests.length) { tests.pop()(); } else { console.log('Tests done'); Qt.quit(); } } Component.onCompleted: { tests.unshift(function () { console.log('Expecting ReferenceError for "invalid" on import'); importModule('os', function (success) { invalid; }); }); tests.unshift(function() { console.log('Expecting TypeError for "lock" property'); call('os.getcwd', [], function (result) { console.lock(result); }); }); test_next(); } onError: { // Remove full path to .qml file var msg = traceback.replace(Qt.resolvedUrl('.'), ''); console.log('Got error: ' + msg); test_next(); } } pyotherside-1.6.2/tests/test_callback_errors/test_callback_errors.txt000066400000000000000000000004421475412515400263500ustar00rootroot00000000000000Expecting ReferenceError for "invalid" on import Got error: test_callback_errors.qml:23: ReferenceError: invalid is not defined Expecting TypeError for "lock" property Got error: test_callback_errors.qml:29: TypeError: Property 'lock' of object [object Object] is not a function Tests done pyotherside-1.6.2/tests/test_callparam/000077500000000000000000000000001475412515400202245ustar00rootroot00000000000000pyotherside-1.6.2/tests/test_callparam/test_callparam.qml000066400000000000000000000012661475412515400237370ustar00rootroot00000000000000import QtQuick 2.0 import io.thp.pyotherside 1.0 Rectangle { id: page width: 300 height: 300 Python { Component.onCompleted: { // We use call_sync() instead of call() in order // to guarantee the right ordering of Python's output // and the output of the JS engine for the error // This should fail with an error: // "Not a parameter list in call to print: 123" call_sync('print', 123); // This should work and print "123" on the console call_sync('print', [123]); } onError: { console.log('Received error: ' + traceback); } } } pyotherside-1.6.2/tests/test_datetime/000077500000000000000000000000001475412515400200645ustar00rootroot00000000000000pyotherside-1.6.2/tests/test_datetime/test_datetime.py000066400000000000000000000012371475412515400232740ustar00rootroot00000000000000import datetime def submit_datetime(dt): assert dt is not None print('Python got datetime:', dt) return dt def submit_date(date): assert date is not None print('Python got date:', date) return date def submit_time(time): assert time is not None print('Python got time:', time) return time def get_datetime_value(): v = datetime.datetime.now() print('Python returning datetime:', v) return v def get_time_value(): v = datetime.datetime.now().time() print('Python returning time:', v) return v def get_date_value(): v = datetime.datetime.now().date() print('Python returning date:', v) return v pyotherside-1.6.2/tests/test_datetime/test_datetime.qml000066400000000000000000000063331475412515400234370ustar00rootroot00000000000000import QtQuick 2.0 import io.thp.pyotherside 1.2 Python { property var tests: ([]) property var someJsDate: (new Date()) property date someDate: '2014-02-12' // XXX: Not possible, see https://qt-project.org/forums/viewthread/8935 // property time someTime: '11:22:33' Component.onCompleted: { addImportPath(Qt.resolvedUrl('.')); importModule('test_datetime', function () { var someTime = call_sync('test_datetime.get_time_value', []); // Note: Qt doesn't support microseconds, so these are trimmed // to milliseconds during conversion between data types console.log('Got time value from Python: ' + someTime); var someDateFromPython = call_sync('test_datetime.get_date_value', []); console.log('Got date value from Python: ' + someDateFromPython); var someDateTimeFromPython = call_sync('test_datetime.get_datetime_value', []); console.log('Got datetime value from Python: ' + someDateTimeFromPython); function test_next() { console.log('================================'); if (tests.length == 0) { console.log('Tests completed'); Qt.quit(); } else { var test = tests.pop(); console.log('-> ' + test.name); call(test.func, [test.expected], function (reply) { if (reply === undefined || reply === null) { error('Got undefined or null'); return; } console.log('Got: ' + reply); console.log('Expected: ' + test.expected); if (reply.toString() !== test.expected.toString()) { error('Results do not match'); return; } test_next(); }); } } tests.unshift({ name: 'Submit back and forth date (QML date property)', func: 'test_datetime.submit_date', expected: someDate }); tests.unshift({ name: 'Submit back and forth time (QML time from Python)', func: 'test_datetime.submit_time', expected: someTime }); tests.unshift({ name: 'Submit back and forth datetime (JS "new Date()")', func: 'test_datetime.submit_datetime', expected: someJsDate }); tests.unshift({ name: 'Submit back and forth date (JS var from Python)', func: 'test_datetime.submit_date', expected: someDateFromPython }); tests.unshift({ name: 'Submit back and forth datetime (JS var from Python)', func: 'test_datetime.submit_datetime', expected: someDateTimeFromPython }); test_next(); }); } onError: { console.log('Error: ' + traceback); console.log('Tests failed'); Qt.quit(); } } pyotherside-1.6.2/tests/test_errors/000077500000000000000000000000001475412515400176045ustar00rootroot00000000000000pyotherside-1.6.2/tests/test_errors/README000066400000000000000000000003641475412515400204670ustar00rootroot00000000000000Test if JS errors from pyotherside.send() are propagated to the PyOtherSide error handler. Thanks to Osmo Salomaa for the original report and test case. ---- https://github.com/thp/pyotherside/issues/9 https://gist.github.com/otsaloma/8258322 pyotherside-1.6.2/tests/test_errors/test_errors.py000066400000000000000000000003001475412515400225220ustar00rootroot00000000000000import pyotherside import threading import time def run(): while True: pyotherside.send("test-errors") time.sleep(3) thread = threading.Thread(target=run) thread.start() pyotherside-1.6.2/tests/test_errors/test_errors.qml000066400000000000000000000015341475412515400226750ustar00rootroot00000000000000import QtQuick 2.0 import io.thp.pyotherside 1.0 Rectangle { id: page width: 300 height: 300 Component.onCompleted: { py.addImportPath(Qt.resolvedUrl('.').substr('file://'.length)); py.setHandler("test-errors", page.testErrors); py.importModule("test_errors", null); } Python { id: py onError: { console.log("PYTHON ERROR: " + traceback); msg.text += '\n' + traceback; } } Text { id: msg anchors { top: parent.top left: parent.left right: parent.right } text: "Testing (you should see errors appearing here)..." wrapMode: Text.Wrap } function testErrors() { console.log("starting"); page.nonexistentMethod(); console.log("finished"); } } pyotherside-1.6.2/tests/test_issue23/000077500000000000000000000000001475412515400175655ustar00rootroot00000000000000pyotherside-1.6.2/tests/test_issue23/issue23.qml000066400000000000000000000012421475412515400215740ustar00rootroot00000000000000/** * Issue #23: importModule runs forever * https://github.com/thp/pyotherside/issues/23 **/ import QtQuick 2.0 import io.thp.pyotherside 1.3 Rectangle { width: 300 height: 300 Text { id: text anchors.centerIn: parent } Python { Component.onCompleted: { importModule('gi.repository.Gio', function() { console.log('import completed'); call('gi.repository.Gio.Settings.new("org.gnome.Vino").keys', [], function(result) { text.text = result.join('\n'); }); }); } onError: console.log('Error: ' + traceback); } } pyotherside-1.6.2/tests/test_iterable/000077500000000000000000000000001475412515400200575ustar00rootroot00000000000000pyotherside-1.6.2/tests/test_iterable/test_iterable.py000066400000000000000000000003021475412515400232520ustar00rootroot00000000000000def get_set(): return set((1, 2, 3)) def get_iterable_generator_expression(): return (x * 2 for x in range(4)) def get_iterable_generator(): for i in range(5): yield i * 3 pyotherside-1.6.2/tests/test_iterable/test_iterable.qml000066400000000000000000000042071475412515400234230ustar00rootroot00000000000000import QtQuick 2.0 import io.thp.pyotherside 1.3 Python { property var tests: ([]) Component.onCompleted: { addImportPath(Qt.resolvedUrl('.')); importModule('test_iterable', function () { function test_next() { console.log('================================'); if (tests.length == 0) { console.log('Tests completed'); Qt.quit(); } else { var test = tests.pop(); console.log('-> ' + test.name); call(test.func, [], function (reply) { if (reply === undefined || reply === null) { error('Got undefined or null'); return; } // Sort, because a Python set is unordered (to make expected work below) reply.sort(function (a, b) { return a - b; }); console.log('Got: ' + reply); console.log('Expected: ' + test.expected); if (reply.toString() !== test.expected.toString()) { error('Results do not match'); return; } test_next(); }); } } tests.unshift({ name: 'Getting set returns JS array', func: 'test_iterable.get_set', expected: [1, 2, 3] }); tests.unshift({ name: 'Getting generator expression returns JS array', func: 'test_iterable.get_iterable_generator_expression', expected: [0, 2, 4, 6] }); tests.unshift({ name: 'Getting generator returns JS array', func: 'test_iterable.get_iterable_generator', expected: [0, 3, 6, 9, 12] }); test_next(); }); } onError: { console.log('Error: ' + traceback); console.log('Tests failed'); Qt.quit(); } } pyotherside-1.6.2/tests/test_nested_import/000077500000000000000000000000001475412515400211445ustar00rootroot00000000000000pyotherside-1.6.2/tests/test_nested_import/README000066400000000000000000000002701475412515400220230ustar00rootroot00000000000000Test for importModule() with dots in the module name. Tests the (broken) behavior of QML API import 1.0, and the fixed 1.2 behavior. See: https://github.com/thp/pyotherside/issues/3 pyotherside-1.6.2/tests/test_nested_import/example_api10.qml000066400000000000000000000046531475412515400243140ustar00rootroot00000000000000import QtQuick 2.0 import io.thp.pyotherside 1.0 Python { Component.onCompleted: { addImportPath(Qt.resolvedUrl('.')); /** * Here, we test the broken behavior of the PyOtherSide 1.0 API * for imports with "." in the name: * 1. It uses PyImport_ImportModule() which does a "*"_import * 2. The variable in the globals dict that gets set is the full * module name (and not the module after the "." for * non-"*"-imports) including the dot, which is broken, anyway, as * there's not way to retrieve that name in normal Python syntax * (names cannot contain a "."), so for this test we use a dirty * way of accessing they key via the globals() dict (just for * testing - I hope nobody used that in old code, but we want to * have a stable API, so we will drag this behavior along with the * 1.0 API support - new code should definitely use the 1.2 API) **/ importModule('thp_io.pyotherside.nested', function () { console.log('"nested" imported successfully'); // In API version 1.0, we expect the import to have done a "*" // import, and to add insult to injury, we assign the module // name with a ".", which basically makes the import unaccessible // from normal Python code (the entry in the globals dict contains // a ".", which isn't a valid name in Python), so we access the // globals dictionary directly console.log('repr of the module: ' + evaluate('repr(globals()["thp_io.pyotherside.nested"])')); call('globals()["thp_io.pyotherside.nested"].info', [], function (result) { console.log('from nested.info(): ' + result); }); importModule('thp_io.pyotherside.nested.module', function () { console.log('"nested.module" imported successfully'); // Globals hack - see above call('globals()["thp_io.pyotherside.nested.module"].info', [], function (result) { console.log('from nested.module.info(): ' + result); // Globals hack again - see above console.log('nested.module.value: ' + evaluate('globals()["thp_io.pyotherside.nested.module"].value')); Qt.quit(); }); }); }); } } pyotherside-1.6.2/tests/test_nested_import/example_api12.qml000066400000000000000000000017551475412515400243160ustar00rootroot00000000000000import QtQuick 2.0 import io.thp.pyotherside 1.2 Python { Component.onCompleted: { addImportPath(Qt.resolvedUrl('.')); importModule('thp_io.pyotherside.nested', function () { console.log('"nested" imported successfully'); console.log('repr of the module: ' + evaluate('repr(thp_io.pyotherside.nested)')); call('thp_io.pyotherside.nested.info', [], function (result) { console.log('from nested.info(): ' + result); }); importModule('thp_io.pyotherside.nested.module', function () { console.log('"nested.module" imported successfully'); call('thp_io.pyotherside.nested.module.info', [], function (result) { console.log('from nested.module.info(): ' + result); console.log('nested.module.value: ' + evaluate('thp_io.pyotherside.nested.module.value')); Qt.quit(); }); }); }); } } pyotherside-1.6.2/tests/test_nested_import/thp_io/000077500000000000000000000000001475412515400224265ustar00rootroot00000000000000pyotherside-1.6.2/tests/test_nested_import/thp_io/__init__.py000066400000000000000000000000001475412515400245250ustar00rootroot00000000000000pyotherside-1.6.2/tests/test_nested_import/thp_io/pyotherside/000077500000000000000000000000001475412515400247655ustar00rootroot00000000000000pyotherside-1.6.2/tests/test_nested_import/thp_io/pyotherside/__init__.py000066400000000000000000000000001475412515400270640ustar00rootroot00000000000000pyotherside-1.6.2/tests/test_nested_import/thp_io/pyotherside/nested/000077500000000000000000000000001475412515400262475ustar00rootroot00000000000000pyotherside-1.6.2/tests/test_nested_import/thp_io/pyotherside/nested/__init__.py000066400000000000000000000000641475412515400303600ustar00rootroot00000000000000def info(): return 'This is the nested package' pyotherside-1.6.2/tests/test_nested_import/thp_io/pyotherside/nested/module.py000066400000000000000000000001071475412515400301040ustar00rootroot00000000000000def info(): return 'This is the nested.module module' value = 123 pyotherside-1.6.2/tests/test_qrc_crash/000077500000000000000000000000001475412515400202355ustar00rootroot00000000000000pyotherside-1.6.2/tests/test_qrc_crash/editor.py000066400000000000000000000003641475412515400221000ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import pyotherside class Crasher: def __init__(self): pass def __del__(self): print('Finalizing', self) def foo(arg): print('Got arg:', arg) return 'hello world' pyotherside-1.6.2/tests/test_qrc_crash/editor.qml000066400000000000000000000011011475412515400222270ustar00rootroot00000000000000import QtQuick 2.0 import io.thp.pyotherside 1.5 Rectangle { width: 300 height: 300 Python { id: python Component.onCompleted: { addImportPath(Qt.resolvedUrl('.')); importModule('editor', function () { call('editor.Crasher', [], function (result) { console.log('Got result: ' + result); call('editor.foo', [result], function (res2) { console.log('res2 = ' + res2); }); }); }); } } } pyotherside-1.6.2/tests/test_qrc_crash/editor.qrc000066400000000000000000000001711475412515400222310ustar00rootroot00000000000000 editor.qml editor.py pyotherside-1.6.2/tests/test_qrc_crash/main.cpp000066400000000000000000000003461475412515400216700ustar00rootroot00000000000000#include #include int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QQuickView view; view.setSource(QUrl("qrc:/editor.qml")); view.show(); return app.exec(); } pyotherside-1.6.2/tests/test_qrc_crash/test_qrc_crash.pro000066400000000000000000000001661475412515400237660ustar00rootroot00000000000000TEMPLATE = app TARGET = test_qrc_crash INCLUDEPATH += . QT += qml quick SOURCES += main.cpp RESOURCES += editor.qrc pyotherside-1.6.2/tests/test_sys_argv/000077500000000000000000000000001475412515400201255ustar00rootroot00000000000000pyotherside-1.6.2/tests/test_sys_argv/test_sys_argv.qml000066400000000000000000000006001475412515400235300ustar00rootroot00000000000000import QtQuick 2.0 import io.thp.pyotherside 1.5 Text { id: txt Python { Component.onCompleted: { importModule('sys', function() { var args = evaluate('sys.argv'); for (var i=0; i * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #include "pyobject_converter.h" #include "qvariant_converter.h" #include "qpython.h" #include "converter.h" #include "qml_python_bridge.h" #include "tests.h" // "Ensure that the current thread is ready to call the Python C API regardless // of the current state of Python, or of the global interpreter lock." // -- https://docs.python.org/3.9/c-api/init.html#c.PyGILState_Ensure struct GrabGIL { GrabGIL() : gil(PyGILState_Ensure()) { } ~GrabGIL() { PyGILState_Release(gil); } GrabGIL(const GrabGIL &) = delete; GrabGIL(GrabGIL &&) = delete; GrabGIL &operator=(const GrabGIL &) = delete; GrabGIL &operator=(GrabGIL &&) = delete; PyGILState_STATE gil; }; #define ENSURE_PYTHON_GIL_HELD GrabGIL gil TestPyOtherSide::TestPyOtherSide() : QObject() { } QTEST_MAIN(TestPyOtherSide) template void test_converter_for(Converter *conv) { V v, w, x; QVERIFY(Py_IsInitialized()); /* Convert from/to Integer */ v = conv->fromInteger(123); QVERIFY(conv->type(v) == Converter::INTEGER); QVERIFY(conv->integer(v) == 123); /* Convert from/to Float */ v = conv->fromFloating(42.23); QVERIFY(conv->type(v) == Converter::FLOATING); QVERIFY(conv->floating(v) == 42.23); /* Convert from/to Bool */ v = conv->fromBoolean(true); QVERIFY(conv->type(v) == Converter::BOOLEAN); QVERIFY(conv->boolean(v)); v = conv->fromBoolean(false); QVERIFY(conv->type(v) == Converter::BOOLEAN); QVERIFY(!conv->boolean(v)); /* Convert from/to String */ v = conv->fromString("Hello World"); QVERIFY(conv->type(v) == Converter::STRING); QVERIFY(strcmp(conv->string(v), "Hello World") == 0); /* Convert from/to Bytes */ static const char BUF[] = { 'a', 'b', '\0', 'c', 'd' }; v = conv->fromBytes(QByteArray(BUF, sizeof(BUF))); QVERIFY(conv->type(v) == Converter::BYTES); QByteArray res = conv->bytes(v); QVERIFY(res.size() == sizeof(BUF)); QVERIFY(memcmp(BUF, res.constData(), res.size()) == 0); /* Convert from/to List */ ListBuilder *builder = conv->newList(); v = conv->fromInteger(444); builder->append(v); v = conv->fromString("Hello"); builder->append(v); v = builder->value(); delete builder; ListIterator *iterator = conv->list(v); QVERIFY(iterator->next(&w)); QVERIFY(conv->type(w) == Converter::INTEGER); QVERIFY(conv->integer(w) == 444); QVERIFY(iterator->next(&w)); QVERIFY(conv->type(w) == Converter::STRING); QVERIFY(strcmp(conv->string(w), "Hello") == 0); delete iterator; /* Convert from/to Dict */ DictBuilder *builder2 = conv->newDict(); v = conv->fromBoolean(true); builder2->set(conv->fromString("a"), v); v = builder2->value(); delete builder2; DictIterator *iterator2 = conv->dict(v); QVERIFY(iterator2->next(&w, &x)); QVERIFY(conv->type(w) == Converter::STRING); QVERIFY(strcmp(conv->string(w), "a") == 0); QVERIFY(conv->type(x) == Converter::BOOLEAN); QVERIFY(conv->boolean(x) == true); delete iterator2; /* Convert from/to generic PyObject */ PyObject *obj = PyCapsule_New(conv, "test", NULL); v = conv->fromPyObject(PyObjectRef(obj)); QVERIFY(conv->type(v) == Converter::PYOBJECT); // Check if getting a new reference works PyObject *o = conv->pyObject(v).newRef(); QVERIFY(o == obj); Py_DECREF(o); Py_CLEAR(obj); delete conv; } void destruct(PyObject *obj) { bool *destructor_called = (bool *)PyCapsule_GetPointer(obj, "test"); *destructor_called = true; } void TestPyOtherSide::testPyObjectRefAssignment() { ENSURE_PYTHON_GIL_HELD; // Test assignment operator of PyObjectRef bool destructor_called_foo = false; PyObject *foo = PyCapsule_New(&destructor_called_foo, "test", destruct); bool destructor_called_bar = false; PyObject *bar = PyCapsule_New(&destructor_called_bar, "test", destruct); QVERIFY(foo); QVERIFY(foo->ob_refcnt == 1); QVERIFY(bar); QVERIFY(bar->ob_refcnt == 1); { PyObjectRef a(foo); PyObjectRef b(bar); PyObjectRef c; // empty // foo got a new reference in a QVERIFY(foo->ob_refcnt == 2); // bar got a new reference in b QVERIFY(bar->ob_refcnt == 2); // Overwrite empty reference with reference to bar c = b; // bar got a new reference in c QVERIFY(bar->ob_refcnt == 3); // reference count for foo is unchanged QVERIFY(foo->ob_refcnt == 2); // Overwrite reference to bar with reference to foo b = a; // bar lost a reference in b QVERIFY(bar->ob_refcnt == 2); // foo got a new reference in b QVERIFY(foo->ob_refcnt == 3); // Overwrite reference to foo with empty reference a = PyObjectRef(); // foo lost a reference in a QVERIFY(foo->ob_refcnt == 2); Py_DECREF(foo); // there is still a reference to foo in b QVERIFY(foo->ob_refcnt == 1); QVERIFY(!destructor_called_foo); // a falls out of scope (but is empty) // b falls out of scope, foo loses a reference // c falls out of scope, bar loses a reference } // Now that b fell out of scope, foo was destroyed QVERIFY(destructor_called_foo); // But we still have a single reference to bar QVERIFY(!destructor_called_bar); QVERIFY(bar->ob_refcnt == 1); Py_CLEAR(bar); // Now bar is also gone QVERIFY(destructor_called_bar); } void TestPyOtherSide::testPyObjectRefRoundTrip() { ENSURE_PYTHON_GIL_HELD; // Simulate a complete round-trip of a PyObject reference, from PyOtherSide // to QML and back. // Create a Python object, i.e. in a Python function. bool destructor_called = false; PyObject *o = PyCapsule_New(&destructor_called, "test", destruct); QVERIFY(o->ob_refcnt == 1); // Convert the object to a QVariant and increment its refcount. QVariant v = convertPyObjectToQVariant(o); // Decrement refcount and pass QVariant to QML. QVERIFY(o->ob_refcnt == 2); Py_DECREF(o); QVERIFY(o->ob_refcnt == 1); // Pass QVariant back to PyOtherSide, which converts it to a PyObject, // incrementing its refcount. PyObject *o2 = convertQVariantToPyObject(v); QVERIFY(o->ob_refcnt == 2); // The QVariant is deleted, i.e. by a JS variable falling out of scope. // This deletes the PyObjectRef and thus decrements the object's refcount. v = QVariant(); // At this point, we only have one reference (the one from o2) QVERIFY(o->ob_refcnt == 1); // There's still a reference, so the destructor must not have been called QVERIFY(!destructor_called); // Now, at this point, the last remaining reference is removed, which // will cause the destructor to be called Py_DECREF(o2); // There are no references left, so the capsule's destructor is called. QVERIFY(destructor_called); } void TestPyOtherSide::testQObjectRef() { ENSURE_PYTHON_GIL_HELD; QObject *o = new QObject(); QObjectRef ref(o); QVERIFY(ref.value() == o); delete o; QVERIFY(ref.value() == NULL); } void TestPyOtherSide::testQVariantConverter() { ENSURE_PYTHON_GIL_HELD; test_converter_for(new QVariantConverter); } void TestPyOtherSide::testPyObjectConverter() { ENSURE_PYTHON_GIL_HELD; test_converter_for(new PyObjectConverter); } void TestPyOtherSide::testConvertToPythonAndBack() { ENSURE_PYTHON_GIL_HELD; QVariantList l; l << "Hello" << 123 << 45.667 << true; QVariantList l2; l2 << "A" << QVariant() << "B" << 4711; l << QVariant(l2); QVariant v(l); PyObject *o = convertQVariantToPyObject(v); QVariant v2 = convertPyObjectToQVariant(o); QVERIFY(v == v2); } static void testEvaluateWith(QPython *py) { QVariant squares = py->evaluate("[x*x for x in range(10)]"); QVERIFY(squares.canConvert(QMetaType::QVariantList)); QVariantList squares_list = squares.toList(); QVERIFY(squares_list.size() == 10); QVERIFY(squares_list[0] == 0); QVERIFY(squares_list[1] == 1); QVERIFY(squares_list[2] == 4); QVERIFY(squares_list[3] == 9); QVERIFY(squares_list[4] == 16); QVERIFY(squares_list[5] == 25); QVERIFY(squares_list[6] == 36); QVERIFY(squares_list[7] == 49); QVERIFY(squares_list[8] == 64); QVERIFY(squares_list[9] == 81); } void TestPyOtherSide::testEvaluate() { // No need to grab GIL state here, as QPython objects create it // PyOtherSide API 1.0 QPython10 py10; testEvaluateWith(&py10); // PyOtherSide API 1.2 QPython12 py12; testEvaluateWith(&py12); // PyOtherSide API 1.3 QPython13 py13; testEvaluateWith(&py13); } void TestPyOtherSide::testSetToList() { ENSURE_PYTHON_GIL_HELD; // Test if a Python set is converted to a list PyObject *set = PySet_New(NULL); QVERIFY(set != NULL); PyObject *o = NULL; o = PyLong_FromLong(123); QVERIFY(o != NULL); QVERIFY(PySet_Add(set, o) == 0); o = PyLong_FromLong(321); QVERIFY(o != NULL); QVERIFY(PySet_Add(set, o) == 0); o = PyLong_FromLong(444); QVERIFY(o != NULL); QVERIFY(PySet_Add(set, o) == 0); // This will not be added (no duplicates in a set) o = PyLong_FromLong(123); QVERIFY(o != NULL); QVERIFY(PySet_Add(set, o) == 0); // At this point, we should have 3 items (123, 321 and 444) QVERIFY(PySet_Size(set) == 3); QVariant v = convertPyObjectToQVariant(set); QVERIFY(v.canConvert(QMetaType::QVariantList)); QList l = v.toList(); QVERIFY(l.size() == 3); QVERIFY(l.contains(123)); QVERIFY(l.contains(321)); QVERIFY(l.contains(444)); } void TestPyOtherSide::testIntMoreThan32Bits() { ENSURE_PYTHON_GIL_HELD; // See https://github.com/thp/pyotherside/issues/86 // Affected: Devices and OSes where long is 32 bits, but long long is 64 bits long long two_fortytwo = 4398046511104LL; PyObject *o = PyLong_FromLongLong(two_fortytwo); QVERIFY(o); QVariant v = convertPyObjectToQVariant(o); QVERIFY(v.toLongLong() == two_fortytwo); } pyotherside-1.6.2/tests/tests.h000066400000000000000000000026641475412515400165540ustar00rootroot00000000000000 /** * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 and Qt 6 * Copyright (c) 2011, 2013-2025, Thomas Perl * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. **/ #ifndef PYOTHERSIDE_TESTS_H #define PYOTHERSIDE_TESTS_H #include #include #include class TestPyOtherSide : public QObject { Q_OBJECT public: TestPyOtherSide(); private slots: void testEvaluate(); void testQVariantConverter(); void testPyObjectConverter(); void testPyObjectRefRoundTrip(); void testPyObjectRefAssignment(); void testQObjectRef(); void testConvertToPythonAndBack(); void testSetToList(); void testIntMoreThan32Bits(); }; #endif /* PYOTHERSIDE_TESTS_H */ pyotherside-1.6.2/tests/tests.pro000066400000000000000000000012421475412515400171140ustar00rootroot00000000000000QT += testlib qml CONFIG -= app_bundle include(../pyotherside.pri) DEFINES += PYOTHERSIDE_VERSION=\\\"$${VERSION}\\\" SOURCES += tests.cpp HEADERS += tests.h SOURCES += ../src/qpython.cpp SOURCES += ../src/qpython_worker.cpp SOURCES += ../src/qpython_priv.cpp SOURCES += ../src/pyobject_ref.cpp SOURCES += ../src/qobject_ref.cpp HEADERS += ../src/qpython.h HEADERS += ../src/qpython_worker.h HEADERS += ../src/qpython_priv.h HEADERS += ../src/converter.h HEADERS += ../src/qvariant_converter.h HEADERS += ../src/pyobject_converter.h HEADERS += ../src/pyobject_ref.h HEADERS += ../src/qobject_ref.h DEPENDPATH += . ../src INCLUDEPATH += . ../src include(../python.pri)