From a64ff80cdf1fffb8ab854f8b85d829660bc59323 Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Thu, 12 Feb 2026 12:10:30 +0100 Subject: [PATCH] docs: clarify Service.interface(), VarlinkError, RequestHandler, and server classes Improve pydoc documentation to address common points of confusion: - Service.interface() expects a varlink interface name that maps to a .varlink file in interface_dir, not a Python class - VarlinkError subclasses for user-defined errors require manually constructing the {"error": ..., "parameters": {...}} dict - RequestHandler requires a `service` class variable wired to the Service instance - ThreadingServer and ForkingServer live in the varlink package, not socketserver; add docstrings and list all server classes in the module docstring --- varlink/__init__.py | 10 ++++++-- varlink/error.py | 24 ++++++++++++++++++- varlink/server.py | 56 +++++++++++++++++++++++++++++++++++++-------- 3 files changed, 78 insertions(+), 12 deletions(-) diff --git a/varlink/__init__.py b/varlink/__init__.py index 2d501f2..d89f133 100644 --- a/varlink/__init__.py +++ b/varlink/__init__.py @@ -3,11 +3,17 @@ See https://www.varlink.org for more information about the varlink protocol and interface definition files. -For server implementations use the :class:`varlink.Server` class. +For server implementations, create a :class:`varlink.Service`, define your interfaces with the +``@service.interface()`` decorator, wire it to a :class:`varlink.RequestHandler` subclass, and run it +with one of the server classes: + +- :class:`varlink.Server` -- single-connection base server (analogous to ``socketserver.TCPServer``) +- :class:`varlink.ThreadingServer` -- multi-threaded server for concurrent connections +- :class:`varlink.ForkingServer` -- multi-process server (Unix/Linux only) For client implementations use the :class:`varlink.Client` class. -For installation and examples, see the GIT repository https://github.com/varlink/python. +For installation and examples, see the GIT repository https://github.com/varlink/python or the `source code <_modules/varlink/tests/test_orgexamplemore.html>`_ of :mod:`varlink.tests.test_orgexamplemore` diff --git a/varlink/error.py b/varlink/error.py index 255537d..4e89aab 100644 --- a/varlink/error.py +++ b/varlink/error.py @@ -16,7 +16,29 @@ def default(self, o): class VarlinkError(Exception): - """The base class for varlink error exceptions""" + """The base class for varlink error exceptions. + + User-defined errors + ~~~~~~~~~~~~~~~~~~~ + + To raise custom errors matching ``error`` declarations in a ``.varlink`` file, + subclass ``VarlinkError`` and pass a dict with ``"error"`` and ``"parameters"`` + keys to the base ``__init__``. The parameter names and types must match the + ``.varlink`` definition manually; there is no automatic binding:: + + # Given: error ActionFailed (field: string) in the .varlink file + + class ActionFailed(VarlinkError): + def __init__(self, reason): + VarlinkError.__init__(self, { + "error": "com.example.ActionFailed", + "parameters": {"field": reason}, + }) + + # In a method handler: + raise ActionFailed("disk full") + + """ @classmethod def new(cls, message, namespaced=False): diff --git a/varlink/server.py b/varlink/server.py index f8a128c..5f58a4d 100644 --- a/varlink/server.py +++ b/varlink/server.py @@ -28,16 +28,29 @@ class Service: >>> ) For the class implementing the methods of a specific varlink interface - a decorator is used: + the ``@service.interface()`` decorator is used. The argument is a varlink + interface name, **not** a Python class. The decorator loads the file + ``{name}.varlink`` from the ``interface_dir`` given to the Service constructor. + For example, ``@service.interface('com.redhat.system.accounts')`` loads + ``com.redhat.system.accounts.varlink`` from ``interface_dir``: >>> @service.interface('com.redhat.system.accounts') >>> class Accounts: - >>> pass + >>> def GetAccounts(self): + >>> return {"accounts": []} - The varlink file corresponding to this interface is loaded from the 'interface_dir' - specified in the constructor of the Service. It has to end in '.varlink'. + The methods defined on the decorated class directly implement the varlink + interface methods. Each method receives the varlink call parameters as + keyword arguments and must return a dict matching the method's return type. - Use a :class:`RequestHandler` with your Service object and run a :class:`Server` with it. + To wire a Service to a network server, create a :class:`RequestHandler` subclass + and set its ``service`` class variable to your Service instance: + + >>> class ServiceRequestHandler(varlink.RequestHandler): + >>> service = service + >>> + >>> server = varlink.ThreadingServer("unix:@example", ServiceRequestHandler) + >>> server.serve_forever() If you want to use your own server with the Service object, split the incoming stream for every null byte and feed it to the :meth:`Service.handle` method. @@ -275,6 +288,22 @@ def _set_interface(self, filename, interface_class): return interface_class def interface(self, filename): + """Decorator that registers a class as the handler for a varlink interface. + + :param filename: A varlink interface name (e.g. ``'com.example.service'``). + The file ``{filename}.varlink`` is loaded from the ``interface_dir`` given + to the Service constructor. This must be an interface name or a file path, + **not** a Python class or type. + + Example:: + + @service.interface('com.example.service') + class Example: + def Echo(self, message): + return {"message": message} + + """ + def decorator(interface_class): self._add_interface(filename, interface_class()) return interface_class @@ -325,8 +354,17 @@ def get_listen_fd() -> Union[int, None]: class RequestHandler(StreamRequestHandler): """Varlink request handler - To use as an argument for the VarlinkServer constructor. - Instantiate your own class and set the class variable service to your global :class:`Service` object. + Subclass this and set the ``service`` class variable to your :class:`Service` instance. + Then pass your subclass to a :class:`Server` (or :class:`ThreadingServer`) constructor:: + + class ServiceRequestHandler(varlink.RequestHandler): + service = service # required class variable + + server = varlink.ThreadingServer(address, ServiceRequestHandler) + server.serve_forever() + + The ``service`` class variable is **required**; without it, incoming requests cannot + be dispatched. """ service: Optional[Service] = None @@ -541,10 +579,10 @@ def __exit__(self, *args): class ThreadingServer(ThreadingMixIn, Server): - pass + """Multi-threaded varlink server that handles each connection in a new thread.""" if hasattr(os, "fork"): class ForkingServer(ForkingMixIn, Server): - pass + """Multi-process varlink server that forks for each connection (Unix/Linux only)."""