Finesse code style

In general, try to follow Flake8, pylint and Black code styles.

Specific notes

‘__init__.py’ files

To avoid Flake8 flagging imports in __init__.py files, the imports intended to be available to other modules importing the package via from package import * should be explicitly listed in __all__ as strings.

Absolute and relative imports

Relative imports like from .script import parse are preferred in Finesse to absolute imports like from finesse.script import parse in order to take advantage of a few minor benefits:

  • The import is guaranteed to get the intended sibling module. With absolute imports it’s possible a different version of Finesse installed on the PATH with higher priority can be imported and wreak havoc.

  • They allow you to install multiple versions of Finesse in your environment if you so wish, and have them all “just work”.

  • You often don’t need to rename imports if you move subpackages around, e.g. an __init__.py with from .parser import KatParser will import from .parser regardless of whether __init__.py is in /finesse/script/ or /finesse/other/.

  • You can quickly see if an import is part of the same package or another one, e.g. a system library, based on whether or not it starts with ...

  • Less text. Instead of from finesse.script.parser import KatParser it’s e.g. from .parser import KatParser.

Indentation of multi-line quotes

The Black formatter treats multi-line quotes (i.e. """) as parentheses. This occasionally leads to seemingly unintuitive reformatting, such as when embedding kat script inside Python. For example, this code:

parse("""
    # some KatScript
""")

gets reformatted to:

parse(
    """
    # some KatScript
"""
)

In this regard, Black is being pretty reasonable. It treats multi-line quotes the same way it treats parentheses that enclose text that is too long for one line, by moving the contents onto the next line and indenting by one more level. It cannot, however, assume that it can reformat the contents of the multi-line string itself, so it leaves that alone. The result is that the opening multi-line quotes get indented by one, but the contents and the closing multi-line quotes get kept as they are.

To avoid this somewhat ugly reformatting, the solution to this is to manually indent the contents and closing multi-line quotes as well, if appropriate. Since KatScript ignores indents, the following would work for the above example:

parse(
    """
    # some KatScript
    """
)

Singletons

The direct use of singletons (single-instance classes) in Finesse is discouraged because they create and modify global state in a way that is difficult to automatically reset. Improperly reset global state can cause problems for unit testing since tests may implicitly rely on a certain default global state. One solution would be for each test that touches code containing singletons to fully reset any global state modified by such singletons, but this is difficult because each test must know about and how to reset every one that was used. Where global state is not fully reset by a test, a seemingly unrelated test may fail elsewhere because it implicitly relied on a particular default state. Improperly reset state can also lead to so-called flaky tests, i.e. tests that sometimes but don’t always fail, depending on the order in which tests are run.

The single-instance class pattern is nevertheless sometimes useful and can still be employed using a safer mechanism. Some Finesse objects makes use of a single, global “datastore” that stores references to single instances of what would otherwise have been singletons. This single datastore is then reset between unit tests by a single tear-down function shared by all tests, ensuring the state is returned to the default for the next test. Two examples of this are _FinesseConfig and _TracebackHandler.

An alternative approach could be to use so-called dependency injection, but this requires a little more boilerplate code to set up and maintain so may be overkill depending on the application. Where reasonable, re-use of the “datastore” concept above is recommended since the infrastructure for resetting state between tests is already implemented.