Pydevice dev - MdsWiki
Navigation
Personal tools

From MdsWiki

Jump to: navigation, search

Contents

How to write a good pydevice

All Python Devices are loaded during the first Device instantiation of a session. Therefore it is crucial that the loading of the pydevice module never fails (e.g. due to missing dependencies) and is lightweight (i.e. does not perform a time and/or memory consuming initialization). Here are some guidelines for a good pydevice. The list is far from complete and may grow over time.

New device drivers should be placed under ./pydevices

New files need to be added to packaging

Add the new files to the appropriate lists in ./deploy/packaging/{debian,redhat}/*devices.noarch

Comply with PEP8 coding and PEP257 documentation styles

Use a reasonably recent IDE if possible, e.g. Spyder 3.x/4.x. Use your IDE's built-in code style checks as you code. This will help you to get familiar with the rules and speed up your coding as you go. A good style will help to improve the readability and maintainability of your code, which is essential if the device is supposed to work with future versions of python, MDSplus, or the devices firmware itself. Users may have a change to improve the code via pull requests on GitHub. For reference here are the official style guide for PEP8 and PEP227.

Provide information on you and the targeted devices

Help us to keep track of the device drivers. Add you GitHub account information in case we need your advice on pull requests, bugs reports, issues concerning the device, e.g. using epydoc style:

"""
@vendor: MyVendor
@models: MyDevice19xx, MyDevice2xxx

@author: my_github_name
@date: 2020-05-13
"""

Support python 2.7 and python 3.x

The module level implementation should be compatible to python 2.7 and 3.x. I.e. the module ran as script should run under both version. Please, checkout the tools provided in MDSplus.version for that purpose.

BAD:

# python2
import urllib2
import Queue
import socket

strvar = "some str %d" % 3
bytevar = b'bytes %s' % strvar
# python3
import urllib.request
import queue

strvar = "some str {}".format(3)  # fine, but may not work on older releases of python2
bytevar = b'bytes %r' % strvar.encode("UTF-8")  # will fail for python <3.5

GOOD:

# python
import MDSplus
from MDSplus.version import ispy2
if ispy2:
    import urllib2 as urllib
    import Queue as queue
    def tostr(x):
        return x
    def tobytes(x):
        return x
else:
    import urllib.request as urllib
    import queue
    def tostr(x):
        return b if isinstance(b, str) else b.decode('utf-8')
    def tobytes(x):
        return s if isinstance(s, bytes) else s.encode('utf-8')

strvar = "some str %d" % 3
# If you cannot ensure isinstance(strvar, tuple) == False,
# use (strvar,) instead of just strvar
bytevar = tobytes("bytes %s" % (strvar,))

Use the most compatible format string style (%)

Python3 provides new format string styles which are not supported on older version. Likewise some styles have been removed in early python3 releases. For the best compatibility use the %-operator.

BAD

str1 = "{} {:r} {:03d}".format(nam, obj, num)
str2 = "hallo {}".format(world)

GOOD:

str1 = "%s %r %03d" % (nam, obj, num)
str2 = "%s %r %03d" % (world,)  # if you cannot ensure world is instance of tuple use (,)

Avoid ambiguous imports

When import from other packages and modules:

  • never ever do from module import *
  • import at most one module/package per line
  • import whole modules instead of objects
  • avoid aliasing (use of as) unless reasonable
  • group and sort your imports sensefully

BAD:

from MDSplus import *
import sys, os, threading
from time import sleep
import numpy as np
import xml.etree.ElementTree

GOOD:

import MDSplus

import os
import sys
import threading
import time

import numpy
import xml.etree.ElementTree as xmltree

Avoid importing external packages at module level

The module level should only import modules that are imported by MDSplus anyway or part of the standard python distribution. Any other third party module should be imported in the methods they are used (python will handle their caching).

BAD:

import MDSplus
import my_vendors_api


class MYDEVICE(MDSplus.Device):
    def init(self):
        my_vendors_api.init(self.init_args)

GOOD:

import MDSplus


class MYDEVICE(MDSplus.Device):
    def init(self):
        import my_vendors_api
        my_vendors_api.init(self.init_args)

Hide incomplete device classes, e.g. superclasses

Incomplete devices than inherit from MDSplus.Device (e.g. super classes, temporary classes) should have the '_' prefix. The prefix will tell GUIs and APIs that it is not a functioning device.

BAD:

import MDSplus


class MYDEVICEGROUP(MDSplus.Device):
    """incomplete class with common tools"""


class MYDEVICE1(MYDEVICEGROUP):
    """first variation of the device group"""

GOOD:

import MDSplus


class _MYDEVICEGROUP(MDSplus.Device):
    """incomplete class with common tools"""


class MYDEVICE1(_MYDEVICEGROUP):
    """first variation of the device group"""

Condense device models/variants in one file

If possible define model that inherit from the same super class in one file. This will allows to store the source code with the device node and makes it easier to maintain. Changes to the superclass will affect all subclasses, gathering the subclasses with the superclass makes it obvious which devices are affected.

BAD:

  • _MYDEVICEGROUP.py
import MDSplus


class _MYDEVICEGROUP(MDSplus.Device):
    """incomplete class with common tools"""
  • MYDEVICE1000.py
from . import _MYDEVICEGROUP as SUPER


class MYDEVICE1000(SUPER._MYDEVICEGROUP):
    _model = 1000
  • MYDEVICE2000.py
from . import _MYDEVICEGROUP as SUPER


class MYDEVICE2000(SUPER._MYDEVICEGROUP):
    _model = 2000

GOOD:

  • MYDEVICE.py
import MDSplus


class _MYDEVICE(MDSplus.Device):
    """incomplete class with common tools"""


class MYDEVICE1000(_MYDEVICE):
    _model = 1000


class MYDEVICE2000(_MYDEVICE):
    _model = 2000

BETTER: Use decorators when dynamically defining classes.

Use decorators when dynamically defining classes

In case you have multiple variants of a device that would reflect in different static attributes (e.g. parts list), use builder methods that can be used as decorator to generate the assemblies. A decorator can be parametrized if defined as class implementing the __init__(self, ...) and __call__(self, cls) methods.

BAD:

import MDSplus


class _MYDEVICE(MDSplus.Device):
    """common methods and attributes"""
    parts = [{'path': ':HOSTNAME', 'type': 'text'}]


class MYDEVICE4(_MYDEVICE):
    parts = _MYDEVICE.parts + [
        {'path': ':NUM_CHANNELS', 'type': 'numeric', 'value': 4},
    ]
    for i in range(4):
        parts.append({'path': ':CHANNEL%02d' % i, 'type': 'signal'})
        parts.append({'path': ':CHANNEL%02d:MAX' % i, 'type': 'signal'})
    del(i)


class MYDEVICE8(_MYDEVICE):
    parts = _MYDEVICE.parts + [
        {'path': ':NUM_CHANNELS', 'type': 'numeric', 'value': 8},
    ]
    for i in range(8):
        parts.append({'path': ':CHANNEL%02d' % i, 'type': 'signal'})
        parts.append({'path': ':CHANNEL%02d:MAX' % i, 'type': 'signal'})
    del(i)

GOOD:

import MDSplus


class _MYDEVICE(MDSplus.Device):
    """common methods and attributes"""
    parts = [{'path': ':HOSTNAME', 'type': 'text'}]


def BUILDER(cls):
    cls.parts = _MYDEVICE.parts + [
        {'path': ':NUM_CHANNELS', 'type': 'numeric', 'value': cls._num_chan},
    ]
    for i in range(cls._num_chan):
        cls.parts.extend([
            {'path': ':CHANNEL%02d' % i, 'type': 'signal'},
            {'path': ':CHANNEL%02d:MAX' % i, 'type': 'signal'},
        ])


@BUILDER
class MYDEVICE4(_MYDEVICE):
    _num_chan = 4


@BUILDER
class MYDEVICE8(_MYDEVICE):
    _num_chan = 8

BETTER:

import MDSplus


class BUILDER:
    """Build MYDEVICE - parametrized decorator.

    Assemble device classes with different number of channels.

    PARAMETERS
    ==========
    channels: int
        number of channels of generated device class.
    """

    def __init__(self, channels):
        class SUPER(MDSplus.Device):
            """Define common methods and attributes."""

    _comon_parts = [
            {'path': ':HOSTNAME', 'type': 'text'},
            {'path': ':NUM_CHANNELS', 'type': 'numeric', 'value': self.nchan},
    ]

    def _channel_parts(self):
        for i in range(self.channels):
            yield {'path': ':CHANNEL%02d' % i, 'type': 'signal'},
            yield {'path': ':CHANNEL%02d:MAX' % i, 'type': 'signal'}

    def __call__(self, cls):
        cls.parts = _common_parts + list(self._channel_parts())
        return cls

@BUILDER(4)
class MYDEVICE4(BUILDER.SUPER):
    """mydevice with 4 channels"""


@BUILDER(8)
class MYDEVICE8(BUILDER.SUPER):
    """mydevice with 8 channels"""

Use unique class names for your devices

Device names must be unique throughout the total list of pydevices on a system, ensure this by choosing a descriptive name

BAD:

import MDSplus


class DEVICE(MDSplus.Device):
    """name that has now unique information"""

GOOD:

import MDSplus


class MYVENDOR_MYMODEL_MYVARIANT(MDSplus.Device):
    """descriptive name to increase the chance of uniqueness"""

Avoid atomic list extensions

In particular when defining the special attribute parts:

  • use list + list instead of list.copy() or copy.copy() when extending super classes.
  • use list comprehension [] rather than list.append().
  • use list.extend() rather than multiple list.append() methods.
  • avoid adding multiple parts in one line.
  • use the options key to characterize the parts.
  • ensure options is a tuple, in particular if it has only one element, add the essential leading comma, i.e. ('no_write_shot',).
  • mind the line width, break lists and dictionaries terminated by a comma ','.
  • assign constants to variables rather than duplication.
  • delete temporary variables so they will not appear as attribute.

BAD:

import MDSplus
import copy


class _MYSUPERDEVICE(MDSplus.Device):
    parts = [{'path':':HOST','type':'text'}]


class _MYDEVICE(_MYSUPERDEVICE):
    parts = copy.copy(_MYSUPERDEVICE.parts)
    parts.append({'path':':NUM_CHANNELS','type':'numeric','value':4})
    for i in range(4):
        parts.append({'path':':CHANNEL%02d' % i,'type':'signal', 'options':('no_write_model', 'write_once')})
        parts.append({'path':':CHANNEL%02d:GAIN' % i,'type':'numeric','options':('no_write_shot')})

GOOD:

import MDSplus


class _MYSUPERDEVICE(MDSplus.Device):
    parts = [
        {'path': ':HOST', 'type': 'text'},
    ]


class _MYDEVICE(_MYSUPERDEVICE):
    num_chan = 4
    parts = _MYSUPERDEVICE.parts + [
        {'path': ':NUM_CHANNELS',
         'type': 'numeric',
         'value': num_chan,
         'options': ('no_write_shot', 'write_once'),
        },
    ]
    for i in range(num_chan):
        parts.extend([
            {'path': ':CHANNEL%02d' % i,
             'type': 'signal',
             'options': ('no_write_model', 'write_once'),
            },
            {'path': ':CHANNEL%02d:GAIN' % i,
             'type': 'numeric',
             'options': ('no_write_shot',),
            },
        ])
    del(i, num_chan)

Use the options key to characterize the parts

Add write access restricting options (no_write_model, no_write_shot, write_once) where appropriate.

purpose options comment
constant ('no_write_shot', 'write_once') node contains constants that should not be tempered with by the operator, e.g. hostname, device_id.
setting ('no_write_shot',) node contains device settings that may be adjusted between shot cycles.
measurement ('no_write_model', 'write_once') node contains data that will be filled during a shot cycle, e.g. input channels.

These options will help the operator configuring the device and protect the measured data. In addition, it will trigger color highlighting in jTraverser2.

Avoid using plain print messages

Allow users to control the level of verbosity using a debuglevel. You can use the method dprint(debugleel, formatstr, *params) inherited from MDSplus.Device. The debuglevel is compared to self.debug which is an int set by getenv('DEBUG_DEVICES') and 0 by default. dprint only prints if debuglevel >= self.debug. DON'T print anything at module level (Exceptions should be raised).

BAD:

import MDSplus


class MYDEVICE(MDSplus.Device):
    def init(self):
        if MDSplus.version.ispy2:
            print("Cannot use this under python2")
        print("Entering init with a=%r, b=%r" % (self.a, self.b))

GOOD:

import MDSplus


class MYDEVICE(MDSplus.Device):
    def init(self):
        if MDSplus.version.ispy2:
            raise MDSplus.MDSplusFATAL("Cannot use MYDEVICE under python2") 
        self.dprint(3, "Entering init with a=%r, b=%r", self.a, self.b)
        my_vendors_api.init(self.init_args)

Let action methods raise MDSplusException

Handle python Exceptions in methods by raising an appropriate MDSplusException (MDSplus.mdsExceptions.*). You may also raise SUCCESS exceptions to signal a specific path thru the method. Replace status return values with raised MDSplusException on failure and no return value on success.

BAD:

import MDSplus
import socket


class MYDEVICE(MDSplus.Device):
    def init(self):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect((self._server, self._port))

STILL BAD:

import MDSplus
import socket


class MYDEVICE(MDSplus.Device):
    def init(self):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            sock.connect((self._server, self._port))
        except:
            return 0
        return 1

GOOD:

import MDSplus
import socket


class MYDEVICE(MDSplus.Device):
    def init(self):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            sock.connect((self._server, self._port))
        except socket.timeout:
            raise MDSplus.DevOFFLINE

Use lower case method names

In Action methods, you just need to refer to them in the correct case. Omit the method duplication init = INIT as this is unnecessary (You may see this for old devices but it is not required).