Skip to content

Commit 3016db8

Browse files
authored
Ship multiple Lua versions in separate extension modules (GH-207)
* Build multiple Lupa extension modules for the different bundled Lua versions. * Test all Lupa variants with all Lua versions, not just the latest. * setup.py: Make sure we always remove all known options from the command line, not just the ones that happen to be relevant. * Disable Py27 builds in appveyor due to certificate issues with the old git version. We still have Windows builds in Github Actions. * Improve output in case of LuaJIT build failures. * Import the latest extension module only on demand from "import lupa" (on Py3.7 and later, which have PEP 562: module __getattr__). * Update versions of dependencies and build matrix. Closes #196
1 parent 4cd9e03 commit 3016db8

File tree

13 files changed

+541
-280
lines changed

13 files changed

+541
-280
lines changed

.github/workflows/ci.yml

+16-10
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ jobs:
1010
fail-fast: false
1111

1212
matrix:
13-
os: [windows-2019, ubuntu-18.04, macos-10.15]
14-
python-version: ["2.7", "3.10", "3.9", "3.8", "3.7", "3.6", "3.5", "pypy-2.7", "pypy-3.7", "pypy-3.8"]
15-
lua-version: ["bundle", "lua5.2", "lua5.3", "luajit-5.1"]
13+
os: [windows-2019, ubuntu-20.04, macos-11]
14+
python-version: ["2.7", "3.10", "3.9", "3.8", "3.7", "3.6", "pypy-2.7", "pypy-3.7", "pypy-3.8"]
15+
lua-version: ["bundle", "lua5.3", "lua5.2", "luajit-5.1"]
1616

1717
exclude:
1818
- os: windows-2019
@@ -23,21 +23,27 @@ jobs:
2323
lua-version: lua5.2
2424
- os: windows-2019
2525
lua-version: lua5.3
26+
- os: windows-2019
27+
lua-version: lua5.4
2628
- os: windows-2019
2729
lua-version: luajit-5.1
28-
- os: macos-10.15
30+
- os: macos-11
2931
python-version: 2.7
30-
- os: macos-10.15
32+
- os: macos-11
3133
lua-version: lua5.2
32-
- os: macos-10.15
34+
- os: macos-11
3335
lua-version: lua5.3
34-
- os: macos-10.15
36+
- os: macos-11
37+
lua-version: lua5.4
38+
- os: macos-11
3539
lua-version: luajit-5.1
3640

3741
runs-on: ${{ matrix.os }}
3842

3943
env:
4044
CFLAGS_LTO: ${{ contains(matrix.lua-version, 'bundle') && (contains(matrix.os, 'windows') && '/LTCG' || '-flto') || '' }}
45+
CFLAGS: ${{ contains(matrix.os, 'windows') && '/O2' || '-O2 -fPIC' }} -g
46+
MACOSX_DEPLOYMENT_TARGET: "10.15"
4147

4248
steps:
4349
- uses: actions/checkout@v2
@@ -58,18 +64,18 @@ jobs:
5864
run: sudo apt-get install lib${{ matrix.lua-version }}-dev
5965

6066
- name: Build wheel
61-
run: python -m pip install -r requirements.txt && python setup.py sdist bdist_wheel
67+
run: python -m pip install -r requirements.txt && python setup.py sdist ${{ contains(matrix.python-version, '3.') && 'build_ext -j5' || '' }} bdist_wheel
6268
env:
6369
SETUP_OPTIONS: ${{ !contains(matrix.lua-version, 'luajit') && (contains(matrix.lua-version, 'bundle') && '--use-bundle' || '--no-luajit') || '' }}
64-
CFLAGS: ${{ env.CFLAGS_LTO }} -g
70+
CFLAGS: ${{ env.CFLAGS }} ${{ env.CFLAGS_LTO }}
6571
LDFLAGS: ${{ env.CFLAGS_LTO }}
6672

