Repository: RuleWorld/PyBioNetGen (language: python)

Issue:
Provide option to read the raw model definition rather than the resolved description
`bionetgen.bngmodel` nicely parses models. It would helpful to have an option to read the original model description (with mathematical expressions) rather than a resolved version of the model description (with mathematical expressions replaced with their results).

For example
```
begin parameters
 Vcell 1000 # Typical eukaryotic cell volume ~ 1000 um^3
 Vec   1000*Vcell # Volume of extracellular space around each cell (1/cell density)
end parameters

begin compartments
  EC 3 Vec
end compartments
```

is parsed to 
```
begin parameters
 Vcell 1000
 Vec   1000*Vcell
end parameters

begin compartments
  EC 3 1000000
end compartments
```

The original link between the size of `EC` and the parameter `Vec` has been resolved away.


Discussion / hints:
This will take a bit but I will add this in (I need to adjust how XML outputting works which is why it'll take a bit). The unresolved expressions will be added as a separate attribute and that attribute will be the default one to output during model string generation. 
Having both the resolved and unresolved values would be great.

Fix patch:
diff --git a/bionetgen/core/__init__.py b/bionetgen/core/__init__.py
index 9043cfa..c8f4102 100644
--- a/bionetgen/core/__init__.py
+++ b/bionetgen/core/__init__.py
@@ -3,3 +3,4 @@
 # leads to circular imports
 from .result import BNGResult
 from .plot import BNGPlotter
+from .info import BNGInfo
diff --git a/bionetgen/core/info.py b/bionetgen/core/info.py
new file mode 100644
index 0000000..679bbfb
--- /dev/null
+++ b/bionetgen/core/info.py
@@ -0,0 +1,108 @@
+class BNGInfo:
+    """
+    Used by the Cement app to execute the info subcommand, which involves gathering, preparing, and printing
+    relevant version and path information.
+
+    The gatherInfo() method finds and stores all necessary information.
+    The messageGeneration() method prepares this information in a text string.
+    The run() method simply outputs all the information.
+
+    Requires a configuration file. An additional set of arguments are optional.
+    """
+
+    def __init__(self, config, args=None):
+        self.config = config
+        self.args = args
+
+    def gatherInfo(self):
+        """
+        Gathers information about relevant versions and paths into a dictionary.
+        """
+        import subprocess
+        import os
+        import bionetgen
+        import numpy
+        import pandas
+        import roadrunner
+
+        self.info = {}
+
+        # Add some description for the following information
+        self.info["\nThe following are related to BioNetGen and its execution"] = ""
+
+        # Get BNG version
+        with open(
+            os.path.join(
+                *[os.path.dirname(bionetgen.__file__), "assets", "BNGVERSION"]
+            ),
+            "r",
+        ) as f:
+            read_data = f.read()
+        self.info["BNG version"] = read_data[10:15]
+
+        # Get BNG2.pl path
+        self.info["BNG2.pl path"] = (
+            self.config.get("bionetgen", "bngpath") + " (the main executable for BNG)"
+        )
+
+        # Get Perl version
+        # Read in CLI text
+        result = subprocess.run(["perl", "-v"], stdout=subprocess.PIPE)
+        text = str(result.stdout)
+        # Find start & end indices
+        num_start = text.find("(v") + 2
+        num_end = text.find(")")
+        # Save version info
+        self.info["Perl version"] = text[num_start:num_end] + " (used to run BNG2.pl)"
+
+        # Get CLI version
+        with open(
+            os.path.join(*[os.path.dirname(bionetgen.__file__), "assets", "VERSION"]),
+            "r",
+        ) as f:
+            read_data = f.read()
+        self.info["CLI version"] = read_data
+
+        # Get pyBNG path
+        self.info["pyBNG path"] = (
+            os.path.dirname(bionetgen.__file__) + " (the PyBNG installation)"
+        )
+
+        # Add some description for the following information
+        self.info["\nThe following libraries are required by PyBioNetGen"] = ""
+
+        # Get numpy version
+        self.info["numpy version"] = numpy.version.version
+
+        # Get pandas version
+        self.info["pandas version"] = pandas.__version__
+
+        # Get libRoadRunner version
+        text = roadrunner.getVersionStr()
+        self.info["libRoadRunner version"] = text[0:5]
+
+        return self.info
+
+    def messageGeneration(self):
+        """
+        Takes the dictionary created by gatherInfo() and
+        converts it to a string of text for printing.
+        """
+        self.message = " "
+
+        # Create lines of message using self.info dictionary
+        message_lines = []
+
+        for key in self.info:
+            text = str(key + ": " + self.info[key] + "\n")
+            message_lines.append(text)
+        # Join lines to create entire message
+        self.message = "".join((message_lines))
+
+        return self.message
+
+    def run(self):
+        """
+        Simply prints out the created information message.
+        """
+        print(self.message)
diff --git a/bionetgen/core/main.py b/bionetgen/core/main.py
index 6f8b28f..c576a11 100644
--- a/bionetgen/core/main.py
+++ b/bionetgen/core/main.py
@@ -1,6 +1,7 @@
 import bionetgen as bng
 from bionetgen.core import BNGResult
 from bionetgen.core import BNGPlotter
+from bionetgen.core import BNGInfo
 from bionetgen.modelapi.utils import run_command
 import bionetgen.modelapi.model as mdl
 
@@ -69,6 +70,22 @@ def plotDAT(inp, out=".", kw=dict()):
     plotter.plot()
 
 
+def printInfo(config, args):
+    """
+    ___
+    """
+    # this pulls out the arguments
+    # inp_file = args.input
+    # output = args.output
+    # if you set args.bngpath it should take precedence
+    # config_bngpath = config.get("bionetgen", "bngpath")
+    # and instantiates the CLI object
+    info = BNGInfo(config=config)
+    info.gatherInfo()
+    info.messageGeneration()
+    info.run()
+
+
 class BNGCLI:
     """
     Command Line Interface class to run BNG2.pl on a given
diff --git a/bionetgen/main.py b/bionetgen/main.py
index 7299e64..b8aad47 100644
--- a/bionetgen/main.py
+++ b/bionetgen/main.py
@@ -6,6 +6,7 @@
 from .core.exc import BioNetGenError
 from .core.main import runCLI
 from .core.main import plotDAT
+from .core.main import printInfo
 from .core.notebook import BNGNotebook
 
 # pull defaults defined in core/defaults
@@ -19,7 +20,7 @@ class BNGBase(cement.Controller):
 
     Used to set meta attributes like program name (label) as well
     as command line arguments. Each method is a subcommand in the
-    command line with it's own command line arguments.
+    command line with its own command line arguments.
 
     Subcommands
     -------
@@ -29,6 +30,8 @@ class BNGBase(cement.Controller):
         generates and opens a notebook for a model given by -i, optional
     plot
         plots a gdat/cdat/scan file given by -i into file supplied by -o
+    info
+        provides version and path information about the BNG installation and dependencies
     """
 
     class Meta:
@@ -273,6 +276,25 @@ def plot(self):
         ), "Input file has to be either a gdat or a cdat file"
         plotDAT(args.input, args.output, kw=dict(args._get_kwargs()))
 
