########### Development ########### .. contents:: :local: *************** Adding Features *************** In order to add a feature to ``bezier``: #. **Discuss**: `File an issue`_ to notify maintainer(s) of the proposed changes (i.e. just sending a large PR with a finished feature may catch maintainer(s) off guard). #. **Add tests**: The feature must work fully on CPython versions 3.9, 3.10 and 3.11 and on PyPy 3; on Linux, macOS and Windows. In addition, the feature should have 100% line coverage. #. **Documentation**: The feature must (should) be documented with helpful `doctest`_ examples wherever relevant. .. _File an issue: https://github.com/dhermes/bezier/issues/new .. _doctest: http://www.sphinx-doc.org/en/stable/ext/doctest.html ************* ``libbezier`` ************* To build the Fortran shared library directly, use `CMake`_ version ``3.5`` or later: .. code-block:: console $ SRC_DIR="src/fortran/" $ BUILD_DIR=".../libbezier-debug/build" $ INSTALL_PREFIX=".../libbezier-debug/usr" $ mkdir -p "${BUILD_DIR}" $ cmake \ > -DCMAKE_BUILD_TYPE=Debug \ > -DCMAKE_INSTALL_PREFIX:PATH="${INSTALL_PREFIX}" \ > -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON \ > -S "${SRC_DIR}" \ > -B "${BUILD_DIR}" $ cmake \ > --build "${BUILD_DIR}" \ > --config Debug \ > --target install .. _CMake: https://cmake.org/ **************** Binary Extension **************** Many low-level computations have alternate implementations in Fortran. See the `Python Binary Extension`_ page for a more detailed description. .. _Python Binary Extension: https://bezier.readthedocs.io/en/latest/python/binary-extension.html Building / Compiling ==================== To compile the binary extension (with ``libbezier`` built and installed already) run: .. code-block:: console $ # One of $ BEZIER_INSTALL_PREFIX=.../usr/ python -m pip wheel . $ BEZIER_INSTALL_PREFIX=.../usr/ python -m pip install . $ BEZIER_INSTALL_PREFIX=.../usr/ python setup.py build_ext $ BEZIER_INSTALL_PREFIX=.../usr/ python setup.py build_ext --inplace Using a Release build of ``libbezier`` may make debugging more difficult. Instead, a Debug build of ``libbezier`` will include debug symbols, without optimizations that move code around, etc. To explicitly disable the building of the extension, the ``BEZIER_NO_EXTENSION`` environment variable can be used: .. code-block:: console $ BEZIER_NO_EXTENSION=True .../bin/python -m pip wheel . This environment variable is actually used for the ``nox --session docs`` session to emulate the `RTD`_ build environment (where no Fortran compiler is present). Dependencies ============ Currently, the ``src/fortran/quadpack.f90`` `file`_ has a subset of Fortran 77 subroutines from `QUADPACK`_ converted into Fortran 90 by `John Burkardt`_. This code is Public Domain, so does not conflict with the Apache 2.0 license (as far as we know). QUADPACK is used to perform numerical quadrature to compute the length of a curve segment. .. _file: https://github.com/dhermes/bezier/tree/main/src/fortran/quadpack.f90 .. _QUADPACK: https://en.wikipedia.org/wiki/QUADPACK .. _John Burkardt: https://people.math.sc.edu/Burkardt/f_src/quadpack_double/quadpack_double.html Windows ======= Building the binary extension requires a Fortran compiler. Unfortunately, there is no Fortran compiler provided by MSVC. The `MinGW-w64`_ suite of tools is a port of the GNU Compiler Collection (``gcc``) for Windows. In particular, MinGW includes ``gfortran``. To `install MinGW-w64`_, download a recent `MSYS2 Installer`_ (e.g. ``msys2-x86_64-20230526.exe``). When installing, take note of the install location. It should default to ``C:\msys64``. After installing, use ``pacman`` (provided by MSYS2) to install the MinGW-w64 toolchain: .. code-block:: powershell > pacman -S --needed base-devel mingw-w64-x86_64-toolchain After doing this, you may want to permanently add the MinGW bin directory (e.g. ``C:\msys64\mingw64\bin``) to your ``PATH``. If you don't want to make a permanent change, you'll need to temporarily modify your ``PATH`` in shell sessions where ``bezier`` is being developed: .. code-block:: powershell > $env:Path = "C:\msys64\mingw64\bin;" + $env:Path Additionally, the ``BEZIER_EXTRA_DLL`` environment variable may need to be set for ``nox`` sessions if the MinGW-w64 DLLs cannot be found (to be added the DLL search path): .. code-block:: powershell > $env:BEZIER_EXTRA_DLL = "C:\msys64\mingw64\bin" In addition to a Fortran toolchain, you will also need the MSVC toolchain that was used to build each version of Python. The `Microsoft C++ Build Tools`_ can be installed to enable this toolchain. .. _MinGW-w64: http://mingw-w64.org .. _install MinGW-w64: https://code.visualstudio.com/docs/cpp/config-mingw .. _MSYS2 Installer: https://github.com/msys2/msys2-installer/releases .. _Microsoft C++ Build Tools: https://visualstudio.microsoft.com/visual-cpp-build-tools/ ****************** Running Unit Tests ****************** We recommend using `Nox`_ to run unit tests: .. code-block:: console $ nox --session "unit-3.9" $ nox --session "unit-3.10" $ nox --session "unit-3.11" $ nox --session "unit-pypy3" $ nox --session unit # Run all versions However, `pytest`_ can be used directly (though it won't manage dependencies or build the binary extension): .. code-block:: console $ PYTHONPATH=src/python/ python3.9 -m pytest tests/unit/ $ PYTHONPATH=src/python/ python3.10 -m pytest tests/unit/ $ PYTHONPATH=src/python/ python3.11 -m pytest tests/unit/ $ PYTHONPATH=src/python/ pypy3 -m pytest tests/unit/ .. _Nox: https://nox.readthedocs.io .. _pytest: https://docs.pytest.org Testing the Binary Extension ============================ When using ``nox``, ``libbezier`` will be built and installed into a well-known ``BEZIER_INSTALL_PREFIX`` within the ``nox`` envdir (typically ``.nox/``), the ``bezier`` package will automatically be installed into a virtual environment and the binary extension will be built during install. However, if the tests are run directly from the source tree via .. code-block:: console $ PYTHONPATH=src/python/ python -m pytest tests/unit/ some unit tests may be skipped. The unit tests that explicitly exercise the binary extension will skip (rather than fail) if the extension isn't compiled (with ``build_ext --inplace``) and present in the source tree. Test Coverage ============= ``bezier`` has 100% `line coverage`_. The coverage is checked on every build and uploaded to `coveralls.io`_ via the ``COVERALLS_REPO_TOKEN`` environment variable set in the `GitHub Actions secrets`_. .. _line coverage: https://coveralls.io/github/dhermes/bezier .. _coveralls.io: https://coveralls.io/ .. _GitHub Actions secrets: https://github.com/dhermes/bezier/settings/secrets/actions To run the coverage report locally: .. code-block:: console $ nox --session cover $ # OR $ PYTHONPATH=src/python/ python -m pytest \ > --cov=bezier \ > --cov=tests.unit \ > tests/unit/ Slow Tests ========== To run unit tests without test cases that have been (explicitly) marked slow, use the ``--ignore-slow`` flag: .. code-block:: console $ nox --session "unit-3.9" -- --ignore-slow $ nox --session "unit-3.10" -- --ignore-slow $ nox --session "unit-3.11" -- --ignore-slow $ nox --session unit -- --ignore-slow These slow tests have been identified via: .. code-block:: console $ ... $ nox --session "unit-3.11" -- --durations=10 and then marked with ``pytest.mark.skipif``. Slow Install ============ Installing NumPy with `PyPy`_ can take upwards of two minutes (however the NumPy project has started publishing built wheels for PyPy) and installing SciPy can take as much as seven minutes. This makes it prohibitive to create a new environment for testing. .. _PyPy: https://pypy.org/ In order to avoid this penalty, the ``WHEELHOUSE`` environment variable can be used to instruct ``nox`` to install NumPy and SciPy from locally built wheels when installing the ``pypy3`` sessions. To pre-build NumPy and SciPy wheels: .. code-block:: console $ pypy3 -m virtualenv pypy3-venv $ pypy3-venv/bin/python -m pip wheel --wheel-dir="${WHEELHOUSE}" numpy $ pypy3-venv/bin/python -m pip install "${WHEELHOUSE}/numpy*.whl" $ pypy3-venv/bin/python -m pip wheel --wheel-dir="${WHEELHOUSE}" scipy $ rm -fr pypy3-venv/ In addition to the ``WHEELHOUSE`` environment variable, the paths ``${HOME}/wheelhouse`` and ``/wheelhouse`` will also be searched for pre-built wheels. **************** Functional Tests **************** Line coverage and unit tests are not entirely sufficient to test **numerical software**. As a result, there is a fairly large collection of `functional tests`_ for ``bezier``. These give a broad sampling of curve-curve intersection, triangle-triangle intersection and segment-box intersection problems to check both the accuracy (i.e. detecting all intersections) and the precision of the detected intersections. To run the functional tests: .. code-block:: console $ nox --session "functional-3.9" $ nox --session "functional-3.10" $ nox --session "functional-3.11" $ nox --session "functional-pypy3" $ nox --session functional # Run all versions $ # OR $ PYTHONPATH=src/python/ python3.9 -m pytest tests/functional/ $ PYTHONPATH=src/python/ python3.10 -m pytest tests/functional/ $ PYTHONPATH=src/python/ python3.11 -m pytest tests/functional/ $ PYTHONPATH=src/python/ pypy3 -m pytest tests/functional/ .. _functional tests: https://github.com/dhermes/bezier/tree/main/tests/functional For example, the following curve-curve intersection is a functional test case: .. image:: https://raw.githubusercontent.com/dhermes/bezier/main/docs/images/curves11_and_26.png :align: center and there is a `Curve-Curve Intersection`_ document which captures many of the cases in the functional tests. .. _Curve-Curve Intersection: https://bezier.readthedocs.io/en/latest/algorithms/curve-curve-intersection.html A triangle-triangle intersection functional test case: .. image:: https://raw.githubusercontent.com/dhermes/bezier/main/docs/images/triangles1Q_and_2Q.png :align: center a segment-box functional test case: .. image:: https://raw.githubusercontent.com/dhermes/bezier/main/docs/images/test_goes_through_box08.png :align: center and a "locate point on triangle" functional test case: .. image:: https://raw.githubusercontent.com/dhermes/bezier/main/docs/images/test_triangle3_and_point1.png :align: center Functional Test Data ==================== The curve-curve and triangle-triangle intersection test cases are stored in JSON files: * `curves.json`_ * `curve_intersections.json`_ * `triangles.json`_ * `triangle_intersections.json`_ This way, the test cases are programming language agnostic and can be repurposed. The `JSON schema`_ for these files are stored in the ``tests/functional/schema`` directory. .. _curves.json: https://github.com/dhermes/bezier/blob/main/tests/functional/curves.json .. _curve_intersections.json: https://github.com/dhermes/bezier/blob/main/tests/functional/curve_intersections.json .. _triangles.json: https://github.com/dhermes/bezier/blob/main/tests/functional/triangles.json .. _triangle_intersections.json: https://github.com/dhermes/bezier/blob/main/tests/functional/triangle_intersections.json .. _JSON schema: http://json-schema.org/ ************ Coding Style ************ Code is `PEP8`_ compliant and this is enforced with `flake8`_ and `Pylint`_. .. _PEP8: https://www.python.org/dev/peps/pep-0008/ .. _flake8: http://flake8.pycqa.org .. _Pylint: https://www.pylint.org To check compliance: .. code-block:: console $ nox --session lint A few extensions and overrides have been specified in the `pylintrc`_ configuration for ``bezier``. .. _pylintrc: https://github.com/dhermes/bezier/blob/main/pylintrc Docstring Style =============== We require docstrings on all public objects and enforce this with our ``lint`` checks. The docstrings mostly follow `PEP257`_ and are written in the `Google style`_, e.g. .. code-block:: rest Args: path (str): The path of the file to wrap field_storage (FileStorage): The :class:`FileStorage` instance to wrap temporary (bool): Whether or not to delete the file when the File instance is destructed Returns: BufferedFileStorage: A buffered writable file descriptor In order to support these in Sphinx, we use the `Napoleon`_ extension. In addition, the `sphinx-docstring-typing`_ Sphinx extension is used to allow for `type annotation`_ for arguments and result (introduced in Python 3.5). .. _PEP257: https://www.python.org/dev/peps/pep-0257/ .. _Google style: https://google.github.io/styleguide/pyguide.html#Comments__body .. _Napoleon: https://sphinxcontrib-napoleon.readthedocs.io .. _sphinx-docstring-typing: https://pypi.org/project/sphinx-docstring-typing/ .. _type annotation: https://docs.python.org/3/library/typing.html ************* Documentation ************* The documentation is built with `Sphinx`_ and automatically updated on `RTD`_ every time a commit is pushed to ``main``. .. _Sphinx: http://www.sphinx-doc.org .. _RTD: https://readthedocs.org/ To build the documentation locally: .. code-block:: console $ nox --session docs $ # OR (from a Python 3.9 or later environment) $ PYTHONPATH=src/python/ ./scripts/build-docs.sh Documentation Snippets ====================== A large effort is made to provide useful snippets in documentation. To make sure these snippets are valid (and remain valid over time), `doctest`_ is used to check that the interpreter output in the snippets are valid. To run the documentation tests: .. code-block:: console $ nox --session doctest $ # OR (from a Python 3.9 or later environment) $ PYTHONPATH=src/python/:. sphinx-build -W \ > -b doctest \ > -d docs/build/doctrees \ > docs \ > docs/build/doctest Documentation Images ==================== Many images are included to illustrate the curves / triangles / etc. under consideration and to display the result of the operation being described. To keep these images up-to-date with the doctest snippets, the images are created as doctest cleanup. In addition, the images in the `Curve-Curve Intersection`_ document and this document are generated as part of the functional tests. To regenerate all the images: .. code-block:: console $ nox --session docs_images $ # OR (from a Python 3.9 or later environment) $ export MATPLOTLIBRC=docs/ GENERATE_IMAGES=True PYTHONPATH=src/python/ $ sphinx-build -W \ > -b doctest \ > -d docs/build/doctrees \ > docs \ > docs/build/doctest $ python tests/functional/make_segment_box_images.py $ python tests/functional/make_triangle_locate_images.py $ python tests/functional/make_curve_curve_images.py $ python tests/functional/make_triangle_triangle_images.py $ unset MATPLOTLIBRC GENERATE_IMAGES PYTHONPATH ********************** Continuous Integration ********************** Tests are run on `GitHub Actions`_ (Linux, macOS and Windows) after every commit. To see which tests are run, see the `Linux config`_, the `macOS config`_ and the `Windows config`_. .. _GitHub Actions: https://github.com/dhermes/bezier/actions .. _Linux config: https://github.com/dhermes/bezier/blob/main/.github/workflows/linux.yaml .. _macOS config: https://github.com/dhermes/bezier/blob/main/.github/workflows/macos.yaml .. _Windows config: https://github.com/dhermes/bezier/blob/main/.github/workflows/windows.yaml **************************************** Release Process / Deploying New Versions **************************************** New versions are pushed to `PyPI`_ manually after a ``git`` tag is created. The process is manual (rather than automated) for several reasons: * The documentation and README (which acts as the landing page text on PyPI) will be updated with links scoped to the versioned tag (rather than ``main``). This update occurs via the ``doc_template_release.py`` script. * Several badges on the documentation landing page (``index.rst``) are irrelevant to a fixed version (such as the "latest" version of the package). * The build badges in the README and the documentation will be changed to point to a fixed (and passing) build that has already completed (will be the build that occurred when the tag was pushed). If the builds pushed to PyPI automatically, a build would need to link to itself **while** being run. * Wheels need be built for Linux, macOS and Windows. Building wheels occurs **largely** via the `Building Wheels workflow`_, but still requires access to an ARM macOS (M1) machine. After being built, each wheel will be pushed directly to PyPI via `twine`_. * The release will be manually pushed to `TestPyPI`_ so the landing page can be visually inspected and the package can be installed from TestPyPI rather than from a local file. .. _PyPI: https://pypi.org/project/bezier/ .. _twine: https://packaging.python.org/distributing/ .. _TestPyPI: https://packaging.python.org/guides/using-testpypi/ .. _Building Wheels workflow: https://github.com/dhermes/bezier/blob/main/.github/workflows/wheels.yaml Supported Python Versions ========================= ``bezier`` explicitly supports: - `Python 3.9`_ - `Python 3.10`_ - `Python 3.11`_ - `PyPy 3`_ .. _Python 3.9: https://docs.python.org/3.9/ .. _Python 3.10: https://docs.python.org/3.10/ .. _Python 3.11: https://docs.python.org/3.11/ .. _PyPy 3: https://pypy.org/ Supported versions can be found in the ``noxfile.py`` `config`_. .. _config: https://github.com/dhermes/bezier/blob/main/noxfile.py Versioning ========== ``bezier`` follows `calendar versioning`_. .. _calendar versioning: https://calver.org/ ********************* Environment Variables ********************* This project uses environment variables for building the ``bezier._speedup`` binary extension: - ``BEZIER_INSTALL_PREFIX``: A directory where ``libbezier`` is installed, including the shared library (``lib/``) and headers (``include/``). This environment variable is required to build the binary extension. - ``BEZIER_NO_EXTENSION``: If set, this will indicate that only the pure Python package should be built and installed (i.e. without the binary extension). - ``BEZIER_IGNORE_VERSION_CHECK``: Will instruct ``pip`` and ``setup.py`` to ignore a check on the current version of Python. By default, Python installs of ``bezier`` will explicitly check for supported versions and this opts out of that check (e.g. if a new version of Python was just released). This will only be relevant when installing from source, but a new version of Python will also mean the existing wheels on PyPI won't support that new version. - ``BEZIER_EXTRA_DLL``: Used to add (optional) extra directory to DLL search path on Windows. This is intended to be used in tests primarily, but may also be required when building from source. Multiple directories can be provided, separated by the Windows path separator (``;``). and for running tests and interacting with Continuous Integration services: - ``WHEELHOUSE``: If set, this gives a path to prebuilt NumPy and SciPy wheels for PyPy 3. - ``GENERATE_IMAGES``: Indicates to ``nox --session doctest`` that images should be generated during cleanup of each test case. - ``READTHEDOCS``: Indicates currently running on Read The Docs (RTD). This is used to tell Sphinx to use the RTD theme when **not** running on RTD. - ``COVERALLS_REPO_TOKEN``: To upload the coverage report.