File: _mechanize.py

package info (click to toggle)
python-mechanize 1%3A0.4.10%2Bds-5
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,316 kB
  • sloc: python: 16,656; makefile: 11; sh: 4
file content (853 lines) | stat: -rw-r--r-- 31,661 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
"""Stateful programmatic WWW navigation, after Perl's WWW::Mechanize.

Copyright 2003-2006 John J. Lee <jjl@pobox.com>
Copyright 2003 Andy Lester (original Perl code)

This code is free software; you can redistribute it and/or modify it
under the terms of the BSD or ZPL 2.1 licenses (see the file LICENSE
included with the distribution).

"""
from __future__ import absolute_import

import copy
import os
import re

from . import _request, _response, _rfc3986, _sockettimeout, _urllib2_fork
from ._clientcookie import Cookie
from ._headersutil import normalize_header_name
from ._html import Factory
from ._useragent import UserAgentBase
from .polyglot import pathname2url, HTTPError, is_string, iteritems
from ._response import make_response


class BrowserStateError(Exception):
    pass


class LinkNotFoundError(Exception):
    pass


class FormNotFoundError(Exception):
    pass


def sanepathname2url(path):
    urlpath = pathname2url(path)
    if os.name == "nt" and urlpath.startswith("///"):
        urlpath = urlpath[2:]
    # XXX don't ask me about the mac...
    return urlpath


class History:
    """

    Though this will become public, the implied interface is not yet stable.

    """

    def __init__(self):
        self._history = []  # LIFO

    def add(self, request, response):
        self._history.append((request, response))

    def back(self, n, _response):
        response = _response  # XXX move Browser._response into this class?
        while n > 0 or response is None:
            try:
                request, response = self._history.pop()
            except IndexError:
                raise BrowserStateError("already at start of history")
            n -= 1
        return request, response

    def clear(self):
        del self._history[:]

    def close(self):
        for request, response in self._history:
            if response is not None:
                response.close()
        del self._history[:]

    def __copy__(self):
        ans = self.__class__()
        ans._history = self._history[:]
        return ans


class HTTPRefererProcessor(_urllib2_fork.BaseHandler):
    def http_request(self, request):
        # See RFC 2616 14.36.  The only times we know the source of the
        # request URI has a URI associated with it are redirect, and
        # Browser.click() / Browser.submit() / Browser.follow_link().
        # Otherwise, it's the user's job to add any Referer header before
        # .open()ing.
        if hasattr(request, "redirect_dict"):
            request = self.parent._add_referer_header(
                request, origin_request=False)
        return request

    https_request = http_request


