Repository: Pyomo/pyomo

Issue:
GAMSShell broke help -s
`pyomo help -s` lists the serial solver interfaces, then throws:
```
Traceback (most recent call last):
  File "/usr/bin/pyomo", line 11, in <module>
    sys.exit(main())
  File "/usr/lib64/python3.4/site-packages/pyomo/scripting/pyomo_main.py", line 82, in main
    retval = _options.func(_options)
  File "/usr/lib64/python3.4/site-packages/pyomo/scripting/driver_help.py", line 457, in help_exec
    help_solvers()
  File "/usr/lib64/python3.4/site-packages/pyomo/scripting/driver_help.py", line 347, in help_solvers
    if s == 'py' or opt._metasolver:
AttributeError: 'GAMSShell' object has no attribute '_metasolver'
```


Discussion:
@gseastream Take a look?
This error is due to the fact that I never integrated the GAMS solver into the executable `pyomo` library of commands, so the interface is probably missing some attributes. However, I only get this error when I run the `pyomo help -s` command on a pip-installed version of pyomo. Git/development/trunk versions give this error:

```
Pyomo Solvers and Solver Managers
---------------------------------
Pyomo uses 'solver managers' to execute 'solvers' that perform
optimization and other forms of model analysis.  A solver directly
executes an optimizer, typically using an executable found on the
user's PATH environment.  Solver managers support a flexible mechanism
for asyncronously executing solvers either locally or remotely.  The
following solver managers are available in Pyomo:

    neos       Asynchronously execute solvers on the NEOS server
    phpyro     Specialized PH solver manager that uses pyro
    pyro       Execute solvers remotely using pyro
    serial     Synchronously execute solvers locally

If no solver manager is specified, Pyomo uses the serial solver
manager to execute solvers locally.  The pyro and phpyro solver
managers require the installation and configuration of the pyro
software.  The neos solver manager is used to execute solvers on the
NEOS optimization server.


Serial Solver Interfaces
------------------------
The serial, pyro and phpyro solver managers support the following
solver interfaces:

Traceback (most recent call last):
  File "/home/gseastre/pyomo/bin/pyomo", line 9, in <module>
    load_entry_point('Pyomo', 'console_scripts', 'pyomo')()
  File "/home/gseastre/pyomo/src/pyomo/pyomo/scripting/pyomo_main.py", line 82, in main
    retval = _options.func(_options)
  File "/home/gseastre/pyomo/src/pyomo/pyomo/scripting/driver_help.py", line 452, in help_exec
    help_solvers()
  File "/home/gseastre/pyomo/src/pyomo/pyomo/scripting/driver_help.py", line 351, in help_solvers
    logger.disable(logging.NOTSET)
AttributeError: 'Logger' object has no attribute 'disable'
```

Indicating some other bug has arisen since the last PyPI release. This is as opposed to the following error on a PyPI installation:

```
Serial Solver Interfaces
------------------------
The serial, pyro and phpyro solver managers support the following
solver interfaces:

    asl                  + Interface for solvers using the AMPL Solver
                           Library
    baron                  The BARON MINLP solver
    bilevel_blp_global   + Global solver for continuous bilevel linear
                           problems
    bilevel_blp_local    + Local solver for continuous bilevel linear
                           problems
    bilevel_ld           + Solver for bilevel problems using linear
                           duality
    cbc                    The CBC LP/MIP solver
    conopt                 The CONOPT NLP solver
    cplex                  The CPLEX LP/MIP solver
Traceback (most recent call last):
  File "c:\python36\lib\runpy.py", line 193, in _run_module_as_main
    "__main__", mod_spec)
  File "c:\python36\lib\runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "C:\Python36\Scripts\pyomo.exe\__main__.py", line 9, in <module>
  File "c:\python36\lib\site-packages\pyomo\scripting\pyomo_main.py", line 82, in main
    retval = _options.func(_options)
  File "c:\python36\lib\site-packages\pyomo\scripting\driver_help.py", line 457, in help_exec
    help_solvers()
  File "c:\python36\lib\site-packages\pyomo\scripting\driver_help.py", line 347, in help_solvers
    if s == 'py' or opt._metasolver:
AttributeError: 'GAMSShell' object has no attribute '_metasolver'
```

Resolving patch:
diff --git a/pyomo/contrib/gdpopt/GDPopt.py b/pyomo/contrib/gdpopt/GDPopt.py
--- a/pyomo/contrib/gdpopt/GDPopt.py
+++ b/pyomo/contrib/gdpopt/GDPopt.py
@@ -57,6 +57,8 @@ class GDPoptSolver(pyomo.common.plugin.Plugin):
     pyomo.common.plugin.alias(
         'gdpopt', doc='The GDPopt decomposition-based GDP solver')
 