6773
- name: Run tests
6874
run: python setup.py test
6975
continue-on-error: ${{ contains(matrix.python-version, 'pypy') }}
7076
env:
7177
SETUP_OPTIONS: ${{ !contains(matrix.lua-version, 'luajit') && (contains(matrix.lua-version, 'bundle') && '--use-bundle' || '--no-luajit') || '' }}
72-
CFLAGS: ${{ env.CFLAGS_LTO }} -g
78+
CFLAGS: ${{ env.CFLAGS }} ${{ env.CFLAGS_LTO }}
7379
LDFLAGS: ${{ env.CFLAGS_LTO }}
7480

7581
- name: Upload wheels

.github/workflows/wheels.yml

+28-20
Original file line numberDiff line numberDiff line change
@@ -46,32 +46,42 @@ jobs:
4646

4747
matrix:
4848
image:
49-
- manylinux1_x86_64
50-
- manylinux1_i686
51-
#- manylinux2010_x86_64
52-
#- manylinux2010_i686
53-
- manylinux_2_24_x86_64
54-
- manylinux_2_24_i686
55-
- manylinux_2_24_aarch64
49+
- manylinux2014_x86_64
50+
- manylinux2014_i686
51+
- manylinux_2_28_x86_64
52+
- manylinux_2_28_i686
53+
- manylinux_2_28_aarch64
5654
- musllinux_1_1_x86_64
55+
- musllinux_1_1_aarch64
5756
#- manylinux_2_24_ppc64le
5857
#- manylinux_2_24_ppc64le
5958
#- manylinux_2_24_s390x
6059
pyversion: ["*"]
6160

6261
exclude:
63-
- image: manylinux_2_24_aarch64
62+
- image: manylinux_2_28_aarch64
63+
pyversion: "*"
64+
- image: musllinux_1_1_aarch64
6465
pyversion: "*"
6566
include:
6667
- image: manylinux2014_aarch64
6768
pyversion: "cp36*"
68-
- image: manylinux_2_24_aarch64
69+
- image: manylinux_2_28_aarch64
6970
pyversion: "cp37*"
70-
- image: manylinux_2_24_aarch64
71+
- image: manylinux_2_28_aarch64
7172
pyversion: "cp38*"
72-
- image: manylinux_2_24_aarch64
73+
- image: manylinux_2_28_aarch64
7374
pyversion: "cp39*"
74-
- image: manylinux_2_24_aarch64
75+
- image: manylinux_2_28_aarch64
76+
pyversion: "cp310*"
77+
78+
- image: musllinux_1_1_aarch64
79+
pyversion: "cp37*"
80+
- image: musllinux_1_1_aarch64
81+
pyversion: "cp38*"
82+
- image: musllinux_1_1_aarch64
83+
pyversion: "cp39*"
84+
- image: musllinux_1_1_aarch64
7585
pyversion: "cp310*"
7686

7787
steps:
@@ -111,21 +121,19 @@ jobs:
111121
fail-fast: false
112122

113123
matrix:
114-
os: [macos-10.15, windows-latest]
115-
#os: [macos-10.15, windows-latest, macOS-M1]
116-
#os: [macos-10.15, macOS-M1]
117-
#os: [macos-10.15]
118-
python_version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "pypy-3.7-v7.3.3", "pypy-3.8-v7.3.7"]
124+
os: [macos-11, windows-latest]
125+
#os: [macos-11, windows-latest, macOS-M1]
126+
#os: [macos-11, macOS-M1]
127+
#os: [macos-11]
128+
python_version: ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10", "pypy-3.7-v7.3.3", "pypy-3.8-v7.3.7"]
119129

120130
exclude:
121131
# outdated compilers and probably not worth supporting anymore
122132
- os: windows-latest
123133
python_version: 2.7
124-
- os: windows-latest
125-
python_version: 3.5
126134

127135
runs-on: ${{ matrix.os }}
128-
env: { MACOSX_DEPLOYMENT_TARGET: 10.14 }
136+
env: { MACOSX_DEPLOYMENT_TARGET: 10.15 }
129137

130138
steps:
131139
- uses: actions/checkout@v2

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ MANIFEST
1414
*.patch
1515
wheel*/
1616
lupa/version.py
17+
lupa/lua*.pyx
1718

1819
# Vim swapfiles
1920
*.swp