class Browser(UserAgentBase):
    """Browser-like class with support for history, forms and links.

    :class:`BrowserStateError` is raised whenever the browser is in the wrong
    state to complete the requested operation - e.g., when :meth:`back()` is
    called when the browser history is empty, or when :meth:`follow_link()` is
    called when the current response does not contain HTML data.

    Public attributes:

    request: current request (:class:`mechanize.Request`)

    form: currently selected form (see :meth:`select_form()`)

    :param history: object implementing the :class:`mechanize.History`
                    interface.  Note this interface is still experimental
                    and may change in future. This object is owned
                    by the browser instance and must not be shared
                    among browsers.
    :param request_class: Request class to use. Defaults to
                            :class:`mechanize.Request`
    :param content_parser: A function that is responsible for parsing
        received html/xhtml content. See the builtin
        :func:`mechanize._html.content_parser()` function for details
        on the interface this function must support.
    :param factory_class: HTML Factory class to use. Defaults to
                            :class:`mechanize.Factory`

    """

    handler_classes = copy.copy(UserAgentBase.handler_classes)
    handler_classes["_referer"] = HTTPRefererProcessor
    default_features = copy.copy(UserAgentBase.default_features)
    default_features.append("_referer")

    def __init__(
            self,
            history=None,
            request_class=None,
            content_parser=None,
            factory_class=Factory,
            allow_xhtml=False, ):
        """
        Only named arguments should be passed to this constructor.

        """
        self._handle_referer = True

        if history is None:
            history = History()
        self._history = history

        if request_class is None:
            request_class = _request.Request

        factory = factory_class(allow_xhtml=allow_xhtml)
        factory.set_request_class(request_class)
        if content_parser is not None:
            factory.set_content_parser(content_parser)
        self._factory = factory
        self.request_class = request_class

        self.request = None
        self._set_response(None, False)

        # do this last to avoid __getattr__ problems
        UserAgentBase.__init__(self)

    def __copy__(self):
        '''
        Clone this browser instance. The clone will share the same, thread-safe
        cookie jar, and have all the same handlers/settings, but will not share
        any other state, making it safe to use in another thread.
        '''
        ans = self.__class__()
        self._copy_state(ans)
        ans._handle_referer = self._handle_referer
        for attr in ('_response_type_finder', '_encoding_finder',
                     '_content_parser'):
            setattr(ans._factory, attr, getattr(self._factory, attr))
        ans.request_class = self.request_class
        ans._history = copy.copy(self._history)
        return ans

    def close(self):
        UserAgentBase.close(self)
        if self._response is not None:
            self._response.close()
        if self._history is not None:
            self._history.close()
            self._history = None

        # make use after .close easy to spot
        self.form = None
        self.request = self._response = None
        self.request = self.response = self.set_response = None
        self.geturl = self.reload = self.back = None
        self.clear_history = self.set_cookie = self.links = self.forms = None
        self.viewing_html = self.encoding = self.title = None
        self.select_form = self.click = self.submit = self.click_link = None
        self.follow_link = self.find_link = None

    def set_handle_referer(self, handle):
        """Set whether to add Referer header to each request."""
        self._set_handler("_referer", handle)
        self._handle_referer = bool(handle)

    def _add_referer_header(self, request, origin_request=True):
        if self.request is None:
            return request
        scheme = request.get_type()
        original_scheme = self.request.get_type()
        if scheme not in ["http", "https"]:
            return request
        if not origin_request and not self.request.has_header("Referer"):
            return request

        if (self._handle_referer and original_scheme in ["http", "https"] and
                not (original_scheme == "https" and scheme != "https")):
            # strip URL fragment (RFC 2616 14.36)
            parts = _rfc3986.urlsplit(self.request.get_full_url())
            parts = parts[:-1] + (None, )
            referer = _rfc3986.urlunsplit(parts)
            request.add_unredirected_header("Referer", referer)
        return request

    def open_novisit(self,
                     url_or_request,
                     data=None,
                     timeout=_sockettimeout._GLOBAL_DEFAULT_TIMEOUT):
        """Open a URL without visiting it.

        Browser state (including request, response, history, forms and links)
        is left unchanged by calling this function.

        The interface is the same as for :meth:`open()`.

        This is useful for things like fetching images.

        See also :meth:`retrieve()`

        """
        return self._mech_open(
            url_or_request, data, visit=False, timeout=timeout)

    def open(self,
             url_or_request,
             data=None,
             timeout=_sockettimeout._GLOBAL_DEFAULT_TIMEOUT):
        '''
        Open a URL. Loads the page so that you can subsequently use
        :meth:`forms()`, :meth:`links()`, etc. on it.

        :param url_or_request: Either a URL or a :class:`mechanize.Request`
        :param dict data: data to send with a POST request
        :param timeout: Timeout in seconds
        :return: A :class:`mechanize.Response` object
        '''
        return self._mech_open(url_or_request, data, timeout=timeout)

    def _mech_open(self,
                   url,
                   data=None,
                   update_history=True,
                   visit=None,
                   timeout=_sockettimeout._GLOBAL_DEFAULT_TIMEOUT):
        try:
            url.get_full_url
        except AttributeError:
            # string URL -- convert to absolute URL if required
            scheme, authority = _rfc3986.urlsplit(url)[:2]
            if scheme is None:
                # relative URL
                if self._response is None:
                    raise BrowserStateError("can't fetch relative reference: "
                                            "not viewing any document")
                url = _rfc3986.urljoin(self._response.geturl(), url)

        request = self._request(url, data, visit, timeout)
        visit = request.visit
        if visit is None:
            visit = True

        if visit:
            self._visit_request(request, update_history)

        success = True
        try:
            response = UserAgentBase.open(self, request, data)
        except HTTPError as error:
            success = False
            if error.fp is None:  # not a response
                raise
            response = error

