|
| 1 | +import os |
| 2 | +import importlib |
| 3 | +import imp |
| 4 | +import warnings |
| 5 | +import traceback |
| 6 | +import inspect |
| 7 | +from labscript_utils import dedent |
| 8 | +from labscript_utils.labconfig import LabConfig |
| 9 | + |
| 10 | +"""This file contains the machinery for registering and looking up what BLACS tab and |
| 11 | +runviewer parser classes belong to a particular labscript device. "labscript device" |
| 12 | +here means a device that BLACS needs to communicate with. These devices have |
| 13 | +instructions saved within the 'devices' group of the HDF5 file, and have a tab |
| 14 | +corresponding to them in the BLACS interface. These device classes must have unique |
| 15 | +names, such as "PineBlaster" or "PulseBlaster" etc. |
| 16 | +There are two methods we use to find out which BLACS tab and runviewer parser correspond |
| 17 | +to a device class: the "old" method, and the "new" method. The old method requires that |
| 18 | +the the BLACS tab and runviewer parser be in a file called <DeviceName>.py at the top |
| 19 | +level of labscript_devices folder, and that they have class decorators @BLACS_tab or |
| 20 | +@runviewer_parser to identify them. This method precludes putting code in subfolders or |
| 21 | +splitting it across multiple files. |
| 22 | +The "new" method is more flexible. It allows BLACS tabs and runviewer parsers to be |
| 23 | +defined in any importable file within a subfolder of labscript_devices. Additionally, |
| 24 | +the 'user_devices' configuration setting in labconfig can be used to specify a |
| 25 | +comma-delimited list of names of importable packages containing additional labscript |
| 26 | +devices. |
| 27 | +Classes using the new method can be in files with any name, and do not need class |
| 28 | +decorators. Instead, the classes should be registered by creating a file called |
| 29 | +'register_classes.py', which when imported, makes calls to |
| 30 | +labscript_devices.register_classes() to register which BLACS tab and runviewer parser |
| 31 | +class belong to each device. Tab and parser classes must be passed to register_classes() |
| 32 | +as fully qualified names, i.e. "labscript_devices.submodule.ClassName", not by passing |
| 33 | +in the classes themselves. This ensures imports can be deferred until the classes are |
| 34 | +actually needed. When BLACS and runviewer look up classes with get_BLACS_tab() and |
| 35 | +get_runviewer_parser(), populate_registry() will be called in order to find all files |
| 36 | +called 'register_classes.py' within subfolders (at any depth) of labscript_devices, and |
| 37 | +they will be imported to run their code and hence register their classes. |
| 38 | +The "new" method does not impose any restrictions on code organisation within subfolders |
| 39 | +of labscript_devices, and so is preferable as it allows auxiliary utilities or resource |
| 40 | +files to live in subfolders alongside the device code to which they are relevant, the |
| 41 | +use of subrepositories, the grouping of similar devices within subfolders, and other |
| 42 | +nice things to have. |
| 43 | +The old method may be deprecated in the future. |
| 44 | +""" |
| 45 | + |
| 46 | + |
| 47 | +__all__ = [ |
| 48 | + 'LABSCRIPT_DEVICES_DIRS', |
| 49 | + 'labscript_device', |
| 50 | + 'BLACS_worker', |
| 51 | + 'BLACS_tab', |
| 52 | + 'runviewer_parser', |
| 53 | + 'import_class_by_fullname', |
| 54 | + 'deprecated_import_alias', |
| 55 | + 'get_BLACS_tab', |
| 56 | + 'get_runviewer_parser', |
| 57 | + 'register_classes', |
| 58 | +] |
| 59 | + |
| 60 | + |
| 61 | +def _get_import_paths(import_names): |
| 62 | + """For the given list of packages, return all folders containing their submodules. |
| 63 | + If the packages do not exist, ignore them.""" |
| 64 | + paths = [] |
| 65 | + for name in import_names: |
| 66 | + spec = importlib.util.find_spec(name) |
| 67 | + if spec is not None and spec.submodule_search_locations is not None: |
| 68 | + paths.extend(spec.submodule_search_locations) |
| 69 | + return paths |
| 70 | + |
| 71 | + |
| 72 | +def _get_device_dirs(): |
| 73 | + """Return the directory of labscript_devices, and the folders containing |
| 74 | + submodules of any packages listed in the user_devices labconfig setting""" |
| 75 | + try: |
| 76 | + user_devices = LabConfig().get('DEFAULT', 'user_devices') |
| 77 | + except (LabConfig.NoOptionError, LabConfig.NoSectionError): |
| 78 | + user_devices = 'user_devices' |
| 79 | + # Split on commas, remove whitespace: |
| 80 | + user_devices = [s.strip() for s in user_devices.split(',')] |
| 81 | + return _get_import_paths(['labscript_devices'] + user_devices) |
| 82 | + |
| 83 | + |
| 84 | +LABSCRIPT_DEVICES_DIRS = _get_device_dirs() |
| 85 | + |
| 86 | + |
| 87 | +class ClassRegister(object): |
| 88 | + """A register for looking up classes by module name. Provides a |
| 89 | + decorator and a method for looking up classes decorated with it, |
| 90 | + importing as necessary.""" |
| 91 | + def __init__(self, instancename): |
| 92 | + self.registered_classes = {} |
| 93 | + # The name given to the instance in this namespace, so we can use it in error messages: |
| 94 | + self.instancename = instancename |
| 95 | + |
| 96 | + def __call__(self, cls): |
| 97 | + """Adds the class to the register so that it can be looked up later |
| 98 | + by module name""" |
| 99 | + # Add an attribute to the class so it knows its own name in case |
| 100 | + # it needs to look up other classes in the same module: |
| 101 | + cls.labscript_device_class_name = cls.__module__.split('.')[-1] |
| 102 | + if cls.labscript_device_class_name == '__main__': |
| 103 | + # User is running the module as __main__. Use the filename instead: |
| 104 | + import __main__ |
| 105 | + try: |
| 106 | + cls.labscript_device_class_name = os.path.splitext(os.path.basename(__main__.__file__))[0] |
| 107 | + except AttributeError: |
| 108 | + # Maybe they're running interactively? Or some other funky environment. Either way, we can't proceed. |
| 109 | + raise RuntimeError('Can\'t figure out what the file or module this class is being defined in. ' + |
| 110 | + 'If you are testing, please test from a more standard environment, such as ' + |
| 111 | + 'executing a script from the command line, or if you are using an interactive session, ' + |
| 112 | + 'writing your code in a separate module and importing it.') |
| 113 | + |
| 114 | + # Add it to the register: |
| 115 | + self.registered_classes[cls.labscript_device_class_name] = cls |
| 116 | + return cls |
| 117 | + |
| 118 | + def __getitem__(self, name): |
| 119 | + try: |
| 120 | + # Ensure the module's code has run (this does not re-import it if it is already in sys.modules) |
| 121 | + importlib.import_module('.' + name, 'labscript_devices') |
| 122 | + except ImportError: |
| 123 | + msg = """No %s registered for a device named %s. Ensure that there is a file |
| 124 | + 'register_classes.py' with a call to |
| 125 | + labscript_devices.register_classes() for this device, with the device |
| 126 | + name passed to register_classes() matching the name of the device class. |
| 127 | + Fallback method of looking for and importing a module in |
| 128 | + labscript_devices with the same name as the device also failed. If using |
| 129 | + this method, check that the module exists, has the same name as the |
| 130 | + device class, and can be imported with no errors. Import error |
| 131 | + was:\n\n""" |
| 132 | + msg = dedent(msg) % (self.instancename, name) + traceback.format_exc() |
| 133 | + raise ImportError(msg) |
| 134 | + # Class definitions in that module have executed now, check to see if class is in our register: |
| 135 | + try: |
| 136 | + return self.registered_classes[name] |
| 137 | + except KeyError: |
| 138 | + # No? No such class is defined then, or maybe the user forgot to decorate it. |
| 139 | + raise ValueError('No class decorated as a %s found in module %s, '%(self.instancename, 'labscript_devices' + '.' + name) + |
| 140 | + 'Did you forget to decorate the class definition with @%s?'%(self.instancename)) |
| 141 | + |
| 142 | + |
| 143 | +# Decorating labscript device classes and BLACS worker classes was never used for |
| 144 | +# anything and has been deprecated. These decorators can be removed with no ill |
| 145 | +# effects. Do nothing, and emit a warning telling the user this. |
| 146 | +def deprecated_decorator(name): |
| 147 | + def null_decorator(cls): |
| 148 | + msg = '@%s decorator is unnecessary and can be removed' % name |
| 149 | + warnings.warn(msg, stacklevel=2) |
| 150 | + return cls |
| 151 | + |
| 152 | + return null_decorator |
| 153 | + |
| 154 | + |
| 155 | +labscript_device = deprecated_decorator('labscript_device') |
| 156 | +BLACS_worker = deprecated_decorator('BLACS_worker') |
| 157 | + |
| 158 | + |
| 159 | +# These decorators can still be used, but their use will be deprecated in the future |
| 160 | +# once all devices in mainline are moved into subfolders with a register_classes.py that |
| 161 | +# will play the same role. For the moment we support both mechanisms of registering |
| 162 | +# which BLACS tab and runviewer parser class belong to a particular device. |
| 163 | +BLACS_tab = ClassRegister('BLACS_tab') |
| 164 | +runviewer_parser = ClassRegister('runviewer_parser') |
| 165 | + |
| 166 | + |
| 167 | +def import_class_by_fullname(fullname): |
| 168 | + """Import and return a class defined by its fully qualified name as an absolute |
| 169 | + import path, i.e. "module.submodule.ClassName".""" |
| 170 | + split = fullname.split('.') |
| 171 | + module_name = '.'.join(split[:-1]) |
| 172 | + class_name = split[-1] |
| 173 | + module = importlib.import_module(module_name) |
| 174 | + return getattr(module, class_name) |
| 175 | + |
| 176 | + |
| 177 | +def deprecated_import_alias(fullname): |
| 178 | + """A way of allowing a class to be imported from an old location whilst a) not |
| 179 | + actually importing it until it is instantiated and b) emitting a warning pointing to |
| 180 | + the new import location. fullname must be a fully qualified class name with an |
| 181 | + absolute import path. Use by calling in the module where the class used to be: |
| 182 | + ClassName = deprecated_import_alias("new.path.to.ClassName")""" |
| 183 | + calling_module_name = inspect.getmodule(inspect.stack()[1][0]).__name__ |
| 184 | + cls = [] |
| 185 | + def wrapper(*args, **kwargs): |
| 186 | + if not cls: |
| 187 | + cls.append(import_class_by_fullname(fullname)) |
| 188 | + shortname = fullname.split('.')[-1] |
| 189 | + newmodule = '.'.join(fullname.split('.')[:-1]) |
| 190 | + msg = """Importing %s from %s is deprecated, please instead import it from |
| 191 | + %s. Importing anyway for backward compatibility, but this may cause some |
| 192 | + unexpected behaviour.""" |
| 193 | + msg = dedent(msg) % (shortname, calling_module_name, newmodule) |
| 194 | + warnings.warn(msg, stacklevel=2) |
| 195 | + return cls[0](*args, **kwargs) |
| 196 | + return wrapper |
| 197 | + |
| 198 | + |
| 199 | +# Dictionaries containing the import paths to BLACS tab and runviewer parser classes, |
| 200 | +# not the classes themselves. These will be populated by calls to register_classes from |
| 201 | +# code within register_classes.py files within subfolders of labscript_devices. |
| 202 | +BLACS_tab_registry = {} |
| 203 | +runviewer_parser_registry = {} |
| 204 | +# The script files that registered each device, for use in error messages: |
| 205 | +_register_classes_script_files = {} |
| 206 | + |
| 207 | +# Wrapper functions to get devices out of the class registries. |
| 208 | +def get_BLACS_tab(name): |
| 209 | + if not BLACS_tab_registry: |
| 210 | + populate_registry() |
| 211 | + if name in BLACS_tab_registry: |
| 212 | + return import_class_by_fullname(BLACS_tab_registry[name]) |
| 213 | + # Fall back on file naming convention + decorator method: |
| 214 | + return BLACS_tab[name] |
| 215 | + |
| 216 | + |
| 217 | +def get_runviewer_parser(name): |
| 218 | + if not runviewer_parser_registry: |
| 219 | + populate_registry() |
| 220 | + if name in runviewer_parser_registry: |
| 221 | + return import_class_by_fullname(runviewer_parser_registry[name]) |
| 222 | + # Fall back on file naming convention + decorator method: |
| 223 | + return runviewer_parser[name] |
| 224 | + |
| 225 | + |
| 226 | +def register_classes(labscript_device_name, BLACS_tab=None, runviewer_parser=None): |
| 227 | + """Register the name of the BLACS tab and/or runviewer parser that belong to a |
| 228 | + particular labscript device. labscript_device_name should be a string of just the |
| 229 | + device name, i.e. "DeviceName". BLACS_tab_fullname and runviewer_parser_fullname |
| 230 | + should be strings containing the fully qualified import paths for the BLACS tab and |
| 231 | + runviewer parser classes, such as "labscript_devices.DeviceName.DeviceTab" and |
| 232 | + "labscript_devices.DeviceName.DeviceParser". These need not be in the same module as |
| 233 | + the device class as in this example, but should be within labscript_devices. This |
| 234 | + function should be called from a file called "register_classes.py" within a |
| 235 | + subfolder of labscript_devices. When BLACS or runviewer start up, they will call |
| 236 | + populate_registry(), which will find and run all such files to populate the class |
| 237 | + registries prior to looking up the classes they need""" |
| 238 | + if labscript_device_name in _register_classes_script_files: |
| 239 | + other_script =_register_classes_script_files[labscript_device_name] |
| 240 | + msg = """A device named %s has already been registered by the script %s. |
| 241 | + Labscript devices must have unique names.""" |
| 242 | + raise ValueError(dedent(msg) % (labscript_device_name, other_script)) |
| 243 | + BLACS_tab_registry[labscript_device_name] = BLACS_tab |
| 244 | + runviewer_parser_registry[labscript_device_name] = runviewer_parser |
| 245 | + script_filename = os.path.abspath(inspect.stack()[1][0].f_code.co_filename) |
| 246 | + _register_classes_script_files[labscript_device_name] = script_filename |
| 247 | + |
| 248 | + |
| 249 | +def populate_registry(): |
| 250 | + """Walk the labscript_devices folder looking for files called register_classes.py, |
| 251 | + and run them (i.e. import them). These files are expected to make calls to |
| 252 | + register_classes() to inform us of what BLACS tabs and runviewer classes correspond |
| 253 | + to their labscript device classes.""" |
| 254 | + # We import the register_classes modules as a direct submodule of labscript_devices. |
| 255 | + # But they cannot all have the same name, so we import them as |
| 256 | + # labscript_devices._register_classes_script_<num> with increasing number. |
| 257 | + module_num = 0 |
| 258 | + for devices_dir in LABSCRIPT_DEVICES_DIRS: |
| 259 | + for folder, _, filenames in os.walk(devices_dir): |
| 260 | + if 'register_classes.py' in filenames: |
| 261 | + # The module name is the path to the file, relative to the labscript suite |
| 262 | + # install directory: |
| 263 | + # Open the file using the import machinery, and import it as module_name. |
| 264 | + fp, pathname, desc = imp.find_module('register_classes', [folder]) |
| 265 | + module_name = 'labscript_devices._register_classes_%d' % module_num |
| 266 | + _ = imp.load_module(module_name, fp, pathname, desc) |
| 267 | + module_num += 1 |
0 commit comments