Contributing to BayBE

All contributions to BayBE are welcome!

… no matter if bug fixes, new features, or just typo corrections.

To shorten the overall development and review process, this page contains are a few sections that can make your life easier.

General Workflow

To implement your contributions in a local development environment, we recommend the following workflow:

  1. Clone a fork of the repository to your local machine.

  2. Create and activate a virtual python environment using one of the supported python versions.

  3. Change into the root folder of the cloned repository and install an editable version including all development dependencies:

    pip install -e '.[dev]'
    
  4. Run our tests to verify everything works as expected:

    pytest
    
  5. Install our pre-commit hooks:

    pre-commit install
    
  6. Create a new branch for your contribution:

    git checkout -b <your_branch_name>
    
  7. Implement your changes.

  8. Optional but recommended to prevent complaints from our CI pipeline: Test your code.

    There are several test environments you can run via tox, each corresponding to a developer tool in a certain Python version. You can retrieve all available environments via tox list. For more information, see our README about tests.

    For instance, running all code tests in Python 3.12 can be achieved via:

    tox -e fulltest-py312
    

    If you want to challenge your machine, you can run all checks in all Python versions in parallel via:

    tox -p
    

    This can be considered the ultimate one-stop check to make sure your code is ready for merge.

  9. Push the updated branch back to your fork:

    git push origin
    
  10. Open a pull request via Github’s web page.

Synchronizing Pull Requests

A common situation encountered when submitting a pull request (PR) is that the upstream branch has evolved since the moment your PR branch was created, and a synchronization is needed in order to prepare your branch for a merge (e.g., to remove existing conflicts).

Because we care about our Git history and would like to keep it clean and easy to follow, we generally recommend rebasing your branch onto the latest upstream commit in such situations, especially if your changes are orthogonal to what has happened on the remote branch in the meantime. Compared to merging, this has the advantage of keeping the history of your commits (and thus of the entire repository) linear, and your own PR free of changes that happened remotely, which also greatly simplifies the review process (e.g., it produces simpler diffs).

That said, the above is only a recommendation and by no means a requirement. However, depending on the complexity of your PR commit history, we reserve the right to merge your branch using a squash-rebase as a last resort to keep our history clean. By following the guideline above, this step can be easily avoided in most cases.

Developer Tools

In order to maintain a high code quality, we use a variety of code developer tools. When following the above described workflow, pre-commit will automatically trigger (most) necessary checks during your development process. In any case, these checks are also conducted in our CI pipeline, which must pass before your pull request is considered ready for review. If you have questions or problems, simply ask for advice.

Tool

Purpose

ruff

code linting and formatting

mypy

static type checking

pydocstyle
pydoclint

analyzing docstrings

typos

basic spell checking

pytest

testing

pytest-cov

measuring test coverage

sphinx

generating our documentation

pip-audit

detecting vulnerabilities in dependencies

tox

orchestrating all the above

Executing a specific one of these tools is easiest by using the corresponding tox environment,

tox -e <env>

where <env> is any of the environment names found via tox list.

Code Design

When reading BayBE’s code, you will notice certain re-occurring design patterns. These patterns are by no means enforced, but following them can streamline your own development process:

  • We build most our classes with attrs, which is useful for lean class design and attribute validation.

  • Our (de-)serialization machinery is built upon cattrs, separating object serialization from class design.

  • The modular nature of BayBE’s components is reflected in our test suite through the use of hypothesis property tests.

Extending BayBE’s Functionality

For most parts, BayBE’s code and functional components are organized into different subpackages. When extending its functionality (for instance, by adding new component subclasses), make sure that the newly written code is well integrated into the existing package and module hierarchy. In particular, public functionality should be imported into the appropriate high-level namespaces for easier user import. For an example, see our parameter namespace.

Writing Docstrings

Our docstrings generally follow the Google Python Style Guide. Basic style and consistency checks are automatically performed via pre-commit during development and in our CI pipeline.

Apart from that, we generally recommend adhering to the following guideline:

  • Each function should have a docstring containing:

    • a short one-line summary at the top,

    • an optional extended summary or description below and

    • all relevant sections (Args, Raises, …).

    Potential exceptions are functions whose docstring is to be fully inherited from a parent class. In this case, use # noqa: D102 to disable the automatic checks locally.

  • Use type hints (for variables/constants, attributes, function/method signatures, …). Avoid repeating type hints in docstrings.

  • When referencing objects (classes, functions, …), use :<key>:`path.to.function` where <key> is to be replaced with the respective role (class, func, …)

  • Use double backticks for literals like in ``MyString``.

Writing attrs classes

  • Place attribute docstrings below the attribute declaration, not in the class docstring. Separate different attributes using a blank line. For example:

    @define
    class Cookies:
      """A delicious recipe for chocolate-banana cookies."""
    
      chocolate: float
      """Chocolate is naturally measured in terms of floats."""
    
      bananas: int
      """For bananas, we use integers, of course."""
    
  • Unless another more specific name is suitable, use our default naming convention for attrs defaults and validators:

    @my_attribute.default
    def _default_my_attribute(self): ...
    
    @my_attribute.validator
    def _validate_my_attribute(self, attribute, value): ...
    

    A one-line docstring suffices for these methods, but they should have a Raises: section if applicable. Linter warnings regarding missing attribute docstrings can be silenced using # noqa: DOC101, DOC103.