Contributing

Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given.

You can contribute in many ways:

Types of Contributions

Report Bugs

Report bugs at https://github.com/glotaran/pyglotaran/issues.

If you are reporting a bug, please include:

  • Your operating system name and version.

  • Any details about your local setup that might be helpful in troubleshooting.

  • Detailed steps to reproduce the bug.

Fix Bugs

Look through the GitHub issues for bugs. Anything tagged with “bug” and “help wanted” is open to whoever wants to implement it.

Implement Features

Look through the GitHub issues for features. Anything tagged with “enhancement” and “help wanted” is open to whoever wants to implement it.

Write Documentation

pyglotaran could always use more documentation, whether as part of the official pyglotaran docs, in docstrings, or even on the web in blog posts, articles, and such. If you are writing docstrings please use the NumPyDoc style to write them.

Submit Feedback

The best way to send feedback is to file an issue at https://github.com/glotaran/pyglotaran/issues.

If you are proposing a feature:

  • Explain in detail how it would work.

  • Keep the scope as narrow as possible, to make it easier to implement.

  • Remember that this is a volunteer-driven project, and that contributions are welcome :)

Get Started!

Ready to contribute? Here’s how to set up pyglotaran for local development.

  1. Fork the pyglotaran repo on GitHub.

  2. Clone your fork locally:

    $ git clone https://github.com/<your_name_here>/pyglotaran.git
    
  3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:

    $ mkvirtualenv pyglotaran
    (pyglotaran)$ cd pyglotaran
    (pyglotaran)$ python -m pip install -r requirements_dev.txt
    (pyglotaran)$ pip install -e . --process-dependency-links
    
  4. Install the pre-commit hooks, to automatically format and check your code:

    $ pre-commit install
    
  5. Create a branch for local development:

    $ git checkout -b name-of-your-bugfix-or-feature
    

    Now you can make your changes locally.

  6. When you’re done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:

    $ pre-commit run -a
    $ py.test
    

    Or to run all at once:

    $ tox
    
  7. Commit your changes and push your branch to GitHub:

    $ git add .
    $ git commit -m "Your detailed description of your changes."
    $ git push origin name-of-your-bugfix-or-feature
    
  8. Submit a pull request through the GitHub website.

  9. Add the change referring the pull request ((#<PR_nr>)) to changelog.md. If you are in doubt in which section your pull request belongs, just ask a maintainer what they think where it belongs.

Note

By default pull requests will use the template located at .github/PULL_REQUEST_TEMPLATE.md. But we also provide custom tailored templates located inside of .github/PULL_REQUEST_TEMPLATE. Sadly the GitHub Web Interface doesn’t provide an easy way to select them as it does for issue templates (see this comment for more details).

To use them you need to add the following query parameters to the url when creating the pull request and hit enter:

  • ✨ Feature PR: ?expand=1&template=feature_PR.md

  • 🩹 Bug Fix PR: ?expand=1&template=bug_fix_PR

  • 📚 Documentation PR: ?expand=1&template=docs_PR.md

Pull Request Guidelines

Before you submit a pull request, check that it meets these guidelines:

  1. The pull request should include tests.

  2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring.

  3. The pull request should work for Python 3.10 and 3.11 Check your Github Actions https://github.com/<your_name_here>/pyglotaran/actions and make sure that the tests pass for all supported Python versions.

Docstrings

We use numpy style docstrings, which can also be autogenerated from function/method signatures by extensions for your editor.

Some extensions for popular editors are:

Note

If your pull request improves the docstring coverage (check pre-commit run -a interrogate), please raise the value of the interrogate setting fail-under in pyproject.toml. That way the next person will improve the docstring coverage as well and everyone can enjoy a better documentation.

Warning

As soon as all our docstrings are in proper shape we will enforce that it stays that way. If you want to check if your docstrings are fine you can use pydocstyle and darglint.

Tips

To run a subset of tests:

$ py.test tests.test_pyglotaran

Deprecations

Only maintainers are allowed to decide about deprecations, thus you should first open an issue and check back with them if they are ok with deprecating something.

To make deprecations as robust as possible and give users all needed information to adjust their code, we provide helper functions inside the module glotaran.deprecation.

The functions you most likely want to use are

Those functions not only make it easier to deprecate something, but they also check that that deprecations will be removed when they are due and that at least the imports in the warning work. Thus all deprecations need to be tested.

Tests for deprecations should be placed in glotaran/deprecation/modules/test which also provides the test helper functions deprecation_warning_on_call_test_helper and changed_import_test_warn. Since the tests for deprecation are mainly for maintainability and not to test the functionality (those tests should be in the appropriate place) deprecation_warning_on_call_test_helper will by default just test that a GlotaranApiDeprecationWarning was raised and ignore all raise Exception s. An exception to this rule is when adding back removed functionality (which shouldn’t happen in the first place but might), which should be implemented in a file under glotaran/deprecation/modules and filenames should be like the relative import path from glotaran root, but with _ instead of ..

E.g. glotaran.analysis.scheme would map to analysis_scheme.py

The only exceptions to this rule are the root __init__.py which is named glotaran_root.py and testing changed imports which should be placed in test_changed_imports.py.

Deprecating a Function, method or class

Deprecating a function, method or class is as easy as adding the deprecate decorator to it. Other decorators (e.g. @staticmethod or @classmethod) should be placed both deprecate in order to work.

glotaran/some_module.py
from glotaran.deprecation import deprecate

@deprecate(
    deprecated_qual_name_usage="glotaran.some_module.function_to_deprecate(filename)",
    new_qual_name_usage='glotaran.some_module.new_function(filename, format_name="legacy")',
    to_be_removed_in_version="0.6.0",
)
def function_to_deprecate(*args, **kwargs):
    ...

Deprecating a call argument

When deprecating a call argument you should use warn_deprecated and set the argument to deprecate to a default value (e.g. "deprecated") to check against. Note that for this use case we need to set check_qual_names=(False, False) which will deactivate the import testing. This might not always be possible, e.g. if the argument is positional only, so it might make more sense to deprecate the whole callable, just discuss what to do with our trusted maintainers.

glotaran/some_module.py
from glotaran.deprecation import deprecate

def function_to_deprecate(args1, new_arg="new_default_behavior", deprecated_arg="deprecated", **kwargs):
    if deprecated_arg != "deprecated":
        warn_deprecated(
            deprecated_qual_name_usage="deprecated_arg",
            new_qual_name_usage='new_arg="legacy"',
            to_be_removed_in_version="0.6.0",
            check_qual_names=(False, False)
        )
        new_arg = "legacy"
    ...

Deprecating a module attribute

Sometimes it might be necessary to remove an attribute (function, class, or constant) from a module to prevent circular imports or just to streamline the API. In those cases you would use deprecate_module_attribute inside a module __getattr__ function definition. This will import the attribute from the new location and return it when an import or use is requested.

glotaran/old_package/__init__.py
def __getattr__(attribute_name: str):
    from glotaran.deprecation import deprecate_module_attribute

    if attribute_name == "deprecated_attribute":
        return deprecate_module_attribute(
            deprecated_qual_name="glotaran.old_package.deprecated_attribute",
            new_qual_name="glotaran.new_package.new_attribute_name",
            to_be_removed_in_version="0.6.0",
        )

    raise AttributeError(f"module {__name__} has no attribute {attribute_name}")

Deprecating a submodule

For a better logical structure, it might be needed to move modules to a different location in the project. In those cases, you would use deprecate_submodule, which imports the module from the new location, add it to sys.modules and as an attribute to the parent package.

glotaran/old_package/__init__.py
from glotaran.deprecation import deprecate_submodule

module_name = deprecate_submodule(
    deprecated_module_name="glotaran.old_package.module_name",
    new_module_name="glotaran.new_package.new_module_name",
    to_be_removed_in_version="0.6.0",
)

Deprecating dict entries

The possible dict deprecation actions are:

  • Swapping of keys {"foo": 1} -> {"bar": 1} (done via swap_keys=("foo", "bar"))

  • Replacing of matching values {"foo": 1} -> {"foo": 2} (done via replace_rules=({"foo": 1}, {"foo": 2}))

  • Replacing of matching values and swapping of keys {"foo": 1} -> {"bar": 2} (done via replace_rules=({"foo": 1}, {"bar": 2}))

For full examples have a look at the examples from the docstring (deprecate_dict_entry()).

Deprecation Errors

In some cases deprecations cannot have a replacement with the original behavior maintained. This will be mostly the case when at this point in time and in the object hierarchy there isn’t enough information available to calculate the appropriate values. Rather than using a ‘dummy’ value not to break the API, which could cause undefined behavior down the line, those cases should throw an error which informs the users about the new usage. In general this should only be used if it is unavoidable due to massive refactoring of the internal structure and tried to avoid by any means in a reasonable context.

If you have one of those rare cases you can use raise_deprecation_error().

Testing Result consistency

To test the consistency of results locally you need to clone the pyglotaran-examples and run them:

$ git clone https://github.com/glotaran/pyglotaran-examples
$ cd pyglotaran-examples
$ python scripts/run_examples.py run-all --headless

Note

Make sure you got the the latest version (git pull) and are on the correct branch for both pyglotaran and pyglotaran-examples.

The results from the examples will be saved in you home folder under pyglotaran_examples_results. Those results than will be compared to the ‘gold standard’ defined by the maintainers.

To test the result consistency run:

$ pytest validation/pyglotaran-examples/test_result_consistency.py

If needed this will clone the ‘gold standard’ results to the folder comparison-results, update them and test your current results against them.

Deploying

A reminder for the maintainers on how to deploy. Make sure all your changes are committed (including an entry in changelog.md), the version number only needs to be changed in glotaran/__init__.py.

Then make a new release on GitHub and give the tag a proper name, e.g. v0.3.0 since it might be included in a citation.

Github Actions will then deploy to PyPI if the tests pass.