+    _metasolver = False
+
     CONFIG = ConfigBlock("GDPopt")
     CONFIG.declare("bound_tolerance", ConfigValue(
         default=1E-6, domain=NonNegativeFloat,
diff --git a/pyomo/scripting/driver_help.py b/pyomo/scripting/driver_help.py
--- a/pyomo/scripting/driver_help.py
+++ b/pyomo/scripting/driver_help.py
@@ -335,11 +335,15 @@ def help_solvers():
     wrapper = textwrap.TextWrapper(subsequent_indent=' '*(n+9))
     try:
         # Disable warnings
-        logger.disable(logging.WARNING)
+        logging.disable(logging.WARNING)
         for s in solver_list:
             # Create a solver, and see if it is available
             with pyomo.opt.SolverFactory(s) as opt:
-                if s == 'py' or opt._metasolver:
+                if s == 'py' or (hasattr(opt, "_metasolver") and opt._metasolver):
+                    # py is a metasolver, but since we don't specify a subsolver
+                    # for this test, opt is actually an UnknownSolver, so we
+                    # can't try to get the _metasolver attribute from it.
+                    # Also, default to False if the attribute isn't implemented
                     msg = '    %-'+str(n)+'s   + %s'
                 elif opt.available(False):
                     msg = '    %-'+str(n)+'s   * %s'
@@ -348,7 +352,7 @@ def help_solvers():
                 print(wrapper.fill(msg % (s, pyomo.opt.SolverFactory.doc(s))))
     finally:
         # Reset logging level
-        logger.disable(logging.NOTSET)
+        logging.disable(logging.NOTSET)
     print("")
     wrapper = textwrap.TextWrapper(subsequent_indent='')
     print(wrapper.fill("An asterisk indicates solvers that are currently available to be run from Pyomo with the serial solver manager. A plus indicates meta-solvers, that are always available."))
diff --git a/pyomo/solvers/plugins/solvers/GAMS.py b/pyomo/solvers/plugins/solvers/GAMS.py
--- a/pyomo/solvers/plugins/solvers/GAMS.py
+++ b/pyomo/solvers/plugins/solvers/GAMS.py
@@ -74,11 +74,13 @@ class GAMSDirect(pyomo.common.plugin.Plugin):
     Visit Python API page on gams.com for installation help.
     """
     pyomo.common.plugin.implements(IOptSolver)
-    pyomo.common.plugin.alias('_gams_direct', doc='The GAMS modeling language')
+    pyomo.common.plugin.alias('_gams_direct',
+        doc='Direct python interface to the GAMS modeling language')
 
     def __init__(self, **kwds):
         self._version = None
         self._default_variable_value = None
+        self._metasolver = False
 
         self._capabilities = Options()
         self._capabilities.linear = True
@@ -524,11 +526,13 @@ def solve(self, *args, **kwds):
 class GAMSShell(pyomo.common.plugin.Plugin):
     """A generic shell interface to GAMS solvers."""
     pyomo.common.plugin.implements(IOptSolver)
-    pyomo.common.plugin.alias('_gams_shell', doc='The GAMS modeling language')
+    pyomo.common.plugin.alias('_gams_shell',
+        doc='Shell interface to the GAMS modeling language')
 
     def __init__(self, **kwds):
         self._version = None
         self._default_variable_value = None
+        self._metasolver = False
 
         self._capabilities = Options()
         self._capabilities.linear = True


Test patch:
diff --git a/pyomo/scripting/tests/test_cmds.py b/pyomo/scripting/tests/test_cmds.py
new file mode 100644
--- /dev/null
+++ b/pyomo/scripting/tests/test_cmds.py
@@ -0,0 +1,35 @@
+#  ___________________________________________________________________________
+#
+#  Pyomo: Python Optimization Modeling Objects
+#  Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC
+#  Under the terms of Contract DE-NA0003525 with National Technology and
+#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
+#  rights in this software.
+#  This software is distributed under the 3-clause BSD License.
+#  ___________________________________________________________________________
+#
+import re
+import pyutilib.th as unittest
+from pyutilib.misc.redirect_io import capture_output
+
+from pyomo.environ import SolverFactory
+from pyomo.scripting.driver_help import help_solvers
+
+class TestScriptingCommands(unittest.TestCase):
+    def test_help_solvers(self):
+        with capture_output() as OUT:
+            help_solvers()
+        OUT = OUT.getvalue()
+        self.assertTrue(re.search('Pyomo Solvers and Solver Managers', OUT))
+        self.assertTrue(re.search('Serial Solver', OUT))
+        # Test known solvers and metasolver flags
+        # ASL is a metasolver
+        self.assertTrue(re.search('asl +\+', OUT))
+        # PS is bundles with Pyomo so should always be available
+        self.assertTrue(re.search('ps +\*', OUT))
+        for solver in ('ipopt','baron','cbc','glpk'):
+            s = SolverFactory(solver)
+            if s.available():
+                self.assertTrue(re.search("%s +\* [a-zA-Z]" % solver, OUT))
+            else:
+                self.assertTrue(re.search("%s +[a-zA-Z]" % solver, OUT))