Repository: MechanicalSoup/MechanicalSoup

Issue:
Implement nr option?
In MechanicalSoup, almost all of our methods that find some element return or operate on the first one that is found (`Form.choose_submit` appears to be the recently-modified exception). If, for example, you have a page with two `<form>...</form>` with no unique attributes, then you cannot select the second form with `StatefulBrowser.select_form`.

This problem is solved in Mechanize by giving these methods an `nr=1` argument, which selects the _nr_-th match, and the first by default. We could certainly add this to many of the methods and retain the current default behavior entirely. I think this alone would be a worthwhile enhancement.

One related (but perhaps separate) issue is the lack of feedback when you omit the `nr` option and get an element that you don't expect, likely resulting in an indecipherable error, or worse, silently incorrect behavior. In my ideal world, you'd specify `nr` when you know you want the _nr_-th match, and if `nr` is omitted you demand that there is only one match. This seems like the safest option, but may be too draconian. Perhaps it would be suitable for a "debug" or "strict" mode?

Discussion:
moy: Looks good to me. 0-based index shouldn't be a problem for programmers. 

Patch:
diff --git a/docs/ChangeLog.rst b/docs/ChangeLog.rst
index 1fd119d..cbaa569 100644
--- a/docs/ChangeLog.rst
+++ b/docs/ChangeLog.rst
@@ -23,7 +23,9 @@ Main changes:
 * We now have a documentation: https://mechanicalsoup.readthedocs.io/
 
 * ``StatefulBrowser.select_form`` can now be called without argument,
-  and defaults to ``"form"`` in this case.
+  and defaults to ``"form"`` in this case. It also has a new argument,
+  ``nr`` (defaults to 0), which can be used to specify the index of
+  the form to select if multiple forms match the selection criteria.
 
 * We now use requirement files. You can install the dependencies of
   Mechanicalsoup with e.g.::
diff --git a/mechanicalsoup/stateful_browser.py b/mechanicalsoup/stateful_browser.py
index 8d27bbc..5baf098 100644
--- a/mechanicalsoup/stateful_browser.py
+++ b/mechanicalsoup/stateful_browser.py
@@ -147,31 +147,32 @@ class StatefulBrowser(Browser):
         """
         return self.open(self.absolute_url(url), *args, **kwargs)
 
-    def select_form(self, selector="form", *args, **kwargs):
+    def select_form(self, selector="form", nr=0):
         """Select a form in the current page.
 
+        :param selector: CSS selector to identify the form to select.
+            If not specified, ``selector`` defaults to "form", which is
+            useful if, e.g., there is only one form on the page.
+            For ``selector`` syntax, see the `.select() method in BeautifulSoup
+            <https://www.crummy.com/software/BeautifulSoup/bs4/doc/#css-selectors>`__.
+        :param nr: A zero-based index specifying which form among those that
+            match ``selector`` will be selected. Useful when one or more forms
+            have the same attributes as the form you want to select, and its
+            position on the page is the only way to uniquely identify it.
+            Default is the first matching form (``nr=0``).
+
         :return: The selected form as a soup object. It can also be
             retrieved later with :func:`get_current_form`.
-
-        Arguments are the same as the select() method for a soup
-        object. ``selector`` is a string containing a CSS selector.
-
-        If the ``selector`` argument is not given, it defaults
-        to "form", so one can
-        call :func:`~mechanicalsoup.Browser.select_form()` to select
-        the form if there is only one form in the page.
-
-        See also: `.select() method in BeautifulSoup
-        <https://www.crummy.com/software/BeautifulSoup/bs4/doc/#css-selectors>`__
         """
-        found_forms = self.__current_page.select(selector, *args, **kwargs)
-        if len(found_forms) < 1:
+        # nr is a 0-based index for consistency with mechanize
+        found_forms = self.__current_page.select(selector, limit=nr + 1)
+        if len(found_forms) != nr + 1:
             if self.__debug:
-                print('select_form failed for', *args)
+                print('select_form failed for', selector)
                 self.launch_browser()
             raise LinkNotFoundError()
 
-        self.__current_form = Form(found_forms[0])
+        self.__current_form = Form(found_forms[-1])
         return self.__current_form
 
     def submit_selected(self, btnName=None, *args, **kwargs):


Test patch:
diff --git a/tests/test_stateful_browser.py b/tests/test_stateful_browser.py
index 58eb88d..19febf0 100644
--- a/tests/test_stateful_browser.py
+++ b/tests/test_stateful_browser.py
@@ -326,5 +326,20 @@ def test_with():
     assert browser.session is None
 
 
+def test_select_form_nr():
+    """Test the nr option of select_form."""
+    forms = """<form id="a"></form><form id="b"></form><form id="c"></form>"""
+    with mechanicalsoup.StatefulBrowser() as browser:
+        browser.open_fake_page(forms)
+        form = browser.select_form()
+        assert form.form['id'] == "a"
+        form = browser.select_form(nr=1)
+        assert form.form['id'] == "b"
+        form = browser.select_form(nr=2)
+        assert form.form['id'] == "c"
+        with pytest.raises(mechanicalsoup.LinkNotFoundError):
+            browser.select_form(nr=3)
+
+
 if __name__ == '__main__':
     pytest.main(sys.argv)