# !/usr/bin/env python
# -*- coding: utf-8 -*-
import argparse
import logging
from pathlib import Path
import shutil
import seamm_installer
logger = logging.getLogger(__name__)
prolog = """\
# Configuration options for SEAMM.
#
# The options in this file override any defaults in SEAMM
# and its plug-ins; however, command-line arguments will
# in turn override the values here.
#
# The keys should have dashes '-' separating words. In either case,
# the command line options is '--<key with dashes>' and the variable
# name inside SEAMM is '<key with underscores>', e.g. 'log-level' in
# this file corresponds to the command line option '--log-level'
# and the variable in SEAMM 'log_level'.
#
# The file is broken into sections, with a name in square brackets,
# like [lammps-step]. Within each section there can be a series of
# option = value statements. '#' introduces comment lines. The
# section names and variables should be in lower case except for
# the [DEFAULT] and [SEAMM] sections which are special.
#
# [DEFAULT] provides default values for any other section. If an
# option is requested for a section, but does not exist in that
# section, the option is looked for in the [DEFAULT] section. If it
# exists there, the corresponding value is used.
#
# The [SEAMM] section contains options for the SEAMM environment
# itself. On the command line these come before any options for
# plug-ins, which follow the name of the plug-in. The plug-in name is
# also the section in this file for that plug-in.
#
# All other sections are for the plug-ins, and generally have the form
# [xxxxx-step], in lowercase.
#
# Finally, options can refer to options in the same or other sections
# with a syntax like ${section:option}. If the section is omitted,
# the current section and [DEFAULT] are searched, in that
# order. Otherwise the given section and [DEFAULT] are searched.
[DEFAULT]
# Default values for options in any section.
[SEAMM]
# Options for the SEAMM infrastructure.
"""
[docs]
class InstallerBase(object):
"""A base class for plug-in installers.
This base class provides much of the functionality needed by installers for
plug-ins, but not the functionality specific to a given plug-in.
Attributes
----------
section : str
The section of the configuration file to use. Defaults to None.
"""
def __init__(self, ini_file="~/.seamm.d/seamm.ini", logger=logger):
# Create the ini file if it does not exist.
self._check_ini_file(ini_file)
self.logger = logger
# and make the configuration, conda and pip objects
self._configuration = seamm_installer.Configuration(ini_file)
self._conda = seamm_installer.Conda()
self._pip = seamm_installer.Pip()
# Setup the parseer for the command-line
self.options = None
self.subparser = {}
self.parser = self.setup_parser()
# Other attributes
self.section = None
self.path_name = None
self.executables = None
self.resource_path = None
self._exe_config = seamm_installer.Configuration(None)
self.environment = None
@property
def conda(self):
"""The Conda object to use for accessing Conda."""
return self._conda
@property
def configuration(self):
"""The Configuration object for working with the ini file."""
return self._configuration
@property
def exe_config(self):
# The ini data for the executables
return self._exe_config
@property
def pip(self):
"""The Pip object used for working with pip."""
return self._pip
[docs]
def ask_yes_no(self, text, default=None):
"""Ask a simple yes/no question, returning True/False.
Parameters
----------
text : str
The text of the question.
Returns
-------
bool
True for yes; False, no
"""
if default is None:
answer = input(f"{text} y/n: ")
elif default == "yes":
answer = input(f"{text} [y]/n: ")
elif default == "no":
answer = input(f"{text} y/[n]: ")
else:
answer = input(f"{text} y/n: ")
while True:
if len(answer) == 0:
if default == "yes":
return True
elif default == "no":
return False
else:
answer = answer[0].lower()
if answer == "y":
return True
elif answer == "n":
return False
input("Please answer 'y' or 'n': ")
def _check_ini_file(self, ini_file):
"""Ensure that the ini file exists.
If it does not, it will be created and a template written to it. The
template contains a prolog with a description of the file followed by
empty [DEFAULT] and [SEAMM] sections, which ensures that they are
present and at the top of the file.
"""
path = Path(ini_file).expanduser().resolve()
if not path.exists():
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(prolog)
[docs]
def check(self):
"""Check the installation and fix errors if requested.
If the option `yes` is present and True, this method will attempt to
correct any errors in the configuration file. Use `--yes` on the
command line to enable this.
The information in the configuration file is:
installation
How the executables are installed. One of `user`, `modules` or `conda`
conda-environment
The Conda environment if and only if `installation` = `conda`
modules
The environment modules if `installation` = `modules`
{self.path_name}
The path where the executables are. Automatically
defined if `installation` is `conda` or `modules`, but given
by the user is it is `user`.
Returns
-------
bool
True if everything is OK, False otherwise. If `yes` is given as an
option, the return value is after fixing the configuration.
"""
self.logger.debug("Entering check method.")
if not self.configuration.section_exists(self.section):
if self.options.yes or self.ask_yes_no(
f"There is no section for {self.section} in the configuration "
f" file ({self.configuration.path}).\nAdd one?",
default="yes",
):
self.check_configuration_file()
print(
f" Added the {self.section} section to the configuration file "
f"{self.configuration.path}"
)
# Get the values from the executable configuration
data = self.exe_config.get_values("local")
# Save the initial values, if any, of the key configuration variables
if self.path_name in data and data[self.path_name] != "":
path = Path(data[self.path_name]).expanduser().resolve()
initial_exe_path = path
else:
initial_exe_path = None
if "installation" in data and data["installation"] != "":
initial_installation = data["installation"]
else:
initial_installation = None
if "conda-environment" in data and data["conda-environment"] != "":
initial_conda_environment = data["conda-environment"]
else:
initial_conda_environment = None
if "modules" in data and data["modules"] != "":
initial_modules = data["modules"]
else:
initial_modules = None
# Is there a valid -path?
self.logger.debug(
"Checking for the executable in the initial path " f"{initial_exe_path}."
)
if initial_exe_path is None or not self.have_executables(initial_exe_path):
exe_path = None
else:
exe_path = initial_exe_path
self.logger.debug(f"initial-exe-path = {initial_exe_path}.")
# Is there an installation indicated?
if initial_installation in ("conda", "modules", "local", "docker"):
installation = initial_installation
else:
installation = None
self.logger.debug(f"initial-installation = {initial_installation}.")
if installation == "conda":
# Is there a conda environment?
conda_environment = None
if initial_conda_environment is None or not self.conda.exists(
initial_conda_environment
):
if exe_path is not None:
# see if this path corresponds to a Conda environment
for tmp in self.conda.environments:
tmp_path = self.conda.path(tmp) / "bin"
if tmp_path == exe_path:
conda_environment = tmp
break
if conda_environment is not None:
if self.options.yes or self.ask_yes_no(
"The Conda environment in the config file "
"is not correct.\n"
f"It should be {conda_environment}. Fix?",
default="yes",
):
self.exe_config.set_value("local", "installation", "conda")
self.exe_config.set_value(
"local", "conda-environment", conda_environment
)
conda_exe = shutil.which("conda")
if conda_exe is None:
print(
" Cannot find the path to the conda executable! "
"Please fix the path in the configuration file."
)
else:
self.exe_config.set_value("local", "conda", conda_exe)
# Clean up the environment file
self.exe_config.set_value("local", "modules", None)
self.exe_config.set_value("local", "container", None)
self.exe_config.set_value("local", "platform", None)
self.exe_config.save()
print(
" Corrected the conda environment to "
f"{conda_environment}"
)
else:
conda_environment = None
print(
" The mopac.ini file specifies using a Conda environment, "
"however, the executable(s) are not in the environment."
)
else:
# Have a Conda environment!
conda_path = self.conda.path(initial_conda_environment) / "bin"
self.logger.debug(
f"Checking for executable in conda-path: {conda_path}."
)
if self.have_executables(conda_path):
# All is good!
conda_environment = initial_conda_environment
conda_exe = shutil.which("conda")
if conda_exe is None:
print(
" Cannot find the path to the conda executable! "
"Please fix the path in the configuration file."
)
else:
self.exe_config.set_value("local", "conda", conda_exe)
self.exe_config.save()
print(" The conda path is correct.")
else:
conda_environment = None
print(
" The mopac.ini file specifies using a Conda environment, "
"however, the executable(s) are not in the environment."
)
elif installation == "modules":
print(f"Can't check the actual modules {initial_modules} yet")
if initial_conda_environment is not None:
if self.options.yes or self.ask_yes_no(
"A Conda environment is given: "
f"{initial_conda_environment}.\n"
"A Conda environment should not be used when using "
"modules. Remove it from the configuration?",
default="yes",
):
# Clean up the environment file
self.exe_config.set_value("local", "conda", None)
self.exe_config.set_value("local", "conda-environment", None)
self.exe_config.set_value("local", "container", None)
self.exe_config.set_value("local", "platform", None)
self.exe_config.save()
print(
" Using modules, so removed the conda-environment from "
"the configuration"
)
elif installation == "docker":
if "container" in data and data["container"] != "":
container = data["container"]
print(f" Setup to use the docker container {container}")
else:
print(" Setup to use docker, but the container is not set!")
if initial_conda_environment is not None:
if self.options.yes or self.ask_yes_no(
"A Conda environment is given: "
f"{initial_conda_environment}.\n"
"A Conda environment should not be used when using "
"docker. Remove it from the configuration?",
default="yes",
):
# Clean up the environment file
self.exe_config.set_value("local", "conda", None)
self.exe_config.set_value("local", "conda-environment", None)
self.exe_config.set_value("local", "modules", None)
self.exe_config.set_value("local", "conda", None)
self.exe_config.save()
print(
" Using docker, so removed the conda-environment from "
"the configuration"
)
else:
if exe_path is None:
# No path or executable in the path!
environments = self.conda.environments
if self.environment in environments:
# Make sure it is first!
environments.remove(self.environment)
environments.insert(0, self.environment)
for tmp in environments:
tmp_path = self.conda.path(tmp) / "bin"
if self.have_executables(tmp_path):
if self.options.yes or self.ask_yes_no(
f"There are no valid executables in the {self.path_name}"
" in the config file, but there are in the Conda "
f"environment {tmp} ({tmp_path}).\n"
"Use them?",
default="yes",
):
conda_environment = tmp
exe_path = tmp_path
self.exe_config.set_value("local", self.path_name, exe_path)
self.exe_config.set_value("local", "installation", "conda")
self.exe_config.set_value(
"local", "conda-environment", conda_environment
)
# Clean up the environment file
self.exe_config.set_value("local", "conda", None)
self.exe_config.set_value(
"local", "conda-environment", None
)
self.exe_config.set_value("local", "modules", None)
self.exe_config.set_value("local", "conda", None)
self.exe_config.set_value("local", "container", None)
self.exe_config.set_value("local", "platform", None)
self.exe_config.save()
print(
" Will use the conda environment "
f"'{conda_environment}'"
)
break
if exe_path is None:
# Haven't found it. Check in the path.
exe_path = self.executables_in_path()
if exe_path is not None:
if self.options.yes or self.ask_yes_no(
"Found valid executable(s) in the PATH at "
f"{exe_path}\n"
"Use them?",
default="yes",
):
self.exe_config.set_value("local", "installation", "local")
# Clean up the environment file
self.exe_config.set_value("local", "conda", None)
self.exe_config.set_value("local", "conda-environment", None)
self.exe_config.set_value("local", "modules", None)
self.exe_config.set_value("local", "container", None)
self.exe_config.set_value("local", "platform", None)
self.exe_config.save()
print(" Using the executable(s) at {exe_path}")
if exe_path is None:
# Can't find the executable(s)
print(
f" Cannot find the executable(s): {', '.join(self.executables)}."
"\n You will need to install them."
)
if (
initial_installation is not None
and initial_installation != "not installed"
):
if self.options.yes or self.ask_yes_no(
"The configuration file indicates that the executable(s) "
"are installed, but they can't be found.\n"
"Fix the configuration file?",
default="yes",
):
# Update the configuration file.
self.exe_config.set_value("local", "installation", None)
self.exe_config.set_value("local", "conda", None)
self.exe_config.set_value("local", "conda-environment", None)
self.exe_config.set_value("local", "modules", None)
self.exe_config.set_value("local", "container", None)
self.exe_config.set_value("local", "platform", None)
self.exe_config.save()
print(
" Since no executable(s) were found, cleared "
"the configuration."
)
else:
print(" The check completed successfully.")
[docs]
def check_configuration_file(self):
"""Checks that the necessary section for the plug-in is in the
configuration file.
"""
if not self.configuration.section_exists(self.section):
# Get the text of the data
path = self.resource_path / "configuration.txt"
text = path.read_text()
# Add it to the configuration file and write to disk.
self.configuration.add_section(self.section, text)
self.configuration.save()
[docs]
def have_executables(self, path):
"""Check whether the executables are found at the given path.
Parameters
----------
path : pathlib.Path
The directory to check.
Returns
-------
bool
True if all of the executables are found.
"""
for executable in self.executables:
tmp_path = path / executable
if not tmp_path.exists():
self.logger.debug(f"Did not find {executable} in {path}")
return False
self.logger.debug(f"Found all executables in {path}")
return True
[docs]
def executables_in_path(self):
"""Check whether the executables are found in the PATH.
Returns
-------
pathlib.Path
The path where the executables are, or None.
"""
path = None
for executable in self.executables:
path = shutil.which(executable)
if path is not None:
path = Path(path).expanduser().resolve()
break
# And check that have all the executables
if path is not None and self.have_executables(path):
return path
else:
return None
[docs]
def install(self):
"""Install using a Conda environment."""
print(
f" Installing Conda environment '{self.environment}'. This "
"may take a minute or two."
)
self.conda.create_environment(self.environment_file, name=self.environment)
# Update the configuration file.
self.check_configuration_file()
# Update the executable configuration file.
self.exe_config.set_value("local", "installation", "conda")
conda_exe = shutil.which("conda")
if conda_exe is not None:
self.exe_config.set_value("local", "conda", conda_exe)
self.exe_config.set_value("local", "conda-environment", self.environment)
# Clean up the environment file
self.exe_config.set_value("local", "modules", None)
self.exe_config.set_value("local", "container", None)
self.exe_config.set_value("local", "platform", None)
self.exe_config.save()
print(" Done!\n")
[docs]
def run(self):
"""Do what the user asks via the commandline."""
self.options = self.parser.parse_args()
if "method" not in self.options:
self.parser.print_help()
else:
# Run the requested subcommand
self.options.method()
[docs]
def setup_parser(self):
"""Parse the command line into the options."""
parser = argparse.ArgumentParser()
parser.add_argument(
"--log-level",
default="WARNING",
type=str.upper,
choices=["NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
help=("The level of informational output, defaults to " "'%(default)s'"),
)
parser.add_argument(
"--environment",
default="seamm",
type=str.lower,
help="The conda environment for seamm, defaults to '%(default)s'",
)
subparsers = parser.add_subparsers()
self.subparser["subparsers"] = subparsers
# check
self.subparser["check"] = check = subparsers.add_parser("check")
check.add_argument(
"-y", "--yes", action="store_true", help="Answer 'yes' to all prompts"
)
check.set_defaults(method=self.check)
# install
self.subparser["install"] = install = subparsers.add_parser("install")
install.set_defaults(method=self.install)
# update
self.subparser["update"] = update = subparsers.add_parser("update")
update.set_defaults(method=self.update)
# uninstall
uninstall = subparsers.add_parser("uninstall")
self.subparser["uninstall"] = uninstall
uninstall.set_defaults(method=self.uninstall)
# show
self.subparser["show"] = show = subparsers.add_parser("show")
show.set_defaults(method=self.show)
# Parse what we know so that we can set up logging.
tmp = parser.parse_known_args()
self.options = tmp[0]
# Set up the logging
level = self.options.log_level
logging.basicConfig(level=level)
# Don't know why basicConfig doesn't seem to work!
self.logger.setLevel(level)
self.logger.info(f"Logging level is {level}")
return parser
[docs]
def show(self):
"""Show the current installation status."""
self.logger.debug("Entering show")
# See if the executables are already registered in the configuration file
if not self.exe_config.section_exists("local"):
print(" There is no section in the configuration file for 'local'.")
data = self.exe_config.get_values("local")
if "code" in data:
print(f" The command line:\n\t{data['code']}")
else:
print("! There is no command line specified.")
if "installation" in data:
installation = data["installation"]
if installation == "conda":
if "conda-environment" in data and data["conda-environment"] != "":
print(
" run using the Conda environment "
f"{data['conda-environment']}."
)
name, version = self.exe_version(data)
print(f" {name} version {version}.")
else:
print("! run from an unknown Conda environment.")
elif installation == "modules":
if "modules" in data and data["modules"] != "":
print(f" run using module(s) {data['modules']}.")
else:
print("! run using unknown modules.")
elif installation == "local":
pass
elif installation == "docker":
line = f" run using the Docker container {data['container']}"
if "platform" in data:
line += f" for {data['platform']}."
else:
line += "."
print(line)
else:
print(f"! Unknown installation method '{installation}'")
else:
print("! Does not seem to be configured to run!")
[docs]
def uninstall(self):
"""Uninstall the Conda environment."""
# See if the executables are already registered in the configuration file
data = self.exe_config.get_values("local")
if "installation" in data and data["installation"] == "conda":
if "conda-environment" in data and data["conda-environment"] != "":
environment = data["conda-environment"]
print(
f" Uninstalling Conda environment '{environment}'. This "
"may take a minute or two."
)
self.conda.remove_environment(environment)
# Update the configuration file.
self.exe_config.set_value("local", "installation", None)
self.exe_config.set_value("local", "conda", None)
self.exe_config.set_value("local", "conda-environment", None)
self.exe_config.set_value("local", "modules", None)
self.exe_config.set_value("local", "container", None)
self.exe_config.set_value("local", "platform", None)
self.exe_config.save()
print(" Done!\n")
[docs]
def update(self):
"""Update the installation, if possible."""
# See if the executables are already registered in the configuration file
data = self.exe_config.get_values("local")
if "installation" in data and data["installation"] == "conda":
environment = self.environment
if "conda-environment" in data and data["conda-environment"] != "":
environment = data["conda-environment"]
print(
f" Updating Conda environment '{environment}'. This may "
"take a minute or two."
)
self.conda.update_environment(self.environment_file, name=environment)
# Update the configuration file, just in case.
self.exe_config.set_value("local", "installation", "conda")
conda_exe = shutil.which("conda")
if conda_exe is not None:
self.exe_config.set_value("local", "conda", conda_exe)
self.exe_config.set_value("local", "conda-environment", environment)
# Clean up the environment file
self.exe_config.set_value("local", "modules", None)
self.exe_config.set_value("local", "container", None)
self.exe_config.set_value("local", "platform", None)
self.exe_config.save()
print(" Done!\n")
else:
print(
"! Unable to update the executables because they were not installed "
"using Conda"
)