Skip to content

Instantly share code, notes, and snippets.

@juancarlospaco
Last active February 9, 2025 22:05
Show Gist options
  • Save juancarlospaco/683b44c83876a1e79b55803c936e8052 to your computer and use it in GitHub Desktop.
Save juancarlospaco/683b44c83876a1e79b55803c936e8052 to your computer and use it in GitHub Desktop.
setup.py with post-install compilation of Cython and Go.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
#
# To generate DEB package from Python Package:
# sudo pip3 install stdeb
# python3 setup.py --verbose --command-packages=stdeb.command bdist_deb
#
#
# To generate RPM package from Python Package:
# sudo apt-get install rpm
# python3 setup.py bdist_rpm --verbose --fix-python --binary-only
#
#
# To generate EXE MS Windows from Python Package (from MS Windows only):
# python3 setup.py bdist_wininst --verbose
#
#
# To generate PKGBUILD ArchLinux from Python Package (from PyPI only):
# sudo pip3 install git+https://github.com/bluepeppers/pip2arch.git
# pip2arch.py PackageNameHere
#
#
# To Upload to PyPI by executing:
# sudo pip install --upgrade pip setuptools wheel virtualenv
# python3 setup.py bdist_egg bdist_wheel --universal sdist --formats=zip upload --sign
#
#
# How to check if your modules are Cythonizable ?:
# cython -3 --verbose --no-docstrings your_module.py # Pure Python,needs a *.pxd
# cython -3 --verbose --no-docstrings your_module.pyx # Cython Syntax,dont need *.px
# cythonize --in-place your_module.c
"""Generic Setup.py.
ALL THE CONFIG LIVES IN SETUP.CFG,PLEASE EDIT THERE,KEEP IT SIMPLE AND CLEAN."""
import atexit
from setuptools import setup
##############################################################################
# EDIT HERE
MODULES2CYTHONIZE = ("example/somemodule.py", # Put here modules you want to Speed Up.
"example/othermodule.py",
"example/slowmodule.py",
"example/example.py")
##############################################################################
# Dont touch below
def post_install_cythonize():
"""Compile *.PY to *.SO with Cython,delete *.PYC,*.C,*.PY if sucessful."""
import sys
import os
from pathlib import Path
from shutil import which, rmtree
from subprocess import run
try:
from site import getsitepackages
site_packages = getsitepackages()[0]
except (ImportError, Exception):
from distutils.sysconfig import get_python_lib
site_packages = get_python_lib()
gcc, cythoniz = which("gcc"), which("cythonize")
if gcc and cythoniz and site_packages and sys.platform.startswith("linux"):
for py_file in [(Path(site_packages) / f) for f in MODULES2CYTHONIZE]:
if py_file.is_file() and os.access(py_file, os.W_OK):
comand = f"{cythoniz} -3 --inplace --force {py_file}"
try:
run(comand, shell=True, timeout=99, check=True)
except Exception as error:
print(error)
else:
print(f"CREATED Binary file: {py_file.with_suffix('.so')}")
if py_file.with_suffix(".c").is_file():
py_file.with_suffix(".c").unlink() # is C Obfuscated.
if py_file.is_file():
print(f"DELETED unused file: {py_file}")
py_file.unlink() # Because *.SO exist and is faster.
rmtree(py_file.parent / "__pycache__", ignore_errors=True)
else:
print("GCC & Cython not found, install GCC & Cython for Speed up.")
atexit.register(post_install_cythonize)
setup()
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
#
# To generate DEB package from Python Package:
# sudo pip3 install stdeb
# python3 setup.py --verbose --command-packages=stdeb.command bdist_deb
#
#
# To generate RPM package from Python Package:
# sudo apt-get install rpm
# python3 setup.py bdist_rpm --verbose --fix-python --binary-only
#
#
# To generate EXE MS Windows from Python Package (from MS Windows only):
# python3 setup.py bdist_wininst --verbose
#
#
# To generate PKGBUILD ArchLinux from Python Package (from PyPI only):
# sudo pip3 install git+https://github.com/bluepeppers/pip2arch.git
# pip2arch.py PackageNameHere
#
#
# To Upload to PyPI by executing:
# sudo pip install --upgrade pip setuptools wheel virtualenv
# python3 setup.py bdist_egg bdist_wheel --universal sdist --formats=zip upload --sign
#
#
# How to check if your modules are Cythonizable ?:
# cython -3 --verbose --no-docstrings your_module.py # Pure Python,needs a *.pxd
# cython -3 --verbose --no-docstrings your_module.pyx # Cython Syntax,dont need *.px
# cythonize --in-place your_module.c
"""Generic Setup.py.
ALL THE CONFIG LIVES IN SETUP.CFG,PLEASE EDIT THERE,KEEP IT SIMPLE AND CLEAN."""
import atexit
from setuptools import setup
##############################################################################
# EDIT HERE
MODULES2CYTHONIZE = ("example/somemodule.py", # Put here Python or Go files.
"example/othermodule.py",
"example/slowmodule.py",
"example/example.py")
##############################################################################
# Dont touch below
def post_install_compilation():
"""Compile *.PY to *.SO with Cython,Compile Go to Binary using Go."""
import sys
import os
from pathlib import Path
from shutil import which, rmtree
from subprocess import run
try:
from site import getsitepackages
site_packages = getsitepackages()[0]
except (ImportError, Exception):
from distutils.sysconfig import get_python_lib
site_packages = get_python_lib()
gcc, cythoniz, go = which("gcc"), which("cythonize"), which("go")
if gcc and go and cythoniz and site_packages:
is_linux = sys.platform.startswith("linux")
for fyle in [(Path(site_packages) / f) for f in MODULES2COMPILEE]:
is_writable = os.access(py_file, os.W_OK)
is_go = fyle.endswith(".go")
# PYTHON
if fyle.is_file() and is_writable:
comand = f"{cythoniz} -3 --inplace --force {fyle}"
try:
run(comand, shell=True, timeout=99, check=True)
except Exception as error:
print(error)
else:
print(f"CREATED Binary file: {fyle.with_suffix('.so')}")
if fyle.with_suffix(".c").is_file():
fyle.with_suffix(".c").unlink() # is C Obfuscated.
if fyle.is_file():
print(f"DELETED unused file: {fyle}")
fyle.unlink() # Because *.SO exist and is faster.
rmtree(fyle.parent / "__pycache__", ignore_errors=True)
# GO
if fyle.is_file() and is_writable and is_go:
comand = f"{go} build {fyle}"
try:
run(comand, shell=True, timeout=99, check=True)
except Exception as error:
print(error)
else:
print(f"CREATED Binary file: {fyle.with_suffix('')}")
if fyle.with_suffix(".c").is_file():
py_file.with_suffix(".c").unlink() # is C Obfuscated.
if fyle.is_file():
print(f"DELETED unused file: {fyle}")
fyle.unlink() # Because *.SO exist and is faster.
else:
print("GCC,Cython & Go not found, install GCC,Cython & Go.")
if which("go"):
atexit.register(post_install_compilation)
setup()
Copy link

