Contributing

Contributions and issues are most welcome! All issues and pull requests are handled through github on the dls_controls repository. Also, please check for any existing issues before filing a new one. If you have a great idea but it involves big changes, please file a ticket before making a pull request! We want to make sure you don’t spend your time coding something that might not fit the scope of the project.

Setup

The simplest way to set up the Python environment is with pipenv.

Then all you need to do is download the source code and create the environment with developer packages:

$ git clone git://github.com/dls-controls/pymalcolm.git
$ cd pymalcolm
$ pipenv install --dev

Running the tests

Once the environment is created tests can be run using pipenv:

$ pipenv run tests

While 100% code coverage does not make a library bug-free, it significantly reduces the number of easily caught bugs! Please make sure coverage remains the same or is improved by a pull request!

Code Styling

The code in this repository conforms to standards set by the following tools:

  • black for code formatting

  • flake8 for style checks

  • isort for import ordering

  • mypy for static type checking

These tests will be run on code when running pipenv run tests and also automatically at check in. Please read the tool documentation for details on how to fix the errors it reports.

Pipfile.lock

When updating/installing dependencies you may find the Pipfile.lock is also updated. This should only be committed if tested with the Diamond Python 3 environment, and ensuring all dependencies are available using:

$ dls-py3 compare-dependencies

See the Diamond Python 3 Confluence area for more information.

Documentation

Documentation is contained in the docs directory and extracted from docstrings of the API.

Docs follow the underlining convention:

Headling 1 (page title)
=======================

Heading 2
---------

Heading 3
~~~~~~~~~

You can build the docs from the project directory by running:

$ pipenv run docs
$ firefox build/html/index.html

Building the docs

When in the project directory:

$ pipenv install --dev
$ pipenv run docs
$ firefox docs/html/index.html

Coding Conventions

Any class that takes Annotypes in __init__() will either define those Annotypes or import them from its superclass. The Annotypes should all appear in the Class namespace so that any further subclasses can in turn import the necessary types. To facilitate this and create a tidy namespace, use import in __init__.py. To allow ‘re-import’ of the superclass symbols, it is necessary to pull them into a variable local to the namespace (in fact this is only required for the IDE, but also shows more explicitly what we are doing).

For example, in malcolm.modules.builtin.blockpart create copies of the imported Annotypes (The comment is also a convention):

from ..util import set_tags, AWriteable, AConfig, AGroup, AWidget

with Anno("Initial value of the created attribute"):
    AValue = str

# Pull re-used annotypes into our namespace in case we are subclassed
APartName = APartName
AMetaDescription = AMetaDescription
AWriteable = AWriteable
AConfig = AConfig
AGroup = AGroup
AWidget = AWidget

Next import all the Annotypes from blockpart and its superclasses in malcolm.modules.builtin.__init__.py:

from .blockpart import BlockPart, APartName, AMetaDescription, AWriteable, \
AConfig, AGroup, AWidget

When importing from core.modules, import the entire module only. This means that all references to the contents of this module will then have an explicit module namespace. e.g.:

from malcolm.modules import builtin, scanning

def setup(self, registrar):
    registrar.hook(scanning.hooks.ConfigureHook, self.configure)

Note that this does not apply when importing symbols from other files within the same malcolm module. In this case use relative imports (importing a parent module is a circular import). e.g. in malcolm.modules.demo.filewriterpart.py:

from ..util import make_gaussian_blob, interesting_pattern

When implementing a part do all hook registration using registrar.hook in the setup function (not in __init__). e.g.:

class MotionChildPart(builtin.parts.ChildPart):
    """Provides control of a `counter_block` within a `RunnableController`"""

    # Generator instance
    _generator: scanning.hooks.AGenerator = None
    # Where to start
    _completed_steps: int = 0
    # How many steps to do
    _steps_to_do: int = 0
    # When to blow up
    _exception_step: int = 0
    # Which axes we should be moving
    _axes_to_move: Optional[scanning.hooks.AAxesToMove] = None
    # MaybeMover objects to help with async moves
    _movers: Dict[str, MaybeMover] = {}

    def setup(self, registrar: PartRegistrar) -> None:
        super().setup(registrar)
        # Hooks
        registrar.hook(scanning.hooks.PreConfigureHook, self.reload)
        registrar.hook(
            (
                scanning.hooks.ConfigureHook,
                scanning.hooks.PostRunArmedHook,
                scanning.hooks.SeekHook,
            ),
            self.on_configure,
        )
        registrar.hook(scanning.hooks.RunHook, self.on_run)
        # Tell the controller to expose some extra configure parameters
        registrar.report(scanning.hooks.ConfigureHook.create_info(self.on_configure))

Also do not override __init__() just to declare Attributes, instead declare them at the class level and initialise to None, then create the Attribute model in setup.

class CounterPart(Part):
    """Defines a counter `Attribute` with zero and increment `Method` objects"""

    #: Writeable Attribute holding the current counter value
    counter: Optional[AttributeModel] = None
    #: Writeable Attribute holding the amount to increment() by
    delta: Optional[AttributeModel] = None

    def setup(self, registrar: PartRegistrar) -> None:
        super().setup(registrar)
        # Add some Attribute and Methods to the Block
        self.counter = NumberMeta(
            "float64",
            "The current value of the counter",
            tags=[config_tag(), Widget.TEXTINPUT.tag()],
        ).create_attribute_model()
        registrar.add_attribute_model("counter", self.counter, self.counter.set_value)

        self.delta = NumberMeta(
            "float64",
            "The amount to increment() by",
            tags=[config_tag(), Widget.TEXTINPUT.tag()],
        ).create_attribute_model(initial_value=1)
        registrar.add_attribute_model("delta", self.delta, self.delta.set_value)

        registrar.add_method_model(self.zero)
        registrar.add_method_model(self.increment)

Release Checklist

Before a new release, please go through the following checklist:

  • Choose a new PEP440 compliant release number

  • Git tag the version with a message summarizing the changes

  • Push to github and the actions will make a release on pypi

  • Push to internal gitlab and do a dls-release.py of the tag