diff --git a/openflexure_microscope/__init__.py b/openflexure_microscope/__init__.py index 87999abd..ad56e205 100644 --- a/openflexure_microscope/__init__.py +++ b/openflexure_microscope/__init__.py @@ -1,4 +1,4 @@ -__all__ = ["Microscope", "config", "task", "lock", "utilities"] +__all__ = ["Microscope", "config", "task", "utilities"] __version__ = "0.1.0" from .microscope import Microscope diff --git a/openflexure_microscope/camera/capture.py b/openflexure_microscope/camera/capture.py index 8b4952a9..410c5560 100644 --- a/openflexure_microscope/camera/capture.py +++ b/openflexure_microscope/camera/capture.py @@ -81,6 +81,7 @@ def capture_from_exif(path, exif_dict): previous session. Args: + path (str): Path to image file exif_dict (dict): Dictionary containing capture information """ diff --git a/openflexure_microscope/camera/pi.py b/openflexure_microscope/camera/pi.py index 1f24480e..68f4d59b 100644 --- a/openflexure_microscope/camera/pi.py +++ b/openflexure_microscope/camera/pi.py @@ -68,8 +68,6 @@ class PiCameraStreamer(BaseCamera): ] def __init__(self): - global USER_CONFIG_DIR - # Run BaseCamera init BaseCamera.__init__(self) # Attach to Pi camera diff --git a/openflexure_microscope/config.py b/openflexure_microscope/config.py index 76f649be..5b6b2c1c 100644 --- a/openflexure_microscope/config.py +++ b/openflexure_microscope/config.py @@ -3,115 +3,24 @@ import os import errno import logging import shutil -import copy import numpy as np from fractions import Fraction -from .utilities import recursively_apply - """ Attributes: USER_CONFIG_DIR (str): Default path of the user-config directory, containing runtime-config and calibration files. Obtained from ``os.path.join(os.path.expanduser("~"), ".openflexure")``. - USER_CONFIG_FILE (str): Default path of the user microscope_settings.json runtime-config file. + USER_CONFIG_FILE_PATH (str): Default path of the user microscope_settings.json runtime-config file. Obtained from ``os.path.join(USER_CONFIG_DIR, "microscope_settings.json")`` + user_settings (OpenflexureSettingsFile): Default settings file object, which handles expansion and + contraction of settings dictionaries. """ -# HANDLE THE DEFAULT CONFIGURATION FILE - -HERE = os.path.abspath(os.path.dirname(__file__)) -DEFAULT_CONFIG_PATH = os.path.join(HERE, "microscope_settings.default.json") - -USER_CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".openflexure") -USER_CONFIG_FILE = os.path.join(USER_CONFIG_DIR, "microscope_settings.json") - -USER_CONFIG_FILE_OLD = os.path.join(USER_CONFIG_DIR, "microscoperc.json") - -# Load the default config -with open(DEFAULT_CONFIG_PATH, "r") as default_rc: - DEFAULT_CONFIG = default_rc.read() - - -# HANDLE BASIC LOADING AND SAVING OF SETTINGS FILES - - -def load_json_file(config_path) -> dict: - """ - Open a .json config file - - Args: - config_path (str): Path to the config JSON file. If `None`, defaults to `DEFAULT_CONFIG_PATH` - """ - config_path = os.path.expanduser(config_path) - - logging.info("Loading {}...".format(config_path)) - - with open(config_path) as config_file: - try: - config_data = json.load(config_file) - except json.decoder.JSONDecodeError as e: - logging.error(e) - config_data = {} - - # Return loaded config dictionary - return config_data - - -def save_json_file(config_path: str, config_dict: dict): - """ - Save a .json config file - - Args: - config_dict (dict): Dictionary of config data to save. - config_path (str): Path to the config JSON file. - """ - config_path = os.path.expanduser(config_path) - - logging.info("Saving {}...".format(config_path)) - logging.debug(config_dict) - - with open(config_path, "w") as outfile: - json.dump(config_dict, outfile, cls=JSONEncoder, indent=2, sort_keys=True) - - -def create_file(config_path): - if not os.path.exists(os.path.dirname(config_path)): - try: - os.makedirs(os.path.dirname(config_path)) - except OSError as exc: # Guard against race condition - if exc.errno != errno.EEXIST: - raise - - -def initialise_file(config_path, populate: str = "{}\n"): - """ - Check if a file exists, and if not, create it - and optionally populate it with content - - Args: - config_path (str): Path to the file. - populate (str): String to dump to the file, if it is being newly created - """ - config_path = os.path.expanduser(config_path) - - logging.debug("Initialising {}".format(config_path)) - logging.debug("Exists: {}".format(os.path.exists(config_path))) - - if not os.path.exists(config_path): # If user config file doesn't exist - logging.warning("No config file found at {}. Creating...".format(config_path)) - create_file(config_path) - - logging.info("Populating {}...".format(config_path)) - with open(config_path, "w") as outfile: - outfile.write(populate) - - -def settings_file_path(filename:str): - """Generate a full file path for a filename to be stored in user settings""" - global USER_CONFIG_DIR - return os.path.join(USER_CONFIG_DIR, filename) class JSONEncoder(json.JSONEncoder): + """ + A custom JSON encoder, with type conversions for PiCamera fractions, Numpy integers, and Numpy arrays + """ def default(self, o, markers=None): # PiCamera fractions if isinstance(o, Fraction): @@ -127,6 +36,7 @@ class JSONEncoder(json.JSONEncoder): # raising exceptions for unsupported types return json.JSONEncoder.default(self, o) + # MAIN CONFIG CLASS class OpenflexureSettingsFile: @@ -139,7 +49,7 @@ class OpenflexureSettingsFile: """ def __init__(self, config_path: str = None, expand: bool = True): - global DEFAULT_CONFIG, USER_CONFIG_FILE + global DEFAULT_CONFIG, USER_CONFIG_FILE_PATH self.expandable_keys = { "camera_settings": None, @@ -147,7 +57,7 @@ class OpenflexureSettingsFile: } #: Dictionary of keys that can be passed as a file path string and expanded automatically # Set arguments - self.config_path = config_path or USER_CONFIG_FILE + self.config_path = config_path or USER_CONFIG_FILE_PATH self.expand = expand # Initialise basic config file with defaults if it doesn't exist @@ -250,3 +160,98 @@ class OpenflexureSettingsFile: return_config[key] = value return return_config + + +# HANDLE BASIC LOADING AND SAVING OF SETTINGS FILES + +def load_json_file(config_path) -> dict: + """ + Open a .json config file + + Args: + config_path (str): Path to the config JSON file. If `None`, defaults to `DEFAULT_CONFIG_PATH` + """ + config_path = os.path.expanduser(config_path) + + logging.info("Loading {}...".format(config_path)) + + with open(config_path) as config_file: + try: + config_data = json.load(config_file) + except json.decoder.JSONDecodeError as e: + logging.error(e) + config_data = {} + + # Return loaded config dictionary + return config_data + + +def save_json_file(config_path: str, config_dict: dict): + """ + Save a .json config file + + Args: + config_dict (dict): Dictionary of config data to save. + config_path (str): Path to the config JSON file. + """ + config_path = os.path.expanduser(config_path) + + logging.info("Saving {}...".format(config_path)) + logging.debug(config_dict) + + with open(config_path, "w") as outfile: + json.dump(config_dict, outfile, cls=JSONEncoder, indent=2, sort_keys=True) + + +def create_file(config_path): + if not os.path.exists(os.path.dirname(config_path)): + try: + os.makedirs(os.path.dirname(config_path)) + except OSError as exc: # Guard against race condition + if exc.errno != errno.EEXIST: + raise + + +def initialise_file(config_path, populate: str = "{}\n"): + """ + Check if a file exists, and if not, create it + and optionally populate it with content + + Args: + config_path (str): Path to the file. + populate (str): String to dump to the file, if it is being newly created + """ + config_path = os.path.expanduser(config_path) + + logging.debug("Initialising {}".format(config_path)) + logging.debug("Exists: {}".format(os.path.exists(config_path))) + + if not os.path.exists(config_path): # If user config file doesn't exist + logging.warning("No config file found at {}. Creating...".format(config_path)) + create_file(config_path) + + logging.info("Populating {}...".format(config_path)) + with open(config_path, "w") as outfile: + outfile.write(populate) + + +def settings_file_path(filename: str): + """Generate a full file path for a filename to be stored in user settings""" + global USER_CONFIG_DIR + return os.path.join(USER_CONFIG_DIR, filename) + + +# HANDLE THE DEFAULT CONFIGURATION FILE + +HERE = os.path.abspath(os.path.dirname(__file__)) +DEFAULT_CONFIG_FILE_PATH = os.path.join(HERE, "microscope_settings.default.json") + +USER_CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".openflexure") +USER_CONFIG_FILE_PATH = os.path.join(USER_CONFIG_DIR, "microscope_settings.json") + +# Load the default config +with open(DEFAULT_CONFIG_FILE_PATH, "r") as default_rc: + DEFAULT_CONFIG = default_rc.read() + +# Create the default user settings object +user_settings = OpenflexureSettingsFile(config_path=USER_CONFIG_FILE_PATH, expand=True) diff --git a/openflexure_microscope/microscope.py b/openflexure_microscope/microscope.py index 044c90d8..d52efdf7 100644 --- a/openflexure_microscope/microscope.py +++ b/openflexure_microscope/microscope.py @@ -14,7 +14,7 @@ from openflexure_microscope.camera.mock import MockStreamer from openflexure_microscope.plugins import PluginMount from openflexure_microscope.task import TaskOrchestrator from openflexure_microscope.common.lock import CompositeLock -from openflexure_microscope.config import OpenflexureSettingsFile +from openflexure_microscope.config import user_settings class Microscope: @@ -24,8 +24,6 @@ class Microscope: The camera and stage should already be initialised, and passed as arguments. Attributes: - settings_file (:py:class:`openflexure_microscope.config.OpenflexureSettingsFile`): Runtime-config object, - automatically created if None. lock (:py:class:`openflexure_microscope.common.lock.CompositeLock`): Composite lock controlling thread access to multiple pieces of hardware. camera (:py:class:`openflexure_microscope.camera.base.BaseCamera`): Camera object @@ -50,10 +48,8 @@ class Microscope: # Create a task orchestrator self.task = TaskOrchestrator() - # Attach to a settings file - self.settings_file = OpenflexureSettingsFile(expand=True) # Apply settings loaded from file - self.apply_config(self.settings_file.load()) + self.apply_config(user_settings.load()) # Create plugin mount-point and attach plugins from maps self.plugin = PluginMount(self) @@ -223,14 +219,11 @@ class Microscope: settings_current_stage = self.stage.read_config() settings_current["stage_settings"] = settings_current_stage - settings_full = self.settings_file.merge(settings_current) - - #if json_safe: - # settings_full = settings_to_json(settings_full) + settings_full = user_settings.merge(settings_current) return settings_full - def save_config(self, backup: bool = True): + def save_config(self): """ Merges the current settings back to disk """ @@ -245,11 +238,11 @@ class Microscope: self.camera.save_config() if self.stage: self.stage.save_config() - self.settings_file.save(current_config, backup=True) + user_settings.save(current_config, backup=True) @property def config(self) -> dict: - logging.warn( + logging.warning( "Reading microscope through config property is deprecated.\ Please use read_config method instead." ) @@ -257,7 +250,7 @@ class Microscope: @config.setter def config(self, config: dict) -> None: - logging.warn( + logging.warning( "Setting microscope through config property is deprecated.\ Please use apply_config method instead." )