Getting Involved

First of all, thank you for using and contributing to palace! We welcome all forms of contribution, and the mo the merier. By saying this, we also mean that we much prefer receiving many small and self-contained bug reports, feature requests and patches than a giant one. There is no limit for the number of contributions one may or should make. While it may seem appealing to be able to dump all thoughts and feelings into one ticket, it would be more difficult for us to keep track of the progress.

Reporting a Bug

Before filing a bug report, please make sure that the bug has not been already reported by searching our bug tracker.

To facilitate the debugging process, a bug report should at least contain the following information:

  1. The platform, the CPython version and the compiler used to build it. These can be obtained from platform.platform(), platform.python_version() and platform.python_compiler(), respectively.

  2. The version of palace and how you installed it. The earlier is usually provided by pip show palace.

  3. Detailed instructions on how to reproduce the bug, for example a short Python script would be appreciated.

Requesting a Feature

Prior to filing a feature request, please make sure that the feature has not been already requested by searching our mailing list.

Please only ask for features that you (or an incapacitated friend you can personally talk to) require. Do not request features because they seem like a good idea. If they are really useful, they will be requested by someone who requires them.

Submitting a Patch

We accept all kinds of patches, from documentation and CI/CD setup to bug fixes, feature implementations and tests. Contributors must have legal permission to distribute the code and it must be available under LGPLv3+. Furthermore, each contributor retains the copyrights of their patch, to ensure that the licence can never be revoked even if others wants to. It is advisable that the author lists per legal name under the copyright header of each source file perse modify, like so:

Copyright (C) 2038  Foo Bar

The contribution process consists of the following steps:

  1. Clone the upstream repository and configure the local one:

    git clone https://git.sr.ht/~cnx/palace
    cd palace
    git config sendemail.to "~cnx/palace@lists.sr.ht"
    
  2. Start working on your patch and make sure your code complies with the Style Guidelines and passes the test suit run by tox.

  3. Add relevant tests to the patch and work on it until they all pass. In case one is only modifying tests, perse may install palace using CYTHON_TRACE=1 pip install . then run pytest directly to avoid having to build the extension module multiple times.

  4. Update the copyright notices of the files you modified.

  5. Add and commit with a great message.

  6. Send the patch and response to feedback.

In any case, thank you very much for your contributions!

Making a Release

While this is meant for developers doing a palace release, contributors wishing to improve the CI/CD may find it helpful.

  1. Under the local repository, checkout the main branch and sync with the one on SourceHut using git pull.

  2. Bump the version in setup.cfg and docs/source/conf.py, tag the commit and push it to SourceHut. In the release note, make sure to include all user-facing changes since the previous release. This will trigger the CD services to build the wheels and publish them to PyPI.

  3. Create a source distribution by running setup.py sdist. The distribution generated by this command is now referred to as sdist.

  4. Using twine, upload the sdist to PyPI via twine upload $sdist.

  5. Wait for the wheel for your platform to arrive to PyPI and install it. Play around with it for a little to make sure that everything is OK.

  6. Announce to the mailing list. With fear and trembling.

Coding Standards

Philosophy

In order to write safe, efficient, easy-to-use and extendable Python, the following principals should be followed.

The Impl Idiom

Not to be confused with the pimpl idiom.

For memory-safety, whenever possible, we rely on Cython for allocation and deallocation of C++ objects. To do this, the nullary constructor needs to be (re-)declared in Cython, e.g.

cdef extern from 'foobar.h' namespace 'foobar':
    cdef cppclass Foo:
        Foo()
        float meth(size_t crack) except +
        ...

The Cython extension type can then be declared as follows

cdef class Bar:
    cdef Foo impl

    def __init__(self, *args, **kwargs):
        self.impl = ...

    @staticmethod
    def from_baz(baz: Baz) -> Bar:
        bar = Bar.__new__(Bar)
        bar.impl = ...
        return bar

    def meth(self, crack: int) -> float:
        return self.impl.meth(crack)

The Modern Python

One of the goal of palace is to create a Pythonic, i.e. intuitive and concise, interface. To achieve this, we try to make use of some modern Python features, which not only allow users to adopt palace with ease, but also make their programs more readable and less error-prone.

Property Attributes

A large proportion of alure API are getters/setter methods. In Python, it is a good practice to use property to abstract these calls, and thus make the interface more natural with attribute-like referencing and assignments.

