Source code for law.cli.software

# coding: utf-8

"""
"law sw" cli subprogram.
"""


import os
import sys
import shutil
from importlib import import_module

import six

from law.config import Config
from law.logger import get_logger


logger = get_logger(__name__)


# TODO: auto-detect
default_dep_names = ["six", "luigi", "law"]
if six.PY3:
    default_dep_names += ["tenacity", "dateutil"]

dep_names = os.getenv("LAW_SOFTWARE_DEPS", None)
if dep_names:
    dep_names = [name.strip() for name in dep_names.strip().split(",")]
else:
    dep_names = default_dep_names

_deps = None


if "_reloaded_deps" not in globals():
    _reloaded_deps = False


[docs]def setup_parser(sub_parsers): """ Sets up the command line parser for the *software* subprogram and adds it to *sub_parsers*. """ csv = lambda s: [_s.strip() for _s in str(s).strip().split(",")] parser = sub_parsers.add_parser( "software", prog="law software", description="Create or update the law software cache ({}). This is only required for some " "sandboxes that need to forward software into (e.g.) containers.".format(get_sw_dir()), ) parser.add_argument( "--deps", "-d", type=csv, default=dep_names, help="comma-separated names of python packages to cache; " "default: {}".format(",".join(dep_names)), ) parser.add_argument( "--remove", "-r", action="store_true", help="remove the software cache directory and exit", ) parser.add_argument( "--location", "-l", action="store_true", help="print the location of the software cache directory and exit", ) parser.add_argument( "--print-deps", "-p", action="store_true", help="print the list of dependencies and exit", )
[docs]def execute(args): """ Executes the *software* subprogram with parsed commandline *args*. """ sw_dir = get_sw_dir() # just print the cache location? if args.location: print(sw_dir) return # just print the list of dependencies? if args.print_deps: print(",".join(args.deps)) return # just remove the current software cache? if args.remove: remove_software_cache(sw_dir) return # rebuild the software cache build_software_cache(sw_dir, dep_names=args.deps)
def get_software_deps(names=None): global _deps if _deps is not None: return _deps _deps = [] if names is None: names = list(dep_names) for name in names: try: mod = import_module(name) except ImportError as e: print("could not import software dependency '{}': {}".format(name, e)) continue _deps.append(mod) return _deps
[docs]def build_software_cache(sw_dir=None, dep_names=None): """ Builds up the software cache directory at *sw_dir* by simply copying all required python modules identified by *dep_names*, defaulting to a predefined list of package names. *sw_dir* is evaluated with :py:func:`get_sw_dir`. """ # ensure the cache is empty sw_dir = get_sw_dir(sw_dir) remove_software_cache(sw_dir) os.makedirs(sw_dir) # reload dependencies to find the proper module paths reload_dependencies(force=True) # get dependencies deps = get_software_deps(names=dep_names) for mod in deps: path = os.path.dirname(mod.__file__) name, ext = os.path.splitext(os.path.basename(mod.__file__)) # single file or module? if name == "__init__": # copy the entire module name = os.path.basename(path) shutil.copytree(path, os.path.join(sw_dir, name)) else: shutil.copy2(os.path.join(path, name + ".py"), sw_dir) logger.debug("cached '{}'".format(mod))
[docs]def remove_software_cache(sw_dir=None): """ Removes the software cache directory at *sw_dir* which is evaluated with :py:func:`get_sw_dir`. """ sw_dir = get_sw_dir(sw_dir) if os.path.exists(sw_dir): shutil.rmtree(sw_dir)
[docs]def reload_dependencies(force=False, dep_names=None): """ Reloads all python modules that law depends on, idenfied by *dep_names* and defaulting to a predefined list of package names. Unless *force* is *True*, multiple calls to this function will not have any effect. """ global _reloaded_deps # reload only once if _reloaded_deps and not force: return _reloaded_deps = True # get dependencies deps = get_software_deps(names=dep_names) # reload them for mod in deps: six.moves.reload_module(mod) logger.debug("reloaded '{}'".format(mod))
[docs]def use_software_cache(sw_dir=None, reload_deps=False): """ Adjusts ``sys.path`` so that the cached software at *sw_dir* is used. *sw_dir* is evaluated with :py:func:`get_sw_dir`. When *reload_deps* is *True*, :py:func:`reload_dependencies` is invoked. """ sw_dir = get_sw_dir(sw_dir) if not os.path.exists(sw_dir): return # ammend the search path to favor the software cache sys.path.insert(1, sw_dir) # reload dependencies in case they were already loaded if reload_deps: reload_dependencies()
[docs]def get_sw_dir(sw_dir=None): """ Returns the software directory defined in the ``core.software_dir`` config. When *sw_dir* is not *None*, it is expanded and returned instead. """ if sw_dir is None: cfg = Config.instance() sw_dir = cfg.get_expanded("core", "software_dir") sw_dir = os.path.expandvars(os.path.expanduser(sw_dir)) return sw_dir