Skip to content

Commit 2c7deba

Browse files
Merge pull request #61 from philipstarkey/break-circular-dependency
Relocate labscript_devices import machinery
2 parents d8b8343 + 39bef78 commit 2c7deba

File tree

2 files changed

+282
-0
lines changed

2 files changed

+282
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from ._device_registry import *
2+
3+
# Backwards compatibility for labscript-devices < 3.1. If labscript_devices defines the
4+
# device registry as well, undo the above import and use the contents of
5+
# labscript_devices instead. The above import must be done first so that the names are
6+
# available to labscript_devices during the below import, since as of 3.1 it imports
7+
# this module as well.
8+
try:
9+
from labscript_devices import ClassRegister
10+
if ClassRegister.__module__ == 'labscript_devices':
11+
for name in _device_registry.__all__:
12+
del globals()[name]
13+
from labscript_devices import *
14+
except ImportError:
15+
pass
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
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

Comments
 (0)