Source code for kalasiris.kalasiris

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Provides the ability to call ISIS functions."""

# Copyright 2019-2020, Ross A. Beyer (rbeyer@seti.org)
#
# Reuse is permitted under the terms of the license.
# The AUTHORS file and the LICENSE file are at the
# top level of this library.

# Thou shalt only import from the Python Standard Library.
import logging
import os
import subprocess
import sys
from pathlib import Path

# This file shall have *NO* non-Standard Library dependencies.

# kalasiris library version:
__version__ = "1.9.1"

# Set a logger:
logger = logging.getLogger(__name__)

# Set a logger:
logger = logging.getLogger(__name__)


# These definitions and the use of env= in the subprocess.run calls allow us to
# run ISIS in a very lean environment.  Of course, users can override with
# their complete environment by making kalasiris.environ = os.environ
# before any calls to ISIS programs.
environ = {
    "ISISROOT": os.environ["ISISROOT"],
    "PATH": str(Path(os.environ["ISISROOT"]) / "bin"),
    "HOME": os.path.expanduser("~"),
}
try:
    environ["ISISDATA"] = os.environ["ISISDATA"]
except KeyError:
    try:
        environ["ISIS3DATA"] = os.environ["ISIS3DATA"]
    except KeyError:
        raise KeyError("Neither ISISDATA nor ISIS3DATA are in os.environ.")

# If we don't also set $HOME, ISIS tries to make a local ./\$HOME dir
# Can't just use os.environ['HOME'] because not all platforms have
# that environment variable set (Windows uses something different).

# These are the names of the reserved parameters that can be given
# as arguments to any ISIS program, prefixed by a dash (-).
_res_param_no_vals = {"webhelp", "last", "gui", "nogui", "verbose"}
_res_param_maybe = {"help", "log", "info", "save"}

# The ISIS programs in this list do not follow the 'normal' argument
# patters for most ISIS programs, they just consume everything you
# give them, so we need to treat them differently.
_pass_through_programs = {"cneteditor", "qmos", "qnet", "qtie", "qview"}


[docs]def param_fmt(key: str, value: str) -> str: """Returns a "key=value" string from the inputs. This is the pattern that ISIS uses for arguments. If there are any trailing underbars (_) on the key, they will be stripped off. This supports the old pysis syntax to protect Python reserved words like from and min, while still allowing the user to provide 'natural' function calls like isis.stats(from_=cubefile). Additionally, it also supports passing what ISIS calls *reserved parameters* for any ISIS program, denoted with a prefix of a single dash, like ``-restore=file`` or ``-verbose`` via keys with two trailing underbars. So to call ``spiceinit from= some.cub -restore=file`` you would do this:: cubefile = 'some.cub' restore_file = 'some.par' isis.spiceinit(cubefile, restore__=restore_file) Likewise, to call ``getkey -help``, or ``getkey -help=GRPNAME`` do this:: isis.getkey('help__') isis.getkey(help__='GRPNAME') Of course, you'll probably want to do this:: help_text = isis.getkey(help__='').stdout """ # The logic for dealing with a single parameter, like # isis.getkey('help__') # is down in the _build_isis_fn() factory # method. if key.endswith("__"): return "-{}={}".format(key.rstrip("_"), value) else: return "{}={}".format(key.rstrip("_"), value)
def _run_isis_program( cmd: list, subprocess_kwargs: dict = None ) -> subprocess.CompletedProcess: """Wrapper for subprocess.run(). Also logs the elements of *cmd* to the logger at level INFO. """ if subprocess_kwargs is None: subprocess_kwargs = dict() # Set some reasonable defaults, if they aren't already set: subprocess_kwargs.setdefault("env", environ) subprocess_kwargs.setdefault("check", True) subprocess_kwargs.setdefault("stdout", subprocess.PIPE) subprocess_kwargs.setdefault("stderr", subprocess.PIPE) subprocess_kwargs.setdefault("universal_newlines", True) logger.info(" ".join(cmd)) return subprocess.run(cmd, **subprocess_kwargs) def _build_isis_fn(fn_name: str): """This factory builds a simple function to call an ISIS program.""" # Define the structure of the generic function, isis_fn: def isis_fn(*args, **kwargs) -> subprocess.CompletedProcess: __name__ = fn_name # noqa: F841 __doc__ = f"""Runs ISIS3 {fn_name}""" __doc__ += """ Any keyword arguments that begin with an underscore (_) will have their leading underscore removed and passed on to subprocess.run(), please see its documentation to see what is allowed. """ cmd = [fn_name] # Extract any keyword arguments for subprocess.run: subprocess_kwargs = dict() isis_kwargs = dict() for k, v in kwargs.items(): if k.startswith("_"): subprocess_kwargs[k[1:]] = v else: isis_kwargs[k] = v if fn_name in _pass_through_programs: cmd.extend(args) else: args_list = list(args) if len(args) > 0 and not str(args[0]).endswith("__"): cmd.append(param_fmt("from", args_list.pop(0))) for a in args_list: if a.endswith("__") and a.rstrip( "_" ) in _res_param_no_vals.union(_res_param_maybe): cmd.append("-{}".format(a.rstrip("_"))) else: e = ( "only accepts 1 non-keyword argument " "(and sets it to from= ) " "not sure what to do with " + a ) raise IndexError(e) cmd.extend( map(param_fmt, isis_kwargs.keys(), isis_kwargs.values()) ) return _run_isis_program(cmd, subprocess_kwargs) # Then add it, by name to the enclosing module. setattr(sys.modules[__name__], fn_name, isis_fn) # Could have also used sys.modules['kalasiris'] if I wanted to be explicit. def _get_isis_program_names(): """Returns an iterable of ISIS program names. With the new conda distribution, there is a lot of stuff in $ISISROOT/bin that isn't actually an ISIS program. So just slurping the names in $ISISROOT/bin gets too many things that aren't ISIS programs. Instead, the isis conda distribution provides $ISISROOT/bin/xml/ which contains the documentation XML files. Every XML file in that directory corresponds to the name of an ISIS program, which is perfect. """ bindir = Path(environ["ISISROOT"]) / "bin" xmldir = bindir / "xml" for entry in xmldir.iterdir(): if ( entry.is_file() and (bindir / entry.stem).is_file() and os.access(bindir / entry.stem, os.X_OK) and not entry.name.startswith(".") ): if ".xml" == entry.suffix: yield entry.stem # Now use the builder function to automatically create functions # with these names: for p in _get_isis_program_names(): _build_isis_fn(p)