.gitmodules

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
[submodule "third-party/lua54"]
2+
path = third-party/lua54
3+
url = https://github.com/lua/lua.git
14
[submodule "third-party/lua53"]
25
path = third-party/lua53
36
url = https://github.com/lua/lua.git
4-
[submodule "third-party/lua54"]
5-
path = third-party/lua54
7+
[submodule "third-party/lua52"]
8+
path = third-party/lua52
69
url = https://github.com/lua/lua.git
7-
[submodule "third-party/luajit20"]
8-
path = third-party/luajit20
9-
url = https://luajit.org/git/luajit.git
1010
[submodule "third-party/luajit21"]
1111
path = third-party/luajit21
1212
url = https://luajit.org/git/luajit.git
13+
[submodule "third-party/luajit20"]
14+
path = third-party/luajit20
15+
url = https://luajit.org/git/luajit.git

Makefile

+4-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ MANYLINUX_IMAGES= \
1313
manylinux_2_24_aarch64 \
1414
manylinux_2_24_ppc64le \
1515
manylinux_2_24_s390x \
16-
musllinux_1_1_x86_64
16+
musllinux_1_1_x86_64 \
17+
musllinux_1_1_aarch64
1718

1819
.PHONY: all local sdist test clean realclean
1920

@@ -29,7 +30,8 @@ test: local
2930
PYTHONPATH=. $(PYTHON) -m lupa.tests.test
3031

3132
clean:
32-
rm -fr build lupa/_lupa.so
33+
rm -fr build lupa/_lupa*.so lupa/lua*.pyx lupa/*.c
34+
@for dir in third-party/*/; do $(MAKE) -C $${dir} clean; done
3335

3436
realclean: clean
3537
rm -fr lupa/_lupa.c

README.rst

+27-2
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ Major features
3737

3838
* tested with Python 2.7/3.5 and later
3939

40-
* written for LuaJIT2 (tested with LuaJIT 2.0.2), but also works
41-
with the normal Lua interpreter (5.1 and later)
40+
* ships with Lua 5.3 and 5.4 (works with Lua 5.1 and later)
41+
as well as LuaJIT 2.0 and 2.1 on systems that support it.
4242

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

@@ -80,6 +80,31 @@ switching between the two languages at runtime, based on the tradeoff
8080
between simplicity and speed.
8181

8282

83+
Which Lua version?
84+
------------------
85+
86+
The binary wheels include different Lua versions as well as LuaJIT, if supported.
87+
By default, ``import lupa`` uses the latest Lua version, but you can choose
88+
a specific one via import:
89+
90+
.. code:: python
91+
92+
try:
93+
import lupa.luajit20 as lupa
94+
except ImportError:
95+
try:
96+
import lupa.lua54 as lupa
97+
except ImportError:
98+
try:
99+
import lupa.lua53 as lupa
100+
except ImportError:
101+
import lupa
102+
103+
print(f"Using {lupa.LuaRuntime().lua_implementation} (compiled with {lupa.LUA_VERSION})")
104+
105+
Note that LuaJIT 2.1 may also be included (as ``luajit21``) but is currently in Alpha state.
106+
107+
83108
Examples
84109
--------
85110

appveyor.yml

+5-4
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ image: Visual Studio 2019
33

44
environment:
55
matrix:
6-
- python: 27
7-
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2013
8-
- python: 27-x64
9-
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2013
6+
# Disable Py27 builds since they run into certificate issues when retrieving the LuaJIT git submodule.
7+
# - python: 27
8+
# APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2013
9+
# - python: 27-x64
10+
# APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2013
1011
- python: 310
1112
- python: 310-x64
1213
- python: 39

lupa/__init__.py

+47-2
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,54 @@ def _try_import_with_global_library_symbols():
2727

2828
del _try_import_with_global_library_symbols
2929

30-
# the following is all that should stay in the namespace:
3130

