Skip to content

Commit

Permalink
update docstring for all methods, add section in index.rst and README…
Browse files Browse the repository at this point in the history
….rst on What is Lmodule and why you would want to use it (#7)

Signed-off-by: Shahzeb Siddiqui <[email protected]>
  • Loading branch information
shahzebsiddiqui authored Mar 17, 2020
1 parent daa7ae1 commit 704d12d
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 105 deletions.
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
lmodule (Lmod Module)
---------------------

lmodule is a Python API for `Lmod <https://lmod.readthedocs.io/>`_ module system. The API can be used
lmodule is a Python 3 API for `Lmod <https://lmod.readthedocs.io/>`_ module system. The API can be used
end-users and system-administrators. End-users can utilize the API for interacting with module system to run
their application. System Administrators can utilize the API for testing their Software Stack and retrieve
output from the Lmod spider command.
Expand Down
24 changes: 21 additions & 3 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,28 @@ Welcome to lmodule documentation!
===================================

`lmodule <https://github.com/HPC-buildtest/lmodule>`_ is a Python API for `Lmod <https://lmod.readthedocs.io/en/latest>`_
module system. lmodule was part of `buildtest <https://github.com/HPC-buildtest/buildtest-framework>`_ and we found that
a Python API for the Lmod system would benefit other's who may not need to use buildtest.
module system. lmodule was originally part of `buildtest <https://github.com/HPC-buildtest/buildtest-framework>`_ and
we decided that it could benefit the entire community for folks interested in using the API but not relying on buildtest.

The name **lmodule** was decided by taking **Lmod** + **module** = **lmodule**.
What is lmodule?
-----------------

Lmodule is a Python 3 API that allows you to interact with the **module** command provided by Lmod in a programmatic way.
The API comes with three classes:

- Module: This class emulates the ``module`` command

- Spider: This class runs the Lmod ``spider`` command to retrieve all module records in json

- ModuleLoadTest: This class automates ``module load`` of one or more module trees

Why use lmodule?
----------------

Here are few reasons why you would want to use lmodule

1. Currently, there is no Python API for Lmod, however there is a python interface ``LMOD_CMD python`` by Lmod that requires parsing and output is cryptic.
2. Automates ``module load`` test for each module in one or more module trees (Software Stack). This can be used to spot faulty modules in a large software stack. This type of test is meant to run using CI tool for continuous testing of module stack.


.. toctree::
Expand Down
173 changes: 118 additions & 55 deletions lmod/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
import subprocess

def get_user_collections():
"""Get all Lmod user collections that is retrieved by running ``module -t savelist``.
"""Get all user collections that is retrieved by running ``module -t savelist``. The output
is of type ``list`` and each entry in list is the name of the user collection.
:return: Return all module collections
:rtype: list
:return: Return all user collections
:rtype: list
"""

collections = "module -t savelist"
Expand All @@ -22,21 +23,41 @@ def get_user_collections():


class Module:
"""Class declaration for Module class"""
"""This is the class declaration of Module class which emulates the ``module`` command
provided by Lmod
Public Methods:
---------------
avail: implements ``module avail`` option
version: gets Lmod version by reading LMOD_VERSION
is_avail: implements ``module is-avail`` option
get_command: gets the ``module load`` command based on modules passed to the class
test_modules: test the ``module load`` command based on modules passed to the class
save: implements ``module save`` option
describe: implements ``module describe`` option
get_collection: gets the ``module restore`` command with the specified collection name
test_collection: test the ``module restore`` command with the specified collection name
"""

def __init__(self, modules=None, purge=True, force=False, debug=False):
"""Initialize method for Module class.
:param modules: list of modules
:param purge: boolean to control whether to purge modules before loading
:param force: boolean to control whether to force purge modules before loading
:param debug: debug mode for troubleshooting
:type modules: list
:type purge: bool
:type force: bool
:type debug: bool
"""Initialize method for Module class. This class can accept module names that
can be used to load or test modules. You can tweak the behavior of how module
command is generated such as purging of force purge modules. The debug option
can be useful for troubleshooting.
Parameters:
-----------
:param modules: list of modules
:param purge: boolean to control whether to purge modules before loading
:param force: boolean to control whether to force purge modules before loading
:param debug: debug mode for troubleshooting
:type modules: list, optional
:type purge: bool, optional
:type force: bool, optional
:type debug: bool, optional
"""

self.debug = debug
self.modules = modules

Expand Down Expand Up @@ -71,13 +92,17 @@ def __init__(self, modules=None, purge=True, force=False, debug=False):
self.module_load_cmd = ["module purge &&"] + self.module_load_cmd

def avail(self,name=None):
"""This method implements the ``module avail`` command.
"""This method implements the ``module avail`` command. The output of module avail will return available
modules in the system. The output is returned as a list using the ``module -t avail`` which presents the
output in a single line per module.
:param name: argument passed to ``module av``. This is used for showing what modules are available
:type name: str
Parameters:
-----------
:param name: argument passed to ``module avail``. This is used for showing what modules are available
:type name: str, optional
:return: Return output of ``module avail`` as a list
:rtype: list
:return: Return output of ``module avail`` as a list
:rtype: list
"""

cmd = "module -t avail"
Expand All @@ -96,22 +121,27 @@ def avail(self,name=None):
return ret.stdout.split()

def version(self):
"""Get Lmod version by getting value from environment ``LMOD_VERSION``
"""Get Lmod version by reading environment variable ``LMOD_VERSION`` and return as a string
:return: Return the Lmod version
:rtype: str
:return: Return the Lmod version as a string
:rtype: str
"""

return os.getenv("LMOD_VERSION") or None

def is_avail(self, name):
"""This method implements the ``module is-avail`` command.
"""This method implements the ``module is-avail`` command which is used for checking if a module is available
before loading it. The return value is a 0 or 1.
:param name: argument passed to ``module is-avail``. This is used for checking if module is available
:type name: str
Parameters:
------------
:param name: argument passed to ``module is-avail``. This is used for checking if module is available
:type name: str, required
:return: Return output of ``module is-avail``. This checks if module is available and return code is a 0 or 1
:rtype: int
:return: Return output of ``module is-avail``. This checks if module is available and return code is a 0 or 1
:rtype: int
"""

cmd = f"module is-avail {name}"

ret = subprocess.run(
Expand All @@ -125,20 +155,28 @@ def is_avail(self, name):
return ret.returncode

def get_command(self):
""" Get the actual module load command that can be used to load the given modules.
"""Get the actual module load command that can be used to load the given modules.
:return: return the actual module load command
:rtype: str
:return: return the actual module load command
:rtype: str
"""

return " ".join(self.module_load_cmd)

def test_modules(self, login=False):
""" Test all specified modules by loading them using ``module load``.
"""Test all modules passed to Module class by loading them using ``module load``. The default behavior
is to run the command in a sub-shell but this can be changed to run in a new login shell if ``login=True`` is
specified. The return value is a return code (type ``int``) of the ``module load`` command.
Parameters:
-----------
:param login: When ``login=True`` is set, it will run the test in a login shell, the default is to run in a sub-shell
:type login: bool, optional
:return: return code of ``module load`` command
:rtype: int
:return: return code of ``module load`` command
:rtype: int
"""

cmd_executed = self.get_command()

# run test in login shell
Expand All @@ -161,10 +199,16 @@ def test_modules(self, login=False):
return ret.returncode

def save(self, collection="default"):
"""Save active modules into a module collection.
:param collection: collection name to save modules. If none specified, ``default`` is the collection.
:type collection: str
"""Save modules specified in Module class into a user collection. This implements the ``module save`` command
for active modules. In this case, we are saving modules that were passed to the Module class. If no argument
is specified, we will save to ``default`` collection, but user can specify a collection name, in that case
we are running ``module save <collection>``. The collection name must be of type ``str`` in order for
this to work, otherwise an exception of ``TypeError`` will be raised.
Paramters:
-----------
:param collection: collection name to save modules. If none specified, ``default`` is the collection.
:type collection: str, optional
"""

# raise TypeError exception if collection is not a string type since that is required
Expand All @@ -191,10 +235,15 @@ def save(self, collection="default"):
print(ret.stdout)

def describe(self, collection="default"):
"""Show content of a module collection.
:param collection: name of module collection
:type collection: str
"""Show content of a user collection and implements the command ``module describe``. By default, if no argument
is specified it will resort to showing contents of ``default`` collection. One can pass a collection name
which must be of type ``str`` that is the user collection name. Internally it will run ``module describe <collection>``.
If collection name is not of type ``str``, then an exception of ``TypeError`` will be raised.
Parameters:
-----------
:param collection: name of user collection to show.
:type collection: str, optional
"""

# raise TypeError exception if collection is not a string type since that is required
Expand All @@ -219,28 +268,42 @@ def describe(self, collection="default"):
print(ret.stdout)

def get_collection(self, collection="default"):
"""Return the command to restore a collection.
:param collection: collection name to restore
:type collection: str
:return: return the ``module restore`` command with the collection name
:rtype: str
"""Return the module command to restore a collection. If no argument is specified, it will resort to the ``default``
collection, otherwise one can specify a collection name of type ``str``. The output will be of type ``str``
such as ``module restore default``. If argument to class is not of type ``str`` then an exception of type
``TypeError`` will be raised.
Parameters:
-----------
:param collection: collection name to restore
:type collection: str, optional
:return: return the ``module restore`` command with the collection name
:rtype: str
"""

# raise error if collection is not a string
if not isinstance(collection, str):
raise TypeError(f"Type Error: {collection} is not of type string")

return f"module restore {collection}"

def test_collection(self, collection="default"):
"""Test the module collection by running ``module restore <collection>``.
:param collection: collection name to test
:type collection: str
:return: return code of ``module restore`` against the collection name
:rtype: int
"""Test the user collection by running ``module restore`` against a collection name. This is useful, to test a
user collection is working before using it in your scripts. If no argument is specified, it will test the
``default`` collection. One can specify a user collection name which must of be of type ``str`` and it must
exist. The output will be a return code of the ``module restore`` command which would be of type ``int``.
If argument to method is not of type ``str`` an exception of ``TypeError`` will be raised.
Parameters:
-----------
:param collection: collection name to test
:type collection: str, optional
:return: return code of ``module restore`` against the collection name
:rtype: int
"""

# raise error if collection is not a string
if not isinstance(collection, str):
raise TypeError(f"Type Error: {collection} is not of type string")
Expand Down
54 changes: 30 additions & 24 deletions lmod/moduleloadtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
from lmod.module import Module
from lmod.spider import Spider


class ModuleLoadTest:
"""This is the class declaration of ModuleLoadTest. This class will automate module load
test for all modules in one or more module tree (Software Stack).
test for all modules in one or more module tree (Software Stack) retrieved by Spider class. The output
of ``module load`` is a 0 or 1 that can be used to determine PASS/FAIL on a module. Each module test will attempt
to test the ``module load`` command using the ``Module().test_modules()``. In order to run tests properly,
MODULEPATH must be set in your environment.
"""

def __init__(
Expand All @@ -20,35 +22,37 @@ def __init__(
exclude=[],
login=False,
):
"""This is the initializer method that automates testing of modules
"""This is the initializer method for ModuleLoadTest class.
Parameters:
-----------
:param tree: specify one or more module trees to test. The module tree must be root directory where modulefiles
are found. Use a colon ``:`` to define more than one module tree.
:type tree: str
:param purge: control whether to run ``module purge`` before loading each module
:type purge: bool
:param tree: specify one or more module trees to test. The module tree must be root directory where modulefiles
are found. Use a colon ``:`` to define more than one module tree.
:type tree: str
:param force: control whether to run ``module --force purge`` before loading each module
:type purge: bool
:param purge: control whether to run ``module purge`` before loading each module
:type purge: bool
:param login: controls whether to run test in login shell when ``login=True``. By default tests are run in sub-shell.
:type purge: bool
:param force: control whether to run ``module --force purge`` before loading each module
:type purge: bool
:param login: control whether to run test in login shell. Defaults to sub-shell when ``login=False``
:type purge: bool
:param count: control how many tests to run before exiting
:type purge: int
:param count: control how many tests to run before exiting
:type purge: int
:param name: filter modules by software name to test
:type name: list
:param name: filter modules by software name to test
:type name: list
:param include: specify a list of modules to **include** by full canonical name for testing
:type purge: list
:param include: specify a list of modules to **include** by full canonical name for testing
:type purge: list
:param exclude: specify a list of modules to **exclude** by full canonical name for testing
:type purge: list
:param exclude: specify a list of modules to **exclude** by full canonical name for testing
:type purge: list
:return: Result of module load test
:rtype: None
:return: Result of module load test
:rtype: None
"""

# setting module tree to argument passed in or default to MODULEPATH
Expand Down Expand Up @@ -76,8 +80,10 @@ def __init__(
modules = filter_modules or modules

modulecount = 0

print(f"Testing the Following Module Trees: {self.tree}")
print("{:_<80}".format(""))

for module_name in modules:
module_cmd = Module(
module_name, purge=self.purge, force=self.force, debug=self.debug,
Expand Down
Loading

0 comments on commit 704d12d

Please sign in to comment.