pywayland-0.4.18/0000775000175000017500000000000014651243522013526 5ustar epsilonepsilonpywayland-0.4.18/MANIFEST.in0000664000175000017500000000017014651243522015262 0ustar epsilonepsiloninclude *.txt include *.yaml recursive-include pywayland *.pyi recursive-include test *.py recursive-include test *.xml pywayland-0.4.18/requirements-dev.txt0000664000175000017500000000007414651243522017567 0ustar epsilonepsilon# testing/CI libraries pytest pytest-cov coverage coveralls pywayland-0.4.18/README.rst0000664000175000017500000000564614651243522015230 0ustar epsilonepsilonpywayland ========= |ci| |coveralls| |docs| PyWayland provides a wrapper to the ``libwayland`` library using the CFFI library to provide access to the Wayland library calls and written in pure Python. Below is outlined some of the basics of PyWayland and how to get up and running. For more help, see the `full documentation`_. .. _full documentation: http://pywayland.readthedocs.org/ Current Release --------------- PyWayland is still in a developmental state. An current version is available on the `cheese shop`_. Current development versions can be obtained from the `git repository`_, feedback, as well as any bug reports or fixes are highly appreciated. .. _cheese shop: https://pypi.python.org/pypi/pywayland/ .. _git repository: https://github.com/flacjacket/pywayland/ Dependencies ------------ Installing PyWayland requires the Wayland library and the headers to be installed. PyWayland requires the cffi_ package to be installed. PyWayland runs and is tested against Python 3.6+, including sufficient versions of PyPy3 (see `Running Tests`_). See the `installation guide`_ for more information on installing required dependencies .. _cffi: https://cffi.readthedocs.org/ .. _installation guide: http://pywayland.readthedocs.org/en/latest/install.html#installation Installing ---------- Installation can be done through pip to pull the most recently tagged release. To see instructions on running from source, see the relevant documentation on `installing from source`_. .. _installing from source: http://pywayland.readthedocs.org/en/latest/install.html#installing-from-source Building Wayland protocols -------------------------- In order to run from source, you will need to generate the interfaces to the Wayland protocol objects as defined in the wayland.xml file. By default, this file will be located in ``/usr/share/wayland/wayland.xml``. In this case, the protocol files can be generated by the scanner module:: $ python -m pywayland.scanner See the help for this module to use non-default locations for the input and output of the scanner. The scanner is installed as a script ``pywayland-scanner`` when PyWayland is installed. See ``pywayland-scanner -h`` for more information. Running Tests ------------- PyWayland implements a (currently limited) test-suite in ``./tests``. The tests can be run through ``pytest``. Be sure you build the protocol files (see `Building Wayland protocols`_) before running the tests. .. |ci| image:: https://github.com/flacjacket/pywayland/actions/workflows/ci.yml/badge.svg :target: https://github.com/flacjacket/pywayland/actions :alt: Build Status .. |coveralls| image:: https://coveralls.io/repos/flacjacket/pywayland/badge.svg :target: https://coveralls.io/r/flacjacket/pywayland :alt: Build Coverage .. |docs| image:: https://readthedocs.org/projects/pywayland/badge/?version=latest :target: https://pywayland.readthedocs.io/en/latest/ :alt: Documentation Status pywayland-0.4.18/doc/0000775000175000017500000000000014651243522014273 5ustar epsilonepsilonpywayland-0.4.18/doc/scanner.rst0000664000175000017500000000221014651243522016451 0ustar epsilonepsilon.. _pywayland-scanner: PyWayland Scanner ================= The PyWayland scanner parses the ``wayland.xml`` protocol definition file and outputs interfaces with the events, requests, and enums defined by the protocol. See :ref:`scanner` for details on the scanner implementation. Command-line Invocation ----------------------- If you have installed PyWayland, the scanner is placed in your path as ``pywayland-scanner.py``. By default, invoking the scanner reads in the XML file from ``/usr/share/wayland/wayland.xml`` and outputs the protocol definitions to ``./protocol/``. If you are running PyWayland from source, you can use the scanner in ``./bin/pywayland-scanner.py``. This file sets the path to the current source directory and runs method used by the entry-point. Otherwise, this functions the same as above. Script Invocation ----------------- In addition to the command-line use, you can use the scanner from within Python scripts. This is done, for example, when installing or building the docs to ensure the protocol modules are included in both. For details on invoking the scanner module, see :class:`~pywayland.scanner.Scanner`. pywayland-0.4.18/doc/install.rst0000664000175000017500000001110414651243522016470 0ustar epsilonepsilon.. _install: Installation ============ To install PyWayland, you will need to have a base set of dependencies installed. This should be all the configuration that is required to run the packaged version on PyPI_. The additional steps to build and install from source are outlined below. If you have any problems with anything outlined here, `feedback is greatly appreciated `_. .. _PyPI: https://pypi.python.org/pypi/pywayland External Dependencies --------------------- In order to run PyWayland, you will need to have installed the Wayland libraries and headers such that they can be found by CFFI. This can be done with the ``libwayland-dev`` apt package; however, note that it is probably best to use the most recent version of Wayland available from the `Wayland releases`_ site, and the pip package will try to track the most recent version. You will also need to have the Python headers installed and a version of GCC to compile the cffi library. The headers are typically available through the ``python-dev`` package. Optionally, you can have installed the ``wayland-protocols`` package, also available from the `Wayland releases`_ page. The package uploaded to PyPI will already have these protocols included, so this is only needed if you plan on installing from source. Installing with pip ------------------- Once the external dependencies are in place you should just be able to run:: $ pip install pywayland Any additional unfulfilled dependencies should be downloaded. .. _install-source: Installing from Source ---------------------- You can download and run PyWayland from source, which will not only give you the latest improvements and fixes, but will let you build the protocol files against a different version than is available through pip (the version of Wayland the protocol is compiled against is listed on the top of the PyPI_ page). Getting the Source ^^^^^^^^^^^^^^^^^^ You can download the most recent version of PyWayland from the `git repository`_, or clone the repository as:: $ git clone https://github.com/flacjacket/pywayland.git .. _git repository: https://github.com/flacjacket/pywayland Python Dependencies ^^^^^^^^^^^^^^^^^^^ PyWayland depends on a minimal set of dependencies. All Python version require cffi_ (to perform Wayland library calls), which can be pip installed for non-PyPy installations. Note that PyPy platforms ship with cffi. .. _cffi: https://cffi.readthedocs.org/en/latest/ Generating the Wayland Protocol ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ At this point, you have the base PyWayland module, which contains some core objects and objects specific to client and server implementations. The client and server exchange messages defined in the Wayland protocol, which is an XML file that ships with Wayland. The scanner parses this XML file and generates the relevant objects. If the Wayland protocol file is in the default location (``/usr/share/wayland/wayland.xml``) or can be found with ``pkg-config``, you should be able to build the protocol files without any problems:: $ python -m pywayland.scanner This will output the protocol files to the directory ``./pywayland/protocol/``. The input file and the output directory can be set from the command line options, see ``python -m pywayland.scanner -h`` for more information. Running PyWayland inplace ^^^^^^^^^^^^^^^^^^^^^^^^^ Once the protocol files are created, you can generate the cffi module. Note: this is only required if you want to run from the source in place. If the libwayland header files are correctly installed, you will just need to run:: $ python pywayland/ffi_build.py At this point, you should be able to use the PyWayland library. You can check that you have everything installed correctly by running the associated test-suite (note that you will also need ``pytest`` to run the tests). Simply run:: $ pytest from the root directory. Installing PyWayland ^^^^^^^^^^^^^^^^^^^^ The package can be installed from source using typical ``setup.py`` mechanisms:: $ python setup.py install Additional arguments can be used to automatically generate the Wayland protocols for the standard Wayland package (which will fail if it cannot run) and the wayland-protocols package (which will be attempted by default, but will not raise an error if it fails). If you have any problems or have any feedback, please report back to the `issue tracker`_, contribution is always welcome, see :ref:`contributing`. .. _issue tracker: https://github.com/flacjacket/pywayland/issues .. _Wayland releases: https://wayland.freedesktop.org/releases.html pywayland-0.4.18/doc/requirements.txt0000664000175000017500000000003014651243522017550 0ustar epsilonepsilonsphinx sphinx_rtd_theme pywayland-0.4.18/doc/conf.py0000664000175000017500000001447714651243522015607 0ustar epsilonepsilon#!/usr/bin/env python3 import importlib import os import shutil import sys # -- Mock necessary classes ----------------------------------------------- from unittest.mock import MagicMock import sphinx_rtd_theme sys.path.insert(0, os.path.abspath(".")) sys.path.insert(0, os.path.abspath("..")) MOCK_MODULES = ["pywayland._ffi"] sys.modules.update((mod_name, MagicMock()) for mod_name in MOCK_MODULES) # -- Build pywayland.protocol w/docs -------------------------------------- from protocol_build import protocols_build, protocols_version, wayland_version from pywayland import __version__ protocol_build_dir = "../pywayland/protocol/" protocol_doc_dir = "module/protocol" index_header = """\ .. _protocol: Protocol Modules ================ Wayland protocols built against Wayland {} and Wayland Protocols {}. .. toctree:: :maxdepth: 2 """.format( wayland_version, protocols_version ) protocol_header = """\ .. module:: pywayland.protocol.{module} {module} Module {empty:=^{len}}=======""" protocol_rst = """\ {protocol} {empty:-^{len}} .. wl_protocol:: pywayland.protocol.{module} {protocol}""" # There is probably a better way to do this in Sphinx, templating or something # ... but this works def protocol_doc(input_dir, output_dir): modules = os.listdir(input_dir) modules = [ module for module in modules if os.path.isdir(os.path.join(input_dir, module)) and module != "__pycache__" ] existing_files = [ filename for filename in os.listdir(output_dir) if filename != "index.rst" ] rm_files = [ filename for filename in existing_files if os.path.splitext(filename)[0] not in modules ] for rm_file in rm_files: if os.path.isdir(rm_file): shutil.rmtree(os.path.join(output_dir, rm_file)) else: os.remove(os.path.join(output_dir, rm_file)) # Write out the index file index_file = os.path.join(output_dir, "index.rst") if os.path.exists(index_file): with open(index_file) as f: existing_index = f.read() else: existing_index = "" generated_index = index_header + "".join(f" {m}\n" for m in sorted(modules)) if existing_index != generated_index: with open(index_file, "w") as f: f.write(generated_index) for module in modules: output = [protocol_header.format(module=module, len=len(module), empty="")] # get all the python files that we want to document doc_files = os.listdir(os.path.join(input_dir, module)) doc_files = [ os.path.splitext(doc_file)[0] for doc_file in doc_files if doc_file != "__init__.py" and os.path.splitext(doc_file)[1] == ".py" ] # build the rst for each protocol for doc_file in doc_files: mod = importlib.import_module(f"pywayland.protocol.{module}.{doc_file}") # Get out the name of the class in the module class_name = "".join(x.capitalize() for x in doc_file.split("_")) for mod_upper in dir(mod): if mod_upper == class_name: break else: raise RuntimeError(f"Unable to find module: {doc_file}, {mod}") output.append( protocol_rst.format( module=module, protocol=mod_upper, len=len(mod_upper), empty="" ) ) # build the index.rst for the module module_file = os.path.join(output_dir, f"{module}.rst") protocol_output = "\n\n".join(output) # if file exists and is unchanged, skip if os.path.exists(module_file): with open(module_file) as f: existing_output = f.read() if existing_output == protocol_output: continue with open(module_file, "w") as f: f.write("\n\n".join(output)) # Build the protocol directory on RTD if os.environ.get("READTHEDOCS", None): protocols_build(protocol_build_dir) # Re-build the protocol documentation directory if not os.path.exists(protocol_doc_dir): os.makedirs(protocol_doc_dir) protocol_doc(protocol_build_dir, protocol_doc_dir) # -- General configuration ------------------------------------------------ extensions = ["sphinx.ext.autodoc", "sphinx_wl_protocol"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The master toctree document. master_doc = "index" # General information about the project. project = "pywayland" copyright = "2016, Sean Vig" # The short X.Y version. version = __version__.split("a")[0] # The full version, including alpha/beta/rc tags. release = __version__ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # -- Options for HTML output ---------------------------------------------- # Set the html_theme when building locally html_theme = "sphinx_rtd_theme" html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # Output file base name for HTML help builder. htmlhelp_basename = "pywaylanddoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = {} # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ("index", "pywayland.tex", "pywayland Documentation", "Sean Vig", "manual"), ] # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [("index", "pywayland", "pywayland Documentation", ["Sean Vig"], 1)] # -- 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", "pywayland", "pywayland Documentation", "Sean Vig", "pywayland", "Python bindings for the libwayland library", "Miscellaneous", ), ] pywayland-0.4.18/doc/module/0000775000175000017500000000000014651243522015560 5ustar epsilonepsilonpywayland-0.4.18/doc/module/scanner/0000775000175000017500000000000014651243522017211 5ustar epsilonepsilonpywayland-0.4.18/doc/module/scanner/request.rst0000664000175000017500000000013614651243522021433 0ustar epsilonepsilon.. _scanner-request: Request ======= .. automodule:: pywayland.scanner.request :members: pywayland-0.4.18/doc/module/scanner/interface.rst0000664000175000017500000000014614651243522021704 0ustar epsilonepsilon.. _scanner-interface: Interface ========= .. automodule:: pywayland.scanner.interface :members: pywayland-0.4.18/doc/module/scanner/scanner.rst0000664000175000017500000000124114651243522021372 0ustar epsilonepsilon.. module:: pywayland.scanner .. _scanner-scanner: Scanner ======= Using the Scanner module ------------------------ The PyWayland scanner allows you to generate the protocol scanner output within Python scripts. The general procedure to invoke the scanner will be to make a :class:`Protocol` object, scan the input file, and have the Protocol output to a directory. These steps are done as: .. code-block:: python Protocol.parse_file(path_to_xml_file) Protocol.output(path_to_output_dir, {}) See the definitions below for more information on using Protocol objects. Protocol Module --------------- .. autoclass:: pywayland.scanner.Protocol :members: pywayland-0.4.18/doc/module/scanner/method.rst0000664000175000017500000000013214651243522021217 0ustar epsilonepsilon.. _scanner-method: Method ====== .. automodule:: pywayland.scanner.method :members: pywayland-0.4.18/doc/module/scanner/argument.rst0000664000175000017500000000013714651243522021566 0ustar epsilonepsilon.. _scanner-argumer: Argumet ======= .. automodule:: pywayland.scanner.argument :members: pywayland-0.4.18/doc/module/scanner/enum.rst0000664000175000017500000000012214651243522020702 0ustar epsilonepsilon.. _scanner-enum: Enum ==== .. automodule:: pywayland.scanner.enum :members: pywayland-0.4.18/doc/module/scanner/event.rst0000664000175000017500000000012614651243522021063 0ustar epsilonepsilon.. _scanner-event: Event ===== .. automodule:: pywayland.scanner.event :members: pywayland-0.4.18/doc/module/scanner/printer.rst0000664000175000017500000000013614651243522021426 0ustar epsilonepsilon.. _scanner-printer: Printer ======= .. automodule:: pywayland.scanner.printer :members: pywayland-0.4.18/doc/module/scanner/index.rst0000664000175000017500000000025314651243522021052 0ustar epsilonepsilon.. _scanner: Scanner Modules =============== .. toctree:: :maxdepth: 2 argument entry enum event interface method printer request scanner pywayland-0.4.18/doc/module/scanner/entry.rst0000664000175000017500000000012614651243522021103 0ustar epsilonepsilon.. _scanner-entry: Entry ===== .. automodule:: pywayland.scanner.entry :members: pywayland-0.4.18/doc/module/server.rst0000664000175000017500000000053114651243522017617 0ustar epsilonepsilon.. module:: pywayland.server .. _server: Server Modules ============== The base set of objects used by Wayland servers. Client ------ .. autoclass:: Client :members: Display ------- .. autoclass:: Display :members: EventLoop --------- .. autoclass:: EventLoop :members: Listener -------- .. autoclass:: Listener :members: pywayland-0.4.18/doc/module/client.rst0000664000175000017500000000075514651243522017577 0ustar epsilonepsilon.. module:: pywayland.client .. _client: Client Modules ============== The base set of objects used by Wayland clients. Users should only be directly creating :class:`~pywayland.client.Display` and :class:`~pywayland.client.EventQueue` objects. The :class:`~pywayland.client.proxy.Proxy` objects to interfaces should be returned by making request calls. Display ------- .. autoclass:: Display :members: EventQueue ---------- .. autoclass:: pywayland.client.EventQueue :members: pywayland-0.4.18/doc/module/utils.rst0000664000175000017500000000025714651243522017456 0ustar epsilonepsilon.. module:: pywayland.utils .. _utils: Utilities Module ================ AnonymousFile Class ------------------- .. autoclass:: pywayland.utils.AnonymousFile :members: pywayland-0.4.18/doc/module/protocol_core.rst0000664000175000017500000000326614651243522021172 0ustar epsilonepsilon.. module:: pywayland.protocol_core .. _protocol_core: Protocol Core Modules ===================== .. toctree:: :maxdepth: 2 Interface --------- :class:`Interface` objects are only created as a subclass of Interface. The Interface class wraps the protocol objects, and serves to initialize a set of parameters for the Interface and provide decorators for defining :class:`~pywayland.interface.Message` objects on the interface. .. autoclass:: Interface :members: Interface Metaclass ------------------- This metaclass initializes lists for the requests and events on an interface and initializes a cdata struct for the class. .. autoclass:: pywayland.protocol_core.interface.InterfaceMeta :members: Proxy ----- :class:`Proxy` objects are not created directly, and users should generally not create a proxy class on their own. Proxy classes give client side access to the interfaces defined by the Wayland protocol. Proxies are returned from the server after calling protocol methods which return ``new_id``'s. .. autoclass:: Proxy :members: Resource -------- .. autoclass:: Resource :members: Global ------ .. autoclass:: Global :members: Message ------- :class:`~pywayland.interface.Message` objects are used to wrap the method calls on the protocol objects. The Message objects are added to the :class:`~pywayland.interface.Interface`'s as either requests (client-side functions) or events (server-side functions). .. autoclass:: pywayland.protocol_core.message.Message :members: Argument -------- .. autoclass:: pywayland.protocol_core.argument.Argument :members: ArgumentType ------------ .. autoclass:: pywayland.protocol_core.argument.ArgumentType :members: pywayland-0.4.18/doc/module/index.rst0000664000175000017500000000022214651243522017415 0ustar epsilonepsilonModule Reference ================ .. toctree:: :maxdepth: 2 client server protocol_core protocol/index scanner/index utils pywayland-0.4.18/doc/index.rst0000664000175000017500000000161514651243522016137 0ustar epsilonepsilonWelcome to PyWayland's documentation! ===================================== PyWayland provides Python bindings to the Wayland library, using pure Python by making calls through the CFFI module. PyWayland supports Python >=3.6, including sufficiently new versions of PyPy 3. This is currently a highly experimental package, and the usage is likely to change between releases. Check back as development continues, contributions are always welcome! Check out the different sections below for information on installing and running PyWayland. There is also information on running and developing from source (feedback and contributions are always welcome on the `issue tracker`_). Finally, the module documentation is included. .. _issue tracker: https://github.com/flacjacket/pywayland/issues Documentation ------------- .. toctree:: :maxdepth: 2 install contributing scanner module/index pywayland-0.4.18/doc/Makefile0000664000175000017500000001516614651243522015744 0ustar epsilonepsilon# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # 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 " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" 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/pywayland.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pywayland.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/pywayland" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pywayland" @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." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 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." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." pywayland-0.4.18/doc/protocol_build.py0000664000175000017500000000433214651243522017667 0ustar epsilonepsilonimport os import tarfile import urllib.request wayland_version = "1.21.0" protocols_version = "1.25" wayland_source = "https://cgit.freedesktop.org/wayland/wayland/plain/protocol/wayland.xml?id={}".format( wayland_version ) protocols_source = ( "https://wayland.freedesktop.org/releases/wayland-protocols-{}.tar.xz".format( protocols_version ) ) def _is_within_directory(directory, target): """Helper to check for CVE-2007-4559""" abs_directory = os.path.abspath(directory) abs_target = os.path.abspath(target) prefix = os.path.commonprefix([abs_directory, abs_target]) return prefix == abs_directory def _safe_extractall(tar, path=".", members=None, *, numeric_owner=False): """Helper to check for CVE-2007-4559""" for member in tar.getmembers(): member_path = os.path.join(path, member.name) if not _is_within_directory(path, member_path): raise Exception("Attempted Path Traversal in Tar File") tar.extractall(path, members, numeric_owner=numeric_owner) def protocols_build(output_dir): from pywayland.scanner import Protocol # first, we download the wayland.xml file wayland_file = "wayland.xml" urllib.request.urlretrieve(wayland_source, wayland_file) # download the protocols file and extract it protocol_dest = f"wayland-protocols-{protocols_version}" urllib.request.urlretrieve(protocols_source, protocol_dest + ".tar.xz") with tarfile.open(protocol_dest + ".tar.xz") as f: _safe_extractall(f) # walk the directory and generate all the protocols protocol_files = [ wayland_file, *sorted( [ os.path.join(dirpath, filename) for dirpath, _, filenames in os.walk(protocol_dest) for filename in filenames if os.path.splitext(filename)[1] == ".xml" ], reverse=True, ), ] protocols = [Protocol.parse_file(protocol_file) for protocol_file in protocol_files] protocol_imports = { interface.name: protocol.name for protocol in protocols for interface in protocol.interface } for protocol in protocols: protocol.output(output_dir, protocol_imports) pywayland-0.4.18/doc/sphinx_wl_protocol.py0000664000175000017500000000624514651243522020610 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import importlib import inspect from docutils import nodes from docutils.parsers.rst import Directive from docutils.statemachine import ViewList from jinja2 import Template from sphinx.util.docstrings import prepare_docstring from sphinx.util.nodes import nested_parse_with_titles # Adapted from sphinxcontrib-httpdomain def import_object(module_name, class_name): mod = importlib.import_module(module_name) return getattr(mod, class_name) def format_args(func): sig = inspect.signature(func) parameters = [v for k, v in sig.parameters.items() if k != "self"] sig = sig.replace(parameters=parameters) return str(sig) wl_protocol_template = Template( """ .. autoclass:: {{ module }}.{{ class_name }} {% for func, opcode, sig, docs in requests %} .. method:: {{ func }} {{ sig }} .. rubric:: Request -- opcode {{ opcode}} (attached to :class:`~pywayland.server.resource.Resource` instance) {% for doc in docs %} {{ doc }}{% endfor %}{% endfor %} {% for func, opcode, sig, docs in events %} .. method:: {{ func }} {{ sig }} .. rubric:: Event -- opcode {{ opcode}} (attached to :class:`~pywayland.client.proxy.Proxy` instance) {% for doc in docs %} {{ doc }}{% endfor %}{% endfor %} """ ) class WlProtocol(Directive): required_arguments = 2 def make_rst(self): module_name, class_name = self.arguments[:2] obj = import_object(module_name, class_name) events = [ ( event.py_func.__name__, opcode, format_args(event.py_func), prepare_docstring(event.py_func.__doc__), ) for opcode, event in enumerate(obj.events) ] reqs = [ ( req.py_func.__name__, opcode, format_args(req.py_func), prepare_docstring(req.py_func.__doc__), ) for opcode, req in enumerate(obj.requests) ] context = { "module": module_name, "class_name": class_name, "obj": obj, "events": events, "requests": reqs, } rst = wl_protocol_template.render(**context) yield from rst.splitlines() def run(self): node = nodes.section() node.document = self.state.document result = ViewList() for line in self.make_rst(): result.append(line, f"<{self.__class__.__name__}>") nested_parse_with_titles(self.state, result, node) return node.children def setup(app): app.add_directive("wl_protocol", WlProtocol) pywayland-0.4.18/doc/contributing.rst0000664000175000017500000000334214651243522017536 0ustar epsilonepsilon.. _contributing: Contributing ============ |travis| |coveralls| Contributions of any form are always welcome, whether it is general feedback on the use of PyWayland, bug reports, or pull requests. All development is done through GitHub_. If you wish to develop PyWayland, it is recommended that you follow the outline given in :ref:`install-source`. A few things to be aware of when writing code: - Continuous integration testing in done with Travis_, and tests are run against all supported Python versions (currently 3.6+ and PyPy 3). You can check that your changes pass locally by running ``py.test`` from the root directory (this requires installing pytest_). Currently, the tests also run with nose_, however, they may not always in the future. - Code coverage is assessed using Coveralls_. Currently, coverage is fairly low, any work to help this would be greatly appreciated. - Code quality is assessed in the tests with ruff_, be sure any new code meets Python standards. - Type annotations are included in much of the codebase and checked with mypy. Additional checks using other type checkers are appreciated. .. _Coveralls: https://coveralls.io/r/flacjacket/pywayland .. _GitHub: https://github.com/flacjacket/pywayland/ .. _Travis: https://travis-ci.org/flacjacket/pywayland .. _ruff: https://beta.ruff.rs/docs/ .. _nose: https://nose.readthedocs.org .. _pytest: https://pytest.org .. |travis| image:: https://travis-ci.org/flacjacket/pywayland.svg?branch=master :alt: Build Status :target: https://travis-ci.org/flacjacket/pywayland .. |coveralls| image:: https://coveralls.io/repos/flacjacket/pywayland/badge.svg?branch=master :alt: Build Coverage :target: https://coveralls.io/r/flacjacket/pywayland pywayland-0.4.18/requirements.txt0000664000175000017500000000001514651243522017006 0ustar epsilonepsiloncffi>=1.12.0 pywayland-0.4.18/setup.py0000664000175000017500000001407714651243522015251 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import subprocess import sys from setuptools import setup from setuptools.command.build import build from setuptools.command.install import install from setuptools.command.sdist import sdist try: from wheel.bdist_wheel import bdist_wheel except ImportError: bdist_wheel = None # we need this to import the scanner module directly so we can build the # protocol before the cffi module has been compiled sys.path.insert(0, "pywayland") default_xml_file = "/usr/share/wayland/wayland.xml" def get_protocol_command(klass): class ProtocolCommand(klass): user_options = [ ("xml-file=", None, "Location of wayland.xml protocol file"), ("output-dir=", None, "Output location for protocol python files"), ( "wayland-protocols", None, "Force generation of external protocols from wayland-protocols", ), ( "no-wayland-protocols", None, "Disable generation of external protocols from wayland-protocols", ), *klass.user_options, ] boolean_options = [ "wayland-protocols", "no-wayland-protocols", *klass.boolean_options, ] def initialize_options(self): from scanner.__main__ import pkgconfig # try to figure out where the main wayland protocols are installed try: data_dir = pkgconfig("wayland-scanner", "pkgdatadir") except subprocess.CalledProcessError: # silently fallback to the default self.xml_file = default_xml_file else: self.xml_file = os.path.join(data_dir, "wayland.xml") self.output_dir = "./pywayland/protocol" self.wayland_protocols = False self.no_wayland_protocols = False klass.initialize_options(self) def finalize_options(self): assert os.path.exists(self.xml_file), ( f"Specified Wayland protocol file, {self.xml_file}, does not exist " "please specify valid protocol file" ) klass.finalize_options(self) def run(self): from scanner import Protocol from scanner.__main__ import get_wayland_protocols # Generate the wayland interface by default input_files = [self.xml_file] # Unless users says don't build protocols, try to build them if not self.no_wayland_protocols: try: protocol_files = get_wayland_protocols() input_files += protocol_files except Exception: # but only complain if we ask specifically to build them if self.wayland_protocols: raise # Ensure the output dir exists if not os.path.isdir(self.output_dir): os.makedirs(self.output_dir, 0o775) # Run and scan all the above found xml files protocols = [Protocol.parse_file(input_xml) for input_xml in input_files] protocol_imports = { interface.name: protocol.name for protocol in protocols for interface in protocol.interface } for protocol in protocols: protocol.output(self.output_dir, protocol_imports) self.distribution.packages.extend( f"pywayland.protocol.{protocol.name}" for protocol in protocols ) klass.run(self) return ProtocolCommand InstallCommand = get_protocol_command(install) SdistCommand = get_protocol_command(sdist) BuildCommand = get_protocol_command(build) cmdclass = { "install": InstallCommand, "sdist": SdistCommand, } if bdist_wheel is not None: BdistWheelCommand = get_protocol_command(bdist_wheel) cmdclass["bdist_wheel"] = BdistWheelCommand # For the purposes of uploading to PyPI, we'll get the version of Wayland here with open("README.rst") as f: rst_input = f.read().strip().split("\n") try: from pywayland import ( __version__ as pywayland_version, ) from pywayland import ( __wayland_version__ as wayland_version, ) version_tag = f"v{pywayland_version}" except Exception: pass else: version = f"Built against Wayland {wayland_version}\n" rst_input.insert(3, version) # replace all of the badges and links to point to the current version rst_input = rst_input[:-9] rst_input.extend( [ f".. |ci| image:: https://github.com/flacjacket/pywayland/actions/workflows/ci.yml/badge.svg?branch={version_tag}", " :target: https://github.com/flacjacket/pywayland/actions", " :alt: Build Status", f".. |coveralls| image:: https://coveralls.io/repos/flacjacket/pywayland/badge.svg?branch={version_tag}", f" :target: https://coveralls.io/github/flacjacket/pywayland?branch={version_tag}", " :alt: Build Coverage", f".. |docs| image:: https://readthedocs.org/projects/pywayland/badge/?version={version_tag}", f" :target: https://pywayland.readthedocs.io/en/{version_tag}/", " :alt: Documentation Status", ] ) long_description = "\n".join(rst_input) setup( long_description=long_description, long_description_content_type="text/x-rst", cmdclass=cmdclass, cffi_modules=["pywayland/ffi_build.py:ffi_builder"], ) pywayland-0.4.18/LICENSE0000664000175000017500000002363514651243522014544 0ustar epsilonepsilon Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. pywayland-0.4.18/.pre-commit-config.yaml0000664000175000017500000000047114651243522020011 0ustar epsilonepsilonrepos: - repo: https://github.com/psf/black rev: 24.4.2 hooks: - id: black # Use the latest supported version here language_version: python3.11 exclude: ^test/scanner_files/ - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.5.5 hooks: - id: ruff pywayland-0.4.18/requirements-types.txt0000664000175000017500000000002714651243522020153 0ustar epsilonepsilonmypy types-dataclasses pywayland-0.4.18/pyproject.toml0000664000175000017500000000476014651243522016451 0ustar epsilonepsilon[build-system] requires = [ "setuptools>=62.4.0", "wheel", "cffi>=1.12.0; platform_python_implementation != 'PyPy'", ] build-backend = "setuptools.build_meta" [project] name = "pywayland" description = "Python bindings for the libwayland library written in pure Python" authors = [{name = "Sean Vig", email = "sean.v.775@gmail.com"}] requires-python = ">=3.8" license = {text = "Apache License 2.0"} readme = "README.rst" classifiers = [ "Development Status :: 2 - Pre-Alpha", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Desktop Environment :: Window Managers", "Topic :: Software Development :: Libraries", ] dependencies = [ "cffi >= 1.12.0; platform_python_implementation != 'PyPy'", ] dynamic = ["version"] [project.urls] homepage = "https://github.com/flacjacket/pywayland" documentation = "https://pywayland.readthedocs.io" source = "https://github.com/flacjacket/pywayland" issues = "https://github.com/flacjacket/pywayland/issues" [project.optional-dependencies] test = ["pytest"] [project.scripts] pywayland-scanner = "pywayland.scanner.__main__:main" [tool.setuptools] zip-safe = false [tool.setuptools.packages.find] include = ["pywayland*"] [tool.setuptools.package-data] pywayland = ["py.typed"] [tool.setuptools.dynamic] version = {attr = "pywayland.version.__version__"} [tool.ruff] # E/W pycodestyle errors + warnings # F pyflakes # I isort # N pep8 naming # UP pyupgrade # RUF ruff specific rules lint.select = ['E', 'F', 'I', 'W', 'UP', 'RUF'] lint.ignore = ["E501"] line-length = 88 target-version = "py38" [tool.black] target-version = ["py38"] [tool.check-manifest] ignore = [ ".coveragerc", "doc/**", "example/**", "pywayland/protocol/*/*.py", ] [tool.mypy] python_version = 3.8 disallow_subclassing_any = true no_implicit_optional = true show_error_codes = true strict_equality = true warn_incomplete_stub = true warn_redundant_casts = true warn_return_any = true warn_unreachable = true warn_unused_configs = true warn_unused_ignores = true [[tool.mypy.overrides]] module = ["cffi"] ignore_missing_imports = true pywayland-0.4.18/example/0000775000175000017500000000000014651243522015161 5ustar epsilonepsilonpywayland-0.4.18/example/simple-touch.py0000664000175000017500000001204514651243522020146 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # import collections import mmap import os import sys import tempfile this_file = os.path.abspath(__file__) this_dir = os.path.split(this_file)[0] root_dir = os.path.split(this_dir)[0] pywayland_dir = os.path.join(root_dir, "pywayland") if os.path.exists(pywayland_dir): sys.path.append(root_dir) from pywayland.client import Display # noqa: E402 from pywayland.protocol.wayland import ( # noqa: E402 WlCompositor, WlSeat, WlShell, WlShm, ) def create_shm_buffer(touch, width, height): stride = width * 4 size = stride * height with tempfile.TemporaryFile() as f: f.write(b"\x64" * size) f.flush() fd = f.fileno() touch["data"] = mmap.mmap( fd, size, mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE ) pool = touch["shm"].create_pool(fd, size) touch["buffer"] = pool.create_buffer( 0, width, height, stride, WlShm.format.argb8888.value ) pool.destroy() def handle_touch_down(wl_touch, serial, time, surface, id, x, y): # touch = wl_touch.user_data # touch_paint(touch, x, y, id) return 0 def handle_touch_motion(wl_touch, time, id, x, y): # touch = wl_touch.user_data # touch_paint(touch, x, y, id) return 0 def handle_seat_capabilities(wl_seat, capabilities): print("capabilities") seat = wl_seat.user_data touch = seat["touch"] if (capabilities & WlSeat.capability.touch.value) and seat["wl_touch"] is None: seat["wl_touch"] = wl_seat.get_touch() seat["wl_touch"].user_data = touch seat["wl_touch"].dispatcher["down"] = handle_touch_down # seat['wl_touch'].dispatcher['up'] = handle_touch_up seat["wl_touch"].dispatcher["motion"] = handle_touch_motion elif not (capabilities & WlSeat.capability.touch.value) and seat["wl_touch"]: seat["wl_touch"].destroy() seat["wl_touch"] = None return 1 def handle_shm_format(wl_shm, fmt): print("format") touch = wl_shm.user_data if fmt == WlShm.format.argb8888.value: touch["has_argb"] = True return 1 def handle_shell_surface_ping(wl_shell_surface, serial): print("ping") wl_shell_surface.pong(serial) return 1 def handle_registry_global(wl_registry, id_num, iface_name, version): print("global", id_num, iface_name) touch = wl_registry.user_data if iface_name == "wl_compositor": touch["compositor"] = wl_registry.bind(id_num, WlCompositor, version) elif iface_name == "wl_seat": seat = {} seat["touch"] = touch seat["wl_touch"] = None wl_seat = wl_registry.bind(id_num, WlSeat, version) wl_seat.dispatcher["capabilities"] = handle_seat_capabilities wl_seat.user_data = seat seat["seat"] = wl_seat elif iface_name == "wl_shell": touch["shell"] = wl_registry.bind(id_num, WlShell, version) elif iface_name == "wl_shm": touch["has_argb"] = False shm = wl_registry.bind(id_num, WlShm, version) shm.user_data = touch shm.dispatcher["format"] = handle_shm_format touch["shm"] = shm return 1 def touch_create(width, height): touch = {} # Make the display and get the registry touch["display"] = Display() touch["display"].connect() touch["registry"] = touch["display"].get_registry() touch["registry"].user_data = touch touch["registry"].dispatcher["global"] = handle_registry_global touch["display"].dispatch() touch["display"].roundtrip() touch["display"].roundtrip() if not touch["has_argb"]: print("WL_SHM_FORMAT_ARGB32 not available", file=sys.stderr) touch["display"].disconnect() return touch["width"] = width touch["height"] = height touch["surface"] = touch["compositor"].create_surface() touch["shell_surface"] = touch["shell"].get_shell_surface(touch["surface"]) create_shm_buffer(touch, width, height) if touch["shell_surface"]: print("shell") touch["shell_surface"].dispatcher["ping"] = handle_shell_surface_ping touch["shell_surface"].set_toplevel() touch["surface"].user_data = touch touch["shell_surface"].set_title("simple-touch") touch["surface"].attach(touch["buffer"], 0, 0) touch["surface"].damage(0, 0, width, height) touch["surface"].commit() return touch def main(): touch = touch_create(600, 500) while touch["display"].dispatch() != -1: pass touch["display"].disconnect() if __name__ == "__main__": main() pywayland-0.4.18/example/wlinfo.py0000664000175000017500000001331414651243522017033 0ustar epsilonepsilon#!/usr/bin/env python # # Copyright (c) 2024 -- Lars Heuer # All rights reserved. # # SPDX-License-Identifier: CC-PDDC # """\ Prints information about a Wayland compositor. """ from __future__ import annotations from collections.abc import Iterator from dataclasses import dataclass, field from functools import partial from itertools import chain from operator import itemgetter from pywayland.client import Display from pywayland.protocol.wayland import WlOutput, WlRegistry, WlSeat, WlShm @dataclass class Info: common: list = field(default_factory=list) seat: list = field(default_factory=list) output: list = field(default_factory=list) shm: list = field(default_factory=list) roundtrip_needed: bool = False def __str__(self) -> str: def stringify_interface(interface: str, version: int, name: int) -> str: return f"{interface:<{pad}}version: {version:2}, name: {name:2}" def stringify_head_tail(l: list) -> Iterator[str]: # noqa: E741 head, *tail = l yield stringify_interface(*head) yield from tail pad = len(max(map(itemgetter(0), self.common), key=len)) return "\n".join( chain( stringify_head_tail(self.seat), stringify_head_tail(self.shm), stringify_head_tail(self.output), ( stringify_interface(*e) for e in sorted(self.common, key=itemgetter(0)) ), ) ) def _add_interface_info( l: list, *, interface: str, version: int, name: int ) -> None: # noqa: E741 # Not building the complete output string here because # we need the length of the 1st element, see pad in Info.__str__() l.append((f"interface: '{interface}', ", version, name)) def add_seat_info( info: Info, registry: WlRegistry, id_num: int, interface: str, version: int ) -> None: def handle_capabilities(_, capabilities): caps = sorted(cap.name for cap in WlSeat.capability if capabilities & cap.value) append(f"capabilities: {' '.join(caps)}") def handle_name(_, name): append(f"name: {name}") _add_interface_info(info.seat, interface=interface, version=version, name=id_num) append = lambda s: info.seat.append(f" {s}") # noqa: E731 seat = registry.bind(id_num, WlSeat, version) seat.dispatcher["name"] = handle_name seat.dispatcher["capabilities"] = handle_capabilities def add_output_info( info: Info, registry: WlRegistry, id_num: int, interface: str, version: int ) -> None: def handle_geometry( _, x: int, y: int, width: int, height: int, subpixel: int, make: str, model: str, transform: int, ) -> None: subpixel_info = next(m.name for m in WlOutput.subpixel if m.value == subpixel) transform_info = next( m.name for m in WlOutput.transform if m.value == transform ) append(f"x: {x}, y: {y}") append(f"physical_width: {width} mm, physical_height: {height} mm") append(f"make: '{make}', model: '{model}'") append( f"subpixel_orientation: {subpixel_info}, output_transform: {transform_info}" ) def handle_scale(_, scale: int): append(f"scale: {scale}") def handle_name(_, name): append(f"name: {name}") def handle_description(_, description): append(f"description: {description}") def handle_mode(_, flags: int, width: int, height: int, refresh: int) -> None: flag_info = next(m.name for m in WlOutput.mode if flags & m.value) append("mode:") append_sub = lambda s: info.output.append(f" {s}") # noqa: E731 append_sub( f"width: {width} px, height: {height} px, refresh: {refresh / 1000} Hz" ) append_sub(f"flags: {flag_info}") _add_interface_info(info.output, interface=interface, version=version, name=id_num) append = lambda s: info.output.append(f" {s}") # noqa: E731 output = registry.bind(id_num, WlOutput, version) output.dispatcher["name"] = handle_name output.dispatcher["description"] = handle_description output.dispatcher["geometry"] = handle_geometry output.dispatcher["scale"] = handle_scale output.dispatcher["mode"] = handle_mode def add_shm_info( info: Info, registry: WlRegistry, id_num: int, interface: str, version: int ) -> None: def handle_format(_, fmt: int) -> None: format_info = next(f for f in WlShm.format if f.value == fmt) append(f"{hex(format_info.value)}: {format_info.name}") _add_interface_info(info.shm, interface=interface, version=version, name=id_num) append = lambda s: info.shm.append(f" {s}") # noqa: E731 append("formats:") shm = registry.bind(id_num, WlShm, version) shm.dispatcher["format"] = handle_format def handle_registry_global( info: Info, registry: WlRegistry, id_num: int, interface: str, version: int ) -> None: if interface in ("wl_seat", "wl_output", "wl_shm"): globals()[f"add_{interface[3:]}_info"]( info, registry, id_num, interface, version ) info.roundtrip_needed = True else: _add_interface_info( info.common, interface=interface, version=version, name=id_num ) def main(): with Display() as display: registry = display.get_registry() info = Info() registry.dispatcher["global"] = partial(handle_registry_global, info) display.dispatch() while True: info.roundtrip_needed = False display.roundtrip() if not info.roundtrip_needed: break print(info) if __name__ == "__main__": main() pywayland-0.4.18/example/surface.py0000664000175000017500000001164514651243522017172 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import mmap import os import sys this_file = os.path.abspath(__file__) this_dir = os.path.split(this_file)[0] root_dir = os.path.split(this_dir)[0] pywayland_dir = os.path.join(root_dir, "pywayland") if os.path.exists(pywayland_dir): sys.path.append(root_dir) from pywayland.client import Display # noqa: E402 from pywayland.protocol.wayland import WlCompositor, WlShell, WlShm # noqa: E402 from pywayland.utils import AnonymousFile # noqa: E402 WIDTH = 480 HEIGHT = 256 MARGIN = 10 class Window: def __init__(self): self.buffer = None self.compositor = None self.shell = None self.shm = None self.shm_data = None self.surface = None self.line_pos = MARGIN self.line_speed = +1 def shell_surface_ping_handler(shell_surface, serial): shell_surface.pong(serial) print("pinged/ponged") def shm_format_handler(shm, format_): if format_ == WlShm.format.argb8888.value: s = "ARGB8888" elif format_ == WlShm.format.xrgb8888.value: s = "XRGB8888" elif format_ == WlShm.format.rgb565.value: s = "RGB565" else: s = "other format" print(f"Possible shmem format: {s}") def registry_global_handler(registry, id_, interface, version): window = registry.user_data if interface == "wl_compositor": print("got compositor") window.compositor = registry.bind(id_, WlCompositor, version) elif interface == "wl_shell": print("got shell") window.shell = registry.bind(id_, WlShell, version) elif interface == "wl_shm": print("got shm") window.shm = registry.bind(id_, WlShm, version) window.shm.dispatcher["format"] = shm_format_handler def registry_global_remover(registry, id_): print(f"got a registry losing event for {id}") def create_buffer(window): stride = WIDTH * 4 size = stride * HEIGHT with AnonymousFile(size) as fd: window.shm_data = mmap.mmap( fd, size, prot=mmap.PROT_READ | mmap.PROT_WRITE, flags=mmap.MAP_SHARED ) pool = window.shm.create_pool(fd, size) buff = pool.create_buffer(0, WIDTH, HEIGHT, stride, WlShm.format.argb8888.value) pool.destroy() return buff def create_window(window): window.buffer = create_buffer(window) window.surface.attach(window.buffer, 0, 0) window.surface.commit() def redraw(callback, time, destroy_callback=True): window = callback.user_data if destroy_callback: callback._destroy() paint(window) window.surface.damage(0, 0, WIDTH, HEIGHT) callback = window.surface.frame() callback.dispatcher["done"] = redraw callback.user_data = window window.surface.attach(window.buffer, 0, 0) window.surface.commit() def paint(window): mm = window.shm_data # clear mm.seek(0) mm.write(b"\xff" * 4 * WIDTH * HEIGHT) # draw progressing line mm.seek((window.line_pos * WIDTH + MARGIN) * 4) mm.write(b"\x00\x00\x00\xff" * (WIDTH - 2 * MARGIN)) window.line_pos += window.line_speed # maybe reverse direction of progression if window.line_pos >= HEIGHT - MARGIN or window.line_pos <= MARGIN: window.line_speed = -window.line_speed def main(): window = Window() display = Display() display.connect() print("connected to display") registry = display.get_registry() registry.dispatcher["global"] = registry_global_handler registry.dispatcher["global_remove"] = registry_global_remover registry.user_data = window display.dispatch(block=True) display.roundtrip() if window.compositor is None: raise RuntimeError("no compositor found") elif window.shell is None: raise RuntimeError("no shell found") elif window.shm is None: raise RuntimeError("no shm found") window.surface = window.compositor.create_surface() shell_surface = window.shell.get_shell_surface(window.surface) shell_surface.set_toplevel() shell_surface.dispatcher["ping"] = shell_surface_ping_handler frame_callback = window.surface.frame() frame_callback.dispatcher["done"] = redraw frame_callback.user_data = window create_window(window) redraw(frame_callback, 0, destroy_callback=False) while display.dispatch(block=True) != -1: pass import time time.sleep(1) display.disconnect() if __name__ == "__main__": main() pywayland-0.4.18/.coveragerc0000664000175000017500000000041614651243522015650 0ustar epsilonepsilon[run] source = pywayland omit = pywayland/protocol/* pywayland/ffi_build.py [paths] source = src/pywayland .tox/*/lib/python*/site-packages/pywayland [report] exclude_lines = if TYPE_CHECKING: if __name__ == "__main__": except ImportError: pywayland-0.4.18/pywayland/0000775000175000017500000000000014651243522015536 5ustar epsilonepsilonpywayland-0.4.18/pywayland/_ffi/0000775000175000017500000000000014651243522016441 5ustar epsilonepsilonpywayland-0.4.18/pywayland/_ffi/__init__.pyi0000664000175000017500000000000014651243522020711 0ustar epsilonepsilonpywayland-0.4.18/pywayland/_ffi/lib.pyi0000664000175000017500000001056214651243522017736 0ustar epsilonepsilonfrom typing import Any from .ffi import ( CData, CharCData, ClientCData, DispatcherFuncT, DisplayCData, EventLoopCData, InterfaceCData, ListCData, ListenerCData, NotifyFuncT, QueueCData, ResourceCData, ResourceDestroyFuncT, ) WAYLAND_VERSION_MAJOR: int WAYLAND_VERSION_MINOR: int WAYLAND_VERSION_MICRO: int dispatcher_func: DispatcherFuncT resource_destroy_func: ResourceDestroyFuncT notify_func: NotifyFuncT def wl_list_remove(list: ListCData) -> None: ... # Event loop functionality def wl_event_loop_create() -> EventLoopCData: ... def wl_event_loop_destroy(event_loop: EventLoopCData) -> None: ... # Display functionality def wl_display_create() -> DisplayCData: ... def wl_display_destroy(display: DisplayCData) -> None: ... def wl_display_destroy_clients(display: DisplayCData) -> None: ... def wl_display_flush_clients(display: DisplayCData) -> None: ... def wl_display_get_event_loop(display: DisplayCData) -> EventLoopCData: ... def wl_display_add_socket(display: DisplayCData, name: bytes) -> int: ... def wl_display_add_socket_auto(display: DisplayCData) -> CharCData: ... def wl_display_terminate(display: DisplayCData) -> None: ... def wl_display_run(display: DisplayCData) -> None: ... def wl_display_get_serial(display: DisplayCData) -> int: ... def wl_display_next_serial(display: DisplayCData) -> int: ... def wl_display_init_shm(display: DisplayCData) -> int: ... def wl_display_add_shm_format(display: DisplayCData, format: int) -> int: ... def wl_event_queue_destroy(queue: QueueCData) -> None: ... def wl_display_connect(name: bytes | CData) -> DisplayCData: ... def wl_display_connect_to_fd(fd: int) -> DisplayCData: ... def wl_display_disconnect(display: DisplayCData) -> None: ... def wl_display_get_fd(display: DisplayCData) -> int: ... def wl_display_dispatch(display: DisplayCData) -> int: ... def wl_display_dispatch_pending(display: DisplayCData) -> int: ... def wl_display_dispatch_queue(display: DisplayCData, queue: QueueCData) -> int: ... def wl_display_dispatch_queue_pending( display: DisplayCData, queue: QueueCData ) -> int: ... def wl_display_roundtrip(display: DisplayCData) -> int: ... def wl_display_roundtrip_queue(display: DisplayCData, queue: QueueCData) -> int: ... def wl_display_get_error(display: DisplayCData) -> int: ... def wl_display_read_events(display: DisplayCData) -> int: ... def wl_display_prepare_read(display: DisplayCData) -> int: ... def wl_display_prepare_read_queue(display: DisplayCData, queue: QueueCData) -> int: ... def wl_display_flush(display: DisplayCData) -> int: ... def wl_display_create_queue(display: DisplayCData) -> QueueCData: ... # Client functionality def wl_client_create(display: DisplayCData, fd: int) -> ClientCData: ... def wl_client_destroy(client: ClientCData) -> None: ... def wl_client_flush(client: ClientCData) -> None: ... def wl_client_add_destroy_listener( client: ClientCData, listener: ListenerCData ) -> None: ... def wl_client_get_object(client: ClientCData, id: int) -> ResourceCData: ... def wl_client_get_credentials( client: ClientCData, pid: CData, uid: CData, gid: CData ) -> None: ... def os_create_anonymous_file(size: int) -> int: ... # Proxy functionality def wl_proxy_marshal_array(proxy, opcode, args_ptr) -> None: ... def wl_proxy_marshal_array_constructor( proxy, opcode, args_ptr, interface_ptr ) -> Any: ... # Resource functionality def wl_resource_post_event_array( resource: ResourceCData, opcode: int, args: CData ) -> None: ... def wl_resource_post_error( resource: ResourceCData, code: int, msg: bytes, *args: str ) -> None: ... def wl_resource_create( client: ClientCData, interface: InterfaceCData, version: int, id: int ) -> ResourceCData: ... def wl_resource_set_dispatcher( resource: ResourceCData, dispatcher: DispatcherFuncT, implementation: CData, data: CData, destroy: ResourceDestroyFuncT, ) -> None: ... def wl_resource_destroy(resource: ResourceCData) -> None: ... def wl_resource_get_id(resource: ResourceCData) -> int: ... def wl_resource_get_user_data(resource: ResourceCData) -> CData: ... def wl_resource_get_version(resource: ResourceCData) -> int: ... def wl_resource_get_client(resource: ResourceCData) -> ClientCData: ... def wl_resource_add_destroy_listener( resource: ResourceCData, listener: ListenerCData ) -> None: ... WL_EVENT_READABLE: int WL_EVENT_WRITABLE: int WL_EVENT_HANGUP: int WL_EVENT_ERROR: int pywayland-0.4.18/pywayland/_ffi/ffi.pyi0000664000175000017500000000267614651243522017743 0ustar epsilonepsilonfrom typing import Any, Callable, TypeVar, overload class CData: def __getitem__(self, idx: int): ... # required for wl_list_for_each @property def next(self) -> CData: ... class CDataArray: def __setitem__(self, idx: int, elem: CData) -> None: ... class DispatcherFuncT: ... class ResourceDestroyFuncT: ... class NotifyFuncT: ... # built-in cdata types class CharCData(CData): ... # wayland cdata types class ClientCData(CData): ... class DisplayCData(CData): ... class EventLoopCData(CData): ... class InterfaceCData(CData): ... class ListCData(CData): ... class ListenerCData(CData): link: ListCData notify: NotifyFuncT class QueueCData(CData): ... class ResourceCData(CData): ... # custom cdata types class ListenerContainerCData(CData): handle: CData destroy_listener: ListenerCData _FuncType = Callable[..., Any] _F = TypeVar("_F", bound=_FuncType) _CDataT = TypeVar("_CDataT", bound=CData) NULL: CData @overload def new(cdecl: str) -> CData: ... @overload def new(cdecl: str, init: Any) -> CDataArray: ... def gc( cdata: _CDataT, destructor: None | Callable[[_CDataT], None], size: int = 0 ) -> _CDataT: ... def string(cdata: CharCData) -> str: ... def release(cdata: CData) -> None: ... def def_extern() -> Callable[[_F], _F]: ... def new_handle(self: Any) -> CData: ... def from_handle(cdata: CData) -> Any: ... def cast(new_type: str, cdata: CData) -> CData: ... def addressof(cdata: _CDataT) -> _CDataT: ... pywayland-0.4.18/pywayland/client/0000775000175000017500000000000014651243522017014 5ustar epsilonepsilonpywayland-0.4.18/pywayland/client/display.py0000664000175000017500000002564114651243522021043 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations from typing import TYPE_CHECKING from weakref import WeakSet from pywayland import ffi, lib from pywayland.client.eventqueue import EventQueue from pywayland.protocol.wayland import WlDisplay from pywayland.utils import ensure_valid if TYPE_CHECKING: # introduced in standard library in Python 3.8 from typing_extensions import Literal class Display(WlDisplay.proxy_class): # type: ignore """Represents a connection to the compositor A :class:`Display` object represents a client connection to a Wayland compositor. The connection and the corresponding Wayland object are created with :func:`Display.connect`. The display must be connected before it can be used. A connection is terminated using :func:`Display.disconnect`. A :class:`Display` is also used as the :class:`~pywayland.client.proxy.Proxy` for the :class:`pywayland.protocol.wayland.WlDisplay` protocol object on the compositor side. A :class:`Display` object handles all the data sent from and to the compositor. When a :class:`~pywayland.client.proxy.Proxy` marshals a request, it will write its wire representation to the display's write buffer. The data is sent to the compositor when the client calls :func:`flush`. Incoming data is handled in two steps: queueing and dispatching. In the queue step, the data coming from the display fd is interpreted and added to a queue. On the dispatch step, the handler for the incoming event set by the client on the corresponding :class:`~pywayland.client.proxy.Proxy` is called. A :class:`Display` has at least one event queue, called the `default queue`. Clients can create additional event queues with :func:`Display.create_queue` and assign :class:`~pywayland.client.proxy.Proxy`'s to it. Events occurring in a particular proxy are always queued in its assigned queue. A client can ensure that a certain assumption, such as holding a lock or running from a given thread, is true when a proxy event handler is called by assigning that proxy to an event queue and making sure that this queue is only dispatched when the assumption holds. The default queue is dispatched by calling :func:`Display.dispatch`. This will dispatch any events queued on the default queue and attempt to read from the display fd if it's empty. Events read are then queued on the appropriate queues according to the proxy assignment. A user created queue is dispatched with :func:`Display.dispatch_queue`. This function behaves exactly the same as :func:`Display.dispatch` but it dispatches given queue instead of the default queue. A real world example of event queue usage is Mesa's implementation of glSwapBuffers() for the Wayland platform. This function might need to block until a frame callback is received, but dispatching the default queue could cause an event handler on the client to start drawing gain. This problem is solved using another event queue, so that only the events handled by the EGL code are dispatched during the block. :param name_or_fd: Either the name of the display to create or the file descriptor to connect the display to. If not specified, then use the default name, generally ``wayland-0`` :type name_or_fd: ``int`` or ``str`` """ def __init__(self, name_or_fd: int | str | None = None) -> None: """Constructor for the Display object""" super().__init__(None) self._children: WeakSet = WeakSet() self._name_or_fd = name_or_fd self._ptr: ffi.DisplayCData | None = None def __enter__(self) -> Display: """Connect to the display in a context manager""" self.connect() return self def __exit__(self, exc_type, exc_value, traceback) -> Literal[False]: """Disconnect from the display""" self.disconnect() return False def connect(self) -> None: """Connect to a Wayland display Connect to the Wayland display by name of fd. An `int` parameter opens the connection using the file descriptor. The :class:`Display` takes ownership of the fd and will close it when the display is destroyed. The fd will also be closed in case of failure. A string will open the display of the given name. If name is ``None``, its value will be replaced with the ``WAYLAND_DISPLAY`` environment variable if it is set, otherwise display ``"wayland-0"`` will be used. """ if isinstance(self._name_or_fd, int): # argument passed is a file descriptor self._ptr = lib.wl_display_connect_to_fd(self._name_or_fd) else: # connect using string by name, or use default if self._name_or_fd is None: name: ffi.CData | bytes = ffi.NULL else: name = self._name_or_fd.encode() self._ptr = lib.wl_display_connect(name) if self._ptr == ffi.NULL: raise ValueError("Unable to connect to display") self._ptr = ffi.gc(self._ptr, lib.wl_display_disconnect) def disconnect(self) -> None: """Close a connection to a Wayland display Close the connection to display and free all resources associated with it. """ if self._ptr: # we need to be sure the event queues and proxies are destroyed # before we disconnect the client for obj in self._children: if not obj.destroyed: obj.destroy() # run destructor and remove it ffi.release(self._ptr) self._ptr = None _destroy = disconnect @ensure_valid def get_fd(self) -> int: """Get a display context's file descriptor Return the file descriptor associated with a display so it can be integrated into the client's main loop. """ assert self._ptr is not None return lib.wl_display_get_fd(self._ptr) @ensure_valid def dispatch(self, *, block: bool = False, queue: EventQueue | None = None) -> int: """Process incoming events If block is `False`, it does not attempt to read the display fd or event queue and simply returns zero if the queue is empty. If the given queue is empty and `block` is `True`, this function blocks until there are events to be read from the display fd. Events are read and queued on the appropriate event queues. Finally, events on the default event queue are dispatched. .. note:: It is not possible to check if there are events on the queue or not. """ assert self._ptr is not None if block: if queue is None: ret = lib.wl_display_dispatch(self._ptr) else: assert queue._ptr is not None ret = lib.wl_display_dispatch_queue(self._ptr, queue._ptr) else: if queue is None: ret = lib.wl_display_dispatch_pending(self._ptr) else: assert queue._ptr is not None ret = lib.wl_display_dispatch_queue_pending(self._ptr, queue._ptr) if ret == -1: err = lib.wl_display_get_error(self._ptr) raise RuntimeError(f"Failed with error: {err}") return ret @ensure_valid def roundtrip(self, *, queue: EventQueue | None = None) -> int: """Block until all pending request are processed by the server This function blocks until the server has processed all currently issued requests by sending a request to the display server and waiting for a reply before returning. This function uses wl_display_dispatch_queue() internally. It is not allowed to call this function while the thread is being prepared for reading events, and doing so will cause a dead lock. .. note:: This function may dispatch other events being received on the default queue. :param queue: The queue on which to run the roundtrip, if not given, uses the default queue. :type queue: :class:`~pywayland.client.EventQueue` :returns: The number of dispatched events on success or -1 on failure """ assert self._ptr is not None if queue is None: return lib.wl_display_roundtrip(self._ptr) else: assert queue._ptr is not None return lib.wl_display_roundtrip_queue(self._ptr, queue._ptr) @ensure_valid def read(self, *, queue: EventQueue | None = None) -> None: """Read events from display file descriptor Calling this function will result in data available on the display file descriptor being read and read events will be queued on their corresponding event queues. :param queue: If specified, queue the events onto the given event queue, otherwise the default display queue will be used. """ assert self._ptr is not None while True: if queue is None: prepared = lib.wl_display_prepare_read(self._ptr) else: assert queue._ptr is not None prepared = lib.wl_display_prepare_read_queue(self._ptr, queue._ptr) if prepared == 0: break # TODO: add a blocking/non-blocking condition here self.dispatch(block=False, queue=queue) status = lib.wl_display_read_events(self._ptr) if status != 0: raise RuntimeError("Failed to read events") @ensure_valid def flush(self) -> int: """Send all buffered requests on the display to the server Send all buffered data on the client side to the server. Clients should call this function before blocking. On success, the number of bytes sent to the server is returned. On failure, this function returns -1 and errno is set appropriately. :func:`Display.flush` never blocks. It will write as much data as possible, but if all data could not be written, errno will be set to EAGAIN and -1 returned. In that case, use poll on the display file descriptor to wait for it to become writable again. """ assert self._ptr is not None return lib.wl_display_flush(self._ptr) pywayland-0.4.18/pywayland/client/__init__.py0000664000175000017500000000121514651243522021124 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .display import Display # noqa from .eventqueue import EventQueue # noqa pywayland-0.4.18/pywayland/client/eventqueue.py0000664000175000017500000000533214651243522021557 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import functools from typing import TYPE_CHECKING from weakref import WeakKeyDictionary from pywayland import ffi, lib if TYPE_CHECKING: from pywayland.client import Display weakkeydict: WeakKeyDictionary = WeakKeyDictionary() def _event_queue_destroy(display: Display, cdata: ffi.QueueCData) -> None: # we should be careful that the display is still around if display._ptr is None: return lib.wl_event_queue_destroy(cdata) class EventQueue: """A queue for wl_proxy object events. Event queues allows the events on a display to be handled in a thread-safe manner. See :class:`~pywayland.client.Display` for details. :param display: The display object that the event queue is connected to. :type display: :class:`~pywayland.client.Display` """ def __init__(self, display: Display) -> None: """Constructor for the EventQueue object""" # check that we attach to a valid display if display._ptr is None or display._ptr == ffi.NULL: raise ValueError("Display object not connected") ptr = lib.wl_display_create_queue(display._ptr) # create a destructor, save data and display destructor = functools.partial(_event_queue_destroy, display) self._ptr: ffi.QueueCData | None = ffi.gc(ptr, destructor) self._display: Display | None = display display._children.add(self) weakkeydict[self._ptr] = display @property def destroyed(self) -> bool: """Determine the state of the event queue""" return self._ptr is None def destroy(self) -> None: """Destroy an event queue Destroy the given event queue. Any pending event on that queue is discarded. The wl_display object used to create the queue should not be destroyed until all event queues created with it are destroyed with this function. """ if self._ptr is not None and self._display is not None: ffi.release(self._ptr) # delete the pointer and the reference to the display self._ptr = None self._display = None pywayland-0.4.18/pywayland/protocol/0000775000175000017500000000000014651243522017377 5ustar epsilonepsilonpywayland-0.4.18/pywayland/protocol/__init__.py0000664000175000017500000000107414651243522021512 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. pywayland-0.4.18/pywayland/protocol_core/0000775000175000017500000000000014651243522020407 5ustar epsilonepsilonpywayland-0.4.18/pywayland/protocol_core/proxy.py0000664000175000017500000001015314651243522022142 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations from typing import TYPE_CHECKING, Generic, TypeVar from pywayland import ffi, lib from pywayland.dispatcher import Dispatcher from pywayland.utils import ensure_valid if TYPE_CHECKING: from .interface import Interface T = TypeVar("T", bound=Interface) InterfaceT = TypeVar("InterfaceT", bound=Interface) else: T = TypeVar("T") class Proxy(Generic[T]): interface: type[T] def __init__(self, ptr, display=None): """Represents a protocol object on the client side. A :class:`Proxy` acts as a client side proxy to an object existing in the compositor. Events coming from the compositor are also handled by the proxy, which will in turn call the handler set with :func:`Proxy.add_listener`. """ self.user_data = None self.dispatcher = Dispatcher(self.interface.events) # This should only be true for wl_display proxies, as they will # initialize its pointer on a `.connect()` call if ptr is None: self._ptr = ptr self._display = self return self._display = display # parent display is the root-most client Display object, all proxies # should keep the display alive if display is None: raise ValueError( "Non-Display Proxy objects must be associated to a Display" ) display._children.add(self) if ptr == ffi.NULL: raise RuntimeError("Got a null pointer for the proxy") # note that even though we cast to a proxy here, the ptr may be a # wl_display, so the methods must still cast to 'struct wl_proxy *' ptr = ffi.cast("struct wl_proxy *", ptr) self._ptr = ffi.gc(ptr, lib.wl_proxy_destroy) self._handle = ffi.new_handle(self) lib.wl_proxy_add_dispatcher( self._ptr, lib.dispatcher_func, self._handle, ffi.NULL ) self.interface.registry[self._ptr] = self @property def destroyed(self) -> bool: """Determine if proxy has been destroyed Returns true if the proxy has been destroyed. """ return self._ptr is None def _destroy(self) -> None: """Frees the pointer associated with the Proxy""" if self._ptr is not None: if self._display._ptr is not None: ffi.release(self._ptr) else: self._ptr = ffi.gc(self._ptr, None) self._ptr = None destroy = _destroy @ensure_valid def _marshal(self, opcode, *args) -> None: """Marshal the given arguments into the Wayland wire format""" # Create a wl_argument array args_ptr = self.interface.requests[opcode].arguments_to_c(*args) # Write the event into the connection queue proxy = ffi.cast("struct wl_proxy *", self._ptr) lib.wl_proxy_marshal_array(proxy, opcode, args_ptr) def _marshal_constructor( self, opcode: int, interface: type[InterfaceT], *args ) -> Proxy[InterfaceT]: """Marshal the given arguments into the Wayland wire format for a constructor""" # Create a wl_argument array args_ptr = self.interface.requests[opcode].arguments_to_c(*args) # Write the event into the connection queue and build a new proxy from the given args proxy = ffi.cast("struct wl_proxy *", self._ptr) proxy_ptr = lib.wl_proxy_marshal_array_constructor( proxy, opcode, args_ptr, interface._ptr ) return interface.proxy_class(proxy_ptr, self._display) pywayland-0.4.18/pywayland/protocol_core/globals.py0000664000175000017500000000657014651243522022414 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import functools from weakref import WeakKeyDictionary from pywayland import ffi, lib weakkeydict: WeakKeyDictionary = WeakKeyDictionary() # void (*wl_global_bind_func_t)(struct wl_client *client, void *data, uint32_t version, uint32_t id) @ffi.def_extern() def global_bind_func(client_ptr, data, version, id) -> None: # `data` is the handle to Global callback_info = ffi.from_handle(data) version = min(callback_info["interface"].version, version) resource = callback_info["interface"].resource_class(client_ptr, version, id) # Call a user defined handler if callback_info["bind_func"]: # TODO: add some error catching so we don't segfault callback_info["bind_func"](resource) def _global_destroy(display, cdata) -> None: if display._ptr is not None: # TODO: figure out how this can get run... # lib.wl_global_destroy(cdata) pass class Global: """A server-side Interface object for the server Not created directly, created from the :class:`~pywayland.interface.Interface` object. :param display: The display the object is created on :type display: :class:`~pywayland.server.Display` :param version: The version to use for the :class:`~pywayland.interface.Interface`, uses current version if not specified :type version: `int` """ def __init__(self, display, version=None): if display._ptr is None or display._ptr == ffi.NULL: raise ValueError("Display has been destroyed or couldn't initialize") if version is None: version = self.interface.version # we can't keep alive a handle to self without creating a reference # loop, so use this dict as the handle to pass to the global_bind_func # callback self._callback_info = {"interface": self.interface, "bind_func": None} self._handle = ffi.new_handle(self._callback_info) ptr = lib.wl_global_create( display._ptr, self.interface._ptr, version, self._handle, lib.global_bind_func, ) destructor = functools.partial(_global_destroy, display) self._ptr = ffi.gc(ptr, destructor) self._display = display # this c data should keep the display alive weakkeydict[self._ptr] = display @property def bind_func(self): return self._callback_info["bind_func"] @bind_func.setter def bind_func(self, value): self._callback_info["bind_func"] = value def destroy(self): """Destroy the global object""" if self._ptr is not None: # run and remove destructor on c data _global_destroy(self._display, self._ptr) ffi.gc(self._ptr, None) self._ptr = None self._display = None pywayland-0.4.18/pywayland/protocol_core/__init__.py0000664000175000017500000000213114651243522022515 0ustar epsilonepsilon# Copyright 2019 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """The core objects used by the protocol definitions This defines the core objects that are used in building up the protocols and interfaces. Each interface has a set of requests and events that are associated with it. These functions are invoked from the corresponding proxy and resource instances. """ from .argument import Argument, ArgumentType # noqa: F401 from .globals import Global # noqa: F401 from .interface import Interface # noqa: F401 from .proxy import Proxy # noqa: F401 from .resource import Resource # noqa: F401 pywayland-0.4.18/pywayland/protocol_core/message.py0000664000175000017500000002202214651243522022403 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations from operator import attrgetter from typing import Callable, Iterable from weakref import WeakKeyDictionary from pywayland import ffi, lib from .argument import Argument, ArgumentType weakkeydict: WeakKeyDictionary = WeakKeyDictionary() class Message: """Wrapper class for `wl_message` structs Base class that correspond to the methods defined on an interface in the wayland.xml protocol, and are generated by the scanner. Subclasses specify the type of method, whether it is a server-side or client-side method. :param func: The function that is represented by the message :type func: `function` :param signature: The signature of the arguments of the message :type signature: `string` :param types: List of the types of any objects included in the argument list, None if otherwise. :type types: `list` """ def __init__( self, func: Callable, arguments: list[Argument], version: int | None ) -> None: self.py_func = func self.name = func.__name__.strip("_") self.arguments = arguments self.version = version @property def _marshaled_arguments(self) -> Iterable[Argument]: for arg in self.arguments: if arg.interface is None and arg.argument_type == ArgumentType.NewId: yield Argument(ArgumentType.String) yield Argument(ArgumentType.Uint) yield arg def build_message_struct(self, wl_message_struct) -> tuple: """Bulid the wl_message struct for this message :param wl_message_struct: The wl_message cdata struct to use to build the message struct. :return: A tuple of elements which must be kept alive for the message struct to remain valid. """ signature = "".join(argument.signature for argument in self.arguments) if self.version is not None: signature = f"{self.version}{signature}" wl_message_struct.name = name = ffi.new("char[]", self.name.encode()) wl_message_struct.signature = cdata_signature = ffi.new( "char[]", signature.encode() ) wl_message_struct.types = types = ffi.new( "struct wl_interface* []", len(list(self._marshaled_arguments)) ) for index, argument in enumerate(self._marshaled_arguments): if argument.interface is None: types[index] = ffi.NULL else: assert argument.interface._ptr is not None types[index] = argument.interface._ptr return name, cdata_signature, types def c_to_arguments(self, args_ptr): """Create a list of arguments Generate the arguments of the method from a CFFI cdata array of `wl_argument` structs that correspond to the arguments of the method as specified by the method signature. :param args_ptr: Input arguments :type args_ptr: cdata `union wl_argument []` :returns: list of args """ args = [] for i, argument in enumerate(self.arguments): arg_ptr = args_ptr[i] # Match numbers (int, unsigned, float, file descriptor) if argument.argument_type == ArgumentType.Int: args.append(arg_ptr.i) elif argument.argument_type == ArgumentType.Uint: args.append(arg_ptr.u) elif argument.argument_type == ArgumentType.Fixed: f = lib.wl_fixed_to_double(arg_ptr.f) args.append(f) elif argument.argument_type == ArgumentType.FileDescriptor: args.append(arg_ptr.h) elif argument.argument_type == ArgumentType.String: if arg_ptr == ffi.NULL: if not argument.nullable: raise Exception args.append(None) else: args.append(ffi.string(arg_ptr.s).decode()) elif argument.argument_type == ArgumentType.Object: if arg_ptr.o == ffi.NULL: if not argument.nullable: message = f"Got null object parsing arguments for '{self.name}' message, may already be destroyed" raise RuntimeError(message) args.append(None) else: iface = argument.interface proxy_ptr = ffi.cast("struct wl_proxy *", arg_ptr.o) obj = iface.registry.get(proxy_ptr) if obj is None: raise RuntimeError( f"Unable to get object for {proxy_ptr}, was it garbage collected?" ) args.append(obj) elif argument.argument_type == ArgumentType.NewId: from pywayland.protocol.wayland.wl_registry import WlRegistry if ( display := next( map(attrgetter("_display"), WlRegistry.registry.values()), None ) ) is None: raise RuntimeError("Cannot find display") iface = argument.interface assert iface proxy_ptr = ffi.cast("struct wl_proxy *", arg_ptr.o) obj = iface.proxy_class(proxy_ptr, display) args.append(obj) elif argument.argument_type == ArgumentType.Array: array_ptr = arg_ptr.a args.append(ffi.buffer(array_ptr.data, array_ptr.size)[:]) else: raise Exception(f"Bad argument: {argument}") return args def arguments_to_c(self, *args): """Create an array of `wl_argument` C structs Generate the CFFI cdata array of `wl_argument` structs that correspond to the arguments of the method as specified by the method signature. :param args: Input arguments :type args: `list` :returns: cdata `union wl_argument []` of args """ nargs = len(list(self._marshaled_arguments)) args_ptr = ffi.new("union wl_argument []", nargs) arg_iter = iter(args) refs = [] for i, argument in enumerate(self._marshaled_arguments): # New id (set to null for now, will be assigned on marshal) # Then, continue so we don't consume an arg if argument.argument_type == ArgumentType.NewId: args_ptr[i].o = ffi.NULL continue arg = next(arg_iter) # Match numbers (int, unsigned, float, file descriptor) if argument.argument_type == ArgumentType.Int: args_ptr[i].i = arg elif argument.argument_type == ArgumentType.Uint: args_ptr[i].u = arg elif argument.argument_type == ArgumentType.Fixed: if isinstance(arg, int): f = lib.wl_fixed_from_int(arg) else: f = lib.wl_fixed_from_double(arg) args_ptr[i].f = f elif argument.argument_type == ArgumentType.FileDescriptor: args_ptr[i].h = arg elif argument.argument_type == ArgumentType.String: if arg is None: if not argument.nullable: raise Exception new_arg = ffi.NULL else: new_arg = ffi.new("char []", arg.encode()) refs.append(new_arg) args_ptr[i].s = new_arg elif argument.argument_type == ArgumentType.Object: if arg is None: if not argument.nullable: raise Exception new_arg = ffi.NULL else: new_arg = ffi.cast("struct wl_object *", arg._ptr) refs.append(new_arg) args_ptr[i].o = new_arg elif argument.argument_type == ArgumentType.Array: # TODO: this is a bit messy, we probably don't want to put everything in one buffer like this new_arg = ffi.new("struct wl_array *") new_data = ffi.new("void []", len(arg)) new_arg.alloc = new_arg.size = len(arg) ffi.buffer(new_data)[:] = arg refs.append(new_arg) refs.append(new_data) if len(refs) > 0: weakkeydict[args_ptr] = tuple(refs) return args_ptr pywayland-0.4.18/pywayland/protocol_core/resource.py0000664000175000017500000000677414651243522022626 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations from typing import TYPE_CHECKING, Generic, TypeVar from pywayland import ffi, lib from pywayland.dispatcher import Dispatcher from pywayland.server.client import Client from pywayland.utils import ensure_valid if TYPE_CHECKING: from .interface import Interface T = TypeVar("T", bound=Interface) else: T = TypeVar("T") class Resource(Generic[T]): """A server-side Interface object for the client Not created directly, created from the :class:`~pywayland.interface.Interface` object. :param client: The client that the Resource is for :type client: :class:`~pywayland.server.Client` or cdata for ``wl_client *`` :param version: The version to use for the :class:`~pywayland.interface.Interface`, uses current version if not specified :type version: `int` :param id: The id for the item :type id: `int` """ interface: type[T] def __init__(self, client, version: int | None = None, id: int = 0) -> None: if version is None: version = self.interface.version self.version = version self.dispatcher = Dispatcher(self.interface.requests, destructor=True) if isinstance(client, Client): client_ptr = client._ptr else: client_ptr = client assert client_ptr is not None self._ptr: ffi.ResourceCData | None = lib.wl_resource_create( client_ptr, self.interface._ptr, version, id ) self.id = lib.wl_resource_get_id(self._ptr) if self.dispatcher is not None: self._handle = ffi.new_handle(self) lib.wl_resource_set_dispatcher( self._ptr, lib.dispatcher_func, ffi.NULL, self._handle, lib.resource_destroy_func, ) def destroy(self) -> None: """Destroy the Resource""" if self._ptr: lib.wl_resource_destroy(self._ptr) self._ptr = None @ensure_valid def add_destroy_listener(self, listener) -> None: """Add a listener for the destroy signal :param listener: The listener object :type listener: :class:`~pywayland.server.Listener` """ assert self._ptr is not None lib.wl_resource_add_destroy_listener(self._ptr, listener._ptr) @ensure_valid def _post_event(self, opcode, *args) -> None: # Create wl_argument array args_ptr = self.interface.events[opcode].arguments_to_c(*args) # Make the cast to a wl_resource assert self._ptr is not None resource: ffi.ResourceCData = ffi.cast("struct wl_resource *", self._ptr) # type: ignore[assignment] lib.wl_resource_post_event_array(resource, opcode, args_ptr) @ensure_valid def _post_error(self, code, msg="") -> None: assert self._ptr is not None lib.wl_resource_post_error(self._ptr, code, msg.encode()) pywayland-0.4.18/pywayland/protocol_core/argument.py0000664000175000017500000000374314651243522022612 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING from pywayland.scanner.argument import ArgumentType if TYPE_CHECKING: from .interface import Interface class classproperty: def __init__(self, f): self.f = f def __get__(self, obj, owner): return self.f(owner) @dataclass(frozen=True) class Argument: argument_type: ArgumentType nullable: bool = False interface: type[Interface] | None = None @property def signature(self) -> str: if self.argument_type == ArgumentType.Int: base_signature = "i" elif self.argument_type == ArgumentType.Uint: base_signature = "u" elif self.argument_type == ArgumentType.Fixed: base_signature = "f" elif self.argument_type == ArgumentType.String: base_signature = "s" elif self.argument_type == ArgumentType.Object: base_signature = "o" elif self.argument_type == ArgumentType.NewId: if self.interface is None: base_signature = "sun" else: base_signature = "n" elif self.argument_type == ArgumentType.Array: base_signature = "a" elif self.argument_type == ArgumentType.FileDescriptor: base_signature = "h" if self.nullable: return "?" + base_signature return base_signature pywayland-0.4.18/pywayland/protocol_core/interface.py0000664000175000017500000001053314651243522022723 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations from typing import Callable from weakref import WeakKeyDictionary, WeakValueDictionary from pywayland import ffi from .argument import Argument from .globals import Global from .message import Message from .proxy import Proxy from .resource import Resource weakkeydict: WeakKeyDictionary = WeakKeyDictionary() class InterfaceMeta(type): """Metaclass for Interfaces Initializes empty lists for events and requests for the given class. """ def __init__(self, name, bases, dct): self.events = [] self.requests = [] # Initialize the interface cdata self._ptr = ffi.new("struct wl_interface *") class Interface(metaclass=InterfaceMeta): """Wrapper class for wl_wayland structs Base class for interfaces that are defined by the wayland.xml class and generated by the scanner. Sub-classes should use the :class:`InterfaceMeta` metaclass, which will define subclass.events and subclass.requests, the lists of the methods on this interface. These class variables are populated by the :func:`Interface.event` and :func:`Interface.request` decorators. """ _ptr: ffi.InterfaceCData name: str version: int proxy_class: type[Proxy] resource_class: type[Resource] global_class: type[Global] @classmethod def event(cls, *arguments: Argument, version: int | None = None) -> Callable: """Decorator for interface events Adds the decorated method to the list of events of the interface (server-side method). :param signature: Encodes the types of the arguments to the decorated function. :type signature: `string` :param types: List of the types of any objects included in the argument list, None if otherwise. :type types: `list` """ def wrapper(func): cls.events.append(Message(func, arguments, version)) return func return wrapper @classmethod def request(cls, *arguments: Argument, version: int | None = None): """Decorator for interface requests Adds the decorated method to the list of requests of the interface (client-side method). :param signature: Encodes the types of the arguments to the decorated function. :type signature: `string` :param types: List of the types of any objects included in the argument list, None if otherwise. :type types: list """ def wrapper(func): cls.requests.append(Message(func, arguments, version)) return func return wrapper @classmethod def _gen_c(cls): """Creates the wl_interface C struct Generates the CFFI cdata for the wl_interface struct given by the interface. """ cls.registry = WeakValueDictionary() cls._ptr.name = name = ffi.new("char[]", cls.name.encode()) cls._ptr.version = cls.version keep_alive = [] # Determine the number of methods to assign and assign them cls._ptr.method_count = len(cls.requests) cls._ptr.methods = methods_ptr = ffi.new( "struct wl_message[]", len(cls.requests) ) # Iterate over the methods for i, message in enumerate(cls.requests): keep_alive.extend(message.build_message_struct(methods_ptr[i])) cls._ptr.event_count = len(cls.events) cls._ptr.events = events_ptr = ffi.new("struct wl_message[]", len(cls.events)) # Iterate over the methods for i, message in enumerate(cls.events): keep_alive.extend(message.build_message_struct(events_ptr[i])) weakkeydict[cls._ptr] = (name, methods_ptr, events_ptr, *tuple(keep_alive)) pywayland-0.4.18/pywayland/dispatcher.py0000664000175000017500000000712714651243522020245 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import traceback from typing import Callable, Optional from pywayland import ffi, lib from pywayland.protocol_core.message import Message CallbackT = Callable[..., Optional[int]] # int (*wl_dispatcher_func_t)(const void *, void *, uint32_t, const struct wl_message *, union wl_argument *) @ffi.def_extern() def dispatcher_func(data, target, opcode, message, c_args): # `data` is the handle to proxy/resource python object # `target` is the wl_proxy/wl_resource for self, this should be the same as self._ptr # `message` is the wl_message for self._interface.events/requests[opcode] # TODO: handle any user_data attached to the wl_proxy/wl_resource # get the proxy/resource object from the user data handle self = ffi.from_handle(data) # get the callback func = self.dispatcher[opcode] if func is None: return 0 # rebuild the args into python objects args = self.dispatcher.messages[opcode].c_to_arguments(c_args) try: ret = func(self, *args) except Exception: traceback.print_exc() return 0 if ret is None: return 0 else: return ret # void (*wl_resource_destroy_func_t)(struct wl_resource *resource) @ffi.def_extern() def resource_destroy_func(res_ptr): # the user data to the resource is the handle to the resource resource_handle = lib.wl_resource_get_user_data(res_ptr) resource = ffi.from_handle(resource_handle) # if the destructor has been set, run it func = resource.dispatcher.destructor if func is not None: func(resource) class Dispatcher: """Dispatches events or requests from an interface Handles the dispatching of callbacks from events (for :class:`~pywayland.sever.resource.Resource` objects) and requests (for :class:`~pywayland.client.proxy.Proxy` objects). The callbacks for a given message can be set and retrieved by indexing the dispatcher with either the opcode or the name of the message. :param messages: List of messages (events or requests) :type messages: `list` :param destructor: Create destructor dispatcher (for Resources) :type destructor: `bool` """ def __init__(self, messages: list[Message], destructor: bool = False) -> None: self.messages = messages # Create a map of message names to message opcodes self._names = {msg.name: opcode for opcode, msg in enumerate(messages)} self._callback: list[CallbackT | None] = [None] * len(messages) if destructor: self.destructor = None def __getitem__(self, opcode_or_name: str | int) -> CallbackT | None: if isinstance(opcode_or_name, str): opcode_or_name = self._names[opcode_or_name] return self._callback[opcode_or_name] def __setitem__(self, opcode_or_name: str | int, function: CallbackT) -> None: if isinstance(opcode_or_name, str): opcode_or_name = self._names[opcode_or_name] self._callback[opcode_or_name] = function pywayland-0.4.18/pywayland/scanner/0000775000175000017500000000000014651243522017167 5ustar epsilonepsilonpywayland-0.4.18/pywayland/scanner/request.py0000664000175000017500000001241614651243522021235 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import xml.etree.ElementTree as ET from dataclasses import dataclass from typing import Iterable from .argument import Argument, ArgumentType from .description import Description from .method import Method from .printer import Printer # For 'new_id' types with no 'interface' NO_IFACE = "interface" NO_IFACE_VERSION = "version" @dataclass(frozen=True) class Request(Method): """Scanner for request objects (client-side method) Required attributes: `name` Optional attributes: `type` and `since` Child elements: `description` and `arg` """ method_type = "request" type: str | None @classmethod def parse(cls, element: ET.Element) -> Request: name = cls.parse_attribute(element, "name") if name in ("global", "import"): name += "_" return cls( name=name, since=cls.parse_optional_attribute(element, "since"), description=cls.parse_optional_child(element, Description, "description"), arg=cls.parse_repeated_child(element, Argument, "arg"), type=cls.parse_optional_attribute(element, "type"), ) @property def new_id(self) -> Argument | None: for arg in self.arg: if arg.type == ArgumentType.NewId: return arg return None @property def method_args(self) -> Iterable[str]: """Generator of the arguments to the method The `new_id` args are generated in marshaling the args, they do not appear in the args of the method. """ for arg in self.arg: if arg.type == ArgumentType.NewId: # An interface of known type is created for us if arg.interface: continue # A `new_id` with no interface, c.f. wl_registry_bind # Need a string (interface name) and int (interface version) yield f"{NO_IFACE}: type[T]" yield f"{NO_IFACE_VERSION}: int" else: yield arg.signature @property def marshal_args(self) -> Iterable[str]: """Arguments sent to `._marshal`""" for arg in self.arg: if arg.type == ArgumentType.NewId: if not arg.interface: yield f"{NO_IFACE}.name" yield NO_IFACE_VERSION else: yield arg.name def output_doc_params(self, printer: Printer) -> None: """Aguments documented as parameters Anything that is not a `new_id` is """ ret = None for arg in self.arg: if arg.type == ArgumentType.NewId: ret = arg if arg.interface: continue printer(f":param {NO_IFACE}:") printer(" Interface name") printer(f":type {NO_IFACE}:") printer(" `string`") printer(f":param {NO_IFACE_VERSION}:") printer(" Interface version") printer(f":type {NO_IFACE_VERSION}:") printer(" `int`") else: arg.output_doc_param(printer) if ret is not None: ret.output_doc_ret(printer) def output_doc_ret(self, printer: Printer) -> None: """Aguments documented as return values Arguments of type `new_id` are returned from requests. """ for arg in self.arg: if arg.type == ArgumentType.NewId: arg.output_doc_ret(printer) def output_body(self, printer: Printer, opcode: int) -> None: """Output the body of the request to the printer""" if self.new_id: if self.new_id.interface: id_class = self.new_id.interface_class else: id_class = NO_IFACE args = ", ".join([str(opcode), id_class, *list(self.marshal_args)]) printer(f"{self.new_id.name} = self._marshal_constructor({args})") printer(f"return {self.new_id.name}") else: args = ", ".join([str(opcode), *list(self.marshal_args)]) printer(f"self._marshal({args})") if self.type == "destructor": printer("self._destroy()") @property def return_type(self) -> str: """The return type for the request.""" if self.new_id: if self.new_id.interface: return f"Proxy[{self.new_id.interface_class}]" else: return "Proxy[T]" return "None" @property def needs_any(self) -> bool: for arg in self.arg: if arg.type == ArgumentType.Object and not arg.interface: return True return False pywayland-0.4.18/pywayland/scanner/description.py0000664000175000017500000000231314651243522022063 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import xml.etree.ElementTree as ET from dataclasses import dataclass from .element import Element from .printer import Printer @dataclass(frozen=True) class Description(Element): summary: str text: str | None @classmethod def parse(cls, element: ET.Element) -> Description: return cls( summary=cls.parse_attribute(element, "summary"), text=cls.parse_pcdata(element), ) def output(self, printer: Printer) -> None: printer.doc(f'"""{self.summary.capitalize()}') if self.text: printer() printer.docstring(self.text) pywayland-0.4.18/pywayland/scanner/__init__.py0000664000175000017500000000115214651243522021277 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .protocol import Protocol # noqa: F401 pywayland-0.4.18/pywayland/scanner/event.py0000664000175000017500000000507514651243522020671 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import xml.etree.ElementTree as ET from dataclasses import dataclass from typing import Iterator from .argument import Argument, ArgumentType from .description import Description from .method import Method from .printer import Printer @dataclass(frozen=True) class Event(Method): """Scanner for event objects (server-side method) Required attributes: `name` Optional attributes: `since` Child elements: `description` and `arg`` """ method_type = "event" @classmethod def parse(cls, element: ET.Element) -> Event: name = cls.parse_attribute(element, "name") if name in ("global", "import"): name += "_" return cls( name=name, since=cls.parse_optional_attribute(element, "since"), description=cls.parse_optional_child(element, Description, "description"), arg=cls.parse_repeated_child(element, Argument, "arg"), ) @property def method_args(self) -> Iterator[str]: """Generator of the arguments to the method All arguments to be sent to `._post_event` must be passed in """ for arg in self.arg: yield arg.signature def output_doc_params(self, printer: Printer) -> None: """Aguments documented as parameters All arguments are event parameters. """ for arg in self.arg: arg.output_doc_param(printer) def output_body(self, printer: Printer, opcode: int) -> None: """Output the body of the event to the printer""" args = ", ".join([str(opcode)] + [arg.name for arg in self.arg]) printer(f"self._post_event({args})") @property def return_type(self) -> str: return "None" @property def needs_any(self) -> bool: for arg in self.arg: if ( arg.type in (ArgumentType.Object, ArgumentType.NewId) and not arg.interface ): return True return False pywayland-0.4.18/pywayland/scanner/copyright.py0000664000175000017500000000337114651243522021555 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import xml.etree.ElementTree as ET from dataclasses import dataclass from .element import Element from .printer import Printer copyright_default = """\ # Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License.""" @dataclass(frozen=True) class Copyright(Element): text: str @classmethod def parse(cls, element: ET.Element) -> Copyright: text = cls.parse_pcdata(element) assert text is not None return cls(text=text) def output(self, printer: Printer) -> None: for line in self.text.split("\n"): if line: printer("# " + line.rstrip()) else: printer("#") pywayland-0.4.18/pywayland/scanner/method.py0000664000175000017500000001017314651243522021023 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import abc from dataclasses import dataclass from typing import Iterable from .argument import Argument from .description import Description from .element import Element from .printer import Printer @dataclass(frozen=True) class Method(Element, abc.ABC): """Scanner for methods Corresponds to event and requests defined on an interface """ name: str since: str | None description: Description | None arg: list[Argument] def imports( self, interface: str, module_imports: dict[str, str] ) -> list[tuple[str, str]]: """Get the imports required for each of the interfaces :param interface: The name of the interface that the method is a part of. :param module_imports: A mapping from the name of an interface in the associated module that the interface comes from. :return: A list of 2-tuples, each specifying the path to an imported module and the imported class. """ current_protocol = module_imports[interface] imports = [] for arg in self.arg: if arg.interface is None: continue if arg.interface == interface: continue import_class = arg.interface_class import_protocol = module_imports[arg.interface] if current_protocol == import_protocol: import_path = f".{arg.interface}" else: import_path = f"..{import_protocol}" imports.append((import_path, import_class)) return imports @property @abc.abstractmethod def method_type(self) -> str: pass @property @abc.abstractmethod def method_args(self) -> Iterable[str]: pass @property @abc.abstractmethod def return_type(self) -> str: pass @abc.abstractmethod def output_doc_params(self, printer: Printer) -> None: pass @abc.abstractmethod def output_body(self, printer: Printer, opcode: int) -> None: pass def output( self, printer: Printer, opcode: int, in_class: str, module_imports: dict[str, str], ) -> None: """Generate the output for the given method to the printer""" if len(self.arg) > 0: printer(f"@{in_class}.{self.method_type}(") with printer.indented(): for arg in self.arg: printer(arg.argument + ",") if self.since: printer(f"version={self.since},") printer(")") else: if self.since: printer(f"@{in_class}.{self.method_type}(version={self.since})") else: printer(f"@{in_class}.{self.method_type}()") # Generate the definition of the method and args args = ", ".join(["self", *list(self.method_args)]) printer(f"def {self.name}({args}) -> {self.return_type}:") with printer.indented(): # Write the documentation self.output_doc(printer) # Write out the body of the method self.output_body(printer, opcode) def output_doc(self, printer: Printer) -> None: """Output the documentation for the interface""" if self.description: self.description.output(printer) else: printer('"""' + self.name) # Parameter and returns documentation if self.arg: printer() self.output_doc_params(printer) printer('"""') pywayland-0.4.18/pywayland/scanner/argument.py0000664000175000017500000001417314651243522021371 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import enum import xml.etree.ElementTree as ET from dataclasses import dataclass from .description import Description from .element import Element from .printer import Printer NO_IFACE_NAME = "interface" @enum.unique class ArgumentType(enum.Enum): Int = enum.auto() Uint = enum.auto() Fixed = enum.auto() String = enum.auto() Object = enum.auto() NewId = enum.auto() Array = enum.auto() FileDescriptor = enum.auto() @dataclass(frozen=True) class Argument(Element): """Argument to a request or event method Required attributes: `name` and `type` Optional attributes: `summary`, `interface`, and `allow-null` Child elements: `description` """ name: str type: ArgumentType summary: str | None interface: str | None allow_null: bool enum: str | None description: Description | None @classmethod def parse(cls, element: ET.Element) -> Argument: arg_type_str = cls.parse_attribute(element, "type") if arg_type_str == "int": argument_type = ArgumentType.Int elif arg_type_str == "uint": argument_type = ArgumentType.Uint elif arg_type_str == "fixed": argument_type = ArgumentType.Fixed elif arg_type_str == "string": argument_type = ArgumentType.String elif arg_type_str == "object": argument_type = ArgumentType.Object elif arg_type_str == "new_id": argument_type = ArgumentType.NewId elif arg_type_str == "array": argument_type = ArgumentType.Array elif arg_type_str == "fd": argument_type = ArgumentType.FileDescriptor else: raise ValueError(f"Invalid argument type: {arg_type_str}") allow_null = cls.parse_optional_attribute(element, "allow-null") == "true" return cls( name=cls.parse_attribute(element, "name"), type=argument_type, summary=cls.parse_optional_attribute(element, "summary"), interface=cls.parse_optional_attribute(element, "interface"), allow_null=allow_null, enum=cls.parse_optional_attribute(element, "enum"), description=cls.parse_optional_child(element, Description, "description"), ) @property def interface_class(self) -> str: """Returns the Interface class name Gives the class name for the Interface coresponding to the type of the argument. """ assert self.interface is not None return "".join(x.capitalize() for x in self.interface.split("_")) @property def signature(self) -> str: """Output as the argument appears in the signature.""" return f"{self.name}: {self._annotation}" @property def _annotation(self) -> str: """The type annotation for the argument.""" if self.type == ArgumentType.Int: base_annotation = "int" elif self.type == ArgumentType.Uint: base_annotation = "int" elif self.type == ArgumentType.Fixed: base_annotation = "float" elif self.type == ArgumentType.String: base_annotation = "str" elif self.type == ArgumentType.Object: if self.interface: base_annotation = self.interface_class else: base_annotation = "Any" elif self.type == ArgumentType.NewId: if self.interface: base_annotation = self.interface_class else: base_annotation = "Any" elif self.type == ArgumentType.Array: base_annotation = "list" elif self.type == ArgumentType.FileDescriptor: base_annotation = "int" if self.allow_null: return f"{base_annotation} | None" return base_annotation @property def argument(self) -> str: """Output as an Argument""" args = [f"ArgumentType.{self.type.name}"] if self.interface is not None: args.append(f"interface={self.interface_class}") if self.allow_null: args.append("nullable=True") return f"Argument({', '.join(args)})" def output_doc_param(self, printer: Printer) -> None: """Document the argument as a parameter""" # Output the parameter and summary printer.doc(f":param {self.name}:") if self.summary: with printer.indented(): printer.docstring(self.summary) # Determine the type to be output if self.interface: arg_type = self.interface else: arg_type = f"`{self.type}`" # Output the parameter type printer.doc(f":type {self.name}:") with printer.indented(): if self.allow_null: printer.doc(f"{arg_type} or `None`") else: printer.doc(f"{arg_type}") def output_doc_ret(self, printer: Printer) -> None: """Document the argument as a return""" # Determine the type to be output if self.interface: arg_type = self.interface else: # Only new_id's are returned, the only corner case here is for # wl_registry.bind, so no interface => Proxy arg_type = ":class:`pywayland.client.proxy.Proxy` of specified Interface" # Output the type and summary printer.doc(":returns:") with printer.indented(): if self.summary: printer.docstring(f"{arg_type} -- {self.summary}") else: printer.docstring(f"{arg_type}") pywayland-0.4.18/pywayland/scanner/entry.py0000664000175000017500000000422614651243522020706 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import xml.etree.ElementTree as ET from dataclasses import dataclass from .description import Description from .element import Element from .printer import Printer @dataclass(frozen=True) class Entry(Element): """Scanner for enum entries Required attributes: `name` and `value` Optional attributes: `summary` and `since` Child elements: `description` """ name: str value: str summary: str | None since: str | None description: Description | None @classmethod def parse(cls, element: ET.Element) -> Entry: return Entry( name=cls.parse_attribute(element, "name"), value=cls.parse_attribute(element, "value"), summary=cls.parse_optional_attribute(element, "summary"), since=cls.parse_optional_attribute(element, "since"), description=cls.parse_optional_child(element, Description, "description"), ) def output(self, enum_name: str, printer: Printer) -> None: """Generate the output for the entry in the enum""" # keep base 10 ints unchanged, but ensure that hexidecimal ints are # formatted 0xABC if self.value[:2] == "0x": value = f"0x{self.value[2:].upper()}" else: value = self.value try: int(self.name) printer(f"{enum_name}_{self.name} = {value}") except ValueError: if self.name in ("name", "async"): name = self.name + "_" else: name = self.name printer(f"{name} = {value}") pywayland-0.4.18/pywayland/scanner/printer.py0000664000175000017500000002031514651243522021225 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import contextlib import re import textwrap from typing import BinaryIO, Iterator, Mapping, Match HEAD_MSG = "# This file has been autogenerated by the pywayland scanner" # Match the start of unordered lists RE_DOC_LIST = re.compile(r"^(?P\* |- )") # Match the interface and function use in the docstrings RE_DOC = r"(?{})(?P\.[a-z][_a-z]*(?:\(\))?)?(?=[^\w\*']|$)" # The base import path for sphinx directives in docstrings BASE_PATH = "pywayland.protocol" # the width of the doc string DOC_WIDTH = 79 # the size of the tab stops TAB_STOP = 4 class Printer: def __init__( self, protocol: str, interface_name: str | None = None, interface_imports: Mapping[str, str] | None = None, ) -> None: """Base level printer object Allows for storing of lines to be output from the definition of a protocol. Lines are added by directly calling the printer object. :param protocol: The name of the protocol that is currently being generated, used for determining import resolution. :param interface_name: The name of the interface that is being generated, used for determining import resolution. :param interface_imports: The map from the interface name to the protocol it is defined in, for resolving imports. """ self._level = 0 self._lines = [HEAD_MSG, ""] self._protocol_name = protocol self._interface_name = interface_name self._interface_imports = interface_imports self._re_doc = None if interface_imports is not None: interface_names = "|".join(sorted(interface_imports, key=len, reverse=True)) self._re_doc = re.compile(RE_DOC.format(interface_names)) def __call__(self, new_line: str | None = None) -> None: """Add the new line to the printer :param new_line: The new line to add to the file, should be appropriately wrapped. """ if new_line: self._lines.append((" " * TAB_STOP * self._level) + new_line) else: self._lines.append("") def docstring(self, docstring: str) -> None: """Add lines as docstrings In addition to the operations performed by :meth:`Printer.doc()`, will wrap text passed to it to the correct width. :param docstring: The docstring line to add to the printer. """ docstring = self._parse_doc_line(docstring) # create a list of properly indented paragraphs paragraphs = [] for paragraph in docstring.split("\n\n"): # try to detect and properly output lists doc_list_match = RE_DOC_LIST.search(paragraph.lstrip()) base_width = DOC_WIDTH - TAB_STOP * self._level if doc_list_match is None: # not a list, just create the paragraph paragraphs.append( textwrap.fill(paragraph, width=base_width, break_long_words=False) ) else: # the list items are not always separated by paragraph breaks, # so parse each line and see if they denote list items start_list = doc_list_match.group("list_head") lines = paragraph.split("\n") current_list_item = [lines[0].strip()] list_items: list[str] = [] for line in lines[1:]: if line.strip().startswith(start_list): # store the current list item list_items.append(" ".join(current_list_item)) # start a new list item current_list_item = [line.strip()] else: current_list_item.append(line.strip()) # store the last list item list_items.append(" ".join(current_list_item)) # add each list item as a new paragraph paragraphs.extend( textwrap.fill( list_item, width=base_width, subsequent_indent=" " * len(start_list), break_long_words=False, ) for list_item in list_items ) wrapped = "\n\n".join(paragraphs) for line in wrapped.split("\n"): self(line) def doc(self, new_line: str) -> None: """Add lines as docstrings Performs additional massaging of strings, replacing references to other protocols and protocol methods with the appropriate Sphinx cross-reference. :param new_line: The new line to add to the printer output. """ new_line = self._parse_doc_line(new_line) self(new_line) def _parse_doc_line(self, line: str) -> str: """Parse the given docstring input Perform all necessary class and function replacements that match the given set of interfaces that are known. """ assert self._re_doc is not None line = self._re_doc.sub(self._doc_replace, line) return line def _doc_replace(self, match: Match) -> str: """Perform interface and function name replacement on the given match""" if match.group("func") is None: return self._doc_class_replace(match) return self._doc_funcs_replace(match) def _doc_class_replace(self, match: Match) -> str: """Build the sphinx doc class import :param match: The regex match for the given interface class. :returns: The string corresponding to the sphinx formatted class. """ interface_name = match.group("interface") interface_class = "".join(x.capitalize() for x in interface_name.split("_")) if interface_name == self._interface_name: return f":class:`{interface_class}`" if ( self._interface_imports is not None and interface_name in self._interface_imports ): protocol_path = self._interface_imports[interface_name] return f":class:`~{BASE_PATH}.{protocol_path}.{interface_class}`" return f"`{interface_class}`" def _doc_funcs_replace(self, match: Match) -> str: """Build the sphinx doc function definition :param match: The regex match for the given interface and function. :returns: The string corresponding to the sphinx formatted function. """ interface_name = match.group("interface") function_name = match.group("func") interface_class = "".join(x.capitalize() for x in interface_name.split("_")) if interface_name == self._interface_name: return f":func:`{interface_class}{function_name}()`" if ( self._interface_imports is not None and interface_name in self._interface_imports ): protocol_path = self._interface_imports[interface_name] return f":func:`{interface_class}{function_name}() <{BASE_PATH}.{protocol_path}.{interface_class}{function_name}>`" return f"`{interface_class}{function_name}()`" @contextlib.contextmanager def indented(self) -> Iterator[None]: """Indent in a level in the context manager block""" self._level += 1 yield self._level -= 1 def write(self, f: BinaryIO) -> None: """Write the lines added to the printer out to the given file""" for line in self._lines: f.write(line.encode()) f.write(b"\n") pywayland-0.4.18/pywayland/scanner/element.py0000664000175000017500000000431314651243522021173 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import abc import textwrap import xml.etree.ElementTree as ET from typing import TypeVar T = TypeVar("T", bound="Element") class Element(abc.ABC): @classmethod @abc.abstractmethod def parse(cls: type[T], element: ET.Element) -> T: pass @staticmethod def parse_attribute(element: ET.Element, name: str) -> str: obj = Element.parse_optional_attribute(element, name) if obj is None: raise ValueError() return obj @staticmethod def parse_optional_attribute(element: ET.Element, name: str) -> str | None: obj = element.attrib.get(name) return obj @staticmethod def parse_child(element: ET.Element, child_class: type[T], name: str) -> T: obj = Element.parse_optional_child(element, child_class, name) if obj is None: raise ValueError() return obj @staticmethod def parse_optional_child( element: ET.Element, child_class: type[T], name: str ) -> T | None: obj = element.find(name) if obj is None: return None return child_class.parse(obj) @staticmethod def parse_repeated_child( element: ET.Element, child_class: type[T], name: str ) -> list[T]: obj = [child_class.parse(elem) for elem in element.findall(name)] return obj @staticmethod def parse_pcdata(element: ET.Element) -> str | None: text = element.text if text: # We need to strip each line while keeping paragraph breaks return textwrap.dedent(text.expandtabs(8).rstrip().lstrip("\n")) return None pywayland-0.4.18/pywayland/scanner/__main__.py0000664000175000017500000000745514651243522021274 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import argparse import logging import os import shlex import subprocess from .protocol import Protocol logger = logging.getLogger(__name__) def pkgconfig(package: str, variable: str) -> str: """pkg-config""" pkgconfig_env = os.environ.get("PKG_CONFIG", "pkg-config") cmd = f"{pkgconfig_env} --variable={variable} {package}" args = shlex.split(cmd) return subprocess.check_output(args).decode().strip() def get_wayland_protocols() -> list[str]: # use pkg-config to try to find the wayland-protocol directory try: protocols_dir = pkgconfig("wayland-protocols", "pkgdatadir") except subprocess.CalledProcessError: raise OSError("Unable to find wayland-protocols using pkgconfig") protocols = [] # walk the wayland-protocol dir for dirpath, _, filenames in os.walk(protocols_dir): for filename in filenames: file_base, file_ext = os.path.splitext(filename) # only generate protocols for xml files if file_ext == ".xml": protocols.append(os.path.join(dirpath, filename)) # this is pretty brittle, there is an xdg_popup in both the unstable and # stable xdg_shell implementations. process the unstable version first so # the import is correct when the pop-up tries to import other interfaces. return sorted(protocols, reverse=True) def main() -> None: this_dir = os.path.split(__file__)[0] protocol_dir = os.path.join(this_dir, "..", "protocol") # try to figure out where the wayland.xml file is installed, otherwise use # default try: xml_dir = pkgconfig("wayland-scanner", "pkgdatadir") xml_file = os.path.join(xml_dir, "wayland.xml") except subprocess.CalledProcessError: xml_file = "/usr/share/wayland/wayland.xml" parser = argparse.ArgumentParser( description="Generate wayland protocol files from xml" ) parser.add_argument( "-o", "--output-dir", metavar="DIR", default=protocol_dir, type=str, help="Directory to output protocol files", ) parser.add_argument( "--with-protocols", action="store_true", help="Also locate and build wayland-protocol xml files (using pkg-config)", ) parser.add_argument( "-i", "--input", metavar="XML_FILE", default=[xml_file], nargs="+", type=str, help="Path to input xml file to scan", ) args = parser.parse_args() if not os.path.exists(args.output_dir): os.makedirs(args.output_dir, 0o775) input_files = args.input if args.with_protocols: protocols_files = get_wayland_protocols() input_files += protocols_files protocols = [Protocol.parse_file(input_file) for input_file in input_files] logger.info(f"Parsed {len(protocols)} input xml files") protocol_imports = { interface.name: protocol.name for protocol in protocols for interface in protocol.interface } for protocol in protocols: protocol.output(args.output_dir, protocol_imports) logger.info(f"Generated protocol: {protocol.name}") if __name__ == "__main__": logging.basicConfig(level=logging.INFO) main() pywayland-0.4.18/pywayland/scanner/interface.py0000664000175000017500000001336614651243522021512 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import itertools import xml.etree.ElementTree as ET from dataclasses import dataclass from .description import Description from .element import Element from .enum import Enum from .event import Event from .printer import Printer from .request import Request @dataclass(frozen=True) class Interface(Element): name: str version: str description: Description | None enum: list[Enum] event: list[Event] request: list[Request] @classmethod def parse(cls, element: ET.Element) -> Interface: """Scanner for interface objects Required attributes: `name` and `version` Child elements: `description`, `request`, `event`, `enum` """ name = cls.parse_attribute(element, "name") return cls( name=name, version=cls.parse_attribute(element, "version"), description=cls.parse_optional_child(element, Description, "description"), enum=cls.parse_repeated_child(element, Enum, "enum"), event=cls.parse_repeated_child(element, Event, "event"), request=cls.parse_repeated_child(element, Request, "request"), ) @property def class_name(self) -> str: """Returns the name of the class of the interface Camel cases the name of the interface, to be used as the class name. """ return "".join(x.capitalize() for x in self.name.split("_")) def output(self, printer: Printer, module_imports: dict[str, str]) -> None: """Generate the output for the interface to the printer""" # Imports imports = set( _import for method in itertools.chain(self.request, self.event) for _import in method.imports(self.name, module_imports) ) needs_argument_type = any( len(method.arg) > 0 for method in self.request ) or any(len(method.arg) > 0 for method in self.event) printer("from __future__ import annotations") printer() if self.enum: printer("import enum") printer() typing_imports = [] define_t = any(req.new_id and not req.new_id.interface for req in self.request) needs_any = any(req.needs_any for req in self.request) or any( event.needs_any for event in self.event ) if define_t: typing_imports.extend(["TypeVar"]) if needs_any: typing_imports.append("Any") if typing_imports: printer(f"from typing import {', '.join(sorted(typing_imports))}") printer() if needs_argument_type: printer("from pywayland.protocol_core import (") printer(" Argument,") printer(" ArgumentType,") printer(" Global,") printer(" Interface,") printer(" Proxy,") printer(" Resource,") printer(")") printer() else: printer( "from pywayland.protocol_core import Global, Interface, Proxy, Resource" ) printer() for module, import_ in sorted(imports): printer(f"from {module} import {import_}") if imports: printer() if define_t: printer('T = TypeVar("T", bound=Interface)') printer() printer() # Class definition printer(f"class {self.class_name}(Interface):") with printer.indented(): # Docstring if self.description: self.description.output(printer) printer('"""') printer() # Class attributes printer(f'name = "{self.name}"') printer(f"version = {self.version}") # Enums for enum in self.enum: printer() enum.output(printer) proxy_class_name = f"{self.class_name}Proxy" resource_class_name = f"{self.class_name}Resource" global_class_name = f"{self.class_name}Global" printer() printer() printer(f"class {proxy_class_name}(Proxy[{self.class_name}]):") with printer.indented(): printer(f"interface = {self.class_name}") for opcode, request in enumerate(self.request): printer() request.output(printer, opcode, self.class_name, module_imports) printer() printer() printer(f"class {resource_class_name}(Resource):") with printer.indented(): printer(f"interface = {self.class_name}") for opcode, event in enumerate(self.event): printer() event.output(printer, opcode, self.class_name, module_imports) printer() printer() printer(f"class {global_class_name}(Global):") with printer.indented(): printer(f"interface = {self.class_name}") printer() printer() printer(f"{self.class_name}._gen_c()") printer(f"{self.class_name}.proxy_class = {proxy_class_name}") printer(f"{self.class_name}.resource_class = {resource_class_name}") printer(f"{self.class_name}.global_class = {global_class_name}") pywayland-0.4.18/pywayland/scanner/enum.py0000664000175000017500000000376214651243522020515 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import xml.etree.ElementTree as ET from dataclasses import dataclass from .description import Description from .element import Element from .entry import Entry from .printer import Printer @dataclass(frozen=True) class Enum(Element): """Scanner for enum objects Required attributes: `name` and `since` Child elements: `description` and `entry` """ name: str since: str | None is_bitfield: bool description: Description | None entry: list[Entry] @classmethod def parse(cls, element: ET.Element) -> Enum: is_bitfield = cls.parse_optional_attribute(element, "bitfield") == "true" return Enum( name=cls.parse_attribute(element, "name"), since=cls.parse_optional_attribute(element, "since"), is_bitfield=is_bitfield, description=cls.parse_optional_child(element, Description, "description"), entry=cls.parse_repeated_child(element, Entry, "entry"), ) def output(self, printer: Printer) -> None: """Generate the output for the enum to the printer""" name = self.name if self.name != "version" else "version_" if self.is_bitfield: printer(f"class {name}(enum.IntFlag):") else: printer(f"class {name}(enum.IntEnum):") with printer.indented(): for entry in self.entry: entry.output(self.name, printer) pywayland-0.4.18/pywayland/scanner/protocol.py0000664000175000017500000000720214651243522021403 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import os import xml.etree.ElementTree as ET from dataclasses import dataclass from .copyright import Copyright, copyright_default from .description import Description from .element import Element from .interface import Interface from .printer import Printer @dataclass(frozen=True) class Protocol(Element): """Protocol scanner object Main scanner object that acts on the input xml files to generate protocol files. Required attributes: `name` Child elements: `copyright?`, `description?`, and `interface+` :param input_file: Name of input XML file """ name: str copyright: Copyright | None description: Description | None interface: list[Interface] @classmethod def parse_file(cls, input_file: str) -> Protocol: if not os.path.exists(input_file): raise ValueError(f"Input xml file does not exist: {input_file}") xmlroot = ET.parse(input_file).getroot() if xmlroot.tag != "protocol": raise ValueError( f"Input file not a valid Wayland protocol file: {input_file}" ) return cls.parse(xmlroot) @classmethod def parse(cls, element: ET.Element) -> Protocol: return cls( name=cls.parse_attribute(element, "name"), copyright=cls.parse_optional_child(element, Copyright, "copyright"), description=cls.parse_optional_child(element, Description, "description"), interface=cls.parse_repeated_child(element, Interface, "interface"), ) def __repr__(self) -> str: return f"Protocol({self.name})" def output(self, output_dir: str, module_imports: dict[str, str]) -> None: """Output the scanned files to the given directory :param output_dir: Path of directory to output protocol files to :type output_dir: string """ protocol_name = self.name.replace("-", "_") output_dir = os.path.join(output_dir, protocol_name) if not os.path.exists(output_dir): os.mkdir(output_dir) # First, we'll create the __init__.py file printer = Printer(protocol_name) if self.copyright: self.copyright.output(printer) else: printer(copyright_default) printer() for iface in sorted(self.interface, key=lambda x: x.name): printer(f"from .{iface.name} import {iface.class_name} # noqa: F401") init_path = os.path.join(output_dir, "__init__.py") with open(init_path, "wb") as f: printer.write(f) # Now build all the modules for iface in self.interface: module_path = os.path.join(output_dir, iface.name + ".py") printer = Printer(self.name.replace("-", "_"), iface.name, module_imports) if self.copyright: self.copyright.output(printer) else: printer(copyright_default) printer() iface.output(printer, module_imports) with open(module_path, "wb") as f: printer.write(f) pywayland-0.4.18/pywayland/__init__.py0000664000175000017500000000163114651243522017650 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .version import __version__ # noqa: F401 try: from ._ffi import ffi, lib # noqa: F401 except ImportError: raise ImportError( "No module named pywayland._ffi, be sure to run `python ./pywayland/ffi_build.py`" ) __wayland_version__ = f"{lib.WAYLAND_VERSION_MAJOR}.{lib.WAYLAND_VERSION_MINOR}.{lib.WAYLAND_VERSION_MICRO}" pywayland-0.4.18/pywayland/py.typed0000664000175000017500000000000014651243522017223 0ustar epsilonepsilonpywayland-0.4.18/pywayland/version.py0000664000175000017500000000112414651243522017573 0ustar epsilonepsilon# Copyright 2016 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. __version__ = "0.4.18" pywayland-0.4.18/pywayland/server/0000775000175000017500000000000014651243522017044 5ustar epsilonepsilonpywayland-0.4.18/pywayland/server/display.py0000664000175000017500000001400614651243522021064 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations from typing import TYPE_CHECKING from pywayland import ffi, lib from pywayland.utils import ensure_valid from .eventloop import EventLoop if TYPE_CHECKING: from typing_extensions import Literal def _full_display_gc(ptr: ffi.DisplayCData) -> None: """Destroy the Display cdata pointer, but only after destroying the clients""" lib.wl_display_destroy_clients(ptr) lib.wl_display_destroy(ptr) class Display: """Create a Wayland Display object""" def __init__(self, ptr=None) -> None: if ptr is None: ptr = lib.wl_display_create() if ptr == ffi.NULL: raise MemoryError("Unable to create wl_display object") self._ptr = ffi.gc(ptr, _full_display_gc) def __enter__(self) -> Display: """Use the Display in a context manager, which automatically destroys the Display""" return self def __exit__(self, exc_type, exc_value, traceback) -> Literal[False]: """Destroy the used display""" self.destroy() return False @property def destroyed(self) -> bool: """Returns if the display has been destroyed""" return self._ptr is None def destroy(self) -> None: """Destroy Wayland display object. This function emits the :class:`Display` destroy signal, releases all the sockets added to this display, free's all the globals associated with this display, free's memory of additional shared memory formats and destroy the display object. .. seealso:: :meth:`Display.add_destroy_listener()` """ if self._ptr is not None: ffi.release(self._ptr) self._ptr = None @ensure_valid def add_socket(self, name: str | None = None) -> str: """Add a socket to Wayland display for the clients to connect. This adds a Unix socket to Wayland display which can be used by clients to connect to Wayland display. If `None` is passed as name, then it would look for `WAYLAND_DISPLAY` environment variable for the socket name. If `WAYLAND_DISPLAY` is not set, then default `wayland-0` is used. The Unix socket will be created in the directory pointed to by environment variable `XDG_RUNTIME_DIR`. If `XDG_RUNTIME_DIR` is not set, then this function throws an exception. The length of socket path, i.e., the path set in `XDG_RUNTIME_DIR` and the socket name, must not exceed the maxium length of a Unix socket path. The function also fails if the user do not have write permission in the `XDG_RUNTIME_DIR` path or if the socket name is already in use. :param name: Name of the Unix socket. :type name: string or None """ if name is None: name_ptr = lib.wl_display_add_socket_auto(self._ptr) if name_ptr == ffi.NULL: raise RuntimeError("Unable to create socket") name = ffi.string(name_ptr) else: ret = lib.wl_display_add_socket(self._ptr, name.encode()) if ret == -1: # TODO: raise better raise Exception() return name @ensure_valid def get_serial(self) -> int: """Get the current serial number This function returns the most recent serial number, but does not increment it. """ return lib.wl_display_get_serial(self._ptr) @ensure_valid def next_serial(self) -> int: """Get the next serial This function increments the display serial number and returns the new value. """ return lib.wl_display_next_serial(self._ptr) @ensure_valid def get_event_loop(self) -> EventLoop: """Get the event loop for the display :returns: The :class:`~pywayland.server.EventLoop` for the Display """ return EventLoop(self) @ensure_valid def terminate(self) -> None: """Stop the display from running""" lib.wl_display_terminate(self._ptr) @ensure_valid def run(self) -> None: """Run the display""" lib.wl_display_run(self._ptr) @ensure_valid def init_shm(self) -> None: """Initialize shm for this display""" ret = lib.wl_display_init_shm(self._ptr) if ret == -1: raise MemoryError("Unable to create shm for display") @ensure_valid def add_shm_format(self, shm_format) -> None: """Add support for a Shm pixel format Add the specified :class:`~pywayland.protocol.wayland.WlShm.format` format to the list of formats the :class:`~pywayland.protocol.wayland.WlShm` object advertises when a client binds to it. Adding a format to the list means that clients will know that the compositor supports this format and may use it for creating :class:`~pywayland.protocol.wayland.WlShm` buffers. The compositor must be able to handle the pixel format when a client requests it. The compositor by default supports ``WL_SHM_FORMAT_ARGB8888`` and ``WL_SHM_FORMAT_XRGB8888``. :param shm_format: The shm pixel format to advertise :type shm_format: :class:`~pywayland.protocol.wayland.WlShm.format` """ lib.wl_display_add_shm_format(self._ptr, shm_format.value) @ensure_valid def flush_clients(self) -> None: """Flush client connections""" lib.wl_display_flush_clients(self._ptr) pywayland-0.4.18/pywayland/server/listener.py0000664000175000017500000001116014651243522021242 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations from logging import getLogger from typing import Callable from pywayland import ffi, lib from pywayland.utils import wl_container_of logger = getLogger(__package__) # void (*wl_notify_func_t)(struct wl_listener *listener, void *data); @ffi.def_extern() def notify_func(listener_ptr, data): container = wl_container_of( listener_ptr, "struct wl_listener_container *", "destroy_listener" ) listener = ffi.from_handle(container.handle) if listener._signal is not None and listener._signal._data_wrapper is not None: data = listener._signal._data_wrapper(data) callback = listener._notify try: callback(listener, data) except Exception: logger.exception("Exception in callback function") class Listener: """A single listener for Wayland signals Provides the means to listen for `wl_listener` signal notifications. Many Wayland objects use `wl_listener` for notification of significant events like object destruction. Clients should create :class:`Listener` objects manually and can register them as listeners to objects destroy events using the object's ``.add_destroy_listener()`` method. A listener can only listen to one signal at a time. :param function: callback function for the Listener :type function: callable """ container: ffi.ListenerContainerCData def __init__(self, function: Callable) -> None: self._handle = ffi.new_handle(self) # we need a way to get this Python object from the `struct # wl_listener*`, so we put the pointer in a container struct that # contains both the wl_listener and a pointer to our ffi handle self.container = ffi.new("struct wl_listener_container *") # type: ignore[assignment] self.container.handle = self._handle self._ptr: ffi.ListenerCData | None = ffi.addressof( self.container.destroy_listener ) self._ptr.notify = lib.notify_func self._notify = function self._signal = None def remove(self) -> None: """Remove the listener""" if self._ptr and self._ptr.link != ffi.NULL: lib.wl_list_remove(ffi.addressof(self._ptr.link)) self._ptr = None class Signal: """A source of a type of observable event Signals are recognized points where significant events can be observed. Compositors as well as the server can provide signals. Observers are wl_listener's that are added through #wl_signal_add. Signals are emitted using #wl_signal_emit, which will invoke all listeners until that listener is removed by wl_list_remove() (or whenever the signal is destroyed). """ def __init__(self, *, ptr=None, data_wrapper=None): if ptr is None: self._ptr = ffi.new("struct wl_listener *") lib.wl_signal_init(self._ptr) else: self._ptr = ptr self._data_wrapper = data_wrapper self._link: list[Listener] = [] def add(self, listener): """Add the specified listener to this signal :param listener: The listener to add :type listener: :class:`Listener` """ # we are creating a reference loop, as the listener resolves the data # wrapper function, and the signal keeps the listener alive listener._signal = self self._link.append(listener) lib.wl_signal_add(self._ptr, listener._ptr) def emit(self, data=None): """Emits this signal, notifying all registered listeners :param data: The data that will be emitted with the signal """ if data is not None: data_ptr = ffi.new_handle(data) else: data_ptr = ffi.NULL lib.wl_signal_emit(self._ptr, data_ptr) def remove_listeners(self) -> None: """Removes all listeners from this signal. This can be useful when an object is about to be destroyed and all listeners need to be removed from that object. """ for listener in self._link[:]: listener.remove() pywayland-0.4.18/pywayland/server/__init__.py0000664000175000017500000000133514651243522021157 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .client import Client # noqa from .display import Display # noqa from .eventloop import EventLoop # noqa from .listener import Listener, Signal # noqa pywayland-0.4.18/pywayland/server/client.py0000664000175000017500000001260714651243522020702 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import functools import logging from typing import Any from pywayland import ffi, lib from pywayland.utils import ensure_valid from .display import Display from .listener import Listener def _client_destroy(display: Display, cdata: ffi.ClientCData) -> None: # do nothing if the display is already destroyed if display.destroyed: logging.error("Display destroyed before client") return lib.wl_client_destroy(cdata) class Client: """Create a client for the given file descriptor Given a file descriptor corresponding to one end of a socket, create a client struct and add the new client to the compositors client list. At that point, the client is initialized and ready to run, as if the client had connected to the servers listening socket. Alternatively, pass a pointer to an existing client and use that instead of creating a new one. The other end of the socket can be passed to :meth:`~pywayland.client.Display.connect()` on the client side or used with the WAYLAND_SOCKET environment variable on the client side. :param display: The display object :type display: :class:`Display` :param fd: The file descriptor for the socket to the client :type fd: `int` :param ptr: A pointer to an existing wl_client :type ptr: `ffi.ClientCData` """ def __init__( self, display: Display | None = None, fd: int | None = None, ptr: ffi.ClientCData | None = None, ) -> None: if ptr is None: if display is None or fd is None: raise ValueError("display and fd needed to create new client") if display.destroyed: raise ValueError("Display has been destroyed") ptr = lib.wl_client_create(display._ptr, fd) destructor = functools.partial(_client_destroy, display) self._ptr: ffi.ClientCData | None = ffi.gc(ptr, destructor) else: self._ptr = ptr def destroy(self) -> None: """Destroy the client""" if self._ptr is not None: ffi.release(self._ptr) self._ptr = None @ensure_valid def flush(self) -> None: """Flush pending events to the client Events sent to clients are queued in a buffer and written to the socket later - typically when the compositor has handled all requests and goes back to block in the event loop. This function flushes all queued up events for a client immediately. """ assert self._ptr is not None lib.wl_client_flush(self._ptr) @ensure_valid def get_credentials(self) -> tuple[int, int, int]: """Return Unix credentials for the client. This function returns the process ID, the user ID and the group ID for the given client. The credentials come from getsockopt() with SO_PEERCRED, on the client socket fd. """ assert self._ptr is not None pid = ffi.new("pid_t *") uid = ffi.new("uid_t *") gid = ffi.new("gid_t *") lib.wl_client_get_credentials(self._ptr, pid, uid, gid) return pid[0], uid[0], gid[0] @ensure_valid def add_destroy_listener(self, listener: Listener) -> None: """Add a listener for the destroy signal :param listener: The listener object :type listener: :class:`~pywayland.server.Listener` """ assert self._ptr is not None and listener._ptr is not None lib.wl_client_add_destroy_listener(self._ptr, listener._ptr) @ensure_valid def get_object(self, object_id: int) -> Any: """Look up an object in the client name space This looks up an object in the client object name space by its object ID. :param object_id: The object id :type object_id: `int` :returns: The object, or ``None`` if there is not object for the given ID """ assert self._ptr is not None res_ptr = lib.wl_client_get_object(self._ptr, object_id) # If the object doesn't exist, this returns NULL, and asking for # forgiveness doesn't work, becuase it will seg fault if res_ptr == ffi.NULL: return None resource_handle = lib.wl_resource_get_user_data(res_ptr) return ffi.from_handle(resource_handle) @classmethod def from_resource(cls, resource: ffi.ResourceCData) -> Client: """Look up the corresponding wl_client for a wl_resource :param resource: The wl_resource :type resource: pywayland.protocol_core.Resource :returns: A `Client` instance. """ return cls(ptr=lib.wl_resource_get_client(resource)) def __eq__(self, other) -> bool: """Compare this client with another""" return hasattr(other, "_ptr") and self._ptr == other._ptr pywayland-0.4.18/pywayland/server/eventloop.py0000664000175000017500000002165614651243522021443 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import enum from collections import namedtuple from typing import TYPE_CHECKING from weakref import WeakSet from pywayland import ffi, lib from pywayland.utils import ensure_valid if TYPE_CHECKING: from pywayland.server import Display CallbackInfo = namedtuple("CallbackInfo", ["callback", "data"]) # TODO: add error handling to all callbacks # int (*wl_event_loop_fd_func_t)(int fd, uint32_t mask, void *data) @ffi.def_extern() def event_loop_fd_func(fd, mask, data_ptr) -> int: callback_info = ffi.from_handle(data_ptr) ret = callback_info.callback(fd, mask, callback_info.data) if isinstance(ret, int): return ret return 0 # int (*wl_event_loop_signal_func_t)(int signal_number, void *data) @ffi.def_extern() def event_loop_signal_func(signal_number, data_ptr) -> int: callback_info = ffi.from_handle(data_ptr) ret = callback_info.callback(signal_number, callback_info.data) if isinstance(ret, int): return ret return 0 # int (*wl_event_loop_timer_func_t)(void *data) @ffi.def_extern() def event_loop_timer_func(data_ptr): callback_info = ffi.from_handle(data_ptr) ret = callback_info.callback(callback_info.data) if isinstance(ret, int): return ret return 0 # void (void *data) @ffi.def_extern() def event_loop_idle_func(data_ptr): callback_info = ffi.from_handle(data_ptr) callback_info.callback(callback_info.data) class EventSource: """Parameters for the EventLoop callbacks :param cdata: The struct corresponding to the EventSource :type cdata: `ffi cdata` """ def __init__(self, eventloop, cdata): self._eventloop = eventloop self._ptr = cdata def remove(self): """Remove the callback from the event loop""" if self._ptr is not None: lib.wl_event_source_remove(self._ptr) self._ptr = None @ensure_valid def check(self): """Insert the EventSource into the check list""" lib.wl_event_source_check(self._ptr) @ensure_valid def timer_update(self, timeout): """Set the timeout of the times callback :param timeout: Delay for timeout in ms :type timeout: `int` """ lib.wl_event_source_timer_update(self._ptr, timeout) class EventLoop: """An event loop to add events to Returns an event loop. Either returns the event loop of a given display (which will trigger when the Display is run), or creates a new event loop (which can be triggered by using :meth:`EventLoop.dispatch()`). :param display: The display to create the EventLoop on (default to `None`) :type display: :class:`~pywayland.server.Display` """ class FdMask(enum.IntFlag): WL_EVENT_READABLE = lib.WL_EVENT_READABLE WL_EVENT_WRITABLE = lib.WL_EVENT_WRITABLE WL_EVENT_HANGUP = lib.WL_EVENT_HANGUP WL_EVENT_ERROR = lib.WL_EVENT_ERROR def __init__(self, display: Display | None = None) -> None: if display: self._ptr: ffi.EventLoopCData | None = lib.wl_display_get_event_loop( display._ptr ) else: # if we are creating an eventloop. we need to destroy it later ptr = lib.wl_event_loop_create() self._ptr = ffi.gc(ptr, lib.wl_event_loop_destroy) self.event_sources: WeakSet[EventSource] = WeakSet() self._callback_handles: list = [] def destroy(self): """Destroy the event loop""" if self._ptr is not None: for event_source in self.event_sources: event_source.remove() # destroy the pointer and remove the destructor ffi.release(self._ptr) self._ptr = None @ensure_valid def add_fd(self, fd, callback, mask=FdMask.WL_EVENT_READABLE, data=None): """Add file descriptor callback Triggers function call when file descriptor state matches the mask. The callback should take three arguments: * `fd` - file descriptor (int) * `mask` - file descriptor mask (uint) * `data` - any object :param fd: File descriptor :type fd: `int` :param callback: Callback function :type fd: function with callback `int(int fd, uint32_t mask, void *data)` :param mask: File descriptor mask :type fd: `int` :param data: User data to send to callback :type data: `object` :returns: :class:`EventSource` for specified callback .. seealso:: :meth:`pywayland.server.eventloop.EventSource.check()` """ callback = CallbackInfo(callback=callback, data=data) handle = ffi.new_handle(callback) self._callback_handles.append(handle) event_source_cdata = lib.wl_event_loop_add_fd( self._ptr, fd, mask.value, lib.event_loop_fd_func, handle ) event_source = EventSource(self, event_source_cdata) self.event_sources.add(event_source) return event_source @ensure_valid def add_signal(self, signal_number, callback, data=None): """Add signal callback Triggers function call signal is received. The callback should take three arguments: * `signal_number` - signal (int) * `data` - any object :param signal_number: Signal number to trigger on :type signal_number: `int` :param callback: Callback function :type fd: function with callback `int(int signal_number, void *data)` :param data: User data to send to callback :type data: `object` :returns: :class:`EventSource` for specified callback """ callback = CallbackInfo(callback=callback, data=data) handle = ffi.new_handle(callback) self._callback_handles.append(handle) event_source_cdata = lib.wl_event_loop_add_signal( self._ptr, signal_number, lib.event_loop_signal_func, handle ) event_source = EventSource(self, event_source_cdata) self.event_sources.add(event_source) return event_source @ensure_valid def add_timer(self, callback, data=None): """Add timer callback Triggers function call after a specified time. The callback should take one argument: * `data` - any object :param callback: Callback function :type callback: function with callback `int(void *data)` :param data: User data to send to callback :type data: `object` :returns: :class:`EventSource` for specified callback .. seealso:: :meth:`pywayland.server.eventloop.EventSource.timer_update()` """ callback = CallbackInfo(callback=callback, data=data) handle = ffi.new_handle(callback) self._callback_handles.append(handle) event_source_cdata = lib.wl_event_loop_add_timer( self._ptr, lib.event_loop_timer_func, handle ) event_source = EventSource(self, event_source_cdata) self.event_sources.add(event_source) return event_source @ensure_valid def add_idle(self, callback, data=None): """Add idle callback :param callback: Callback function :type callback: function with callback `void(void *data)` :param data: User data to send to callback :returns: :class:`EventSource` for specified callback """ callback = CallbackInfo(callback=callback, data=data) handle = ffi.new_handle(callback) self._callback_handles.append(handle) event_source_cdata = lib.wl_event_loop_add_idle( self._ptr, lib.event_loop_idle_func, handle ) event_source = EventSource(self, event_source_cdata) self.event_sources.add(event_source) return event_source @ensure_valid def add_destroy_listener(self, listener): """Add a listener for the destroy signal :param listener: The listener object :type listener: :class:`~pywayland.server.Listener` """ lib.wl_event_loop_add_destroy_listener(self._ptr, listener._ptr) @ensure_valid def dispatch(self, timeout): """Dispatch callbacks on the event loop""" lib.wl_event_loop_dispatch(self._ptr, timeout) @ensure_valid def dispatch_idle(self): """Dispatch idle callback on the event loop""" lib.wl_event_loop_dispatch_idle(self._ptr) pywayland-0.4.18/pywayland/utils.py0000664000175000017500000000777414651243522017267 0ustar epsilonepsilon# Copyright 2015 Sean Vig # Copyright 2021 Matt Colligan # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import os from functools import wraps from typing import Callable, Iterator from . import ffi, lib def ensure_valid(func: Callable) -> Callable: @wraps(func) def wrapper(self, *args, **kwargs): if self._ptr is None: raise ValueError(f"{self.__class__.__name__} object has been destroyed") return func(self, *args, **kwargs) return wrapper class AnonymousFile: """Anonymous file object Provides access to anonymous file objects that can be used by Wayland clients to render to surfaces. Uses a method similar to Weston to open an anonymous file, so XDG_RUNTIME_DIR must be set for this to work properly. This class provides a content manager, that is, it can be used with Python ``with`` statements, where the value returned is the file descriptor. """ def __init__(self, size: int) -> None: self.size = size self.fd: int | None = None def __enter__(self) -> int: self.open() assert self.fd is not None return self.fd def __exit__(self, exc_type, exc_value, traceback) -> None: self.close() def open(self) -> None: """Open an anonymous file Opens the anonymous file and sets the ``fd`` property to the file descriptor that has been opened. """ if self.fd is not None: raise OSError("File is already open") self.fd = lib.os_create_anonymous_file(self.size) if self.fd < 0: raise OSError("Unable to create anonymous file") def close(self) -> None: """Close the anonymous file Closes the file descriptor and sets the ``fd`` property to ``None``. Does nothing if the file is not open. """ if self.fd is None: return os.close(self.fd) self.fd = None def wl_container_of(ptr: ffi.CData, ctype: str, member: str, *, ffi=ffi) -> ffi.CData: """ #define wl_container_of(ptr, sample, member) \ (__typeof__(sample))((char *)(ptr) - \ offsetof(__typeof__(*sample), member)) :param ptr: Pointer to contained object :param ctype: ctype of container as string :param member: Member name of contained object in ctype :param ffi: ffi module to use. The default is pywayland, but this allows the use of this macro by other ffi modules that use `wl_list`s. """ return ffi.cast(ctype, ffi.cast("char *", ptr) - ffi.offsetof(ctype, member)) # type: ignore[no-any-return] def wl_list_for_each( ctype: str, head: ffi.CData, member: str, *, ffi=ffi ) -> Iterator[ffi.CData]: """ #define wl_list_for_each(pos, head, member) \ for (pos = wl_container_of((head)->next, pos, member); \ &pos->member != (head); \ pos = wl_container_of(pos->member.next, pos, member)) :param ctype: ctype of container as string :param head: The struct wl_list :param member: Member name of struct wl_list in ctype :param ffi: ffi module to use. The default is pywayland, but this allows the use of this macro by other ffi modules that use `wl_list`s. """ pos = wl_container_of(head.next, ctype, member, ffi=ffi) while getattr(pos, member) != head: yield pos pos = wl_container_of(getattr(pos, member).next, ctype, member, ffi=ffi) pywayland-0.4.18/pywayland/ffi_build.py0000664000175000017500000003272414651243522020043 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from cffi import FFI ############################################################################### # wayland-version.h ############################################################################### CDEF = """ #define WAYLAND_VERSION_MAJOR ... #define WAYLAND_VERSION_MINOR ... #define WAYLAND_VERSION_MICRO ... """ ############################################################################### # wayland-util.h ############################################################################### # wl_fixed_t handling CDEF += """ typedef int32_t wl_fixed_t; static inline double wl_fixed_to_double(wl_fixed_t f); static inline wl_fixed_t wl_fixed_from_double(double d); static inline wl_fixed_t wl_fixed_from_int(int i); """ # Event/request dispatching structs CDEF += """ struct wl_message { const char *name; const char *signature; const struct wl_interface **types; }; struct wl_interface { const char *name; int version; int method_count; const struct wl_message *methods; int event_count; const struct wl_message *events; }; union wl_argument { int32_t i; /**< signed integer */ uint32_t u; /**< unsigned integer */ wl_fixed_t f; /**< fixed point */ const char *s; /**< string */ struct wl_object *o; /**< object */ uint32_t n; /**< new_id */ struct wl_array *a; /**< array */ int32_t h; /**< file descriptor */ }; """ # wl_array methods CDEF += """ struct wl_array { size_t size; size_t alloc; void *data; }; """ # wl_list methods CDEF += """ struct wl_list { struct wl_list *prev; struct wl_list *next; }; void wl_list_remove(struct wl_list *elm); int wl_list_empty(const struct wl_list *list); """ # Dispatcher callback CDEF += """ typedef int (*wl_dispatcher_func_t)(const void *, void *, uint32_t, const struct wl_message *, union wl_argument *); """ ############################################################################### # wayland-client.h ############################################################################### # wl_eventqueue methods CDEF += """ void wl_event_queue_destroy(struct wl_event_queue *queue); """ # wl_proxy methods CDEF += """ void wl_proxy_marshal_array(struct wl_proxy *p, uint32_t opcode, union wl_argument *args); struct wl_proxy *wl_proxy_create(struct wl_proxy *factory, const struct wl_interface *interface); struct wl_proxy * wl_proxy_marshal_array_constructor(struct wl_proxy *proxy, uint32_t opcode, union wl_argument *args, const struct wl_interface *interface); void wl_proxy_destroy(struct wl_proxy *proxy); int wl_proxy_add_dispatcher(struct wl_proxy *proxy, wl_dispatcher_func_t dispatcher_func, const void * dispatcher_data, void *data); void wl_proxy_set_user_data(struct wl_proxy *proxy, void *user_data); void *wl_proxy_get_user_data(struct wl_proxy *proxy); """ # wl_display methods CDEF += """ struct wl_display* wl_display_connect(const char *name); struct wl_display* wl_display_connect_to_fd(int fd); void wl_display_disconnect(struct wl_display *display); int wl_display_get_fd(struct wl_display *display); int wl_display_dispatch(struct wl_display *display); int wl_display_dispatch_pending(struct wl_display *display); int wl_display_dispatch_queue(struct wl_display *display, struct wl_event_queue *queue); int wl_display_dispatch_queue_pending(struct wl_display *display, struct wl_event_queue *queue); int wl_display_roundtrip(struct wl_display *display); int wl_display_roundtrip_queue(struct wl_display *display, struct wl_event_queue *queue); int wl_display_get_error(struct wl_display *display); int wl_display_read_events(struct wl_display *display); int wl_display_prepare_read(struct wl_display *display); int wl_display_prepare_read_queue(struct wl_display *display, struct wl_event_queue *queue); int wl_display_flush(struct wl_display *display); struct wl_event_queue *wl_display_create_queue(struct wl_display *display); """ ############################################################################### # wayland-server.h ############################################################################### # wl_eventloop enum CDEF += """ enum { WL_EVENT_READABLE = ..., WL_EVENT_WRITABLE = ..., WL_EVENT_HANGUP = ..., WL_EVENT_ERROR = ... }; """ # wl_listener struct CDEF += """ typedef void (*wl_notify_func_t)(struct wl_listener *listener, void *data); struct wl_listener { struct wl_list link; wl_notify_func_t notify; }; """ CDEF += """ struct wl_signal { struct wl_list listener_list; }; void wl_signal_add(struct wl_signal *signal, struct wl_listener *listener); void wl_signal_emit(struct wl_signal *signal, void *data); """ # wl_eventloop callbacks and methods CDEF += """ typedef int (*wl_event_loop_fd_func_t)(int fd, uint32_t mask, void *data); typedef int (*wl_event_loop_timer_func_t)(void *data); typedef int (*wl_event_loop_signal_func_t)(int signal_number, void *data); typedef void (*wl_event_loop_idle_func_t)(void *data); void wl_event_loop_add_destroy_listener(struct wl_event_loop *loop, struct wl_listener * listener); struct wl_event_loop *wl_event_loop_create(void); void wl_event_loop_destroy(struct wl_event_loop *loop); struct wl_event_source *wl_event_loop_add_fd(struct wl_event_loop *loop, int fd, uint32_t mask, wl_event_loop_fd_func_t func, void *data); int wl_event_source_fd_update(struct wl_event_source *source, uint32_t mask); struct wl_event_source *wl_event_loop_add_timer(struct wl_event_loop *loop, wl_event_loop_timer_func_t func, void *data); struct wl_event_source * wl_event_loop_add_signal(struct wl_event_loop *loop, int signal_number, wl_event_loop_signal_func_t func, void *data); int wl_event_loop_dispatch(struct wl_event_loop *loop, int timeout); void wl_event_loop_dispatch_idle(struct wl_event_loop *loop); struct wl_event_source *wl_event_loop_add_idle(struct wl_event_loop *loop, wl_event_loop_idle_func_t func, void *data); int wl_event_loop_get_fd(struct wl_event_loop *loop); """ # wl_event_source methods CDEF += """ int wl_event_source_timer_update(struct wl_event_source *source, int ms_delay); int wl_event_source_remove(struct wl_event_source *source); void wl_event_source_check(struct wl_event_source *source); """ # wl_display methods CDEF += """ struct wl_display * wl_display_create(void); void wl_display_destroy(struct wl_display *display); struct wl_event_loop *wl_display_get_event_loop(struct wl_display *display); int wl_display_add_socket(struct wl_display *display, const char *name); const char *wl_display_add_socket_auto(struct wl_display *display); void wl_display_terminate(struct wl_display *display); void wl_display_run(struct wl_display *display); uint32_t wl_display_get_serial(struct wl_display *display); uint32_t wl_display_next_serial(struct wl_display *display); void wl_display_destroy_clients(struct wl_display *display); void wl_display_flush_clients(struct wl_display *display); int wl_display_init_shm(struct wl_display *display); uint32_t *wl_display_add_shm_format(struct wl_display *display, uint32_t format); """ # wl_global methods CDEF += """ typedef void (*wl_global_bind_func_t)(struct wl_client *client, void *data, uint32_t version, uint32_t id); struct wl_global *wl_global_create(struct wl_display *display, const struct wl_interface *interface, int version, void *data, wl_global_bind_func_t bind); void wl_global_destroy(struct wl_global *global); """ # wl_client methods CDEF += """ struct wl_client; struct wl_client *wl_client_create(struct wl_display *display, int fd); void wl_client_destroy(struct wl_client *client); void wl_client_flush(struct wl_client *client); typedef int pid_t; typedef unsigned int uid_t; typedef unsigned int gid_t; void wl_client_get_credentials(struct wl_client *client, pid_t *pid, uid_t *uid, gid_t *gid); void wl_client_add_destroy_listener(struct wl_client *client, struct wl_listener *listener); struct wl_resource * wl_client_get_object(struct wl_client *client, uint32_t id); """ # wl_resource methods CDEF += """ typedef void (*wl_resource_destroy_func_t)(struct wl_resource *resource); void wl_resource_post_event_array(struct wl_resource *resource, uint32_t opcode, union wl_argument *args); void wl_resource_post_error(struct wl_resource *resource, uint32_t code, const char *msg, ...); struct wl_resource * wl_resource_create(struct wl_client *client, const struct wl_interface *interface, int version, uint32_t id); void wl_resource_set_dispatcher(struct wl_resource *resource, wl_dispatcher_func_t dispatcher, const void *implementation, void *data, wl_resource_destroy_func_t destroy); void wl_resource_destroy(struct wl_resource *resource); uint32_t wl_resource_get_id(struct wl_resource *resource); void * wl_resource_get_user_data(struct wl_resource *resource); int wl_resource_get_version(struct wl_resource *resource); struct wl_client * wl_resource_get_client(struct wl_resource *resource); void wl_resource_add_destroy_listener(struct wl_resource *resource, struct wl_listener * listener); """ # anonymous file methods (from Weston) CDEF += """ int os_create_anonymous_file(int size); """ # cffi callback functions CDEF += """ extern "Python" int dispatcher_func(const void *, void *, uint32_t, const struct wl_message *, union wl_argument *); extern "Python" void resource_destroy_func(struct wl_resource *); extern "Python" int event_loop_fd_func(int, uint32_t, void *); extern "Python" int event_loop_signal_func(int, void *); extern "Python" int event_loop_timer_func(void *); extern "Python" void event_loop_idle_func(void *); extern "Python" void global_bind_func(struct wl_client *, void *, uint32_t, uint32_t); extern "Python" void notify_func(struct wl_listener *, void *); struct wl_listener_container { void *handle; struct wl_listener destroy_listener; }; """ SOURCE = """ #include #include #include #include #include #include """ SOURCE += """ struct wl_listener_container { void *handle; struct wl_listener destroy_listener; }; """ SOURCE += """ /* This code is taken from Weston (MIT licensed) to provide access to anonymous * files with CLOEXEC set * Copyright (c) 2012 Collabora, Ltd. * http://cgit.freedesktop.org/wayland/weston/tree/shared/os-compatibility.c?id=1.8.0 */ static int set_cloexec_or_close(int fd) { long flags; if (fd == -1) return -1; flags = fcntl(fd, F_GETFD); if (flags == -1) goto err; if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1) goto err; return fd; err: close(fd); return -1; } static int create_tmpfile_cloexec(char *tmpname) { int fd; #ifdef HAVE_MKOSTEMP fd = mkostemp(tmpname, O_CLOEXEC); if (fd >= 0) unlink(tmpname); #else fd = mkstemp(tmpname); if (fd >= 0) { fd = set_cloexec_or_close(fd); unlink(tmpname); } #endif return fd; } int os_create_anonymous_file(off_t size) { static const char template[] = "/weston-shared-XXXXXX"; const char *path; char *name; int fd; int ret; path = getenv("XDG_RUNTIME_DIR"); if (!path) { errno = ENOENT; return -1; } name = malloc(strlen(path) + sizeof(template)); if (!name) return -1; strcpy(name, path); strcat(name, template); fd = create_tmpfile_cloexec(name); free(name); if (fd < 0) return -1; #ifdef HAVE_POSIX_FALLOCATE ret = posix_fallocate(fd, 0, size); if (ret != 0) { close(fd); errno = ret; return -1; } #else ret = ftruncate(fd, size); if (ret < 0) { close(fd); return -1; } #endif return fd; } """ ffi_builder = FFI() ffi_builder.set_source( "pywayland._ffi", SOURCE, libraries=["wayland-client", "wayland-server"] ) ffi_builder.cdef(CDEF) if __name__ == "__main__": ffi_builder.compile() pywayland-0.4.18/test/0000775000175000017500000000000014651243522014505 5ustar epsilonepsilonpywayland-0.4.18/test/test_registry_bind.py0000664000175000017500000000473514651243522020773 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import threading import time from pywayland.client import Display as ClientDisplay from pywayland.protocol.wayland import WlCompositor from pywayland.server import Display as ServerDisplay def _get_registry_callback(registry, id, iface_name, version): global compositor if iface_name == "wl_compositor": compositor = registry.bind(id, WlCompositor, version) def _run_client(): c = ClientDisplay() start = time.time() while time.time() < start + 10: try: c.connect() except Exception: time.sleep(0.1) continue break reg = c.get_registry() reg.dispatcher["global"] = _get_registry_callback c.dispatch(block=True) c.roundtrip() c.disconnect() def _bind_handler(registry): global bound bound = registry def _kill_server(data): # the `data` is the server Display data.terminate() return 1 def test_get_registry(): global bound, compositor bound = None compositor = None # run the client in a thread client = threading.Thread(target=_run_client) client.start() # create a server s = ServerDisplay() # Add a compositor so we can query for it global_ = WlCompositor.global_class(s) global_.bind_func = _bind_handler # Add a timer to kill the server after 0.5 sec (should be more than enough # time, don't know a more deterministic way...) e = s.get_event_loop() source = e.add_timer(_kill_server, data=s) source.timer_update(500) # start the server s.add_socket() s.run() s.destroy() # wait for the client (shouldn't block on roundtrip once the server is down) client.join() # make sure we got the compositor in the client assert compositor # and that it has the compositor requests assert compositor.create_surface # Check that the server got the bind and ran the handler assert bound is not None pywayland-0.4.18/test/test_server_display.py0000664000175000017500000000201614651243522021150 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import pytest from pywayland.client.display import Display as ClientDisplay from pywayland.server.display import Display as ServerDisplay def test_display_socket(): """Test that we can create a socket and have a client connect to it""" s = ServerDisplay() s.add_socket("wayland-0") with pytest.raises(Exception): s.add_socket("wayland-0") c = ClientDisplay() c.connect() assert c._ptr c.disconnect() s.destroy() pywayland-0.4.18/test/test_utils.py0000664000175000017500000000155614651243522017265 0ustar epsilonepsilon# Copyright 2021 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import pytest from pywayland.utils import AnonymousFile def test_anonymous_file(): with AnonymousFile(10) as fd: assert fd > 0 f = AnonymousFile(10) f.close() f.open() with pytest.raises(IOError, match="File is already open"): f.open() f.close() f.close() pywayland-0.4.18/test/test_scanner.py0000664000175000017500000000444114651243522017552 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import tempfile import pytest from pywayland.scanner import Protocol this_dir = os.path.split(__file__)[0] scanner_dir = os.path.join(this_dir, "scanner_files") input_file = os.path.join(scanner_dir, "test_scanner_input.xml") interface_tests = [ "__init__.py", "wl_core.py", "wl_events.py", "wl_requests.py", "wl_destructor.py", pytest.param("wl_xfail.py", marks=pytest.mark.xfail), ] @pytest.fixture(scope="session") def protocol_directory(): scanner = Protocol.parse_file(input_file) imports = {interface.name: scanner.name for interface in scanner.interface} generated_files = [ iface if isinstance(iface, str) else iface.values[0] for iface in interface_tests ] with tempfile.TemporaryDirectory() as output_dir: scanner.output(output_dir, imports) test_dir = os.path.join(output_dir, "scanner_test") assert os.path.exists(test_dir) assert set(os.listdir(test_dir)) == set(generated_files) yield test_dir @pytest.mark.parametrize("iface_name", interface_tests) def test_interface(protocol_directory, iface_name): # Get the generated file output generated_file = os.path.join(protocol_directory, iface_name) with open(generated_file) as f: gen_lines = [line.strip("\n") for line in f.readlines()] # Get output to check against check_file = os.path.join(scanner_dir, iface_name) with open(check_file) as f: check_lines = [line.strip("\n") for line in f.readlines()] # Run through both files, checking each line for gen_line, check_line in zip(gen_lines, check_lines): assert gen_line == check_line # Should both be the same length assert len(gen_lines) == len(check_lines) pywayland-0.4.18/test/test_protocol_interface.py0000664000175000017500000001230114651243522021774 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import re import pytest from cffi import FFI from pywayland.protocol.wayland import ( WlBuffer, WlCallback, WlCompositor, WlDataDevice, WlDataDeviceManager, WlDataOffer, WlDataSource, WlDisplay, WlKeyboard, WlOutput, WlPointer, WlRegion, WlRegistry, WlSeat, WlShell, WlShellSurface, WlShm, WlShmPool, WlSubcompositor, WlSubsurface, WlSurface, WlTouch, ) ffi = FFI() ffi.cdef( """ struct wl_message { const char *name; const char *signature; const struct wl_interface **types; }; struct wl_interface { const char *name; int version; int method_count; const struct wl_message *methods; int event_count; const struct wl_message *events; }; extern const struct wl_interface wl_buffer_interface; extern const struct wl_interface wl_callback_interface; extern const struct wl_interface wl_compositor_interface; extern const struct wl_interface wl_data_device_manager_interface; extern const struct wl_interface wl_data_device_interface; extern const struct wl_interface wl_data_offer_interface; extern const struct wl_interface wl_data_source_interface; extern const struct wl_interface wl_display_interface; extern const struct wl_interface wl_keyboard_interface; extern const struct wl_interface wl_output_interface; extern const struct wl_interface wl_pointer_interface; extern const struct wl_interface wl_region_interface; extern const struct wl_interface wl_registry_interface; extern const struct wl_interface wl_seat_interface; extern const struct wl_interface wl_shell_interface; extern const struct wl_interface wl_shell_surface_interface; extern const struct wl_interface wl_shm_pool_interface; extern const struct wl_interface wl_shm_interface; extern const struct wl_interface wl_subcompositor_interface; extern const struct wl_interface wl_subsurface_interface; extern const struct wl_interface wl_surface_interface; extern const struct wl_interface wl_touch_interface; """ ) C = ffi.verify( """ #include """, libraries=["wayland-server", "wayland-client"], ) # Check the generated cdata interfaces against actual ones, list of all # interfaces as of wayland 1.7.0 interfaces = [ (WlBuffer, C.wl_buffer_interface), (WlCallback, C.wl_callback_interface), (WlCompositor, C.wl_compositor_interface), (WlDataDeviceManager, C.wl_data_device_manager_interface), (WlDataDevice, C.wl_data_device_interface), (WlDataOffer, C.wl_data_offer_interface), (WlDataSource, C.wl_data_source_interface), (WlDisplay, C.wl_display_interface), (WlKeyboard, C.wl_keyboard_interface), (WlOutput, C.wl_output_interface), (WlPointer, C.wl_pointer_interface), (WlRegion, C.wl_region_interface), (WlRegistry, C.wl_registry_interface), (WlSeat, C.wl_seat_interface), (WlShell, C.wl_shell_interface), (WlShellSurface, C.wl_shell_surface_interface), (WlShmPool, C.wl_shm_pool_interface), (WlShm, C.wl_shm_interface), (WlSubcompositor, C.wl_subcompositor_interface), (WlSubsurface, C.wl_subsurface_interface), (WlSurface, C.wl_surface_interface), (WlTouch, C.wl_touch_interface), ] re_arg = re.compile(r"(\??)([uifsonah])") def verify_wl_message(py_ptr, wl_ptr): """Verify the wl_message Check the wl_message associated with the given cdata pointer against the given wl_interface object """ assert ffi.string(wl_ptr.name) == ffi.string(py_ptr.name) assert ffi.string(wl_ptr.signature) == ffi.string(py_ptr.signature) signature = ffi.string(wl_ptr.signature).decode() nargs = len(re_arg.findall(signature)) for j in range(nargs): wl_type = wl_ptr.types[j] py_type = py_ptr.types[j] if wl_type == ffi.NULL: assert py_type == ffi.NULL, j else: assert py_type != ffi.NULL assert ffi.string(wl_type.name) == ffi.string(py_type.name) @pytest.mark.parametrize("py_cls,wl_ptr", interfaces) def test_wl_interface(py_cls, wl_ptr): """Verify that the wl_interface of the Python class Check the wl_interface associated with the given Python class against the given wl_interface object """ py_ptr = py_cls._ptr assert ffi.string(wl_ptr.name) == ffi.string(py_ptr.name) assert wl_ptr.version == py_ptr.version assert wl_ptr.event_count == py_ptr.event_count assert wl_ptr.method_count == py_ptr.method_count for i in range(wl_ptr.method_count): verify_wl_message(py_ptr.methods[i], wl_ptr.methods[i]) assert wl_ptr.event_count == py_ptr.event_count for i in range(wl_ptr.event_count): verify_wl_message(py_ptr.events[i], wl_ptr.events[i]) pywayland-0.4.18/test/scanner_files/0000775000175000017500000000000014651243522017320 5ustar epsilonepsilonpywayland-0.4.18/test/scanner_files/wl_core.py0000664000175000017500000000734114651243522021331 0ustar epsilonepsilon# This file has been autogenerated by the pywayland scanner # Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import enum from pywayland.protocol_core import ( Argument, ArgumentType, Global, Interface, Proxy, Resource, ) from .wl_requests import WlRequests class WlCore(Interface): """Interface object The interface object with the most basic content. """ name = "wl_core" version = 1 class the_enum(enum.IntEnum): zero = 0 one = 1 hex_two = 0x2 class WlCoreProxy(Proxy[WlCore]): interface = WlCore @WlCore.request( Argument(ArgumentType.NewId, interface=WlCore), Argument(ArgumentType.Int), Argument(ArgumentType.Uint), Argument(ArgumentType.Fixed), ) def make_request(self, the_int: int, the_uint: int, the_fixed: float) -> Proxy[WlCore]: """A request The request asks the server for an event. :param the_int: the arg summary :type the_int: `ArgumentType.Int` :param the_uint: :type the_uint: `ArgumentType.Uint` :param the_fixed: :type the_fixed: `ArgumentType.Fixed` :returns: :class:`WlCore` """ id = self._marshal_constructor(0, WlCore, the_int, the_uint, the_fixed) return id @WlCore.request( Argument(ArgumentType.Int), Argument(ArgumentType.Uint), Argument(ArgumentType.Fixed), Argument(ArgumentType.NewId, interface=WlCore), ) def make_request2(self, the_int: int, the_uint: int, the_fixed: float) -> Proxy[WlCore]: """A request The request asks the server for an event but move the args around. :param the_int: the arg summary :type the_int: `ArgumentType.Int` :param the_uint: :type the_uint: `ArgumentType.Uint` :param the_fixed: :type the_fixed: `ArgumentType.Fixed` :returns: :class:`WlCore` -- a :class:`WlCore` object """ id = self._marshal_constructor(1, WlCore, the_int, the_uint, the_fixed) return id class WlCoreResource(Resource): interface = WlCore @WlCore.event( Argument(ArgumentType.NewId, interface=WlCore), Argument(ArgumentType.Object, interface=WlRequests), ) def send_event(self, id: WlCore, object: WlRequests) -> None: """A :class:`WlCore` event Send an event, but also put in some docs for our interface :class:`WlCore`, some other interface wl_other, a local function call :func:`WlCore.func()`, and another function call wl_other.func. :param id: a :class:`WlCore` object :type id: :class:`WlCore` :param object: a :class:`~pywayland.protocol.scanner_test.WlRequests` object :type object: :class:`~pywayland.protocol.scanner_test.WlRequests` """ self._post_event(0, id, object) class WlCoreGlobal(Global): interface = WlCore WlCore._gen_c() WlCore.proxy_class = WlCoreProxy WlCore.resource_class = WlCoreResource WlCore.global_class = WlCoreGlobal pywayland-0.4.18/test/scanner_files/wl_events.py0000664000175000017500000001027014651243522021700 0ustar epsilonepsilon# This file has been autogenerated by the pywayland scanner # Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations from pywayland.protocol_core import ( Argument, ArgumentType, Global, Interface, Proxy, Resource, ) from .wl_core import WlCore from .wl_requests import WlRequests class WlEvents(Interface): """Events object The interface object with the different types of events. """ name = "wl_events" version = 2 class WlEventsProxy(Proxy[WlEvents]): interface = WlEvents class WlEventsResource(Resource): interface = WlEvents @WlEvents.event( Argument(ArgumentType.NewId, interface=WlRequests), Argument(ArgumentType.Int), Argument(ArgumentType.Uint), Argument(ArgumentType.FileDescriptor), ) def send_event(self, id: WlRequests, the_int: int, the_uint: int, the_fd: int) -> None: """Send the data Request for data from the client. Send the data as the specified mime type over the passed file descriptor, then close it. :param id: :type id: :class:`~pywayland.protocol.scanner_test.WlRequests` :param the_int: :type the_int: `ArgumentType.Int` :param the_uint: the arg summary :type the_uint: `ArgumentType.Uint` :param the_fd: :type the_fd: `ArgumentType.FileDescriptor` """ self._post_event(0, id, the_int, the_uint, the_fd) @WlEvents.event() def no_args(self) -> None: """Event with no args An event method that does not have any arguments. """ self._post_event(1) @WlEvents.event( Argument(ArgumentType.NewId, interface=WlCore), ) def create_id(self, id: WlCore) -> None: """Create an id With a description :param id: :type id: :class:`~pywayland.protocol.scanner_test.WlCore` """ self._post_event(2, id) @WlEvents.event( Argument(ArgumentType.NewId, interface=WlCore), ) def create_id2(self, id: WlCore) -> None: """Create an id without a description :param id: :type id: :class:`~pywayland.protocol.scanner_test.WlCore` """ self._post_event(3, id) @WlEvents.event( Argument(ArgumentType.String, nullable=True), ) def allow_null_event(self, null_string: str | None) -> None: """A event with an allowed null argument An event where one of the arguments is allowed to be null. :param null_string: :type null_string: `ArgumentType.String` or `None` """ self._post_event(4, null_string) @WlEvents.event( Argument(ArgumentType.NewId, interface=WlRequests), Argument(ArgumentType.Object, interface=WlCore, nullable=True), ) def make_import(self, id: WlRequests, object: WlCore | None) -> None: """Event that causes an import An event method that causes an imoprt of other interfaces :param id: :type id: :class:`~pywayland.protocol.scanner_test.WlRequests` :param object: :type object: :class:`~pywayland.protocol.scanner_test.WlCore` or `None` """ self._post_event(5, id, object) @WlEvents.event(version=2) def versioned(self) -> None: """A versioned event An event that is versioned. """ self._post_event(6) class WlEventsGlobal(Global): interface = WlEvents WlEvents._gen_c() WlEvents.proxy_class = WlEventsProxy WlEvents.resource_class = WlEventsResource WlEvents.global_class = WlEventsGlobal pywayland-0.4.18/test/scanner_files/__init__.py0000664000175000017500000000154614651243522021437 0ustar epsilonepsilon# This file has been autogenerated by the pywayland scanner # Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .wl_core import WlCore # noqa: F401 from .wl_destructor import WlDestructor # noqa: F401 from .wl_events import WlEvents # noqa: F401 from .wl_requests import WlRequests # noqa: F401 from .wl_xfail import WlXfail # noqa: F401 pywayland-0.4.18/test/scanner_files/wl_destructor.py0000664000175000017500000000501014651243522022566 0ustar epsilonepsilon# This file has been autogenerated by the pywayland scanner # Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations from pywayland.protocol_core import ( Argument, ArgumentType, Global, Interface, Proxy, Resource, ) class WlDestructor(Interface): """Destructor object An interface object with a destructor request. And a multiline description. """ name = "wl_destructor" version = 1 class WlDestructorProxy(Proxy[WlDestructor]): interface = WlDestructor @WlDestructor.request( Argument(ArgumentType.NewId, interface=WlDestructor), Argument(ArgumentType.Int), Argument(ArgumentType.Int), Argument(ArgumentType.Int), Argument(ArgumentType.Int), Argument(ArgumentType.Uint), ) def create_interface(self, x: int, y: int, width: int, height: int, format: int) -> Proxy[WlDestructor]: """Create another interface Create a :class:`WlDestructor` interface object :param x: :type x: `ArgumentType.Int` :param y: :type y: `ArgumentType.Int` :param width: :type width: `ArgumentType.Int` :param height: :type height: `ArgumentType.Int` :param format: :type format: `ArgumentType.Uint` :returns: :class:`WlDestructor` """ id = self._marshal_constructor(0, WlDestructor, x, y, width, height, format) return id @WlDestructor.request() def destroy(self) -> None: """Destroy the interface Destroy the created interface. """ self._marshal(1) self._destroy() class WlDestructorResource(Resource): interface = WlDestructor class WlDestructorGlobal(Global): interface = WlDestructor WlDestructor._gen_c() WlDestructor.proxy_class = WlDestructorProxy WlDestructor.resource_class = WlDestructorResource WlDestructor.global_class = WlDestructorGlobal pywayland-0.4.18/test/scanner_files/wl_xfail.py0000664000175000017500000000235014651243522021477 0ustar epsilonepsilon# This file has been autogenerated by the pywayland scanner # Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations from pywayland.protocol_core import Global, Interface, Proxy, Resource class WlXfail(Interface): """Xfailing interface Items that do not really work yet are put in here, they should be moved once they shart working. """ name = "wl_xfail" version = 1 class WlXfailProxy(Proxy[WlXfail]): interface = WlXfail class WlXfailResource(Resource): interface = WlXfail class WlXfailGlobal(Global): interface = WlXfail WlXfail._gen_c() WlXfail.proxy_class = WlXfailProxy WlXfail.resource_class = WlXfailResource WlXfail.global_class = WlXfailGlobal pywayland-0.4.18/test/scanner_files/test_scanner_input.xml0000664000175000017500000001705414651243522023760 0ustar epsilonepsilon Copyright 2015 Sean Vig Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. The interface object with the most basic content. The request asks the server for an event. The request asks the server for an event but move the args around. Send an event, but also put in some docs for our interface wl_core, some other interface wl_other, a local function call wl_core.func, and another function call wl_other.func. This is the enum. The interface object with the different types of requests. The request asks the server for an event. A request method that does not have any arguments. With a description A request where one of the arguments is allowed to be null. A request method that causes an imoprt of other interfaces, both as a new_id and as an object. A request that is versioned. A method with an argument for a new_id, but with no corresponding interface (c.f. wl_registry.bind). The interface object with the different types of events. Request for data from the client. Send the data as the specified mime type over the passed file descriptor, then close it. An event method that does not have any arguments. With a description An event where one of the arguments is allowed to be null. An event method that causes an imoprt of other interfaces An event that is versioned. An interface object with a destructor request. And a multiline description. Create a wl_destructor interface object Destroy the created interface. Items that do not really work yet are put in here, they should be moved once they shart working. pywayland-0.4.18/test/scanner_files/wl_requests.py0000664000175000017500000001235514651243522022255 0ustar epsilonepsilon# This file has been autogenerated by the pywayland scanner # Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations from typing import TypeVar from pywayland.protocol_core import ( Argument, ArgumentType, Global, Interface, Proxy, Resource, ) from .wl_core import WlCore from .wl_events import WlEvents T = TypeVar("T", bound=Interface) class WlRequests(Interface): """Request object The interface object with the different types of requests. """ name = "wl_requests" version = 2 class WlRequestsProxy(Proxy[WlRequests]): interface = WlRequests @WlRequests.request( Argument(ArgumentType.NewId, interface=WlCore), Argument(ArgumentType.Int), Argument(ArgumentType.Uint), Argument(ArgumentType.FileDescriptor), ) def make_request(self, the_int: int, the_uint: int, the_fd: int) -> Proxy[WlCore]: """A request The request asks the server for an event. :param the_int: :type the_int: `ArgumentType.Int` :param the_uint: the arg summary :type the_uint: `ArgumentType.Uint` :param the_fd: :type the_fd: `ArgumentType.FileDescriptor` :returns: :class:`~pywayland.protocol.scanner_test.WlCore` """ id = self._marshal_constructor(0, WlCore, the_int, the_uint, the_fd) return id @WlRequests.request() def no_args(self) -> None: """Request with no args A request method that does not have any arguments. """ self._marshal(1) @WlRequests.request( Argument(ArgumentType.NewId, interface=WlCore), ) def create_id(self) -> Proxy[WlCore]: """Create an id With a description :returns: :class:`~pywayland.protocol.scanner_test.WlCore` """ id = self._marshal_constructor(2, WlCore) return id @WlRequests.request( Argument(ArgumentType.NewId, interface=WlCore), ) def create_id2(self) -> Proxy[WlCore]: """Create an id without a description :returns: :class:`~pywayland.protocol.scanner_test.WlCore` """ id = self._marshal_constructor(3, WlCore) return id @WlRequests.request( Argument(ArgumentType.Uint), Argument(ArgumentType.String, nullable=True), ) def allow_null(self, serial: int, mime_type: str | None) -> None: """Request that allows for null arguments A request where one of the arguments is allowed to be null. :param serial: :type serial: `ArgumentType.Uint` :param mime_type: :type mime_type: `ArgumentType.String` or `None` """ self._marshal(4, serial, mime_type) @WlRequests.request( Argument(ArgumentType.NewId, interface=WlEvents), Argument(ArgumentType.Object, interface=WlCore, nullable=True), ) def make_import(self, object: WlCore | None) -> Proxy[WlEvents]: """Request that causes an import A request method that causes an imoprt of other interfaces, both as a new_id and as an object. :param object: :type object: :class:`~pywayland.protocol.scanner_test.WlCore` or `None` :returns: :class:`~pywayland.protocol.scanner_test.WlEvents` """ id = self._marshal_constructor(5, WlEvents, object) return id @WlRequests.request(version=2) def versioned(self) -> None: """A versioned request A request that is versioned. """ self._marshal(6) @WlRequests.request( Argument(ArgumentType.Uint), Argument(ArgumentType.NewId), ) def new_id_no_interface(self, name: int, interface: type[T], version: int) -> Proxy[T]: """Create a new id, but with no interface A method with an argument for a new_id, but with no corresponding interface (c.f. wl_registry.bind). :param name: :type name: `ArgumentType.Uint` :param interface: Interface name :type interface: `string` :param version: Interface version :type version: `int` :returns: :class:`pywayland.client.proxy.Proxy` of specified Interface """ id = self._marshal_constructor(7, interface, name, interface.name, version) return id class WlRequestsResource(Resource): interface = WlRequests class WlRequestsGlobal(Global): interface = WlRequests WlRequests._gen_c() WlRequests.proxy_class = WlRequestsProxy WlRequests.resource_class = WlRequestsResource WlRequests.global_class = WlRequestsGlobal pywayland-0.4.18/test/test_resource.py0000664000175000017500000000551014651243522017746 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import socket from pywayland.protocol.wayland import WlDisplay from pywayland.server import Client, Display, Listener def test_create_resource(): s1, s2 = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM, 0) display = Display() client = Client(display, s1.fileno()) # Create resource res = WlDisplay.resource_class(client, version=4) assert res.version == 4 # Fetching the client object by id gives the resource back again assert client.get_object(res.id) == res client.user_data = 0xBEE assert client.user_data == 0xBEE client.destroy() display.destroy() s2.close() def _destroy_callback(data): global destroyed destroyed = True def _destroy_notify(listener, data): global notified notified = True def test_destroy_resource(): global destroyed, notified destroyed = False notified = False s1, s2 = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM, 0) display = Display() client = Client(display, s1.fileno()) listener = Listener(_destroy_notify) # Create resource res = WlDisplay.resource_class(client, version=4) # Attach a destructor and a destroy notification res.dispatcher.destructor = _destroy_callback res.add_destroy_listener(listener) # Destroy the resource res.destroy() assert destroyed assert notified assert client.get_object(res.id) is None # Create resource res = WlDisplay.resource_class(client, version=2) # Attach a destructor and a destroy notification res.dispatcher.destructor = _destroy_callback res.add_destroy_listener(listener) # Destroy the client client.destroy() assert destroyed assert notified display.destroy() s2.close() def notest_create_resource_with_same_id(): s1, s2 = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM, 0) display = Display() client = Client(display, s1.fileno()) # Create resource res = WlDisplay.resource_class(client, version=2) assert client.get_object(res.id) == res # This should replace the old one res2 = WlDisplay.resource_class(client, version=1, id=res.id) assert client.get_object(res.id) == res2 res2.destroy() res.destroy() client.destroy() display.destroy() s2.close() pywayland-0.4.18/test/__init__.py0000664000175000017500000000107414651243522016620 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. pywayland-0.4.18/test/test_event_queue.py0000664000175000017500000000310714651243522020444 0ustar epsilonepsilon# Copyright 2021 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import threading import time import pytest from pywayland.client.display import Display from pywayland.client.eventqueue import EventQueue from pywayland.server.display import Display as ServerDisplay def _run_server(): s = ServerDisplay() e = s.get_event_loop() source = e.add_timer(_kill_server, data=s) source.timer_update(500) s.add_socket() s.run() s.destroy() def _kill_server(data): data.terminate() return 1 def test_event_queue(): server = threading.Thread(target=_run_server) server.start() try: display = Display() start = time.time() while time.time() < start + 1: try: display.connect() except Exception: time.sleep(0.1) else: break else: pytest.fail("Could not connect to server") with display: event_queue = EventQueue(display) display.dispatch(queue=event_queue) finally: server.join() pywayland-0.4.18/test/test_server_eventloop.py0000664000175000017500000001037614651243522021526 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import signal import time from pywayland.server.eventloop import EventLoop from pywayland.server.listener import Listener class _GetCallback: def __init__(self): self.callback = None self.n_calls = 0 def _fd_callback(fd, mask, data): data.callback = fd data.n_calls += 1 return 0 def _signal_callback(signal_number, data): data.callback = signal_number data.n_calls += 1 return 1 def _timer_callback(data): data.n_calls += 1 return 1 def test_event_loop_post_dispatch_check(): callback = _GetCallback() event_loop = EventLoop() r, w = os.pipe() try: source = event_loop.add_fd( r, _fd_callback, EventLoop.FdMask.WL_EVENT_READABLE, callback ) source.check() event_loop.dispatch(0) assert callback.callback == r finally: os.close(r) os.close(w) def test_event_loop_signal(): callback = _GetCallback() event_loop = EventLoop() event_loop.add_signal(signal.SIGUSR1, _signal_callback, callback) event_loop.dispatch(0) assert callback.callback is None os.kill(os.getpid(), signal.SIGUSR1) event_loop.dispatch(0) assert callback.callback == signal.SIGUSR1 def test_event_loop_multiple_same_signals(): callback = _GetCallback() event_loop = EventLoop() signal_rm = event_loop.add_signal(signal.SIGUSR1, _signal_callback, callback) event_loop.add_signal(signal.SIGUSR1, _signal_callback, callback) event_loop.dispatch(0) assert callback.n_calls == 0 # Check callback gets 2 calls for _ in range(5): callback.n_calls = 0 os.kill(os.getpid(), signal.SIGUSR1) event_loop.dispatch(0) assert callback.n_calls == 2 # Remove one of the signals signal_rm.remove() # Callback only gets call now for _ in range(5): callback.n_calls = 0 os.kill(os.getpid(), signal.SIGUSR1) event_loop.dispatch(0) assert callback.n_calls == 1 def test_event_loop_timer(): callback = _GetCallback() event_loop = EventLoop() source = event_loop.add_timer(_timer_callback, callback) source.timer_update(10) event_loop.dispatch(0) assert callback.n_calls == 0 event_loop.dispatch(20) assert callback.n_calls == 1 def _timer_update_callback1(data): data.n_calls += 1 data.source2.timer_update(1000) return 1 def _timer_update_callback2(data): data.n_calls += 1 data.source1.timer_update(1000) return 1 def test_event_loop_timer_updates(): callback = _GetCallback() event_loop = EventLoop() source1 = event_loop.add_timer(_timer_update_callback1, callback) source1.timer_update(10) source2 = event_loop.add_timer(_timer_update_callback2, callback) source2.timer_update(10) callback.source1 = source1 callback.source2 = source2 assert callback.n_calls == 0 # Wait 15 ms, so both timers should be called when we dispatch time.sleep(0.015) # This should take < 1 sec start_time = time.time() event_loop.dispatch(20) end_time = time.time() assert callback.n_calls == 2 assert end_time - start_time < 1 def _destroy_notify_a(listener, data): global a a = True def _destroy_notify_b(listener, data): global b b = True def test_event_loop_destroy(): global a, b a = False b = False event_loop = EventLoop() listener_a = Listener(_destroy_notify_a) listener_b = Listener(_destroy_notify_b) event_loop.add_destroy_listener(listener_a) event_loop.add_destroy_listener(listener_b) listener_a.remove() event_loop.destroy() import gc gc.collect() assert a is False assert b is True pywayland-0.4.18/test/test_registry_query.py0000664000175000017500000000432514651243522021217 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import threading import time from pywayland.client import Display as ClientDisplay from pywayland.protocol.wayland import WlCompositor from pywayland.server import Display as ServerDisplay got_compositor = None def _get_registry_callback(proxy, id, iface_name, version): global got_compositor if iface_name == "wl_compositor": got_compositor = proxy return 1 def _run_client(): global got_compositor c = ClientDisplay() start = time.time() while time.time() < start + 10: try: c.connect() except Exception: time.sleep(0.1) continue break reg = c.get_registry() reg.dispatcher["global"] = _get_registry_callback c.roundtrip() c.disconnect() def _kill_server(data): # the `data` is the server Display data.terminate() def test_get_registry(): global got_compositor got_compositor = None # run the client in a thread client = threading.Thread(target=_run_client) client.start() # create the server s = ServerDisplay() # Add a compositor so we can query for it (and keep it alive) compositor = WlCompositor.global_class(s) # noqa # Add a timer to kill the server after 0.5 sec (should be more than enough # time, don't know a more deterministic way...) e = s.get_event_loop() source = e.add_timer(_kill_server, data=s) source.timer_update(500) # start up the server s.add_socket() s.run() s.destroy() # wait for the client (shouldn't block on roundtrip once the server is down) client.join() # make sure we got the compositor in the client assert got_compositor pywayland-0.4.18/test/test_client_destroy.py0000664000175000017500000000250014651243522021142 0ustar epsilonepsilon# Copyright 2015 Sean Vig # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import socket from pywayland.server.client import Client from pywayland.server.display import Display from pywayland.server.listener import Listener def destroy_notify_a(*args): global a a = 1 def destroy_notify_b(*args): global b b = 1 def test_client_destroy_listener(): global a, b s1, s2 = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM, 0) a = 0 b = 0 display = Display() client = Client(display, s1.fileno()) destroy_listener_a = Listener(destroy_notify_a) destroy_listener_b = Listener(destroy_notify_b) client.add_destroy_listener(destroy_listener_a) client.add_destroy_listener(destroy_listener_b) destroy_listener_a.remove() client.destroy() assert a == 0 assert b == 1