Give AlbumentationsX a star on GitHub — it powers this leaderboard

Star on GitHub

lupa

Python wrapper around Lua and LuaJIT

Downloads: 0 (30 days)

Description

Lupa
====

.. image:: logo/logo-220x200.png

Lupa integrates the runtimes of Lua_ or LuaJIT2_ into CPython.
It is a partial rewrite of LunaticPython_ in Cython_ with some
additional features such as proper coroutine support.

.. _Lua: http://lua.org/
.. _LuaJIT2: http://luajit.org/
.. _LunaticPython: http://labix.org/lunatic-python
.. _Cython: http://cython.org

For questions not answered here, please contact the `Lupa mailing list`_.

.. _`Lupa mailing list`: http://www.freelists.org/list/lupa-dev

.. contents:: :local:


Major features
--------------

* separate Lua runtime states through a ``LuaRuntime`` class

* Python coroutine wrapper for Lua coroutines

* iteration support for Python objects in Lua and Lua objects in
  Python

* proper encoding and decoding of strings (configurable per runtime,
  UTF-8 by default)

* frees the GIL and supports threading in separate runtimes when
  calling into Lua

* tested with Python 3.8 and later

* ships with Lua 5.1, 5.2, 5.3 and 5.4
  as well as LuaJIT 2.0 and 2.1 on systems that support it.

* easy to hack on and extend as it is written in Cython, not C


Why the name?
-------------

In Latin, "lupa" is a female wolf, as elegant and wild as it sounds.
If you don't like this kind of straight forward allegory to an
endangered species, you may also happily assume it's just an
amalgamation of the phonetic sounds that start the words "Lua" and
"Python", two from each to keep the balance.


Why use it?
-----------

It complements Python very well.  Lua is a language as dynamic as
Python, but LuaJIT compiles it to very fast machine code, sometimes
faster than many statically compiled languages for computational code.
The language runtime is very small and carefully designed for
embedding.  The complete binary module of Lupa, including a statically
linked LuaJIT2 runtime, only weighs some 800KB on a 64 bit machine.
With standard Lua 5.2, it's less than 600KB.

However, the Lua ecosystem lacks many of the batteries that Python
readily includes, either directly in its standard library or as third
party packages. This makes real-world Lua applications harder to write
than equivalent Python applications. Lua is therefore not commonly
used as primary language for large applications, but it makes for a
fast, high-level and resource-friendly backup language inside of
Python when raw speed is required and the edit-compile-run cycle of
binary extension modules is too heavy and too static for agile
development or hot-deployment.

Lupa is a very fast and thin wrapper around Lua or LuaJIT.  It makes it
easy to write dynamic Lua code that accompanies dynamic Python code by
switching between the two languages at runtime, based on the tradeoff
between simplicity and speed.


Which Lua version?
------------------

The binary wheels include different Lua versions as well as LuaJIT, if supported.
By default, ``import lupa`` uses the latest Lua version, but you can choose
a specific one via import:

.. code:: python

    try:
        import lupa.luajit21 as lupa
    except ImportError:
        try:
            import lupa.lua54 as lupa
        except ImportError:
            try:
                import lupa.lua53 as lupa
            except ImportError:
                import lupa

    print(f"Using {lupa.LuaRuntime().lua_implementation} (compiled with {lupa.LUA_VERSION})")


Examples
--------

..
      >>> import lupa.lua54 as lupa

      ## doctest helpers:
      >>> try: _ = sorted
      ... except NameError:
      ...     def sorted(seq):
      ...         l = list(seq)
      ...         l.sort()
      ...         return l

.. code:: python

      >>> from lupa.lua54 import LuaRuntime
      >>> lua = LuaRuntime(unpack_returned_tuples=True)

      >>> lua.eval('1+1')
      2

      >>> lua_func = lua.eval('function(f, n) return f(n) end')

      >>> def py_add1(n): return n+1
      >>> lua_func(py_add1, 2)
      3

      >>> lua.eval('python.eval(" 2 ** 2 ")') == 4
      True
      >>> lua.eval('python.builtins.str(4)') == '4'
      True

The function ``lua_type(obj)`` can be used to find out the type of a
wrapped Lua object in Python code, as provided by Lua's ``type()``
function:

.. code:: python

      >>> lupa.lua_type(lua_func)
      'function'
      >>> lupa.lua_type(lua.eval('{}'))
      'table'

To help in distinguishing between wrapped Lua objects and normal
Python objects, it returns ``None`` for the latter:

.. code:: python

      >>> lupa.lua_type(123) is None
      True
      >>> lupa.lua_type('abc') is None
      True
      >>> lupa.lua_type({}) is None
      True

Note the flag ``unpack_returned_tuples=True`` that is passed to create
the Lua runtime.  It is new in Lupa 0.21 and changes the behaviour of
tuples that get returned by Python functions.  With this flag, they
explode into separate Lua values:

