diff --git a/.circleci/config.yml b/.circleci/config.yml index 4e010b90..72ae21e7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -68,6 +68,13 @@ install_python_2: &install_python_2 echo "Miniconda 2 is already installed, continuing to build." fi +run_linter: &run_linter + name: run linter + command: | + $HOME/conda/bin/pip install --upgrade pylint + cd ~/repo + $HOME/conda/bin/pylint spython + test_spython: &test_spython name: Test Singularity Python (Singularity Version 2 and 3) command: | @@ -99,6 +106,7 @@ jobs: - singularity/debian-install-3: singularity-version: 3.1.0 - run: *install_spython + - run: *run_linter - save_cache: paths: - /home/circleci/conda @@ -121,6 +129,7 @@ jobs: - singularity/debian-install-3: singularity-version: 3.1.0 - run: *install_spython + - run: *run_linter - save_cache: paths: - /home/circleci/conda diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 00000000..68d70de2 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,511 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=0 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --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, + missing-docstring, + multiple-statements, + no-member, + protected-access, + R, + redefined-builtin, + redefined-outer-name, + trailing-whitespace, + unused-argument, + wrong-import-order + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + baz, + toto, + tutu, + tata + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[STRING] + +# This flag controls whether the implicit-str-concat-in-sequence should +# generate a warning on implicit string concatenation in sequences defined over +# several lines. +check-str-concat-over-line-jumps=no + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package.. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[LOGGING] + +# Format style used to check logging format string. `old` means using % +# formatting, while `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement. +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a84d995..8e327554 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +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) + - fix crash in some error conditions (0.0.61) + - more OCI commands accept sudo parameter - 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) diff --git a/setup.py b/setup.py index 1557e3b0..636d2e8a 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ def get_requirements(lookup=None): '''get_requirements reads in requirements and versions from the lookup obtained with get_lookup''' - if lookup == None: + if lookup is None: lookup = get_lookup() install_requires = [] @@ -43,7 +43,7 @@ def get_requirements(lookup=None): if "exact_version" in module_meta: dependency = "%s==%s" %(module_name,module_meta['exact_version']) elif "min_version" in module_meta: - if module_meta['min_version'] == None: + if module_meta['min_version'] is None: dependency = module_name else: dependency = "%s>=%s" %(module_name,module_meta['min_version']) diff --git a/spython/client/__init__.py b/spython/client/__init__.py index 4da736bb..9ac8e3b7 100644 --- a/spython/client/__init__.py +++ b/spython/client/__init__.py @@ -63,23 +63,6 @@ def get_parser(): return parser - -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 - - def set_verbosity(args): '''determine the message level in the environment to set based on args. ''' @@ -113,7 +96,6 @@ def version(): 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/spython/client/shell.py b/spython/client/shell.py index ab60c090..b37a088f 100644 --- a/spython/client/shell.py +++ b/spython/client/shell.py @@ -6,12 +6,7 @@ # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -import sys - 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: diff --git a/spython/client/test.py b/spython/client/test.py index b4e794fb..360215dc 100644 --- a/spython/client/test.py +++ b/spython/client/test.py @@ -6,12 +6,6 @@ # 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 - - 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/spython/image/__init__.py b/spython/image/__init__.py index 0b5a606c..58661630 100644 --- a/spython/image/__init__.py +++ b/spython/image/__init__.py @@ -8,45 +8,21 @@ import hashlib import os import re -from spython.utils import get_singularity_version - +from spython.logger import bot +from spython.utils import split_uri 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__() - 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.+)://", image) - if match: - uri = match.group('uri') - - return uri - - - 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('/') - - def parse_image_name(self, image): ''' simply split the uri from the image. Singularity handles @@ -58,25 +34,24 @@ def parse_image_name(self, image): ''' self._image = image - self.uri = self.get_uri(image) - self.image = self.remove_uri(image) + self.protocol, self.image = split_uri(image) 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) def get_hash(self, image=None): @@ -99,4 +74,4 @@ def get_hash(self, image=None): hasher.update(chunk) return hasher.hexdigest() - bot.warning('%s does not exist.' %image) + bot.warning('%s does not exist.' % image) \ No newline at end of file diff --git a/spython/image/cmd/create.py b/spython/image/cmd/create.py index 7a9e1ed9..34f8eddf 100644 --- a/spython/image/cmd/create.py +++ b/spython/image/cmd/create.py @@ -6,6 +6,7 @@ # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +import os from spython.logger import bot def create(self,image_path, size=1024, sudo=False): diff --git a/spython/image/cmd/export.py b/spython/image/cmd/export.py index a247f9da..1c973fc9 100644 --- a/spython/image/cmd/export.py +++ b/spython/image/cmd/export.py @@ -6,7 +6,6 @@ # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -from spython.logger import bot import tempfile def export(self, image_path, tmptar=None): @@ -29,5 +28,5 @@ def export(self, image_path, tmptar=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/spython/image/cmd/importcmd.py b/spython/image/cmd/importcmd.py index eae8ddb3..449526e6 100644 --- a/spython/image/cmd/importcmd.py +++ b/spython/image/cmd/importcmd.py @@ -6,8 +6,6 @@ # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -from spython.logger import bot - def importcmd(self, image_path, input_source): '''import will import (stdin) to the image @@ -25,4 +23,3 @@ def importcmd(self, image_path, input_source): output = self.run_command(cmd, sudo=False) self.println(output) return image_path - diff --git a/spython/image/cmd/utils.py b/spython/image/cmd/utils.py index 3f800ab9..a7ae8f41 100644 --- a/spython/image/cmd/utils.py +++ b/spython/image/cmd/utils.py @@ -6,6 +6,7 @@ # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +import os from spython.logger import bot def compress(self, image_path): @@ -26,5 +27,5 @@ def decompress(self, image_path, quiet=True): extracted_file = image_path.replace('.gz','') cmd = ['gzip','-d','-f', image_path] - result = self.run_command(cmd, quiet=quiet) # exits if return code != 0 + 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 6cca5dd4..8f888fdd 100644 --- a/spython/instance/__init__.py +++ b/spython/instance/__init__.py @@ -11,29 +11,29 @@ 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 @@ -42,7 +42,7 @@ def generate_name(self, name=None): supply one. ''' # If no name provided, use robot name - if name == None: + if name is None: name = self.RobotNamer.generate() self.name = name.replace('-','_') @@ -58,7 +58,7 @@ def parse_image_name(self, image): ''' self._image = image - self.uri = 'instance://' + self.protocol = 'instance' def get_uri(self): @@ -74,15 +74,15 @@ def _update_metadata(self, kwargs=None): ''' # 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'] @@ -92,8 +92,8 @@ def _update_metadata(self, kwargs=None): 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/spython/instance/cmd/iutils.py b/spython/instance/cmd/iutils.py index 738acfb3..f6a3cdd7 100644 --- a/spython/instance/cmd/iutils.py +++ b/spython/instance/cmd/iutils.py @@ -5,6 +5,8 @@ # 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 def parse_table(table_string, header, remove_rows=1): '''parse a table to json from a string, where a header is expected by default. @@ -27,8 +29,8 @@ def parse_table(table_string, header, remove_rows=1): 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 @@ -49,7 +51,7 @@ def get(self, name, return_json=False, quiet=False): 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 diff --git a/spython/instance/cmd/start.py b/spython/instance/cmd/start.py index 044e2a92..08e895cd 100644 --- a/spython/instance/cmd/start.py +++ b/spython/instance/cmd/start.py @@ -8,7 +8,7 @@ from spython.logger import bot -def start(self, image=None, name=None, args=None, sudo=False, options=[], capture=False): +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 @@ -30,7 +30,7 @@ def start(self, image=None, name=None, args=None, sudo=False, options=[], captur 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 @@ -51,13 +51,13 @@ def start(self, image=None, name=None, args=None, sudo=False, options=[], captur # 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/spython/logger/message.py b/spython/logger/message.py index 453bd729..dc7b7f93 100644 --- a/spython/logger/message.py +++ b/spython/logger/message.py @@ -206,7 +206,7 @@ def show_progress( 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() diff --git a/spython/logger/progress.py b/spython/logger/progress.py index d21ce031..666cfc6c 100644 --- a/spython/logger/progress.py +++ b/spython/logger/progress.py @@ -54,7 +54,7 @@ def __init__(self, label='', width=32, hide=None, empty_char=BAR_EMPTY_CHAR, 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) def show(self, progress, count=None): @@ -104,7 +104,7 @@ def bar(it, label='', width=32, hide=None, empty_char=BAR_EMPTY_CHAR, 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/spython/logger/spinner.py b/spython/logger/spinner.py index df1fd1e9..22d40980 100644 --- a/spython/logger/spinner.py +++ b/spython/logger/spinner.py @@ -6,9 +6,6 @@ # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -import os -import sys - import sys import time import threading @@ -34,7 +31,7 @@ def changing_arrows(): for cursor in '<^>v': yield cursor def select_generator(self, generator): - if generator == None: + if generator is None: generator = choice(['cursor', 'arrow', 'balloons']) diff --git a/spython/main/apps.py b/spython/main/apps.py index 3f2e26fb..4003a26c 100644 --- a/spython/main/apps.py +++ b/spython/main/apps.py @@ -6,8 +6,6 @@ # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -from spython.logger import bot - def apps(self, image=None, full_path=False, root=''): ''' return list of SCIF apps in image. The Singularity software serves diff --git a/spython/main/base/__init__.py b/spython/main/base/__init__.py index 978d8d6c..b4644c7e 100644 --- a/spython/main/base/__init__.py +++ b/spython/main/base/__init__.py @@ -37,10 +37,10 @@ def __repr__(self): 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() def version(self): '''a wrapped to get_singularity_version, takes no arguments. diff --git a/spython/main/base/command.py b/spython/main/base/command.py index 103cf408..ca3031e6 100644 --- a/spython/main/base/command.py +++ b/spython/main/base/command.py @@ -6,19 +6,15 @@ # 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 @@ -129,7 +125,7 @@ def run_command(self, cmd, ''' # 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/spython/main/base/flags.py b/spython/main/base/flags.py index af420364..1a333781 100644 --- a/spython/main/base/flags.py +++ b/spython/main/base/flags.py @@ -6,31 +6,6 @@ # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. - -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 @@ -69,3 +44,26 @@ def parse_verbosity(self, args): instance Persistent instance command group ''' + +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/spython/main/build.py b/spython/main/build.py index 6e4eef12..f67ba047 100644 --- a/spython/main/build.py +++ b/spython/main/build.py @@ -93,7 +93,7 @@ def build(self, recipe=None, 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/spython/main/execute.py b/spython/main/execute.py index 4df805a6..5da8ae3a 100644 --- a/spython/main/execute.py +++ b/spython/main/execute.py @@ -8,8 +8,6 @@ from spython.logger import bot from spython.utils import stream_command -import os -import sys def execute(self, diff --git a/spython/main/export.py b/spython/main/export.py index 49a89442..ff61b6c0 100644 --- a/spython/main/export.py +++ b/spython/main/export.py @@ -7,7 +7,6 @@ from spython.logger import bot -from spython.utils import stream_command import tempfile import shutil import os @@ -38,7 +37,7 @@ def export(self, # 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 == None: + if output_file is None: basename, ext = os.path.splitext(image_path) output_file = self._get_filename(basename, 'sandbox', pwd=False) @@ -83,7 +82,7 @@ def _export(self, cmd = self._init_command('export') # If the user has specified export to pipe, we don't need a file - if pipe == True: + if pipe: cmd.append(image_path) else: @@ -98,11 +97,11 @@ def _export(self, return None # if user has specified output file, move it there, return path - if output_file != None: + 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) + return self._run_command(cmd, sudo=sudo) \ No newline at end of file diff --git a/spython/main/help.py b/spython/main/help.py index 20a676fd..ebf6a1bf 100644 --- a/spython/main/help.py +++ b/spython/main/help.py @@ -18,7 +18,7 @@ def help(self, command=None): check_install() cmd = ['singularity','--help'] - if command != None: + if command is not None: cmd.append(command) help = self._run_command(cmd) return help diff --git a/spython/main/inspect.py b/spython/main/inspect.py index 796df504..7b995150 100644 --- a/spython/main/inspect.py +++ b/spython/main/inspect.py @@ -7,7 +7,6 @@ import json as jsonp -from spython.logger import bot from spython.utils import ( check_install, run_command @@ -41,7 +40,8 @@ def inspect(self, image=None, json=True, app=None, quiet=True): if "version 3" in self.version(): 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') @@ -52,8 +52,12 @@ def inspect(self, image=None, json=True, app=None, quiet=True): if result['return_code'] == 0: result = jsonp.loads(result['message'][0]) + # Unify output to singularity 3 format + if "version 3" not in self.version(): + result = result['data'] + # Fix up labels - labels = parse_labels(result) + result = parse_labels(result) if not quiet: print(jsonp.dumps(result, indent=4)) @@ -70,22 +74,12 @@ def parse_labels(result): 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/spython/main/instances.py b/spython/main/instances.py index 987a3d54..a133f0e4 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, check_install ) +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 @@ -66,7 +66,7 @@ def instances(self, name=None, return_json=False, quiet=False): 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 @@ -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 != None and instances not in [None,[]]: + if name is not None and instances not in [None,[]]: if len(instances) == 1: instances = instances[0] @@ -106,7 +106,7 @@ def stopall(self, sudo=False, quiet=True): instances) ''' - from spython.utils import run_command, check_install + from spython.utils import check_install check_install() subgroup = 'instance.stop' diff --git a/spython/main/parse/converters.py b/spython/main/parse/converters.py index af77d276..6a2fd191 100644 --- a/spython/main/parse/converters.py +++ b/spython/main/parse/converters.py @@ -6,11 +6,7 @@ # 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 diff --git a/spython/main/parse/docker.py b/spython/main/parse/docker.py index c99be13a..262dbcb5 100644 --- a/spython/main/parse/docker.py +++ b/spython/main/parse/docker.py @@ -8,11 +8,9 @@ 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 class DockerRecipe(Recipe): diff --git a/spython/main/parse/recipe.py b/spython/main/parse/recipe.py index f0a3692e..ffe7c883 100644 --- a/spython/main/parse/recipe.py +++ b/spython/main/parse/recipe.py @@ -5,10 +5,8 @@ # 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 @@ -284,7 +282,7 @@ def _clean_line(self, line): return line.split('#')[0].strip() - def _write_script(path, lines, chmod=True): + 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. @@ -298,7 +296,7 @@ def _write_script(path, lines, chmod=True): if len(lines) > 0: lastline = lines.pop() for line in lines: - self.install.append('echo "%s" >> %s' %path) + self.install.append('echo "%s" >> %s' %line %path) self.install.append(lastline) if chmod is True: diff --git a/spython/main/parse/singularity.py b/spython/main/parse/singularity.py index c289c7ec..e001c3f3 100644 --- a/spython/main/parse/singularity.py +++ b/spython/main/parse/singularity.py @@ -6,13 +6,10 @@ # 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 @@ -88,7 +85,6 @@ def _test(self, lines): ''' self._write_script('/tests.sh', lines) - testrun = "/bin/bash /tests.sh" self.test = "/bin/bash /tests.sh" @@ -125,17 +121,17 @@ def _files(self, lines): # 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): @@ -330,7 +326,6 @@ def load_recipe(self): self.config = dict() section = None - name = None while len(lines) > 0: @@ -382,10 +377,8 @@ def _add_section(self, line, section=None): 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/spython/main/pull.py b/spython/main/pull.py index 6673a3de..ac2adf6a 100644 --- a/spython/main/pull.py +++ b/spython/main/pull.py @@ -10,9 +10,6 @@ from spython.utils import stream_command import os import re -import shutil -import sys -import tempfile def pull(self, image=None, diff --git a/spython/main/run.py b/spython/main/run.py index 3ecf0404..8599db5e 100644 --- a/spython/main/run.py +++ b/spython/main/run.py @@ -6,7 +6,6 @@ # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -from spython.logger import bot from spython.utils import stream_command import json diff --git a/spython/oci/__init__.py b/spython/oci/__init__.py index 46452eab..6ffbcfa9 100644 --- a/spython/oci/__init__.py +++ b/spython/oci/__init__.py @@ -30,15 +30,15 @@ def __init__(self, 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) @@ -56,7 +56,7 @@ def get_container_id(self, container_id=None): ''' # 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 @@ -72,7 +72,7 @@ def get_uri(self): # 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]" @@ -92,7 +92,7 @@ def _get_sudo(self, sudo=None): ========== sudo: if None, use self.sudo. Otherwise return sudo. ''' - if sudo == None: + if sudo is None: sudo = self.sudo return sudo diff --git a/spython/oci/cmd/actions.py b/spython/oci/cmd/actions.py index 09f2a0f4..6425d495 100644 --- a/spython/oci/cmd/actions.py +++ b/spython/oci/cmd/actions.py @@ -119,11 +119,11 @@ def _run(self, bundle, # Additional Logging Files cmd = cmd + ['--log-format', log_format] - if log_path != None: + if log_path is not None: cmd = cmd + ['--log-path', log_path] - if pid_file != None: + if pid_file is not None: cmd = cmd + ['--pid-file', pid_file] - if sync_socket != None: + if sync_socket is not None: cmd = cmd + ['--sync-socket', sync_socket] if empty_process: cmd.append('--empty-process') @@ -132,7 +132,7 @@ def _run(self, bundle, cmd.append(container_id) # Generate the instance - result = self._send_command(cmd, sudo=True) + self._send_command(cmd, sudo=True) # Get the status to report to the user! # TODO: Singularity seems to create even with error, can we check and @@ -225,7 +225,7 @@ def execute(self, command=None, container_id=None, sudo=False, stream=False): # Add the container_id cmd.append(container_id) - if command != None: + if command is not None: if not isinstance(command, list): command = [command] @@ -236,7 +236,7 @@ def execute(self, command=None, container_id=None, sudo=False, stream=False): return stream_command(cmd, sudo=sudo) return self._run_command(cmd, sudo=sudo, quiet=True) -def update(self, container_id, from_file=None): +def update(self, container_id, from_file=None, sudo=False): '''update container cgroup resources for a specific container_id, The container must have state "running" or "created." @@ -255,7 +255,7 @@ def update(self, container_id, from_file=None): # singularity oci delete cmd = self._init_command('update') - if from_file != None: + if from_file is not None: cmd = cmd + ['--from-file', from_file] # Add the container_id diff --git a/spython/oci/cmd/mounts.py b/spython/oci/cmd/mounts.py index 61fcd73c..659524e7 100644 --- a/spython/oci/cmd/mounts.py +++ b/spython/oci/cmd/mounts.py @@ -6,11 +6,6 @@ # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -from spython.logger import bot -from .states import _state_command -import sys - - def mount(self, image, sudo=None): '''create an OCI bundle from SIF image @@ -21,8 +16,8 @@ def mount(self, image, sudo=None): return self._state_command(image, command="mount", sudo=sudo) -def umount(self, image): - '''delete an OCI bundle createdfrom SIF image +def umount(self, image, sudo=None): + '''delete an OCI bundle created from SIF image Parameters ========== diff --git a/spython/oci/cmd/states.py b/spython/oci/cmd/states.py index dfcd36c6..52a9ed34 100644 --- a/spython/oci/cmd/states.py +++ b/spython/oci/cmd/states.py @@ -6,7 +6,6 @@ # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -from spython.logger import bot import json @@ -36,7 +35,7 @@ def state(self, container_id=None, sudo=None, sync_socket=None): # singularity oci state cmd = self._init_command('state') - if sync_socket != None: + if sync_socket is not None: cmd = cmd + ['--sync-socket', sync_socket] # Finally, add the container_id @@ -45,7 +44,7 @@ def state(self, container_id=None, sudo=None, sync_socket=None): # Get the instance state result = self._run_command(cmd, sudo=sudo, quiet=True) - if result != None: + if result is not None: # If successful, a string is returned to parse if isinstance(result, str): @@ -134,7 +133,7 @@ def kill(self, container_id=None, sudo=None, signal=None): cmd.append(container_id) # Add the signal, if defined - if signal != None: + if signal is not None: cmd = cmd + ['--signal', signal] # Run the command, return return code diff --git a/spython/tests/test_client.py b/spython/tests/test_client.py index 9c56c03e..63ceb4fa 100644 --- a/spython/tests/test_client.py +++ b/spython/tests/test_client.py @@ -7,12 +7,10 @@ # 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 @@ -82,7 +80,9 @@ def test_commands(self): 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/spython/tests/test_instances.py b/spython/tests/test_instances.py index 590d9acb..6c3d398a 100644 --- a/spython/tests/test_instances.py +++ b/spython/tests/test_instances.py @@ -6,13 +6,10 @@ # 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 @@ -27,6 +24,16 @@ def setUp(self): def tearDown(self): shutil.rmtree(self.tmpdir) + 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) + def test_instances(self): print('Pulling testing container') @@ -53,7 +60,7 @@ def test_instances(self): 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) @@ -67,6 +74,8 @@ def test_instances(self): 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/spython/tests/test_oci.py b/spython/tests/test_oci.py index 0cb8844e..355ab165 100644 --- a/spython/tests/test_oci.py +++ b/spython/tests/test_oci.py @@ -8,12 +8,10 @@ from spython.utils import get_installdir from spython.main.base.generate import RobotNamer -from spython.logger import bot from spython.main import Client import unittest import tempfile import shutil -import json import os print("############################################################## test_oci") @@ -42,6 +40,10 @@ def _build_sandbox(self): shutil.copyfile(self.config, '%s/config.json' %image) return image + def test_oci_image(self): + image=self.cli.oci.OciImage('oci://imagename') + self.assertEqual(image.get_uri(), '[singularity-python-oci:oci://imagename]') + def test_oci(self): image = self._build_sandbox() diff --git a/spython/tests/test_utils.py b/spython/tests/test_utils.py index 7294e033..4aba8502 100644 --- a/spython/tests/test_utils.py +++ b/spython/tests/test_utils.py @@ -6,11 +6,9 @@ # 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") @@ -46,7 +44,7 @@ def test_write_read_files(self): bad_json = {"Wakkawakkawakka'}":[{True},"2",3]} tmpfile = tempfile.mkstemp()[1] os.remove(tmpfile) - with self.assertRaises(TypeError) as cm: + with self.assertRaises(TypeError): write_json(bad_json,tmpfile) print("...Case 2: Providing good json") @@ -77,8 +75,14 @@ def test_check_get_singularity_version(self): 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") @@ -93,11 +97,26 @@ def test_get_installdir(self): self.assertTrue(whereami.endswith('spython')) + 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') + 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') diff --git a/spython/utils/__init__.py b/spython/utils/__init__.py index 702fdb3a..46419f35 100644 --- a/spython/utils/__init__.py +++ b/spython/utils/__init__.py @@ -13,5 +13,6 @@ stream_command, run_command, format_container_name, + split_uri, remove_uri ) diff --git a/spython/utils/fileio.py b/spython/utils/fileio.py index 3c4519a0..69a638e5 100644 --- a/spython/utils/fileio.py +++ b/spython/utils/fileio.py @@ -10,14 +10,8 @@ import errno import os -import re import json from spython.logger import bot -from subprocess import ( - Popen, - PIPE, - STDOUT -) import sys diff --git a/spython/utils/terminal.py b/spython/utils/terminal.py index 6c79498a..dada60ac 100644 --- a/spython/utils/terminal.py +++ b/spython/utils/terminal.py @@ -11,7 +11,6 @@ import os import re -import json from spython.logger import bot import subprocess import sys @@ -177,7 +176,22 @@ def format_container_name(name, special_characters=None): for e in name if e.isalnum() or e in special_characters) +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('/') + + 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]