Repository: coin-or/pulp

Issue:
indexs vs indexes
Hi, this issue is around a seemingly misspelled parameter ([indexs](https://github.com/coin-or/pulp/blob/449e4b2950367f1d21d38a6afb07baf0dbc18f17/pulp/pulp.py#L348)), so it's not a bug in the sense that is blocks functionality.  However, it may complicate development for [pulp](https://github.com/coin-or/pulp) developers/users as they have to remember the correct incorrect spelling of the word is they want to provide that parameter explicitly within their code (a common best practice). 

If I could correct this parameter name in a non-breaking way with a deprecation warning, would you be open to that PR?  If so, I'd love to contribute. 

Details for the issue
--------------------

#### What did you do?
Assigned variables via `LpVariable.dicts()` method.  Had to look at [the docs](https://www.coin-or.org/PuLP/pulp.html) to confirm the correct variable name. 

#### What did you expect to see?
`LpVariable.dicts(indexs=)`

#### What did you see instead?
`LpVariable.dicts(indexes=)` 

Useful extra information
-------------------------

The info below often helps, please fill it out if you're able to. :)

#### What operating system are you using?

- [ ] Windows: ( _version:_ ___ )
- [ ] Linux: ( _distro:_ ___ )
- [x ] Mac OS: (  Catalina version: 10.15.7)
- [ ] Other: ___

#### I'm using python version:

- [ ] 2.7
- [ ] 3.4
- [ ] 3.5
- [x ] 3.6
- [ ] Other: ___

#### I installed PuLP via:

- [x ] pypi (python -m pip install pulp)
- [ ] github (python -m pip install -U git+https://github.com/coin-or/pulp)
- [ ] Other: ___ (conda?)

#### Did you also

- [ ] Tried out the latest github version: https://github.com/coin-or/pulp
- [x ] Searched for an existing similar issue: https://github.com/coin-or/pulp/issues?utf8=%E2%9C%93&q=is%3Aissue%20


Discussion:
CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/coin-or/pulp?pullRequest=491) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/coin-or/pulp?pullRequest=491) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/coin-or/pulp?pullRequest=491) it.</sub>
bburns632: Hello all, I signed the CLA form twice, so hoping that comes through soon.  

With regard to the test errors, it appears `assertWarn` is not supported in unittest in python 2.x.  ([similar issue](https://github.com/berkerpeksag/astor/issues/90)).

I can refactor but if you're planning to drop python 2.x support any time soon, this tiny PR can also sit until then.  Please advise. 


bburns632: On further thought, it seemed silly to consider not supporting 2.7 with this PR for one unit test.  I commented it out.  
pchtsp: Thanks. With respect to python 2.7: we will support it as long as it does not impose too many constraints.
Regarding the CLA, for some reason it still appears as not signed. Are you sure you signed it correctly? could you sign it yet again?

Patch:
diff --git a/HISTORY b/HISTORY
index 7ce0f0e..07fb28e 100644
--- a/HISTORY
+++ b/HISTORY
@@ -7,6 +7,7 @@
     fixed bugs
     added HiGHS solver
     added pysmps dependency for mps parsing
+    deprecated 'indexs' parameter LpVariable dicts and matrix
 2.5.1 2021-09-28
     updated docs
     fixed minor issues
diff --git a/pulp/pulp.py b/pulp/pulp.py
index 6cf0dd7..025251d 100644
--- a/pulp/pulp.py
+++ b/pulp/pulp.py
@@ -315,20 +315,39 @@ class LpVariable(LpElement):
     def matrix(
         cls,
         name,
-        indexs,
+        indices=None,  # required param. enforced within function for backwards compatibility
         lowBound=None,
         upBound=None,
         cat=const.LpContinuous,
         indexStart=[],
+        indexs=None,
     ):
-        if not isinstance(indexs, tuple):
-            indexs = (indexs,)
+
+        # Backwards Compatiblity with Deprecation Warning for indexs
+        if indices is not None and indexs is not None:
+            raise TypeError(
+                "Both 'indices' and 'indexs' provided to LpVariable.matrix.  Use one only, preferably 'indices'."
+            )
+        elif indices is not None:
+            pass
+        elif indexs is not None:
+            warnings.warn(
+                "'indexs' is deprecated; use 'indices'.", DeprecationWarning, 2
+            )
+            indices = indexs
+        else:
+            raise TypeError(
+                "LpVariable.matrix missing both 'indices' and deprecated 'indexs' arguments."
+            )
+
+        if not isinstance(indices, tuple):
+            indices = (indices,)
         if "%" not in name:
-            name += "_%s" * len(indexs)
+            name += "_%s" * len(indices)
 
-        index = indexs[0]
-        indexs = indexs[1:]
-        if len(indexs) == 0:
+        index = indices[0]
+        indices = indices[1:]
+        if len(indices) == 0:
             return [
                 LpVariable(name % tuple(indexStart + [i]), lowBound, upBound, cat)
                 for i in index
@@ -336,7 +355,7 @@ class LpVariable(LpElement):
         else:
             return [
                 LpVariable.matrix(
-                    name, indexs, lowBound, upBound, cat, indexStart + [i]
+                    name, indices, lowBound, upBound, cat, indexStart + [i]
                 )
                 for i in index
             ]
@@ -345,17 +364,18 @@ class LpVariable(LpElement):
     def dicts(
         cls,
         name,
-        indexs,
+        indices=None,  # required param. enforced within function for backwards compatibility
         lowBound=None,
         upBound=None,
         cat=const.LpContinuous,
         indexStart=[],
+        indexs=None,
     ):
         """
         This function creates a dictionary of :py:class:`LpVariable` with the specified associated parameters.
 
         :param name: The prefix to the name of each LP variable created
-        :param indexs: A list of strings of the keys to the dictionary of LP
+        :param indices: A list of strings of the keys to the dictionary of LP
             variables, and the main part of the variable name itself
         :param lowBound: The lower bound on these variables' range. Default is
             negative infinity
@@ -363,18 +383,37 @@ class LpVariable(LpElement):
             positive infinity
         :param cat: The category these variables are in, Integer or
             Continuous(default)
+        :param indexs: (deprecated) Replaced with `indices` parameter
 
         :return: A dictionary of :py:class:`LpVariable`
         """
-        if not isinstance(indexs, tuple):
-            indexs = (indexs,)
+
+        # Backwards Compatiblity with Deprecation Warning for indexs
+        if indices is not None and indexs is not None:
+            raise TypeError(
+                "Both 'indices' and 'indexs' provided to LpVariable.dicts.  Use one only, preferably 'indices'."
+            )
+        elif indices is not None:
+            pass
+        elif indexs is not None:
+            warnings.warn(
+                "'indexs' is deprecated; use 'indices'.", DeprecationWarning, 2
+            )
+            indices = indexs
+        else:
+            raise TypeError(
+                "LpVariable.dicts missing both 'indices' and deprecated 'indexs' arguments."
+            )
+
+        if not isinstance(indices, tuple):
+            indices = (indices,)
         if "%" not in name:
-            name += "_%s" * len(indexs)
+            name += "_%s" * len(indices)
 
-        index = indexs[0]
-        indexs = indexs[1:]
+        index = indices[0]
+        indices = indices[1:]
         d = {}
-        if len(indexs) == 0:
+        if len(indices) == 0:
             for i in index:
                 d[i] = LpVariable(
                     name % tuple(indexStart + [str(i)]), lowBound, upBound, cat
@@ -382,20 +421,20 @@ class LpVariable(LpElement):
         else:
             for i in index:
                 d[i] = LpVariable.dicts(
-                    name, indexs, lowBound, upBound, cat, indexStart + [i]
+                    name, indices, lowBound, upBound, cat, indexStart + [i]
                 )
         return d
 
     @classmethod
-    def dict(cls, name, indexs, lowBound=None, upBound=None, cat=const.LpContinuous):
-        if not isinstance(indexs, tuple):
-            indexs = (indexs,)
+    def dict(cls, name, indices, lowBound=None, upBound=None, cat=const.LpContinuous):
+        if not isinstance(indices, tuple):
+            indices = (indices,)
         if "%" not in name:
-            name += "_%s" * len(indexs)
+            name += "_%s" * len(indices)
 
-        lists = indexs
+        lists = indices
 
-        if len(indexs) > 1:
+        if len(indices) > 1:
             # Cartesian product
             res = []
             while len(lists):
@@ -412,8 +451,8 @@ class LpVariable(LpElement):
                     res = [[f] for f in first]
                 lists = lists[:-1]
             index = [tuple(r) for r in res]
-        elif len(indexs) == 1:
-            index = indexs[0]
+        elif len(indices) == 1:
+            index = indices[0]
         else:
             return {}
 


Test patch:
diff --git a/pulp/tests/test_pulp.py b/pulp/tests/test_pulp.py
index 0f3f31b..af697e8 100644
--- a/pulp/tests/test_pulp.py
+++ b/pulp/tests/test_pulp.py
@@ -1189,6 +1189,99 @@ class BaseSolverTest:
                 prob, self.solver, [const.LpStatusOptimal], {x: 4, y: -1, z: 6, w: 0}
             )
 
+        def test_LpVariable_indexs_param(self):
+            """
+            Test that 'indexs' param continues to work
+            """
+
+            prob = LpProblem(self._testMethodName, const.LpMinimize)
+            customers = [1, 2, 3]
+            agents = ["A", "B", "C"]
+
+            print("\t Testing 'indexs' param continues to work for LpVariable.dicts")
+            # explicit param creates a dict of type LpVariable
+            assign_vars = LpVariable.dicts(name="test", indexs=(customers, agents))
+            for k, v in assign_vars.items():
+                for a, b in v.items():
+                    self.assertIsInstance(b, LpVariable)
+
+            # param by position creates a dict of type LpVariable
+            assign_vars = LpVariable.dicts("test", (customers, agents))
+            for k, v in assign_vars.items():
+                for a, b in v.items():
+                    self.assertIsInstance(b, LpVariable)
+
+            print("\t Testing 'indexs' param continues to work for LpVariable.matrix")
+            # explicit param creates list of list of LpVariable
+            assign_vars_matrix = LpVariable.matrix(
+                name="test", indexs=(customers, agents)
+            )
+            for a in assign_vars_matrix:
+                for b in a:
+                    self.assertIsInstance(b, LpVariable)
+
+            # param by position creates list of list of LpVariable
+            assign_vars_matrix = LpVariable.matrix("test", (customers, agents))
+            for a in assign_vars_matrix:
+                for b in a:
+                    self.assertIsInstance(b, LpVariable)
+
+        def test_LpVariable_indices_param(self):
+            """
+            Test that 'indices' argument works
+            """
+            prob = LpProblem(self._testMethodName, const.LpMinimize)
+            customers = [1, 2, 3]
+            agents = ["A", "B", "C"]
+
+            print("\t Testing 'indices' argument works in LpVariable.dicts")
+            # explicit param creates a dict of type LpVariable
+            assign_vars = LpVariable.dicts(name="test", indices=(customers, agents))
+            for k, v in assign_vars.items():
+                for a, b in v.items():
+                    self.assertIsInstance(b, LpVariable)
+
+            print("\t Testing 'indices' param continues to work for LpVariable.matrix")
+            # explicit param creates list of list of LpVariable
+            assign_vars_matrix = LpVariable.matrix(
+                name="test", indices=(customers, agents)
+            )
+            for a in assign_vars_matrix:
+                for b in a:
+                    self.assertIsInstance(b, LpVariable)
+
+        def test_LpVariable_indexs_deprecation_logic(self):
+            """
+            Test that logic put in place for deprecation handling of indexs works
+            """
+            print(
+                "\t Test that logic put in place for deprecation handling of indexs works"
+            )
+            prob = LpProblem(self._testMethodName, const.LpMinimize)
+            customers = [1, 2, 3]
+            agents = ["A", "B", "C"]
+
+            with self.assertRaises(TypeError):
+                # both variables
+                assign_vars_matrix = LpVariable.dicts(
+                    name="test",
+                    indices=(customers, agents),
+                    indexs=(customers, agents),
+                )
+
+            with self.assertRaises(TypeError):
+                # no variables
+                assign_vars_matrix = LpVariable.dicts(
+                    name="test",
+                )
+
+            # Not supported in 2.7.  Introduced to unittest in 3.2
+            # with self.assertWarns(DeprecationWarning):
+            #    assign_vars_matrix = LpVariable.dicts(
+            #        name="test",
+            #        indexs=(customers, agents),
+            #    )
+
 
 class PULP_CBC_CMDTest(BaseSolverTest.PuLPTest):
     solveInst = PULP_CBC_CMD