Due to implementation details, Cython has to hijack the @property decorator to make it work for read-write properties. Unfortunately, the Cython-generated descriptors do not play very well with other builtin decorators, thus in some cases, it is recommended to alias the call to property as follows

getter = property
setter = lambda fset: property(fset=fset, doc=fset.__doc__)

Then @getter and @setter can be used to decorate read-only and write-only properties, respectively, without any trouble even if other decorators are used for the same extension type method.

Context Managers

The alure API defines many objects that need manual tear-down in a particular order. Instead of trying to be clever and perform automatic clean-ups at garbage collection, we should put the user in control. To quote The Zen of Python,

If the implementation is hard to explain, it’s a bad idea.
If the implementation is easy to explain, it may be a good idea.

With that being said, it does not mean we do not provide any level of abstraction. A simplified case in point would be

cdef class Device:
    cdef alure.Device impl

    def __init__(self, name: str = '') -> None:
        self.impl = devmgr.open_playback(name)

    def __enter__(self) -> Device:
        return self

    def __exit__(self, *exc) -> Optional[bool]:
        self.close()

    def close(self) -> None:
        self.impl.close()

Now if the with statement is used, it will make sure the device will be closed, regardless of whatever may happen within the inner block

with Device() as dev:
    ...

as it is equivalent to

dev = Device()
try:
    ...
finally:
    dev.close()

Other than closure/destruction of objects, typical uses of context managers also include saving and restoring various kinds of global state (as seen in Context), locking and unlocking resources, etc.

The Double Reference

While wrapping C++ interfaces, the impl idiom might not be adequate, since the derived Python methods need to be callable from C++. Luckily, Cython can handle Python objects within C++ classes just fine, although we’ll need to handle the reference count ourselves, e.g.

cdef cppclass CppDecoder(alure.BaseDecoder):
    Decoder pyo

    __init__(Decoder decoder):
        this.pyo = decoder
        Py_INCREF(pyo)

    __dealloc__():
        Py_DECREF(pyo)

    bool seek(uint64_t pos):
        return pyo.seek(pos)

With this being done, we can now write the wrapper as simply as

cdef class BaseDecoder:
    cdef shared_ptr[alure.Decoder] pimpl

    def __cinit__(self, *args, **kwargs) -> None:
        self.pimpl = shared_ptr[alure.Decoder](new CppDecoder(self))

    def seek(pos: int) -> bool:
        ...

Because __cinit__ is called by __new__, any Python class derived from BaseDecoder will be exposed to C++ as an attribute of CppDecoder. Effectively, this means the users can have the alure API calling their inherited Python object as naturally as if palace is implemented in pure Python.

In practice, BaseDecoder will also need to take into account other guarding mechanisms like abc.ABC. Due to Cython limitations, implementation as a pure Python class and aliasing of @getter/@setter should be considered.

Style Guidelines

Python and Cython

Generally, palace follows PEP 8 and PEP 257, with the following preferences and exceptions:

  • Hanging indentation is always preferred, where continuation lines are indented by 4 spaces.

  • Comments and one-line docstrings are limited to column 79 instead of 72 like for multi-line docstrings.

  • Cython extern declarations need not follow the 79-character limit.

  • Break long lines before a binary operator.

  • Use form feeds sparingly to break long modules into pages of relating functions and classes.

  • Prefer single-quoted strings over double-quoted strings, unless the string contains single quote characters.

  • Avoid trailing commas at all costs.

  • Line breaks within comments and docstrings should not cut a phrase in half.

  • Everything deserves a docstring. Palace follows numpydoc which support documenting attributes as well as constants and module-level variables. In additional to docstrings, type annotations should be employed for all public names.

  • Use numpydoc markups moderately to keep docstrings readable as plain text.

C++

C++ codes should follow GNU style, which is best documented at Octave.

reStructuredText

Overall, palace’s documentation follows CPython documenting style guide, with a few additional preferences.

In order for reStructuredText to be rendered correctly, the body of constructs beginning with a marker (lists, hyperlink targets, comments, etc.) must be aligned relative to the marker. For this reason, it is convenient to set your editor indentation level to 3 spaces, since most constructs starts with two dots and a space. However, be aware of that bullet items require 2-space alignment and other exceptions.

Limit all lines to a maximum of 80 characters. Similar to comments and docstrings, phrases should not be broken in the middle. The source code of this guide itself is a good example on how line breaks should be handled. Additionally, two spaces should also be used after a sentence-ending period in multi-sentence paragraph, except after the final sentence.