From 9d949254814be78df8d1f3368dd33878fba35ad7 Mon Sep 17 00:00:00 2001 From: Vanessa Sochat Date: Tue, 28 May 2019 18:28:54 -0400 Subject: [PATCH 01/21] WIP to fix up recipe parsers, writers, and nase, still writing writers Signed-off-by: Vanessa Sochat --- CHANGELOG.md | 1 + spython/main/parse/__init__.py | 3 - spython/main/parse/base.py | 59 ++++ spython/main/parse/converters.py | 221 -------------- spython/main/parse/environment.py | 64 ---- spython/main/parse/parsers/README.md | 11 + spython/main/parse/parsers/__init__.py | 9 + spython/main/parse/parsers/base.py | 97 ++++++ spython/main/parse/{ => parsers}/docker.py | 191 ++++++++---- .../main/parse/{ => parsers}/singularity.py | 97 +++--- spython/main/parse/recipe.py | 284 ++---------------- spython/version.py | 2 +- 12 files changed, 385 insertions(+), 654 deletions(-) create mode 100644 spython/main/parse/base.py delete mode 100644 spython/main/parse/converters.py delete mode 100644 spython/main/parse/environment.py create mode 100644 spython/main/parse/parsers/README.md create mode 100644 spython/main/parse/parsers/__init__.py create mode 100644 spython/main/parse/parsers/base.py rename spython/main/parse/{ => parsers}/docker.py (75%) rename spython/main/parse/{ => parsers}/singularity.py (89%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55ea0d19..b5d79696 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ The client here will eventually be released as "spython" (and eventually to singularity on pypi), and the versions here will coincide with these releases. ## [master](https://github.com/singularityhub/singularity-cli/tree/master) + - refactor recipe parsers, writers, and base (0.0.63) - fix crash in some error conditions (0.0.62) - more OCI commands accept sudo parameter - working directory, the last one defined, should be added to runscript (0.0.61) diff --git a/spython/main/parse/__init__.py b/spython/main/parse/__init__.py index b505d158..16f1d581 100644 --- a/spython/main/parse/__init__.py +++ b/spython/main/parse/__init__.py @@ -7,6 +7,3 @@ with this file, You can obtain one at http://mozilla.org/MPL/2.0/. ''' - -from .docker import DockerRecipe -from .singularity import SingularityRecipe diff --git a/spython/main/parse/base.py b/spython/main/parse/base.py new file mode 100644 index 00000000..2cf40438 --- /dev/null +++ b/spython/main/parse/base.py @@ -0,0 +1,59 @@ + +# Copyright (C) 2017-2019 Vanessa Sochat. + +# This Source Code Form is subject to the terms of the +# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed +# with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import sys + +from spython.logger import bot +from spython.utils import ( read_file, write_file ) + + +class Recipe(object): + '''a recipe includes an environment, labels, runscript or command, + and install sequence. This object is interacted with by a Parser + (intended to popualte the recipe with content) and a Writer (intended + to write a recipe to file). The parsers and writers are located in + parsers.py, and writers.py, respectively. The user is also free to use + the recipe class to build recipes. + + Parameters + ========== + recipe: the original recipe file, parsed by the subclass either + DockerRecipe or SingularityRecipe + + ''' + + def __init__(self, recipe=None): + + self.cmd = None + self.comments = [] + self.entrypoint = None + self.environ = [] + self.files = [] + self.install = [] + self.labels = [] + self.ports = [] + self.test = None + self.volumes = [] + self.workdir = None + + self.source = recipe + + def __str__(self): + ''' show the user the recipe object, along with the type. E.g., + + [spython-recipe][source:Singularity] + [spython-recipe][source:Dockerfile] + + ''' + base = "[spython-recipe]" + if self.source: + base = "%s[source:%s]" %(base, self.source) + return base + + def __repr__(self): + return self.__str__() diff --git a/spython/main/parse/converters.py b/spython/main/parse/converters.py deleted file mode 100644 index d646778a..00000000 --- a/spython/main/parse/converters.py +++ /dev/null @@ -1,221 +0,0 @@ - -# Copyright (C) 2017-2019 Vanessa Sochat. - -# This Source Code Form is subject to the terms of the -# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed -# with this file, You can obtain one at http://mozilla.org/MPL/2.0/. - - -import re -from spython.logger import bot - -# Singularity to Dockerfile -# Easier, parsed line by line - -def singularity2docker(self, runscript="/bin/bash", force=False): - '''convert a Singularity recipe to a (best estimated) Dockerfile''' - - recipe = [ "FROM %s" %self.fromHeader ] - - # Comments go up front! - recipe += self.comments - - # First add files, labels - recipe += write_lines('ADD', self.files) - recipe += write_lines('LABEL', self.labels) - recipe += write_lines('ENV', self.environ) - - # Install routine is added as RUN commands - recipe += write_lines('RUN', self.install) - - # Take preference for user, entrypoint, command, then default - runscript = self._create_runscript(runscript, force) - recipe.append('CMD %s' %runscript) - - if self.test is not None: - recipe.append(write_lines('HEALTHCHECK', self.test)) - - # Clean up extra white spaces - return '\n'.join(recipe).replace('\n\n','\n') - - -def write_lines(label, lines): - '''write a list of lines with a header for a section. - - Parameters - ========== - lines: one or more lines to write, with header appended - - ''' - result = [] - continued = False - for line in lines: - if continued: - result.append(line) - else: - result.append('%s %s' %(label, line)) - continued = False - if line.endswith('\\'): - continued = True - - return result - - -# Dockerfile to Singularity -# Here we deal with "sections" and not individual lines - -def create_runscript(self, default="/bin/bash", force=False): - '''create_entrypoint is intended to create a singularity runscript - based on a Docker entrypoint or command. We first use the Docker - ENTRYPOINT, if defined. If not, we use the CMD. If neither is found, - we use function default. - - Parameters - ========== - default: set a default entrypoint, if the container does not have - an entrypoint or cmd. - force: If true, use default and ignore Dockerfile settings - - ''' - entrypoint = default - - # Only look at Docker if not enforcing default - if not force: - if self.entrypoint is not None: - entrypoint = ' '.join(self.entrypoint) - if self.cmd is not None: - entrypoint = entrypoint + ' ' + ' '.join(self.cmd) - - # Entrypoint should use exec - if not entrypoint.startswith('exec'): - entrypoint = "exec %s" % entrypoint - - # Should take input arguments into account - if not re.search('"?[$]@"?', entrypoint): - entrypoint = '%s "$@"' % entrypoint - return entrypoint - - -def create_section(self, attribute, name=None): - '''create a section based on key, value recipe pairs, - This is used for files or label - - Parameters - ========== - attribute: the name of the data section, either labels or files - name: the name to write to the recipe file (e.g., %name). - if not defined, the attribute name is used. - - ''' - - # Default section name is the same as attribute - if name is None: - name = attribute - - # Put a space between sections - section = ['\n'] - - # Only continue if we have the section and it's not empty - try: - section = getattr(self, attribute) - except AttributeError: - bot.debug('Recipe does not have section for %s' %attribute) - return section - - # if the section is empty, don't print it - if len(section) == 0: - return section - - # Files or Labels - if attribute in ['labels', 'files']: - return create_keyval_section(section, name) - - # An environment section needs exports - if attribute in ['environ']: - return create_env_section(section, name) - - # Post, Setup - return finish_section(section, name) - - -def finish_section(section, name): - '''finish_section will add the header to a section, to finish the recipe - take a custom command or list and return a section. - - Parameters - ========== - section: the section content, without a header - name: the name of the section for the header - - ''' - if not isinstance(section, list): - section = [section] - - header = ['%' + name ] - return header + section - - -def create_keyval_section(pairs, name): - '''create a section based on key, value recipe pairs, - This is used for files or label - - Parameters - ========== - section: the list of values to return as a parsed list of lines - name: the name of the section to write (e.g., files) - - ''' - section = ['%' + name ] - for pair in pairs: - section.append(' '.join(pair).strip().strip('\\')) - return section - - -def create_env_section(pairs, name): - '''environment key value pairs need to be joined by an equal, and - exported at the end. - - Parameters - ========== - section: the list of values to return as a parsed list of lines - name: the name of the section to write (e.g., files) - - ''' - section = ['%' + name ] - for pair in pairs: - section.append("export %s" %pair) - return section - - -def docker2singularity(self, runscript="/bin/bash", force=False): - '''docker2singularity will return a Singularity build recipe based on - a the loaded recipe object. It doesn't take any arguments as the - recipe object contains the sections, and the calling function - determines saving / output logic. - ''' - - recipe = ['Bootstrap: docker'] - recipe += [ "From: %s" %self.fromHeader ] - - # Sections with key value pairs - recipe += self._create_section('files') - recipe += self._create_section('labels') - recipe += self._create_section('install', 'post') - recipe += self._create_section('environ', 'environment') - - # Take preference for user, entrypoint, command, then default - runscript = self._create_runscript(runscript, force) - - # If a working directory was used, add it as a cd - if self.workdir is not None: - runscript = [self.workdir] + [runscript] - - # Finish the recipe, also add as startscript - recipe += finish_section(runscript, 'runscript') - recipe += finish_section(runscript, 'startscript') - - if self.test is not None: - recipe += finish_section(self.test, 'test') - - # Clean up extra white spaces - return '\n'.join(recipe).replace('\n\n','\n') diff --git a/spython/main/parse/environment.py b/spython/main/parse/environment.py deleted file mode 100644 index b7fe51c0..00000000 --- a/spython/main/parse/environment.py +++ /dev/null @@ -1,64 +0,0 @@ - -# Copyright (C) 2017-2019 Vanessa Sochat. - -# This Source Code Form is subject to the terms of the -# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed -# with this file, You can obtain one at http://mozilla.org/MPL/2.0/. - -import re - - -def parse_env(envlist): - '''parse_env will parse a single line (with prefix like ENV removed) to - a list of commands in the format KEY=VALUE For example: - - ENV PYTHONBUFFER 1 --> [PYTHONBUFFER=1] - - ::Notes - Docker: https://docs.docker.com/engine/reference/builder/#env - - ''' - if not isinstance(envlist, list): - envlist = [envlist] - - exports = [] - - for env in envlist: - - pieces = re.split("( |\\\".*?\\\"|'.*?')", env) - pieces = [p for p in pieces if p.strip()] - - while len(pieces) > 0: - - current = pieces.pop(0) - - if current.endswith('='): - - # Case 1: ['A='] --> A= - - next = "" - - # Case 2: ['A=', '"1 2"'] --> A=1 2 - - if len(pieces) > 0: - next = pieces.pop(0) - exports.append("%s%s" %(current, next)) - - # Case 2: ['A=B'] --> A=B - - elif '=' in current: - exports.append(current) - - # Case 3: ENV \\ - - elif current.endswith('\\'): - continue - - # Case 4: ['A', 'B'] --> A=B - - else: - - next = pieces.pop(0) - exports.append("%s=%s" %(current, next)) - - return exports diff --git a/spython/main/parse/parsers/README.md b/spython/main/parse/parsers/README.md new file mode 100644 index 00000000..85b1d2bf --- /dev/null +++ b/spython/main/parse/parsers/README.md @@ -0,0 +1,11 @@ +# Parsers + +A parser class is intended to read in a container recipe file, and parse +sections into a spython.main.recipe Recipe object. To create a new subclass +of parser, you can copy one of the current (Docker or Singularity) as an +example, and keep in mind the following: + + - The base class, `ParserBase` in [base.py](base.py) has already added an instantiated (and empty) Recipe() for the subclass to interact with (fill with content). + - The subclass is encouraged to define the name (self.name) attribute for printing to the user. + - The subclass should take the input file as an argument to pass the the ParserBase, which will handle reading in lines to a list self.lines. + - The subclass should have a main method, parse, that when called will read the input file and populate the recipe (and return it to the user). diff --git a/spython/main/parse/parsers/__init__.py b/spython/main/parse/parsers/__init__.py new file mode 100644 index 00000000..12d3e4f4 --- /dev/null +++ b/spython/main/parse/parsers/__init__.py @@ -0,0 +1,9 @@ + +# Copyright (C) 2017-2019 Vanessa Sochat. + +# This Source Code Form is subject to the terms of the +# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed +# with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from .docker import DockerParser +from .singularity import SingularityParser diff --git a/spython/main/parse/parsers/base.py b/spython/main/parse/parsers/base.py new file mode 100644 index 00000000..b7e90b2c --- /dev/null +++ b/spython/main/parse/parsers/base.py @@ -0,0 +1,97 @@ + +# Copyright (C) 2017-2019 Vanessa Sochat. + +# This Source Code Form is subject to the terms of the +# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed +# with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import json +import os + +from spython.logger import bot +from spython.utils import read_file +from ..recipe import Recipe + +class ParserBase(object): + '''a parser Base is intended to provide helper functions for a parser, + namely to read lines in files, and otherwise interact with outputs. + Input should be some recipe (text file to describe a container build) + and output of parse() is a Recipe (spython.main.parse.recipe.Recipe) + object, which can be used to write to file, etc. + ''' + + lines = [] + + def __init__(self, filename=None, load=True): + '''a generic recipe parser holds the original file, and provides + shared functions for interacting with files. If the subclass has + a parse function defined, we parse the filename + + Parameters + ========== + recipe: the recipe file to parse. + + ''' + self.filename = recipe + self._run_checks() + self.recipe = Recipe(self.filename) + + if self.filename and load is True: + + # Read in the raw lines of the file + self.lines = read_file(self.filename) + + # If parsing function defined, parse the recipe + if hasattr(self, 'parse'): + self.parse() + + + def _run_checks(self): + '''basic sanity checks for the file name (and others if needed) before + attempting parsing. + ''' + if self.recipe is not None: + + # Does the recipe provided exist? + if not os.path.exists(self.recipe): + bot.exit("Cannot find %s, is the path correct?" % self.recipe) + + # Ensure we carry fullpath + self.recipe = os.path.abspath(self.recipe) + + +# Printing + + def __str__(self): + ''' show the user the recipe object, along with the type. E.g., + + [spython-parser][docker] + [spython-parser][singularity] + + ''' + base = "[spython-parser]" + if hasattr(self, 'name'): + base = "%s[%s]" %(base, self.name) + return base + + def __repr__(self): + return self.__str__() + + +# Lines + + def _split_line(self, line): + '''clean a line to prepare it for parsing, meaning separation + of commands. We remove newlines (from ends) along with extra spaces. + + Parameters + ========== + line: the string to parse into parts + + Returns + ======= + parts: a list of line pieces, the command is likely first + + ''' + return [x.strip() for x in line.split(' ', 1)] + diff --git a/spython/main/parse/docker.py b/spython/main/parse/parsers/docker.py similarity index 75% rename from spython/main/parse/docker.py rename to spython/main/parse/parsers/docker.py index ebf744ee..1ffe7119 100644 --- a/spython/main/parse/docker.py +++ b/spython/main/parse/parsers/docker.py @@ -9,25 +9,57 @@ import os import re -from .environment import parse_env -from .recipe import Recipe from spython.logger import bot +from .base import ParserBase -class DockerRecipe(Recipe): - def __init__(self, recipe=None): - '''a Docker recipe parses a Docker Recipe into the expected fields of - labels, environment, and install/runtime commands. We save working - directory as we parse, and the last one can be added to the runscript - of a Singularity recipe. +class DockerParser(ParserBase): + + name = 'docker' + + def __init__(self, recipe='Dockerfile', load=True): + '''a docker parser will read in a Dockerfile and put it into a Recipe + object. Parameters ========== - recipe: the recipe file (Dockerfile) to parse + recipe: the Dockerfile to parse. If not defined, deafults to + Dockerfile assumed to be in the $PWD. ''' - self.name = "docker" - super(DockerRecipe, self).__init__(recipe) + super(DockerParser, self).__init__(recipe, load) + + + def parse(self): + '''parse is the base function for parsing the Dockerfile, and extracting + elements into the correct data structures. Everything is parsed into + lists or dictionaries that can be assembled again on demand. + + Environment: Since Docker also exports environment as we go, + we add environment to the environment section and + install + + Labels: include anything that is a LABEL, ARG, or (deprecated) + maintainer. + + Add/Copy: are treated the same + + ''' + parser = None + previous = None + + for line in self.lines: + + parser = self._get_mapping(line, parser, previous) + + # Parse it, if appropriate + if parser: + parser(line) + + previous = line + + # Instantiated by ParserBase + return self.recipe # Setup for each Parser @@ -56,14 +88,14 @@ def _from(self, line): Parameters ========== line: the line from the recipe file to parse for FROM - + recipe: the recipe object to populate. ''' fromHeader = self._setup('FROM', line) # Singularity does not support AS level - self.fromHeader = re.sub("AS .+", "", fromHeader[0], flags=re.I) + self.recipe.fromHeader = re.sub("AS .+", "", fromHeader[0], flags=re.I) - if "scratch" in self.fromHeader: + if "scratch" in self.recipe.fromHeader: bot.warning('scratch is no longer available on Docker Hub.') bot.debug('FROM %s' %self.fromHeader) @@ -79,7 +111,7 @@ def _run(self, line): ''' line = self._setup('RUN', line) - self.install += line + self.recipe.install += line def _test(self, line): @@ -90,7 +122,7 @@ def _test(self, line): line: the line from the recipe file to parse for FROM ''' - self.test = self._setup('HEALTHCHECK', line) + self.recipe.test = self._setup('HEALTHCHECK', line) # Arg Parser @@ -123,13 +155,59 @@ def _env(self, line): line = self._setup('ENV', line) # Extract environment (list) from the line - environ = parse_env(line) + environ = self.parse_env(line) # Add to global environment, run during install - self.install += environ + self.recipe.install += environ # Also define for global environment - self.environ += environ + self.recipe.environ += environ + + + def parse_env(self, envlist): + '''parse_env will parse a single line (with prefix like ENV removed) to + a list of commands in the format KEY=VALUE For example: + + ENV PYTHONBUFFER 1 --> [PYTHONBUFFER=1] + Docker: https://docs.docker.com/engine/reference/builder/#env + ''' + if not isinstance(envlist, list): + envlist = [envlist] + + exports = [] + + for env in envlist: + + pieces = re.split("( |\\\".*?\\\"|'.*?')", env) + pieces = [p for p in pieces if p.strip()] + + while len(pieces) > 0: + current = pieces.pop(0) + + if current.endswith('='): + + # Case 1: ['A='] --> A= + next = "" + + # Case 2: ['A=', '"1 2"'] --> A=1 2 + if len(pieces) > 0: + next = pieces.pop(0) + exports.append("%s%s" %(current, next)) + + # Case 3: ['A=B'] --> A=B + elif '=' in current: + exports.append(current) + + # Case 4: ENV \\ + elif current.endswith('\\'): + continue + + # Case 5: ['A', 'B'] --> A=B + else: + next = pieces.pop(0) + exports.append("%s=%s" %(current, next)) + + return exports # Add and Copy Parser @@ -211,7 +289,7 @@ def expandPath(path): bot.warning("%s doesn't exist, ensure exists for build" % source) # The pair is added to the files as a list - self.files.append([expandPath(source), expandPath(dest)]) + self.recipe.files.append([expandPath(source), expandPath(dest)]) def _parse_http(self, url, dest): @@ -227,7 +305,7 @@ def _parse_http(self, url, dest): file_name = os.path.basename(url) download_path = "%s/%s" %(dest, file_name) command = "curl %s -o %s" %(url, download_path) - self.install.append(command) + self.recipe.install.append(command) def _parse_archive(self, targz, dest): @@ -242,7 +320,7 @@ def _parse_archive(self, targz, dest): ''' # Add command to extract it - self.install.append("tar -zvf %s %s" %(targz, dest)) + self.recipe.install.append("tar -zvf %s %s" %(targz, dest)) # Ensure added to container files return self._add_files(targz, dest) @@ -260,7 +338,7 @@ def _comment(self, line): line: the line from the recipe file to parse to INSTALL ''' - self.install.append(line) + self.recipe.install.append(line) def _default(self, line): @@ -273,7 +351,7 @@ def _default(self, line): ''' if line.strip().startswith('#'): return self._comment(line) - self.install.append(line) + self.recipe.install.append(line) # Ports and Volumes @@ -290,7 +368,7 @@ def _volume(self, line): ''' volumes = self._setup('VOLUME', line) if len(volumes) > 0: - self.volumes += volumes + self.recipe.volumes += volumes return self._comment("# %s" %line) @@ -304,7 +382,7 @@ def _expose(self, line): ''' ports = self._setup('EXPOSE', line) if len(ports) > 0: - self.ports += ports + self.recipe.ports += ports return self._comment("# %s" %line) @@ -320,9 +398,9 @@ def _workdir(self, line): ''' # Save the last working directory to add to the runscript workdir = self._setup('WORKDIR', line) - self.workdir = "cd %s" %(''.join(workdir)) - self.install.append(self.workdir) - + workdir = "cd %s" %(''.join(workdir)) + self.recipe.install.append(workdir) + self.recipe.workdir = workdir # Entrypoint and Command @@ -338,7 +416,7 @@ def _cmd(self, line): ''' cmd = self._setup('CMD', line)[0] - self.cmd = self._load_list(cmd) + self.recipe.cmd = self._load_list(cmd) def _load_list(self, line): @@ -364,7 +442,7 @@ def _entry(self, line): ''' entrypoint = self._setup('ENTRYPOINT', line)[0] - self.entrypoint = self._load_list(entrypoint) + self.recipe.entrypoint = self._load_list(entrypoint) # Labels @@ -378,7 +456,7 @@ def _label(self, line): ''' label = self._setup('LABEL', line) - self.labels += [ label ] + self.recipe.labels += [ label ] # Main Parsing Functions @@ -440,30 +518,41 @@ def _get_mapping(self, line, parser=None, previous=None): return self._default - def _parse(self): - '''parse is the base function for parsing the Dockerfile, and extracting - elements into the correct data structures. Everything is parsed into - lists or dictionaries that can be assembled again on demand. + def _clean_line(self, line): + '''clean line will remove comments, and strip the line of newlines + or spaces. - Environment: Since Docker also exports environment as we go, - we add environment to the environment section and - install - - Labels: include anything that is a LABEL, ARG, or (deprecated) - maintainer. + Parameters + ========== + line: the string to parse into parts - Add/Copy: are treated the same + Returns + ======= + line: a cleaned line ''' - parser = None - previous = None + # A line that is None should return empty string + line = line or '' + return line.split('#')[0].strip() - for line in self.lines: - parser = self._get_mapping(line, parser, previous) + def _write_script(self, path, lines, chmod=True): + '''write a script with some lines content to path in the image. This + is done by way of adding echo statements to the install section. - # Parse it, if appropriate - if parser: - parser(line) + Parameters + ========== + path: the path to the file to write + lines: the lines to echo to the file + chmod: If true, change permission to make u+x + + ''' + if len(lines) > 0: + lastline = lines.pop() + for line in lines: + self.recipe.install.append('echo "%s" >> %s' %line %path) + self.recipe.install.append(lastline) + + if chmod is True: + self.recipe.install.append('chmod u+x %s' %path) - previous = line diff --git a/spython/main/parse/singularity.py b/spython/main/parse/parsers/singularity.py similarity index 89% rename from spython/main/parse/singularity.py rename to spython/main/parse/parsers/singularity.py index e001c3f3..cd1c0fc1 100644 --- a/spython/main/parse/singularity.py +++ b/spython/main/parse/parsers/singularity.py @@ -10,24 +10,53 @@ import sys from spython.logger import bot -from spython.main.parse.recipe import Recipe +from .base import ParserBase -class SingularityRecipe(Recipe): +class SingularityParser(ParserBase): - def __init__(self, recipe=None): - '''a Docker recipe parses a Docker Recipe into the expected fields of - labels, environment, and install/runtime commands + name = 'singularity' + + def __init__(self, recipe="Singularity", load=True): + '''a SingularityParser parses a Singularity file into expected fields of + labels, environment, and install/runtime commands. The base class + ParserBase will instantiate an empty Recipe() object to populate, + and call parse() here on the recipe. Parameters ========== - recipe: the recipe file (Dockerfile) to parse + recipe: the recipe file (Singularity) to parse + load: load and parse the recipe (defaults to True) ''' + super(SingularityParser, self).__init__(recipe, load) + + + def parse(self): + '''parse is the base function for parsing the recipe, and extracting + elements into the correct data structures. Everything is parsed into + lists or dictionaries that can be assembled again on demand. + + Singularity: we parse files/labels first, then install. + cd first in a line is parsed as WORKDIR + + ''' + # If the recipe isn't loaded, load it + if not hasattr(self, 'config'): + self.load_recipe() - self.name = 'singularity' - self.filename = "Singularity" - super(SingularityRecipe, self).__init__(recipe) + # Parse each section + for section, lines in self.config.items(): + bot.debug(section) + + # Get the correct parsing function + parser = self._get_mapping(section) + + # Parse it, if appropriate + if parser: + parser(lines) + + return self.recipe # Setup for each Parser @@ -68,8 +97,8 @@ def _from(self, line): line: the line from the recipe file to parse for FROM ''' - self.fromHeader = line - bot.debug('FROM %s' %self.fromHeader) + self.recipe.fromHeader = line + bot.debug('FROM %s' % self.recipe.fromHeader) # Run and Test Parser @@ -85,7 +114,7 @@ def _test(self, lines): ''' self._write_script('/tests.sh', lines) - self.test = "/bin/bash /tests.sh" + self.recipe.test = "/bin/bash /tests.sh" # Env Parser @@ -101,7 +130,7 @@ def _env(self, lines): ''' environ = [x for x in lines if not x.startswith('export')] - self.environ += environ + self.recipe.environ += environ # Files for container @@ -115,7 +144,7 @@ def _files(self, lines): lines: pairs of files, one pair per line ''' - self.files += lines + self.recipe.files += lines # Comments and Help @@ -131,7 +160,7 @@ def _comments(self, lines): ''' for line in lines: comment = self._comment(line) - self.comments.append(comment) + self.recipe.comments.append(comment) def _comment(self, line): @@ -170,7 +199,7 @@ def _run(self, lines): self._write_script('/entrypoint.sh', lines) runscript = "/bin/bash /entrypoint.sh" - self.cmd = runscript + self.recipe.cmd = runscript # Labels @@ -183,7 +212,7 @@ def _labels(self, lines): lines: the lines from the recipe with key,value pairs ''' - self.labels += lines + self.recipe.labels += lines def _post(self, lines): @@ -194,7 +223,7 @@ def _post(self, lines): lines: the lines from the recipe with install commands ''' - self.install += lines + self.recipe.install += lines # Main Parsing Functions @@ -234,32 +263,6 @@ def _get_mapping(self, section): return self._comments - def _parse(self): - '''parse is the base function for parsing the recipe, and extracting - elements into the correct data structures. Everything is parsed into - lists or dictionaries that can be assembled again on demand. - - Singularity: we parse files/labels first, then install. - cd first in a line is parsed as WORKDIR - - ''' - # If the recipe isn't loaded, load it - if not hasattr(self, 'config'): - self.load_recipe() - - # Parse each section - for section, lines in self.config.items(): - bot.debug(section) - - # Get the correct parsing function - parser = self._get_mapping(section) - - # Parse it, if appropriate - if parser: - parser(lines) - - - # Loading Functions def _load_from(self, line): @@ -268,7 +271,7 @@ def _load_from(self, line): # Remove any comments line = line.split('#',1)[0] line = re.sub('(F|f)(R|r)(O|o)(M|m):','', line).strip() - bot.info('FROM %s' %line) + bot.info('FROM %s' % line) self.config['from'] = line @@ -277,8 +280,7 @@ def _load_bootstrap(self, line): exit on fail (there is no other option to convert to Dockerfile! ''' if 'docker' not in line.lower(): - bot.error('docker not detected as Bootstrap!') - sys.exit(1) + bot.exit('docker not detected as Bootstrap!') def _load_section(self, lines, section): @@ -385,3 +387,4 @@ def _add_section(self, line, section=None): bot.debug("Adding section %s" %section) return section + diff --git a/spython/main/parse/recipe.py b/spython/main/parse/recipe.py index f51778bb..2cf40438 100644 --- a/spython/main/parse/recipe.py +++ b/spython/main/parse/recipe.py @@ -5,26 +5,20 @@ # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -import tempfile import os import sys from spython.logger import bot from spython.utils import ( read_file, write_file ) -from spython.main.parse.converters import ( - create_runscript, - create_section, - docker2singularity, - singularity2docker -) class Recipe(object): '''a recipe includes an environment, labels, runscript or command, - and install sequence. This object is subclassed by a Singularity or - Docker recipe, and can be used to convert between the two. The user - can read in one recipe type to convert to another, or provide raw - commands and metadata for generating a recipe. + and install sequence. This object is interacted with by a Parser + (intended to popualte the recipe with content) and a Writer (intended + to write a recipe to file). The parsers and writers are located in + parsers.py, and writers.py, respectively. The user is also free to use + the recipe class to build recipes. Parameters ========== @@ -34,76 +28,6 @@ class Recipe(object): ''' def __init__(self, recipe=None): - self.load(recipe) - - def load(self, recipe): - '''load a recipe file into the client, first performing checks, and - then parsing the file. - - Parameters - ========== - recipe: the original recipe file, parsed by the subclass either - DockerRecipe or SingularityRecipe - - ''' - self.recipe = recipe # the recipe file - self._run_checks() # does the recipe file exist? - self.parse() - - def __str__(self): - ''' show the user the recipe object, along with the type. E.g., - - [spython-recipe][docker] - [spython-recipe][singularity] - - ''' - - base = "[spython-recipe]" - if self.recipe: - base = "%s[%s]" %(base, self.recipe) - return base - - def __repr__(self): - return self.__str__() - - - def _run_checks(self): - '''basic sanity checks for the file name (and others if needed) before - attempting parsing. - ''' - if self.recipe is not None: - - # Does the recipe provided exist? - if not os.path.exists(self.recipe): - bot.error("Cannot find %s, is the path correct?" %self.recipe) - sys.exit(1) - - # Ensure we carry fullpath - self.recipe = os.path.abspath(self.recipe) - - -# Parse - - def parse(self): - '''parse is the base function for parsing the recipe, whether it be - a Dockerfile or Singularity recipe. The recipe is read in as lines, - and saved to a list if needed for the future. If the client has - it, the recipe type specific _parse function is called. - - Instructions for making a client subparser: - - It should have a main function _parse that parses a list of lines - from some recipe text file into the appropriate sections, e.g., - - self.fromHeader - self.environ - self.labels - self.install - self.files - self.test - self.entrypoint - - ''' self.cmd = None self.comments = [] @@ -117,193 +41,19 @@ def parse(self): self.volumes = [] self.workdir = None - if self.recipe: - - # Read in the raw lines of the file - self.lines = read_file(self.recipe) - - # If properly instantiated by Docker or Singularity Recipe, parse - if hasattr(self, '_parse'): - self._parse() - - -# Convert and Save - - - def save(self, output_file=None, - convert_to=None, - runscript="/bin/bash", - force=False): - - '''save will convert a recipe to a specified format (defaults to the - opposite of the recipe type originally loaded, (e.g., docker--> - singularity and singularity-->docker) and write to an output file, - if specified. If not specified, a temporary file is used. - - Parameters - ========== - output_file: the file to save to, not required (estimates default) - convert_to: can be manually forced (docker or singularity) - runscript: default runscript (entrypoint) to use - force: if True, override discovery from Dockerfile - - ''' - - converted = self.convert(convert_to, runscript, force) - if output_file is None: - output_file = self._get_conversion_outfile(convert_to=None) - bot.info('Saving to %s' %output_file) - write_file(output_file, converted) - - - def convert(self, convert_to=None, - runscript="/bin/bash", - force=False): - - '''This is a convenience function for the user to easily call to get - the most likely desired result, conversion to the opposite format. - We choose the selection based on the recipe name - meaning that we - perform conversion with default based on recipe name. If the recipe - object is DockerRecipe, we convert to Singularity. If the recipe - object is SingularityRecipe, we convert to Docker. The user can - override this by setting the variable convert_to - - Parameters - ========== - convert_to: can be manually forced (docker or singularity) - runscript: default runscript (entrypoint) to use - force: if True, override discovery from Dockerfile - - ''' - converter = self._get_converter(convert_to) - return converter(runscript=runscript, force=force) - + self.source = recipe - -# Internal Helpers - - - def _get_converter(self, convert_to=None): - '''see convert and save. This is a helper function that returns - the proper conversion function, but doesn't call it. We do this - so that in the case of convert, we do the conversion and return - a string. In the case of save, we save the recipe to file for the - user. - - Parameters - ========== - convert_to: a string either docker or singularity, if a different - - Returns - ======= - converter: the function to do the conversion - - ''' - conversion = self._get_conversion_type(convert_to) - - # Perform conversion - if conversion == "singularity": - return self.docker2singularity - return self.singularity2docker - - - - def _get_conversion_outfile(self, convert_to=None): - '''a helper function to return a conversion temporary output file - based on kind of conversion - - Parameters - ========== - convert_to: a string either docker or singularity, if a different - - ''' - conversion = self._get_conversion_type(convert_to) - prefix = "Singularity" - if conversion == "docker": - prefix = "Dockerfile" - suffix = next(tempfile._get_candidate_names()) - return "%s.%s" %(prefix, suffix) - - - - def _get_conversion_type(self, convert_to=None): - '''a helper function to return the conversion type based on user - preference and input recipe. - - Parameters - ========== - convert_to: a string either docker or singularity (default None) - - ''' - acceptable = ['singularity', 'docker'] - - # Default is to convert to opposite kind - conversion = "singularity" - if self.name == "singularity": - conversion = "docker" - - # Unless the user asks for a specific type - if convert_to is not None and convert_to in acceptable: - conversion = convert_to - return conversion - - - - def _split_line(self, line): - '''clean a line to prepare it for parsing, meaning separation - of commands. We remove newlines (from ends) along with extra spaces. - - Parameters - ========== - line: the string to parse into parts - - Returns - ======= - parts: a list of line pieces, the command is likely first - - ''' - return [x.strip() for x in line.split(' ', 1)] - - - def _clean_line(self, line): - '''clean line will remove comments, and strip the line of newlines - or spaces. - - Parameters - ========== - line: the string to parse into parts - - Returns - ======= - line: a cleaned line - - ''' - # A line that is None should return empty string - line = line or '' - return line.split('#')[0].strip() - - - def _write_script(self, path, lines, chmod=True): - '''write a script with some lines content to path in the image. This - is done by way of adding echo statements to the install section. - - Parameters - ========== - path: the path to the file to write - lines: the lines to echo to the file - chmod: If true, change permission to make u+x + def __str__(self): + ''' show the user the recipe object, along with the type. E.g., + + [spython-recipe][source:Singularity] + [spython-recipe][source:Dockerfile] ''' - if len(lines) > 0: - lastline = lines.pop() - for line in lines: - self.install.append('echo "%s" >> %s' %line %path) - self.install.append(lastline) - - if chmod is True: - self.install.append('chmod u+x %s' %path) + base = "[spython-recipe]" + if self.source: + base = "%s[source:%s]" %(base, self.source) + return base -Recipe.docker2singularity = docker2singularity -Recipe.singularity2docker = singularity2docker -Recipe._create_section = create_section -Recipe._create_runscript = create_runscript + def __repr__(self): + return self.__str__() diff --git a/spython/version.py b/spython/version.py index 081ea332..f28cfde2 100644 --- a/spython/version.py +++ b/spython/version.py @@ -6,7 +6,7 @@ # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -__version__ = "0.0.62" +__version__ = "0.0.63" AUTHOR = 'Vanessa Sochat' AUTHOR_EMAIL = 'vsochat@stanford.edu' NAME = 'spython' From 39bdc626570c77456f910928f1af095a5cdbc722 Mon Sep 17 00:00:00 2001 From: Vanessa Sochat Date: Wed, 29 May 2019 08:17:58 -0400 Subject: [PATCH 02/21] adding writers Signed-off-by: Vanessa Sochat --- spython/client/recipe.py | 34 ++-- spython/client/shell.py | 45 ++--- spython/main/parse/base.py | 59 ------- spython/main/parse/parsers/base.py | 4 +- spython/main/parse/parsers/docker.py | 1 - spython/main/parse/parsers/singularity.py | 2 - spython/main/parse/recipe.py | 8 +- spython/main/parse/writers/__init__.py | 10 ++ spython/main/parse/writers/base.py | 67 ++++++++ spython/main/parse/writers/docker.py | 139 +++++++++++++++ spython/main/parse/writers/singularity.py | 200 ++++++++++++++++++++++ spython/tests/test_oci.py | 30 +++- 12 files changed, 478 insertions(+), 121 deletions(-) delete mode 100644 spython/main/parse/base.py create mode 100644 spython/main/parse/writers/__init__.py create mode 100644 spython/main/parse/writers/base.py create mode 100644 spython/main/parse/writers/docker.py create mode 100644 spython/main/parse/writers/singularity.py diff --git a/spython/client/recipe.py b/spython/client/recipe.py index 736cc872..60c69ef8 100644 --- a/spython/client/recipe.py +++ b/spython/client/recipe.py @@ -6,15 +6,18 @@ # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +from spython.utils import write_file import sys def main(args, options, parser): - '''This function serves as a wrapper around the DockerRecipe and - SingularityRecipe converters. We can either save to file if - args.outfile is defined, or print to the console if not. + '''This function serves as a wrapper around the DockerParser, + SingularityParser, DockerWriter, and SingularityParser converters. + We can either save to file if args.outfile is defined, or print + to the console if not. ''' - from spython.main.parse import ( DockerRecipe, SingularityRecipe ) + from spython.main.parse.parsers import ( DockerParser, SingularityParser ) + from spython.main.parse.writers import ( DockerWriter, SingularityWriter ) # We need something to work with if not args.files: @@ -27,17 +30,21 @@ def main(args, options, parser): outfile = args.files[1] # Choose the recipe parser - parser = SingularityRecipe + parser = SingularityParser + writer = SingularityWriter if args.input == "docker": - parser = DockerRecipe + parser = DockerParser + writer = DockerWriter elif args.input == "singularity": - parser = SingularityRecipe(args.files[0]) + parser = SingularityParser + writer = SingularityParser else: if "dockerfile" in args.files[0].lower(): - parser = DockerRecipe + parser = DockerParser + writer = DockerWriter # Initialize the chosen parser - parser = parser(args.files[0]) + recipe = parser(args.files[0]) # By default, discover entrypoint / cmd from Dockerfile entrypoint = "/bin/bash" @@ -47,11 +54,14 @@ def main(args, options, parser): entrypoint = args.entrypoint force = True + # Do the conversion + recipeWriter = writer(recipe) + result = recipeWriter.convert(runscript=entrypoint, force=force) + # If the user specifies an output file, save to it if outfile is not None: - parser.save(outfile, runscript=entrypoint, force=force) + write_file(outfile, result) # Otherwise, convert and print to screen else: - recipe = parser.convert(runscript=entrypoint, force=force) - print(recipe) + print(result) diff --git a/spython/client/shell.py b/spython/client/shell.py index b37a088f..c5d0794d 100644 --- a/spython/client/shell.py +++ b/spython/client/shell.py @@ -25,50 +25,39 @@ def main(args, options, parser): except ImportError: pass - -def ipython(image): - '''give the user an ipython shell +def prepare_client(image): + '''prepare a client to embed in a shell with recipe parsers and writers. ''' - # The client will announce itself (backend/database) unless it's get from spython.main import get_client - from spython.main.parse import ( DockerRecipe, SingularityRecipe ) + from spython.main.parse import parsers + from spython.main.parse import writers client = get_client() client.load(image) # Add recipe parsers - client.DockerRecipe = DockerRecipe - client.SingularityRecipe = SingularityRecipe + client.parsers = parsers + client.writers = writers + return client +def ipython(image): + '''give the user an ipython shell + ''' + client = prepare_client(image) # pylint: disable=unused-variable from IPython import embed embed() def bpython(image): - + '''give the user a bpython shell + ''' import bpython - from spython.main import get_client - from spython.main.parse import ( DockerRecipe, SingularityRecipe ) - - client = get_client() - client.load(image) - - # Add recipe parsers - client.DockerRecipe = DockerRecipe - client.SingularityRecipe = SingularityRecipe - + client = prepare_client(image) bpython.embed(locals_={'client': client}) def python(image): + '''give the user a python shell + ''' import code - from spython.main import get_client - from spython.main.parse import ( DockerRecipe, SingularityRecipe ) - - client = get_client() - client.load(image) - - # Add recipe parsers - client.DockerRecipe = DockerRecipe - client.SingularityRecipe = SingularityRecipe - + client = prepare_client(image) code.interact(local={"client":client}) diff --git a/spython/main/parse/base.py b/spython/main/parse/base.py deleted file mode 100644 index 2cf40438..00000000 --- a/spython/main/parse/base.py +++ /dev/null @@ -1,59 +0,0 @@ - -# Copyright (C) 2017-2019 Vanessa Sochat. - -# This Source Code Form is subject to the terms of the -# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed -# with this file, You can obtain one at http://mozilla.org/MPL/2.0/. - -import os -import sys - -from spython.logger import bot -from spython.utils import ( read_file, write_file ) - - -class Recipe(object): - '''a recipe includes an environment, labels, runscript or command, - and install sequence. This object is interacted with by a Parser - (intended to popualte the recipe with content) and a Writer (intended - to write a recipe to file). The parsers and writers are located in - parsers.py, and writers.py, respectively. The user is also free to use - the recipe class to build recipes. - - Parameters - ========== - recipe: the original recipe file, parsed by the subclass either - DockerRecipe or SingularityRecipe - - ''' - - def __init__(self, recipe=None): - - self.cmd = None - self.comments = [] - self.entrypoint = None - self.environ = [] - self.files = [] - self.install = [] - self.labels = [] - self.ports = [] - self.test = None - self.volumes = [] - self.workdir = None - - self.source = recipe - - def __str__(self): - ''' show the user the recipe object, along with the type. E.g., - - [spython-recipe][source:Singularity] - [spython-recipe][source:Dockerfile] - - ''' - base = "[spython-recipe]" - if self.source: - base = "%s[source:%s]" %(base, self.source) - return base - - def __repr__(self): - return self.__str__() diff --git a/spython/main/parse/parsers/base.py b/spython/main/parse/parsers/base.py index b7e90b2c..2f6dca4f 100644 --- a/spython/main/parse/parsers/base.py +++ b/spython/main/parse/parsers/base.py @@ -5,7 +5,6 @@ # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -import json import os from spython.logger import bot @@ -32,7 +31,7 @@ def __init__(self, filename=None, load=True): recipe: the recipe file to parse. ''' - self.filename = recipe + self.filename = filename self._run_checks() self.recipe = Recipe(self.filename) @@ -94,4 +93,3 @@ def _split_line(self, line): ''' return [x.strip() for x in line.split(' ', 1)] - diff --git a/spython/main/parse/parsers/docker.py b/spython/main/parse/parsers/docker.py index 1ffe7119..163a6e0e 100644 --- a/spython/main/parse/parsers/docker.py +++ b/spython/main/parse/parsers/docker.py @@ -555,4 +555,3 @@ def _write_script(self, path, lines, chmod=True): if chmod is True: self.recipe.install.append('chmod u+x %s' %path) - diff --git a/spython/main/parse/parsers/singularity.py b/spython/main/parse/parsers/singularity.py index cd1c0fc1..061727bc 100644 --- a/spython/main/parse/parsers/singularity.py +++ b/spython/main/parse/parsers/singularity.py @@ -7,7 +7,6 @@ import re -import sys from spython.logger import bot from .base import ParserBase @@ -387,4 +386,3 @@ def _add_section(self, line, section=None): bot.debug("Adding section %s" %section) return section - diff --git a/spython/main/parse/recipe.py b/spython/main/parse/recipe.py index 2cf40438..eb0a56fe 100644 --- a/spython/main/parse/recipe.py +++ b/spython/main/parse/recipe.py @@ -5,12 +5,6 @@ # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -import os -import sys - -from spython.logger import bot -from spython.utils import ( read_file, write_file ) - class Recipe(object): '''a recipe includes an environment, labels, runscript or command, @@ -23,7 +17,7 @@ class Recipe(object): Parameters ========== recipe: the original recipe file, parsed by the subclass either - DockerRecipe or SingularityRecipe + DockerParser or SingularityParser ''' diff --git a/spython/main/parse/writers/__init__.py b/spython/main/parse/writers/__init__.py new file mode 100644 index 00000000..5da9b378 --- /dev/null +++ b/spython/main/parse/writers/__init__.py @@ -0,0 +1,10 @@ + +# Copyright (C) 2017-2019 Vanessa Sochat. + +# This Source Code Form is subject to the terms of the +# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed +# with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +from .docker import DockerWriter +from .singularity import SingularityWriter diff --git a/spython/main/parse/writers/base.py b/spython/main/parse/writers/base.py new file mode 100644 index 00000000..b1f5e805 --- /dev/null +++ b/spython/main/parse/writers/base.py @@ -0,0 +1,67 @@ + +# Copyright (C) 2017-2019 Vanessa Sochat. + +# This Source Code Form is subject to the terms of the +# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed +# with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import os +import tempfile +from spython.logger import bot +from spython.utils import write_file + + +class WriterBase(object): + + def __init__(self, recipe=None): + '''a writer base will take a recipe object (parser.base.Recipe) and + provide helpers for writing to file. + + Parameters + ========== + recipe: the recipe instance to parse + + ''' + self.recipe = recipe + + + def write(self, output_file=None, force=False): + '''convert a recipe to a specified format, and write to file, meaning + we use the loaded recipe to write to an output file. + If the output file is not specified, a temporary file is used. + + Parameters + ========== + output_file: the file to save to, not required (estimates default) + force: if True, if file exists, over-write existing file + + ''' + if output_file is None: + output_file = self._get_conversion_outfile() + + # Cut out early if file exists and we aren't overwriting + if os.path.exists(output_file) and not force: + bot.exit('%s exists, and force is False.' % output_file) + + # Do the conversion if function is provided by subclass + if hasattr(self, 'convert'): + converted = self.convert() + bot.info('Saving to %s' % output_file) + write_file(output_file, converted) + + + def _get_conversion_outfile(self): + '''a helper function to return a conversion temporary output file + based on kind of conversion + + Parameters + ========== + convert_to: a string either docker or singularity, if a different + + ''' + prefix = 'spythonRecipe' + if hasattr(self, 'name'): + prefix = self.name + suffix = next(tempfile._get_candidate_names()) + return "%s.%s" %(prefix, suffix) diff --git a/spython/main/parse/writers/docker.py b/spython/main/parse/writers/docker.py new file mode 100644 index 00000000..d015ee78 --- /dev/null +++ b/spython/main/parse/writers/docker.py @@ -0,0 +1,139 @@ + +# Copyright (C) 2017-2019 Vanessa Sochat. + +# This Source Code Form is subject to the terms of the +# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed +# with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import re +from spython.logger import bot + +from .base import WriterBase + +# FROM Validation + +# Regular expressions to parse registry, collection, repo, tag and version +_docker_uri = re.compile( + "(?:(?P[^/@]+[.:][^/@]*)/)?" + "(?P(?:[^:@/]+/)+)?" + "(?P[^:@/]+)" + "(?::(?P[^:@]+))?" + "(?:@(?P.+))?" + "$") + +# Reduced to match registry:port/repo or registry.com/repo +_reduced_uri = re.compile( + "(?:(?P[^/@]+[.:][^/@]*)/)?" + "(?P[^:@/]+)" + "(?::(?P[^:@]+))?" + "(?:@(?P.+))?" + "$" + "(?P.)?") + +# Default +_default_uri = re.compile( + "(?:(?P[^/@]+)/)?" + "(?P(?:[^:@/]+/)+)" + "(?P[^:@/]+)" + "(?::(?P[^:@]+))?" + "(?:@(?P.+))?" + "$") + + +class DockerWriter(WriterBase): + + name = 'docker' + + def __init__(self, recipe="Dockerfile", load=True): + '''a DockerWriter will take a Recipe as input, and write + to a Dockerfile. + + Parameters + ========== + recipe: the Recipe object to write to file. + + ''' + super(DockerWriter, self).__init__(recipe, load) + + + def validate(self): + '''validate that all (required) fields are included for the Docker + recipe. We minimimally just need a FROM image, and must ensure + it's in a valid format. If anything is missing, we exit with error. + ''' + if self.recipe is None: + bot.exit('Please provide a Recipe() to the writer first.') + + if self.recipe.fromHeader is None: + bot.exit("Dockerfile requires a fromHeader.") + + # Parse the provided name + uri_regexes = [ _reduced_uri, + _default_uri, + _docker_uri ] + + for r in uri_regexes: + match = r.match(self.recipe.fromHeader) + if match: + break + + if not match: + bot.exit('FROM header %s not valid.' % self.recipe.fromHeader) + + def convert(self, runscript="/bin/bash", force=False): + '''convert is called by the parent class to convert the recipe object + (at self.recipe) to the output file content to write to file. + ''' + self.validate() + + recipe = [ "FROM %s" % self.recipe.fromHeader ] + + # Comments go up front! + recipe += self.recipe.comments + + # First add files, labels, environment + recipe += write_lines('ADD', self.recipe.files) + recipe += write_lines('LABEL', self.recipe.labels) + recipe += write_lines('ENV', self.recipe.environ) + + # Install routine is added as RUN commands + recipe += write_lines('RUN', self.recipe.install) + + if self.recipe.workdir is not None: + recipe.append('WORKDIR %s' % self.recipe.workdir) + + # write the command, and entrypoint as is + if self.recipe.cmd is not None: + recipe.append('CMD %s' % self.recipe.cmd) + + if self.recipe.entrypoint is not None: + recipe.append('ENTRYPOINT %s' % self.recipe.entrypoint) + + if self.recipe.test is not None: + recipe.append(write_lines('HEALTHCHECK', self.recipe.test)) + + # Clean up extra white spaces + return '\n'.join(recipe).replace('\n\n','\n') + + +def write_lines(label, lines): + '''write a list of lines with a header for a section. + + Parameters + ========== + lines: one or more lines to write, with header appended + + ''' + result = [] + continued = False + for line in lines: + if continued: + result.append(line) + else: + result.append('%s %s' %(label, line)) + continued = False + if line.endswith('\\'): + continued = True + + return result diff --git a/spython/main/parse/writers/singularity.py b/spython/main/parse/writers/singularity.py new file mode 100644 index 00000000..6955b8db --- /dev/null +++ b/spython/main/parse/writers/singularity.py @@ -0,0 +1,200 @@ + +# Copyright (C) 2017-2019 Vanessa Sochat. + +# This Source Code Form is subject to the terms of the +# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed +# with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import re +from spython.logger import bot + +from .base import WriterBase + + +class SingularityWriter(WriterBase): + + name = 'singularity' + + def __init__(self, recipe="Singularity", load=True): + '''a SingularityWriter will take a Recipe as input, and write + to a Singularity recipe file. + + Parameters + ========== + recipe: the Recipe object to write to file. + + ''' + super(SingularityWriter, self).__init__(recipe, load) + + + def validate(self): + '''validate that all (required) fields are included for the Docker + recipe. We minimimally just need a FROM image, and must ensure + it's in a valid format. If anything is missing, we exit with error. + ''' + if self.recipe is None: + bot.exit('Please provide a Recipe() to the writer first.') + + if self.recipe.fromHeader is None: + bot.exit("Singularity recipe requires a from header.") + + + + def convert(self, runscript="/bin/bash", force=False): + '''docker2singularity will return a Singularity build recipe based on + a the loaded recipe object. It doesn't take any arguments as the + recipe object contains the sections, and the calling function + determines saving / output logic. + ''' + self.validate() + + recipe = ['Bootstrap: docker'] + recipe += [ "From: %s" % self.fromHeader ] + + # Sections with key value pairs + recipe += self._create_section('files') + recipe += self._create_section('labels') + recipe += self._create_section('install', 'post') + recipe += self._create_section('environ', 'environment') + + # Take preference for user, entrypoint, command, then default + runscript = self._create_runscript(runscript, force) + + # If a working directory was used, add it as a cd + if self.workdir is not None: + runscript = [self.workdir] + [runscript] + + # Finish the recipe, also add as startscript + recipe += finish_section(runscript, 'runscript') + recipe += finish_section(runscript, 'startscript') + + if self.test is not None: + recipe += finish_section(self.test, 'test') + + # Clean up extra white spaces + return '\n'.join(recipe).replace('\n\n','\n') + + + def create_runscript(self, default="/bin/bash", force=False): + '''create_entrypoint is intended to create a singularity runscript + based on a Docker entrypoint or command. We first use the Docker + ENTRYPOINT, if defined. If not, we use the CMD. If neither is found, + we use function default. + + Parameters + ========== + default: set a default entrypoint, if the container does not have + an entrypoint or cmd. + force: If true, use default and ignore Dockerfile settings + ''' + entrypoint = default + + # Only look at Docker if not enforcing default + if not force: + if self.entrypoint is not None: + entrypoint = ' '.join(self.entrypoint) + if self.cmd is not None: + entrypoint = entrypoint + ' ' + ' '.join(self.cmd) + entrypoint = default + entrypoint + + # Entrypoint should use exec + if not entrypoint.startswith('exec'): + entrypoint = "exec %s" % entrypoint + + # Should take input arguments into account + if not re.search('"?[$]@"?', entrypoint): + entrypoint = '%s "$@"' % entrypoint + return entrypoint + + + + def create_section(self, attribute, name=None): + '''create a section based on key, value recipe pairs, + This is used for files or label + + Parameters + ========== + attribute: the name of the data section, either labels or files + name: the name to write to the recipe file (e.g., %name). + if not defined, the attribute name is used. + + ''' + + # Default section name is the same as attribute + if name is None: + name = attribute + + # Put a space between sections + section = ['\n'] + + # Only continue if we have the section and it's not empty + try: + section = getattr(self, attribute) + except AttributeError: + bot.debug('Recipe does not have section for %s' % attribute) + return section + + # if the section is empty, don't print it + if len(section) == 0: + return section + + # Files or Labels + if attribute in ['labels', 'files']: + return create_keyval_section(section, name) + + # An environment section needs exports + if attribute in ['environ']: + return create_env_section(section, name) + + # Post, Setup + return finish_section(section, name) + + +def finish_section(section, name): + '''finish_section will add the header to a section, to finish the recipe + take a custom command or list and return a section. + + Parameters + ========== + section: the section content, without a header + name: the name of the section for the header + + ''' + if not isinstance(section, list): + section = [section] + + header = ['%' + name ] + return header + section + + +def create_keyval_section(pairs, name): + '''create a section based on key, value recipe pairs, + This is used for files or label + + Parameters + ========== + section: the list of values to return as a parsed list of lines + name: the name of the section to write (e.g., files) + + ''' + section = ['%' + name ] + for pair in pairs: + section.append(' '.join(pair).strip().strip('\\')) + return section + + +def create_env_section(pairs, name): + '''environment key value pairs need to be joined by an equal, and + exported at the end. + + Parameters + ========== + section: the list of values to return as a parsed list of lines + name: the name of the section to write (e.g., files) + + ''' + section = ['%' + name ] + for pair in pairs: + section.append("export %s" %pair) + return section diff --git a/spython/tests/test_oci.py b/spython/tests/test_oci.py index 355ab165..a4f4b5c9 100644 --- a/spython/tests/test_oci.py +++ b/spython/tests/test_oci.py @@ -37,7 +37,7 @@ def _build_sandbox(self): self.assertTrue(os.path.exists(image)) print('Copying OCI config.json to sandbox...') - shutil.copyfile(self.config, '%s/config.json' %image) + shutil.copyfile(self.config, '%s/config.json' % image) return image def test_oci_image(self): @@ -61,23 +61,27 @@ def test_oci(self): print(result) self.assertEqual(result['status'], 'created') - print('...Case 3. Execute command to running bundle.') + print('...Case 3. Execute command to non running bundle.') result = self.cli.oci.execute(container_id=self.name, sudo=True, command=['ls','/']) print(result) - self.assertTrue('bin' in result) - - print('...Case 4. Check status of existing bundle.') - state = self.cli.oci.state(self.name, sudo=True) - self.assertEqual(state['status'], 'created') + self.assertTrue(result['return_code'] == 255) - print('...Case 5. Start container return value 0.') + print('...Case 4. Start container return value 0.') state = self.cli.oci.start(self.name, sudo=True) self.assertEqual(state, 0) - print('...Case 6. Testing that state is now running.') + print('...Case 5. Execute command to running bundle.') + result = self.cli.oci.execute(container_id=self.name, + sudo=True, + command=['ls','/']) + + print(result) + self.assertTrue('bin' in result) + + print('...Case 6. Check status of existing bundle.') state = self.cli.oci.state(self.name, sudo=True) self.assertEqual(state['status'], 'running') @@ -85,10 +89,18 @@ def test_oci(self): state = self.cli.oci.pause(self.name, sudo=True) self.assertEqual(state, 0) + print('...check status of paused bundle.') + state = self.cli.oci.state(self.name, sudo=True) + self.assertEqual(state['status'], 'paused') + print('...Case 8. Resume paused container return value 0.') state = self.cli.oci.resume(self.name, sudo=True) self.assertEqual(state, 0) + print('...check status of resumed bundle.') + state = self.cli.oci.state(self.name, sudo=True) + self.assertEqual(state['status'], 'running') + print('...Case 9. Kill container.') state = self.cli.oci.kill(self.name, sudo=True) self.assertEqual(state, 0) From e0fc43279436ba922de3c494c8a6263b3de7d8a4 Mon Sep 17 00:00:00 2001 From: Vanessa Sochat Date: Wed, 29 May 2019 09:23:10 -0400 Subject: [PATCH 03/21] final tweaks to first round, ready to open WIP PR (need to figure out how to add test data Signed-off-by: Vanessa Sochat --- .circleci/config.yml | 3 + spython/client/recipe.py | 20 +- spython/main/parse/docker.py | 469 ++++++++++++++++++++++ spython/main/parse/parsers/base.py | 8 +- spython/main/parse/parsers/docker.py | 6 +- spython/main/parse/writers/base.py | 17 + spython/main/parse/writers/docker.py | 27 +- spython/main/parse/writers/singularity.py | 43 +- spython/tests/test_client.py | 4 +- spython/tests/test_parsers.py | 73 ++++ spython/tests/test_recipe.py | 35 ++ spython/tests/test_writers.py | 56 +++ spython/tests/testdata/Dockerfile | 86 ++++ spython/tests/testdata/Singularity | 18 + 14 files changed, 825 insertions(+), 40 deletions(-) create mode 100644 spython/main/parse/docker.py create mode 100644 spython/tests/test_parsers.py create mode 100644 spython/tests/test_recipe.py create mode 100644 spython/tests/test_writers.py create mode 100644 spython/tests/testdata/Dockerfile create mode 100644 spython/tests/testdata/Singularity diff --git a/.circleci/config.yml b/.circleci/config.yml index 72ae21e7..40692664 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -79,6 +79,9 @@ test_spython: &test_spython name: Test Singularity Python (Singularity Version 2 and 3) command: | cd ~/repo/spython + $HOME/conda/bin/python -m unittest tests.test_recipe + $HOME/conda/bin/python -m unittest tests.test_parsers + $HOME/conda/bin/python -m unittest tests.test_writers $HOME/conda/bin/python -m unittest tests.test_client $HOME/conda/bin/python -m unittest tests.test_utils $HOME/conda/bin/python -m unittest tests.test_instances diff --git a/spython/client/recipe.py b/spython/client/recipe.py index 60c69ef8..0475ed6a 100644 --- a/spython/client/recipe.py +++ b/spython/client/recipe.py @@ -16,8 +16,8 @@ def main(args, options, parser): to the console if not. ''' - from spython.main.parse.parsers import ( DockerParser, SingularityParser ) - from spython.main.parse.writers import ( DockerWriter, SingularityWriter ) + from spython.main.parse import parsers + from spython.main.parse import writers # We need something to work with if not args.files: @@ -30,18 +30,18 @@ def main(args, options, parser): outfile = args.files[1] # Choose the recipe parser - parser = SingularityParser - writer = SingularityWriter + parser = parsers.SingularityParser + writer = writers.SingularityWriter if args.input == "docker": - parser = DockerParser - writer = DockerWriter + parser = parsers.DockerParser + writer = writers.DockerWriter elif args.input == "singularity": - parser = SingularityParser - writer = SingularityParser + parser = parsers.SingularityParser + writer = writers.SingularityWriter else: if "dockerfile" in args.files[0].lower(): - parser = DockerParser - writer = DockerWriter + parser = parsers.DockerParser + writer = writers.DockerWriter # Initialize the chosen parser recipe = parser(args.files[0]) diff --git a/spython/main/parse/docker.py b/spython/main/parse/docker.py new file mode 100644 index 00000000..2eda8a62 --- /dev/null +++ b/spython/main/parse/docker.py @@ -0,0 +1,469 @@ + +# Copyright (C) 2017-2019 Vanessa Sochat. + +# This Source Code Form is subject to the terms of the +# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed +# with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import json +import os +import re + +from .environment import parse_env +from .recipe import Recipe +from spython.logger import bot + +class DockerRecipe(Recipe): + + def __init__(self, recipe=None): + '''a Docker recipe parses a Docker Recipe into the expected fields of + labels, environment, and install/runtime commands. We save working + directory as we parse, and the last one can be added to the runscript + of a Singularity recipe. + + Parameters + ========== + recipe: the recipe file (Dockerfile) to parse + + ''' + self.name = "docker" + super(DockerRecipe, self).__init__(recipe) + + +# Setup for each Parser + + def _setup(self, action, line): + ''' replace the command name from the group, alert the user of content, + and clean up empty spaces + ''' + bot.debug('[in] %s' % line) + + # Replace ACTION at beginning + line = re.sub('^%s' % action, '', line) + + # Handle continuation lines without ACTION by padding with leading space + line = " " + line + + # Split into components + return [x for x in self._split_line(line) if x not in ['', None]] + + +# From Parser + + def _from(self, line): + ''' get the FROM container image name from a FROM line! + + Parameters + ========== + line: the line from the recipe file to parse for FROM + + ''' + fromHeader = self._setup('FROM', line) + + # Singularity does not support AS level + self.fromHeader = re.sub("AS .+", "", fromHeader[0], flags=re.I) + + if "scratch" in self.fromHeader: + bot.warning('scratch is no longer available on Docker Hub.') + bot.debug('FROM %s' %self.fromHeader) + + +# Run and Test Parser + + def _run(self, line): + ''' everything from RUN goes into the install list + + Parameters + ========== + line: the line from the recipe file to parse for FROM + + ''' + line = self._setup('RUN', line) + self.install += line + + + def _test(self, line): + ''' A healthcheck is generally a test command + + Parameters + ========== + line: the line from the recipe file to parse for FROM + + ''' + self.test = self._setup('HEALTHCHECK', line) + + +# Arg Parser + + def _arg(self, line): + '''singularity doesn't have support for ARG, so instead will issue + a warning to the console for the user to export the variable + with SINGULARITY prefixed at build. + + Parameters + ========== + line: the line from the recipe file to parse for ARG + + ''' + line = self._setup('ARG', line) + bot.warning("ARG is not supported for Singularity! To get %s" %line[0]) + bot.warning("in the container, on host export SINGULARITY_%s" %line[0]) + +# Env Parser + + def _env(self, line): + '''env will parse a line that beings with ENV, indicative of one or + more environment variables. + + Parameters + ========== + line: the line from the recipe file to parse for ADD + + ''' + line = self._setup('ENV', line) + + # Extract environment (list) from the line + environ = parse_env(line) + + # Add to global environment, run during install + self.install += environ + + # Also define for global environment + self.environ += environ + + +# Add and Copy Parser + + + def _copy(self, lines): + '''parse_add will copy multiple files from one location to another. + This likely will need tweaking, as the files might need to be + mounted from some location before adding to the image. + The add command is done for an entire directory. It is also + possible to have more than one file copied to a destination: + https://docs.docker.com/engine/reference/builder/#copy + e.g.: / + ''' + lines = self._setup('COPY', lines) + + for line in lines: + values = line.split(" ") + topath = values.pop() + for frompath in values: + self._add_files(frompath, topath) + + + def _add(self, lines): + '''Add can also handle https, and compressed files. + + Parameters + ========== + line: the line from the recipe file to parse for ADD + + ''' + lines = self._setup('ADD', lines) + + for line in lines: + values = line.split(" ") + frompath = values.pop(0) + + # Custom parsing for frompath + + # If it's a web address, add to install routine to get + if frompath.startswith('http'): + for topath in values: + self._parse_http(frompath, topath) + + # Add the file, and decompress in install + elif re.search("[.](gz|gzip|bz2|xz)$", frompath.strip()): + for topath in values: + self._parse_archive(frompath, topath) + + # Just add the files + else: + for topath in values: + self._add_files(frompath, topath) + + +# File Handling + + def _add_files(self, source, dest): + '''add files is the underlying function called to add files to the + list, whether originally called from the functions to parse archives, + or https. We make sure that any local references are changed to + actual file locations before adding to the files list. + + Parameters + ========== + source: the source + dest: the destiation + ''' + + def expandPath(path): + return os.getcwd() if path == "." else path + + # Warn the user Singularity doesn't support expansion + if '*' in source: + bot.warning("Singularity doesn't support expansion, * found in %s" % source) + + # Warning if file/folder (src) doesn't exist + if not os.path.exists(source): + bot.warning("%s doesn't exist, ensure exists for build" % source) + + # The pair is added to the files as a list + self.files.append([expandPath(source), expandPath(dest)]) + + + def _parse_http(self, url, dest): + '''will get the filename of an http address, and return a statement + to download it to some location + + Parameters + ========== + url: the source url to retrieve with curl + dest: the destination folder to put it in the image + + ''' + file_name = os.path.basename(url) + download_path = "%s/%s" %(dest, file_name) + command = "curl %s -o %s" %(url, download_path) + self.install.append(command) + + + def _parse_archive(self, targz, dest): + '''parse_targz will add a line to the install script to extract a + targz to a location, and also add it to the files. + + Parameters + ========== + targz: the targz to extract + dest: the location to extract it to + + ''' + + # Add command to extract it + self.install.append("tar -zvf %s %s" %(targz, dest)) + + # Ensure added to container files + return self._add_files(targz, dest) + + +# Comments and Default + + def _comment(self, line): + '''Simply add the line to the install as a comment. This function is + equivalent to default, but added in the case we need future custom + parsing (meaning a comment is different from a line. + + Parameters + ========== + line: the line from the recipe file to parse to INSTALL + + ''' + self.install.append(line) + + + def _default(self, line): + '''the default action assumes a line that is either a command (a + continuation of a previous, for example) or a comment. + + Parameters + ========== + line: the line from the recipe file to parse to INSTALL + ''' + if line.strip().startswith('#'): + return self._comment(line) + self.install.append(line) + + +# Ports and Volumes + + def _volume(self, line): + '''We don't have logic for volume for Singularity, so we add as + a comment in the install, and a metadata value for the recipe + object + + Parameters + ========== + line: the line from the recipe file to parse to INSTALL + + ''' + volumes = self._setup('VOLUME', line) + if len(volumes) > 0: + self.volumes += volumes + return self._comment("# %s" %line) + + + def _expose(self, line): + '''Again, just add to metadata, and comment in install. + + Parameters + ========== + line: the line from the recipe file to parse to INSTALL + + ''' + ports = self._setup('EXPOSE', line) + if len(ports) > 0: + self.ports += ports + return self._comment("# %s" %line) + + +# Working Directory + + def _workdir(self, line): + '''A Docker WORKDIR command simply implies to cd to that location + + Parameters + ========== + line: the line from the recipe file to parse for WORKDIR + + ''' + # Save the last working directory to add to the runscript + workdir = self._setup('WORKDIR', line) + self.workdir = "cd %s" %(''.join(workdir)) + self.install.append(''.join(self.workdir)) + + +# Entrypoint and Command + + def _cmd(self, line): + '''_cmd will parse a Dockerfile CMD command + + eg: CMD /code/run_uwsgi.sh --> /code/run_uwsgi.sh. + If a list is provided, it's parsed to a list. + + Parameters + ========== + line: the line from the recipe file to parse for CMD + + ''' + cmd = self._setup('CMD', line)[0] + self.cmd = self._load_list(cmd) + + + def _load_list(self, line): + '''load an entrypoint or command, meaning it can be wrapped in a list + or a regular string. We try loading as json to return an actual + list. E.g., after _setup, we might go from 'ENTRYPOINT ["one", "two"]' + to '["one", "two"]', and this function loads as json and returns + ["one", "two"] + ''' + try: + line = json.loads(line) + except json.JSONDecodeError: + pass + return line + + + def _entry(self, line): + '''_entrypoint will parse a Dockerfile ENTRYPOINT command + + Parameters + ========== + line: the line from the recipe file to parse for CMD + + ''' + entrypoint = self._setup('ENTRYPOINT', line)[0] + self.entrypoint = self._load_list(entrypoint) + + +# Labels + + def _label(self, line): + '''_label will parse a Dockerfile label + + Parameters + ========== + line: the line from the recipe file to parse for CMD + + ''' + label = self._setup('LABEL', line) + self.labels += [ label ] + + +# Main Parsing Functions + + + def _get_mapping(self, line, parser=None, previous=None): + '''mapping will take the command from a Dockerfile and return a map + function to add it to the appropriate place. Any lines that don't + cleanly map are assumed to be comments. + + Parameters + ========== + line: the list that has been parsed into parts with _split_line + parser: the previously used parser, for context + + Returns + ======= + function: to map a line to its command group + + ''' + + # Split the command into cleanly the command and rest + if not isinstance(line, list): + line = self._split_line(line) + + # No line we will give function to handle empty line + if len(line) == 0: + return None + + cmd = line[0].upper() + + mapping = {"ADD": self._add, + "ARG": self._arg, + "COPY": self._copy, + "CMD": self._cmd, + "ENTRYPOINT": self._entry, + "ENV": self._env, + "EXPOSE": self._expose, + "FROM": self._from, + "HEALTHCHECK": self._test, + "RUN": self._run, + "WORKDIR": self._workdir, + "MAINTAINER": self._label, + "VOLUME": self._volume, + "LABEL": self._label} + + # If it's a command line, return correct function + if cmd in mapping: + return mapping[cmd] + + # If it's a continued line, return previous + cleaned = self._clean_line(line[-1]) + previous = self._clean_line(previous) + + # if we are continuing from last + if cleaned.endswith('\\') and parser or previous.endswith('\\'): + return parser + + return self._default + + + def _parse(self): + '''parse is the base function for parsing the Dockerfile, and extracting + elements into the correct data structures. Everything is parsed into + lists or dictionaries that can be assembled again on demand. + + Environment: Since Docker also exports environment as we go, + we add environment to the environment section and + install + + Labels: include anything that is a LABEL, ARG, or (deprecated) + maintainer. + + Add/Copy: are treated the same + + ''' + parser = None + previous = None + + for line in self.lines: + + parser = self._get_mapping(line, parser, previous) + + # Parse it, if appropriate + if parser: + parser(line) + + previous = line diff --git a/spython/main/parse/parsers/base.py b/spython/main/parse/parsers/base.py index 2f6dca4f..ce6f7799 100644 --- a/spython/main/parse/parsers/base.py +++ b/spython/main/parse/parsers/base.py @@ -49,14 +49,14 @@ def _run_checks(self): '''basic sanity checks for the file name (and others if needed) before attempting parsing. ''' - if self.recipe is not None: + if self.filename is not None: # Does the recipe provided exist? - if not os.path.exists(self.recipe): - bot.exit("Cannot find %s, is the path correct?" % self.recipe) + if not os.path.exists(self.filename): + bot.exit("Cannot find %s, is the path correct?" % self.filename) # Ensure we carry fullpath - self.recipe = os.path.abspath(self.recipe) + self.filename = os.path.abspath(self.filename) # Printing diff --git a/spython/main/parse/parsers/docker.py b/spython/main/parse/parsers/docker.py index 163a6e0e..e8f30c6f 100644 --- a/spython/main/parse/parsers/docker.py +++ b/spython/main/parse/parsers/docker.py @@ -97,7 +97,7 @@ def _from(self, line): if "scratch" in self.recipe.fromHeader: bot.warning('scratch is no longer available on Docker Hub.') - bot.debug('FROM %s' %self.fromHeader) + bot.debug('FROM %s' % self.recipe.fromHeader) # Run and Test Parser @@ -398,8 +398,8 @@ def _workdir(self, line): ''' # Save the last working directory to add to the runscript workdir = self._setup('WORKDIR', line) - workdir = "cd %s" %(''.join(workdir)) - self.recipe.install.append(workdir) + workdir_cd = "cd %s" %(''.join(workdir)) + self.recipe.install.append(workdir_cd) self.recipe.workdir = workdir # Entrypoint and Command diff --git a/spython/main/parse/writers/base.py b/spython/main/parse/writers/base.py index b1f5e805..bf5e06a8 100644 --- a/spython/main/parse/writers/base.py +++ b/spython/main/parse/writers/base.py @@ -65,3 +65,20 @@ def _get_conversion_outfile(self): prefix = self.name suffix = next(tempfile._get_candidate_names()) return "%s.%s" %(prefix, suffix) + +# Printing + + def __str__(self): + ''' show the user the recipe object, along with the type. E.g., + + [spython-writer][docker] + [spython-writer][singularity] + + ''' + base = "[spython-writer]" + if hasattr(self, 'name'): + base = "%s[%s]" %(base, self.name) + return base + + def __repr__(self): + return self.__str__() diff --git a/spython/main/parse/writers/docker.py b/spython/main/parse/writers/docker.py index d015ee78..61f83e0e 100644 --- a/spython/main/parse/writers/docker.py +++ b/spython/main/parse/writers/docker.py @@ -45,7 +45,7 @@ class DockerWriter(WriterBase): name = 'docker' - def __init__(self, recipe="Dockerfile", load=True): + def __init__(self, recipe="Dockerfile"): '''a DockerWriter will take a Recipe as input, and write to a Dockerfile. @@ -54,7 +54,7 @@ def __init__(self, recipe="Dockerfile", load=True): recipe: the Recipe object to write to file. ''' - super(DockerWriter, self).__init__(recipe, load) + super(DockerWriter, self).__init__(recipe) def validate(self): @@ -93,7 +93,7 @@ def convert(self, runscript="/bin/bash", force=False): recipe += self.recipe.comments # First add files, labels, environment - recipe += write_lines('ADD', self.recipe.files) + recipe += write_files('ADD', self.recipe.files) recipe += write_lines('LABEL', self.recipe.labels) recipe += write_lines('ENV', self.recipe.environ) @@ -117,6 +117,19 @@ def convert(self, runscript="/bin/bash", force=False): return '\n'.join(recipe).replace('\n\n','\n') +def write_files(label, lines): + '''write a list of lines with a header for a section. + + Parameters + ========== + lines: one or more lines to write, with header appended + + ''' + result = [] + for line in lines: + result.append('%s %s %s' %(label, line[0], line[1])) + return result + def write_lines(label, lines): '''write a list of lines with a header for a section. @@ -128,10 +141,16 @@ def write_lines(label, lines): result = [] continued = False for line in lines: - if continued: + + # Skip comments and empty lines + if line.strip() == "" or line.strip().startswith('#'): + continue + + if continued or "USER" in line: result.append(line) else: result.append('%s %s' %(label, line)) + continued = False if line.endswith('\\'): continued = True diff --git a/spython/main/parse/writers/singularity.py b/spython/main/parse/writers/singularity.py index 6955b8db..792dc091 100644 --- a/spython/main/parse/writers/singularity.py +++ b/spython/main/parse/writers/singularity.py @@ -16,7 +16,7 @@ class SingularityWriter(WriterBase): name = 'singularity' - def __init__(self, recipe="Singularity", load=True): + def __init__(self, recipe="Singularity"): '''a SingularityWriter will take a Recipe as input, and write to a Singularity recipe file. @@ -25,7 +25,7 @@ def __init__(self, recipe="Singularity", load=True): recipe: the Recipe object to write to file. ''' - super(SingularityWriter, self).__init__(recipe, load) + super(SingularityWriter, self).__init__(recipe) def validate(self): @@ -50,7 +50,7 @@ def convert(self, runscript="/bin/bash", force=False): self.validate() recipe = ['Bootstrap: docker'] - recipe += [ "From: %s" % self.fromHeader ] + recipe += [ "From: %s" % self.recipe.fromHeader ] # Sections with key value pairs recipe += self._create_section('files') @@ -62,21 +62,21 @@ def convert(self, runscript="/bin/bash", force=False): runscript = self._create_runscript(runscript, force) # If a working directory was used, add it as a cd - if self.workdir is not None: - runscript = [self.workdir] + [runscript] + if self.recipe.workdir is not None: + runscript = [self.recipe.workdir] + [runscript] # Finish the recipe, also add as startscript recipe += finish_section(runscript, 'runscript') recipe += finish_section(runscript, 'startscript') - if self.test is not None: - recipe += finish_section(self.test, 'test') + if self.recipe.test is not None: + recipe += finish_section(self.recipe.test, 'test') # Clean up extra white spaces return '\n'.join(recipe).replace('\n\n','\n') - def create_runscript(self, default="/bin/bash", force=False): + def _create_runscript(self, default="/bin/bash", force=False): '''create_entrypoint is intended to create a singularity runscript based on a Docker entrypoint or command. We first use the Docker ENTRYPOINT, if defined. If not, we use the CMD. If neither is found, @@ -92,10 +92,19 @@ def create_runscript(self, default="/bin/bash", force=False): # Only look at Docker if not enforcing default if not force: - if self.entrypoint is not None: - entrypoint = ' '.join(self.entrypoint) - if self.cmd is not None: - entrypoint = entrypoint + ' ' + ' '.join(self.cmd) + if self.recipe.entrypoint is not None: + + # The provided entrypoint can be a string or a list + if isinstance(self.recipe.entrypoint, list): + entrypoint = ' '.join(self.recipe.entrypoint) + else: + entrypoint = ''.join(self.recipe.entrypoint) + + if self.recipe.cmd is not None: + if isinstance(self.recipe.cmd, list): + entrypoint = entrypoint + ' ' + ' '.join(self.recipe.cmd) + else: + entrypoint = entrypoint + ' ' + ''.join(self.recipe.cmd) entrypoint = default + entrypoint # Entrypoint should use exec @@ -108,8 +117,7 @@ def create_runscript(self, default="/bin/bash", force=False): return entrypoint - - def create_section(self, attribute, name=None): + def _create_section(self, attribute, name=None): '''create a section based on key, value recipe pairs, This is used for files or label @@ -130,7 +138,7 @@ def create_section(self, attribute, name=None): # Only continue if we have the section and it's not empty try: - section = getattr(self, attribute) + section = getattr(self.recipe, attribute) except AttributeError: bot.debug('Recipe does not have section for %s' % attribute) return section @@ -139,8 +147,8 @@ def create_section(self, attribute, name=None): if len(section) == 0: return section - # Files or Labels - if attribute in ['labels', 'files']: + # Files + if attribute in ['files']: return create_keyval_section(section, name) # An environment section needs exports @@ -180,6 +188,7 @@ def create_keyval_section(pairs, name): ''' section = ['%' + name ] for pair in pairs: + print(pair) section.append(' '.join(pair).strip().strip('\\')) return section diff --git a/spython/tests/test_client.py b/spython/tests/test_client.py index 63ceb4fa..f2520e84 100644 --- a/spython/tests/test_client.py +++ b/spython/tests/test_client.py @@ -24,8 +24,8 @@ def setUp(self): self.tmpdir = tempfile.mkdtemp() def tearDown(self): - #shutil.rmtree(self.tmpdir) - print('not') + shutil.rmtree(self.tmpdir) + def test_commands(self): print('Testing client.build command') diff --git a/spython/tests/test_parsers.py b/spython/tests/test_parsers.py new file mode 100644 index 00000000..9f79667a --- /dev/null +++ b/spython/tests/test_parsers.py @@ -0,0 +1,73 @@ +#!/usr/bin/python + +# Copyright (C) 2017-2019 Vanessa Sochat. + +# This Source Code Form is subject to the terms of the +# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed +# with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from spython.utils import get_installdir +from spython.main import Client +import unittest +import tempfile +import shutil +import os + + +print("########################################################### test_client") + +class TestClient(unittest.TestCase): + + def setUp(self): + self.pwd = get_installdir() + self.cli = Client + self.tmpdir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.tmpdir) + + def test_docker_parser(self): + + print('Testing spython.main.parse.parsers DockerParser') + from spython.main.parse.parsers import DockerParser + + dockerfile = os.path.join(self.pwd, 'tests', 'testdata', 'Dockerfile') + parser = DockerParser(dockerfile) + + self.assertEqual(str(parser), '[spython-parser][docker]') + + # Test all fields from recipe + self.assertEqual(parser.recipe.fromHeader, 'python:3.5.1') + self.assertEqual(parser.recipe.cmd, '/code/run_uwsgi.sh') + self.assertEqual(parser.recipe.entrypoint, None) + self.assertEqual(parser.recipe.workdir, '/code') + self.assertEqual(parser.recipe.volumes, []) + self.assertEqual(parser.recipe.ports, ['3031']) + self.assertEqual(parser.recipe.files[0], ['requirements.txt', '/tmp/requirements.txt']) + self.assertEqual(parser.recipe.environ, ['PYTHONUNBUFFERED=1']) + self.assertEqual(parser.recipe.source, dockerfile) + + def test_singularity_parser(self): + + print('Testing spython.main.parse.parsers SingularityParser') + from spython.main.parse.parsers import SingularityParser + + recipe = os.path.join(self.pwd, 'tests', 'testdata', 'Singularity') + parser = SingularityParser(recipe) + + self.assertEqual(str(parser), '[spython-parser][singularity]') + + # Test all fields from recipe + self.assertEqual(parser.recipe.fromHeader, 'continuumio/miniconda3') + self.assertEqual(parser.recipe.cmd, 'exec /opt/conda/bin/spython "$@"') + self.assertEqual(parser.recipe.entrypoint, None) + self.assertEqual(parser.recipe.workdir, None) + self.assertEqual(parser.recipe.volumes, []) + self.assertEqual(parser.recipe.ports, ['3031']) + self.assertEqual(parser.recipe.files, []) + self.assertEqual(parser.recipe.environ, []) + self.assertEqual(parser.recipe.source, recipe) + + +if __name__ == '__main__': + unittest.main() diff --git a/spython/tests/test_recipe.py b/spython/tests/test_recipe.py new file mode 100644 index 00000000..890f1271 --- /dev/null +++ b/spython/tests/test_recipe.py @@ -0,0 +1,35 @@ +#!/usr/bin/python + +# Copyright (C) 2019 Vanessa Sochat. + +# This Source Code Form is subject to the terms of the +# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed +# with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from spython.utils import get_installdir +import unittest + + +print("########################################################### test_recipe") + +class TestRecipe(unittest.TestCase): + + def setUp(self): + self.pwd = get_installdir() + + def test_recipe_base(self): + + print('Testing spython.main.parse.base Recipe') + from spython.main.parse.recipe import Recipe + recipe = Recipe() + self.assertEqual(str(recipe), '[spython-recipe]') + + attributes = ['cmd', 'comments', 'entrypoint', 'environ', 'files', + 'install', 'labels', 'ports', 'source', 'test', + 'volumes', 'workdir'] + + for att in attributes: + self.assertTrue(hasattr(recipe, att)) + +if __name__ == '__main__': + unittest.main() diff --git a/spython/tests/test_writers.py b/spython/tests/test_writers.py new file mode 100644 index 00000000..63f5ee8d --- /dev/null +++ b/spython/tests/test_writers.py @@ -0,0 +1,56 @@ +#!/usr/bin/python + +# Copyright (C) 2019 Vanessa Sochat. + +# This Source Code Form is subject to the terms of the +# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed +# with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from spython.utils import get_installdir +import unittest +import tempfile +import shutil +import os + + +print("########################################################## test_writers") + +class TestWriters(unittest.TestCase): + + def setUp(self): + self.pwd = get_installdir() + self.tmpdir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.tmpdir) + + def test_docker_writer(self): + + print('Testing spython.main.parse.writers DockerWriter') + from spython.main.parse.writers import DockerWriter + from spython.main.parse.parsers import DockerParser + + dockerfile = os.path.join(self.pwd, 'tests', 'testdata', 'Dockerfile') + parser = DockerParser(dockerfile) + writer = DockerWriter(parser.recipe) + + self.assertEqual(str(writer), '[spython-writer][docker]') + print(writer.convert()) + + + def test_singularity_writer(self): + + print('Testing spython.main.parse.writers SingularityWriter') + from spython.main.parse.writers import SingularityWriter + from spython.main.parse.parsers import SingularityParser + + recipe = os.path.join(self.pwd, 'tests', 'testdata', 'Singularity') + parser = SingularityParser(recipe) + writer = SingularityWriter(parser.recipe) + + self.assertEqual(str(writer), '[spython-writer][docker]') + print(writer.convert()) + + +if __name__ == '__main__': + unittest.main() diff --git a/spython/tests/testdata/Dockerfile b/spython/tests/testdata/Dockerfile new file mode 100644 index 00000000..83aa477e --- /dev/null +++ b/spython/tests/testdata/Dockerfile @@ -0,0 +1,86 @@ +FROM python:3.5.1 +ENV PYTHONUNBUFFERED 1 + +################################################################################ +# CORE +# Do not modify this section + +RUN apt-get update && apt-get install -y \ + pkg-config \ + cmake \ + openssl \ + wget \ + git \ + vim + +RUN apt-get update && apt-get install -y \ + anacron \ + autoconf \ + automake \ + libarchive-dev \ + libtool \ + libopenblas-dev \ + libglib2.0-dev \ + gfortran \ + libxml2-dev \ + libxmlsec1-dev \ + libhdf5-dev \ + libgeos-dev \ + libsasl2-dev \ + libldap2-dev \ + squashfs-tools \ + build-essential + +# Install Singularity +RUN git clone -b vault/release-2.5 https://www.github.com/sylabs/singularity.git +WORKDIR singularity +RUN ./autogen.sh && ./configure --prefix=/usr/local && make && make install + +# Install Python requirements out of /tmp so not triggered if other contents of /code change +ADD requirements.txt /tmp/requirements.txt +RUN pip install --upgrade pip +RUN pip install -r /tmp/requirements.txt + +ADD . /code/ + +################################################################################ +# PLUGINS +# You are free to comment out those plugins that you don't want to use + +# Install LDAP (uncomment if wanted) +# RUN pip install python3-ldap +# RUN pip install django-auth-ldap + +# Install Globus (uncomment if wanted) +# RUN /bin/bash /code/scripts/globus/globus-install.sh + +# Install SAML (uncomment if wanted) +# RUN pip install python3-saml +# RUN pip install social-auth-core[saml] + +################################################################################ +# BASE + +RUN mkdir -p /code && mkdir -p /code/images +RUN mkdir -p /var/www/images && chmod -R 0755 /code/images/ + +USER tacos + +WORKDIR /code +RUN apt-get remove -y gfortran + +RUN apt-get autoremove -y +RUN apt-get clean +RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +# Install crontab to setup job +RUN echo "0 0 * * * /usr/bin/python /code/manage.py generate_tree" >> /code/cronjob +RUN crontab /code/cronjob +RUN rm /code/cronjob + +# Create hashed temporary upload locations +RUN mkdir -p /var/www/images/_upload/{0..9} && chmod 777 -R /var/www/images/_upload + +CMD /code/run_uwsgi.sh + +EXPOSE 3031 diff --git a/spython/tests/testdata/Singularity b/spython/tests/testdata/Singularity new file mode 100644 index 00000000..e71cf0a3 --- /dev/null +++ b/spython/tests/testdata/Singularity @@ -0,0 +1,18 @@ +Bootstrap: docker +From: continuumio/miniconda3 + +%runscript + exec /opt/conda/bin/spython "$@" + +%labels + maintainer vsochat@stanford.edu + +%post + apt-get update && apt-get install -y git + +# Dependencies + cd /opt + git clone https://www.github.com/singularityhub/singularity-cli + cd singularity-cli + /opt/conda/bin/pip install setuptools + /opt/conda/bin/python setup.py install From a89a6808f7fd8e24bc7ef5137f28c1e571bcd2f1 Mon Sep 17 00:00:00 2001 From: Vanessa Sochat Date: Wed, 29 May 2019 11:21:16 -0400 Subject: [PATCH 04/21] more linting, and updates to docs Signed-off-by: Vanessa Sochat --- .circleci/config.yml | 4 +- .pylintrc | 2 - docs/api/_sources/changelog.md.txt | 9 + docs/api/changelog.html | 18 + docs/api/genindex.html | 52 +- docs/api/modules/index.html | 1 + docs/api/modules/spython/client.html | 22 +- docs/api/modules/spython/client/recipe.html | 34 +- docs/api/modules/spython/client/shell.html | 59 +-- docs/api/modules/spython/client/test.html | 6 - docs/api/modules/spython/image.html | 59 +-- docs/api/modules/spython/image/cmd.html | 4 +- .../api/modules/spython/image/cmd/create.html | 245 +++++++++ .../api/modules/spython/image/cmd/export.html | 5 +- .../modules/spython/image/cmd/importcmd.html | 3 - docs/api/modules/spython/image/cmd/utils.html | 7 +- docs/api/modules/spython/instance.html | 64 +-- docs/api/modules/spython/instance/cmd.html | 2 +- .../modules/spython/instance/cmd/iutils.html | 10 +- .../modules/spython/instance/cmd/start.html | 11 +- .../modules/spython/instance/cmd/stop.html | 3 +- docs/api/modules/spython/logger/message.html | 4 +- docs/api/modules/spython/logger/progress.html | 20 +- docs/api/modules/spython/logger/spinner.html | 5 +- docs/api/modules/spython/main.html | 13 +- docs/api/modules/spython/main/apps.html | 6 +- docs/api/modules/spython/main/base.html | 35 +- .../modules/spython/main/base/command.html | 8 +- docs/api/modules/spython/main/base/flags.html | 48 +- .../api/modules/spython/main/base/logger.html | 2 +- .../api/modules/spython/main/base/sutils.html | 15 +- docs/api/modules/spython/main/build.html | 10 +- docs/api/modules/spython/main/execute.html | 23 +- docs/api/modules/spython/main/export.html | 316 ++++++++++++ docs/api/modules/spython/main/help.html | 4 +- docs/api/modules/spython/main/inspect.html | 28 +- docs/api/modules/spython/main/instances.html | 10 +- .../spython/main/parse/converters.html | 23 +- .../modules/spython/main/parse/docker.html | 61 ++- .../modules/spython/main/parse/recipe.html | 295 +---------- .../spython/main/parse/singularity.html | 29 +- docs/api/modules/spython/main/pull.html | 62 +-- docs/api/modules/spython/main/run.html | 23 +- docs/api/modules/spython/oci.html | 12 +- .../modules/spython/tests/test_client.html | 19 +- .../modules/spython/tests/test_instances.html | 19 +- .../api/modules/spython/tests/test_utils.html | 46 +- docs/api/modules/spython/utils/fileio.html | 6 - docs/api/modules/spython/utils/terminal.html | 39 +- docs/api/objects.inv | Bin 2088 -> 2072 bytes docs/api/searchindex.js | 2 +- docs/api/source/spython.client.html | 25 +- docs/api/source/spython.image.html | 14 - docs/api/source/spython.instance.cmd.html | 2 +- docs/api/source/spython.main.base.html | 67 ++- docs/api/source/spython.main.html | 4 +- docs/api/source/spython.main.parse.html | 99 +--- docs/api/source/spython.tests.html | 10 + docs/api/source/spython.utils.html | 15 +- spython/client/__init__.py | 4 +- spython/client/shell.py | 13 +- spython/image/cmd/__init__.py | 4 +- spython/image/cmd/create.py | 9 +- spython/image/cmd/utils.py | 4 +- spython/instance/__init__.py | 2 +- spython/instance/cmd/__init__.py | 2 +- spython/instance/cmd/iutils.py | 2 +- spython/instance/cmd/start.py | 3 +- spython/instance/cmd/stop.py | 3 +- spython/logger/message.py | 2 +- spython/logger/progress.py | 16 +- spython/main/__init__.py | 4 +- spython/main/apps.py | 4 +- spython/main/base/__init__.py | 27 +- spython/main/base/logger.py | 2 +- spython/main/base/sutils.py | 2 +- spython/main/execute.py | 17 +- spython/main/help.py | 2 +- spython/main/inspect.py | 4 +- spython/main/instances.py | 6 +- spython/main/parse/docker.py | 469 ------------------ spython/main/parse/parsers/docker.py | 4 +- spython/main/parse/parsers/singularity.py | 10 +- spython/main/parse/writers/docker.py | 10 +- spython/main/parse/writers/singularity.py | 10 +- spython/main/run.py | 19 +- spython/oci/cmd/__init__.py | 8 +- spython/tests/test_client.py | 4 +- spython/tests/test_instances.py | 2 +- spython/tests/test_oci.py | 10 +- spython/tests/test_utils.py | 23 +- spython/utils/terminal.py | 16 +- 92 files changed, 1283 insertions(+), 1473 deletions(-) create mode 100644 docs/api/modules/spython/image/cmd/create.html create mode 100644 docs/api/modules/spython/main/export.html delete mode 100644 spython/main/parse/docker.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 40692664..d3da51b9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -107,7 +107,7 @@ jobs: - singularity/install-go: go-version: 1.11.5 - singularity/debian-install-3: - singularity-version: 3.1.0 + singularity-version: 3.2.1 - run: *install_spython - run: *run_linter - save_cache: @@ -130,7 +130,7 @@ jobs: - singularity/install-go: go-version: 1.11.5 - singularity/debian-install-3: - singularity-version: 3.1.0 + singularity-version: 3.2.1 - run: *install_spython - run: *run_linter - save_cache: diff --git a/.pylintrc b/.pylintrc index 68d70de2..a45b570f 100644 --- a/.pylintrc +++ b/.pylintrc @@ -62,12 +62,10 @@ confidence= # --disable=W". disable=attribute-defined-outside-init, bad-continuation, - bad-whitespace, bare-except, blacklisted-name, duplicate-code, fixme, - import-error, invalid-name, len-as-condition, line-too-long, diff --git a/docs/api/_sources/changelog.md.txt b/docs/api/_sources/changelog.md.txt index 3f2c6e4a..b5d79696 100644 --- a/docs/api/_sources/changelog.md.txt +++ b/docs/api/_sources/changelog.md.txt @@ -17,6 +17,15 @@ The client here will eventually be released as "spython" (and eventually to singularity on pypi), and the versions here will coincide with these releases. ## [master](https://github.com/singularityhub/singularity-cli/tree/master) + - refactor recipe parsers, writers, and base (0.0.63) + - fix crash in some error conditions (0.0.62) + - more OCI commands accept sudo parameter + - working directory, the last one defined, should be added to runscript (0.0.61) + - adding deprecation message for image.export (0.0.60) + - adding --force option to build + - fixing warning for files, only relevant for sources (0.0.59) + - deprecating pulling by commit or hash, not supported for Singularity (0.0.58) + - export command added back, points to build given Singularity 3.x+ - print but with logger, should be println (0.0.57) - Fixing bug with instance not having name when not started (0.0.56) - instance start has been moved to non-private diff --git a/docs/api/changelog.html b/docs/api/changelog.html index 07ff9594..fc79dfd3 100644 --- a/docs/api/changelog.html +++ b/docs/api/changelog.html @@ -171,6 +171,24 @@

master

@@ -478,8 +470,6 @@

O

P

    -
  • parse_verbosity() (in module spython.main.base.flags) +
  • prepare_client() (in module spython.client.shell)
  • println() (in module spython.main.base.logger) @@ -533,11 +525,7 @@

    R

  • Recipe (class in spython.main.parse.recipe)
  • remove_uri() (in module spython.utils.terminal) - -
  • RobotNamer (class in spython.main.base.generate)
      @@ -566,8 +554,6 @@

      R

      S

      - - - - - - - - - - - -
    • spython.main.build
    • spython.main.execute
    • +
    • spython.main.export
    • spython.main.help
    • spython.main.inspect
    • spython.main.instances
    • diff --git a/docs/api/modules/spython/client.html b/docs/api/modules/spython/client.html index 09bb1cea..f7c64236 100644 --- a/docs/api/modules/spython/client.html +++ b/docs/api/modules/spython/client.html @@ -167,11 +167,11 @@

      Source code for spython.client

                                       add_help=False)
       
           # Global Options
      -    parser.add_argument('--debug','-d', dest="debug", 
      +    parser.add_argument('--debug', '-d', dest="debug", 
                               help="use verbose logging to debug.", 
                               default=False, action='store_true')
       
      -    parser.add_argument('--quiet','-q', dest="quiet", 
      +    parser.add_argument('--quiet', '-q', dest="quiet", 
                               help="suppress all normal output", 
                               default=False, action='store_true')
       
      @@ -211,23 +211,6 @@ 

      Source code for spython.client

           return parser
      - -
      [docs]def get_subparsers(parser): - '''get_subparser will get a dictionary of subparsers, to help with printing help - ''' - - actions = [action for action in parser._actions - if isinstance(action, argparse._SubParsersAction)] - - subparsers = dict() - for action in actions: - # get all subparsers and print help - for choice, subparser in action.choices.items(): - subparsers[choice] = subparser - - return subparsers
      - -
      [docs]def set_verbosity(args): '''determine the message level in the environment to set based on args. ''' @@ -261,7 +244,6 @@

      Source code for spython.client

       
      [docs]def main(): parser = get_parser() - subparsers = get_subparsers(parser) def help(return_code=0): '''print help, including the software version and active client diff --git a/docs/api/modules/spython/client/recipe.html b/docs/api/modules/spython/client/recipe.html index e0f7336b..e09368de 100644 --- a/docs/api/modules/spython/client/recipe.html +++ b/docs/api/modules/spython/client/recipe.html @@ -156,15 +156,18 @@

      Source code for spython.client.recipe

       # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
       
       
      +from spython.utils import write_file
       import sys
       
       
      [docs]def main(args, options, parser): - '''This function serves as a wrapper around the DockerRecipe and - SingularityRecipe converters. We can either save to file if - args.outfile is defined, or print to the console if not. + '''This function serves as a wrapper around the DockerParser, + SingularityParser, DockerWriter, and SingularityParser converters. + We can either save to file if args.outfile is defined, or print + to the console if not. ''' - from spython.main.parse import ( DockerRecipe, SingularityRecipe ) + from spython.main.parse import parsers + from spython.main.parse import writers # We need something to work with if not args.files: @@ -177,17 +180,21 @@

      Source code for spython.client.recipe

               outfile = args.files[1]
       
           # Choose the recipe parser
      -    parser = SingularityRecipe
      +    parser = parsers.SingularityParser
      +    writer = writers.SingularityWriter
           if args.input == "docker":
      -        parser = DockerRecipe
      +        parser = parsers.DockerParser
      +        writer = writers.DockerWriter
           elif args.input == "singularity":
      -        parser = SingularityRecipe(args.files[0])
      +        parser = parsers.SingularityParser
      +        writer = writers.SingularityWriter        
           else:
               if "dockerfile" in args.files[0].lower():
      -            parser = DockerRecipe
      +            parser = parsers.DockerParser
      +            writer = writers.DockerWriter
       
           # Initialize the chosen parser
      -    parser = parser(args.files[0])
      +    recipe = parser(args.files[0])
       
           # By default, discover entrypoint / cmd from Dockerfile
           entrypoint = "/bin/bash"
      @@ -197,14 +204,17 @@ 

      Source code for spython.client.recipe

               entrypoint = args.entrypoint
               force = True
       
      +    # Do the conversion
      +    recipeWriter = writer(recipe)
      +    result = recipeWriter.convert(runscript=entrypoint, force=force)
      +
           # If the user specifies an output file, save to it
           if outfile is not None:
      -        parser.save(outfile, runscript=entrypoint, force=force)
      +        write_file(outfile, result)
       
           # Otherwise, convert and print to screen
           else:
      -        recipe = parser.convert(runscript=entrypoint, force=True)
      -        print(recipe)
      + print(result)
      diff --git a/docs/api/modules/spython/client/shell.html b/docs/api/modules/spython/client/shell.html index ba848be8..0c1b6a5e 100644 --- a/docs/api/modules/spython/client/shell.html +++ b/docs/api/modules/spython/client/shell.html @@ -156,20 +156,15 @@

      Source code for spython.client.shell

       # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
       
       
      -import sys
      -
       
      [docs]def main(args, options, parser): - - from spython.main import Client as cli - # If we have options, first is image image = None if len(options) > 0: image = options.pop(0) - lookup = { 'ipython': ipython, - 'python': python, - 'bpython': bpython } + lookup = {'ipython': ipython, + 'python': python, + 'bpython': bpython} shells = ['ipython', 'python', 'bpython'] @@ -180,52 +175,46 @@

      Source code for spython.client.shell

               except ImportError:
                   pass
      - -
      [docs]def ipython(image): - '''give the user an ipython shell +
      [docs]def prepare_client(image): + '''prepare a client to embed in a shell with recipe parsers and writers. ''' - # The client will announce itself (backend/database) unless it's get from spython.main import get_client - from spython.main.parse import ( DockerRecipe, SingularityRecipe ) + from spython.main.parse import parsers + from spython.main.parse import writers client = get_client() client.load(image) # Add recipe parsers - client.DockerRecipe = DockerRecipe - client.SingularityRecipe = SingularityRecipe + client.parsers = parsers + client.writers = writers + return client
      +
      [docs]def ipython(image): + '''give the user an ipython shell + ''' + client = prepare_client(image) # pylint: disable=unused-variable from IPython import embed embed()
      [docs]def bpython(image): + '''give the user a bpython shell + ''' + client = prepare_client(image) - import bpython - from spython.main import get_client - from spython.main.parse import ( DockerRecipe, SingularityRecipe ) - - client = get_client() - client.load(image) - - # Add recipe parsers - client.DockerRecipe = DockerRecipe - client.SingularityRecipe = SingularityRecipe + try: + import bpython + except ImportError: + return python(image) bpython.embed(locals_={'client': client})
      [docs]def python(image): + '''give the user a python shell + ''' import code - from spython.main import get_client - from spython.main.parse import ( DockerRecipe, SingularityRecipe ) - - client = get_client() - client.load(image) - - # Add recipe parsers - client.DockerRecipe = DockerRecipe - client.SingularityRecipe = SingularityRecipe - + client = prepare_client(image) code.interact(local={"client":client})
      diff --git a/docs/api/modules/spython/client/test.html b/docs/api/modules/spython/client/test.html index b7b29598..3c35076c 100644 --- a/docs/api/modules/spython/client/test.html +++ b/docs/api/modules/spython/client/test.html @@ -156,12 +156,6 @@

      Source code for spython.client.test

       # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
       
       
      -from spython.logger import bot
      -from spython.utils import check_install
      -import sys
      -import os
      -
      -
       
      [docs]def main(args, options, parser): print('TBA, additional tests for Singularity containers.') print('What would you like to see? Let us know!') diff --git a/docs/api/modules/spython/image.html b/docs/api/modules/spython/image.html index e0648b12..35e6e1b7 100644 --- a/docs/api/modules/spython/image.html +++ b/docs/api/modules/spython/image.html @@ -156,45 +156,21 @@

      Source code for spython.image

       import hashlib
       import os
       import re
      -
      +from spython.logger import bot
      +from spython.utils import split_uri
       
       
      [docs]class ImageBase(object): def __str__(self): - if hasattr(self, 'uri'): - if self.uri: - return "%s://%s" %(self.uri, self.image) + protocol = getattr(self, 'protocol', None) + if protocol: + return "%s://%s" %(protocol, self.image) return self.image def __repr__(self): return self.__str__() - -
      [docs] def get_uri(self, image): - '''get the uri of an image, or the string (optional) that appears before - ://. Optional. If none found, returns '' - ''' - image = image or '' - uri = '' - - match = re.match("^(?P<uri>.+)://", image) - if match: - uri = match.group('uri') - - return uri
      - - -
      [docs] def remove_uri(self, image): - '''remove_image_uri will return just the image name. - this will also remove all spaces from the uri. - ''' - image = image or '' - uri = self.get_uri(image) or '' - image = image.replace('%s://' %uri,'', 1) - return image.strip('-').rstrip('/')
      - -
      [docs] def parse_image_name(self, image): ''' simply split the uri from the image. Singularity handles @@ -206,25 +182,24 @@

      Source code for spython.image

       
               '''
               self._image = image
      -        self.uri = self.get_uri(image)
      -        self.image = self.remove_uri(image)
      + self.protocol, self.image = split_uri(image)
      [docs]class Image(ImageBase): def __init__(self, image=None): - '''An image here is an image file or a record. - The user can choose to load the image when starting the client, or - update the main client with an image. The image object is kept - with the main client to make running additional commands easier. + '''An image here is an image file or a record. + The user can choose to load the image when starting the client, or + update the main client with an image. The image object is kept + with the main client to make running additional commands easier. - Parameters - ========== - image: the image uri to parse (required) + Parameters + ========== + image: the image uri to parse (required) - ''' - super(ImageBase, self).__init__() - self.parse_image_name(image) + ''' + super().__init__() + self.parse_image_name(image)
      [docs] def get_hash(self, image=None): @@ -247,7 +222,7 @@

      Source code for spython.image

                           hasher.update(chunk)
                       return hasher.hexdigest()
       
      -        bot.warning('%s does not exist.' %image)
      + bot.warning('%s does not exist.' % image)
      diff --git a/docs/api/modules/spython/image/cmd.html b/docs/api/modules/spython/image/cmd.html index f31c2559..dcf0c1a1 100644 --- a/docs/api/modules/spython/image/cmd.html +++ b/docs/api/modules/spython/image/cmd.html @@ -178,8 +178,8 @@

      Source code for spython.image.cmd

               group = "image"
       
           from spython.main.base.logger import println
      -    from spython.main.base.command import ( init_command, run_command )
      -    from .utils import ( compress, decompress )
      +    from spython.main.base.command import (init_command, run_command)
      +    from .utils import (compress, decompress)
           from .create import create
           from .importcmd import importcmd
           from .export import export
      diff --git a/docs/api/modules/spython/image/cmd/create.html b/docs/api/modules/spython/image/cmd/create.html
      new file mode 100644
      index 00000000..0b0a5ee9
      --- /dev/null
      +++ b/docs/api/modules/spython/image/cmd/create.html
      @@ -0,0 +1,245 @@
      +
      +
      +
      +
      +  
      +
      +  
      +  
      +  
      +  
      +  spython.image.cmd.create — Singularity Python API 1 documentation
      +  
      +
      +  
      +  
      +  
      +  
      +
      +  
      +
      +  
      +  
      +    
      +
      +  
      +
      +  
      +  
      +    
      +     
      +
      +  
      +  
      +
      +
      +
      +
      +
      +   
      +  
      + + + + +
      + + + + + +
      + +
      + + + + + + + + + + + + + + + + + +
      + + + + +
      +
      +
      +
      + +

      Source code for spython.image.cmd.create

      +
      +# Copyright (C) 2017-2019 Vanessa Sochat.
      +
      +# This Source Code Form is subject to the terms of the
      +# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
      +# with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
      +
      +
      +import os
      +from spython.logger import bot
      +
      +
      [docs]def create(self, image_path, size=1024, sudo=False): + '''create will create a a new image + + Parameters + ========== + image_path: full path to image + size: image sizein MiB, default is 1024MiB + filesystem: supported file systems ext3/ext4 (ext[2/3]: default ext3 + + ''' + from spython.utils import check_install + check_install() + + cmd = self.init_command('image.create') + cmd = cmd + ['--size', str(size), image_path] + + output = self.run_command(cmd, sudo=sudo) + self.println(output) + + if not os.path.exists(image_path): + bot.exit("Could not create image %s" % image_path) + + return image_path
      +
      + +
      + +
      +
      + + +
      + +
      +

      + © Copyright 2018, Vanessa Sochat. + +

      +
      + Built with Sphinx using a theme provided by Read the Docs. + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/api/modules/spython/image/cmd/export.html b/docs/api/modules/spython/image/cmd/export.html index f83da22e..38ace97e 100644 --- a/docs/api/modules/spython/image/cmd/export.html +++ b/docs/api/modules/spython/image/cmd/export.html @@ -174,11 +174,14 @@

      Source code for spython.image.cmd.export

           from spython.utils import check_install
           check_install()
       
      +    if 'version 3' in self.version():
      +        bot.exit('export is deprecated after Singularity 2.*')
      +
           if tmptar is None:
               tmptar = "/%s/tmptar.tar" %(tempfile.mkdtemp())
           cmd = ['singularity', 'image.export', '-f', tmptar, image_path]
       
      -    output = self.run_command(cmd, sudo=False)
      +    self.run_command(cmd, sudo=False)
           return tmptar
      diff --git a/docs/api/modules/spython/image/cmd/importcmd.html b/docs/api/modules/spython/image/cmd/importcmd.html index e06b9585..425f7811 100644 --- a/docs/api/modules/spython/image/cmd/importcmd.html +++ b/docs/api/modules/spython/image/cmd/importcmd.html @@ -158,8 +158,6 @@

      Source code for spython.image.cmd.importcmd

       # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
       
       
      -from spython.logger import bot
      -
       
      [docs]def importcmd(self, image_path, input_source): '''import will import (stdin) to the image @@ -177,7 +175,6 @@

      Source code for spython.image.cmd.importcmd

           output = self.run_command(cmd, sudo=False)
           self.println(output)        
           return image_path
      -
      diff --git a/docs/api/modules/spython/image/cmd/utils.html b/docs/api/modules/spython/image/cmd/utils.html index 347ed9b2..e76f76a8 100644 --- a/docs/api/modules/spython/image/cmd/utils.html +++ b/docs/api/modules/spython/image/cmd/utils.html @@ -158,6 +158,7 @@

      Source code for spython.image.cmd.utils

       # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
       
       
      +import os
       from spython.logger import bot
       
       
      [docs]def compress(self, image_path): @@ -176,9 +177,9 @@

      Source code for spython.image.cmd.utils

           if not os.path.exists(image_path):
               bot.exit("Cannot find image %s" %image_path)
               
      -    extracted_file = image_path.replace('.gz','')
      -    cmd = ['gzip','-d','-f', image_path]
      -    result = self.run_command(cmd, quiet=quiet) # exits if return code != 0
      +    extracted_file = image_path.replace('.gz', '')
      +    cmd = ['gzip', '-d', '-f', image_path]
      +    self.run_command(cmd, quiet=quiet) # exits if return code != 0
           return extracted_file
      diff --git a/docs/api/modules/spython/instance.html b/docs/api/modules/spython/instance.html index 0117208d..f2540883 100644 --- a/docs/api/modules/spython/instance.html +++ b/docs/api/modules/spython/instance.html @@ -159,29 +159,29 @@

      Source code for spython.instance

       
      [docs]class Instance(ImageBase): def __init__(self, image, start=True, name=None, **kwargs): - '''An instance is an image running as an instance with services. - This class has functions appended under cmd/__init__ and is - instantiated when the user calls Client. - - Parameters - ========== - image: the Singularity image uri to parse (required) - start: boolean to start the instance (default is True) - name: a name for the instance (will generate RobotName - if not provided) - ''' - super(ImageBase, self).__init__() - self.parse_image_name(image) - self.generate_name(name) - - # Update metadats from arguments - self._update_metadata(kwargs) - self.options = [] - self.cmd = [] - - # Start the instance - if start is True: - self.start(**kwargs) + '''An instance is an image running as an instance with services. + This class has functions appended under cmd/__init__ and is + instantiated when the user calls Client. + + Parameters + ========== + image: the Singularity image uri to parse (required) + start: boolean to start the instance (default is True) + name: a name for the instance (will generate RobotName + if not provided) + ''' + super().__init__() + self.parse_image_name(image) + self.generate_name(name) + + # Update metadats from arguments + self._update_metadata(kwargs) + self.options = [] + self.cmd = [] + + # Start the instance + if start is True: + self.start(**kwargs) # Unique resource identifier @@ -190,9 +190,9 @@

      Source code for spython.instance

                  supply one.
               '''
               # If no name provided, use robot name
      -        if name == None:
      +        if name is None:
                   name = self.RobotNamer.generate()
      -        self.name = name.replace('-','_')
      + self.name = name.replace('-', '_')
      [docs] def parse_image_name(self, image): @@ -206,7 +206,7 @@

      Source code for spython.instance

       
               '''
               self._image = image
      -        self.uri = 'instance://'
      + self.protocol = 'instance'
      [docs] def get_uri(self): @@ -222,15 +222,15 @@

      Source code for spython.instance

               '''
       
               # If not given metadata, use instance.list to get it for container
      -        if kwargs == None and hasattr(self, 'name'):
      +        if kwargs is None and hasattr(self, 'name'):
                   kwargs = self._list(self.name, quiet=True, return_json=True)
       
               # Add acceptable arguments
               for arg in ['pid', 'name']:
       
      -           # Skip over non-iterables:
      -           if arg in kwargs:
      -               setattr(self, arg, kwargs[arg])
      +            # Skip over non-iterables:
      +            if arg in kwargs:
      +                setattr(self, arg, kwargs[arg])
              
               if "image" in kwargs:
                   self._image = kwargs['image']
      @@ -240,8 +240,8 @@ 

      Source code for spython.instance

       
           def __str__(self):
               if hasattr(self, 'name'):
      -            if self.uri:
      -                return "%s%s" %(self.uri, self.name)
      +            if self.protocol:
      +                return "%s://%s" %(self.protocol, self.name)
               return os.path.basename(self._image)
       
           def __repr__(self):
      diff --git a/docs/api/modules/spython/instance/cmd.html b/docs/api/modules/spython/instance/cmd.html
      index 0e8cdf97..e5c0dc0a 100644
      --- a/docs/api/modules/spython/instance/cmd.html
      +++ b/docs/api/modules/spython/instance/cmd.html
      @@ -169,7 +169,7 @@ 

      Source code for spython.instance.cmd

           from spython.utils import run_command as run_cmd
       
           # run_command uses run_cmd, but wraps to catch error
      -    from spython.main.base.command import ( init_command, run_command )
      +    from spython.main.base.command import (init_command, run_command)
           from spython.main.base.generate import RobotNamer
           from .start import start
           from .stop import stop
      diff --git a/docs/api/modules/spython/instance/cmd/iutils.html b/docs/api/modules/spython/instance/cmd/iutils.html
      index c4caa8a3..a0311ca6 100644
      --- a/docs/api/modules/spython/instance/cmd/iutils.html
      +++ b/docs/api/modules/spython/instance/cmd/iutils.html
      @@ -157,6 +157,8 @@ 

      Source code for spython.instance.cmd.iutils

       # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
       # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
       
      +from spython.instance import Instance
      +from spython.logger import bot
       
       
      [docs]def parse_table(table_string, header, remove_rows=1): '''parse a table to json from a string, where a header is expected by default. @@ -179,8 +181,8 @@

      Source code for spython.instance.cmd.iutils

               item = {}
               # This assumes no white spaces in each entry, which should be the case
               row = [x for x in row.split(' ') if x]
      -        for e in range(len(row)):
      -            item[header[e]] = row[e]
      +        for i, r in enumerate(row):
      +            item[header[i]] = r
               parsed.append(item)
           return parsed
      @@ -201,7 +203,7 @@

      Source code for spython.instance.cmd.iutils

           cmd = self._init_command(subgroup)
       
           cmd.append(name)
      -    output = run_command(cmd, quiet=True)
      +    output = self.run_command(cmd, quiet=True)
       
           # Success, we have instances
       
      @@ -213,7 +215,7 @@ 

      Source code for spython.instance.cmd.iutils

       
               # Prepare json result from table
       
      -        header = ['daemon_name','pid','container_image']
      +        header = ['daemon_name', 'pid', 'container_image']
               instances = parse_table(output['message'][0], header)
       
               # Does the user want instance objects instead?
      diff --git a/docs/api/modules/spython/instance/cmd/start.html b/docs/api/modules/spython/instance/cmd/start.html
      index c0bfe24a..ce9c6adf 100644
      --- a/docs/api/modules/spython/instance/cmd/start.html
      +++ b/docs/api/modules/spython/instance/cmd/start.html
      @@ -160,7 +160,7 @@ 

      Source code for spython.instance.cmd.start

       
       from spython.logger import bot
       
      -
      [docs]def start(self, image=None, name=None, args=None, sudo=False, options=[], capture=False): +
      [docs]def start(self, image=None, name=None, args=None, sudo=False, options=None, capture=False): '''start an instance. This is done by default when an instance is created. Parameters @@ -177,12 +177,11 @@

      Source code for spython.instance.cmd.start

              singularity [...] instance.start [...] <container path> <instance name>
       
           '''        
      -    from spython.utils import ( run_command, 
      -                                check_install )
      +    from spython.utils import (run_command, check_install)
           check_install()
       
           # If name provided, over write robot (default)
      -    if name != None:
      +    if name is not None:
               self.name = name
       
           # If an image isn't provided, we have an initialized instance
      @@ -203,13 +202,13 @@ 

      Source code for spython.instance.cmd.start

       
           # Add options, if they are provided
           if not isinstance(options, list):
      -        options = options.split(' ')
      +        options = [] if options is None else options.split(' ')
       
           # Assemble the command!
           cmd = cmd + options + [image, self.name]
       
           # If arguments are provided
      -    if args != None:
      +    if args is not None:
               if not isinstance(args, list):
                   args = [args]
               cmd = cmd + args
      diff --git a/docs/api/modules/spython/instance/cmd/stop.html b/docs/api/modules/spython/instance/cmd/stop.html
      index 68ce6613..44f402dc 100644
      --- a/docs/api/modules/spython/instance/cmd/stop.html
      +++ b/docs/api/modules/spython/instance/cmd/stop.html
      @@ -172,8 +172,7 @@ 

      Source code for spython.instance.cmd.stop

              singularity [...] instance.stop [...] <instance name>
       
           '''        
      -    from spython.utils import ( check_install, 
      -                                run_command )
      +    from spython.utils import (check_install, run_command)
           check_install()
       
           subgroup = 'instance.stop'
      diff --git a/docs/api/modules/spython/logger/message.html b/docs/api/modules/spython/logger/message.html
      index e9a9a035..ac055895 100644
      --- a/docs/api/modules/spython/logger/message.html
      +++ b/docs/api/modules/spython/logger/message.html
      @@ -354,7 +354,7 @@ 

      Source code for spython.logger.message

                   percent = "%5s" % ("{0:.1f}").format(percent)
                   output = '\r' + prefix + \
                       " |%s| %s%s %s" % (bar, percent, '%', suffix)
      -            sys.stdout.write(output),
      +            sys.stdout.write(output)
                   if iteration == total and carriage_return:
                       sys.stdout.write('\n')
                   sys.stdout.flush()
      @@ -424,7 +424,7 @@

      Source code for spython.logger.message

               not, a numbered list is used.
               '''
       
      -        labels = [str(x) for x in range(1,len(rows)+1)]
      +        labels = [str(x) for x in range(1, len(rows) + 1)]
               if isinstance(rows, dict):
                   labels = list(rows.keys())
                   rows = list(rows.values())
      diff --git a/docs/api/modules/spython/logger/progress.html b/docs/api/modules/spython/logger/progress.html
      index 868b87c8..2c34e256 100644
      --- a/docs/api/modules/spython/logger/progress.html
      +++ b/docs/api/modules/spython/logger/progress.html
      @@ -192,17 +192,17 @@ 

      Source code for spython.logger.progress

                       self.hide = not STREAM.isatty()
                   except AttributeError:  # output does not support isatty()
                       self.hide = True
      -        self.empty_char =    empty_char
      -        self.filled_char =   filled_char
      +        self.empty_char = empty_char
      +        self.filled_char = filled_char
               self.expected_size = expected_size
      -        self.every =         every
      -        self.start =         time.time()
      -        self.ittimes =       []
      -        self.eta =           0
      -        self.etadelta =      time.time()
      -        self.etadisp =       self.format_time(self.eta)
      +        self.every = every
      +        self.start = time.time()
      +        self.ittimes = []
      +        self.eta = 0
      +        self.etadelta = time.time()
      +        self.etadisp = self.format_time(self.eta)
               self.last_progress = 0
      -        if (self.expected_size):
      +        if self.expected_size:
                   self.show(0)
       
       
      [docs] def show(self, progress, count=None): @@ -252,7 +252,7 @@

      Source code for spython.logger.progress

       
           count = len(it) if expected_size is None else expected_size
       
      -    with Bar(label=label, width=width, hide=hide, empty_char=BAR_EMPTY_CHAR,
      +    with ProgressBar(label=label, width=width, hide=hide, empty_char=BAR_EMPTY_CHAR,
                    filled_char=BAR_FILLED_CHAR, expected_size=count, every=every) \
                   as bar:
               for i, item in enumerate(it):
      diff --git a/docs/api/modules/spython/logger/spinner.html b/docs/api/modules/spython/logger/spinner.html
      index 4cfb891b..57887173 100644
      --- a/docs/api/modules/spython/logger/spinner.html
      +++ b/docs/api/modules/spython/logger/spinner.html
      @@ -154,9 +154,6 @@ 

      Source code for spython.logger.spinner

       # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
       
       
      -import os
      -import sys
      -
       import sys
       import time
       import threading
      @@ -182,7 +179,7 @@ 

      Source code for spython.logger.spinner

                   for cursor in '<^>v': yield cursor
      [docs] def select_generator(self, generator): - if generator == None: + if generator is None: generator = choice(['cursor', 'arrow', 'balloons']) diff --git a/docs/api/modules/spython/main.html b/docs/api/modules/spython/main.html index 030303b4..799475da 100644 --- a/docs/api/modules/spython/main.html +++ b/docs/api/modules/spython/main.html @@ -177,14 +177,17 @@

      Source code for spython.main

           from .execute import execute 
           from .help import help
           from .inspect import inspect
      -    from .instances import ( instances, stopall ) # global instance commands
      +    from .instances import (instances, stopall) # global instance commands
           from .run import run
           from .pull import pull
      +    from .export import (export, _export)
       
           # Actions
           Client.apps = apps
           Client.build = build
           Client.execute = execute
      +    Client.export = export
      +    Client._export = _export
           Client.help = help
           Client.inspect = inspect
           Client.instances = instances
      @@ -216,10 +219,10 @@ 

      Source code for spython.main

           cli = Client()
       
           # Pass on verbosity
      -    cli.image.debug = cli.debug
      -    cli.image.quiet = cli.quiet
      -    cli.instance.debug = cli.debug
      -    cli.instance.quiet = cli.quiet 
      +    for subclient in [cli.image, cli.instance]:
      +        subclient.debug = cli.debug
      +        subclient.quiet = cli.quiet
      +        subclient.version = cli.version
       
           return cli
      diff --git a/docs/api/modules/spython/main/apps.html b/docs/api/modules/spython/main/apps.html index 8d225e93..986fe257 100644 --- a/docs/api/modules/spython/main/apps.html +++ b/docs/api/modules/spython/main/apps.html @@ -156,8 +156,6 @@

      Source code for spython.main.apps

       # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
       
       
      -from spython.logger import bot
      -
       
      [docs]def apps(self, image=None, full_path=False, root=''): ''' return list of SCIF apps in image. The Singularity software serves @@ -178,7 +176,7 @@

      Source code for spython.main.apps

           if image is None:
               image = self._get_uri()
       
      -    cmd = self._init_command('apps') + [ image ]
      +    cmd = self._init_command('apps') + [image]
           output = self._run_command(cmd)
       
           if full_path is True:
      @@ -186,7 +184,7 @@ 

      Source code for spython.main.apps

       
           if len(output) > 0:
               output = ''.join(output).split('\n')
      -        output = ['%s%s' %(root,x) for x in output if x]
      +        output = ['%s%s' %(root, x) for x in output if x]
       
           return output
      diff --git a/docs/api/modules/spython/main/base.html b/docs/api/modules/spython/main/base.html index 053d4817..37dc8fc9 100644 --- a/docs/api/modules/spython/main/base.html +++ b/docs/api/modules/spython/main/base.html @@ -162,24 +162,35 @@

      Source code for spython.main.base

           get_singularity_version
       )
       
      +from .command import (
      +    generate_bind_list,
      +    init_command,
      +    run_command
      +)
      +from .flags import parse_verbosity
      +from .sutils import (
      +    get_uri,
      +    load,
      +    setenv,
      +    get_filename
      +)
      +from .logger import (
      +    println,
      +    init_level
      +)
      +from .generate import RobotNamer
      +
       import json
       import sys
       import os
       import re
       
      -
      -from .command import ( generate_bind_list, init_command, run_command )
      -from .flags import parse_verbosity
      -from .sutils import ( get_uri, load, setenv, get_filename )
      -from .logger import ( println,  init_level )
      -from .generate import RobotNamer
      -
       
      [docs]class Client: def __str__(self): base = "[singularity-python]" if hasattr(self, 'simage'): - if self.simage.image not in [None,'']: + if self.simage.image not in [None, '']: base = "%s[%s]" %(base, self.simage) return base @@ -187,10 +198,10 @@

      Source code for spython.main.base

               return self.__str__()
       
           def __init__(self):
      -       '''the base client for singularity, will have commands added to it.
      -          upon init, store verbosity requested in environment MESSAGELEVEL.
      -       '''
      -       self._init_level()
      +        '''the base client for singularity, will have commands added to it.
      +           upon init, store verbosity requested in environment MESSAGELEVEL.
      +        '''
      +        self._init_level()
       
       
      [docs] def version(self): '''a wrapped to get_singularity_version, takes no arguments. diff --git a/docs/api/modules/spython/main/base/command.html b/docs/api/modules/spython/main/base/command.html index 16f0ae77..b6e8f36d 100644 --- a/docs/api/modules/spython/main/base/command.html +++ b/docs/api/modules/spython/main/base/command.html @@ -158,19 +158,15 @@

      Source code for spython.main.base.command

       # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
       
       
      -from spython.main.base.logger import println
       from spython.utils import ( 
      -    run_command as run_cmd, 
      -    check_install 
      +    run_command as run_cmd
       )
       
       from spython.logger import bot
       
       import subprocess
      -import json
       import sys
       import os
      -import re
       
       
       
      @@ -281,7 +277,7 @@ 

      Source code for spython.main.base.command

       
           '''
           # First preference to function, then to client setting
      -    if quiet == None:
      +    if quiet is None:
               quiet = self.quiet
       
           result = run_cmd(cmd, sudo=sudo, capture=capture, quiet=quiet)
      diff --git a/docs/api/modules/spython/main/base/flags.html b/docs/api/modules/spython/main/base/flags.html
      index a357ea77..6c33bdfb 100644
      --- a/docs/api/modules/spython/main/base/flags.html
      +++ b/docs/api/modules/spython/main/base/flags.html
      @@ -158,31 +158,6 @@ 

      Source code for spython.main.base.flags

       # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
       
       
      -
      -
      [docs]def parse_verbosity(self, args): - '''parse_verbosity will take an argument object, and return the args - passed (from a dictionary) to a list - - Parameters - ========== - args: the argparse argument objects - - ''' - - flags = [] - - if args.silent is True: - flags.append('--silent') - elif args.quiet is True: - flags.append('--quiet') - elif args.debug is True: - flags.append('--debug') - elif args.verbose is True: - flags.append('-' + 'v' * args.verbose) - - return flags
      - - ''' GLOBAL OPTIONS: -d|--debug Print debugging information @@ -221,6 +196,29 @@

      Source code for spython.main.base.flags

           instance   Persistent instance command group                                
       
       '''
      +
      +
      [docs]def parse_verbosity(self, args): + '''parse_verbosity will take an argument object, and return the args + passed (from a dictionary) to a list + + Parameters + ========== + args: the argparse argument objects + + ''' + + flags = [] + + if args.silent is True: + flags.append('--silent') + elif args.quiet is True: + flags.append('--quiet') + elif args.debug is True: + flags.append('--debug') + elif args.verbose is True: + flags.append('-' + 'v' * args.verbose) + + return flags
      diff --git a/docs/api/modules/spython/main/base/logger.html b/docs/api/modules/spython/main/base/logger.html index 3c859ee3..ec2b2845 100644 --- a/docs/api/modules/spython/main/base/logger.html +++ b/docs/api/modules/spython/main/base/logger.html @@ -187,7 +187,7 @@

      Source code for spython.main.base.logger

              quiet: a runtime variable to over-ride the default.
       
           '''
      -    if isinstance(output,bytes):
      +    if isinstance(output, bytes):
               output = output.decode('utf-8')
           if self.quiet is False and quiet is False:
               print(output)
      diff --git a/docs/api/modules/spython/main/base/sutils.html b/docs/api/modules/spython/main/base/sutils.html index c82997e0..56060191 100644 --- a/docs/api/modules/spython/main/base/sutils.html +++ b/docs/api/modules/spython/main/base/sutils.html @@ -197,17 +197,20 @@

      Source code for spython.main.base.sutils

           bot.debug('%s set to %s' % (variable, value))
      -
      [docs]def get_filename(self, image=None, ext='simg'): - '''return an image filename based on the image uri. If an image uri is - not specified, we look for the loaded image. +
      [docs]def get_filename(self, image, ext='sif', pwd=True): + '''return an image filename based on the image uri. Parameters ========== - image: the uri to base off of ext: the extension to use + pwd: derive a filename for the pwd ''' - return "%s.%s" %(re.sub('^.*://','',image).replace('/','-'), ext)
      - + if pwd is True: + image = os.path.basename(image) + image = re.sub('^.*://', '', image) + if not image.endswith(ext): + image = "%s.%s" %(image, ext) + return image
      [docs]def get_uri(self): diff --git a/docs/api/modules/spython/main/build.html b/docs/api/modules/spython/main/build.html index 11084fbe..f2972945 100644 --- a/docs/api/modules/spython/main/build.html +++ b/docs/api/modules/spython/main/build.html @@ -170,7 +170,8 @@

      Source code for spython.main.build

                       robot_name=False,
                       ext='simg',
                       sudo=True,
      -                stream=False):
      +                stream=False,
      +                force=False):
       
           '''build a singularity image, optionally for an isolated build
              (requires sudo). If you specify to stream, expect the image name
      @@ -202,6 +203,10 @@ 

      Source code for spython.main.build

       
           if 'version 3' in self.version():
               ext = 'sif'
      +        
      +    # Force the build if the image / sandbox exists
      +    if force is True:
      +        cmd.append('--force')
       
           # No image provided, default to use the client's loaded image
           if recipe is None:
      @@ -226,7 +231,6 @@ 

      Source code for spython.main.build

                   bot.exit('%s does not exist!' % build_folder)
               image = os.path.join(build_folder, image)
               
      -
           # The user wants to run an isolated build
           if isolated is True:
               cmd.append('--isolated')
      @@ -239,7 +243,7 @@ 

      Source code for spython.main.build

           cmd = cmd + [image, recipe]
       
           if stream is False:
      -        output = self._run_command(cmd, sudo=sudo, capture=False)
      +        self._run_command(cmd, sudo=sudo, capture=False)
           else:
               # Here we return the expected image, and an iterator! 
               # The caller must iterate over
      diff --git a/docs/api/modules/spython/main/execute.html b/docs/api/modules/spython/main/execute.html
      index fff043d0..5cb26c81 100644
      --- a/docs/api/modules/spython/main/execute.html
      +++ b/docs/api/modules/spython/main/execute.html
      @@ -158,21 +158,18 @@ 

      Source code for spython.main.execute

       
       from spython.logger import bot
       from spython.utils import stream_command
      -import os
      -import sys
       
       
       
      [docs]def execute(self, - image = None, - command = None, - app = None, - writable = False, - contain = False, - bind = None, - stream = False, - nv = False, + image=None, + command=None, + app=None, + writable=False, + contain=False, + bind=None, + stream=False, + nv=False, return_result=False): - ''' execute: send a command to a container Parameters @@ -216,6 +213,10 @@

      Source code for spython.main.execute

               if isinstance(image, self.instance):
                   image = image.get_uri()
       
      +        # If image is still None, not defined by user or previously with client
      +        if image is None:
      +            bot.exit('Please load or provide an image.')
      +
               # Does the user want to use bind paths option?
               if bind is not None:
                   cmd += self._generate_bind_list(bind)
      diff --git a/docs/api/modules/spython/main/export.html b/docs/api/modules/spython/main/export.html
      new file mode 100644
      index 00000000..0c6e4be8
      --- /dev/null
      +++ b/docs/api/modules/spython/main/export.html
      @@ -0,0 +1,316 @@
      +
      +
      +
      +
      +  
      +
      +  
      +  
      +  
      +  
      +  spython.main.export — Singularity Python API 1 documentation
      +  
      +
      +  
      +  
      +  
      +  
      +
      +  
      +
      +  
      +  
      +    
      +
      +  
      +
      +  
      +  
      +    
      +     
      +
      +  
      +  
      +
      +
      +
      +
      +
      +   
      +  
      + + + + +
      + + + + + +
      + +
      + + + + + + + + + + + + + + + + + +
      + + + + +
      +
      +
      +
      + +

      Source code for spython.main.export

      +
      +# Copyright (C) 2017-2019 Vanessa Sochat.
      +
      +# This Source Code Form is subject to the terms of the
      +# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
      +# with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
      +
      +
      +from spython.logger import bot
      +import tempfile
      +import shutil
      +import os
      +
      +
      +def export(self,
      +           image_path,
      +           pipe=False,
      +           output_file=None,
      +           command=None,
      +           sudo=False):
      +
      +    '''export will export an image, sudo must be used. If we have Singularity
      +       versions after 3, export is replaced with building into a sandbox.
      +
      +       Parameters
      +       ==========
      +       image_path: full path to image
      +       pipe: export to pipe and not file (default, False)
      +       output_file: if pipe=False, export tar to this file. If not specified, 
      +       will generate temporary directory.
      +    '''
      +    from spython.utils import check_install
      +    check_install()
      +
      +    if 'version 3' in self.version() or '2.6' in self.version():
      +
      +        # If export is deprecated, we run a build
      +        bot.warning('Export is not supported for Singularity 3.x. Building to sandbox instead.')
      +
      +        if output_file is None:
      +            basename, _ = os.path.splitext(image_path)
      +            output_file = self._get_filename(basename, 'sandbox', pwd=False)
      +
      +        return self.build(recipe=image_path,
      +                          image=output_file,
      +                          sandbox=True,
      +                          force=True,
      +                          sudo=sudo)
      +
      +    # If not version 3, run deprecated command
      +    elif '2.5' in self.version():
      +        return self._export(image_path=image_path,
      +                            pipe=pipe,
      +                            output_file=output_file,
      +                            command=command)
      +
      +    bot.warning('Unsupported version of Singularity, %s' % self.version())
      +
      +
      +def _export(self,
      +            image_path,
      +            pipe=False,
      +            output_file=None,
      +            command=None):
      +    ''' the older deprecated function, running export for previous
      +               versions of Singularity that support it
      +
      +           USAGE: singularity [...] export [export options...] <container path>
      +           Export will dump a tar stream of the container image contents to standard
      +           out (stdout). 
      +           note: This command must be executed as root.
      +           EXPORT OPTIONS:
      +               -f/--file       Output to a file instead of a pipe
      +                  --command    Replace the tar command (DEFAULT: 'tar cf - .')
      +           EXAMPLES:
      +               $ sudo singularity export /tmp/Debian.img > /tmp/Debian.tar
      +               $ sudo singularity export /tmp/Debian.img | gzip -9 > /tmp/Debian.tar.gz
      +               $ sudo singularity export -f Debian.tar /tmp/Debian.img
      +
      +    '''
      +    sudo = True
      +    cmd = self._init_command('export')
      +    
      +    # If the user has specified export to pipe, we don't need a file
      +    if pipe:
      +        cmd.append(image_path)
      +
      +    else:
      +        _, tmptar = tempfile.mkstemp(suffix=".tar")
      +        os.remove(tmptar)
      +        cmd = cmd + ["-f", tmptar, image_path]
      +        self._run_command(cmd, sudo=sudo)
      +
      +        # Was there an error?            
      +        if not os.path.exists(tmptar):
      +            print('Error generating image tar')
      +            return None
      +
      +        # if user has specified output file, move it there, return path
      +        if output_file is not None:
      +            shutil.copyfile(tmptar, output_file)
      +            return output_file
      +        else:
      +            return tmptar
      +
      +    # Otherwise, return output of pipe    
      +    return self._run_command(cmd, sudo=sudo)
      +
      + +
      + +
      +
      + + +
      + +
      +

      + © Copyright 2018, Vanessa Sochat. + +

      +
      + Built with Sphinx using a theme provided by Read the Docs. + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/api/modules/spython/main/help.html b/docs/api/modules/spython/main/help.html index 10b9cc93..59106549 100644 --- a/docs/api/modules/spython/main/help.html +++ b/docs/api/modules/spython/main/help.html @@ -167,8 +167,8 @@

      Source code for spython.main.help

           from spython.utils import check_install
           check_install()
       
      -    cmd = ['singularity','--help']
      -    if command != None:
      +    cmd = ['singularity', '--help']
      +    if command is not None:
               cmd.append(command)
           help = self._run_command(cmd)
           return help
      diff --git a/docs/api/modules/spython/main/inspect.html b/docs/api/modules/spython/main/inspect.html index 1e1b2cc7..d821e95d 100644 --- a/docs/api/modules/spython/main/inspect.html +++ b/docs/api/modules/spython/main/inspect.html @@ -157,7 +157,6 @@

      Source code for spython.main.inspect

       
       import json as jsonp
       
      -from spython.logger import bot
       from spython.utils import ( 
           check_install, 
           run_command
      @@ -184,14 +183,15 @@ 

      Source code for spython.main.inspect

           if app is not None:
               cmd = cmd + ['--app', app]
       
      -    options = ['e','d','l','r','hf','t']
      +    options = ['e', 'd', 'l', 'r', 'hf', 't']
       
           # After Singularity 3.0, helpfile was changed to H from
       
           if "version 3" in self.version():
      -        options = ['e','d','l','r','H','t']
      +        options = ['e', 'd', 'l', 'r', 'H', 't']
       
      -    [cmd.append('-%s' % x) for x in options]
      +    for x in options:
      +        cmd.append('-%s' % x)
       
           if json is True:
               cmd.append('--json')
      @@ -202,8 +202,12 @@ 

      Source code for spython.main.inspect

           if result['return_code'] == 0:
               result = jsonp.loads(result['message'][0])
       
      +        # Unify output to singularity 3 format
      +        if "data" in result:
      +            result = result['data']
      +
               # Fix up labels
      -        labels = parse_labels(result)
      +        result = parse_labels(result)
       
               if not quiet:
                   print(jsonp.dumps(result, indent=4))
      @@ -220,23 +224,13 @@ 

      Source code for spython.main.inspect

              result: the json object to parse from inspect
           '''
       
      -    if "data" in result:
      -        labels = result['data']['attributes'].get('labels') or {}
      -
      -    elif 'attributes' in result:
      -        labels = result['attributes'].get('labels') or {}
      -
      -    # If labels included, try parsing to json
      -
      +    labels = result['attributes'].get('labels') or {}
           try:
               labels = jsonp.loads(labels)
           except:
               pass
       
      -    if "data" in result:
      -        result['data']['attributes']['labels'] = labels
      -    else:
      -        result['attributes']['labels'] = labels
      +    result['attributes']['labels'] = labels
       
           return result
      diff --git a/docs/api/modules/spython/main/instances.html b/docs/api/modules/spython/main/instances.html index 22f6e24a..ff5ca3dc 100644 --- a/docs/api/modules/spython/main/instances.html +++ b/docs/api/modules/spython/main/instances.html @@ -157,7 +157,7 @@

      Source code for spython.main.instances

       
       
       from spython.logger import bot
      -from spython.utils import ( run_command, check_install )
      +from spython.utils import run_command
       
       
      [docs]def instances(self, name=None, return_json=False, quiet=False): '''list instances. For Singularity, this is provided as a command sub @@ -207,7 +207,7 @@

      Source code for spython.main.instances

       
               # Prepare json result from table
       
      -        header = ['daemon_name','pid','container_image']
      +        header = ['daemon_name', 'pid', 'container_image']
               instances = parse_table(output['message'][0], header)
       
               # Does the user want instance objects instead?
      @@ -216,7 +216,7 @@ 

      Source code for spython.main.instances

                   for i in instances:
                       
                       # If the user has provided a name, only add instance matches
      -                if name != None:
      +                if name is not None:
                           if name != i['daemon_name']:
                               continue
       
      @@ -239,7 +239,7 @@ 

      Source code for spython.main.instances

               bot.info('No instances found.')
       
           # If we are given a name, return just one
      -    if name != None and instances not in [None,[]]:
      +    if name is not None and instances not in [None, []]:
               if len(instances) == 1:
                   instances = instances[0]
       
      @@ -256,7 +256,7 @@ 

      Source code for spython.main.instances

                    instances)
       
           '''
      -    from spython.utils import run_command, check_install
      +    from spython.utils import check_install
           check_install()
       
           subgroup = 'instance.stop'
      diff --git a/docs/api/modules/spython/main/parse/converters.html b/docs/api/modules/spython/main/parse/converters.html
      index 7c231723..393f5b02 100644
      --- a/docs/api/modules/spython/main/parse/converters.html
      +++ b/docs/api/modules/spython/main/parse/converters.html
      @@ -156,10 +156,8 @@ 

      Source code for spython.main.parse.converters

      # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -import json -import os import re -import sys +from spython.logger import bot # Singularity to Dockerfile # Easier, parsed line by line @@ -232,19 +230,19 @@

      Source code for spython.main.parse.converters

      entrypoint = default # Only look at Docker if not enforcing default - if force is False: + if not force: if self.entrypoint is not None: - entrypoint = ''.join(self.entrypoint) - elif self.cmd is not None: - entrypoint = ''.join(self.cmd) + entrypoint = ' '.join(self.entrypoint) + if self.cmd is not None: + entrypoint = entrypoint + ' ' + ' '.join(self.cmd) # Entrypoint should use exec if not entrypoint.startswith('exec'): - entrypoint = "exec %s" %entrypoint + entrypoint = "exec %s" % entrypoint # Should take input arguments into account if not re.search('"?[$]@"?', entrypoint): - entrypoint = '%s "$@"' %entrypoint + entrypoint = '%s "$@"' % entrypoint return entrypoint
      @@ -357,7 +355,14 @@

      Source code for spython.main.parse.converters

      # Take preference for user, entrypoint, command, then default runscript = self._create_runscript(runscript, force) + + # If a working directory was used, add it as a cd + if self.workdir is not None: + runscript = [self.workdir] + [runscript] + + # Finish the recipe, also add as startscript recipe += finish_section(runscript, 'runscript') + recipe += finish_section(runscript, 'startscript') if self.test is not None: recipe += finish_section(self.test, 'test') diff --git a/docs/api/modules/spython/main/parse/docker.html b/docs/api/modules/spython/main/parse/docker.html index 968b9375..7e2c05c5 100644 --- a/docs/api/modules/spython/main/parse/docker.html +++ b/docs/api/modules/spython/main/parse/docker.html @@ -155,21 +155,21 @@

      Source code for spython.main.parse.docker

       # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
       # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
       
      -
      +import json
       import os
       import re
      -import sys
       
       from .environment import parse_env
       from .recipe import Recipe
      -from spython.utils import read_file
       from spython.logger import bot
       
       
      [docs]class DockerRecipe(Recipe): def __init__(self, recipe=None): '''a Docker recipe parses a Docker Recipe into the expected fields of - labels, environment, and install/runtime commands + labels, environment, and install/runtime commands. We save working + directory as we parse, and the last one can be added to the runscript + of a Singularity recipe. Parameters ========== @@ -349,21 +349,19 @@

      Source code for spython.main.parse.docker

                  dest: the destiation
               '''
       
      -        # Create data structure to iterate over
      -
      -        paths = {'source': source,
      -                 'dest': dest}
      -
      -        for pathtype, path in paths.items():
      -            if path == ".":
      -                paths[pathtype] = os.getcwd()
      - 
      -            # Warning if doesn't exist
      -            if not os.path.exists(path):
      -                bot.warning("%s doesn't exist, ensure exists for build" %path)
      -
      +        def expandPath(path):
      +            return os.getcwd() if path == "." else path
      +        
      +        # Warn the user Singularity doesn't support expansion
      +        if '*' in source:
      +            bot.warning("Singularity doesn't support expansion, * found in %s" % source)
      +        
      +        # Warning if file/folder (src) doesn't exist
      +        if not os.path.exists(source):
      +            bot.warning("%s doesn't exist, ensure exists for build" % source)
      +        
               # The pair is added to the files as a list
      -        self.files.append([paths['source'], paths['dest']])
      +        self.files.append([expandPath(source), expandPath(dest)])
       
       
           def _parse_http(self, url, dest):
      @@ -470,9 +468,10 @@ 

      Source code for spython.main.parse.docker

                  line: the line from the recipe file to parse for WORKDIR
       
               '''
      +        # Save the last working directory to add to the runscript
               workdir = self._setup('WORKDIR', line)
      -        line = "cd %s" %(''.join(workdir))
      -        self.install.append(line)
      +        self.workdir = "cd %s" %(''.join(workdir))
      +        self.install.append(''.join(self.workdir))
       
       
       # Entrypoint and Command
      @@ -481,14 +480,29 @@ 

      Source code for spython.main.parse.docker

               '''_cmd will parse a Dockerfile CMD command
                  
                  eg: CMD /code/run_uwsgi.sh --> /code/run_uwsgi.sh.
      -               The 
      +               If a list is provided, it's parsed to a list.
       
                  Parameters
                  ==========
                  line: the line from the recipe file to parse for CMD
       
               '''
      -        self.cmd = self._setup('CMD', line)
      +        cmd = self._setup('CMD', line)[0]
      +        self.cmd = self._load_list(cmd)
      +
      +
      +    def _load_list(self, line):
      +        '''load an entrypoint or command, meaning it can be wrapped in a list
      +           or a regular string. We try loading as json to return an actual
      +           list. E.g., after _setup, we might go from 'ENTRYPOINT ["one", "two"]'
      +           to '["one", "two"]', and this function loads as json and returns
      +           ["one", "two"]
      +        '''
      +        try:
      +            line = json.loads(line)
      +        except json.JSONDecodeError:
      +            pass
      +        return line
       
       
           def _entry(self, line):
      @@ -499,7 +513,8 @@ 

      Source code for spython.main.parse.docker

                  line: the line from the recipe file to parse for CMD
       
               '''
      -        self.entrypoint = self._setup('ENTRYPOINT', line)
      +        entrypoint = self._setup('ENTRYPOINT', line)[0]
      +        self.entrypoint = self._load_list(entrypoint)
       
       
       # Labels
      diff --git a/docs/api/modules/spython/main/parse/recipe.html b/docs/api/modules/spython/main/parse/recipe.html
      index f7769172..f11f69d9 100644
      --- a/docs/api/modules/spython/main/parse/recipe.html
      +++ b/docs/api/modules/spython/main/parse/recipe.html
      @@ -155,107 +155,23 @@ 

      Source code for spython.main.parse.recipe

       # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
       # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
       
      -import json
      -import tempfile
      -import os
      -import re
      -import sys
      -
      -from spython.logger import bot
      -from spython.utils import ( read_file, write_file )
      -from spython.main.parse.converters import (
      -    create_runscript,
      -    create_section,
      -    docker2singularity,
      -    singularity2docker
      -)
      -
       
       
      [docs]class Recipe(object): '''a recipe includes an environment, labels, runscript or command, - and install sequence. This object is subclassed by a Singularity or - Docker recipe, and can be used to convert between the two. The user - can read in one recipe type to convert to another, or provide raw - commands and metadata for generating a recipe. + and install sequence. This object is interacted with by a Parser + (intended to popualte the recipe with content) and a Writer (intended + to write a recipe to file). The parsers and writers are located in + parsers.py, and writers.py, respectively. The user is also free to use + the recipe class to build recipes. Parameters ========== recipe: the original recipe file, parsed by the subclass either - DockerRecipe or SingularityRecipe + DockerParser or SingularityParser ''' def __init__(self, recipe=None): - self.load(recipe) - -
      [docs] def load(self, recipe): - '''load a recipe file into the client, first performing checks, and - then parsing the file. - - Parameters - ========== - recipe: the original recipe file, parsed by the subclass either - DockerRecipe or SingularityRecipe - - ''' - self.recipe = recipe # the recipe file - self._run_checks() # does the recipe file exist? - self.parse()
      - - def __str__(self): - ''' show the user the recipe object, along with the type. E.g., - - [spython-recipe][docker] - [spython-recipe][singularity] - - ''' - - base = "[spython-recipe]" - if self.recipe: - base = "%s[%s]" %(base, self.recipe) - return base - - def __repr__(self): - return self.__str__() - - - def _run_checks(self): - '''basic sanity checks for the file name (and others if needed) before - attempting parsing. - ''' - if self.recipe is not None: - - # Does the recipe provided exist? - if not os.path.exists(self.recipe): - bot.error("Cannot find %s, is the path correct?" %self.recipe) - sys.exit(1) - - # Ensure we carry fullpath - self.recipe = os.path.abspath(self.recipe) - - -# Parse - -
      [docs] def parse(self): - '''parse is the base function for parsing the recipe, whether it be - a Dockerfile or Singularity recipe. The recipe is read in as lines, - and saved to a list if needed for the future. If the client has - it, the recipe type specific _parse function is called. - - Instructions for making a client subparser: - - It should have a main function _parse that parses a list of lines - from some recipe text file into the appropriate sections, e.g., - - self.fromHeader - self.environ - self.labels - self.install - self.files - self.test - self.entrypoint - - ''' self.cmd = None self.comments = [] @@ -267,197 +183,24 @@

      Source code for spython.main.parse.recipe

               self.ports = []
               self.test = None
               self.volumes = []
      +        self.workdir = None
       
      -        if self.recipe:
      -
      -            # Read in the raw lines of the file
      -            self.lines = read_file(self.recipe)
      -
      -            # If properly instantiated by Docker or Singularity Recipe, parse
      -            if hasattr(self, '_parse'):
      -                self._parse()
      - - -# Convert and Save - - -
      [docs] def save(self, output_file=None, - convert_to=None, - runscript="/bin/bash", - force=False): - - '''save will convert a recipe to a specified format (defaults to the - opposite of the recipe type originally loaded, (e.g., docker--> - singularity and singularity-->docker) and write to an output file, - if specified. If not specified, a temporary file is used. - - Parameters - ========== - output_file: the file to save to, not required (estimates default) - convert_to: can be manually forced (docker or singularity) - runscript: default runscript (entrypoint) to use - force: if True, override discovery from Dockerfile - - ''' - - converted = self.convert(convert_to, runscript, force) - if output_file is None: - output_file = self._get_conversion_outfile(convert_to=None) - bot.info('Saving to %s' %output_file) - write_file(output_file, converted)
      - - -
      [docs] def convert(self, convert_to=None, - runscript="/bin/bash", - force=False): - - '''This is a convenience function for the user to easily call to get - the most likely desired result, conversion to the opposite format. - We choose the selection based on the recipe name - meaning that we - perform conversion with default based on recipe name. If the recipe - object is DockerRecipe, we convert to Singularity. If the recipe - object is SingularityRecipe, we convert to Docker. The user can - override this by setting the variable convert_to - - Parameters - ========== - convert_to: can be manually forced (docker or singularity) - runscript: default runscript (entrypoint) to use - force: if True, override discovery from Dockerfile - - ''' - converter = self._get_converter(convert_to) - return converter(runscript=runscript, force=force)
      + self.source = recipe - - -# Internal Helpers - - - def _get_converter(self, convert_to=None): - '''see convert and save. This is a helper function that returns - the proper conversion function, but doesn't call it. We do this - so that in the case of convert, we do the conversion and return - a string. In the case of save, we save the recipe to file for the - user. - - Parameters - ========== - convert_to: a string either docker or singularity, if a different - - Returns - ======= - converter: the function to do the conversion - - ''' - conversion = self._get_conversion_type(convert_to) - - # Perform conversion - if conversion == "singularity": - return self.docker2singularity - return self.singularity2docker - - - - def _get_conversion_outfile(self, convert_to=None): - '''a helper function to return a conversion temporary output file - based on kind of conversion - - Parameters - ========== - convert_to: a string either docker or singularity, if a different - - ''' - conversion = self._get_conversion_type(convert_to) - prefix = "Singularity" - if conversion == "docker": - prefix = "Dockerfile" - suffix = next(tempfile._get_candidate_names()) - return "%s.%s" %(prefix, suffix) - - - - def _get_conversion_type(self, convert_to=None): - '''a helper function to return the conversion type based on user - preference and input recipe. - - Parameters - ========== - convert_to: a string either docker or singularity (default None) - - ''' - acceptable = ['singularity', 'docker'] - - # Default is to convert to opposite kind - conversion = "singularity" - if self.name == "singularity": - conversion = "docker" - - # Unless the user asks for a specific type - if convert_to is not None and convert_to in acceptable: - conversion = convert_to - return conversion - - - - def _split_line(self, line): - '''clean a line to prepare it for parsing, meaning separation - of commands. We remove newlines (from ends) along with extra spaces. - - Parameters - ========== - line: the string to parse into parts - - Returns - ======= - parts: a list of line pieces, the command is likely first - - ''' - return [x.strip() for x in line.split(' ', 1)] - - - def _clean_line(self, line): - '''clean line will remove comments, and strip the line of newlines - or spaces. - - Parameters - ========== - line: the string to parse into parts - - Returns - ======= - line: a cleaned line + def __str__(self): + ''' show the user the recipe object, along with the type. E.g., + + [spython-recipe][source:Singularity] + [spython-recipe][source:Dockerfile] ''' - # A line that is None should return empty string - line = line or '' - return line.split('#')[0].strip() - - - def _write_script(path, lines, chmod=True): - '''write a script with some lines content to path in the image. This - is done by way of adding echo statements to the install section. - - Parameters - ========== - path: the path to the file to write - lines: the lines to echo to the file - chmod: If true, change permission to make u+x + base = "[spython-recipe]" + if self.source: + base = "%s[source:%s]" %(base, self.source) + return base - ''' - if len(lines) > 0: - lastline = lines.pop() - for line in lines: - self.install.append('echo "%s" >> %s' %path) - self.install.append(lastline) - - if chmod is True: - self.install.append('chmod u+x %s' %path)
      - -Recipe.docker2singularity = docker2singularity -Recipe.singularity2docker = singularity2docker -Recipe._create_section = create_section -Recipe._create_runscript = create_runscript + def __repr__(self): + return self.__str__()
      diff --git a/docs/api/modules/spython/main/parse/singularity.html b/docs/api/modules/spython/main/parse/singularity.html index 6694a8a4..63f53913 100644 --- a/docs/api/modules/spython/main/parse/singularity.html +++ b/docs/api/modules/spython/main/parse/singularity.html @@ -156,13 +156,10 @@

      Source code for spython.main.parse.singularity

      # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -import json -import os import re import sys from spython.logger import bot -from spython.utils import read_file from spython.main.parse.recipe import Recipe @@ -238,7 +235,6 @@

      Source code for spython.main.parse.singularity

      ''' self._write_script('/tests.sh', lines) - testrun = "/bin/bash /tests.sh" self.test = "/bin/bash /tests.sh" @@ -275,17 +271,17 @@

      Source code for spython.main.parse.singularity

      # Comments and Help def _comments(self, lines): - ''' comments is a wrapper for comment, intended to be given a list - of comments. + ''' comments is a wrapper for comment, intended to be given a list + of comments. - Parameters - ========== - lines: the list of lines to parse + Parameters + ========== + lines: the list of lines to parse - ''' - for line in lines: - comment = self._comment(line) - self.comments.append(comment) + ''' + for line in lines: + comment = self._comment(line) + self.comments.append(comment) def _comment(self, line): @@ -480,7 +476,6 @@

      Source code for spython.main.parse.singularity

      self.config = dict() section = None - name = None while len(lines) > 0: @@ -532,10 +527,8 @@

      Source code for spython.main.parse.singularity

      line = line.split('#',1)[0].strip() # Is there a section name? - parts = line.split(' ') - if len(parts) > 1: - name = ' '.join(parts[1:]) - section = re.sub('[%]|(\s+)','',parts[0]).lower() + parts = line.split(' ') + section = re.sub(r'[%]|(\s+)','',parts[0]).lower() if section not in self.config: self.config[section] = [] diff --git a/docs/api/modules/spython/main/pull.html b/docs/api/modules/spython/main/pull.html index 792c74f4..39d8edf3 100644 --- a/docs/api/modules/spython/main/pull.html +++ b/docs/api/modules/spython/main/pull.html @@ -160,9 +160,6 @@

      Source code for spython.main.pull

       from spython.utils import stream_command
       import os
       import re
      -import shutil
      -import sys
      -import tempfile
       
       
      [docs]def pull(self, image=None, @@ -171,8 +168,6 @@

      Source code for spython.main.pull

                ext="simg",
                force=False,
                capture=False,
      -         name_by_commit=False,
      -         name_by_hash=False,
                stream=False):
       
           '''pull will pull a singularity hub or Docker image
      @@ -210,32 +205,20 @@ 

      Source code for spython.main.pull

           if not re.search('^(shub|docker)://', image):
               bot.exit("pull only valid for docker and shub. Use sregistry client.")
       
      -    # Did the user ask for a custom pull folder?
      -    if pull_folder:
      -        self.setenv('SINGULARITY_PULLFOLDER', pull_folder)
      -
           # If we still don't have a custom name, base off of image uri.
      -    # Determine how to tell client to name the image, preference to hash
      -
      -    if name_by_hash is True:
      -        cmd.append('--hash')
      -
      -    elif name_by_commit is True:
      -        cmd.append('--commit')
      -
      -    elif name is None:
      +    if name is None:
               name = self._get_filename(image, ext)
      -        
      -    # Only add name if we aren't naming by hash or commit
      -    if not name_by_commit and not name_by_hash:
       
      -        # Regression Singularity 3.* onward, PULLFOLDER not honored
      -        # https://github.com/sylabs/singularity/issues/2788
      -        if pull_folder and 'version 3' in self.version():
      -            pull_folder_name = os.path.join(pull_folder, os.path.basename(name))
      -            cmd = cmd + ["--name", pull_folder_name]          
      -        else:
      -            cmd = cmd + ["--name", name]
      +    print('name is %s' % name)        
      +
      +    # Regression Singularity 3.* onward, PULLFOLDER not honored
      +    # https://github.com/sylabs/singularity/issues/2788
      +    if pull_folder and 'version 3' in self.version():
      +        final_image = os.path.join(pull_folder, os.path.basename(name))
      +        cmd = cmd + ["--name", final_image]          
      +    else:
      +        final_image = name
      +        cmd = cmd + ["--name", name]
       
           if force is True:
               cmd = cmd + ["--force"]
      @@ -247,27 +230,8 @@ 

      Source code for spython.main.pull

           if name is None:
               name = ''
       
      -    final_image = os.path.join(pull_folder, name)
      -
      -    # Option 1: For hash or commit, need return value to get final_image
      -    if name_by_commit or name_by_hash:
      -
      -        # Set pull to temporary location
      -        tmp_folder = tempfile.mkdtemp()
      -        self.setenv('SINGULARITY_PULLFOLDER', tmp_folder)
      -        self._run_command(cmd, capture=capture)
      -
      -        try:
      -            tmp_image = os.path.join(tmp_folder, os.listdir(tmp_folder)[0])
      -            final_image = os.path.join(pull_folder, os.path.basename(tmp_image))
      -            shutil.move(tmp_image, final_image)
      -            shutil.rmtree(tmp_folder)
      -
      -        except:
      -            bot.error('Issue pulling image with commit or hash, try without?')
      -
      -    # Option 2: Streaming we just run to show user
      -    elif stream is False:
      +    # Option 1: Streaming we just run to show user
      +    if stream is False:
               self._run_command(cmd, capture=capture)
       
           # Option 3: A custom name we can predict (not commit/hash) and can also show
      diff --git a/docs/api/modules/spython/main/run.html b/docs/api/modules/spython/main/run.html
      index 64d6d69a..fb7f880f 100644
      --- a/docs/api/modules/spython/main/run.html
      +++ b/docs/api/modules/spython/main/run.html
      @@ -161,17 +161,16 @@ 

      Source code for spython.main.run

       import json
       
       
      [docs]def run(self, - image = None, - args = None, - app = None, - sudo = False, - writable = False, - contain = False, - bind = None, - stream = False, - nv = False, + image=None, + args=None, + app=None, + sudo=False, + writable=False, + contain=False, + bind=None, + stream=False, + nv=False, return_result=False): - ''' run will run the container, with or withour arguments (which should be provided in a list) @@ -210,6 +209,10 @@

      Source code for spython.main.run

           if isinstance(image, self.instance):
               image = image.get_uri()
       
      +    # If image is still None, not defined by user or previously with client
      +    if image is None:
      +        bot.exit('Please load or provide an image.')
      +
           # Does the user want to use bind paths option?
           if bind is not None:
               cmd += self._generate_bind_list(bind)
      diff --git a/docs/api/modules/spython/oci.html b/docs/api/modules/spython/oci.html
      index 168372fc..e5e1496e 100644
      --- a/docs/api/modules/spython/oci.html
      +++ b/docs/api/modules/spython/oci.html
      @@ -178,15 +178,15 @@ 

      Source code for spython.oci

                   sudo: if init is called with or without sudo, keep a record and use
                         for following commands unless sudo is provided to function.
               '''
      -        super(ImageBase, self).__init__()
      +        super().__init__()
       
               # Will typically be None, unless used outside of Client
               self.container_id = container_id
      -        self.uri = 'oci://'
      +        self.protocol = 'oci'
               self.sudo = sudo
       
               # If bundle is provided, create it
      -        if bundle != None and container_id != None and create:
      +        if bundle is not None and container_id is not None and create:
                   self.bundle = bundle
                   self.create(bundle, container_id, **kwargs)
       
      @@ -204,7 +204,7 @@ 

      Source code for spython.oci

               '''
       
               # The user must provide a container_id, or have one with the client
      -        if container_id == None and self.container_id == None:
      +        if container_id is None and self.container_id is None:
                   bot.exit('You must provide a container_id.')
       
               # Choose whichever is not None, with preference for function provided
      @@ -220,7 +220,7 @@ 

      Source code for spython.oci

       # Naming
       
           def __str__(self):
      -        if self.container_id != None:
      +        if self.container_id is not None:
                   return "[singularity-python-oci:%s]" % self.container_id
               return "[singularity-python-oci]"
       
      @@ -240,7 +240,7 @@ 

      Source code for spython.oci

                  ==========
                  sudo: if None, use self.sudo. Otherwise return sudo.
               '''
      -        if sudo == None:
      +        if sudo is None:
                   sudo = self.sudo
               return sudo
       
      diff --git a/docs/api/modules/spython/tests/test_client.html b/docs/api/modules/spython/tests/test_client.html
      index 8a6306a3..b2599225 100644
      --- a/docs/api/modules/spython/tests/test_client.html
      +++ b/docs/api/modules/spython/tests/test_client.html
      @@ -155,12 +155,10 @@ 

      Source code for spython.tests.test_client

       # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
       
       from spython.utils import get_installdir
      -from spython.logger import bot
       from spython.main import Client
       import unittest
       import tempfile
       import shutil
      -import json
       import os
       
       
      @@ -189,12 +187,19 @@ 

      Source code for spython.tests.test_client

               self.assertTrue(os.path.exists(created_container))
               os.remove(container)
       
      +        print('Testing client.export command')
      +        sandbox = "busybox:1.30.sandbox"
      +        created_sandbox = self.cli.export('docker://busybox:1.30.1')
      +        self.assertEqual(created_sandbox, sandbox)
      +        self.assertTrue(os.path.exists(created_sandbox))
      +        shutil.rmtree(created_sandbox)
      +
               print("Testing client.pull command")
               print("...Case 1: Testing naming pull by image name")
               image = self.cli.pull("shub://vsoch/singularity-images", 
                                     pull_folder=self.tmpdir)
               self.assertTrue(os.path.exists(image))
      -        self.assertTrue('vsoch-singularity-images' in image)
      +        self.assertTrue('singularity-images' in image)
               print(image)
       
               print('Testing client.run command')
      @@ -212,18 +217,20 @@ 

      Source code for spython.tests.test_client

               self.assertTrue(os.path.exists(container))
       
               print('Testing client.execute command')
      -        result = self.cli.execute(container,'ls /')
      +        result = self.cli.execute(container, 'ls /')
               print(result)
               self.assertTrue('tmp\nusr\nvar' in result)
       
               print('Testing client.execute command with return code')
      -        result = self.cli.execute(container,'ls /', return_result=True)
      +        result = self.cli.execute(container, 'ls /', return_result=True)
               print(result)
               self.assertTrue('tmp\nusr\nvar' in result['message'])
               self.assertEqual(result['return_code'], 0)
       
               print("Testing client.inspect command")
      -        labels = self.cli.inspect(container)
      +        result = self.cli.inspect(container)
      +        self.assertEqual(result['type'], 'container')
      +        self.assertTrue('attributes' in result)
       
               os.remove(container)
      diff --git a/docs/api/modules/spython/tests/test_instances.html b/docs/api/modules/spython/tests/test_instances.html index 838afc1a..f4bb7274 100644 --- a/docs/api/modules/spython/tests/test_instances.html +++ b/docs/api/modules/spython/tests/test_instances.html @@ -154,13 +154,10 @@

      Source code for spython.tests.test_instances

      # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
       # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
       
      -from spython.utils import get_installdir
      -from spython.logger import bot
       from spython.main import Client
       import unittest
       import tempfile
       import shutil
      -import json
       import os
       
       
      @@ -175,6 +172,16 @@ 

      Source code for spython.tests.test_instances

      [docs]    def tearDown(self):
               shutil.rmtree(self.tmpdir)
      +
      [docs] def test_instance_class(self): + instance = self.cli.instance('docker://ubuntu', start=False) + self.assertEqual(instance.get_uri(), 'instance://' + instance.name) + self.assertNotEqual(instance.name, '') + + name = 'coolName' + instance = self.cli.instance('docker://busybox:1.30.1', start=False, name=name) + self.assertEqual(instance.get_uri(), 'instance://' + instance.name) + self.assertEqual(instance.name, name)
      +
      [docs] def test_instances(self): print('Pulling testing container') @@ -201,10 +208,10 @@

      Source code for spython.tests.test_instances

      print("...Case 3: Commands to instances")
               result = self.cli.execute(myinstance, ['echo', 'hello'])
      -        self.assertTrue('hello\n' == result)
      +        self.assertEqual(result, 'hello\n')
       
               print('...Case 4: Return value from instance')
      -        result = self.cli.execute(myinstance,'ls /', return_result=True)
      +        result = self.cli.execute(myinstance, 'ls /', return_result=True)
               print(result)
               self.assertTrue('tmp\nusr\nvar' in result['message'])
               self.assertEqual(result['return_code'], 0)
      @@ -215,6 +222,8 @@ 

      Source code for spython.tests.test_instances

      self.assertEqual(instances, [])
               myinstance1 = self.cli.instance(image)
               myinstance2 = self.cli.instance(image)
      +        self.assertTrue(myinstance1 is not None)
      +        self.assertTrue(myinstance2 is not None)
               instances = self.cli.instances()
               self.assertEqual(len(instances), 2)
               self.cli.instance_stopall()
      diff --git a/docs/api/modules/spython/tests/test_utils.html b/docs/api/modules/spython/tests/test_utils.html
      index d56577d2..62377974 100644
      --- a/docs/api/modules/spython/tests/test_utils.html
      +++ b/docs/api/modules/spython/tests/test_utils.html
      @@ -154,11 +154,9 @@ 

      Source code for spython.tests.test_utils

       # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
       # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
       
      -from spython.utils import get_installdir
       import unittest
       import tempfile
       import shutil
      -import json
       import os
       
       print("############################################################ test_utils")
      @@ -180,31 +178,31 @@ 

      Source code for spython.tests.test_utils

               import json
               tmpfile = tempfile.mkstemp()[1]
               os.remove(tmpfile)
      -        write_file(tmpfile,"hello!")
      +        write_file(tmpfile, "hello!")
               self.assertTrue(os.path.exists(tmpfile))        
       
               print("Testing utils.read_file...")
               from spython.utils import read_file
               content = read_file(tmpfile)[0]
      -        self.assertEqual("hello!",content)
      +        self.assertEqual("hello!", content)
       
               from spython.utils import write_json
               print("Testing utils.write_json...")
               print("...Case 1: Providing bad json")
      -        bad_json = {"Wakkawakkawakka'}":[{True},"2",3]}
      +        bad_json = {"Wakkawakkawakka'}": [{True}, "2", 3]}
               tmpfile = tempfile.mkstemp()[1]
               os.remove(tmpfile)        
      -        with self.assertRaises(TypeError) as cm:
      -            write_json(bad_json,tmpfile)
      +        with self.assertRaises(TypeError):
      +            write_json(bad_json, tmpfile)
       
               print("...Case 2: Providing good json")        
      -        good_json = {"Wakkawakkawakka":[True,"2",3]}
      +        good_json = {"Wakkawakkawakka": [True, "2", 3]}
               tmpfile = tempfile.mkstemp()[1]
               os.remove(tmpfile)
      -        write_json(good_json,tmpfile)
      -        with open(tmpfile,'r') as filey:
      +        write_json(good_json, tmpfile)
      +        with open(tmpfile, 'r') as filey:
                   content = json.loads(filey.read())
      -        self.assertTrue(isinstance(content,dict))
      +        self.assertTrue(isinstance(content, dict))
               self.assertTrue("Wakkawakkawakka" in content)
      @@ -225,8 +223,14 @@

      Source code for spython.tests.test_utils

               from spython.utils import get_singularity_version
               version = get_singularity_version()
               self.assertTrue(version != "")
      +        oldValue = os.environ.get('SPYTHON_SINGULARITY_VERSION')
               os.environ['SPYTHON_SINGULARITY_VERSION'] = "3.0"
               version = get_singularity_version()
      +        # Restore for other tests
      +        if oldValue is None:
      +            del os.environ['SPYTHON_SINGULARITY_VERSION']
      +        else:
      +            os.environ['SPYTHON_SINGULARITY_VERSION'] = oldValue
               self.assertTrue(version == "3.0")
      @@ -241,12 +245,26 @@

      Source code for spython.tests.test_utils

               self.assertTrue(whereami.endswith('spython'))
      +
      [docs] def test_split_uri(self): + from spython.utils import split_uri + protocol, image = split_uri('docker://ubuntu') + self.assertEqual(protocol, 'docker') + self.assertEqual(image, 'ubuntu') + + protocol, image = split_uri('http://image/path/with/slash/') + self.assertEqual(protocol, 'http') + self.assertEqual(image, 'image/path/with/slash') + + protocol, image = split_uri('no/proto/') + self.assertEqual(protocol, '') + self.assertEqual(image, 'no/proto')
      +
      [docs] def test_remove_uri(self): print("Testing utils.remove_uri") from spython.utils import remove_uri - self.assertEqual(remove_uri('docker://ubuntu'),'ubuntu') - self.assertEqual(remove_uri('shub://vanessa/singularity-images'),'vanessa/singularity-images')
      - + self.assertEqual(remove_uri('docker://ubuntu'), 'ubuntu') + self.assertEqual(remove_uri('shub://vanessa/singularity-images'), 'vanessa/singularity-images') + self.assertEqual(remove_uri('vanessa/singularity-images'), 'vanessa/singularity-images')
      if __name__ == '__main__': diff --git a/docs/api/modules/spython/utils/fileio.html b/docs/api/modules/spython/utils/fileio.html index 186bfccd..b1826dca 100644 --- a/docs/api/modules/spython/utils/fileio.html +++ b/docs/api/modules/spython/utils/fileio.html @@ -158,14 +158,8 @@

      Source code for spython.utils.fileio

       
       import errno
       import os
      -import re
       import json
       from spython.logger import bot
      -from subprocess import (
      -    Popen,
      -    PIPE,
      -    STDOUT
      -)
       import sys
       
       
      diff --git a/docs/api/modules/spython/utils/terminal.html b/docs/api/modules/spython/utils/terminal.html
      index 315b90bf..6931cff5 100644
      --- a/docs/api/modules/spython/utils/terminal.html
      +++ b/docs/api/modules/spython/utils/terminal.html
      @@ -159,7 +159,6 @@ 

      Source code for spython.utils.terminal

       import os
       import re
       
      -import json
       from spython.logger import bot
       import subprocess
       import sys
      @@ -196,10 +195,9 @@ 

      Source code for spython.utils.terminal

       
       
       
      [docs]def get_singularity_version(): - '''get the singularity client version. Useful in the case that functionality - has changed, etc. Can be "hacked" if needed by exporting - SPYTHON_SINGULARITY_VERSION, which is checked before checking on the - command line. + '''get the full singularity client version as reported by + singularity --version [...]. For Singularity 3.x, this means: + "singularity version 3.0.1-1" ''' version = os.environ.get('SPYTHON_SINGULARITY_VERSION', "") if version == "": @@ -239,9 +237,9 @@

      Source code for spython.utils.terminal

           if sudo is True:
               cmd = ['sudo'] + cmd
       
      -    process = subprocess.Popen(cmd, 
      -                               stdout = subprocess.PIPE, 
      -                               universal_newlines = True)
      +    process = subprocess.Popen(cmd,
      +                               stdout=subprocess.PIPE,
      +                               universal_newlines=True)
           for line in iter(process.stdout.readline, ""):
               if not re.search(no_newline_regexp, line):
                   yield line
      @@ -281,9 +279,9 @@ 

      Source code for spython.utils.terminal

               stdout = subprocess.PIPE
       
           # Use the parent stdout and stderr
      -    process = subprocess.Popen(cmd, 
      -                               stderr = subprocess.PIPE, 
      -                               stdout = stdout)
      +    process = subprocess.Popen(cmd,
      +                               stderr=subprocess.PIPE,
      +                               stdout=stdout)
           lines = ()
           found_match = False
       
      @@ -303,7 +301,7 @@ 

      Source code for spython.utils.terminal

                       found_match = False
       
           output = {'message': lines,
      -              'return_code': process.returncode }
      +              'return_code': process.returncode}
       
           return output
      @@ -326,10 +324,25 @@

      Source code for spython.utils.terminal

                          for e in name if e.isalnum() or e in special_characters)
      +
      [docs]def split_uri(container): + '''Split the uri of a container into the protocol and image part + + An empty protocol is returned if none found. + A trailing slash is removed from the image part. + ''' + parts = container.split('://', 1) + if len(parts) == 2: + protocol, image = parts + else: + protocol = '' + image = parts[0] + return protocol, image.rstrip('/')
      + +
      [docs]def remove_uri(container): '''remove_uri will remove docker:// or shub:// from the uri ''' - return container.replace('docker://', '').replace('shub://', '')
      + return split_uri(container)[1]
      diff --git a/docs/api/objects.inv b/docs/api/objects.inv index d34a2335af8c70c3394a2c0af654aa86e4506c41..ca1f06fd1faa8e028dd4dd40501a9d89e1b617a0 100644 GIT binary patch delta 1923 zcmV-}2YmRb5SS2Jm5$qVL}JkP-vVFEh6oj)yyU za{~O-NrCcqwFmPH;9)uxAn)8>(OoSFsNrr+WnKAAD-(Ilt5TE<0s-UFEU5d%nPBI6 z@VTK(#8Rhe7JoEQ-4PPVWI;`pmb9(`sdxl3s+Giu)jR;t(Sb5sfiRCHv4iE(h|(`} z9!p(b!Ehwu0+4dKm6$Z{g7yh+Et<=poWT|;<4frED4U?7Xc>xy+vNECTwlAe()p|7 zydj&bYuoZgPR=ZlzpZSRt(mH_BFi)tCcpv1XlVJ<!RM$m9~HQ#d7=+qHCrp2%zyDF2GcvBiMNQOn1X?Qm>1dJ{7ThdvN+_>BN`RlTK{ zvNZHk{((w>6VY4wV%2Hb6+E=ZG-y~Ok&gVHu~fCG@Sj`Jw5jALl>VH`<|?KSJf0dMela&5*8u}ApL|#n}E--wAI2u*96DEg%vVWyWUjpwTgJ!(Gn||h!g5vI;l;4>j zzW}yB;+~`B6%F$Jv&?(SY9I=M>gZ_Y^J6d%fCbgD)iH!su>s{@N8rBNoq%KsF{YI8 zqHG!0-GCf(%HhdQ(@pv136QQi0vX0W@#y>tV(=2y@lKJf4T(giG!8K=MPcTXf;!p@ zw14{F`1yFn@2RQYh;h;<7SGJiY{d18VXlU$?mL@hcM~)X4<`&5x0e|~7leaZhFa91 z5-khqQoJ2jJsrMf0$ZZwU+95~PoVaO#ap0*#T|9Mg>`5Vu42K4iIGSJh0m~Tm&2@W zxW+SAwuN?rxQWFY7KI}j4iVl+e;ZAnwSO`bPoZ8=P>u^=99wF%27)Pw);?&7BAT#i zBT+5u-~E(MWel!A`Y24cf@iBZ+QW@qUFVhFu=7e)~V%q|Ufh`n2ef!MGRF{Vqx z-~cfXfmtDRNZB;z92h1kw}uP5NPnG$yanr0Ef-HGG1K=ls7%2$`$Y@shYAjLVf_WS zGFqL(1AKV|IB_O2P~;^;jYz~#q`so%17tURTm*`p!@Z0t_$ifsOZbg0OY9uF#jrxH zjPoeyQDvMKrFr?g?!Pa?`Ie_8M*i&6oHt8Z9iVFAa-?}#4kE2ZCh=I(c6!iw=u zOp$Tkg-fbx!6#Tg6l!>Gb!^3o3G<-*x))-3A!8?BmBi31#1dq{U`-#P?>x{>IBzf# ziP$<`mxHFNo7UUY7QGEnXY6W#`mV|x(KwX6evq|XcqVi#o)a|KC4Um?FYX)DtwHHJ ziz7?30(HreUyTUBq1D%O@(X_uD6HRFCkLuRMwXBj{`u>5;7iXEgXr+Oq3Clj6--;Z z*tZUM=H-H6LbkU$NS@u>t^5v1x3>?fBL`emuPa(+*RQhSVwXI7``epOAKw4|`NP!_ z5=1Ka+$HWNYE|<7s((&>tA1XM$qds<@h8NxlrR%Cvj(2oo&L_ko4ecp{`r?@^0`*c zBmiZC@2;fgnXH?a>;8sjf7IpHBDf4U%G3E1`kGGsRo}k@M|frIq)o$Z<>zv|RMvpw6UjL9>UvlOo`M`8icx-=y`Q>yX^vNu1ZM90O(&_?eMbpX;z^U10Xpr*&`0pmzRa#;wqBI>N-ZwF_IXT z=0NA`^9Vc5!+*~;;}S}pp*he%bw^xbt}<%tN6yL;RfPZ`lTu3rG;@$fE3t9rGulPCb!c??5-;Hua)@ zuCHA<@%-jEZ^-uP+O~X=(rgmM-&QuuHcV|SV42on5`P^i21Bc#p00dz0kp^g3?!4) z&?;HoWA|#L?07VDxH(}a#;hb8T&j-H<1lUlrC&*PY$)Pt?(=bA&va}qdJ{86ht3h0 z={EwQv6 z%(b_mVtCb-_z-1dce;nKe)x75*wJ{U$2?=Q#8y#4GhQ z5?uWOVbPpbsubkSfia4aXQ^`~(+)dFr8=Yd5r0!Ks%j@(O#xL)QGE%%#~j=F`fmDJ zmlWi8cck>r{`dv){T}xeEiG73-#^K`XS_r*lcWSk>pmZXc|bhK#?HVHW?~CUzYf5C z)*XRV4k=}<5?S7GVTJ)U=Ty^^okch1=Ob^mKVUG^w~Sy*l=KTbF!>3UUblD)v@h`)TW?_<8bS*wSidj= zlu`VQ^Jdx2+JZY_T0^n=-DRMxl1kL7hkpc<=j^4xDKuLEx~oByLQ4g!(v%^v8qIP7 zScG*0S8V<*;yY-XU7{k4k4b%1oB*VNTcfE{(rMx zt9bt1ye*|^o!+(;tyl=zpwL~c4Fn4B2KbgNi+<}nyC)QPhYCP6x~CstfZb3HLmS4O z0ABA8@1D-Ihyk=~LgRYAyM||n|(NTy$jL1-n%oS2|Z?)2G_;jEyF@wScn)q zBvEw0RD{4B7dj+w26GM!laxBsg@0Y7wuHPHYhx{E&qT2c?)I=gG}E5&%v3*=32ZX! z&$yG&>J%RE%Oj$Z6H$($$T_KHBz`3I14Ri|k%BK*gSo$5|HzqI8DYV;H zncmA5Am~9QOp7wS^j-Vkm;PSMk{|3BsTw8WtSUYChqftoptdt#=Y#m=`hTzUY*T8l z^+1*88Lq2a-U-E4?sQWIO=8u0(N^d%YC%#3dW-;_>}>**7#j?v20Ku9Padj9FXG#3 zk~wX1clk8SSK0i2nkMv^T|QN}*tc7P0(6kA0`%3eMc=E9Fq^yy)&FMxtDqZE=^yI1 zcmKzMuBGXz>9@tbhdoe<5r2Qjo+@~*kY@+|_rq7sOjcWIm{e%Bw=R3Pt!9`I7MnxE z*lnJEQSQ6b@GecQHl10a(MrE{%-tp%u^>z}M#7lrlByfl3Fi-4dvAOgW3m;y96{-| z6=He!X(wOj#L^4GBP6iyUg%D^nFy)bXwj~h?rAUHMq(14Ya}LACx1>D3?(felqE9H zN}0o(1@+|ug!)VPCd+G3#%3_G3@cEVno^gfLNwHT9VfkLvw_08Q-*4mCY4i$dFJ03 zSO>mr{Sd^No-q`C-jf8=*6yON!wIRHNEt+Q2a%PZRcTKqVtrvk$l|q{{w3G1lKx>E zJ$d`vn@=C!|Ni;I)qfFJq$==p8@XMy*3rACCiboVc{N0{Oee;ln5tajM6$%0W+iv# z;}vi2ZvXq|Uy-Qy8ojE5bP;@ar8G|@UB6s+0fzn251r;=DR9-HEr{a_`sJ=0|Rg7tJFcISE?@0`F) z#+O&8k?o%PJg}c$C+CMb=V9iYq=(pVjwTaFEEm{j#mfU97Dke(O`d7ISuvO=4}MuP aT4b2!8Ha72Hx2Zz$;ijEy#E2B1(;zFG^+{# diff --git a/docs/api/searchindex.js b/docs/api/searchindex.js index 0c1b5576..ec540325 100644 --- a/docs/api/searchindex.js +++ b/docs/api/searchindex.js @@ -1 +1 @@ -Search.setIndex({docnames:["changelog","index","source/modules","source/spython","source/spython.client","source/spython.image","source/spython.image.cmd","source/spython.instance","source/spython.instance.cmd","source/spython.logger","source/spython.main","source/spython.main.base","source/spython.main.parse","source/spython.tests","source/spython.utils"],envversion:{"sphinx.domains.c":1,"sphinx.domains.changeset":1,"sphinx.domains.cpp":1,"sphinx.domains.javascript":1,"sphinx.domains.math":2,"sphinx.domains.python":1,"sphinx.domains.rst":1,"sphinx.domains.std":1,"sphinx.ext.intersphinx":1,"sphinx.ext.todo":1,"sphinx.ext.viewcode":1,sphinx:56},filenames:["changelog.md","index.rst","source/modules.rst","source/spython.rst","source/spython.client.rst","source/spython.image.rst","source/spython.image.cmd.rst","source/spython.instance.rst","source/spython.instance.cmd.rst","source/spython.logger.rst","source/spython.main.rst","source/spython.main.base.rst","source/spython.main.parse.rst","source/spython.tests.rst","source/spython.utils.rst"],objects:{"":{spython:[3,0,0,"-"]},"spython.client":{get_parser:[4,1,1,""],get_subparsers:[4,1,1,""],main:[4,1,1,""],recipe:[4,0,0,"-"],set_verbosity:[4,1,1,""],shell:[4,0,0,"-"],test:[4,0,0,"-"],version:[4,1,1,""]},"spython.client.recipe":{main:[4,1,1,""]},"spython.client.shell":{bpython:[4,1,1,""],ipython:[4,1,1,""],main:[4,1,1,""],python:[4,1,1,""]},"spython.client.test":{main:[4,1,1,""]},"spython.image":{Image:[5,2,1,""],ImageBase:[5,2,1,""],cmd:[6,0,0,"-"]},"spython.image.Image":{get_hash:[5,3,1,""]},"spython.image.ImageBase":{get_uri:[5,3,1,""],parse_image_name:[5,3,1,""],remove_uri:[5,3,1,""]},"spython.image.cmd":{"export":[6,0,0,"-"],create:[6,0,0,"-"],generate_image_commands:[6,1,1,""],importcmd:[6,0,0,"-"],utils:[6,0,0,"-"]},"spython.image.cmd.create":{create:[6,1,1,""]},"spython.image.cmd.export":{"export":[6,1,1,""]},"spython.image.cmd.importcmd":{importcmd:[6,1,1,""]},"spython.image.cmd.utils":{compress:[6,1,1,""],decompress:[6,1,1,""]},"spython.instance":{Instance:[7,2,1,""],cmd:[8,0,0,"-"]},"spython.instance.Instance":{generate_name:[7,3,1,""],get_uri:[7,3,1,""],parse_image_name:[7,3,1,""]},"spython.instance.cmd":{generate_instance_commands:[8,1,1,""],iutils:[8,0,0,"-"],start:[8,0,0,"-"],stop:[8,0,0,"-"]},"spython.instance.cmd.iutils":{get:[8,1,1,""],parse_table:[8,1,1,""]},"spython.instance.cmd.start":{start:[8,1,1,""]},"spython.instance.cmd.stop":{stop:[8,1,1,""]},"spython.logger":{message:[9,0,0,"-"],progress:[9,0,0,"-"],spinner:[9,0,0,"-"]},"spython.logger.message":{SingularityMessage:[9,2,1,""],convert2boolean:[9,1,1,""],get_logging_level:[9,1,1,""],get_user_color_preference:[9,1,1,""]},"spython.logger.message.SingularityMessage":{abort:[9,3,1,""],addColor:[9,3,1,""],critical:[9,3,1,""],custom:[9,3,1,""],debug:[9,3,1,""],emit:[9,3,1,""],emitError:[9,3,1,""],emitOutput:[9,3,1,""],error:[9,3,1,""],exit:[9,3,1,""],get_logs:[9,3,1,""],info:[9,3,1,""],isEnabledFor:[9,3,1,""],is_quiet:[9,3,1,""],log:[9,3,1,""],newline:[9,3,1,""],println:[9,3,1,""],show_progress:[9,3,1,""],spinner:[9,4,1,""],table:[9,3,1,""],useColor:[9,3,1,""],verbose1:[9,3,1,""],verbose2:[9,3,1,""],verbose3:[9,3,1,""],verbose:[9,3,1,""],warning:[9,3,1,""],write:[9,3,1,""]},"spython.logger.progress":{ProgressBar:[9,2,1,""],bar:[9,1,1,""]},"spython.logger.progress.ProgressBar":{done:[9,3,1,""],format_time:[9,3,1,""],show:[9,3,1,""]},"spython.logger.spinner":{Spinner:[9,2,1,""]},"spython.logger.spinner.Spinner":{balloons_cursor:[9,5,1,""],changing_arrows:[9,5,1,""],delay:[9,4,1,""],run:[9,3,1,""],select_generator:[9,3,1,""],spinning:[9,4,1,""],spinning_cursor:[9,5,1,""],start:[9,3,1,""],stop:[9,3,1,""]},"spython.main":{apps:[10,0,0,"-"],base:[11,0,0,"-"],build:[10,0,0,"-"],execute:[10,0,0,"-"],get_client:[10,1,1,""],help:[10,0,0,"-"],inspect:[10,0,0,"-"],instances:[10,0,0,"-"],parse:[12,0,0,"-"],pull:[10,0,0,"-"],run:[10,0,0,"-"]},"spython.main.apps":{apps:[10,1,1,""]},"spython.main.base":{Client:[11,2,1,""],command:[11,0,0,"-"],flags:[11,0,0,"-"],generate:[11,0,0,"-"],logger:[11,0,0,"-"],sutils:[11,0,0,"-"]},"spython.main.base.Client":{RobotNamer:[11,4,1,""],apps:[11,3,1,""],build:[11,3,1,""],debug:[11,4,1,""],execute:[11,3,1,""],help:[11,3,1,""],image:[11,4,1,""],inspect:[11,3,1,""],instance:[11,4,1,""],instance_stopall:[11,3,1,""],instances:[11,3,1,""],load:[11,3,1,""],oci:[11,4,1,""],pull:[11,3,1,""],quiet:[11,4,1,""],run:[11,3,1,""],setenv:[11,3,1,""],version:[11,3,1,""]},"spython.main.base.command":{generate_bind_list:[11,1,1,""],init_command:[11,1,1,""],run_command:[11,1,1,""],send_command:[11,1,1,""]},"spython.main.base.flags":{parse_verbosity:[11,1,1,""]},"spython.main.base.generate":{RobotNamer:[11,2,1,""],main:[11,1,1,""]},"spython.main.base.generate.RobotNamer":{generate:[11,3,1,""]},"spython.main.base.logger":{init_level:[11,1,1,""],println:[11,1,1,""]},"spython.main.base.sutils":{get_filename:[11,1,1,""],get_uri:[11,1,1,""],load:[11,1,1,""],setenv:[11,1,1,""]},"spython.main.build":{build:[10,1,1,""]},"spython.main.execute":{execute:[10,1,1,""]},"spython.main.help":{help:[10,1,1,""]},"spython.main.inspect":{inspect:[10,1,1,""],parse_labels:[10,1,1,""]},"spython.main.instances":{instances:[10,1,1,""],stopall:[10,1,1,""]},"spython.main.parse":{converters:[12,0,0,"-"],docker:[12,0,0,"-"],environment:[12,0,0,"-"],recipe:[12,0,0,"-"],singularity:[12,0,0,"-"]},"spython.main.parse.converters":{create_env_section:[12,1,1,""],create_keyval_section:[12,1,1,""],create_runscript:[12,1,1,""],create_section:[12,1,1,""],docker2singularity:[12,1,1,""],finish_section:[12,1,1,""],singularity2docker:[12,1,1,""],write_lines:[12,1,1,""]},"spython.main.parse.docker":{DockerRecipe:[12,2,1,""]},"spython.main.parse.environment":{parse_env:[12,1,1,""]},"spython.main.parse.recipe":{Recipe:[12,2,1,""]},"spython.main.parse.recipe.Recipe":{convert:[12,3,1,""],docker2singularity:[12,3,1,""],load:[12,3,1,""],parse:[12,3,1,""],save:[12,3,1,""],singularity2docker:[12,3,1,""]},"spython.main.parse.singularity":{SingularityRecipe:[12,2,1,""]},"spython.main.parse.singularity.SingularityRecipe":{load_recipe:[12,3,1,""]},"spython.main.pull":{pull:[10,1,1,""]},"spython.main.run":{run:[10,1,1,""]},"spython.tests":{test_client:[13,0,0,"-"],test_instances:[13,0,0,"-"],test_utils:[13,0,0,"-"]},"spython.tests.test_client":{TestClient:[13,2,1,""]},"spython.tests.test_client.TestClient":{setUp:[13,3,1,""],tearDown:[13,3,1,""],test_commands:[13,3,1,""]},"spython.tests.test_instances":{TestInstances:[13,2,1,""]},"spython.tests.test_instances.TestInstances":{setUp:[13,3,1,""],tearDown:[13,3,1,""],test_instances:[13,3,1,""]},"spython.tests.test_utils":{TestUtils:[13,2,1,""]},"spython.tests.test_utils.TestUtils":{setUp:[13,3,1,""],tearDown:[13,3,1,""],test_check_get_singularity_version:[13,3,1,""],test_check_install:[13,3,1,""],test_get_installdir:[13,3,1,""],test_remove_uri:[13,3,1,""],test_write_read_files:[13,3,1,""]},"spython.utils":{fileio:[14,0,0,"-"],terminal:[14,0,0,"-"]},"spython.utils.fileio":{mkdir_p:[14,1,1,""],read_file:[14,1,1,""],read_json:[14,1,1,""],write_file:[14,1,1,""],write_json:[14,1,1,""]},"spython.utils.terminal":{check_install:[14,1,1,""],format_container_name:[14,1,1,""],get_installdir:[14,1,1,""],get_singularity_version:[14,1,1,""],remove_uri:[14,1,1,""],run_command:[14,1,1,""],stream_command:[14,1,1,""]},spython:{client:[4,0,0,"-"],image:[5,0,0,"-"],instance:[7,0,0,"-"],logger:[9,0,0,"-"],main:[10,0,0,"-"],tests:[13,0,0,"-"],utils:[14,0,0,"-"],version:[3,0,0,"-"]}},objnames:{"0":["py","module","Python module"],"1":["py","function","Python function"],"2":["py","class","Python class"],"3":["py","method","Python method"],"4":["py","attribute","Python attribute"],"5":["py","staticmethod","Python static method"]},objtypes:{"0":"py:module","1":"py:function","2":"py:class","3":"py:method","4":"py:attribute","5":"py:staticmethod"},terms:{"1024mib":6,"95m":9,"boolean":[0,9,10,11],"byte":11,"case":[10,13,14],"char":11,"class":[5,7,9,11,12,13],"default":[0,6,8,9,10,11,12,14],"export":[0,3,5,12,14],"function":[0,4,6,9,10,11,12,13,14],"import":[0,6,10],"int":9,"new":[6,14],"public":[3,12,14],"return":[0,5,7,8,9,10,11,12,13,14],"static":9,"true":[6,7,8,9,10,11,12,14],Added:0,Adding:0,For:[10,11,12],The:[0,1,6,8,10,11,12,14],These:6,Useful:14,Will:9,With:8,__init__:6,_pars:12,abil:0,abort:9,about:[10,11],access:[10,11],account:0,action:[0,11],actual:[5,11],add:[12,14],addcolor:9,added:[0,6,9,10,11],adding:0,addit:[0,6,11],adjust:0,after:13,alia:11,all:[0,5,9,10,11,14],allow:[10,11],along:7,also:[5,11],ani:[10,11,12],anoth:12,api:0,app:[2,3,11],appear:[5,11],append:[12,14],applic:14,appropri:12,arbitari:[10,11],arg:[0,4,8,9,10,11],argpars:11,argument:[0,8,10,11,12],around:4,ascii:9,associ:[10,11],assum:[8,9,13],attempt:[11,14],attribut:[0,12],automat:[10,11],avail:14,back:[11,12,14],backward:0,balloons_cursor:9,bar:[9,14],base:[3,4,5,7,9,10,12,13],bash:12,basic:0,been:0,befor:[5,13,14],behaviour:0,being:9,best:12,between:12,bin:12,bind:[0,8,10,11],bindlist:11,both:[4,11],bpython:4,branch:0,bug:0,build:[0,2,3,11,12],build_fold:[10,11],builder:[10,11,12],built:[10,11],bundl:11,call:[0,8,9,10,11,12],can:[0,3,4,11,12,14],captur:[8,10,11,14],carriage_return:9,chang:[0,14],changelog:1,changing_arrow:9,charact:[9,14],check:[0,9,11,12,13,14],check_instal:14,choos:12,cli:0,client:[0,1,2,3,6,8,10,11,12,14],close:[0,14],cmd:[3,5,7,11,12,14],code:[0,3,10,11,12,14],coincid:0,col_width:9,color:9,column:[8,9],com:[10,11,12],command:[0,3,6,8,10,12,13,14],commit:0,commonli:6,complet:[5,7,9,10,11],compress:6,config:12,configur:9,consol:[0,4,14],contain:[0,1,8,10,11,12,14],content:[1,2],context:[10,11],continu:0,conveni:12,convers:[0,12],convert2boolean:9,convert:[0,3,4,10,11],convert_to:12,copi:[0,3,12,14],copyright:[3,12,14],correct:0,couldn:[10,11],count:9,creat:[3,5,8,9,10,11,12,14],create_entrypoint:12,create_env_sect:12,create_keyval_sect:12,create_runscript:12,create_sect:12,criteria:5,critic:[0,9],current:9,custom:[6,9,10,11,12],data:[10,11,12,14],debug:[6,8,9,10,11],decompress:6,deconstruct:13,defil:[10,11],defin:[4,6,10,11,12,14],delai:9,delim:11,delimit:11,deprec:[0,6],desir:12,dest:0,destin:0,determin:[4,9,12,14],dict:14,dictionari:[4,9,11],differ:[0,10,11],dimens:8,directori:[10,11,13,14],disabl:[10,11],discoveri:12,distribut:[3,12,14],doc:12,docker2singular:12,docker:[0,3,5,7,10,11,14],dockerfil:[0,12],dockerrecip:[0,4,12],document:0,doe:[11,12],doesn:[7,10,11,12],don:[8,10,11,14],done:[8,9,10,11],driver:[10,11],each:[0,8,14],easili:12,either:[4,11,12],emit:9,emiterror:9,emitoutput:9,empti:[0,11],empty_char:9,enabl:9,encod:9,end:[12,14],engin:12,enhanc:0,entir:[10,11],entri:9,entrypoint:12,env:[0,12],environ:[3,4,6,8,9,10,11],environment:9,envlist:12,equal:12,error:[0,9,10,11],estim:12,etc:[8,14],eventu:0,everi:9,exampl:[11,12,14],except:14,exec:[0,11],execut:[0,2,3,11],exercis:13,exist:[5,11],exit:[9,11],expect:[8,10,11],expected_s:9,expos:[6,10,11],express:14,ext3:[6,10,11],ext4:6,ext:[6,10,11],extens:[10,11],fail:0,fals:[0,6,8,9,10,11,12,14],file:[0,3,4,5,6,10,11,12,14],fileio:[2,3],filenam:[11,14],filesystem:[6,10,11],filled_char:9,find:14,finish:12,finish_sect:12,first:[5,9,11,12],fix:[0,10],fixtur:13,flag:[0,3,10],flexibl:0,folder:[10,11],follow:11,forc:[10,11,12],form:[3,12,14],format:[0,12,14],format_container_nam:14,format_tim:9,found:[5,10,11,12,13],from:[0,5,6,7,8,10,11,12,14],fromhead:[0,12],full:[0,6,10,11],full_path:[10,11],fun:[10,11],futur:12,ged:[10,11],gener:[0,3,7,9,10,12],generate_bind_list:11,generate_image_command:[6,11],generate_instance_command:8,generate_nam:7,get:[0,4,5,8,10,11,12,13,14],get_client:10,get_filenam:11,get_hash:5,get_installdir:14,get_log:9,get_logging_level:9,get_pars:4,get_singularity_vers:[11,14],get_subpars:4,get_uri:[5,7,11],get_user_color_prefer:9,github:[0,10,11],give:[4,5,8,10,11],given:[0,6,11],global:11,group:[0,6,8,10,11],guidanc:0,hack:14,haikun:11,handl:[0,5,7],hang:[0,8],has:[0,9,11,12,14],hash:[0,5],have:[0,12,14],header:[0,8,12],help:[2,3,4,11],helper:6,here:0,hide:9,highli:[10,11],histori:9,hold:[6,8],honor:0,hook:13,host:[10,11],how:0,http:[3,10,11,12,14],hub:[10,11],idea:12,ideal:8,ignor:12,imag:[0,1,2,3,4,7,8,10,11,14],image_path:[6,10,11],imagebas:[5,7],imagecli:11,implement:[0,11],import_typ:6,importcmd:[3,5],imprt:6,includ:[0,9,10,11,12],incompat:0,index:1,indic:8,info:9,inform:[9,10,11],init:10,init_command:11,init_level:11,initi:[10,11],input:6,input_sourc:6,inspect:[0,2,3,11],inspir:11,instal:[10,11,12,13,14],instanc:[0,1,2,3,11],instance_stopal:11,instead:[10,11],instruct:12,integ:8,integr:[10,11],intend:[5,12],interact:11,interest:14,interpret:9,ipython:4,is_quiet:9,isenabledfor:9,isol:[10,11],issu:[0,10,11],item:0,iter:[9,10,11],iutil:[3,7],join:[9,12],join_newlin:9,json:[0,8,10,11,14],json_obj:14,jsonifi:8,just:[5,10,11],kei:[9,12],kill:0,know:0,kwarg:7,label:[9,10,11,12],length:[9,11],level:[0,4,5,6,8,9,11],libexecdir:[10,11],licens:[1,3,12,14],like:[0,8,12],line:[0,11,12,14],linux:[10,11],list:[0,8,9,10,11,12,14],load:[0,5,7,10,11,12],load_recip:12,local:11,locat:[10,11],log:[0,9,11],logger:[0,1,2,3,10],logic:12,look:[8,10,11],main:[1,2,3,4,6,8,9],mainli:6,make:[10,11,12],manual:[0,12],map:[10,11],master:1,match:8,md5:5,mean:[10,11,12,14],merg:0,messag:[0,2,3,4,10,11],messagelevel:[6,8,9],metadata:12,method:13,methodnam:13,mib:6,migrat:0,min_level:9,miss:0,mkdir:14,mkdir_p:14,mode:[10,14],modul:[1,2],more:[10,11,12],most:[10,12],mount:[10,11],move:0,mozilla:[3,12,14],mpl:[1,3,12,14],much:11,multipl:0,must:[6,8,9],name:[0,5,7,8,9,10,11,12,14],name_by_commit:[10,11],name_by_hash:[10,11],namespac:[5,7],need:[8,10,11,12,14],neither:12,newlin:[9,14],nicer:14,no_newline_regexp:14,non:[0,11],none:[5,6,7,8,9,10,11,12,14],note:12,now:0,number:[8,9],nvidia:[0,10,11],object:[5,9,10,11,12,14],obtain:[3,12,14],oci:[0,11],off:11,one:[0,3,7,10,11,12,14],onli:[0,5,9,10,11],open:14,opposit:12,option:[4,5,8,9,10,11,14],orb:0,org:[3,12,14],origin:[0,10,12],other:14,otherwis:[0,11],out:9,outfil:4,output:[0,4,8,10,11,12,14],output_fil:12,over:11,overrid:[10,11,12],overriden:11,packag:[0,1,2],pair:12,param:[9,14],paramet:[5,6,7,8,10,11,12,14],parent:[11,14],pars:[0,3,5,7,8,10,11,14],parse_env:12,parse_image_nam:[5,7],parse_label:10,parse_t:8,parse_verbos:11,parser:[0,4],particular:13,pass:[6,8,11],path:[0,5,6,8,10,11,14],paus:0,perform:[10,11,12],pid:[8,10,11],pipe:14,poorli:11,prefer:[10,11],prefix:[9,12],pretti:14,pretty_print:14,print:[0,4,9,10,11,12,14],print_pretti:14,println:[0,9,11],prioriti:5,privat:0,process:11,progess:14,progress:[2,3,14],progressbar:9,prompt:9,properli:[6,14],provid:[6,8,10,11,12,13],pull:[0,2,3,11],pull_fold:[10,11],pwd:[10,11],pypi:0,python:[0,4,11],pythonbuff:12,quiet:[0,6,8,9,10,11,14],raw:[10,11,12],read:[10,11,12,14],read_fil:[13,14],read_json:14,readlin:14,reason:[5,10,11],recip:[0,2,3,10,11],recommend:[10,11],refer:12,registri:[5,7],regular:14,rel:[0,10,11],releas:0,remov:[0,5,8,12,14],remove_image_uri:5,remove_row:8,remove_uri:[5,14],renam:0,repositori:0,repres:0,request:0,requir:[10,11,12],respect:0,result:[0,10,11,12,14],resum:0,return_cod:9,return_json:[8,10,11],return_result:[10,11],revers:0,ride:11,right:0,robot:[7,10,11],robot_nam:[10,11],robotnam:11,root:[10,11],row:[8,9],run:[0,2,3,8,9,11,14],run_command:[11,14],runscript:[10,11,12],runtest:13,runtim:[10,11],same:14,sandbox:[10,11],save:[4,5,12],sci:[10,11],scientif:[10,11],scif:[10,11],screen:[10,11],second:[5,9],section:[0,12],see:[10,11],select:[9,12],select_gener:9,self:[6,8,10,11,12],send:[10,11,14],send_command:11,sens:[10,11],sequenc:12,serv:[4,10,11],set:[4,9,10,11,12,13,14],set_verbos:4,setenv:11,setup:[0,13],share:[10,11],shell:[0,2,3],should:[0,9,10,11,12,13,14],show:[9,10,11],show_progress:9,shub:14,simag:11,simg:[10,11],simpli:[5,7],singl:[8,10,11,12],singular:[0,3,5,6,7,8,10,11,13,14],singularity2dock:12,singularitymessag:9,singularityrecip:[0,4,12],singularitywar:[10,11],size:6,sizein:6,skip:14,sochat:[3,12,14],softwar:[10,11,13,14],some:12,sourc:[0,3,4,5,6,7,8,9,10,11,12,13,14],space:5,special:14,special_charact:14,specif:[10,11,12],specifi:[6,9,10,11,12],sphinx:0,spin:9,spinner:[2,3],spinning_cursor:9,split:[0,5,7],spython:[0,1],spython_singularity_vers:14,src:0,standard:[9,10,11],start:[0,3,7,9,14],statement:0,stderr:[9,11],stdin:6,stdout:[9,11,14],stop:[0,3,7,9,10,11],stopal:10,stream:[0,9,10,11,14],stream_command:14,string:[5,8,10,11],strip:0,structur:14,sub:[10,11],subclass:12,subcommand:6,subject:[3,12,14],submodul:[1,2,5,7],subpackag:[1,2],subpars:[4,12],subprocess:14,success:[0,11],sudo:[6,8,10,11,14],suffix:9,suppli:[7,14],support:[0,6,8,9],suppress:10,sutil:[3,10],symbol:9,system:[6,10,11],tabl:[8,9],table_str:8,take:[10,11,12,14],taken:0,tar:6,teardown:13,tempfil:0,temporari:[6,12],term:[3,12,14],termin:[0,2,3,9,11],test:[0,2,3,10,11,12],test_check_get_singularity_vers:13,test_check_instal:13,test_command:13,test_get_installdir:13,test_remove_uri:13,test_write_read_fil:13,testcas:13,testclient:13,testinst:13,testutil:13,text:[9,10,11,12],them:0,thi:[0,3,4,5,8,10,11,12,14],those:14,tmp:8,tmptar:6,tokenchar:11,tokenlength:11,top:8,total:9,track:0,tupl:8,turn:10,two:12,type:12,typo:0,ubuntu:[5,7,11],uid:[10,11],under:[1,6,9],unittest:13,unset:0,updat:10,uri:[0,5,7,8,10,11,14],url:0,usag:[8,14],use:[0,6,7,10,11,12,14],usecolor:9,used:[6,9,10,11,12,13],useful:[5,11],user:[4,7,8,9,10,11,12,14],uses:[11,14],using:[10,11],usual:9,utf:11,util:[1,2,3,5],valu:[0,10,11,12],vanessa:[3,12,14],variabl:[0,6,8,9,11,12],vault:0,verbos:[6,8,9],verbose1:9,verbose2:9,verbose3:9,version:[0,1,2,4,5,11,13,14],via:[6,8],volum:11,want:8,warn:9,whatev:6,when:[0,8],where:[8,10,11,13],whether:12,which:[9,10,11,14],width:9,within:[10,11],withour:[10,11],without:[12,14],won:14,work:[0,1],wrap:[9,11],wrapper:[4,11],writabl:[10,11],write:[9,10,11,12,14],write_fil:[13,14],write_json:14,write_lin:12,x1b:9,yes:11,yet:11,yield:14,you:[3,10,11,12,14],your:[9,10,11]},titles:["CHANGELOG","Welcome to Singularity Python API\u2019s documentation!","spython","spython package","spython.client package","spython.image package","spython.image.cmd package","spython.instance package","spython.instance.cmd package","spython.logger package","spython.main package","spython.main.base package","spython.main.parse package","spython.tests package","spython.utils package"],titleterms:{"export":6,api:1,app:10,base:11,build:10,changelog:0,client:4,cmd:[6,8],command:11,content:[3,4,5,6,7,8,9,10,11,12,13,14],convert:12,creat:6,docker:12,document:1,environ:12,execut:10,fileio:14,flag:11,gener:11,help:10,imag:[5,6],importcmd:6,indic:1,inspect:10,instanc:[7,8,10],iutil:8,logger:[9,11],main:[10,11,12],master:0,messag:9,modul:[3,4,5,6,7,8,9,10,11,12,13,14],packag:[3,4,5,6,7,8,9,10,11,12,13,14],pars:12,progress:9,pull:10,python:1,recip:[4,12],run:10,shell:4,singular:[1,12],spinner:9,spython:[2,3,4,5,6,7,8,9,10,11,12,13,14],start:8,stop:8,submodul:[3,4,6,8,9,10,11,12,13,14],subpackag:[3,5,7,10],sutil:11,tabl:1,termin:14,test:[4,13],test_client:13,test_inst:13,test_util:13,util:[6,14],version:3,welcom:1}}) \ No newline at end of file +Search.setIndex({docnames:["changelog","index","source/modules","source/spython","source/spython.client","source/spython.image","source/spython.image.cmd","source/spython.instance","source/spython.instance.cmd","source/spython.logger","source/spython.main","source/spython.main.base","source/spython.main.parse","source/spython.tests","source/spython.utils"],envversion:{"sphinx.domains.c":1,"sphinx.domains.changeset":1,"sphinx.domains.cpp":1,"sphinx.domains.javascript":1,"sphinx.domains.math":2,"sphinx.domains.python":1,"sphinx.domains.rst":1,"sphinx.domains.std":1,"sphinx.ext.intersphinx":1,"sphinx.ext.todo":1,"sphinx.ext.viewcode":1,sphinx:56},filenames:["changelog.md","index.rst","source/modules.rst","source/spython.rst","source/spython.client.rst","source/spython.image.rst","source/spython.image.cmd.rst","source/spython.instance.rst","source/spython.instance.cmd.rst","source/spython.logger.rst","source/spython.main.rst","source/spython.main.base.rst","source/spython.main.parse.rst","source/spython.tests.rst","source/spython.utils.rst"],objects:{"":{spython:[3,0,0,"-"]},"spython.client":{get_parser:[4,1,1,""],main:[4,1,1,""],recipe:[4,0,0,"-"],set_verbosity:[4,1,1,""],shell:[4,0,0,"-"],test:[4,0,0,"-"],version:[4,1,1,""]},"spython.client.recipe":{main:[4,1,1,""]},"spython.client.shell":{bpython:[4,1,1,""],ipython:[4,1,1,""],main:[4,1,1,""],prepare_client:[4,1,1,""],python:[4,1,1,""]},"spython.client.test":{main:[4,1,1,""]},"spython.image":{Image:[5,2,1,""],ImageBase:[5,2,1,""],cmd:[6,0,0,"-"]},"spython.image.Image":{get_hash:[5,3,1,""]},"spython.image.ImageBase":{parse_image_name:[5,3,1,""]},"spython.image.cmd":{"export":[6,0,0,"-"],create:[6,0,0,"-"],generate_image_commands:[6,1,1,""],importcmd:[6,0,0,"-"],utils:[6,0,0,"-"]},"spython.image.cmd.create":{create:[6,1,1,""]},"spython.image.cmd.export":{"export":[6,1,1,""]},"spython.image.cmd.importcmd":{importcmd:[6,1,1,""]},"spython.image.cmd.utils":{compress:[6,1,1,""],decompress:[6,1,1,""]},"spython.instance":{Instance:[7,2,1,""],cmd:[8,0,0,"-"]},"spython.instance.Instance":{generate_name:[7,3,1,""],get_uri:[7,3,1,""],parse_image_name:[7,3,1,""]},"spython.instance.cmd":{generate_instance_commands:[8,1,1,""],iutils:[8,0,0,"-"],start:[8,0,0,"-"],stop:[8,0,0,"-"]},"spython.instance.cmd.iutils":{get:[8,1,1,""],parse_table:[8,1,1,""]},"spython.instance.cmd.start":{start:[8,1,1,""]},"spython.instance.cmd.stop":{stop:[8,1,1,""]},"spython.logger":{message:[9,0,0,"-"],progress:[9,0,0,"-"],spinner:[9,0,0,"-"]},"spython.logger.message":{SingularityMessage:[9,2,1,""],convert2boolean:[9,1,1,""],get_logging_level:[9,1,1,""],get_user_color_preference:[9,1,1,""]},"spython.logger.message.SingularityMessage":{abort:[9,3,1,""],addColor:[9,3,1,""],critical:[9,3,1,""],custom:[9,3,1,""],debug:[9,3,1,""],emit:[9,3,1,""],emitError:[9,3,1,""],emitOutput:[9,3,1,""],error:[9,3,1,""],exit:[9,3,1,""],get_logs:[9,3,1,""],info:[9,3,1,""],isEnabledFor:[9,3,1,""],is_quiet:[9,3,1,""],log:[9,3,1,""],newline:[9,3,1,""],println:[9,3,1,""],show_progress:[9,3,1,""],spinner:[9,4,1,""],table:[9,3,1,""],useColor:[9,3,1,""],verbose1:[9,3,1,""],verbose2:[9,3,1,""],verbose3:[9,3,1,""],verbose:[9,3,1,""],warning:[9,3,1,""],write:[9,3,1,""]},"spython.logger.progress":{ProgressBar:[9,2,1,""],bar:[9,1,1,""]},"spython.logger.progress.ProgressBar":{done:[9,3,1,""],format_time:[9,3,1,""],show:[9,3,1,""]},"spython.logger.spinner":{Spinner:[9,2,1,""]},"spython.logger.spinner.Spinner":{balloons_cursor:[9,5,1,""],changing_arrows:[9,5,1,""],delay:[9,4,1,""],run:[9,3,1,""],select_generator:[9,3,1,""],spinning:[9,4,1,""],spinning_cursor:[9,5,1,""],start:[9,3,1,""],stop:[9,3,1,""]},"spython.main":{apps:[10,0,0,"-"],base:[11,0,0,"-"],build:[10,0,0,"-"],execute:[10,0,0,"-"],get_client:[10,1,1,""],help:[10,0,0,"-"],inspect:[10,0,0,"-"],instances:[10,0,0,"-"],parse:[12,0,0,"-"],pull:[10,0,0,"-"],run:[10,0,0,"-"]},"spython.main.apps":{apps:[10,1,1,""]},"spython.main.base":{Client:[11,2,1,""],command:[11,0,0,"-"],flags:[11,0,0,"-"],generate:[11,0,0,"-"],logger:[11,0,0,"-"],sutils:[11,0,0,"-"]},"spython.main.base.Client":{"export":[11,3,1,""],RobotNamer:[11,4,1,""],apps:[11,3,1,""],build:[11,3,1,""],debug:[11,4,1,""],execute:[11,3,1,""],help:[11,3,1,""],image:[11,4,1,""],inspect:[11,3,1,""],instance:[11,4,1,""],instance_stopall:[11,3,1,""],instances:[11,3,1,""],load:[11,3,1,""],oci:[11,4,1,""],pull:[11,3,1,""],quiet:[11,4,1,""],run:[11,3,1,""],setenv:[11,3,1,""],version:[11,3,1,""]},"spython.main.base.command":{generate_bind_list:[11,1,1,""],init_command:[11,1,1,""],run_command:[11,1,1,""],send_command:[11,1,1,""]},"spython.main.base.flags":{parse_verbosity:[11,1,1,""]},"spython.main.base.generate":{RobotNamer:[11,2,1,""],main:[11,1,1,""]},"spython.main.base.generate.RobotNamer":{generate:[11,3,1,""]},"spython.main.base.logger":{init_level:[11,1,1,""],println:[11,1,1,""]},"spython.main.base.sutils":{get_filename:[11,1,1,""],get_uri:[11,1,1,""],load:[11,1,1,""],setenv:[11,1,1,""]},"spython.main.build":{build:[10,1,1,""]},"spython.main.execute":{execute:[10,1,1,""]},"spython.main.help":{help:[10,1,1,""]},"spython.main.inspect":{inspect:[10,1,1,""],parse_labels:[10,1,1,""]},"spython.main.instances":{instances:[10,1,1,""],stopall:[10,1,1,""]},"spython.main.parse":{converters:[12,0,0,"-"],docker:[12,0,0,"-"],environment:[12,0,0,"-"],recipe:[12,0,0,"-"],singularity:[12,0,0,"-"]},"spython.main.parse.converters":{create_env_section:[12,1,1,""],create_keyval_section:[12,1,1,""],create_runscript:[12,1,1,""],create_section:[12,1,1,""],docker2singularity:[12,1,1,""],finish_section:[12,1,1,""],singularity2docker:[12,1,1,""],write_lines:[12,1,1,""]},"spython.main.parse.docker":{DockerRecipe:[12,2,1,""]},"spython.main.parse.environment":{parse_env:[12,1,1,""]},"spython.main.parse.recipe":{Recipe:[12,2,1,""]},"spython.main.parse.singularity":{SingularityRecipe:[12,2,1,""]},"spython.main.parse.singularity.SingularityRecipe":{load_recipe:[12,3,1,""]},"spython.main.pull":{pull:[10,1,1,""]},"spython.main.run":{run:[10,1,1,""]},"spython.tests":{test_client:[13,0,0,"-"],test_instances:[13,0,0,"-"],test_utils:[13,0,0,"-"]},"spython.tests.test_client":{TestClient:[13,2,1,""]},"spython.tests.test_client.TestClient":{setUp:[13,3,1,""],tearDown:[13,3,1,""],test_commands:[13,3,1,""]},"spython.tests.test_instances":{TestInstances:[13,2,1,""]},"spython.tests.test_instances.TestInstances":{setUp:[13,3,1,""],tearDown:[13,3,1,""],test_instance_class:[13,3,1,""],test_instances:[13,3,1,""]},"spython.tests.test_utils":{TestUtils:[13,2,1,""]},"spython.tests.test_utils.TestUtils":{setUp:[13,3,1,""],tearDown:[13,3,1,""],test_check_get_singularity_version:[13,3,1,""],test_check_install:[13,3,1,""],test_get_installdir:[13,3,1,""],test_remove_uri:[13,3,1,""],test_split_uri:[13,3,1,""],test_write_read_files:[13,3,1,""]},"spython.utils":{fileio:[14,0,0,"-"],terminal:[14,0,0,"-"]},"spython.utils.fileio":{mkdir_p:[14,1,1,""],read_file:[14,1,1,""],read_json:[14,1,1,""],write_file:[14,1,1,""],write_json:[14,1,1,""]},"spython.utils.terminal":{check_install:[14,1,1,""],format_container_name:[14,1,1,""],get_installdir:[14,1,1,""],get_singularity_version:[14,1,1,""],remove_uri:[14,1,1,""],run_command:[14,1,1,""],split_uri:[14,1,1,""],stream_command:[14,1,1,""]},spython:{client:[4,0,0,"-"],image:[5,0,0,"-"],instance:[7,0,0,"-"],logger:[9,0,0,"-"],main:[10,0,0,"-"],tests:[13,0,0,"-"],utils:[14,0,0,"-"],version:[3,0,0,"-"]}},objnames:{"0":["py","module","Python module"],"1":["py","function","Python function"],"2":["py","class","Python class"],"3":["py","method","Python method"],"4":["py","attribute","Python attribute"],"5":["py","staticmethod","Python static method"]},objtypes:{"0":"py:module","1":"py:function","2":"py:class","3":"py:method","4":"py:attribute","5":"py:staticmethod"},terms:{"1024mib":6,"95m":9,"boolean":[0,9,10,11],"byte":11,"case":[10,13],"char":11,"class":[5,7,9,11,12,13],"default":[0,6,8,9,10,11,12,14],"export":[0,3,5,11,12],"function":[0,4,6,9,10,11,12,13,14],"import":[0,6,10],"int":9,"new":[6,11,14],"public":[3,12,14],"return":[0,5,7,8,9,10,11,12,13,14],"static":9,"true":[6,7,8,9,10,11,12,14],Added:0,Adding:0,For:[10,11,12,14],The:[0,1,6,8,10,11,12,14],These:6,Useful:[],Will:9,With:8,__init__:6,_pars:[],abil:0,abort:9,about:[10,11],accept:0,access:[10,11],account:0,action:[0,11],actual:[5,11],add:[12,14],addcolor:9,added:[0,6,9,10,11],adding:0,addit:[0,6,11],adjust:0,after:[11,13],alia:11,all:[0,9,10,11,14],allow:[10,11],along:7,also:[11,12],ani:[10,11,12],anoth:[],api:0,app:[2,3,11],appear:11,append:[12,14],applic:[11,14],appropri:[],arbitari:[10,11],arg:[0,4,8,9,10,11],argpars:11,argument:[0,8,10,11,12],around:4,ascii:9,associ:[10,11],assum:[8,9,13],attempt:[11,14],attribut:[0,12],automat:[10,11],avail:[11,14],back:[0,11,12,14],backward:0,balloons_cursor:9,bar:[9,14],base:[0,3,4,5,7,9,10,12,13],bash:12,basic:0,been:0,befor:13,behaviour:0,being:9,best:12,between:[],bin:12,bind:[0,8,10,11],bindlist:11,bootstrap:11,both:[4,11],bourn:11,bpython:4,branch:0,bug:0,build:[0,2,3,11,12],build_fold:[10,11],builder:[10,11,12],built:[10,11],bundl:11,call:[0,8,9,10,11,12],can:[0,3,4,11,12,14],capabl:11,captur:[8,10,11,14],carriage_return:9,chang:0,changelog:1,changing_arrow:9,charact:[9,14],check:[0,9,11,13,14],check_instal:14,choos:[],cli:0,client:[0,1,2,3,6,8,10,11,14],close:[0,14],cmd:[3,5,7,11,12,14],code:[0,3,10,11,12,14],coincid:0,col_width:9,color:9,column:[8,9],com:[10,11,12],command:[0,3,6,8,10,12,13,14],commit:0,commonli:6,complet:[5,7,9,10,11],compress:6,condit:0,config:12,configur:9,consol:[0,4,14],contain:[0,1,8,10,11,12,14],content:[1,2],context:[10,11],continu:0,conveni:[],convers:0,convert2boolean:9,convert:[0,3,4,10,11],convert_to:[],copi:[0,3,12,14],copyright:[3,12,14],correct:0,couldn:[10,11],count:9,crash:0,creat:[3,5,8,9,10,11,12,14],create_entrypoint:12,create_env_sect:12,create_keyval_sect:12,create_runscript:12,create_sect:12,criteria:5,critic:[0,9],crypto:11,current:9,custom:[6,9,10,11,12],data:[10,11,12,14],debug:[6,8,9,10,11],decompress:6,deconstruct:13,defil:[10,11],defin:[0,4,6,10,11,12,14],delai:9,delim:11,delimit:11,deprec:[0,6,11],deriv:11,descriptor:11,desir:[],dest:0,destin:0,determin:[4,9,12,14],dict:14,dictionari:[9,11],differ:[0,10,11],dimens:8,directori:[0,10,11,13,14],disabl:[10,11],discoveri:[],displai:11,distribut:[3,12,14],doc:12,docker2singular:12,docker:[0,3,5,7,10,11,14],dockerfil:[0,12],dockerpars:[4,12],dockerrecip:[0,12],dockerwrit:4,document:0,doe:[11,12],doesn:[7,10,11,12],don:[8,10,11,14],done:[8,9,10,11],driver:[10,11],each:[0,8,14],easili:[],either:[4,11,12],emb:4,emit:9,emiterror:9,emitoutput:9,empti:[0,11,14],empty_char:9,enabl:9,encod:9,end:[12,14],engin:12,enhanc:0,entir:[10,11],entri:9,entrypoint:12,env:[0,12],environ:[3,4,6,8,9,10,11],environment:9,envlist:12,equal:12,error:[0,9,10,11],estim:12,etc:8,eventu:0,everi:9,exampl:[11,12,14],except:14,exec:[0,11],execut:[0,2,3,11],exercis:13,exist:[5,11],exit:[9,11],expect:[8,10,11],expected_s:9,expos:[6,10,11],express:14,ext3:[6,10,11],ext4:6,ext:[6,10,11],extens:[10,11],fail:0,fals:[0,6,8,9,10,11,12,14],file:[0,3,4,5,6,10,11,12,14],fileio:[2,3],filenam:[11,14],filesystem:[6,10,11],filled_char:9,find:14,finish:12,finish_sect:12,first:[5,9,11,12],fix:[0,10],fixtur:13,flag:[0,3,10],flexibl:0,folder:[10,11],follow:11,forc:[0,10,11,12],form:[3,12,14],format:[0,12,14],format_container_nam:14,format_tim:9,found:[10,11,12,13,14],free:12,from:[0,5,6,7,8,10,11,14],fromhead:0,full:[0,6,10,11,14],full_path:[10,11],fun:[10,11],futur:[],ged:[10,11],gener:[0,3,7,9,10],generate_bind_list:11,generate_image_command:[6,11],generate_instance_command:8,generate_nam:7,get:[0,5,8,10,11,13,14],get_client:10,get_filenam:11,get_hash:5,get_installdir:14,get_log:9,get_logging_level:9,get_pars:4,get_singularity_vers:[11,14],get_subpars:[],get_uri:[7,11],get_user_color_prefer:9,github:[0,10,11],give:[4,5,8,10,11],given:[0,6,11],global:11,group:[0,6,8,10,11],guidanc:0,hack:[],haikun:11,handl:[0,5,7],hang:[0,8],has:[0,9,11],hash:[0,5],have:[0,11,12,14],header:[0,8,12],help:[2,3,4,11],helper:6,here:0,hide:9,highli:[10,11],histori:9,hold:[6,8],honor:0,hook:13,host:[10,11],how:0,http:[3,10,11,12,14],hub:[10,11],idea:12,ideal:8,ignor:12,imag:[0,1,2,3,4,7,8,10,11,14],image_path:[6,10,11],imagebas:[5,7],imagecli:11,implement:[0,11],import_typ:6,importcmd:[3,5],imprt:6,includ:[0,9,10,11,12],incompat:0,increas:11,index:1,indic:8,info:9,inform:[9,10,11],init:10,init_command:11,init_level:11,initi:[10,11],input:6,input_sourc:6,inspect:[0,2,3,11],inspir:11,instal:[10,11,12,13,14],instanc:[0,1,2,3,11],instance_stopal:11,instead:[10,11],instruct:[],integ:8,integr:[10,11],intend:[5,12],interact:[11,12],interest:14,interpret:9,ipython:4,is_quiet:9,isenabledfor:9,isol:[10,11],issu:[0,10,11],item:0,iter:[9,10,11],iutil:[3,7],join:[9,12],join_newlin:9,json:[0,8,10,11,14],json_obj:14,jsonifi:8,just:[10,11],kei:[9,12],kill:0,know:0,kwarg:7,label:[9,10,11,12],last:0,launch:11,length:[9,11],level:[0,4,5,6,8,9,11],libexecdir:[10,11],licens:[1,3,12,14],like:[0,8,12],line:[0,11,12,14],lint:11,linux:[10,11],list:[0,8,9,10,11,12,14],load:[0,5,7,10,11,12],load_recip:12,local:11,locat:[10,11,12],log:[0,9,11],logger:[0,1,2,3,10],logic:12,look:[8,10,11],main:[1,2,3,4,6,8,9],mainli:6,make:[10,11],manag:11,manual:0,map:[10,11],master:1,match:8,md5:5,mean:[10,11,14],merg:0,messag:[0,2,3,4,10,11],messagelevel:[6,8,9],metadata:11,method:13,methodnam:13,mib:6,migrat:0,min_level:9,miss:0,mkdir:14,mkdir_p:14,mode:[10,14],modul:[1,2],more:[0,10,11,12],most:10,mount:[10,11],move:0,mozilla:[3,12,14],mpl:[1,3,12,14],much:11,multipl:0,must:[6,8,9,11],name:[0,7,8,9,10,11,12,14],name_by_commit:[],name_by_hash:[],namespac:[5,7],need:[8,10,11,12,14],neither:12,newlin:[9,14],nicer:14,no_newline_regexp:14,non:[0,11],none:[5,6,7,8,9,10,11,12,14],normal:11,note:12,now:0,number:[8,9],nvidia:[0,10,11],object:[5,9,10,11,12,14],obtain:[3,12,14],oci:[0,11],off:[],one:[0,3,7,10,11,12,14],onli:[0,5,9,10,11],open:14,opposit:[],option:[0,4,8,9,10,11,14],orb:0,org:[3,12,14],origin:[0,10,12],other:14,otherwis:[0,11],out:9,outfil:4,output:[0,4,8,10,11,12,14],output_fil:11,over:11,overrid:[10,11],overriden:11,packag:[0,1,2],pair:12,param:[9,14],paramet:[0,5,6,7,8,10,11,12,14],parent:[11,14],pars:[0,3,5,7,8,10,11,14],parse_env:12,parse_image_nam:[5,7],parse_label:10,parse_t:8,parse_verbos:11,parser:[0,4,12],part:14,particular:13,pass:[6,8,11],path:[0,5,6,8,10,11,14],paus:0,perform:[10,11],persist:11,pid:[8,10,11],pipe:[11,14],point:0,poorli:11,popualt:12,prefer:[10,11],prefix:[9,12],prepar:4,prepare_cli:4,pretti:14,pretty_print:14,print:[0,4,9,10,11,12,14],print_pretti:14,println:[0,9,11],prioriti:5,privat:0,process:11,progess:14,progress:[2,3,14],progressbar:9,prompt:9,properli:[6,14],protocol:14,provid:[6,8,10,11,13],pull:[0,2,3,11],pull_fold:[10,11],pwd:[10,11],pypi:0,python:[0,4,11],pythonbuff:12,quiet:[0,6,8,9,10,11,14],raw:[10,11],read:[10,11,14],read_fil:[13,14],read_json:14,readlin:14,reason:[5,10,11],recip:[0,2,3,10,11],recommend:[10,11],refactor:0,refer:12,registri:[5,7],regular:14,rel:[0,10,11],releas:0,relev:0,remov:[0,8,12,14],remove_image_uri:[],remove_row:8,remove_uri:14,renam:0,replac:11,report:14,repositori:0,repres:0,request:0,requir:[10,11],respect:[0,12],result:[0,10,11,14],resum:0,return_cod:9,return_json:[8,10,11],return_result:[10,11],revers:0,ride:11,right:0,robot:[7,10,11],robot_nam:[10,11],robotnam:11,root:[10,11],row:[8,9],run:[0,2,3,8,9,11,14],run_command:[11,14],runscript:[0,10,11,12],runtest:13,runtim:[10,11],same:14,sandbox:[10,11],save:[4,5,12],sci:[10,11],scientif:[10,11],scif:[10,11],screen:[10,11],second:[5,9],section:[0,12],see:[10,11],select:9,select_gener:9,self:[6,8,10,11,12],selftest:11,send:[10,11,14],send_command:11,sens:[10,11],sequenc:12,serv:[4,10,11],set:[4,9,10,11,12,13,14],set_verbos:4,setenv:11,setup:[0,13],share:[10,11],shell:[0,2,3,11],should:[0,9,10,11,13,14],show:[9,10,11],show_progress:9,shub:14,sif:11,siflist:11,sign:11,signatur:11,silent:11,simag:11,simg:[10,11],simpli:[5,7],singl:[8,10,11,12],singular:[0,3,5,6,7,8,10,11,13,14],singularity2dock:12,singularitymessag:9,singularitypars:[4,12],singularityrecip:[0,12],singularitywar:[10,11],size:6,sizein:6,skip:14,slash:14,sochat:[3,12,14],softwar:[10,11,13,14],some:[0,11],sourc:[0,3,4,5,6,7,8,9,10,11,12,13,14],space:[],special:14,special_charact:14,specif:[10,11],specifi:[6,9,10,11],sphinx:0,spin:9,spinner:[2,3],spinning_cursor:9,split:[0,5,7,14],split_uri:14,spython:[0,1],spython_singularity_vers:[],src:0,standard:[9,10,11],start:[0,3,7,9,14],statement:0,stderr:[9,11],stdin:6,stdout:[9,11,14],stop:[0,3,7,9,10,11],stopal:10,stream:[0,9,10,11,14],stream_command:14,string:[8,10,11],strip:0,structur:14,sub:[10,11],subclass:12,subcommand:6,subject:[3,12,14],submodul:[1,2,5,7],subpackag:[1,2],subpars:[],subprocess:14,success:[0,11],sudo:[0,6,8,10,11,14],suffix:9,summari:11,suppli:[7,14],support:[0,6,8,9],suppress:[10,11],sutil:[3,10],symbol:9,system:[6,10,11],tabl:[8,9],table_str:8,take:[10,11,12,14],taken:0,tar:[6,11],teardown:13,tempfil:0,temporari:[6,11],term:[3,12,14],termin:[0,2,3,9,11],test:[0,2,3,10,11],test_check_get_singularity_vers:13,test_check_instal:13,test_command:13,test_get_installdir:13,test_instance_class:13,test_remove_uri:13,test_split_uri:13,test_write_read_fil:13,testcas:13,testclient:13,testinst:13,testscript:11,testutil:13,text:[9,10,11],them:0,thi:[0,3,4,5,8,10,11,12,14],those:14,tmp:8,tmptar:6,tokenchar:11,tokenlength:11,top:8,total:9,track:0,trail:14,tupl:8,turn:10,two:[],type:12,typo:0,ubuntu:[5,7,11],uid:[10,11],under:[1,6,9],unittest:13,unset:0,updat:10,uri:[0,5,7,8,10,11,14],url:0,usag:[8,11,14],use:[0,6,7,10,11,12,14],usecolor:9,used:[6,9,10,11,12,13],useful:[5,11],user:[4,7,8,9,10,11,12,14],uses:[11,14],using:[10,11],usual:9,utf:11,util:[1,2,3,5],valu:[0,10,11,12],vanessa:[3,12,14],variabl:[0,6,8,9,11],vault:0,verbos:[6,8,9,11],verbose1:9,verbose2:9,verbose3:9,verifi:11,version:[0,1,2,4,5,11,13,14],via:[6,8],volum:11,want:8,warn:[0,9],whatev:6,when:[0,8],where:[8,10,11,13],whether:[],which:[9,10,11],width:9,within:[10,11],withour:[10,11],without:[12,14],won:14,work:[0,1],wrap:[9,11],wrapper:[4,11],writabl:[10,11],write:[9,10,11,12,14],write_fil:[13,14],write_json:14,write_lin:12,writer:[0,4,12],x1b:9,yes:11,yet:11,yield:14,you:[3,10,11,12,14],your:[9,10,11]},titles:["CHANGELOG","Welcome to Singularity Python API\u2019s documentation!","spython","spython package","spython.client package","spython.image package","spython.image.cmd package","spython.instance package","spython.instance.cmd package","spython.logger package","spython.main package","spython.main.base package","spython.main.parse package","spython.tests package","spython.utils package"],titleterms:{"export":6,api:1,app:10,base:11,build:10,changelog:0,client:4,cmd:[6,8],command:11,content:[3,4,5,6,7,8,9,10,11,12,13,14],convert:12,creat:6,docker:12,document:1,environ:12,execut:10,fileio:14,flag:11,gener:11,help:10,imag:[5,6],importcmd:6,indic:1,inspect:10,instanc:[7,8,10],iutil:8,logger:[9,11],main:[10,11,12],master:0,messag:9,modul:[3,4,5,6,7,8,9,10,11,12,13,14],packag:[3,4,5,6,7,8,9,10,11,12,13,14],pars:12,progress:9,pull:10,python:1,recip:[4,12],run:10,shell:4,singular:[1,12],spinner:9,spython:[2,3,4,5,6,7,8,9,10,11,12,13,14],start:8,stop:8,submodul:[3,4,6,8,9,10,11,12,13,14],subpackag:[3,5,7,10],sutil:11,tabl:1,termin:14,test:[4,13],test_client:13,test_inst:13,test_util:13,util:[6,14],version:3,welcom:1}}) \ No newline at end of file diff --git a/docs/api/source/spython.client.html b/docs/api/source/spython.client.html index bf6c876c..f413e9c9 100644 --- a/docs/api/source/spython.client.html +++ b/docs/api/source/spython.client.html @@ -182,9 +182,10 @@

      Submodules
      spython.client.recipe.main(args, options, parser)[source]
      -

      This function serves as a wrapper around the DockerRecipe and -SingularityRecipe converters. We can either save to file if -args.outfile is defined, or print to the console if not.

      +

      This function serves as a wrapper around the DockerParser, +SingularityParser, DockerWriter, and SingularityParser converters. +We can either save to file if args.outfile is defined, or print +to the console if not.

      @@ -193,7 +194,8 @@

      Submodules
      spython.client.shell.bpython(image)[source]
      -
      +

      give the user a bpython shell

      +
      @@ -206,10 +208,17 @@

      Submodulesspython.client.shell.main(args, options, parser)[source]

      +
      +
      +spython.client.shell.prepare_client(image)[source]
      +

      prepare a client to embed in a shell with recipe parsers and writers.

      +
      +
      spython.client.shell.python(image)[source]
      -
      +

      give the user a python shell

      +

      @@ -227,12 +236,6 @@

      Submodulesspython.client.get_parser()[source]
      -
      -
      -spython.client.get_subparsers(parser)[source]
      -

      get_subparser will get a dictionary of subparsers, to help with printing help

      -
      -
      spython.client.main()[source]
      diff --git a/docs/api/source/spython.image.html b/docs/api/source/spython.image.html index 7f5d7fe0..24db7e33 100644 --- a/docs/api/source/spython.image.html +++ b/docs/api/source/spython.image.html @@ -212,13 +212,6 @@

      Subpackages class spython.image.ImageBase[source]

      Bases: object

      -
      -
      -get_uri(image)[source]
      -

      get the uri of an image, or the string (optional) that appears before -://. Optional. If none found, returns ‘’

      -
      -
      parse_image_name(image)[source]
      @@ -231,13 +224,6 @@

      Subpackages -
      -remove_uri(image)[source]
      -

      remove_image_uri will return just the image name. -this will also remove all spaces from the uri.

      -

      -

      diff --git a/docs/api/source/spython.instance.cmd.html b/docs/api/source/spython.instance.cmd.html index 3966c2a3..0e064d30 100644 --- a/docs/api/source/spython.instance.cmd.html +++ b/docs/api/source/spython.instance.cmd.html @@ -206,7 +206,7 @@

      Submodules

      spython.instance.cmd.start module

      -spython.instance.cmd.start.start(self, image=None, name=None, args=None, sudo=False, options=[], capture=False)[source]
      +spython.instance.cmd.start.start(self, image=None, name=None, args=None, sudo=False, options=None, capture=False)[source]

      start an instance. This is done by default when an instance is created.

      Parameters
      diff --git a/docs/api/source/spython.main.base.html b/docs/api/source/spython.main.base.html index f93c2151..f0e8f251 100644 --- a/docs/api/source/spython.main.base.html +++ b/docs/api/source/spython.main.base.html @@ -260,6 +260,45 @@

      Submodules

      spython.main.base.flags module

      +
      +
      GLOBAL OPTIONS:

      -d|–debug Print debugging information +-h|–help Display usage summary +-s|–silent Only print errors +-q|–quiet Suppress all normal output

      +
      +
      +
      --version
      +

      Show application version

      +
      +
      +
      +

      -v|–verbose Increase verbosity +1 +-x|–sh-debug Print shell wrapper debugging information

      +
      +
      GENERAL COMMANDS:

      help Show additional help for a command or container +selftest Run some self tests for singularity install

      +
      +
      CONTAINER USAGE COMMANDS:

      exec Execute a command within container +run Launch a runscript within container +shell Run a Bourne shell within container +test Launch a testscript within container

      +
      +
      CONTAINER MANAGEMENT COMMANDS:

      apps List available apps within a container +bootstrap Deprecated use build instead +build Build a new Singularity container +check Perform container lint checks +inspect Display container’s metadata +mount Mount a Singularity container image +pull Pull a Singularity/Docker container to $PWD +siflist list data object descriptors of a SIF container image +sign Sign a group of data objects in container +verify Verify the crypto signature of group of data objects in container

      +
      +
      COMMAND GROUPS:

      capability User’s capabilities management command group +image Container image command group +instance Persistent instance command group

      +
      +
      spython.main.base.flags.parse_verbosity(self, args)[source]
      @@ -338,14 +377,13 @@

      Submodules

      spython.main.base.sutils module

      -spython.main.base.sutils.get_filename(self, image=None, ext='simg')[source]
      -

      return an image filename based on the image uri. If an image uri is -not specified, we look for the loaded image.

      +spython.main.base.sutils.get_filename(self, image, ext='sif', pwd=True)[source] +

      return an image filename based on the image uri.

      Parameters
        -
      • image (the uri to base off of)

      • ext (the extension to use)

      • +
      • pwd (derive a filename for the pwd)

      @@ -414,7 +452,7 @@

      Submodules
      -build(recipe=None, image=None, isolated=False, sandbox=False, writable=False, build_folder=None, robot_name=False, ext='simg', sudo=True, stream=False)
      +build(recipe=None, image=None, isolated=False, sandbox=False, writable=False, build_folder=None, robot_name=False, ext='simg', sudo=True, stream=False, force=False)

      build a singularity image, optionally for an isolated build (requires sudo). If you specify to stream, expect the image name and an iterator to be returned.

      @@ -463,6 +501,23 @@

      Submodules +
      +export(image_path, pipe=False, output_file=None, command=None, sudo=False)
      +

      export will export an image, sudo must be used. If we have Singularity +versions after 3, export is replaced with building into a sandbox.

      +
      +
      Parameters
      +
        +
      • image_path (full path to image)

      • +
      • pipe (export to pipe and not file (default, False))

      • +
      • output_file (if pipe=False, export tar to this file. If not specified,)

      • +
      • will generate temporary directory.

      • +
      +
      +
      +

      +
      help(command=None)
      @@ -553,7 +608,7 @@

      Submodules
      -pull(image=None, name=None, pull_folder='', ext='simg', force=False, capture=False, name_by_commit=False, name_by_hash=False, stream=False)
      +pull(image=None, name=None, pull_folder='', ext='simg', force=False, capture=False, stream=False)

      pull will pull a singularity hub or Docker image

      Parameters
      diff --git a/docs/api/source/spython.main.html b/docs/api/source/spython.main.html index fa6bf28c..38b7760c 100644 --- a/docs/api/source/spython.main.html +++ b/docs/api/source/spython.main.html @@ -234,7 +234,7 @@

      Submodules

      spython.main.build module

      -spython.main.build.build(self, recipe=None, image=None, isolated=False, sandbox=False, writable=False, build_folder=None, robot_name=False, ext='simg', sudo=True, stream=False)[source]
      +spython.main.build.build(self, recipe=None, image=None, isolated=False, sandbox=False, writable=False, build_folder=None, robot_name=False, ext='simg', sudo=True, stream=False, force=False)[source]

      build a singularity image, optionally for an isolated build (requires sudo). If you specify to stream, expect the image name and an iterator to be returned.

      @@ -368,7 +368,7 @@

      Submodules

      spython.main.pull module

      -spython.main.pull.pull(self, image=None, name=None, pull_folder='', ext='simg', force=False, capture=False, name_by_commit=False, name_by_hash=False, stream=False)[source]
      +spython.main.pull.pull(self, image=None, name=None, pull_folder='', ext='simg', force=False, capture=False, stream=False)[source]

      pull will pull a singularity hub or Docker image

      Parameters
      diff --git a/docs/api/source/spython.main.parse.html b/docs/api/source/spython.main.parse.html index b58a0283..8fb21f4d 100644 --- a/docs/api/source/spython.main.parse.html +++ b/docs/api/source/spython.main.parse.html @@ -330,103 +330,16 @@

      Submodulesclass spython.main.parse.recipe.Recipe(recipe=None)[source]

      Bases: object

      a recipe includes an environment, labels, runscript or command, -and install sequence. This object is subclassed by a Singularity or -Docker recipe, and can be used to convert between the two. The user -can read in one recipe type to convert to another, or provide raw -commands and metadata for generating a recipe.

      +and install sequence. This object is interacted with by a Parser +(intended to popualte the recipe with content) and a Writer (intended +to write a recipe to file). The parsers and writers are located in +parsers.py, and writers.py, respectively. The user is also free to use +the recipe class to build recipes.

      Parameters
      -

      recipe (the original recipe file, parsed by the subclass either) – DockerRecipe or SingularityRecipe

      +

      recipe (the original recipe file, parsed by the subclass either) – DockerParser or SingularityParser

      -
      -
      -convert(convert_to=None, runscript='/bin/bash', force=False)[source]
      -

      This is a convenience function for the user to easily call to get -the most likely desired result, conversion to the opposite format. -We choose the selection based on the recipe name - meaning that we -perform conversion with default based on recipe name. If the recipe -object is DockerRecipe, we convert to Singularity. If the recipe -object is SingularityRecipe, we convert to Docker. The user can -override this by setting the variable convert_to

      -
      -
      Parameters
      -
        -
      • convert_to (can be manually forced (docker or singularity))

      • -
      • runscript (default runscript (entrypoint) to use)

      • -
      • force (if True, override discovery from Dockerfile)

      • -
      -
      -
      -
      - -
      -
      -docker2singularity(runscript='/bin/bash', force=False)
      -

      docker2singularity will return a Singularity build recipe based on -a the loaded recipe object. It doesn’t take any arguments as the -recipe object contains the sections, and the calling function -determines saving / output logic.

      -
      - -
      -
      -load(recipe)[source]
      -

      load a recipe file into the client, first performing checks, and -then parsing the file.

      -
      -
      Parameters
      -

      recipe (the original recipe file, parsed by the subclass either) – DockerRecipe or SingularityRecipe

      -
      -
      -
      - -
      -
      -parse()[source]
      -

      parse is the base function for parsing the recipe, whether it be -a Dockerfile or Singularity recipe. The recipe is read in as lines, -and saved to a list if needed for the future. If the client has -it, the recipe type specific _parse function is called.

      -

      Instructions for making a client subparser:

      -
      -

      It should have a main function _parse that parses a list of lines -from some recipe text file into the appropriate sections, e.g.,

      -

      self.fromHeader -self.environ -self.labels -self.install -self.files -self.test -self.entrypoint

      -
      -
      - -
      -
      -save(output_file=None, convert_to=None, runscript='/bin/bash', force=False)[source]
      -

      save will convert a recipe to a specified format (defaults to the -opposite of the recipe type originally loaded, (e.g., docker–> -singularity and singularity–>docker) and write to an output file, -if specified. If not specified, a temporary file is used.

      -
      -
      Parameters
      -
        -
      • output_file (the file to save to, not required (estimates default))

      • -
      • convert_to (can be manually forced (docker or singularity))

      • -
      • runscript (default runscript (entrypoint) to use)

      • -
      • force (if True, override discovery from Dockerfile)

      • -
      -
      -
      -
      - -
      -
      -singularity2docker(runscript='/bin/bash', force=False)
      -

      convert a Singularity recipe to a (best estimated) Dockerfile

      -
      -

      diff --git a/docs/api/source/spython.tests.html b/docs/api/source/spython.tests.html index afbd17a3..0faa68ab 100644 --- a/docs/api/source/spython.tests.html +++ b/docs/api/source/spython.tests.html @@ -196,6 +196,11 @@

      Submodules +
      +test_instance_class()[source]
      +
      +
      test_instances()[source]
      @@ -247,6 +252,11 @@

      Submodulestest_remove_uri()[source]

      +
      +
      +test_split_uri()[source]
      +
      +
      test_write_read_files()[source]
      diff --git a/docs/api/source/spython.utils.html b/docs/api/source/spython.utils.html index a69b48ff..a12dd069 100644 --- a/docs/api/source/spython.utils.html +++ b/docs/api/source/spython.utils.html @@ -251,10 +251,9 @@

      Submodules
      spython.utils.terminal.get_singularity_version()[source]
      -

      get the singularity client version. Useful in the case that functionality -has changed, etc. Can be “hacked” if needed by exporting -SPYTHON_SINGULARITY_VERSION, which is checked before checking on the -command line.

      +

      get the full singularity client version as reported by +singularity –version […]. For Singularity 3.x, this means: +“singularity version 3.0.1-1”

      @@ -283,6 +282,14 @@

      Submodules +
      +spython.utils.terminal.split_uri(container)[source]
      +

      Split the uri of a container into the protocol and image part

      +

      An empty protocol is returned if none found. +A trailing slash is removed from the image part.

      +

      +
      spython.utils.terminal.stream_command(cmd, no_newline_regexp='Progess', sudo=False)[source]
      diff --git a/spython/client/__init__.py b/spython/client/__init__.py index 9ac8e3b7..4e564eb6 100644 --- a/spython/client/__init__.py +++ b/spython/client/__init__.py @@ -19,11 +19,11 @@ def get_parser(): add_help=False) # Global Options - parser.add_argument('--debug','-d', dest="debug", + parser.add_argument('--debug', '-d', dest="debug", help="use verbose logging to debug.", default=False, action='store_true') - parser.add_argument('--quiet','-q', dest="quiet", + parser.add_argument('--quiet', '-q', dest="quiet", help="suppress all normal output", default=False, action='store_true') diff --git a/spython/client/shell.py b/spython/client/shell.py index c5d0794d..f9625c34 100644 --- a/spython/client/shell.py +++ b/spython/client/shell.py @@ -12,9 +12,9 @@ def main(args, options, parser): if len(options) > 0: image = options.pop(0) - lookup = { 'ipython': ipython, - 'python': python, - 'bpython': bpython } + lookup = {'ipython': ipython, + 'python': python, + 'bpython': bpython} shells = ['ipython', 'python', 'bpython'] @@ -51,8 +51,13 @@ def ipython(image): def bpython(image): '''give the user a bpython shell ''' - import bpython client = prepare_client(image) + + try: + import bpython + except ImportError: + return python(image) + bpython.embed(locals_={'client': client}) def python(image): diff --git a/spython/image/cmd/__init__.py b/spython/image/cmd/__init__.py index 5c36ad43..85dc1c14 100644 --- a/spython/image/cmd/__init__.py +++ b/spython/image/cmd/__init__.py @@ -28,8 +28,8 @@ class ImageClient(object): group = "image" from spython.main.base.logger import println - from spython.main.base.command import ( init_command, run_command ) - from .utils import ( compress, decompress ) + from spython.main.base.command import (init_command, run_command) + from .utils import (compress, decompress) from .create import create from .importcmd import importcmd from .export import export diff --git a/spython/image/cmd/create.py b/spython/image/cmd/create.py index 34f8eddf..2b155bec 100644 --- a/spython/image/cmd/create.py +++ b/spython/image/cmd/create.py @@ -9,7 +9,7 @@ import os from spython.logger import bot -def create(self,image_path, size=1024, sudo=False): +def create(self, image_path, size=1024, sudo=False): '''create will create a a new image Parameters @@ -22,14 +22,13 @@ def create(self,image_path, size=1024, sudo=False): from spython.utils import check_install check_install() - cmd = self.init_command('image.create') - cmd = cmd + ['--size', str(size), image_path ] + cmd = cmd + ['--size', str(size), image_path] - output = self.run_command(cmd,sudo=sudo) + output = self.run_command(cmd, sudo=sudo) self.println(output) if not os.path.exists(image_path): - bot.exit("Could not create image %s" %image_path) + bot.exit("Could not create image %s" % image_path) return image_path diff --git a/spython/image/cmd/utils.py b/spython/image/cmd/utils.py index a7ae8f41..d99fbe57 100644 --- a/spython/image/cmd/utils.py +++ b/spython/image/cmd/utils.py @@ -25,7 +25,7 @@ def decompress(self, image_path, quiet=True): if not os.path.exists(image_path): bot.exit("Cannot find image %s" %image_path) - extracted_file = image_path.replace('.gz','') - cmd = ['gzip','-d','-f', image_path] + extracted_file = image_path.replace('.gz', '') + cmd = ['gzip', '-d', '-f', image_path] self.run_command(cmd, quiet=quiet) # exits if return code != 0 return extracted_file diff --git a/spython/instance/__init__.py b/spython/instance/__init__.py index 8f888fdd..c76cffdc 100644 --- a/spython/instance/__init__.py +++ b/spython/instance/__init__.py @@ -44,7 +44,7 @@ def generate_name(self, name=None): # If no name provided, use robot name if name is None: name = self.RobotNamer.generate() - self.name = name.replace('-','_') + self.name = name.replace('-', '_') def parse_image_name(self, image): diff --git a/spython/instance/cmd/__init__.py b/spython/instance/cmd/__init__.py index ea09a42b..283545ce 100644 --- a/spython/instance/cmd/__init__.py +++ b/spython/instance/cmd/__init__.py @@ -19,7 +19,7 @@ def generate_instance_commands(): from spython.utils import run_command as run_cmd # run_command uses run_cmd, but wraps to catch error - from spython.main.base.command import ( init_command, run_command ) + from spython.main.base.command import (init_command, run_command) from spython.main.base.generate import RobotNamer from .start import start from .stop import stop diff --git a/spython/instance/cmd/iutils.py b/spython/instance/cmd/iutils.py index f6a3cdd7..c5707df3 100644 --- a/spython/instance/cmd/iutils.py +++ b/spython/instance/cmd/iutils.py @@ -63,7 +63,7 @@ def get(self, name, return_json=False, quiet=False): # Prepare json result from table - header = ['daemon_name','pid','container_image'] + header = ['daemon_name', 'pid', 'container_image'] instances = parse_table(output['message'][0], header) # Does the user want instance objects instead? diff --git a/spython/instance/cmd/start.py b/spython/instance/cmd/start.py index 08e895cd..bf12ab1e 100644 --- a/spython/instance/cmd/start.py +++ b/spython/instance/cmd/start.py @@ -25,8 +25,7 @@ def start(self, image=None, name=None, args=None, sudo=False, options=None, capt singularity [...] instance.start [...] ''' - from spython.utils import ( run_command, - check_install ) + from spython.utils import (run_command, check_install) check_install() # If name provided, over write robot (default) diff --git a/spython/instance/cmd/stop.py b/spython/instance/cmd/stop.py index cd3c8f3e..449fa569 100644 --- a/spython/instance/cmd/stop.py +++ b/spython/instance/cmd/stop.py @@ -20,8 +20,7 @@ def stop(self, name=None, sudo=False): singularity [...] instance.stop [...] ''' - from spython.utils import ( check_install, - run_command ) + from spython.utils import (check_install, run_command) check_install() subgroup = 'instance.stop' diff --git a/spython/logger/message.py b/spython/logger/message.py index dc7b7f93..12931b0a 100644 --- a/spython/logger/message.py +++ b/spython/logger/message.py @@ -276,7 +276,7 @@ def table(self, rows, col_width=2): not, a numbered list is used. ''' - labels = [str(x) for x in range(1,len(rows)+1)] + labels = [str(x) for x in range(1, len(rows) + 1)] if isinstance(rows, dict): labels = list(rows.keys()) rows = list(rows.values()) diff --git a/spython/logger/progress.py b/spython/logger/progress.py index 666cfc6c..4da64069 100644 --- a/spython/logger/progress.py +++ b/spython/logger/progress.py @@ -44,15 +44,15 @@ def __init__(self, label='', width=32, hide=None, empty_char=BAR_EMPTY_CHAR, self.hide = not STREAM.isatty() except AttributeError: # output does not support isatty() self.hide = True - self.empty_char = empty_char - self.filled_char = filled_char + self.empty_char = empty_char + self.filled_char = filled_char self.expected_size = expected_size - self.every = every - self.start = time.time() - self.ittimes = [] - self.eta = 0 - self.etadelta = time.time() - self.etadisp = self.format_time(self.eta) + self.every = every + self.start = time.time() + self.ittimes = [] + self.eta = 0 + self.etadelta = time.time() + self.etadisp = self.format_time(self.eta) self.last_progress = 0 if self.expected_size: self.show(0) diff --git a/spython/main/__init__.py b/spython/main/__init__.py index 499a5b9a..52118eb8 100644 --- a/spython/main/__init__.py +++ b/spython/main/__init__.py @@ -29,10 +29,10 @@ def get_client(quiet=False, debug=False): from .execute import execute from .help import help from .inspect import inspect - from .instances import ( instances, stopall ) # global instance commands + from .instances import (instances, stopall) # global instance commands from .run import run from .pull import pull - from .export import ( export, _export ) + from .export import (export, _export) # Actions Client.apps = apps diff --git a/spython/main/apps.py b/spython/main/apps.py index 4003a26c..7927253c 100644 --- a/spython/main/apps.py +++ b/spython/main/apps.py @@ -26,7 +26,7 @@ def apps(self, image=None, full_path=False, root=''): if image is None: image = self._get_uri() - cmd = self._init_command('apps') + [ image ] + cmd = self._init_command('apps') + [image] output = self._run_command(cmd) if full_path is True: @@ -34,6 +34,6 @@ def apps(self, image=None, full_path=False, root=''): if len(output) > 0: output = ''.join(output).split('\n') - output = ['%s%s' %(root,x) for x in output if x] + output = ['%s%s' %(root, x) for x in output if x] return output diff --git a/spython/main/base/__init__.py b/spython/main/base/__init__.py index b4644c7e..4dd8207f 100644 --- a/spython/main/base/__init__.py +++ b/spython/main/base/__init__.py @@ -12,24 +12,35 @@ get_singularity_version ) +from .command import ( + generate_bind_list, + init_command, + run_command +) +from .flags import parse_verbosity +from .sutils import ( + get_uri, + load, + setenv, + get_filename +) +from .logger import ( + println, + init_level +) +from .generate import RobotNamer + import json import sys import os import re - -from .command import ( generate_bind_list, init_command, run_command ) -from .flags import parse_verbosity -from .sutils import ( get_uri, load, setenv, get_filename ) -from .logger import ( println, init_level ) -from .generate import RobotNamer - class Client: def __str__(self): base = "[singularity-python]" if hasattr(self, 'simage'): - if self.simage.image not in [None,'']: + if self.simage.image not in [None, '']: base = "%s[%s]" %(base, self.simage) return base diff --git a/spython/main/base/logger.py b/spython/main/base/logger.py index 30c9612f..a387831e 100644 --- a/spython/main/base/logger.py +++ b/spython/main/base/logger.py @@ -35,7 +35,7 @@ def println(self, output, quiet=False): quiet: a runtime variable to over-ride the default. ''' - if isinstance(output,bytes): + if isinstance(output, bytes): output = output.decode('utf-8') if self.quiet is False and quiet is False: print(output) diff --git a/spython/main/base/sutils.py b/spython/main/base/sutils.py index aefcc0c5..6f122a4e 100644 --- a/spython/main/base/sutils.py +++ b/spython/main/base/sutils.py @@ -55,7 +55,7 @@ def get_filename(self, image, ext='sif', pwd=True): ''' if pwd is True: image = os.path.basename(image) - image = re.sub('^.*://','', image) + image = re.sub('^.*://', '', image) if not image.endswith(ext): image = "%s.%s" %(image, ext) return image diff --git a/spython/main/execute.py b/spython/main/execute.py index d7883925..eb20fbd9 100644 --- a/spython/main/execute.py +++ b/spython/main/execute.py @@ -11,16 +11,15 @@ def execute(self, - image = None, - command = None, - app = None, - writable = False, - contain = False, - bind = None, - stream = False, - nv = False, + image=None, + command=None, + app=None, + writable=False, + contain=False, + bind=None, + stream=False, + nv=False, return_result=False): - ''' execute: send a command to a container Parameters diff --git a/spython/main/help.py b/spython/main/help.py index ebf6a1bf..b41d9fe2 100644 --- a/spython/main/help.py +++ b/spython/main/help.py @@ -17,7 +17,7 @@ def help(self, command=None): from spython.utils import check_install check_install() - cmd = ['singularity','--help'] + cmd = ['singularity', '--help'] if command is not None: cmd.append(command) help = self._run_command(cmd) diff --git a/spython/main/inspect.py b/spython/main/inspect.py index c004da8f..c51fd34f 100644 --- a/spython/main/inspect.py +++ b/spython/main/inspect.py @@ -33,12 +33,12 @@ def inspect(self, image=None, json=True, app=None, quiet=True): if app is not None: cmd = cmd + ['--app', app] - options = ['e','d','l','r','hf','t'] + options = ['e', 'd', 'l', 'r', 'hf', 't'] # After Singularity 3.0, helpfile was changed to H from if "version 3" in self.version(): - options = ['e','d','l','r','H','t'] + options = ['e', 'd', 'l', 'r', 'H', 't'] for x in options: cmd.append('-%s' % x) diff --git a/spython/main/instances.py b/spython/main/instances.py index a133f0e4..36281d62 100644 --- a/spython/main/instances.py +++ b/spython/main/instances.py @@ -7,7 +7,7 @@ from spython.logger import bot -from spython.utils import ( run_command ) +from spython.utils import run_command def instances(self, name=None, return_json=False, quiet=False): '''list instances. For Singularity, this is provided as a command sub @@ -57,7 +57,7 @@ def instances(self, name=None, return_json=False, quiet=False): # Prepare json result from table - header = ['daemon_name','pid','container_image'] + header = ['daemon_name', 'pid', 'container_image'] instances = parse_table(output['message'][0], header) # Does the user want instance objects instead? @@ -89,7 +89,7 @@ def instances(self, name=None, return_json=False, quiet=False): bot.info('No instances found.') # If we are given a name, return just one - if name is not None and instances not in [None,[]]: + if name is not None and instances not in [None, []]: if len(instances) == 1: instances = instances[0] diff --git a/spython/main/parse/docker.py b/spython/main/parse/docker.py deleted file mode 100644 index 2eda8a62..00000000 --- a/spython/main/parse/docker.py +++ /dev/null @@ -1,469 +0,0 @@ - -# Copyright (C) 2017-2019 Vanessa Sochat. - -# This Source Code Form is subject to the terms of the -# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed -# with this file, You can obtain one at http://mozilla.org/MPL/2.0/. - -import json -import os -import re - -from .environment import parse_env -from .recipe import Recipe -from spython.logger import bot - -class DockerRecipe(Recipe): - - def __init__(self, recipe=None): - '''a Docker recipe parses a Docker Recipe into the expected fields of - labels, environment, and install/runtime commands. We save working - directory as we parse, and the last one can be added to the runscript - of a Singularity recipe. - - Parameters - ========== - recipe: the recipe file (Dockerfile) to parse - - ''' - self.name = "docker" - super(DockerRecipe, self).__init__(recipe) - - -# Setup for each Parser - - def _setup(self, action, line): - ''' replace the command name from the group, alert the user of content, - and clean up empty spaces - ''' - bot.debug('[in] %s' % line) - - # Replace ACTION at beginning - line = re.sub('^%s' % action, '', line) - - # Handle continuation lines without ACTION by padding with leading space - line = " " + line - - # Split into components - return [x for x in self._split_line(line) if x not in ['', None]] - - -# From Parser - - def _from(self, line): - ''' get the FROM container image name from a FROM line! - - Parameters - ========== - line: the line from the recipe file to parse for FROM - - ''' - fromHeader = self._setup('FROM', line) - - # Singularity does not support AS level - self.fromHeader = re.sub("AS .+", "", fromHeader[0], flags=re.I) - - if "scratch" in self.fromHeader: - bot.warning('scratch is no longer available on Docker Hub.') - bot.debug('FROM %s' %self.fromHeader) - - -# Run and Test Parser - - def _run(self, line): - ''' everything from RUN goes into the install list - - Parameters - ========== - line: the line from the recipe file to parse for FROM - - ''' - line = self._setup('RUN', line) - self.install += line - - - def _test(self, line): - ''' A healthcheck is generally a test command - - Parameters - ========== - line: the line from the recipe file to parse for FROM - - ''' - self.test = self._setup('HEALTHCHECK', line) - - -# Arg Parser - - def _arg(self, line): - '''singularity doesn't have support for ARG, so instead will issue - a warning to the console for the user to export the variable - with SINGULARITY prefixed at build. - - Parameters - ========== - line: the line from the recipe file to parse for ARG - - ''' - line = self._setup('ARG', line) - bot.warning("ARG is not supported for Singularity! To get %s" %line[0]) - bot.warning("in the container, on host export SINGULARITY_%s" %line[0]) - -# Env Parser - - def _env(self, line): - '''env will parse a line that beings with ENV, indicative of one or - more environment variables. - - Parameters - ========== - line: the line from the recipe file to parse for ADD - - ''' - line = self._setup('ENV', line) - - # Extract environment (list) from the line - environ = parse_env(line) - - # Add to global environment, run during install - self.install += environ - - # Also define for global environment - self.environ += environ - - -# Add and Copy Parser - - - def _copy(self, lines): - '''parse_add will copy multiple files from one location to another. - This likely will need tweaking, as the files might need to be - mounted from some location before adding to the image. - The add command is done for an entire directory. It is also - possible to have more than one file copied to a destination: - https://docs.docker.com/engine/reference/builder/#copy - e.g.: / - ''' - lines = self._setup('COPY', lines) - - for line in lines: - values = line.split(" ") - topath = values.pop() - for frompath in values: - self._add_files(frompath, topath) - - - def _add(self, lines): - '''Add can also handle https, and compressed files. - - Parameters - ========== - line: the line from the recipe file to parse for ADD - - ''' - lines = self._setup('ADD', lines) - - for line in lines: - values = line.split(" ") - frompath = values.pop(0) - - # Custom parsing for frompath - - # If it's a web address, add to install routine to get - if frompath.startswith('http'): - for topath in values: - self._parse_http(frompath, topath) - - # Add the file, and decompress in install - elif re.search("[.](gz|gzip|bz2|xz)$", frompath.strip()): - for topath in values: - self._parse_archive(frompath, topath) - - # Just add the files - else: - for topath in values: - self._add_files(frompath, topath) - - -# File Handling - - def _add_files(self, source, dest): - '''add files is the underlying function called to add files to the - list, whether originally called from the functions to parse archives, - or https. We make sure that any local references are changed to - actual file locations before adding to the files list. - - Parameters - ========== - source: the source - dest: the destiation - ''' - - def expandPath(path): - return os.getcwd() if path == "." else path - - # Warn the user Singularity doesn't support expansion - if '*' in source: - bot.warning("Singularity doesn't support expansion, * found in %s" % source) - - # Warning if file/folder (src) doesn't exist - if not os.path.exists(source): - bot.warning("%s doesn't exist, ensure exists for build" % source) - - # The pair is added to the files as a list - self.files.append([expandPath(source), expandPath(dest)]) - - - def _parse_http(self, url, dest): - '''will get the filename of an http address, and return a statement - to download it to some location - - Parameters - ========== - url: the source url to retrieve with curl - dest: the destination folder to put it in the image - - ''' - file_name = os.path.basename(url) - download_path = "%s/%s" %(dest, file_name) - command = "curl %s -o %s" %(url, download_path) - self.install.append(command) - - - def _parse_archive(self, targz, dest): - '''parse_targz will add a line to the install script to extract a - targz to a location, and also add it to the files. - - Parameters - ========== - targz: the targz to extract - dest: the location to extract it to - - ''' - - # Add command to extract it - self.install.append("tar -zvf %s %s" %(targz, dest)) - - # Ensure added to container files - return self._add_files(targz, dest) - - -# Comments and Default - - def _comment(self, line): - '''Simply add the line to the install as a comment. This function is - equivalent to default, but added in the case we need future custom - parsing (meaning a comment is different from a line. - - Parameters - ========== - line: the line from the recipe file to parse to INSTALL - - ''' - self.install.append(line) - - - def _default(self, line): - '''the default action assumes a line that is either a command (a - continuation of a previous, for example) or a comment. - - Parameters - ========== - line: the line from the recipe file to parse to INSTALL - ''' - if line.strip().startswith('#'): - return self._comment(line) - self.install.append(line) - - -# Ports and Volumes - - def _volume(self, line): - '''We don't have logic for volume for Singularity, so we add as - a comment in the install, and a metadata value for the recipe - object - - Parameters - ========== - line: the line from the recipe file to parse to INSTALL - - ''' - volumes = self._setup('VOLUME', line) - if len(volumes) > 0: - self.volumes += volumes - return self._comment("# %s" %line) - - - def _expose(self, line): - '''Again, just add to metadata, and comment in install. - - Parameters - ========== - line: the line from the recipe file to parse to INSTALL - - ''' - ports = self._setup('EXPOSE', line) - if len(ports) > 0: - self.ports += ports - return self._comment("# %s" %line) - - -# Working Directory - - def _workdir(self, line): - '''A Docker WORKDIR command simply implies to cd to that location - - Parameters - ========== - line: the line from the recipe file to parse for WORKDIR - - ''' - # Save the last working directory to add to the runscript - workdir = self._setup('WORKDIR', line) - self.workdir = "cd %s" %(''.join(workdir)) - self.install.append(''.join(self.workdir)) - - -# Entrypoint and Command - - def _cmd(self, line): - '''_cmd will parse a Dockerfile CMD command - - eg: CMD /code/run_uwsgi.sh --> /code/run_uwsgi.sh. - If a list is provided, it's parsed to a list. - - Parameters - ========== - line: the line from the recipe file to parse for CMD - - ''' - cmd = self._setup('CMD', line)[0] - self.cmd = self._load_list(cmd) - - - def _load_list(self, line): - '''load an entrypoint or command, meaning it can be wrapped in a list - or a regular string. We try loading as json to return an actual - list. E.g., after _setup, we might go from 'ENTRYPOINT ["one", "two"]' - to '["one", "two"]', and this function loads as json and returns - ["one", "two"] - ''' - try: - line = json.loads(line) - except json.JSONDecodeError: - pass - return line - - - def _entry(self, line): - '''_entrypoint will parse a Dockerfile ENTRYPOINT command - - Parameters - ========== - line: the line from the recipe file to parse for CMD - - ''' - entrypoint = self._setup('ENTRYPOINT', line)[0] - self.entrypoint = self._load_list(entrypoint) - - -# Labels - - def _label(self, line): - '''_label will parse a Dockerfile label - - Parameters - ========== - line: the line from the recipe file to parse for CMD - - ''' - label = self._setup('LABEL', line) - self.labels += [ label ] - - -# Main Parsing Functions - - - def _get_mapping(self, line, parser=None, previous=None): - '''mapping will take the command from a Dockerfile and return a map - function to add it to the appropriate place. Any lines that don't - cleanly map are assumed to be comments. - - Parameters - ========== - line: the list that has been parsed into parts with _split_line - parser: the previously used parser, for context - - Returns - ======= - function: to map a line to its command group - - ''' - - # Split the command into cleanly the command and rest - if not isinstance(line, list): - line = self._split_line(line) - - # No line we will give function to handle empty line - if len(line) == 0: - return None - - cmd = line[0].upper() - - mapping = {"ADD": self._add, - "ARG": self._arg, - "COPY": self._copy, - "CMD": self._cmd, - "ENTRYPOINT": self._entry, - "ENV": self._env, - "EXPOSE": self._expose, - "FROM": self._from, - "HEALTHCHECK": self._test, - "RUN": self._run, - "WORKDIR": self._workdir, - "MAINTAINER": self._label, - "VOLUME": self._volume, - "LABEL": self._label} - - # If it's a command line, return correct function - if cmd in mapping: - return mapping[cmd] - - # If it's a continued line, return previous - cleaned = self._clean_line(line[-1]) - previous = self._clean_line(previous) - - # if we are continuing from last - if cleaned.endswith('\\') and parser or previous.endswith('\\'): - return parser - - return self._default - - - def _parse(self): - '''parse is the base function for parsing the Dockerfile, and extracting - elements into the correct data structures. Everything is parsed into - lists or dictionaries that can be assembled again on demand. - - Environment: Since Docker also exports environment as we go, - we add environment to the environment section and - install - - Labels: include anything that is a LABEL, ARG, or (deprecated) - maintainer. - - Add/Copy: are treated the same - - ''' - parser = None - previous = None - - for line in self.lines: - - parser = self._get_mapping(line, parser, previous) - - # Parse it, if appropriate - if parser: - parser(line) - - previous = line diff --git a/spython/main/parse/parsers/docker.py b/spython/main/parse/parsers/docker.py index e8f30c6f..1709b366 100644 --- a/spython/main/parse/parsers/docker.py +++ b/spython/main/parse/parsers/docker.py @@ -122,7 +122,7 @@ def _test(self, line): line: the line from the recipe file to parse for FROM ''' - self.recipe.test = self._setup('HEALTHCHECK', line) + self.recipe.test = self._setup('HEALTHCHECK', line) # Arg Parser @@ -456,7 +456,7 @@ def _label(self, line): ''' label = self._setup('LABEL', line) - self.recipe.labels += [ label ] + self.recipe.labels += [label] # Main Parsing Functions diff --git a/spython/main/parse/parsers/singularity.py b/spython/main/parse/parsers/singularity.py index 061727bc..997e3a9f 100644 --- a/spython/main/parse/parsers/singularity.py +++ b/spython/main/parse/parsers/singularity.py @@ -268,8 +268,8 @@ def _load_from(self, line): '''load the From section of the recipe for the Dockerfile. ''' # Remove any comments - line = line.split('#',1)[0] - line = re.sub('(F|f)(R|r)(O|o)(M|m):','', line).strip() + line = line.split('#', 1)[0] + line = re.sub('(F|f)(R|r)(O|o)(M|m):', '', line).strip() bot.info('FROM %s' % line) self.config['from'] = line @@ -375,14 +375,14 @@ def _add_section(self, line, section=None): ''' # Remove any comments - line = line.split('#',1)[0].strip() + line = line.split('#', 1)[0].strip() # Is there a section name? parts = line.split(' ') - section = re.sub(r'[%]|(\s+)','',parts[0]).lower() + section = re.sub(r'[%]|(\s+)', '', parts[0]).lower() if section not in self.config: self.config[section] = [] - bot.debug("Adding section %s" %section) + bot.debug("Adding section %s" % section) return section diff --git a/spython/main/parse/writers/docker.py b/spython/main/parse/writers/docker.py index 61f83e0e..db34b72b 100644 --- a/spython/main/parse/writers/docker.py +++ b/spython/main/parse/writers/docker.py @@ -69,9 +69,9 @@ def validate(self): bot.exit("Dockerfile requires a fromHeader.") # Parse the provided name - uri_regexes = [ _reduced_uri, - _default_uri, - _docker_uri ] + uri_regexes = [_reduced_uri, + _default_uri, + _docker_uri] for r in uri_regexes: match = r.match(self.recipe.fromHeader) @@ -87,7 +87,7 @@ def convert(self, runscript="/bin/bash", force=False): ''' self.validate() - recipe = [ "FROM %s" % self.recipe.fromHeader ] + recipe = ["FROM %s" % self.recipe.fromHeader] # Comments go up front! recipe += self.recipe.comments @@ -114,7 +114,7 @@ def convert(self, runscript="/bin/bash", force=False): recipe.append(write_lines('HEALTHCHECK', self.recipe.test)) # Clean up extra white spaces - return '\n'.join(recipe).replace('\n\n','\n') + return '\n'.join(recipe).replace('\n\n', '\n') def write_files(label, lines): diff --git a/spython/main/parse/writers/singularity.py b/spython/main/parse/writers/singularity.py index 792dc091..77ac2e2e 100644 --- a/spython/main/parse/writers/singularity.py +++ b/spython/main/parse/writers/singularity.py @@ -50,7 +50,7 @@ def convert(self, runscript="/bin/bash", force=False): self.validate() recipe = ['Bootstrap: docker'] - recipe += [ "From: %s" % self.recipe.fromHeader ] + recipe += ["From: %s" % self.recipe.fromHeader] # Sections with key value pairs recipe += self._create_section('files') @@ -73,7 +73,7 @@ def convert(self, runscript="/bin/bash", force=False): recipe += finish_section(self.recipe.test, 'test') # Clean up extra white spaces - return '\n'.join(recipe).replace('\n\n','\n') + return '\n'.join(recipe).replace('\n\n', '\n') def _create_runscript(self, default="/bin/bash", force=False): @@ -172,7 +172,7 @@ def finish_section(section, name): if not isinstance(section, list): section = [section] - header = ['%' + name ] + header = ['%' + name] return header + section @@ -186,7 +186,7 @@ def create_keyval_section(pairs, name): name: the name of the section to write (e.g., files) ''' - section = ['%' + name ] + section = ['%' + name] for pair in pairs: print(pair) section.append(' '.join(pair).strip().strip('\\')) @@ -203,7 +203,7 @@ def create_env_section(pairs, name): name: the name of the section to write (e.g., files) ''' - section = ['%' + name ] + section = ['%' + name] for pair in pairs: section.append("export %s" %pair) return section diff --git a/spython/main/run.py b/spython/main/run.py index 69532a30..4f861c00 100644 --- a/spython/main/run.py +++ b/spython/main/run.py @@ -11,17 +11,16 @@ import json def run(self, - image = None, - args = None, - app = None, - sudo = False, - writable = False, - contain = False, - bind = None, - stream = False, - nv = False, + image=None, + args=None, + app=None, + sudo=False, + writable=False, + contain=False, + bind=None, + stream=False, + nv=False, return_result=False): - ''' run will run the container, with or withour arguments (which should be provided in a list) diff --git a/spython/oci/cmd/__init__.py b/spython/oci/cmd/__init__.py index 749c6061..489887a3 100644 --- a/spython/oci/cmd/__init__.py +++ b/spython/oci/cmd/__init__.py @@ -15,13 +15,13 @@ def generate_oci_commands(): from spython.main.base.logger import println # run_command uses run_cmd, but wraps to catch error - from spython.main.base.command import ( run_command, send_command ) + from spython.main.base.command import (run_command, send_command) from spython.main.base.generate import RobotNamer # Oci Command Groups - from .mounts import ( mount, umount ) - from .states import ( kill, state, start, pause, resume, _state_command ) - from .actions import ( attach, create, delete, execute, run, _run, update ) + from .mounts import (mount, umount) + from .states import (kill, state, start, pause, resume, _state_command) + from .actions import (attach, create, delete, execute, run, _run, update) # Oci Commands OciImage.start = start diff --git a/spython/tests/test_client.py b/spython/tests/test_client.py index f2520e84..d53abb0c 100644 --- a/spython/tests/test_client.py +++ b/spython/tests/test_client.py @@ -69,12 +69,12 @@ def test_commands(self): self.assertTrue(os.path.exists(container)) print('Testing client.execute command') - result = self.cli.execute(container,'ls /') + result = self.cli.execute(container, 'ls /') print(result) self.assertTrue('tmp\nusr\nvar' in result) print('Testing client.execute command with return code') - result = self.cli.execute(container,'ls /', return_result=True) + result = self.cli.execute(container, 'ls /', return_result=True) print(result) self.assertTrue('tmp\nusr\nvar' in result['message']) self.assertEqual(result['return_code'], 0) diff --git a/spython/tests/test_instances.py b/spython/tests/test_instances.py index 6c3d398a..483370d1 100644 --- a/spython/tests/test_instances.py +++ b/spython/tests/test_instances.py @@ -63,7 +63,7 @@ def test_instances(self): self.assertEqual(result, 'hello\n') print('...Case 4: Return value from instance') - result = self.cli.execute(myinstance,'ls /', return_result=True) + result = self.cli.execute(myinstance, 'ls /', return_result=True) print(result) self.assertTrue('tmp\nusr\nvar' in result['message']) self.assertEqual(result['return_code'], 0) diff --git a/spython/tests/test_oci.py b/spython/tests/test_oci.py index a4f4b5c9..e72e3ef8 100644 --- a/spython/tests/test_oci.py +++ b/spython/tests/test_oci.py @@ -41,7 +41,7 @@ def _build_sandbox(self): return image def test_oci_image(self): - image=self.cli.oci.OciImage('oci://imagename') + image = self.cli.oci.OciImage('oci://imagename') self.assertEqual(image.get_uri(), '[singularity-python-oci:oci://imagename]') def test_oci(self): @@ -63,8 +63,8 @@ def test_oci(self): print('...Case 3. Execute command to non running bundle.') result = self.cli.oci.execute(container_id=self.name, - sudo=True, - command=['ls','/']) + sudo=True, + command=['ls', '/']) print(result) self.assertTrue(result['return_code'] == 255) @@ -76,7 +76,7 @@ def test_oci(self): print('...Case 5. Execute command to running bundle.') result = self.cli.oci.execute(container_id=self.name, sudo=True, - command=['ls','/']) + command=['ls', '/']) print(result) self.assertTrue('bin' in result) @@ -109,7 +109,7 @@ def test_oci(self): # Bug in singularity that kill doesn't kill completely - this returns # 255. When testsupdated to 3.1.* add signal=K to run result = self.cli.oci.delete(self.name, sudo=True) - self.assertTrue(result in [0,255]) + self.assertTrue(result in [0, 255]) if __name__ == '__main__': diff --git a/spython/tests/test_utils.py b/spython/tests/test_utils.py index 4aba8502..50af8d1d 100644 --- a/spython/tests/test_utils.py +++ b/spython/tests/test_utils.py @@ -30,31 +30,31 @@ def test_write_read_files(self): import json tmpfile = tempfile.mkstemp()[1] os.remove(tmpfile) - write_file(tmpfile,"hello!") + write_file(tmpfile, "hello!") self.assertTrue(os.path.exists(tmpfile)) print("Testing utils.read_file...") from spython.utils import read_file content = read_file(tmpfile)[0] - self.assertEqual("hello!",content) + self.assertEqual("hello!", content) from spython.utils import write_json print("Testing utils.write_json...") print("...Case 1: Providing bad json") - bad_json = {"Wakkawakkawakka'}":[{True},"2",3]} + bad_json = {"Wakkawakkawakka'}": [{True}, "2", 3]} tmpfile = tempfile.mkstemp()[1] os.remove(tmpfile) with self.assertRaises(TypeError): - write_json(bad_json,tmpfile) + write_json(bad_json, tmpfile) print("...Case 2: Providing good json") - good_json = {"Wakkawakkawakka":[True,"2",3]} + good_json = {"Wakkawakkawakka": [True, "2", 3]} tmpfile = tempfile.mkstemp()[1] os.remove(tmpfile) - write_json(good_json,tmpfile) - with open(tmpfile,'r') as filey: + write_json(good_json, tmpfile) + with open(tmpfile, 'r') as filey: content = json.loads(filey.read()) - self.assertTrue(isinstance(content,dict)) + self.assertTrue(isinstance(content, dict)) self.assertTrue("Wakkawakkawakka" in content) @@ -114,10 +114,9 @@ def test_split_uri(self): def test_remove_uri(self): print("Testing utils.remove_uri") from spython.utils import remove_uri - self.assertEqual(remove_uri('docker://ubuntu'),'ubuntu') - self.assertEqual(remove_uri('shub://vanessa/singularity-images'),'vanessa/singularity-images') - self.assertEqual(remove_uri('vanessa/singularity-images'),'vanessa/singularity-images') - + self.assertEqual(remove_uri('docker://ubuntu'), 'ubuntu') + self.assertEqual(remove_uri('shub://vanessa/singularity-images'), 'vanessa/singularity-images') + self.assertEqual(remove_uri('vanessa/singularity-images'), 'vanessa/singularity-images') if __name__ == '__main__': diff --git a/spython/utils/terminal.py b/spython/utils/terminal.py index dada60ac..ba2ecd86 100644 --- a/spython/utils/terminal.py +++ b/spython/utils/terminal.py @@ -89,9 +89,9 @@ def stream_command(cmd, no_newline_regexp="Progess", sudo=False): if sudo is True: cmd = ['sudo'] + cmd - process = subprocess.Popen(cmd, - stdout = subprocess.PIPE, - universal_newlines = True) + process = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + universal_newlines=True) for line in iter(process.stdout.readline, ""): if not re.search(no_newline_regexp, line): yield line @@ -131,9 +131,9 @@ def run_command(cmd, stdout = subprocess.PIPE # Use the parent stdout and stderr - process = subprocess.Popen(cmd, - stderr = subprocess.PIPE, - stdout = stdout) + process = subprocess.Popen(cmd, + stderr=subprocess.PIPE, + stdout=stdout) lines = () found_match = False @@ -153,7 +153,7 @@ def run_command(cmd, found_match = False output = {'message': lines, - 'return_code': process.returncode } + 'return_code': process.returncode} return output @@ -187,7 +187,7 @@ def split_uri(container): protocol, image = parts else: protocol = '' - image=parts[0] + image = parts[0] return protocol, image.rstrip('/') From c42ed4aaa36ba475a0f0e5c2dd68950a257acc12 Mon Sep 17 00:00:00 2001 From: Vanessa Sochat Date: Wed, 29 May 2019 11:34:03 -0400 Subject: [PATCH 05/21] fixing python 2 errors with list copy and json decode error Signed-off-by: Vanessa Sochat --- spython/main/parse/parsers/docker.py | 2 +- spython/main/parse/parsers/singularity.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spython/main/parse/parsers/docker.py b/spython/main/parse/parsers/docker.py index 1709b366..9f9cf268 100644 --- a/spython/main/parse/parsers/docker.py +++ b/spython/main/parse/parsers/docker.py @@ -428,7 +428,7 @@ def _load_list(self, line): ''' try: line = json.loads(line) - except json.JSONDecodeError: + except: # json.JSONDecodeError pass return line diff --git a/spython/main/parse/parsers/singularity.py b/spython/main/parse/parsers/singularity.py index 997e3a9f..10d59d59 100644 --- a/spython/main/parse/parsers/singularity.py +++ b/spython/main/parse/parsers/singularity.py @@ -320,7 +320,7 @@ def load_recipe(self): ''' # Comments between sections, add to top of file - lines = self.lines.copy() + lines = self.lines[:] comments = [] # Start with a fresh config! From ce9125a4c89a22cbcef301756c12b7f3fe736738 Mon Sep 17 00:00:00 2001 From: Vanessa Sochat Date: Wed, 29 May 2019 11:43:35 -0400 Subject: [PATCH 06/21] missing ipython Signed-off-by: Vanessa Sochat --- .circleci/config.yml | 1 - spython/client/shell.py | 7 ++++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d3da51b9..7935d849 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -132,7 +132,6 @@ jobs: - singularity/debian-install-3: singularity-version: 3.2.1 - run: *install_spython - - run: *run_linter - save_cache: paths: - /home/circleci/conda diff --git a/spython/client/shell.py b/spython/client/shell.py index f9625c34..a4a4cf59 100644 --- a/spython/client/shell.py +++ b/spython/client/shell.py @@ -45,7 +45,12 @@ def ipython(image): '''give the user an ipython shell ''' client = prepare_client(image) # pylint: disable=unused-variable - from IPython import embed + + try: + from IPython import embed + except ImportError: + return python(image) + embed() def bpython(image): From 5c9fedc981434384ea923148fc22835ccaff8f04 Mon Sep 17 00:00:00 2001 From: Vanessa Sochat Date: Thu, 30 May 2019 10:23:31 -0400 Subject: [PATCH 07/21] more fixes to linting Signed-off-by: Vanessa Sochat --- .pylintrc | 5 --- spython/client/__init__.py | 23 +++++++---- spython/client/shell.py | 6 +-- spython/instance/cmd/__init__.py | 4 +- spython/logger/progress.py | 4 +- spython/logger/spinner.py | 12 ++++-- spython/main/__init__.py | 48 +++++++++++------------ spython/main/apps.py | 2 +- spython/main/help.py | 5 +-- spython/main/instances.py | 2 +- spython/main/parse/parsers/docker.py | 22 +++++------ spython/main/parse/parsers/singularity.py | 6 +-- spython/main/parse/writers/singularity.py | 4 +- spython/oci/__init__.py | 2 +- spython/utils/terminal.py | 2 +- 15 files changed, 76 insertions(+), 71 deletions(-) diff --git a/.pylintrc b/.pylintrc index a45b570f..56d037f1 100644 --- a/.pylintrc +++ b/.pylintrc @@ -63,19 +63,14 @@ confidence= disable=attribute-defined-outside-init, bad-continuation, bare-except, - blacklisted-name, duplicate-code, fixme, invalid-name, - len-as-condition, line-too-long, missing-docstring, - multiple-statements, no-member, protected-access, R, - redefined-builtin, - redefined-outer-name, trailing-whitespace, unused-argument, wrong-import-order diff --git a/spython/client/__init__.py b/spython/client/__init__.py index 4e564eb6..2047cb85 100644 --- a/spython/client/__init__.py +++ b/spython/client/__init__.py @@ -97,7 +97,7 @@ def main(): parser = get_parser() - def help(return_code=0): + def print_help(return_code=0): '''print help, including the software version and active client and exit with return code. ''' @@ -107,7 +107,7 @@ def help(return_code=0): sys.exit(return_code) if len(sys.argv) == 1: - help() + print_help() try: # We capture all primary arguments, and take secondary to pass on args, options = parser.parse_known_args() @@ -115,7 +115,7 @@ def help(return_code=0): sys.exit(0) # The main function - main = None + func = None # If the user wants the version if args.version is True: @@ -126,14 +126,21 @@ def help(return_code=0): set_verbosity(args) # Does the user want help for a subcommand? - if args.command == 'recipe': from .recipe import main - elif args.command == 'shell': from .shell import main - elif args.command == 'test': from .test import main - else: help() + if args.command == 'recipe': + from .recipe import main as func + + elif args.command == 'shell': + from .shell import main as func + + elif args.command == 'test': + from .test import main as func + + else: + print_help() # Pass on to the correct parser if args.command is not None: - main(args=args, options=options, parser=parser) + func(args=args, options=options, parser=parser) if __name__ == '__main__': diff --git a/spython/client/shell.py b/spython/client/shell.py index a4a4cf59..7baba508 100644 --- a/spython/client/shell.py +++ b/spython/client/shell.py @@ -9,12 +9,12 @@ def main(args, options, parser): # If we have options, first is image image = None - if len(options) > 0: + if options: image = options.pop(0) lookup = {'ipython': ipython, 'python': python, - 'bpython': bpython} + 'bpython': run_bpython} shells = ['ipython', 'python', 'bpython'] @@ -53,7 +53,7 @@ def ipython(image): embed() -def bpython(image): +def run_bpython(image): '''give the user a bpython shell ''' client = prepare_client(image) diff --git a/spython/instance/cmd/__init__.py b/spython/instance/cmd/__init__.py index 283545ce..26bc1028 100644 --- a/spython/instance/cmd/__init__.py +++ b/spython/instance/cmd/__init__.py @@ -15,7 +15,7 @@ def generate_instance_commands(): from spython.instance import Instance from spython.main.base.logger import println - from spython.main.instances import instances + from spython.main.instances import list_instances from spython.utils import run_command as run_cmd # run_command uses run_cmd, but wraps to catch error @@ -28,7 +28,7 @@ def generate_instance_commands(): Instance._init_command = init_command Instance.run_command = run_cmd Instance._run_command = run_command - Instance._list = instances # list command is used to get metadata + Instance._list = list_instances # list command is used to get metadata Instance._println = println Instance.start = start # intended to be called on init, not by user Instance.stop = stop diff --git a/spython/logger/progress.py b/spython/logger/progress.py index 4da64069..17f6efd4 100644 --- a/spython/logger/progress.py +++ b/spython/logger/progress.py @@ -106,7 +106,7 @@ def bar(it, label='', width=32, hide=None, empty_char=BAR_EMPTY_CHAR, with ProgressBar(label=label, width=width, hide=hide, empty_char=BAR_EMPTY_CHAR, filled_char=BAR_FILLED_CHAR, expected_size=count, every=every) \ - as bar: + as pbar: for i, item in enumerate(it): yield item - bar.show(i + 1) + pbar.show(i + 1) diff --git a/spython/logger/spinner.py b/spython/logger/spinner.py index 22d40980..ad792094 100644 --- a/spython/logger/spinner.py +++ b/spython/logger/spinner.py @@ -18,17 +18,20 @@ class Spinner: @staticmethod def spinning_cursor(): while 1: - for cursor in '|/-\\': yield cursor + for cursor in '|/-\\': + yield cursor @staticmethod def balloons_cursor(): while 1: - for cursor in '. o O @ *': yield cursor + for cursor in '. o O @ *': + yield cursor @staticmethod def changing_arrows(): while 1: - for cursor in '<^>v': yield cursor + for cursor in '<^>v': + yield cursor def select_generator(self, generator): if generator is None: @@ -47,7 +50,8 @@ def __init__(self, delay=None, generator=None): self.spinner_generator = self.changing_arrows() elif generator == 'balloons': self.spinner_generator = self.balloons_cursor() - if delay is None: delay = 0.2 + if delay is None: + delay = 0.2 else: self.spinner_generator = self.spinning_cursor() diff --git a/spython/main/__init__.py b/spython/main/__init__.py index 52118eb8..a13cdbbc 100644 --- a/spython/main/__init__.py +++ b/spython/main/__init__.py @@ -18,53 +18,53 @@ def get_client(quiet=False, debug=False): ''' from spython.utils import get_singularity_version - from .base import Client + from .base import Client as client - Client.quiet = quiet - Client.debug = debug + client.quiet = quiet + client.debug = debug # Do imports here, can be customized from .apps import apps from .build import build from .execute import execute - from .help import help + from .help import helpcmd from .inspect import inspect - from .instances import (instances, stopall) # global instance commands + from .instances import (list_instances, stopall) # global instance commands from .run import run from .pull import pull from .export import (export, _export) # Actions - Client.apps = apps - Client.build = build - Client.execute = execute - Client.export = export - Client._export = _export - Client.help = help - Client.inspect = inspect - Client.instances = instances - Client.run = run - Client.pull = pull + client.apps = apps + client.build = build + client.execute = execute + client.export = export + client._export = _export + client.help = helpcmd + client.inspect = inspect + client.instances = list_instances + client.run = run + client.pull = pull # Command Groups, Images from spython.image.cmd import generate_image_commands # deprecated - Client.image = generate_image_commands() + client.image = generate_image_commands() # Commands Groups, Instances from spython.instance.cmd import generate_instance_commands # instance level commands - Client.instance = generate_instance_commands() - Client.instance_stopall = stopall - Client.instance.version = Client.version + client.instance = generate_instance_commands() + client.instance_stopall = stopall + client.instance.version = client.version # Commands Groups, OCI (Singularity version 3 and up) if "version 3" in get_singularity_version(): from spython.oci.cmd import generate_oci_commands - Client.oci = generate_oci_commands()() # first () runs function, second + client.oci = generate_oci_commands()() # first () runs function, second # initializes OciImage class - Client.oci.debug = Client.debug - Client.oci.quiet = Client.quiet - Client.oci.OciImage.quiet = Client.quiet - Client.oci.OciImage.debug = Client.debug + client.oci.debug = client.debug + client.oci.quiet = client.quiet + client.oci.OciImage.quiet = client.quiet + client.oci.OciImage.debug = client.debug # Initialize diff --git a/spython/main/apps.py b/spython/main/apps.py index 7927253c..755d1935 100644 --- a/spython/main/apps.py +++ b/spython/main/apps.py @@ -32,7 +32,7 @@ def apps(self, image=None, full_path=False, root=''): if full_path is True: root = '/scif/apps/' - if len(output) > 0: + if output: output = ''.join(output).split('\n') output = ['%s%s' %(root, x) for x in output if x] diff --git a/spython/main/help.py b/spython/main/help.py index b41d9fe2..6aab7fed 100644 --- a/spython/main/help.py +++ b/spython/main/help.py @@ -6,7 +6,7 @@ # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -def help(self, command=None): +def helpcmd(self, command=None): '''help prints the general function help, or help for a specific command Parameters @@ -20,5 +20,4 @@ def help(self, command=None): cmd = ['singularity', '--help'] if command is not None: cmd.append(command) - help = self._run_command(cmd) - return help + return self._run_command(cmd) diff --git a/spython/main/instances.py b/spython/main/instances.py index 36281d62..c28b40ab 100644 --- a/spython/main/instances.py +++ b/spython/main/instances.py @@ -9,7 +9,7 @@ from spython.logger import bot from spython.utils import run_command -def instances(self, name=None, return_json=False, quiet=False): +def list_instances(self, name=None, return_json=False, quiet=False): '''list instances. For Singularity, this is provided as a command sub group. diff --git a/spython/main/parse/parsers/docker.py b/spython/main/parse/parsers/docker.py index 9f9cf268..c2689c69 100644 --- a/spython/main/parse/parsers/docker.py +++ b/spython/main/parse/parsers/docker.py @@ -181,18 +181,18 @@ def parse_env(self, envlist): pieces = re.split("( |\\\".*?\\\"|'.*?')", env) pieces = [p for p in pieces if p.strip()] - while len(pieces) > 0: + while pieces: current = pieces.pop(0) if current.endswith('='): # Case 1: ['A='] --> A= - next = "" + nextone = "" # Case 2: ['A=', '"1 2"'] --> A=1 2 - if len(pieces) > 0: - next = pieces.pop(0) - exports.append("%s%s" %(current, next)) + if pieces: + nextone = pieces.pop(0) + exports.append("%s%s" %(current, nextone)) # Case 3: ['A=B'] --> A=B elif '=' in current: @@ -204,8 +204,8 @@ def parse_env(self, envlist): # Case 5: ['A', 'B'] --> A=B else: - next = pieces.pop(0) - exports.append("%s=%s" %(current, next)) + nextone = pieces.pop(0) + exports.append("%s=%s" %(current, nextone)) return exports @@ -367,7 +367,7 @@ def _volume(self, line): ''' volumes = self._setup('VOLUME', line) - if len(volumes) > 0: + if volumes: self.recipe.volumes += volumes return self._comment("# %s" %line) @@ -381,7 +381,7 @@ def _expose(self, line): ''' ports = self._setup('EXPOSE', line) - if len(ports) > 0: + if ports: self.recipe.ports += ports return self._comment("# %s" %line) @@ -483,7 +483,7 @@ def _get_mapping(self, line, parser=None, previous=None): line = self._split_line(line) # No line we will give function to handle empty line - if len(line) == 0: + if not line: return None cmd = line[0].upper() @@ -547,7 +547,7 @@ def _write_script(self, path, lines, chmod=True): chmod: If true, change permission to make u+x ''' - if len(lines) > 0: + if lines: lastline = lines.pop() for line in lines: self.recipe.install.append('echo "%s" >> %s' %line %path) diff --git a/spython/main/parse/parsers/singularity.py b/spython/main/parse/parsers/singularity.py index 10d59d59..d2bb311d 100644 --- a/spython/main/parse/parsers/singularity.py +++ b/spython/main/parse/parsers/singularity.py @@ -289,7 +289,7 @@ def _load_section(self, lines, section): while True: - if len(lines) == 0: + if not lines: break next_line = lines[0] @@ -304,7 +304,7 @@ def _load_section(self, lines, section): members.append(new_member) # Add the list to the config - if len(members) > 0: + if members: if section is not None: self.config[section] += members @@ -328,7 +328,7 @@ def load_recipe(self): section = None - while len(lines) > 0: + while lines: # Clean up white trailing/leading space line = lines.pop(0) diff --git a/spython/main/parse/writers/singularity.py b/spython/main/parse/writers/singularity.py index 77ac2e2e..50fbe1a5 100644 --- a/spython/main/parse/writers/singularity.py +++ b/spython/main/parse/writers/singularity.py @@ -144,7 +144,7 @@ def _create_section(self, attribute, name=None): return section # if the section is empty, don't print it - if len(section) == 0: + if not section: return section # Files @@ -205,5 +205,5 @@ def create_env_section(pairs, name): ''' section = ['%' + name] for pair in pairs: - section.append("export %s" %pair) + section.append("export %s" % pair) return section diff --git a/spython/oci/__init__.py b/spython/oci/__init__.py index 6ffbcfa9..0ffc8102 100644 --- a/spython/oci/__init__.py +++ b/spython/oci/__init__.py @@ -116,7 +116,7 @@ def _run_and_return(self, cmd, sudo=None): return_result=True) # Successful return with no output - if len(result) == 0: + if not result: return # Show the response to the user, only if not quiet. diff --git a/spython/utils/terminal.py b/spython/utils/terminal.py index ba2ecd86..b5bbf31b 100644 --- a/spython/utils/terminal.py +++ b/spython/utils/terminal.py @@ -59,7 +59,7 @@ def get_singularity_version(): return version if version['return_code'] == 0: - if len(version['message']) > 0: + if version['message']: version = version['message'][0].strip('\n') return version From 777fb5b62f4ca8d162eddd9588921935a48fba2b Mon Sep 17 00:00:00 2001 From: Vanessa Sochat Date: Thu, 30 May 2019 13:08:51 -0400 Subject: [PATCH 08/21] fixing length as condition pylint error Signed-off-by: Vanessa Sochat --- spython/main/base/generate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spython/main/base/generate.py b/spython/main/base/generate.py index 83195153..9e68616c 100644 --- a/spython/main/base/generate.py +++ b/spython/main/base/generate.py @@ -69,7 +69,7 @@ def _select(self, select_from): ========== should be a list of things to select from ''' - if len(select_from) <= 0: + if not select_from: return '' return choice(select_from) From 6fceb5c20dc58852757ee1fc7ebadc4334c3b803 Mon Sep 17 00:00:00 2001 From: Vanessa Sochat Date: Thu, 30 May 2019 13:22:53 -0400 Subject: [PATCH 09/21] fixing client bug Signed-off-by: Vanessa Sochat --- spython/main/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spython/main/__init__.py b/spython/main/__init__.py index a13cdbbc..9aa197fb 100644 --- a/spython/main/__init__.py +++ b/spython/main/__init__.py @@ -68,7 +68,7 @@ def get_client(quiet=False, debug=False): # Initialize - cli = Client() + cli = client() # Pass on verbosity for subclient in [cli.image, cli.instance]: From 3e68913ffce868b167946442e393ffc69e12de9d Mon Sep 17 00:00:00 2001 From: Vanessa Sochat Date: Thu, 30 May 2019 15:12:17 -0400 Subject: [PATCH 10/21] fixing tests for writers and parsers Signed-off-by: Vanessa Sochat --- spython/main/parse/parsers/docker.py | 2 +- spython/tests/test_parsers.py | 1 - spython/tests/test_writers.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/spython/main/parse/parsers/docker.py b/spython/main/parse/parsers/docker.py index c2689c69..1007d4f3 100644 --- a/spython/main/parse/parsers/docker.py +++ b/spython/main/parse/parsers/docker.py @@ -400,7 +400,7 @@ def _workdir(self, line): workdir = self._setup('WORKDIR', line) workdir_cd = "cd %s" %(''.join(workdir)) self.recipe.install.append(workdir_cd) - self.recipe.workdir = workdir + self.recipe.workdir = workdir[0] # Entrypoint and Command diff --git a/spython/tests/test_parsers.py b/spython/tests/test_parsers.py index 9f79667a..16fb23ea 100644 --- a/spython/tests/test_parsers.py +++ b/spython/tests/test_parsers.py @@ -63,7 +63,6 @@ def test_singularity_parser(self): self.assertEqual(parser.recipe.entrypoint, None) self.assertEqual(parser.recipe.workdir, None) self.assertEqual(parser.recipe.volumes, []) - self.assertEqual(parser.recipe.ports, ['3031']) self.assertEqual(parser.recipe.files, []) self.assertEqual(parser.recipe.environ, []) self.assertEqual(parser.recipe.source, recipe) diff --git a/spython/tests/test_writers.py b/spython/tests/test_writers.py index 63f5ee8d..5b481834 100644 --- a/spython/tests/test_writers.py +++ b/spython/tests/test_writers.py @@ -48,7 +48,7 @@ def test_singularity_writer(self): parser = SingularityParser(recipe) writer = SingularityWriter(parser.recipe) - self.assertEqual(str(writer), '[spython-writer][docker]') + self.assertEqual(str(writer), '[spython-writer][singularity]') print(writer.convert()) From ff88fce60bcc18994431858d1a44b1dc95c29c7d Mon Sep 17 00:00:00 2001 From: Vanessa Sochat Date: Thu, 30 May 2019 15:22:40 -0400 Subject: [PATCH 11/21] need to expose ports Signed-off-by: Vanessa Sochat --- .pylintrc | 1 + spython/client/recipe.py | 4 ++-- spython/main/parse/writers/docker.py | 5 ++++- spython/main/parse/writers/singularity.py | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.pylintrc b/.pylintrc index f1bd2f5d..ae8fbc2a 100644 --- a/.pylintrc +++ b/.pylintrc @@ -71,6 +71,7 @@ disable=attribute-defined-outside-init, no-member, protected-access, R, + useless-super-delegation, unidiomatic-typecheck, trailing-whitespace, unused-argument, diff --git a/spython/client/recipe.py b/spython/client/recipe.py index 0475ed6a..d542eedd 100644 --- a/spython/client/recipe.py +++ b/spython/client/recipe.py @@ -44,7 +44,7 @@ def main(args, options, parser): writer = writers.DockerWriter # Initialize the chosen parser - recipe = parser(args.files[0]) + recipeParser = parser(args.files[0]) # By default, discover entrypoint / cmd from Dockerfile entrypoint = "/bin/bash" @@ -55,7 +55,7 @@ def main(args, options, parser): force = True # Do the conversion - recipeWriter = writer(recipe) + recipeWriter = writer(recipeParser.recipe) result = recipeWriter.convert(runscript=entrypoint, force=force) # If the user specifies an output file, save to it diff --git a/spython/main/parse/writers/docker.py b/spython/main/parse/writers/docker.py index db34b72b..2e8fb803 100644 --- a/spython/main/parse/writers/docker.py +++ b/spython/main/parse/writers/docker.py @@ -45,7 +45,7 @@ class DockerWriter(WriterBase): name = 'docker' - def __init__(self, recipe="Dockerfile"): + def __init__(self, recipe=None): '''a DockerWriter will take a Recipe as input, and write to a Dockerfile. @@ -100,6 +100,9 @@ def convert(self, runscript="/bin/bash", force=False): # Install routine is added as RUN commands recipe += write_lines('RUN', self.recipe.install) + # Expose ports + recipe += write_lines('EXPOSE', self.recipe.ports) + if self.recipe.workdir is not None: recipe.append('WORKDIR %s' % self.recipe.workdir) diff --git a/spython/main/parse/writers/singularity.py b/spython/main/parse/writers/singularity.py index 50fbe1a5..86a17e86 100644 --- a/spython/main/parse/writers/singularity.py +++ b/spython/main/parse/writers/singularity.py @@ -16,7 +16,7 @@ class SingularityWriter(WriterBase): name = 'singularity' - def __init__(self, recipe="Singularity"): + def __init__(self, recipe=None): '''a SingularityWriter will take a Recipe as input, and write to a Singularity recipe file. From af25656d54a0ca921ca4cd34d901ebf70e028f2a Mon Sep 17 00:00:00 2001 From: Vanessa Sochat Date: Thu, 30 May 2019 17:31:48 -0400 Subject: [PATCH 12/21] adding fixed up docs and new tests Signed-off-by: Vanessa Sochat --- .circleci/config.yml | 5 +- docs/pages/recipes.md | 519 +++++++++------------- spython/client/__init__.py | 17 +- spython/client/recipe.py | 84 ++-- spython/main/parse/parsers/__init__.py | 16 + spython/main/parse/parsers/base.py | 4 +- spython/main/parse/parsers/singularity.py | 2 +- spython/main/parse/recipe.py | 29 ++ spython/main/parse/writers/__init__.py | 17 + spython/main/parse/writers/singularity.py | 3 +- spython/tests/helpers.sh | 25 ++ spython/tests/test_client.sh | 56 +++ spython/tests/test_parsers.py | 17 + spython/tests/test_recipe.py | 23 +- spython/tests/test_writers.py | 15 + 15 files changed, 495 insertions(+), 337 deletions(-) create mode 100644 spython/tests/helpers.sh create mode 100755 spython/tests/test_client.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index a720c177..1014d472 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -74,6 +74,9 @@ run_linter: &run_linter test_spython: &test_spython name: Test Singularity Python (Singularity Version 2 and 3) command: | + cd ~/repo/spython/tests + /bin/bash test_client.sh + cd ~/repo/spython pytest -k 'not test_oci' @@ -193,4 +196,4 @@ jobs: paths: - ~/conda key: v1-dependencies-py2 - - run: *test_spython \ No newline at end of file + - run: *test_spython diff --git a/docs/pages/recipes.md b/docs/pages/recipes.md index 9b7b0b9c..86f52c0b 100644 --- a/docs/pages/recipes.md +++ b/docs/pages/recipes.md @@ -6,17 +6,25 @@ permalink: /recipes toc: false --- -# Singularity Python Converters +# Singularity Python Recipes -We will here discuss the Singularity Python converters that will help you -to convert between recipe files. What kind of things might you want to do? +We will here discuss the Singularity Python recipe writers and parsers that will +help you to convert between Singularity and Docker recipes. First, let's +define what these things are: + + - a *Recipe* is a base class that holds general variables and instructions for a container recipe (e.g., environment, labels, install steps). + - a *parser* is a class that knows how to read in a special recipe type (e.g., Dockerfile) and parse it into the Recipe class. + - a *writer* is a class that knows how to use a filled in Recipe to write a special recipe type (e.g., Singularity) with the content. + +Now we can answer what kind of things might you want to do: - convert a Dockerfile to a Singularity Recipe - - convert a Singularity Recipe to a Dockerfile (TBA) + - convert a Singularity Recipe to a Dockerfile - read in a recipe of either type, and modify it before doing the above # Command Line Client + You don't need to interact with Python to use the converter! It's sometimes much easier to use the command line and spit something out into the terminal, for quick visual inspection or piping into an output file. If you use the @@ -24,30 +32,34 @@ for quick visual inspection or piping into an output file. If you use the ``` -spython --help +spython recipe --help -Singularity Python [v0.0.21] +usage: spython recipe [-h] [--entrypoint ENTRYPOINT] [--json] [--force] + [--parser {auto,docker,singularity}] + [--writer {auto,docker,singularity}] + [files [files ...]] -usage: spython [--debug] [--quiet] [--version] general usage ... - -Singularity Client +positional arguments: + files the recipe input file and [optional] output file optional arguments: - --debug, -d use verbose logging to debug. - --quiet, -q suppress all normal output - --version show singularity and spython version + -h, --help show this help message and exit + --entrypoint ENTRYPOINT + define custom entry point and prevent discovery + --json dump the (base) recipe content as json to the terminal + --force if the output file exists, overwrite. + --parser {auto,docker,singularity} + Is the input a Dockerfile or Singularity recipe? + --writer {auto,docker,singularity} + Should we write to Dockerfile or Singularity recipe? +``` -actions: - actions for Singularity +## Auto Detection - general usage description - recipe Recipe conversion and parsing - shell Interact with singularity python - test Container testing (TBD) -``` +The most basic usage is auto generation - meaning you provide a Dockerfile or +Singularity recipe, and we automatically detect it and convert to the other type. +Until we add additional writers and/or parsers, this is reasonable to do: -We can generate a *Singularity recipe* printed to the console by just providing -the input Dockerfile ``` $ spython recipe Dockerfile @@ -56,26 +68,17 @@ From: python:3.5.1 ... ``` -We could pipe that somewhere... - -``` -$ spython recipe Dockerfile >> Singularity.snowflake -``` - -Or give the filename to the function: +Instead of printing to the screen. we can provide a filename to write to file: ``` $ spython recipe Dockerfile Singularity.snowflake -WARNING /tmp/requirements.txt doesn't exist, ensure exists for build -WARNING requirements.txt doesn't exist, ensure exists for build -WARNING /code/ doesn't exist, ensure exists for build -Saving to Singularity.snowflake ``` -The same can be done for converting a Dockerfile to Singularity +The same auto-detection can be done for converting a Dockerfile to Singularity ``` -$ spython recipe Singularity >> Dockerfile +$ spython recipe Singularity +$ spython recipe Singularity Dockerfile ``` And don't forget you can interact with Docker images natively with Singularity! @@ -84,29 +87,26 @@ And don't forget you can interact with Docker images natively with Singularity! $ singularity pull docker://ubuntu:latest ``` +## Customize Writers and Parsers -## Custom Generation -What else can we do, other than giving an input file and optional output file? -Let's ask for help for the "recipe" command: +If you want to specify the writer or parser to use, this can be done with +the `--writer` and `--parser` argument, respectively. The following would +convert a Dockerfile into a version of itself: +```bash +$ spython recipe --writer docker Dockerfile ``` -$ spython recipe --help -usage: spython recipe [-h] [--entrypoint ENTRYPOINT] [files [files ...]] -positional arguments: - files the recipe input file and [optional] output file +or if our file is named something non-traditional, we would need to specify +the parser too: -optional arguments: - -h, --help show this help message and exit - --entrypoint ENTRYPOINT - define custom entry point and prevent discovery +```bash +$ spython recipe --parser singularity container.def ``` -See the `--entrypoint` argument? If you **don't** specify it, the recipe will be -written and use the Dockerfile `ENTRYPOINT` and then `CMD`, in that order. If -neither of these exist, it defaults to `/bin/bash`. If you **do** specify it, -then your custom entrypoint will be used instead. For example, if I instead -want to change the shell: +## Custom Entrypoint + +Another customization to a recipe can be modifying the entrypoint on the fly. ``` $ spython recipe --entrypoint /bin/sh Dockerfile @@ -115,23 +115,24 @@ $ spython recipe --entrypoint /bin/sh Dockerfile exec /bin/sh "$@" ``` -Notice that the last line (which usually defaults to `/bin/bash`) is what I -specified. Finally, you can ask for help and print with more verbosity! Just ask for `--debug` +## Debug Generation -``` +Finally, you can ask for help and print with more verbosity! Just ask for `--debug` + +```bash $ spython --debug recipe Dockerfile DEBUG Logging level DEBUG -DEBUG Singularity Python Version: 0.0.21 +DEBUG Singularity Python Version: 0.0.63 DEBUG [in] FROM python:3.5.1 -DEBUG FROM ['python:3.5.1'] +DEBUG FROM python:3.5.1 DEBUG [in] ENV PYTHONUNBUFFERED 1 DEBUG [in] RUN apt-get update && apt-get install -y \ -DEBUG [in] RUN apt-get update && apt-get install -y \ -DEBUG [in] RUN git clone https://www.github.com/singularityware/singularity.git -DEBUG [in] WORKDIR singularity -DEBUG [in] RUN ./autogen.sh && ./configure --prefix=/usr/local && make && make install -DEBUG [in] ADD requirements.txt /tmp/requirements.txt -WARNING requirements.txt doesn't exist, ensure exists for build +DEBUG [in] pkg-config \ +DEBUG [in] cmake \ +DEBUG [in] openssl \ +DEBUG [in] wget \ +DEBUG [in] git \ +DEBUG [in] vim ... ``` or less, ask for `--quiet` @@ -140,314 +141,226 @@ or less, ask for `--quiet` $ spython --quiet recipe Dockerfile ``` - # Python API -## Dockerfile Conversion -We will first review conversion of a Dockerfile, from within Python. +# Recipes -### Load the Dockerfile -Let's say we are running Python interactively from our present working directory, -in which we have a Dockerfile. +If you want to create a generic recipe (without association with a container +technology) you can do that. -``` -from spython.main.parse import DockerRecipe -recipe = DockerRecipe('Dockerfile') +```python +from spython.main.parse.recipe import Recipe +recipe = Recipe ``` -If you don't have the paths locally that are specified in `ADD`, or `COPY` (this -might be the case if you are building on a different host) you will get a -warning. +By default, the recipe starts empty. -``` -WARNING /tmp/requirements.txt doesn't exist, ensure exists for build -WARNING requirements.txt doesn't exist, ensure exists for build -WARNING /code/ doesn't exist, ensure exists for build +```python +recipe.json() +{} ``` -That's all you need to do to load! The loading occurs when you create the object. -The finished object is a spython recipe +Generally, you can inspect the attributes to see what can be added! Here +are some examples: -``` -recipe -Out[2]: [spython-recipe][/home/vanessa/Documents/Dropbox/Code/sregistry/singularity-cli/Dockerfile] +```python +recipe.cmd = ['echo', 'hello'] +recipe.entrypoint = '/bin/bash' +recipe.comments = ['This recipe is great', 'Yes it is!'] +recipe.environ = ['PANCAKES=WITHSYRUP'] +recipe.files = [['one', 'two']] +recipe.test = ['true'] +recipe.install = ['apt-get update'] +recipe.labels = ['Maintainer vanessasaur'] +recipe.ports = ['3031'] +recipe.volumes = ['/data'] +recipe.workdir = '/code' ``` -and we can remember it's from a docker base +And then verify they are added: -``` -$ recipe.name -'docker' +```python +recipe.json() +{'cmd': ['echo', 'hello'], + 'comments': ['This recipe is great', 'Yes it is!'], + 'entrypoint': '/bin/bash', + 'environ': ['PANCAKES=WITHSYRUP'], + 'files': [['one', 'two']], + 'install': ['apt-get update'], + 'labels': ['Maintainer vanessasaur'], + 'ports': ['3031'], + 'test': ['true'], + 'volumes': ['/data'], + 'workdir': '/code'} ``` -It has all of the parsed sections from the Dockerfile, -named as you would expect them! These are generally lists and dictionary -data structure that can be easily parsed into another recipe type. At this point -you could inspect them, and modify as needed before doing the conversion. +And then you can use a [writer](#writer) to print a custom recipe type to file. +# Parsers -``` -$ recipe.environ -['PYTHONUNBUFFERED=1'] - -$ recipe.files -[['requirements.txt', '/tmp/requirements.txt'], - ['/home/vanessa/Documents/Dropbox/Code/sregistry/singularity-cli', '/code/']] - -$ recipe.cmd -['/code/run_uwsgi.sh'] +Your first interaction will be with a parser, all of which are defined at +`spython.main.parse.parsers`. If you know the parser you want directly, you +can import it: -$ recipe.install -['PYTHONUNBUFFERED=1', - '\n', - '################################################################################\n', - '# CORE\n', - '# Do not modify this section\n'] -... +```python +from spython.main.parse.parsers import DockerParser ``` -Since Dockerfiles handle defining environment variables at build time and setting -them for the container at runtime, when we encounter an `ENV` section we add -the variable both to the `environ` list *and* as a command for the install -section. - -### Convert to Singularity Recipe -To do the conversion from the Dockerfile to a Singularity recipe, simply call -"convert." This function estimates your desired output based on the input (i.e., -a Dockerfile base is expected to be desired to convert to Singularity Recipe, -and vice versa). This will return a string to the console of your recipe. +or you can use a helper function to get it: +```python +from spython.main.parse.parsers import get_parser +DockerParser = get_parser('docker') +# spython.main.parse.parsers.docker.DockerParser ``` -result = recipe.convert() -print(result) -Bootstrap: docker -From: python:3.5.1 -%files -requirements.txt /tmp/requirements.txt -/home/vanessa/Documents/Dropbox/Code/sregistry/singularity-cli /code/ -%labels -%post -PYTHONUNBUFFERED=1 - -################################################################################ -# CORE -# Do not modify this section - -apt-get update && apt-get install -y \ - pkg-config \ - cmake \ - openssl \ - wget \ - git \ - vim - -apt-get update && apt-get install -y \ - anacron \ - autoconf \ - automake \ - libtool \ - libopenblas-dev \ - libglib2.0-dev \ - gfortran \ - libxml2-dev \ - libxmlsec1-dev \ - libhdf5-dev \ - libgeos-dev \ - libsasl2-dev \ - libldap2-dev \ - build-essential - -# Install Singularity -git clone https://www.github.com/singularityware/singularity.git -cd singularity -./autogen.sh && ./configure --prefix=/usr/local && make && make install - -# Install Python requirements out of /tmp so not triggered if other contents of /code change -pip install -r /tmp/requirements.txt - - -################################################################################ -# PLUGINS -# You are free to comment out those plugins that you don't want to use - -# Install LDAP (uncomment if wanted) -# RUN pip install python3-ldap -# RUN pip install django-auth-ldap - - -mkdir /code -mkdir -p /var/www/images +then give it a Dockerfile to munch on. -cd /code -apt-get remove -y gfortran - -apt-get autoremove -y -apt-get clean -rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* - - -# Install crontab to setup job -echo "0 0 * * * /usr/bin/python /code/manage.py generate_tree" >> /code/cronjob -crontab /code/cronjob -rm /code/cronjob - - -# EXPOSE 3031 -%environment -export PYTHONUNBUFFERED=1 -%runscript -exec /code/run_uwsgi.sh "$@" +```python +parser=DockerParser('Dockerfile') ``` -Note in the above because Singularity recipes do not understand labels like -`EXPOSE` and `VOLUME` they are commented out. -You can also ask for a specific type, either of these would work. +By default, it will parse the Dockerfile (or other container recipe) into a `Recipe` +class, provided at `parser.recipe`: -``` -recipe.convert(convert_to='docker') # convert to Docker -recipe.convert(convert_to='singularity') # convert to Singularity +```python +parser.recipe +[spython-recipe][source:/home/vanessa/Documents/Dropbox/Code/sregistry/singularity-cli/Dockerfile] ``` +You can quickly see the fields with the .json function: -### Save to Singularity Recipe -if you want to save to file, the same logic applies as above, except you can -use the "save" function. If you don't specify an output file, one will -be generated for you in the present working directory, a Singularity or -Dockerfile with a randomly generated extension. - -``` -$ recipe.save() -Saving to Singularity.8q5lkg1n +```python +parser.recipe.json() +{'cmd': '/code/run_uwsgi.sh', + 'environ': ['PYTHONUNBUFFERED=1'], + 'files': [['requirements.txt', '/tmp/requirements.txt'], + ['/home/vanessa/Documents/Dropbox/Code/sregistry/singularity-cli', + '/code/']], + 'install': ['PYTHONUNBUFFERED=1', +... ``` -And you can also name it whatever you like :) +All of these fields are attributes of the recipe, so you could change or otherwise +interact with them: +```python +parser.recipe.entrypoint = '/bin/sh' ``` -$ recipe.save('Singularity.special-snowflake') -Saving to Singularity.special-snowflake -``` - -
      +or if you don't want to, you can skip automatic parsing: -## Singularity Conversion -We will do the same action, but in the opposite direction, convering a Singularity recipe -to a Dockerfile! This is a harder direction because we have to convert each line -from `%post` into a Dockerfile, and we are going from a "chunk" representation to -a "lines" one that warrants more detail. We do our best estimate of ordering by -doing the following: - - - files and labels come first, assuming that content should be added to the container at the beginning. - - any change of directory (cd) at the beginning of a line is replaced with `WORKDIR` +```python +parser = DockerParser('Dockerfile', load=False) +parser.recipe.json() +``` +And then parse it later: -### Load the Singularity Recipe +```python +parser.parse() +``` +The same is available for Singularity recipes: +```python +SingularityParser = get_parser("Singularity") +parser = SingularityParser("Singularity") ``` -from spython.main.parse import SingularityRecipe -recipe = SingularityRecipe('Singularity') +```python +parser.recipe.json() +Out[16]: +{'cmd': 'exec /opt/conda/bin/spython "$@"', + 'install': ['apt-get update && apt-get install -y git', + '# Dependencies', + 'cd /opt', + 'git clone https://www.github.com/singularityhub/singularity-cli', + 'cd singularity-cli', + '/opt/conda/bin/pip install setuptools', + '/opt/conda/bin/python setup.py install'], + 'labels': ['maintainer vsochat@stanford.edu']} -FROM willmclaren/ensembl-vep ``` -We know we have read in a Singularity file! +# Writers -``` -$ recipe.name -'singularity' -``` +Once you have loaded a recipe and possibly made changes, what comes next? +You would want to write it to a possibly different recipe file. For example, +let's read in some Dockerfile, and then hand off the recipe to a SingularityWriter. +The same functions are available to get a writer, or you can import directly. -If you peek at the loaded configuration, you will see that it gets parsed into -the Singularity sections. +```python +from spython.main.parse.writers import get_writer +from spython.main.parse.parsers import get_parser -``` -$ recipe.config -{'comments': ['# sudo singularity build ensembl-vep Singularity'], - 'environment': ['LANGUAGE=en_US', - 'LANG="en_US.UTF-8"', - 'LC_ALL=C', - 'export LANGUAGE LANG LC_ALL', - ''], - 'from': 'willmclaren/ensembl-vep', - 'help': ['This is a singularity file for VEP docker (v1)', ''], - 'labels': ['DARTH VADER', 'QUASI MODO', 'LizardLips NoThankYou', ''], - 'post': ['mkdir /.vep;', - 'mkdir /vep_genomes;', - 'git clone https://github.com/Ensembl/VEP_plugins.git;', - 'git clone https://github.com/griffithlab/pVAC-Seq.git;', - 'cp /pVAC-Seq/pvacseq/VEP_plugins/Wildtype.pm /VEP_plugins;', - 'rm -r /pVAC-Seq;', - ''], - 'runscript': ['exec /home/vep/src/ensembl-vep/vep "$@"']} +DockerParser = get_parser('docker') +SingularityWriter = get_writer('singularity') +# from spython.main.parse.writers import SingularityWriter ``` -### Convert to Dockerfile -You can then use the same convert function to generate your Dockerfile. +First, again parse the Dockerfile: -``` -$ dockerfile = recipe.convert() -$ print(dockerfile) -print(recipe.convert()) -FROM: willmclaren/ensembl-vep -# This is a singularity file for VEP docker (v1) -# sudo singularity build ensembl-vep Singularity -LABEL DARTH VADER -LABEL QUASI MODO -LABEL LizardLips NoThankYou -ENV LANGUAGE=en_US -ENV LANG="en_US.UTF-8" -ENV LC_ALL=C -RUN mkdir /.vep; -RUN mkdir /vep_genomes; -RUN git clone https://github.com/Ensembl/VEP_plugins.git; -RUN git clone https://github.com/griffithlab/pVAC-Seq.git; -RUN cp /pVAC-Seq/pvacseq/VEP_plugins/Wildtype.pm /VEP_plugins; -RUN rm -r /pVAC-Seq; -CMD exec /home/vep/src/ensembl-vep/vep "$@" +```python +parser = DockerParser('Dockerfile') ``` -or instead save it to file: +And then give the recipe object at `parser.recipe` to the writer! -``` -$ recipe.save('Dockerfile') +```python +writer = SingularityWriter(parser.recipe) ``` -## Python Shell -You can also interact with the above functions most quickly via `spython shell`. +How do you generate the new recipe? You can do: +```python +writer.convert() ``` -$ spython shell -Python 3.5.2 |Anaconda 4.2.0 (64-bit)| (default, Jul 2 2016, 17:53:06) -Type "copyright", "credits" or "license" for more information. -IPython 5.1.0 -- An enhanced Interactive Python. -? -> Introduction and overview of IPython's features. -%quickref -> Quick reference. -help -> Python's own help system. -object? -> Details about 'object', use 'object??' for extra details. +To better print it to the screen, you can use print: + +```python +print(writer.convert()) +Bootstrap: docker +From: python:3.5.1 +%files +requirements.txt /tmp/requirements.txt +... +%environment +export PYTHONUNBUFFERED=1 +%runscript +cd /code +exec /bin/bash/bin/bash /code/run_uwsgi.sh "$@" +%startscript +cd /code +exec /bin/bash/bin/bash /code/run_uwsgi.sh "$@" ``` -The parser is added to the client, and you can use it just like before! +Or return to a string, and save to file as you normally would. -``` -In [1]: parser = client.DockerRecipe('Dockerfile') -WARNING /tmp/requirements.txt doesn't exist, ensure exists for build -WARNING requirements.txt doesn't exist, ensure exists for build -WARNING /code/ doesn't exist, ensure exists for build -``` -``` -recipe = parser.convert() -print(recipe) +```python +result = writer.convert() ``` -or do the same for Singularity: +The same works for a DockerWriter. +```python +SingularityParser = get_parser('singularity') +DockerWriter = get_writer('docker') +parser = SingularityParser('Singularity') +writer = DockerWriter(parser.recipe) +print(writer.convert()) +``` ``` -$ parser = client.SingularityRecipe('Singularity') -$ recipe.convert() # convert to Docker +FROM continuumio/miniconda3 +LABEL maintainer vsochat@stanford.edu +RUN apt-get update && apt-get install -y git +RUN cd /opt +RUN git clone https://www.github.com/singularityhub/singularity-cli +RUN cd singularity-cli +RUN /opt/conda/bin/pip install setuptools +RUN /opt/conda/bin/python setup.py install +CMD exec /opt/conda/bin/spython "$@" ```
      diff --git a/spython/client/__init__.py b/spython/client/__init__.py index 2047cb85..da12cf59 100644 --- a/spython/client/__init__.py +++ b/spython/client/__init__.py @@ -46,15 +46,28 @@ def get_parser(): help="define custom entry point and prevent discovery", default=None, type=str) + recipe.add_argument('--json', dest="json", + help="dump the (base) recipe content as json to the terminal", + default=False, action='store_true') + + recipe.add_argument('--force', dest="force", + help="if the output file exists, overwrite.", + default=False, action='store_true') + recipe.add_argument("files", nargs='*', help="the recipe input file and [optional] output file", type=str) - parser.add_argument("-i", "--input", type=str, - default="auto", dest="input", + recipe.add_argument("--parser", type=str, + default="auto", dest="parser", choices=["auto", "docker", "singularity"], help="Is the input a Dockerfile or Singularity recipe?") + recipe.add_argument("--writer", type=str, + default="auto", dest="writer", + choices=["auto", "docker", "singularity"], + help="Should we write to Dockerfile or Singularity recipe?") + # General Commands subparsers.add_parser("shell", help="Interact with singularity python") diff --git a/spython/client/recipe.py b/spython/client/recipe.py index d542eedd..c244bc9e 100644 --- a/spython/client/recipe.py +++ b/spython/client/recipe.py @@ -5,9 +5,18 @@ # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +from spython.main.parse.writers import get_writer +from spython.main.parse.parsers import get_parser -from spython.utils import write_file +from spython.logger import bot +from spython.utils import ( + write_file, + write_json +) + +import json import sys +import os def main(args, options, parser): '''This function serves as a wrapper around the DockerParser, @@ -15,10 +24,6 @@ def main(args, options, parser): We can either save to file if args.outfile is defined, or print to the console if not. ''' - - from spython.main.parse import parsers - from spython.main.parse import writers - # We need something to work with if not args.files: parser.print_help() @@ -29,19 +34,31 @@ def main(args, options, parser): if len(args.files) > 1: outfile = args.files[1] - # Choose the recipe parser - parser = parsers.SingularityParser - writer = writers.SingularityWriter - if args.input == "docker": - parser = parsers.DockerParser - writer = writers.DockerWriter - elif args.input == "singularity": - parser = parsers.SingularityParser - writer = writers.SingularityWriter - else: + # First try to get writer and parser, if not defined will return None + writer = get_writer(args.writer) + parser = get_parser(args.parser) + + # If the user wants to auto-detect the type + if args.parser == "auto": if "dockerfile" in args.files[0].lower(): - parser = parsers.DockerParser - writer = writers.DockerWriter + parser = get_parser('docker') + elif "singularity" in args.files[0].lower(): + parser = get_parser('singularity') + + # If the parser still isn't defined, no go. + if parser is None: + bot.exit('Please provide a Dockerfile or Singularity recipe, or define the --parser type.') + + # If the writer needs auto-detect + if args.writer == "auto": + if parser.name == "docker": + writer = get_writer('singularity') + else: + writer = get_writer('docker') + + # If the writer still isn't defined, no go + if writer is None: + bot.exit('Please define the --writer type.') # Initialize the chosen parser recipeParser = parser(args.files[0]) @@ -52,16 +69,33 @@ def main(args, options, parser): if args.entrypoint is not None: entrypoint = args.entrypoint + + # This is only done if the user intended to print json here + recipeParser.entrypoint = args.entrypoint + recipeParser.cmd = None force = True - # Do the conversion - recipeWriter = writer(recipeParser.recipe) - result = recipeWriter.convert(runscript=entrypoint, force=force) + if args.json is True: - # If the user specifies an output file, save to it - if outfile is not None: - write_file(outfile, result) + if outfile is not None: + if not os.path.exists(outfile): + if force: + write_json(outfile, recipeParser.recipe.json()) + else: + bot.exit('%s exists, set --force to overwrite.' % outfile) + else: + print(json.dumps(recipeParser.recipe.json(), indent=4)) - # Otherwise, convert and print to screen else: - print(result) + + # Do the conversion + recipeWriter = writer(recipeParser.recipe) + result = recipeWriter.convert(runscript=entrypoint, force=force) + + # If the user specifies an output file, save to it + if outfile is not None: + write_file(outfile, result) + + # Otherwise, convert and print to screen + else: + print(result) diff --git a/spython/main/parse/parsers/__init__.py b/spython/main/parse/parsers/__init__.py index 12d3e4f4..c1362da4 100644 --- a/spython/main/parse/parsers/__init__.py +++ b/spython/main/parse/parsers/__init__.py @@ -7,3 +7,19 @@ from .docker import DockerParser from .singularity import SingularityParser + +def get_parser(name): + '''get_parser is a simple helper function to return a parser based on it's + name, if it exists. If there is no writer defined, we return None. + + Parameters + ========== + name: the name of the parser to return. + ''' + name = name.lower() + parsers = {'docker': DockerParser, + 'singularity': SingularityParser, + 'dockerfile': DockerParser} + + if name in parsers: + return parsers[name] diff --git a/spython/main/parse/parsers/base.py b/spython/main/parse/parsers/base.py index ce6f7799..867871df 100644 --- a/spython/main/parse/parsers/base.py +++ b/spython/main/parse/parsers/base.py @@ -35,13 +35,13 @@ def __init__(self, filename=None, load=True): self._run_checks() self.recipe = Recipe(self.filename) - if self.filename and load is True: + if self.filename: # Read in the raw lines of the file self.lines = read_file(self.filename) # If parsing function defined, parse the recipe - if hasattr(self, 'parse'): + if load is True and hasattr(self, 'parse'): self.parse() diff --git a/spython/main/parse/parsers/singularity.py b/spython/main/parse/parsers/singularity.py index d2bb311d..cd9e2e82 100644 --- a/spython/main/parse/parsers/singularity.py +++ b/spython/main/parse/parsers/singularity.py @@ -310,7 +310,7 @@ def _load_section(self, lines, section): def load_recipe(self): - '''load will return a loaded in singularity recipe. The idea + '''load_recipe will return a loaded in singularity recipe. The idea is that these sections can then be parsed into a Dockerfile, or printed back into their original form. diff --git a/spython/main/parse/recipe.py b/spython/main/parse/recipe.py index eb0a56fe..a049de2b 100644 --- a/spython/main/parse/recipe.py +++ b/spython/main/parse/recipe.py @@ -49,5 +49,34 @@ def __str__(self): base = "%s[source:%s]" %(base, self.source) return base + def json(self): + '''return a dictionary version of the recipe, intended to be parsed + or printed as json. + + Returns: a dictionary of attributes including cmd, comments, + entrypoint, environ, files, install, labels, ports, + test, volumes, and workdir. + ''' + attributes = ['cmd', + 'comments', + 'entrypoint', + 'environ', + 'files', + 'install', + 'labels', + 'ports', + 'test', + 'volumes', + 'workdir'] + + result = {} + + for attrib in attributes: + value = getattr(self, attrib) + if value: + result[attrib] = value + + return result + def __repr__(self): return self.__str__() diff --git a/spython/main/parse/writers/__init__.py b/spython/main/parse/writers/__init__.py index 5da9b378..346eb5d3 100644 --- a/spython/main/parse/writers/__init__.py +++ b/spython/main/parse/writers/__init__.py @@ -8,3 +8,20 @@ from .docker import DockerWriter from .singularity import SingularityWriter + + +def get_writer(name): + '''get_writer is a simple helper function to return a writer based on it's + name, if it exists. If there is no writer defined, we return None. + + Parameters + ========== + name: the name of the writer to return. + ''' + name = name.lower() + writers = {'docker': DockerWriter, + 'singularity': SingularityWriter, + 'dockerfile': DockerWriter} + + if name in writers: + return writers[name] diff --git a/spython/main/parse/writers/singularity.py b/spython/main/parse/writers/singularity.py index 86a17e86..5f0729a6 100644 --- a/spython/main/parse/writers/singularity.py +++ b/spython/main/parse/writers/singularity.py @@ -63,7 +63,7 @@ def convert(self, runscript="/bin/bash", force=False): # If a working directory was used, add it as a cd if self.recipe.workdir is not None: - runscript = [self.recipe.workdir] + [runscript] + runscript = ["cd " + self.recipe.workdir] + [runscript] # Finish the recipe, also add as startscript recipe += finish_section(runscript, 'runscript') @@ -188,7 +188,6 @@ def create_keyval_section(pairs, name): ''' section = ['%' + name] for pair in pairs: - print(pair) section.append(' '.join(pair).strip().strip('\\')) return section diff --git a/spython/tests/helpers.sh b/spython/tests/helpers.sh new file mode 100644 index 00000000..30015528 --- /dev/null +++ b/spython/tests/helpers.sh @@ -0,0 +1,25 @@ +runTest() { + + # The first argument is the code we should get + ERROR="${1:-}" + shift + OUTPUT=${1:-} + shift + + "$@" > "${OUTPUT}" 2>&1 + RETVAL="$?" + + if [ "$ERROR" = "0" -a "$RETVAL" != "0" ]; then + echo "$@ (retval=$RETVAL) ERROR" + cat ${OUTPUT} + echo "Output in ${OUTPUT}" + exit 1 + elif [ "$ERROR" != "0" -a "$RETVAL" = "0" ]; then + echo "$@ (retval=$RETVAL) ERROR" + echo "Output in ${OUTPUT}" + cat ${OUTPUT} + exit 1 + else + echo "$@ (retval=$RETVAL) OK" + fi +} diff --git a/spython/tests/test_client.sh b/spython/tests/test_client.sh new file mode 100755 index 00000000..40e459b9 --- /dev/null +++ b/spython/tests/test_client.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +# Include help functions +. helpers.sh + +echo +echo "************** START: test_client.sh **********************" + +# Create temporary testing directory +echo "Creating temporary directory to work in." +tmpdir=$(mktemp -d) +output=$(mktemp ${tmpdir:-/tmp}/spython_test.XXXXXX) +here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +echo "Testing help commands..." + +# Test help for all commands +for command in recipe shell; + do + runTest 0 $output spython $command --help +done + +echo "#### Testing recipe auto generation" +runTest 1 $output spython recipe $here/testdata/Dockerfile | grep "FROM" +runTest 0 $output spython recipe $here/testdata/Dockerfile | grep "%post" +runTest 1 $output spython recipe $here/testdata/Singularity | grep "%post" +runTest 0 $output spython recipe $here/testdata/Singularity | grep "FROM" + +echo "#### Testing recipe targeted generation" +runTest 0 $output spython recipe --writer docker $here/testdata/Dockerfile | grep "FROM" +runTest 1 $output spython recipe --writer docker $here/testdata/Dockerfile | grep "%post" +runTest 0 $output spython recipe --writer singularity $here/testdata/Singularity | grep "%post" +runTest 1 $output spython recipe --writer singularity $here/testdata/Singularity | grep "FROM" + +echo "#### Testing recipe file generation" +outfile=$(mktemp ${tmpdir:-/tmp}/spython_recipe.XXXXXX) +runTest 0 $output spython recipe $here/testdata/Dockerfile $outfile +runTest 0 $output test -f "$outfile" +runTest 0 $output cat $outfile | grep "%post" +rm $outfile + +echo "#### Testing recipe json export" +runTest 0 $output spython recipe --json $here/testdata/Dockerfile | grep "ports" +runTest 0 $output spython recipe $here/testdata/Dockerfile $outfile +runTest 0 $output test -f "$outfile" +runTest 0 $output cat $outfile | grep "%post" + +# Force is false, should fail +echo "#### Testing recipe json export, writing to file" +runTest 0 $output spython recipe --json $here/testdata/Dockerfile $outfile +runTest 0 $output spython recipe --force --json $here/testdata/Dockerfile $outfile +runTest 0 $output test -f "$outfile" +runTest 0 $output cat $outfile | grep "ports" + +echo "Finish testing basic client" +rm -rf ${tmpdir} diff --git a/spython/tests/test_parsers.py b/spython/tests/test_parsers.py index 16fb23ea..3ab71062 100644 --- a/spython/tests/test_parsers.py +++ b/spython/tests/test_parsers.py @@ -26,6 +26,23 @@ def setUp(self): def tearDown(self): shutil.rmtree(self.tmpdir) + + def test_parsers(self): + + print('Testing spython.main.parse.parsers.get_parser') + from spython.main.parse.parsers import get_parser + from spython.main.parse.parsers import DockerParser, SingularityParser + + parser = get_parser('docker') + self.assertEqual(parser, DockerParser) + + parser = get_parser('Dockerfile') + self.assertEqual(parser, DockerParser) + + parser = get_parser('Singularity') + self.assertEqual(parser, SingularityParser) + + def test_docker_parser(self): print('Testing spython.main.parse.parsers DockerParser') diff --git a/spython/tests/test_recipe.py b/spython/tests/test_recipe.py index 890f1271..e660195f 100644 --- a/spython/tests/test_recipe.py +++ b/spython/tests/test_recipe.py @@ -25,11 +25,32 @@ def test_recipe_base(self): self.assertEqual(str(recipe), '[spython-recipe]') attributes = ['cmd', 'comments', 'entrypoint', 'environ', 'files', - 'install', 'labels', 'ports', 'source', 'test', + 'install', 'labels', 'ports', 'test', 'volumes', 'workdir'] for att in attributes: self.assertTrue(hasattr(recipe, att)) + print('Checking that empty recipe returns empty') + result = recipe.json() + self.assertTrue(not result) + + print('Checking that non-empty recipe returns values') + recipe.cmd = ['echo', 'hello'] + recipe.entrypoint = '/bin/bash' + recipe.comments = ['This recipe is great', 'Yes it is!'] + recipe.environ = ['PANCAKES=WITHSYRUP'] + recipe.files = [['one', 'two']] + recipe.test = ['true'] + recipe.install = ['apt-get update'] + recipe.labels = ['Maintainer vanessasaur'] + recipe.ports = ['3031'] + recipe.volumes = ['/data'] + recipe.workdir = '/code' + + result = recipe.json() + for att in attributes: + self.assertTrue(att in result) + if __name__ == '__main__': unittest.main() diff --git a/spython/tests/test_writers.py b/spython/tests/test_writers.py index 5b481834..be81673c 100644 --- a/spython/tests/test_writers.py +++ b/spython/tests/test_writers.py @@ -24,6 +24,21 @@ def setUp(self): def tearDown(self): shutil.rmtree(self.tmpdir) + def test_writers(self): + + print('Testing spython.main.parse.parsers.get_parser') + from spython.main.parse.writers import get_writer + from spython.main.parse.writers import DockerWriter, SingularityWriter + + writer = get_writer('docker') + self.assertEqual(writer, DockerWriter) + + writer = get_writer('Dockerfile') + self.assertEqual(writer, DockerWriter) + + writer = get_writer('Singularity') + self.assertEqual(writer, SingularityWriter) + def test_docker_writer(self): print('Testing spython.main.parse.writers DockerWriter') From 5a8d25ee4fdf5ef5fdf1580e899cfb1dadfa39e4 Mon Sep 17 00:00:00 2001 From: Vanessa Sochat Date: Thu, 30 May 2019 17:42:00 -0400 Subject: [PATCH 13/21] quick fix to TOC Signed-off-by: Vanessa Sochat --- docs/_data/toc.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/_data/toc.yml b/docs/_data/toc.yml index 092029df..21a57fb6 100644 --- a/docs/_data/toc.yml +++ b/docs/_data/toc.yml @@ -16,6 +16,9 @@ - title: "OCI Commands" url: "/singularity-cli/commands-oci" slug: oci + - title: "Recipe Generation" + url: "/singularity-cli/recipes" + slug: recipes - title: "Python API Docstring" url: "https://singularityhub.github.io/singularity-cli/api/source/spython.main.html" slug: docstring From c40b3c5070ff63b5f2070edfc888cfc9d9a5bb17 Mon Sep 17 00:00:00 2001 From: Vanessa Sochat Date: Thu, 30 May 2019 17:52:00 -0400 Subject: [PATCH 14/21] bumping version Signed-off-by: Vanessa Sochat --- spython/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spython/version.py b/spython/version.py index de6ba3d8..7bb0416e 100644 --- a/spython/version.py +++ b/spython/version.py @@ -6,7 +6,7 @@ # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -__version__ = "0.0.63" +__version__ = "0.0.64" AUTHOR = 'Vanessa Sochat' AUTHOR_EMAIL = 'vsochat@stanford.edu' NAME = 'spython' From 38c7727be83c0da58637490db0e5ebb77d02d2fc Mon Sep 17 00:00:00 2001 From: Vanessa Sochat Date: Thu, 30 May 2019 18:00:26 -0400 Subject: [PATCH 15/21] quick update to api docs; Signed-off-by: Vanessa Sochat --- docs/api/_sources/changelog.md.txt | 3 +- docs/api/changelog.html | 3 +- docs/api/genindex.html | 81 ++++++++++++----- docs/api/modules/spython/client.html | 40 ++++++-- docs/api/modules/spython/client/recipe.html | 86 ++++++++++++------ docs/api/modules/spython/client/shell.html | 13 ++- docs/api/modules/spython/image.html | 2 +- docs/api/modules/spython/instance.html | 2 +- docs/api/modules/spython/instance/cmd.html | 4 +- docs/api/modules/spython/logger/progress.html | 4 +- docs/api/modules/spython/logger/spinner.html | 12 ++- docs/api/modules/spython/main.html | 50 +++++----- docs/api/modules/spython/main/apps.html | 2 +- docs/api/modules/spython/main/base.html | 14 ++- .../modules/spython/main/base/command.html | 4 +- .../modules/spython/main/base/generate.html | 8 +- docs/api/modules/spython/main/help.html | 5 +- docs/api/modules/spython/main/instances.html | 2 +- .../modules/spython/main/parse/docker.html | 2 +- .../modules/spython/main/parse/recipe.html | 29 ++++++ docs/api/modules/spython/oci.html | 4 +- .../api/modules/spython/tests/test_utils.html | 21 +++++ docs/api/modules/spython/utils/terminal.html | 35 ++++--- docs/api/objects.inv | Bin 2072 -> 2143 bytes docs/api/searchindex.js | 2 +- docs/api/source/spython.client.html | 12 +-- docs/api/source/spython.instance.html | 83 +++++++++++++++++ docs/api/source/spython.main.base.html | 8 +- docs/api/source/spython.main.html | 8 +- docs/api/source/spython.main.parse.html | 12 +++ docs/api/source/spython.tests.html | 6 ++ docs/api/source/spython.utils.html | 6 ++ 32 files changed, 419 insertions(+), 144 deletions(-) diff --git a/docs/api/_sources/changelog.md.txt b/docs/api/_sources/changelog.md.txt index b5d79696..a0a34518 100644 --- a/docs/api/_sources/changelog.md.txt +++ b/docs/api/_sources/changelog.md.txt @@ -17,7 +17,8 @@ The client here will eventually be released as "spython" (and eventually to singularity on pypi), and the versions here will coincide with these releases. ## [master](https://github.com/singularityhub/singularity-cli/tree/master) - - refactor recipe parsers, writers, and base (0.0.63) + - refactor recipe parsers, writers, and base (0.0.64) + - updated testing to use pytest, linting fixes, and oci state fixes (0.0.63) - fix crash in some error conditions (0.0.62) - more OCI commands accept sudo parameter - working directory, the last one defined, should be added to runscript (0.0.61) diff --git a/docs/api/changelog.html b/docs/api/changelog.html index fc79dfd3..551f191c 100644 --- a/docs/api/changelog.html +++ b/docs/api/changelog.html @@ -171,7 +171,8 @@

      master

        -
      • refactor recipe parsers, writers, and base (0.0.63)

      • +
      • refactor recipe parsers, writers, and base (0.0.64)

      • +
      • updated testing to use pytest, linting fixes, and oci state fixes (0.0.63)

      • fix crash in some error conditions (0.0.62)

        • more OCI commands accept sudo parameter

        • diff --git a/docs/api/genindex.html b/docs/api/genindex.html index 8ea0896e..8f86fd49 100644 --- a/docs/api/genindex.html +++ b/docs/api/genindex.html @@ -159,6 +159,7 @@

          Index

          | G | H | I + | J | L | M | N @@ -200,8 +201,6 @@

          B

        -
      • bpython() (in module spython.client.shell) -
      • build() (in module spython.main.build)
          @@ -246,8 +245,12 @@

          C

          D

          - +
          +

          J

          + + +
          +

          L

          @@ -630,10 +655,10 @@

          S

        • spython.logger.message (module)
        • - - + - +
        • test_check_get_singularity_version() (spython.tests.test_utils.TestUtils method) +
        • +
        • test_check_get_singularity_version_info() (spython.tests.test_utils.TestUtils method)
        • test_check_install() (spython.tests.test_utils.TestUtils method)
        • test_commands() (spython.tests.test_client.TestClient method) -
        • -
        • test_get_installdir() (spython.tests.test_utils.TestUtils method)
        • diff --git a/docs/api/modules/spython/client.html b/docs/api/modules/spython/client.html index f7c64236..d84bcc0b 100644 --- a/docs/api/modules/spython/client.html +++ b/docs/api/modules/spython/client.html @@ -194,15 +194,28 @@

          Source code for spython.client

                                    help="define custom entry point and prevent discovery", 
                                    default=None, type=str)
           
          +    recipe.add_argument('--json', dest="json", 
          +                        help="dump the (base) recipe content as json to the terminal", 
          +                        default=False, action='store_true')
          +
          +    recipe.add_argument('--force', dest="force", 
          +                        help="if the output file exists, overwrite.", 
          +                        default=False, action='store_true')
          +
               recipe.add_argument("files", nargs='*',
                                   help="the recipe input file and [optional] output file", 
                                   type=str)
           
          -    parser.add_argument("-i", "--input", type=str, 
          -                        default="auto", dest="input",
          +    recipe.add_argument("--parser", type=str, 
          +                        default="auto", dest="parser",
                                   choices=["auto", "docker", "singularity"],
                                   help="Is the input a Dockerfile or Singularity recipe?")
           
          +    recipe.add_argument("--writer", type=str, 
          +                        default="auto", dest="writer",
          +                        choices=["auto", "docker", "singularity"],
          +                        help="Should we write to Dockerfile or Singularity recipe?")
          +
               # General Commands
           
               subparsers.add_parser("shell", help="Interact with singularity python")
          @@ -245,7 +258,7 @@ 

          Source code for spython.client

           
               parser = get_parser()
           
          -    def help(return_code=0):
          +    def print_help(return_code=0):
                   '''print help, including the software version and active client 
                      and exit with return code.
                   '''
          @@ -255,7 +268,7 @@ 

          Source code for spython.client

                   sys.exit(return_code)
               
               if len(sys.argv) == 1:
          -        help()
          +        print_help()
               try:
                   # We capture all primary arguments, and take secondary to pass on
                   args, options = parser.parse_known_args()
          @@ -263,7 +276,7 @@ 

          Source code for spython.client

                   sys.exit(0)
           
               # The main function
          -    main = None
          +    func = None
           
               # If the user wants the version
               if args.version is True:
          @@ -274,14 +287,21 @@ 

          Source code for spython.client

               set_verbosity(args)
           
               # Does the user want help for a subcommand?
          -    if args.command == 'recipe': from .recipe import main 
          -    elif args.command == 'shell': from .shell import main 
          -    elif args.command == 'test': from .test import main 
          -    else: help()
          +    if args.command == 'recipe': 
          +        from .recipe import main as func
          +
          +    elif args.command == 'shell':
          +        from .shell import main as func
          +
          +    elif args.command == 'test': 
          +        from .test import main as func
          +
          +    else:
          +        print_help()
           
               # Pass on to the correct parser
               if args.command is not None:
          -        main(args=args, options=options, parser=parser)
          + func(args=args, options=options, parser=parser)
          if __name__ == '__main__': diff --git a/docs/api/modules/spython/client/recipe.html b/docs/api/modules/spython/client/recipe.html index e09368de..cdc0faaa 100644 --- a/docs/api/modules/spython/client/recipe.html +++ b/docs/api/modules/spython/client/recipe.html @@ -155,9 +155,18 @@

          Source code for spython.client.recipe

           # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
           # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
           
          +from spython.main.parse.writers import get_writer
          +from spython.main.parse.parsers import get_parser
           
          -from spython.utils import write_file
          +from spython.logger import bot
          +from spython.utils import (
          +    write_file, 
          +    write_json
          +)
          +
          +import json
           import sys
          +import os
           
           
          [docs]def main(args, options, parser): '''This function serves as a wrapper around the DockerParser, @@ -165,10 +174,6 @@

          Source code for spython.client.recipe

                  We can either save to file if args.outfile is defined, or print 
                  to the console if not.
               '''
          -
          -    from spython.main.parse import parsers
          -    from spython.main.parse import writers
          -
               # We need something to work with
               if not args.files:
                   parser.print_help()
          @@ -179,22 +184,34 @@ 

          Source code for spython.client.recipe

               if len(args.files) > 1:
                   outfile = args.files[1]
           
          -    # Choose the recipe parser
          -    parser = parsers.SingularityParser
          -    writer = writers.SingularityWriter
          -    if args.input == "docker":
          -        parser = parsers.DockerParser
          -        writer = writers.DockerWriter
          -    elif args.input == "singularity":
          -        parser = parsers.SingularityParser
          -        writer = writers.SingularityWriter        
          -    else:
          +    # First try to get writer and parser, if not defined will return None
          +    writer = get_writer(args.writer)
          +    parser = get_parser(args.parser)
          +
          +    # If the user wants to auto-detect the type
          +    if args.parser == "auto":
                   if "dockerfile" in args.files[0].lower():
          -            parser = parsers.DockerParser
          -            writer = writers.DockerWriter
          +            parser = get_parser('docker')
          +        elif "singularity" in args.files[0].lower():
          +            parser = get_parser('singularity')
          +
          +    # If the parser still isn't defined, no go.
          +    if parser is None:
          +        bot.exit('Please provide a Dockerfile or Singularity recipe, or define the --parser type.')
          +
          +    # If the writer needs auto-detect
          +    if args.writer == "auto":
          +        if parser.name == "docker":
          +            writer = get_writer('singularity')
          +        else:
          +            writer = get_writer('docker')
          +
          +    # If the writer still isn't defined, no go
          +    if writer is None:
          +        bot.exit('Please define the --writer type.')
           
               # Initialize the chosen parser
          -    recipe = parser(args.files[0])
          +    recipeParser = parser(args.files[0])
           
               # By default, discover entrypoint / cmd from Dockerfile
               entrypoint = "/bin/bash"
          @@ -202,19 +219,36 @@ 

          Source code for spython.client.recipe

           
               if args.entrypoint is not None:
                   entrypoint = args.entrypoint
          +
          +        # This is only done if the user intended to print json here
          +        recipeParser.entrypoint = args.entrypoint
          +        recipeParser.cmd = None
                   force = True
           
          -    # Do the conversion
          -    recipeWriter = writer(recipe)
          -    result = recipeWriter.convert(runscript=entrypoint, force=force)
          +    if args.json is True:
           
          -    # If the user specifies an output file, save to it
          -    if outfile is not None:
          -        write_file(outfile, result)
          +        if outfile is not None:
          +            if not os.path.exists(outfile):
          +                if force:
          +                    write_json(outfile, recipeParser.recipe.json())
          +                else:
          +                    bot.exit('%s exists, set --force to overwrite.' % outfile)
          +        else:
          +            print(json.dumps(recipeParser.recipe.json(), indent=4))
           
          -    # Otherwise, convert and print to screen
               else:
          -        print(result)
          + + # Do the conversion + recipeWriter = writer(recipeParser.recipe) + result = recipeWriter.convert(runscript=entrypoint, force=force) + + # If the user specifies an output file, save to it + if outfile is not None: + write_file(outfile, result) + + # Otherwise, convert and print to screen + else: + print(result)
          diff --git a/docs/api/modules/spython/client/shell.html b/docs/api/modules/spython/client/shell.html index 0c1b6a5e..28b129b8 100644 --- a/docs/api/modules/spython/client/shell.html +++ b/docs/api/modules/spython/client/shell.html @@ -159,12 +159,12 @@

          Source code for spython.client.shell

           
          [docs]def main(args, options, parser): # If we have options, first is image image = None - if len(options) > 0: + if options: image = options.pop(0) lookup = {'ipython': ipython, 'python': python, - 'bpython': bpython} + 'bpython': run_bpython} shells = ['ipython', 'python', 'bpython'] @@ -195,10 +195,15 @@

          Source code for spython.client.shell

               '''give the user an ipython shell
               '''
               client = prepare_client(image) # pylint: disable=unused-variable
          -    from IPython import embed
          +
          +    try:
          +        from IPython import embed
          +    except ImportError:
          +        return python(image)
          +
               embed()
          -
          [docs]def bpython(image): +
          [docs]def run_bpython(image): '''give the user a bpython shell ''' client = prepare_client(image) diff --git a/docs/api/modules/spython/image.html b/docs/api/modules/spython/image.html index 35e6e1b7..65f48179 100644 --- a/docs/api/modules/spython/image.html +++ b/docs/api/modules/spython/image.html @@ -198,7 +198,7 @@

          Source code for spython.image

                       image: the image uri to parse (required)
           
                   '''
          -        super().__init__()
          +        super(Image, self).__init__()
                   self.parse_image_name(image)
           
           
          diff --git a/docs/api/modules/spython/instance.html b/docs/api/modules/spython/instance.html
          index f2540883..f38db8d4 100644
          --- a/docs/api/modules/spython/instance.html
          +++ b/docs/api/modules/spython/instance.html
          @@ -170,7 +170,7 @@ 

          Source code for spython.instance

                       name: a name for the instance (will generate RobotName 
                               if not provided)
                   '''
          -        super().__init__()
          +        super(Instance, self).__init__()
                   self.parse_image_name(image)
                   self.generate_name(name)
           
          diff --git a/docs/api/modules/spython/instance/cmd.html b/docs/api/modules/spython/instance/cmd.html
          index e5c0dc0a..343b2cdd 100644
          --- a/docs/api/modules/spython/instance/cmd.html
          +++ b/docs/api/modules/spython/instance/cmd.html
          @@ -165,7 +165,7 @@ 

          Source code for spython.instance.cmd

               from spython.instance import Instance
           
               from spython.main.base.logger import println
          -    from spython.main.instances import instances
          +    from spython.main.instances import list_instances
               from spython.utils import run_command as run_cmd
           
               # run_command uses run_cmd, but wraps to catch error
          @@ -178,7 +178,7 @@ 

          Source code for spython.instance.cmd

               Instance._init_command = init_command
               Instance.run_command = run_cmd
               Instance._run_command = run_command
          -    Instance._list = instances  # list command is used to get metadata
          +    Instance._list = list_instances  # list command is used to get metadata
               Instance._println = println
               Instance.start = start     # intended to be called on init, not by user
               Instance.stop = stop
          diff --git a/docs/api/modules/spython/logger/progress.html b/docs/api/modules/spython/logger/progress.html
          index 2c34e256..c24fe95e 100644
          --- a/docs/api/modules/spython/logger/progress.html
          +++ b/docs/api/modules/spython/logger/progress.html
          @@ -254,10 +254,10 @@ 

          Source code for spython.logger.progress

           
               with ProgressBar(label=label, width=width, hide=hide, empty_char=BAR_EMPTY_CHAR,
                        filled_char=BAR_FILLED_CHAR, expected_size=count, every=every) \
          -            as bar:
          +            as pbar:
                   for i, item in enumerate(it):
                       yield item
          -            bar.show(i + 1)
          + pbar.show(i + 1)
          diff --git a/docs/api/modules/spython/logger/spinner.html b/docs/api/modules/spython/logger/spinner.html index 57887173..5c35d183 100644 --- a/docs/api/modules/spython/logger/spinner.html +++ b/docs/api/modules/spython/logger/spinner.html @@ -166,17 +166,20 @@

          Source code for spython.logger.spinner

           
          [docs] @staticmethod def spinning_cursor(): while 1: - for cursor in '|/-\\': yield cursor
          + for cursor in '|/-\\': + yield cursor
          [docs] @staticmethod def balloons_cursor(): while 1: - for cursor in '. o O @ *': yield cursor
          + for cursor in '. o O @ *': + yield cursor
          [docs] @staticmethod def changing_arrows(): while 1: - for cursor in '<^>v': yield cursor
          + for cursor in '<^>v': + yield cursor
          [docs] def select_generator(self, generator): if generator is None: @@ -195,7 +198,8 @@

          Source code for spython.logger.spinner

                       self.spinner_generator = self.changing_arrows()
                   elif generator == 'balloons':
                       self.spinner_generator = self.balloons_cursor()
          -            if delay is None: delay = 0.2
          +            if delay is None:
          +                delay = 0.2
                   else:
                       self.spinner_generator = self.spinning_cursor()
           
          diff --git a/docs/api/modules/spython/main.html b/docs/api/modules/spython/main.html
          index 799475da..54db9303 100644
          --- a/docs/api/modules/spython/main.html
          +++ b/docs/api/modules/spython/main.html
          @@ -166,57 +166,57 @@ 

          Source code for spython.main

           
               '''
               from spython.utils import get_singularity_version
          -    from .base import Client
          +    from .base import Client as client
           
          -    Client.quiet = quiet
          -    Client.debug = debug
          +    client.quiet = quiet
          +    client.debug = debug
           
               # Do imports here, can be customized
               from .apps import apps
               from .build import build
               from .execute import execute 
          -    from .help import help
          +    from .help import helpcmd
               from .inspect import inspect
          -    from .instances import (instances, stopall) # global instance commands
          +    from .instances import (list_instances, stopall) # global instance commands
               from .run import run
               from .pull import pull
               from .export import (export, _export)
           
               # Actions
          -    Client.apps = apps
          -    Client.build = build
          -    Client.execute = execute
          -    Client.export = export
          -    Client._export = _export
          -    Client.help = help
          -    Client.inspect = inspect
          -    Client.instances = instances
          -    Client.run = run
          -    Client.pull = pull
          +    client.apps = apps
          +    client.build = build
          +    client.execute = execute
          +    client.export = export
          +    client._export = _export
          +    client.help = helpcmd
          +    client.inspect = inspect
          +    client.instances = list_instances
          +    client.run = run
          +    client.pull = pull
           
               # Command Groups, Images
               from spython.image.cmd import generate_image_commands  # deprecated
          -    Client.image = generate_image_commands()
          +    client.image = generate_image_commands()
           
               # Commands Groups, Instances
               from spython.instance.cmd import generate_instance_commands  # instance level commands
          -    Client.instance = generate_instance_commands()
          -    Client.instance_stopall = stopall
          -    Client.instance.version = Client.version
          +    client.instance = generate_instance_commands()
          +    client.instance_stopall = stopall
          +    client.instance.version = client.version
           
               # Commands Groups, OCI (Singularity version 3 and up)
               if "version 3" in get_singularity_version():
                   from spython.oci.cmd import generate_oci_commands
          -        Client.oci = generate_oci_commands()()  # first () runs function, second
          +        client.oci = generate_oci_commands()()  # first () runs function, second
                                                           # initializes OciImage class
          -        Client.oci.debug = Client.debug
          -        Client.oci.quiet = Client.quiet
          -        Client.oci.OciImage.quiet = Client.quiet
          -        Client.oci.OciImage.debug = Client.debug
          +        client.oci.debug = client.debug
          +        client.oci.quiet = client.quiet
          +        client.oci.OciImage.quiet = client.quiet
          +        client.oci.OciImage.debug = client.debug
           
           
               # Initialize
          -    cli = Client()
          +    cli = client()
           
               # Pass on verbosity
               for subclient in [cli.image, cli.instance]:
          diff --git a/docs/api/modules/spython/main/apps.html b/docs/api/modules/spython/main/apps.html
          index 986fe257..2254608f 100644
          --- a/docs/api/modules/spython/main/apps.html
          +++ b/docs/api/modules/spython/main/apps.html
          @@ -182,7 +182,7 @@ 

          Source code for spython.main.apps

               if full_path is True:
                   root = '/scif/apps/'
           
          -    if len(output) > 0:
          +    if output:
                   output = ''.join(output).split('\n')
                   output = ['%s%s' %(root, x) for x in output if x]
           
          diff --git a/docs/api/modules/spython/main/base.html b/docs/api/modules/spython/main/base.html
          index 37dc8fc9..db801dff 100644
          --- a/docs/api/modules/spython/main/base.html
          +++ b/docs/api/modules/spython/main/base.html
          @@ -159,7 +159,8 @@ 

          Source code for spython.main.base

           from spython.logger import bot
           from spython.utils import ( 
               check_install,
          -    get_singularity_version
          +    get_singularity_version,
          +    get_singularity_version_info
           )
           
           from .command import (
          @@ -185,7 +186,7 @@ 

          Source code for spython.main.base

           import os
           import re
           
          -
          [docs]class Client: +
          [docs]class Client: def __str__(self): base = "[singularity-python]" @@ -203,11 +204,16 @@

          Source code for spython.main.base

                   '''
                   self._init_level()
           
          -
          [docs] def version(self): - '''a wrapped to get_singularity_version, takes no arguments. +
          [docs] def version(self): + '''Shortcut to get_singularity_version, takes no arguments. ''' return get_singularity_version()
          +
          [docs] def version_info(self): + '''Shortcut to get_singularity_version_info, takes no arguments. + ''' + return get_singularity_version_info()
          + def _check_install(self): '''ensure that singularity is installed, and exit if not. ''' diff --git a/docs/api/modules/spython/main/base/command.html b/docs/api/modules/spython/main/base/command.html index b6e8f36d..5598e0de 100644 --- a/docs/api/modules/spython/main/base/command.html +++ b/docs/api/modules/spython/main/base/command.html @@ -158,9 +158,7 @@

          Source code for spython.main.base.command

           # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
           
           
          -from spython.utils import ( 
          -    run_command as run_cmd
          -)
          +from spython.utils import run_command as run_cmd
           
           from spython.logger import bot
           
          diff --git a/docs/api/modules/spython/main/base/generate.html b/docs/api/modules/spython/main/base/generate.html
          index 69f668e3..6d3665ab 100644
          --- a/docs/api/modules/spython/main/base/generate.html
          +++ b/docs/api/modules/spython/main/base/generate.html
          @@ -161,7 +161,7 @@ 

          Source code for spython.main.base.generate

           
           from random import choice
           
          -
          [docs]class RobotNamer: +
          [docs]class RobotNamer: _descriptors = [ 'chunky', 'buttery', 'delicious', 'scruptious', 'dinosaur', 'boopy', @@ -196,7 +196,7 @@

          Source code for spython.main.base.generate

               ]
           
           
          -
          [docs] def generate(self, delim='-', length=4, chars='0123456789'): +
          [docs] def generate(self, delim='-', length=4, chars='0123456789'): ''' Generate a robot name. Inspiration from Haikunator, but much more poorly implemented ;) @@ -221,13 +221,13 @@

          Source code for spython.main.base.generate

                       ==========
                       should be a list of things to select from
                   '''
          -        if len(select_from) <= 0:
          +        if not select_from:
                       return ''
           
                   return choice(select_from)
          -
          [docs]def main(): +
          [docs]def main(): bot = RobotNamer() print(bot.generate())
          diff --git a/docs/api/modules/spython/main/help.html b/docs/api/modules/spython/main/help.html index 59106549..e9525334 100644 --- a/docs/api/modules/spython/main/help.html +++ b/docs/api/modules/spython/main/help.html @@ -156,7 +156,7 @@

          Source code for spython.main.help

           # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
           
           
          -
          [docs]def help(self, command=None): +
          [docs]def helpcmd(self, command=None): '''help prints the general function help, or help for a specific command Parameters @@ -170,8 +170,7 @@

          Source code for spython.main.help

               cmd = ['singularity', '--help']
               if command is not None:
                   cmd.append(command)
          -    help = self._run_command(cmd)
          -    return help
          + return self._run_command(cmd)
          diff --git a/docs/api/modules/spython/main/instances.html b/docs/api/modules/spython/main/instances.html index ff5ca3dc..20bf1353 100644 --- a/docs/api/modules/spython/main/instances.html +++ b/docs/api/modules/spython/main/instances.html @@ -159,7 +159,7 @@

          Source code for spython.main.instances

           from spython.logger import bot
           from spython.utils import run_command
           
          -
          [docs]def instances(self, name=None, return_json=False, quiet=False): +
          [docs]def list_instances(self, name=None, return_json=False, quiet=False): '''list instances. For Singularity, this is provided as a command sub group. diff --git a/docs/api/modules/spython/main/parse/docker.html b/docs/api/modules/spython/main/parse/docker.html index 7e2c05c5..d04a2723 100644 --- a/docs/api/modules/spython/main/parse/docker.html +++ b/docs/api/modules/spython/main/parse/docker.html @@ -471,7 +471,7 @@

          Source code for spython.main.parse.docker

                   # Save the last working directory to add to the runscript
                   workdir = self._setup('WORKDIR', line)
                   self.workdir = "cd %s" %(''.join(workdir))
          -        self.install.append(''.join(self.workdir))
          +        self.install.append(self.workdir)
           
           
           # Entrypoint and Command
          diff --git a/docs/api/modules/spython/main/parse/recipe.html b/docs/api/modules/spython/main/parse/recipe.html
          index f11f69d9..945f00c8 100644
          --- a/docs/api/modules/spython/main/parse/recipe.html
          +++ b/docs/api/modules/spython/main/parse/recipe.html
          @@ -199,6 +199,35 @@ 

          Source code for spython.main.parse.recipe

                       base = "%s[source:%s]" %(base, self.source)
                   return base
           
          +
          [docs] def json(self): + '''return a dictionary version of the recipe, intended to be parsed + or printed as json. + + Returns: a dictionary of attributes including cmd, comments, + entrypoint, environ, files, install, labels, ports, + test, volumes, and workdir. + ''' + attributes = ['cmd', + 'comments', + 'entrypoint', + 'environ', + 'files', + 'install', + 'labels', + 'ports', + 'test', + 'volumes', + 'workdir'] + + result = {} + + for attrib in attributes: + value = getattr(self, attrib) + if value: + result[attrib] = value + + return result
          + def __repr__(self): return self.__str__()
          diff --git a/docs/api/modules/spython/oci.html b/docs/api/modules/spython/oci.html index e5e1496e..d28c8536 100644 --- a/docs/api/modules/spython/oci.html +++ b/docs/api/modules/spython/oci.html @@ -178,7 +178,7 @@

          Source code for spython.oci

                       sudo: if init is called with or without sudo, keep a record and use
                             for following commands unless sudo is provided to function.
                   '''
          -        super().__init__()
          +        super(OciImage, self).__init__()
           
                   # Will typically be None, unless used outside of Client
                   self.container_id = container_id
          @@ -264,7 +264,7 @@ 

          Source code for spython.oci

                                              return_result=True)
           
                   # Successful return with no output
          -        if len(result) == 0:
          +        if not result:
                       return 
           
                   # Show the response to the user, only if not quiet.
          diff --git a/docs/api/modules/spython/tests/test_utils.html b/docs/api/modules/spython/tests/test_utils.html
          index 62377974..31f5d3bc 100644
          --- a/docs/api/modules/spython/tests/test_utils.html
          +++ b/docs/api/modules/spython/tests/test_utils.html
          @@ -158,6 +158,7 @@ 

          Source code for spython.tests.test_utils

           import tempfile
           import shutil
           import os
          +from semver import VersionInfo
           
           print("############################################################ test_utils")
           
          @@ -233,6 +234,26 @@ 

          Source code for spython.tests.test_utils

                       os.environ['SPYTHON_SINGULARITY_VERSION'] = oldValue
                   self.assertTrue(version == "3.0")
          +
          [docs] def test_check_get_singularity_version_info(self): + '''Check that the version_info is correct''' + from spython.utils import get_singularity_version_info + oldValue = os.environ.get('SPYTHON_SINGULARITY_VERSION') + os.environ['SPYTHON_SINGULARITY_VERSION'] = "2.3.1" + version = get_singularity_version_info() + assert version == VersionInfo(2, 3, 1) + assert version > VersionInfo(2, 3, 0) + assert version < VersionInfo(3, 0, 0) + os.environ['SPYTHON_SINGULARITY_VERSION'] = "singularity version 3.2.1-1" + version = get_singularity_version_info() + assert version == VersionInfo(3, 2, 1, "1") + assert version > VersionInfo(2, 0, 0) + assert version < VersionInfo(3, 3, 0) + # Restore for other tests + if oldValue is None: + del os.environ['SPYTHON_SINGULARITY_VERSION'] + else: + os.environ['SPYTHON_SINGULARITY_VERSION'] = oldValue
          +
          [docs] def test_get_installdir(self): '''get install directory should return the base of where singularity diff --git a/docs/api/modules/spython/utils/terminal.html b/docs/api/modules/spython/utils/terminal.html index 6931cff5..bbac7598 100644 --- a/docs/api/modules/spython/utils/terminal.html +++ b/docs/api/modules/spython/utils/terminal.html @@ -158,18 +158,17 @@

          Source code for spython.utils.terminal

           
           import os
           import re
          -
          +import semver
           from spython.logger import bot
           import subprocess
           import sys
           
          -
           ################################################################################
           # Local commands and requests
           ################################################################################
           
           
          -
          [docs]def check_install(software='singularity', quiet=True): +
          [docs]def check_install(software='singularity', quiet=True): '''check_install will attempt to run the singularity command, and return True if installed. The command line utils will not run without this check. @@ -194,7 +193,7 @@

          Source code for spython.utils.terminal

               return found
          -
          [docs]def get_singularity_version(): +
          [docs]def get_singularity_version(): '''get the full singularity client version as reported by singularity --version [...]. For Singularity 3.x, this means: "singularity version 3.0.1-1" @@ -207,18 +206,27 @@

          Source code for spython.utils.terminal

                       return version
           
                   if version['return_code'] == 0:
          -            if len(version['message']) > 0:
          +            if version['message']:
                           version = version['message'][0].strip('\n')
           
               return version
          -
          [docs]def get_installdir(): +
          [docs]def get_singularity_version_info(): + '''get the full singularity client version as a semantic version" + ''' + version_string = get_singularity_version() + prefix = 'singularity version ' + if version_string.startswith(prefix): + version_string = version_string[len(prefix):] + return semver.parse_version_info(version_string)
          + +
          [docs]def get_installdir(): '''get_installdir returns the installation directory of the application ''' return os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
          -
          [docs]def stream_command(cmd, no_newline_regexp="Progess", sudo=False): +
          [docs]def stream_command(cmd, no_newline_regexp="Progess", sudo=False): '''stream a command (yield) back to the user, as each line is available. # Example usage: @@ -249,7 +257,7 @@

          Source code for spython.utils.terminal

                   raise subprocess.CalledProcessError(return_code, cmd)
          -
          [docs]def run_command(cmd, +
          [docs]def run_command(cmd, sudo=False, capture=True, no_newline_regexp="Progess", @@ -287,8 +295,9 @@

          Source code for spython.utils.terminal

           
               for line in process.communicate():
                   if line:
          -            if isinstance(line, bytes):
          -                line = line.decode('utf-8')
          +            if type(line) is not str:
          +                if isinstance(line, bytes):
          +                    line = line.decode('utf-8')                
                       lines = lines + (line,)
                       if re.search(no_newline_regexp, line) and found_match is True:
                           if quiet is False:
          @@ -313,7 +322,7 @@ 

          Source code for spython.utils.terminal

           
                    
           
          -
          [docs]def format_container_name(name, special_characters=None): +
          [docs]def format_container_name(name, special_characters=None): '''format_container_name will take a name supplied by the user, remove all special characters (except for those defined by "special-characters" and return the new image name. @@ -324,7 +333,7 @@

          Source code for spython.utils.terminal

                              for e in name if e.isalnum() or e in special_characters)
          -
          [docs]def split_uri(container): +
          [docs]def split_uri(container): '''Split the uri of a container into the protocol and image part An empty protocol is returned if none found. @@ -339,7 +348,7 @@

          Source code for spython.utils.terminal

               return protocol, image.rstrip('/')
          -
          [docs]def remove_uri(container): +
          [docs]def remove_uri(container): '''remove_uri will remove docker:// or shub:// from the uri ''' return split_uri(container)[1]
          diff --git a/docs/api/objects.inv b/docs/api/objects.inv index ca1f06fd1faa8e028dd4dd40501a9d89e1b617a0..dc227db663ea468e9c9aa141642d8220eb1af24e 100644 GIT binary patch delta 1995 zcmV;+2Q>JY5Z@4xs((TOG?!goBd?by2^U>xwIn2Q*vv&jA3q=t2M$QoFY?5yq<(o_ zvZA3l`6&^s;W_?M*jwpImh^jj^ZJM6`l`F0Q-UkG^rTJU0i;JPvl?-Mxn|urRW6BA z^lFJVfhiHGf&d)nq{iXpv7B%{Le{YY9RO$&3xIy6DdBal-bg|AmWPz+xY)x?g4u%DySE)Ar$~s)%pJze z!^Y&CkT@ABSlur6V17X&Oos-f#`I<8YF42V@79!;g}-W5B2V?|6gkI8Vpy63oqusg z*l8Yqt{In5>VGWFfd;BO;tF$B(6&i)R+gwL1OSpv*=RDxf49SgI;f zS$QF#)TIRuM^-|jDqrp;ZJPOlb_s4B+MGQ(gDq0hFYLhNCuL2HPlcr9`exL1NZNSUoA&Z(+m3P*+pWl03@X{NBBaU=-B^^hFzkiBKzllo!IS%~^@k(8d1Xuq+v}n#M zRSWXwz!=5Iv$nmG*$z8Lr8=Yd5mPX#>L*-H0aZ^?wglf}j@$K#-Sjh)6y$ezr1Y-+ z;}^vDd)!mBv|vHDf0B96c!^{tNePbDHXnj{Ks>08oq-{&iCa+mbpY;b-4RHYkW$7f zk$>e47j_s>b51oq*;#dCettrg>5fRbu}=b=Uto$}!UoAWBD_-WdvKItPh54f%Y{%(|^`mScitt0t&V!O0$KBpK;zSyII?CwP&qt z3+;qy4aMp=CjeC(sYI>pEttG@k*Gb$OdBplDYR6u4ni3M>mMv9fK^yGaMiXZfVYL` z@gBCI1eUmZgrYgoHb7kFTZ>!8bR(Vs16hk3f_Mi~6&WQ$t{7)mYE}J>NHznTu(n4I=E-|{<*MIE6UO@Ej{O-bNg8AXy%eWxJ_1t$WX(28wL`*v> zQFOpmguoonJfv(ElWI>UJEMl_N}Z|5E|RGxZ|O{`Gwm@$~`p6E>3qfB61 zSbxEtj8^CHfL|UFjhu;c7)8!WEhF(GsV`XmfccFW0!+|3+$-NgoMP#Bgn!@IvOwox z0&SU&x(g8Wpc1A>Szh|i{QAH=aAbrr4F>U=<9qDzg++EwQWk< zV{A~hd4@~>5-X`;Xv2Y^vvel;&S*NsKkiBV^0-4 zSID!S?EB%XyxP^RH29)g?XAn+-BvS9h!&ee!?fEx-MN|BdWoM=U>@Ejl-2KuT}jYN zzje&r@5YD)@lK49Fz>=8RrlZ%&L1*8_jd-iY{d$HjQqSIfS^pxTYtD5?%B!L#cAmU z;Sn<6xMYvmcQ9@zT-21vNNhXvP}9G{EL83>0N!42GoI`9_O8yHFc?Z&KB!jCJST1( z-YlrE7a&yUmWQEio59GktUz6A4qK86(a`4WIO&Bw2o%LKd$JmM7P* zlK!Gep1l3-&8H9VfB*d9>WC{+75LdCHtX6td3V*OzSVzT4ar)jlj2WIRVi^IS>nv5 zlRNv>l{a^{|NZl?NYrzUUUNaZ3ckBinkTZZU#`0wmi^I|+kc7>%7@BN=TF#cHt|<` z{|+7TRa;NCYJ{u&{EmIHtn~_z>Jm5$qVL}JkP-vVFEh6oj)yyU za{~O-NrCcqwFmPH;9)uxAn)8>(OoSFsNrr+WnKAAD-(Ilt5TE<0s-UFEU5d%nPBI6 z@VTK(#8Rhe7JoEQ-4PPVWI;`pmb9(`sdxl3s+Giu)jR;t(Sb5sfiRCHv4iE(h|(`} z9!p(b!Ehwu0+4dKm6$Z{g7yh+Et<=poWT|;<4frED4U?7Xc>xy+vNECTwlAe()p|7 zydj&bYuoZgPR=ZlzpZSRt(mH_BFi)tCcpv1XlVJ<!RM$m9~HQ#d7=+qHCrp2%zyDF2GcvBiMNQOn1X?Qm>1dJ{7ThdvN+_>BN`RlTK{ zvNZHk{((w>6VY4wV%2Hb6+E=ZG-y~Ok&gVHu~fCG@Sj`Jw5jALl>VH`<|?KSJf0dMela&5*8u}ApL|#n}E--wAI2u*96DEg%vVWyWUjpwTgJ!(Gn||h!g5vI;l;4>j zzW}yB;+~`B6%F$Jv&?(SY9I=M>gZ_Y^J6d%fCbgD)iH!su>s{@N8rBNoq%KsF{YI8 zqHG!0-GCf(%HhdQ(@pv136QQi0vX0W@#y>tV(=2y@lKJf4T(giG!8K=MPcTXf;!p@ zw14{F`1yFn@2RQYh;h;<7SGJiY{d18VXlU$?mL@hcM~)X4<`&5x0e|~7leaZhFa91 z5-khqQoJ2jJsrMf0$ZZwU+95~PoVaO#ap0*#T|9Mg>`5Vu42K4iIGSJh0m~Tm&2@W zxW+SAwuN?rxQWFY7KI}j4iVl+e;ZAnwSO`bPoZ8=P>u^=99wF%27)Pw);?&7BAT#i zBT+5u-~E(MWel!A`Y24cf@iBZ+QW@qUFVhFu=7e)~V%q|Ufh`n2ef!MGRF{Vqx z-~cfXfmtDRNZB;z92h1kw}uP5NPnG$yanr0Ef-HGG1K=ls7%2$`$Y@shYAjLVf_WS zGFqL(1AKV|IB_O2P~;^;jYz~#q`so%17tURTm*`p!@Z0t_$ifsOZbg0OY9uF#jrxH zjPoeyQDvMKrFr?g?!Pa?`Ie_8M*i&6oHt8Z9iVFAa-?}#4kE2ZCh=I(c6!iw=u zOp$Tkg-fbx!6#Tg6l!>Gb!^3o3G<-*x))-3A!8?BmBi31#1dq{U`-#P?>x{>IBzf# ziP$<`mxHFNo7UUY7QGEnXY6W#`mV|x(KwX6evq|XcqVi#o)a|KC4Um?FYX)DtwHHJ ziz7?30(HreUyTUBq1D%O@(X_uD6HRFCkLuRMwXBj{`u>5;7iXEgXr+Oq3Clj6--;Z z*tZUM=H-H6LbkU$NS@u>t^5v1x3>?fBL`emuPa(+*RQhSVwXI7``epOAKw4|`NP!_ z5=1Ka+$HWNYE|<7s((&>tA1XM$qds<@h8NxlrR%Cvj(2oo&L_ko4ecp{`r?@^0`*c zBmiZC@2;fgnXH?a>;8sjf7IpHBDf4U%G3E1`kGGsRo}k@M|frIq)o$Z<>zv|RMvpSubmodules

          spython.client.shell module

          -
          -
          -spython.client.shell.bpython(image)[source]
          -

          give the user a bpython shell

          -
          -
          spython.client.shell.ipython(image)[source]
          @@ -220,6 +214,12 @@

          Submodules +
          +spython.client.shell.run_bpython(image)[source]
          +

          give the user a bpython shell

          +

          +

          spython.client.test module

          diff --git a/docs/api/source/spython.instance.html b/docs/api/source/spython.instance.html index 91fb8bb1..ec5767f1 100644 --- a/docs/api/source/spython.instance.html +++ b/docs/api/source/spython.instance.html @@ -192,6 +192,16 @@

          Subpackages class spython.instance.Instance(image, start=True, name=None, **kwargs)[source]

          Bases: spython.image.ImageBase

          +
          +
          +RobotNamer = <spython.main.base.generate.RobotNamer object>
          +
          + +
          +
          +debug = False
          +
          +
          generate_name(name=None)[source]
          @@ -205,6 +215,12 @@

          Subpackages +
          +instance
          +

          alias of Instance

          +

          +
          parse_image_name(image)[source]
          @@ -217,6 +233,73 @@

          Subpackages +
          +quiet = False
          +

          + +
          +
          +run_command(sudo=False, capture=True, no_newline_regexp='Progess', quiet=False)
          +

          run_command uses subprocess to send a command to the terminal. If +capture is True, we use the parent stdout, so the progress bar (and +other commands of interest) are piped to the user. This means we +don’t return the output to parse.

          +
          +
          Parameters
          +
            +
          • cmd (the command to send, should be a list for subprocess)

          • +
          • sudo (if needed, add to start of command)

          • +
          • no_newline_regexp (the regular expression to determine skipping a) – newline. Defaults to finding Progress

          • +
          • capture (if True, don’t set stdout and have it go to console. This) – option can print a progress bar, but won’t return the lines +as output.

          • +
          +
          +
          +
          + +
          +
          +start(image=None, name=None, args=None, sudo=False, options=None, capture=False)
          +

          start an instance. This is done by default when an instance is created.

          +
          +
          Parameters
          +
            +
          • image (optionally, an image uri (if called as a command from Client))

          • +
          • name (a name for the instance)

          • +
          • sudo (if the user wants to run the command with sudo)

          • +
          • capture (capture output, default is False. With True likely to hang.)

          • +
          • args (arguments to provide to the instance (supported Singularity 3.1+))

          • +
          • options (a list of tuples, each an option to give to the start command) – [(“–bind”, “/tmp”),…]

          • +
          • USAGE

          • +
          • singularity […] instance.start […] <container path> <instance name>

          • +
          +
          +
          +
          + +
          +
          +stop(name=None, sudo=False)
          +

          stop an instance. This is done by default when an instance is created.

          +
          +
          Parameters
          +
            +
          • name (a name for the instance)

          • +
          • sudo (if the user wants to run the command with sudo)

          • +
          • USAGE

          • +
          • singularity […] instance.stop […] <instance name>

          • +
          +
          +
          +
          + +
          +
          +classmethod version()
          +

          Shortcut to get_singularity_version, takes no arguments.

          +
          +

          diff --git a/docs/api/source/spython.main.base.html b/docs/api/source/spython.main.base.html index f0e8f251..a7f79007 100644 --- a/docs/api/source/spython.main.base.html +++ b/docs/api/source/spython.main.base.html @@ -669,7 +669,13 @@

          Submodules
          version()[source]
          -

          a wrapped to get_singularity_version, takes no arguments.

          +

          Shortcut to get_singularity_version, takes no arguments.

          +
          + +
          +
          +version_info()[source]
          +

          Shortcut to get_singularity_version_info, takes no arguments.

          diff --git a/docs/api/source/spython.main.html b/docs/api/source/spython.main.html index 38b7760c..159a97d9 100644 --- a/docs/api/source/spython.main.html +++ b/docs/api/source/spython.main.html @@ -285,8 +285,8 @@

          Submodules

          spython.main.help module

          -
          -spython.main.help.help(self, command=None)[source]
          +
          +spython.main.help.helpcmd(self, command=None)[source]

          help prints the general function help, or help for a specific command

          Parameters
          @@ -330,8 +330,8 @@

          Submodules

          spython.main.instances module

          -
          -spython.main.instances.instances(self, name=None, return_json=False, quiet=False)[source]
          +
          +spython.main.instances.list_instances(self, name=None, return_json=False, quiet=False)[source]

          list instances. For Singularity, this is provided as a command sub group.

          singularity instance.list

          diff --git a/docs/api/source/spython.main.parse.html b/docs/api/source/spython.main.parse.html index 8fb21f4d..a9989f79 100644 --- a/docs/api/source/spython.main.parse.html +++ b/docs/api/source/spython.main.parse.html @@ -340,6 +340,18 @@

          Submodules

          recipe (the original recipe file, parsed by the subclass either) – DockerParser or SingularityParser

          +
          +
          +json()[source]
          +

          return a dictionary version of the recipe, intended to be parsed +or printed as json.

          +
          +
          Returns: a dictionary of attributes including cmd, comments,

          entrypoint, environ, files, install, labels, ports, +test, volumes, and workdir.

          +
          +
          +
          +

          diff --git a/docs/api/source/spython.tests.html b/docs/api/source/spython.tests.html index 0faa68ab..b1c912f9 100644 --- a/docs/api/source/spython.tests.html +++ b/docs/api/source/spython.tests.html @@ -233,6 +233,12 @@

          Submodules +
          +test_check_get_singularity_version_info()[source]
          +

          Check that the version_info is correct

          +
          +
          test_check_install()[source]
          diff --git a/docs/api/source/spython.utils.html b/docs/api/source/spython.utils.html index a12dd069..a9b294a7 100644 --- a/docs/api/source/spython.utils.html +++ b/docs/api/source/spython.utils.html @@ -256,6 +256,12 @@

          Submodules +
          +spython.utils.terminal.get_singularity_version_info()[source]
          +

          get the full singularity client version as a semantic version”

          +

          +
          spython.utils.terminal.remove_uri(container)[source]
          From 3196ddbcaf3626acb131047f7787929f7362994d Mon Sep 17 00:00:00 2001 From: Vanessa Sochat Date: Sat, 1 Jun 2019 11:28:35 -0400 Subject: [PATCH 16/21] adding recipe tests for recipe converter Signed-off-by: Vanessa Sochat --- spython/main/parse/parsers/docker.py | 21 ---- spython/main/parse/parsers/singularity.py | 23 +++- spython/main/parse/writers/docker.py | 13 ++- spython/main/parse/writers/singularity.py | 20 +++- spython/tests/test_conversion.py | 105 ++++++++++++++++++ spython/tests/testdata/README.md | 9 ++ .../tests/testdata/docker2singularity/add.def | 8 ++ .../testdata/docker2singularity/add.docker | 2 + .../tests/testdata/docker2singularity/cmd.def | 6 + .../testdata/docker2singularity/cmd.docker | 2 + .../testdata/docker2singularity/comments.def | 12 ++ .../docker2singularity/comments.docker | 6 + .../testdata/docker2singularity/copy.def | 8 ++ .../testdata/docker2singularity/copy.docker | 2 + .../docker2singularity/entrypoint.def | 6 + .../docker2singularity/entrypoint.docker | 2 + .../testdata/docker2singularity/expose.def | 9 ++ .../testdata/docker2singularity/expose.docker | 3 + .../testdata/docker2singularity/from.def | 6 + .../testdata/docker2singularity/from.docker | 1 + .../docker2singularity/healthcheck.def | 8 ++ .../docker2singularity/healthcheck.docker | 2 + .../testdata/docker2singularity/label.def | 8 ++ .../testdata/docker2singularity/label.docker | 2 + .../docker2singularity/multiple-lines.def | 13 +++ .../docker2singularity/multiple-lines.docker | 7 ++ .../testdata/docker2singularity/user.def | 11 ++ .../testdata/docker2singularity/user.docker | 5 + .../testdata/docker2singularity/workdir.def | 10 ++ .../docker2singularity/workdir.docker | 2 + .../testdata/singularity2docker/files.def | 5 + .../testdata/singularity2docker/files.docker | 3 + .../testdata/singularity2docker/from.def | 2 + .../testdata/singularity2docker/from.docker | 1 + .../testdata/singularity2docker/labels.def | 5 + .../testdata/singularity2docker/labels.docker | 3 + .../singularity2docker/multiple-lines.def | 9 ++ .../singularity2docker/multiple-lines.docker | 6 + .../testdata/singularity2docker/post.def | 6 + .../testdata/singularity2docker/post.docker | 4 + .../testdata/singularity2docker/runscript.def | 4 + .../singularity2docker/runscript.docker | 2 + .../testdata/singularity2docker/test.def | 4 + .../testdata/singularity2docker/test.docker | 4 + 44 files changed, 357 insertions(+), 33 deletions(-) create mode 100644 spython/tests/test_conversion.py create mode 100644 spython/tests/testdata/README.md create mode 100644 spython/tests/testdata/docker2singularity/add.def create mode 100644 spython/tests/testdata/docker2singularity/add.docker create mode 100644 spython/tests/testdata/docker2singularity/cmd.def create mode 100644 spython/tests/testdata/docker2singularity/cmd.docker create mode 100644 spython/tests/testdata/docker2singularity/comments.def create mode 100644 spython/tests/testdata/docker2singularity/comments.docker create mode 100644 spython/tests/testdata/docker2singularity/copy.def create mode 100644 spython/tests/testdata/docker2singularity/copy.docker create mode 100644 spython/tests/testdata/docker2singularity/entrypoint.def create mode 100644 spython/tests/testdata/docker2singularity/entrypoint.docker create mode 100644 spython/tests/testdata/docker2singularity/expose.def create mode 100644 spython/tests/testdata/docker2singularity/expose.docker create mode 100644 spython/tests/testdata/docker2singularity/from.def create mode 100644 spython/tests/testdata/docker2singularity/from.docker create mode 100644 spython/tests/testdata/docker2singularity/healthcheck.def create mode 100644 spython/tests/testdata/docker2singularity/healthcheck.docker create mode 100644 spython/tests/testdata/docker2singularity/label.def create mode 100644 spython/tests/testdata/docker2singularity/label.docker create mode 100644 spython/tests/testdata/docker2singularity/multiple-lines.def create mode 100644 spython/tests/testdata/docker2singularity/multiple-lines.docker create mode 100644 spython/tests/testdata/docker2singularity/user.def create mode 100644 spython/tests/testdata/docker2singularity/user.docker create mode 100644 spython/tests/testdata/docker2singularity/workdir.def create mode 100644 spython/tests/testdata/docker2singularity/workdir.docker create mode 100644 spython/tests/testdata/singularity2docker/files.def create mode 100644 spython/tests/testdata/singularity2docker/files.docker create mode 100644 spython/tests/testdata/singularity2docker/from.def create mode 100644 spython/tests/testdata/singularity2docker/from.docker create mode 100644 spython/tests/testdata/singularity2docker/labels.def create mode 100644 spython/tests/testdata/singularity2docker/labels.docker create mode 100644 spython/tests/testdata/singularity2docker/multiple-lines.def create mode 100644 spython/tests/testdata/singularity2docker/multiple-lines.docker create mode 100644 spython/tests/testdata/singularity2docker/post.def create mode 100644 spython/tests/testdata/singularity2docker/post.docker create mode 100644 spython/tests/testdata/singularity2docker/runscript.def create mode 100644 spython/tests/testdata/singularity2docker/runscript.docker create mode 100644 spython/tests/testdata/singularity2docker/test.def create mode 100644 spython/tests/testdata/singularity2docker/test.docker diff --git a/spython/main/parse/parsers/docker.py b/spython/main/parse/parsers/docker.py index 1007d4f3..782cc5b3 100644 --- a/spython/main/parse/parsers/docker.py +++ b/spython/main/parse/parsers/docker.py @@ -534,24 +534,3 @@ def _clean_line(self, line): # A line that is None should return empty string line = line or '' return line.split('#')[0].strip() - - - def _write_script(self, path, lines, chmod=True): - '''write a script with some lines content to path in the image. This - is done by way of adding echo statements to the install section. - - Parameters - ========== - path: the path to the file to write - lines: the lines to echo to the file - chmod: If true, change permission to make u+x - - ''' - if lines: - lastline = lines.pop() - for line in lines: - self.recipe.install.append('echo "%s" >> %s' %line %path) - self.recipe.install.append(lastline) - - if chmod is True: - self.recipe.install.append('chmod u+x %s' %path) diff --git a/spython/main/parse/parsers/singularity.py b/spython/main/parse/parsers/singularity.py index cd9e2e82..5edc759c 100644 --- a/spython/main/parse/parsers/singularity.py +++ b/spython/main/parse/parsers/singularity.py @@ -102,8 +102,6 @@ def _from(self, line): # Run and Test Parser - - def _test(self, lines): ''' A healthcheck is generally a test command @@ -221,7 +219,7 @@ def _post(self, lines): ========== lines: the lines from the recipe with install commands - ''' + ''' self.recipe.install += lines @@ -270,7 +268,6 @@ def _load_from(self, line): # Remove any comments line = line.split('#', 1)[0] line = re.sub('(F|f)(R|r)(O|o)(M|m):', '', line).strip() - bot.info('FROM %s' % line) self.config['from'] = line @@ -386,3 +383,21 @@ def _add_section(self, line, section=None): bot.debug("Adding section %s" % section) return section + + + def _write_script(self, path, lines, chmod=True): + '''write a script with some lines content to path in the image. This + is done by way of adding echo statements to the install section. + + Parameters + ========== + path: the path to the file to write + lines: the lines to echo to the file + chmod: If true, change permission to make u+x + + ''' + for line in lines: + self.recipe.install.append('echo "%s" >> %s' % (line, path)) + + if chmod is True: + self.recipe.install.append('chmod u+x %s' % path) diff --git a/spython/main/parse/writers/docker.py b/spython/main/parse/writers/docker.py index 2e8fb803..fd18af6d 100644 --- a/spython/main/parse/writers/docker.py +++ b/spython/main/parse/writers/docker.py @@ -114,10 +114,11 @@ def convert(self, runscript="/bin/bash", force=False): recipe.append('ENTRYPOINT %s' % self.recipe.entrypoint) if self.recipe.test is not None: - recipe.append(write_lines('HEALTHCHECK', self.recipe.test)) + recipe += write_lines('HEALTHCHECK', self.recipe.test) # Clean up extra white spaces - return '\n'.join(recipe).replace('\n\n', '\n') + recipe = '\n'.join(recipe).replace('\n\n', '\n') + return recipe.rstrip() def write_files(label, lines): @@ -130,7 +131,10 @@ def write_files(label, lines): ''' result = [] for line in lines: - result.append('%s %s %s' %(label, line[0], line[1])) + if isinstance(line, list): + result.append('%s %s %s' %(label, line[0], line[1])) + else: + result.append('%s %s' %(label, line)) return result def write_lines(label, lines): @@ -141,6 +145,9 @@ def write_lines(label, lines): lines: one or more lines to write, with header appended ''' + if not isinstance(lines, list): + lines = [lines] + result = [] continued = False for line in lines: diff --git a/spython/main/parse/writers/singularity.py b/spython/main/parse/writers/singularity.py index 5f0729a6..6c440beb 100644 --- a/spython/main/parse/writers/singularity.py +++ b/spython/main/parse/writers/singularity.py @@ -73,7 +73,8 @@ def convert(self, runscript="/bin/bash", force=False): recipe += finish_section(self.recipe.test, 'test') # Clean up extra white spaces - return '\n'.join(recipe).replace('\n\n', '\n') + recipe = '\n'.join(recipe).replace('\n\n', '\n') + return recipe.rstrip() def _create_runscript(self, default="/bin/bash", force=False): @@ -92,6 +93,7 @@ def _create_runscript(self, default="/bin/bash", force=False): # Only look at Docker if not enforcing default if not force: + if self.recipe.entrypoint is not None: # The provided entrypoint can be a string or a list @@ -105,7 +107,6 @@ def _create_runscript(self, default="/bin/bash", force=False): entrypoint = entrypoint + ' ' + ' '.join(self.recipe.cmd) else: entrypoint = entrypoint + ' ' + ''.join(self.recipe.cmd) - entrypoint = default + entrypoint # Entrypoint should use exec if not entrypoint.startswith('exec'): @@ -148,7 +149,7 @@ def _create_section(self, attribute, name=None): return section # Files - if attribute in ['files']: + if attribute in ['files', 'labels']: return create_keyval_section(section, name) # An environment section needs exports @@ -168,12 +169,21 @@ def finish_section(section, name): section: the section content, without a header name: the name of the section for the header - ''' + ''' + if not isinstance(section, list): section = [section] + # Convert USER lines to change user + lines = [] + for line in section: + if "USER" in line: + username = line.replace('USER', '').rstrip() + line = "su - %s" % username + ' # ' + line + lines.append(line) + header = ['%' + name] - return header + section + return header + lines def create_keyval_section(pairs, name): diff --git a/spython/tests/test_conversion.py b/spython/tests/test_conversion.py new file mode 100644 index 00000000..3a3ad0c2 --- /dev/null +++ b/spython/tests/test_conversion.py @@ -0,0 +1,105 @@ +#!/usr/bin/python + +# Copyright (C) 2019 Vanessa Sochat. + +# This Source Code Form is subject to the terms of the +# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed +# with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from spython.utils import get_installdir +import unittest +import tempfile +import shutil +import filecmp +from glob import glob +import os + +print("########################################################test_conversion") + +class TestConversion(unittest.TestCase): + + def setUp(self): + self.pwd = get_installdir() + self.d2s = os.path.join(self.pwd, 'tests', 'testdata', 'docker2singularity') + self.s2d = os.path.join(self.pwd, 'tests', 'testdata', 'singularity2docker') + self.tmpdir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.tmpdir) + + def test_pairs(self): + + print('Testing that each recipe file has a pair of the other type.') + dockerfiles = glob(os.path.join(self.d2s, '*.docker')) + dockerfiles += glob(os.path.join(self.s2d, '*.docker')) + + for dockerfile in dockerfiles: + name, _ = os.path.splitext(dockerfile) + recipe = "%s.def" % name + + if not os.path.exists(recipe): + print('%s does not exist.' % recipe) + self.assertTrue(os.path.exists(recipe)) + + + def test_docker2singularity(self): + + print('Testing spython conversion from docker2singularity') + from spython.main.parse.parsers import DockerParser + from spython.main.parse.writers import SingularityWriter + + dockerfiles = glob(os.path.join(self.d2s, '*.docker')) + + for dockerfile in dockerfiles: + name, _ = os.path.splitext(dockerfile) + + # Matching Singularity recipe ends with name + recipe = "%s.def" % name + + parser = DockerParser(dockerfile) + writer = SingularityWriter(parser.recipe) + + suffix = next(tempfile._get_candidate_names()) + output_file = "%s.%s" %(os.path.join(self.tmpdir, + os.path.basename(recipe)), suffix) + + # Write generated content to file + with open(output_file, 'w') as filey: + filey.write(writer.convert()) + + # Compare to actual + if not filecmp.cmp(recipe, output_file): + print('Comparison %s to %s failed.' %(recipe, output_file)) + self.assertTrue(filecmp.cmp(recipe, output_file)) + + def test_singularity2docker(self): + + print('Testing spython conversion from singularity2docker') + from spython.main.parse.parsers import SingularityParser + from spython.main.parse.writers import DockerWriter + + recipes = glob(os.path.join(self.s2d, '*.def')) + + for recipe in recipes: + name, _ = os.path.splitext(recipe) + dockerfile = "%s.docker" % name + + parser = SingularityParser(recipe) + writer = DockerWriter(parser.recipe) + + suffix = next(tempfile._get_candidate_names()) + output_file = "%s.%s" %(os.path.join(self.tmpdir, + os.path.basename(dockerfile)), suffix) + + # Write generated content to file + with open(output_file, 'w') as filey: + filey.write(writer.convert()) + + # Compare to actual + if not filecmp.cmp(dockerfile, output_file): + print('Comparison %s to %s failed.' %(dockerfile, output_file)) + self.assertTrue(filecmp.cmp(dockerfile, output_file)) + + +if __name__ == '__main__': + unittest.main() diff --git a/spython/tests/testdata/README.md b/spython/tests/testdata/README.md new file mode 100644 index 00000000..bff1c7c2 --- /dev/null +++ b/spython/tests/testdata/README.md @@ -0,0 +1,9 @@ +# Test Data + +This folder contains test data for Singularity Python. + + - [Singularity](Singularity) and [Docker](Docker) are generic recipes used to run the tests in [one folder up](../). They are not inended to be converted between one another. + - [singularity2docker](singularity2docker) is a folder of docker recipes (`*.docker`) and Singularity recipes (`*.def`) that are tested for conversion *from* Singularity to Docker. + - [docker2singularity](docker2singularity) is a folder of Singularity recipes (`*.def`) and docker recipes (`*.docker`) that are tested for conversion *from* Docker to Singularity. + +To add a new pair of recipes to either folder, simply write a .def and .docker file with the same name. They will be tested by [test_conversion.py](../test_conversion.py). diff --git a/spython/tests/testdata/docker2singularity/add.def b/spython/tests/testdata/docker2singularity/add.def new file mode 100644 index 00000000..9196f8df --- /dev/null +++ b/spython/tests/testdata/docker2singularity/add.def @@ -0,0 +1,8 @@ +Bootstrap: docker +From: busybox:latest +%files +/home/vanessa/Documents/Dropbox/Code/sregistry/singularity-cli /opt +%runscript +exec /bin/bash "$@" +%startscript +exec /bin/bash "$@" \ No newline at end of file diff --git a/spython/tests/testdata/docker2singularity/add.docker b/spython/tests/testdata/docker2singularity/add.docker new file mode 100644 index 00000000..30e72391 --- /dev/null +++ b/spython/tests/testdata/docker2singularity/add.docker @@ -0,0 +1,2 @@ +FROM busybox:latest +ADD . /opt diff --git a/spython/tests/testdata/docker2singularity/cmd.def b/spython/tests/testdata/docker2singularity/cmd.def new file mode 100644 index 00000000..d40c42a1 --- /dev/null +++ b/spython/tests/testdata/docker2singularity/cmd.def @@ -0,0 +1,6 @@ +Bootstrap: docker +From: busybox:latest +%runscript +exec /bin/bash echo hello "$@" +%startscript +exec /bin/bash echo hello "$@" \ No newline at end of file diff --git a/spython/tests/testdata/docker2singularity/cmd.docker b/spython/tests/testdata/docker2singularity/cmd.docker new file mode 100644 index 00000000..22810608 --- /dev/null +++ b/spython/tests/testdata/docker2singularity/cmd.docker @@ -0,0 +1,2 @@ +FROM busybox:latest +CMD ["echo", "hello"] diff --git a/spython/tests/testdata/docker2singularity/comments.def b/spython/tests/testdata/docker2singularity/comments.def new file mode 100644 index 00000000..6cedd2e1 --- /dev/null +++ b/spython/tests/testdata/docker2singularity/comments.def @@ -0,0 +1,12 @@ +Bootstrap: docker +From: busybox:latest +%post + +# This is a really important line +cp /bin/echo /opt/echo + +# I'm sure you agree with me? +%runscript +exec /bin/bash "$@" +%startscript +exec /bin/bash "$@" \ No newline at end of file diff --git a/spython/tests/testdata/docker2singularity/comments.docker b/spython/tests/testdata/docker2singularity/comments.docker new file mode 100644 index 00000000..ad1d35ff --- /dev/null +++ b/spython/tests/testdata/docker2singularity/comments.docker @@ -0,0 +1,6 @@ +FROM busybox:latest + +# This is a really important line +RUN cp /bin/echo /opt/echo + +# I'm sure you agree with me? diff --git a/spython/tests/testdata/docker2singularity/copy.def b/spython/tests/testdata/docker2singularity/copy.def new file mode 100644 index 00000000..9196f8df --- /dev/null +++ b/spython/tests/testdata/docker2singularity/copy.def @@ -0,0 +1,8 @@ +Bootstrap: docker +From: busybox:latest +%files +/home/vanessa/Documents/Dropbox/Code/sregistry/singularity-cli /opt +%runscript +exec /bin/bash "$@" +%startscript +exec /bin/bash "$@" \ No newline at end of file diff --git a/spython/tests/testdata/docker2singularity/copy.docker b/spython/tests/testdata/docker2singularity/copy.docker new file mode 100644 index 00000000..de2857b3 --- /dev/null +++ b/spython/tests/testdata/docker2singularity/copy.docker @@ -0,0 +1,2 @@ +FROM busybox:latest +COPY . /opt diff --git a/spython/tests/testdata/docker2singularity/entrypoint.def b/spython/tests/testdata/docker2singularity/entrypoint.def new file mode 100644 index 00000000..da3fba3a --- /dev/null +++ b/spython/tests/testdata/docker2singularity/entrypoint.def @@ -0,0 +1,6 @@ +Bootstrap: docker +From: busybox:latest +%runscript +exec /bin/bash run_uwsgi.sh "$@" +%startscript +exec /bin/bash run_uwsgi.sh "$@" \ No newline at end of file diff --git a/spython/tests/testdata/docker2singularity/entrypoint.docker b/spython/tests/testdata/docker2singularity/entrypoint.docker new file mode 100644 index 00000000..0b1cac4d --- /dev/null +++ b/spython/tests/testdata/docker2singularity/entrypoint.docker @@ -0,0 +1,2 @@ +FROM busybox:latest +ENTRYPOINT /bin/bash run_uwsgi.sh diff --git a/spython/tests/testdata/docker2singularity/expose.def b/spython/tests/testdata/docker2singularity/expose.def new file mode 100644 index 00000000..74502b27 --- /dev/null +++ b/spython/tests/testdata/docker2singularity/expose.def @@ -0,0 +1,9 @@ +Bootstrap: docker +From: busybox:latest +%post +# EXPOSE 3031 +# EXPOSE 9000 +%runscript +exec /bin/bash "$@" +%startscript +exec /bin/bash "$@" \ No newline at end of file diff --git a/spython/tests/testdata/docker2singularity/expose.docker b/spython/tests/testdata/docker2singularity/expose.docker new file mode 100644 index 00000000..24262a87 --- /dev/null +++ b/spython/tests/testdata/docker2singularity/expose.docker @@ -0,0 +1,3 @@ +FROM busybox:latest +EXPOSE 3031 +EXPOSE 9000 diff --git a/spython/tests/testdata/docker2singularity/from.def b/spython/tests/testdata/docker2singularity/from.def new file mode 100644 index 00000000..b71937ec --- /dev/null +++ b/spython/tests/testdata/docker2singularity/from.def @@ -0,0 +1,6 @@ +Bootstrap: docker +From: busybox:latest +%runscript +exec /bin/bash "$@" +%startscript +exec /bin/bash "$@" \ No newline at end of file diff --git a/spython/tests/testdata/docker2singularity/from.docker b/spython/tests/testdata/docker2singularity/from.docker new file mode 100644 index 00000000..9a3adf68 --- /dev/null +++ b/spython/tests/testdata/docker2singularity/from.docker @@ -0,0 +1 @@ +FROM busybox:latest diff --git a/spython/tests/testdata/docker2singularity/healthcheck.def b/spython/tests/testdata/docker2singularity/healthcheck.def new file mode 100644 index 00000000..b2c0bd2a --- /dev/null +++ b/spython/tests/testdata/docker2singularity/healthcheck.def @@ -0,0 +1,8 @@ +Bootstrap: docker +From: busybox:latest +%runscript +exec /bin/bash "$@" +%startscript +exec /bin/bash "$@" +%test +true \ No newline at end of file diff --git a/spython/tests/testdata/docker2singularity/healthcheck.docker b/spython/tests/testdata/docker2singularity/healthcheck.docker new file mode 100644 index 00000000..d91f0d59 --- /dev/null +++ b/spython/tests/testdata/docker2singularity/healthcheck.docker @@ -0,0 +1,2 @@ +FROM busybox:latest +HEALTHCHECK true diff --git a/spython/tests/testdata/docker2singularity/label.def b/spython/tests/testdata/docker2singularity/label.def new file mode 100644 index 00000000..6c8bb8fd --- /dev/null +++ b/spython/tests/testdata/docker2singularity/label.def @@ -0,0 +1,8 @@ +Bootstrap: docker +From: busybox:latest +%labels +maintainer dinosaur +%runscript +exec /bin/bash "$@" +%startscript +exec /bin/bash "$@" \ No newline at end of file diff --git a/spython/tests/testdata/docker2singularity/label.docker b/spython/tests/testdata/docker2singularity/label.docker new file mode 100644 index 00000000..ec9a8f24 --- /dev/null +++ b/spython/tests/testdata/docker2singularity/label.docker @@ -0,0 +1,2 @@ +FROM busybox:latest +LABEL maintainer dinosaur diff --git a/spython/tests/testdata/docker2singularity/multiple-lines.def b/spython/tests/testdata/docker2singularity/multiple-lines.def new file mode 100644 index 00000000..02d6f86c --- /dev/null +++ b/spython/tests/testdata/docker2singularity/multiple-lines.def @@ -0,0 +1,13 @@ +Bootstrap: docker +From: busybox:latest +%post + +apt-get update && \ +apt-get install -y git \ +wget \ +curl \ +squashfs-tools +%runscript +exec /bin/bash "$@" +%startscript +exec /bin/bash "$@" \ No newline at end of file diff --git a/spython/tests/testdata/docker2singularity/multiple-lines.docker b/spython/tests/testdata/docker2singularity/multiple-lines.docker new file mode 100644 index 00000000..780db838 --- /dev/null +++ b/spython/tests/testdata/docker2singularity/multiple-lines.docker @@ -0,0 +1,7 @@ +FROM busybox:latest + +RUN apt-get update && \ + apt-get install -y git \ + wget \ + curl \ + squashfs-tools diff --git a/spython/tests/testdata/docker2singularity/user.def b/spython/tests/testdata/docker2singularity/user.def new file mode 100644 index 00000000..c46871bc --- /dev/null +++ b/spython/tests/testdata/docker2singularity/user.def @@ -0,0 +1,11 @@ +Bootstrap: docker +From: busybox:latest +%post +echo "cloud" +su - rainman # USER rainman +echo "makeitrain" +su - root # USER root +%runscript +exec /bin/bash "$@" +%startscript +exec /bin/bash "$@" \ No newline at end of file diff --git a/spython/tests/testdata/docker2singularity/user.docker b/spython/tests/testdata/docker2singularity/user.docker new file mode 100644 index 00000000..7d87433e --- /dev/null +++ b/spython/tests/testdata/docker2singularity/user.docker @@ -0,0 +1,5 @@ +FROM busybox:latest +RUN echo "cloud" +USER rainman +RUN echo "makeitrain" +USER root diff --git a/spython/tests/testdata/docker2singularity/workdir.def b/spython/tests/testdata/docker2singularity/workdir.def new file mode 100644 index 00000000..7e1276f8 --- /dev/null +++ b/spython/tests/testdata/docker2singularity/workdir.def @@ -0,0 +1,10 @@ +Bootstrap: docker +From: busybox:latest +%post +cd /code +%runscript +cd /code +exec /bin/bash "$@" +%startscript +cd /code +exec /bin/bash "$@" \ No newline at end of file diff --git a/spython/tests/testdata/docker2singularity/workdir.docker b/spython/tests/testdata/docker2singularity/workdir.docker new file mode 100644 index 00000000..9e51c915 --- /dev/null +++ b/spython/tests/testdata/docker2singularity/workdir.docker @@ -0,0 +1,2 @@ +FROM busybox:latest +WORKDIR /code diff --git a/spython/tests/testdata/singularity2docker/files.def b/spython/tests/testdata/singularity2docker/files.def new file mode 100644 index 00000000..80851e92 --- /dev/null +++ b/spython/tests/testdata/singularity2docker/files.def @@ -0,0 +1,5 @@ +Bootstrap: docker +From: busybox:latest +%files +file.txt /opt/file.txt +/path/to/thing /opt/thing diff --git a/spython/tests/testdata/singularity2docker/files.docker b/spython/tests/testdata/singularity2docker/files.docker new file mode 100644 index 00000000..920b21f9 --- /dev/null +++ b/spython/tests/testdata/singularity2docker/files.docker @@ -0,0 +1,3 @@ +FROM busybox:latest +ADD file.txt /opt/file.txt +ADD /path/to/thing /opt/thing \ No newline at end of file diff --git a/spython/tests/testdata/singularity2docker/from.def b/spython/tests/testdata/singularity2docker/from.def new file mode 100644 index 00000000..3d7b01b6 --- /dev/null +++ b/spython/tests/testdata/singularity2docker/from.def @@ -0,0 +1,2 @@ +Bootstrap: docker +From: busybox:latest diff --git a/spython/tests/testdata/singularity2docker/from.docker b/spython/tests/testdata/singularity2docker/from.docker new file mode 100644 index 00000000..f51439e3 --- /dev/null +++ b/spython/tests/testdata/singularity2docker/from.docker @@ -0,0 +1 @@ +FROM busybox:latest \ No newline at end of file diff --git a/spython/tests/testdata/singularity2docker/labels.def b/spython/tests/testdata/singularity2docker/labels.def new file mode 100644 index 00000000..e990711a --- /dev/null +++ b/spython/tests/testdata/singularity2docker/labels.def @@ -0,0 +1,5 @@ +Bootstrap: docker +From: busybox:latest +%labels +Maintainer dinosaur +Version 1.0.0 diff --git a/spython/tests/testdata/singularity2docker/labels.docker b/spython/tests/testdata/singularity2docker/labels.docker new file mode 100644 index 00000000..3d126e35 --- /dev/null +++ b/spython/tests/testdata/singularity2docker/labels.docker @@ -0,0 +1,3 @@ +FROM busybox:latest +LABEL Maintainer dinosaur +LABEL Version 1.0.0 \ No newline at end of file diff --git a/spython/tests/testdata/singularity2docker/multiple-lines.def b/spython/tests/testdata/singularity2docker/multiple-lines.def new file mode 100644 index 00000000..b2e9aab9 --- /dev/null +++ b/spython/tests/testdata/singularity2docker/multiple-lines.def @@ -0,0 +1,9 @@ +Bootstrap: docker +From: busybox:latest +%post + +apt-get update && \ +apt-get install -y git \ + wget \ + curl \ + squashfs-tools diff --git a/spython/tests/testdata/singularity2docker/multiple-lines.docker b/spython/tests/testdata/singularity2docker/multiple-lines.docker new file mode 100644 index 00000000..75ccaa68 --- /dev/null +++ b/spython/tests/testdata/singularity2docker/multiple-lines.docker @@ -0,0 +1,6 @@ +FROM busybox:latest +RUN apt-get update && \ +apt-get install -y git \ +wget \ +curl \ +squashfs-tools \ No newline at end of file diff --git a/spython/tests/testdata/singularity2docker/post.def b/spython/tests/testdata/singularity2docker/post.def new file mode 100644 index 00000000..35991e4b --- /dev/null +++ b/spython/tests/testdata/singularity2docker/post.def @@ -0,0 +1,6 @@ +Bootstrap: docker +From: busybox:latest +%post +apt-get update +apt-get install -y git \ + wget diff --git a/spython/tests/testdata/singularity2docker/post.docker b/spython/tests/testdata/singularity2docker/post.docker new file mode 100644 index 00000000..6872d43e --- /dev/null +++ b/spython/tests/testdata/singularity2docker/post.docker @@ -0,0 +1,4 @@ +FROM busybox:latest +RUN apt-get update +RUN apt-get install -y git \ +wget \ No newline at end of file diff --git a/spython/tests/testdata/singularity2docker/runscript.def b/spython/tests/testdata/singularity2docker/runscript.def new file mode 100644 index 00000000..669f6948 --- /dev/null +++ b/spython/tests/testdata/singularity2docker/runscript.def @@ -0,0 +1,4 @@ +Bootstrap: docker +From: busybox:latest +%runscript +exec /bin/bash echo hello "$@" diff --git a/spython/tests/testdata/singularity2docker/runscript.docker b/spython/tests/testdata/singularity2docker/runscript.docker new file mode 100644 index 00000000..94fd139c --- /dev/null +++ b/spython/tests/testdata/singularity2docker/runscript.docker @@ -0,0 +1,2 @@ +FROM busybox:latest +CMD exec /bin/bash echo hello "$@" \ No newline at end of file diff --git a/spython/tests/testdata/singularity2docker/test.def b/spython/tests/testdata/singularity2docker/test.def new file mode 100644 index 00000000..06376c26 --- /dev/null +++ b/spython/tests/testdata/singularity2docker/test.def @@ -0,0 +1,4 @@ +Bootstrap: docker +From: busybox:latest +%test +true diff --git a/spython/tests/testdata/singularity2docker/test.docker b/spython/tests/testdata/singularity2docker/test.docker new file mode 100644 index 00000000..cbd12cc9 --- /dev/null +++ b/spython/tests/testdata/singularity2docker/test.docker @@ -0,0 +1,4 @@ +FROM busybox:latest +RUN echo "true" >> /tests.sh +RUN chmod u+x /tests.sh +HEALTHCHECK /bin/bash /tests.sh \ No newline at end of file From caa29da705cec7c43d749a454de324d8b83d831a Mon Sep 17 00:00:00 2001 From: Vanessa Sochat Date: Sat, 1 Jun 2019 11:31:16 -0400 Subject: [PATCH 17/21] updating docs Signed-off-by: Vanessa Sochat --- docs/api/genindex.html | 50 ++----- docs/api/objects.inv | Bin 2143 -> 2007 bytes docs/api/py-modindex.html | 20 --- docs/api/searchindex.js | 2 +- docs/api/source/spython.html | 8 +- docs/api/source/spython.main.html | 8 +- docs/api/source/spython.main.parse.html | 167 ++---------------------- 7 files changed, 26 insertions(+), 229 deletions(-) diff --git a/docs/api/genindex.html b/docs/api/genindex.html index 8f86fd49..bee5f47d 100644 --- a/docs/api/genindex.html +++ b/docs/api/genindex.html @@ -220,20 +220,12 @@

          C

        • Client (class in spython.main.base)
        • compress() (in module spython.image.cmd.utils) -
        • -
        • convert2boolean() (in module spython.logger.message) -
        • -
        • create() (in module spython.image.cmd.create)
    • debug() (spython.logger.message.SingularityMessage method) -
    • -
    • decompress() (in module spython.image.cmd.utils)
    •     spython.main.parse
          - spython.main.parse.converters -
          - spython.main.parse.docker -
          - spython.main.parse.environment -
          spython.main.parse.recipe
          - spython.main.parse.singularity -
          diff --git a/docs/api/searchindex.js b/docs/api/searchindex.js index b995557f..ccc2c7d0 100644 --- a/docs/api/searchindex.js +++ b/docs/api/searchindex.js @@ -1 +1 @@ -Search.setIndex({docnames:["changelog","index","source/modules","source/spython","source/spython.client","source/spython.image","source/spython.image.cmd","source/spython.instance","source/spython.instance.cmd","source/spython.logger","source/spython.main","source/spython.main.base","source/spython.main.parse","source/spython.tests","source/spython.utils"],envversion:{"sphinx.domains.c":1,"sphinx.domains.changeset":1,"sphinx.domains.cpp":1,"sphinx.domains.javascript":1,"sphinx.domains.math":2,"sphinx.domains.python":1,"sphinx.domains.rst":1,"sphinx.domains.std":1,"sphinx.ext.intersphinx":1,"sphinx.ext.todo":1,"sphinx.ext.viewcode":1,sphinx:56},filenames:["changelog.md","index.rst","source/modules.rst","source/spython.rst","source/spython.client.rst","source/spython.image.rst","source/spython.image.cmd.rst","source/spython.instance.rst","source/spython.instance.cmd.rst","source/spython.logger.rst","source/spython.main.rst","source/spython.main.base.rst","source/spython.main.parse.rst","source/spython.tests.rst","source/spython.utils.rst"],objects:{"":{spython:[3,0,0,"-"]},"spython.client":{get_parser:[4,1,1,""],main:[4,1,1,""],recipe:[4,0,0,"-"],set_verbosity:[4,1,1,""],shell:[4,0,0,"-"],test:[4,0,0,"-"],version:[4,1,1,""]},"spython.client.recipe":{main:[4,1,1,""]},"spython.client.shell":{ipython:[4,1,1,""],main:[4,1,1,""],prepare_client:[4,1,1,""],python:[4,1,1,""],run_bpython:[4,1,1,""]},"spython.client.test":{main:[4,1,1,""]},"spython.image":{Image:[5,2,1,""],ImageBase:[5,2,1,""],cmd:[6,0,0,"-"]},"spython.image.Image":{get_hash:[5,3,1,""]},"spython.image.ImageBase":{parse_image_name:[5,3,1,""]},"spython.image.cmd":{"export":[6,0,0,"-"],create:[6,0,0,"-"],generate_image_commands:[6,1,1,""],importcmd:[6,0,0,"-"],utils:[6,0,0,"-"]},"spython.image.cmd.create":{create:[6,1,1,""]},"spython.image.cmd.export":{"export":[6,1,1,""]},"spython.image.cmd.importcmd":{importcmd:[6,1,1,""]},"spython.image.cmd.utils":{compress:[6,1,1,""],decompress:[6,1,1,""]},"spython.instance":{Instance:[7,2,1,""],cmd:[8,0,0,"-"]},"spython.instance.Instance":{RobotNamer:[7,4,1,""],debug:[7,4,1,""],generate_name:[7,3,1,""],get_uri:[7,3,1,""],instance:[7,4,1,""],parse_image_name:[7,3,1,""],quiet:[7,4,1,""],run_command:[7,3,1,""],start:[7,3,1,""],stop:[7,3,1,""],version:[7,5,1,""]},"spython.instance.cmd":{generate_instance_commands:[8,1,1,""],iutils:[8,0,0,"-"],start:[8,0,0,"-"],stop:[8,0,0,"-"]},"spython.instance.cmd.iutils":{get:[8,1,1,""],parse_table:[8,1,1,""]},"spython.instance.cmd.start":{start:[8,1,1,""]},"spython.instance.cmd.stop":{stop:[8,1,1,""]},"spython.logger":{message:[9,0,0,"-"],progress:[9,0,0,"-"],spinner:[9,0,0,"-"]},"spython.logger.message":{SingularityMessage:[9,2,1,""],convert2boolean:[9,1,1,""],get_logging_level:[9,1,1,""],get_user_color_preference:[9,1,1,""]},"spython.logger.message.SingularityMessage":{abort:[9,3,1,""],addColor:[9,3,1,""],critical:[9,3,1,""],custom:[9,3,1,""],debug:[9,3,1,""],emit:[9,3,1,""],emitError:[9,3,1,""],emitOutput:[9,3,1,""],error:[9,3,1,""],exit:[9,3,1,""],get_logs:[9,3,1,""],info:[9,3,1,""],isEnabledFor:[9,3,1,""],is_quiet:[9,3,1,""],log:[9,3,1,""],newline:[9,3,1,""],println:[9,3,1,""],show_progress:[9,3,1,""],spinner:[9,4,1,""],table:[9,3,1,""],useColor:[9,3,1,""],verbose1:[9,3,1,""],verbose2:[9,3,1,""],verbose3:[9,3,1,""],verbose:[9,3,1,""],warning:[9,3,1,""],write:[9,3,1,""]},"spython.logger.progress":{ProgressBar:[9,2,1,""],bar:[9,1,1,""]},"spython.logger.progress.ProgressBar":{done:[9,3,1,""],format_time:[9,3,1,""],show:[9,3,1,""]},"spython.logger.spinner":{Spinner:[9,2,1,""]},"spython.logger.spinner.Spinner":{balloons_cursor:[9,6,1,""],changing_arrows:[9,6,1,""],delay:[9,4,1,""],run:[9,3,1,""],select_generator:[9,3,1,""],spinning:[9,4,1,""],spinning_cursor:[9,6,1,""],start:[9,3,1,""],stop:[9,3,1,""]},"spython.main":{apps:[10,0,0,"-"],base:[11,0,0,"-"],build:[10,0,0,"-"],execute:[10,0,0,"-"],get_client:[10,1,1,""],help:[10,0,0,"-"],inspect:[10,0,0,"-"],instances:[10,0,0,"-"],parse:[12,0,0,"-"],pull:[10,0,0,"-"],run:[10,0,0,"-"]},"spython.main.apps":{apps:[10,1,1,""]},"spython.main.base":{Client:[11,2,1,""],command:[11,0,0,"-"],flags:[11,0,0,"-"],generate:[11,0,0,"-"],logger:[11,0,0,"-"],sutils:[11,0,0,"-"]},"spython.main.base.Client":{"export":[11,3,1,""],RobotNamer:[11,4,1,""],apps:[11,3,1,""],build:[11,3,1,""],debug:[11,4,1,""],execute:[11,3,1,""],help:[11,3,1,""],image:[11,4,1,""],inspect:[11,3,1,""],instance:[11,4,1,""],instance_stopall:[11,3,1,""],instances:[11,3,1,""],load:[11,3,1,""],oci:[11,4,1,""],pull:[11,3,1,""],quiet:[11,4,1,""],run:[11,3,1,""],setenv:[11,3,1,""],version:[11,3,1,""],version_info:[11,3,1,""]},"spython.main.base.command":{generate_bind_list:[11,1,1,""],init_command:[11,1,1,""],run_command:[11,1,1,""],send_command:[11,1,1,""]},"spython.main.base.flags":{parse_verbosity:[11,1,1,""]},"spython.main.base.generate":{RobotNamer:[11,2,1,""],main:[11,1,1,""]},"spython.main.base.generate.RobotNamer":{generate:[11,3,1,""]},"spython.main.base.logger":{init_level:[11,1,1,""],println:[11,1,1,""]},"spython.main.base.sutils":{get_filename:[11,1,1,""],get_uri:[11,1,1,""],load:[11,1,1,""],setenv:[11,1,1,""]},"spython.main.build":{build:[10,1,1,""]},"spython.main.execute":{execute:[10,1,1,""]},"spython.main.help":{helpcmd:[10,1,1,""]},"spython.main.inspect":{inspect:[10,1,1,""],parse_labels:[10,1,1,""]},"spython.main.instances":{list_instances:[10,1,1,""],stopall:[10,1,1,""]},"spython.main.parse":{converters:[12,0,0,"-"],docker:[12,0,0,"-"],environment:[12,0,0,"-"],recipe:[12,0,0,"-"],singularity:[12,0,0,"-"]},"spython.main.parse.converters":{create_env_section:[12,1,1,""],create_keyval_section:[12,1,1,""],create_runscript:[12,1,1,""],create_section:[12,1,1,""],docker2singularity:[12,1,1,""],finish_section:[12,1,1,""],singularity2docker:[12,1,1,""],write_lines:[12,1,1,""]},"spython.main.parse.docker":{DockerRecipe:[12,2,1,""]},"spython.main.parse.environment":{parse_env:[12,1,1,""]},"spython.main.parse.recipe":{Recipe:[12,2,1,""]},"spython.main.parse.recipe.Recipe":{json:[12,3,1,""]},"spython.main.parse.singularity":{SingularityRecipe:[12,2,1,""]},"spython.main.parse.singularity.SingularityRecipe":{load_recipe:[12,3,1,""]},"spython.main.pull":{pull:[10,1,1,""]},"spython.main.run":{run:[10,1,1,""]},"spython.tests":{test_client:[13,0,0,"-"],test_instances:[13,0,0,"-"],test_utils:[13,0,0,"-"]},"spython.tests.test_client":{TestClient:[13,2,1,""]},"spython.tests.test_client.TestClient":{setUp:[13,3,1,""],tearDown:[13,3,1,""],test_commands:[13,3,1,""]},"spython.tests.test_instances":{TestInstances:[13,2,1,""]},"spython.tests.test_instances.TestInstances":{setUp:[13,3,1,""],tearDown:[13,3,1,""],test_instance_class:[13,3,1,""],test_instances:[13,3,1,""]},"spython.tests.test_utils":{TestUtils:[13,2,1,""]},"spython.tests.test_utils.TestUtils":{setUp:[13,3,1,""],tearDown:[13,3,1,""],test_check_get_singularity_version:[13,3,1,""],test_check_get_singularity_version_info:[13,3,1,""],test_check_install:[13,3,1,""],test_get_installdir:[13,3,1,""],test_remove_uri:[13,3,1,""],test_split_uri:[13,3,1,""],test_write_read_files:[13,3,1,""]},"spython.utils":{fileio:[14,0,0,"-"],terminal:[14,0,0,"-"]},"spython.utils.fileio":{mkdir_p:[14,1,1,""],read_file:[14,1,1,""],read_json:[14,1,1,""],write_file:[14,1,1,""],write_json:[14,1,1,""]},"spython.utils.terminal":{check_install:[14,1,1,""],format_container_name:[14,1,1,""],get_installdir:[14,1,1,""],get_singularity_version:[14,1,1,""],get_singularity_version_info:[14,1,1,""],remove_uri:[14,1,1,""],run_command:[14,1,1,""],split_uri:[14,1,1,""],stream_command:[14,1,1,""]},spython:{client:[4,0,0,"-"],image:[5,0,0,"-"],instance:[7,0,0,"-"],logger:[9,0,0,"-"],main:[10,0,0,"-"],tests:[13,0,0,"-"],utils:[14,0,0,"-"],version:[3,0,0,"-"]}},objnames:{"0":["py","module","Python module"],"1":["py","function","Python function"],"2":["py","class","Python class"],"3":["py","method","Python method"],"4":["py","attribute","Python attribute"],"5":["py","classmethod","Python class method"],"6":["py","staticmethod","Python static method"]},objtypes:{"0":"py:module","1":"py:function","2":"py:class","3":"py:method","4":"py:attribute","5":"py:classmethod","6":"py:staticmethod"},terms:{"1024mib":6,"95m":9,"boolean":[0,9,10,11],"byte":11,"case":[10,13],"char":11,"class":[5,7,9,11,12,13],"default":[0,6,7,8,9,10,11,12,14],"export":[0,3,5,11,12],"function":[0,4,6,9,10,11,12,13,14],"import":[0,6,10],"int":9,"new":[6,11,14],"public":[3,12,14],"return":[0,5,7,8,9,10,11,12,13,14],"static":9,"true":[6,7,8,9,10,11,12,14],Added:0,Adding:0,For:[10,11,12,14],The:[0,1,6,8,10,11,12,14],These:6,Useful:[],Will:9,With:[7,8],__init__:6,_pars:[],abil:0,abort:9,about:[10,11],accept:0,access:[10,11],account:0,action:[0,11],actual:[5,11],add:[7,12,14],addcolor:9,added:[0,6,9,10,11],adding:0,addit:[0,6,11],adjust:0,after:[11,13],alia:[7,11],all:[0,9,10,11,14],allow:[10,11],along:7,also:[11,12],ani:[10,11,12],anoth:[],api:0,app:[2,3,11],appear:11,append:[12,14],applic:[11,14],appropri:[],arbitari:[10,11],arg:[0,4,7,8,9,10,11],argpars:11,argument:[0,7,8,10,11,12],around:4,ascii:9,associ:[10,11],assum:[8,9,13],attempt:[11,14],attribut:[0,12],automat:[10,11],avail:[11,14],back:[0,11,12,14],backward:0,balloons_cursor:9,bar:[7,9,14],base:[0,3,4,5,7,9,10,12,13],bash:12,basic:0,been:0,befor:13,behaviour:0,being:9,best:12,between:[],bin:12,bind:[0,7,8,10,11],bindlist:11,bootstrap:11,both:[4,11],bourn:11,bpython:4,branch:0,bug:0,build:[0,2,3,11,12],build_fold:[10,11],builder:[10,11,12],built:[10,11],bundl:11,call:[0,7,8,9,10,11,12],can:[0,3,4,7,11,12,14],capabl:11,captur:[7,8,10,11,14],carriage_return:9,chang:0,changelog:1,changing_arrow:9,charact:[9,14],check:[0,9,11,13,14],check_instal:14,choos:[],classmethod:7,cli:0,client:[0,1,2,3,6,7,8,10,11,14],close:[0,14],cmd:[3,5,7,11,12,14],code:[0,3,10,11,12,14],coincid:0,col_width:9,color:9,column:[8,9],com:[10,11,12],command:[0,3,6,7,8,10,12,13,14],comment:12,commit:0,commonli:6,complet:[5,7,9,10,11],compress:6,condit:0,config:12,configur:9,consol:[0,4,7,14],contain:[0,1,7,8,10,11,12,14],content:[1,2],context:[10,11],continu:0,conveni:[],convers:0,convert2boolean:9,convert:[0,3,4,10,11],convert_to:[],copi:[0,3,12,14],copyright:[3,12,14],correct:[0,13],couldn:[10,11],count:9,crash:0,creat:[3,5,7,8,9,10,11,12,14],create_entrypoint:12,create_env_sect:12,create_keyval_sect:12,create_runscript:12,create_sect:12,criteria:5,critic:[0,9],crypto:11,current:9,custom:[6,9,10,11,12],data:[10,11,12,14],debug:[6,7,8,9,10,11],decompress:6,deconstruct:13,defil:[10,11],defin:[0,4,6,10,11,12,14],delai:9,delim:11,delimit:11,deprec:[0,6,11],deriv:11,descriptor:11,desir:[],dest:0,destin:0,determin:[4,7,9,12,14],dict:14,dictionari:[9,11,12],differ:[0,10,11],dimens:8,directori:[0,10,11,13,14],disabl:[10,11],discoveri:[],displai:11,distribut:[3,12,14],doc:12,docker2singular:12,docker:[0,3,5,7,10,11,14],dockerfil:[0,12],dockerpars:[4,12],dockerrecip:[0,12],dockerwrit:4,document:0,doe:[11,12],doesn:[7,10,11,12],don:[7,8,10,11,14],done:[7,8,9,10,11],driver:[10,11],each:[0,7,8,14],easili:[],either:[4,11,12],emb:4,emit:9,emiterror:9,emitoutput:9,empti:[0,11,14],empty_char:9,enabl:9,encod:9,end:[12,14],engin:12,enhanc:0,entir:[10,11],entri:9,entrypoint:12,env:[0,12],environ:[3,4,6,8,9,10,11],environment:9,envlist:12,equal:12,error:[0,9,10,11],estim:12,etc:8,eventu:0,everi:9,exampl:[11,12,14],except:14,exec:[0,11],execut:[0,2,3,11],exercis:13,exist:[5,11],exit:[9,11],expect:[8,10,11],expected_s:9,expos:[6,10,11],express:[7,14],ext3:[6,10,11],ext4:6,ext:[6,10,11],extens:[10,11],fail:0,fals:[0,6,7,8,9,10,11,12,14],file:[0,3,4,5,6,10,11,12,14],fileio:[2,3],filenam:[11,14],filesystem:[6,10,11],filled_char:9,find:[7,14],finish:12,finish_sect:12,first:[5,9,11,12],fix:[0,10],fixtur:13,flag:[0,3,10],flexibl:0,folder:[10,11],follow:11,forc:[0,10,11,12],form:[3,12,14],format:[0,12,14],format_container_nam:14,format_tim:9,found:[10,11,12,13,14],free:12,from:[0,5,6,7,8,10,11,14],fromhead:0,full:[0,6,10,11,14],full_path:[10,11],fun:[10,11],futur:[],ged:[10,11],gener:[0,3,7,9,10],generate_bind_list:11,generate_image_command:[6,11],generate_instance_command:8,generate_nam:7,get:[0,5,8,10,11,13,14],get_client:10,get_filenam:11,get_hash:5,get_installdir:14,get_log:9,get_logging_level:9,get_pars:4,get_singularity_vers:[7,11,14],get_singularity_version_info:[11,14],get_subpars:[],get_uri:[7,11],get_user_color_prefer:9,github:[0,10,11],give:[4,5,7,8,10,11],given:[0,6,11],global:11,group:[0,6,8,10,11],guidanc:0,hack:[],haikun:11,handl:[0,5,7],hang:[0,7,8],has:[0,9,11],hash:[0,5],have:[0,7,11,12,14],header:[0,8,12],help:[2,3,4,11],helpcmd:10,helper:6,here:0,hide:9,highli:[10,11],histori:9,hold:[6,8],honor:0,hook:13,host:[10,11],how:0,http:[3,10,11,12,14],hub:[10,11],idea:12,ideal:8,ignor:12,imag:[0,1,2,3,4,7,8,10,11,14],image_path:[6,10,11],imagebas:[5,7],imagecli:11,implement:[0,11],import_typ:6,importcmd:[3,5],imprt:6,includ:[0,9,10,11,12],incompat:0,increas:11,index:1,indic:8,info:9,inform:[9,10,11],init:10,init_command:11,init_level:11,initi:[10,11],input:6,input_sourc:6,inspect:[0,2,3,11],inspir:11,instal:[10,11,12,13,14],instanc:[0,1,2,3,11],instance_stopal:11,instead:[10,11],instruct:[],integ:8,integr:[10,11],intend:[5,12],interact:[11,12],interest:[7,14],interpret:9,ipython:4,is_quiet:9,isenabledfor:9,isol:[10,11],issu:[0,10,11],item:0,iter:[9,10,11],iutil:[3,7],join:[9,12],join_newlin:9,json:[0,8,10,11,12,14],json_obj:14,jsonifi:8,just:[10,11],kei:[9,12],kill:0,know:0,kwarg:7,label:[9,10,11,12],last:0,launch:11,length:[9,11],level:[0,4,5,6,8,9,11],libexecdir:[10,11],licens:[1,3,12,14],like:[0,7,8,12],line:[0,7,11,12,14],lint:[0,11],linux:[10,11],list:[0,7,8,9,10,11,12,14],list_inst:10,load:[0,5,7,10,11,12],load_recip:12,local:11,locat:[10,11,12],log:[0,9,11],logger:[0,1,2,3,10],logic:12,look:[8,10,11],main:[1,2,3,4,6,7,8,9],mainli:6,make:[10,11],manag:11,manual:0,map:[10,11],master:1,match:8,md5:5,mean:[7,10,11,14],merg:0,messag:[0,2,3,4,10,11],messagelevel:[6,8,9],metadata:11,method:13,methodnam:13,mib:6,migrat:0,min_level:9,miss:0,mkdir:14,mkdir_p:14,mode:[10,14],modul:[1,2],more:[0,10,11,12],most:10,mount:[10,11],move:0,mozilla:[3,12,14],mpl:[1,3,12,14],much:11,multipl:0,must:[6,8,9,11],name:[0,7,8,9,10,11,12,14],name_by_commit:[],name_by_hash:[],namespac:[5,7],need:[7,8,10,11,12,14],neither:12,newlin:[7,9,14],nicer:14,no_newline_regexp:[7,14],non:[0,11],none:[5,6,7,8,9,10,11,12,14],normal:11,note:12,now:0,number:[8,9],nvidia:[0,10,11],object:[5,7,9,10,11,12,14],obtain:[3,12,14],oci:[0,11],off:[],one:[0,3,7,10,11,12,14],onli:[0,5,9,10,11],open:14,opposit:[],option:[0,4,7,8,9,10,11,14],orb:0,org:[3,12,14],origin:[0,10,12],other:[7,14],otherwis:[0,11],out:9,outfil:4,output:[0,4,7,8,10,11,12,14],output_fil:11,over:11,overrid:[10,11],overriden:11,packag:[0,1,2],pair:12,param:[9,14],paramet:[0,5,6,7,8,10,11,12,14],parent:[7,11,14],pars:[0,3,5,7,8,10,11,14],parse_env:12,parse_image_nam:[5,7],parse_label:10,parse_t:8,parse_verbos:11,parser:[0,4,12],part:14,particular:13,pass:[6,8,11],path:[0,5,6,7,8,10,11,14],paus:0,perform:[10,11],persist:11,pid:[8,10,11],pipe:[7,11,14],point:0,poorli:11,popualt:12,port:12,prefer:[10,11],prefix:[9,12],prepar:4,prepare_cli:4,pretti:14,pretty_print:14,print:[0,4,7,9,10,11,12,14],print_pretti:14,println:[0,9,11],prioriti:5,privat:0,process:11,progess:[7,14],progress:[2,3,7,14],progressbar:9,prompt:9,properli:[6,14],protocol:14,provid:[6,7,8,10,11,13],pull:[0,2,3,11],pull_fold:[10,11],pwd:[10,11],pypi:0,pytest:0,python:[0,4,11],pythonbuff:12,quiet:[0,6,7,8,9,10,11,14],raw:[10,11],read:[10,11,14],read_fil:[13,14],read_json:14,readlin:14,reason:[5,10,11],recip:[0,2,3,10,11],recommend:[10,11],refactor:0,refer:12,registri:[5,7],regular:[7,14],rel:[0,10,11],releas:0,relev:0,remov:[0,8,12,14],remove_image_uri:[],remove_row:8,remove_uri:14,renam:0,replac:11,report:14,repositori:0,repres:0,request:0,requir:[10,11],respect:[0,12],result:[0,10,11,14],resum:0,return_cod:9,return_json:[8,10,11],return_result:[10,11],revers:0,ride:11,right:0,robot:[7,10,11],robot_nam:[10,11],robotnam:[7,11],root:[10,11],row:[8,9],run:[0,2,3,7,8,9,11,14],run_bpython:4,run_command:[7,11,14],runscript:[0,10,11,12],runtest:13,runtim:[10,11],same:14,sandbox:[10,11],save:[4,5,12],sci:[10,11],scientif:[10,11],scif:[10,11],screen:[10,11],second:[5,9],section:[0,12],see:[10,11],select:9,select_gener:9,self:[6,8,10,11,12],selftest:11,semant:14,send:[7,10,11,14],send_command:11,sens:[10,11],sequenc:12,serv:[4,10,11],set:[4,7,9,10,11,12,13,14],set_verbos:4,setenv:11,setup:[0,13],share:[10,11],shell:[0,2,3,11],shortcut:[7,11],should:[0,7,9,10,11,13,14],show:[9,10,11],show_progress:9,shub:14,sif:11,siflist:11,sign:11,signatur:11,silent:11,simag:11,simg:[10,11],simpli:[5,7],singl:[8,10,11,12],singular:[0,3,5,6,7,8,10,11,13,14],singularity2dock:12,singularitymessag:9,singularitypars:[4,12],singularityrecip:[0,12],singularitywar:[10,11],size:6,sizein:6,skip:[7,14],slash:14,sochat:[3,12,14],softwar:[10,11,13,14],some:[0,11],sourc:[0,3,4,5,6,7,8,9,10,11,12,13,14],space:[],special:14,special_charact:14,specif:[10,11],specifi:[6,9,10,11],sphinx:0,spin:9,spinner:[2,3],spinning_cursor:9,split:[0,5,7,14],split_uri:14,spython:[0,1],spython_singularity_vers:[],src:0,standard:[9,10,11],start:[0,3,7,9,14],state:0,statement:0,stderr:[9,11],stdin:6,stdout:[7,9,11,14],stop:[0,3,7,9,10,11],stopal:10,stream:[0,9,10,11,14],stream_command:14,string:[8,10,11],strip:0,structur:14,sub:[10,11],subclass:12,subcommand:6,subject:[3,12,14],submodul:[1,2,5,7],subpackag:[1,2],subpars:[],subprocess:[7,14],success:[0,11],sudo:[0,6,7,8,10,11,14],suffix:9,summari:11,suppli:[7,14],support:[0,6,7,8,9],suppress:[10,11],sutil:[3,10],symbol:9,system:[6,10,11],tabl:[8,9],table_str:8,take:[7,10,11,12,14],taken:0,tar:[6,11],teardown:13,tempfil:0,temporari:[6,11],term:[3,12,14],termin:[0,2,3,7,9,11],test:[0,2,3,10,11,12],test_check_get_singularity_vers:13,test_check_get_singularity_version_info:13,test_check_instal:13,test_command:13,test_get_installdir:13,test_instance_class:13,test_remove_uri:13,test_split_uri:13,test_write_read_fil:13,testcas:13,testclient:13,testinst:13,testscript:11,testutil:13,text:[9,10,11],them:0,thi:[0,3,4,5,7,8,10,11,12,14],those:14,tmp:[7,8],tmptar:6,tokenchar:11,tokenlength:11,top:8,total:9,track:0,trail:14,tupl:[7,8],turn:10,two:[],type:12,typo:0,ubuntu:[5,7,11],uid:[10,11],under:[1,6,9],unittest:13,unset:0,updat:[0,10],uri:[0,5,7,8,10,11,14],url:0,usag:[7,8,11,14],use:[0,6,7,10,11,12,14],usecolor:9,used:[6,9,10,11,12,13],useful:[5,11],user:[4,7,8,9,10,11,12,14],uses:[7,11,14],using:[10,11],usual:9,utf:11,util:[1,2,3,5],valu:[0,10,11,12],vanessa:[3,12,14],variabl:[0,6,8,9,11],vault:0,verbos:[6,8,9,11],verbose1:9,verbose2:9,verbose3:9,verifi:11,version:[0,1,2,4,5,7,11,12,13,14],version_info:[11,13],via:[6,8],volum:[11,12],want:[7,8],warn:[0,9],whatev:6,when:[0,7,8],where:[8,10,11,13],whether:[],which:[9,10,11],width:9,within:[10,11],withour:[10,11],without:[12,14],won:[7,14],work:[0,1],workdir:12,wrap:9,wrapper:[4,11],writabl:[10,11],write:[9,10,11,12,14],write_fil:[13,14],write_json:14,write_lin:12,writer:[0,4,12],x1b:9,yes:11,yet:11,yield:14,you:[3,10,11,12,14],your:[9,10,11]},titles:["CHANGELOG","Welcome to Singularity Python API\u2019s documentation!","spython","spython package","spython.client package","spython.image package","spython.image.cmd package","spython.instance package","spython.instance.cmd package","spython.logger package","spython.main package","spython.main.base package","spython.main.parse package","spython.tests package","spython.utils package"],titleterms:{"export":6,api:1,app:10,base:11,build:10,changelog:0,client:4,cmd:[6,8],command:11,content:[3,4,5,6,7,8,9,10,11,12,13,14],convert:12,creat:6,docker:12,document:1,environ:12,execut:10,fileio:14,flag:11,gener:11,help:10,imag:[5,6],importcmd:6,indic:1,inspect:10,instanc:[7,8,10],iutil:8,logger:[9,11],main:[10,11,12],master:0,messag:9,modul:[3,4,5,6,7,8,9,10,11,12,13,14],packag:[3,4,5,6,7,8,9,10,11,12,13,14],pars:12,progress:9,pull:10,python:1,recip:[4,12],run:10,shell:4,singular:[1,12],spinner:9,spython:[2,3,4,5,6,7,8,9,10,11,12,13,14],start:8,stop:8,submodul:[3,4,6,8,9,10,11,12,13,14],subpackag:[3,5,7,10],sutil:11,tabl:1,termin:14,test:[4,13],test_client:13,test_inst:13,test_util:13,util:[6,14],version:3,welcom:1}}) \ No newline at end of file +Search.setIndex({docnames:["changelog","index","source/modules","source/spython","source/spython.client","source/spython.image","source/spython.image.cmd","source/spython.instance","source/spython.instance.cmd","source/spython.logger","source/spython.main","source/spython.main.base","source/spython.main.parse","source/spython.tests","source/spython.utils"],envversion:{"sphinx.domains.c":1,"sphinx.domains.changeset":1,"sphinx.domains.cpp":1,"sphinx.domains.javascript":1,"sphinx.domains.math":2,"sphinx.domains.python":1,"sphinx.domains.rst":1,"sphinx.domains.std":1,"sphinx.ext.intersphinx":1,"sphinx.ext.todo":1,"sphinx.ext.viewcode":1,sphinx:56},filenames:["changelog.md","index.rst","source/modules.rst","source/spython.rst","source/spython.client.rst","source/spython.image.rst","source/spython.image.cmd.rst","source/spython.instance.rst","source/spython.instance.cmd.rst","source/spython.logger.rst","source/spython.main.rst","source/spython.main.base.rst","source/spython.main.parse.rst","source/spython.tests.rst","source/spython.utils.rst"],objects:{"":{spython:[3,0,0,"-"]},"spython.client":{get_parser:[4,1,1,""],main:[4,1,1,""],recipe:[4,0,0,"-"],set_verbosity:[4,1,1,""],shell:[4,0,0,"-"],test:[4,0,0,"-"],version:[4,1,1,""]},"spython.client.recipe":{main:[4,1,1,""]},"spython.client.shell":{ipython:[4,1,1,""],main:[4,1,1,""],prepare_client:[4,1,1,""],python:[4,1,1,""],run_bpython:[4,1,1,""]},"spython.client.test":{main:[4,1,1,""]},"spython.image":{Image:[5,2,1,""],ImageBase:[5,2,1,""],cmd:[6,0,0,"-"]},"spython.image.Image":{get_hash:[5,3,1,""]},"spython.image.ImageBase":{parse_image_name:[5,3,1,""]},"spython.image.cmd":{"export":[6,0,0,"-"],create:[6,0,0,"-"],generate_image_commands:[6,1,1,""],importcmd:[6,0,0,"-"],utils:[6,0,0,"-"]},"spython.image.cmd.create":{create:[6,1,1,""]},"spython.image.cmd.export":{"export":[6,1,1,""]},"spython.image.cmd.importcmd":{importcmd:[6,1,1,""]},"spython.image.cmd.utils":{compress:[6,1,1,""],decompress:[6,1,1,""]},"spython.instance":{Instance:[7,2,1,""],cmd:[8,0,0,"-"]},"spython.instance.Instance":{RobotNamer:[7,4,1,""],debug:[7,4,1,""],generate_name:[7,3,1,""],get_uri:[7,3,1,""],instance:[7,4,1,""],parse_image_name:[7,3,1,""],quiet:[7,4,1,""],run_command:[7,3,1,""],start:[7,3,1,""],stop:[7,3,1,""],version:[7,5,1,""]},"spython.instance.cmd":{generate_instance_commands:[8,1,1,""],iutils:[8,0,0,"-"],start:[8,0,0,"-"],stop:[8,0,0,"-"]},"spython.instance.cmd.iutils":{get:[8,1,1,""],parse_table:[8,1,1,""]},"spython.instance.cmd.start":{start:[8,1,1,""]},"spython.instance.cmd.stop":{stop:[8,1,1,""]},"spython.logger":{message:[9,0,0,"-"],progress:[9,0,0,"-"],spinner:[9,0,0,"-"]},"spython.logger.message":{SingularityMessage:[9,2,1,""],convert2boolean:[9,1,1,""],get_logging_level:[9,1,1,""],get_user_color_preference:[9,1,1,""]},"spython.logger.message.SingularityMessage":{abort:[9,3,1,""],addColor:[9,3,1,""],critical:[9,3,1,""],custom:[9,3,1,""],debug:[9,3,1,""],emit:[9,3,1,""],emitError:[9,3,1,""],emitOutput:[9,3,1,""],error:[9,3,1,""],exit:[9,3,1,""],get_logs:[9,3,1,""],info:[9,3,1,""],isEnabledFor:[9,3,1,""],is_quiet:[9,3,1,""],log:[9,3,1,""],newline:[9,3,1,""],println:[9,3,1,""],show_progress:[9,3,1,""],spinner:[9,4,1,""],table:[9,3,1,""],useColor:[9,3,1,""],verbose1:[9,3,1,""],verbose2:[9,3,1,""],verbose3:[9,3,1,""],verbose:[9,3,1,""],warning:[9,3,1,""],write:[9,3,1,""]},"spython.logger.progress":{ProgressBar:[9,2,1,""],bar:[9,1,1,""]},"spython.logger.progress.ProgressBar":{done:[9,3,1,""],format_time:[9,3,1,""],show:[9,3,1,""]},"spython.logger.spinner":{Spinner:[9,2,1,""]},"spython.logger.spinner.Spinner":{balloons_cursor:[9,6,1,""],changing_arrows:[9,6,1,""],delay:[9,4,1,""],run:[9,3,1,""],select_generator:[9,3,1,""],spinning:[9,4,1,""],spinning_cursor:[9,6,1,""],start:[9,3,1,""],stop:[9,3,1,""]},"spython.main":{apps:[10,0,0,"-"],base:[11,0,0,"-"],build:[10,0,0,"-"],execute:[10,0,0,"-"],get_client:[10,1,1,""],help:[10,0,0,"-"],inspect:[10,0,0,"-"],instances:[10,0,0,"-"],parse:[12,0,0,"-"],pull:[10,0,0,"-"],run:[10,0,0,"-"]},"spython.main.apps":{apps:[10,1,1,""]},"spython.main.base":{Client:[11,2,1,""],command:[11,0,0,"-"],flags:[11,0,0,"-"],generate:[11,0,0,"-"],logger:[11,0,0,"-"],sutils:[11,0,0,"-"]},"spython.main.base.Client":{"export":[11,3,1,""],RobotNamer:[11,4,1,""],apps:[11,3,1,""],build:[11,3,1,""],debug:[11,4,1,""],execute:[11,3,1,""],help:[11,3,1,""],image:[11,4,1,""],inspect:[11,3,1,""],instance:[11,4,1,""],instance_stopall:[11,3,1,""],instances:[11,3,1,""],load:[11,3,1,""],oci:[11,4,1,""],pull:[11,3,1,""],quiet:[11,4,1,""],run:[11,3,1,""],setenv:[11,3,1,""],version:[11,3,1,""],version_info:[11,3,1,""]},"spython.main.base.command":{generate_bind_list:[11,1,1,""],init_command:[11,1,1,""],run_command:[11,1,1,""],send_command:[11,1,1,""]},"spython.main.base.flags":{parse_verbosity:[11,1,1,""]},"spython.main.base.generate":{RobotNamer:[11,2,1,""],main:[11,1,1,""]},"spython.main.base.generate.RobotNamer":{generate:[11,3,1,""]},"spython.main.base.logger":{init_level:[11,1,1,""],println:[11,1,1,""]},"spython.main.base.sutils":{get_filename:[11,1,1,""],get_uri:[11,1,1,""],load:[11,1,1,""],setenv:[11,1,1,""]},"spython.main.build":{build:[10,1,1,""]},"spython.main.execute":{execute:[10,1,1,""]},"spython.main.help":{helpcmd:[10,1,1,""]},"spython.main.inspect":{inspect:[10,1,1,""],parse_labels:[10,1,1,""]},"spython.main.instances":{list_instances:[10,1,1,""],stopall:[10,1,1,""]},"spython.main.parse":{recipe:[12,0,0,"-"]},"spython.main.parse.recipe":{Recipe:[12,2,1,""]},"spython.main.parse.recipe.Recipe":{json:[12,3,1,""]},"spython.main.pull":{pull:[10,1,1,""]},"spython.main.run":{run:[10,1,1,""]},"spython.tests":{test_client:[13,0,0,"-"],test_instances:[13,0,0,"-"],test_utils:[13,0,0,"-"]},"spython.tests.test_client":{TestClient:[13,2,1,""]},"spython.tests.test_client.TestClient":{setUp:[13,3,1,""],tearDown:[13,3,1,""],test_commands:[13,3,1,""]},"spython.tests.test_instances":{TestInstances:[13,2,1,""]},"spython.tests.test_instances.TestInstances":{setUp:[13,3,1,""],tearDown:[13,3,1,""],test_instance_class:[13,3,1,""],test_instances:[13,3,1,""]},"spython.tests.test_utils":{TestUtils:[13,2,1,""]},"spython.tests.test_utils.TestUtils":{setUp:[13,3,1,""],tearDown:[13,3,1,""],test_check_get_singularity_version:[13,3,1,""],test_check_get_singularity_version_info:[13,3,1,""],test_check_install:[13,3,1,""],test_get_installdir:[13,3,1,""],test_remove_uri:[13,3,1,""],test_split_uri:[13,3,1,""],test_write_read_files:[13,3,1,""]},"spython.utils":{fileio:[14,0,0,"-"],terminal:[14,0,0,"-"]},"spython.utils.fileio":{mkdir_p:[14,1,1,""],read_file:[14,1,1,""],read_json:[14,1,1,""],write_file:[14,1,1,""],write_json:[14,1,1,""]},"spython.utils.terminal":{check_install:[14,1,1,""],format_container_name:[14,1,1,""],get_installdir:[14,1,1,""],get_singularity_version:[14,1,1,""],get_singularity_version_info:[14,1,1,""],remove_uri:[14,1,1,""],run_command:[14,1,1,""],split_uri:[14,1,1,""],stream_command:[14,1,1,""]},spython:{client:[4,0,0,"-"],image:[5,0,0,"-"],instance:[7,0,0,"-"],logger:[9,0,0,"-"],main:[10,0,0,"-"],tests:[13,0,0,"-"],utils:[14,0,0,"-"],version:[3,0,0,"-"]}},objnames:{"0":["py","module","Python module"],"1":["py","function","Python function"],"2":["py","class","Python class"],"3":["py","method","Python method"],"4":["py","attribute","Python attribute"],"5":["py","classmethod","Python class method"],"6":["py","staticmethod","Python static method"]},objtypes:{"0":"py:module","1":"py:function","2":"py:class","3":"py:method","4":"py:attribute","5":"py:classmethod","6":"py:staticmethod"},terms:{"1024mib":6,"95m":9,"boolean":[0,9,10,11],"byte":11,"case":[10,13],"char":11,"class":[5,7,9,11,12,13],"default":[0,6,7,8,9,10,11,14],"export":[0,3,5,11],"function":[0,4,6,9,10,11,13,14],"import":[0,6,10],"int":9,"new":[6,11,14],"public":[3,12,14],"return":[0,5,7,8,9,10,11,12,13,14],"static":9,"true":[6,7,8,9,10,11,14],Added:0,Adding:0,For:[10,11,14],The:[0,1,6,8,10,11,12,14],These:6,Useful:[],Will:9,With:[7,8],__init__:6,_pars:[],abil:0,abort:9,about:[10,11],accept:0,access:[10,11],account:0,action:[0,11],actual:[5,11],add:[7,14],addcolor:9,added:[0,6,9,10,11],adding:0,addit:[0,6,11],adjust:0,after:[11,13],alia:[7,11],all:[0,9,10,11,14],allow:[10,11],along:7,also:[11,12],ani:[10,11],anoth:[],api:0,app:[2,3,11],appear:11,append:14,applic:[11,14],appropri:[],arbitari:[10,11],arg:[0,4,7,8,9,10,11],argpars:11,argument:[0,7,8,10,11],around:4,ascii:9,associ:[10,11],assum:[8,9,13],attempt:[11,14],attribut:[0,12],automat:[10,11],avail:[11,14],back:[0,11,14],backward:0,balloons_cursor:9,bar:[7,9,14],base:[0,3,4,5,7,9,10,12,13],bash:[],basic:0,been:0,befor:13,behaviour:0,being:9,best:[],between:[],bin:[],bind:[0,7,8,10,11],bindlist:11,bootstrap:11,both:[4,11],bourn:11,bpython:4,branch:0,bug:0,build:[0,2,3,11,12],build_fold:[10,11],builder:[10,11],built:[10,11],bundl:11,call:[0,7,8,9,10,11],can:[0,3,4,7,11,12,14],capabl:11,captur:[7,8,10,11,14],carriage_return:9,chang:0,changelog:1,changing_arrow:9,charact:[9,14],check:[0,9,11,13,14],check_instal:14,choos:[],classmethod:7,cli:0,client:[0,1,2,3,6,7,8,10,11,14],close:[0,14],cmd:[3,5,7,11,12,14],code:[0,3,10,11,12,14],coincid:0,col_width:9,color:9,column:[8,9],com:[10,11],command:[0,3,6,7,8,10,12,13,14],comment:12,commit:0,commonli:6,complet:[5,7,9,10,11],compress:6,condit:0,config:[],configur:9,consol:[0,4,7,14],contain:[0,1,7,8,10,11,14],content:[1,2],context:[10,11],continu:0,conveni:[],convers:0,convert2boolean:9,convert:[0,3,4,10,11],convert_to:[],copi:[0,3,12,14],copyright:[3,12,14],correct:[0,13],couldn:[10,11],count:9,crash:0,creat:[3,5,7,8,9,10,11,14],create_entrypoint:[],create_env_sect:[],create_keyval_sect:[],create_runscript:[],create_sect:[],criteria:5,critic:[0,9],crypto:11,current:9,custom:[6,9,10,11],data:[10,11,14],debug:[6,7,8,9,10,11],decompress:6,deconstruct:13,defil:[10,11],defin:[0,4,6,10,11,14],delai:9,delim:11,delimit:11,deprec:[0,6,11],deriv:11,descriptor:11,desir:[],dest:0,destin:0,determin:[4,7,9,14],dict:14,dictionari:[9,11,12],differ:[0,10,11],dimens:8,directori:[0,10,11,13,14],disabl:[10,11],discoveri:[],displai:11,distribut:[3,12,14],doc:[],docker2singular:[],docker:[0,3,5,7,10,11,14],dockerfil:0,dockerpars:[4,12],dockerrecip:0,dockerwrit:4,document:0,doe:11,doesn:[7,10,11],don:[7,8,10,11,14],done:[7,8,9,10,11],driver:[10,11],each:[0,7,8,14],easili:[],either:[4,11,12],emb:4,emit:9,emiterror:9,emitoutput:9,empti:[0,11,14],empty_char:9,enabl:9,encod:9,end:14,engin:[],enhanc:0,entir:[10,11],entri:9,entrypoint:12,env:0,environ:[3,4,6,8,9,10,11],environment:9,envlist:[],equal:[],error:[0,9,10,11],estim:[],etc:8,eventu:0,everi:9,exampl:[11,14],except:14,exec:[0,11],execut:[0,2,3,11],exercis:13,exist:[5,11],exit:[9,11],expect:[8,10,11],expected_s:9,expos:[6,10,11],express:[7,14],ext3:[6,10,11],ext4:6,ext:[6,10,11],extens:[10,11],fail:0,fals:[0,6,7,8,9,10,11,14],file:[0,3,4,5,6,10,11,12,14],fileio:[2,3],filenam:[11,14],filesystem:[6,10,11],filled_char:9,find:[7,14],finish:[],finish_sect:[],first:[5,9,11],fix:[0,10],fixtur:13,flag:[0,3,10],flexibl:0,folder:[10,11],follow:11,forc:[0,10,11],form:[3,12,14],format:[0,14],format_container_nam:14,format_tim:9,found:[10,11,13,14],free:12,from:[0,5,6,7,8,10,11,14],fromhead:0,full:[0,6,10,11,14],full_path:[10,11],fun:[10,11],futur:[],ged:[10,11],gener:[0,3,7,9,10],generate_bind_list:11,generate_image_command:[6,11],generate_instance_command:8,generate_nam:7,get:[0,5,8,10,11,13,14],get_client:10,get_filenam:11,get_hash:5,get_installdir:14,get_log:9,get_logging_level:9,get_pars:4,get_singularity_vers:[7,11,14],get_singularity_version_info:[11,14],get_subpars:[],get_uri:[7,11],get_user_color_prefer:9,github:[0,10,11],give:[4,5,7,8,10,11],given:[0,6,11],global:11,group:[0,6,8,10,11],guidanc:0,hack:[],haikun:11,handl:[0,5,7],hang:[0,7,8],has:[0,9,11],hash:[0,5],have:[0,7,11,14],header:[0,8],help:[2,3,4,11],helpcmd:10,helper:6,here:0,hide:9,highli:[10,11],histori:9,hold:[6,8],honor:0,hook:13,host:[10,11],how:0,http:[3,10,11,12,14],hub:[10,11],idea:[],ideal:8,ignor:[],imag:[0,1,2,3,4,7,8,10,11,14],image_path:[6,10,11],imagebas:[5,7],imagecli:11,implement:[0,11],import_typ:6,importcmd:[3,5],imprt:6,includ:[0,9,10,11,12],incompat:0,increas:11,index:1,indic:8,info:9,inform:[9,10,11],init:10,init_command:11,init_level:11,initi:[10,11],input:6,input_sourc:6,inspect:[0,2,3,11],inspir:11,instal:[10,11,12,13,14],instanc:[0,1,2,3,11],instance_stopal:11,instead:[10,11],instruct:[],integ:8,integr:[10,11],intend:[5,12],interact:[11,12],interest:[7,14],interpret:9,ipython:4,is_quiet:9,isenabledfor:9,isol:[10,11],issu:[0,10,11],item:0,iter:[9,10,11],iutil:[3,7],join:9,join_newlin:9,json:[0,8,10,11,12,14],json_obj:14,jsonifi:8,just:[10,11],kei:9,kill:0,know:0,kwarg:7,label:[9,10,11,12],last:0,launch:11,length:[9,11],level:[0,4,5,6,8,9,11],libexecdir:[10,11],licens:[1,3,12,14],like:[0,7,8],line:[0,7,11,14],lint:[0,11],linux:[10,11],list:[0,7,8,9,10,11,14],list_inst:10,load:[0,5,7,10,11],load_recip:[],local:11,locat:[10,11,12],log:[0,9,11],logger:[0,1,2,3,10],logic:[],look:[8,10,11],main:[1,2,3,4,6,7,8,9],mainli:6,make:[10,11],manag:11,manual:0,map:[10,11],master:1,match:8,md5:5,mean:[7,10,11,14],merg:0,messag:[0,2,3,4,10,11],messagelevel:[6,8,9],metadata:11,method:13,methodnam:13,mib:6,migrat:0,min_level:9,miss:0,mkdir:14,mkdir_p:14,mode:[10,14],modul:[1,2],more:[0,10,11],most:10,mount:[10,11],move:0,mozilla:[3,12,14],mpl:[1,3,12,14],much:11,multipl:0,must:[6,8,9,11],name:[0,7,8,9,10,11,14],name_by_commit:[],name_by_hash:[],namespac:[5,7],need:[7,8,10,11,14],neither:[],newlin:[7,9,14],nicer:14,no_newline_regexp:[7,14],non:[0,11],none:[5,6,7,8,9,10,11,12,14],normal:11,note:[],now:0,number:[8,9],nvidia:[0,10,11],object:[5,7,9,10,11,12,14],obtain:[3,12,14],oci:[0,11],off:[],one:[0,3,7,10,11,12,14],onli:[0,5,9,10,11],open:14,opposit:[],option:[0,4,7,8,9,10,11,14],orb:0,org:[3,12,14],origin:[0,10,12],other:[7,14],otherwis:[0,11],out:9,outfil:4,output:[0,4,7,8,10,11,14],output_fil:11,over:11,overrid:[10,11],overriden:11,packag:[0,1,2],pair:[],param:[9,14],paramet:[0,5,6,7,8,10,11,12,14],parent:[7,11,14],pars:[0,3,5,7,8,10,11,14],parse_env:[],parse_image_nam:[5,7],parse_label:10,parse_t:8,parse_verbos:11,parser:[0,4,12],part:14,particular:13,pass:[6,8,11],path:[0,5,6,7,8,10,11,14],paus:0,perform:[10,11],persist:11,pid:[8,10,11],pipe:[7,11,14],point:0,poorli:11,popualt:12,port:12,prefer:[10,11],prefix:9,prepar:4,prepare_cli:4,pretti:14,pretty_print:14,print:[0,4,7,9,10,11,12,14],print_pretti:14,println:[0,9,11],prioriti:5,privat:0,process:11,progess:[7,14],progress:[2,3,7,14],progressbar:9,prompt:9,properli:[6,14],protocol:14,provid:[6,7,8,10,11,13],pull:[0,2,3,11],pull_fold:[10,11],pwd:[10,11],pypi:0,pytest:0,python:[0,4,11],pythonbuff:[],quiet:[0,6,7,8,9,10,11,14],raw:[10,11],read:[10,11,14],read_fil:[13,14],read_json:14,readlin:14,reason:[5,10,11],recip:[0,2,3,10,11],recommend:[10,11],refactor:0,refer:[],registri:[5,7],regular:[7,14],rel:[0,10,11],releas:0,relev:0,remov:[0,8,14],remove_image_uri:[],remove_row:8,remove_uri:14,renam:0,replac:11,report:14,repositori:0,repres:0,request:0,requir:[10,11],respect:[0,12],result:[0,10,11,14],resum:0,return_cod:9,return_json:[8,10,11],return_result:[10,11],revers:0,ride:11,right:0,robot:[7,10,11],robot_nam:[10,11],robotnam:[7,11],root:[10,11],row:[8,9],run:[0,2,3,7,8,9,11,14],run_bpython:4,run_command:[7,11,14],runscript:[0,10,11,12],runtest:13,runtim:[10,11],same:14,sandbox:[10,11],save:[4,5],sci:[10,11],scientif:[10,11],scif:[10,11],screen:[10,11],second:[5,9],section:0,see:[10,11],select:9,select_gener:9,self:[6,8,10,11],selftest:11,semant:14,send:[7,10,11,14],send_command:11,sens:[10,11],sequenc:12,serv:[4,10,11],set:[4,7,9,10,11,13,14],set_verbos:4,setenv:11,setup:[0,13],share:[10,11],shell:[0,2,3,11],shortcut:[7,11],should:[0,7,9,10,11,13,14],show:[9,10,11],show_progress:9,shub:14,sif:11,siflist:11,sign:11,signatur:11,silent:11,simag:11,simg:[10,11],simpli:[5,7],singl:[8,10,11],singular:[0,3,5,6,7,8,10,11,13,14],singularity2dock:[],singularitymessag:9,singularitypars:[4,12],singularityrecip:0,singularitywar:[10,11],size:6,sizein:6,skip:[7,14],slash:14,sochat:[3,12,14],softwar:[10,11,13,14],some:[0,11],sourc:[0,3,4,5,6,7,8,9,10,11,12,13,14],space:[],special:14,special_charact:14,specif:[10,11],specifi:[6,9,10,11],sphinx:0,spin:9,spinner:[2,3],spinning_cursor:9,split:[0,5,7,14],split_uri:14,spython:[0,1],spython_singularity_vers:[],src:0,standard:[9,10,11],start:[0,3,7,9,14],state:0,statement:0,stderr:[9,11],stdin:6,stdout:[7,9,11,14],stop:[0,3,7,9,10,11],stopal:10,stream:[0,9,10,11,14],stream_command:14,string:[8,10,11],strip:0,structur:14,sub:[10,11],subclass:12,subcommand:6,subject:[3,12,14],submodul:[1,2,5,7],subpackag:[1,2],subpars:[],subprocess:[7,14],success:[0,11],sudo:[0,6,7,8,10,11,14],suffix:9,summari:11,suppli:[7,14],support:[0,6,7,8,9],suppress:[10,11],sutil:[3,10],symbol:9,system:[6,10,11],tabl:[8,9],table_str:8,take:[7,10,11,14],taken:0,tar:[6,11],teardown:13,tempfil:0,temporari:[6,11],term:[3,12,14],termin:[0,2,3,7,9,11],test:[0,2,3,10,11,12],test_check_get_singularity_vers:13,test_check_get_singularity_version_info:13,test_check_instal:13,test_command:13,test_get_installdir:13,test_instance_class:13,test_remove_uri:13,test_split_uri:13,test_write_read_fil:13,testcas:13,testclient:13,testinst:13,testscript:11,testutil:13,text:[9,10,11],them:0,thi:[0,3,4,5,7,8,10,11,12,14],those:14,tmp:[7,8],tmptar:6,tokenchar:11,tokenlength:11,top:8,total:9,track:0,trail:14,tupl:[7,8],turn:10,two:[],type:[],typo:0,ubuntu:[5,7,11],uid:[10,11],under:[1,6,9],unittest:13,unset:0,updat:[0,10],uri:[0,5,7,8,10,11,14],url:0,usag:[7,8,11,14],use:[0,6,7,10,11,12,14],usecolor:9,used:[6,9,10,11,13],useful:[5,11],user:[4,7,8,9,10,11,12,14],uses:[7,11,14],using:[10,11],usual:9,utf:11,util:[1,2,3,5],valu:[0,10,11],vanessa:[3,12,14],variabl:[0,6,8,9,11],vault:0,verbos:[6,8,9,11],verbose1:9,verbose2:9,verbose3:9,verifi:11,version:[0,1,2,4,5,7,11,12,13,14],version_info:[11,13],via:[6,8],volum:[11,12],want:[7,8],warn:[0,9],whatev:6,when:[0,7,8],where:[8,10,11,13],whether:[],which:[9,10,11],width:9,within:[10,11],withour:[10,11],without:14,won:[7,14],work:[0,1],workdir:12,wrap:9,wrapper:[4,11],writabl:[10,11],write:[9,10,11,12,14],write_fil:[13,14],write_json:14,write_lin:[],writer:[0,4,12],x1b:9,yes:11,yet:11,yield:14,you:[3,10,11,12,14],your:[9,10,11]},titles:["CHANGELOG","Welcome to Singularity Python API\u2019s documentation!","spython","spython package","spython.client package","spython.image package","spython.image.cmd package","spython.instance package","spython.instance.cmd package","spython.logger package","spython.main package","spython.main.base package","spython.main.parse package","spython.tests package","spython.utils package"],titleterms:{"export":6,api:1,app:10,base:11,build:10,changelog:0,client:4,cmd:[6,8],command:11,content:[3,4,5,6,7,8,9,10,11,12,13,14],convert:12,creat:6,docker:12,document:1,environ:12,execut:10,fileio:14,flag:11,gener:11,help:10,imag:[5,6],importcmd:6,indic:1,inspect:10,instanc:[7,8,10],iutil:8,logger:[9,11],main:[10,11,12],master:0,messag:9,modul:[3,4,5,6,7,8,9,10,11,12,13,14],packag:[3,4,5,6,7,8,9,10,11,12,13,14],pars:12,progress:9,pull:10,python:1,recip:[4,12],run:10,shell:4,singular:[1,12],spinner:9,spython:[2,3,4,5,6,7,8,9,10,11,12,13,14],start:8,stop:8,submodul:[3,4,6,8,9,10,11,12,13,14],subpackag:[3,5,7,10],sutil:11,tabl:1,termin:14,test:[4,13],test_client:13,test_inst:13,test_util:13,util:[6,14],version:3,welcom:1}}) \ No newline at end of file diff --git a/docs/api/source/spython.html b/docs/api/source/spython.html index 59f43cb0..6945905f 100644 --- a/docs/api/source/spython.html +++ b/docs/api/source/spython.html @@ -230,11 +230,11 @@

      Subpackagesspython.main.parse package diff --git a/docs/api/source/spython.main.html b/docs/api/source/spython.main.html index 159a97d9..f0a4237f 100644 --- a/docs/api/source/spython.main.html +++ b/docs/api/source/spython.main.html @@ -196,11 +196,11 @@

      Subpackagesspython.main.parse package diff --git a/docs/api/source/spython.main.parse.html b/docs/api/source/spython.main.parse.html index a9989f79..73b29c46 100644 --- a/docs/api/source/spython.main.parse.html +++ b/docs/api/source/spython.main.parse.html @@ -185,143 +185,14 @@

      spython.main.parse package

      Submodules

      -
      -

      spython.main.parse.converters module

      -
      -
      -spython.main.parse.converters.create_env_section(pairs, name)[source]
      -
      -
      environment key value pairs need to be joined by an equal, and

      exported at the end.

      -
      -
      -
      -
      Parameters
      -
        -
      • section (the list of values to return as a parsed list of lines)

      • -
      • name (the name of the section to write (e.g., files))

      • -
      -
      -
      -
      - -
      -
      -spython.main.parse.converters.create_keyval_section(pairs, name)[source]
      -
      -
      create a section based on key, value recipe pairs,

      This is used for files or label

      -
      -
      -
      -
      Parameters
      -
        -
      • section (the list of values to return as a parsed list of lines)

      • -
      • name (the name of the section to write (e.g., files))

      • -
      -
      -
      -
      - -
      -
      -spython.main.parse.converters.create_runscript(self, default='/bin/bash', force=False)[source]
      -

      create_entrypoint is intended to create a singularity runscript -based on a Docker entrypoint or command. We first use the Docker -ENTRYPOINT, if defined. If not, we use the CMD. If neither is found, -we use function default.

      -
      -
      Parameters
      -
        -
      • default (set a default entrypoint, if the container does not have) – an entrypoint or cmd.

      • -
      • force (If true, use default and ignore Dockerfile settings)

      • -
      -
      -
      -
      - -
      -
      -spython.main.parse.converters.create_section(self, attribute, name=None)[source]
      -
      -
      create a section based on key, value recipe pairs,

      This is used for files or label

      -
      -
      -
      -
      Parameters
      -
        -
      • attribute (the name of the data section, either labels or files)

      • -
      • name (the name to write to the recipe file (e.g., %name).) – if not defined, the attribute name is used.

      • -
      -
      -
      -
      - -
      -
      -spython.main.parse.converters.docker2singularity(self, runscript='/bin/bash', force=False)[source]
      -

      docker2singularity will return a Singularity build recipe based on -a the loaded recipe object. It doesn’t take any arguments as the -recipe object contains the sections, and the calling function -determines saving / output logic.

      -
      - -
      -
      -spython.main.parse.converters.finish_section(section, name)[source]
      -

      finish_section will add the header to a section, to finish the recipe -take a custom command or list and return a section.

      -
      -
      Parameters
      -
        -
      • section (the section content, without a header)

      • -
      • name (the name of the section for the header)

      • -
      -
      -
      -
      - -
      -
      -spython.main.parse.converters.singularity2docker(self, runscript='/bin/bash', force=False)[source]
      -

      convert a Singularity recipe to a (best estimated) Dockerfile

      -
      - -
      -
      -spython.main.parse.converters.write_lines(label, lines)[source]
      -

      write a list of lines with a header for a section.

      -
      -
      Parameters
      -

      lines (one or more lines to write, with header appended)

      -
      -
      -
      - +
      +

      spython.main.parse.converters module

      -
      -

      spython.main.parse.docker module

      -
      -
      -class spython.main.parse.docker.DockerRecipe(recipe=None)[source]
      -

      Bases: spython.main.parse.recipe.Recipe

      -
      - +
      +

      spython.main.parse.docker module

      -
      -

      spython.main.parse.environment module

      -
      -
      -spython.main.parse.environment.parse_env(envlist)[source]
      -

      parse_env will parse a single line (with prefix like ENV removed) to -a list of commands in the format KEY=VALUE For example:

      -
      -

      ENV PYTHONBUFFER 1 –> [PYTHONBUFFER=1]

      -
      -
      -
      ::Notes

      Docker: https://docs.docker.com/engine/reference/builder/#env

      -
      -
      -
      - +
      +

      spython.main.parse.environment module

      spython.main.parse.recipe module

      @@ -355,30 +226,8 @@

      Submodules -

      spython.main.parse.singularity module

      -
      -
      -class spython.main.parse.singularity.SingularityRecipe(recipe=None)[source]
      -

      Bases: spython.main.parse.recipe.Recipe

      -
      -
      -load_recipe()[source]
      -

      load will return a loaded in singularity recipe. The idea -is that these sections can then be parsed into a Dockerfile, -or printed back into their original form.

      -
      -
      Returns
      -

      config

      -
      -
      Return type
      -

      a parsed recipe Singularity recipe

      -
      -
      -
      - -
      - +
      +

      spython.main.parse.singularity module

      Module contents

      From d8ae0df256f558a02be6d072bd843c078306b297 Mon Sep 17 00:00:00 2001 From: Vanessa Sochat Date: Sat, 1 Jun 2019 11:54:50 -0400 Subject: [PATCH 18/21] we cannot hard code local paths in conversions Signed-off-by: Vanessa Sochat --- CHANGELOG.md | 2 ++ spython/main/parse/parsers/docker.py | 5 +---- spython/tests/test_conversion.py | 2 +- spython/tests/testdata/docker2singularity/add.def | 2 +- spython/tests/testdata/docker2singularity/copy.def | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0a34518..3010184a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ singularity on pypi), and the versions here will coincide with these releases. ## [master](https://github.com/singularityhub/singularity-cli/tree/master) - refactor recipe parsers, writers, and base (0.0.64) + - adding testing for recipe convertion + - paths for files, add, copy, will not be expanded as it adds hardcoded paths - updated testing to use pytest, linting fixes, and oci state fixes (0.0.63) - fix crash in some error conditions (0.0.62) - more OCI commands accept sudo parameter diff --git a/spython/main/parse/parsers/docker.py b/spython/main/parse/parsers/docker.py index 782cc5b3..95f66f97 100644 --- a/spython/main/parse/parsers/docker.py +++ b/spython/main/parse/parsers/docker.py @@ -277,9 +277,6 @@ def _add_files(self, source, dest): dest: the destiation ''' - def expandPath(path): - return os.getcwd() if path == "." else path - # Warn the user Singularity doesn't support expansion if '*' in source: bot.warning("Singularity doesn't support expansion, * found in %s" % source) @@ -289,7 +286,7 @@ def expandPath(path): bot.warning("%s doesn't exist, ensure exists for build" % source) # The pair is added to the files as a list - self.recipe.files.append([expandPath(source), expandPath(dest)]) + self.recipe.files.append([source, dest]) def _parse_http(self, url, dest): diff --git a/spython/tests/test_conversion.py b/spython/tests/test_conversion.py index 3a3ad0c2..cf5a4e54 100644 --- a/spython/tests/test_conversion.py +++ b/spython/tests/test_conversion.py @@ -26,7 +26,7 @@ def setUp(self): def tearDown(self): shutil.rmtree(self.tmpdir) - + def test_pairs(self): print('Testing that each recipe file has a pair of the other type.') diff --git a/spython/tests/testdata/docker2singularity/add.def b/spython/tests/testdata/docker2singularity/add.def index 9196f8df..89a52c49 100644 --- a/spython/tests/testdata/docker2singularity/add.def +++ b/spython/tests/testdata/docker2singularity/add.def @@ -1,7 +1,7 @@ Bootstrap: docker From: busybox:latest %files -/home/vanessa/Documents/Dropbox/Code/sregistry/singularity-cli /opt +. /opt %runscript exec /bin/bash "$@" %startscript diff --git a/spython/tests/testdata/docker2singularity/copy.def b/spython/tests/testdata/docker2singularity/copy.def index 9196f8df..89a52c49 100644 --- a/spython/tests/testdata/docker2singularity/copy.def +++ b/spython/tests/testdata/docker2singularity/copy.def @@ -1,7 +1,7 @@ Bootstrap: docker From: busybox:latest %files -/home/vanessa/Documents/Dropbox/Code/sregistry/singularity-cli /opt +. /opt %runscript exec /bin/bash "$@" %startscript From e614ed4e766785e4d481ba705c57f162eb65b18d Mon Sep 17 00:00:00 2001 From: Vanessa Sochat Date: Sun, 2 Jun 2019 10:48:20 -0400 Subject: [PATCH 19/21] fixing useless-super-delegation Signed-off-by: Vanessa Sochat --- .pylintrc | 1 - CHANGELOG.md | 3 +-- spython/main/parse/parsers/docker.py | 9 +++++---- spython/main/parse/parsers/singularity.py | 6 +++--- spython/main/parse/writers/docker.py | 2 +- spython/main/parse/writers/singularity.py | 2 +- 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.pylintrc b/.pylintrc index ae8fbc2a..f1bd2f5d 100644 --- a/.pylintrc +++ b/.pylintrc @@ -71,7 +71,6 @@ disable=attribute-defined-outside-init, no-member, protected-access, R, - useless-super-delegation, unidiomatic-typecheck, trailing-whitespace, unused-argument, diff --git a/CHANGELOG.md b/CHANGELOG.md index 3010184a..d6c299e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,9 +18,8 @@ singularity on pypi), and the versions here will coincide with these releases. ## [master](https://github.com/singularityhub/singularity-cli/tree/master) - refactor recipe parsers, writers, and base (0.0.64) - - adding testing for recipe convertion - paths for files, add, copy, will not be expanded as it adds hardcoded paths - - updated testing to use pytest, linting fixes, and oci state fixes (0.0.63) + - oci state fixes and added Client.version_info() (0.0.63) - fix crash in some error conditions (0.0.62) - more OCI commands accept sudo parameter - working directory, the last one defined, should be added to runscript (0.0.61) diff --git a/spython/main/parse/parsers/docker.py b/spython/main/parse/parsers/docker.py index 95f66f97..3f4a3e99 100644 --- a/spython/main/parse/parsers/docker.py +++ b/spython/main/parse/parsers/docker.py @@ -17,17 +17,18 @@ class DockerParser(ParserBase): name = 'docker' - def __init__(self, recipe='Dockerfile', load=True): + def __init__(self, filename='Dockerfile', load=True): '''a docker parser will read in a Dockerfile and put it into a Recipe object. Parameters ========== - recipe: the Dockerfile to parse. If not defined, deafults to - Dockerfile assumed to be in the $PWD. + filename: the Dockerfile to parse. If not defined, deafults to + Dockerfile assumed to be in the $PWD. + load: whether to load the recipe file (default True) ''' - super(DockerParser, self).__init__(recipe, load) + super(DockerParser, self).__init__(filename, load) def parse(self): diff --git a/spython/main/parse/parsers/singularity.py b/spython/main/parse/parsers/singularity.py index 5edc759c..ad45c550 100644 --- a/spython/main/parse/parsers/singularity.py +++ b/spython/main/parse/parsers/singularity.py @@ -16,7 +16,7 @@ class SingularityParser(ParserBase): name = 'singularity' - def __init__(self, recipe="Singularity", load=True): + def __init__(self, filename="Singularity", load=True): '''a SingularityParser parses a Singularity file into expected fields of labels, environment, and install/runtime commands. The base class ParserBase will instantiate an empty Recipe() object to populate, @@ -24,11 +24,11 @@ def __init__(self, recipe="Singularity", load=True): Parameters ========== - recipe: the recipe file (Singularity) to parse + filename: the recipe file (Singularity) to parse load: load and parse the recipe (defaults to True) ''' - super(SingularityParser, self).__init__(recipe, load) + super(SingularityParser, self).__init__(filename, load) def parse(self): diff --git a/spython/main/parse/writers/docker.py b/spython/main/parse/writers/docker.py index fd18af6d..8297328b 100644 --- a/spython/main/parse/writers/docker.py +++ b/spython/main/parse/writers/docker.py @@ -45,7 +45,7 @@ class DockerWriter(WriterBase): name = 'docker' - def __init__(self, recipe=None): + def __init__(self, recipe=None): # pylint: disable=useless-super-delegation '''a DockerWriter will take a Recipe as input, and write to a Dockerfile. diff --git a/spython/main/parse/writers/singularity.py b/spython/main/parse/writers/singularity.py index 6c440beb..ad6974e7 100644 --- a/spython/main/parse/writers/singularity.py +++ b/spython/main/parse/writers/singularity.py @@ -16,7 +16,7 @@ class SingularityWriter(WriterBase): name = 'singularity' - def __init__(self, recipe=None): + def __init__(self, recipe=None): # pylint: disable=useless-super-delegation '''a SingularityWriter will take a Recipe as input, and write to a Singularity recipe file. From a1938e122ea22c5cd2919848577b9411baddc779 Mon Sep 17 00:00:00 2001 From: Vanessa Sochat Date: Mon, 3 Jun 2019 09:13:50 -0400 Subject: [PATCH 20/21] adding abstract class to ParserBase for parse, other fixes Signed-off-by: Vanessa Sochat --- .pylintrc | 1 - spython/main/parse/parsers/base.py | 21 ++++++++++++++++----- spython/utils/terminal.py | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.pylintrc b/.pylintrc index f1bd2f5d..56d037f1 100644 --- a/.pylintrc +++ b/.pylintrc @@ -71,7 +71,6 @@ disable=attribute-defined-outside-init, no-member, protected-access, R, - unidiomatic-typecheck, trailing-whitespace, unused-argument, wrong-import-order diff --git a/spython/main/parse/parsers/base.py b/spython/main/parse/parsers/base.py index 867871df..63bd2bae 100644 --- a/spython/main/parse/parsers/base.py +++ b/spython/main/parse/parsers/base.py @@ -5,6 +5,7 @@ # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +import abc import os from spython.logger import bot @@ -19,20 +20,21 @@ class ParserBase(object): object, which can be used to write to file, etc. ''' - lines = [] - - def __init__(self, filename=None, load=True): + def __init__(self, filename, load=True): '''a generic recipe parser holds the original file, and provides shared functions for interacting with files. If the subclass has a parse function defined, we parse the filename Parameters ========== - recipe: the recipe file to parse. + filename: the recipe file to parse. + load: if True, load the filename into the Recipe. If not loaded, + the user can call self.parse() at a later time. ''' self.filename = filename self._run_checks() + self.lines = [] self.recipe = Recipe(self.filename) if self.filename: @@ -41,10 +43,19 @@ def __init__(self, filename=None, load=True): self.lines = read_file(self.filename) # If parsing function defined, parse the recipe - if load is True and hasattr(self, 'parse'): + if load is True: self.parse() + @abc.abstractmethod + def parse(self): + '''parse is the base function for parsing an input filename, and + extracting elements into the correct Recipe sections. The exact + logic and supporting functions will vary based on the recipe type. + ''' + return + + def _run_checks(self): '''basic sanity checks for the file name (and others if needed) before attempting parsing. diff --git a/spython/utils/terminal.py b/spython/utils/terminal.py index cb79d043..2f9c54dd 100644 --- a/spython/utils/terminal.py +++ b/spython/utils/terminal.py @@ -147,7 +147,7 @@ def run_command(cmd, for line in process.communicate(): if line: - if type(line) is not str: + if type(line) is not str: # pylint: disable=unidiomatic-typecheck if isinstance(line, bytes): line = line.decode('utf-8') lines = lines + (line,) From d817f375f625448e407c953345e04abe92210269 Mon Sep 17 00:00:00 2001 From: Vanessa Sochat Date: Mon, 3 Jun 2019 09:17:27 -0400 Subject: [PATCH 21/21] throw NotImplementedError for docker bootstrap in singularity parser Signed-off-by: Vanessa Sochat --- spython/main/parse/parsers/singularity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spython/main/parse/parsers/singularity.py b/spython/main/parse/parsers/singularity.py index ad45c550..524b2adb 100644 --- a/spython/main/parse/parsers/singularity.py +++ b/spython/main/parse/parsers/singularity.py @@ -276,7 +276,7 @@ def _load_bootstrap(self, line): exit on fail (there is no other option to convert to Dockerfile! ''' if 'docker' not in line.lower(): - bot.exit('docker not detected as Bootstrap!') + raise NotImplementedError('docker not detected as Bootstrap!') def _load_section(self, lines, section):