Skip to content

Commit 89d53a9

Browse files
committed
fix(ci): robust setup.py for dependency submission and docs update
1 parent d99c153 commit 89d53a9

11 files changed

Lines changed: 413 additions & 181 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
name: python_packaging
3+
description: Guidelines for creating robust Python extension setup.py files that survive CI environments.
4+
---
5+
# Python Packaging in QuanuX
6+
7+
When creating or modifying `setup.py` files for Python extensions (especially those using Cython, PyBind11, or Numpy), you must ensure they are robust against missing build-time dependencies.
8+
9+
## The Problem
10+
CI/CD systems (like GitHub's Dependency Submission Action) often run in environments where build dependencies like `Cython` or `numpy` are NOT installed. If `setup.py` unconditionally imports them, the dependency graph generation fails.
11+
12+
## The Solution: Robust Setup Pattern
13+
Always wrap build-time imports in `try-except` blocks. If they are missing, the script should still run successfully but yield an empty list of extensions.
14+
15+
### Template
16+
17+
```python
18+
import os
19+
from setuptools import setup, Extension
20+
21+
# 1. Wrap build dependencies
22+
try:
23+
from Cython.Build import cythonize
24+
import numpy
25+
except ImportError:
26+
cythonize = None
27+
numpy = None
28+
29+
ext_modules = []
30+
31+
# 2. Conditional Build Logic
32+
if cythonize and numpy:
33+
# Define extensions here
34+
extensions = [
35+
Extension(
36+
"my_extension",
37+
sources=["src/my_extension.pyx"],
38+
include_dirs=[numpy.get_include()],
39+
# ...
40+
)
41+
]
42+
ext_modules = cythonize(extensions, language_level=3)
43+
else:
44+
print("Build dependencies (Cython/Numpy) not found. Skipping extension build.")
45+
# This print is important for debugging but allows the script to exit with code 0
46+
47+
# 3. Setup Call
48+
setup(
49+
name="my_package",
50+
ext_modules=ext_modules, # Will be empty if deps missing
51+
# ... standard metadata ...
52+
)
53+
```
54+
55+
## Checklist
56+
- [ ] Import `Cython.Build.cythonize` inside `try-except ImportError`.
57+
- [ ] Import `numpy` inside `try-except ImportError` (if used).
58+
- [ ] Check for existence of required directories (e.g. `_deps`, `sdk`) before adding them to `include_dirs`.
59+
- [ ] Ensure `python setup.py --name` runs successfully in a bare environment.
Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,37 @@
1-
from setuptools import setup, Extension
2-
from Cython.Build import cythonize
31
import os
2+
from setuptools import setup, Extension
3+
try:
4+
from Cython.Build import cythonize
5+
except ImportError:
6+
cythonize = None
47

58
# Adjust path to where pybind11 headers are
69
pybind_include = os.path.abspath("../../server/indicators/build/_deps/pybind11-src/include")
710

8-
print(f"Using pybind11 include: {pybind_include}")
11+
ext_modules = []
912

10-
pybind_ext = Extension(
11-
"bench_pybind",
12-
["benchmark_pybind.cpp"],
13-
include_dirs=[pybind_include],
14-
language="c++",
15-
extra_compile_args=["-std=c++17", "-O3", "-undefined", "dynamic_lookup"] # Mac specific linking
16-
)
13+
if cythonize:
14+
print(f"Using pybind11 include: {pybind_include}")
1715

18-
cython_ext = Extension(
19-
"bench_cython",
20-
["benchmark_cython.pyx"],
21-
extra_compile_args=["-O3"]
22-
)
16+
pybind_ext = Extension(
17+
"bench_pybind",
18+
["benchmark_pybind.cpp"],
19+
include_dirs=[pybind_include],
20+
language="c++",
21+
extra_compile_args=["-std=c++17", "-O3", "-undefined", "dynamic_lookup"] # Mac specific linking
22+
)
23+
24+
cython_ext = Extension(
25+
"bench_cython",
26+
["benchmark_cython.pyx"],
27+
extra_compile_args=["-O3"]
28+
)
29+
30+
ext_modules = cythonize([cython_ext], compiler_directives={'language_level': "3"}) + [pybind_ext]
31+
else:
32+
print("Cython not found, skipping extension build")
2333

2434
setup(
2535
name="benchmarks",
26-
ext_modules=cythonize([cython_ext], compiler_directives={'language_level': "3"}) + [pybind_ext],
36+
ext_modules=ext_modules,
2737
)

docs/DEVELOPMENT.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,35 @@ Ensure you are running `quanuxctl` with `PYTHONPATH=.` from the project root, or
115115

116116
### CMake "Missing header"
117117
If headers like `nats.h` are missing, ensure the `FetchContent_MakeAvailable(cnats)` step succeeded in the CMake output. Try clearing the `build/` directory and re-running `cmake ..`.
118+
119+
---
120+
121+
## Python Extension Guidelines
122+
123+
### Robust Packaging (CI/CD)
124+
When writing `setup.py` files for extensions (Cython/PyBind11), you **must** handle missing build dependencies gracefully. CI environments often run dependency checks without installing build tools like Cython or Numpy.
125+
126+
**Requirements:**
127+
1. Wrap `Cython` and `numpy` imports in `try-except ImportError` blocks.
128+
2. Only define `Extension` objects if dependencies are present.
129+
3. Ensure `python setup.py --name` succeeds in a bare environment.
130+
131+
**Example Pattern:**
132+
```python
133+
try:
134+
from Cython.Build import cythonize
135+
import numpy
136+
except ImportError:
137+
cythonize = None
138+
139+
ext_modules = []
140+
if cythonize:
141+
# Define extensions
142+
ext_modules = cythonize([...])
143+
144+
setup(
145+
name="my_extension",
146+
ext_modules=ext_modules
147+
)
148+
```
149+
For more details, see the agent skill `python_packaging`.

docs/man/QUANUX_DEV.7

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
.\" Manpage for quanux_dev
2+
.TH QUANUX_DEV 7 "February 2026" "1.0" "QuanuX HFT Suite"
3+
.SH NAME
4+
quanux_dev \- QuanuX Development Guidelines and Standards
5+
.SH DESCRIPTION
6+
This manual page outlines the development standards, coding conventions, and architectural guidelines for the QuanuX High-Frequency Trading platform.
7+
8+
.SH C++ STANDARDS
9+
.B Version:
10+
C++20 is the required standard for all core components (`execution-node`, `server/indicators`).
11+
.PP
12+
.B Compiler Flags:
13+
- `-O3`: Maximum optimization for HFT hot paths.
14+
- `-march=native`: Target the specific CPU architecture (AVX2/AVX-512).
15+
- `-fno-rtti`, `-fno-exceptions`: Disabled in hot paths to minimize runtime overhead.
16+
17+
.SH PYTHON EXTENSIONS
18+
QuanuX relies heavily on Python extensions (Cython/PyBind11) for strategy logic and data analysis.
19+
.PP
20+
.B robust-packaging:
21+
To ensure compatibility with CI/CD environments (like GitHub Dependency Submission), all `setup.py` files must:
22+
.RS
23+
1. Wrap build-time imports (`Cython`, `numpy`) in `try-except ImportError` blocks.
24+
2. Only define `Extension` objects if dependencies are present.
25+
3. Exit successfully (code 0) even if dependencies are missing (building nothing).
26+
.RE
27+
.PP
28+
Example:
29+
.RS
30+
.nf
31+
try:
32+
from Cython.Build import cythonize
33+
except ImportError:
34+
cythonize = None
35+
36+
ext_modules = []
37+
if cythonize:
38+
ext_modules = cythonize([...])
39+
40+
setup(..., ext_modules=ext_modules)
41+
.fi
42+
.RE
43+
44+
.SH DIRECTORY STRUCTURE
45+
.TP
46+
.B execution-node/
47+
Core C++ trading engine and adapter interfaces.
48+
.TP
49+
.B server/strategies/
50+
Python-based trading strategies.
51+
.TP
52+
.B extensions/
53+
Vendor-specific integrations (Rithmic, Databento, etc.).
54+
55+
.SH SEE ALSO
56+
quanux_stats(8), quanuxctl(1)
57+
58+
.SH AUTHOR
59+
QuanuX Development Team

execution-node/cython/setup.py

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,45 @@
11
from setuptools import setup, Extension
2-
from Cython.Build import cythonize
3-
import numpy
2+
try:
3+
from Cython.Build import cythonize
4+
import numpy
5+
except ImportError:
6+
cythonize = None
7+
numpy = None
48

5-
extensions = [
6-
Extension(
7-
"adapter",
8-
sources=["src/adapter.pyx"],
9-
language="c++",
10-
include_dirs=[numpy.get_include()]
11-
),
12-
Extension(
13-
"core",
14-
sources=["src/core.pyx"],
15-
language="c++",
16-
include_dirs=[numpy.get_include()]
17-
),
18-
Extension(
19-
"direct_adapter",
20-
sources=["src/direct_adapter.pyx"],
21-
language="c++",
22-
include_dirs=[numpy.get_include()]
23-
),
24-
Extension(
25-
"nats_adapter",
26-
sources=["src/nats_adapter.pyx"],
27-
language="c++",
28-
include_dirs=[numpy.get_include()]
29-
)
30-
]
9+
ext_modules = []
10+
11+
if cythonize and numpy:
12+
extensions = [
13+
Extension(
14+
"adapter",
15+
sources=["src/adapter.pyx"],
16+
language="c++",
17+
include_dirs=[numpy.get_include()]
18+
),
19+
Extension(
20+
"core",
21+
sources=["src/core.pyx"],
22+
language="c++",
23+
include_dirs=[numpy.get_include()]
24+
),
25+
Extension(
26+
"direct_adapter",
27+
sources=["src/direct_adapter.pyx"],
28+
language="c++",
29+
include_dirs=[numpy.get_include()]
30+
),
31+
Extension(
32+
"nats_adapter",
33+
sources=["src/nats_adapter.pyx"],
34+
language="c++",
35+
include_dirs=[numpy.get_include()]
36+
)
37+
]
38+
ext_modules = cythonize(extensions, language_level=3)
39+
else:
40+
print("Cython or Numpy not found, skipping extension build")
3141

3242
setup(
3343
name="quanux_node",
34-
ext_modules=cythonize(extensions, language_level=3)
44+
ext_modules=ext_modules
3545
)
Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,45 @@
11
import os
22
import sys
33
from setuptools import setup, Extension
4-
from Cython.Build import cythonize
4+
try:
5+
from Cython.Build import cythonize
6+
except ImportError:
7+
cythonize = None
58

6-
current_dir = os.path.dirname(os.path.abspath(__file__))
7-
# Assume CMake build is at ../build
8-
build_dir = os.path.abspath(os.path.join(current_dir, "../build"))
9-
include_dir = os.path.join(build_dir, "_deps/databento-src/include")
10-
lib_dir = os.path.join(build_dir, "lib/Release")
9+
ext_modules = []
1110

12-
# If headers don't exist, we can't build.
13-
# We will assume user runs cmake first.
11+
if cythonize:
12+
current_dir = os.path.dirname(os.path.abspath(__file__))
13+
# Assume CMake build is at ../build
14+
build_dir = os.path.abspath(os.path.join(current_dir, "../build"))
15+
include_dir = os.path.join(build_dir, "_deps/databento-src/include")
16+
lib_dir = os.path.join(build_dir, "lib/Release")
1417

15-
httplib_dir = os.path.join(build_dir, "_deps/httplib-src")
16-
json_dir = os.path.join(build_dir, "_deps/json-src/include")
18+
# If headers don't exist, we can't build.
19+
# We will assume user runs cmake first.
20+
21+
httplib_dir = os.path.join(build_dir, "_deps/httplib-src")
22+
json_dir = os.path.join(build_dir, "_deps/json-src/include")
1723

18-
extensions = [
19-
Extension(
20-
"databento_ext",
21-
["databento.pyx"],
22-
include_dirs=[include_dir, httplib_dir, json_dir],
23-
library_dirs=[lib_dir, "/usr/local/lib"],
24-
libraries=["databento", "zstd", "curl", "ssl", "crypto", "brotlicommon", "brotlidec", "brotlienc"],
25-
language="c++",
26-
extra_compile_args=["-std=c++17", "-O3", "-undefined", "dynamic_lookup"]
27-
)
28-
]
24+
if os.path.exists(include_dir) and os.path.exists(lib_dir):
25+
extensions = [
26+
Extension(
27+
"databento_ext",
28+
["databento.pyx"],
29+
include_dirs=[include_dir, httplib_dir, json_dir],
30+
library_dirs=[lib_dir, "/usr/local/lib"],
31+
libraries=["databento", "zstd", "curl", "ssl", "crypto", "brotlicommon", "brotlidec", "brotlienc"],
32+
language="c++",
33+
extra_compile_args=["-std=c++17", "-O3", "-undefined", "dynamic_lookup"]
34+
)
35+
]
36+
ext_modules = cythonize(extensions, compiler_directives={'language_level': "3"})
37+
else:
38+
print(f"Databento include/lib not found at {build_dir}, skipping extension build")
39+
else:
40+
print("Cython not found, skipping extension build")
2941

3042
setup(
3143
name="databento_ext",
32-
ext_modules=cythonize(extensions, compiler_directives={'language_level': "3"}),
44+
ext_modules=ext_modules,
3345
)
Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,33 @@
11
import os
22
from setuptools import setup, Extension
3-
from Cython.Build import cythonize
3+
try:
4+
from Cython.Build import cythonize
5+
except ImportError:
6+
cythonize = None
47

58
current_dir = os.path.dirname(os.path.abspath(__file__))
69
third_party_dir = os.path.abspath(os.path.join(current_dir, "../third_party"))
710

8-
print(f"Build info:")
9-
print(f" Third Party: {third_party_dir}")
11+
ext_modules = []
1012

11-
extensions = [
12-
Extension(
13-
"duckdb_ext",
14-
["duckdb_ext.pyx", os.path.join(third_party_dir, "duckdb.cpp")],
15-
include_dirs=[third_party_dir],
16-
language="c++",
17-
extra_compile_args=["-std=c++17", "-O3", "-undefined", "dynamic_lookup", "-DDUCKDB_STATIC_BUILD"]
18-
)
19-
]
13+
if cythonize:
14+
print(f"Build info:")
15+
print(f" Third Party: {third_party_dir}")
16+
17+
extensions = [
18+
Extension(
19+
"duckdb_ext",
20+
["duckdb_ext.pyx", os.path.join(third_party_dir, "duckdb.cpp")],
21+
include_dirs=[third_party_dir],
22+
language="c++",
23+
extra_compile_args=["-std=c++17", "-O3", "-undefined", "dynamic_lookup", "-DDUCKDB_STATIC_BUILD"]
24+
)
25+
]
26+
ext_modules = cythonize(extensions, compiler_directives={'language_level': "3"})
27+
else:
28+
print("Cython not found, skipping extension build")
2029

2130
setup(
2231
name="duckdb_ext",
23-
ext_modules=cythonize(extensions, compiler_directives={'language_level': "3"}),
32+
ext_modules=ext_modules,
2433
)

0 commit comments

Comments
 (0)