Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
276eba7
Extract reusable shot queue widget
Mar 17, 2026
cbbcc4f
Fix shot queue widget edge cases
Mar 17, 2026
afe5018
Merge branch 'master' into RunmanagerQueue
Mar 17, 2026
e44fbe1
Make excepthook warning hook idempotent
Mar 17, 2026
b522ed3
Fix SecureSocket detection in ls_zprocess Context
Mar 17, 2026
5025633
Support NumPy scalar JSON serialization
Mar 17, 2026
5916918
Merge branch 'BugFix2026' into RunmanagerQueue
Mar 17, 2026
5ccbfba
Configure shared Qt application defaults
Mar 18, 2026
805d94e
Fix TOML globals profile bootstrap
Mar 25, 2026
206a8ce
Merge branch 'RunmanagerTOMLGlobals' into Production
Mar 25, 2026
dee0033
Migrate shared config handling to TOML
Mar 19, 2026
806fab5
Normalize labconfig TOML section names
Mar 19, 2026
c24a4fe
Updated example.toml.
Mar 19, 2026
7a623ba
Refine TOML labconfig normalization
Mar 20, 2026
11e77a8
Fix zlock launch with TOML port values
Mar 20, 2026
c172cae
Prefer canonical TOML over stale legacy app configs
Mar 21, 2026
5725e3e
Set macOS app icons from splash path
Mar 26, 2026
89fdbde
Inline redundant splash QApplication helper
Mar 26, 2026
dcbe963
Merge branch 'labscript-suite:master' into MacOS_icon
ispielma Mar 26, 2026
2367a7a
Merge branch 'MacOS_icon' into Production
Mar 26, 2026
101c53b
Merge branch 'Production' into toml
Mar 26, 2026
454570c
Add shared indexed filepath helper
Mar 26, 2026
8bf4faa
Make indexed filepath start explicit
Apr 6, 2026
d87ad88
Simplify shared shot queue controls
Apr 6, 2026
7c13f18
Use context-menu deletion in shot queue widget
Apr 6, 2026
2999939
Handle macOS delete key in shot queue
Apr 6, 2026
5876c9b
Move shot queue column support into shared widget
Apr 6, 2026
a660654
Support path column selection in shot queue widget
Apr 6, 2026
9e581db
Use external scrollbar for shot queue widget
Apr 6, 2026
fe14e61
Reserve corner space for shot queue scrollbar
Apr 6, 2026
94d9d2e
Remove shot queue scrollbar corner workaround
Apr 6, 2026
c515061
Make shot queue default hookups optional
Apr 6, 2026
65f8ccd
Add lookup formatter for runmanager templates
Apr 7, 2026
d0175eb
Tighten lookup format unresolved handling
Apr 7, 2026
f1d4a98
Show active config path in window title
Apr 7, 2026
93756e3
Merge branch 'UpdatedTitleBars' into Production
Apr 7, 2026
01ea7b1
Merge branch 'UpdatedTitleBars' into RunmanagerGlobalFormatStrings
Apr 7, 2026
2621679
Merge branch 'UpdatedTitleBars' into RunmanagerQueueSimple
Apr 7, 2026
cf2da0f
Merge branch 'RunmanagerGlobalFormatStrings' into RunmanagerQueueSimple
Apr 7, 2026
465943d
Add shared plugin framework
Apr 25, 2026
fe141ea
Merge branch 'PluginRefactor' into Production
Apr 25, 2026
48da3c3
Merge branch 'Production' into RunmanagerQueueSimple
Apr 25, 2026
3cef7b3
More generalized plugins.
Apr 25, 2026
dd8664f
Final revision.
Apr 26, 2026
4c7866c
Merge branch 'PluginRefactor' into RunmanagerQueueSimple
Apr 26, 2026
83f44ff
Add shared plugin framework to labscript-utils
Apr 26, 2026
aaa642b
Added ability to share services.
Apr 27, 2026
44f7aab
Add plugin service aggregation tests
Apr 27, 2026
a4f8067
Merge PluginRefactor into RunmanagerQueueSimple
Apr 28, 2026
57c889f
Add ordered plugin setup activities
May 11, 2026
8bb9c06
Merge branch 'PluginRefactor' into RunmanagerQueueSimple
May 11, 2026
17e36cc
Set macOS Qt application names from desktop app config
May 12, 2026
5c8ae53
Merge branch 'MacOS_icon' into RunmanagerQueueSimple
May 12, 2026
b8989e3
Read macOS Qt app name from application property
May 12, 2026
80e1368
Merge branch 'MacOS_icon' into RunmanagerQueueSimple
May 12, 2026
a95f883
Set macOS QApplication argv name from splash
May 12, 2026
e21d4b8
Merge branch 'MacOS_icon' into RunmanagerQueueSimple
May 12, 2026
c5cbf71
Configure splash icon through QApplication helper
May 12, 2026
6dae69b
Rename splash image path argument
May 12, 2026
7862f92
Configure macOS Qt application presentation
May 12, 2026
3080080
Merge branch 'MacOS_icon' into RunmanagerQueueSimple
May 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## Unreleased