32-
from lupa._lupa import *
31+
# Find the implementation with the latest Lua version available.
32+
_newest_lib = None
33+
34+
35+
def _import_newest_lib():
36+
global _newest_lib
37+
if _newest_lib is not None:
38+
return _newest_lib
39+
40+
import os.path
41+
import re
42+
43+
package_dir = os.path.dirname(__file__)
44+
modules = [
45+
match.groups() for match in (
46+
re.match(r"((lua[a-z]*)([0-9]*))\..*", filename)
47+
for filename in os.listdir(package_dir)
48+
)
49+
if match
50+
]
51+
if not modules:
52+
raise RuntimeError("Failed to import Lupa binary module.")
53+
# prefer Lua over LuaJIT and high versions over low versions.
54+
module_name = max(modules, key=lambda m: (m[1] == 'lua', tuple(map(int, m[2] or '0'))))
55+
_newest_lib = __import__(module_name[0], level=1, fromlist="*", globals=globals())
56+
57+
return _newest_lib
58+
59+
60+
def __getattr__(name):
61+
"""
62+
Get a name from the latest available Lua (or LuaJIT) module.
63+
Imports the module as needed.
64+
"""
65+
lua = _newest_lib if _newest_lib is not None else _import_newest_lib()
66+
return getattr(lua, name)
67+
68+
69+
import sys
70+
if sys.version_info < (3, 7):
71+
# Module level "__getattr__" requires Py3.7 or later => import latest Lua now
72+
_import_newest_lib()
73+
globals().update(
74+
(name, getattr(_newest_lib, name))
75+
for name in _newest_lib.__all__
76+
)
77+
del sys
3378

3479
try:
3580
from lupa.version import __version__

lupa/tests/__init__.py

+62-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,68 @@
99
import lupa
1010

1111

12+
class LupaTestCase(unittest.TestCase):
13+
"""
14+
Subclasses can use 'self.lupa' to get the test module, which build_suite_for_module() below will vary.
15+
"""
16+
lupa = lupa
17+
18+
19+
def find_lua_modules():
20+
modules = [lupa]
21+
imported = set()
22+
for filename in os.listdir(os.path.dirname(os.path.dirname(__file__))):
23+
if not filename.startswith('lua'):
24+
continue
25+
module_name = "lupa." + filename.partition('.')[0]
26+
if module_name in imported:
27+
continue
28+
try:
29+
module = __import__(module_name, fromlist='*', level=0)
30+
except ImportError:
31+
pass
32+
else:
33+
imported.add(module_name)
34+
modules.append(module)
35+
36+
return modules
37+
38+
39+
def build_suite_for_modules(loader, test_module_globals):
40+
suite = unittest.TestSuite()
41+
all_lua_modules = find_lua_modules()
42+
43+
for module in all_lua_modules[1:]:
44+
suite.addTests(doctest.DocTestSuite(module))
45+
46+
def add_tests(cls):
47+
tests = loader.loadTestsFromTestCase(cls)
48+
suite.addTests(tests)
49+
50+
for name, test_class in test_module_globals.items():
51+
if (not isinstance(test_class, type) or
52+
not name.startswith('Test') or
53+
not issubclass(test_class, unittest.TestCase)):
54+
continue
55+
56+
if issubclass(test_class, LupaTestCase):
57+
prefix = test_class.__name__ + "_"
58+
qprefix = getattr(test_class, '__qualname__', test_class.__name__) + "_"
59+
60+
for module in all_lua_modules:
61+
class TestClass(test_class):
62+
lupa = module
63+
64+
module_name = module.__name__.rpartition('.')[2]
65+
TestClass.__name__ = prefix + module_name
66+
TestClass.__qualname__ = qprefix + module_name
67+
add_tests(TestClass)
68+
else:
69+
add_tests(test_class)
70+
71+
return suite
72+
73+
1274
def suite():
1375
test_dir = os.path.abspath(os.path.dirname(__file__))
1476

@@ -18,7 +80,6 @@ def suite():
1880
tests.append('lupa.tests.' + filename[:-3])
1981

2082
suite = unittest.defaultTestLoader.loadTestsFromNames(tests)
21-
suite.addTest(doctest.DocTestSuite(lupa._lupa))
2283

2384
# Long version of
2485
# suite.addTest(doctest.DocFileSuite('../../README.rst'))

0 commit comments

Comments
 (0)