Skip to content

Commit

Permalink
Merge pull request #285 from jpgill86/global-config-file
Browse files Browse the repository at this point in the history
Allow users to change CLI argument defaults in a global config file
  • Loading branch information
jpgill86 authored Dec 27, 2020
2 parents 44744bb + f82c712 commit 6f3c751
Show file tree
Hide file tree
Showing 10 changed files with 434 additions and 77 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ include README.rst
include requirements*.txt

# package data included in source distribution and installation
include neurotic/global_config_template.txt
include neurotic/example/example-notebook.ipynb
include neurotic/example/metadata.yml
include neurotic/tests/metadata-for-tests.yml
27 changes: 20 additions & 7 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,12 @@ The command line interface accepts other arguments too:

.. code-block::
usage: neurotic [-h] [-V] [--debug] [--no-lazy] [--thick-traces]
[--show-datetime] [--ui-scale {tiny,small,medium,large,huge}]
usage: neurotic [-h] [-V] [--debug | --no-debug] [--lazy | --no-lazy]
[--thick-traces | --no-thick-traces]
[--show-datetime | --no-show-datetime]
[--ui-scale {tiny,small,medium,large,huge}]
[--theme {light,dark,original,printer-friendly}]
[--launch-example-notebook]
[--use-factory-defaults] [--launch-example-notebook]
[file] [dataset]
neurotic lets you curate, visualize, annotate, and share your behavioral ephys
Expand All @@ -274,23 +276,34 @@ The command line interface accepts other arguments too:
-h, --help show this help message and exit
-V, --version show program's version number and exit
--debug enable detailed log messages for debugging
--no-lazy do not use fast loading (default: use fast loading)
--no-debug disable detailed log messages for debugging (default)
--lazy enable fast loading (default)
--no-lazy disable fast loading
--thick-traces enable support for traces with thick lines, which has
a performance cost (default: disable thick line
support)
a performance cost
--no-thick-traces disable support for traces with thick lines (default)
--show-datetime display the real-world date and time, which may be
inaccurate depending on file type and acquisition
software (default: do not display)
software
--no-show-datetime do not display the real-world date and time (default)
--ui-scale {tiny,small,medium,large,huge}
the scale of user interface elements, such as text
(default: medium)
--theme {light,dark,original,printer-friendly}
a color theme for the GUI (default: light)
--use-factory-defaults
start with "factory default" settings, ignoring other
args and your global config file
alternative modes:
--launch-example-notebook
launch Jupyter with an example notebook instead of
starting the standalone app (other args will be
ignored)
Defaults for arguments and options can be changed in a global config file,
.neurotic\neurotic-config.txt, located in your home directory.
Citing *neurotic*
-----------------

Expand Down
21 changes: 21 additions & 0 deletions docs/globalconfig.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.. _global-config:

Changing Default Behavior
=========================

Default parameters used by the command line interface for launching the app,
such as which metadata file to open initially, can be configured using global
configuration settings located in ``.neurotic/neurotic-config.txt`` in your
home directory:

- Windows: ``C:\Users\<username>\.neurotic\neurotic-config.txt``
- macOS: ``/Users/<username>/.neurotic/neurotic-config.txt``
- Linux: ``/home/<username>/.neurotic/neurotic-config.txt``

The file can be opened easily using the "View global config file" menu action.

If this file does not exist when *neurotic* is launched, the following template
is created for you:

.. literalinclude:: ../neurotic/global_config_template.txt
:language: toml
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ at the same datasets!
citations
metadata
examples
globalconfig
api
releasenotes

Expand Down
95 changes: 87 additions & 8 deletions neurotic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,25 @@
Curate, visualize, annotate, and share your behavioral ephys data using Python
"""

from .version import version as __version__
from .version import git_revision as __git_revision__


import os
import sys
import shutil
import copy
import pkg_resources
import collections.abc
import logging
import logging.handlers
import toml

from .version import version as __version__
from .version import git_revision as __git_revision__


# set the user's directory for global settings file, logs, and more
neurotic_dir = os.path.join(os.path.expanduser('~'), '.neurotic')
if not os.path.exists(neurotic_dir):
os.mkdir(neurotic_dir)


class FileLoggingFormatter(logging.Formatter):
"""
Expand Down Expand Up @@ -42,10 +53,7 @@ def format(self, record):


# set the file path for logging
log_dir = os.path.join(os.path.expanduser('~'), '.neurotic')
if not os.path.exists(log_dir):
os.mkdir(log_dir)
log_file = os.path.join(log_dir, 'neurotic-log.txt')
log_file = os.path.join(neurotic_dir, 'neurotic-log.txt')

# set the default level for logging to INFO unless it was set to a custom level
# before importing the package
Expand All @@ -69,6 +77,77 @@ def format(self, record):
logger.addHandler(logger_streamhandler)