#         except (IOError, socket.error, OSError) as error:
#             Yes, urllib2 really does raise all these :-((
#             See test_urllib2.py for examples of socket.gaierror and OSError,
#             plus note that FTPHandler raises IOError.
#             XXX I don't seem to have an example of exactly socket.error being
#              raised, only socket.gaierror...
#             I don't want to start fixing these here, though, since this is a
#             subclass of OpenerDirector, and it would break old code.  Even in
#             Python core, a fix would need some backwards-compat. hack to be
#             acceptable.
#             raise

        if visit:
            self._set_response(response, False)
            response = copy.copy(self._response)
        elif response is not None:
            response = _response.upgrade_response(response)

        if not success:
            raise response
        return response

    def __str__(self):
        text = []
        text.append("<%s " % self.__class__.__name__)
        if self._response:
            text.append("visiting %s" % self._response.geturl())
        else:
            text.append("(not visiting a URL)")
        if self.form:
            text.append("\n selected form:\n %s\n" % str(self.form))
        text.append(">")
        return "".join(text)

    def response(self):
        """Return a copy of the current response.

        The returned object has the same interface as the object returned by
        :meth:`.open()`

        """
        return copy.copy(self._response)

    def open_local_file(self, filename):
        path = sanepathname2url(os.path.abspath(filename))
        url = 'file://' + path
        return self.open(url)

    def set_response(self, response):
        """Replace current response with (a copy of) response.

        response may be None.

        This is intended mostly for HTML-preprocessing.
        """
        self._set_response(response, True)

    def _set_response(self, response, close_current):
        # sanity check, necessary but far from sufficient
        if not (response is None or
                (hasattr(response, "info") and hasattr(response, "geturl") and
                 hasattr(response, "read"))):
            raise ValueError("not a response object")

        self.form = None
        if response is not None:
            response = _response.upgrade_response(response)
        if close_current and self._response is not None:
            self._response.close()
        self._response = response
        self._factory.set_response(response)

    def visit_response(self, response, request=None):
        """Visit the response, as if it had been :meth:`open()` ed.

        Unlike :meth:`set_response()`, this updates history rather than
        replacing the current response.
        """
        if request is None:
            request = _request.Request(response.geturl())
        self._visit_request(request, True)
        self._set_response(response, False)

    def _visit_request(self, request, update_history):
        if self._response is not None:
            self._response.close()
        if self.request is not None and update_history:
            self._history.add(self.request, self._response)
        self._response = None
        # we want self.request to be assigned even if UserAgentBase.open
        # fails
        self.request = request

    def set_html(self, html, url="http://example.com/"):
        """Set the response to dummy with given HTML, and URL if given.

        Allows you to then parse that HTML, especially to extract forms
        information. If no URL was given then the default is "example.com".
        """
        response = make_response(html, [("Content-type", "text/html")], url)
        self._set_response(response, True)

    def geturl(self):
        """Get URL of current document."""
        if self._response is None:
            raise BrowserStateError("not viewing any document")
        return self._response.geturl()

    def reload(self):
        """Reload current document, and return response object."""
        if self.request is None:
            raise BrowserStateError("no URL has yet been .open()ed")
        if self._response is not None:
            self._response.close()
        return self._mech_open(self.request, update_history=False)

    def back(self, n=1):
        """Go back n steps in history, and return response object.

        n: go back this number of steps (default 1 step)

        """
        if self._response is not None:
            self._response.close()
        self.request, response = self._history.back(n, self._response)
        self.set_response(response)
        if not response.read_complete:
            return self.reload()
        return copy.copy(response)

    def clear_history(self):
        self._history.clear()

    def set_cookie(self, cookie_string):
        """Set a cookie.

        Note that it is NOT necessary to call this method under ordinary
        circumstances: cookie handling is normally entirely automatic.  The
        intended use case is rather to simulate the setting of a cookie by
        client script in a web page (e.g. JavaScript).  In that case, use of
        this method is necessary because mechanize currently does not support
        JavaScript, VBScript, etc.

        The cookie is added in the same way as if it had arrived with the
        current response, as a result of the current request.  This means that,
        for example, if it is not appropriate to set the cookie based on the
        current request, no cookie will be set.

        The cookie will be returned automatically with subsequent responses
        made by the Browser instance whenever that's appropriate.

        cookie_string should be a valid value of the Set-Cookie header.

        For example:

        .. code-block:: python

            browser.set_cookie(
                "sid=abcdef; expires=Wednesday, 09-Nov-06 23:12:40 GMT")

        Currently, this method does not allow for adding RFC 2986 cookies.
        This limitation will be lifted if anybody requests it.

        See also :meth:`set_simple_cookie()` for an easier way to set cookies
        without needing to create a Set-Cookie header string.
        """
        if self._response is None:
            raise BrowserStateError("not viewing any document")
        if self.request.get_type() not in ["http", "https"]:
            raise BrowserStateError("can't set cookie for non-HTTP/HTTPS "
                                    "transactions")
        cookiejar = self._ua_handlers["_cookies"].cookiejar
        response = self.response()  # copy
        headers = response.info()
        headers["Set-cookie"] = cookie_string
        cookiejar.extract_cookies(response, self.request)

    def set_simple_cookie(self, name, value, domain, path='/'):
        '''
        Similar to :meth:`set_cookie()` except that instead of using a
        cookie string, you simply specify the `name`, `value`, `domain`
        and optionally the `path`.
        The created cookie will never expire. For example:

        .. code-block:: python

            browser.set_simple_cookie('some_key', 'some_value', '.example.com',
                                      path='/some-page')
        '''
        self.cookiejar.set_cookie(
            Cookie(0, name, value, None, False, domain, True, False, path,
                   True, False, None, False, None, None, None))

    @property
    def cookiejar(self):
        ' Return the current cookiejar (:class:`mechanize.CookieJar`) or None '
        try:
            return self._ua_handlers["_cookies"].cookiejar
        except Exception:
            pass

    def set_header(self, header, value=None):
        '''
        Convenience method to set a header value in `self.addheaders`
        so that the header is sent out with all requests automatically.

        :param header: The header name, e.g. User-Agent
        :param value: The header value. If set to None the header is removed.
        '''
        found = False
        header = normalize_header_name(header)
        q = header.lower()
        remove = []
        for i, (k, v) in enumerate(tuple(self.addheaders)):
            if k.lower() == q:
                if value:
                    self.addheaders[i] = (header, value)
                    found = True
                else:
                    remove.append(i)
        if not found:
            self.addheaders.append((header, value))
        if remove:
            for i in reversed(remove):
                del self.addheaders[i]

    def links(self, **kwds):
        """Return iterable over links (:class:`mechanize.Link` objects)."""
        if not self.viewing_html():
            raise BrowserStateError("not viewing HTML")
        links = self._factory.links()
        if kwds:
            return self._filter_links(links, **kwds)
        else:
            return links

    def forms(self):
        """Return iterable over forms.

        The returned form objects implement the :class:`mechanize.HTMLForm`
        interface.

        """
        if not self.viewing_html():
            raise BrowserStateError("not viewing HTML")
        return self._factory.forms()

    def global_form(self):
        """Return the global form object, or None if the factory implementation
        did not supply one.

        The "global" form object contains all controls that are not descendants
        of any FORM element.

        The returned form object implements the :class:`mechanize.HTMLForm`
        interface.

        This is a separate method since the global form is not regarded as part
        of the sequence of forms in the document -- mostly for
        backwards-compatibility.

        """
        if not self.viewing_html():
            raise BrowserStateError("not viewing HTML")
        return self._factory.global_form

    def viewing_html(self):
        """Return whether the current response contains HTML data."""
        if self._response is None:
            raise BrowserStateError("not viewing any document")
        return self._factory.is_html

    def encoding(self):
        if self._response is None:
            raise BrowserStateError("not viewing any document")
        return self._factory.encoding

    def title(self):
        ' Return title, or None if there is no title element in the document. '
        if not self.viewing_html():
            raise BrowserStateError("not viewing HTML")
        return self._factory.title

    def select_form(self, name=None, predicate=None, nr=None, **attrs):
        """Select an HTML form for input.

        This is a bit like giving a form the "input focus" in a browser.

        If a form is selected, the Browser object supports the HTMLForm
        interface, so you can call methods like :meth:`set_value()`,
        :meth:`set()`, and :meth:`click()`.

        Another way to select a form is to assign to the .form attribute.  The
        form assigned should be one of the objects returned by the
        :meth:`forms()` method.

        If no matching form is found,
        :class:`mechanize.FormNotFoundError` is raised.

        If `name` is specified, then the form must have the indicated name.

        If `predicate` is specified, then the form must match that function.
        The predicate function is passed the :class:`mechanize.HTMLForm` as its
        single argument, and should return a boolean value indicating whether
        the form matched.

        `nr`, if supplied, is the sequence number of the form (where 0 is the
        first).  Note that control 0 is the first form matching all the other
        arguments (if supplied); it is not necessarily the first control in the
        form.  The "global form" (consisting of all form controls not contained
        in any FORM element) is considered not to be part of this sequence and
        to have no name, so will not be matched unless both name and nr are
        None.

        You can also match on any HTML attribute of the `<form>` tag by passing
        in the attribute name and value as keyword arguments. To convert HTML
        attributes into syntactically valid python keyword arguments, the
        following simple rule is used. The python keyword argument name is
        converted to an HTML attribute name by: Replacing all underscores with
        hyphens and removing any trailing underscores. You can pass in strings,
        functions or regular expression objects as the values to match. For
        example:

        .. code-block:: python

            # Match form with the exact action specified
            br.select_form(action='http://foo.com/submit.php')
            # Match form with a class attribute that contains 'login'
            br.select_form(class_=lambda x: 'login' in x)
            # Match form with a data-form-type attribute that matches a regex
            br.select_form(data_form_type=re.compile(r'a|b'))

        """
        if not self.viewing_html():
            raise BrowserStateError("not viewing HTML")
        if name is None and predicate is None and nr is None and not attrs:
            raise ValueError(
                "at least one argument must be supplied to specify form")

        global_form = self._factory.global_form
        if nr is None and name is None and predicate is not None and predicate(
                global_form):
            self.form = global_form
            return

        def attr_selector(q):
            if is_string(q):
                return lambda x: x == q
            if callable(q):
                return q
            return lambda x: q.match(x) is not None
        attrsq = {aname.rstrip('_').replace('_', '-'): attr_selector(v)
                  for aname, v in iteritems(attrs)}

        def form_attrs_match(form_attrs):
            for aname, q in iteritems(attrsq):
                val = form_attrs.get(aname)
                if val is None or not q(val):
                    return False
            return True

        orig_nr = nr
        for form in self.forms():
            if name is not None and name != form.name:
                continue
            if predicate is not None and not predicate(form):
                continue
            if nr:
                nr -= 1
                continue
            if attrs and not form_attrs_match(form.attrs):
                continue
            self.form = form
            break  # success
        else:
            # failure
            description = []
            if name is not None:
                description.append("name '%s'" % name)
            if predicate is not None:
                description.append("predicate %s" % predicate)
            if orig_nr is not None:
                description.append("nr %d" % orig_nr)
            if attrs:
                for k, v in iteritems(attrs):
                    description.append('%s = %r' % (k, v))
            description = ", ".join(description)
            raise FormNotFoundError("no form matching " + description)

    def click(self, *args, **kwds):
        """See :meth:`mechanize.HTMLForm.click()` for documentation."""
        if not self.viewing_html():
            raise BrowserStateError("not viewing HTML")
        request = self.form.click(*args, **kwds)
        return self._add_referer_header(request)

    def submit(self, *args, **kwds):
        """Submit current form.

        Arguments are as for :meth:`mechanize.HTMLForm.click()`.

        Return value is same as for :meth:`open()`.
        """
        return self.open(self.click(*args, **kwds))

    def click_link(self, link=None, **kwds):
        """Find a link and return a Request object for it.

        Arguments are as for :meth:`find_link()`, except that a link may be
        supplied as the first argument.

        """
        if not self.viewing_html():
            raise BrowserStateError("not viewing HTML")
        if not link:
            link = self.find_link(**kwds)
        else:
            if kwds:
                raise ValueError(
                    "either pass a Link, or keyword arguments, not both")
        request = self.request_class(link.absolute_url)
        return self._add_referer_header(request)

    def follow_link(self, link=None, **kwds):
        """Find a link and :meth:`open()` it.

        Arguments are as for :meth:`click_link()`.

        Return value is same as for :meth:`open()`.

        """
        return self.open(self.click_link(link, **kwds))

    def find_link(self,
                  text=None,
                  text_regex=None,
                  name=None,
                  name_regex=None,
                  url=None,
                  url_regex=None,
                  tag=None,
                  predicate=None,
                  nr=0):
        """Find a link in current page.

        Links are returned as :class:`mechanize.Link` objects. Examples:

        .. code-block:: python

            # Return third link that .search()-matches the regexp "python" (by
            # ".search()-matches", I mean that the regular expression method
            # .search() is used, rather than .match()).
            find_link(text_regex=re.compile("python"), nr=2)

            # Return first http link in the current page that points to
            # somewhere on python.org whose link text (after tags have been
            # removed) is exactly "monty python".
            find_link(text="monty python",
                    url_regex=re.compile("http.*python.org"))

            # Return first link with exactly three HTML attributes.
            find_link(predicate=lambda link: len(link.attrs) == 3)

        Links include anchors `<a>`, image maps `<area>`, and frames
        `<iframe>`.

        All arguments must be passed by keyword, not position.  Zero or more
        arguments may be supplied.  In order to find a link, all arguments
        supplied must match.

        If a matching link is not found, :class:`mechanize.LinkNotFoundError`
        is raised.

        :param text: link text between link tags: e.g. <a href="blah">this
            bit</a> with whitespace compressed.
        :param text_regex: link text between tag (as defined above) must match
            the regular expression object or regular expression string passed
            as this argument, if supplied
        :param name: as for text and text_regex, but matched
            against the name HTML attribute of the link tag
        :param url: as for text and text_regex, but matched against the
            URL of the link tag (note this matches against Link.url, which is a
            relative or absolute URL according to how it was written in the
            HTML)
        :param tag: element name of opening tag, e.g. "a"
        :param predicate: a function taking a Link object as its single
            argument, returning a boolean result, indicating whether the links
        :param nr: matches the nth link that matches all other
            criteria (default 0)

        """
        try:
            return next(self._filter_links(
                self._factory.links(), text, text_regex, name, name_regex, url,
                url_regex, tag, predicate, nr))
        except StopIteration:
            raise LinkNotFoundError()

    def __getattr__(self, name):
        # pass through _form.HTMLForm methods and attributes
        form = self.__dict__.get("form")
        if form is None:
            raise AttributeError(
                "%s instance has no attribute %s (perhaps you forgot to "
                ".select_form()?)" % (self.__class__, name))
        return getattr(form, name)

    def __getitem__(self, name):
        if self.form is None:
            raise BrowserStateError('No form selected')
        return self.form[name]

    def __setitem__(self, name, val):
        if self.form is None:
            raise BrowserStateError('No form selected')
        self.form[name] = val

    def _filter_links(self,
                      links,
                      text=None,
                      text_regex=None,
                      name=None,
                      name_regex=None,
                      url=None,
                      url_regex=None,
                      tag=None,
                      predicate=None,
                      nr=0):
        if not self.viewing_html():
            raise BrowserStateError("not viewing HTML")

        orig_nr = nr

        for link in links:
            if url is not None and url != link.url:
                continue
            if url_regex is not None and not re.search(url_regex, link.url):
                continue
            if (text is not None and (link.text is None or text != link.text)):
                continue
            if (
                    text_regex is not None and (
                        link.text is None or not re.search(
                            text_regex, link.text))):
                continue
            if name is not None and name != dict(link.attrs).get("name"):
                continue
            if name_regex is not None:
                link_name = dict(link.attrs).get("name")
                if link_name is None or not re.search(name_regex, link_name):
                    continue
            if tag is not None and tag != link.tag:
                continue
            if predicate is not None and not predicate(link):
                continue
            if nr:
                nr -= 1
                continue
            yield link
            nr = orig_nr