- TOML-backed labconfig and app-config writing now depends on `tomli-w`. The package
dependency is declared in `labscript-utils`, but existing editable or conda
environments may need that dependency installed explicitly when upgrading.


## [2.15.0] - 2019-12-04

This release includes one bugfix, one enhancement, one update for compatibility with a
Expand Down Expand Up @@ -60,4 +67,4 @@ compatibility with a newer version of a library.
([PR #91](https://bitbucket.org/labscript_suite/labscript_utils/pull-requests/91))

- Compatibility with `importlib_metadata` >= 0.21. Contributed by Chris Billington
([PR #92](https://bitbucket.org/labscript_suite/labscript_utils/pull-requests/88))
([PR #92](https://bitbucket.org/labscript_suite/labscript_utils/pull-requests/88))
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,9 @@ Shared modules used by the [*labscript suite*](https://github.com/labscript-suit
## Installation

labscript-utils is distributed as a Python package on [PyPI](https://pypi.org/user/labscript-suite) and [Anaconda Cloud](https://anaconda.org/labscript-suite), and should be installed with other components of the _labscript suite_. Please see the [installation guide](https://docs.labscriptsuite.org/en/latest/installation) for details.

TOML-backed configuration support depends on `tomli-w` at runtime. This dependency is
declared in `labscript-utils` package metadata, so fresh installs should receive it
automatically. Existing editable or conda environments created before this dependency
was added may need `conda install tomli-w` or an updated `labscript-utils`
installation.
16 changes: 8 additions & 8 deletions docs/source/labconfig.rst
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
The *labconfig.ini* file
The *labconfig.toml* file
========================

The `labconfig.ini` file is a global configuration file for your **labscript-suite** installation.
The `labconfig.toml` file is a global configuration file for your **labscript-suite** installation.
It contains configurable settings that govern how the individual components of the suite operate.
The name of this file must be the host computer's system name.
So if my system's name was `heisenberg`, the labconfig file name would be `heisenberg.ini`.
So if my system's name was `heisenberg`, the labconfig file name would be `heisenberg.toml`.
This file should be located in the `labscript-suite` directory in the user space, in the `labconfig` subdirectory.

When :doc:`installing the **labscript-suite** for the first time <labscript-suite:installation/index>`,
running the `labscript-profile-create` command will automatically generate the `labscript-suite` user space directory in the correct place
and generate a `labconfig.ini` file for use on your system.
By editing the `ini` file named after your system, you can update the configuration settings of your **labscript-suite** installation.
and generate a `labconfig.toml` file for use on your system.
By editing the TOML file named after your system, you can update the configuration settings of your **labscript-suite** installation.

The Default *labconfig.ini*
---------------------------
The Default *labconfig.toml*
----------------------------

Below is a copy of the default lab configuration if you were to install the **labscript-suite** today.

Expand All @@ -23,5 +23,5 @@ Below is a copy of the default lab configuration if you were to install the **la
Instead, if keys are missing from your local profile, default behavior will be assumed.
To implement the added functionality, you will need to manually add/change the keys in your local labconfig.

.. include:: ../../labscript_profile/default_profile/labconfig/example.ini
.. include:: ../../labscript_profile/default_profile/labconfig/example.toml
:code:
133 changes: 119 additions & 14 deletions labscript_profile/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import site
import sys
import os
from configparser import ConfigParser, NoSectionError, NoOptionError
import configparser
from ast import literal_eval
from pathlib import Path
from subprocess import check_output
import socket
from getpass import getuser

from .toml_config import TomlConfigParser, dump_toml_file, load_toml_file

# The contents of this file are imported every time the Python interpreter starts up,
# owing to our custom .pth file that runs the below two functions. This ensures that
Expand Down Expand Up @@ -36,6 +38,13 @@ def hostname():


def default_labconfig_path():
if LABSCRIPT_SUITE_PROFILE is None:
return None
return LABSCRIPT_SUITE_PROFILE / 'labconfig' / f'{hostname()}.toml'

# LEGACY INI COMPATIBILITY. DEPRECATED CODE, WILL BE REMOVED.
def legacy_labconfig_path():
"""Temporary legacy INI labconfig location. Slated for removal soon."""
if LABSCRIPT_SUITE_PROFILE is None:
return None
return LABSCRIPT_SUITE_PROFILE / 'labconfig' / f'{hostname()}.ini'
Expand All @@ -47,17 +56,113 @@ def add_userlib_and_pythonlib():
re-implements finding and reading the config file so as to not import
labscript_utils, since we dont' want to import something like labscript_utils every
time the interpreter starts up"""
labconfig = default_labconfig_path()
if labconfig is not None and labconfig.exists():
# str() below is for py36 compat, where ConfigParser can't deal with Path objs
config = ConfigParser(
defaults={'labscript_suite': str(LABSCRIPT_SUITE_PROFILE)}
)
config.read(labconfig)
for option in ['userlib', 'pythonlib']:
try:
paths = config.get('DEFAULT', option).split(',')
except (NoSectionError, NoOptionError):
paths = []
for path in paths:
labconfig = ensure_labconfig()
if labconfig is None:
return
if not labconfig.exists():
return
config = TomlConfigParser(defaults={'labscript_suite': str(LABSCRIPT_SUITE_PROFILE)})
config.read_toml(labconfig)
for option in ['userlib', 'pythonlib']:
try:
paths = config.get('default', option)
except (configparser.NoSectionError, configparser.NoOptionError):
paths = ''
if paths:
for path in paths.split(','):
site.addsitedir(path)


def ensure_labconfig():
"""Return the TOML labconfig path, converting a legacy INI file if needed.

The legacy conversion path is temporary and slated for removal soon.
"""
labconfig = default_labconfig_path()
if labconfig is None:
return None
if labconfig.exists():
return labconfig
# LEGACY INI COMPATIBILITY. DEPRECATED CODE, WILL BE REMOVED.
legacy = legacy_labconfig_path()
if legacy is None or not legacy.exists():
return labconfig
_convert_legacy_labconfig(legacy, labconfig)
return labconfig


def _example_labconfig_data():
if LABSCRIPT_SUITE_PROFILE is not None:
profile_example = LABSCRIPT_SUITE_PROFILE / 'labconfig' / 'example.toml'
if profile_example.exists():
return load_toml_file(profile_example)
bundled_example = Path(__file__).resolve().parent / 'default_profile' / 'labconfig' / 'example.toml'
if bundled_example.exists():
return load_toml_file(bundled_example)
return {}

# LEGACY INI COMPATIBILITY. DEPRECATED CODE, WILL BE REMOVED.
def _coerce_legacy_value(value, template_value):
if template_value is None:
return value
if isinstance(template_value, bool):
if isinstance(value, bool):
return value
if isinstance(value, str):
lowered = value.strip().lower()
if lowered in configparser.ConfigParser.BOOLEAN_STATES:
return configparser.ConfigParser.BOOLEAN_STATES[lowered]
return value
if isinstance(template_value, int):
try:
return int(value)
except (TypeError, ValueError):
return value
if isinstance(template_value, float):
try:
return float(value)
except (TypeError, ValueError):
return value
if isinstance(template_value, list):
if not isinstance(value, str):
return value
try:
parsed = literal_eval(value)
except (SyntaxError, ValueError):
return value
if not isinstance(parsed, (list, tuple)):
return value
if not template_value:
return list(parsed)
template_item = template_value[0]
return [_coerce_legacy_value(item, template_item) for item in parsed]
return value

# LEGACY INI COMPATIBILITY. DEPRECATED CODE, WILL BE REMOVED.
def _convert_legacy_labconfig(legacy_path, toml_path):
"""Temporary legacy migration path from INI to TOML. Slated for removal soon."""
config = configparser.ConfigParser(interpolation=None)
config.read(legacy_path)
template_data = _example_labconfig_data()
default_template = template_data.get('default', {})
data = {
'default': {
key: _coerce_legacy_value(value, default_template.get(key))
for key, value in config.defaults().items()
}
}
for section in config.sections():
canonical_section = section.lower()
section_items = dict(config._sections[section])
section_items.pop('__name__', None)
autoload = section_items.get('autoload_config_file')
# LEGACY INI COMPATIBILITY. DEPRECATED CODE, WILL BE REMOVED.
if isinstance(autoload, str) and autoload.lower().endswith('.ini'):
section_items['autoload_config_file'] = autoload[:-4] + '.toml'
section_template = template_data.get(canonical_section, {})
data[canonical_section] = {
key: _coerce_legacy_value(value, section_template.get(key))
for key, value in section_items.items()
}
toml_path.parent.mkdir(parents=True, exist_ok=True)
dump_toml_file(toml_path, data)
66 changes: 40 additions & 26 deletions labscript_profile/create.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
import sys
import os
import shutil
import configparser
from pathlib import Path
from subprocess import check_output
from labscript_profile import LABSCRIPT_SUITE_PROFILE, default_labconfig_path
import h5py
from labscript_profile import (
LABSCRIPT_SUITE_PROFILE,
default_labconfig_path,
legacy_labconfig_path,
)
import argparse
from labscript_profile.toml_config import dump_toml_file, load_toml_file

_here = os.path.dirname(os.path.abspath(__file__))
DEFAULT_PROFILE_CONTENTS = os.path.join(_here, 'default_profile')


def _replace_backslashes(value):
if isinstance(value, dict):
return {key: _replace_backslashes(child) for key, child in value.items()}
if isinstance(value, list):
return [_replace_backslashes(child) for child in value]
if isinstance(value, str):
return value.replace('\\', os.path.sep)
return value


def make_shared_secret(directory):
"""Create a new zprocess shared secret file in the given directory and return its
filepath"""
Expand All @@ -31,37 +46,34 @@ def make_labconfig_file(apparatus_name = None):
Overrides the default apparatus name with the provided one if not None
"""

source_path = os.path.join(LABSCRIPT_SUITE_PROFILE, 'labconfig', 'example.ini')
source_path = os.path.join(LABSCRIPT_SUITE_PROFILE, 'labconfig', 'example.toml')
target_path = default_labconfig_path()
if os.path.exists(target_path):
# LEGACY INI COMPATIBILITY. DEPRECATED CODE, WILL BE REMOVED.
legacy_path = legacy_labconfig_path()
if os.path.exists(target_path) or (legacy_path is not None and os.path.exists(legacy_path)):
raise FileExistsError(target_path)
with open(source_path) as infile, open(target_path, 'w') as outfile:
data = infile.read()
data = data.replace('\\', os.path.sep)
outfile.write(data)

# Now change some things about it:
config = configparser.ConfigParser(interpolation=None)
config.read(target_path)
config = load_toml_file(source_path)
if os.path.sep != '\\':
config = _replace_backslashes(config)
if sys.platform == 'linux':
config.set('programs', 'text_editor', 'gedit')
config['programs']['text_editor'] = 'gedit'
elif sys.platform == 'darwin':
config.set('programs', 'text_editor', 'open')
config.set('programs', 'text_editor_arguments', '-a TextEdit {file}')
config['programs']['text_editor'] = 'open'
config['programs']['text_editor_arguments'] = '-a TextEdit {file}'
if sys.platform != 'win32':
config.set('programs', 'hdf5_viewer', 'hdfview')
config.set('DEFAULT', 'shared_drive', '$HOME/labscript_shared')
config['programs']['hdf5_viewer'] = 'hdfview'
config['default']['shared_drive'] = '$HOME/labscript_shared'
shared_secret = make_shared_secret(target_path.parent)
shared_secret_entry = Path(
'%(labscript_suite)s', shared_secret.relative_to(LABSCRIPT_SUITE_PROFILE)
)
config.set('security', 'shared_secret', str(shared_secret_entry))
config['security']['shared_secret'] = str(shared_secret_entry)
if apparatus_name is not None:
print(f'\tSetting apparatus name to \'{apparatus_name}\'')
config.set('DEFAULT', 'apparatus_name', apparatus_name)
config['default']['apparatus_name'] = apparatus_name

with open(target_path, 'w') as f:
config.write(f)
target_path.parent.mkdir(parents=True, exist_ok=True)
dump_toml_file(target_path, config)

def compile_connection_table():
"""Compile the connection table defined in the labconfig file
Expand All @@ -75,17 +87,19 @@ def compile_connection_table():
# if runmanager doesn't import, skip compilation
return

config = configparser.ConfigParser(defaults = {'labscript_suite': str(LABSCRIPT_SUITE_PROFILE)})
config.read(default_labconfig_path())
from labscript_utils.labconfig import LabConfig

config = LabConfig()

# The path to the user's connection_table.py script
script_path = os.path.expandvars(config['paths']['connection_table_py'])
# path to the connection_table.h5 destination
output_h5_path = os.path.expandvars(config['paths']['connection_table_h5'])
# create output directory, if needed
Path(output_h5_path).parent.mkdir(parents=True, exist_ok=True)
# compile the h5 file
runmanager.new_globals_file(output_h5_path)
# Create a fresh HDF5 target for the compile output.
with h5py.File(output_h5_path, 'w'):
pass

def dummy_callback(success):
pass
Expand Down Expand Up @@ -164,4 +178,4 @@ def create_profile(apparatus_name = None, compile_table = False):

if compile_table:
print('Compiling the example connection table')
compile_connection_table()
compile_connection_table()
Loading