+    @cement.ex(
+        help="Provides version information for BNG and dependencies",
+        arguments=[
+            (
+                ["-d", "--detail"],
+                {"help": "___", "default": False, "action": "store_true"},
+            ),
+        ],
+    )
+    def info(self):
+        """
+        Information subcommand to provide installation versions and paths.
+
+        Currently provides version information for BioNetGen, the BNG CLI, Perl,
+        numpy, pandas, and libroadrunner. Also provides BNG2.pl and pyBNG paths.
+        """
+        args = self.app.pargs
+        printInfo(self.app.config, args)
+
 
 class BioNetGen(cement.App):
     """
diff --git a/docs/source/cli.rst b/docs/source/cli.rst
index 61b70ca..af34206 100644
--- a/docs/source/cli.rst
+++ b/docs/source/cli.rst
@@ -67,6 +67,15 @@ and the resulting notebook will be ready to load in your model using PyBioNetGen
    
    bionetgen notebook -i mymodel.bngl -o mynotebook.ipynb
 
+Info
+====
+
+This subcommand simply prints out information about software versions and installation paths.
+
+.. code-block: shell
+
+   bionetgen info
+
 Tutorial
 ========
 


Test patch:
diff --git a/tests/test_bionetgen.py b/tests/test_bionetgen.py
index a651492..fca17f5 100644
--- a/tests/test_bionetgen.py
+++ b/tests/test_bionetgen.py
@@ -61,3 +61,11 @@ def test_action_loading():
     all_action_model = os.path.join(*[tfold, "models", "all_actions.bngl"])
     m = bng.bngmodel(all_action_model)
     assert len(m.actions) == 27
+
+
+def test_bionetgen_info():
+    # test info subcommand
+    argv = ["info"]
+    with BioNetGenTest(argv=argv) as app:
+        app.run()
+        assert app.exit_code == 0