├── .github └── workflows │ ├── build.yaml │ ├── coverage.yaml │ ├── ruff.yaml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── COVERAGE.md ├── LICENSE.txt ├── README.md ├── cliff.toml ├── pyproject.toml ├── src └── hatch_cython │ ├── __about__.py │ ├── __init__.py │ ├── config │ ├── __init__.py │ ├── autoimport.py │ ├── config.py │ ├── defaults.py │ ├── files.py │ ├── flags.py │ ├── includes.py │ ├── macros.py │ ├── platform.py │ └── templates.py │ ├── constants.py │ ├── devel.py │ ├── hooks.py │ ├── plugin.py │ ├── temp.py │ ├── types.py │ └── utils.py ├── taskfile.yaml ├── test_libraries ├── bootstrap.py ├── only_included │ ├── LICENSE.txt │ ├── hatch.toml │ ├── pyproject.toml │ ├── src │ │ └── example_only_included │ │ │ ├── __about__.py │ │ │ ├── __init__.py │ │ │ ├── compile.py │ │ │ ├── did.pyi │ │ │ └── dont_compile.py │ └── tests │ │ ├── __init__.py │ │ ├── test_compiled.py │ │ └── test_didnt.py ├── simple_structure │ ├── .gitignore │ ├── LICENSE.txt │ ├── example_lib │ │ └── .gitkeep │ ├── hatch.toml │ ├── pyproject.toml │ └── tests │ │ └── .gitkeep └── src_structure │ ├── .gitignore │ ├── LICENSE.txt │ ├── hatch.toml │ ├── include │ ├── .gitkeep │ └── something.cc │ ├── pyproject.toml │ ├── scripts │ ├── .gitkeep │ └── custom_include.py │ ├── src │ └── example_lib │ │ ├── __about__.py │ │ ├── __init__.py │ │ ├── _alias.pyx │ │ ├── custom_includes.pyi │ │ ├── custom_includes.pyx │ │ ├── mod_a │ │ ├── __init__.py │ │ ├── adds.pyi │ │ ├── adds.pyx │ │ ├── deep_nest │ │ │ └── creates.pyx │ │ ├── some_defn.pxd │ │ └── some_defn.py │ │ ├── no_compile │ │ └── abc.py │ │ ├── normal.py │ │ ├── platform │ │ ├── darwin.pyx │ │ ├── freebsd.pyx │ │ ├── linux.pyx │ │ └── windows.pyx │ │ ├── templated.pyi.in │ │ ├── templated.pyx.in │ │ ├── templated_maxosx_sample.pyi │ │ ├── templated_maxosx_sample.pyx │ │ ├── test.pyi │ │ └── test.pyx │ └── tests │ ├── __init__.py │ ├── test_aliased.py │ ├── test_custom_includes.py │ ├── test_example.py │ ├── test_normal.py │ ├── test_platform.py │ ├── test_submodule.py │ └── test_templated.py └── tests ├── __init__.py ├── test_config.py ├── test_files.py ├── test_includes.py ├── test_macros.py ├── test_platform.py ├── test_platform_pyversion.py ├── test_plugin.py ├── test_plugin_excludes.py ├── test_setuppy.py ├── test_templates.py ├── test_utils.py ├── test_xenv_merge.py └── utils.py /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | branches: 8 | - main 9 | 10 | permissions: 11 | contents: write 12 | id-token: write 13 | 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.sha }} 16 | cancel-in-progress: true 17 | 18 | defaults: 19 | run: 20 | shell: bash 21 | 22 | jobs: 23 | validate: 24 | name: Python ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} 25 | runs-on: ${{ matrix.os }} 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | os: [ubuntu-latest, windows-latest, macos-latest] 30 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | 35 | - name: Install llvm 36 | if: startsWith(matrix.os, 'macos') 37 | run: | 38 | brew remove --ignore-dependencies python 39 | rm -rf /usr/local/bin/2to3* 40 | rm -rf /usr/local/bin/idle3* 41 | rm -rf /usr/local/bin/pydoc3* 42 | rm -rf /usr/local/bin/python* 43 | brew cleanup 44 | brew update -f 45 | brew install libomp llvm 46 | echo " 47 | export PATH=\"$(brew --prefix)/opt/llvm/bin:\$PATH\" 48 | export DYLD_LIBRARY_PATH=\"$(brew --prefix)/lib:$DYLD_LIBRARY_PATH\" 49 | export LDFLAGS=\"-L$(brew --prefix)/opt/llvm/lib\" 50 | export CPPFLAGS=\"-I$(brew --prefix)/opt/llvm/include\" 51 | export LD=ld.lld 52 | export AR=llvm-ar 53 | export RANLIB=llvm-ranlib 54 | 55 | " >> $HOME/.bash_profile 56 | 57 | - uses: actions/setup-go@v5 58 | with: 59 | go-version: stable 60 | 61 | - name: Install Task 62 | run: | 63 | go install github.com/go-task/task/v3/cmd/task@latest 64 | 65 | - uses: hecrj/setup-rust-action@v2 66 | with: 67 | rust-version: stable 68 | 69 | - uses: cargo-bins/cargo-binstall@main 70 | 71 | - name: Install jaq 72 | run: | 73 | cargo binstall -y jaq 74 | 75 | - name: Set up Python ${{ matrix.python-version }} 76 | uses: actions/setup-python@v5 77 | with: 78 | python-version: ${{ matrix.python-version }} 79 | 80 | - name: Install Hatch 81 | run: | 82 | pip install --upgrade pip 83 | pip install --upgrade hatch 84 | 85 | - name: Run tests 86 | run: hatch run cov 87 | 88 | - name: Test src lib compilation 89 | shell: bash 90 | run: | 91 | task example 92 | 93 | - name: Test simple lib compilation 94 | shell: bash 95 | run: | 96 | task simple-structure 97 | 98 | - name: Test build target directives 99 | shell: bash 100 | run: | 101 | task only-included 102 | 103 | sdist: 104 | needs: [validate] 105 | runs-on: ubuntu-latest 106 | steps: 107 | - uses: actions/checkout@v4 108 | - name: Set up Python 109 | uses: actions/setup-python@v5 110 | - name: Install dependencies 111 | run: | 112 | python -m pip install --upgrade pip 113 | pip install hatch 114 | - name: Build package 115 | run: hatch build -t sdist 116 | - name: Upload sdist 117 | uses: actions/upload-artifact@v4 118 | with: 119 | name: hatch-cython-sdist 120 | path: ./dist/ 121 | 122 | wheel: 123 | needs: [validate] 124 | runs-on: ${{ matrix.os }} 125 | strategy: 126 | matrix: 127 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] 128 | os: [ubuntu-latest, windows-latest, macos-latest] 129 | steps: 130 | - uses: actions/checkout@v4 131 | - name: Set up Python ${{ matrix.python-version }} 132 | uses: actions/setup-python@v5 133 | with: 134 | python-version: ${{ matrix.python-version }} 135 | - name: Install dependencies 136 | run: | 137 | python -m pip install --upgrade pip 138 | pip install hatch 139 | - name: Build package 140 | run: hatch build -t wheel 141 | - name: Upload wheel 142 | uses: actions/upload-artifact@v4 143 | with: 144 | name: wheel-${{ matrix.python-version }}-${{ matrix.os }} 145 | path: ./dist/ 146 | 147 | release: 148 | needs: [sdist, wheel] 149 | runs-on: ubuntu-latest 150 | if: startsWith(github.ref, 'refs/tags/') 151 | environment: 152 | name: pypi 153 | url: https://pypi.org/p/hatch-cython 154 | steps: 155 | - name: Download all workflow run artifacts 156 | uses: actions/download-artifact@v4 157 | with: 158 | path: dist/ 159 | merge-multiple: true 160 | - name: Display downloaded files 161 | run: ls -R ./dist 162 | - name: Release 163 | uses: softprops/action-gh-release@v2 164 | with: 165 | generate_release_notes: true 166 | fail_on_unmatched_files: true 167 | prerelease: ${{ contains(github.ref, 'rc') }} 168 | files: |- 169 | dist/* 170 | - name: Publish package 171 | uses: pypa/gh-action-pypi-publish@release/v1 172 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yaml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | on: [push, pull_request] 3 | jobs: 4 | cov: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout 8 | uses: actions/checkout@v4 9 | - name: Set up Python 3.11 10 | uses: actions/setup-python@v5 11 | with: 12 | python-version: '3.11' 13 | - name: Install dependencies 14 | run: pip install '.[test]' 15 | - name: Run tests and collect coverage 16 | run: pytest --cov --ignore=test_libraries 17 | - name: Upload coverage to Codecov 18 | uses: codecov/codecov-action@v3 19 | env: 20 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 21 | -------------------------------------------------------------------------------- /.github/workflows/ruff.yaml: -------------------------------------------------------------------------------- 1 | name: Ruff 2 | on: [push, pull_request] 3 | jobs: 4 | ruff: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | - uses: chartboost/ruff-action@v1 9 | with: 10 | src: ./src 11 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [v*] 6 | pull_request: 7 | branches: [v*, main] 8 | 9 | concurrency: 10 | group: test-${{ github.head_ref }} 11 | cancel-in-progress: true 12 | 13 | env: 14 | PYTHONUNBUFFERED: '1' 15 | FORCE_COLOR: '1' 16 | 17 | jobs: 18 | run: 19 | name: Python ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} 20 | runs-on: ${{ matrix.os }} 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | os: [ubuntu-latest, windows-latest, macos-latest] 25 | python-version: ['3.8', '3.9', '3.10', '3.11'] 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - name: Install llvm 31 | if: startsWith(matrix.os, 'macos') 32 | run: | 33 | brew remove --ignore-dependencies python 34 | rm -rf /usr/local/bin/2to3* 35 | rm -rf /usr/local/bin/idle3* 36 | rm -rf /usr/local/bin/pydoc3* 37 | rm -rf /usr/local/bin/python* 38 | brew cleanup 39 | brew update -f 40 | brew install libomp llvm 41 | echo " 42 | export PATH=\"$(brew --prefix)/opt/llvm/bin:\$PATH\" 43 | export DYLD_LIBRARY_PATH=\"$(brew --prefix)/lib:$DYLD_LIBRARY_PATH\" 44 | export LDFLAGS=\"-L$(brew --prefix)/opt/llvm/lib\" 45 | export CPPFLAGS=\"-I$(brew --prefix)/opt/llvm/include\" 46 | export LD=ld.lld 47 | export AR=llvm-ar 48 | export RANLIB=llvm-ranlib 49 | 50 | " >> $HOME/.bash_profile 51 | 52 | - uses: actions/setup-go@v5 53 | with: 54 | go-version: stable 55 | 56 | - name: Install Task 57 | run: | 58 | go install github.com/go-task/task/v3/cmd/task@latest 59 | 60 | - uses: hecrj/setup-rust-action@v2 61 | with: 62 | rust-version: stable 63 | 64 | - uses: cargo-bins/cargo-binstall@main 65 | 66 | - name: Install jaq 67 | run: | 68 | cargo binstall -y jaq 69 | 70 | - name: Set up Python ${{ matrix.python-version }} 71 | uses: actions/setup-python@v5 72 | with: 73 | python-version: ${{ matrix.python-version }} 74 | 75 | - name: Install Hatch 76 | run: | 77 | pip install --upgrade pip 78 | pip install --upgrade hatch 79 | 80 | - name: Run tests 81 | run: hatch run cov 82 | 83 | - name: Test src lib compilation 84 | shell: bash 85 | run: | 86 | task example 87 | 88 | - name: Test simple lib compilation 89 | shell: bash 90 | run: | 91 | task simple-structure 92 | 93 | - name: Test build target directives 94 | shell: bash 95 | run: | 96 | task only-included 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.c 2 | *.cpp 3 | *.so 4 | __pycache__ 5 | pytest_cache 6 | ruff_cache 7 | .DS_Store 8 | dist/ 9 | .coverage* 10 | .coverage 11 | *.html 12 | .mise.toml 13 | *.ipynb 14 | .vscode/settings.json 15 | .vscode/ 16 | .ruff_cache 17 | .pytest_cache 18 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.5.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | 9 | - repo: https://github.com/psf/black 10 | rev: 23.11.0 11 | hooks: 12 | - id: black 13 | 14 | - repo: https://github.com/astral-sh/ruff-pre-commit 15 | rev: v0.1.6 16 | hooks: 17 | - id: ruff 18 | args: [--fix, --exit-non-zero-on-fix] 19 | 20 | - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks 21 | rev: v2.11.0 22 | hooks: 23 | - id: pretty-format-toml 24 | args: [--autofix] 25 | - id: pretty-format-yaml 26 | args: [--autofix, --indent, '4'] 27 | 28 | - repo: https://github.com/compilerla/conventional-pre-commit 29 | rev: v3.0.0 30 | hooks: 31 | - id: conventional-pre-commit 32 | stages: [commit-msg] 33 | args: [] 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [unreleased] 6 | 7 | ### Bug Fixes 8 | 9 | - Implementation 10 | 11 | ### Features 12 | 13 | - Explicit build targets (#46, #47) 14 | 15 | ## [0.5.1] - 2024-02-19 16 | 17 | ### Bug Fixes 18 | 19 | - Build permissions ([#28](https://github.com/joshua-auchincloss/hatch-cython/issues/28)) 20 | - Ci 21 | - No auto changelog 22 | - Release ci [ci skip] ([#40](https://github.com/joshua-auchincloss/hatch-cython/issues/40)) 23 | - Ci 24 | - Release 25 | 26 | ### Features 27 | 28 | - Gh releases 29 | 30 | ### Miscellaneous Tasks 31 | 32 | - V0.5.1 ([#33](https://github.com/joshua-auchincloss/hatch-cython/issues/33)) 33 | - Changelog 34 | - #34 - ci 35 | - Pr #35 - ci 36 | - Add 3.12 for validation [ci skip] 37 | - Pr #36 - ci 38 | - Doc [ci skip] 39 | - Changelog 40 | - Pr #38 from dev 41 | - Tighten ci ([#39](https://github.com/joshua-auchincloss/hatch-cython/issues/39)) 42 | - Changelog 43 | - V0.5.1 44 | 45 | ### Merge 46 | 47 | - #29 from build 48 | - Pr #37 from dev 49 | 50 | ### Release 51 | 52 | - V0.5.1 53 | - V0.5.1 54 | 55 | ## [0.5.0] - 2023-11-30 56 | 57 | ### Bug Fixes 58 | 59 | - Version [ci skip] 60 | - Trigger build 61 | - Depreciate `retain_intermediate_artifacts` 62 | - Macos linkage 63 | - Coverage 64 | - Brew links 65 | - Brew commands 66 | - Random mac errs 67 | - Random brew errs 68 | - Build script, format 69 | 70 | ### Miscellaneous Tasks 71 | 72 | - Pr #23 from joshua-auchincloss/v0.4.0-patch 73 | - Changelog 74 | 75 | ### Testing 76 | 77 | - Different lib structures 78 | - Skip brew 79 | - Tests 80 | 81 | ### Merge 82 | 83 | - Pr #24 from joshua-auchincloss/v0.4.0-patch 84 | - #26/v0.5.0 85 | 86 | ## [0.4.0] - 2023-09-14 87 | 88 | ### Bug Fixes 89 | 90 | - Tests 91 | - Tests 92 | 93 | ### Miscellaneous Tasks 94 | 95 | - Readme [ci skip] 96 | - Lint [ci skip] 97 | - Readme [ci skip] 98 | 99 | ### Refactor 100 | 101 | - Plugin [ci skip] 102 | 103 | ### Testing 104 | 105 | - Sdist [ci skip] 106 | - Sdist pre-ext 107 | - Platform specific files 108 | 109 | ### Merge 110 | 111 | - Pr #22 from joshua-auchincloss/v0.4.0 112 | 113 | ## [0.3.0] - 2023-09-01 114 | 115 | ### Miscellaneous Tasks 116 | 117 | - Readme [ci skip] 118 | 119 | ### Testing 120 | 121 | - Template keywords 122 | 123 | ### Merge 124 | 125 | - Pr #19 from joshua-auchincloss/v0.3.0 126 | 127 | ## [0.2.6] - 2023-08-31 128 | 129 | ### Bug Fixes 130 | 131 | - Tests 132 | 133 | ### Features 134 | 135 | - Add pythran support [ci skip] 136 | 137 | ### Miscellaneous Tasks 138 | 139 | - Readme [ci skip] 140 | - Version [ci skip] 141 | 142 | ### Testing 143 | 144 | - Templated files 145 | 146 | ### Merge 147 | 148 | - Pr #18 from joshua-auchincloss/v0.2.6 149 | 150 | ## [0.2.5] - 2023-08-30 151 | 152 | ### Miscellaneous Tasks 153 | 154 | - Readme 155 | 156 | ### Testing 157 | 158 | - Add macro support 159 | 160 | ### Merge 161 | 162 | - Pr #16 from joshua-auchincloss/v0.2.5 163 | - Pr #17 from joshua-auchincloss/v0.2.5 164 | 165 | ## [0.2.4] - 2023-08-29 166 | 167 | ### Miscellaneous Tasks 168 | 169 | - Readme [ci skip] 170 | 171 | ### Refactor 172 | 173 | - Memoize properties 174 | - Config 175 | 176 | ### Testing 177 | 178 | - Add `compile_py` option 179 | - Module aliasing 180 | 181 | ### Merge 182 | 183 | - Pr #15 from joshua-auchincloss/v0.2.4 184 | 185 | ## [0.2.3] - 2023-08-28 186 | 187 | ### Features 188 | 189 | - Add exclude dir support 190 | 191 | ### Miscellaneous Tasks 192 | 193 | - Readme [ci skip] 194 | 195 | ### Merge 196 | 197 | - Pr #14 from joshua-auchincloss/v0.2.3 198 | 199 | ## [0.2.2] - 2023-08-28 200 | 201 | ### Bug Fixes 202 | 203 | - Dict types 204 | - Sorting error 205 | - Sorting in tests 206 | 207 | ### Miscellaneous Tasks 208 | 209 | - Readme [ci skip] 210 | - Pr #12 from joshua-auchincloss/v0.2.1-post [ci skip] 211 | 212 | ### Testing 213 | 214 | - Fix `Callable` in py38 215 | 216 | ### Merge 217 | 218 | - Pr #13 from joshua-auchincloss/v0.2.2 219 | 220 | ## [0.2.1] - 2023-08-28 221 | 222 | ### Bug Fixes 223 | 224 | - Build [ci skip] 225 | - Prompt build 226 | - Add numpy to test deps 227 | - Debug statements 228 | 229 | ### Miscellaneous Tasks 230 | 231 | - Pr #9 from joshua-auchincloss/v0.2.0-patch [ci skip] 232 | 233 | ### Testing 234 | 235 | - Fix & integration tests 236 | - Fix dir in tests 237 | - Fix env vars 238 | 239 | ### Merge 240 | 241 | - Pr #10 from joshua-auchincloss/v0.2.0-patch 242 | - Pr #11 from joshua-auchincloss/v0.2.0-post 243 | 244 | ## [0.2.0] - 2023-08-27 245 | 246 | ### Bug Fixes 247 | 248 | - Use `llvmlite` 249 | - Add dep on mac 250 | - Debug 251 | - No xcode install 252 | - Build 253 | - Deps 254 | - Deps 255 | - Add `CXX` variable 256 | - Split args 257 | - Coverage 258 | - Coverage 259 | - Revert runner 260 | - `3.9` type compat 261 | - Windows tests 262 | 263 | ### Refactor 264 | 265 | - Devel [ci skip] 266 | 267 | ### Testing 268 | 269 | - Architecture conditions 270 | - Autoinclude 271 | - Debug build 272 | - Fix paths 273 | - Add link guard 274 | - Fix llvm links 275 | - Explicit link & arch 276 | - Use `c++` instead of `g+` 277 | - Explicit arch 278 | - Arch 279 | - Fix args 280 | - Fix cc 281 | - Fix 282 | - `g++` 283 | - Build verbose 284 | - Explicit runner arch 285 | - Platform env vars 286 | - Add marker integration 287 | - Fix darwin ci bug 288 | 289 | ### Merge 290 | 291 | - Pr #7 from joshua-auchincloss/v0.1.9-patch 292 | - Pr #8 from joshua-auchincloss/v0.2.0 293 | 294 | ## [0.1.9] - 2023-08-26 295 | 296 | ### Miscellaneous Tasks 297 | 298 | - Document & version [ci skip] 299 | 300 | ### Testing 301 | 302 | - `os.name` -> `platform.platform()` 303 | - Llvm 304 | 305 | ### Merge 306 | 307 | - Pr #6 from joshua-auchincloss/v0.1.9 308 | 309 | ## [0.1.8] - 2023-08-25 310 | 311 | ### Features 312 | 313 | - Support `.pxd` 314 | - Add `openmp` support 315 | 316 | ### Miscellaneous Tasks 317 | 318 | - Documentation [ci skip] 319 | - Readme [ci skip] 320 | - Version 321 | 322 | ### Merge 323 | 324 | - Pr #5 from joshua-auchincloss/v0.1.8 325 | 326 | ## [0.1.7] - 2023-08-25 327 | 328 | ### Bug Fixes 329 | 330 | - `_post_import_attr` 331 | - Err format strs [ci skip] 332 | 333 | ### Merge 334 | 335 | - Pr #4 from joshua-auchincloss/v0.1.7 336 | 337 | ## [0.1.6] - 2023-08-25 338 | 339 | ### Bug Fixes 340 | 341 | - Add to `__known__` 342 | 343 | ### Testing 344 | 345 | - Add 3.8, 3.9 support 346 | - Add library aliasing 347 | 348 | ### Merge 349 | 350 | - Pr #3 from joshua-auchincloss/v0.1.6 351 | 352 | ## [0.1.5] - 2023-08-25 353 | 354 | ### Testing 355 | 356 | - Fix nested modules 357 | 358 | ### Merge 359 | 360 | - Pr #2 from joshua-auchincloss/v0.1.5 361 | 362 | ## [0.1.4] - 2023-08-25 363 | 364 | ### Bug Fixes 365 | 366 | - Config kwargs 367 | 368 | ## [0.1.3] - 2023-08-25 369 | 370 | ### Bug Fixes 371 | 372 | - Test branches 373 | - Test 374 | - Tests 375 | - No nil values 376 | 377 | ### Features 378 | 379 | - Add std config 380 | 381 | ### Refactor 382 | 383 | - Pyi & release [ci skip] 384 | 385 | ### Testing 386 | 387 | - Fix nt & linux 388 | 389 | ### Merge 390 | 391 | - Pr #1 from joshua-auchincloss/v0.1.3 392 | 393 | ## [0.1.2] - 2023-08-25 394 | 395 | ### Bug Fixes 396 | 397 | - Lint 398 | 399 | ### Miscellaneous Tasks 400 | 401 | - Readme [ci skip] 402 | 403 | ### Refactor 404 | 405 | - Remove`__main__` 406 | 407 | ## [0.1.1] - 2023-08-25 408 | 409 | ### Features 410 | 411 | - Includes & numpy support 412 | 413 | ## [0.1.0-patch] - 2023-08-25 414 | 415 | ### Bug Fixes 416 | 417 | - Version & hooks 418 | 419 | ## [0.1.0-release] - 2023-08-24 420 | 421 | ### Bug Fixes 422 | 423 | - Release 424 | 425 | ## [0.1.0] - 2023-08-24 426 | 427 | ### Bug Fixes 428 | 429 | - Tests 430 | - Nt file paths 431 | - Normalized included files 432 | - Dll 433 | - Debug verbose 434 | - Add .pyd 435 | 436 | ### Features 437 | 438 | - Full cross-platform 439 | 440 | ### Testing 441 | 442 | - Cross-platform 443 | - Non norm 444 | - Nt build 445 | - Nt 446 | - Nt build 447 | - Debug 448 | 449 | 450 | -------------------------------------------------------------------------------- /COVERAGE.md: -------------------------------------------------------------------------------- 1 | | Name | Stmts | Miss | Branch | BrPart | Cover | 2 | | --------------------------------------- | ------: | -----: | ------: | -----: | ------: | 3 | | src/hatch_cython/\_\_init\_\_.py | 2 | 0 | 0 | 0 | 100% | 4 | | src/hatch_cython/config/\_\_init\_\_.py | 2 | 0 | 0 | 0 | 100% | 5 | | src/hatch_cython/config/autoimport.py | 9 | 0 | 2 | 0 | 100% | 6 | | src/hatch_cython/config/config.py | 143 | 9 | 60 | 6 | 92% | 7 | | src/hatch_cython/config/defaults.py | 27 | 0 | 8 | 1 | 97% | 8 | | src/hatch_cython/config/files.py | 35 | 0 | 16 | 1 | 98% | 9 | | src/hatch_cython/config/flags.py | 69 | 1 | 24 | 0 | 99% | 10 | | src/hatch_cython/config/includes.py | 15 | 0 | 8 | 0 | 100% | 11 | | src/hatch_cython/config/macros.py | 12 | 0 | 7 | 0 | 100% | 12 | | src/hatch_cython/config/platform.py | 75 | 0 | 30 | 3 | 97% | 13 | | src/hatch_cython/config/templates.py | 62 | 5 | 32 | 4 | 90% | 14 | | src/hatch_cython/constants.py | 15 | 0 | 0 | 0 | 100% | 15 | | src/hatch_cython/devel.py | 5 | 0 | 0 | 0 | 100% | 16 | | src/hatch_cython/hooks.py | 5 | 1 | 2 | 0 | 86% | 17 | | src/hatch_cython/plugin.py | 248 | 12 | 156 | 10 | 95% | 18 | | src/hatch_cython/temp.py | 13 | 0 | 2 | 0 | 100% | 19 | | src/hatch_cython/utils.py | 39 | 0 | 16 | 0 | 100% | 20 | | **TOTAL** | **776** | **28** | **363** | **25** | **95%** | 21 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present joshua-auchincloss 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hatch-cython 2 | 3 | | | | 4 | | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 5 | | CI/CD | [![Build](https://github.com/joshua-auchincloss/hatch-cython/actions/workflows/build.yaml/badge.svg)](https://github.com/joshua-auchincloss/hatch-cython/actions) [![Tests](https://github.com/joshua-auchincloss/hatch-cython/actions/workflows/test.yml/badge.svg)](https://github.com/joshua-auchincloss/hatch-cython/actions)[![codecov](https://codecov.io/gh/joshua-auchincloss/hatch-cython/graph/badge.svg?token=T12ACNLFWV)](https://codecov.io/gh/joshua-auchincloss/hatch-cython) | 6 | | Package | [![PyPI - Version](https://img.shields.io/pypi/v/hatch-cython.svg?logo=pypi&label=PyPI&logoColor=silver)](https://pypi.org/project/hatch-cython/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/hatch-cython.svg?color=blue&label=Downloads&logo=pypi&logoColor=silver)](https://pypi.org/project/hatch-cython/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/hatch-cython.svg?logo=python&label=Python&logoColor=silver)](https://pypi.org/project/hatch-cython/) | 7 | | Meta | [![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/pypa/hatch) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) | 8 | 9 | --- 10 | 11 | **Table of Contents** 12 | 13 | - [Usage](#usage) 14 | - [Configuration Options](#configuration-options) 15 | - [Notes](#notes) 16 | - [Templating (Tempita)](#templating-tempita) 17 | - [License](#license) 18 | 19 | ## Usage 20 | 21 | The build hook name is `cython`. 22 | 23 | - _pyproject.toml_ 24 | 25 | ```toml 26 | # [tool.hatch.build.hooks.cython] 27 | # or 28 | # (hatch.toml) 29 | # [build.targets.wheel.hooks.cython] 30 | # or 31 | [tool.hatch.build.targets.wheel.hooks.cython] 32 | dependencies = ["hatch-cython"] 33 | 34 | # [tool.hatch.build.hooks.cython.options] 35 | # or 36 | # (hatch.toml) 37 | # [build.targets.wheel.hooks.cython.options] 38 | # or 39 | [tool.hatch.build.targets.wheel.hooks.cython.options] 40 | # include .h or .cpp directories 41 | includes = [] 42 | # include numpy headers 43 | include_numpy = false 44 | include_pyarrow = false 45 | 46 | # include_{custom} 47 | include_somelib = { 48 | # must be included in build-dependencies 49 | pkg = "somelib", 50 | # somelib.gets_include() -> str 51 | include = "gets_include", 52 | # somelib.gets_libraries() -> list[str] 53 | libraries = "gets_libraries", 54 | # somelib.gets_library_dirs() -> list[str] 55 | library_dirs = "gets_library_dirs", 56 | # somelib.some_setup_op() before build 57 | required_call = "some_setup_op" 58 | } 59 | 60 | compile_args = [ 61 | # single string 62 | "-v", 63 | # by platform 64 | { platforms = ["linux", "darwin"], arg = "-Wcpp" }, 65 | # by platform & arch 66 | { platforms = "darwin", arch = "x86_64", arg = "-arch x86_64" }, 67 | { platforms = ["darwin"], arch = "arm64", arg = "-arch arm64" }, 68 | # with pep508 markers 69 | { platforms = ["darwin"], arch = "x86_64", arg = "-I/usr/local/opt/llvm/include", depends_path = true, marker = "python_version <= '3.10'" }, 70 | ] 71 | 72 | directives = { boundscheck = false, nonecheck = false, language_level = 3, binding = true } 73 | 74 | compile_kwargs = { } 75 | ``` 76 | 77 | - _hatch.toml_ 78 | 79 | ```toml 80 | # [build.hooks.cython] 81 | # or 82 | [build.targets.wheel.hooks.cython] 83 | dependencies = ["hatch-cython"] 84 | 85 | # [build.hooks.cython.options] 86 | # or 87 | [build.targets.wheel.hooks.cython.options] 88 | directives = { boundscheck = false, nonecheck = false, language_level = 3, binding = true } 89 | compile_args = [ 90 | "-O3", 91 | ] 92 | includes = [] 93 | include_numpy = false 94 | # equivalent to include_pyarrow = true 95 | include_somelib = { pkg = "pyarrow", include="get_include", libraries="get_libraries", library_dirs="get_library_dirs", required_call="create_library_symlinks" } 96 | define_macros = [ 97 | # ["ABC"] -> ["ABC", "FOO"] | ["ABC", "DEF"] 98 | ["NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION"], 99 | ] 100 | ``` 101 | 102 | ## Configuration Options 103 | 104 | | Field | Type | 105 | | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 106 | | src | `str \| None` 
 directory within `src` dir or `.`  which aliases the package being built. e.g. `package_a` -> `src/package_a_lib` 
 `src = "package_a"` | 107 | | directives | directives to cython (see [compiler-directives]) | 108 | | compile_args | str or `{ platforms = ["*"] \| "*", arg = str }`. see [extensions] for what args may be relevant | 109 | | extra_link_args | str or `{ platforms = ["*"] \| "*", arg = str }`. see [extensions] for what args may be relevant | 110 | | env | `{ env = "VAR1", arg = "VALUE", platforms = ["*"], arch = ["*"] }`
 if flag is one of:
 - ARFLAGS
 - LDSHARED 
 - LDFLAGS
 - CPPFLAGS 
 - CFLAGS 
 - CCSHARED
the current env vars will be merged with the value (provided platform & arch applies), separated by a space. This can be enabled by adding `{ env = "MYVAR" ... , merges = true }` to the definition. | 111 | | includes | list str | 112 | | include\_{package} | `{ pkg = str, include = str, libraries = str\| None, library_dirs = str \| None , required_call = str \| None }` 
where all fields, but `pkg`, are attributes of `pkg` in the type of `callable() -> list[str] \| str` \| `list[str] \| str`. `pkg` is a module, or loadable module object, which may be imported through `import x.y.z`. | 113 | | include_numpy \| include_pyarrow \| include_pythran | bool
3rd party named imports. must have the respective opt in `dependencies` | 114 | | parallel | bool = False 
if parallel, add openmp headers
important: if using macos, you need the *homebrew* llvm vs _apple's_ llvm in order to pass `-fopenmp` to clang compiler | 115 | | compiler | compiler used at build-time. if `msvc` (Microsoft Visual Studio), `/openmp` is used as argument to compile instead of `-fopenmp`  when `parallel = true`. `default = false` | 116 | | compile_py | whether to include `.py` files when building cython exts. note, this can be enabled & you can do per file / matched file ignores as below. `default = true` | 117 | | define_macros | list of list str (of len 1 or 2). len 1 == [KEY] == `#define KEY FOO` . len 2 == [KEY, VALUE] == `#define KEY VALUE`. see [extensions] | 118 | | \*\* kwargs | keyword = value pair arguments to pass to the extension module when building. see [extensions] | 119 | 120 | ### Files 121 | 122 | ```toml 123 | [build.targets.wheel.hooks.cython.options.files] 124 | exclude = [ 125 | # anything matching no_compile is ignored by cython 126 | "*/no_compile/*", 127 | # note - anything "*" is escaped to "([^\s]*)" (non whitespace). 128 | # if you need an actual * for python regex, use as below: 129 | # this excludes all pyd or pytempl extensions 130 | "([^.]\\*).(pyd$|pytempl$)", 131 | # only windows 132 | { matches = "*/windows", platforms = ["linux", "darwin", "freebsd"] }, 133 | # only darwin 134 | { matches = "*/darwin", platforms = ["linux", "freebsd", "windows"] }, 135 | # only linux 136 | { matches = "*/linux", platforms = ["darwin", "freebsd", "windows"] }, 137 | # only freebsd 138 | { matches = "*/freebsd", platforms = ["linux", "darwin", "windows"] } 139 | ] 140 | aliases = {"abclib._filewithoutsuffix" = "abclib.importalias"} 141 | ``` 142 | 143 | ### Explicit Build Targets 144 | 145 | If explicit targets are required (i.e. `hatch-cython` _only_ builds the files specified), use `options.files.targets`. Specifying this option will implicly enable `compile_py`, in addition to checking all `c`, `cpp`, and `cc` files against the specified inclusions. 146 | 147 | ```toml 148 | [build.targets.wheel.hooks.cython.options.files] 149 | targets = [ 150 | # using a single string 151 | "*/compile.py", 152 | # or a match clause 153 | { matches = "*/windows", platforms = ["windows"] }, 154 | { matches = "*/posix", platforms = ["darwin", "freebsd", "linux"] }, 155 | ] 156 | ``` 157 | 158 | ## sdist 159 | 160 | Sdist archives may be generated normally. `hatch` must be defined as the `build-system` build-backend in `pyproject.toml`. As such, hatch will automatically install `hatch-cython`, and perform the specified e.g. platform-specific adjustments to the compile-time arguments. This allows the full build-process to be respected, and generated following specifications of the developer._Note_: If `hatch-cython` is specified to run outside of a wheel-step processes, the extension module is skipped. As such, the `.c` & `.cpp`, as well as templated files, may be generated and stored in the sdist should you wish. However, there is currently little purpose to this, as the extension will likely have differed compile arguments. 161 | 162 | ## Templating 163 | 164 | Cython tempita is supported for any files suffixed with `.in`, where the extension output is: 165 | 166 | - `.pyx.in` 167 | - `.pyd.in` 168 | - `.pyi.in` 169 | For these files, expect the output `.pyx.in` -> `.pyx`. Thus, with aliasing this would look like: 170 | 171 | ```toml 172 | [build.targets.wheel.hooks.cython.options.files] 173 | aliases = {"abclib._somemod" = "abclib.somemod"} 174 | ``` 175 | 176 | - 1. Source files `somemod.pyi.in`, `_somemod.pyx.in` 177 | - 2. Processed templates `somemod.pyi`, `_somemod.pyx` 178 | - 3. Compiled module `abclib.somemod{.pyi,.pyx}` 179 | 180 | An example of this is included in: 181 | 182 | - [pyi stub file](./test_libraries/src_structure/src/example_lib/templated.pyi.in) 183 | - [pyx cython source file](./test_libraries/src_structure/src/example_lib/templated.pyx.in) 184 | - [pyi stub (rendered)](./test_libraries/src_structure/src/example_lib/templated_maxosx_sample.pyi) 185 | - [pyx cython source (rendered)](./test_libraries/src_structure/src/example_lib/templated_maxosx_sample.pyi) 186 | 187 | ### Template Arguments 188 | 189 | Per-file matched namespaces are supported for templating. This follows the above `platforms`, `arch`, & `marker` formats, where if supplied & passing the condition the argument is passed to the template as a named series of keyword arguments. 190 | 191 | If an `index` value is provided, and all other kwargs to templates are `keywords` for each index value. Follows FIFO priority for all keys except global, which is evaluated first and overriden if there are other matching index directives. The engine will attempt to merge the items of the keywords, roughly following: 192 | 193 | ```py 194 | args = { 195 | "index": [ 196 | {"keyword": "global", ...}, 197 | {"keyword": "thisenv", ...}, 198 | ], 199 | "global": {"abc": 1, "other": 2}, 200 | "thisenv": {"other": 3}, 201 | } 202 | 203 | merge(args) -> {"abc": 1, "other": 3} 204 | ``` 205 | 206 | In hatch.toml: 207 | 208 | ```toml 209 | [build.targets.wheel.hooks.cython.options.templates] 210 | index = [ 211 | {keyword = "global", matches = "*" }, 212 | {keyword = "templated_mac", matches = "templated.*.in", platforms = ["darwin"] }, 213 | {keyword = "templated_mac_py38", matches = "templated.*.in", platforms = ["darwin"], marker = "python == '3.8'" }, 214 | {keyword = "templated_win", matches = "templated.*.in", platforms = ["windows"] }, 215 | {keyword = "templated_win_x86_64", matches = "templated.*.in", platforms = ["windows"], arch = ["x86_64"] }, 216 | 217 | ] 218 | 219 | # these are passed as arguments for templating 220 | 221 | # 'global' is a special directive reserved & overriden by all other matched values 222 | global = { supported = ["int"] } 223 | 224 | templated_mac = { supported = ["int", "float"] } 225 | templated_mac_py38 = { supported = ["int", "float"] } 226 | 227 | templated_win = { supported = ["int", "float", "complex"] } 228 | 229 | # assuming numpy is cimported in the template 230 | templated_win_x86_64 = { supported = ["int", "float", "np.double"]} 231 | ``` 232 | 233 | ## Notes 234 | 235 | - MacOS users with brew installed will have `brew --prefix` libs and include paths added in compilation step. Code parsing is found [here](./src/hatch_cython/config/defaults.py#L11) 236 | - Github Runners now run MacOS on m1 platforms. You may have ci issues if you are using MacOS m1 runners and you do not disable `macos-max-compat` in hatch. e.g. 237 | 238 | ```toml 239 | # hatch.toml 240 | 241 | [build.targets.wheel] 242 | macos-max-compat = false 243 | ``` 244 | 245 | ## Development 246 | 247 | ### Requirements 248 | 249 | - a c / c++ compiler 250 | - python 3.8-<=3.12 251 | - [git-cliff] (`pip install git-cliff`) 252 | - [tasks] 253 | 254 | ### Scripts 255 | 256 | - test: library & coverage 257 | - `hatch run cov` 258 | - test: src structure [example](./test_libraries/src_structure/hatch.toml) 259 | - `task example` 260 | - test: simple structure [example](./test_libraries/simple_structure/hatch.toml) 261 | - `task simple-structure` 262 | - commit: precommitt 263 | - `task precommit` 264 | 265 | ## License 266 | 267 | `hatch-cython` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. 268 | 269 | [extensions]: (https://docs.python.org/3/distutils/apiref.html#distutils.core.Extension) 270 | [compiler-directives]: (https://cython.readthedocs.io/en/latest/src/userguide/source_files_and_compilation.html#compiler-directives) 271 | [git-cliff]: https://git-cliff.org 272 | [tasks]: https://taskfile.dev 273 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | [changelog] 2 | body = """ 3 | {% if version %}\ 4 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 5 | {% else %}\ 6 | ## [unreleased] 7 | {% endif %}\ 8 | {% for group, commits in commits | group_by(attribute="group") %} 9 | ### {{ group | upper_first }} 10 | {% for commit in commits %} 11 | - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\ 12 | {% endfor %} 13 | {% endfor %}\n 14 | """ 15 | footer = """ 16 | 17 | """ 18 | header = """ 19 | # Changelog\n 20 | All notable changes to this project will be documented in this file.\n 21 | """ 22 | postprocessors = [ 23 | {pattern = '', replace = "https://github.com/joshua-auchincloss/hatch-cython"} 24 | ] 25 | trim = true 26 | 27 | [git] 28 | commit_parsers = [ 29 | {message = "^feat", group = "Features"}, 30 | {message = "^fix", group = "Bug Fixes"}, 31 | {message = "^doc", group = "Documentation"}, 32 | {message = "^perf", group = "Performance"}, 33 | {message = "^refactor", group = "Refactor"}, 34 | {message = "^style", group = "Styling"}, 35 | {message = "^test", group = "Testing"}, 36 | {message = "^chore\\(release\\): prepare for", skip = true}, 37 | {message = "^chore\\(deps\\)", skip = true}, 38 | {message = "^chore\\(pr\\)", skip = true}, 39 | {message = "^chore\\(pull\\)", skip = true}, 40 | {message = "^chore|ci", group = "Miscellaneous Tasks"}, 41 | {body = ".*security", group = "Security"}, 42 | {message = "^revert", group = "Revert"} 43 | ] 44 | commit_preprocessors = [ 45 | # replace issue numbers 46 | {pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"} 47 | ] 48 | conventional_commits = true 49 | filter_commits = false 50 | filter_unconventional = true 51 | ignore_tags = "" 52 | protect_breaking_commits = false 53 | sort_commits = "oldest" 54 | split_commits = false 55 | tag_pattern = "v[0-9].*" 56 | topo_order = false 57 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "hatchling.build" 3 | requires = ["hatchling"] 4 | 5 | [project] 6 | authors = [ 7 | {name = "joshua-auchincloss", email = "joshua.auchincloss@proton.me"} 8 | ] 9 | classifiers = [ 10 | "Development Status :: 4 - Beta", 11 | "License :: OSI Approved :: MIT License", 12 | "Programming Language :: Python", 13 | "Programming Language :: Python :: 3.8", 14 | "Programming Language :: Python :: 3.9", 15 | "Programming Language :: Python :: 3.10", 16 | "Programming Language :: Python :: 3.11", 17 | "Programming Language :: Python :: 3.12", 18 | "Programming Language :: Python :: Implementation :: CPython", 19 | "Programming Language :: Python :: Implementation :: PyPy", 20 | "Topic :: Software Development :: Build Tools", 21 | "Topic :: Software Development :: Code Generators", 22 | "Topic :: Software Development :: Compilers" 23 | ] 24 | dependencies = [ 25 | "Cython", 26 | "hatch", 27 | "hatchling", 28 | "setuptools", 29 | "typing_extensions; python_version < '3.10'" 30 | ] 31 | description = 'Cython build hooks for hatch' 32 | dynamic = ["version"] 33 | keywords = [] 34 | license = "MIT" 35 | name = "hatch-cython" 36 | readme = "README.md" 37 | requires-python = ">=3.8" 38 | 39 | [project.entry-points.hatch] 40 | cython = "hatch_cython.hooks" 41 | 42 | [project.optional-dependencies] 43 | test = ["coverage[toml]", "pytest", "pytest-cov", "toml", "numpy"] 44 | 45 | [project.urls] 46 | Documentation = "https://github.com/joshua-auchincloss/hatch-cython#readme" 47 | Issues = "https://github.com/joshua-auchincloss/hatch-cython/issues" 48 | Source = "https://github.com/joshua-auchincloss/hatch-cython" 49 | 50 | [tool.black] 51 | line-length = 120 52 | skip-string-normalization = true 53 | target-version = ["py310"] 54 | 55 | [tool.coverage.paths] 56 | hatch_cython = ["src/hatch_cython", "*/hatch-cython/src/hatch_cython"] 57 | tests = ["tests", "*/hatch-cython/tests"] 58 | 59 | [tool.coverage.report] 60 | exclude_lines = [ 61 | "pragma: no cover", 62 | "no cov", 63 | "if __name__ == .__main__.:", 64 | "if TYPE_CHECKING:" 65 | ] 66 | skip_empty = true 67 | 68 | [tool.coverage.run] 69 | branch = true 70 | omit = ["src/hatch_cython/__about__.py", "src/hatch_cython/types.py"] 71 | parallel = true 72 | source_pkgs = ["hatch_cython"] 73 | 74 | [[tool.hatch.envs.all.matrix]] 75 | python = ["3.10", "3.11"] 76 | 77 | [tool.hatch.envs.default] 78 | dependencies = ["coverage[toml]>=6.5", "pytest", "toml", "numpy"] 79 | 80 | [tool.hatch.envs.default.scripts] 81 | cov = ["test-cov", "cov-report"] 82 | cov-report = [ 83 | "- coverage combine", 84 | "coverage report --format=markdown > COVERAGE.md", 85 | "coverage html", 86 | "coverage report" 87 | ] 88 | test = "pytest {args:tests} -v" 89 | test-cov = "coverage run -m pytest -vv {args:tests}" 90 | 91 | [tool.hatch.envs.lint] 92 | dependencies = ["black", "mypy", "ruff"] 93 | detached = true 94 | 95 | [tool.hatch.envs.lint.scripts] 96 | all = ["style", "typing"] 97 | fmt = ["black {args:.}", "ruff --fix {args:.}", "style"] 98 | style = ["ruff {args:.}", "black --check --diff {args:.}"] 99 | typing = "mypy --install-types --non-interactive {args:src/hatch_cython tests}" 100 | 101 | [tool.hatch.version] 102 | path = "src/hatch_cython/__about__.py" 103 | 104 | [tool.ruff] 105 | line-length = 120 106 | target-version = "py310" 107 | 108 | [tool.ruff.lint] 109 | ignore = [ 110 | "B027", 111 | "FBT001", 112 | "FBT002", 113 | "FBT003", 114 | "S105", 115 | "S106", 116 | "S107", 117 | "C901", 118 | "PLR0911", 119 | "PLR0912", 120 | "PLR0913", 121 | "PLR0915" 122 | ] 123 | select = [ 124 | "A", 125 | "ARG", 126 | "B", 127 | "C", 128 | "DTZ", 129 | "E", 130 | "EM", 131 | "F", 132 | "FBT", 133 | "I", 134 | "ICN", 135 | "ISC", 136 | "N", 137 | "PLC", 138 | "PLE", 139 | "PLR", 140 | "PLW", 141 | "Q", 142 | "RUF", 143 | "S", 144 | "T", 145 | "TID", 146 | "UP", 147 | "W", 148 | "YTT" 149 | ] 150 | unfixable = [] 151 | 152 | [tool.ruff.lint.flake8-tidy-imports] 153 | ban-relative-imports = "all" 154 | 155 | [tool.ruff.lint.isort] 156 | known-first-party = ["hatch_cython"] 157 | 158 | [tool.ruff.lint.per-file-ignores] 159 | "**/__init__.py" = ["F401"] 160 | # Tests can use magic values, assertions, and relative imports 161 | "tests/**/*" = ["PLR2004", "S101", "TID252"] 162 | -------------------------------------------------------------------------------- /src/hatch_cython/__about__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023-present joshua-auchincloss 2 | # 3 | # SPDX-License-Identifier: MIT 4 | __version__ = "0.6.0rc0" 5 | -------------------------------------------------------------------------------- /src/hatch_cython/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023-present joshua-auchincloss 2 | # 3 | # SPDX-License-Identifier: MIT 4 | from hatch_cython.hooks import hatch_register_build_hook 5 | from hatch_cython.plugin import CythonBuildHook 6 | -------------------------------------------------------------------------------- /src/hatch_cython/config/__init__.py: -------------------------------------------------------------------------------- 1 | from hatch_cython.config.config import Config, parse_from_dict 2 | from hatch_cython.config.platform import PlatformArgs 3 | -------------------------------------------------------------------------------- /src/hatch_cython/config/autoimport.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | 3 | 4 | @dataclass 5 | class Autoimport: 6 | pkg: str 7 | 8 | include: str 9 | libraries: str = field(default=None) 10 | library_dirs: str = field(default=None) 11 | required_call: str = field(default=None) 12 | 13 | 14 | __packages__ = { 15 | a.pkg: a 16 | for a in ( 17 | Autoimport("numpy", "get_include"), 18 | Autoimport( 19 | "pyarrow", 20 | include="get_include", 21 | libraries="get_libraries", 22 | library_dirs="get_library_dirs", 23 | required_call="create_library_symlinks", 24 | ), 25 | Autoimport("pythran", "get_include"), 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/hatch_cython/config/config.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Generator 2 | from dataclasses import asdict, dataclass, field 3 | from importlib import import_module 4 | from os import path 5 | from typing import Optional 6 | 7 | from hatch.utils.ci import running_in_ci 8 | from hatchling.builders.hooks.plugin.interface import BuildHookInterface 9 | 10 | from hatch_cython.config.autoimport import Autoimport 11 | from hatch_cython.config.defaults import brew_path, get_default_compile, get_default_link 12 | from hatch_cython.config.files import FileArgs 13 | from hatch_cython.config.flags import EnvFlags, parse_env_args 14 | from hatch_cython.config.includes import parse_includes 15 | from hatch_cython.config.macros import DefineMacros, parse_macros 16 | from hatch_cython.config.platform import ListedArgs, PlatformArgs, parse_platform_args 17 | from hatch_cython.config.templates import Templates, parse_template_kwds 18 | from hatch_cython.constants import DIRECTIVES, EXIST_TRIM, INCLUDE, LTPY311, MUST_UNIQUE 19 | from hatch_cython.types import CallableT, ListStr 20 | 21 | # fields tracked by this plugin 22 | __known__ = frozenset( 23 | ( 24 | "src", 25 | "env", 26 | "files", 27 | "includes", 28 | "libraries", 29 | "templates", 30 | "compile_py", 31 | "directives", 32 | "library_dirs", 33 | "compile_args", 34 | "define_macros", 35 | "compiled_sdist", 36 | "extra_link_args", 37 | "cythonize_kwargs", 38 | ) 39 | ) 40 | 41 | 42 | def parse_from_dict(cls: BuildHookInterface): 43 | given = cls.config.get("options", {}) 44 | 45 | passed = given.copy() 46 | kwargs = {} 47 | for key, val in given.items(): 48 | if key in __known__: 49 | parsed: any 50 | if key == "files": 51 | val: dict 52 | parsed: FileArgs = FileArgs(**val) 53 | elif key == "define_macros": 54 | val: list 55 | parsed: DefineMacros = parse_macros(val) 56 | elif key == "templates": 57 | val: dict 58 | parsed: Templates = parse_template_kwds(val) 59 | else: 60 | val: any 61 | parsed: any = val 62 | kwargs[key] = parsed 63 | passed.pop(key) 64 | continue 65 | 66 | compile_args = parse_platform_args(kwargs, "compile_args", get_default_compile) 67 | link_args = parse_platform_args(kwargs, "extra_link_args", get_default_link) 68 | envflags = parse_env_args(kwargs) 69 | cfg = Config(**kwargs, compile_args=compile_args, extra_link_args=link_args, envflags=envflags) 70 | 71 | for maybe_dep, spec in passed.copy().items(): 72 | is_include = maybe_dep.startswith(INCLUDE) 73 | if is_include and spec: 74 | cfg.resolve_pkg( 75 | cls, 76 | parse_includes(maybe_dep, spec), 77 | ) 78 | passed.pop(maybe_dep) 79 | continue 80 | elif is_include: 81 | passed.pop(maybe_dep) 82 | continue 83 | elif maybe_dep == "parallel" and passed.get(maybe_dep): 84 | comp = [ 85 | PlatformArgs(arg="/openmp", platforms="windows"), 86 | PlatformArgs(arg="-fopenmp", platforms=["linux"]), 87 | ] 88 | link = [ 89 | PlatformArgs(arg="/openmp", platforms="windows"), 90 | PlatformArgs(arg="-fopenmp", platforms="linux"), 91 | PlatformArgs( 92 | arg="-lomp", platforms="darwin", arch=["x86_64"], marker=LTPY311, apply_to_marker=running_in_ci 93 | ), 94 | ] 95 | 96 | brew = brew_path() 97 | if brew: 98 | comp.extend( 99 | [ 100 | PlatformArgs( 101 | arg=f"-I{brew}/opt/llvm/include", 102 | platforms=["darwin"], 103 | depends_path=True, 104 | ), 105 | PlatformArgs( 106 | arg=f"-I{brew}/opt/libomp/include", 107 | platforms=["darwin"], 108 | depends_path=True, 109 | ), 110 | ] 111 | ) 112 | link.extend( 113 | [ 114 | PlatformArgs( 115 | arg=f"-L{brew}/opt/llvm/lib/c++ -Wl,-rpath,{brew}/llvm/lib/c++", 116 | platforms=["darwin"], 117 | depends_path=True, 118 | ), 119 | PlatformArgs( 120 | arg=f"-L{brew}/opt/libomp/lib -Wl,-rpath,{brew}/opt/libomp/lib", 121 | platforms=["darwin"], 122 | depends_path=True, 123 | ), 124 | ] 125 | ) 126 | 127 | cma = ({*cfg.compile_args}).union({*comp}) 128 | cfg.compile_args = list(cma) 129 | seb = ({*cfg.extra_link_args}).union({*link}) 130 | cfg.extra_link_args = list(seb) 131 | passed.pop(maybe_dep) 132 | 133 | cfg.compile_kwargs = passed 134 | return cfg 135 | 136 | 137 | @dataclass 138 | class Config: 139 | src: Optional[str] = field(default=None) # noqa: UP007 140 | files: FileArgs = field(default_factory=FileArgs) 141 | includes: ListStr = field(default_factory=list) 142 | define_macros: DefineMacros = field(default_factory=list) 143 | libraries: ListStr = field(default_factory=list) 144 | library_dirs: ListStr = field(default_factory=list) 145 | directives: dict = field(default_factory=lambda: DIRECTIVES) 146 | compile_args: ListedArgs = field(default_factory=get_default_compile) 147 | compile_kwargs: dict = field(default_factory=dict) 148 | cythonize_kwargs: dict = field(default_factory=dict) 149 | extra_link_args: ListedArgs = field(default_factory=get_default_link) 150 | compiled_sdist: bool = field(default=False) 151 | envflags: EnvFlags = field(default_factory=EnvFlags) 152 | compile_py: bool = field(default=True) 153 | templates: Templates = field(default_factory=Templates) 154 | 155 | def __post_init__(self): 156 | self.directives = {**DIRECTIVES, **self.directives} 157 | 158 | @property 159 | def compile_args_for_platform(self): 160 | return self._arg_impl(self.compile_args) 161 | 162 | @property 163 | def compile_links_for_platform(self): 164 | return self._arg_impl(self.extra_link_args) 165 | 166 | def _post_import_attr( 167 | self, 168 | cls: BuildHookInterface, 169 | im: Autoimport, 170 | att: str, 171 | mod: any, 172 | extend: CallableT[[ListStr], None], 173 | append: CallableT[[str], None], 174 | ): 175 | attr = getattr(im, att) 176 | if attr is not None: 177 | try: 178 | libraries = getattr(mod, attr) 179 | if callable(libraries): 180 | libraries = libraries() 181 | 182 | if isinstance(libraries, str): 183 | append(libraries) 184 | elif isinstance(libraries, (list, Generator)): # noqa: UP038 185 | extend(libraries) 186 | elif isinstance(libraries, dict): 187 | extend(libraries.values()) 188 | else: 189 | cls.app.display_warning(f"{im.pkg}.{attr} has an invalid type ({type(libraries)})") 190 | 191 | except AttributeError: 192 | cls.app.display_warning(f"{im.pkg}.{attr}") 193 | 194 | def resolve_pkg( 195 | self, 196 | cls: BuildHookInterface, 197 | im: Autoimport, 198 | ): 199 | mod = import_module(im.pkg) 200 | self._post_import_attr( 201 | cls, 202 | im, 203 | "include", 204 | mod, 205 | self.includes.extend, 206 | self.includes.append, 207 | ) 208 | self._post_import_attr( 209 | cls, 210 | im, 211 | "libraries", 212 | mod, 213 | self.libraries.extend, 214 | self.libraries.append, 215 | ) 216 | self._post_import_attr( 217 | cls, 218 | im, 219 | "library_dirs", 220 | mod, 221 | self.library_dirs.extend, 222 | self.library_dirs.append, 223 | ) 224 | if im.required_call is not None: 225 | if hasattr(mod, im.required_call): 226 | call = getattr(mod, im.required_call) 227 | call() 228 | else: 229 | cls.app.display_warning(f"{im.pkg}.{im.required_call} is invalid") 230 | 231 | def _arg_impl(self, target: ListedArgs): 232 | args = {"any": []} 233 | 234 | def with_argvalue(arg: str): 235 | # be careful with e.g. -Ox flags 236 | matched = list(filter(lambda s: arg.startswith(s), MUST_UNIQUE)) 237 | if len(matched): 238 | m = matched[0] 239 | args[m] = arg.split(" ") 240 | else: 241 | args["any"].append(arg.split(" ")) 242 | 243 | for arg in target: 244 | # if compile-arg format, check platform applies 245 | if isinstance(arg, PlatformArgs): 246 | if arg.applies() and arg.is_exist(EXIST_TRIM): 247 | with_argvalue(arg.arg) 248 | # else assume string / user knows what theyre doing and add to the call params 249 | else: 250 | with_argvalue(arg) 251 | 252 | flat = [] 253 | 254 | def flush(it): 255 | if isinstance(it, list): 256 | for v in it: 257 | flush(v) 258 | else: 259 | flat.append(it) 260 | 261 | # side effect 262 | list(map(flush, args.values())) 263 | return list({*flat}) 264 | 265 | def asdict(self): 266 | d = asdict(self) 267 | d["envflags"]["env"] = self.envflags.masked_environ() 268 | d["templates"] = self.templates.asdict() 269 | return d 270 | 271 | def validate_include_opts(self): 272 | for opt in self.includes: 273 | if not path.exists(opt): 274 | msg = f"{opt} does not exist" 275 | raise ValueError(msg) 276 | -------------------------------------------------------------------------------- /src/hatch_cython/config/defaults.py: -------------------------------------------------------------------------------- 1 | from subprocess import CalledProcessError, check_output 2 | 3 | from hatch_cython.config.platform import PlatformArgs 4 | from hatch_cython.constants import POSIX_CORE 5 | from hatch_cython.utils import aarch, plat 6 | 7 | BREW = "brew" 8 | 9 | 10 | def brew_path(): 11 | if plat() == "darwin": 12 | # no user input - S603 is false positive 13 | try: 14 | proc = check_output([BREW, "--prefix"]) # noqa: S603 15 | except (CalledProcessError, FileNotFoundError): 16 | proc = None 17 | dec = proc.decode().replace("\n", "") if proc else None 18 | if dec and dec != "": 19 | return dec 20 | return "/opt/homebrew" if aarch() == "arm64" else "/usr/local" 21 | 22 | 23 | def get_default_link(): 24 | base = [ 25 | PlatformArgs(arg="-L/usr/local/lib", platforms=POSIX_CORE, depends_path=True), 26 | PlatformArgs(arg="-L/usr/local/opt", platforms=POSIX_CORE, depends_path=True), 27 | ] 28 | 29 | brew = brew_path() 30 | if brew: 31 | base.extend( 32 | [ 33 | PlatformArgs(arg=f"-L{brew}/opt", platforms=POSIX_CORE, depends_path=True), 34 | PlatformArgs(arg=f"-L{brew}/lib", platforms=POSIX_CORE, depends_path=True), 35 | ] 36 | ) 37 | return base 38 | 39 | 40 | def get_default_compile(): 41 | base = [ 42 | PlatformArgs(arg="-O2"), 43 | PlatformArgs(arg="-I/usr/local/include", platforms=POSIX_CORE, depends_path=True), 44 | ] 45 | brew = brew_path() 46 | if brew: 47 | base.extend( 48 | [ 49 | PlatformArgs(arg=f"-I{brew}/include", platforms=POSIX_CORE, depends_path=True), 50 | ] 51 | ) 52 | return base 53 | -------------------------------------------------------------------------------- /src/hatch_cython/config/files.py: -------------------------------------------------------------------------------- 1 | import re 2 | from dataclasses import dataclass, field 3 | 4 | from hatch_cython.config.platform import PlatformBase 5 | from hatch_cython.types import DictT, ListT, UnionT 6 | from hatch_cython.utils import parse_user_glob 7 | 8 | 9 | @dataclass 10 | class OptExclude(PlatformBase): 11 | matches: str = field(default="*") 12 | 13 | 14 | @dataclass 15 | class OptInclude(PlatformBase): 16 | matches: str = field(default="*") 17 | 18 | 19 | @dataclass 20 | class FileArgs: 21 | targets: ListT[UnionT[str, OptInclude]] = field(default_factory=list) 22 | exclude: ListT[UnionT[str, OptExclude]] = field(default_factory=list) 23 | aliases: DictT[str, str] = field(default_factory=dict) 24 | 25 | def __post_init__(self): 26 | rep = {} 27 | for k, v in self.aliases.items(): 28 | rep[parse_user_glob(k)] = v 29 | self.aliases = rep 30 | self.exclude = [ 31 | *[OptExclude(**d) for d in self.exclude if isinstance(d, dict)], 32 | *[OptExclude(matches=s) for s in self.exclude if isinstance(s, str)], 33 | ] 34 | self.targets = [ 35 | *[OptInclude(**d) for d in self.targets if isinstance(d, dict)], 36 | *[OptInclude(matches=s) for s in self.targets if isinstance(s, str)], 37 | ] 38 | 39 | @property 40 | def explicit_targets(self): 41 | return len(self.targets) > 0 42 | 43 | def matches_alias(self, other: str) -> UnionT[str, None]: 44 | matched = [re.match(v, other) for v in self.aliases.keys()] 45 | if any(matched): 46 | first = 0 47 | for ok in matched: 48 | if ok: 49 | break 50 | first += 1 51 | return self.aliases[list(self.aliases.keys())[first]] 52 | -------------------------------------------------------------------------------- /src/hatch_cython/config/flags.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from os import environ, pathsep 3 | from typing import ClassVar 4 | 5 | from hatch_cython.config.platform import PlatformArgs, parse_to_plat 6 | from hatch_cython.types import CallableT, DictT 7 | 8 | 9 | @dataclass 10 | class EnvFlag(PlatformArgs): 11 | env: str = field(default="") 12 | merges: bool = field(default=False) 13 | sep: str = field(default=" ") 14 | 15 | def __hash__(self) -> int: 16 | return hash(self.field) 17 | 18 | 19 | __flags__ = ( 20 | EnvFlag(env="CC", merges=False), 21 | EnvFlag(env="CPP", merges=False), 22 | EnvFlag(env="CXX", merges=False), 23 | EnvFlag(env="CFLAGS", merges=True), 24 | EnvFlag(env="CCSHARED", merges=True), 25 | EnvFlag(env="CPPFLAGS", merges=True), 26 | EnvFlag(env="LDFLAGS", merges=True), 27 | EnvFlag(env="LDSHARED", merges=True), 28 | EnvFlag(env="SHLIB_SUFFIX", merges=False), 29 | EnvFlag(env="AR", merges=False), 30 | EnvFlag(env="ARFLAGS", merges=True), 31 | EnvFlag(env="PATH", merges=True, sep=pathsep), 32 | ) 33 | 34 | 35 | @dataclass 36 | class EnvFlags: 37 | CC: PlatformArgs = None 38 | CPP: PlatformArgs = None 39 | CXX: PlatformArgs = None 40 | 41 | CFLAGS: PlatformArgs = None 42 | CCSHARED: PlatformArgs = None 43 | 44 | CPPFLAGS: PlatformArgs = None 45 | 46 | LDFLAGS: PlatformArgs = None 47 | LDSHARED: PlatformArgs = None 48 | 49 | SHLIB_SUFFIX: PlatformArgs = None 50 | 51 | AR: PlatformArgs = None 52 | ARFLAGS: PlatformArgs = None 53 | 54 | PATH: PlatformArgs = None 55 | 56 | custom: DictT[str, PlatformArgs] = field(default_factory=dict) 57 | env: dict = field(default_factory=environ.copy) 58 | 59 | __known__: ClassVar[DictT[str, EnvFlag]] = {e.env: e for e in __flags__} 60 | 61 | def __post_init__(self): 62 | for flag in __flags__: 63 | self.merge_to_env(flag, self.get_from_self) 64 | for flag in self.custom.values(): 65 | self.merge_to_env(flag, self.get_from_custom) 66 | 67 | def merge_to_env(self, flag: EnvFlag, get: CallableT[[str], EnvFlag]): 68 | var = environ.get(flag.env) 69 | override: EnvFlag = get(flag.env) 70 | if override and flag.merges: 71 | add = var + flag.sep if var else "" 72 | self.env[flag.env] = add + override.arg 73 | elif override: 74 | self.env[flag.env] = override.arg 75 | 76 | def get_from_self(self, attr): 77 | return getattr(self, attr) 78 | 79 | def get_from_custom(self, attr): 80 | return self.custom.get(attr) 81 | 82 | def masked_environ(self) -> dict: 83 | out = {} 84 | for k, v in self.env.items(): 85 | if k not in self.__known__: 86 | out[k] = "*" * len(v) 87 | else: 88 | out[k] = v 89 | return out 90 | 91 | 92 | def parse_env_args( 93 | kwargs: dict, 94 | ) -> EnvFlags: 95 | try: 96 | args: list = kwargs.pop("env") 97 | for i, arg in enumerate(args): 98 | parse_to_plat(EnvFlag, arg, args, i, require_argform=True) 99 | except KeyError: 100 | args = [] 101 | kw = {"custom": {}} 102 | for arg in args: 103 | arg: EnvFlag 104 | if arg.applies(): 105 | if arg.env in EnvFlags.__known__: 106 | kw[arg.env] = arg 107 | else: 108 | kw["custom"][arg.env] = arg 109 | envflags = EnvFlags(**kw) 110 | return envflags 111 | -------------------------------------------------------------------------------- /src/hatch_cython/config/includes.py: -------------------------------------------------------------------------------- 1 | from hatch_cython.config.autoimport import Autoimport, __packages__ 2 | from hatch_cython.constants import INCLUDE 3 | 4 | 5 | def parse_includes(kw: str, val: str): 6 | alias = kw.replace(INCLUDE, "") 7 | import_p = __packages__.get(alias) 8 | if import_p is None: 9 | if isinstance(val, str): 10 | import_p = Autoimport(pkg=alias, include=val) 11 | elif isinstance(val, dict): 12 | if "pkg" not in val: 13 | val["pkg"] = alias 14 | import_p = Autoimport(**val) 15 | else: 16 | msg = " ".join( 17 | ( 18 | "%s (%s) is invalid, either provide a known package or", 19 | "a path in the format of module.get_xxx where get_xxx is", 20 | "the directory to be included", 21 | ) 22 | ).format(val, type(val)) 23 | raise ValueError(msg) 24 | return import_p 25 | -------------------------------------------------------------------------------- /src/hatch_cython/config/macros.py: -------------------------------------------------------------------------------- 1 | from hatch_cython.types import ListT, TupleT, UnionT 2 | 3 | DefineMacros = ListT[TupleT[str, UnionT[str, None]]] 4 | 5 | 6 | def parse_macros(define: ListT[ListT[str]]) -> DefineMacros: 7 | """Parses define_macros from list[list[str, ...]] -> list[tuple[str, str|None]] 8 | 9 | Args: 10 | define (ListT[ListT[str]]): list of listed strings of len 1 or 2. raises error if len > 2 11 | 12 | Raises: 13 | ValueError: length > 2 or types are not valid 14 | 15 | Returns: 16 | DefineMacros: list[tuple[str,str|None]] 17 | """ 18 | for i, inst in enumerate(define): 19 | size = len(inst) 20 | if not (isinstance(inst, list) and size in (1, 2) and all(isinstance(v, str) or v is None for v in inst)): 21 | msg = "".join( 22 | f"define_macros[{i}]: macros must be defined as [name, ], " 23 | "where None value denotes #define FOO" 24 | ) 25 | raise ValueError(msg, inst) 26 | inst: list 27 | if size == 1: 28 | define[i] = (inst[0], None) 29 | else: 30 | define[i] = (inst[0], inst[1]) 31 | return define 32 | -------------------------------------------------------------------------------- /src/hatch_cython/config/platform.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Hashable 2 | from dataclasses import dataclass 3 | from os import path 4 | 5 | from packaging.markers import Marker 6 | 7 | from hatch_cython.constants import ANON 8 | from hatch_cython.types import CallableT, ListStr, ListT, UnionT 9 | from hatch_cython.utils import aarch, plat 10 | 11 | 12 | @dataclass 13 | class PlatformBase(Hashable): 14 | platforms: UnionT[ListStr, str] = "*" 15 | arch: UnionT[ListStr, str] = "*" 16 | depends_path: bool = False 17 | marker: str = None 18 | apply_to_marker: CallableT[[], bool] = None 19 | 20 | def __post_init__(self): 21 | self.do_rewrite("platforms") 22 | self.do_rewrite("arch") 23 | 24 | def do_rewrite(self, attr: str): 25 | att = getattr(self, attr) 26 | if isinstance(att, list): 27 | setattr(self, attr, [p.lower() for p in att]) 28 | elif isinstance(att, str): 29 | setattr(self, attr, att.lower()) 30 | 31 | def check_marker(self): 32 | do = True 33 | if self.apply_to_marker: 34 | do = self.apply_to_marker() 35 | if do: 36 | marker = Marker(self.marker) 37 | return marker.evaluate() 38 | return False 39 | 40 | def _applies_impl(self, attr: str, defn: str): 41 | if self.marker: 42 | ok = self.check_marker() 43 | if not ok: 44 | return False 45 | 46 | att = getattr(self, attr) 47 | if isinstance(att, list): 48 | # https://docs.python.org/3/library/platform.html#platform.machine 49 | # "" is a possible value so we have to add conditions for anon 50 | _anon = ANON in att and defn == "" 51 | return defn in att or "*" in att or _anon 52 | _anon = ANON == att and defn == "" 53 | return (att in (defn, "*")) or _anon 54 | 55 | def applies(self, platform: UnionT[None, str] = None, arch: UnionT[None, str] = None): 56 | if platform is None: 57 | platform = plat() 58 | if arch is None: 59 | arch = aarch() 60 | 61 | _isplatform = self._applies_impl("platforms", platform) 62 | _isarch = self._applies_impl("arch", arch) 63 | return _isplatform and _isarch 64 | 65 | def is_exist(self, trim: int = 0): 66 | if self.depends_path: 67 | return path.exists(self.arg[trim:]) 68 | return True 69 | 70 | 71 | @dataclass 72 | class PlatformArgs(PlatformBase): 73 | arg: str = None 74 | 75 | def __hash__(self) -> int: 76 | return hash(self.arg) 77 | 78 | 79 | def parse_to_plat(cls, arg, args: UnionT[list, dict], key: UnionT[int, str], require_argform: bool, **kwargs): 80 | if isinstance(arg, dict): 81 | args[key] = cls(**arg, **kwargs) 82 | elif require_argform: 83 | msg = f"arg {key} is invalid. must be of type ({{ flag = ... , platform = '*' }}) given {arg} ({type(arg)})" 84 | raise ValueError(msg) 85 | 86 | 87 | def parse_platform_args( 88 | kwargs: dict, 89 | name: str, 90 | default: CallableT[[], ListT[PlatformArgs]], 91 | ) -> ListT[UnionT[str, PlatformArgs]]: 92 | try: 93 | args = [*default(), *kwargs.pop(name)] 94 | for i, arg in enumerate(args): 95 | parse_to_plat(PlatformArgs, arg, args, i, require_argform=False) 96 | except KeyError: 97 | args = default() 98 | return args 99 | 100 | 101 | ListedArgs = ListT[UnionT[PlatformArgs, str]] 102 | """ 103 | List[str | PlatformArgs] 104 | """ 105 | -------------------------------------------------------------------------------- /src/hatch_cython/config/templates.py: -------------------------------------------------------------------------------- 1 | import re 2 | from dataclasses import asdict, dataclass, field 3 | from textwrap import dedent 4 | 5 | from hatchling.builders.hooks.plugin.interface import BuildHookInterface 6 | 7 | from hatch_cython.config.platform import PlatformBase 8 | from hatch_cython.constants import NORM_GLOB 9 | from hatch_cython.types import ListStr, ListT, UnionT 10 | from hatch_cython.utils import parse_user_glob 11 | 12 | 13 | def idx_search_mod(s: str): 14 | if not s.startswith("*"): 15 | s = "*" + s 16 | # replace the ./ because we do so to the file 17 | return s.replace("./", "") 18 | 19 | 20 | @dataclass 21 | class IndexItem(PlatformBase): 22 | keyword: str = "*" 23 | matches: UnionT[str, ListStr] = field(default_factory=list) 24 | 25 | def __post_init__(self): 26 | matches = self.matches 27 | if isinstance(matches, str): 28 | matches = [matches] 29 | for i in range(len(matches)): 30 | matches[i] = parse_user_glob(matches[i], r"([^.]*)", idx_search_mod) 31 | self.matches = sorted(matches, key=lambda it: -1 if it == NORM_GLOB else 1) 32 | 33 | def file_match(self, file: str) -> bool: 34 | for patt in self.matches: 35 | # we take the local part out since we match on extensions 36 | if re.match(patt, file.replace("./", "")): 37 | return True 38 | return False 39 | 40 | 41 | class Templates: 42 | index: ListT[IndexItem] 43 | kwargs: dict 44 | 45 | def __init__(self, index: ListT[IndexItem] = None, **kwargs): 46 | if index is None: 47 | index = [] 48 | 49 | # reverse everything & put global first so that it is always overriden. 50 | # FIFO priority after that 51 | self.index = sorted(index[::-1], key=lambda it: -1 if it.keyword == "global" else 1) # noqa: C415 52 | for kw, arg in kwargs.items(): 53 | if not isinstance(arg, dict): 54 | msg = ( 55 | f"'{kw} = {arg}' ({type(arg)}) is invalid. " 56 | "keyword arguments must be defined as " 57 | "'keyword = { abc = 1, ... }' in your " 58 | "pyproject.toml / hatch.toml" 59 | ) 60 | raise ValueError(msg, arg) 61 | self.kwargs = kwargs 62 | 63 | def __repr__(self) -> str: 64 | return dedent(f"""Templates(index={self.index!r}, kwargs={self.kwargs!r})""") 65 | 66 | def asdict(self): 67 | return { 68 | "index": [asdict(i) for i in self.index], 69 | "kwargs": self.kwargs, 70 | } 71 | 72 | def find(self, cls: BuildHookInterface, *files: str): 73 | kwds = {} 74 | 75 | for can in self.index: 76 | for file in files: 77 | if can.file_match(file) and can.applies(): 78 | add = self.kwargs.get(can.keyword) 79 | if add is None: 80 | msg = ( 81 | f"'{can.keyword}' is defined but returns no " 82 | "kwargs. To define kwargs, put " 83 | f"'{can.keyword} = {{ abc = 1, ... }}' in your" 84 | "pyproject.toml / hatch.toml" 85 | ) 86 | cls.app.display_warning(msg) 87 | else: 88 | kwds = {**kwds, **add} 89 | # raise ValueError(kwds, files, self.index) 90 | return kwds 91 | 92 | def __eq__(self, other: object) -> bool: 93 | if not isinstance(other, Templates): 94 | return False 95 | return other.index == self.index and other.kwargs == self.kwargs 96 | 97 | 98 | def parse_template_kwds(clsvars: dict): 99 | idx = [IndexItem(**kw) for kw in clsvars.pop("index", [])] 100 | return Templates(index=idx, **clsvars) 101 | -------------------------------------------------------------------------------- /src/hatch_cython/constants.py: -------------------------------------------------------------------------------- 1 | from hatch_cython.types import CorePlatforms, ListT, Set 2 | 3 | NORM_GLOB = r"([^\s]*)" 4 | UAST = "${U_AST}" 5 | EXIST_TRIM = 2 6 | ANON = "anon" 7 | INCLUDE = "include_" 8 | OPTIMIZE = "-O2" 9 | DIRECTIVES = { 10 | "binding": True, 11 | "language_level": 3, 12 | } 13 | LTPY311 = "python_version < '3.11'" 14 | MUST_UNIQUE = ["-O", "-arch", "-march"] 15 | POSIX_CORE: ListT[CorePlatforms] = ["darwin", "linux"] 16 | 17 | precompiled_extensions: Set[str] = { 18 | # py is left out as we have it optional / runtime value 19 | ".pyx", 20 | ".pxd", 21 | } 22 | intermediate_extensions: Set[str] = { 23 | ".c", 24 | ".cpp", 25 | } 26 | templated_extensions: Set[str] = {f"{f}.in" for f in {".py", ".pyi", *precompiled_extensions, *intermediate_extensions}} 27 | compiled_extensions: Set[str] = { 28 | ".dll", 29 | # unix 30 | ".so", 31 | # windows 32 | ".pyd", 33 | } 34 | -------------------------------------------------------------------------------- /src/hatch_cython/devel.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023-present joshua-auchincloss 2 | # 3 | # SPDX-License-Identifier: MIT 4 | import os 5 | import sys 6 | 7 | src = os.sep.join(__file__.split(os.sep)[:-2]) 8 | sys.path.append(src) 9 | 10 | from hatch_cython import CythonBuildHook # noqa: E402, F401 11 | -------------------------------------------------------------------------------- /src/hatch_cython/hooks.py: -------------------------------------------------------------------------------- 1 | from hatchling.plugin import hookimpl 2 | 3 | from hatch_cython.plugin import CythonBuildHook 4 | 5 | 6 | @hookimpl 7 | def hatch_register_build_hook(): 8 | return CythonBuildHook 9 | -------------------------------------------------------------------------------- /src/hatch_cython/plugin.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import subprocess 4 | import sys 5 | from contextlib import contextmanager 6 | from glob import glob 7 | from tempfile import TemporaryDirectory 8 | 9 | from Cython.Tempita import sub as render_template 10 | from hatchling.builders.hooks.plugin.interface import BuildHookInterface 11 | 12 | from hatch_cython.config import parse_from_dict 13 | from hatch_cython.constants import ( 14 | compiled_extensions, 15 | intermediate_extensions, 16 | precompiled_extensions, 17 | templated_extensions, 18 | ) 19 | from hatch_cython.temp import ExtensionArg, setup_py 20 | from hatch_cython.types import CallableT, DictT, ListStr, ListT, P, Set 21 | from hatch_cython.utils import autogenerated, memo, parse_user_glob, plat 22 | 23 | 24 | class CythonBuildHook(BuildHookInterface): 25 | PLUGIN_NAME = "cython" 26 | 27 | precompiled_extensions: Set[str] 28 | intermediate_extensions: Set[str] 29 | templated_extensions: Set[str] 30 | compiled_extensions: Set[str] 31 | 32 | def __init__(self, *args: P.args, **kwargs: P.kwargs): 33 | self.precompiled_extensions = precompiled_extensions.copy() 34 | self.intermediate_extensions = intermediate_extensions.copy() 35 | self.templated_extensions = templated_extensions.copy() 36 | self.compiled_extensions = compiled_extensions.copy() 37 | 38 | super().__init__(*args, **kwargs) 39 | 40 | _ = self.options 41 | 42 | @property 43 | @memo 44 | def is_src(self): 45 | return os.path.exists(os.path.join(self.root, "src")) 46 | 47 | @property 48 | def is_windows(self): 49 | return plat() == "windows" 50 | 51 | def normalize_path(self, pattern: str): 52 | if self.is_windows: 53 | return pattern.replace("/", "\\") 54 | return pattern.replace("\\", "/") 55 | 56 | def normalize_glob(self, pattern: str): 57 | return pattern.replace("\\", "/") 58 | 59 | @property 60 | @memo 61 | def dir_name(self): 62 | return self.options.src if self.options.src is not None else self.metadata.name 63 | 64 | @property 65 | @memo 66 | def project_dir(self): 67 | if self.is_src: 68 | src = f"./src/{self.dir_name}" 69 | else: 70 | src = f"./{self.dir_name}" 71 | return src 72 | 73 | def render_templates(self): 74 | for template in self.templated_globs: 75 | outfile = template[:-3] 76 | with open(template, encoding="utf-8") as f: 77 | tmpl = f.read() 78 | 79 | kwds = self.options.templates.find(self, outfile, template) 80 | data = render_template(tmpl, **kwds) 81 | with open(outfile, "w", encoding="utf-8") as f: 82 | f.write(autogenerated(kwds) + "\n\n" + data) 83 | 84 | @property 85 | @memo 86 | def precompiled_globs(self): 87 | _globs = [] 88 | for ex in self.precompiled_extensions: 89 | _globs.extend((f"{self.project_dir}/*{ex}", f"{self.project_dir}/**/*{ex}")) 90 | return list(set(_globs)) 91 | 92 | @property 93 | @memo 94 | def options_exclude(self): 95 | return [parse_user_glob(e.matches) for e in self.options.files.exclude if e.applies()] 96 | 97 | @property 98 | @memo 99 | def options_include(self): 100 | return [parse_user_glob(e.matches) for e in self.options.files.targets if e.applies()] 101 | 102 | def wanted(self, item: str): 103 | not_excluded = not any(re.match(e, self.normalize_glob(item), re.IGNORECASE) for e in self.options_exclude) 104 | if self.options.files.explicit_targets: 105 | return not_excluded and any(re.match(opt, self.normalize_glob(item)) for opt in self.options_include) 106 | return not_excluded 107 | 108 | def filter_ensure_wanted(self, tgts: ListStr): 109 | return list( 110 | filter( 111 | self.wanted, 112 | tgts, 113 | ) 114 | ) 115 | 116 | @property 117 | def included_files(self): 118 | included = set() 119 | self.app.display_debug("user globs") 120 | for patt in self.precompiled_globs: 121 | globbed = glob(patt, recursive=True) 122 | self.app.display_info(f"{patt} globbed {globbed!r}") 123 | if len(globbed) == 0: 124 | continue 125 | matched = self.filter_ensure_wanted(globbed) 126 | included = included.union(matched) 127 | return list(included) 128 | 129 | @property 130 | def normalized_included_files(self): 131 | """ 132 | Produces files in posix format 133 | """ 134 | return list({self.normalize_glob(f) for f in self.included_files}) 135 | 136 | def normalize_aliased_filelike(self, path: str): 137 | # sometimes we end up with a case where non src produces 138 | # '..example_lib._alias' 139 | while ".." in path: 140 | path = path.replace("..", "") 141 | return path 142 | 143 | @property 144 | def grouped_included_files(self) -> ListT[ExtensionArg]: 145 | grouped: DictT[str, set] = {} 146 | for norm in self.normalized_included_files: 147 | root, ext = os.path.splitext(norm) 148 | ok = True 149 | if ext == ".pxd": 150 | pyfile = norm.replace(".pxd", ".py") 151 | if os.path.exists(pyfile): 152 | norm = pyfile # noqa: PLW2901 153 | else: 154 | ok = False 155 | self.app.display_warning(f"attempted to use .pxd file without .py file ({norm})") 156 | if self.is_src: 157 | root = root.replace("./src/", "") 158 | root = self.normalize_aliased_filelike(root.replace("/", ".")) 159 | alias = self.options.files.matches_alias(root) 160 | self.app.display_debug(f"check alias {ok} {root} -> {norm} -> {alias}") 161 | if alias: 162 | root = alias 163 | self.app.display_debug(f"aliased {root} -> {norm}") 164 | if grouped.get(root) and ok: 165 | grouped[root].add(norm) 166 | elif ok: 167 | grouped[root] = {norm} 168 | return [ExtensionArg(name=key, files=list(files)) for key, files in grouped.items()] 169 | 170 | @property 171 | @memo 172 | def artifact_globs(self): 173 | artifact_globs = [] 174 | for included_file in self.normalized_included_files: 175 | root, _ = os.path.splitext(included_file) 176 | artifact_globs.extend(f"{root}.*{ext}" for ext in self.precompiled_extensions) 177 | return artifact_globs 178 | 179 | @property 180 | @memo 181 | def templated_globs(self): 182 | return self._globs(self.templated_extensions) 183 | 184 | @property 185 | @memo 186 | def normalized_dist_globs(self): 187 | return list(map(self.normalize_glob, self.artifact_globs)) 188 | 189 | def artifact_patterns(self, source: ListStr): 190 | return [f"/{artifact_glob}" for artifact_glob in source] 191 | 192 | @property 193 | def artifacts(self): 194 | return ( 195 | self.artifact_patterns(self.normalized_dist_globs) 196 | if not self.sdist 197 | else self.artifact_patterns(self.intermediate) 198 | ) 199 | 200 | @contextmanager 201 | def get_build_dirs(self): 202 | with TemporaryDirectory() as temp_dir: 203 | yield os.path.realpath(temp_dir) 204 | 205 | def _globs(self, exts: ListStr, normalize: CallableT[[str], str] = None): 206 | if normalize is None: 207 | normalize = self.normalize_glob 208 | globs = [ 209 | *(f"{self.project_dir}/**/*{ext}" for ext in exts), 210 | *(f"{self.project_dir}/*{ext}" for ext in exts), 211 | ] 212 | globbed = [] 213 | for g in globs: 214 | globbed += list(map(normalize, glob(g, recursive=True))) 215 | return list(filter(self.wanted, set(globbed))) 216 | 217 | @property 218 | def precompiled(self): 219 | return self._globs(self.precompiled_extensions, self.normalize_glob) 220 | 221 | @property 222 | def intermediate(self): 223 | return self._globs(self.intermediate_extensions, self.normalize_path) 224 | 225 | @property 226 | def compiled(self): 227 | return self._globs(self.compiled_extensions, self.normalize_path) 228 | 229 | @property 230 | def autogenerated(self): 231 | return list(filter(os.path.exists, (s.replace(".in", "") for s in self.templated_globs))) 232 | 233 | @property 234 | def inclusion_map(self): 235 | include = {} 236 | for compl in self.compiled: 237 | include[compl] = compl 238 | self.app.display_debug("Derived inclusion map") 239 | self.app.display_debug(include) 240 | return include 241 | 242 | def rm_recurse(self, li: ListStr): 243 | self.app.display_debug("Removing by match") 244 | self.app.display_debug(li) 245 | for f in li: 246 | os.remove(f) 247 | 248 | def clean(self, _: ListStr): 249 | self.rm_recurse(self.autogenerated) 250 | self.rm_recurse(self.intermediate) 251 | self.rm_recurse(self.compiled) 252 | 253 | @property 254 | @memo 255 | def options(self): 256 | config = parse_from_dict(self) 257 | if config.compile_py: 258 | self.precompiled_extensions.add(".py") 259 | if config.files.explicit_targets: 260 | self.precompiled_extensions.add(".py") 261 | self.precompiled_extensions.add(".c") 262 | self.precompiled_extensions.add(".cc") 263 | self.precompiled_extensions.add(".cpp") 264 | return config 265 | 266 | @property 267 | def sdist(self): 268 | return self.target_name == "sdist" 269 | 270 | @property 271 | def wheel(self): 272 | return self.target_name == "wheel" 273 | 274 | def build_ext(self): 275 | with self.get_build_dirs() as temp: 276 | self.render_templates() 277 | 278 | shared_temp_build_dir = os.path.join(temp, "build") 279 | temp_build_dir = os.path.join(temp, "tmp") 280 | 281 | os.mkdir(shared_temp_build_dir) 282 | os.mkdir(temp_build_dir) 283 | 284 | self.app.display_info("Building c/c++ extensions...") 285 | self.app.display_info(self.normalized_included_files) 286 | setup_file = os.path.join(temp, "setup.py") 287 | with open(setup_file, "w") as f: 288 | setup = setup_py( 289 | *self.grouped_included_files, 290 | options=self.options, 291 | sdist=self.sdist, 292 | ) 293 | self.app.display_debug(setup) 294 | f.write(setup) 295 | 296 | self.options.validate_include_opts() 297 | 298 | process = subprocess.run( # noqa: PLW1510 299 | [ # noqa: S603 300 | sys.executable, 301 | setup_file, 302 | "build_ext", 303 | "--inplace", 304 | "--verbose", 305 | "--build-lib", 306 | shared_temp_build_dir, 307 | "--build-temp", 308 | temp_build_dir, 309 | ], 310 | stdout=subprocess.PIPE, 311 | stderr=subprocess.STDOUT, 312 | env=self.options.envflags.env, 313 | ) 314 | stdout = process.stdout.decode("utf-8") 315 | if process.returncode: 316 | self.app.display_error(f"cythonize exited non null status {process.returncode}") 317 | self.app.display_error(stdout) 318 | msg = "failed compilation" 319 | raise Exception(msg) 320 | else: 321 | self.app.display_info(stdout) 322 | 323 | self.app.display_success("Post-build artifacts") 324 | 325 | def initialize(self, _: str, build_data: dict): 326 | self.app.display_mini_header(self.PLUGIN_NAME) 327 | self.app.display_debug("options") 328 | self.app.display_debug(self.options.asdict(), level=1) 329 | self.app.display_debug("sdist") 330 | self.app.display_debug(self.sdist, level=1) 331 | self.app.display_waiting("pre-build artifacts") 332 | 333 | if len(self.grouped_included_files) != 0: 334 | self.build_ext() 335 | self.app.display_info(glob(f"{self.project_dir}/*/**", recursive=True)) 336 | 337 | if self.sdist and not self.options.compiled_sdist: 338 | self.clean(None) 339 | 340 | build_data["infer_tag"] = True 341 | build_data["artifacts"].extend(self.artifacts) 342 | build_data["force_include"].update(self.inclusion_map) 343 | build_data["pure_python"] = False 344 | 345 | self.app.display_info("Extensions complete") 346 | self.app.display_debug(build_data) 347 | -------------------------------------------------------------------------------- /src/hatch_cython/temp.py: -------------------------------------------------------------------------------- 1 | from typing import TypedDict 2 | 3 | from hatch_cython.config import Config 4 | from hatch_cython.types import ListStr, ListT 5 | from hatch_cython.utils import options_kws 6 | 7 | 8 | class ExtensionArg(TypedDict): 9 | name: str 10 | files: ListStr 11 | 12 | 13 | def setup_py( 14 | *files: ListT[ListStr], 15 | options: Config, 16 | sdist: bool, 17 | ): 18 | code = """ 19 | from setuptools import Extension, setup 20 | from Cython.Build import cythonize 21 | 22 | INCLUDES = {includes!r} 23 | EXTENSIONS = {ext_files!r} 24 | 25 | if __name__ == "__main__": 26 | exts = [ 27 | Extension( ex.get("name"), 28 | ex.get("files"), 29 | extra_compile_args={compile_args!r}, 30 | extra_link_args={extra_link_args!r}, 31 | include_dirs=INCLUDES, 32 | libraries={libs!r}, 33 | library_dirs={lib_dirs!r}, 34 | define_macros={define_macros!r}, 35 | {keywords} 36 | ) for ex in EXTENSIONS 37 | ] 38 | ext_modules = cythonize( 39 | exts, 40 | compiler_directives={directives!r}, 41 | include_path=INCLUDES, 42 | {cython} 43 | ) 44 | 45 | """ 46 | if not sdist: 47 | code += """ 48 | setup(ext_modules=ext_modules) 49 | """ 50 | 51 | kwds = options_kws(options.compile_kwargs) 52 | cython = options_kws(options.cythonize_kwargs) 53 | return code.format( 54 | compile_args=options.compile_args_for_platform, 55 | extra_link_args=options.compile_links_for_platform, 56 | directives=options.directives, 57 | ext_files=files, 58 | keywords=kwds, 59 | cython=cython, 60 | includes=options.includes, 61 | libs=options.libraries, 62 | lib_dirs=options.library_dirs, 63 | define_macros=options.define_macros, 64 | ).strip() 65 | -------------------------------------------------------------------------------- /src/hatch_cython/types.py: -------------------------------------------------------------------------------- 1 | from sys import version_info 2 | from typing import Literal, TypeVar, Union 3 | 4 | T = TypeVar("T") 5 | 6 | vmaj = (version_info[0], version_info[1]) 7 | if vmaj >= (3, 10): 8 | from collections.abc import Callable 9 | from typing import ParamSpec 10 | 11 | TupleT = tuple 12 | DictT = dict 13 | ListT = list 14 | Set = set 15 | else: 16 | from typing import Callable, Dict, List, Set, Tuple # noqa: UP035, F401 17 | 18 | from typing_extensions import ParamSpec 19 | 20 | TupleT = Tuple # noqa: UP006 21 | DictT = Dict # noqa: UP006 22 | ListT = List # noqa: UP006 23 | 24 | P = ParamSpec("P") 25 | ListStr = ListT[str] 26 | UnionT = Union 27 | CorePlatforms = Literal[ 28 | "darwin", 29 | "linux", 30 | "windows", 31 | ] 32 | CallableT = Callable 33 | -------------------------------------------------------------------------------- /src/hatch_cython/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | from textwrap import dedent 4 | 5 | from Cython import __version__ as __cythonversion__ 6 | 7 | from hatch_cython.__about__ import __version__ 8 | from hatch_cython.constants import NORM_GLOB, UAST 9 | from hatch_cython.types import CallableT, P, T, UnionT 10 | 11 | 12 | def stale(src: str, dest: str): 13 | if not os.path.exists(src) or not os.path.exists(dest): 14 | return True 15 | return (os.path.getmtime(src) >= os.path.getmtime(dest)) or (os.path.getctime(src) >= os.path.getctime(dest)) 16 | 17 | 18 | def memo(func: CallableT[P, T]) -> CallableT[P, T]: 19 | keyed = {} 20 | 21 | def wrapped(*args: P.args, **kwargs: P.kwargs) -> T: 22 | nonlocal keyed 23 | 24 | # if we have a class, memo will reserve objects between 25 | # instances, so we need to key this by the id of the instance 26 | # note: dont call hasattr here because hasattr is pretty much 27 | # a try catch for property access - ergo we get infinite recursive 28 | # calls if a property is memoed 29 | if len(args) and func.__name__ in dir(args[0]): 30 | idof = id(args[0]) 31 | else: 32 | idof = None 33 | 34 | if idof not in keyed: 35 | keyed[idof] = func(*args, **kwargs) 36 | return keyed[idof] 37 | 38 | return wrapped 39 | 40 | 41 | @memo 42 | def plat(): 43 | return platform.system().lower() 44 | 45 | 46 | @memo 47 | def aarch(): 48 | return platform.machine().lower() 49 | 50 | 51 | def options_kws(kwds: dict): 52 | return ",\n\t".join((f"{k}={v!r}" for k, v in kwds.items())) 53 | 54 | 55 | def parse_user_glob( 56 | uglob: str, 57 | variant: UnionT[None, str] = None, 58 | modifier: UnionT[CallableT[[str], str], None] = None, 59 | ): 60 | if variant is None: 61 | variant = NORM_GLOB 62 | pre = uglob.replace("\\*", UAST) 63 | if modifier: 64 | pre = modifier(pre) 65 | imd = pre.replace("*", variant) 66 | return imd.replace(UAST, "*") 67 | 68 | 69 | def autogenerated(keywords: dict): 70 | return dedent( 71 | f"""# DO NOT EDIT. 72 | # Autoformatted by hatch-cython. 73 | # Version: {__version__} 74 | # Cython: {__cythonversion__} 75 | # Platform: {plat()} 76 | # Architecture: {aarch()} 77 | # Keywords: {keywords!r} 78 | """ 79 | ) 80 | -------------------------------------------------------------------------------- /taskfile.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | tasks: 4 | std-test: 5 | dir: '{{ .DIR }}' 6 | env: 7 | HATCH_CYTHON_DEVEL: 1 8 | cmds: 9 | - hatch --verbose build --clean 10 | - if [ -d "/Users/runner" ]; then hatch env remove; fi 11 | - hatch run test 12 | 13 | example: 14 | dir: test_libraries/src_structure 15 | cmds: 16 | - task: std-test 17 | vars: 18 | DIR: test_libraries/src_structure 19 | - if [ "$(cat ./caller_outputs.json | jaq .call)" -ne "true" ]; then exit 1; fi 20 | - if [ "$(cat ./caller_outputs.json | jaq .includes)" -ne "true" ]; then exit 1; fi 21 | 22 | only-included: 23 | dir: test_libraries/only_included 24 | cmds: 25 | - task: std-test 26 | vars: 27 | DIR: test_libraries/only_included 28 | 29 | simple-structure: 30 | dir: test_libraries/simple_structure 31 | cmds: 32 | - rm -rf ./example_lib/* 33 | - rm -rf ./tests/* 34 | - cp -r ../src_structure/src/example_lib/* ./example_lib/ 35 | - cp -r ../src_structure/tests/* ./tests/ 36 | - rm ./tests/test_custom_includes.py 37 | - rm ./example_lib/custom_includes.* 38 | - task: std-test 39 | vars: 40 | DIR: test_libraries/simple_structure 41 | - rm -rf ./example_lib/* 42 | - rm -rf ./tests/* 43 | - echo "" > ./example_lib/.gitkeep 44 | - echo "" > ./tests/.gitkeep 45 | 46 | clean: 47 | cmds: 48 | - rm -rf dist 49 | - rm -rf **/dist 50 | - rm -rf .coverag* 51 | - rm -rf __pycache__ 52 | - rm -rf **/__pycache__ 53 | - rm -rf ./test_libraries/src_structure/src/**/**/*.c 54 | - rm -rf ./test_libraries/src_structure/src/**/**/*.so 55 | - rm -rf ./test_libraries/src_structure/src/**/**/*.cpp 56 | - rm -rf ./test_libraries/src_structure/src/**/**/*.html 57 | - rm -rf ./**/*.so 58 | - rm -rf ./**/*.html 59 | - rm -rf .ruff_cache 60 | - rm -rf **/.ruff_cache 61 | - rm -rf .pytest_cache 62 | - rm -rf **/.pytest_cache 63 | 64 | lint: 65 | cmds: 66 | - black . 67 | - isort . 68 | - ruff src --fix 69 | - ruff . --fix 70 | 71 | precommit: 72 | cmds: 73 | - task lint 74 | - pre-commit run --all-files 75 | 76 | missed: 77 | cmds: 78 | - python3 -m http.server -d ./htmlcov 79 | -------------------------------------------------------------------------------- /test_libraries/bootstrap.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import sys 3 | from glob import glob 4 | from logging import DEBUG, StreamHandler, getLogger 5 | 6 | logger = getLogger(__file__) 7 | logger.setLevel(DEBUG) 8 | logger.addHandler(StreamHandler(sys.stdout)) 9 | 10 | if __name__ == "__main__": 11 | logger.info(sys.executable) 12 | logger.info(sys.version_info) 13 | ext = "whl" if (len(sys.argv) == 1) else sys.argv[1] 14 | artifact = glob(f"dist/example*.{ext}")[0] 15 | proc = subprocess.run( # noqa: PLW1510 16 | [sys.executable, "-m", "pip", "install", artifact, "--force-reinstall"], # noqa: S603 17 | stdout=subprocess.PIPE, 18 | stderr=subprocess.STDOUT, 19 | ) 20 | if proc.returncode: 21 | logger.error(proc.stdout.decode()) 22 | raise SystemExit(1) 23 | -------------------------------------------------------------------------------- /test_libraries/only_included/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-present joshua-auchincloss 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /test_libraries/only_included/hatch.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | ignore-vcs = true 3 | 4 | [build.hooks.custom] 5 | dependencies = [ 6 | "Cython", 7 | "setuptools", 8 | "llvmlite", 9 | "hatch", 10 | "typing_extensions; python_version < '3.10'" 11 | ] 12 | path = "../../src/hatch_cython/devel.py" 13 | 14 | [build.hooks.custom.options] 15 | src = "example_only_included" 16 | 17 | [build.hooks.custom.options.files] 18 | aliases = {"example_only_included.compile" = "example_only_included.did"} 19 | targets = ["*/compile.py"] 20 | 21 | [build.targets.wheel] 22 | macos-max-compat = false 23 | packages = ["example_only_included"] 24 | 25 | [[envs.all.matrix]] 26 | python = ["3.8", "3.9", "3.10", "3.11", "3.12"] 27 | 28 | [envs.default] 29 | dependencies = ["coverage", "pytest", "beartype", "numpy"] 30 | 31 | [envs.default.scripts] 32 | cov = ["install", "test-cov", "cov-report"] 33 | cov-report = ["- coverage combine", "coverage report"] 34 | install = "python ../bootstrap.py" 35 | test = ["install", "pytest {args:tests}"] 36 | test-cov = "coverage run -m pytest {args:tests}" 37 | 38 | [envs.lint] 39 | dependencies = ["black>=23.1.0", "mypy>=1.0.0", "ruff>=0.0.243"] 40 | detached = true 41 | 42 | [envs.lint.scripts] 43 | all = ["style", "typing"] 44 | fmt = ["black {args:.}", "ruff --fix {args:.}", "style"] 45 | style = ["ruff {args:.}", "black --check --diff {args:.}"] 46 | -------------------------------------------------------------------------------- /test_libraries/only_included/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "hatchling.build" 3 | requires = ["hatchling"] 4 | 5 | [project] 6 | authors = [ 7 | {name = "joshua-auchincloss", email = "joshua.auchincloss@proton.me"} 8 | ] 9 | classifiers = [ 10 | "Development Status :: 4 - Beta", 11 | "Programming Language :: Python", 12 | "Programming Language :: Python :: 3.8", 13 | "Programming Language :: Python :: 3.9", 14 | "Programming Language :: Python :: 3.10", 15 | "Programming Language :: Python :: 3.11", 16 | "Programming Language :: Python :: 3.12", 17 | "Programming Language :: Python :: Implementation :: CPython", 18 | "Programming Language :: Python :: Implementation :: PyPy" 19 | ] 20 | dependencies = [] 21 | description = '' 22 | dynamic = ["version"] 23 | keywords = [] 24 | license = {file = "LICENSE.txt"} 25 | name = "example-only-included" 26 | requires-python = ">=3.8" 27 | 28 | [project.urls] 29 | Documentation = "https://github.com/unknown/only-included#readme" 30 | Issues = "https://github.com/unknown/only-included/issues" 31 | Source = "https://github.com/unknown/only-included" 32 | 33 | [tool.coverage.paths] 34 | example_only_included = [ 35 | "src/example_only_included", 36 | "*/example-only-included/src/example_only_included" 37 | ] 38 | tests = ["tests", "*/example-only-included/tests"] 39 | 40 | [tool.coverage.report] 41 | exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"] 42 | 43 | [tool.coverage.run] 44 | branch = true 45 | omit = ["src/example_only_included/__about__.py"] 46 | parallel = true 47 | source_pkgs = ["example_only_included", "tests"] 48 | 49 | [[tool.hatch.envs.all.matrix]] 50 | python = ["3.8", "3.9", "3.10", "3.11", "3.12"] 51 | 52 | [tool.hatch.envs.default] 53 | dependencies = ["coverage[toml]>=6.5", "pytest"] 54 | 55 | [tool.hatch.envs.default.scripts] 56 | cov = ["test-cov", "cov-report"] 57 | cov-report = ["- coverage combine", "coverage report"] 58 | test = "pytest {args:tests}" 59 | test-cov = "coverage run -m pytest {args:tests}" 60 | 61 | [tool.hatch.envs.types] 62 | dependencies = ["mypy>=1.0.0"] 63 | 64 | [tool.hatch.envs.types.scripts] 65 | check = "mypy --install-types --non-interactive {args:src/example_only_included tests}" 66 | 67 | [tool.hatch.version] 68 | path = "src/example_only_included/__about__.py" 69 | 70 | [tool.ruff] 71 | line-length = 120 72 | target-version = "py37" 73 | 74 | [tool.ruff.lint] 75 | ignore = [ 76 | # Allow non-abstract empty methods in abstract base classes 77 | "B027", 78 | # Allow boolean positional values in function calls, like `dict.get(... True)` 79 | "FBT003", 80 | # Ignore checks for possible passwords 81 | "S105", 82 | "S106", 83 | "S107", 84 | # Ignore complexity 85 | "C901", 86 | "PLR0911", 87 | "PLR0912", 88 | "PLR0913", 89 | "PLR0915" 90 | ] 91 | select = [ 92 | "A", 93 | "ARG", 94 | "B", 95 | "C", 96 | "DTZ", 97 | "E", 98 | "EM", 99 | "F", 100 | "FBT", 101 | "I", 102 | "ICN", 103 | "ISC", 104 | "N", 105 | "PLC", 106 | "PLE", 107 | "PLR", 108 | "PLW", 109 | "Q", 110 | "RUF", 111 | "S", 112 | "T", 113 | "TID", 114 | "UP", 115 | "W", 116 | "YTT" 117 | ] 118 | unfixable = [] 119 | 120 | [tool.ruff.lint.flake8-tidy-imports] 121 | ban-relative-imports = "all" 122 | 123 | [tool.ruff.lint.isort] 124 | known-first-party = ["example", "example_lib"] 125 | 126 | [tool.ruff.lint.per-file-ignores] 127 | # Tests can use magic values, assertions, and relative imports 128 | "tests/**/*" = ["PLR2004", "S101", "TID252"] 129 | -------------------------------------------------------------------------------- /test_libraries/only_included/src/example_only_included/__about__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024-present joshua-auchincloss 2 | # 3 | # SPDX-License-Identifier: MIT 4 | __version__ = "0.0.1" 5 | -------------------------------------------------------------------------------- /test_libraries/only_included/src/example_only_included/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024-present joshua-auchincloss 2 | # 3 | # SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /test_libraries/only_included/src/example_only_included/compile.py: -------------------------------------------------------------------------------- 1 | def did_compile(): 2 | return "did" in __file__ 3 | -------------------------------------------------------------------------------- /test_libraries/only_included/src/example_only_included/did.pyi: -------------------------------------------------------------------------------- 1 | def did_compile() -> bool: ... 2 | -------------------------------------------------------------------------------- /test_libraries/only_included/src/example_only_included/dont_compile.py: -------------------------------------------------------------------------------- 1 | def did_compile(): 2 | return ".so" in __file__ or ".pyd" in __file__ or ".dll" in __file__ 3 | -------------------------------------------------------------------------------- /test_libraries/only_included/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024-present joshua-auchincloss 2 | # 3 | # SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /test_libraries/only_included/tests/test_compiled.py: -------------------------------------------------------------------------------- 1 | from src.example_only_included.did import did_compile 2 | 3 | 4 | def test_compilation(): 5 | assert did_compile() 6 | -------------------------------------------------------------------------------- /test_libraries/only_included/tests/test_didnt.py: -------------------------------------------------------------------------------- 1 | from src.example_only_included.dont_compile import did_compile 2 | 3 | 4 | def test_did_not_compile(): 5 | assert not did_compile() 6 | -------------------------------------------------------------------------------- /test_libraries/simple_structure/.gitignore: -------------------------------------------------------------------------------- 1 | example_lib/** 2 | tests/** 3 | !example_lib/.gitkeep 4 | !tests/.gitkeep 5 | caller_outputs.json 6 | -------------------------------------------------------------------------------- /test_libraries/simple_structure/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present joshua-auchincloss 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /test_libraries/simple_structure/example_lib/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshua-auchincloss/hatch-cython/17f8699a2d22db53e75024bf417501f178260adb/test_libraries/simple_structure/example_lib/.gitkeep -------------------------------------------------------------------------------- /test_libraries/simple_structure/hatch.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | ignore-vcs = true 3 | 4 | [build.hooks.custom] 5 | dependencies = [ 6 | "Cython", 7 | "setuptools", 8 | "numpy", 9 | "llvmlite", 10 | "hatch", 11 | "typing_extensions; python_version < '3.10'" 12 | ] 13 | path = "../../src/hatch_cython/devel.py" 14 | 15 | [build.hooks.custom.options] 16 | compile_args = [ 17 | {arg = "-v"}, 18 | {platforms = [ 19 | "linux", 20 | "darwin" 21 | ], arg = "-Wcpp"}, 22 | {platforms = [ 23 | "darwin" 24 | ], arch = "x86_64", arg = "-I/usr/local/opt/llvm/include", depends_path = true, marker = "python_version <= '3.10'"}, 25 | {platforms = [ 26 | "darwin" 27 | ], arch = "x86_64", arg = "-I/opt/homebrew/opt/llvm/include", depends_path = true, marker = "python_version <= '3.10'"} 28 | ] 29 | cythonize_kwargs = {annotate = true, nthreads = 4} 30 | define_macros = [["NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION"]] 31 | directives = {language_level = 3, boundscheck = false} 32 | env = [ 33 | {env = "CC", arg = "c++", platforms = [ 34 | "darwin", 35 | "linux" 36 | ]}, 37 | {env = "CPP", arg = "c++", platforms = [ 38 | "darwin", 39 | "linux" 40 | ]}, 41 | {env = "CXX", arg = "c++", platforms = [ 42 | "darwin", 43 | "linux" 44 | ]} 45 | ] 46 | extra_link_args = [ 47 | {platforms = [ 48 | "darwin" 49 | ], arch = "x86_64", arg = "-L/usr/local/opt/llvm/lib", depends_path = true, marker = "python_version <= '3.10'"}, 50 | {platforms = [ 51 | "darwin" 52 | ], arch = "x86_64", arg = "-L/opt/homebrew/opt/llvm/lib", depends_path = true, marker = "python_version <= '3.10'"} 53 | ] 54 | include_numpy = true 55 | parallel = true 56 | src = "example_lib" 57 | 58 | [build.hooks.custom.options.files] 59 | aliases = {"example_lib._alias" = "example_lib.aliased"} 60 | exclude = [ 61 | {matches = "*/no_compile/*"}, 62 | {matches = "*_sample*"}, 63 | {matches = "*/windows", platforms = [ 64 | "linux", 65 | "darwin", 66 | "freebsd" 67 | ]}, 68 | {matches = "*/darwin", platforms = [ 69 | "linux", 70 | "freebsd", 71 | "windows" 72 | ]}, 73 | {matches = "*/linux", platforms = [ 74 | "darwin", 75 | "freebsd", 76 | "windows" 77 | ]}, 78 | {matches = "*/freebsd", platforms = [ 79 | "linux", 80 | "darwin", 81 | "windows" 82 | ]} 83 | ] 84 | 85 | [build.hooks.custom.options.templates] 86 | global = {supported = ["int"]} 87 | index = [ 88 | {keyword = "global", matches = "*"}, 89 | {keyword = "templated_mac", matches = "templated.*.in", platforms = [ 90 | "darwin" 91 | ]}, 92 | {keyword = "templated_win", matches = "templated.*.in", platforms = [ 93 | "windows" 94 | ]} 95 | ] 96 | templated_mac = {supported = ["int", "float"]} 97 | templated_win = {supported = ["int", "float", "complex"]} 98 | 99 | [build.targets.wheel] 100 | macos-max-compat = false 101 | packages = ["example_lib"] 102 | 103 | [[envs.all.matrix]] 104 | python = ["3.8", "3.9", "3.10", "3.11", "3.12"] 105 | 106 | [envs.default] 107 | dependencies = ["coverage", "pytest", "beartype", "numpy"] 108 | 109 | [envs.default.scripts] 110 | cov = ["install", "test-cov", "cov-report"] 111 | cov-report = ["- coverage combine", "coverage report"] 112 | install = "python ../bootstrap.py" 113 | test = ["install", "pytest {args:tests}"] 114 | test-cov = "coverage run -m pytest {args:tests}" 115 | 116 | [envs.lint] 117 | dependencies = ["black>=23.1.0", "mypy>=1.0.0", "ruff>=0.0.243"] 118 | detached = true 119 | 120 | [envs.lint.scripts] 121 | all = ["style", "typing"] 122 | fmt = ["black {args:.}", "ruff --fix {args:.}", "style"] 123 | style = ["ruff {args:.}", "black --check --diff {args:.}"] 124 | -------------------------------------------------------------------------------- /test_libraries/simple_structure/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "hatchling.build" 3 | requires = ["hatchling"] 4 | 5 | [project] 6 | authors = [ 7 | {name = "joshua-auchincloss", email = "joshua.auchincloss@proton.me"} 8 | ] 9 | classifiers = [ 10 | "Development Status :: 4 - Beta", 11 | "Programming Language :: Python", 12 | "Programming Language :: Python :: 3.8", 13 | "Programming Language :: Python :: 3.9", 14 | "Programming Language :: Python :: 3.10", 15 | "Programming Language :: Python :: 3.11", 16 | "Programming Language :: Python :: Implementation :: CPython", 17 | "Programming Language :: Python :: Implementation :: PyPy" 18 | ] 19 | dependencies = ["pytest"] 20 | description = '' 21 | dynamic = ["version"] 22 | keywords = [] 23 | license = {file = "LICENSE.txt"} 24 | name = "example_lib" 25 | requires-python = ">=3.7" 26 | 27 | [project.urls] 28 | Documentation = "https://github.com/unknown/example#readme" 29 | Issues = "https://github.com/unknown/simple_structure/issues" 30 | Source = "https://github.com/unknown/example" 31 | 32 | [tool.black] 33 | line-length = 120 34 | skip-string-normalization = true 35 | target-version = ["py37"] 36 | 37 | [tool.coverage.paths] 38 | example = ["*/example_lib/*", "*/example_lib", "*/simple_structure/example_lib"] 39 | tests = ["tests", "*/simple_structure/tests"] 40 | 41 | [tool.coverage.report] 42 | exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"] 43 | 44 | [tool.coverage.run] 45 | branch = true 46 | omit = ["example_lib/__about__.py"] 47 | parallel = true 48 | source_pkgs = ["example_lib"] 49 | 50 | [tool.hatch.version] 51 | path = "example_lib/__about__.py" 52 | 53 | [tool.ruff] 54 | line-length = 120 55 | target-version = "py37" 56 | 57 | [tool.ruff.lint] 58 | ignore = [ 59 | # Allow non-abstract empty methods in abstract base classes 60 | "B027", 61 | # Allow boolean positional values in function calls, like `dict.get(... True)` 62 | "FBT003", 63 | # Ignore checks for possible passwords 64 | "S105", 65 | "S106", 66 | "S107", 67 | # Ignore complexity 68 | "C901", 69 | "PLR0911", 70 | "PLR0912", 71 | "PLR0913", 72 | "PLR0915" 73 | ] 74 | select = [ 75 | "A", 76 | "ARG", 77 | "B", 78 | "C", 79 | "DTZ", 80 | "E", 81 | "EM", 82 | "F", 83 | "FBT", 84 | "I", 85 | "ICN", 86 | "ISC", 87 | "N", 88 | "PLC", 89 | "PLE", 90 | "PLR", 91 | "PLW", 92 | "Q", 93 | "RUF", 94 | "S", 95 | "T", 96 | "TID", 97 | "UP", 98 | "W", 99 | "YTT" 100 | ] 101 | unfixable = [] 102 | 103 | [tool.ruff.lint.flake8-tidy-imports] 104 | ban-relative-imports = "all" 105 | 106 | [tool.ruff.lint.isort] 107 | known-first-party = ["example", "example_lib"] 108 | 109 | [tool.ruff.lint.per-file-ignores] 110 | # Tests can use magic values, assertions, and relative imports 111 | "tests/**/*" = ["PLR2004", "S101", "TID252"] 112 | -------------------------------------------------------------------------------- /test_libraries/simple_structure/tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshua-auchincloss/hatch-cython/17f8699a2d22db53e75024bf417501f178260adb/test_libraries/simple_structure/tests/.gitkeep -------------------------------------------------------------------------------- /test_libraries/src_structure/.gitignore: -------------------------------------------------------------------------------- 1 | templated.pyi 2 | templated.pyx 3 | caller_outputs.json 4 | -------------------------------------------------------------------------------- /test_libraries/src_structure/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present joshua-auchincloss 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /test_libraries/src_structure/hatch.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | ignore-vcs = true 3 | 4 | [build.hooks.custom] 5 | dependencies = [ 6 | "Cython", 7 | "setuptools", 8 | "numpy", 9 | "llvmlite", 10 | "hatch", 11 | "typing_extensions; python_version < '3.10'" 12 | ] 13 | path = "../../src/hatch_cython/devel.py" 14 | 15 | [build.hooks.custom.options] 16 | compile_args = [ 17 | {arg = "-v"}, 18 | {platforms = [ 19 | "linux", 20 | "darwin" 21 | ], arg = "-Wcpp"}, 22 | {platforms = [ 23 | "darwin" 24 | ], arch = "x86_64", arg = "-I/usr/local/opt/llvm/include", depends_path = true, marker = "python_version <= '3.10'"}, 25 | {platforms = [ 26 | "darwin" 27 | ], arch = "x86_64", arg = "-I/opt/homebrew/opt/llvm/include", depends_path = true, marker = "python_version <= '3.10'"} 28 | ] 29 | compiled_sdist = true 30 | cythonize_kwargs = {annotate = true, nthreads = 4} 31 | define_macros = [["NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION"]] 32 | directives = {language_level = 3, boundscheck = false} 33 | env = [ 34 | {env = "CC", arg = "c++", platforms = [ 35 | "darwin", 36 | "linux" 37 | ]}, 38 | {env = "CPP", arg = "c++", platforms = [ 39 | "darwin", 40 | "linux" 41 | ]}, 42 | {env = "CXX", arg = "c++", platforms = [ 43 | "darwin", 44 | "linux" 45 | ]} 46 | ] 47 | extra_link_args = [ 48 | {platforms = [ 49 | "darwin" 50 | ], arch = "x86_64", arg = "-L/usr/local/opt/llvm/lib", depends_path = true, marker = "python_version <= '3.10'"}, 51 | {platforms = [ 52 | "darwin" 53 | ], arch = "x86_64", arg = "-L/opt/homebrew/opt/llvm/lib", depends_path = true, marker = "python_version <= '3.10'"} 54 | ] 55 | include_my_lib_resolver = {pkg = "scripts.custom_include", include = "gets_includes", required_call = "must_call"} 56 | include_numpy = true 57 | parallel = true 58 | src = "example_lib" 59 | 60 | [build.hooks.custom.options.files] 61 | aliases = {"example_lib._alias" = "example_lib.aliased"} 62 | exclude = [ 63 | {matches = "*/no_compile/*"}, 64 | {matches = "*_sample*"}, 65 | {matches = "*/windows", platforms = [ 66 | "linux", 67 | "darwin", 68 | "freebsd" 69 | ]}, 70 | {matches = "*/darwin", platforms = [ 71 | "linux", 72 | "freebsd", 73 | "windows" 74 | ]}, 75 | {matches = "*/linux", platforms = [ 76 | "darwin", 77 | "freebsd", 78 | "windows" 79 | ]}, 80 | {matches = "*/freebsd", platforms = [ 81 | "linux", 82 | "darwin", 83 | "windows" 84 | ]} 85 | ] 86 | 87 | [build.hooks.custom.options.templates] 88 | global = {supported = ["int"]} 89 | index = [ 90 | {keyword = "global", matches = "*"}, 91 | {keyword = "templated_mac", matches = "templated.*.in", platforms = [ 92 | "darwin" 93 | ]}, 94 | {keyword = "templated_win", matches = "templated.*.in", platforms = [ 95 | "windows" 96 | ]} 97 | ] 98 | templated_mac = {supported = ["int", "float"]} 99 | templated_win = {supported = ["int", "float", "complex"]} 100 | 101 | [build.targets.custom.force-include] 102 | "include" = "include/" 103 | "scripts" = "scripts/" 104 | 105 | [build.targets.sdist] 106 | include = ["scripts/", "include/", "src/"] 107 | 108 | [build.targets.wheel] 109 | macos-max-compat = false 110 | packages = ["src/example_lib"] 111 | 112 | [build.targets.wheel.force-include] 113 | "include" = "include/" 114 | "scripts" = "scripts/" 115 | 116 | [[envs.all.matrix]] 117 | python = ["3.8", "3.9", "3.10", "3.11", "3.12"] 118 | 119 | [envs.default] 120 | dependencies = ["coverage", "pytest", "beartype", "numpy"] 121 | skip-install = true 122 | 123 | [envs.default.scripts] 124 | cov = ["install", "test-cov", "cov-report"] 125 | cov-report = ["- coverage combine", "coverage report"] 126 | install = "python ../bootstrap.py" 127 | test = ["install", "pytest {args:tests}"] 128 | test-cov = "coverage run -m pytest {args:tests}" 129 | 130 | [envs.lint] 131 | dependencies = ["black>=23.1.0", "mypy>=1.0.0", "ruff>=0.0.243"] 132 | detached = true 133 | 134 | [envs.lint.scripts] 135 | all = ["style", "typing"] 136 | fmt = ["black {args:.}", "ruff --fix {args:.}", "style"] 137 | style = ["ruff {args:.}", "black --check --diff {args:.}"] 138 | typing = "mypy --install-types --non-interactive {args:src/funcclasses tests}" 139 | -------------------------------------------------------------------------------- /test_libraries/src_structure/include/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshua-auchincloss/hatch-cython/17f8699a2d22db53e75024bf417501f178260adb/test_libraries/src_structure/include/.gitkeep -------------------------------------------------------------------------------- /test_libraries/src_structure/include/something.cc: -------------------------------------------------------------------------------- 1 | typedef long long int wide_int; 2 | 3 | namespace pyutil 4 | { 5 | wide_int bwf(wide_int *val) 6 | { 7 | auto inter = ((*val) << 2); 8 | return inter + (*val); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test_libraries/src_structure/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "hatchling.build" 3 | requires = ["hatchling"] 4 | 5 | [project] 6 | authors = [ 7 | {name = "joshua-auchincloss", email = "joshua.auchincloss@proton.me"} 8 | ] 9 | classifiers = [ 10 | "Development Status :: 4 - Beta", 11 | "Programming Language :: Python", 12 | "Programming Language :: Python :: 3.8", 13 | "Programming Language :: Python :: 3.9", 14 | "Programming Language :: Python :: 3.10", 15 | "Programming Language :: Python :: 3.11", 16 | "Programming Language :: Python :: Implementation :: CPython", 17 | "Programming Language :: Python :: Implementation :: PyPy" 18 | ] 19 | dependencies = ["pytest"] 20 | description = '' 21 | dynamic = ["version"] 22 | keywords = [] 23 | license = {file = "LICENSE.txt"} 24 | name = "example_lib" 25 | requires-python = ">=3.7" 26 | 27 | [project.urls] 28 | Documentation = "https://github.com/unknown/example#readme" 29 | Issues = "https://github.com/unknown/example/issues" 30 | Source = "https://github.com/unknown/example" 31 | 32 | [tool.black] 33 | line-length = 120 34 | skip-string-normalization = true 35 | target-version = ["py37"] 36 | 37 | [tool.coverage.paths] 38 | example = ["*/example_lib/*", "*/src/example_lib", "*/example/src/example_lib"] 39 | tests = ["tests", "*/example/tests"] 40 | 41 | [tool.coverage.report] 42 | exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"] 43 | 44 | [tool.coverage.run] 45 | branch = true 46 | omit = ["src/example_lib/__about__.py"] 47 | parallel = true 48 | source_pkgs = ["example_lib"] 49 | 50 | [tool.hatch.version] 51 | path = "src/example_lib/__about__.py" 52 | 53 | [tool.ruff] 54 | line-length = 120 55 | target-version = "py37" 56 | 57 | [tool.ruff.lint] 58 | ignore = [ 59 | # Allow non-abstract empty methods in abstract base classes 60 | "B027", 61 | # Allow boolean positional values in function calls, like `dict.get(... True)` 62 | "FBT003", 63 | # Ignore checks for possible passwords 64 | "S105", 65 | "S106", 66 | "S107", 67 | # Ignore complexity 68 | "C901", 69 | "PLR0911", 70 | "PLR0912", 71 | "PLR0913", 72 | "PLR0915" 73 | ] 74 | select = [ 75 | "A", 76 | "ARG", 77 | "B", 78 | "C", 79 | "DTZ", 80 | "E", 81 | "EM", 82 | "F", 83 | "FBT", 84 | "I", 85 | "ICN", 86 | "ISC", 87 | "N", 88 | "PLC", 89 | "PLE", 90 | "PLR", 91 | "PLW", 92 | "Q", 93 | "RUF", 94 | "S", 95 | "T", 96 | "TID", 97 | "UP", 98 | "W", 99 | "YTT" 100 | ] 101 | unfixable = [] 102 | 103 | [tool.ruff.lint.flake8-tidy-imports] 104 | ban-relative-imports = "all" 105 | 106 | [tool.ruff.lint.isort] 107 | known-first-party = ["example", "example_lib"] 108 | 109 | [tool.ruff.lint.per-file-ignores] 110 | # Tests can use magic values, assertions, and relative imports 111 | "tests/**/*" = ["PLR2004", "S101", "TID252"] 112 | -------------------------------------------------------------------------------- /test_libraries/src_structure/scripts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshua-auchincloss/hatch-cython/17f8699a2d22db53e75024bf417501f178260adb/test_libraries/src_structure/scripts/.gitkeep -------------------------------------------------------------------------------- /test_libraries/src_structure/scripts/custom_include.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | CALLED_INCLUDES = False 5 | CALLED_MUST = False 6 | 7 | 8 | def update(): 9 | with open("caller_outputs.json", "w") as f: 10 | f.write( 11 | json.dumps( 12 | { 13 | "call": CALLED_MUST, 14 | "includes": CALLED_INCLUDES, 15 | } 16 | ) 17 | ) 18 | 19 | 20 | def must_call(): 21 | global CALLED_MUST # noqa: PLW0603 22 | CALLED_MUST = True 23 | update() 24 | 25 | 26 | def gets_includes(): 27 | global CALLED_INCLUDES # noqa: PLW0603 28 | CALLED_INCLUDES = True 29 | update() 30 | return [str(Path(__file__).parent.parent / "include")] 31 | -------------------------------------------------------------------------------- /test_libraries/src_structure/src/example_lib/__about__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023-present joshua-auchincloss 2 | # 3 | # SPDX-License-Identifier: MIT 4 | __version__ = "0.0.1" 5 | -------------------------------------------------------------------------------- /test_libraries/src_structure/src/example_lib/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023-present joshua-auchincloss 2 | # 3 | # SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /test_libraries/src_structure/src/example_lib/_alias.pyx: -------------------------------------------------------------------------------- 1 | # distutils: language=c++ 2 | 3 | cpdef str some_aliased(str name): 4 | return name 5 | -------------------------------------------------------------------------------- /test_libraries/src_structure/src/example_lib/custom_includes.pyi: -------------------------------------------------------------------------------- 1 | def bwfpy(b: int) -> int: ... 2 | -------------------------------------------------------------------------------- /test_libraries/src_structure/src/example_lib/custom_includes.pyx: -------------------------------------------------------------------------------- 1 | # distutils: language=c++ 2 | 3 | ctypedef long long int wide_int 4 | 5 | cdef extern from "something.cc" namespace "pyutil": 6 | cdef wide_int bwf(wide_int *b) 7 | 8 | cpdef wide_int bwfpy(wide_int b): 9 | return bwf(&b) 10 | -------------------------------------------------------------------------------- /test_libraries/src_structure/src/example_lib/mod_a/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshua-auchincloss/hatch-cython/17f8699a2d22db53e75024bf417501f178260adb/test_libraries/src_structure/src/example_lib/mod_a/__init__.py -------------------------------------------------------------------------------- /test_libraries/src_structure/src/example_lib/mod_a/adds.pyi: -------------------------------------------------------------------------------- 1 | def fmul(a: float, b: float) -> float: ... 2 | def imul(a: int, b: int) -> int: ... 3 | -------------------------------------------------------------------------------- /test_libraries/src_structure/src/example_lib/mod_a/adds.pyx: -------------------------------------------------------------------------------- 1 | cpdef float fmul(float a, float b): 2 | return a * b 3 | 4 | cpdef int imul(int a, int b): 5 | return a * b 6 | -------------------------------------------------------------------------------- /test_libraries/src_structure/src/example_lib/mod_a/deep_nest/creates.pyx: -------------------------------------------------------------------------------- 1 | cdef class MyClass: 2 | def __cinit__(self): 3 | pass 4 | 5 | cpdef str do(self): 6 | return "abc" 7 | 8 | cpdef MyClass fast_create(): 9 | return MyClass.__new__(MyClass) 10 | -------------------------------------------------------------------------------- /test_libraries/src_structure/src/example_lib/mod_a/some_defn.pxd: -------------------------------------------------------------------------------- 1 | # distutils: language=c++ 2 | cimport cython 3 | 4 | 5 | @cython.final 6 | cdef class ValueDefn: 7 | cdef public int value 8 | cpdef bint set(self, int value) 9 | 10 | 11 | cdef inline int vmul(ValueDefn v1, ValueDefn v2): 12 | cdef int result 13 | result = v1.value ** v2.value 14 | return result 15 | -------------------------------------------------------------------------------- /test_libraries/src_structure/src/example_lib/mod_a/some_defn.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | 4 | class ValueDefn(object): # noqa: UP004 5 | def __init__(self, value: Optional[int] = None): 6 | self.value = value if value else 0 7 | 8 | def set(self, value): # noqa: A003, RUF100 9 | v = self.value 10 | self.value = value 11 | if v: 12 | return True 13 | return False 14 | -------------------------------------------------------------------------------- /test_libraries/src_structure/src/example_lib/no_compile/abc.py: -------------------------------------------------------------------------------- 1 | def test_nocompile(): 2 | return 41 3 | -------------------------------------------------------------------------------- /test_libraries/src_structure/src/example_lib/normal.py: -------------------------------------------------------------------------------- 1 | def normal_func(a: int, b: int): 2 | return a + b 3 | -------------------------------------------------------------------------------- /test_libraries/src_structure/src/example_lib/platform/darwin.pyx: -------------------------------------------------------------------------------- 1 | cpdef void test_darwin(): 2 | pass 3 | -------------------------------------------------------------------------------- /test_libraries/src_structure/src/example_lib/platform/freebsd.pyx: -------------------------------------------------------------------------------- 1 | cpdef void test_freebsd(): 2 | pass 3 | -------------------------------------------------------------------------------- /test_libraries/src_structure/src/example_lib/platform/linux.pyx: -------------------------------------------------------------------------------- 1 | cpdef void test_linux(): 2 | pass 3 | -------------------------------------------------------------------------------- /test_libraries/src_structure/src/example_lib/platform/windows.pyx: -------------------------------------------------------------------------------- 1 | cpdef void test_win(): 2 | pass 3 | -------------------------------------------------------------------------------- /test_libraries/src_structure/src/example_lib/templated.pyi.in: -------------------------------------------------------------------------------- 1 | {{for typ in supported}} 2 | 3 | # {{typ}} C adder ({{typ}}) 4 | def c{{typ[:1]}}add(a: {{typ}}, b: {{typ}}) -> {{typ}}: 5 | """ 6 | Takes `a` `{{typ}}` plus `b` `{{typ}}` 7 | """ 8 | ... 9 | 10 | # {{typ}} C mul ({{typ}}) 11 | def c{{typ[:1]}}mul(a: {{typ}}, b: {{typ}}) -> {{typ}}: 12 | """ 13 | Takes `a` `{{typ}}` times `b` `{{typ}}` 14 | """ 15 | ... 16 | 17 | def c{{typ[:1]}}pow(a: {{typ}}, b: {{typ}}) -> {{typ}}: 18 | """ 19 | Takes `a` `{{typ}}` to the power of `b` `{{typ}}` 20 | """ 21 | ... 22 | 23 | {{endfor}} 24 | -------------------------------------------------------------------------------- /test_libraries/src_structure/src/example_lib/templated.pyx.in: -------------------------------------------------------------------------------- 1 | {{for typ in supported}} 2 | 3 | # {{typ}} C adder ({{typ}}) 4 | cpdef {{typ}} c{{typ[:1]}}add({{typ}} a, {{typ}} b): 5 | return a + b 6 | 7 | # {{typ}} C mul ({{typ}}) 8 | cpdef {{typ}} c{{typ[:1]}}mul({{typ}} a, {{typ}} b): 9 | return a * b 10 | 11 | # {{typ}} C pow ({{typ}}) 12 | cpdef {{typ}} c{{typ[:1]}}pow({{typ}} a, {{typ}} b): 13 | return a ** b 14 | 15 | {{endfor}} 16 | -------------------------------------------------------------------------------- /test_libraries/src_structure/src/example_lib/templated_maxosx_sample.pyi: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. 2 | # Autoformatted by hatch-cython. 3 | # Version: 0.3.0 4 | # Cython: 3.0.2 5 | # Platform: darwin 6 | # Architecture: x86_64 7 | # Keywords: {'supported': ['int', 'float']} 8 | 9 | # int C adder (int) 10 | def ciadd(a: int, b: int) -> int: 11 | """ 12 | Takes `a` `int` plus `b` `int` 13 | """ 14 | ... 15 | 16 | # int C mul (int) 17 | def cimul(a: int, b: int) -> int: 18 | """ 19 | Takes `a` `int` times `b` `int` 20 | """ 21 | ... 22 | 23 | def cipow(a: int, b: int) -> int: 24 | """ 25 | Takes `a` `int` to the power of `b` `int` 26 | """ 27 | ... 28 | 29 | # float C adder (float) 30 | def cfadd(a: float, b: float) -> float: 31 | """ 32 | Takes `a` `float` plus `b` `float` 33 | """ 34 | ... 35 | 36 | # float C mul (float) 37 | def cfmul(a: float, b: float) -> float: 38 | """ 39 | Takes `a` `float` times `b` `float` 40 | """ 41 | ... 42 | 43 | def cfpow(a: float, b: float) -> float: 44 | """ 45 | Takes `a` `float` to the power of `b` `float` 46 | """ 47 | ... 48 | -------------------------------------------------------------------------------- /test_libraries/src_structure/src/example_lib/templated_maxosx_sample.pyx: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. 2 | # Autoformatted by hatch-cython. 3 | # Version: 0.3.0 4 | # Cython: 3.0.2 5 | # Platform: darwin 6 | # Architecture: x86_64 7 | # Keywords: {'supported': ['int', 'float']} 8 | 9 | # int C adder (int) 10 | cpdef int ciadd(int a, int b): 11 | return a + b 12 | 13 | # int C mul (int) 14 | cpdef int cimul(int a, int b): 15 | return a * b 16 | 17 | # int C pow (int) 18 | cpdef int cipow(int a, int b): 19 | return a ** b 20 | 21 | # float C adder (float) 22 | cpdef float cfadd(float a, float b): 23 | return a + b 24 | 25 | # float C mul (float) 26 | cpdef float cfmul(float a, float b): 27 | return a * b 28 | 29 | # float C pow (float) 30 | cpdef float cfpow(float a, float b): 31 | return a ** b 32 | -------------------------------------------------------------------------------- /test_libraries/src_structure/src/example_lib/test.pyi: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from numpy import longlong 4 | from numpy.typing import NDArray 5 | 6 | T = TypeVar("T") 7 | 8 | def hello_world(name: str): ... 9 | def hello_numpy(arr: NDArray[longlong]): ... 10 | -------------------------------------------------------------------------------- /test_libraries/src_structure/src/example_lib/test.pyx: -------------------------------------------------------------------------------- 1 | # distutils: language=c++ 2 | 3 | cimport numpy as cnp 4 | 5 | from cython.parallel import parallel, prange 6 | 7 | cnp.import_array() 8 | 9 | ctypedef cnp.longlong_t dtype 10 | 11 | cpdef str hello_world(str name): 12 | cdef str response 13 | response = f"hello, {name}" 14 | return response 15 | 16 | 17 | cpdef dtype hello_numpy(cnp.ndarray[dtype, ndim=1] arr): 18 | cdef dtype tot 19 | cdef Py_ssize_t cap 20 | cdef int i 21 | tot = 0 22 | cap = arr.size 23 | with nogil, parallel(): 24 | for i in prange(cap): 25 | with gil: 26 | tot += arr[i] 27 | return tot 28 | -------------------------------------------------------------------------------- /test_libraries/src_structure/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023-present joshua-auchincloss 2 | # 3 | # SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /test_libraries/src_structure/tests/test_aliased.py: -------------------------------------------------------------------------------- 1 | def test_aliased(): 2 | from example_lib.aliased import some_aliased 3 | 4 | assert some_aliased("abc") == "abc" 5 | -------------------------------------------------------------------------------- /test_libraries/src_structure/tests/test_custom_includes.py: -------------------------------------------------------------------------------- 1 | from example_lib.custom_includes import bwfpy 2 | 3 | 4 | def test_custom_includes(): 5 | assert bwfpy(4) == 20 6 | -------------------------------------------------------------------------------- /test_libraries/src_structure/tests/test_example.py: -------------------------------------------------------------------------------- 1 | from numpy import linspace, longlong 2 | 3 | from example_lib.test import hello_numpy, hello_world 4 | 5 | 6 | def test_hello_world(): 7 | assert hello_world("world") == "hello, world" 8 | 9 | 10 | def test_hello_numpy(): 11 | assert hello_numpy(linspace(0, 100, 25, dtype=longlong)) == 1240 12 | -------------------------------------------------------------------------------- /test_libraries/src_structure/tests/test_normal.py: -------------------------------------------------------------------------------- 1 | from example_lib.normal import normal_func 2 | 3 | 4 | def test_normal(): 5 | assert normal_func(1, 2) == 3 6 | -------------------------------------------------------------------------------- /test_libraries/src_structure/tests/test_platform.py: -------------------------------------------------------------------------------- 1 | from platform import system 2 | 3 | 4 | def test_platform_specific(): 5 | sys = system().lower() 6 | 7 | if sys == "windows": 8 | from example_lib.platform.windows import test_win 9 | 10 | test_win() 11 | elif sys == "linux": 12 | from example_lib.platform.linux import test_linux 13 | 14 | test_linux() 15 | elif sys == "darwin": 16 | from example_lib.platform.darwin import test_darwin 17 | 18 | test_darwin() 19 | -------------------------------------------------------------------------------- /test_libraries/src_structure/tests/test_submodule.py: -------------------------------------------------------------------------------- 1 | def test_muls(): 2 | from example_lib.mod_a.adds import fmul, imul 3 | 4 | assert fmul(5.5, 5.5) == 30.25 5 | assert imul(21, 2) == 42 6 | 7 | 8 | def test_vals(): 9 | from example_lib.mod_a.some_defn import ValueDefn 10 | 11 | v = ValueDefn(10) 12 | assert v.value == 10 13 | v.set(5) 14 | assert v.value == 5 15 | 16 | 17 | def test_deep_nesting(): 18 | from example_lib.mod_a.deep_nest.creates import fast_create 19 | 20 | o = fast_create() 21 | assert o.do() == "abc" 22 | -------------------------------------------------------------------------------- /test_libraries/src_structure/tests/test_templated.py: -------------------------------------------------------------------------------- 1 | from platform import system 2 | 3 | from example_lib.templated import ciadd, cipow 4 | 5 | PLAT = system().lower() 6 | 7 | 8 | def test_template_ops(): 9 | assert ciadd(1, 2) == 3 10 | assert cipow(2, 2) == 4 11 | 12 | if PLAT in ("windows", "darwin"): 13 | from example_lib.templated import cfpow 14 | 15 | assert cfpow(5.5, 2) == 30.25 16 | if PLAT == "windows": 17 | from example_lib.templated import ccadd 18 | 19 | assert ccadd(complex(real=4, imag=2), complex(real=2, imag=0)) == complex(real=6, imag=2) 20 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023-present joshua-auchincloss 2 | # 3 | # SPDX-License-Identifier: MIT 4 | from os import getcwd, name 5 | from sys import path 6 | 7 | if name == "nt": 8 | from pathlib import WindowsPath 9 | 10 | p = WindowsPath(getcwd()) / "src" 11 | else: 12 | from pathlib import PosixPath 13 | 14 | p = PosixPath(getcwd()) / "src" 15 | 16 | path.append(str(p)) 17 | 18 | from hatch_cython.__about__ import __version__ # noqa: E402 19 | from hatch_cython.devel import CythonBuildHook, src # noqa: E402 20 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | from types import SimpleNamespace 3 | from unittest.mock import patch 4 | 5 | from toml import loads 6 | 7 | from hatch_cython.config import parse_from_dict 8 | from hatch_cython.config.defaults import brew_path 9 | from hatch_cython.utils import aarch, plat 10 | 11 | from .utils import arch_platform, import_module, patch_brew, patch_path, pyversion 12 | 13 | 14 | def test_brew_path(): 15 | if plat() == "darwin" and aarch() == "x86_64": 16 | assert brew_path() == "/usr/local" 17 | elif plat() == "darwin" and aarch() == "arm64": 18 | assert brew_path() == "/opt/homebrew" 19 | else: 20 | assert brew_path() is None 21 | 22 | 23 | def test_brew_fails_safely(): 24 | with patch("hatch_cython.config.defaults.BREW", "some-cmd-that-doesnt-exist"): 25 | with patch("hatch_cython.utils.memo", lambda f: f): 26 | with arch_platform("x86_64", "darwin", brew=False): 27 | assert brew_path() == "/usr/local" 28 | with arch_platform("arm64", "darwin", brew=False): 29 | assert brew_path() == "/opt/homebrew" 30 | 31 | 32 | def test_config_with_brew(): 33 | with pyversion("3", "9"), arch_platform("arm64", "darwin"), patch_path("arm64"), patch_brew("/opt/homebrew"): 34 | ok = parse_from_dict(SimpleNamespace(config={"options": {"parallel": True}})) 35 | assert sorted(ok.compile_args_for_platform) == sorted(["-O2", "-I/opt/homebrew/include"]) 36 | assert ok.compile_links_for_platform == ["-L/opt/homebrew/lib"] 37 | 38 | 39 | def test_config_parser(): 40 | data = """ 41 | [options] 42 | includes = [] 43 | include_numpy = false 44 | include_pyarrow = false 45 | 46 | include_somelib = { pkg = "somelib", include = "gets_include", libraries = "gets_libraries", library_dirs = "gets_library_dirs", required_call = "some_setup_op" } 47 | 48 | compile_args = [ 49 | { platforms = ["windows"], arg = "-std=c++17" }, 50 | { platforms = ["linux", "darwin"], arg = "-I/abc/def" }, 51 | { platforms = ["linux", "darwin"], arg = "-Wcpp" }, 52 | { platforms = ["darwin"], arg = "-L/usr/local/opt/llvm/include" }, 53 | { arch = ["anon"], arg = "-O1" }, 54 | { arch = ["x86_64"], arg = "-O2" }, 55 | { arch = ["arm64"], arg = "-O3" }, 56 | { arg = "-py39", marker = "python_version == '3.9'" }, 57 | ] 58 | extra_link_args = [ 59 | { platforms = ["darwin"], arg = "-L/usr/local/opt/llvm/lib" }, 60 | { platforms = ["windows"], arg = "-LC://abc/def" }, 61 | { platforms = ["linux"], arg = "-L/etc/ssl/ssl.h" }, 62 | { arch = ["arm64"], arg = "-L/usr/include/cpu/simd.h" }, 63 | ] 64 | 65 | define_macros = [ 66 | ["NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION"], 67 | ["ABC_DEF"], 68 | ["GHI_JKL"], 69 | ] 70 | directives = { boundscheck = false, nonecheck = false, language_level = 3, binding = true } 71 | 72 | abc_compile_kwarg = "test" 73 | """ # noqa: E501 74 | 75 | def gets_include(): 76 | return "abc" 77 | 78 | def gets_libraries(): 79 | return ["lib-a"] 80 | 81 | def gets_library_dirs(): 82 | return ["dir-a"] 83 | 84 | ran = False 85 | 86 | def some_setup_op(): 87 | nonlocal ran 88 | ran = True 89 | 90 | with pyversion(): 91 | with import_module( 92 | gets_include=gets_include, 93 | gets_libraries=gets_libraries, 94 | gets_library_dirs=gets_library_dirs, 95 | some_setup_op=some_setup_op, 96 | ): 97 | 98 | def getcfg(): 99 | parsed = loads(dedent(data)) 100 | return parse_from_dict(SimpleNamespace(config=parsed)) 101 | 102 | cfg = getcfg() 103 | assert ran 104 | assert len(getcfg().compile_args) 105 | 106 | with pyversion("3", "9"), arch_platform("arm64", "darwin"), patch_path("arm64"): 107 | cfg = getcfg() 108 | assert sorted(cfg.compile_args_for_platform) == sorted( 109 | [ 110 | "-I/abc/def", 111 | "-py39", 112 | "-Wcpp", 113 | "-I/opt/homebrew/include", 114 | "-O3", 115 | "-L/usr/local/opt/llvm/include", 116 | ] 117 | ) 118 | 119 | with arch_platform("x86_64", "windows"): 120 | cfg = getcfg() 121 | assert sorted(cfg.compile_args_for_platform) == sorted( 122 | [ 123 | "-std=c++17", 124 | "-O2", 125 | ] 126 | ) 127 | assert sorted(cfg.compile_links_for_platform) == sorted(["-LC://abc/def"]) 128 | with arch_platform("x86_64", "linux"), patch_path("x86_64", "/usr/local/opt"): 129 | cfg = getcfg() 130 | assert sorted(cfg.compile_args_for_platform) == sorted( 131 | [ 132 | "-I/usr/local/include", 133 | "-I/abc/def", 134 | "-Wcpp", 135 | "-O2", 136 | ] 137 | ) 138 | assert sorted(cfg.compile_links_for_platform) == sorted( 139 | ["-L/usr/local/lib", "-L/usr/local/opt", "-L/etc/ssl/ssl.h"] 140 | ) 141 | with arch_platform("x86_64", "darwin"), patch_path("x86_64"): 142 | cfg = getcfg() 143 | assert sorted(cfg.compile_args_for_platform) == sorted( 144 | [ 145 | "-I/usr/local/include", 146 | "-I/abc/def", 147 | "-Wcpp", 148 | "-L/usr/local/opt/llvm/include", 149 | "-O2", 150 | ] 151 | ) 152 | assert sorted(cfg.compile_links_for_platform) == sorted( 153 | [ 154 | "-L/usr/local/lib", 155 | "-L/usr/local/opt", 156 | "-L/usr/local/opt/llvm/lib", 157 | ] 158 | ) 159 | 160 | with arch_platform("arm64", "windows"): 161 | cfg = getcfg() 162 | 163 | assert sorted(cfg.compile_args_for_platform) == sorted( 164 | [ 165 | "-std=c++17", 166 | "-O3", 167 | ] 168 | ) 169 | assert sorted(cfg.compile_links_for_platform) == sorted(["-LC://abc/def", "-L/usr/include/cpu/simd.h"]) 170 | with arch_platform("arm64", "linux"), patch_path("x86_64"): 171 | cfg = getcfg() 172 | 173 | assert sorted(cfg.compile_args_for_platform) == sorted( 174 | [ 175 | "-I/usr/local/include", 176 | "-I/abc/def", 177 | "-Wcpp", 178 | "-O3", 179 | ] 180 | ) 181 | assert sorted(cfg.compile_links_for_platform) == sorted( 182 | [ 183 | "-L/usr/local/lib", 184 | "-L/usr/local/opt", 185 | "-L/etc/ssl/ssl.h", 186 | "-L/usr/include/cpu/simd.h", 187 | ] 188 | ) 189 | with arch_platform("arm64", "darwin"), patch_path("arm64"): 190 | cfg = getcfg() 191 | assert sorted(cfg.compile_args_for_platform) == sorted( 192 | [ 193 | "-I/opt/homebrew/include", 194 | "-I/abc/def", 195 | "-Wcpp", 196 | "-L/usr/local/opt/llvm/include", 197 | "-O3", 198 | ] 199 | ) 200 | assert sorted(cfg.compile_links_for_platform) == sorted( 201 | [ 202 | "-L/opt/homebrew/lib", 203 | "-L/usr/local/opt/llvm/lib", 204 | "-L/usr/include/cpu/simd.h", 205 | ] 206 | ) 207 | 208 | with arch_platform("", "windows"): 209 | cfg = getcfg() 210 | 211 | assert sorted(cfg.compile_args_for_platform) == sorted(["-std=c++17", "-O1"]) 212 | assert sorted(cfg.compile_links_for_platform) == sorted( 213 | [ 214 | "-LC://abc/def", 215 | ] 216 | ) 217 | with arch_platform("", "linux"), patch_path("x86_64"): 218 | cfg = getcfg() 219 | 220 | assert sorted(cfg.compile_args_for_platform) == sorted( 221 | ["-I/usr/local/include", "-I/abc/def", "-Wcpp", "-O1"] 222 | ) 223 | assert sorted(cfg.compile_links_for_platform) == sorted( 224 | [ 225 | "-L/usr/local/lib", 226 | "-L/usr/local/opt", 227 | "-L/etc/ssl/ssl.h", 228 | ] 229 | ) 230 | with arch_platform("", "darwin"), patch_path("x86_64"): 231 | cfg = getcfg() 232 | assert sorted(cfg.compile_args_for_platform) == sorted( 233 | [ 234 | "-I/usr/local/include", 235 | "-I/abc/def", 236 | "-Wcpp", 237 | "-L/usr/local/opt/llvm/include", 238 | "-O1", 239 | ] 240 | ) 241 | assert sorted(cfg.compile_links_for_platform) == sorted( 242 | [ 243 | "-L/usr/local/lib", 244 | "-L/usr/local/opt", 245 | "-L/usr/local/opt/llvm/lib", 246 | ] 247 | ) 248 | 249 | cfg = getcfg() 250 | 251 | assert cfg.directives == {"boundscheck": False, "nonecheck": False, "language_level": 3, "binding": True} 252 | assert cfg.libraries == gets_libraries() 253 | assert cfg.library_dirs == gets_library_dirs() 254 | assert gets_include() in cfg.includes 255 | assert cfg.compile_kwargs == {"abc_compile_kwarg": "test"} 256 | 257 | 258 | def test_defaults(): 259 | data = """ 260 | [options] 261 | """ 262 | parsed = loads(dedent(data)) 263 | 264 | def getcfg(): 265 | return parse_from_dict(SimpleNamespace(config=parsed)) 266 | 267 | cfg = getcfg() 268 | assert cfg.directives == {"language_level": 3, "binding": True} 269 | 270 | with arch_platform("x86_64", "windows"): 271 | cfg = getcfg() 272 | 273 | assert sorted(cfg.compile_args_for_platform) == sorted( 274 | [ 275 | "-O2", 276 | ] 277 | ) 278 | assert sorted(cfg.compile_links_for_platform) == sorted([]) 279 | with arch_platform("x86_64", "linux"), patch_path("x86_64"): 280 | cfg = getcfg() 281 | assert sorted(cfg.compile_args_for_platform) == sorted(["-I/usr/local/include", "-O2"]) 282 | assert sorted(cfg.compile_links_for_platform) == sorted(["-L/usr/local/opt", "-L/usr/local/lib"]) 283 | with arch_platform("x86_64", "darwin"), patch_path("x86_64"): 284 | cfg = getcfg() 285 | assert sorted(cfg.compile_args_for_platform) == sorted( 286 | [ 287 | "-O2", 288 | "-I/usr/local/include", 289 | ] 290 | ) 291 | assert sorted(cfg.compile_links_for_platform) == sorted(["-L/usr/local/opt", "-L/usr/local/lib"]) 292 | 293 | with arch_platform("arm64", "windows"): 294 | cfg = getcfg() 295 | 296 | assert sorted(cfg.compile_args_for_platform) == sorted( 297 | [ 298 | "-O2", 299 | ] 300 | ) 301 | assert sorted(cfg.compile_links_for_platform) == sorted([]) 302 | with arch_platform("arm64", "linux"), patch_path("x86_64"): 303 | cfg = getcfg() 304 | 305 | assert sorted(cfg.compile_args_for_platform) == sorted( 306 | [ 307 | "-I/usr/local/include", 308 | "-O2", 309 | ] 310 | ) 311 | assert sorted(cfg.compile_links_for_platform) == sorted(["-L/usr/local/lib", "-L/usr/local/opt"]) 312 | with arch_platform("arm64", "darwin"), patch_path("arm64"): 313 | cfg = getcfg() 314 | 315 | assert sorted(cfg.compile_args_for_platform) == sorted( 316 | [ 317 | "-I/opt/homebrew/include", 318 | "-O2", 319 | ] 320 | ) 321 | assert sorted(cfg.compile_links_for_platform) == sorted( 322 | [ 323 | "-L/opt/homebrew/lib", 324 | ] 325 | ) 326 | 327 | with arch_platform("", "windows"): 328 | cfg = getcfg() 329 | 330 | assert sorted(cfg.compile_args_for_platform) == sorted(["-O2"]) 331 | assert sorted(cfg.compile_links_for_platform) == sorted([]) 332 | 333 | with arch_platform("", "linux"), patch_path("x86_64", "/etc/ssl/ssl.h"): 334 | cfg = getcfg() 335 | 336 | assert sorted(cfg.compile_args_for_platform) == sorted( 337 | [ 338 | "-O2", 339 | "-I/usr/local/include", 340 | ] 341 | ) 342 | assert sorted(cfg.compile_links_for_platform) == sorted( 343 | [ 344 | "-L/usr/local/opt", 345 | "-L/usr/local/lib", 346 | ] 347 | ) 348 | 349 | with arch_platform("", "darwin"), patch_path("x86_64"): 350 | cfg = getcfg() 351 | assert sorted(cfg.compile_args_for_platform) == sorted( 352 | [ 353 | "-O2", 354 | "-I/usr/local/include", 355 | ] 356 | ) 357 | assert sorted(cfg.compile_links_for_platform) == sorted( 358 | [ 359 | "-L/usr/local/opt", 360 | "-L/usr/local/lib", 361 | ] 362 | ) 363 | 364 | cfg = getcfg() 365 | assert cfg.compile_kwargs == {} 366 | -------------------------------------------------------------------------------- /tests/test_files.py: -------------------------------------------------------------------------------- 1 | from hatch_cython.config.files import FileArgs 2 | 3 | 4 | def test_file_config(): 5 | cfg = { 6 | "exclude": [ 7 | "*/abc", 8 | {"matches": "*/123fg"}, 9 | ], 10 | "aliases": {}, 11 | } 12 | 13 | fa = FileArgs(**cfg) 14 | 15 | assert sorted([f.matches for f in fa.exclude if f.applies()]) == ["*/123fg", "*/abc"] 16 | 17 | 18 | def test_fc_with_explicit_targets(): 19 | cfg = { 20 | "targets": [ 21 | "*/abc.py", 22 | {"matches": "*/def.py"}, 23 | ], 24 | "exclude": [], 25 | "aliases": {}, 26 | } 27 | 28 | fa = FileArgs(**cfg) 29 | 30 | assert fa.explicit_targets 31 | 32 | 33 | def test_fc_aliasing(): 34 | cfg = { 35 | "targets": [], 36 | "exclude": [], 37 | "aliases": { 38 | "somelib.abc.next": "somelib.abc.not_first", 39 | "somelib.abc.alias": "somelib.abc.compiled", 40 | }, 41 | } 42 | 43 | fa = FileArgs(**cfg) 44 | assert fa.matches_alias("somelib.abc.alias") == "somelib.abc.compiled" 45 | -------------------------------------------------------------------------------- /tests/test_includes.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | 3 | from hatch_cython.config.autoimport import Autoimport 4 | from hatch_cython.config.includes import parse_includes 5 | 6 | 7 | def test_includes(): 8 | parse = { 9 | "pkg": "somelib", 10 | "include": "gets_include", 11 | "libraries": "gets_libraries", 12 | "library_dirs": "gets_library_dirs", 13 | "required_call": "some_setup_op", 14 | } 15 | ok = parse_includes( 16 | "include_abc", 17 | parse.copy(), 18 | ) 19 | assert ok == Autoimport(**parse) 20 | ok = parse_includes( 21 | "include_abc", 22 | "some_attr", 23 | ) 24 | assert ok == Autoimport(pkg="abc", include="some_attr") 25 | 26 | 27 | def test_with_resolved(): 28 | parse = { 29 | "include": "gets_include", 30 | "libraries": "gets_libraries", 31 | "library_dirs": "gets_library_dirs", 32 | "required_call": "some_setup_op", 33 | } 34 | ok = parse_includes( 35 | "include_abc", 36 | parse.copy(), 37 | ) 38 | 39 | assert ok == Autoimport(pkg="abc", **parse) 40 | 41 | 42 | def test_invalid(): 43 | with raises(ValueError, match="either provide a known package"): 44 | parse_includes("list", []) 45 | -------------------------------------------------------------------------------- /tests/test_macros.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | 3 | from hatch_cython.config.macros import parse_macros 4 | 5 | 6 | def test_macros(): 7 | test_ok = [ 8 | ["NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION"], 9 | ["ABC_DEF"], 10 | ["GHI_JKL"], 11 | ] 12 | assert parse_macros(test_ok) == [ 13 | ("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION"), 14 | ("ABC_DEF", None), 15 | ("GHI_JKL", None), 16 | ] 17 | 18 | 19 | def test_invalid(): 20 | test_not_ok = [ 21 | # extra value 22 | ["a", "b", "c"] 23 | ] 24 | with raises( 25 | ValueError, 26 | match="macros must be defined", 27 | ): 28 | parse_macros(test_not_ok) 29 | -------------------------------------------------------------------------------- /tests/test_platform.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | 3 | from hatch_cython.config.platform import PlatformArgs, parse_platform_args, parse_to_plat 4 | 5 | 6 | def test_platform(): 7 | out = [{"arg": "a"}, "b"] 8 | parse_to_plat(PlatformArgs, {"arg": "a"}, out, 0, False) 9 | assert out[0] == PlatformArgs( 10 | platforms="*", arch="*", depends_path=False, marker=None, apply_to_marker=None, arg="a" 11 | ) 12 | assert out[1] == "b" 13 | 14 | parsed = parse_platform_args( 15 | { 16 | "some": [{"arg": "a"}, "b"], 17 | }, 18 | "some", 19 | lambda: [], 20 | ) 21 | assert parsed == out 22 | 23 | 24 | def test_invalid_platform(): 25 | with raises(ValueError, match="arg 0 is invalid"): 26 | parse_to_plat(PlatformArgs, "a", ["a"], 0, True) 27 | -------------------------------------------------------------------------------- /tests/test_platform_pyversion.py: -------------------------------------------------------------------------------- 1 | from hatch_cython.types import CallableT, DictT, ListT, P, TupleT, UnionT 2 | 3 | 4 | # basic test to assert we can use subscriptable generics 5 | def test_type_compat(): 6 | TupleT[int, str] 7 | DictT[str, str] 8 | ListT[str] 9 | CallableT[P, str] 10 | UnionT[str, None] 11 | -------------------------------------------------------------------------------- /tests/test_plugin.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from os import getcwd, path 3 | from pathlib import Path # noqa: F401 4 | from sys import path as syspath 5 | from types import SimpleNamespace 6 | 7 | import pytest 8 | from toml import load 9 | 10 | from hatch_cython.plugin import CythonBuildHook 11 | from hatch_cython.utils import plat 12 | 13 | from .utils import arch_platform, override_dir 14 | 15 | 16 | def join(*rel): 17 | return path.join(getcwd(), *rel) 18 | 19 | 20 | def read(rel: str): 21 | return open(join(*rel.split("/"))).read() 22 | 23 | 24 | @pytest.fixture 25 | def new_src_proj(tmp_path): 26 | project_dir = tmp_path / "app" 27 | project_dir.mkdir() 28 | (project_dir / "bootstrap.py").write_text(read("test_libraries/bootstrap.py")) 29 | (project_dir / "pyproject.toml").write_text(read("test_libraries/src_structure/pyproject.toml")) 30 | (project_dir / "hatch.toml").write_text(read("test_libraries/src_structure/hatch.toml")) 31 | (project_dir / "LICENSE.txt").write_text(read("test_libraries/src_structure/LICENSE.txt")) 32 | shutil.copytree(join("test_libraries/src_structure", "src"), (project_dir / "src")) 33 | shutil.copytree(join("test_libraries/src_structure", "tests"), (project_dir / "tests")) 34 | shutil.copytree(join("test_libraries/src_structure", "include"), (project_dir / "include")) 35 | shutil.copytree(join("test_libraries/src_structure", "scripts"), (project_dir / "scripts")) 36 | return project_dir 37 | 38 | 39 | def test_wheel_build_hook(new_src_proj): 40 | with override_dir(new_src_proj): 41 | syspath.insert(0, str(new_src_proj)) 42 | hook = CythonBuildHook( 43 | new_src_proj, 44 | load(new_src_proj / "hatch.toml")["build"]["hooks"]["custom"], 45 | {}, 46 | SimpleNamespace(name="example_lib"), 47 | directory=new_src_proj, 48 | target_name="wheel", 49 | ) 50 | 51 | assert hook.is_src 52 | 53 | assert not hook.options.files.explicit_targets 54 | 55 | with arch_platform("", "windows"): 56 | assert hook.is_windows 57 | 58 | with arch_platform("", "darwin"): 59 | assert not hook.is_windows 60 | 61 | with arch_platform("", "linux"): 62 | assert not hook.is_windows 63 | 64 | assert hook.dir_name == "example_lib" 65 | 66 | proj = "./src/example_lib" 67 | assert hook.project_dir == proj 68 | 69 | assert sorted(hook.precompiled_globs) == sorted( 70 | [ 71 | "./src/example_lib/*.py", 72 | "./src/example_lib/**/*.py", 73 | "./src/example_lib/*.pyx", 74 | "./src/example_lib/**/*.pyx", 75 | "./src/example_lib/*.pxd", 76 | "./src/example_lib/**/*.pxd", 77 | ] 78 | ) 79 | 80 | hook.clean([]) 81 | build_data = { 82 | "artifacts": [], 83 | "force_include": {}, 84 | } 85 | hook.initialize("0.1.0", build_data) 86 | 87 | assert sorted(hook.normalized_included_files) == sorted( 88 | [ 89 | "./src/example_lib/__about__.py", 90 | "./src/example_lib/__init__.py", 91 | "./src/example_lib/_alias.pyx", 92 | "./src/example_lib/custom_includes.pyx", 93 | "./src/example_lib/mod_a/__init__.py", 94 | "./src/example_lib/mod_a/adds.pyx", 95 | f"./src/example_lib/platform/{plat()}.pyx", 96 | "./src/example_lib/mod_a/deep_nest/creates.pyx", 97 | "./src/example_lib/mod_a/some_defn.pxd", 98 | "./src/example_lib/mod_a/some_defn.py", 99 | "./src/example_lib/normal.py", 100 | "./src/example_lib/templated.pyx", 101 | "./src/example_lib/test.pyx", 102 | ] 103 | ) 104 | 105 | assert sorted( 106 | [{**ls, "files": sorted(ls.get("files"))} for ls in hook.grouped_included_files], 107 | key=lambda x: x.get("name"), 108 | ) == [ 109 | {"name": "example_lib.__about__", "files": ["./src/example_lib/__about__.py"]}, 110 | {"name": "example_lib.__init__", "files": ["./src/example_lib/__init__.py"]}, 111 | {"name": "example_lib.aliased", "files": ["./src/example_lib/_alias.pyx"]}, 112 | {"name": "example_lib.custom_includes", "files": ["./src/example_lib/custom_includes.pyx"]}, 113 | {"name": "example_lib.mod_a.__init__", "files": ["./src/example_lib/mod_a/__init__.py"]}, 114 | {"name": "example_lib.mod_a.adds", "files": ["./src/example_lib/mod_a/adds.pyx"]}, 115 | {"name": "example_lib.mod_a.deep_nest.creates", "files": ["./src/example_lib/mod_a/deep_nest/creates.pyx"]}, 116 | {"name": "example_lib.mod_a.some_defn", "files": ["./src/example_lib/mod_a/some_defn.py"]}, 117 | {"name": "example_lib.normal", "files": ["./src/example_lib/normal.py"]}, 118 | {"name": f"example_lib.platform.{plat()}", "files": [f"./src/example_lib/platform/{plat()}.pyx"]}, 119 | {"name": "example_lib.templated", "files": ["./src/example_lib/templated.pyx"]}, 120 | {"name": "example_lib.test", "files": ["./src/example_lib/test.pyx"]}, 121 | ] 122 | 123 | rf = sorted( 124 | [ 125 | "./src/example_lib/__about__.*.pxd", 126 | "./src/example_lib/__about__.*.py", 127 | "./src/example_lib/__about__.*.pyx", 128 | "./src/example_lib/__init__.*.pxd", 129 | "./src/example_lib/__init__.*.py", 130 | "./src/example_lib/__init__.*.pyx", 131 | "./src/example_lib/_alias.*.pxd", 132 | "./src/example_lib/_alias.*.py", 133 | "./src/example_lib/_alias.*.pyx", 134 | "./src/example_lib/custom_includes.*.pxd", 135 | "./src/example_lib/custom_includes.*.py", 136 | "./src/example_lib/custom_includes.*.pyx", 137 | "./src/example_lib/mod_a/__init__.*.pxd", 138 | "./src/example_lib/mod_a/__init__.*.py", 139 | "./src/example_lib/mod_a/__init__.*.pyx", 140 | "./src/example_lib/mod_a/adds.*.pxd", 141 | "./src/example_lib/mod_a/adds.*.py", 142 | "./src/example_lib/mod_a/adds.*.pyx", 143 | "./src/example_lib/mod_a/deep_nest/creates.*.pxd", 144 | "./src/example_lib/mod_a/deep_nest/creates.*.py", 145 | "./src/example_lib/mod_a/deep_nest/creates.*.pyx", 146 | "./src/example_lib/mod_a/some_defn.*.pxd", 147 | "./src/example_lib/mod_a/some_defn.*.pxd", 148 | "./src/example_lib/mod_a/some_defn.*.py", 149 | "./src/example_lib/mod_a/some_defn.*.py", 150 | "./src/example_lib/mod_a/some_defn.*.pyx", 151 | "./src/example_lib/mod_a/some_defn.*.pyx", 152 | "./src/example_lib/normal.*.pxd", 153 | "./src/example_lib/normal.*.py", 154 | "./src/example_lib/normal.*.pyx", 155 | "./src/example_lib/templated.*.pxd", 156 | "./src/example_lib/templated.*.py", 157 | "./src/example_lib/templated.*.pyx", 158 | f"./src/example_lib/platform/{plat()}.*.pxd", 159 | f"./src/example_lib/platform/{plat()}.*.py", 160 | f"./src/example_lib/platform/{plat()}.*.pyx", 161 | "./src/example_lib/test.*.pxd", 162 | "./src/example_lib/test.*.py", 163 | "./src/example_lib/test.*.pyx", 164 | ] 165 | ) 166 | assert sorted(hook.normalized_dist_globs) == rf 167 | assert build_data.get("infer_tag") 168 | assert not build_data.get("pure_python") 169 | assert sorted(hook.artifacts) == sorted(build_data.get("artifacts")) == sorted([f"/{f}" for f in rf]) 170 | assert len(build_data.get("force_include")) == 12 171 | 172 | syspath.remove(str(new_src_proj)) 173 | -------------------------------------------------------------------------------- /tests/test_plugin_excludes.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from sys import path as syspath 3 | from types import SimpleNamespace 4 | 5 | import pytest 6 | from toml import load 7 | 8 | from hatch_cython.plugin import CythonBuildHook 9 | 10 | from .test_plugin import join, read 11 | from .utils import override_dir 12 | 13 | 14 | @pytest.fixture 15 | def new_explicit_proj(tmp_path): 16 | project_dir = tmp_path / "app" 17 | project_dir.mkdir() 18 | (project_dir / "bootstrap.py").write_text(read("test_libraries/bootstrap.py")) 19 | (project_dir / "pyproject.toml").write_text(read("test_libraries/only_included/pyproject.toml")) 20 | (project_dir / "hatch.toml").write_text(read("test_libraries/only_included/hatch.toml")) 21 | (project_dir / "LICENSE.txt").write_text(read("test_libraries/only_included/LICENSE.txt")) 22 | shutil.copytree(join("test_libraries/only_included", "src"), (project_dir / "src")) 23 | shutil.copytree(join("test_libraries/only_included", "tests"), (project_dir / "tests")) 24 | return project_dir 25 | 26 | 27 | def test_explicit_includes(new_explicit_proj): 28 | with override_dir(new_explicit_proj): 29 | syspath.insert(0, str(new_explicit_proj)) 30 | hook = CythonBuildHook( 31 | new_explicit_proj, 32 | load(new_explicit_proj / "hatch.toml")["build"]["hooks"]["custom"], 33 | {}, 34 | SimpleNamespace(name="example_only_included"), 35 | directory=new_explicit_proj, 36 | target_name="wheel", 37 | ) 38 | assert hook.normalized_included_files == ["./src/example_only_included/compile.py"] 39 | assert hook.options.files.explicit_targets 40 | 41 | syspath.remove(str(new_explicit_proj)) 42 | -------------------------------------------------------------------------------- /tests/test_setuppy.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from collections.abc import Generator 3 | from textwrap import dedent 4 | from unittest.mock import patch 5 | 6 | from hatch_cython.config import Config, PlatformArgs 7 | from hatch_cython.plugin import setup_py 8 | 9 | from .utils import arch_platform, true_if_eq 10 | 11 | 12 | def clean(s: str): 13 | return "\n".join(v.strip() for v in s.splitlines() if v.strip() != "") 14 | 15 | 16 | EXPECT = dedent( 17 | """ 18 | from setuptools import Extension, setup 19 | from Cython.Build import cythonize 20 | 21 | INCLUDES = ['/123'] 22 | EXTENSIONS = ({'name': 'abc.def', 'files': ['./abc/def.pyx']}, {'name': 'abc.depb', 'files': ['./abc/depb.py']}) 23 | 24 | if __name__ == "__main__": 25 | exts = [ 26 | Extension( ex.get("name"), 27 | ex.get("files"), 28 | extra_compile_args=['-O2'], 29 | extra_link_args=['-I/etc/abc/linka.h'], 30 | include_dirs=INCLUDES, 31 | libraries=['/abc'], 32 | library_dirs=['/def'], 33 | define_macros=[], 34 | 35 | ) for ex in EXTENSIONS 36 | ] 37 | ext_modules = cythonize( 38 | exts, 39 | compiler_directives={'binding': True, 'language_level': 3}, 40 | include_path=INCLUDES, 41 | abc='def' 42 | ) 43 | setup(ext_modules=ext_modules)""" 44 | ) 45 | 46 | 47 | def test_setup_py(): 48 | cfg = Config( 49 | includes=["/123"], 50 | libraries=["/abc"], 51 | library_dirs=["/def"], 52 | cythonize_kwargs={"abc": "def"}, 53 | extra_link_args=[PlatformArgs(arg="-I/etc/abc/linka.h")], 54 | ) 55 | defs = [ 56 | {"name": "abc.def", "files": ["./abc/def.pyx"]}, 57 | {"name": "abc.depb", "files": ["./abc/depb.py"]}, 58 | ] 59 | with patch("hatch_cython.config.config.path.exists", true_if_eq()): 60 | with arch_platform("x86_64", ""): 61 | setup1 = setup_py( 62 | *defs, 63 | options=cfg, 64 | sdist=False, 65 | ) 66 | setup2 = setup_py( 67 | *defs, 68 | options=cfg, 69 | sdist=True, 70 | ) 71 | 72 | assert clean(setup1) == clean(EXPECT) 73 | assert clean(setup2) == clean("\n".join(EXPECT.splitlines()[:-1])) 74 | 75 | 76 | def test_solo_ext_type_validations(): 77 | cfg = Config( 78 | includes=["/123"], 79 | libraries=["/abc"], 80 | library_dirs=["/def"], 81 | cythonize_kwargs={"abc": "def"}, 82 | extra_link_args=[PlatformArgs(arg="-I/etc/abc/linka.h")], 83 | ) 84 | 85 | with patch("hatch_cython.config.config.path.exists", true_if_eq()): 86 | with arch_platform("x86_64", ""): 87 | setup = setup_py( 88 | {"name": "abc.def", "files": ["./abc/def.pyx"]}, 89 | options=cfg, 90 | sdist=True, 91 | ) 92 | tested = False 93 | exteq = "EXTENSIONS =" 94 | for ln in setup.splitlines(): 95 | if ln.startswith(exteq): 96 | tested = True 97 | ext = ast.literal_eval(ln.replace(exteq, "").strip()) 98 | assert isinstance(ext, (list, tuple, Generator)) # noqa: UP038 99 | for ex in ext: 100 | assert isinstance(ex, dict) 101 | assert isinstance(ex.get("name"), str) 102 | assert isinstance(ex.get("files"), list) 103 | 104 | if not tested: 105 | raise ValueError(setup, tested, "missed test") 106 | -------------------------------------------------------------------------------- /tests/test_templates.py: -------------------------------------------------------------------------------- 1 | from hatch_cython.config.templates import Templates, parse_template_kwds 2 | 3 | from .utils import arch_platform 4 | 5 | 6 | def test_templates(): 7 | def test(kwds: dict): 8 | with arch_platform("x86_64", "darwin"): 9 | parsed = parse_template_kwds(kwds.copy()) 10 | found = parsed.find(None, "abc/def/templated.pyx.in") 11 | assert found == kwds.get("templated_mac") 12 | found = parsed.find(None, "./abc/def/templated.pyx.in") 13 | assert found == kwds.get("templated_mac") 14 | 15 | with arch_platform("x86_64", "windows"): 16 | parsed = parse_template_kwds(kwds.copy()) 17 | found = parsed.find(None, "abc/def/templated.pyx.in") 18 | assert found == kwds.get("templated_win") 19 | found = parsed.find(None, "./abc/def/templated.pyx.in") 20 | assert found == kwds.get("templated_win") 21 | 22 | kwds = { 23 | "index": [ 24 | {"keyword": "global", "matches": "*"}, 25 | {"keyword": "templated_mac", "matches": "templated.*.in", "platforms": ["darwin"]}, 26 | {"keyword": "templated_win", "matches": "templated.*.in", "platforms": ["windows"]}, 27 | ], 28 | "global": {"supported": ["int"]}, 29 | "templated_mac": {"supported": ["int", "float"]}, 30 | "templated_win": {"supported": ["int", "float", "complex"]}, 31 | } 32 | 33 | test(kwds) 34 | form2 = { 35 | "index": [ 36 | {"keyword": "global", "matches": "*"}, 37 | {"keyword": "templated_mac", "matches": "*/templated*", "platforms": ["darwin"]}, 38 | {"keyword": "templated_win", "matches": "*/templated*", "platforms": ["windows"]}, 39 | ], 40 | "global": {"supported": ["int"]}, 41 | "templated_mac": {"supported": ["int", "float"]}, 42 | "templated_win": {"supported": ["int", "float", "complex"]}, 43 | } 44 | test(form2) 45 | 46 | 47 | def test_defaults(): 48 | parsed = parse_template_kwds({}) 49 | assert parsed == Templates(index=[]) 50 | assert repr(parsed) == "Templates(index=[], kwargs={})" 51 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | import pytest 4 | 5 | from src.hatch_cython.utils import memo, stale 6 | 7 | 8 | def test_memo(): 9 | ran = 0 10 | 11 | class TestC: 12 | def __init__(self) -> None: 13 | pass 14 | 15 | @memo 16 | def do(self): 17 | nonlocal ran 18 | ran += 1 19 | return 42 + ran 20 | 21 | @property 22 | @memo 23 | def ok_property(self): 24 | return "OK" 25 | 26 | tc = TestC() 27 | 28 | assert tc.do() == 43 29 | assert tc.do() == 43 30 | 31 | tc2 = TestC() 32 | assert id(tc) != id(tc2) 33 | 34 | assert tc2.do() == 44 35 | 36 | assert tc.ok_property == "OK" 37 | 38 | 39 | @pytest.fixture 40 | def new_tmp_dir(tmp_path): 41 | project_dir = tmp_path / "app" 42 | project_dir.mkdir() 43 | return project_dir 44 | 45 | 46 | def test_stale(new_tmp_dir): 47 | src = new_tmp_dir / "test.txt" 48 | dest = new_tmp_dir / "dest.txt" 49 | 50 | src.write_text("hello world") 51 | 52 | # mtime may not be within resolution to run tests consistently, so we need to wait for sys 53 | # to reflect the modification times 54 | # https://stackoverflow.com/questions/19059877/python-os-path-getmtime-time-not-changing 55 | sleep(5) 56 | 57 | dest.write_text("hello world") 58 | 59 | sleep(5) 60 | 61 | assert not stale( 62 | str(src), 63 | str(dest), 64 | ) 65 | 66 | with src.open("w") as f: 67 | f.write("now stale") 68 | 69 | sleep(5) 70 | 71 | assert stale( 72 | str(src), 73 | str(dest), 74 | ) 75 | 76 | dest.unlink() 77 | 78 | sleep(5) 79 | 80 | assert stale( 81 | str(src), 82 | str(dest), 83 | ) 84 | -------------------------------------------------------------------------------- /tests/test_xenv_merge.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | from types import SimpleNamespace 3 | 4 | import toml 5 | 6 | from hatch_cython.config import parse_from_dict 7 | 8 | from .utils import arch_platform, override_env 9 | 10 | 11 | def test_xenvvars(): 12 | data = """ 13 | [options] 14 | env = [ 15 | { env = "CC", arg = "c++" }, 16 | { env = "CPP", arg = "c++" }, 17 | { env = "CXX", arg = "c++" }, 18 | { env = "CFLAGS", arg = "flag2" }, 19 | { env = "CPPFLAGS", arg = "flag2" }, 20 | { env = "LDFLAGS", arg = "flag2" }, 21 | { env = "CUSTOM_1", arg = "flag2", arch = "x86_64" }, 22 | { env = "CUSTOM_2", arg = "flag2", merges = true }, 23 | ] 24 | """ 25 | 26 | def getcfg(): 27 | parsed = toml.loads(dedent(data)) 28 | return parse_from_dict(SimpleNamespace(config=parsed)) 29 | 30 | f1 = "flag1" 31 | f2 = "flag2" 32 | with override_env({"CFLAGS": f1, "CPPFLAGS": f1, "LDFLAGS": f1}): 33 | cfg = getcfg() 34 | 35 | f12 = "flag1 flag2" 36 | assert cfg.envflags.CC.arg == "c++" 37 | assert cfg.envflags.LDFLAGS.arg == "flag2" 38 | 39 | assert cfg.envflags.env.get("CC") == "c++" 40 | assert cfg.envflags.env.get("CFLAGS") == f12 41 | assert cfg.envflags.env.get("CPPFLAGS") == f12 42 | assert cfg.envflags.env.get("LDFLAGS") == f12 43 | 44 | with override_env({"CUSTOM_1": f1}): 45 | with arch_platform("x86_64", ""): 46 | cfg = getcfg() 47 | assert cfg.envflags.env.get("CUSTOM_1") == f2 48 | with arch_platform("", ""): 49 | cfg = getcfg() 50 | assert cfg.envflags.env.get("CUSTOM_1") == f1 51 | 52 | with override_env({"CUSTOM_2": f1}): 53 | cfg = getcfg() 54 | assert cfg.envflags.env.get("CUSTOM_2") == f12 55 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | from contextlib import contextmanager 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | from hatch_cython.types import UnionT 7 | 8 | 9 | def true_if_eq(*vals): 10 | def inner(v: str, *extra): 11 | merge = [*vals, *extra] 12 | ok = any(v == val for val in merge) 13 | print(f"ok: {v} {ok} ", merge) # noqa: T201 14 | return ok 15 | 16 | return inner 17 | 18 | 19 | true_x64_mac = true_if_eq("/usr/local/include", "/usr/local/lib", "/usr/local/opt") 20 | true_arm_mac = true_if_eq("/opt/homebrew/lib", "/opt/homebrew/include") 21 | 22 | 23 | @contextmanager 24 | def patch_path(arch: str, *extra: str): 25 | arches = { 26 | "x86_64": true_x64_mac, 27 | "arm64": true_arm_mac, 28 | } 29 | h = arches[arch] 30 | 31 | def wrap(path): 32 | return h(path, *extra) 33 | 34 | with patch("hatch_cython.config.config.path.exists", wrap): 35 | yield 36 | 37 | 38 | @contextmanager 39 | def patch_brew(prefix): 40 | with patch("hatch_cython.config.defaults.brew_path", lambda: prefix): 41 | yield 42 | 43 | 44 | @contextmanager 45 | def arch_platform(arch: str, platform: str, brew: UnionT[str, None] = True): 46 | def aarchgetter(): 47 | return arch 48 | 49 | def platformgetter(): 50 | return platform 51 | 52 | expect_brew = None 53 | if platform == "darwin": 54 | expect_brew = "/usr/local" if arch == "x86_64" else "/opt/homebrew" 55 | 56 | try: 57 | with patch("hatch_cython.utils.plat", platformgetter): 58 | with patch("hatch_cython.config.defaults.plat", platformgetter): 59 | with patch("hatch_cython.config.platform.plat", platformgetter): 60 | with patch("hatch_cython.plugin.plat", platformgetter): 61 | with patch("hatch_cython.utils.aarch", aarchgetter): 62 | with patch("hatch_cython.config.defaults.aarch", aarchgetter): 63 | with patch("hatch_cython.config.platform.aarch", aarchgetter): 64 | if brew: 65 | with patch_brew(expect_brew): 66 | yield 67 | else: 68 | yield 69 | finally: 70 | print(f"Clean {arch}-{platform}") # noqa: T201 71 | del aarchgetter, platformgetter 72 | pass 73 | 74 | 75 | @contextmanager 76 | def pyversion(maj="3", min="10", p="0"): # noqa: A002 77 | try: 78 | with patch("platform.python_version_tuple", lambda: (maj, min, p)): 79 | yield 80 | finally: 81 | pass 82 | 83 | 84 | @contextmanager 85 | def import_module(gets_include, gets_libraries=None, gets_library_dirs=None, some_setup_op=None): 86 | def get_import(name: str): 87 | print(f"patched {name}") # noqa: T201 88 | return SimpleNamespace( 89 | gets_include=gets_include, 90 | gets_libraries=gets_libraries, 91 | gets_library_dirs=gets_library_dirs, 92 | some_setup_op=some_setup_op, 93 | ) 94 | 95 | try: 96 | with patch( 97 | "hatch_cython.config.config.import_module", 98 | get_import, 99 | ): 100 | yield 101 | finally: 102 | pass 103 | 104 | 105 | @contextmanager 106 | def override_dir(path: str): 107 | cwd = os.getcwd() 108 | try: 109 | os.chdir(path) 110 | yield 111 | finally: 112 | os.chdir(cwd) 113 | 114 | 115 | @contextmanager 116 | def override_env(d: dict): 117 | current = os.environ.copy() 118 | 119 | try: 120 | new = os.environ.copy() 121 | for k, v in d.items(): 122 | new[k] = v 123 | os.environ.update(new) 124 | with patch( 125 | "hatch_cython.config.flags.environ", 126 | new, 127 | ): 128 | yield 129 | finally: 130 | for k, v in current.items(): 131 | os.environ[k] = v 132 | --------------------------------------------------------------------------------