Coding Style¶
We really appreciate your help developing BioSimSpace
and
welcome pull requests to the devel
branch. To help us more
quickly review your pull request, and to keep a consistent coding
style throughout, we ask that you please follow the below coding
styles (and that you aren’t offended if we modify your submission
so that it meets these styles).
Python¶
BioSimSpace uses the Python programming language. Our aim is to provide a simple and robust API where unnecessary implementation details are hidden from the user.
“Hold on a second, this code isn’t very Pythonic!“
Indeed it is not. BioSimSpace is built on top of an existing C++ framework and the wrapped objects are designed to mimic the underlying C++ API. In addition, this is partly a design choice, since BioSimSpace is intended primarily to be used by novices, who may be unfamiliar with Python, or programming in general. We want to make it as easy as possible for these users to get up and running with molecular simulation. BioSimSpace also needs to be robust and portable, hence we need to use encapsulation to shield the user from unintended consequences.
With this in mind, we use the following coding conventions:
Naming¶
We follow a C++ style naming convention.
Packages: CamelCase
Classes: CamelCase
Methods: camelCase
Functions: camelCase
Variables: snake_case
For example, to instantiate a minimisation protocol from the Protocol
package:
import BioSimSpace as BSS
protocol = BSS.Protocol.Minimisation()
(Note that sire BioSimSpace repository, on top of which BioSimSpace is built, has recently undergone a modernisation program and now has a fully PEP8-compliant API. We indent to move BioSimSpace towards using Python compliant API too, while preserving backwards compatibility.)
We use black to autoformat our Python code. Please use this if you plan on submitting code. They are easy to configure and use via your IDE (or from the command-line) and help ensure a consistent code style and minimise diffs during pull requests.
Modules¶
BioSimSpace is a collection of packages, e.g. BioSimSpace.Gateway
and
BioSimSpace.Protocol
. Within each package is a set of modules that
implement the required functionality. Rather than directly exposing all of
the modules we choose to hide implementation details from the user. Instead
we use the package __init__.py
to selectively import the required
classes and functions.
Module files containing implementation details are prefixed with an underscore, i.e.
_process.py
Where possible, external packages are hidden inside each module, e.g.
import mdtraj as _mdtraj
Each module file contains an
__all__
variable that lists the specific items that should be imported.The package
__init__.py
can be used to safely expose the required functionality to the user with:
from module import *
This results in a clean API and documentation, with all extraneous information,
e.g. external modules, hidden from the user. This is important when working
interactively, since IPython and Jupyter
do not respect the __all__
variable when auto-completing, meaning that the
user will see a full list of the available names when hitting tab. When
following the conventions above, the user will only be able to access the
exposed names. This greatly improves the clarity of the package, allowing
a new user to quickly determine the available functionality. Any user wishing
expose further implementation detail can, of course, type an underscore to
show the hidden names when searching.
Encapsulation¶
BioSimSpace aims to provide a means of writing robust and portable workflow components (nodes). To this end, we choose to use an object oriented approach where data is encapsulated, with getters used to retrieve data from an object.
To avoid unintended consequences, getters that return mutable data types, e.g. lists and dictionaries, should return a copy of the data. This prevents the user unintentionally modifying the private data contained in the object. Setters should be used to explicitly modify member data.
For example:
# A class that holds a list of numbers.
class MyClass():
# A private class member variable containing a list of numbers.
_list = [1, 2, 3, 4, 5]
def getList(self):
return self._list
# Create an instance of the class.
c = MyClass()
n = c.getList()
print(n)
[1, 2, 3, 4, 5]
# Update n.
n.append(6)
# The private member data has been modified!
print(c.getList())
[1, 2, 3, 4, 5, 6]
Instead use:
class MyClass():
# A private class member variable containing a list of numbers.
_list = [1, 2, 3, 4, 5]
def getList(self):
return self._list.copy()
# Create an instance of the class.
c = MyClass()
n = c.getList()
print(n)
[1, 2, 3, 4, 5]
# Update n.
n.append(6)
# The private member data is untouched.
print(c.getList())
[1, 2, 3, 4, 5]