Skip to content

Commit

Permalink
Let nmr user choose whether and where to add path to folder name (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
matterhorn103 authored Jun 11, 2024
1 parent 1e8b748 commit 4c1fdae
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 97 deletions.
1 change: 1 addition & 0 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ group = "gil"
dest_path = "copy full path here"
inc_init = false
inc_solv = false
inc_path = false # Only for nmr - set to "after" to append path, "before" to prepend
nmrcheck_style = false
spec = "400er"
repeat_switch = false
Expand Down
4 changes: 2 additions & 2 deletions mora_the_explorer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ def set_dark_mode(app):
app.setPalette(dark_palette)


def run(rsrc_dir):
def run(rsrc_dir: Path):
"""Run Mora the Explorer."""

# Load configuration
logging.info(f"Program resources located at {rsrc_dir}")
logging.info("Loading program settings...")
config = Config(rsrc_dir)
config = Config(rsrc_dir / "config.toml")
logging.info("...complete")

logging.info("Initializing program...")
Expand Down
12 changes: 9 additions & 3 deletions mora_the_explorer/checknmr.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,12 +212,17 @@ def format_name(
return name


def format_name_admin(folder, metadata) -> str:
def format_name_admin(folder, metadata, inc_path=False) -> str:
"""Format folder name in Klaus' desired fashion."""
# First do normally but with everything included
name = format_name(folder, metadata, inc_group=True, inc_init=True, inc_solv=True)
# Add location details in front
name = metadata["server_location"].replace("/", "_").replace("\\", "_") + "_" + name
# Add location details if requested
if inc_path:
location = metadata["server_location"].replace("/", "_").replace("\\", "_")
if inc_path == "before" or inc_path is True:
name = location + "_" + name
elif inc_path == "after":
name = name + "_" + location
return name


Expand Down Expand Up @@ -514,6 +519,7 @@ def check_nmr(
new_folder_name = format_name_admin(
folder,
metadata,
inc_path=fed_options["inc_path"]
)
else:
new_folder_name = format_name(
Expand Down
180 changes: 127 additions & 53 deletions mora_the_explorer/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,89 +8,163 @@


class Config:
"""Returns a container for the combined app and user configuration data."""
"""Returns a container for the combined app and user configuration data.
A Config object contains two kinds of config: a `user_config` and an `app_config`.
def __init__(self, resource_directory):
self.rsrc_dir = resource_directory
Both are Python dictionaries with the same structures as `config.toml`, which
currently has the following tables of key/value pairs:
# Load app config from config.toml
with open(self.rsrc_dir / "config.toml", "rb") as f:
self.app_config = tomllib.load(f)
logging.info(
f"App configuration loaded from: {self.rsrc_dir / "config.toml"}"
)
```toml
[options] # or [default_options] in the defaults file
[paths]
[groups]
[groups.other]
[spectrometers.xxx]
[spectrometers.yyy] # etc. for each spectrometer
```
# Load user config from config.toml in user's config directory
# Make one if it doesn't exist yet
The app's resources directory contains a `config.toml` holding all the default app
settings, and a second `config.toml` gets saved to a sensible location on the user's
system to store their personal choices.
The `user_config` usually only has the `[options]` table from `config.toml`,
containing the options the user sets, such as search options and save
destination. It may contain any of the above tables, though. Any missing entries in
the `[options]` table are filled from the `[default_options]` table in the defaults
`config.toml` file.
The `app_config` specifies the other details for how the app should work, e.g. the
information about the available groups and spectrometers, as well as the default
user options.
# Config should be saved to:
At program start, both the app config and user config are loaded from their files,
anything in `user_config` is put into `app_config`, overwriting where appropriate,
and anything missing from `user_config["options"]` is filled from
`app_config["default_options"]`. After this, there is no further interaction
between the two configs; only `user_config` changes during runtime due to the user's
actions, while `app_config` stays static.
Calling `Config.save()` saves `user_config` to file, not the `app_config`.
"""

def __init__(self, app_config_file: Path):

# Load app config from config.toml
self.app_config = self.load_config_toml(app_config_file)
logging.info(f"App configuration loaded from: {app_config_file}")

# Load or create user config
# platformdirs automatically saves the config file in the place appropriate to
# the os, which should be:
# Windows: c:/Users/<user>/AppData/Roaming/mora_the_explorer/config.toml
# macOS: /Users/<user>/Library/Application Support/mora_the_explorer/config.toml
# Linux: /home/<user>/.config/mora_the_explorer/config.toml

# User options used to be stored in config.json pre v1.7, so also check for that
# platformdirs automatically saves the config file in the place appropriate to the os
self.user_config_file = Path(platformdirs.user_config_dir(
"mora_the_explorer",
roaming=True,
ensure_exists=True,
)
) / "config.toml"
user_config_json = self.user_config_file.with_name("config.json")

# Load user config from config.toml in user's config directory
if self.user_config_file.exists() is True:
with open(self.user_config_file, "rb") as f:
self.user_config = tomllib.load(f)
self.user_config = self.load_config_toml(self.user_config_file)
logging.info(f"User configuration loaded from: {self.user_config_file}")
elif user_config_json.exists() is True:
# Load json, save as toml instead, remove old json to avoid confusion
with open(user_config_json, encoding="utf-8") as f:
old_options = json.load(f)
# Add new options
self.user_config = {
"options": old_options,
"paths": {"Linux": "overwrite with default mount point"},
}
with open(self.user_config_file, "wb") as f:
tomli_w.dump(self.user_config, f)
user_config_json.unlink()
self.extend_user_config(self.user_config, self.app_config)
# Update any app settings specified in the user config
self.update_app_config(self.user_config)

# User options used to be stored in config.json pre v1.7, so also check for it
elif self.user_config_file.with_name("config.json").exists() is True:
self.user_config = self.load_user_config_json(
self.user_config_file.with_name("config.json")
)
logging.info("Old config.json found, read, and converted to config.toml")
self.extend_user_config(self.user_config, self.app_config)

# If no user config file exists, make one and save it
else:
# Create config containing only options and any other things that should be made
# obvious to the user that they can be configured
self.user_config = {
"options": copy(self.app_config["default_options"]),
"paths": {"Linux": "overwrite with default mount point"},
}
self.user_config_file.parent.mkdir(parents=True, exist_ok=True)
with open(self.user_config_file, "wb") as f:
tomli_w.dump(self.user_config, f)
self.user_config = {"options": {}}
self.extend_user_config(self.user_config, self.app_config)
logging.info("New default user config created with default options")
self.user_config_file.parent.mkdir(parents=True, exist_ok=True)
self.save()

# Overwrite any app config settings that have been specified in the user config
# Only works properly for a couple of nesting levels
for table in self.user_config.keys():
# Expose some parts of user and app configs at top level
self.options = self.user_config["options"]
self.paths = self.app_config["paths"]
self.groups = self.app_config["groups"]
self.specs = self.app_config["spectrometers"]


def load_config_toml(self, path: Path):
"""Load a config from a TOML file."""

with open(path, "rb") as f:
config = tomllib.load(f)
return config


def load_user_config_json(self, path: Path):
"""Load a user's config from a JSON file and replace it with a TOML."""

with open(path, encoding="utf-8") as f:
old_options = json.load(f)
# Add new options
config = {
"options": old_options,
"paths": {"Linux": "overwrite with default mount point"},
}
with open(self.user_config_file, "wb") as f:
tomli_w.dump(config, f)
# Remove old json to avoid confusion
path.unlink()
return config


def extend_user_config(self, user_config, app_config):
"""Make sure the user's config contains everything it needs to by default."""

# First just anything in the default options table
for option in app_config["default_options"]:
if option not in user_config["options"]:
user_config["options"][option] = app_config["default_options"][option]
# Then anything from other tables that needs to be present i.e. anything for
# which it should be made obvious to the user that it can be configured
if "paths" in user_config:
if "linux" in user_config["paths"]:
pass
else:
user_config["paths"]["linux"] = "overwrite with default mount point"


def update_app_config(self, config: dict):
"""Overwrite any app config settings that are specified in the given config."""
# NOTE: Only works properly for a couple of nesting levels
for table in config:
if table in self.app_config:
for k, v in self.user_config[table].items():
for k, v in config[table].items():
logging.info(
f"Updating default app_config option `[{table}] {k} = {repr(self.app_config[table][k])}` with value {repr(v)} from user's config.toml"
f"Updating default app config option `[{table}] {k} = {repr(self.app_config[table][k])}` with value {repr(v)} from provided config.toml"
)
# Make sure tables within tables are only updated, not overwritten
if isinstance(v, dict):
self.app_config[table][k].update(v)
else:
self.app_config[table][k] = v

# Expose some parts of user and app configs at top level
self.options = self.user_config["options"]
self.paths = self.app_config["paths"]
self.groups = self.app_config["groups"]
self.specs = self.app_config["spectrometers"]

def save(self):
# Save user config to file
with open(self.user_config_file, "wb") as f:
def save(self, path: Path | None = None):
"""Save user config to file.
If no path is provided, it defaults to the current value of `user_config_file`.
"""
if path is None:
path = self.user_config_file
with open(path, "wb") as f:
tomli_w.dump(self.user_config, f)
logging.info(
f"The following user options were saved to {self.user_config_file}:"
f"The following user options were saved to {path}:"
)
logging.info(self.user_config)
6 changes: 6 additions & 0 deletions mora_the_explorer/explorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ def connect_signals(self):
self.opts.nmrcheck_style_checkbox.stateChanged.connect(
self.main_window.nmrcheck_style_switched
)
self.opts.inc_path_checkbox.stateChanged.connect(
self.main_window.inc_path_changed
)
self.opts.inc_path_box.currentTextChanged.connect(
self.main_window.inc_path_changed
)
self.opts.spec_buttons.buttonClicked.connect(self.main_window.spec_changed)
self.opts.repeat_check_checkbox.stateChanged.connect(
self.main_window.repeat_switched
Expand Down
46 changes: 19 additions & 27 deletions mora_the_explorer/ui/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,22 +155,27 @@ def adapt_to_group(self, group=None):
self.opts.other_box.show()
else:
self.opts.other_box.hide()
# If nmr group has been selected, disable the naming option checkboxes
# as they will be treated as selected anyway
# If nmr group has been selected, disable the initials/solvent naming option
# checkboxes as they will be treated as selected anyway, and show the options
# for prepending/appending the path
if group == "nmr":
self.opts.inc_init_checkbox.setEnabled(False)
self.opts.nmrcheck_style_checkbox.setEnabled(False)
self.opts.inc_solv_checkbox.setEnabled(False)
self.opts.nmrcheck_style_checkbox.hide()
self.opts.inc_path_checkbox.show()
self.opts.inc_path_box.show()
else:
# Only enable initials checkbox if nmrcheck_style option is not selected,
# disable otherwise
self.opts.inc_init_checkbox.setEnabled(
not self.config.options["nmrcheck_style"]
)
self.opts.nmrcheck_style_checkbox.setEnabled(True)
if group == "nmr" or self.config.options["spec"] == "hf":
self.opts.inc_solv_checkbox.setEnabled(False)
else:
self.opts.inc_solv_checkbox.setEnabled(True)
self.opts.inc_solv_checkbox.setEnabled(
self.config.specs[self.config.options["spec"]]["allow_solvent"]
)
self.opts.nmrcheck_style_checkbox.show()
self.opts.inc_path_checkbox.hide()
self.opts.inc_path_box.hide()
self.refresh_visible_specs()

def dest_path_changed(self, new_path):
Expand All @@ -193,6 +198,12 @@ def inc_solv_switched(self):
self.config.options["inc_solv"] = self.opts.inc_solv_checkbox.isChecked()
self.opts.save_button.setEnabled(True)

def inc_path_changed(self):
if self.opts.inc_path_checkbox.isChecked():
self.config.options["inc_path"] = self.opts.inc_path_box.currentText()
else:
self.config.options["inc_path"] = False

def nmrcheck_style_switched(self):
self.config.options["nmrcheck_style"] = (
self.opts.nmrcheck_style_checkbox.isChecked()
Expand Down Expand Up @@ -231,25 +242,6 @@ def adapt_to_spec(self, spec: str):
else:
self.opts.only_button.show()
self.opts.since_button.show()
#if self.config.options["spec"] == "hf":
# # Including the solvent in the title is not supported for high-field measurements so disable option
# self.opts.inc_solv_checkbox.setEnabled(False)
# self.opts.repeat_check_checkbox.setEnabled(False)
# self.opts.date_selector.setDisplayFormat("yyyy")
# self.opts.only_button.hide()
# self.opts.since_button.hide()
# self.opts.today_button.setEnabled(False)
#else:
# if (
# self.config.options["group"] != "nmr"
# and self.config.options["nmrcheck_style"] is False
# ):
# self.opts.inc_solv_checkbox.setEnabled(True)
# self.opts.repeat_check_checkbox.setEnabled(True)
# self.opts.date_selector.setDisplayFormat("dd MMM yyyy")
# self.opts.only_button.show()
# self.opts.since_button.show()
# self.opts.today_button.setEnabled(True)

def repeat_switched(self):
self.config.options["repeat_switch"] = (
Expand Down
Loading

0 comments on commit 4c1fdae

Please sign in to comment.