ghost commented May 25, 2021

Now with the PEP 517 build standard being solidified, this post-install cythonization stopped working with the isolated build environments.

Most of my configuration was in setup.cfg, and I wanted to use setuptools "Development Mode" but still distribute cythonized code. My setup.cfg file looks like this:

[metadata]
name = test_module
version = attr: test_module.__version__
author = 
author_email = 
platform = any
classifiers = 
    Development Status :: 5 - Stable
    Intended Audience :: Developers
    Programming Language :: Python
    Programming Language :: Python :: 3.6
    Operating System :: OS Independent

[options]
zip_safe = true
include_package_data = true
python_requires = >= 3.6.9
packages = find:
install_requires =
    numpy
test_suite = tests
tests_require =
    test_module[test]

[options.packages.find]
exclude = tests

pyproject.toml

[build-system]
requires = ["setuptools==56.2.0", "wheel", "Cython==0.29.23"]
build-backend = "setuptools.build_meta"

I was able to reuse some code and came up with a new solution:

import os

from setuptools import setup
from setuptools.command.install import install
from shutil import which
from subprocess import run

pkg_dir = "test_module"

class Install(install):
    def run(self):
        package_dir = f"build/lib/{pkg_dir}"
        cy = which("cythonize")
        for root, _, files in os.walk(package_dir):
            for file in files:
                if not file.endswith(".py"):
                    continue
                command = f"{cy} -3 --inplace --force {os.path.join(root, file)}"
                try:
                    run(command, shell=True, timeout=99, check=True)
                except Exception as e:
                    raise e
                else:
                    os.remove(os.path.join(root, file))
        super().run()


setup(
    cmdclass={
        "install": Install
    }
)

When using the build utility from pypa, run the command:

$ python -m build

This will build both the wheel and the tarball forms of your package. The tarball will contain your python code, and will be cythonized when you install it.
You can install the tarball with this command

$ python -m pip install test_module-dev.tar.gz

Since wheels dont support post-install commands, the wheel will contain your cythonized code.

The best thing about this is it still supports the setuptools "Development Mode"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment