diff --git a/.codeclimate.yml b/.codeclimate.yml index 54b94e29b5304291f5e660edc3cb38d3ddbfa2f7..b177cae40914bfb1f2138a22a63eca1e08abac80 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,19 +1,19 @@ -engines: - duplication: - enabled: true - config: - languages: - python: - fixme: - enabled: true - markdownlint: - enabled: true - pep8: - enabled: true - radon: - enabled: true -exclude_paths: -- example/** -ratings: - paths: - - snakehouse/** +engines: + duplication: + enabled: true + config: + languages: + python: + fixme: + enabled: true + markdownlint: + enabled: true + pep8: + enabled: true + radon: + enabled: true +exclude_paths: +- example/** +ratings: + paths: + - snakehouse/** diff --git a/.gitattributes b/.gitattributes index 6ec0c5375dae1e104bd3c0a8db096cf3b7c17659..c096f8e1200b3df22b48ce56801cf3977ec45a6b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ -*.mako text eol=lf -*.sh text eol=lf +*.mako text eol=lf +*.sh text eol=lf +snakehouse/mako.py text eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb0e72f42c24e6993dbf227d3fcd0c4f0dcb918e..38d373526d8e02cc9087829b28fbbbf08f20e764 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,25 +1,25 @@ -name: CI -run-name: ${{ github.actor }} -on: [ push ] -jobs: - tests: - runs-on: ubuntu-20.04 - strategy: - matrix: - python-version: [ "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12" ] - steps: - - uses: actions/checkout@main - - uses: actions/setup-python@main - with: - python-version: ${{ matrix.python-version }} - cache: pip - - name: Install everything - run: | - pip install -U pip setuptools wheel disttools packaging pyproject.toml - python setup.py install - - name: Test - run: | - cd example - python setup.py test - env: - DEBUG: "1" +name: CI +run-name: ${{ github.actor }} +on: [ push ] +jobs: + tests: + runs-on: ubuntu-20.04 + strategy: + matrix: + python-version: [ "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12" ] + steps: + - uses: actions/checkout@main + - uses: actions/setup-python@main + with: + python-version: ${{ matrix.python-version }} + cache: pip + - name: Install everything + run: | + pip install -U pip setuptools wheel disttools packaging pyproject.toml + python setup.py install + - name: Test + run: | + cd example + python setup.py test + env: + DEBUG: "1" diff --git a/.gitignore b/.gitignore index 8a87afaecc8e86c10e4a8235d55010ae531e6119..e06eb1a8ed003e0eace7990796b6648cff4b62af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,16 @@ -*.pyc -*.pyd -.idea/ -*.egg-info -build -*/build -*/dist -docs/_build -dist -.eggs -*.c -__bootstrap__.* -venv/ -.venv/ - - +*.pyc +*.pyd +.idea/ +*.egg-info +build +*/build +*/dist +docs/_build +dist +.eggs +*.c +__bootstrap__.* +venv/ +.venv/ + + diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..41fc7eec093b08efe5e745b7d6fc430ff9048228 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,21 @@ +stages: + - test + - build + + +.test: + stage: test + before_script: + - pip install --break-system-packages --upgrade Cython setuptools pip coverage nose2 + - pip install --break-system-packages -r requirements.txt + - python setup.py build_ext --inplace + - python -m coverage run -m nose2 -vv -F + - python -m coverage report + variables: + TESTING: "1" + coverage: /TOTAL.*\s+(\d+\%)/ + + +test_python310: + extends: .test + image: python:3.10 \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 3678d1a33ad62ee3f595fa6b6bf32bf81046ac71..93a38f6cb014da082b655510f483acb28629acc8 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,10 +1,10 @@ -version: 2 -build: - os: ubuntu-22.04 - tools: - python: "3.9" -python: - install: - - requirements: requirements.txt -sphinx: - configuration: docs/conf.py +version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3.9" +python: + install: + - requirements: requirements.txt +sphinx: + configuration: docs/conf.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d12692eedd0f9f771bd8841bd35fd40a8c3ddbd4..44408fd94749ba424dd645ccb98bc2dafeb29644 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,77 +1,77 @@ -# v1.7 - -* should work on Windows now - -# v1.6 - -* certified for Python 3.11 and 3.12 -* CI moved to GitHub Actions -* downgraded Mako, since newer Mako requires Python 3.8 - -# v1.5 - -* fixed logging -* `snakehouse` doesn't need cython and satella installed in advance -* added `find_all` -* made available for PyPy users -* deprecated `find_pyx`, `find_c` and `find_pyx_and_c` - -# v1.4 - -* added `find_pyx`, `find_c` and `find_pyx_and_c` -* added documentation - -# v1.3.2 - -* added `read_requirements_txt` - -# v1.3.1 - -* Cython build step ("cythonize") will be parallelized by default - -# v1.3 - -* added an option to build every file as a separate extension, for - usage in tests - -# v1.2.3 - -* `Multibuild` will pass given kwargs to `Extension` object -* added an option to monkey-patch `distutils` to compile multicore - -# v1.2.2 - -* snakehouse will pass the remaining arguments to Multibuild to Cython's Extension - -# v1.2.1 - -* snakehouse won't complain anymore if installing - from a source wheel - -# v1.2 - -* fixed issue #1 - -# v1.1.2 - -* bugfix release: fixed behaviour if there was only - a single file in Multibuild - -# v1.1.1 - -* allowed Linux-style paths on Windows build environments - -# v1.1 - -* added the capability to insert standard `Extension`s - in the snakehouse build() command - -# v1.0.2 - -* got rid of some C compiler warnings -* module will now use mako to render the files - -# v1.0.1 - -* standard C files will be allowed in the builds -* added support for Pythons 3.5-3.6 +# v1.7 + +* should work on Windows now + +# v1.6 + +* certified for Python 3.11 and 3.12 +* CI moved to GitHub Actions +* downgraded Mako, since newer Mako requires Python 3.8 + +# v1.5 + +* fixed logging +* `snakehouse` doesn't need cython and satella installed in advance +* added `find_all` +* made available for PyPy users +* deprecated `find_pyx`, `find_c` and `find_pyx_and_c` + +# v1.4 + +* added `find_pyx`, `find_c` and `find_pyx_and_c` +* added documentation + +# v1.3.2 + +* added `read_requirements_txt` + +# v1.3.1 + +* Cython build step ("cythonize") will be parallelized by default + +# v1.3 + +* added an option to build every file as a separate extension, for + usage in tests + +# v1.2.3 + +* `Multibuild` will pass given kwargs to `Extension` object +* added an option to monkey-patch `distutils` to compile multicore + +# v1.2.2 + +* snakehouse will pass the remaining arguments to Multibuild to Cython's Extension + +# v1.2.1 + +* snakehouse won't complain anymore if installing + from a source wheel + +# v1.2 + +* fixed issue #1 + +# v1.1.2 + +* bugfix release: fixed behaviour if there was only + a single file in Multibuild + +# v1.1.1 + +* allowed Linux-style paths on Windows build environments + +# v1.1 + +* added the capability to insert standard `Extension`s + in the snakehouse build() command + +# v1.0.2 + +* got rid of some C compiler warnings +* module will now use mako to render the files + +# v1.0.1 + +* standard C files will be allowed in the builds +* added support for Pythons 3.5-3.6 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 08144455d25d5e2e159c73eecb952f19ea9b3b7e..7525a7f851505164fdc5c883ef3b6bdf6eaf80ae 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -1,4 +1,4 @@ -List of contributors -==================== - -Piotr Maślanka <pmaslanka at smok dot co> +List of contributors +==================== + +Piotr Maślanka <pmaslanka at smok dot co> diff --git a/LICENSE b/LICENSE index e6624836b5fc69344b8b2a63e26436a4280802c5..a8cbfadc2dcd3c5715406263a6a8624e78e5f880 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2020-2024 SMOK sp. z o. o. - -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: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -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. +MIT License + +Copyright (c) 2020-2024 SMOK sp. z o. o. + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. diff --git a/MANIFEST.in b/MANIFEST.in index 1aafccb6ddb8ba349adeafbd0655602a5d7ae8b0..fae74cbef848f98af068068a6563691294b63e09 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ -include LICENSE -include README.md -include CONTRIBUTORS.md -include snakehouse/templates/*.mako -include requirements.txt +include LICENSE +include README.md +include CONTRIBUTORS.md +include snakehouse/templates/*.mako +include requirements.txt diff --git a/README.md b/README.md index 03bb535dc2c0a0091cede6d421e22188acd5e90b..d04c92ff05af4294b082e6b16eef738eabfbee62 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,37 @@ -snakehouse -========== - -[](https://codeclimate.com/github/smok-serwis/snakehouse) -[](https://codeclimate.com/github/smok-serwis/snakehouse) -[](https://pypi.python.org/pypi/snakehouse) -[](https://badge.fury.io/py/snakehouse) -[](https://pypi.python.org/pypi/snakehouse) -[]() -[](http://snakehouse.readthedocs.io/en/latest/?badge=latest) -[](https://github.com/smok-serwis/snakehouse) - -**IMPORTANT!** - -Since for now we've lost our PyPI access, please install the packages in a following way: - -``` -pip install git+https://github.com/smok-serwis/snakehouse.git -``` - -snakehouse is a tool to pack mutiple .pyx files -into a single extension so that they are importable as separate -Python modules inside Python. - -Inspired by [this StackOverflow discussion](https://stackoverflow.com/questions/30157363/collapse-multiple-submodules-to-one-cython-extension). - -Tested and works on CPython 3.5-3.12, -both Windows and [Linux](https://travis-ci.org/github/smok-serwis/snakehouse). - -It doesn't work on PyPy due to lack of -`PyModule_FromDefAndSpec` symbol. - -READ BEFORE YOU USE -=================== - -Be sure to read the [docs](http://snakehouse.readthedocs.io/en/latest/) -before you start using it. +snakehouse +========== + +[](https://codeclimate.com/github/smok-serwis/snakehouse) +[](https://codeclimate.com/github/smok-serwis/snakehouse) +[](https://pypi.python.org/pypi/snakehouse) +[](https://badge.fury.io/py/snakehouse) +[](https://pypi.python.org/pypi/snakehouse) +[]() +[](http://snakehouse.readthedocs.io/en/latest/?badge=latest) +[](https://github.com/smok-serwis/snakehouse) + +**IMPORTANT!** + +Since for now we've lost our PyPI access, please install the packages in a following way: + +``` +pip install git+https://github.com/smok-serwis/snakehouse.git +``` + +snakehouse is a tool to pack mutiple .pyx files +into a single extension so that they are importable as separate +Python modules inside Python. + +Inspired by [this StackOverflow discussion](https://stackoverflow.com/questions/30157363/collapse-multiple-submodules-to-one-cython-extension). + +Tested and works on CPython 3.5-3.12, +both Windows and [Linux](https://travis-ci.org/github/smok-serwis/snakehouse). + +It doesn't work on PyPy due to lack of +`PyModule_FromDefAndSpec` symbol. + +READ BEFORE YOU USE +=================== + +Be sure to read the [docs](http://snakehouse.readthedocs.io/en/latest/) +before you start using it. diff --git a/docs/Makefile b/docs/Makefile index d4bb2cbb9eddb1bb1b4f366623044af8e4830919..73a28c7134cd1760744f34bac4ebdedfbed40f72 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,20 +1,20 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/accelerating.rst b/docs/accelerating.rst index 754946ae7fd5982a5c1b745e57fa8810d483389f..2e5539fa48260014b50dcb6bb6797e86267c6750 100644 --- a/docs/accelerating.rst +++ b/docs/accelerating.rst @@ -1,17 +1,17 @@ -Accelerating builds -=================== - -distutils by default compiles using a single process. -To enable faster, multiprocess compilations just use: - -.. code-block:: python - - from snakehouse import monkey_patch_parallel_compilation - - monkey_patch_parallel_compilation() - -In your :code:`setup.py` before your call to :code:`setup()`. - -It is also used in example_ so you can just copy that. - -.. _example: https://github.com/smok-serwis/snakehouse/blob/develop/example/setup.py +Accelerating builds +=================== + +distutils by default compiles using a single process. +To enable faster, multiprocess compilations just use: + +.. code-block:: python + + from snakehouse import monkey_patch_parallel_compilation + + monkey_patch_parallel_compilation() + +In your :code:`setup.py` before your call to :code:`setup()`. + +It is also used in example_ so you can just copy that. + +.. _example: https://github.com/smok-serwis/snakehouse/blob/develop/example/setup.py diff --git a/docs/conf.py b/docs/conf.py index 0ea113419691b0e2059d0f56634ba60b87ea9b14..394969bf92bc5070831828f2dd99a05640b78061 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,55 +1,55 @@ -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -import os -import sys -sys.path.insert(0, os.path.abspath('..')) - - -# -- Project information ----------------------------------------------------- - -project = 'snakehouse' -copyright = '2020-2021 SMOK sp. z o. o.' -author = 'Piotr Maślanka' - -from snakehouse import __version__ -# The full version, including alpha/beta/rc tags -release = __version__ - - -# -- General configuration --------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = ['sphinx.ext.autodoc'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'alabaster' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('..')) + + +# -- Project information ----------------------------------------------------- + +project = 'snakehouse' +copyright = '2020-2021 SMOK sp. z o. o.' +author = 'Piotr Maślanka' + +from snakehouse import __version__ +# The full version, including alpha/beta/rc tags +release = __version__ + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] diff --git a/docs/coverage.rst b/docs/coverage.rst index 120dca27fb91771095ed5bc2990804f9e22a6f3f..b2acef3f0893d842568b95ef34a32d8832202b40 100644 --- a/docs/coverage.rst +++ b/docs/coverage.rst @@ -1,7 +1,7 @@ -Coverage -======== - -:code:`snakehouse` is fully compatible with coverage. Go see how it's done -in tempsdb_. - -.. _tempsdb: https://github.com/smok-serwis/tempsdb +Coverage +======== + +:code:`snakehouse` is fully compatible with coverage. Go see how it's done +in tempsdb_. + +.. _tempsdb: https://github.com/smok-serwis/tempsdb diff --git a/docs/index.rst b/docs/index.rst index b0789102e7f493abc1942fbe70c28af2d0be0aef..d169b9e2ef96b906983b6942b2ff5f2d32a44b18 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,106 +1,106 @@ -Welcome to snakehouse's documentation! -====================================== - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - usage - utilities - coverage - accelerating - -What is snakehouse? -=================== - -Snakehouse_ is a package that helps_ you put multiple :code:`.pyx` files in a single -Python :code:`so`/:code:`DLL`, so that each is importable by Python as if they -were just plain :code:`.py` files. - -.. _helps: https://stackoverflow.com/questions/30157363/collapse-multiple-submodules-to-one-cython-extension - -.. _Snakehouse: https://github.com/smok-serwis/snakehouse - -How do I install it? --------------------- - -Do it via - -.. code-block:: bash - - pip install snakehouse - -All dependencies will be installed automatically. - -Mandatory reading and limitations -================================= - -Take a look at example_ on how to multi-build your Cython extensions. - -.. _example: https://github.com/smok-serwis/snakehouse/blob/develop/example/setup.py - -Don't place modules compiled that way in root .py file's top level imports. -Wrap them in a layer of indirection instead! - -So if your module is called :code:`example`, make a :code:`start_example/__main__.py` with -the following code: - -.. code-block:: python - - if __name__ == '__main__': - from example import run - run() - -Or however you do start your application. - -This applies to unit tests as well! - -When something goes wrong (eg. the application throws an unhandled exception) -the built module has a tendency to dump core. -Try to debug it first by passing :code:`dont_snakehouse=True` to your -modules in the debug mode. - -Also note that if you are compiling in :code:`dont_snakehouse` -mode then your modules should have at least one of the following: - -* a normal Python :code:`def` -* a normal Python class (not :code:`cdef class`) -* a line of Python initialization, eg. - -.. code-block:: python - - a = None - -or - -.. code-block:: python - - import logging - - logger = logging.getLogger(__name__) - -Otherwise :code:`PyInit` won't be generated by Cython -and such module will be unimportable in Python. Normal import won't suffice. - -Please install Snakehouse in a separate venv. This is because it requires ancient version of -several packages, because authors of these packages were quick to drop support for -earlier Pythons. - -Contributions -============= - -Contributions are most welcome. Just add yourself to :code:`CONTRIBUTORS.md` list -at your pull request. At this moment most pressing issues are the segfaulting problem, -where snakehouse-built libraries segfault_ the Python interpreter when there's an unhandled -exception (sometimes, I can't really seem to pinpoint the problem source). - -Try to unit test what you're changing, but that is by no way a requirement. - -.. _segfault: https://github.com/smok-serwis/snakehouse/issues/7 - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` +Welcome to snakehouse's documentation! +====================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + usage + utilities + coverage + accelerating + +What is snakehouse? +=================== + +Snakehouse_ is a package that helps_ you put multiple :code:`.pyx` files in a single +Python :code:`so`/:code:`DLL`, so that each is importable by Python as if they +were just plain :code:`.py` files. + +.. _helps: https://stackoverflow.com/questions/30157363/collapse-multiple-submodules-to-one-cython-extension + +.. _Snakehouse: https://github.com/smok-serwis/snakehouse + +How do I install it? +-------------------- + +Do it via + +.. code-block:: bash + + pip install snakehouse + +All dependencies will be installed automatically. + +Mandatory reading and limitations +================================= + +Take a look at example_ on how to multi-build your Cython extensions. + +.. _example: https://github.com/smok-serwis/snakehouse/blob/develop/example/setup.py + +Don't place modules compiled that way in root .py file's top level imports. +Wrap them in a layer of indirection instead! + +So if your module is called :code:`example`, make a :code:`start_example/__main__.py` with +the following code: + +.. code-block:: python + + if __name__ == '__main__': + from example import run + run() + +Or however you do start your application. + +This applies to unit tests as well! + +When something goes wrong (eg. the application throws an unhandled exception) +the built module has a tendency to dump core. +Try to debug it first by passing :code:`dont_snakehouse=True` to your +modules in the debug mode. + +Also note that if you are compiling in :code:`dont_snakehouse` +mode then your modules should have at least one of the following: + +* a normal Python :code:`def` +* a normal Python class (not :code:`cdef class`) +* a line of Python initialization, eg. + +.. code-block:: python + + a = None + +or + +.. code-block:: python + + import logging + + logger = logging.getLogger(__name__) + +Otherwise :code:`PyInit` won't be generated by Cython +and such module will be unimportable in Python. Normal import won't suffice. + +Please install Snakehouse in a separate venv. This is because it requires ancient version of +several packages, because authors of these packages were quick to drop support for +earlier Pythons. + +Contributions +============= + +Contributions are most welcome. Just add yourself to :code:`CONTRIBUTORS.md` list +at your pull request. At this moment most pressing issues are the segfaulting problem, +where snakehouse-built libraries segfault_ the Python interpreter when there's an unhandled +exception (sometimes, I can't really seem to pinpoint the problem source). + +Try to unit test what you're changing, but that is by no way a requirement. + +.. _segfault: https://github.com/smok-serwis/snakehouse/issues/7 + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat index 922152e96a04a242e6fc40f124261d74890617d8..2119f51099bf37e4fdb6071dce9f451ea44c62dd 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,35 +1,35 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/usage.rst b/docs/usage.rst index 7b58d170d336d53e4bbe890019b369b5343f562f..605fa282c75293991396d3a137eeb08ae069ac46 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -1,52 +1,52 @@ -Usage -===== - -To use snakehouse just use the following in your :code:`setup.py`: - -.. code-block:: python - - from snakehouse import Multibuild, build - - extensions = build([ - Multibuild('example_module', list_of_pyx_files) - ], compiler_directives={ - 'language_level': '3', - }) - - setup(name='example_module', - version='0.1', - packages=['example_module'], - ext_modules=extensions - ) - -You can pass also :code:`setuptools`'s :code:`Extensions` objects, as detailed in -example_. - -.. _example: https://github.com/smok-serwis/snakehouse/blob/develop/example/setup.py - -Full pydoc of :code:`Multibuild` and :code:`build` is here - -.. autoclass:: snakehouse.Multibuild - :members: - -.. autofunction:: snakehouse.build - -You should use :code:`dont_snakehouse` for debugging and unit tests, as -snakehouse has a sad tendency to dump core on unhandled exceptions. To prevent that -from happening remember to handle your exceptions and debug using this flag. - -If you need to locate all .pyx files in a certain directory, you can do the following: - -.. code-block:: python - - from snakehouse import Multibuild, build, find_all - - extensions = build([ - Multibuild('example_module', find_all('src')) - ], compiler_directives={ - 'language_level': '3', - }) - -The documentation to :class:`~snakehouse.find_all` is as follows: - -.. autoclass:: snakehouse.find_all +Usage +===== + +To use snakehouse just use the following in your :code:`setup.py`: + +.. code-block:: python + + from snakehouse import Multibuild, build + + extensions = build([ + Multibuild('example_module', list_of_pyx_files) + ], compiler_directives={ + 'language_level': '3', + }) + + setup(name='example_module', + version='0.1', + packages=['example_module'], + ext_modules=extensions + ) + +You can pass also :code:`setuptools`'s :code:`Extensions` objects, as detailed in +example_. + +.. _example: https://github.com/smok-serwis/snakehouse/blob/develop/example/setup.py + +Full pydoc of :code:`Multibuild` and :code:`build` is here + +.. autoclass:: snakehouse.Multibuild + :members: + +.. autofunction:: snakehouse.build + +You should use :code:`dont_snakehouse` for debugging and unit tests, as +snakehouse has a sad tendency to dump core on unhandled exceptions. To prevent that +from happening remember to handle your exceptions and debug using this flag. + +If you need to locate all .pyx files in a certain directory, you can do the following: + +.. code-block:: python + + from snakehouse import Multibuild, build, find_all + + extensions = build([ + Multibuild('example_module', find_all('src')) + ], compiler_directives={ + 'language_level': '3', + }) + +The documentation to :class:`~snakehouse.find_all` is as follows: + +.. autoclass:: snakehouse.find_all diff --git a/docs/utilities.rst b/docs/utilities.rst index 740466266551d9e17dd364ed35d7350acae5bdc4..df44886a4b1d6fd6a403ef24e2936fa4762e8776 100644 --- a/docs/utilities.rst +++ b/docs/utilities.rst @@ -1,37 +1,37 @@ -Helper functions -================ - -Snakehouse contains a bunch of functions to help you with everyday work. They are -meant to be primarily used in your :code:`setup.py`. - -Finding files -~~~~~~~~~~~~~ - -Instead of manually specifying list of pyx and c files to compile you can use the following -functions: - -.. autofunction:: snakehouse.find_pyx - -.. autofunction:: snakehouse.find_c - -.. autofunction:: snakehouse.find_pyx_and_c - -Specifying requirements -~~~~~~~~~~~~~~~~~~~~~~~ - -If you add a MANIFEST.in file with contents: - -.. code-block:: - - include requirements.txt - -Then you can write the following in your setup.py: - -.. code-block:: python - - from snakehouse import read_requirements_txt - - setup(install_requires=read_requirements_txt()) - -.. autofunction:: snakehouse.read_requirements_txt - +Helper functions +================ + +Snakehouse contains a bunch of functions to help you with everyday work. They are +meant to be primarily used in your :code:`setup.py`. + +Finding files +~~~~~~~~~~~~~ + +Instead of manually specifying list of pyx and c files to compile you can use the following +functions: + +.. autofunction:: snakehouse.find_pyx + +.. autofunction:: snakehouse.find_c + +.. autofunction:: snakehouse.find_pyx_and_c + +Specifying requirements +~~~~~~~~~~~~~~~~~~~~~~~ + +If you add a MANIFEST.in file with contents: + +.. code-block:: + + include requirements.txt + +Then you can write the following in your setup.py: + +.. code-block:: python + + from snakehouse import read_requirements_txt + + setup(install_requires=read_requirements_txt()) + +.. autofunction:: snakehouse.read_requirements_txt + diff --git a/example/example2/__init__.py b/example/example2/__init__.py index fa84e47ee529b348bfcf12c1c64433e3c127dde4..840b5a74012dd744cda5d19baa3f9f39d6ba5977 100644 --- a/example/example2/__init__.py +++ b/example/example2/__init__.py @@ -1,5 +1,5 @@ -import logging -import typing as tp - -logger = logging.getLogger(__name__) - +import logging +import typing as tp + +logger = logging.getLogger(__name__) + diff --git a/example/example2/example.pyx b/example/example2/example.pyx index 4dcfb73e3bcb6adfd9e60c79597c93fd2bd57363..5bb8d6acf00da9b613a98ed2b4164343e4b40e35 100644 --- a/example/example2/example.pyx +++ b/example/example2/example.pyx @@ -1,2 +1,2 @@ -def test(x, y): - return x+y +def test(x, y): + return x+y diff --git a/example/example3/__init__.py b/example/example3/__init__.py index fa84e47ee529b348bfcf12c1c64433e3c127dde4..840b5a74012dd744cda5d19baa3f9f39d6ba5977 100644 --- a/example/example3/__init__.py +++ b/example/example3/__init__.py @@ -1,5 +1,5 @@ -import logging -import typing as tp - -logger = logging.getLogger(__name__) - +import logging +import typing as tp + +logger = logging.getLogger(__name__) + diff --git a/example/example3/example3/__init__.py b/example/example3/example3/__init__.py index fa84e47ee529b348bfcf12c1c64433e3c127dde4..840b5a74012dd744cda5d19baa3f9f39d6ba5977 100644 --- a/example/example3/example3/__init__.py +++ b/example/example3/example3/__init__.py @@ -1,5 +1,5 @@ -import logging -import typing as tp - -logger = logging.getLogger(__name__) - +import logging +import typing as tp + +logger = logging.getLogger(__name__) + diff --git a/example/example3/example3/example3/__init__.py b/example/example3/example3/example3/__init__.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..2e36ba8a3f8d0b688c825edf3b50d1f77a9f348d 100644 --- a/example/example3/example3/example3/__init__.py +++ b/example/example3/example3/example3/__init__.py @@ -0,0 +1,2 @@ +from example3.example3.example3.__bootstrap__ import bootstrap_cython_submodules +bootstrap_cython_submodules() diff --git a/example/example3/example3/example3/test.pyx b/example/example3/example3/example3/test.pyx index c3169d453708e44aff9970b8f007e8b94924c4d1..6bf4d58aaf9caac81209d14bf7335b5941688177 100644 --- a/example/example3/example3/example3/test.pyx +++ b/example/example3/example3/example3/test.pyx @@ -1,2 +1,2 @@ -def test(a, b): - return a+b +def test(a, b): + return a+b diff --git a/example/example_module/__init__.py b/example/example_module/__init__.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e3a236add01b14f378ff1dcdcf057ec64e12d161 100644 --- a/example/example_module/__init__.py +++ b/example/example_module/__init__.py @@ -0,0 +1,2 @@ +from example_module.__bootstrap__ import bootstrap_cython_submodules +bootstrap_cython_submodules() diff --git a/example/example_module/test.pyx b/example/example_module/test.pyx index d619e6d41164133b380059ccd2f977ceb50b40b3..05741ed0964123c7acf2be89c9afec1fc5425468 100644 --- a/example/example_module/test.pyx +++ b/example/example_module/test.pyx @@ -1,8 +1,8 @@ -import logging -import typing as tp - -logger = logging.getLogger(__name__) - -def times_two(x): - return x*2 - +import logging +import typing as tp + +logger = logging.getLogger(__name__) + +def times_two(x): + return x*2 + diff --git a/example/example_module/test2.pyx b/example/example_module/test2.pyx index 29fcd98a06327644c064bd040def06b67dfb5c67..1fe0652d1892a9ccd1e176612fe3b231f48d6b2c 100644 --- a/example/example_module/test2.pyx +++ b/example/example_module/test2.pyx @@ -1,9 +1,9 @@ -cdef extern from "test_n.h": - float _times_five(float) - - -def times_five(x): - return _times_five(x) - -def times_three(x): - return x*3 +cdef extern from "test_n.h": + float _times_five(float) + + +def times_five(x): + return _times_five(x) + +def times_three(x): + return x*3 diff --git a/example/example_module/test3/__init__.py b/example/example_module/test3/__init__.py index fa84e47ee529b348bfcf12c1c64433e3c127dde4..840b5a74012dd744cda5d19baa3f9f39d6ba5977 100644 --- a/example/example_module/test3/__init__.py +++ b/example/example_module/test3/__init__.py @@ -1,5 +1,5 @@ -import logging -import typing as tp - -logger = logging.getLogger(__name__) - +import logging +import typing as tp + +logger = logging.getLogger(__name__) + diff --git a/example/example_module/test3/test2.pyx b/example/example_module/test3/test2.pyx index 503bfe88f7a8caba07b15a05393a9e45ed05e3bb..672fc329b1abc4f1c9aaac4d763f8970a511f4a1 100644 --- a/example/example_module/test3/test2.pyx +++ b/example/example_module/test3/test2.pyx @@ -1,2 +1,2 @@ -def times_seven(a): - return a*7 +def times_seven(a): + return a*7 diff --git a/example/example_module/test3/test3.pyx b/example/example_module/test3/test3.pyx index 0d8accb9ca4f74e8aa170141d8cac94aa81c76d1..782e5d40950e9d4887016fe185689418256461b3 100644 --- a/example/example_module/test3/test3.pyx +++ b/example/example_module/test3/test3.pyx @@ -1,2 +1,2 @@ -def times_four(x): - return x*4 +def times_four(x): + return x*4 diff --git a/example/example_module/test_n.c b/example/example_module/test_n.c index a511066d49471ba9d91b94fb54996e418680c89e..d493eb7759fd3641ca6b04123ebfa6c2a0e79c52 100644 --- a/example/example_module/test_n.c +++ b/example/example_module/test_n.c @@ -1,3 +1,3 @@ -float _times_five(float v) { - return v*5; +float _times_five(float v) { + return v*5; } \ No newline at end of file diff --git a/example/example_module/test_n.h b/example/example_module/test_n.h index b03c01152c0bbc326d64e2d8981a8baf561e7290..fe3cd7d7c76058deb397de275021a2d81b46533a 100644 --- a/example/example_module/test_n.h +++ b/example/example_module/test_n.h @@ -1 +1 @@ -float _times_five(float); +float _times_five(float); diff --git a/example/setup.py b/example/setup.py index c123f29f7e92ac7ef7dedd372da2e7414c39af67..2e0723c88cb07f6add3992729a5835c6b4d8ce58 100644 --- a/example/setup.py +++ b/example/setup.py @@ -1,51 +1,47 @@ -import os - -from setuptools import setup - -from snakehouse import Multibuild, build, monkey_patch_parallel_compilation, find_pyx_and_c, \ - find_all -from setuptools import Extension - -monkey_patch_parallel_compilation() - -dont_snakehouse = False -if 'DEBUG' in os.environ: - print('Debug is enabled!') - dont_snakehouse = True - - -# note that you can include standard Extension classes in this list, those won't be touched -# and will be directed directly to Cython.Build.cythonize() -cython_multibuilds = [ - # note that Windows-style pathes are supported on Linux build environment, - # the reverse not necessarily being true (issue #5) - Multibuild('example_module', find_all('example_module', True), - define_macros=[("CYTHON_TRACE_NOGIL", "1")], - dont_snakehouse=dont_snakehouse), - Extension('example2.example', ['example2/example.pyx']), - Multibuild('example3.example3.example3', ['example3/example3/example3/test.pyx'], - dont_snakehouse=dont_snakehouse) -] - -# first argument is used directly by snakehouse, the rest and **kwargs are passed to -# Cython.Build.cythonize() -ext_modules = build(cython_multibuilds, - compiler_directives={ - 'language_level': '3', - }) - -setup(name='example_module', - version='0.1', - packages=['example_module', 'example2'], - install_requires=[ - 'Cython', 'snakehouse' - ], - zip_safe=False, - tests_require=[ - "nose2" - ], - test_suite='nose2.collector.collector', - python_requires='!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*', - ext_modules=ext_modules -) - +import os + +from setuptools import setup + +from snakehouse import Multibuild, build, monkey_patch_parallel_compilation, find_pyx_and_c, \ + find_all +from setuptools import Extension + +monkey_patch_parallel_compilation() + +dont_snakehouse = False +if 'DEBUG' in os.environ: + print('Debug is enabled!') + dont_snakehouse = True + + +# note that you can include standard Extension classes in this list, those won't be touched +# and will be directed directly to Cython.Build.cythonize() +cython_multibuilds = [ + # note that Windows-style pathes are supported on Linux build environment, + # the reverse not necessarily being true (issue #5) + Multibuild('example_module', find_all('example_module', True), + define_macros=[("CYTHON_TRACE_NOGIL", "1")], + dont_snakehouse=dont_snakehouse), + # Extension('example2.example', ['example2/example.pyx']), + # Multibuild('example3.example3.example3', ['example3/example3/example3/test.pyx'], + # dont_snakehouse=dont_snakehouse) +] + +# first argument is used directly by snakehouse, the rest and **kwargs are passed to +# Cython.Build.cythonize() +ext_modules = build(cython_multibuilds, + compiler_directives={ + 'language_level': '3', + }) + +setup(name='example_module', + version='0.1', + packages=['example_module', 'example2'], + install_requires=[ + 'Cython', 'snakehouse' + ], + zip_safe=False, + python_requires='!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*', + ext_modules=ext_modules +) + diff --git a/example/tests/__init__.py b/example/tests/__init__.py index fa84e47ee529b348bfcf12c1c64433e3c127dde4..840b5a74012dd744cda5d19baa3f9f39d6ba5977 100644 --- a/example/tests/__init__.py +++ b/example/tests/__init__.py @@ -1,5 +1,5 @@ -import logging -import typing as tp - -logger = logging.getLogger(__name__) - +import logging +import typing as tp + +logger = logging.getLogger(__name__) + diff --git a/example/tests/test_test.py b/example/tests/test_test.py index cb226877fed3ef07929d5dd2c9fee412e224e3fe..dd5181e598de75199c5c02c83eb3b7198058ac3b 100644 --- a/example/tests/test_test.py +++ b/example/tests/test_test.py @@ -1,30 +1,30 @@ -from example_module.test import times_two -from example_module.test2 import times_three, times_five -from example_module.test3.test3 import times_four -from example_module.test3.test2 import times_seven -from example2.example import test -from example3.example3.example3.test import test as test_three -import unittest - - -class TestExample(unittest.TestCase): - def test_seven(self): - self.assertEqual(times_seven(4), 4*7) - - def test_three(self): - self.assertEqual(test_three(2, 3), 5) - - def test_test(self): - self.assertEqual(test(2, 3), 5) - - def test_five(self): - self.assertEqual(times_five(2), 10) - - def test_two(self): - self.assertEqual(times_two(2), 4) - - def test_three(self): - self.assertEqual(times_three(2), 6) - - def test_four(self): - self.assertEqual(times_four(2), 8) +from example_module.test import times_two +from example_module.test2 import times_three, times_five +from example_module.test3.test3 import times_four +from example_module.test3.test2 import times_seven +from example2.example import test +from example3.example3.example3.test import test as test_three +import unittest + + +class TestExample(unittest.TestCase): + def test_seven(self): + self.assertEqual(times_seven(4), 4*7) + + def test_three(self): + self.assertEqual(test_three(2, 3), 5) + + def test_test(self): + self.assertEqual(test(2, 3), 5) + + def test_five(self): + self.assertEqual(times_five(2), 10) + + def test_two(self): + self.assertEqual(times_two(2), 4) + + def test_three(self): + self.assertEqual(times_three(2), 6) + + def test_four(self): + self.assertEqual(times_four(2), 8) diff --git a/requirements.txt b/requirements.txt index dcb94b4e453994b2ede1a5d1815bf587d7aa317d..4700652c0eaa5ed4a31a3439b24aaf55b9e1aeff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Cython -mako==1.1.0 -satella==2.20.0 -MarkupSafe==1.1.1 +Cython +mako==1.1.0 +satella==2.20.0 +MarkupSafe==1.1.1 diff --git a/setup.cfg b/setup.cfg index 00677200a89039df3be2c41b665bcf8d45663880..8b7cea7daa005bc4aa4f515deb32c8fd429b34ea 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,60 +1,57 @@ -# coding: utf-8 -[metadata] -name = snakehouse -keywords = cython, extension, multiple, pyx -version = 1.7 -long-description = file: README.md -long-description-content-type = text/markdown; charset=UTF-8 -license_files = LICENSE -author = Piotr Maślanka -author_email = pmaslanka@smok.co -description = Utilities for packing multiple pyx files into a single Cython extension -url = https://github.com/smok-serwis/snakehouse -project-urls = - Code = https://github.com/smok-serwis/snakehouse - Issue tracker = https://github.com/smok-serwis/snakehouse/issues -classifier = - Programming Language :: Python - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3.12 - Programming Language :: Python :: Implementation :: CPython - Operating System :: OS Independent - Development Status :: 5 - Production/Stable - License :: OSI Approved :: MIT License - Topic :: Software Development :: Code Generators - Topic :: Software Development :: Build Tools - -[options] -install_requires = - Cython - mako == 1.1.0 - satella == 2.20.0 - MarkupSafe == 1.1.1 - -python_requires = !=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.* -packages = find: - - -[options.packages.find] -exclude = - docs - example - -[options.package_data] -snakehouse = templates/*.mako - -[pycodestyle] -max-line-length = 100 - -[pep8] -max-line-length = 100 - -[bdist_wheel] -universal = 0 - +# coding: utf-8 +[metadata] +name = snakehouse +keywords = cython, extension, multiple, pyx +version = 1.7 +long_description = file: README.md +long_description-content-type = text/markdown; charset=UTF-8 +license_files = LICENSE +author = Piotr Maślanka +author_email = pmaslanka@smok.co +description = Utilities for packing multiple pyx files into a single Cython extension +url = https://github.com/smok-serwis/snakehouse +project_urls = + Code = https://github.com/smok-serwis/snakehouse + Issue tracker = https://github.com/smok-serwis/snakehouse/issues +classifier = + Programming Language :: Python + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 + Programming Language :: Python :: Implementation :: CPython + Operating System :: OS Independent + Development Status :: 5 - Production/Stable + License :: OSI Approved :: MIT License + Topic :: Software Development :: Code Generators + Topic :: Software Development :: Build Tools + +[options] +install_requires = + Cython + satella + +python_requires = !=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.* +packages = snakehouse + + +[options.packages.find] +exclude = + docs + example + +[pycodestyle] +max-line-length = 100 + +[pep8] +max-line-length = 100 + +[bdist_wheel] +universal = 0 + +[aliases] +test=pytest \ No newline at end of file diff --git a/setup.py b/setup.py index b024da80e9c1c8c800cc1b46e90c1e783cb446cc..93213959e1b316e30d2d1de32c43335306647c69 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import setup - - -setup() +from setuptools import setup + + +setup(tests_require=['pytest']) diff --git a/snakehouse/__init__.py b/snakehouse/__init__.py index 70f9ed1c2e9b9e3bf048fe7bce9f43b8e575e788..f9b0063608886d64f25c63d13ac6c6e9fb9cfbca 100644 --- a/snakehouse/__init__.py +++ b/snakehouse/__init__.py @@ -1,10 +1,10 @@ -import pkg_resources -from .build import build -from .multibuild import Multibuild, find_all -from .faster_builds import monkey_patch_parallel_compilation -from .requirements import read_requirements_txt, find_c, find_pyx_and_c, find_pyx - -try: - __version__ = pkg_resources.require('snakehouse')[0].version -except pkg_resources.DistributionNotFound: - __version__ = '1.7' +import pkg_resources +from .build import build +from .multibuild import Multibuild, find_all +from .faster_builds import monkey_patch_parallel_compilation +from .requirements import read_requirements_txt, find_c, find_pyx_and_c, find_pyx + +try: + __version__ = pkg_resources.require('snakehouse')[0].version +except pkg_resources.DistributionNotFound: + __version__ = '1.7' diff --git a/snakehouse/templates/bootstrap.mako b/snakehouse/bootstrap.py similarity index 51% rename from snakehouse/templates/bootstrap.mako rename to snakehouse/bootstrap.py index 9bebeb0a03d3e4864abe59f0d3477fa30a3bbb36..989aa56adf3f60a59486206de5c06c443bbf5192 100644 --- a/snakehouse/templates/bootstrap.mako +++ b/snakehouse/bootstrap.py @@ -1,5 +1,8 @@ -import sys +from .templating import Output +def get_file(cdef_sections=None, get_definition_sections=None, module_set=None): + output = Output() + output.append_many("""import sys cdef extern from "Python.h": ctypedef struct PyModuleDef: const char* m_name; @@ -7,29 +10,29 @@ cdef extern from "Python.h": void Py_INCREF(object) object PyModule_FromDefAndSpec(PyModuleDef *definition, object spec) int PyModule_ExecDef(object module, PyModuleDef* definition) +""") + for cdef_section in cdef_sections: + output.append_many(""" +cdef extern from "{cdef_section.h_file_name}": + object PyInit_{cdef_section.coded_module_name}() +""", cdef_section=cdef_section) -% for cdef_section in cdef_sections: -cdef extern from "${cdef_section.h_file_name}": - object PyInit_${cdef_section.coded_module_name}() -% endfor - -cdef object get_definition_by_name(str name): -% for i, getdef_section in enumerate(get_definition_sections): -% if i == 0: - if name == "${getdef_section.module_name}": - return PyInit_${getdef_section.coded_module_name}() -% else: - elif name == "${getdef_section.module_name}": - return PyInit_${getdef_section.coded_module_name}() -% endif -% endfor - + output.append('cdef object get_definition_by_name(str name):') + for i, getdef_section in enumerate(get_definition_sections): + if i == 0: + output.append_many(""" + if name == "{getdef_section.module_name}": + return PyInit_{getdef_section.coded_module_name}()""", getdef_section=getdef_section) + else: + output.append_many(f""" elif name == "{getdef_section.module_name}": + return PyInit_{getdef_section.coded_module_name}()""", getdef_section=getdef_section) + output.append_many(""" cdef class CythonPackageLoader: cdef PyModuleDef* definition cdef object def_o cdef str name - + def __init__(self, name): self.def_o = get_definition_by_name(name) self.definition = <PyModuleDef*>self.def_o @@ -47,10 +50,9 @@ cdef class CythonPackageLoader: def exec_module(self, module): PyModule_ExecDef(module, self.definition) - class CythonPackageMetaPathFinder: def __init__(self, modules_set): - self.modules_set = modules_set + self.modules_set = frozenset(modules_set) def find_module(self, fullname, path): if fullname not in self.modules_set: @@ -61,5 +63,7 @@ class CythonPackageMetaPathFinder: pass def bootstrap_cython_submodules(): - modules_set = ${module_set} - sys.meta_path.append(CythonPackageMetaPathFinder(modules_set)) + sys.meta_path.append(CythonPackageMetaPathFinder({module_set})) +""", module_set=module_set) + + return str(output) \ No newline at end of file diff --git a/snakehouse/build.py b/snakehouse/build.py index 5d7186831804e281f1adb202e9ed2619b148b5ea..f9dd3437af41a1313f5e5c49be541dc152c17dfe 100644 --- a/snakehouse/build.py +++ b/snakehouse/build.py @@ -1,36 +1,38 @@ -import logging -import multiprocessing -import sys -import typing as tp -from Cython.Build import cythonize -from setuptools import Extension -from .multibuild import Multibuild - -MultiBuildType = tp.Union[Multibuild, Exception] - -logger = logging.getLogger(__name__) - - -def build(extensions: tp.List[MultiBuildType], *args, nthreads=None, **kwargs): - if nthreads is None: - nthreads = multiprocessing.cpu_count() - if sys.platform == 'win32': - print('Sorry, multiprocessing is not yet supported on Windows') - nthreads = 0 - kwargs['nthreads'] = nthreads - returns = [] - multi_builds = [] - for multi_build in extensions: - if isinstance(multi_build, Extension): - returns.append(multi_build) - elif isinstance(multi_build, Multibuild): - multi_build.generate() - multi_builds.append(multi_build) - returns.extend(multi_build.for_cythonize()) - else: - raise ValueError('Invalid value in list, expected either an instance of Multibuild ' - 'or an Extension') - values = cythonize(returns, *args, **kwargs) - for multi_build in multi_builds: - multi_build.do_after_cython() - return values +import logging +import multiprocessing +import sys +import typing as tp +from Cython.Build import cythonize +from setuptools import Extension +from .multibuild import Multibuild + +MultiBuildType = tp.Union[Multibuild, Exception] + +logger = logging.getLogger(__name__) + + +def build(extensions: tp.List[MultiBuildType], *args, nthreads=None, + debug: bool = False, **kwargs): + """A call to build things. """ + if nthreads is None: + nthreads = multiprocessing.cpu_count() + if sys.platform == 'win32': + print('Sorry, multiprocessing is not yet supported on Windows') + nthreads = 0 + kwargs['nthreads'] = nthreads + returns = [] + multi_builds = [] + for multi_build in extensions: + if isinstance(multi_build, Extension): + returns.append(multi_build) + elif isinstance(multi_build, Multibuild): + multi_build.generate() + multi_builds.append(multi_build) + returns.extend(multi_build.for_cythonize()) + else: + raise ValueError('Invalid value in list, expected either an instance of Multibuild ' + 'or an Extension') + values = cythonize(returns, *args, **kwargs) + for multi_build in multi_builds: + multi_build.do_after_cython() + return values diff --git a/snakehouse/faster_builds.py b/snakehouse/faster_builds.py index 8c1d90eb8fc48a05b8b37ab43d29f06bb77b7a8f..0189751ddcf170172fba439db45412407879e35c 100644 --- a/snakehouse/faster_builds.py +++ b/snakehouse/faster_builds.py @@ -1,49 +1,49 @@ -import multiprocessing -import typing as tp -import sys - -__all__ = ['monkey_patch_parallel_compilation'] - - -def monkey_patch_parallel_compilation(cores: tp.Optional[int] = None) -> None: - """ - This monkey-patches distutils to provide parallel compilation, even if you have - a single extension built from multiple .c files. - - Invoke in your setup.py file - - :param cores: amount of cores. Leave at default (None) for autodetection. - """ - if sys.platform == 'win32': - print('Sorry, parallel builds are not supported on Windows') - return - - if cores is None: - cores = multiprocessing.cpu_count() - - # monkey-patch for parallel compilation - def parallelCCompile(self, sources, output_dir=None, macros=None, include_dirs=None, debug=0, - extra_preargs=None, extra_postargs=None, depends=None): - # those lines are copied from distutils.ccompiler.CCompiler directly - macros, objects, extra_postargs, pp_opts, build = self._setup_compile(output_dir, macros, - include_dirs, sources, - depends, - extra_postargs) - cc_args = self._get_cc_args(pp_opts, debug, extra_preargs) - # parallel code - import multiprocessing.pool - - def single_compile(obj): - try: - src, ext = build[obj] - except KeyError: - return - self._compile(obj, src, ext, cc_args, extra_postargs, pp_opts) - - # evaluate everything - for _ in multiprocessing.pool.ThreadPool(cores).imap(single_compile, objects): - pass - return objects - - import distutils.ccompiler - distutils.ccompiler.CCompiler.compile = parallelCCompile +import multiprocessing +import typing as tp +import sys + +__all__ = ['monkey_patch_parallel_compilation'] + + +def monkey_patch_parallel_compilation(cores: tp.Optional[int] = None) -> None: + """ + This monkey-patches distutils to provide parallel compilation, even if you have + a single extension built from multiple .c files. + + Invoke in your setup.py file + + :param cores: amount of cores. Leave at default (None) for autodetection. + """ + if sys.platform == 'win32': + print('Sorry, parallel builds are not supported on Windows') + return + + if cores is None: + cores = multiprocessing.cpu_count() + + # monkey-patch for parallel compilation + def parallelCCompile(self, sources, output_dir=None, macros=None, include_dirs=None, debug=0, + extra_preargs=None, extra_postargs=None, depends=None): + # those lines are copied from distutils.ccompiler.CCompiler directly + macros, objects, extra_postargs, pp_opts, build = self._setup_compile(output_dir, macros, + include_dirs, sources, + depends, + extra_postargs) + cc_args = self._get_cc_args(pp_opts, debug, extra_preargs) + # parallel code + import multiprocessing.pool + + def single_compile(obj): + try: + src, ext = build[obj] + except KeyError: + return + self._compile(obj, src, ext, cc_args, extra_postargs, pp_opts) + + # evaluate everything + for _ in multiprocessing.pool.ThreadPool(cores).imap(single_compile, objects): + pass + return objects + + import distutils.ccompiler + distutils.ccompiler.CCompiler.compile = parallelCCompile diff --git a/snakehouse/multibuild.py b/snakehouse/multibuild.py index 1ec2a33cdf2bb92a07077d5ff60682f8087caf95..b62f63a03c1d16c168ea734d361d833ab113fd50 100644 --- a/snakehouse/multibuild.py +++ b/snakehouse/multibuild.py @@ -1,250 +1,293 @@ -import hashlib -import itertools -import os -import logging -import collections -import typing as tp -import warnings - -import pkg_resources -from satella.files import split, find_files -from mako.template import Template -from setuptools import Extension - - -CdefSection = collections.namedtuple('CdefSection', ('h_file_name', 'module_name', 'coded_module_name')) -GetDefinitionSection = collections.namedtuple('GetDefinitionSection', ( - 'module_name', 'pyinit_name', 'coded_module_name' -)) - -logger = logging.getLogger(__name__) - - -def load_mako_lines(template_name: str) -> tp.List[str]: - return pkg_resources.resource_string('snakehouse', os.path.join('templates', template_name)).decode('utf8') - - -def cull_path(path): - if not path: - return path - if path[0] == os.path.sep: - if len(path) > 1: - if path[1] == os.path.sep: - return path[2:] - return path[1:] - else: - return path - - -def render_mako(template_name: str, **kwargs) -> str: - tpl = Template(pkg_resources.resource_string( - 'snakehouse', os.path.join('templates', template_name)).decode('utf8')) - return tpl.render(**kwargs) - - -LINES_IN_HFILE = len(load_mako_lines('hfile.mako').split('\n')) - - -class find_all: - """ - A directive for :class:`snakehouse.Multibuild` to locate all .pyx - files, and possibly all the .c files depending on the switch - - :param directory: base directory to look for files in - :param include_c_files: whether to also hook up the located .c files (default False) - :param only_c_files: whether to look up only .c files (default False) - """ - - def __init__(self, directory: str, include_c_files: bool = False, - only_c_files: bool = False): - self.dir = directory - self.include_c_files = include_c_files - self.only_c_files = only_c_files - - def __iter__(self): - if self.only_c_files: - pyx_files = [] - else: - pyx_files = find_files(self.dir, r'(.*)\.pyx', scan_subdirectories=True) - - if self.include_c_files: - pyx_files = itertools.chain(pyx_files, - find_files(self.dir, r'(.*)\.c', scan_subdirectories=True)) - return pyx_files - - -class Multibuild: - """ - This specifies a single Cython extension, called {extension_name}.__bootstrap__ - - All kwargs will be sent straight to Cython's Extension - - :param extension_name: the module name - :param files: list of pyx and c files - :param kwargs: extra arguments to be passed to Extension() object - :param dont_snakehouse: snakehouse won't be enabled, each element will be built - as a separate extension. It is for these cases when you're testing and something segfaults. - """ - def __init__(self, extension_name: str, files: tp.Iterator[str], - dont_snakehouse: bool = False, - **kwargs): - # sanitize path separators so that Linux-style paths are supported on Windows - logger.warning('Building extension %s with files %s', extension_name, files) - files = list(files) - self.dont_snakehouse = dont_snakehouse - self.kwargs = kwargs - if files: - files = [os.path.join(*split(file)) for file in files] - self.files = list([file for file in files if not file.endswith('__bootstrap__.pyx')]) - self.pyx_files = [file for file in self.files if file.endswith('.pyx')] - self.c_files = [file for file in self.files if file.endswith('.c') or file.endswith('.cpp')] - else: - self.pyx_files = [] - self.c_files = [] - - self.do_generate = True - if not self.pyx_files: - warnings.warn('No pyx files, probably installing from a source archive, skipping ' - 'generating files', RuntimeWarning) - self.do_generate = False - else: - self.extension_name = extension_name # type: str - if len(self.files) == 1: - self.bootstrap_directory, _ = os.path.split(self.files[0]) # type: str - else: - self.bootstrap_directory = os.path.commonpath(self.files) # type: str - self.modules = [] # type: tp.List[tp.Tuple[str, str, str]] - self.module_name_to_loader_function = {} - for filename in self.pyx_files: - with open(filename, 'rb') as f_in: - self.module_name_to_loader_function[filename] = hashlib.sha256(f_in.read()).hexdigest() - - def generate_header_files(self): - for filename in self.pyx_files: - path, name, cmod_name_path, module_name, coded_module_name, complete_module_name = self.transform_module_name(filename) - if not name.endswith('.pyx'): - continue - - h_file = filename.replace('.pyx', '.h') - - if os.path.exists(h_file): - with open(h_file, 'r') as f_in: - data = f_in.readlines() - - linesep = 'cr' if '\r\n' in data[0] else 'lf' - rendered_mako = render_mako('hfile.mako', initpy_name=coded_module_name) + \ - '\r\n' if linesep == 'cr' else '\n' - assert len(rendered_mako) > 0 - - if any('#define SNAKEHOUSE_FILE' in line for line in data): - data = [rendered_mako, *data[LINES_IN_HFILE:]] - else: - data = [rendered_mako, *data] - else: - rendered_mako = render_mako('hfile.mako', initpy_name=coded_module_name) - assert len(rendered_mako) > 0 - data = rendered_mako - - with open(h_file, 'w') as f_out: - f_out.write(''.join(data)) - - def transform_module_name(self, filename): - path, name = os.path.split(filename) - module_name = name.replace('.pyx', '') - if path.startswith(self.bootstrap_directory): - cmod_name_path = path[len(self.bootstrap_directory):] - else: - cmod_name_path = path - path = cull_path(path) - cmod_name_path = cull_path(cmod_name_path) - - if path: - intro = '.'.join((e for e in cmod_name_path.split(os.path.sep) if e)) - if not intro: - complete_module_name = '%s.%s' % (self.extension_name, module_name) - else: - complete_module_name = '%s.%s.%s' % (self.extension_name, - intro, - module_name) - else: - complete_module_name = '%s.%s' % (self.extension_name, module_name) - - coded_module_name = self.module_name_to_loader_function[filename] - return path, name, cmod_name_path, module_name, coded_module_name, complete_module_name - - def do_after_cython(self): - if self.dont_snakehouse: - return - self.generate_header_files() - for filename in self.pyx_files: - path, name, cmod_name_path, module_name, coded_module_name, complete_module_name = self.transform_module_name(filename) - to_replace = '__Pyx_PyMODINIT_FUNC PyInit_%s' % (module_name, ) - replace_with = '__Pyx_PyMODINIT_FUNC PyInit_%s' % (coded_module_name, ) - with open(filename.replace('.pyx', '.c'), 'r') as f_in: - data_in = f_in.read() - data = data_in.replace(to_replace, replace_with) - with open(filename.replace('.pyx', '.c'), 'w') as f_out: - f_out.write(data) - - def generate_bootstrap(self) -> str: - - cdef_section = [] - for filename in self.pyx_files: - path, name, cmod_name_path, module_name, coded_module_name, complete_module_name = self.transform_module_name(filename) - - if os.path.exists(filename.replace('.pyx', '.c')): - os.unlink(filename.replace('.pyx', '.c')) - - h_path_name = os.path.join(cmod_name_path, name.replace('.pyx', '.h')).replace('\\', '\\\\') - - cdef_section.append(CdefSection(h_path_name, module_name, coded_module_name)) - - self.modules.append((complete_module_name, module_name, coded_module_name)) - - get_definition = [] - for mod_name, init_fun_name, coded_module_name in self.modules: - get_definition.append(GetDefinitionSection(mod_name, init_fun_name, coded_module_name)) - - return render_mako('bootstrap.mako', cdef_sections=cdef_section, - get_definition_sections=get_definition, - module_set=repr(set(x[0] for x in self.modules))) - - def write_bootstrap_file(self): - with open(os.path.join(self.bootstrap_directory, '__bootstrap__.pyx'), 'w') as f_out: - f_out.write(self.generate_bootstrap()) - - def alter_init(self): - if os.path.exists(os.path.join(self.bootstrap_directory, '__init__.py')): - with open(os.path.join(self.bootstrap_directory, '__init__.py'), 'r') as f_in: - data = f_in.read() - else: - data = '' - - if 'bootstrap_cython_submodules' not in data: - data = render_mako('initpy.mako', module_name=self.extension_name) + data - - with open(os.path.join(self.bootstrap_directory, '__init__.py'), 'w') as f_out: - f_out.write(data) - - def generate(self): - if not self.dont_snakehouse and self.do_generate: - self.write_bootstrap_file() - self.alter_init() - - def for_cythonize(self, *args, **kwargs): - if self.dont_snakehouse: - extensions = [] - len_to_sub = len(self.bootstrap_directory) + len(os.path.pathsep) - for pyx_file in self.pyx_files: - file_name = pyx_file[len_to_sub:-4].replace('\\', '.').replace('/', '.') - ext = Extension(self.extension_name+'.'+file_name, - [pyx_file] + self.c_files, *args, **kwargs) - extensions.append(ext) - return extensions - else: - kwargs.update(self.kwargs) - for_cythonize = [*self.files, os.path.join(self.bootstrap_directory, '__bootstrap__.pyx')] - return [Extension(self.extension_name+".__bootstrap__", - for_cythonize, - *args, - **kwargs)] +import hashlib +import itertools +import os +import logging +import collections +import typing as tp +import warnings + +import pkg_resources +from satella.files import split, find_files +from setuptools import Extension +from .templating import HFILE_MAKO, INITPY_MAKO +from. bootstrap import get_file + + +CdefSection = collections.namedtuple('CdefSection', ('h_file_name', 'module_name', 'coded_module_name')) +GetDefinitionSection = collections.namedtuple('GetDefinitionSection', ( + 'module_name', 'pyinit_name', 'coded_module_name' +)) + +logger = logging.getLogger(__name__) + + +def cull_path(path): + if not path: + return path + if path[0] == os.path.sep: + if len(path) > 1: + if path[1] == os.path.sep: + return path[2:] + return path[1:] + else: + return path + + +LINES_IN_HFILE = len(HFILE_MAKO.split('\n')) + + +class find_all: + """ + A directive for :class:`snakehouse.Multibuild` to locate all .pyx + files, and possibly all the .c files depending on the switch + + :param directory: base directory to look for files in + :param include_c_files: whether to also hook up the located .c files (default False) + :param only_c_files: whether to look up only .c files (default False) + """ + + def __init__(self, directory: str, include_c_files: bool = False, + only_c_files: bool = False): + self.dir = directory + self.include_c_files = include_c_files + self.only_c_files = only_c_files + + def __iter__(self): + if self.only_c_files: + pyx_files = [] + else: + pyx_files = find_files(self.dir, r'(.*)\.pyx', scan_subdirectories=True) + + if self.include_c_files: + pyx_files = itertools.chain(pyx_files, + find_files(self.dir, r'(.*)\.c', scan_subdirectories=True)) + return pyx_files + +class BreakException(Exception): + pass +class Multibuild: + """ + This specifies a single Cython extension, called {extension_name}.__bootstrap__ + + All kwargs will be sent straight to Cython's Extension + + :param extension_name: the module name + :param files: list of pyx and c files + :param kwargs: extra arguments to be passed to Extension() object + :param dont_snakehouse: snakehouse won't be enabled, each element will be built + as a separate extension. It is for these cases when you're testing and something segfaults. + """ + def __init__(self, extension_name: str, files: tp.Iterator[str], + dont_snakehouse: bool = False, + **kwargs): + # sanitize path separators so that Linux-style paths are supported on Windows + logger.warning('Building extension %s with files %s', extension_name, files) + files = list(files) + self.dont_snakehouse = dont_snakehouse + self.kwargs = kwargs + self.clear_all_bootstraps() + if files: + files = [os.path.join(*split(file)) for file in files] + self.files = list([file for file in files if not file.endswith('__bootstrap__.pyx')]) + self.pyx_files = [file for file in self.files if file.endswith('.pyx')] + self.c_files = [file for file in self.files if file.endswith('.c') or file.endswith('.cpp')] + else: + self.pyx_files = [] + self.c_files = [] + + self.do_generate = True + if not self.pyx_files: + warnings.warn('No pyx files, probably installing from a source archive, skipping ' + 'generating files', RuntimeWarning) + self.do_generate = False + else: + self.extension_name = extension_name # type: str + if len(self.files) == 1: + self.bootstrap_directory, _ = os.path.split(self.files[0]) # type: str + else: + self.bootstrap_directory = os.path.commonpath(self.files) # type: str + self.modules = [] # type: tp.List[tp.Tuple[str, str, str]] + self.module_name_to_loader_function = {} + for filename in self.pyx_files: + with open(filename, 'rb') as f_in: + self.module_name_to_loader_function[filename] = hashlib.sha256(f_in.read()).hexdigest() + + def fix_all_c_files(self): + return + for root, dirs, files in os.walk("."): + try: + for c_name in files: + for allowed in ['.h', '.c', '.pyx', '.pxd']: + if not c_name.endswith(allowed): + raise BreakException + name = os.path.join(root, c_name) + if c_name == '__bootstrap__.c': + print('Skipping %s' % (c_name, )) + continue + + if not c_name.endswith('.c'): + continue + with open(name, 'r', encoding='utf-8') as fin: + data = fin.read() + is_windows = '\r\n' in data + + with open(name, 'w', encoding='utf-8') as fout: + c_name = c_name.replace('.c', '.h') + fout.write('#include "%s"' % (c_name, )) + fout.write('\r\n' if is_windows else '\n') + fout.write(data) + except BreakException: + pass + + def generate_header_files(self): + for filename in self.pyx_files: + path, name, cmod_name_path, module_name, coded_module_name, complete_module_name = self.transform_module_name(filename) + if not name.endswith('.pyx'): + continue + + h_file = filename.replace('.pyx', '.h') + + if os.path.exists(h_file): + with open(h_file, 'r', encoding='utf-8') as f_in: + data = f_in.readlines() + print(f'writing {data} to {h_file}') + data = ['#include "Python.h"\n'] + rendered_mako = HFILE_MAKO.format(initpy_name=coded_module_name) + + if any('#define SNAKEHOUSE_FILE' in line for line in data): + data = [*data, rendered_mako] + else: + data = [*data, rendered_mako] + else: + rendered_mako = HFILE_MAKO.format(initpy_name=coded_module_name) + assert len(rendered_mako) > 0 + data = '#include "Python.h"\n' + rendered_mako + + with open(h_file, 'w') as f_out: + f_out.write(''.join(data)) + + def transform_module_name(self, filename): + path, name = os.path.split(filename) + module_name = name.replace('.pyx', '') + if path.startswith(self.bootstrap_directory): + cmod_name_path = path[len(self.bootstrap_directory):] + else: + cmod_name_path = path + path = cull_path(path) + cmod_name_path = cull_path(cmod_name_path) + + if path: + intro = '.'.join((e for e in cmod_name_path.split(os.path.sep) if e)) + if not intro: + complete_module_name = '%s.%s' % (self.extension_name, module_name) + else: + complete_module_name = '%s.%s.%s' % (self.extension_name, + intro, + module_name) + else: + complete_module_name = '%s.%s' % (self.extension_name, module_name) + + coded_module_name = self.module_name_to_loader_function[filename] + return path, name, cmod_name_path, module_name, coded_module_name, complete_module_name + + def do_after_cython(self): + if self.dont_snakehouse: + return + self.generate_header_files() + for filename in self.pyx_files: + path, name, cmod_name_path, module_name, coded_module_name, complete_module_name = self.transform_module_name(filename) + to_replace = '__Pyx_PyMODINIT_FUNC PyInit_%s' % (module_name, ) + replace_with = '__Pyx_PyMODINIT_FUNC PyInit_%s' % (coded_module_name, ) + with open(filename.replace('.pyx', '.c'), 'r') as f_in: + data_in = f_in.read() + data = data_in.replace(to_replace, replace_with) + with open(filename.replace('.pyx', '.c'), 'w') as f_out: + f_out.write(data) + for root, dirs, files in os.walk('.'): + for file in files: + c_name = os.path.join(root, file) + if file == '__bootstrap__.c': + with open(c_name, 'r', encoding='utf-8') as fin: + data = fin.read() + data = data.replace('__Pyx_PyMODINIT_FUNC PyInit___bootstrap__(void)', + 'PyMODINIT_FUNC PyInit__'+coded_module_name+'(void)') + data = data.replace('__Pyx_PyMODINIT_FUNC init__bootstrap__(void)', + 'PyMODINIT_FUNC init__'+coded_module_name+'(void)') + with open(c_name, 'w', encoding='utf-8') as fout: + for filename in self.pyx_files: + path, name, cmod_name_path, module_name, coded_module_name, complete_module_name = self.transform_module_name( + filename) + h_path_name = os.path.join(cmod_name_path, name.replace('.pyx', '.h')).replace('\\', '\\\\') + fout.write('#include "%s"\n' % (h_path_name, )) + fout.write(data) + c_name = os.path.join(root, file) + + def generate_bootstrap(self) -> str: + + cdef_section = [] + for filename in self.pyx_files: + path, name, cmod_name_path, module_name, coded_module_name, complete_module_name = self.transform_module_name(filename) + + if os.path.exists(filename.replace('.pyx', '.c')): + os.unlink(filename.replace('.pyx', '.c')) + + h_path_name = os.path.join(cmod_name_path, name.replace('.pyx', '.h')).replace('\\', '\\\\') + + cdef_section.append(CdefSection(h_path_name, module_name, coded_module_name)) + + self.modules.append((complete_module_name, module_name, coded_module_name)) + + get_definition = [] + for mod_name, init_fun_name, coded_module_name in self.modules: + get_definition.append(GetDefinitionSection(mod_name, init_fun_name, coded_module_name)) + + return get_file(cdef_sections=cdef_section, get_definition_sections=get_definition, + module_set=[x[0] for x in self.modules]) + + def clear_all_bootstraps(self): + for root, dirs, files in os.walk('.'): + for file in files: + c_name = os.path.join(root, file) + if c_name.startswith('__bootstrap__'): + os.unlink(c_name) + + def write_bootstrap_file(self): + with open(os.path.join(self.bootstrap_directory, '__bootstrap__.pyx'), 'w') as f_out: + f_out.write(self.generate_bootstrap()) + + def alter_init(self): + if os.path.exists(os.path.join(self.bootstrap_directory, '__init__.py')): + with open(os.path.join(self.bootstrap_directory, '__init__.py'), 'r') as f_in: + data = f_in.read() + else: + data = '' + + if 'bootstrap_cython_submodules' not in data: + data = INITPY_MAKO.format(module_name=self.extension_name) + data + + with open(os.path.join(self.bootstrap_directory, '__init__.py'), 'w') as f_out: + f_out.write(data) + + def generate(self): + if not self.dont_snakehouse and self.do_generate: + self.write_bootstrap_file() + self.alter_init() + + def for_cythonize(self, debug: bool = False, *args, **kwargs): + if self.dont_snakehouse: + extensions = [] + len_to_sub = len(self.bootstrap_directory) + len(os.path.pathsep) + for pyx_file in self.pyx_files: + file_name = pyx_file[len_to_sub:-4].replace('\\', '.').replace('/', '.') + ext = Extension(self.extension_name+'.'+file_name, + [pyx_file] + self.c_files, *args, **kwargs) + extensions.append(ext) + return extensions + else: + kwargs.update(self.kwargs) + for_cythonize = list(set([*self.files, os.path.join(self.bootstrap_directory, '__bootstrap__.pyx')])) + print(f'Rolling for cythonize {for_cythonize}') + return [Extension(self.extension_name+".__bootstrap__", + for_cythonize, + *args, + **kwargs)] diff --git a/snakehouse/requirements.py b/snakehouse/requirements.py index 85eb4e8bddee486671960f6646d4871d8889847f..1e8fbe03d5ec48a6ec0f1b4796a5c24c5591afc3 100644 --- a/snakehouse/requirements.py +++ b/snakehouse/requirements.py @@ -1,64 +1,62 @@ -import typing as tp -import warnings - -from satella.coding import for_argument -from satella.files import read_lines, find_files - - -@for_argument(returns=list) -def find_pyx(directory_path: str) -> tp.List[str]: - """ - Return all .pyx files found in given directory. - - :param directory_path: directory to look through - :return: .pyx files found - """ - warnings.warn('This is deprecated. Use find_all instead', DeprecationWarning) - return find_files(directory_path, '(.*)\\.pyx$', scan_subdirectories=True) - - -@for_argument(returns=list) -def find_c(directory_path: str) -> tp.List[str]: - """ - Return all .c files found in given directory. - - :param directory_path: directory to look through - :return: .c files found - """ - warnings.warn('This is deprecated. Use find_all instead', DeprecationWarning) - return find_files(directory_path, '(.*)\\.c$', scan_subdirectories=True) - - -def find_pyx_and_c(directory_path: str) -> tp.List[str]: - """ - Return a list of all .pyx and .c files found in given directory. - - :param directory_path: - :return: list of all .pyx and .c files found in given directory - """ - warnings.warn('This is deprecated. Use find_all instead', DeprecationWarning) - files = find_pyx(directory_path) - files.extend(find_c(directory_path)) - return files - - -def read_requirements_txt(path: str = 'requirements.txt'): - """ - Read requirements.txt and parse it into a list of packages - as given by setup(install_required=). - - This means it will read in all lines, discard empty and commented ones, - and discard all those who are an URL. - - Remember to include your requirements.txt inside your MANIFEST.in! - - :param path: path to requirements.txt. Default is `requirements.txt`. - :return: list of packages ready to be fed to setup(install_requires=) - """ - lines = read_lines(path) - lines = (line.strip() for line in lines) - lines = (line for line in lines if not line.startswith('#')) - lines = (line for line in lines if not line.startswith('git+')) - lines = (line for line in lines if not line.startswith('http')) - lines = (line for line in lines if line) - return list(lines) +import typing as tp +import warnings + +from satella.coding import for_argument +from satella.files import read_lines, find_files + + +def find_pyx(directory_path: str) -> tp.List[str]: + """ + Return all .pyx files found in given directory. + + :param directory_path: directory to look through + :return: .pyx files found + """ + warnings.warn('This is deprecated. Use find_all instead', DeprecationWarning) + return list(find_files(directory_path, '(.*)\\.pyx$', scan_subdirectories=True)) + + +def find_c(directory_path: str) -> tp.List[str]: + """ + Return all .c files found in given directory. + + :param directory_path: directory to look through + :return: .c files found + """ + warnings.warn('This is deprecated. Use find_all instead', DeprecationWarning) + return list(find_files(directory_path, '(.*)\\.c$', scan_subdirectories=True)) + + +def find_pyx_and_c(directory_path: str) -> tp.List[str]: + """ + Return a list of all .pyx and .c files found in given directory. + + :param directory_path: + :return: list of all .pyx and .c files found in given directory + """ + warnings.warn('This is deprecated. Use find_all instead', DeprecationWarning) + files = find_pyx(directory_path) + files.extend(find_c(directory_path)) + return files + + +def read_requirements_txt(path: str = 'requirements.txt'): + """ + Read requirements.txt and parse it into a list of packages + as given by setup(install_required=). + + This means it will read in all lines, discard empty and commented ones, + and discard all those who are an URL. + + Remember to include your requirements.txt inside your MANIFEST.in! + + :param path: path to requirements.txt. Default is `requirements.txt`. + :return: list of packages ready to be fed to setup(install_requires=) + """ + lines = read_lines(path) + lines = (line.strip() for line in lines) + lines = (line for line in lines if not line.startswith('#')) + lines = (line for line in lines if not line.startswith('git+')) + lines = (line for line in lines if not line.startswith('http')) + lines = (line for line in lines if line) + return list(lines) diff --git a/snakehouse/templates/hfile.mako b/snakehouse/templates/hfile.mako deleted file mode 100644 index 309812b7f7593138b18e9df01979dd32c929763c..0000000000000000000000000000000000000000 --- a/snakehouse/templates/hfile.mako +++ /dev/null @@ -1,3 +0,0 @@ -#include "Python.h" -#define SNAKEHOUSE_FILE -PyObject* PyInit_${initpy_name}(void); diff --git a/snakehouse/templates/initpy.mako b/snakehouse/templates/initpy.mako deleted file mode 100644 index 020fe76eeae514935ba3395ee559fa37bda17d8c..0000000000000000000000000000000000000000 --- a/snakehouse/templates/initpy.mako +++ /dev/null @@ -1,2 +0,0 @@ -from ${module_name}.__bootstrap__ import bootstrap_cython_submodules -bootstrap_cython_submodules() diff --git a/snakehouse/templating.py b/snakehouse/templating.py new file mode 100644 index 0000000000000000000000000000000000000000..17aaf4df0770f91b6460ac64fd5ac363ec3f738c --- /dev/null +++ b/snakehouse/templating.py @@ -0,0 +1,32 @@ +"""A brain-dead implementation of Mako""" +import re +import typing as tp + + +FOR_MATCH = re.compile(r'% for (.*?) in (.*?)\:') + +HFILE_MAKO = """ +extern PyObject* PyInit_{initpy_name}(); +""" + + +INITPY_MAKO = """from {module_name}.__bootstrap__ import bootstrap_cython_submodules +bootstrap_cython_submodules() +""" + +class Output: + def __init__(self): + self.elements_containing = {} #: type dict[str, list[str]] + self.data = [] #: type: str | tuple[str, str, str] + + def __str__(self): + return '\n'.join(self.data) + def append(self, ostr: str, **kwargs): + if kwargs: + ostr = ostr.format(**kwargs) + self.data.append(ostr) + def append_many(self, lines: str, **kwargs): + for line in lines.split('\n'): + if kwargs: + line = line.format(**kwargs) + self.data.append(line)