diff --git a/src/xfel_calibrate/calibrate.py b/src/xfel_calibrate/calibrate.py index b44252144dbc8d59654534c226d153a226668959..8a36c93c4547f8d15ed2794bcd1ad381a694053f 100755 --- a/src/xfel_calibrate/calibrate.py +++ b/src/xfel_calibrate/calibrate.py @@ -1,16 +1,12 @@ #!/usr/bin/env python -import argparse -import inspect import json import locale import math import os -import pprint import re import shutil import stat -import string import sys import textwrap import warnings @@ -29,6 +25,15 @@ import cal_tools.tools from .finalize import tex_escape from .notebooks import notebooks +from .nb_args import ( + consolize_name, + deconsolize_args, + extend_params, + first_markdown_cell, + get_notebook_function, + make_extended_parser, + set_figure_format, +) from .settings import ( default_report_path, finalize_time_limit, @@ -47,143 +52,6 @@ from .settings import ( PKG_DIR = os.path.dirname(os.path.abspath(__file__)) -# Add a class combining raw description formatting with -# Metavariable default outputs -class RawTypeFormatter(argparse.RawDescriptionHelpFormatter, - argparse.MetavarTypeHelpFormatter, - argparse.ArgumentDefaultsHelpFormatter): - pass - - -# The argument parser for calibrate.py, will be extended depending -# on the options given. - -def make_initial_parser(**kwargs): - parser = argparse.ArgumentParser( - description="Main entry point for offline calibration", - formatter_class=RawTypeFormatter, - **kwargs - ) - - parser.add_argument('detector', metavar='DETECTOR', type=str, - help='The detector to calibrate: ' + ", ".join(notebooks)) - - parser.add_argument('type', metavar='TYPE', type=str, - help='Type of calibration.') - - parser.add_argument('--no-cluster-job', - action="store_true", - default=False, - help="Do not run as a cluster job") - - parser.add_argument('--prepare-only', action="store_true", - help="Prepare notebooks but don't run them") - - parser.add_argument('--report-to', type=str, - help='Filename (and optionally path) for output' - ' report') - - parser.add_argument('--not-reproducible', action='store_true', - help='Disable checks to allow the processing result ' - 'to not be reproducible based on its metadata.') - - parser.add_argument('--skip-report', action='store_true', - help='Skip report generation in finalize step.') - - parser.add_argument('--skip-env-freeze', action='store_true', - help='Skip recording the Python environment for ' - 'reproducibility purposes, requires ' - '--not-reproducible to run.') - - parser.add_argument('--concurrency-par', type=str, - help='Name of concurrency parameter.' - 'If not given, it is taken from configuration.') - - parser.add_argument('--constants-from', type=str, help=( - "Path to a calibration-metadata.yml file. If given, " - "retrieved-constants will be copied to use for a new correction." - )) - - parser.add_argument('--priority', type=int, default=2, - help="Priority of batch jobs. If priority<=1, reserved" - " nodes become available.") - - parser.add_argument('--vector-figs', action="store_true", default=False, - help="Use vector graphics for figures in the report.") - - parser.add_argument('--slurm-mem', type=int, default=500, - help="Requested node RAM in GB") - - parser.add_argument('--slurm-name', type=str, default='xfel_calibrate', - help='Name of slurm job') - - parser.add_argument('--slurm-scheduling', type=int, default=0, - help='Change scheduling priority for a slurm job ' - '+- 2147483645 (negative value increases ' - 'priority)') - - parser.add_argument('--request-time', type=str, default='Now', - help='Time of request to process notebook. Iso format') - - parser.add_argument_group('required arguments') - - parser.add_argument('--slurm-partition', type=str, default="", - help="Submit jobs in this Slurm partition") - - parser.add_argument('--reservation', type=str, default="", - help="Submit jobs in this Slurm reservation, " - "overriding --slurm-partition if both are set") - - return parser - - -# Helper functions for parser extensions - -def make_intelli_list(ltype): - """ Parses a list from range and comma expressions. - - An expression of the form "1-5,6" will be parsed into the following - list: [1,2,3,4,6] - - """ - class IntelliListAction(argparse.Action): - element_type = ltype - - def __init__(self, *args, **kwargs): - super(IntelliListAction, self).__init__(*args, **kwargs) - - def __call__(self, parser, namespace, values, option_string=None): - parsed_values = [] - values = ",".join(values) - if isinstance(values, str): - for rcomp in values.split(","): - if "-" in rcomp: - start, end = rcomp.split("-") - parsed_values += list(range(int(start), int(end))) - else: - parsed_values += [int(rcomp)] - elif isinstance(values, (list, tuple)): - parsed_values = values - else: - parsed_values = [values, ] - - parsed_values = [self.element_type(p) for p in parsed_values] - print("Parsed input {} to {}".format(values, parsed_values)) - setattr(namespace, self.dest, parsed_values) - - return IntelliListAction - - -def consolize_name(name): - """ Names of console parameters don't have underscores """ - return name.replace("_", "-") - - -def deconsolize_args(args): - """ Variable names have underscores """ - return {k.replace("-", "_"): v for k, v in args.items()} - - def extract_title_author(nb): """ Tries to extract title, author from markdown. @@ -226,86 +94,6 @@ def get_python_version(python_exe): return check_output([python_exe, '--version']).decode('utf-8').split()[1] -def get_cell_n(nb, cell_type, cell_n): - """ - Return notebook cell with given number and given type - - :param nb: jupyter notebook - :param cell_type: cell type, 'code' or 'markdown' - :param cell_n: cell number (count from 0) - :return: notebook cell - """ - counter = 0 - for cell in nb.cells: - if cell.cell_type == cell_type: - if counter == cell_n: - return cell - counter += 1 - - -def first_code_cell(nb): - """ Return the first code cell of a notebook """ - return get_cell_n(nb, 'code', 0) - - -def first_markdown_cell(nb): - """ Return the first markdown cell of a notebook """ - return get_cell_n(nb, 'markdown', 0) - - -def make_epilog(nb, caltype=None): - """ Make an epilog from the notebook to add to parser help - """ - msg = "" - header_cell = first_markdown_cell(nb) - lines = header_cell.source.split("\n") - if caltype: - msg += "{:<15} {}".format(caltype, lines[0]) + "\n" - else: - msg += "{}".format(lines[0]) + "\n" - pp = pprint.PrettyPrinter(indent=(17 if caltype else 0)) - if len(lines[1:]): - plines = pp.pformat(lines[1:])[1:-1].split("\n") - for line in plines: - sline = line.replace("'", "", 1) - sline = sline.replace("', '", " " * (17 if caltype else 0), 1) - sline = sline[::-1].replace("'", "", 1)[::-1] - sline = sline.replace(" ,", " ") - if len(sline) > 1 and sline[0] == ",": - sline = sline[1:] - msg += sline + "\n" - msg += "\n" - return msg - - -def get_notebook_function(nb, fname): - flines = [] - def_found = False - indent = None - for cell in nb.cells: - if cell.cell_type == 'code': - lines = cell.source.split("\n") - for line in lines: - - if def_found: - lin = len(line) - len(line.lstrip()) - if indent is None: - if lin != 0: - indent = lin - flines.append(line) - elif lin >= indent: - flines.append(line) - else: - return "\n".join(flines) - - if re.search(r"def\s+{}\(.*\):\s*".format(fname), line) and not def_found: - # print("Found {} in line {}".format(fname, line)) - # set this to indent level - def_found = True - flines.append(line) - return None - - def balance_sequences(in_folder: str, run: int, sequences: List[int], sequences_per_node: int, karabo_da: Union[list, str], max_nodes: int = 8): @@ -367,188 +155,6 @@ def balance_sequences(in_folder: str, run: int, sequences: List[int], if l.size > 0] -def make_extended_parser() -> argparse.ArgumentParser: - """Create an ArgumentParser using information from the notebooks""" - - # extend the parser according to user input - # the first case is if a detector was given, but no calibration type - if len(sys.argv) == 3 and "-h" in sys.argv[2]: - detector = sys.argv[1].upper() - try: - det_notebooks = notebooks[detector] - except KeyError: - # TODO: This should really go to stderr not stdout - print("Not one of the known detectors: {}".format(notebooks.keys())) - sys.exit(1) - - msg = "Options for detector {}\n".format(detector) - msg += "*" * len(msg) + "\n\n" - - # basically, this creates help in the form of - # - # TYPE some description that is - # indented for this type. - # - # The information is extracted from the first markdown cell of - # the notebook. - for caltype, notebook in det_notebooks.items(): - if notebook.get("notebook") is None: - if notebook.get("user", {}).get("notebook") is None: - raise KeyError( - f"`{detector}` does not have a notebook path, for " - "notebooks that are stored in pycalibration set the " - "`notebook` key to a relative path or set the " - "`['user']['notebook']` key to an absolute path/path " - "pattern. Notebook configuration dictionary contains " - f"only: `{notebook}`" - "" - ) - # Everything should be indented by 17 spaces - msg += caltype.ljust(17) + "User defined notebook, arguments may vary\n" - msg += " "*17 + "User notebook expected to be at path:\n" - msg += " "*17 + notebook["user"]["notebook"] + "\n" - else: - nbpath = os.path.join(PKG_DIR, notebook["notebook"]) - nb = nbformat.read(nbpath, as_version=4) - msg += make_epilog(nb, caltype=caltype) - - return make_initial_parser(epilog=msg) - elif len(sys.argv) <= 3: - return make_initial_parser() - - # A detector and type was given. We derive the arguments - # from the corresponding notebook - args, _ = make_initial_parser(add_help=False).parse_known_args() - try: - nb_info = notebooks[args.detector.upper()][args.type.upper()] - except KeyError: - print("Not one of the known calibrations or detectors") - sys.exit(1) - - if nb_info["notebook"]: - notebook = os.path.join(PKG_DIR, nb_info["notebook"]) - else: - # If `"notebook"` entry is None, then set it to the user provided - # notebook TODO: This is a very hacky workaround, better implementation - # is not really possible with the current state of this module - user_notebook_path = nb_info["user"]["notebook"] - # Pull out the variables in the templated path string, and get values - # from command line args (e.g. --proposal 1234 -> {proposal}) - user_notebook_variables = [ - name for (_, name, _, _) in string.Formatter().parse(user_notebook_path) - if name is not None - ] - - user_notebook_parser = argparse.ArgumentParser(add_help=False) - for var in user_notebook_variables: - user_notebook_parser.add_argument(f"--{var}") - - user_notebook_args, _ = user_notebook_parser.parse_known_args() - - nb_info["notebook"] = user_notebook_path.format(**vars(user_notebook_args)) - notebook = nb_info["notebook"] - - cvar = nb_info.get("concurrency", {}).get("parameter", None) - - nb = nbformat.read(notebook, as_version=4) - - # extend parameters if needed - ext_func = nb_info.get("extend parms", None) - if ext_func is not None: - extend_params(nb, ext_func) - - # No extend parms function - add statically defined parameters from the - # first code cell - parser = make_initial_parser() - add_args_from_nb(nb, parser, cvar=cvar) - return parser - -def add_args_from_nb(nb, parser, cvar=None, no_required=False): - """Add argparse arguments for parameters in the first cell of a notebook. - - Uses nbparameterise to extract the parameter information. Each foo_bar - parameter gets a --foo-bar command line option. - Boolean parameters get a pair of flags like --abc and --no-abc. - - :param nb: NotebookNode object representing a loaded .ipynb file - :param parser: argparse.ArgumentParser instance - :param str cvar: Name of the concurrency parameter. - :param bool no_required: If True, none of the added options are required. - """ - parser.description = make_epilog(nb) - parms = extract_parameters(nb, lang='python') - - for p in parms: - helpstr = ("Default: %(default)s" if not p.comment - else "{}. Default: %(default)s".format(p.comment.replace("#", " ").strip())) - required = (p.comment is not None - and "required" in p.comment - and not no_required - and p.name != cvar) - - # This may be not a public API - # May require reprogramming in case of argparse updates - pars_group = parser._action_groups[2 if required else 1] - - default = p.value if (not required) else None - - if issubclass(p.type, list) or p.name == cvar: - ltype = type(p.value[0]) if issubclass(p.type, list) else p.type - range_allowed = "RANGE ALLOWED" in p.comment.upper() if p.comment else False - pars_group.add_argument(f"--{consolize_name(p.name)}", - nargs='+', - type=ltype if not range_allowed else str, - default=default, - help=helpstr, - required=required, - action=make_intelli_list(ltype) if range_allowed else None) - elif issubclass(p.type, bool): - # For a boolean, make --XYZ and --no-XYZ options. - alt_group = pars_group.add_mutually_exclusive_group(required=required) - alt_group.add_argument(f"--{consolize_name(p.name)}", - action="store_true", - default=default, - help=helpstr, - dest=p.name) - alt_group.add_argument(f"--no-{consolize_name(p.name)}", - action="store_false", - default=default, - help=f"Opposite of --{consolize_name(p.name)}", - dest=p.name) - else: - pars_group.add_argument(f"--{consolize_name(p.name)}", - type=p.type, - default=default, - help=helpstr, - required=required) - -def extend_params(nb, extend_func_name): - """Add parameters in the first code cell by calling a function in the notebook - """ - func = get_notebook_function(nb, extend_func_name) - - if func is None: - warnings.warn( - f"Didn't find concurrency function {extend_func_name} in notebook", - RuntimeWarning - ) - return - - # Make a temporary parser that won't exit if it sees -h or --help - pre_parser = make_initial_parser(add_help=False) - add_args_from_nb(nb, pre_parser, no_required=True) - known, _ = pre_parser.parse_known_args() - args = deconsolize_args(vars(known)) - - df = {} - exec(func, df) - f = df[extend_func_name] - sig = inspect.signature(f) - - extension = f(*[args[p] for p in sig.parameters]) - fcc = first_code_cell(nb) - fcc["source"] += "\n" + extension - def get_par_attr(parms, key, attr, default=None): """ @@ -581,19 +187,6 @@ def flatten_list(l): return '' -def set_figure_format(nb, enable_vector_format): - """Set svg format in inline backend for figures - - If parameter enable_vector_format is set to True, svg format will - be used for figures in the notebook rendering. Subsequently vector - graphics figures will be used for report. - """ - - if enable_vector_format: - cell = get_cell_n(nb, 'code', 1) - cell.source += "\n%config InlineBackend.figure_formats = ['svg']\n" - - def create_finalize_script(fmt_args, temp_path, job_list) -> str: """ Create a finalize script to produce output report diff --git a/src/xfel_calibrate/nb_args.py b/src/xfel_calibrate/nb_args.py new file mode 100644 index 0000000000000000000000000000000000000000..c32eaa152d6b9d7435d6a6d85c1fb21daf811f70 --- /dev/null +++ b/src/xfel_calibrate/nb_args.py @@ -0,0 +1,430 @@ +"""Manipulating notebooks & translating parameters to command-line options +""" +import argparse +import inspect +import os.path +import pprint +import re +import string +import sys +import warnings + +import nbformat +from nbparameterise import extract_parameters + +from .notebooks import notebooks + +PKG_DIR = os.path.dirname(os.path.abspath(__file__)) + +# Add a class combining raw description formatting with +# Metavariable default outputs +class RawTypeFormatter(argparse.RawDescriptionHelpFormatter, + argparse.MetavarTypeHelpFormatter, + argparse.ArgumentDefaultsHelpFormatter): + pass + + +# The argument parser for calibrate.py, will be extended depending +# on the options given. + +def make_initial_parser(**kwargs): + parser = argparse.ArgumentParser( + description="Main entry point for offline calibration", + formatter_class=RawTypeFormatter, + **kwargs + ) + + parser.add_argument('detector', metavar='DETECTOR', type=str, + help='The detector to calibrate: ' + ", ".join(notebooks)) + + parser.add_argument('type', metavar='TYPE', type=str, + help='Type of calibration.') + + parser.add_argument('--no-cluster-job', + action="store_true", + default=False, + help="Do not run as a cluster job") + + parser.add_argument('--prepare-only', action="store_true", + help="Prepare notebooks but don't run them") + + parser.add_argument('--report-to', type=str, + help='Filename (and optionally path) for output' + ' report') + + parser.add_argument('--not-reproducible', action='store_true', + help='Disable checks to allow the processing result ' + 'to not be reproducible based on its metadata.') + + parser.add_argument('--skip-report', action='store_true', + help='Skip report generation in finalize step.') + + parser.add_argument('--skip-env-freeze', action='store_true', + help='Skip recording the Python environment for ' + 'reproducibility purposes, requires ' + '--not-reproducible to run.') + + parser.add_argument('--concurrency-par', type=str, + help='Name of concurrency parameter.' + 'If not given, it is taken from configuration.') + + parser.add_argument('--constants-from', type=str, help=( + "Path to a calibration-metadata.yml file. If given, " + "retrieved-constants will be copied to use for a new correction." + )) + + parser.add_argument('--priority', type=int, default=2, + help="Priority of batch jobs. If priority<=1, reserved" + " nodes become available.") + + parser.add_argument('--vector-figs', action="store_true", default=False, + help="Use vector graphics for figures in the report.") + + parser.add_argument('--slurm-mem', type=int, default=500, + help="Requested node RAM in GB") + + parser.add_argument('--slurm-name', type=str, default='xfel_calibrate', + help='Name of slurm job') + + parser.add_argument('--slurm-scheduling', type=int, default=0, + help='Change scheduling priority for a slurm job ' + '+- 2147483645 (negative value increases ' + 'priority)') + + parser.add_argument('--request-time', type=str, default='Now', + help='Time of request to process notebook. Iso format') + + parser.add_argument_group('required arguments') + + parser.add_argument('--slurm-partition', type=str, default="", + help="Submit jobs in this Slurm partition") + + parser.add_argument('--reservation', type=str, default="", + help="Submit jobs in this Slurm reservation, " + "overriding --slurm-partition if both are set") + + return parser + + +# Helper functions for parser extensions + +def make_intelli_list(ltype): + """ Parses a list from range and comma expressions. + + An expression of the form "1-5,6" will be parsed into the following + list: [1,2,3,4,6] + + """ + class IntelliListAction(argparse.Action): + element_type = ltype + + def __init__(self, *args, **kwargs): + super(IntelliListAction, self).__init__(*args, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + parsed_values = [] + values = ",".join(values) + if isinstance(values, str): + for rcomp in values.split(","): + if "-" in rcomp: + start, end = rcomp.split("-") + parsed_values += list(range(int(start), int(end))) + else: + parsed_values += [int(rcomp)] + elif isinstance(values, (list, tuple)): + parsed_values = values + else: + parsed_values = [values, ] + + parsed_values = [self.element_type(p) for p in parsed_values] + print("Parsed input {} to {}".format(values, parsed_values)) + setattr(namespace, self.dest, parsed_values) + + return IntelliListAction + + +def consolize_name(name): + """ Names of console parameters don't have underscores """ + return name.replace("_", "-") + + +def add_args_from_nb(nb, parser, cvar=None, no_required=False): + """Add argparse arguments for parameters in the first cell of a notebook. + + Uses nbparameterise to extract the parameter information. Each foo_bar + parameter gets a --foo-bar command line option. + Boolean parameters get a pair of flags like --abc and --no-abc. + + :param nb: NotebookNode object representing a loaded .ipynb file + :param parser: argparse.ArgumentParser instance + :param str cvar: Name of the concurrency parameter. + :param bool no_required: If True, none of the added options are required. + """ + parser.description = make_epilog(nb) + parms = extract_parameters(nb, lang='python') + + for p in parms: + helpstr = ("Default: %(default)s" if not p.comment + else "{}. Default: %(default)s".format(p.comment.replace("#", " ").strip())) + required = (p.comment is not None + and "required" in p.comment + and not no_required + and p.name != cvar) + + # This may be not a public API + # May require reprogramming in case of argparse updates + pars_group = parser._action_groups[2 if required else 1] + + default = p.value if (not required) else None + + if issubclass(p.type, list) or p.name == cvar: + ltype = type(p.value[0]) if issubclass(p.type, list) else p.type + range_allowed = "RANGE ALLOWED" in p.comment.upper() if p.comment else False + pars_group.add_argument(f"--{consolize_name(p.name)}", + nargs='+', + type=ltype if not range_allowed else str, + default=default, + help=helpstr, + required=required, + action=make_intelli_list(ltype) if range_allowed else None) + elif issubclass(p.type, bool): + # For a boolean, make --XYZ and --no-XYZ options. + alt_group = pars_group.add_mutually_exclusive_group(required=required) + alt_group.add_argument(f"--{consolize_name(p.name)}", + action="store_true", + default=default, + help=helpstr, + dest=p.name) + alt_group.add_argument(f"--no-{consolize_name(p.name)}", + action="store_false", + default=default, + help=f"Opposite of --{consolize_name(p.name)}", + dest=p.name) + else: + pars_group.add_argument(f"--{consolize_name(p.name)}", + type=p.type, + default=default, + help=helpstr, + required=required) + +def get_cell_n(nb, cell_type, cell_n): + """ + Return notebook cell with given number and given type + + :param nb: jupyter notebook + :param cell_type: cell type, 'code' or 'markdown' + :param cell_n: cell number (count from 0) + :return: notebook cell + """ + counter = 0 + for cell in nb.cells: + if cell.cell_type == cell_type: + if counter == cell_n: + return cell + counter += 1 + + +def first_code_cell(nb): + """ Return the first code cell of a notebook """ + return get_cell_n(nb, 'code', 0) + + +def first_markdown_cell(nb): + """ Return the first markdown cell of a notebook """ + return get_cell_n(nb, 'markdown', 0) + + +def set_figure_format(nb, enable_vector_format): + """Set svg format in inline backend for figures + + If parameter enable_vector_format is set to True, svg format will + be used for figures in the notebook rendering. Subsequently vector + graphics figures will be used for report. + """ + + if enable_vector_format: + cell = get_cell_n(nb, 'code', 1) + cell.source += "\n%config InlineBackend.figure_formats = ['svg']\n" + + +def get_notebook_function(nb, fname): + flines = [] + def_found = False + indent = None + for cell in nb.cells: + if cell.cell_type == 'code': + lines = cell.source.split("\n") + for line in lines: + + if def_found: + lin = len(line) - len(line.lstrip()) + if indent is None: + if lin != 0: + indent = lin + flines.append(line) + elif lin >= indent: + flines.append(line) + else: + return "\n".join(flines) + + if re.search(r"def\s+{}\(.*\):\s*".format(fname), line) and not def_found: + # print("Found {} in line {}".format(fname, line)) + # set this to indent level + def_found = True + flines.append(line) + return None + + +def make_epilog(nb, caltype=None): + """ Make an epilog from the notebook to add to parser help + """ + msg = "" + header_cell = first_markdown_cell(nb) + lines = header_cell.source.split("\n") + if caltype: + msg += "{:<15} {}".format(caltype, lines[0]) + "\n" + else: + msg += "{}".format(lines[0]) + "\n" + pp = pprint.PrettyPrinter(indent=(17 if caltype else 0)) + if len(lines[1:]): + plines = pp.pformat(lines[1:])[1:-1].split("\n") + for line in plines: + sline = line.replace("'", "", 1) + sline = sline.replace("', '", " " * (17 if caltype else 0), 1) + sline = sline[::-1].replace("'", "", 1)[::-1] + sline = sline.replace(" ,", " ") + if len(sline) > 1 and sline[0] == ",": + sline = sline[1:] + msg += sline + "\n" + msg += "\n" + return msg + + +def deconsolize_args(args): + """ Variable names have underscores """ + return {k.replace("-", "_"): v for k, v in args.items()} + + +def extend_params(nb, extend_func_name): + """Add parameters in the first code cell by calling a function in the notebook + """ + func = get_notebook_function(nb, extend_func_name) + + if func is None: + warnings.warn( + f"Didn't find concurrency function {extend_func_name} in notebook", + RuntimeWarning + ) + return + + # Make a temporary parser that won't exit if it sees -h or --help + pre_parser = make_initial_parser(add_help=False) + add_args_from_nb(nb, pre_parser, no_required=True) + known, _ = pre_parser.parse_known_args() + args = deconsolize_args(vars(known)) + + df = {} + exec(func, df) + f = df[extend_func_name] + sig = inspect.signature(f) + + extension = f(*[args[p] for p in sig.parameters]) + fcc = first_code_cell(nb) + fcc["source"] += "\n" + extension + + +def make_extended_parser() -> argparse.ArgumentParser: + """Create an ArgumentParser using information from the notebooks""" + + # extend the parser according to user input + # the first case is if a detector was given, but no calibration type + if len(sys.argv) == 3 and "-h" in sys.argv[2]: + detector = sys.argv[1].upper() + try: + det_notebooks = notebooks[detector] + except KeyError: + # TODO: This should really go to stderr not stdout + print("Not one of the known detectors: {}".format(notebooks.keys())) + sys.exit(1) + + msg = "Options for detector {}\n".format(detector) + msg += "*" * len(msg) + "\n\n" + + # basically, this creates help in the form of + # + # TYPE some description that is + # indented for this type. + # + # The information is extracted from the first markdown cell of + # the notebook. + for caltype, notebook in det_notebooks.items(): + if notebook.get("notebook") is None: + if notebook.get("user", {}).get("notebook") is None: + raise KeyError( + f"`{detector}` does not have a notebook path, for " + "notebooks that are stored in pycalibration set the " + "`notebook` key to a relative path or set the " + "`['user']['notebook']` key to an absolute path/path " + "pattern. Notebook configuration dictionary contains " + f"only: `{notebook}`" + "" + ) + # Everything should be indented by 17 spaces + msg += caltype.ljust(17) + "User defined notebook, arguments may vary\n" + msg += " "*17 + "User notebook expected to be at path:\n" + msg += " "*17 + notebook["user"]["notebook"] + "\n" + else: + nbpath = os.path.join(PKG_DIR, notebook["notebook"]) + nb = nbformat.read(nbpath, as_version=4) + msg += make_epilog(nb, caltype=caltype) + + return make_initial_parser(epilog=msg) + elif len(sys.argv) <= 3: + return make_initial_parser() + + # A detector and type was given. We derive the arguments + # from the corresponding notebook + args, _ = make_initial_parser(add_help=False).parse_known_args() + try: + nb_info = notebooks[args.detector.upper()][args.type.upper()] + except KeyError: + print("Not one of the known calibrations or detectors") + sys.exit(1) + + if nb_info["notebook"]: + notebook = os.path.join(PKG_DIR, nb_info["notebook"]) + else: + # If `"notebook"` entry is None, then set it to the user provided + # notebook TODO: This is a very hacky workaround, better implementation + # is not really possible with the current state of this module + user_notebook_path = nb_info["user"]["notebook"] + # Pull out the variables in the templated path string, and get values + # from command line args (e.g. --proposal 1234 -> {proposal}) + user_notebook_variables = [ + name for (_, name, _, _) in string.Formatter().parse(user_notebook_path) + if name is not None + ] + + user_notebook_parser = argparse.ArgumentParser(add_help=False) + for var in user_notebook_variables: + user_notebook_parser.add_argument(f"--{var}") + + user_notebook_args, _ = user_notebook_parser.parse_known_args() + + nb_info["notebook"] = user_notebook_path.format(**vars(user_notebook_args)) + notebook = nb_info["notebook"] + + cvar = nb_info.get("concurrency", {}).get("parameter", None) + + nb = nbformat.read(notebook, as_version=4) + + # extend parameters if needed + ext_func = nb_info.get("extend parms", None) + if ext_func is not None: + extend_params(nb, ext_func) + + # No extend parms function - add statically defined parameters from the + # first code cell + parser = make_initial_parser() + add_args_from_nb(nb, parser, cvar=cvar) + return parser