global_config = {
'defaults': {
# defaults used by the command line interface
'file': None,
'dataset': None,
'debug': False,
'lazy': True,
'thick_traces': False,
'show_datetime': False,
'ui_scale': 'medium',
'theme': 'light',
},
}

# keep a copy of the original config before it is modified
_global_config_factory_defaults = copy.deepcopy(global_config)

# the global config file is a text file in TOML format owned by the user that
# allows alternate defaults to be specified to replace those in global_config
global_config_file = os.path.join(neurotic_dir, 'neurotic-config.txt')

if not os.path.exists(global_config_file):
# copy a template global config file containing commented-out defaults
shutil.copy(
pkg_resources.resource_filename(
'neurotic', 'global_config_template.txt'),
global_config_file)

def update_dict(d, d_new):
"""
Recursively update the contents of a dictionary. Unlike dict.update(), this
function preserves items in inner dictionaries that are absent from d_new.
For example, if given
>>> d = {'x': 0, 'inner': {'a': 1, 'b': 2}}
>>> d_new = {'inner': {'c': 3}}
then using d.update(d_new) will entirely replace d['inner'] with
d_new['inner']:
>>> d.update(d_new)
>>> d == {'x': 0, 'inner': {'c': 3}}
In contrast, update_dict(d, d_new) will preserve items found in d['inner']
but not in d_new['inner']:
>>> update_dict(d, d_new)
>>> d == {'x': 0, 'inner': {'a': 1, 'b': 2, 'c': 3}}
"""
for k_new, v_new in d_new.items():
if isinstance(v_new, collections.abc.Mapping):
d[k_new] = update_dict(d.get(k_new, {}), v_new)
else:
d[k_new] = v_new
return d

def update_global_config_from_file(file=global_config_file):
"""
Update the global_config dictionary with data from the global config file,
using recursion to traverse nested dictionaries.
"""
with open(file, 'r') as f:
update_dict(global_config, toml.loads(f.read()))

try:
update_global_config_from_file()
except Exception as e:
logger.error(f'Ignoring global config file due to parsing error ({global_config_file}): {e}')


from .datasets import *
from .gui import *
from .scripts import *
25 changes: 25 additions & 0 deletions neurotic/global_config_template.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# --- neurotic global config --------------------------------------------------
# Use this file to configure the way neurotic behaves. This file uses the TOML
# format.

[defaults]
# When the app is launched, the following customizable defaults are used unless
# overridden by command line arguments. Example uses include:
# - Uncomment the "file" parameter and insert the path to your favorite
# metadata file so that it always opens automatically
# - Uncomment the "lazy" parameter and set it to false to always disable fast
# loading, ensuring that expensive procedures like spike detection and
# filtering are performed by default
# To open the example metadata file by default, either leave the "file"
# parameter commented or set it to "example". To initially select the first
# dataset in the file, either leave the "dataset" parameter commented or set it
# to "none".

# file = "example"
# dataset = "none"
# debug = false
# lazy = true
# thick_traces = false
# show_datetime = false
# ui_scale = "medium"
# theme = "light"
20 changes: 19 additions & 1 deletion neurotic/gui/standalone.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import neo
from ephyviewer import QT, QT_MODE

from .. import __version__, _elephant_tools, default_log_level, log_file
from .. import __version__, _elephant_tools, global_config_file, default_log_level, log_file
from ..datasets import MetadataSelector, load_dataset
from ..datasets.metadata import _selector_labels
from ..gui.config import EphyviewerConfigurator, available_themes, available_ui_scales
Expand Down Expand Up @@ -221,6 +221,11 @@ def create_menus(self):
do_toggle_show_datetime.setChecked(self.show_datetime)
do_toggle_show_datetime.triggered.connect(self.toggle_show_datetime)

options_menu.addSeparator()

do_view_global_config_file = options_menu.addAction('View global &config file')
do_view_global_config_file.triggered.connect(self.view_global_config_file)

appearance_menu = menu_bar.addMenu(self.tr('&Appearance'))

ui_scale_group = QT.QActionGroup(appearance_menu)
Expand Down Expand Up @@ -424,6 +429,19 @@ def on_load_dataset_finished(self):
self.metadata_selector.setEnabled(True)
self.stacked_layout.setCurrentIndex(0) # show metadata selector

def view_global_config_file(self):
"""
Open the global config file in an editor.
"""

try:
open_path_with_default_program(global_config_file)
except FileNotFoundError as e:
logger.error(f'The global config file was not found: {e}')
self.statusBar().showMessage('ERROR: The global config file could '
'not be found', msecs=5000)
return

def toggle_debug_logging(self, checked):
"""
Toggle log filtering level between its original level and debug mode
Expand Down
Loading

0 comments on commit 6f3c751

Please sign in to comment.