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:
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.)
_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="author@example.com" },
]
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
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.