.. code:: python

      >>> lua.execute('a,b,c = python.eval("(1,2)")')
      >>> g = lua.globals()
      >>> g.a
      1
      >>> g.b
      2
      >>> g.c is None
      True

When set to False, functions that return a tuple pass it through to the
Lua code:

.. code:: python

      >>> non_explode_lua = lupa.LuaRuntime(unpack_returned_tuples=False)
      >>> non_explode_lua.execute('a,b,c = python.eval("(1,2)")')
      >>> g = non_explode_lua.globals()
      >>> g.a
      (1, 2)
      >>> g.b is None
      True
      >>> g.c is None
      True

Since the default behaviour (to not explode tuples) might change in a
later version of Lupa, it is best to always pass this flag explicitly.


Python objects in Lua
---------------------

Python objects are either converted when passed into Lua (e.g.
numbers and strings) or passed as wrapped object references.

.. code:: python

      >>> wrapped_type = lua.globals().type     # Lua's own type() function
      >>> wrapped_type(1) == 'number'
      True
      >>> wrapped_type('abc') == 'string'
      True

Wrapped Lua objects get unwrapped when they are passed back into Lua,
and arbitrary Python objects get wrapped in different ways:

.. code:: python

      >>> wrapped_type(wrapped_type) == 'function'  # unwrapped Lua function
      True
      >>> wrapped_type(len) == 'userdata'       # wrapped Python function
      True
      >>> wrapped_type([]) == 'userdata'        # wrapped Python object
      True

Lua supports two main protocols on objects: calling and indexing.  It
does not distinguish between attribute access and item access like
Python does, so the Lua operations ``obj[x]`` and ``obj.x`` both map
to indexing.  To decide which Python protocol to use for Lua wrapped
objects, Lupa employs a simple heuristic.

Pratically all Python objects allow attribute access, so if the object
also has a ``__getitem__`` method, it is preferred when turning it
into an indexable Lua object.  Otherwise, it becomes a simple object
that uses attribute access for indexing from inside Lua.

Obviously, this heuristic will fail to provide the required behaviour
in many cases, e.g. when attribute access is required to an object
that happens to support item access.  To be explicit about the
protocol that should be used, Lupa provides the helper functions
``as_attrgetter()`` and ``as_itemgetter()`` that restrict the view on
an object to a certain protocol, both from Python and from inside
Lua:

.. code:: python

      >>> lua_func = lua.eval('function(obj) return obj["get"] end')
      >>> d = {'get' : 'value'}

      >>> value = lua_func(d)
      >>> value == d['get'] == 'value'
      True

      >>> value = lua_func( lupa.as_itemgetter(d) )
      >>> value == d['get'] == 'value'
      True

      >>> dict_get = lua_func( lupa.as_attrgetter(d) )
      >>> dict_get == d.get
      True
      >>> dict_get('get') == d.get('get') == 'value'
      True

      >>> lua_func = lua.eval(
      ...     'function(obj) return python.as_attrgetter(obj)["get"] end')
      >>> dict_get = lua_func(d)
      >>> dict_get('get') == d.get('get') == 'value'
      True

Note that unlike Lua function objects, callable Python objects support
indexing in Lua:

.. code:: python

      >>> def py_func(): pass
      >>> py_func.ATTR = 2

      >>> lua_func = lua.eval('function(obj) return obj.ATTR end')
      >>> lua_func(py_func)
      2
      >>> lua_func = lua.eval(
      ...     'function(obj) return python.as_attrgetter(obj).ATTR end')
      >>> lua_func(py_func)
      2
      >>> lua_func = lua.eval(
      ...     'function(obj) return python.as_attrgetter(obj)["ATTR"] end')
      >>> lua_func(py_func)
      2


Iteration in Lua
----------------

Iteration over Python objects from Lua's for-loop is fully supported.
However, Python iterables need to be converted using one of the
utility functions which are described here.  This is similar to the
functions like ``pairs()`` in Lua.

To iterate over a plain Python iterable, use the ``python.iter()``
function.  For example, you can manually copy a Python list into a Lua
table like this:

.. code:: python

      >>> lua_copy = lua.eval('''
      ...     function(L)
      ...         local t, i = {}, 1
      ...         for item in python.iter(L) do
      ...             t[i] = item
      ...             i = i + 1
      ...         end
      ...         return t
      ...     end
      ... ''')

      >>> table = lua_copy([1,2,3,4])
      >>> len(table)
      4
      >>> table[1]   # Lua indexing
      1

Python's ``enumerate()`` function is also supported, so the above
could be simplified to:

.. code:: python

      >>> lua_copy = lua.eval('''
      ...     function(L)
      ...         local t = {}
      ...         for index, item in python.enumerate(L) do
      ...             t[ index+1 ] = item
      ...         end
      ...         return t
      ...     end
      ... ''')

      >>> table = lua_copy([1,2,3,4])
      >>> len(table)
      4
      >>> table[1]   # Lua indexing
      1

For iterators that return tuples, such as ``dict.iteritems()``, it is
convenient to use the special ``python.iterex()`` function that
auto