How to replace setup.py with a pyproject.toml for a native C build dependency?

Question:

I came across this little project for creating a C-compiled version of the Black-Scholes function to be used in python.

Although the example code seem to have been published in July this year, it seem that the use setup.py type of build has been deprecated beyond legacy builds. Any compilation fails, first complaining about missing MS C++ 14 compiler (which is not true), then further investigation, seem to indicate that setup.py can no longer be used.

Q: How can I convert the setup.py to a valid pyproject.toml file?

from setuptools import setup, Extension

ext = Extension('bs', sources=['black_scholes/bs.c'])

setup(
    name="black_scholes",
    version="0.0.1",
    description="European Options Pricing Library",
    packages=['black_scholes'],
    ext_modules=[ext]
)

From the somewhat ambiguous website (above), I created the following tree structure.

$ tree -L 3 ./
./
├── black_scholes
│   ├── black_scholes
│   │   ├── Makefile
│   │   ├── __init__.py
│   │   └── bs.c
│   ├── pyproject.toml
│   └── setup.py
├── README.md
└── bs_test.py

Possibly relevant questions:

Asked By: not2qubit

||

Answers:

After having wasted 2 days on trying to circumvent the required Visual Studio C++ Build tools requirements, the only unfortunate option that would work, was to submit to the >7GB download in order to get my 20 line C-function to compile and install nicely on Py3.10. (Follow this.)

Using an external _custom_build.py

Here are the files that worked:

# setup.py
from setuptools import setup, Extension

ext = Extension('bs', sources=['black_scholes/bs.c'])

setup(
    name="black_scholes",
    version="0.0.1",
    description="European Options Pricing Library",
    packages=['black_scholes'],
    ext_modules=[ext]
)

Then for the pyproject.toml:

# pyproject.toml
[build-system]
requires = ["setuptools>=61.0", "cython"]
build-backend = "setuptools.build_meta"

[project]
name        = "black_scholes"
description = "European Options Pricing Library"
version     = "0.0.1"
readme      = "README.md"
requires-python = ">=3.7"
authors = [
  { name="Example Author", email="[email protected]" },
]
classifiers = [
    "Programming Language :: Python :: 3",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
]
keywords = ["quant", "portfolio"]

[project.urls]
"Homepage" = "https://pyquantnews.com/how-to-45x-python-performance-with-c/"

[tool.setuptools]
py-modules = ["_custom_build"]

[tool.setuptools.cmdclass]
build_py = "_custom_build.build_py"

This is using an external build file called _custom_build.py, as suggested from the SO link above.

# _custom_build.py

from setuptools import Extension
from setuptools.command.build_py import build_py as _build_py

class build_py(_build_py):
    def run(self):
        self.run_command("build_ext")
        return super().run()

    def initialize_options(self):
        super().initialize_options()
        if self.distribution.ext_modules == None:
            self.distribution.ext_modules = []

        self.distribution.ext_modules.append(

            Extension(
                "bs",
                sources=["black_scholes/bs.c"],
                extra_compile_args=["-std=c17", "-lm", "-Wl", "-c", "-fPIC"],
            )
        )

However, it seem that the extra_compile_args are completely ignored…

It would have been great if someone could come up with an alternative solution to build using smaller compiler, like MinGW or so.

The final tree should look like this:

$ tree -L 3
.
├── black_scholes
│   ├── black_scholes
│   │   ├── Makefile
│   │   └── bs.c
│   ├── .gitignore
│   ├── README.md
│   ├── __init__.py
│   ├── _custom_build.py
│   ├── pyproject.toml
│   └── setup.py
└── bs_test.py

Using a src build with setup.py & pyproject.toml

UPDATE: 2022-11-14

The above procedure turned out to be very messy and also gave different results depending on how you used pip install. In the end I completely changed the flat folder structure to use a src based structure. The working project now look like this:

# tree -L 3
.
├── docs
├── examples
│   └── fbs_test.py
├── src
│   ├── black_scholes
│   │   └── __init__.py
│   └── lib
│       ├── Makefile
│       └── fbs.c
├── .gitignore
├── LICENSE.md
├── README.md
├── clean.sh
├── pyproject.toml
└── setup.py

and the content of the files are like this:

# setup.py

from setuptools import setup, find_packages, Extension

ext = Extension(
    name                = 'black_scholes.fbs',          # 'mypackage.mymodule'
    sources             = ['src/lib/fbs.c'],            # list of source files (to compile)
    include_dirs        = ['src/lib'],                  # list of directories to search for C/C++ header files (in Unix form for portability)
    py_limited_api      = True                          # opt-in flag for the usage of Python's limited API <python:c-api/stable>.
)

setup_args = dict(
    packages        = find_packages(where="src"),       # list 
    package_dir     = {"": "src"},                      # mapping
    ext_modules     = [ext],                            # list
    scripts         = ["examples/fbs_test.py"]          # list
)

setup(**setup_args)

and

# pyproject.toml

[build-system]
requires        = ['setuptools>=61.0']                  # 'cython'
build-backend   = 'setuptools.build_meta'   

[project]
name            = 'black_scholes'

# ...

[tool.setuptools]
package-dir = {"" = "src"}
#py-modules = ["_custom_build"]

[tool.setuptools.packages.find]
where = ["src"]

Here it is very important that the package name coincide with the src/black_scholes directory name. If not you will have all sorts of very weird run-time errors even after the package has compiled and installed.

Answered By: not2qubit