From MdsWiki
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).