flacjacket-pywayland-7f48c57/0000775000175000017500000000000015240077162016072 5ustar epsilonepsilonflacjacket-pywayland-7f48c57/MANIFEST.in0000664000175000017500000000016715240077162017634 0ustar epsilonepsiloninclude *.py include *.yaml recursive-include pywayland *.pyi recursive-include test *.py recursive-include test *.xml flacjacket-pywayland-7f48c57/README.rst0000664000175000017500000000573315240077162017571 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. 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 If ``pywayland._ffi`` is not built yet, run the scanner as a top-level module from the ``./pywayland`` directory instead:: $ cd pywayland $ python -m 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 flacjacket-pywayland-7f48c57/.gitignore0000664000175000017500000000043215240077162020061 0ustar epsilonepsilon# Python bytecode *.pyc # cffi output *.so *.o *.c # Test output .coverage .cache .mypy_cache # Build output /dist/ /build/ /pywayland.egg-info .eggs/ # Scanner output /pywayland/protocol # Vim swap files *.swp .ycm_extra_conf.py # Doc output /doc/_build/ /doc/module/protocol/ flacjacket-pywayland-7f48c57/doc/0000775000175000017500000000000015240077162016637 5ustar epsilonepsilonflacjacket-pywayland-7f48c57/doc/scanner.rst0000664000175000017500000000216315240077162021024 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 :ref:`scanner`. flacjacket-pywayland-7f48c57/doc/install.rst0000664000175000017500000001002415240077162021034 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 let you build the protocol files against a different version than is available through pip. 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/ Wayland Protocols ^^^^^^^^^^^^^^^^^ 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. Running PyWayland inplace ^^^^^^^^^^^^^^^^^^^^^^^^^ If the libwayland header files are correctly installed and if the Wayland protocol file is in the default location (``/usr/share/wayland/wayland.xml``) or can be found with ``pkg-config``, you can build the cffi module and then generate the protocol files without any problems:: $ python pywayland/ffi_build.py $ python -m pywayland.scanner --with-protocols 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. You can check that you have everything installed correctly by running the associated test-suite . Simply run ``pytest`` from the root directory. Installing PyWayland ^^^^^^^^^^^^^^^^^^^^ The package can be installed from source using typical pip mechanisms:: $ pip install . This uses the custom build backend, which runs protocol generation and cffi binding generation automatically. Manual pre-compilation is not required for regular pip builds. 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 flacjacket-pywayland-7f48c57/doc/conf.py0000664000175000017500000001421715240077162020143 0ustar epsilonepsilon#!/usr/bin/env python3 import importlib import os import re import shutil import sys # -- Mock necessary classes ----------------------------------------------- from unittest.mock import MagicMock import sphinx_rtd_theme # noqa: F401 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 ( # noqa: E402 protocols_build, protocols_version, wayland_version, ) from pywayland import __version__ # noqa: E402 protocol_build_dir = "../pywayland/protocol/" protocol_doc_dir = "module/protocol" index_header = f"""\ .. _protocol: Protocol Modules ================ Wayland protocols built against Wayland {wayland_version} and Wayland Protocols {protocols_version}. .. toctree:: :maxdepth: 2 """ 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.rstrip(".py") for module in modules if os.path.isfile(os.path.join(input_dir, module)) and module != "__init__.py" ] 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 interfaces that we want to document protocol_file = os.path.join(input_dir, f"{module}.py") interfaces = [] with open(protocol_file) as f: for line in f: match = re.search(r"class (\w+)\(Interface\):", line) if match: interfaces.append(match.group(1)) # build the rst for each interface for interface in interfaces: mod = importlib.import_module(f"pywayland.protocol.{module}") if interface not in dir(mod): raise RuntimeError(f"Unable to find module: {interface}, {mod}") output.append( protocol_rst.format( module=module, protocol=interface, len=len(interface), 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" # 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", ), ] flacjacket-pywayland-7f48c57/doc/module/0000775000175000017500000000000015240077162020124 5ustar epsilonepsilonflacjacket-pywayland-7f48c57/doc/module/scanner/0000775000175000017500000000000015240077162021555 5ustar epsilonepsilonflacjacket-pywayland-7f48c57/doc/module/scanner/request.rst0000664000175000017500000000015315240077162023776 0ustar epsilonepsilon.. _scanner-request: Request ======= .. automodule:: pywayland.scanner.request :members: :noindex: flacjacket-pywayland-7f48c57/doc/module/scanner/interface.rst0000664000175000017500000000016315240077162024247 0ustar epsilonepsilon.. _scanner-interface: Interface ========= .. automodule:: pywayland.scanner.interface :members: :noindex: flacjacket-pywayland-7f48c57/doc/module/scanner/method.rst0000664000175000017500000000014715240077162023571 0ustar epsilonepsilon.. _scanner-method: Method ====== .. automodule:: pywayland.scanner.method :members: :noindex: flacjacket-pywayland-7f48c57/doc/module/scanner/argument.rst0000664000175000017500000000015715240077162024134 0ustar epsilonepsilon.. _scanner-argument: Argument ======== .. automodule:: pywayland.scanner.argument :members: :noindex: flacjacket-pywayland-7f48c57/doc/module/scanner/enum.rst0000664000175000017500000000013715240077162023254 0ustar epsilonepsilon.. _scanner-enum: Enum ==== .. automodule:: pywayland.scanner.enum :members: :noindex: flacjacket-pywayland-7f48c57/doc/module/scanner/event.rst0000664000175000017500000000014315240077162023426 0ustar epsilonepsilon.. _scanner-event: Event ===== .. automodule:: pywayland.scanner.event :members: :noindex: flacjacket-pywayland-7f48c57/doc/module/scanner/protocol.rst0000664000175000017500000000015615240077162024152 0ustar epsilonepsilon.. _scanner-protocol: Protocol ======== .. autoclass:: pywayland.scanner.Protocol :members: :noindex: flacjacket-pywayland-7f48c57/doc/module/scanner/printer.rst0000664000175000017500000000015315240077162023771 0ustar epsilonepsilon.. _scanner-printer: Printer ======= .. automodule:: pywayland.scanner.printer :members: :noindex: flacjacket-pywayland-7f48c57/doc/module/scanner/index.rst0000664000175000017500000000120115240077162023410 0ustar epsilonepsilon.. module:: pywayland.scanner .. _scanner: Scanner Modules =============== 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. .. toctree:: :maxdepth: 2 argument entry enum event interface method printer protocol request flacjacket-pywayland-7f48c57/doc/module/scanner/entry.rst0000664000175000017500000000014315240077162023446 0ustar epsilonepsilon.. _scanner-entry: Entry ===== .. automodule:: pywayland.scanner.entry :members: :noindex: flacjacket-pywayland-7f48c57/doc/module/server.rst0000664000175000017500000000053115240077162022163 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: flacjacket-pywayland-7f48c57/doc/module/client.rst0000664000175000017500000000067015240077162022137 0ustar epsilonepsilon.. module:: pywayland.client .. _client: Client Modules ============== The base set of objects used by Wayland clients. Users should only be directly creating :class:`Display` and :class:`EventQueue` objects. The :class:`~pywayland.protocol_core.Proxy` objects to interfaces should be returned by making request calls. Display ------- .. autoclass:: Display :members: EventQueue ---------- .. autoclass:: EventQueue :members: flacjacket-pywayland-7f48c57/doc/module/utils.rst0000664000175000017500000000025715240077162022022 0ustar epsilonepsilon.. module:: pywayland.utils .. _utils: Utilities Module ================ AnonymousFile Class ------------------- .. autoclass:: pywayland.utils.AnonymousFile :members: flacjacket-pywayland-7f48c57/doc/module/protocol_core.rst0000664000175000017500000000302515240077162023527 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:`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:`Message` objects are used to wrap the method calls on the protocol objects. The Message objects are added to the :class:`Interface`'s as either requests (client-side functions) or events (server-side functions). .. autoclass:: Message :members: Argument -------- .. autoclass:: Argument :members: ArgumentType ------------ .. autoclass:: ArgumentType :members: flacjacket-pywayland-7f48c57/doc/module/index.rst0000664000175000017500000000022215240077162021761 0ustar epsilonepsilonModule Reference ================ .. toctree:: :maxdepth: 2 client server protocol_core protocol/index scanner/index utils flacjacket-pywayland-7f48c57/doc/index.rst0000664000175000017500000000161515240077162020503 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 flacjacket-pywayland-7f48c57/doc/Makefile0000664000175000017500000001516615240077162020310 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." flacjacket-pywayland-7f48c57/doc/protocol_build.py0000664000175000017500000000345415240077162022237 0ustar epsilonepsilonimport os import subprocess import tarfile import urllib.request wayland_version = "1.26.0" protocols_version = "1.49" wayland_source = f"https://cgit.freedesktop.org/wayland/wayland/plain/protocol/wayland.xml?id={wayland_version}" protocols_source = f"https://gitlab.freedesktop.org/wayland/wayland-protocols/-/releases/{protocols_version}/downloads/wayland-protocols-{protocols_version}.tar.xz" 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): # 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) subprocess.run( [ "python", "-m", "pywayland.scanner", "--with-protocols", "--output-dir", output_dir, "--input", wayland_file, "--input-protocols", protocol_dest, ] ) flacjacket-pywayland-7f48c57/doc/sphinx_wl_protocol.py0000664000175000017500000000631215240077162023147 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|replace('_ ',' ') }}{% 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|replace('_ ',' ') }}{% 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) flacjacket-pywayland-7f48c57/doc/contributing.rst0000664000175000017500000000334215240077162022102 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 flacjacket-pywayland-7f48c57/LICENSE0000664000175000017500000002363515240077162017110 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. flacjacket-pywayland-7f48c57/.pre-commit-config.yaml0000664000175000017500000000047315240077162022357 0ustar epsilonepsilonrepos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.16.0 hooks: - id: ruff-check args: [--fix] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy rev: v2.3.0 hooks: - id: mypy args: [] files: "^(pywayland|example)\/.*" flacjacket-pywayland-7f48c57/.github/0000775000175000017500000000000015240077162017432 5ustar epsilonepsilonflacjacket-pywayland-7f48c57/.github/workflows/0000775000175000017500000000000015240077162021467 5ustar epsilonepsilonflacjacket-pywayland-7f48c57/.github/workflows/ci.yml0000664000175000017500000001561215240077162022612 0ustar epsilonepsilonname: CI on: pull_request: push: workflow_dispatch: jobs: build-wayland: name: Build Wayland libraries runs-on: ubuntu-26.04 strategy: fail-fast: false matrix: wayland-version: ["1.26.0"] wayland-protocols-version: ["1.49"] steps: - name: Setup environment run: | echo "CPATH=${HOME}/wayland/usr/include" >> $GITHUB_ENV echo "LD_LIBRARY_PATH=${HOME}/wayland/usr/lib" >> $GITHUB_ENV echo "LIBRARY_PATH=${HOME}/wayland/usr/lib" >> $GITHUB_ENV echo "PKG_CONFIG_PATH=${HOME}/wayland/usr/share/pkgconfig:${HOME}/wayland/usr/lib/pkgconfig" >> $GITHUB_ENV - name: Install dependencies run: | sudo apt-get install -y --no-install-recommends \ ninja-build sudo pip install meson - name: Download and unpack Wayland source run: | wget --no-check-certificate $WAYLAND_URL wget --no-check-certificate $WAYLAND_PROTOCOLS_URL tar -xJf wayland-${{ matrix.wayland-version }}.tar.xz tar -xJf wayland-protocols-${{ matrix.wayland-protocols-version }}.tar.xz env: WAYLAND_URL: https://gitlab.freedesktop.org/wayland/wayland/-/releases/${{ matrix.wayland-version }}/downloads/wayland-${{ matrix.wayland-version }}.tar.xz WAYLAND_PROTOCOLS_URL: https://gitlab.freedesktop.org/wayland/wayland-protocols/-/releases/${{ matrix.wayland-protocols-version }}/downloads/wayland-protocols-${{ matrix.wayland-protocols-version }}.tar.xz - name: Build wayland working-directory: wayland-${{ matrix.wayland-version }} run: | meson setup build --prefix=/usr -Ddocumentation=false DESTDIR=~/wayland ninja -C build install sudo ninja -C build install - name: Build wayland-protocols working-directory: wayland-protocols-${{ matrix.wayland-protocols-version }} run: | meson setup build --prefix=/usr DESTDIR=~/wayland ninja -C build install sudo ninja -C build install - name: Create artifact run: tar czf ~/wayland.tar.gz -C ${HOME}/wayland/ . - name: Upload built libraries uses: actions/upload-artifact@v7 with: name: wayland-${{ matrix.wayland-version }}_${{ matrix.wayland-protocols-version }} path: ~/wayland.tar.gz if-no-files-found: error unit-test: name: Python ${{ matrix.python-version}} unit tests runs-on: ubuntu-26.04 needs: build-wayland strategy: fail-fast: false matrix: python-version: - "3.10" - "3.11" - "3.12" - "3.13" - "3.14" - "pypy-3.11" wayland-version: ["1.26.0"] wayland-protocols-version: ["1.49"] steps: - uses: actions/checkout@v7 - name: Download wayland libraries uses: actions/download-artifact@v7 with: name: wayland-${{ matrix.wayland-version }}_${{ matrix.wayland-protocols-version }} - name: Unpack wayland artifact run: sudo tar xf wayland.tar.gz -C / - name: Setup Python uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - name: Set environment variables run: | echo "XDG_RUNTIME_DIR=/tmp" >> $GITHUB_ENV - name: Install Python dependencies run: | pip install -e .[test] - name: Run unit tests run: | pytest --cov pywayland --cov-report term-missing - name: Push coverage run: coveralls --service=github env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_FLAG_NAME: ${{ matrix.python-version }} COVERALLS_PARALLEL: true coverage: name: Finalize Coverage needs: unit-test runs-on: ubuntu-26.04 steps: - name: Coveralls Finished uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} parallel-finished: true docs-test: name: docs check runs-on: ubuntu-26.04 needs: build-wayland env: python-version: "3.14" wayland-version: "1.26.0" wayland-protocols-version: "1.49" steps: - uses: actions/checkout@v7 - name: Download wayland libraries uses: actions/download-artifact@v7 with: name: wayland-${{ env.wayland-version }}_${{ env.wayland-protocols-version }} - name: Unpack wayland artifact run: sudo tar xf wayland.tar.gz -C / - name: Setup Python uses: actions/setup-python@v7 with: python-version: ${{ env.python-version }} - name: Set environment variables run: | echo "XDG_RUNTIME_DIR=/tmp" >> $GITHUB_ENV - name: Install Python dependencies run: | pip install -e .[docs] - name: Build docs run: | sphinx-build -W -b html doc doc/_build/html pre-commit-test: name: pre-commit test runs-on: ubuntu-26.04 needs: build-wayland env: python-version: "3.14" wayland-version: "1.26.0" wayland-protocols-version: "1.49" steps: - uses: actions/checkout@v7 - name: Download wayland libraries uses: actions/download-artifact@v7 with: name: wayland-${{ env.wayland-version }}_${{ env.wayland-protocols-version }} - name: Unpack wayland artifact run: sudo tar xf wayland.tar.gz -C / - name: Setup Python uses: actions/setup-python@v7 with: python-version: ${{ env.python-version }} - name: Set environment variables run: | echo "XDG_RUNTIME_DIR=/tmp" >> $GITHUB_ENV - name: Generate protocol files working-directory: pywayland run: | python -m scanner --with-protocols - name: Run pre-commit test run: | pip install pre-commit pre-commit run --show-diff-on-failure --color=always --all-files packaging-test: name: packaging test runs-on: ubuntu-26.04 needs: build-wayland env: python-version: "3.14" wayland-version: "1.26.0" wayland-protocols-version: "1.49" steps: - uses: actions/checkout@v7 - name: Download wayland libraries uses: actions/download-artifact@v7 with: name: wayland-${{ env.wayland-version }}_${{ env.wayland-protocols-version }} - name: Unpack wayland artifact run: sudo tar xf wayland.tar.gz -C / - name: Setup Python uses: actions/setup-python@v7 with: python-version: ${{ env.python-version }} - name: Set environment variables run: | echo "XDG_RUNTIME_DIR=/tmp" >> $GITHUB_ENV - name: Install Python dependencies run: | pip install build check-manifest twine - name: Run packaging test run: | check-manifest python -m build --sdist twine check dist/* flacjacket-pywayland-7f48c57/.github/workflows/release.yml0000664000175000017500000001773515240077162023647 0ustar epsilonepsilonname: Publish to PyPI on: pull_request: push: release: types: [published] workflow_dispatch: jobs: build-wayland: name: Build Wayland libraries runs-on: ubuntu-26.04 container: quay.io/pypa/manylinux_2_34_x86_64 env: python-version: "3.14" wayland-version: "1.26.0" wayland-protocols-version: "1.49" steps: - name: Install dependencies run: | yum -y install \ libffi-devel \ libxml2-devel \ ninja-build \ wget - name: Download and unpack Wayland source run: | wget $WAYLAND_URL wget $WAYLAND_PROTOCOLS_URL tar -xJf wayland-${{ env.wayland-version }}.tar.xz tar -xJf wayland-protocols-${{ env.wayland-protocols-version }}.tar.xz env: WAYLAND_URL: https://gitlab.freedesktop.org/wayland/wayland/-/releases/${{ env.wayland-version }}/downloads/wayland-${{ env.wayland-version }}.tar.xz WAYLAND_PROTOCOLS_URL: https://gitlab.freedesktop.org/wayland/wayland-protocols/-/releases/${{ env.wayland-protocols-version }}/downloads/wayland-protocols-${{ env.wayland-protocols-version }}.tar.xz - name: Set environment variables run: | echo "CPATH=/wayland/usr/include" >> $GITHUB_ENV echo "LD_LIBRARY_PATH=/wayland/usr/lib" >> $GITHUB_ENV echo "LIBRARY_PATH=/wayland/usr/lib" >> $GITHUB_ENV echo "PKG_CONFIG_PATH=/wayland/usr/share/pkgconfig:/wayland/usr/lib/pkgconfig" >> $GITHUB_ENV - name: Set python version run: | PYTHON_ROOT=$(find /opt/python -name "cp${PYTHON_VERSION/./}-*" -not -name "*t") echo "${PYTHON_ROOT}/bin" >> $GITHUB_PATH shell: bash env: PYTHON_VERSION: ${{ env.python-version }} - name: Install meson run: | pip install meson - name: Build Wayland working-directory: wayland-${{ env.wayland-version }} run: | meson setup build --prefix=/usr -Ddocumentation=false DESTDIR=/wayland ninja -C build install ninja -C build install - name: Build Wayland protocols working-directory: wayland-protocols-${{ env.wayland-protocols-version }} run: | meson setup build --prefix=/usr DESTDIR=/wayland ninja -C build install ninja -C build install - name: Create artifact run: tar czf /wayland.tar.gz -C /wayland/ . - name: Upload built libraries uses: actions/upload-artifact@v7 with: name: wayland path: /wayland.tar.gz if-no-files-found: error build-wheel-cpython: name: Build CPython wheels runs-on: ubuntu-26.04 container: quay.io/pypa/manylinux_2_34_x86_64 needs: build-wayland strategy: fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - name: Download wayland libraries uses: actions/download-artifact@v7 with: name: wayland - name: Unpack wayland artifact run: tar xf wayland.tar.gz -C / - name: Set python version run: | PYTHON_ROOT=$(find /opt/python -name "cp${PYTHON_VERSION/./}-*" -not -name "*t") echo "${PYTHON_ROOT}/bin" >> $GITHUB_PATH shell: bash env: PYTHON_VERSION: ${{ matrix.python-version }} - uses: actions/checkout@v7 - name: Install Python dependencies run: pip install auditwheel build - name: Build wheels run: | python -m build --wheel auditwheel repair --plat manylinux_2_34_x86_64 -w output_wheels dist/pywayland-*.whl - name: Upload wheels uses: actions/upload-artifact@v7 with: name: wheels-${{ matrix.python-version }} path: output_wheels/*.whl build-wheel-pypy: name: Build PyPy wheels runs-on: ubuntu-26.04 container: quay.io/pypa/manylinux_2_34_x86_64 needs: build-wayland strategy: fail-fast: false matrix: python-version: ["3.11"] pypy-version: ["7.3"] steps: - name: Download wayland libraries uses: actions/download-artifact@v7 with: name: wayland - name: Unpack wayland artifact run: tar xf wayland.tar.gz -C / - name: Set python version run: | ls /opt/python/ PYTHON_ROOT=$(find /opt/python -name "pp${PYTHON_VERSION/./}-*_pp${PP_VERSION/./}" -not -name "*t") echo "${PYTHON_ROOT}" echo "${PYTHON_ROOT}/bin" >> $GITHUB_PATH shell: bash env: PYTHON_VERSION: ${{ matrix.python-version }} PP_VERSION: ${{ matrix.pypy-version }} - uses: actions/checkout@v7 - name: Install Python dependencies run: pip install auditwheel build - name: Build wheels run: | python -m build --wheel auditwheel repair --plat manylinux_2_34_x86_64 -w output_wheels dist/pywayland-*.whl - name: Upload wheels uses: actions/upload-artifact@v7 with: name: wheels-pypy-${{ matrix.python-version }} path: output_wheels/*.whl test-wheel: name: Test wheels runs-on: ubuntu-26.04 needs: [build-wheel-cpython, build-wheel-pypy] strategy: fail-fast: false matrix: python-version: - "3.10" - "3.11" - "3.12" - "3.13" - "3.14" - "pypy-3.11" steps: - name: Download wheels uses: actions/download-artifact@v7 with: name: wheels-${{ matrix.python-version }} - name: Setup Python uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - name: Install wheel run: pip install pywayland-*.whl - name: Check installation shell: python run: | import pywayland print(pywayland.__wayland_version__) import pywayland.protocol.wayland import pywayland.protocol.xdg_shell build-source: name: Build source package runs-on: ubuntu-26.04 container: quay.io/pypa/manylinux_2_34_x86_64 needs: build-wayland env: python-version: "3.14" steps: - name: Download wayland libraries uses: actions/download-artifact@v7 with: name: wayland - name: Unpack wayland artifact run: tar xf wayland.tar.gz -C / - name: Set python version run: | PYTHON_ROOT=$(find /opt/python -name "cp${PYTHON_VERSION/./}-*" -not -name "*t") echo "${PYTHON_ROOT}/bin" >> $GITHUB_PATH shell: bash env: PYTHON_VERSION: ${{ env.python-version }} - uses: actions/checkout@v7 - name: Install Python dependencies run: pip install build - name: Build source run: | python -m build --sdist - name: Upload source uses: actions/upload-artifact@v7 with: name: source path: dist/*.tar.gz upload-wheel: name: Upload wheels runs-on: ubuntu-26.04 needs: [test-wheel, build-source] steps: - name: Download wheels uses: actions/download-artifact@v7 with: pattern: wheels-* path: dist/ merge-multiple: true - name: Download source uses: actions/download-artifact@v7 with: name: source path: dist/ - name: Check artifacts run: ls dist/ - name: Publish package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 if: github.event_name == 'release' with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} - name: Publish package to TestPyPI uses: pypa/gh-action-pypi-publish@release/v1 if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') with: user: __token__ password: ${{ secrets.TEST_PYPI_API_TOKEN }} repository_url: https://test.pypi.org/legacy/ skip_existing: true flacjacket-pywayland-7f48c57/pyproject.toml0000664000175000017500000000564115240077162021014 0ustar epsilonepsilon[build-system] requires = [ "setuptools>=77.0.0", "wheel", "cffi>=1.12.0; platform_python_implementation != 'PyPy'", ] build-backend = "build_backend" backend-path = ["."] [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.10" license = "Apache-2.0" readme = "README.rst" classifiers = [ "Development Status :: 2 - Pre-Alpha", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "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", "pytest-cov", "coverage>=7.15.0", "coveralls>=4.1.0", "setuptools>=77.0.0", ] docs = [ "sphinx", "sphinx_rtd_theme", ] [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", "_ffi*.so"] [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 exclude = ["test/scanner_files/"] [tool.check-manifest] ignore = [ ".coveragerc", "doc/**", "example/**", "pywayland/protocol/**", ] [tool.mypy] packages = ["pywayland", "example"] warn_unused_configs = true warn_redundant_casts = true warn_unused_ignores = true strict_equality = true check_untyped_defs = true disallow_subclassing_any = true disallow_untyped_decorators = true disallow_any_generics = true no_implicit_optional = true disallow_untyped_calls = true disallow_incomplete_defs = true disallow_untyped_defs = true show_error_codes = true warn_incomplete_stub = true warn_return_any = true warn_unreachable = true [[tool.mypy.overrides]] module = "pywayland.protocol.*" disallow_any_generics = false warn_return_any = false [[tool.mypy.overrides]] module = ["cffi"] ignore_missing_imports = true flacjacket-pywayland-7f48c57/example/0000775000175000017500000000000015240077162017525 5ustar epsilonepsilonflacjacket-pywayland-7f48c57/example/color_manager.py0000664000175000017500000000707015240077162022713 0ustar epsilonepsilon#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2025 Lubosz Sarnecki # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import enum from typing import TYPE_CHECKING from pywayland.client import Display from pywayland.protocol.color_management_v1 import WpColorManagerV1 if TYPE_CHECKING: from pywayland.protocol.color_management_v1 import WpColorManagerV1Proxy from pywayland.protocol.wayland import WlRegistryProxy class App: def __init__(self) -> None: self.color_manager: WpColorManagerV1Proxy | None = None self.render_intents: list[enum.IntEnum] = [] self.features: list[enum.IntEnum] = [] self.transfer_functions: list[enum.IntEnum] = [] self.primaries: list[enum.IntEnum] = [] def color_manager_supported_intent_cb( color_manager: WpColorManagerV1Proxy, render_intent: int ) -> None: app: App = color_manager.user_data app.render_intents.append(WpColorManagerV1.render_intent(render_intent)) def color_manager_supported_feature_cb( color_manager: WpColorManagerV1Proxy, feature: int ) -> None: app: App = color_manager.user_data app.features.append(WpColorManagerV1.feature(feature)) def color_manager_supported_tf_named_cb( color_manager: WpColorManagerV1Proxy, transfer_function: int ) -> None: app: App = color_manager.user_data app.transfer_functions.append(WpColorManagerV1.transfer_function(transfer_function)) def color_manager_supported_primaries_named_cb( color_manager: WpColorManagerV1Proxy, primaries: int ) -> None: app: App = color_manager.user_data app.primaries.append(WpColorManagerV1.primaries(primaries)) def color_manager_done_cb(color_manager: WpColorManagerV1Proxy) -> None: app: App = color_manager.user_data print(f"Render intents ({len(app.render_intents)})") for render_intent in app.render_intents: print(f"\t{render_intent.name}") print(f"Features ({len(app.features)})") for feature in app.features: print(f"\t{feature.name}") print(f"Transfer Functions ({len(app.transfer_functions)})") for transfer_function in app.transfer_functions: print(f"\t{transfer_function.name}") print(f"Primaries ({len(app.primaries)})") for primaries in app.primaries: print(f"\t{primaries.name}") def registry_global_cb( registry: WlRegistryProxy, name: int, interface: str, version: int ) -> None: app: App = registry.user_data if interface == "wp_color_manager_v1": app.color_manager = registry.bind(name, WpColorManagerV1, version) app.color_manager.dispatcher["supported_intent"] = ( color_manager_supported_intent_cb ) app.color_manager.dispatcher["supported_feature"] = ( color_manager_supported_feature_cb ) app.color_manager.dispatcher["supported_tf_named"] = ( color_manager_supported_tf_named_cb ) app.color_manager.dispatcher["supported_primaries_named"] = ( color_manager_supported_primaries_named_cb ) app.color_manager.dispatcher["done"] = color_manager_done_cb app.color_manager.user_data = app def main() -> None: app = App() display = Display() display.connect() registry = display.get_registry() registry.dispatcher["global"] = registry_global_cb registry.user_data = app display.dispatch(block=True) display.roundtrip() if app.color_manager is None: print("Compositor does not provide wp_color_manager_v1") display.disconnect() if __name__ == "__main__": main() flacjacket-pywayland-7f48c57/example/wlinfo.py0000664000175000017500000001463315240077162021404 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 dataclasses import dataclass, field from functools import partial from itertools import chain from operator import itemgetter from typing import TYPE_CHECKING from pywayland.client import Display from pywayland.protocol.wayland import WlOutput, WlSeat, WlShm if TYPE_CHECKING: from collections.abc import Iterator from pywayland.protocol.wayland import ( WlOutputProxy, WlRegistryProxy, WlSeatProxy, WlShmProxy, ) FieldList = list[tuple[str, int, int] | str] @dataclass class Info: common: FieldList = field(default_factory=list) seat: FieldList = field(default_factory=list) output: FieldList = field(default_factory=list) shm: FieldList = 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(field_list: FieldList) -> Iterator[str]: for item in field_list: if isinstance(item, tuple): yield stringify_interface(*item) else: yield item 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( field_list: FieldList, *, interface: str, version: int, name: int ) -> None: # Not building the complete output string here because # we need the length of the 1st element, see pad in Info.__str__() field_list.append((f"interface: {interface}", version, name)) def add_seat_info( info: Info, registry: WlRegistryProxy, id_num: int, interface: str, version: int ) -> None: def handle_capabilities(_: object, capabilities: int) -> None: caps = [ cap.name for cap in WlSeat.capability if capabilities & cap.value and isinstance(cap.name, str) ] if caps: caps = sorted(caps) append(f"capabilities: {' '.join(caps)}") def handle_name(_: object, name: str) -> None: append(f"name: {name}") def append(s: str) -> None: return info.seat.append(f" {s}") _add_interface_info(info.seat, interface=interface, version=version, name=id_num) seat: WlSeatProxy = registry.bind(id_num, WlSeat, version) seat.dispatcher["name"] = handle_name seat.dispatcher["capabilities"] = handle_capabilities def add_output_info( info: Info, registry: WlRegistryProxy, id_num: int, interface: str, version: int ) -> None: def handle_geometry( _: object, 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(_: object, scale: int) -> None: append(f"scale: {scale}") def handle_name(_: object, name: str) -> None: append(f"name: {name}") def handle_description(_: object, description: str) -> None: append(f"description: {description}") def handle_mode( _: object, flags: int, width: int, height: int, refresh: int ) -> None: def append_sub(s: str) -> None: return info.output.append(f" {s}") flag_info = next(m.name for m in WlOutput.mode if flags & m.value) append("mode:") append_sub( f"width: {width} px, height: {height} px, refresh: {refresh / 1000} Hz" ) append_sub(f"flags: {flag_info}") def append(s: str) -> None: return info.output.append(f" {s}") _add_interface_info(info.output, interface=interface, version=version, name=id_num) output: WlOutputProxy = 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: WlRegistryProxy, id_num: int, interface: str, version: int ) -> None: def handle_format(_: object, 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}") def append(s: str) -> None: return info.shm.append(f" {s}") _add_interface_info(info.shm, interface=interface, version=version, name=id_num) append("formats:") shm: WlShmProxy = registry.bind(id_num, WlShm, version) shm.dispatcher["format"] = handle_format def handle_registry_global( info: Info, registry: WlRegistryProxy, 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() -> None: 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() flacjacket-pywayland-7f48c57/example/surface.py0000664000175000017500000001607315240077162021536 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 mmap import os import sys from typing import TYPE_CHECKING from pywayland.client import Display from pywayland.protocol.wayland import WlCompositor, WlShell, WlShm from pywayland.protocol.xdg_shell import XdgWmBase from pywayland.utils import AnonymousFile if TYPE_CHECKING: from pywayland.protocol.wayland import ( WlBufferProxy, WlCallbackProxy, WlCompositorProxy, WlRegistryProxy, WlShellProxy, WlShellSurfaceProxy, WlShmPoolProxy, WlShmProxy, WlSurfaceProxy, ) from pywayland.protocol.xdg_shell import ( XdgSurfaceProxy, XdgToplevelProxy, XdgWmBaseProxy, ) 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) WIDTH = 480 HEIGHT = 256 MARGIN = 10 class Window: def __init__(self) -> None: self.buffer: WlBufferProxy | None = None self.compositor: WlCompositorProxy | None = None self.shell: WlShellProxy | None = None self.shm: WlShmProxy | None = None self.surface: WlSurfaceProxy | None = None self.wm_base: XdgWmBaseProxy | None = None self.shm_data: mmap.mmap self.line_pos = MARGIN self.line_speed = +1 def shell_surface_ping_handler(shell_surface: WlShellSurfaceProxy, serial: int) -> None: shell_surface.pong(serial) print("pinged/ponged") def shm_format_handler(shm: WlShmProxy, format_: int) -> None: format_enum = WlShm.format(format_) print(f"Possible shmem format: {format_enum.name}") def wm_base_ping_handler(xdg_wm_base: XdgWmBaseProxy, serial: int) -> None: xdg_wm_base.pong(serial) def registry_global_handler( registry: WlRegistryProxy, id_: int, interface: str, version: int ) -> None: window: 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 elif interface == "xdg_wm_base": print("got xdg_wm_base") window.wm_base = registry.bind(id_, XdgWmBase, version) window.wm_base.dispatcher["ping"] = wm_base_ping_handler def registry_global_remover(registry: WlRegistryProxy, id_: int) -> None: print(f"got a registry losing event for {id_}") def create_buffer(window: Window) -> WlBufferProxy: assert window.shm is not None 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: WlShmPoolProxy = window.shm.create_pool(fd, size) buff: WlBufferProxy = pool.create_buffer( 0, WIDTH, HEIGHT, stride, WlShm.format.argb8888.value ) pool.destroy() return buff def create_window(window: Window) -> None: assert window.surface is not None window.buffer = create_buffer(window) window.surface.attach(window.buffer, 0, 0) window.surface.commit() def redraw(callback: WlCallbackProxy, time: int, destroy_callback: bool = True) -> None: window: Window = callback.user_data if destroy_callback: callback._destroy() paint(window) assert window.surface is not None 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: Window) -> None: 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 xdg_surface_configure_handler(xdg_surface: XdgSurfaceProxy, serial: int) -> None: xdg_surface.ack_configure(serial) def xdg_toplevel_configure_handler( toplevel: XdgSurfaceProxy, width: int, height: int, states: list[object] ) -> None: print(f"Configure with {width}x{height}") def main() -> None: window = Window() display = Display() display.connect() print("connected to display") registry: WlRegistryProxy = 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 and window.wm_base is None: raise RuntimeError("no shell found") elif window.shm is None: raise RuntimeError("no shm found") window.surface = window.compositor.create_surface() if window.wm_base: xdg_surface: XdgSurfaceProxy xdg_surface = window.wm_base.get_xdg_surface(window.surface) xdg_surface.dispatcher["configure"] = xdg_surface_configure_handler toplevel: XdgToplevelProxy = xdg_surface.get_toplevel() toplevel.set_app_id("pywayland_surface_example") toplevel.set_title("pywayland Surface Example") toplevel.dispatcher["configure"] = xdg_toplevel_configure_handler toplevel.user_data = window elif window.shell: shell_surface: WlShellSurfaceProxy shell_surface = window.shell.get_shell_surface(window.surface) shell_surface.set_toplevel() shell_surface.dispatcher["ping"] = shell_surface_ping_handler # trigger surface configure events window.surface.commit() display.roundtrip() frame_callback: WlCallbackProxy = window.surface.frame() frame_callback.dispatcher["done"] = redraw frame_callback.user_data = window create_window(window) redraw(frame_callback, 0, destroy_callback=False) display.dispatch(block=True) import time time.sleep(1) display.disconnect() if __name__ == "__main__": main() flacjacket-pywayland-7f48c57/example/simple_touch.py0000664000175000017500000001441315240077162022575 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 mmap import os import sys import tempfile from typing import TYPE_CHECKING from pywayland.client import Display from pywayland.protocol.wayland import WlCompositor, WlSeat, WlShell, WlShm if TYPE_CHECKING: from typing import TypedDict from pywayland.protocol.wayland import ( WlBufferProxy, WlCompositorProxy, WlRegistryProxy, WlSeatProxy, WlShellProxy, WlShellSurfaceProxy, WlShmProxy, WlSurface, WlSurfaceProxy, WlTouch, WlTouchProxy, ) class TouchDictT(TypedDict, total=False): display: Display registry: WlRegistryProxy compositor: WlCompositorProxy shell: WlShellProxy shell_surface: WlShellSurfaceProxy shm: WlShmProxy has_argb: bool surface: WlSurfaceProxy buffer: WlBufferProxy width: int height: int data: mmap.mmap wl_touch: WlTouchProxy seat: WlSeatProxy class SeatDictT(TypedDict, total=False): touch: TouchDictT wl_touch: WlTouchProxy | None seat: WlSeatProxy 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) def create_shm_buffer(touch: TouchDictT, width: int, height: int) -> None: 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: WlTouch, serial: int, time: int, surface: WlSurface, id: int, x: float, y: float, ) -> int: return 0 def handle_touch_up(wl_touch: WlTouch, serial: int, time: int, id: int) -> int: return 0 def handle_touch_motion( wl_touch: WlTouch, time: int, id: int, x: float, y: float ) -> int: print(wl_touch, time, id, x, y) return 0 def handle_seat_capabilities(wl_seat: WlSeatProxy, capabilities: int) -> int: print("capabilities") seat: SeatDictT = wl_seat.user_data touch = seat["touch"] if (capabilities & WlSeat.capability.touch.value) and seat["wl_touch"] is None: wl_touch = wl_seat.get_touch() wl_touch.user_data = touch wl_touch.dispatcher["up"] = handle_touch_up wl_touch.dispatcher["down"] = handle_touch_down wl_touch.dispatcher["motion"] = handle_touch_motion seat["wl_touch"] = wl_touch 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: WlShmProxy, fmt: int) -> int: print("format") touch: TouchDictT = wl_shm.user_data if fmt == WlShm.format.argb8888.value: touch["has_argb"] = True return 1 def handle_shell_surface_ping( wl_shell_surface: WlShellSurfaceProxy, serial: int ) -> int: print("ping") wl_shell_surface.pong(serial) return 1 def handle_registry_global( wl_registry: WlRegistryProxy, id_num: int, iface_name: str, version: int ) -> int: print("global", id_num, iface_name) touch: TouchDictT = wl_registry.user_data if iface_name == "wl_compositor": touch["compositor"] = wl_registry.bind(id_num, WlCompositor, version) elif iface_name == "wl_seat": seat: SeatDictT = {} seat["touch"] = touch seat["wl_touch"] = None seat["seat"] = wl_registry.bind(id_num, WlSeat, version) seat["seat"].dispatcher["capabilities"] = handle_seat_capabilities seat["seat"].user_data = seat elif iface_name == "wl_shell": touch["shell"] = wl_registry.bind(id_num, WlShell, version) elif iface_name == "wl_shm": touch["has_argb"] = False touch["shm"] = wl_registry.bind(id_num, WlShm, version) touch["shm"].user_data = touch touch["shm"].dispatcher["format"] = handle_shm_format return 1 def touch_create(width: int, height: int) -> TouchDictT: touch: TouchDictT = {} # 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(block=True) touch["display"].roundtrip() if not touch["has_argb"]: print("WL_SHM_FORMAT_ARGB32 not available", file=sys.stderr) return touch touch["width"] = width touch["height"] = height touch["surface"] = touch["compositor"].create_surface() if "shell" in touch: touch["shell_surface"] = touch["shell"].get_shell_surface(touch["surface"]) if "shell_surface" in touch: print("shell") touch["shell_surface"].dispatcher["ping"] = handle_shell_surface_ping touch["shell_surface"].set_toplevel() touch["shell_surface"].set_title("simple-touch") touch["surface"].user_data = touch create_shm_buffer(touch, width, height) touch["surface"].attach(touch["buffer"], 0, 0) touch["surface"].damage(0, 0, width, height) touch["surface"].commit() return touch def main() -> None: touch = touch_create(600, 500) touch["display"].dispatch(block=True) touch["display"].disconnect() if __name__ == "__main__": main() flacjacket-pywayland-7f48c57/.coveragerc0000664000175000017500000000041615240077162020214 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: flacjacket-pywayland-7f48c57/pywayland/0000775000175000017500000000000015240077162020102 5ustar epsilonepsilonflacjacket-pywayland-7f48c57/pywayland/_ffi/0000775000175000017500000000000015240077162021005 5ustar epsilonepsilonflacjacket-pywayland-7f48c57/pywayland/_ffi/__init__.pyi0000664000175000017500000000000015240077162023255 0ustar epsilonepsilonflacjacket-pywayland-7f48c57/pywayland/_ffi/lib.pyi0000664000175000017500000001635715240077162022312 0ustar epsilonepsilonfrom .ffi import ( CData, CharCData, DispatcherFunc, EventLoopFdFunc, EventLoopIdleFunc, EventLoopSignalFunc, EventLoopTimerFunc, GlobalBindFunc, NotifyFunc, ResourceDestroyFunc, WlArgument, WlClient, WlDisplay, WlEventLoop, WlEventSource, WlGlobal, WlInterface, WlList, WlListener, WlProxy, WlQueue, WlResource, WlSignal, ) ############################################################################### # wayland-version.h ############################################################################### WAYLAND_VERSION_MAJOR: int WAYLAND_VERSION_MINOR: int WAYLAND_VERSION_MICRO: int ############################################################################### # wayland-util.h ############################################################################### # wl_fixed_t methods def wl_fixed_to_double(f: int) -> float: ... def wl_fixed_from_double(d: float) -> int: ... def wl_fixed_from_int(i: int) -> int: ... # wl_list methods def wl_list_init(list: WlList) -> None: ... def wl_list_insert(list: WlList, elm: WlList) -> None: ... def wl_list_remove(list: WlList) -> None: ... ############################################################################### # wayland-client-core.h ############################################################################### # wl_event_queue methods def wl_event_queue_destroy(queue: WlQueue) -> None: ... # wl_proxy methods def wl_proxy_marshal_array( proxy: WlProxy, opcode: int, args_ptr: WlArgument ) -> None: ... def wl_proxy_marshal_array_constructor( proxy: WlProxy, opcode: int, args_ptr: WlArgument, interface_ptr: WlInterface ) -> WlProxy: ... def wl_proxy_destroy(proxy: WlProxy) -> None: ... def wl_proxy_add_dispatcher( proxy: WlProxy, dispatcher_func: DispatcherFunc, dispatcher_data: CData, data: CData ) -> int: ... # wl_display methods def wl_display_connect(name: bytes | CharCData) -> WlDisplay: ... def wl_display_connect_to_fd(fd: int) -> WlDisplay: ... def wl_display_disconnect(display: WlDisplay) -> None: ... def wl_display_get_fd(display: WlDisplay) -> int: ... def wl_display_dispatch(display: WlDisplay) -> int: ... def wl_display_dispatch_queue(display: WlDisplay, queue: WlQueue) -> int: ... def wl_display_dispatch_queue_pending(display: WlDisplay, queue: WlQueue) -> int: ... def wl_display_dispatch_pending(display: WlDisplay) -> int: ... def wl_display_get_error(display: WlDisplay) -> int: ... def wl_display_flush(display: WlDisplay) -> int: ... def wl_display_roundtrip_queue(display: WlDisplay, queue: WlQueue) -> int: ... def wl_display_roundtrip(display: WlDisplay) -> int: ... def wl_display_create_queue(display: WlDisplay) -> WlQueue: ... def wl_display_prepare_read_queue(display: WlDisplay, queue: WlQueue) -> int: ... def wl_display_prepare_read(display: WlDisplay) -> int: ... def wl_display_read_events(display: WlDisplay) -> int: ... ############################################################################### # wayland-server-core.h ############################################################################### # event kind enum WL_EVENT_READABLE: int WL_EVENT_WRITABLE: int WL_EVENT_HANGUP: int WL_EVENT_ERROR: int # wl_signal methods def wl_signal_init(ptr: WlSignal) -> None: ... def wl_signal_add(ptr: WlSignal, listener: WlListener) -> None: ... def wl_signal_emit(ptr: WlSignal, data: CData) -> None: ... # wl_event_loop methods def wl_event_loop_create() -> WlEventLoop: ... def wl_event_loop_destroy(event_loop: WlEventLoop) -> None: ... def wl_event_loop_add_fd( ptr: WlEventLoop, fd: int, mask: int, event_loop_fd_func: EventLoopFdFunc, data: CData, ) -> WlEventSource: ... def wl_event_loop_add_timer( ptr: WlEventLoop, event_loop_timer_func: EventLoopTimerFunc, data: CData ) -> WlEventSource: ... def wl_event_loop_add_signal( ptr: WlEventLoop, signal: int, event_loop_signal_func: EventLoopSignalFunc, data: CData, ) -> WlEventSource: ... def wl_event_loop_dispatch(ptr: WlEventLoop, timeout: int) -> int: ... def wl_event_loop_dispatch_idle(ptr: WlEventLoop) -> None: ... def wl_event_loop_add_idle( ptr: WlEventLoop, event_loop_idle_func: EventLoopIdleFunc, data: CData ) -> WlEventSource: ... def wl_event_loop_add_destroy_listener( ptr: WlEventLoop, listener: WlListener ) -> None: ... # wl_event_source methods def wl_event_source_timer_update(ptr: WlEventSource, ms_delay: int) -> int: ... def wl_event_source_remove(ptr: WlEventSource) -> int: ... def wl_event_source_check(ptr: WlEventSource) -> None: ... # wl_display methods def wl_display_create() -> WlDisplay: ... def wl_display_destroy(display: WlDisplay) -> None: ... def wl_display_get_event_loop(display: WlDisplay) -> WlEventLoop: ... def wl_display_add_socket(display: WlDisplay, name: bytes | CharCData) -> int: ... def wl_display_add_socket_auto(display: WlDisplay) -> CharCData: ... def wl_display_terminate(display: WlDisplay) -> None: ... def wl_display_run(display: WlDisplay) -> None: ... def wl_display_flush_clients(display: WlDisplay) -> None: ... def wl_display_destroy_clients(display: WlDisplay) -> None: ... def wl_display_get_serial(display: WlDisplay) -> int: ... def wl_display_next_serial(display: WlDisplay) -> int: ... def wl_display_init_shm(display: WlDisplay) -> int: ... def wl_display_add_shm_format(display: WlDisplay, format: int) -> CData: ... # wl_global methods def wl_global_create( display: WlDisplay, interface: WlInterface, version: int, data: CData, global_bind_func: GlobalBindFunc, ) -> WlGlobal: ... def wl_global_destroy(data: WlGlobal) -> None: ... # wl_client methods def wl_client_create(display: WlDisplay, fd: int) -> WlClient: ... def wl_client_destroy(client: WlClient) -> None: ... def wl_client_flush(client: WlClient) -> None: ... def wl_client_get_credentials( client: WlClient, pid: CData, uid: CData, gid: CData ) -> None: ... def wl_client_add_destroy_listener(client: WlClient, listener: WlListener) -> None: ... def wl_client_get_object(client: WlClient, id: int) -> WlResource: ... # wl_resource methods def wl_resource_post_event_array( resource: WlResource, opcode: int, args: WlArgument ) -> None: ... def wl_resource_post_error( resource: WlResource, code: int, msg: bytes | CharCData ) -> None: ... def wl_resource_create( client: WlClient, interface: WlInterface, version: int, id: int ) -> WlResource: ... def wl_resource_set_dispatcher( resource: WlResource, dispatcher_func: DispatcherFunc, implementation: CData, data: CData, resource_destroy_func: ResourceDestroyFunc, ) -> None: ... def wl_resource_destroy(resource: WlResource) -> None: ... def wl_resource_get_id(resource: WlResource) -> int: ... def wl_resource_get_client(resource: WlResource) -> WlClient: ... def wl_resource_get_user_data(resource: WlResource) -> CData: ... def wl_resource_add_destroy_listener( resource: WlResource, listener: WlListener ) -> None: ... # anonymous file methods def os_create_anonymous_file(size: int) -> int: ... # cffi callback functions dispatcher_func: DispatcherFunc resource_destroy_func: ResourceDestroyFunc event_loop_fd_func: EventLoopFdFunc event_loop_signal_func: EventLoopSignalFunc event_loop_timer_func: EventLoopTimerFunc event_loop_idle_func: EventLoopIdleFunc global_bind_func: GlobalBindFunc notify_func: NotifyFunc flacjacket-pywayland-7f48c57/pywayland/_ffi/ffi.pyi0000664000175000017500000000502215240077162022273 0ustar epsilonepsilonfrom collections.abc import Callable from typing import Any, ParamSpec, Self, TypeVar, overload class CData: def __getitem__(self, idx: int) -> Self: ... def __setitem__(self, idx: int, elem: Self) -> None: ... # built-in cdata types class CharCData(CData): ... # extern functions class DispatcherFunc: ... class ResourceDestroyFunc: ... class EventLoopFdFunc: ... class EventLoopSignalFunc: ... class EventLoopTimerFunc: ... class EventLoopIdleFunc: ... class GlobalBindFunc: ... class NotifyFunc: ... class WlArgument(CData): i: int u: int f: int s: CharCData o: WlObject n: int a: WlArray h: int class WlArray(CData): size: int alloc: int data: CData class WlClient(CData): pass class WlDisplay(CData): pass class WlEventLoop(CData): pass class WlEventSource(CData): pass class WlGlobal(CData): pass class WlInterface(CData): name: CharCData version: int method_count: int methods: WlMessage event_count: int events: WlMessage class WlList(CData): @property def prev(self) -> WlList: ... @property def next(self) -> WlList: ... class WlListener(CData): link: WlList notify: NotifyFunc class WlListenerContainer(CData): handle: CData destroy_listener: WlListener class WlMessage(CData): name: CharCData signature: CharCData types: WlInterface class WlObject(CData): pass class WlProxy(CData): pass class WlQueue(CData): pass class WlResource(CData): pass class WlSignal(CData): listener_list: WlList # special types P = ParamSpec("P") R = TypeVar("R") _CDataT = TypeVar("_CDataT", bound=CData) _CDataO = TypeVar("_CDataO", bound=CData) # Any type of CData NULL: Any def new(cdecl: str, init: Any = None) -> _CDataT: ... # type: ignore [type-var] @overload def gc( cdata: _CDataT, destructor: Callable[[_CDataT], Any], size: int = 0 ) -> _CDataT: ... @overload def gc(cdata: _CDataT, destructor: None, size: int = 0) -> None: ... def buffer(cdata: _CDataT, size: int = -1) -> bytearray: ... def string(cdata: CharCData, maxlen: int = -1) -> bytes: ... def release(x: _CDataT) -> None: ... def def_extern() -> Callable[[Callable[P, R]], Callable[P, R]]: ... def new_handle(x: Any) -> _CDataT: ... # type: ignore [type-var] def from_handle(x: _CDataT) -> Any: ... def cast(cdecl: str, source: _CDataT) -> _CDataO: ... # type: ignore [type-var] def addressof(cdata: _CDataT, *fields_or_indexes: str | int) -> _CDataT: ... def offsetof(cdecl: str, *fields_or_indexes: str | int) -> int: ... flacjacket-pywayland-7f48c57/pywayland/client/0000775000175000017500000000000015240077162021360 5ustar epsilonepsilonflacjacket-pywayland-7f48c57/pywayland/client/display.py0000664000175000017500000002614415240077162023406 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.protocol.wayland import WlDisplayProxy from pywayland.utils import ensure_valid if TYPE_CHECKING: from types import TracebackType from typing import Any, Literal from pywayland.client.eventqueue import EventQueue from pywayland.protocol_core import Proxy class Display(WlDisplayProxy): """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[EventQueue | Proxy[Any]] = WeakSet() self._name_or_fd = name_or_fd self._ptr: ffi.WlDisplay | None = None # type: ignore [assignment] def __enter__(self) -> Display: """Connect to the display in a context manager""" self.connect() return self def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, ) -> 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: name: ffi.CharCData | bytes # connect using string by name, or use default if self._name_or_fd is None: name = 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) flacjacket-pywayland-7f48c57/pywayland/client/__init__.py0000664000175000017500000000123115240077162023466 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: F401 from .eventqueue import EventQueue # noqa: F401 flacjacket-pywayland-7f48c57/pywayland/client/eventqueue.py0000664000175000017500000000536115240077162024125 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[ffi.WlQueue | None, Display] = WeakKeyDictionary() def _event_queue_destroy(display: Display, cdata: ffi.WlQueue) -> 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.WlQueue | 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 flacjacket-pywayland-7f48c57/pywayland/protocol_core/0000775000175000017500000000000015240077162022753 5ustar epsilonepsilonflacjacket-pywayland-7f48c57/pywayland/protocol_core/proxy.py0000664000175000017500000001124715240077162024513 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 typing import Any, Self from pywayland.client import Display as ClientDisplay from .interface import Interface T = TypeVar("T", bound=Interface) else: T = TypeVar("T") class Proxy(Generic[T]): """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`. :param ptr: The pointer to the Wayland proxy object :type ptr: ffi.WlProxy or None :param display: The display associated with this proxy :type display: :class:`~pywayland.client.Display` or Self or None """ interface: type[T] def __init__( self, ptr: ffi.WlProxy | None, display: ClientDisplay | Self | None = None ) -> None: self._ptr: ffi.WlProxy | None self._display: ClientDisplay | Self | None self.user_data: Any = 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" ) if hasattr(display, "_children"): 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.CData = 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 and 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: int, *args: Any) -> None: """Marshal the given arguments into the Wayland wire format""" assert self._ptr is not None # Create wl_argument array args_ptr = self.interface.requests[opcode].arguments_to_c(*args) # Write the event into the connection queue proxy: ffi.WlProxy = ffi.cast("struct wl_proxy *", self._ptr) lib.wl_proxy_marshal_array(proxy, opcode, args_ptr) def _marshal_constructor( self, opcode: int, interface: type[Interface], *args: Any ) -> Any: """Marshal the given arguments into the Wayland wire format for a constructor""" assert self._ptr is not None # Create 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.WlProxy = 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) flacjacket-pywayland-7f48c57/pywayland/protocol_core/globals.py0000664000175000017500000000767515240077162024767 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, Generic, TypeVar from weakref import WeakKeyDictionary from pywayland import ffi, lib if TYPE_CHECKING: from pywayland.server import Display as ServerDisplay from .interface import Interface T = TypeVar("T", bound=Interface) else: T = TypeVar("T") weakkeydict: WeakKeyDictionary[ffi.WlGlobal, ServerDisplay] = 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: ffi.WlClient, data: ffi.CData, version: int, id: int ) -> 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: ServerDisplay, cdata: ffi.CData) -> None: if display._ptr is not None: # TODO: figure out how this can get run... # lib.wl_global_destroy(cdata) pass class Global(Generic[T]): """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` """ interface: type[T] def __init__(self, display: ServerDisplay, version: int | None = None): if display._ptr is None or display._ptr == ffi.NULL: raise ValueError("Display has been destroyed or couldn't initialize") self._ptr: ffi.WlGlobal | None 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.CData = 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: ServerDisplay | None = display # this c data should keep the display alive weakkeydict[self._ptr] = display @property def bind_func(self) -> type[T] | None: return self._callback_info["bind_func"] @bind_func.setter def bind_func(self, value: type[T] | None) -> None: self._callback_info["bind_func"] = value def destroy(self) -> None: """Destroy the global object""" if self._ptr is not None and self._display 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 flacjacket-pywayland-7f48c57/pywayland/protocol_core/__init__.py0000664000175000017500000000220415240077162025062 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 .message import Message # noqa: F401 from .proxy import Proxy # noqa: F401 from .resource import Resource # noqa: F401 flacjacket-pywayland-7f48c57/pywayland/protocol_core/message.py0000664000175000017500000002347315240077162024762 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 TYPE_CHECKING from weakref import WeakKeyDictionary from pywayland import ffi, lib from .argument import Argument, ArgumentType if TYPE_CHECKING: from collections.abc import Callable, Iterable from typing import Any, ParamSpec, TypeVar P = ParamSpec("P") R = TypeVar("R") from pywayland.protocol_core import Proxy weakkeydict: WeakKeyDictionary[ffi.WlArgument, tuple[ffi.CData, ...]] = ( WeakKeyDictionary() ) class Message: """Wrapper class for `wl_message` structs Base class that corresponds 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: Callable :param arguments: The arguments of the message :type arguments: tuple of :class:`~pywayland.protocol_core.Argument` :param version: The version of the message, or None :type version: int or None """ def __init__( self, func: Callable[P, R], arguments: tuple[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: ffi.WlMessage ) -> tuple[ffi.CharCData, ffi.CharCData, ffi.WlInterface]: """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}" name: ffi.CharCData = ffi.new("char[]", self.name.encode()) wl_message_struct.name = name cdata_signature: ffi.CharCData = ffi.new("char[]", signature.encode()) wl_message_struct.signature = cdata_signature types: ffi.WlInterface = ffi.new( "struct wl_interface* []", len(list(self._marshaled_arguments)) ) wl_message_struct.types = types 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: ffi.WlArgument ) -> list[int | float | str | bytearray | Proxy[Any] | ffi.CData | None]: """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: list[int | float | str | bytearray | Proxy[Any] | ffi.CData | None] = [] proxy_ptr: ffi.WlProxy 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 if iface is None: continue 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 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: Any) -> ffi.WlArgument: """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.WlArgument = ffi.new("union wl_argument []", nargs) arg_iter = iter(args) refs: list[ffi.CData] = [] 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: new_string: ffi.CharCData if arg is None: if not argument.nullable: raise Exception new_string = ffi.NULL else: new_string = ffi.new("char []", arg.encode()) refs.append(new_string) args_ptr[i].s = new_string elif argument.argument_type == ArgumentType.Object: new_obj: ffi.WlObject if arg is None: if not argument.nullable: raise Exception new_obj = ffi.NULL else: new_obj = ffi.cast("struct wl_object *", arg._ptr) refs.append(new_obj) args_ptr[i].o = new_obj 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_array: ffi.WlArray = ffi.new("struct wl_array *") new_data: ffi.CData = ffi.new("void []", len(arg)) new_array.alloc = new_array.size = len(arg) ffi.buffer(new_data)[:] = arg refs.append(new_array) refs.append(new_data) if len(refs) > 0: weakkeydict[args_ptr] = tuple(refs) return args_ptr flacjacket-pywayland-7f48c57/pywayland/protocol_core/resource.py0000664000175000017500000000732115240077162025157 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 typing import Any from pywayland.server.listener import Listener 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: Client | ffi.WlClient, 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.WlResource | None = lib.wl_resource_create( client_ptr, self.interface._ptr, version, id ) self._handle: ffi.CData = 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 get_id(self) -> int: """Get the id of the Resource""" assert self._ptr is not None return lib.wl_resource_get_id(self._ptr) @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_resource_add_destroy_listener(self._ptr, listener._ptr) @ensure_valid def _post_event(self, opcode: int, *args: Any) -> None: assert self._ptr is not None # Create wl_argument array args_ptr = self.interface.events[opcode].arguments_to_c(*args) # Write the event array to this object resource: ffi.WlResource = ffi.cast("struct wl_resource *", self._ptr) lib.wl_resource_post_event_array(resource, opcode, args_ptr) @ensure_valid def _post_error(self, code: int, msg: str = "") -> None: assert self._ptr is not None lib.wl_resource_post_error(self._ptr, code, msg.encode()) flacjacket-pywayland-7f48c57/pywayland/protocol_core/argument.py0000664000175000017500000000353515240077162025155 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 @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 flacjacket-pywayland-7f48c57/pywayland/protocol_core/interface.py0000664000175000017500000001152715240077162025273 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 WeakKeyDictionary, WeakValueDictionary from pywayland import ffi from .message import Message if TYPE_CHECKING: from collections.abc import Callable from typing import Any, ParamSpec, TypeVar from .argument import Argument from .globals import Global from .proxy import Proxy from .resource import Resource P = ParamSpec("P") R = TypeVar("R") weakkeydict: WeakKeyDictionary[ffi.WlInterface, tuple[object, ...]] = ( WeakKeyDictionary() ) class InterfaceMeta(type): """Metaclass for Interfaces Initializes empty lists for events and requests for the given class. """ def __init__(self, name: str, bases: tuple[Any], dct: dict[Any, Any]): self.events: list[Message] = [] self.requests: list[Message] = [] # Initialize the interface cdata self._ptr: ffi.WlInterface = 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.WlInterface name: str version: int proxy_class: type[Proxy[Any]] resource_class: type[Resource[Any]] global_class: type[Global[Any]] registry: WeakValueDictionary[ffi.WlProxy, Proxy[Any]] @classmethod def event( cls, *arguments: Argument, version: int | None = None ) -> Callable[[Callable[P, R]], Callable[P, R]]: """Decorator for interface events Adds the decorated method to the list of events of the interface (server-side method). :param arguments: Arguments for the event :type arguments: :class:`~pywayland.protocol_core.Argument` :param version: Version of the event (optional) :type version: int or None """ def wrapper(func: Callable[P, R]) -> Callable[P, R]: cls.events.append(Message(func, arguments, version)) return func return wrapper @classmethod def request( cls, *arguments: Argument, version: int | None = None ) -> Callable[[Callable[P, R]], Callable[P, R]]: """Decorator for interface requests Adds the decorated method to the list of requests of the interface (client-side method). :param arguments: Arguments for the request :type arguments: :class:`~pywayland.protocol_core.Argument` :param version: Version of the request (optional) :type version: int or None """ def wrapper(func: Callable[P, R]) -> Callable[P, R]: cls.requests.append(Message(func, arguments, version)) return func return wrapper @classmethod def _gen_c(cls) -> None: """Creates the wl_interface C struct Generates the CFFI cdata for the wl_interface struct given by the interface. """ cls.registry = WeakValueDictionary() name: ffi.CharCData = ffi.new("char[]", cls.name.encode()) cls._ptr.name = name cls._ptr.version = cls.version keep_alive: list[tuple[ffi.CharCData, ffi.CharCData, ffi.WlInterface]] = [] # Determine the number of methods to assign and assign them cls._ptr.method_count = len(cls.requests) methods_ptr: ffi.WlMessage = ffi.new("struct wl_message[]", len(cls.requests)) cls._ptr.methods = methods_ptr # Iterate over the methods for i, message in enumerate(cls.requests): keep_alive.append(message.build_message_struct(methods_ptr[i])) cls._ptr.event_count = len(cls.events) events_ptr: ffi.WlMessage = ffi.new("struct wl_message[]", len(cls.events)) cls._ptr.events = events_ptr # Iterate over the methods for i, message in enumerate(cls.events): keep_alive.append(message.build_message_struct(events_ptr[i])) weakkeydict[cls._ptr] = (name, methods_ptr, events_ptr, *tuple(keep_alive)) flacjacket-pywayland-7f48c57/pywayland/dispatcher.py0000664000175000017500000000741115240077162022605 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 collections.abc import Callable from typing import TYPE_CHECKING from pywayland import ffi, lib if TYPE_CHECKING: from pywayland.protocol_core.message import Message CallbackT = Callable[..., int | None] # int (*wl_dispatcher_func_t)(const void *, void *, uint32_t, const struct wl_message *, union wl_argument *) @ffi.def_extern() def dispatcher_func( data: ffi.CData, target: ffi.CData, opcode: int, message: Message, c_args: ffi.WlArgument, ) -> int: # `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) ret: int | None 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: ffi.WlResource) -> None: # 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 flacjacket-pywayland-7f48c57/pywayland/scanner/0000775000175000017500000000000015240077162021533 5ustar epsilonepsilonflacjacket-pywayland-7f48c57/pywayland/scanner/request.py0000664000175000017500000001311115240077162023572 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 collections.abc import Iterable from dataclasses import dataclass 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[Interface]" yield f"{NO_IFACE_VERSION}: int" elif arg.type == ArgumentType.Object and arg.interface: # Method args for a known interface return the proxy class of the interface signature = f"{arg.name}: {arg.interface_class}Proxy" if arg.allow_null: signature += " | None" yield signature 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"{self.new_id.interface_class}Proxy" else: # If interface is unknown, the return type is a proxy of any type, # which is not a valid type hint. So we return "Any" instead. return "Any" return "None" flacjacket-pywayland-7f48c57/pywayland/scanner/description.py0000664000175000017500000000231315240077162024427 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) flacjacket-pywayland-7f48c57/pywayland/scanner/__init__.py0000664000175000017500000000115215240077162023643 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 flacjacket-pywayland-7f48c57/pywayland/scanner/event.py0000664000175000017500000000445615240077162023237 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 collections.abc import Iterator from dataclasses import dataclass from .argument import Argument 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" flacjacket-pywayland-7f48c57/pywayland/scanner/copyright.py0000664000175000017500000000337115240077162024121 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("#") flacjacket-pywayland-7f48c57/pywayland/scanner/method.py0000664000175000017500000001112015240077162023360 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 collections.abc import Iterable from dataclasses import dataclass from .argument import Argument, ArgumentType 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, all_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 all_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 = all_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 = all_imports[arg.interface] # all interfaces are now in the same file for each protocol if current_protocol == import_protocol: continue # all protocol files are now in the same directory import_path = f".{import_protocol}" imports.append((import_path, import_class)) if self.method_type == "request": # import the proxy class if the argument is a proxy if arg.type == ArgumentType.Object: imports.append((import_path, f"{import_class}Proxy")) # import the proxy class if the return type is a proxy if self.return_type == f"{import_class}Proxy": imports.append((import_path, f"{import_class}Proxy")) 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) -> 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('"""') flacjacket-pywayland-7f48c57/pywayland/scanner/argument.py0000664000175000017500000001421215240077162023727 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 = "object" elif self.type == ArgumentType.NewId: if self.interface: base_annotation = self.interface_class else: base_annotation = "type[Interface]" 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}") flacjacket-pywayland-7f48c57/pywayland/scanner/entry.py0000664000175000017500000000422615240077162023252 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}") flacjacket-pywayland-7f48c57/pywayland/scanner/printer.py0000664000175000017500000002021515240077162023570 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 collections.abc import Iterator, Mapping from re import Match from typing import BinaryIO 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, all_imports: Mapping[str, str], interface_name: 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 all_imports: The map to the protocol for resolving imports. :param interface_name: The name of the interface that is being generated, used for determining import resolution. """ self._level = 0 self._lines = [HEAD_MSG, ""] self._protocol_name = protocol self._all_imports = all_imports self.interface_name = interface_name self._re_doc = None if all_imports is not None: interface_names = "|".join(sorted(all_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]) -> 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]) -> 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._all_imports is not None and interface_name in self._all_imports: protocol_path = self._all_imports[interface_name] return f":class:`~{BASE_PATH}.{protocol_path}.{interface_class}`" return f"`{interface_class}`" def _doc_funcs_replace(self, match: Match[str]) -> 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._all_imports is not None and interface_name in self._all_imports: protocol_path = self._all_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") def clear(self) -> None: """Clear saved printer lines""" self._lines = [] flacjacket-pywayland-7f48c57/pywayland/scanner/element.py0000664000175000017500000000431315240077162023537 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 flacjacket-pywayland-7f48c57/pywayland/scanner/__main__.py0000664000175000017500000001172315240077162023631 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(input_dir: str | None = None) -> list[str]: if input_dir: protocols_dir = input_dir else: # 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 = [] # wayland protocols stages in incremental importance for stages in ("unstable", "staging", "stable"): # walk the wayland-protocol dir for dirpath, _, filenames in os.walk(os.path.join(protocols_dir, stages)): for filename in filenames: _, file_ext = os.path.splitext(filename) # only generate protocols for xml files if file_ext == ".xml": protocols.append(os.path.join(dirpath, filename)) return protocols 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", ) parser.add_argument( "--input-protocols", metavar="DIR", type=str, help="Custom directory to wayland-protocol xml files instead of pkg-config", ) args = parser.parse_args() if not os.path.exists(args.output_dir): os.makedirs(args.output_dir, 0o775) input_files = [] if args.with_protocols: protocols_files = get_wayland_protocols(args.input_protocols) input_files += protocols_files # add wayland.xml at last input_files += args.input protocols = [Protocol.parse_file(input_file) for input_file in input_files] logger.info(f"Parsed {len(protocols)} input xml files") # items are created in order of importance (1.stable, 2.staging, 3.unstable) # in case duplicates appear in the protocols all_imports = { interface.name: protocol.name for protocol in protocols for interface in protocol.interface } for protocol in protocols: # create a new copy so that we can overwrite created interface keys with # the interfaces in the current protocol, this is in case an interface name # is repeated multiple times fixing the issue of importing and redifining # the interface in the same protocol file # if a protocol other than the ones with repeated interfaces where to referece # the interface name, it would resolve to the interface with highest priority # (1.stable, 2.staging, 3.unstable) since the xml files does not have a hard # referece to the wanted interface protocol current_protocol_imports = all_imports.copy() current_protocol_imports.update( {interface.name: protocol.name for interface in protocol.interface} ) protocol.output(args.output_dir, current_protocol_imports) logger.info(f"Generated protocol: {protocol.name}") if __name__ == "__main__": logging.basicConfig(level=logging.INFO) main() flacjacket-pywayland-7f48c57/pywayland/scanner/interface.py0000664000175000017500000001235015240077162024046 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] def __lt__(self, other: Interface) -> bool: return self.name < other.name @classmethod def parse(cls, element: ET.Element) -> Interface: """Scanner for interface objects Required attributes: `name` and `version` Child elements: `description`, `request`, `event`, `enum` """ return cls( name=cls.parse_attribute(element, "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("_")) @property def proxy_class_name(self) -> str: return f"{self.class_name}Proxy" @property def resource_class_name(self) -> str: return f"{self.class_name}Resource" @property def global_class_name(self) -> str: return f"{self.class_name}Global" @property def needs_any_type(self) -> bool: return any(req.return_type == "Any" for req in self.request) @property def needs_argument_type(self) -> bool: return any(len(method.arg) > 0 for method in self.request + self.event) def get_imports(self, all_imports: dict[str, str]) -> set[tuple[str, str]]: """Return necessary imports for this interface""" return set( _import for method in itertools.chain(self.request, self.event) for _import in method.imports(self.name, all_imports) ) def output_interface(self, printer: Printer) -> None: """Generate the output only of the interface class""" 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) def output_requests(self, printer: Printer) -> None: """Generate the output only of the proxy class""" printer() printer() printer(f"class {self.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) def output_events(self, printer: Printer) -> None: """Generate the output only of the proxy class""" printer() printer() printer(f"class {self.resource_class_name}(Resource[{self.class_name}]):") with printer.indented(): printer(f"interface = {self.class_name}") for opcode, event in enumerate(self.event): printer() event.output(printer, opcode, self.class_name) def output_global(self, printer: Printer) -> None: """Generate the output only of the proxy class""" printer() printer() printer(f"class {self.global_class_name}(Global[{self.class_name}]):") with printer.indented(): printer(f"interface = {self.class_name}") def output_attributes(self, printer: Printer) -> None: """Generate the output only of the attributes of the interface""" printer() printer() printer(f"{self.class_name}._gen_c()") printer(f"{self.class_name}.proxy_class = {self.proxy_class_name}") printer(f"{self.class_name}.resource_class = {self.resource_class_name}") printer(f"{self.class_name}.global_class = {self.global_class_name}") flacjacket-pywayland-7f48c57/pywayland/scanner/enum.py0000664000175000017500000000376215240077162023061 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) flacjacket-pywayland-7f48c57/pywayland/scanner/protocol.py0000664000175000017500000001057715240077162023760 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, all_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 """ if not os.path.exists(output_dir): os.mkdir(output_dir) protocol_name = self.name.replace("-", "_") protocol_path = os.path.join(output_dir, f"{protocol_name}.py") printer = Printer(protocol_name, all_imports) if self.copyright: self.copyright.output(printer) else: printer(copyright_default) printer() printer("from __future__ import annotations") if any(iface.enum for iface in self.interface): printer() printer("import enum") if any(iface.needs_any_type for iface in self.interface): printer() printer("from typing import Any") pywayland_imports = ["Interface", "Global", "Proxy", "Resource"] if any(iface.needs_argument_type for iface in self.interface): pywayland_imports.extend(["Argument", "ArgumentType"]) printer() printer( f"from pywayland.protocol_core import {(', ').join(sorted(pywayland_imports))}" ) interface_imports = set() for iface in self.interface: interface_imports |= iface.get_imports(all_imports) if interface_imports: printer() for module, import_ in sorted(interface_imports): printer(f"from {module} import {import_}") with open(protocol_path, "wb") as f: printer.write(f) printer.clear() output_methods = [ "output_interface", "output_events", "output_requests", "output_global", "output_attributes", ] for output_method in output_methods: for iface in sorted(self.interface): printer.interface_name = iface.name iface_method = getattr(iface, output_method) iface_method(printer) with open(protocol_path, "ab") as f: printer.write(f) printer.clear() flacjacket-pywayland-7f48c57/pywayland/__init__.py0000664000175000017500000000205015240077162022210 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 from .version import __version__ # noqa: F401 try: from ._ffi import ffi, lib # noqa: F401 __wayland_version__ = f"{lib.WAYLAND_VERSION_MAJOR}.{lib.WAYLAND_VERSION_MINOR}.{lib.WAYLAND_VERSION_MICRO}" except ImportError: if not os.getenv("PYTHON_CROSSENV"): raise ImportError( "No module named pywayland._ffi, be sure to run `python ./pywayland/ffi_build.py`" ) __wayland_version__ = os.getenv("PYTHON_CROSSENV_WAYLAND_VERSION", "") flacjacket-pywayland-7f48c57/pywayland/py.typed0000664000175000017500000000000015240077162021567 0ustar epsilonepsilonflacjacket-pywayland-7f48c57/pywayland/version.py0000664000175000017500000000112415240077162022137 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.19" flacjacket-pywayland-7f48c57/pywayland/server/0000775000175000017500000000000015240077162021410 5ustar epsilonepsilonflacjacket-pywayland-7f48c57/pywayland/server/display.py0000664000175000017500000001505215240077162023432 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 types import TracebackType from typing import Literal from pywayland.protocol.wayland import WlShm def _full_display_gc(ptr: ffi.WlDisplay) -> 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: ffi.WlDisplay | None = 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.WlDisplay | None = 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: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, ) -> 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 """ assert self._ptr is not 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).decode() 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. """ assert self._ptr is not None 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. """ assert self._ptr is not None 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""" assert self._ptr is not None lib.wl_display_terminate(self._ptr) @ensure_valid def run(self) -> None: """Run the display""" assert self._ptr is not None lib.wl_display_run(self._ptr) @ensure_valid def init_shm(self) -> None: """Initialize shm for this display""" assert self._ptr is not None 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: WlShm.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` """ assert self._ptr is not None lib.wl_display_add_shm_format(self._ptr, shm_format.value) @ensure_valid def flush_clients(self) -> None: """Flush client connections""" assert self._ptr is not None lib.wl_display_flush_clients(self._ptr) flacjacket-pywayland-7f48c57/pywayland/server/listener.py0000664000175000017500000001207115240077162023610 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 TYPE_CHECKING from pywayland import ffi, lib from pywayland.utils import wl_container_of if TYPE_CHECKING: from collections.abc import Callable from typing import Any, ParamSpec, TypeVar P = ParamSpec("P") R = TypeVar("R") logger = getLogger(__package__) # void (*wl_notify_func_t)(struct wl_listener *listener, void *data); @ffi.def_extern() def notify_func(listener_ptr: ffi.WlListener, data: ffi.CData) -> None: container: ffi.WlListenerContainer = 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 """ def __init__(self, function: Callable[P, R]) -> None: self._ptr: ffi.WlListener | None self._handle: ffi.CData = 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.WlListenerContainer = ffi.new( "struct wl_listener_container *" ) self.container.handle = self._handle self._ptr = ffi.addressof(self.container.destroy_listener) self._ptr.notify = lib.notify_func # type: ignore [assignment] self._notify = function self._signal: Signal | None = 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: ffi.WlSignal | None = None, data_wrapper: Callable[P, R] | None = None, ) -> None: if ptr is None: self._ptr: ffi.WlSignal = ffi.new("struct wl_signal *") lib.wl_signal_init(self._ptr) else: self._ptr = ptr self._data_wrapper = data_wrapper self._link: list[Listener] = [] def add(self, listener: Listener) -> None: """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) if listener._ptr is not None: lib.wl_signal_add(self._ptr, listener._ptr) def emit(self, data: Any = None) -> None: """Emits this signal, notifying all registered listeners :param data: The data that will be emitted with the signal """ data_ptr: ffi.CData 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() flacjacket-pywayland-7f48c57/pywayland/server/__init__.py0000664000175000017500000000136515240077162023526 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: F401 from .display import Display # noqa: F401 from .eventloop import EventLoop # noqa: F401 from .listener import Listener, Signal # noqa: F401 flacjacket-pywayland-7f48c57/pywayland/server/client.py0000664000175000017500000001300715240077162023241 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 TYPE_CHECKING from pywayland import ffi, lib from pywayland.utils import ensure_valid if TYPE_CHECKING: from typing import Any from .display import Display from .listener import Listener def _client_destroy(display: Display, cdata: ffi.WlClient) -> 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: cdata `struct wl_client *` """ def __init__( self, display: Display | None = None, fd: int | None = None, ptr: ffi.WlClient | None = None, ) -> None: if ptr is None: if display is None or display._ptr 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.WlClient | 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[ffi.CData, ffi.CData, ffi.CData]: """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.CData = ffi.new("pid_t *") uid: ffi.CData = ffi.new("uid_t *") gid: ffi.CData = 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.WlResource) -> Client: """Look up the corresponding wl_client for a wl_resource :param resource: The wl_resource :type resource: cdata `struct wl_resource *` :returns: A `Client` instance. """ return cls(ptr=lib.wl_resource_get_client(resource)) def __eq__(self, other: object) -> bool: """Compare this client with another""" return hasattr(other, "_ptr") and self._ptr == other._ptr flacjacket-pywayland-7f48c57/pywayland/server/eventloop.py0000664000175000017500000002361715240077162024006 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 collections.abc import Callable from typing import TypeVar from pywayland.server import Display, Listener T = TypeVar("T") 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: int, mask: int, data_ptr: ffi.CData) -> 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: int, data_ptr: ffi.CData) -> 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: ffi.CData) -> int: 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: ffi.CData) -> None: 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: EventLoop, cdata: ffi.WlEventSource | None) -> None: self._eventloop = eventloop self._ptr = cdata def remove(self) -> None: """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) -> None: """Insert the EventSource into the check list""" assert self._ptr is not None lib.wl_event_source_check(self._ptr) @ensure_valid def timer_update(self, timeout: int) -> None: """Set the timeout of the times callback :param timeout: Delay for timeout in ms :type timeout: `int` """ assert self._ptr is not None 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 and display._ptr is not None: self._ptr: ffi.WlEventLoop | 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[ffi.WlEventSource] = [] def destroy(self) -> None: """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: int, callback: Callable[[int, int, T], int], mask: FdMask, data: T ) -> EventSource: """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 callback: function with callback `int(int fd, uint32_t mask, void *data)` :param mask: File descriptor mask :type mask: `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()` """ assert self._ptr is not None callback_info = CallbackInfo(callback=callback, data=data) handle: ffi.WlEventSource = ffi.new_handle(callback_info) 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: int, callback: Callable[[int, T], int], data: T ) -> EventSource: """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 callabck: 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 """ assert self._ptr is not None callback_info = CallbackInfo(callback=callback, data=data) handle: ffi.WlEventSource = ffi.new_handle(callback_info) 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: Callable[[T], int], data: T) -> EventSource: """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()` """ assert self._ptr is not None callback_info = CallbackInfo(callback=callback, data=data) handle: ffi.WlEventSource = ffi.new_handle(callback_info) 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: Callable[[T], None], data: T) -> EventSource: """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 """ assert self._ptr is not None callback_info = CallbackInfo(callback=callback, data=data) handle: ffi.WlEventSource = ffi.new_handle(callback_info) 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: 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_event_loop_add_destroy_listener(self._ptr, listener._ptr) @ensure_valid def dispatch(self, timeout: int) -> None: """Dispatch callbacks on the event loop""" assert self._ptr is not None lib.wl_event_loop_dispatch(self._ptr, timeout) @ensure_valid def dispatch_idle(self) -> None: """Dispatch idle callback on the event loop""" assert self._ptr is not None lib.wl_event_loop_dispatch_idle(self._ptr) flacjacket-pywayland-7f48c57/pywayland/utils.py0000664000175000017500000001073415240077162021621 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 TYPE_CHECKING, cast from . import ffi, lib if TYPE_CHECKING: from collections.abc import Callable, Iterator from types import TracebackType from typing import Any, ParamSpec, TypeVar P = ParamSpec("P") R = TypeVar("R") def ensure_valid(func: Callable[P, R]) -> Callable[P, R]: @wraps(func) def wrapper(self: Any, *args: P.args, **kwargs: P.kwargs) -> R: if self._ptr is None: raise ValueError(f"{self.__class__.__name__} object has been destroyed") return func(self, *args, **kwargs) return wrapper # type: ignore [return-value] 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: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, ) -> 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._CDataT, ctype: str, member: str, *, ffi: Any = ffi ) -> ffi._CDataO: # type: ignore [type-var] """ #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. """ cdata = ffi.cast("char *", ptr) - ffi.offsetof(ctype, member) return ffi.cast(ctype, cast(ffi.CData, cdata)) # type: ignore [no-any-return] def wl_list_for_each( ctype: str, head: ffi.WlList, member: str, *, ffi: Any = 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: ffi.CData = 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) flacjacket-pywayland-7f48c57/pywayland/ffi_build.py0000664000175000017500000003275715240077162022415 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 pathlib import Path 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 methods 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); """ # wl_message struct CDEF += """ struct wl_message { const char *name; const char *signature; const struct wl_interface **types; }; """ # wl_interface struct CDEF += """ struct wl_interface { const char *name; int version; int method_count; const struct wl_message *methods; int event_count; const struct wl_message *events; }; """ # wl_argument union CDEF += """ 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 struct 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_init (struct wl_list *list); void wl_list_insert (struct wl_list *list, struct wl_list *elm); void wl_list_remove(struct wl_list *elm); """ # dispatcher function callback CDEF += """ typedef int (*wl_dispatcher_func_t)(const void *, void *, uint32_t, const struct wl_message *, union wl_argument *); """ ############################################################################### # wayland-client-core.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_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); """ # 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_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_dispatch_pending(struct wl_display *display); int wl_display_get_error(struct wl_display *display); int wl_display_flush(struct wl_display *display); int wl_display_roundtrip_queue(struct wl_display *display, struct wl_event_queue *queue); int wl_display_roundtrip(struct wl_display *display); struct wl_event_queue *wl_display_create_queue(struct wl_display *display); int wl_display_prepare_read_queue(struct wl_display *display, struct wl_event_queue *queue); int wl_display_prepare_read(struct wl_display *display); int wl_display_read_events(struct wl_display *display); """ ############################################################################### # wayland-server-core.h ############################################################################### # event kind 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; }; """ # wl_signal methods CDEF += """ struct wl_signal { struct wl_list listener_list; }; void wl_signal_init(struct wl_signal *signal); void wl_signal_add(struct wl_signal *signal, struct wl_listener *listener); void wl_signal_emit(struct wl_signal *signal, void *data); """ # wl_event_loop 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); 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); 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); void wl_event_loop_add_destroy_listener(struct wl_event_loop *loop, struct wl_listener * listener); """ # 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); void wl_display_flush_clients(struct wl_display *display); void wl_display_destroy_clients(struct wl_display *display); uint32_t wl_display_get_serial(struct wl_display *display); uint32_t wl_display_next_serial(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 += """ typedef int pid_t; typedef unsigned int uid_t; typedef unsigned int gid_t; 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); 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); struct wl_client * wl_resource_get_client(struct wl_resource *resource); void * wl_resource_get_user_data(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) def ffi_compile(verbose: bool = False) -> None: # Build into the project root so the generated module lands at # pywayland/_ffi.* regardless of the current working directory. ffi_builder.compile(tmpdir=Path(__file__).parent.parent.as_posix(), verbose=verbose) if __name__ == "__main__": ffi_compile() flacjacket-pywayland-7f48c57/build_backend.py0000664000175000017500000000322715240077162021216 0ustar epsilonepsilonfrom __future__ import annotations import subprocess import sys from collections.abc import Callable from functools import wraps from pathlib import Path from typing import Any, ParamSpec, TypeVar from setuptools import build_meta as _orig from setuptools.build_meta import * # noqa: F403 from setuptools.dist import Distribution P = ParamSpec("P") R = TypeVar("R") def run_scanner(fn: Callable[P, R]) -> Callable[P, R]: @wraps(fn) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: repo_root = Path(__file__).resolve().parent # Run the scanner module to generate the protocol files subprocess.run( [sys.executable, "-m", "scanner", "--with-protocols"], check=True, cwd=repo_root / "pywayland", ) return fn(*args, **kwargs) return wrapper def run_ffi_build(fn: Callable[P, R]) -> Callable[P, R]: @wraps(fn) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: repo_root = Path(__file__).resolve().parent # Run the ffi_build.py script to generate the CFFI bindings subprocess.run( [sys.executable, "pywayland/ffi_build.py"], check=True, cwd=repo_root ) Distribution.has_ext_modules = lambda self: True return fn(*args, **kwargs) return wrapper @run_scanner @run_ffi_build def build_wheel(*args: Any, **kwargs: Any) -> str: return _orig.build_wheel(*args, **kwargs) @run_scanner @run_ffi_build def build_editable(*args: Any, **kwargs: Any) -> str: return _orig.build_editable(*args, **kwargs) @run_scanner def build_sdist(*args: Any, **kwargs: Any) -> str: return _orig.build_sdist(*args, **kwargs) flacjacket-pywayland-7f48c57/test/0000775000175000017500000000000015240077162017051 5ustar epsilonepsilonflacjacket-pywayland-7f48c57/test/test_registry_bind.py0000664000175000017500000000473515240077162023337 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 flacjacket-pywayland-7f48c57/test/test_server_display.py0000664000175000017500000000201615240077162023514 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() flacjacket-pywayland-7f48c57/test/test_utils.py0000664000175000017500000000540715240077162021630 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 import ffi, lib from pywayland.utils import ( AnonymousFile, ensure_valid, wl_container_of, wl_list_for_each, ) def test_ensure_valid_decorator_valid(): class Dummy: def __init__(self): self._ptr = 1 @ensure_valid def foo(self): return "ok" d = Dummy() assert d.foo() == "ok" def test_ensure_valid_decorator_invalid(): class Dummy: def __init__(self): self._ptr = None @ensure_valid def foo(self): return "fail" d = Dummy() with pytest.raises(ValueError): d.foo() def test_anonymous_file_open_close(): af = AnonymousFile(10) assert af.fd is None af.open() assert af.fd is not None af.close() assert af.fd is None def test_anonymous_file_context_manager(): af = AnonymousFile(10) with af as fd: assert af.fd == fd assert af.fd is not None assert af.fd is None def test_anonymous_file_open_twice(): af = AnonymousFile(10) af.open() with pytest.raises(OSError, match="File is already open"): af.open() af.close() def test_anonymous_file_close_not_open(): af = AnonymousFile(10) af.close() def test_wl_container_of(): ctype = "struct wl_listener *" member = "link" container = ffi.new(ctype) member_ptr = ffi.addressof(container[0], member) recovered = wl_container_of(member_ptr, ctype, member) assert recovered == container def test_wl_list_for_each(): ctype = "struct wl_listener *" member = "link" list_ptr = ffi.new("struct wl_list *") elem1 = ffi.new(ctype) elem1_list_ptr = ffi.addressof(elem1, member) elem2 = ffi.new(ctype) elem2_list_ptr = ffi.addressof(elem2, member) elem3 = ffi.new(ctype) elem3_list_ptr = ffi.addressof(elem3, member) lib.wl_list_init(list_ptr) lib.wl_list_insert(list_ptr, elem1_list_ptr) # e1 is the first element lib.wl_list_insert(list_ptr, elem2_list_ptr) # e2 is now the first element lib.wl_list_insert(elem2_list_ptr, elem3_list_ptr) # insert e3 after e2 result = list(wl_list_for_each(ctype, list_ptr, member)) assert result == [elem2, elem3, elem1] flacjacket-pywayland-7f48c57/test/test_scanner.py0000664000175000017500000000412015240077162022110 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, "scanner-test-v1.xml") protocol_tests = ["scanner_test_v1.py"] @pytest.fixture(scope="session") def protocol_directory(): protocol = Protocol.parse_file(input_file) imports = {interface.name: protocol.name for interface in protocol.interface} test_files = [ file if isinstance(file, str) else file.values[0] for file in protocol_tests ] with tempfile.TemporaryDirectory() as output_dir: protocol.output(output_dir, imports) assert os.path.exists(output_dir) assert set(os.listdir(output_dir)) == set(test_files) yield output_dir @pytest.mark.parametrize("protocol_name", protocol_tests) def test_protocol(protocol_directory, protocol_name): # Get the generated file output generated_file = os.path.join(protocol_directory, protocol_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, protocol_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) flacjacket-pywayland-7f48c57/test/test_protocol_interface.py0000664000175000017500000001230115240077162024340 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]) flacjacket-pywayland-7f48c57/test/scanner_files/0000775000175000017500000000000015240077162021664 5ustar epsilonepsilonflacjacket-pywayland-7f48c57/test/scanner_files/scanner_test_v1.py0000664000175000017500000003334415240077162025343 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 typing import Any from pywayland.protocol_core import Argument, ArgumentType, Global, Interface, Proxy, Resource 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 WlDestructor(Interface): """Destructor object An interface object with a destructor request. And a multiline description. """ name = "wl_destructor" version = 1 class WlEvents(Interface): """Events object The interface object with the different types of events. """ name = "wl_events" version = 2 class WlRequests(Interface): """Request object The interface object with the different types of requests. """ name = "wl_requests" version = 2 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 WlCoreResource(Resource[WlCore]): 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_v1.WlRequests` object :type object: :class:`~pywayland.protocol.scanner_test_v1.WlRequests` """ self._post_event(0, id, object) class WlDestructorResource(Resource[WlDestructor]): interface = WlDestructor class WlEventsResource(Resource[WlEvents]): 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_v1.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_v1.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_v1.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_v1.WlRequests` :param object: :type object: :class:`~pywayland.protocol.scanner_test_v1.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 WlRequestsResource(Resource[WlRequests]): interface = WlRequests class WlXfailResource(Resource[WlXfail]): interface = WlXfail 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) -> WlCoreProxy: """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) -> WlCoreProxy: """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 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) -> WlDestructorProxy: """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 WlEventsProxy(Proxy[WlEvents]): interface = WlEvents 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) -> WlCoreProxy: """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_v1.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) -> WlCoreProxy: """Create an id With a description :returns: :class:`~pywayland.protocol.scanner_test_v1.WlCore` """ id = self._marshal_constructor(2, WlCore) return id @WlRequests.request( Argument(ArgumentType.NewId, interface=WlCore), ) def create_id2(self) -> WlCoreProxy: """Create an id without a description :returns: :class:`~pywayland.protocol.scanner_test_v1.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: WlCoreProxy | None) -> WlEventsProxy: """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_v1.WlCore` or `None` :returns: :class:`~pywayland.protocol.scanner_test_v1.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[Interface], version: int) -> Any: """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 WlXfailProxy(Proxy[WlXfail]): interface = WlXfail class WlCoreGlobal(Global[WlCore]): interface = WlCore class WlDestructorGlobal(Global[WlDestructor]): interface = WlDestructor class WlEventsGlobal(Global[WlEvents]): interface = WlEvents class WlRequestsGlobal(Global[WlRequests]): interface = WlRequests class WlXfailGlobal(Global[WlXfail]): interface = WlXfail WlCore._gen_c() WlCore.proxy_class = WlCoreProxy WlCore.resource_class = WlCoreResource WlCore.global_class = WlCoreGlobal WlDestructor._gen_c() WlDestructor.proxy_class = WlDestructorProxy WlDestructor.resource_class = WlDestructorResource WlDestructor.global_class = WlDestructorGlobal WlEvents._gen_c() WlEvents.proxy_class = WlEventsProxy WlEvents.resource_class = WlEventsResource WlEvents.global_class = WlEventsGlobal WlRequests._gen_c() WlRequests.proxy_class = WlRequestsProxy WlRequests.resource_class = WlRequestsResource WlRequests.global_class = WlRequestsGlobal WlXfail._gen_c() WlXfail.proxy_class = WlXfailProxy WlXfail.resource_class = WlXfailResource WlXfail.global_class = WlXfailGlobal flacjacket-pywayland-7f48c57/test/scanner_files/scanner-test-v1.xml0000664000175000017500000001705715240077162025352 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. flacjacket-pywayland-7f48c57/test/test_resource.py0000664000175000017500000000557215240077162022322 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.get_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) res_id = res.get_id() # 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.get_id()) == res # This should replace the old one res2 = WlDisplay.resource_class(client, version=1, id=res.get_id()) assert client.get_object(res.get_id()) == res2 res2.destroy() res.destroy() client.destroy() display.destroy() s2.close() flacjacket-pywayland-7f48c57/test/__init__.py0000664000175000017500000000107415240077162021164 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. flacjacket-pywayland-7f48c57/test/test_event_queue.py0000664000175000017500000000310715240077162023010 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() flacjacket-pywayland-7f48c57/test/test_server_eventloop.py0000664000175000017500000001037615240077162024072 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 flacjacket-pywayland-7f48c57/test/test_registry_query.py0000664000175000017500000000433315240077162023562 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: F841 # 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 flacjacket-pywayland-7f48c57/test/test_client_destroy.py0000664000175000017500000000252015240077162023510 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) # noqa: RUF059 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