=== added file 'CHANGES.rst'
--- old/CHANGES.rst	1970-01-01 00:00:00 +0000
+++ new/CHANGES.rst	2021-04-05 08:32:53 +0000
@@ -0,0 +1,271 @@
+1.4.4 (unreleased)
+------------------
+
+- Add nextUntil method
+
+
+1.4.3 (2020-11-21)
+------------------
+
+- No longer use a universal wheel
+
+
+1.4.2 (2020-11-21)
+------------------
+
+- Fix exception raised when calling `PyQuery("<textarea></textarea>").text()`
+
+- python2 is no longer supported
+
+1.4.1 (2019-10-26)
+------------------
+
+- This is the latest release with py2 support
+
+- Remove py33, py34 support
+
+- web scraping improvements: default timeout and session support
+
+- Add API methods to serialize form-related elements according to spec
+
+- Include HTML markup when querying textarea text/value
+
+
+1.4.0 (2018-01-11)
+------------------
+
+- Refactoring of `.text()` to match firefox behavior.
+
+
+1.3.0 (2017-10-21)
+------------------
+
+- Remove some unmaintained modules: ``pyquery.ajax`` and ``pyquery.rules``
+
+- Code cleanup. No longer use ugly hacks required by python2.6/python3.2.
+
+- Run tests with python3.6 on CI
+
+- Add a ``method`` argument to ``.outer_html()``
+
+
+1.2.17 (2016-10-14)
+-------------------
+
+- ``PyQuery('<input value="">').val()`` is ``''``
+- ``PyQuery('<input>').val()`` is ``''``
+
+
+1.2.16 (2016-10-14)
+-------------------
+
+- ``.attr('value', '')`` no longer removes the ``value`` attribute
+
+- ``<input type="checkbox">`` without ``value="..."`` have a ``.val()`` of
+  ``'on'``
+
+- ``<input type="radio">`` without ``value="..."`` have a ``.val()`` of
+  ``'on'``
+
+- ``<select>`` without ``<option selected>`` have the value of their first
+  ``<option>`` (or ``None`` if there are no options)
+
+
+1.2.15 (2016-10-11)
+-------------------
+
+- .val() should never raise
+
+- drop py26 support
+
+- improve .extend() by returning self
+
+
+1.2.14 (2016-10-10)
+-------------------
+
+- fix val() for <textarea> and <select>, to match jQuery behavior
+
+
+1.2.13 (2016-04-12)
+-------------------
+
+- Note explicit support for Python 3.5
+
+1.2.12 (2016-04-12)
+-------------------
+
+- make_links_absolute now take care of whitespaces
+
+- added pseudo selector :has()
+
+- add cookies arguments as allowed arguments for requests
+
+
+1.2.11 (2016-02-02)
+-------------------
+
+- Preserve namespaces attribute on PyQuery copies.
+
+- Do not raise an error when the http response code is 2XX
+
+1.2.10 (2016-01-05)
+-------------------
+
+- Fixed #118: implemented usage ``lxml.etree.tostring`` within ``outer_html`` method
+
+- Fixed #117: Raise HTTP Error if HTTP status code is not equal to 200
+
+- Fixed #112: make_links_absolute does not apply to form actions
+
+- Fixed #98: contains act like jQuery
+
+
+1.2.9 (2014-08-22)
+------------------
+
+- Support for keyword arguments in PyQuery custom functions
+
+- Fixed #78: items must take care or the parent
+
+- Fixed #65 PyQuery.make_links_absolute() no longer creates 'href' attribute
+  when it isn't there
+
+- Fixed #19. ``is_()`` was broken.
+
+- Fixed #9. ``.replaceWith(PyQuery element)`` raises error
+
+- Remove official python3.2 support (mostly because of 3rd party semi-deps)
+
+
+1.2.8 (2013-12-21)
+------------------
+
+- Fixed #22: Open by filename fails when file contains invalid xml
+
+- Bug fix in .remove_class()
+
+
+1.2.7 (2013-12-21)
+------------------
+
+- Use pep8 name for methods but keep an alias for camel case method.
+  Eg: remove_attr and removeAttr works
+  Fix #57
+
+- .text() now return an empty string instead of None if there is no text node.
+  Fix #45
+
+- Fixed #23: removeClass adds class attribute to elements which previously
+  lacked one
+
+
+1.2.6 (2013-10-11)
+------------------
+
+- README_fixt.py was not include in the release. Fix #54.
+
+
+1.2.5 (2013-10-10)
+------------------
+
+- cssselect compat. See https://github.com/SimonSapin/cssselect/pull/22
+
+- tests improvments. no longer require a eth connection.
+
+- fix #55
+
+1.2.4
+-----
+
+- Moved to github. So a few files are renamed from .txt to .rst
+
+- Added .xhtml_to_html() and .remove_namespaces()
+
+- Use requests to fetch urls (if available)
+
+- Use restkit's proxy instead of Paste (which will die with py3)
+
+- Allow to open https urls
+
+- python2.5 is no longer supported (may work, but tests are broken)
+
+1.2.3
+-----
+
+- Allow to pass this in .filter() callback
+
+- Add .contents() .items()
+
+- Add tox.ini
+
+- Bug fixes: fix #35 #55 #64 #66
+
+1.2.2
+-----
+
+- Fix cssselectpatch to match the newer implementation of cssselect. Fixes issue #62, #52 and #59 (Haoyu Bai)
+
+- Fix issue #37 (Caleb Burns)
+
+1.2.1
+-----
+
+- Allow to use a custom css translator.
+
+- Fix issue 44: case problem with xml documents
+
+1.2
+---
+
+- PyQuery now uses `cssselect <http://pypi.python.org/pypi/cssselect>`_. See issue 43.
+
+- Fix issue 40: forward .html() extra arguments to ``lxml.etree.tostring``
+
+1.1.1
+-----
+
+- Minor release. Include test file so you can run tests from the tarball.
+
+
+1.1
+---
+
+- fix issues 30, 31, 32 - py3 improvements / webob 1.2+ support
+
+
+1.0
+---
+
+- fix issues 24
+
+0.7
+---
+
+- Python 3 compatible
+
+- Add __unicode__ method
+
+- Add root and encoding attribute
+
+- fix issues 19, 20, 22, 23 
+
+0.6.1
+------
+
+- Move README.txt at package root
+
+- Add CHANGES.txt and add it to long_description
+
+0.6
+----
+
+- Added PyQuery.outerHtml
+
+- Added PyQuery.fn
+
+- Added PyQuery.map
+
+- Change PyQuery.each behavior to reflect jQuery api
+
+

=== added file 'LICENSE.txt'
--- old/LICENSE.txt	1970-01-01 00:00:00 +0000
+++ new/LICENSE.txt	2021-04-05 08:32:53 +0000
@@ -0,0 +1,29 @@
+Copyright (C) 2008 - Olivier Lauzanne <olauzanne@gmail.com>
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+  1. Redistributions of source code must retain the above copyright
+     notice, this list of conditions and the following disclaimer.
+
+  2. Redistributions in binary form must reproduce the above copyright
+     notice, this list of conditions and the following disclaimer in
+     the documentation and/or other materials provided with the
+     distribution.
+
+  3. Neither the name of Infrae nor the names of its contributors may
+     be used to endorse or promote products derived from this software
+     without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INFRAE OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

=== added file 'MANIFEST.in'
--- old/MANIFEST.in	1970-01-01 00:00:00 +0000
+++ new/MANIFEST.in	2021-04-05 08:32:53 +0000
@@ -0,0 +1,9 @@
+graft docs
+prune docs/_build
+graft pyquery
+graft tests
+include *.py
+include *.txt
+include *_fixt.py *.rst *.cfg *.ini
+global-exclude *.pyc
+global-exclude __pycache__

=== added file 'PKG-INFO'
--- old/PKG-INFO	1970-01-01 00:00:00 +0000
+++ new/PKG-INFO	2021-04-05 08:32:53 +0000
@@ -0,0 +1,376 @@
+Metadata-Version: 2.1
+Name: pyquery
+Version: 1.4.4.dev0
+Summary: A jquery-like library for python
+Home-page: https://github.com/gawel/pyquery
+Author: Olivier Lauzanne
+Author-email: olauzanne@gmail.com
+Maintainer: Gael Pasgrimaud
+Maintainer-email: gael@gawel.org
+License: BSD
+Description: 
+        pyquery: a jquery-like library for python
+        =========================================
+        
+        .. image:: https://travis-ci.org/gawel/pyquery.svg
+           :alt: Build Status
+           :target: https://travis-ci.org/gawel/pyquery
+        
+        pyquery allows you to make jquery queries on xml documents.
+        The API is as much as possible the similar to jquery. pyquery uses lxml for fast
+        xml and html manipulation.
+        
+        This is not (or at least not yet) a library to produce or interact with
+        javascript code. I just liked the jquery API and I missed it in python so I
+        told myself "Hey let's make jquery in python". This is the result.
+        
+        The `project`_ is being actively developped on a git repository on Github. I
+        have the policy of giving push access to anyone who wants it and then to review
+        what they do. So if you want to contribute just email me.
+        
+        Please report bugs on the `github
+        <https://github.com/gawel/pyquery/issues>`_ issue
+        tracker.
+        
+        .. _deliverance: http://www.gawel.org/weblog/en/2008/12/skinning-with-pyquery-and-deliverance
+        .. _project: https://github.com/gawel/pyquery/
+        
+        I've spent hours maintaining this software, with love.
+        Please consider tiping if you like it:
+        
+        BTC: 1PruQAwByDndFZ7vTeJhyWefAghaZx9RZg
+        
+        ETH: 0xb6418036d8E06c60C4D91c17d72Df6e1e5b15CE6
+        
+        LTC: LY6CdZcDbxnBX9GFBJ45TqVj8NykBBqsmT
+        
+        ..
+           >>> (urlopen, your_url, path_to_html_file) = getfixture('readme_fixt')
+        
+        Quickstart
+        ==========
+        
+        You can use the PyQuery class to load an xml document from a string, a lxml
+        document, from a file or from an url::
+        
+            >>> from pyquery import PyQuery as pq
+            >>> from lxml import etree
+            >>> import urllib
+            >>> d = pq("<html></html>")
+            >>> d = pq(etree.fromstring("<html></html>"))
+            >>> d = pq(url=your_url)
+            >>> d = pq(url=your_url,
+            ...        opener=lambda url, **kw: urlopen(url).read())
+            >>> d = pq(filename=path_to_html_file)
+        
+        Now d is like the $ in jquery::
+        
+            >>> d("#hello")
+            [<p#hello.hello>]
+            >>> p = d("#hello")
+            >>> print(p.html())
+            Hello world !
+            >>> p.html("you know <a href='http://python.org/'>Python</a> rocks")
+            [<p#hello.hello>]
+            >>> print(p.html())
+            you know <a href="http://python.org/">Python</a> rocks
+            >>> print(p.text())
+            you know Python rocks
+        
+        You can use some of the pseudo classes that are available in jQuery but that
+        are not standard in css such as :first :last :even :odd :eq :lt :gt :checked
+        :selected :file::
+        
+            >>> d('p:first')
+            [<p#hello.hello>]
+        
+        
+        
+        See http://pyquery.rtfd.org/ for the full documentation
+        
+        News
+        ====
+        
+        1.4.4 (unreleased)
+        ------------------
+        
+        - Add nextUntil method
+        
+        
+        1.4.3 (2020-11-21)
+        ------------------
+        
+        - No longer use a universal wheel
+        
+        
+        1.4.2 (2020-11-21)
+        ------------------
+        
+        - Fix exception raised when calling `PyQuery("<textarea></textarea>").text()`
+        
+        - python2 is no longer supported
+        
+        1.4.1 (2019-10-26)
+        ------------------
+        
+        - This is the latest release with py2 support
+        
+        - Remove py33, py34 support
+        
+        - web scraping improvements: default timeout and session support
+        
+        - Add API methods to serialize form-related elements according to spec
+        
+        - Include HTML markup when querying textarea text/value
+        
+        
+        1.4.0 (2018-01-11)
+        ------------------
+        
+        - Refactoring of `.text()` to match firefox behavior.
+        
+        
+        1.3.0 (2017-10-21)
+        ------------------
+        
+        - Remove some unmaintained modules: ``pyquery.ajax`` and ``pyquery.rules``
+        
+        - Code cleanup. No longer use ugly hacks required by python2.6/python3.2.
+        
+        - Run tests with python3.6 on CI
+        
+        - Add a ``method`` argument to ``.outer_html()``
+        
+        
+        1.2.17 (2016-10-14)
+        -------------------
+        
+        - ``PyQuery('<input value="">').val()`` is ``''``
+        - ``PyQuery('<input>').val()`` is ``''``
+        
+        
+        1.2.16 (2016-10-14)
+        -------------------
+        
+        - ``.attr('value', '')`` no longer removes the ``value`` attribute
+        
+        - ``<input type="checkbox">`` without ``value="..."`` have a ``.val()`` of
+          ``'on'``
+        
+        - ``<input type="radio">`` without ``value="..."`` have a ``.val()`` of
+          ``'on'``
+        
+        - ``<select>`` without ``<option selected>`` have the value of their first
+          ``<option>`` (or ``None`` if there are no options)
+        
+        
+        1.2.15 (2016-10-11)
+        -------------------
+        
+        - .val() should never raise
+        
+        - drop py26 support
+        
+        - improve .extend() by returning self
+        
+        
+        1.2.14 (2016-10-10)
+        -------------------
+        
+        - fix val() for <textarea> and <select>, to match jQuery behavior
+        
+        
+        1.2.13 (2016-04-12)
+        -------------------
+        
+        - Note explicit support for Python 3.5
+        
+        1.2.12 (2016-04-12)
+        -------------------
+        
+        - make_links_absolute now take care of whitespaces
+        
+        - added pseudo selector :has()
+        
+        - add cookies arguments as allowed arguments for requests
+        
+        
+        1.2.11 (2016-02-02)
+        -------------------
+        
+        - Preserve namespaces attribute on PyQuery copies.
+        
+        - Do not raise an error when the http response code is 2XX
+        
+        1.2.10 (2016-01-05)
+        -------------------
+        
+        - Fixed #118: implemented usage ``lxml.etree.tostring`` within ``outer_html`` method
+        
+        - Fixed #117: Raise HTTP Error if HTTP status code is not equal to 200
+        
+        - Fixed #112: make_links_absolute does not apply to form actions
+        
+        - Fixed #98: contains act like jQuery
+        
+        
+        1.2.9 (2014-08-22)
+        ------------------
+        
+        - Support for keyword arguments in PyQuery custom functions
+        
+        - Fixed #78: items must take care or the parent
+        
+        - Fixed #65 PyQuery.make_links_absolute() no longer creates 'href' attribute
+          when it isn't there
+        
+        - Fixed #19. ``is_()`` was broken.
+        
+        - Fixed #9. ``.replaceWith(PyQuery element)`` raises error
+        
+        - Remove official python3.2 support (mostly because of 3rd party semi-deps)
+        
+        
+        1.2.8 (2013-12-21)
+        ------------------
+        
+        - Fixed #22: Open by filename fails when file contains invalid xml
+        
+        - Bug fix in .remove_class()
+        
+        
+        1.2.7 (2013-12-21)
+        ------------------
+        
+        - Use pep8 name for methods but keep an alias for camel case method.
+          Eg: remove_attr and removeAttr works
+          Fix #57
+        
+        - .text() now return an empty string instead of None if there is no text node.
+          Fix #45
+        
+        - Fixed #23: removeClass adds class attribute to elements which previously
+          lacked one
+        
+        
+        1.2.6 (2013-10-11)
+        ------------------
+        
+        - README_fixt.py was not include in the release. Fix #54.
+        
+        
+        1.2.5 (2013-10-10)
+        ------------------
+        
+        - cssselect compat. See https://github.com/SimonSapin/cssselect/pull/22
+        
+        - tests improvments. no longer require a eth connection.
+        
+        - fix #55
+        
+        1.2.4
+        -----
+        
+        - Moved to github. So a few files are renamed from .txt to .rst
+        
+        - Added .xhtml_to_html() and .remove_namespaces()
+        
+        - Use requests to fetch urls (if available)
+        
+        - Use restkit's proxy instead of Paste (which will die with py3)
+        
+        - Allow to open https urls
+        
+        - python2.5 is no longer supported (may work, but tests are broken)
+        
+        1.2.3
+        -----
+        
+        - Allow to pass this in .filter() callback
+        
+        - Add .contents() .items()
+        
+        - Add tox.ini
+        
+        - Bug fixes: fix #35 #55 #64 #66
+        
+        1.2.2
+        -----
+        
+        - Fix cssselectpatch to match the newer implementation of cssselect. Fixes issue #62, #52 and #59 (Haoyu Bai)
+        
+        - Fix issue #37 (Caleb Burns)
+        
+        1.2.1
+        -----
+        
+        - Allow to use a custom css translator.
+        
+        - Fix issue 44: case problem with xml documents
+        
+        1.2
+        ---
+        
+        - PyQuery now uses `cssselect <http://pypi.python.org/pypi/cssselect>`_. See issue 43.
+        
+        - Fix issue 40: forward .html() extra arguments to ``lxml.etree.tostring``
+        
+        1.1.1
+        -----
+        
+        - Minor release. Include test file so you can run tests from the tarball.
+        
+        
+        1.1
+        ---
+        
+        - fix issues 30, 31, 32 - py3 improvements / webob 1.2+ support
+        
+        
+        1.0
+        ---
+        
+        - fix issues 24
+        
+        0.7
+        ---
+        
+        - Python 3 compatible
+        
+        - Add __unicode__ method
+        
+        - Add root and encoding attribute
+        
+        - fix issues 19, 20, 22, 23 
+        
+        0.6.1
+        ------
+        
+        - Move README.txt at package root
+        
+        - Add CHANGES.txt and add it to long_description
+        
+        0.6
+        ----
+        
+        - Added PyQuery.outerHtml
+        
+        - Added PyQuery.fn
+        
+        - Added PyQuery.map
+        
+        - Change PyQuery.each behavior to reflect jQuery api
+        
+        
+        
+        
+        
+Keywords: jquery html xml scraping
+Platform: UNKNOWN
+Classifier: Intended Audience :: Developers
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Provides-Extra: test

=== added file 'README.rst'
--- old/README.rst	1970-01-01 00:00:00 +0000
+++ new/README.rst	2021-04-05 08:32:53 +0000
@@ -0,0 +1,75 @@
+pyquery: a jquery-like library for python
+=========================================
+
+.. image:: https://travis-ci.org/gawel/pyquery.svg
+   :alt: Build Status
+   :target: https://travis-ci.org/gawel/pyquery
+
+pyquery allows you to make jquery queries on xml documents.
+The API is as much as possible the similar to jquery. pyquery uses lxml for fast
+xml and html manipulation.
+
+This is not (or at least not yet) a library to produce or interact with
+javascript code. I just liked the jquery API and I missed it in python so I
+told myself "Hey let's make jquery in python". This is the result.
+
+The `project`_ is being actively developped on a git repository on Github. I
+have the policy of giving push access to anyone who wants it and then to review
+what they do. So if you want to contribute just email me.
+
+Please report bugs on the `github
+<https://github.com/gawel/pyquery/issues>`_ issue
+tracker.
+
+.. _deliverance: http://www.gawel.org/weblog/en/2008/12/skinning-with-pyquery-and-deliverance
+.. _project: https://github.com/gawel/pyquery/
+
+I've spent hours maintaining this software, with love.
+Please consider tiping if you like it:
+
+BTC: 1PruQAwByDndFZ7vTeJhyWefAghaZx9RZg
+
+ETH: 0xb6418036d8E06c60C4D91c17d72Df6e1e5b15CE6
+
+LTC: LY6CdZcDbxnBX9GFBJ45TqVj8NykBBqsmT
+
+..
+   >>> (urlopen, your_url, path_to_html_file) = getfixture('readme_fixt')
+
+Quickstart
+==========
+
+You can use the PyQuery class to load an xml document from a string, a lxml
+document, from a file or from an url::
+
+    >>> from pyquery import PyQuery as pq
+    >>> from lxml import etree
+    >>> import urllib
+    >>> d = pq("<html></html>")
+    >>> d = pq(etree.fromstring("<html></html>"))
+    >>> d = pq(url=your_url)
+    >>> d = pq(url=your_url,
+    ...        opener=lambda url, **kw: urlopen(url).read())
+    >>> d = pq(filename=path_to_html_file)
+
+Now d is like the $ in jquery::
+
+    >>> d("#hello")
+    [<p#hello.hello>]
+    >>> p = d("#hello")
+    >>> print(p.html())
+    Hello world !
+    >>> p.html("you know <a href='http://python.org/'>Python</a> rocks")
+    [<p#hello.hello>]
+    >>> print(p.html())
+    you know <a href="http://python.org/">Python</a> rocks
+    >>> print(p.text())
+    you know Python rocks
+
+You can use some of the pseudo classes that are available in jQuery but that
+are not standard in css such as :first :last :even :odd :eq :lt :gt :checked
+:selected :file::
+
+    >>> d('p:first')
+    [<p#hello.hello>]
+

=== added file 'README_fixt.py'
--- old/README_fixt.py	1970-01-01 00:00:00 +0000
+++ new/README_fixt.py	2015-11-25 09:40:39 +0000
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+import os
+from webtest import http
+from webtest.debugapp import debug_app
+
+try:
+    from urllib import urlopen
+except ImportError:
+    from urllib.request import urlopen
+
+
+def setup_test(test):
+    server = http.StopableWSGIServer.create(debug_app)
+    server.wait()
+    path_to_html_file = os.path.join('tests', 'test.html')
+    test.globs.update(
+        urlopen=urlopen,
+        server=server,
+        your_url=server.application_url,
+        path_to_html_file=path_to_html_file,
+    )
+setup_test.__test__ = False
+
+
+def teardown_test(test):
+    test.globs['server'].shutdown()
+teardown_test.__test__ = False

=== added file 'conftest.py'
--- old/conftest.py	1970-01-01 00:00:00 +0000
+++ new/conftest.py	2021-04-05 08:32:53 +0000
@@ -0,0 +1,18 @@
+import os
+import pytest
+from webtest import http
+from webtest.debugapp import debug_app
+from urllib.request import urlopen
+
+
+@pytest.fixture
+def readme_fixt():
+    server = http.StopableWSGIServer.create(debug_app)
+    server.wait()
+    path_to_html_file = os.path.join('tests', 'test.html')
+    yield (
+        urlopen,
+        server.application_url,
+        path_to_html_file,
+    )
+    server.shutdown()

=== added directory 'docs'
=== added file 'docs/Makefile'
--- old/docs/Makefile	1970-01-01 00:00:00 +0000
+++ new/docs/Makefile	2015-11-25 09:40:39 +0000
@@ -0,0 +1,153 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = ../bin/sphinx-build
+PAPER         =
+BUILDDIR      = _build
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  html       to make standalone HTML files"
+	@echo "  dirhtml    to make HTML files named index.html in directories"
+	@echo "  singlehtml to make a single large HTML file"
+	@echo "  pickle     to make pickle files"
+	@echo "  json       to make JSON files"
+	@echo "  htmlhelp   to make HTML files and a HTML help project"
+	@echo "  qthelp     to make HTML files and a qthelp project"
+	@echo "  devhelp    to make HTML files and a Devhelp project"
+	@echo "  epub       to make an epub"
+	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
+	@echo "  text       to make text files"
+	@echo "  man        to make manual pages"
+	@echo "  texinfo    to make Texinfo files"
+	@echo "  info       to make Texinfo files and run them through makeinfo"
+	@echo "  gettext    to make PO message catalogs"
+	@echo "  changes    to make an overview of all changed/added/deprecated items"
+	@echo "  linkcheck  to check all external links for integrity"
+	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+	-rm -rf $(BUILDDIR)/*
+
+html:
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+	@echo
+	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files."
+
+json:
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+	@echo
+	@echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+	@echo
+	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
+	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/chut.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/chut.qhc"
+
+devhelp:
+	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+	@echo
+	@echo "Build finished."
+	@echo "To view the help file:"
+	@echo "# mkdir -p $$HOME/.local/share/devhelp/chut"
+	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/chut"
+	@echo "# devhelp"
+
+epub:
+	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+	@echo
+	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+	@echo "Run \`make' in that directory to run these through (pdf)latex" \
+	      "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo "Running LaTeX files through pdflatex..."
+	$(MAKE) -C $(BUILDDIR)/latex all-pdf
+	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+	@echo
+	@echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+	@echo
+	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo
+	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+	@echo "Run \`make' in that directory to run these through makeinfo" \
+	      "(use \`make info' here to do that automatically)."
+
+info:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo "Running Texinfo files through makeinfo..."
+	make -C $(BUILDDIR)/texinfo info
+	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+	@echo
+	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+	@echo
+	@echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+	@echo "Testing of doctests in the sources finished, look at the " \
+	      "results in $(BUILDDIR)/doctest/output.txt."

=== added file 'docs/api.rst'
--- old/docs/api.rst	1970-01-01 00:00:00 +0000
+++ new/docs/api.rst	2015-11-25 09:40:39 +0000
@@ -0,0 +1,10 @@
+================================================
+:mod:`~pyquery.pyquery` -- PyQuery complete API
+================================================
+
+.. automodule:: pyquery.pyquery
+
+.. autoclass:: PyQuery
+   :members:
+
+

=== added file 'docs/attributes.rst'
--- old/docs/attributes.rst	1970-01-01 00:00:00 +0000
+++ new/docs/attributes.rst	2021-04-05 08:32:53 +0000
@@ -0,0 +1,40 @@
+Attributes
+----------
+
+..
+    >>> from pyquery import PyQuery as pq
+
+Using attribute to select specific tag
+In attribute selectors, the value should be a valid CSS identifier or quoted as string::
+
+    >>> d = pq("<option value='1'><option value='2'>")
+    >>> d('option[value="1"]')
+    [<option>]
+
+
+You can play with the attributes with the jquery API::
+
+    >>> p = pq('<p id="hello" class="hello"></p>')('p')
+    >>> p.attr("id")
+    'hello'
+    >>> p.attr("id", "plop")
+    [<p#plop.hello>]
+    >>> p.attr("id", "hello")
+    [<p#hello.hello>]
+
+
+Or in a more pythonic way::
+
+    >>> p.attr.id = "plop"
+    >>> p.attr.id
+    'plop'
+    >>> p.attr["id"] = "ola"
+    >>> p.attr["id"]
+    'ola'
+    >>> p.attr(id='hello', class_='hello2')
+    [<p#hello.hello2>]
+    >>> p.attr.class_
+    'hello2'
+    >>> p.attr.class_ = 'hello'
+
+

=== added file 'docs/changes.rst'
--- old/docs/changes.rst	1970-01-01 00:00:00 +0000
+++ new/docs/changes.rst	2015-11-25 09:40:39 +0000
@@ -0,0 +1,4 @@
+News
+=====
+
+.. include:: ../CHANGES.rst

=== added file 'docs/conf.py'
--- old/docs/conf.py	1970-01-01 00:00:00 +0000
+++ new/docs/conf.py	2021-04-05 08:32:53 +0000
@@ -0,0 +1,283 @@
+# -*- coding: utf-8 -*-
+#
+# pyquery documentation build configuration file, created by
+# sphinx-quickstart on Thu Nov  1 21:48:09 2012.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration -----------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'pyquery'
+copyright = u'2012-2017, Olivier Lauzanne'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '1.3.x'
+# The full version, including alpha/beta/rc tags.
+release = '1.3.x'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+html_theme = 'nature'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'pyquerydoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+  ('index', 'pyquery.tex', u'pyquery Documentation',
+   u'Olivier Lauzanne', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    ('index', 'pyquery', u'pyquery Documentation',
+     [u'Olivier Lauzanne'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output ------------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+texinfo_documents = [
+  ('index', 'pyquery', u'pyquery Documentation',
+   u'Olivier Lauzanne', 'pyquery', 'One line description of project.',
+   'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+from os import path
+pkg_dir = path.abspath(__file__).split('/docs')[0]
+setup = path.join(pkg_dir, 'setup.py')
+if path.isfile(setup):
+    for line_ in open(setup):
+        if line_.startswith("version"):
+            version = line_.split('=')[-1]
+            version = version.strip()
+            version = version.strip("'\"")
+            release = version
+            break
+del pkg_dir, setup, path
+
+try:
+    from pyquery.cssselectpatch import JQueryTranslator
+except ImportError:
+    pass
+else:
+    with open('pseudo_classes.rst', 'w') as fd:
+        fd.write('=========================\n')
+        fd.write('Using pseudo classes\n')
+        fd.write('=========================\n')
+        for k in sorted(dir(JQueryTranslator)):
+            if k.startswith('xpath_'):
+                attr = getattr(JQueryTranslator, k)
+                doc = getattr(attr, '__doc__', '') or ''
+                doc = doc.strip()
+                if doc.startswith('Common implementation'):
+                    continue
+                k = k[6:]
+                if '_' not in k or not doc:
+                    continue
+                k, t = k.split('_', 1)
+                if '_' in t:
+                    continue
+                if t == 'function':
+                    k += '()'
+                fd.write('\n\n:%s\n' % k)
+                fd.write('==================\n\n')
+                fd.write(doc.strip('..').replace('        ', '    '))

=== added file 'docs/conftest.py'
--- old/docs/conftest.py	1970-01-01 00:00:00 +0000
+++ new/docs/conftest.py	2021-04-05 08:32:53 +0000
@@ -0,0 +1,23 @@
+import os
+import sys
+import pytest
+from webtest import http
+from webtest.debugapp import debug_app
+
+
+@pytest.fixture
+def scrap_url():
+    sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
+    from tests.apps import input_app
+    server = http.StopableWSGIServer.create(input_app)
+    server.wait()
+    yield server.application_url.rstrip('/') + '/html'
+    server.shutdown()
+
+
+@pytest.fixture
+def tips_url():
+    server = http.StopableWSGIServer.create(debug_app)
+    server.wait()
+    yield server.application_url.rstrip('/') + '/form.html'
+    server.shutdown()

=== added file 'docs/css.rst'
--- old/docs/css.rst	1970-01-01 00:00:00 +0000
+++ new/docs/css.rst	2015-11-25 09:40:39 +0000
@@ -0,0 +1,45 @@
+CSS
+---
+
+.. Initialize tests
+
+    >>> from pyquery import PyQuery
+    >>> p = PyQuery('<p id="hello" class="hello"><a/></p>')('p')
+
+You can play with css classes::
+
+    >>> p.addClass("toto")
+    [<p#hello.hello.toto>]
+    >>> p.toggleClass("titi toto")
+    [<p#hello.hello.titi>]
+    >>> p.removeClass("titi")
+    [<p#hello.hello>]
+
+Or the css style::
+
+    >>> p.css("font-size", "15px")
+    [<p#hello.hello>]
+    >>> p.attr("style")
+    'font-size: 15px'
+    >>> p.css({"font-size": "17px"})
+    [<p#hello.hello>]
+    >>> p.attr("style")
+    'font-size: 17px'
+
+Same thing the pythonic way ('_' characters are translated to '-')::
+
+    >>> p.css.font_size = "16px"
+    >>> p.attr.style
+    'font-size: 16px'
+    >>> p.css['font-size'] = "15px"
+    >>> p.attr.style
+    'font-size: 15px'
+    >>> p.css(font_size="16px")
+    [<p#hello.hello>]
+    >>> p.attr.style
+    'font-size: 16px'
+    >>> p.css = {"font-size": "17px"}
+    >>> p.attr.style
+    'font-size: 17px'
+
+

=== added file 'docs/future.rst'
--- old/docs/future.rst	1970-01-01 00:00:00 +0000
+++ new/docs/future.rst	2015-11-25 09:40:39 +0000
@@ -0,0 +1,22 @@
+Future
+-------
+
+- SELECTORS: done
+
+- ATTRIBUTES: done
+
+- CSS: done
+
+- HTML: done
+
+- MANIPULATING: missing the wrapInner method
+
+- TRAVERSING: about half done
+
+- EVENTS: nothing to do with server side might be used later for automatic ajax
+
+- CORE UI EFFECTS: did hide and show the rest doesn't really makes sense on
+  server side
+
+- AJAX: some with wsgi app
+

=== added file 'docs/index.rst'
--- old/docs/index.rst	1970-01-01 00:00:00 +0000
+++ new/docs/index.rst	2021-04-05 08:32:53 +0000
@@ -0,0 +1,44 @@
+.. include:: ../README.rst
+
+Full documentation
+==================
+
+.. toctree::
+   :maxdepth: 1
+
+   attributes
+   css
+   pseudo_classes
+   manipulating
+   traversing
+   api
+   scrap
+   tips
+   testing
+   future
+   changes
+
+More documentation
+==================
+
+First there is the Sphinx documentation `here`_.
+Then for more documentation about the API you can use the `jquery website`_.
+The reference I'm now using for the API is ... the `color cheat sheet`_.
+Then you can always look at the `code`_.
+
+.. _jquery website: http://docs.jquery.com/
+.. _code: https://github.com/gawel/pyquery
+.. _color cheat sheet: http://colorcharge.com/wp-content/uploads/2007/12/jquery12_colorcharge.png
+.. _here: http://pyquery.rtfd.org/
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
+
+
+
+

=== added file 'docs/manipulating.rst'
--- old/docs/manipulating.rst	1970-01-01 00:00:00 +0000
+++ new/docs/manipulating.rst	2021-04-05 08:32:53 +0000
@@ -0,0 +1,81 @@
+Manipulating
+------------
+
+..
+    >>> from pyquery import PyQuery as pq
+
+You can also add content to the end of tags::
+
+    >>> d = pq('<p class="hello" id="hello">you know Python rocks</p>')
+    >>> d('p').append(' check out <a href="http://reddit.com/r/python"><span>reddit</span></a>')
+    [<p#hello.hello>]
+    >>> print(d)
+    <p class="hello" id="hello">you know Python rocks check out <a href="http://reddit.com/r/python"><span>reddit</span></a></p>
+
+Or to the beginning::
+
+    >>> p = d('p')
+    >>> p.prepend('check out <a href="http://reddit.com/r/python">reddit</a>')
+    [<p#hello.hello>]
+    >>> print(p.html())
+    check out <a href="http://reddit.com/r/python">reddit</a>you know ...
+
+Prepend or append an element into an other::
+
+    >>> d = pq('<html><body><div id="test"><a href="http://python.org">python</a> !</div></body></html>')
+    >>> p.prependTo(d('#test'))
+    [<p#hello.hello>]
+    >>> print(d('#test').html())
+    <p class="hello" ...
+
+Insert an element after another::
+
+    >>> p.insertAfter(d('#test'))
+    [<p#hello.hello>]
+    >>> print(d('#test').html())
+    <a href="http://python.org">python</a> !
+
+Or before::
+
+    >>> p.insertBefore(d('#test'))
+    [<p#hello.hello>]
+    >>> print(d('body').html())
+    <p class="hello" id="hello">...
+
+Doing something for each elements::
+
+    >>> p.each(lambda i, e: pq(e).addClass('hello2'))
+    [<p#hello.hello.hello2>]
+
+Remove an element::
+
+    >>> d = pq('<html><body><p id="id">Yeah!</p><p>python rocks !</p></div></html>')
+    >>> d.remove('p#id')
+    [<html>]
+    >>> d('p#id')
+    []
+
+Remove what's inside the selection::
+
+    >>> d('p').empty()
+    [<p>]
+
+And you can get back the modified html::
+
+    >>> print(d)
+    <html><body><p/></body></html>
+
+You can generate html stuff::
+
+    >>> from pyquery import PyQuery as pq
+    >>> print(pq('<div>Yeah !</div>').addClass('myclass') + pq('<b>cool</b>'))
+    <div class="myclass">Yeah !</div><b>cool</b>
+
+Remove all namespaces::
+
+    >>> d = pq('<foo xmlns="http://example.com/foo"></foo>')
+    >>> d
+    [<{http://example.com/foo}foo>]
+    >>> d.remove_namespaces()
+    [<foo>]
+

=== added file 'docs/pseudo_classes.rst'
--- old/docs/pseudo_classes.rst	1970-01-01 00:00:00 +0000
+++ new/docs/pseudo_classes.rst	2021-04-05 08:32:53 +0000
@@ -0,0 +1,357 @@
+=========================
+Using pseudo classes
+=========================
+
+
+:button
+==================
+
+Matches all button input elements and the button element::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery(('<div><input type="button"/>'
+        ...          '<button></button></div>'))
+        >>> d(':button')
+        [<input>, <button>]
+
+    
+
+:checkbox
+==================
+
+Matches all checkbox input elements::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div><input type="checkbox"/></div>')
+        >>> d('input:checkbox')
+        [<input>]
+
+    
+
+:checked
+==================
+
+Matches odd elements, zero-indexed::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div><input checked="checked"/></div>')
+        >>> d('input:checked')
+        [<input>]
+
+    
+
+:child
+==================
+
+right is an immediate child of left
+
+:contains()
+==================
+
+Matches all elements that contain the given text
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div><h1/><h1 class="title">title</h1></div>')
+        >>> d('h1:contains("title")')
+        [<h1.title>]
+
+    
+
+:descendant
+==================
+
+right is a child, grand-child or further descendant of left
+
+:disabled
+==================
+
+Matches all elements that are disabled::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div><input disabled="disabled"/></div>')
+        >>> d('input:disabled')
+        [<input>]
+
+    
+
+:empty
+==================
+
+Match all elements that do not contain other elements::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div><h1><span>title</span></h1><h2/></div>')
+        >>> d(':empty')
+        [<h2>]
+
+    
+
+:enabled
+==================
+
+Matches all elements that are enabled::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div><input value="foo" /></div>')
+        >>> d('input:enabled')
+        [<input>]
+
+    
+
+:eq()
+==================
+
+Matches a single element by its index::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div><h1 class="first"/><h1 class="last"/></div>')
+        >>> d('h1:eq(0)')
+        [<h1.first>]
+        >>> d('h1:eq(1)')
+        [<h1.last>]
+
+    
+
+:even
+==================
+
+Matches even elements, zero-indexed::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div><p></p><p class="last"></p></div>')
+        >>> d('p:even')
+        [<p>]
+
+    
+
+:file
+==================
+
+Matches all input elements of type file::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div><input type="file"/></div>')
+        >>> d('input:file')
+        [<input>]
+
+    
+
+:first
+==================
+
+Matches the first selected element::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div><p class="first"></p><p></p></div>')
+        >>> d('p:first')
+        [<p.first>]
+
+    
+
+:gt()
+==================
+
+Matches all elements with an index over the given one::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div><h1 class="first"/><h1 class="last"/></div>')
+        >>> d('h1:gt(0)')
+        [<h1.last>]
+
+    
+
+:has()
+==================
+
+Matches elements which contain at least one element that matches
+    the specified selector. https://api.jquery.com/has-selector/
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div class="foo"><div class="bar"></div></div>')
+        >>> d('.foo:has(".baz")')
+        []
+        >>> d('.foo:has(".foo")')
+        []
+        >>> d('.foo:has(".bar")')
+        [<div.foo>]
+        >>> d('.foo:has(div)')
+        [<div.foo>]
+
+    
+
+:header
+==================
+
+Matches all header elelements (h1, ..., h6)::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div><h1>title</h1></div>')
+        >>> d(':header')
+        [<h1>]
+
+    
+
+:hidden
+==================
+
+Matches all hidden input elements::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div><input type="hidden"/></div>')
+        >>> d('input:hidden')
+        [<input>]
+
+    
+
+:image
+==================
+
+Matches all image input elements::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div><input type="image"/></div>')
+        >>> d('input:image')
+        [<input>]
+
+    
+
+:input
+==================
+
+Matches all input elements::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery(('<div><input type="file"/>'
+        ...          '<textarea></textarea></div>'))
+        >>> d(':input')
+        [<input>, <textarea>]
+
+    
+
+:last
+==================
+
+Matches the last selected element::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div><p></p><p class="last"></p></div>')
+        >>> d('p:last')
+        [<p.last>]
+
+    
+
+:lt()
+==================
+
+Matches all elements with an index below the given one::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div><h1 class="first"/><h1 class="last"/></div>')
+        >>> d('h1:lt(1)')
+        [<h1.first>]
+
+    
+
+:odd
+==================
+
+Matches odd elements, zero-indexed::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div><p></p><p class="last"></p></div>')
+        >>> d('p:odd')
+        [<p.last>]
+
+    
+
+:parent
+==================
+
+Match all elements that contain other elements::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div><h1><span>title</span></h1><h1/></div>')
+        >>> d('h1:parent')
+        [<h1>]
+
+    
+
+:password
+==================
+
+Matches all password input elements::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div><input type="password"/></div>')
+        >>> d('input:password')
+        [<input>]
+
+    
+
+:pseudo
+==================
+
+Translate a pseudo-element.
+
+    Defaults to not supporting pseudo-elements at all,
+    but can be overridden by sub-classes
+
+:radio
+==================
+
+Matches all radio input elements::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div><input type="radio"/></div>')
+        >>> d('input:radio')
+        [<input>]
+
+    
+
+:reset
+==================
+
+Matches all reset input elements::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div><input type="reset"/></div>')
+        >>> d('input:reset')
+        [<input>]
+
+    
+
+:selected
+==================
+
+Matches all elements that are selected::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<select><option selected="selected"/></select>')
+        >>> d('option:selected')
+        [<option>]
+
+    
+
+:submit
+==================
+
+Matches all submit input elements::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div><input type="submit"/></div>')
+        >>> d('input:submit')
+        [<input>]
+
+    
+
+:text
+==================
+
+Matches all text input elements::
+
+        >>> from pyquery import PyQuery
+        >>> d = PyQuery('<div><input type="text"/></div>')
+        >>> d('input:text')
+        [<input>]
+
+    
\ No newline at end of file

=== added file 'docs/scrap.rst'
--- old/docs/scrap.rst	1970-01-01 00:00:00 +0000
+++ new/docs/scrap.rst	2021-04-05 08:32:53 +0000
@@ -0,0 +1,34 @@
+Scraping
+=========
+
+..
+  >>> from pyquery import PyQuery as pq
+  >>> your_url = getfixture('scrap_url')
+
+PyQuery is able to load an html document from a url::
+
+  >>> pq(your_url)
+  [<html>]
+
+By default it uses python's urllib.
+
+If `requests`_ is installed then it will use it. This allow you to use most of `requests`_ parameters::
+
+  >>> pq(your_url, headers={'user-agent': 'pyquery'})
+  [<html>]
+
+  >>> pq(your_url, {'q': 'foo'}, method='post', verify=True)
+  [<html>]
+
+
+Timeout
+-------
+
+The default timeout is 60 seconds, you can change it by setting the timeout parameter which is forwarded to the underlying urllib or requests library.
+
+Session
+-------
+
+When using the requests library you can instantiate a Session object which keeps state between http calls (for example - to keep cookies). You can set the session parameter to use this session object.
+
+.. _requests: http://docs.python-requests.org/en/latest/

=== added file 'docs/testing.rst'
--- old/docs/testing.rst	1970-01-01 00:00:00 +0000
+++ new/docs/testing.rst	2015-11-25 09:40:39 +0000
@@ -0,0 +1,15 @@
+Testing
+-------
+
+If you want to run the tests that you can see above you should do::
+
+    $ git clone git://github.com/gawel/pyquery.git
+    $ cd pyquery
+    $ python bootstrap.py
+    $ bin/buildout install tox
+    $ bin/tox
+
+You can build the Sphinx documentation by doing::
+
+    $ cd docs
+    $ make html

=== added file 'docs/tips.rst'
--- old/docs/tips.rst	1970-01-01 00:00:00 +0000
+++ new/docs/tips.rst	2021-04-05 08:32:53 +0000
@@ -0,0 +1,38 @@
+Tips
+====
+
+..
+    >>> from pyquery import PyQuery as pq
+    >>> your_url = getfixture('tips_url')
+
+Making links absolute
+---------------------
+
+You can make links absolute which can be useful for screen scrapping::
+
+    >>> d = pq(url=your_url, parser='html')
+    >>> d('form').attr('action')
+    '/form-submit'
+    >>> d.make_links_absolute()
+    [<html>]
+
+Using different parsers
+-----------------------
+
+By default pyquery uses the lxml xml parser and then if it doesn't work goes on
+to try the html parser from lxml.html. The xml parser can sometimes be
+problematic when parsing xhtml pages because the parser will not raise an error
+but give an unusable tree (on w3c.org for example).
+
+You can also choose which parser to use explicitly::
+
+   >>> pq('<html><body><p>toto</p></body></html>', parser='xml')
+   [<html>]
+   >>> pq('<html><body><p>toto</p></body></html>', parser='html')
+   [<html>]
+   >>> pq('<html><body><p>toto</p></body></html>', parser='html_fragments')
+   [<p>]
+
+The html and html_fragments parser are the ones from lxml.html.
+
+

=== added file 'docs/traversing.rst'
--- old/docs/traversing.rst	1970-01-01 00:00:00 +0000
+++ new/docs/traversing.rst	2021-04-05 08:32:53 +0000
@@ -0,0 +1,42 @@
+Traversing
+----------
+
+..
+    >>> from pyquery import PyQuery as pq
+
+Some jQuery traversal methods are supported.  Here are a few examples.
+
+You can filter the selection list using a string selector::
+
+    >>> d = pq('<p id="hello" class="hello"><a/></p><p id="test"><a/></p>')
+    >>> d('p').filter('.hello')
+    [<p#hello.hello>]
+
+It is possible to select a single element with eq::
+
+    >>> d('p').eq(0)
+    [<p#hello.hello>]
+
+You can find nested elements::
+
+    >>> d('p').find('a')
+    [<a>, <a>]
+    >>> d('p').eq(1).find('a')
+    [<a>]
+
+Breaking out of a level of traversal is also supported using end::
+
+    >>> d('p').find('a').end()
+    [<p#hello.hello>, <p#test>]
+    >>> d('p').eq(0).end()
+    [<p#hello.hello>, <p#test>]
+    >>> d('p').filter(lambda i: i == 1).end()
+    [<p#hello.hello>, <p#test>]
+
+
+If you want to select a dotted id you need to escape the dot::
+
+    >>> d = pq('<p id="hello.you"><a/></p><p id="test"><a/></p>')
+    >>> d(r'#hello\.you')
+    [<p#hello.you>]
+

=== added directory 'pyquery'
=== added directory 'pyquery.egg-info'
=== added file 'pyquery.egg-info/PKG-INFO'
--- old/pyquery.egg-info/PKG-INFO	1970-01-01 00:00:00 +0000
+++ new/pyquery.egg-info/PKG-INFO	2021-04-05 08:32:53 +0000
@@ -0,0 +1,376 @@
+Metadata-Version: 2.1
+Name: pyquery
+Version: 1.4.4.dev0
+Summary: A jquery-like library for python
+Home-page: https://github.com/gawel/pyquery
+Author: Olivier Lauzanne
+Author-email: olauzanne@gmail.com
+Maintainer: Gael Pasgrimaud
+Maintainer-email: gael@gawel.org
+License: BSD
+Description: 
+        pyquery: a jquery-like library for python
+        =========================================
+        
+        .. image:: https://travis-ci.org/gawel/pyquery.svg
+           :alt: Build Status
+           :target: https://travis-ci.org/gawel/pyquery
+        
+        pyquery allows you to make jquery queries on xml documents.
+        The API is as much as possible the similar to jquery. pyquery uses lxml for fast
+        xml and html manipulation.
+        
+        This is not (or at least not yet) a library to produce or interact with
+        javascript code. I just liked the jquery API and I missed it in python so I
+        told myself "Hey let's make jquery in python". This is the result.
+        
+        The `project`_ is being actively developped on a git repository on Github. I
+        have the policy of giving push access to anyone who wants it and then to review
+        what they do. So if you want to contribute just email me.
+        
+        Please report bugs on the `github
+        <https://github.com/gawel/pyquery/issues>`_ issue
+        tracker.
+        
+        .. _deliverance: http://www.gawel.org/weblog/en/2008/12/skinning-with-pyquery-and-deliverance
+        .. _project: https://github.com/gawel/pyquery/
+        
+        I've spent hours maintaining this software, with love.
+        Please consider tiping if you like it:
+        
+        BTC: 1PruQAwByDndFZ7vTeJhyWefAghaZx9RZg
+        
+        ETH: 0xb6418036d8E06c60C4D91c17d72Df6e1e5b15CE6
+        
+        LTC: LY6CdZcDbxnBX9GFBJ45TqVj8NykBBqsmT
+        
+        ..
+           >>> (urlopen, your_url, path_to_html_file) = getfixture('readme_fixt')
+        
+        Quickstart
+        ==========
+        
+        You can use the PyQuery class to load an xml document from a string, a lxml
+        document, from a file or from an url::
+        
+            >>> from pyquery import PyQuery as pq
+            >>> from lxml import etree
+            >>> import urllib
+            >>> d = pq("<html></html>")
+            >>> d = pq(etree.fromstring("<html></html>"))
+            >>> d = pq(url=your_url)
+            >>> d = pq(url=your_url,
+            ...        opener=lambda url, **kw: urlopen(url).read())
+            >>> d = pq(filename=path_to_html_file)
+        
+        Now d is like the $ in jquery::
+        
+            >>> d("#hello")
+            [<p#hello.hello>]
+            >>> p = d("#hello")
+            >>> print(p.html())
+            Hello world !
+            >>> p.html("you know <a href='http://python.org/'>Python</a> rocks")
+            [<p#hello.hello>]
+            >>> print(p.html())
+            you know <a href="http://python.org/">Python</a> rocks
+            >>> print(p.text())
+            you know Python rocks
+        
+        You can use some of the pseudo classes that are available in jQuery but that
+        are not standard in css such as :first :last :even :odd :eq :lt :gt :checked
+        :selected :file::
+        
+            >>> d('p:first')
+            [<p#hello.hello>]
+        
+        
+        
+        See http://pyquery.rtfd.org/ for the full documentation
+        
+        News
+        ====
+        
+        1.4.4 (unreleased)
+        ------------------
+        
+        - Add nextUntil method
+        
+        
+        1.4.3 (2020-11-21)
+        ------------------
+        
+        - No longer use a universal wheel
+        
+        
+        1.4.2 (2020-11-21)
+        ------------------
+        
+        - Fix exception raised when calling `PyQuery("<textarea></textarea>").text()`
+        
+        - python2 is no longer supported
+        
+        1.4.1 (2019-10-26)
+        ------------------
+        
+        - This is the latest release with py2 support
+        
+        - Remove py33, py34 support
+        
+        - web scraping improvements: default timeout and session support
+        
+        - Add API methods to serialize form-related elements according to spec
+        
+        - Include HTML markup when querying textarea text/value
+        
+        
+        1.4.0 (2018-01-11)
+        ------------------
+        
+        - Refactoring of `.text()` to match firefox behavior.
+        
+        
+        1.3.0 (2017-10-21)
+        ------------------
+        
+        - Remove some unmaintained modules: ``pyquery.ajax`` and ``pyquery.rules``
+        
+        - Code cleanup. No longer use ugly hacks required by python2.6/python3.2.
+        
+        - Run tests with python3.6 on CI
+        
+        - Add a ``method`` argument to ``.outer_html()``
+        
+        
+        1.2.17 (2016-10-14)
+        -------------------
+        
+        - ``PyQuery('<input value="">').val()`` is ``''``
+        - ``PyQuery('<input>').val()`` is ``''``
+        
+        
+        1.2.16 (2016-10-14)
+        -------------------
+        
+        - ``.attr('value', '')`` no longer removes the ``value`` attribute
+        
+        - ``<input type="checkbox">`` without ``value="..."`` have a ``.val()`` of
+          ``'on'``
+        
+        - ``<input type="radio">`` without ``value="..."`` have a ``.val()`` of
+          ``'on'``
+        
+        - ``<select>`` without ``<option selected>`` have the value of their first
+          ``<option>`` (or ``None`` if there are no options)
+        
+        
+        1.2.15 (2016-10-11)
+        -------------------
+        
+        - .val() should never raise
+        
+        - drop py26 support
+        
+        - improve .extend() by returning self
+        
+        
+        1.2.14 (2016-10-10)
+        -------------------
+        
+        - fix val() for <textarea> and <select>, to match jQuery behavior
+        
+        
+        1.2.13 (2016-04-12)
+        -------------------
+        
+        - Note explicit support for Python 3.5
+        
+        1.2.12 (2016-04-12)
+        -------------------
+        
+        - make_links_absolute now take care of whitespaces
+        
+        - added pseudo selector :has()
+        
+        - add cookies arguments as allowed arguments for requests
+        
+        
+        1.2.11 (2016-02-02)
+        -------------------
+        
+        - Preserve namespaces attribute on PyQuery copies.
+        
+        - Do not raise an error when the http response code is 2XX
+        
+        1.2.10 (2016-01-05)
+        -------------------
+        
+        - Fixed #118: implemented usage ``lxml.etree.tostring`` within ``outer_html`` method
+        
+        - Fixed #117: Raise HTTP Error if HTTP status code is not equal to 200
+        
+        - Fixed #112: make_links_absolute does not apply to form actions
+        
+        - Fixed #98: contains act like jQuery
+        
+        
+        1.2.9 (2014-08-22)
+        ------------------
+        
+        - Support for keyword arguments in PyQuery custom functions
+        
+        - Fixed #78: items must take care or the parent
+        
+        - Fixed #65 PyQuery.make_links_absolute() no longer creates 'href' attribute
+          when it isn't there
+        
+        - Fixed #19. ``is_()`` was broken.
+        
+        - Fixed #9. ``.replaceWith(PyQuery element)`` raises error
+        
+        - Remove official python3.2 support (mostly because of 3rd party semi-deps)
+        
+        
+        1.2.8 (2013-12-21)
+        ------------------
+        
+        - Fixed #22: Open by filename fails when file contains invalid xml
+        
+        - Bug fix in .remove_class()
+        
+        
+        1.2.7 (2013-12-21)
+        ------------------
+        
+        - Use pep8 name for methods but keep an alias for camel case method.
+          Eg: remove_attr and removeAttr works
+          Fix #57
+        
+        - .text() now return an empty string instead of None if there is no text node.
+          Fix #45
+        
+        - Fixed #23: removeClass adds class attribute to elements which previously
+          lacked one
+        
+        
+        1.2.6 (2013-10-11)
+        ------------------
+        
+        - README_fixt.py was not include in the release. Fix #54.
+        
+        
+        1.2.5 (2013-10-10)
+        ------------------
+        
+        - cssselect compat. See https://github.com/SimonSapin/cssselect/pull/22
+        
+        - tests improvments. no longer require a eth connection.
+        
+        - fix #55
+        
+        1.2.4
+        -----
+        
+        - Moved to github. So a few files are renamed from .txt to .rst
+        
+        - Added .xhtml_to_html() and .remove_namespaces()
+        
+        - Use requests to fetch urls (if available)
+        
+        - Use restkit's proxy instead of Paste (which will die with py3)
+        
+        - Allow to open https urls
+        
+        - python2.5 is no longer supported (may work, but tests are broken)
+        
+        1.2.3
+        -----
+        
+        - Allow to pass this in .filter() callback
+        
+        - Add .contents() .items()
+        
+        - Add tox.ini
+        
+        - Bug fixes: fix #35 #55 #64 #66
+        
+        1.2.2
+        -----
+        
+        - Fix cssselectpatch to match the newer implementation of cssselect. Fixes issue #62, #52 and #59 (Haoyu Bai)
+        
+        - Fix issue #37 (Caleb Burns)
+        
+        1.2.1
+        -----
+        
+        - Allow to use a custom css translator.
+        
+        - Fix issue 44: case problem with xml documents
+        
+        1.2
+        ---
+        
+        - PyQuery now uses `cssselect <http://pypi.python.org/pypi/cssselect>`_. See issue 43.
+        
+        - Fix issue 40: forward .html() extra arguments to ``lxml.etree.tostring``
+        
+        1.1.1
+        -----
+        
+        - Minor release. Include test file so you can run tests from the tarball.
+        
+        
+        1.1
+        ---
+        
+        - fix issues 30, 31, 32 - py3 improvements / webob 1.2+ support
+        
+        
+        1.0
+        ---
+        
+        - fix issues 24
+        
+        0.7
+        ---
+        
+        - Python 3 compatible
+        
+        - Add __unicode__ method
+        
+        - Add root and encoding attribute
+        
+        - fix issues 19, 20, 22, 23 
+        
+        0.6.1
+        ------
+        
+        - Move README.txt at package root
+        
+        - Add CHANGES.txt and add it to long_description
+        
+        0.6
+        ----
+        
+        - Added PyQuery.outerHtml
+        
+        - Added PyQuery.fn
+        
+        - Added PyQuery.map
+        
+        - Change PyQuery.each behavior to reflect jQuery api
+        
+        
+        
+        
+        
+Keywords: jquery html xml scraping
+Platform: UNKNOWN
+Classifier: Intended Audience :: Developers
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Provides-Extra: test

=== added file 'pyquery.egg-info/SOURCES.txt'
--- old/pyquery.egg-info/SOURCES.txt	1970-01-01 00:00:00 +0000
+++ new/pyquery.egg-info/SOURCES.txt	2021-04-05 08:32:53 +0000
@@ -0,0 +1,48 @@
+CHANGES.rst
+LICENSE.txt
+MANIFEST.in
+README.rst
+README_fixt.py
+conftest.py
+pytest.ini
+setup.cfg
+setup.py
+tox.ini
+docs/Makefile
+docs/api.rst
+docs/attributes.rst
+docs/changes.rst
+docs/conf.py
+docs/conftest.py
+docs/css.rst
+docs/future.rst
+docs/index.rst
+docs/manipulating.rst
+docs/pseudo_classes.rst
+docs/scrap.rst
+docs/testing.rst
+docs/tips.rst
+docs/traversing.rst
+pyquery/__init__.py
+pyquery/cssselectpatch.py
+pyquery/openers.py
+pyquery/pyquery.py
+pyquery/text.py
+pyquery.egg-info/PKG-INFO
+pyquery.egg-info/SOURCES.txt
+pyquery.egg-info/dependency_links.txt
+pyquery.egg-info/entry_points.txt
+pyquery.egg-info/not-zip-safe
+pyquery.egg-info/requires.txt
+pyquery.egg-info/top_level.txt
+tests/__init__.py
+tests/apps.py
+tests/browser_base.py
+tests/doctests.rst
+tests/geckodriver.sh
+tests/invalid.xml
+tests/selenium.sh
+tests/test.html
+tests/test_browser.py
+tests/test_pyquery.py
+tests/test_real_browser.py
\ No newline at end of file

=== added file 'pyquery.egg-info/dependency_links.txt'
--- old/pyquery.egg-info/dependency_links.txt	1970-01-01 00:00:00 +0000
+++ new/pyquery.egg-info/dependency_links.txt	2015-10-08 17:52:29 +0000
@@ -0,0 +1,1 @@
+

=== added file 'pyquery.egg-info/entry_points.txt'
--- old/pyquery.egg-info/entry_points.txt	1970-01-01 00:00:00 +0000
+++ new/pyquery.egg-info/entry_points.txt	2015-10-08 17:52:29 +0000
@@ -0,0 +1,3 @@
+
+      # -*- Entry points: -*-
+      
\ No newline at end of file

=== added file 'pyquery.egg-info/not-zip-safe'
--- old/pyquery.egg-info/not-zip-safe	1970-01-01 00:00:00 +0000
+++ new/pyquery.egg-info/not-zip-safe	2015-10-08 17:52:29 +0000
@@ -0,0 +1,1 @@
+

=== added file 'pyquery.egg-info/requires.txt'
--- old/pyquery.egg-info/requires.txt	1970-01-01 00:00:00 +0000
+++ new/pyquery.egg-info/requires.txt	2021-04-05 08:32:53 +0000
@@ -0,0 +1,9 @@
+cssselect>0.7.9
+lxml>=2.1
+
+[test]
+pytest
+pytest-cov
+requests
+webob
+webtest

=== added file 'pyquery.egg-info/top_level.txt'
--- old/pyquery.egg-info/top_level.txt	1970-01-01 00:00:00 +0000
+++ new/pyquery.egg-info/top_level.txt	2015-10-08 17:52:29 +0000
@@ -0,0 +1,1 @@
+pyquery

=== added file 'pyquery/__init__.py'
--- old/pyquery/__init__.py	1970-01-01 00:00:00 +0000
+++ new/pyquery/__init__.py	2021-04-05 08:32:53 +0000
@@ -0,0 +1,5 @@
+# Copyright (C) 2008 - Olivier Lauzanne <olauzanne@gmail.com>
+#
+# Distributed under the BSD license, see LICENSE.txt
+
+from .pyquery import PyQuery  # NOQA

=== added file 'pyquery/cssselectpatch.py'
--- old/pyquery/cssselectpatch.py	1970-01-01 00:00:00 +0000
+++ new/pyquery/cssselectpatch.py	2021-04-05 08:32:53 +0000
@@ -0,0 +1,466 @@
+# Copyright (C) 2008 - Olivier Lauzanne <olauzanne@gmail.com>
+#
+# Distributed under the BSD license, see LICENSE.txt
+from __future__ import unicode_literals
+from cssselect import xpath as cssselect_xpath
+from cssselect.xpath import ExpressionError
+
+XPathExprOrig = cssselect_xpath.XPathExpr
+
+
+class XPathExpr(XPathExprOrig):
+
+    def __init__(self, path='', element='*', condition='', star_prefix=False):
+        self.path = path
+        self.element = element
+        self.condition = condition
+        self.post_condition = None
+
+    def add_post_condition(self, post_condition):
+        if self.post_condition:
+            self.post_condition = '%s and (%s)' % (self.post_condition,
+                                                   post_condition)
+        else:
+            self.post_condition = post_condition
+
+    def __str__(self):
+        path = XPathExprOrig.__str__(self)
+        if self.post_condition:
+            path = '%s[%s]' % (path, self.post_condition)
+        return path
+
+    def join(self, combiner, other):
+        res = XPathExprOrig.join(self, combiner, other)
+        self.post_condition = other.post_condition
+        return res
+
+
+# keep cssselect < 0.8 compat for now
+
+
+class JQueryTranslator(cssselect_xpath.HTMLTranslator):
+    """This class is used to implement the css pseudo classes
+    (:first, :last, ...) that are not defined in the css standard,
+    but are defined in the jquery API.
+    """
+
+    xpathexpr_cls = XPathExpr
+
+    def xpath_first_pseudo(self, xpath):
+        """Matches the first selected element::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div><p class="first"></p><p></p></div>')
+            >>> d('p:first')
+            [<p.first>]
+
+        ..
+        """
+        xpath.add_post_condition('position() = 1')
+        return xpath
+
+    def xpath_last_pseudo(self, xpath):
+        """Matches the last selected element::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div><p></p><p class="last"></p></div>')
+            >>> d('p:last')
+            [<p.last>]
+
+        ..
+        """
+        xpath.add_post_condition('position() = last()')
+        return xpath
+
+    def xpath_even_pseudo(self, xpath):
+        """Matches even elements, zero-indexed::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div><p></p><p class="last"></p></div>')
+            >>> d('p:even')
+            [<p>]
+
+        ..
+        """
+        # the first element is 1 in xpath and 0 in python and js
+        xpath.add_post_condition('position() mod 2 = 1')
+        return xpath
+
+    def xpath_odd_pseudo(self, xpath):
+        """Matches odd elements, zero-indexed::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div><p></p><p class="last"></p></div>')
+            >>> d('p:odd')
+            [<p.last>]
+
+        ..
+        """
+        xpath.add_post_condition('position() mod 2 = 0')
+        return xpath
+
+    def xpath_checked_pseudo(self, xpath):
+        """Matches odd elements, zero-indexed::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div><input checked="checked"/></div>')
+            >>> d('input:checked')
+            [<input>]
+
+        ..
+        """
+        xpath.add_condition("@checked and name(.) = 'input'")
+        return xpath
+
+    def xpath_selected_pseudo(self, xpath):
+        """Matches all elements that are selected::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<select><option selected="selected"/></select>')
+            >>> d('option:selected')
+            [<option>]
+
+        ..
+        """
+        xpath.add_condition("@selected and name(.) = 'option'")
+        return xpath
+
+    def _format_disabled_xpath(self, disabled=True):
+        """Format XPath condition for :disabled or :enabled pseudo-classes
+        according to the WHATWG spec. See: https://html.spec.whatwg.org
+        /multipage/semantics-other.html#concept-element-disabled
+        """
+        bool_op = '' if disabled else 'not'
+        return '''(
+            ((name(.) = 'button' or name(.) = 'input' or name(.) = 'select'
+                    or name(.) = 'textarea' or name(.) = 'fieldset')
+                and %s(@disabled or (ancestor::fieldset[@disabled]
+                    and not(ancestor::legend[not(preceding-sibling::legend)])))
+            )
+            or
+            ((name(.) = 'option'
+                and %s(@disabled or ancestor::optgroup[@disabled]))
+            )
+            or
+            ((name(.) = 'optgroup' and %s(@disabled)))
+            )''' % (bool_op, bool_op, bool_op)
+
+    def xpath_disabled_pseudo(self, xpath):
+        """Matches all elements that are disabled::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div><input disabled="disabled"/></div>')
+            >>> d('input:disabled')
+            [<input>]
+
+        ..
+        """
+        xpath.add_condition(self._format_disabled_xpath())
+        return xpath
+
+    def xpath_enabled_pseudo(self, xpath):
+        """Matches all elements that are enabled::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div><input value="foo" /></div>')
+            >>> d('input:enabled')
+            [<input>]
+
+        ..
+        """
+        xpath.add_condition(self._format_disabled_xpath(disabled=False))
+        return xpath
+
+    def xpath_file_pseudo(self, xpath):
+        """Matches all input elements of type file::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div><input type="file"/></div>')
+            >>> d('input:file')
+            [<input>]
+
+        ..
+        """
+        xpath.add_condition("@type = 'file' and name(.) = 'input'")
+        return xpath
+
+    def xpath_input_pseudo(self, xpath):
+        """Matches all input elements::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery(('<div><input type="file"/>'
+            ...              '<textarea></textarea></div>'))
+            >>> d(':input')
+            [<input>, <textarea>]
+
+        ..
+        """
+        xpath.add_condition((
+            "(name(.) = 'input' or name(.) = 'select') "
+            "or (name(.) = 'textarea' or name(.) = 'button')"))
+        return xpath
+
+    def xpath_button_pseudo(self, xpath):
+        """Matches all button input elements and the button element::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery(('<div><input type="button"/>'
+            ...              '<button></button></div>'))
+            >>> d(':button')
+            [<input>, <button>]
+
+        ..
+        """
+        xpath.add_condition((
+            "(@type = 'button' and name(.) = 'input') "
+            "or name(.) = 'button'"))
+        return xpath
+
+    def xpath_radio_pseudo(self, xpath):
+        """Matches all radio input elements::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div><input type="radio"/></div>')
+            >>> d('input:radio')
+            [<input>]
+
+        ..
+        """
+        xpath.add_condition("@type = 'radio' and name(.) = 'input'")
+        return xpath
+
+    def xpath_text_pseudo(self, xpath):
+        """Matches all text input elements::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div><input type="text"/></div>')
+            >>> d('input:text')
+            [<input>]
+
+        ..
+        """
+        xpath.add_condition("@type = 'text' and name(.) = 'input'")
+        return xpath
+
+    def xpath_checkbox_pseudo(self, xpath):
+        """Matches all checkbox input elements::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div><input type="checkbox"/></div>')
+            >>> d('input:checkbox')
+            [<input>]
+
+        ..
+        """
+        xpath.add_condition("@type = 'checkbox' and name(.) = 'input'")
+        return xpath
+
+    def xpath_password_pseudo(self, xpath):
+        """Matches all password input elements::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div><input type="password"/></div>')
+            >>> d('input:password')
+            [<input>]
+
+        ..
+        """
+        xpath.add_condition("@type = 'password' and name(.) = 'input'")
+        return xpath
+
+    def xpath_submit_pseudo(self, xpath):
+        """Matches all submit input elements::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div><input type="submit"/></div>')
+            >>> d('input:submit')
+            [<input>]
+
+        ..
+        """
+        xpath.add_condition("@type = 'submit' and name(.) = 'input'")
+        return xpath
+
+    def xpath_hidden_pseudo(self, xpath):
+        """Matches all hidden input elements::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div><input type="hidden"/></div>')
+            >>> d('input:hidden')
+            [<input>]
+
+        ..
+        """
+        xpath.add_condition("@type = 'hidden' and name(.) = 'input'")
+        return xpath
+
+    def xpath_image_pseudo(self, xpath):
+        """Matches all image input elements::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div><input type="image"/></div>')
+            >>> d('input:image')
+            [<input>]
+
+        ..
+        """
+        xpath.add_condition("@type = 'image' and name(.) = 'input'")
+        return xpath
+
+    def xpath_reset_pseudo(self, xpath):
+        """Matches all reset input elements::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div><input type="reset"/></div>')
+            >>> d('input:reset')
+            [<input>]
+
+        ..
+        """
+        xpath.add_condition("@type = 'reset' and name(.) = 'input'")
+        return xpath
+
+    def xpath_header_pseudo(self, xpath):
+        """Matches all header elelements (h1, ..., h6)::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div><h1>title</h1></div>')
+            >>> d(':header')
+            [<h1>]
+
+        ..
+        """
+        # this seems kind of brute-force, is there a better way?
+        xpath.add_condition((
+            "(name(.) = 'h1' or name(.) = 'h2' or name (.) = 'h3') "
+            "or (name(.) = 'h4' or name (.) = 'h5' or name(.) = 'h6')"))
+        return xpath
+
+    def xpath_parent_pseudo(self, xpath):
+        """Match all elements that contain other elements::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div><h1><span>title</span></h1><h1/></div>')
+            >>> d('h1:parent')
+            [<h1>]
+
+        ..
+        """
+        xpath.add_condition("count(child::*) > 0")
+        return xpath
+
+    def xpath_empty_pseudo(self, xpath):
+        """Match all elements that do not contain other elements::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div><h1><span>title</span></h1><h2/></div>')
+            >>> d(':empty')
+            [<h2>]
+
+        ..
+        """
+        xpath.add_condition("not(node())")
+        return xpath
+
+    def xpath_eq_function(self, xpath, function):
+        """Matches a single element by its index::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div><h1 class="first"/><h1 class="last"/></div>')
+            >>> d('h1:eq(0)')
+            [<h1.first>]
+            >>> d('h1:eq(1)')
+            [<h1.last>]
+
+        ..
+        """
+        if function.argument_types() != ['NUMBER']:
+            raise ExpressionError(
+                "Expected a single integer for :eq(), got %r" % (
+                    function.arguments,))
+        value = int(function.arguments[0].value)
+        xpath.add_post_condition('position() = %s' % (value + 1))
+        return xpath
+
+    def xpath_gt_function(self, xpath, function):
+        """Matches all elements with an index over the given one::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div><h1 class="first"/><h1 class="last"/></div>')
+            >>> d('h1:gt(0)')
+            [<h1.last>]
+
+        ..
+        """
+        if function.argument_types() != ['NUMBER']:
+            raise ExpressionError(
+                "Expected a single integer for :gt(), got %r" % (
+                    function.arguments,))
+        value = int(function.arguments[0].value)
+        xpath.add_post_condition('position() > %s' % (value + 1))
+        return xpath
+
+    def xpath_lt_function(self, xpath, function):
+        """Matches all elements with an index below the given one::
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div><h1 class="first"/><h1 class="last"/></div>')
+            >>> d('h1:lt(1)')
+            [<h1.first>]
+
+        ..
+        """
+        if function.argument_types() != ['NUMBER']:
+            raise ExpressionError(
+                "Expected a single integer for :gt(), got %r" % (
+                    function.arguments,))
+
+        value = int(function.arguments[0].value)
+        xpath.add_post_condition('position() < %s' % (value + 1))
+        return xpath
+
+    def xpath_contains_function(self, xpath, function):
+        """Matches all elements that contain the given text
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div><h1/><h1 class="title">title</h1></div>')
+            >>> d('h1:contains("title")')
+            [<h1.title>]
+
+        ..
+        """
+        if function.argument_types() not in (['STRING'], ['IDENT']):
+            raise ExpressionError(
+                "Expected a single string or ident for :contains(), got %r" % (
+                    function.arguments,))
+
+        value = self.xpath_literal(function.arguments[0].value)
+        xpath.add_post_condition('contains(., %s)' % value)
+        return xpath
+
+    def xpath_has_function(self, xpath, function):
+        """Matches elements which contain at least one element that matches
+        the specified selector. https://api.jquery.com/has-selector/
+
+            >>> from pyquery import PyQuery
+            >>> d = PyQuery('<div class="foo"><div class="bar"></div></div>')
+            >>> d('.foo:has(".baz")')
+            []
+            >>> d('.foo:has(".foo")')
+            []
+            >>> d('.foo:has(".bar")')
+            [<div.foo>]
+            >>> d('.foo:has(div)')
+            [<div.foo>]
+
+        ..
+        """
+        if function.argument_types() not in (['STRING'], ['IDENT']):
+            raise ExpressionError(
+                "Expected a single string or ident for :has(), got %r" % (
+                    function.arguments,))
+        value = self.css_to_xpath(
+            function.arguments[0].value, prefix='descendant::',
+        )
+        xpath.add_post_condition(value)
+        return xpath

=== added file 'pyquery/openers.py'
--- old/pyquery/openers.py	1970-01-01 00:00:00 +0000
+++ new/pyquery/openers.py	2021-04-05 08:32:53 +0000
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+from urllib.request import urlopen
+from urllib.parse import urlencode
+from urllib.error import HTTPError
+
+try:
+    import requests
+    HAS_REQUEST = True
+except ImportError:
+    HAS_REQUEST = False
+
+DEFAULT_TIMEOUT = 60
+
+basestring = (str, bytes)
+
+allowed_args = (
+    'auth', 'data', 'headers', 'verify',
+    'cert', 'config', 'hooks', 'proxies', 'cookies'
+)
+
+
+def _query(url, method, kwargs):
+    data = None
+    if 'data' in kwargs:
+        data = kwargs.pop('data')
+    if type(data) in (dict, list, tuple):
+        data = urlencode(data)
+
+    if isinstance(method, basestring) and \
+       method.lower() == 'get' and data:
+        if '?' not in url:
+            url += '?'
+        elif url[-1] not in ('?', '&'):
+            url += '&'
+        url += data
+        data = None
+
+    if data:
+        data = data.encode('utf-8')
+    return url, data
+
+
+def _requests(url, kwargs):
+
+    encoding = kwargs.get('encoding')
+    method = kwargs.get('method', 'get').lower()
+    session = kwargs.get('session')
+    if session:
+        meth = getattr(session, str(method))
+    else:
+        meth = getattr(requests, str(method))
+    if method == 'get':
+        url, data = _query(url, method, kwargs)
+    kw = {}
+    for k in allowed_args:
+        if k in kwargs:
+            kw[k] = kwargs[k]
+    resp = meth(url=url, timeout=kwargs.get('timeout', DEFAULT_TIMEOUT), **kw)
+    if not (200 <= resp.status_code < 300):
+        raise HTTPError(resp.url, resp.status_code,
+                        resp.reason, resp.headers, None)
+    if encoding:
+        resp.encoding = encoding
+    html = resp.text
+    return html
+
+
+def _urllib(url, kwargs):
+    method = kwargs.get('method')
+    url, data = _query(url, method, kwargs)
+    return urlopen(url, data, timeout=kwargs.get('timeout', DEFAULT_TIMEOUT))
+
+
+def url_opener(url, kwargs):
+    if HAS_REQUEST:
+        return _requests(url, kwargs)
+    return _urllib(url, kwargs)

=== added file 'pyquery/pyquery.py'
--- old/pyquery/pyquery.py	1970-01-01 00:00:00 +0000
+++ new/pyquery/pyquery.py	2021-04-05 08:32:53 +0000
@@ -0,0 +1,1677 @@
+# Copyright (C) 2008 - Olivier Lauzanne <olauzanne@gmail.com>
+#
+# Distributed under the BSD license, see LICENSE.txt
+from .cssselectpatch import JQueryTranslator
+from collections import OrderedDict
+from urllib.parse import urlencode
+from urllib.parse import urljoin
+from .openers import url_opener
+from .text import extract_text
+from copy import deepcopy
+from lxml import etree
+import lxml.html
+import inspect
+import itertools
+import types
+
+basestring = (str, bytes)
+
+
+def getargspec(func):
+    args = inspect.signature(func).parameters.values()
+    return [p.name for p in args
+            if p.kind == p.POSITIONAL_OR_KEYWORD]
+
+
+def with_camel_case_alias(func):
+    """decorator for methods who required a camelcase alias"""
+    _camel_case_aliases.add(func.__name__)
+    return func
+
+
+_camel_case_aliases = set()
+
+
+def build_camel_case_aliases(PyQuery):
+    """add camelcase aliases to PyQuery"""
+    for alias in _camel_case_aliases:
+        parts = list(alias.split('_'))
+        name = parts[0] + ''.join([p.title() for p in parts[1:]])
+        func = getattr(PyQuery, alias)
+        f = types.FunctionType(func.__code__, func.__globals__,
+                               name, func.__defaults__)
+        f.__doc__ = (
+            'Alias for :func:`~pyquery.pyquery.PyQuery.%s`') % func.__name__
+        setattr(PyQuery, name, f.__get__(None, PyQuery))
+
+
+def fromstring(context, parser=None, custom_parser=None):
+    """use html parser if we don't have clean xml
+    """
+    if hasattr(context, 'read') and hasattr(context.read, '__call__'):
+        meth = 'parse'
+    else:
+        meth = 'fromstring'
+    if custom_parser is None:
+        if parser is None:
+            try:
+                result = getattr(etree, meth)(context)
+            except etree.XMLSyntaxError:
+                if hasattr(context, 'seek'):
+                    context.seek(0)
+                result = getattr(lxml.html, meth)(context)
+            if isinstance(result, etree._ElementTree):
+                return [result.getroot()]
+            else:
+                return [result]
+        elif parser == 'xml':
+            custom_parser = getattr(etree, meth)
+        elif parser == 'html':
+            custom_parser = getattr(lxml.html, meth)
+        elif parser == 'html5':
+            from lxml.html import html5parser
+            custom_parser = getattr(html5parser, meth)
+        elif parser == 'soup':
+            from lxml.html import soupparser
+            custom_parser = getattr(soupparser, meth)
+        elif parser == 'html_fragments':
+            custom_parser = lxml.html.fragments_fromstring
+        else:
+            raise ValueError('No such parser: "%s"' % parser)
+
+    result = custom_parser(context)
+    if type(result) is list:
+        return result
+    elif isinstance(result, etree._ElementTree):
+        return [result.getroot()]
+    elif result is not None:
+        return [result]
+    else:
+        return []
+
+
+def callback(func, *args):
+    return func(*args[:func.__code__.co_argcount])
+
+
+class NoDefault(object):
+    def __repr__(self):
+        """clean representation in Sphinx"""
+        return '<NoDefault>'
+
+
+no_default = NoDefault()
+del NoDefault
+
+
+class FlexibleElement(object):
+    """property to allow a flexible api"""
+    def __init__(self, pget, pset=no_default, pdel=no_default):
+        self.pget = pget
+        self.pset = pset
+        self.pdel = pdel
+
+    def __get__(self, instance, klass):
+        class _element(object):
+            """real element to support set/get/del attr and item and js call
+            style"""
+            def __call__(prop, *args, **kwargs):
+                return self.pget(instance, *args, **kwargs)
+            __getattr__ = __getitem__ = __setattr__ = __setitem__ = __call__
+
+            def __delitem__(prop, name):
+                if self.pdel is not no_default:
+                    return self.pdel(instance, name)
+                else:
+                    raise NotImplementedError()
+            __delattr__ = __delitem__
+
+            def __repr__(prop):
+                return '<flexible_element %s>' % self.pget.__name__
+        return _element()
+
+    def __set__(self, instance, value):
+        if self.pset is not no_default:
+            self.pset(instance, value)
+        else:
+            raise NotImplementedError()
+
+
+class PyQuery(list):
+    """The main class
+    """
+
+    _translator_class = JQueryTranslator
+
+    def __init__(self, *args, **kwargs):
+        html = None
+        elements = []
+        self._base_url = None
+        self.parser = kwargs.pop('parser', None)
+
+        if (len(args) >= 1 and
+                isinstance(args[0], str) and
+                args[0].split('://', 1)[0] in ('http', 'https')):
+            kwargs['url'] = args[0]
+            if len(args) >= 2:
+                kwargs['data'] = args[1]
+            args = []
+
+        if 'parent' in kwargs:
+            self._parent = kwargs.pop('parent')
+        else:
+            self._parent = no_default
+
+        if 'css_translator' in kwargs:
+            self._translator = kwargs.pop('css_translator')
+        elif self.parser in ('xml',):
+            self._translator = self._translator_class(xhtml=True)
+        elif self._parent is not no_default:
+            self._translator = self._parent._translator
+        else:
+            self._translator = self._translator_class(xhtml=False)
+
+        self.namespaces = kwargs.pop('namespaces', None)
+
+        if kwargs:
+            # specific case to get the dom
+            if 'filename' in kwargs:
+                html = open(kwargs['filename'])
+            elif 'url' in kwargs:
+                url = kwargs.pop('url')
+                if 'opener' in kwargs:
+                    opener = kwargs.pop('opener')
+                    html = opener(url, **kwargs)
+                else:
+                    html = url_opener(url, kwargs)
+                if not self.parser:
+                    self.parser = 'html'
+                self._base_url = url
+            else:
+                raise ValueError('Invalid keyword arguments %s' % kwargs)
+
+            elements = fromstring(html, self.parser)
+            # close open descriptor if possible
+            if hasattr(html, 'close'):
+                try:
+                    html.close()
+                except Exception:
+                    pass
+
+        else:
+            # get nodes
+
+            # determine context and selector if any
+            selector = context = no_default
+            length = len(args)
+            if length == 1:
+                context = args[0]
+            elif length == 2:
+                selector, context = args
+            else:
+                raise ValueError(
+                    "You can't do that. Please, provide arguments")
+
+            # get context
+            if isinstance(context, basestring):
+                try:
+                    elements = fromstring(context, self.parser)
+                except Exception:
+                    raise
+            elif isinstance(context, self.__class__):
+                # copy
+                elements = context[:]
+            elif isinstance(context, list):
+                elements = context
+            elif isinstance(context, etree._Element):
+                elements = [context]
+            else:
+                raise TypeError(context)
+
+            # select nodes
+            if elements and selector is not no_default:
+                xpath = self._css_to_xpath(selector)
+                results = []
+                for tag in elements:
+                    results.extend(
+                        tag.xpath(xpath, namespaces=self.namespaces))
+                elements = results
+
+        list.__init__(self, elements)
+
+    def _css_to_xpath(self, selector, prefix='descendant-or-self::'):
+        selector = selector.replace('[@', '[')
+        return self._translator.css_to_xpath(selector, prefix)
+
+    def _copy(self, *args, **kwargs):
+        kwargs.setdefault('namespaces', self.namespaces)
+        return self.__class__(*args, **kwargs)
+
+    def __call__(self, *args, **kwargs):
+        """return a new PyQuery instance
+        """
+        length = len(args)
+        if length == 0:
+            raise ValueError('You must provide at least a selector')
+        if args[0] == '':
+            return self._copy([])
+        if (len(args) == 1 and
+                isinstance(args[0], str) and
+                not args[0].startswith('<')):
+            args += (self,)
+        result = self._copy(*args, parent=self, **kwargs)
+        return result
+
+    # keep original list api prefixed with _
+    _append = list.append
+    _extend = list.extend
+
+    # improve pythonic api
+    def __add__(self, other):
+        assert isinstance(other, self.__class__)
+        return self._copy(self[:] + other[:])
+
+    def extend(self, other):
+        """Extend with anoter PyQuery object"""
+        assert isinstance(other, self.__class__)
+        self._extend(other[:])
+        return self
+
+    def items(self, selector=None):
+        """Iter over elements. Return PyQuery objects:
+
+            >>> d = PyQuery('<div><span>foo</span><span>bar</span></div>')
+            >>> [i.text() for i in d.items('span')]
+            ['foo', 'bar']
+            >>> [i.text() for i in d('span').items()]
+            ['foo', 'bar']
+            >>> list(d.items('a')) == list(d('a').items())
+            True
+        """
+        if selector:
+            elems = self(selector) or []
+        else:
+            elems = self
+        for elem in elems:
+            yield self._copy(elem, parent=self)
+
+    def xhtml_to_html(self):
+        """Remove xhtml namespace:
+
+            >>> doc = PyQuery(
+            ...         '<html xmlns="http://www.w3.org/1999/xhtml"></html>')
+            >>> doc
+            [<{http://www.w3.org/1999/xhtml}html>]
+            >>> doc.xhtml_to_html()
+            [<html>]
+        """
+        try:
+            root = self[0].getroottree()
+        except IndexError:
+            pass
+        else:
+            lxml.html.xhtml_to_html(root)
+        return self
+
+    def remove_namespaces(self):
+        """Remove all namespaces:
+
+            >>> doc = PyQuery('<foo xmlns="http://example.com/foo"></foo>')
+            >>> doc
+            [<{http://example.com/foo}foo>]
+            >>> doc.remove_namespaces()
+            [<foo>]
+        """
+        try:
+            root = self[0].getroottree()
+        except IndexError:
+            pass
+        else:
+            for el in root.iter('{*}*'):
+                if el.tag.startswith('{'):
+                    el.tag = el.tag.split('}', 1)[1]
+        return self
+
+    def __str__(self):
+        """xml representation of current nodes::
+
+            >>> xml = PyQuery(
+            ...   '<script><![[CDATA[ ]></script>', parser='html_fragments')
+            >>> print(str(xml))
+            <script>&lt;![[CDATA[ ]&gt;</script>
+
+        """
+        return ''.join([etree.tostring(e, encoding=str) for e in self])
+
+    def __unicode__(self):
+        """xml representation of current nodes"""
+        return u''.join([etree.tostring(e, encoding=str)
+                         for e in self])
+
+    def __html__(self):
+        """html representation of current nodes::
+
+            >>> html = PyQuery(
+            ...   '<script><![[CDATA[ ]></script>', parser='html_fragments')
+            >>> print(html.__html__())
+            <script><![[CDATA[ ]></script>
+
+        """
+        return u''.join([lxml.html.tostring(e, encoding=str)
+                         for e in self])
+
+    def __repr__(self):
+        r = []
+        try:
+            for el in self:
+                c = el.get('class')
+                c = c and '.' + '.'.join(c.split(' ')) or ''
+                id = el.get('id')
+                id = id and '#' + id or ''
+                r.append('<%s%s%s>' % (el.tag, id, c))
+            return '[' + (', '.join(r)) + ']'
+        except AttributeError:
+            return list.__repr__(self)
+
+    @property
+    def root(self):
+        """return the xml root element
+        """
+        if self._parent is not no_default:
+            return self._parent[0].getroottree()
+        return self[0].getroottree()
+
+    @property
+    def encoding(self):
+        """return the xml encoding of the root element
+        """
+        root = self.root
+        if root is not None:
+            return self.root.docinfo.encoding
+
+    ##############
+    # Traversing #
+    ##############
+
+    def _filter_only(self, selector, elements, reverse=False, unique=False):
+        """Filters the selection set only, as opposed to also including
+           descendants.
+        """
+        if selector is None:
+            results = elements
+        else:
+            xpath = self._css_to_xpath(selector, 'self::')
+            results = []
+            for tag in elements:
+                results.extend(tag.xpath(xpath, namespaces=self.namespaces))
+        if reverse:
+            results.reverse()
+        if unique:
+            result_list = results
+            results = []
+            for item in result_list:
+                if item not in results:
+                    results.append(item)
+        return self._copy(results, parent=self)
+
+    def parent(self, selector=None):
+        return self._filter_only(
+            selector,
+            [e.getparent() for e in self if e.getparent() is not None],
+            unique=True)
+
+    def prev(self, selector=None):
+        return self._filter_only(
+            selector,
+            [e.getprevious() for e in self if e.getprevious() is not None])
+
+    def next(self, selector=None):
+        return self._filter_only(
+            selector,
+            [e.getnext() for e in self if e.getnext() is not None])
+
+    def _traverse(self, method):
+        for e in self:
+            current = getattr(e, method)()
+            while current is not None:
+                yield current
+                current = getattr(current, method)()
+
+    def _traverse_parent_topdown(self):
+        for e in self:
+            this_list = []
+            current = e.getparent()
+            while current is not None:
+                this_list.append(current)
+                current = current.getparent()
+            this_list.reverse()
+            for j in this_list:
+                yield j
+
+    def _next_all(self):
+        return [e for e in self._traverse('getnext')]
+
+    @with_camel_case_alias
+    def next_all(self, selector=None):
+        """
+        >>> h = '<span><p class="hello">Hi</p><p>Bye</p><img scr=""/></span>'
+        >>> d = PyQuery(h)
+        >>> d('p:last').next_all()
+        [<img>]
+        >>> d('p:last').nextAll()
+        [<img>]
+        """
+        return self._filter_only(selector, self._next_all())
+
+    @with_camel_case_alias
+    def next_until(self, selector, filter_=None):
+        """
+        >>> h = '''
+        ... <h2>Greeting 1</h2>
+        ... <p>Hello!</p><p>World!</p>
+        ... <h2>Greeting 2</h2><p>Bye!</p>
+        ... '''
+        >>> d = PyQuery(h)
+        >>> d('h2:first').nextUntil('h2')
+        [<p>, <p>]
+        """
+        return self._filter_only(
+            filter_, [
+                e
+                for q in itertools.takewhile(
+                    lambda q: not q.is_(selector), self.next_all().items())
+                for e in q
+            ]
+        )
+
+    def _prev_all(self):
+        return [e for e in self._traverse('getprevious')]
+
+    @with_camel_case_alias
+    def prev_all(self, selector=None):
+        """
+        >>> h = '<span><p class="hello">Hi</p><p>Bye</p><img scr=""/></span>'
+        >>> d = PyQuery(h)
+        >>> d('p:last').prev_all()
+        [<p.hello>]
+        >>> d('p:last').prevAll()
+        [<p.hello>]
+        """
+        return self._filter_only(selector, self._prev_all(), reverse=True)
+
+    def siblings(self, selector=None):
+        """
+         >>> h = '<span><p class="hello">Hi</p><p>Bye</p><img scr=""/></span>'
+         >>> d = PyQuery(h)
+         >>> d('.hello').siblings()
+         [<p>, <img>]
+         >>> d('.hello').siblings('img')
+         [<img>]
+
+        """
+        return self._filter_only(selector, self._prev_all() + self._next_all())
+
+    def parents(self, selector=None):
+        """
+        >>> d = PyQuery('<span><p class="hello">Hi</p><p>Bye</p></span>')
+        >>> d('p').parents()
+        [<span>]
+        >>> d('.hello').parents('span')
+        [<span>]
+        >>> d('.hello').parents('p')
+        []
+        """
+        return self._filter_only(
+            selector,
+            [e for e in self._traverse_parent_topdown()],
+            unique=True
+        )
+
+    def children(self, selector=None):
+        """Filter elements that are direct children of self using optional
+        selector:
+
+            >>> d = PyQuery('<span><p class="hello">Hi</p><p>Bye</p></span>')
+            >>> d
+            [<span>]
+            >>> d.children()
+            [<p.hello>, <p>]
+            >>> d.children('.hello')
+            [<p.hello>]
+        """
+        elements = [child for tag in self for child in tag.getchildren()]
+        return self._filter_only(selector, elements)
+
+    def closest(self, selector=None):
+        """
+        >>> d = PyQuery(
+        ...  '<div class="hello"><p>This is a '
+        ...  '<strong class="hello">test</strong></p></div>')
+        >>> d('strong').closest('div')
+        [<div.hello>]
+        >>> d('strong').closest('.hello')
+        [<strong.hello>]
+        >>> d('strong').closest('form')
+        []
+        """
+        result = []
+        for current in self:
+            while (current is not None and
+                    not self._copy(current).is_(selector)):
+                current = current.getparent()
+            if current is not None:
+                result.append(current)
+        return self._copy(result, parent=self)
+
+    def contents(self):
+        """
+        Return contents (with text nodes):
+
+            >>> d = PyQuery('hello <b>bold</b>')
+            >>> d.contents()  # doctest: +ELLIPSIS
+            ['hello ', <Element b at ...>]
+        """
+        results = []
+        for elem in self:
+            results.extend(elem.xpath('child::text()|child::*',
+                           namespaces=self.namespaces))
+        return self._copy(results, parent=self)
+
+    def filter(self, selector):
+        """Filter elements in self using selector (string or function):
+
+            >>> d = PyQuery('<p class="hello">Hi</p><p>Bye</p>')
+            >>> d('p')
+            [<p.hello>, <p>]
+            >>> d('p').filter('.hello')
+            [<p.hello>]
+            >>> d('p').filter(lambda i: i == 1)
+            [<p>]
+            >>> d('p').filter(lambda i: PyQuery(this).text() == 'Hi')
+            [<p.hello>]
+            >>> d('p').filter(lambda i, this: PyQuery(this).text() == 'Hi')
+            [<p.hello>]
+        """
+        if not hasattr(selector, '__call__'):
+            return self._filter_only(selector, self)
+        else:
+            elements = []
+            args = getargspec(callback)
+            try:
+                for i, this in enumerate(self):
+                    if len(args) == 1:
+                        selector.__globals__['this'] = this
+                    if callback(selector, i, this):
+                        elements.append(this)
+            finally:
+                f_globals = selector.__globals__
+                if 'this' in f_globals:
+                    del f_globals['this']
+            return self._copy(elements, parent=self)
+
+    def not_(self, selector):
+        """Return elements that don't match the given selector:
+
+            >>> d = PyQuery('<p class="hello">Hi</p><p>Bye</p><div></div>')
+            >>> d('p').not_('.hello')
+            [<p>]
+        """
+        exclude = set(self._copy(selector, self))
+        return self._copy([e for e in self if e not in exclude],
+                          parent=self)
+
+    def is_(self, selector):
+        """Returns True if selector matches at least one current element, else
+        False:
+
+            >>> d = PyQuery('<p class="hello"><span>Hi</span></p><p>Bye</p>')
+            >>> d('p').eq(0).is_('.hello')
+            True
+
+            >>> d('p').eq(0).is_('span')
+            False
+
+            >>> d('p').eq(1).is_('.hello')
+            False
+
+        ..
+        """
+        return bool(self._filter_only(selector, self))
+
+    def find(self, selector):
+        """Find elements using selector traversing down from self:
+
+            >>> m = '<p><span><em>Whoah!</em></span></p><p><em> there</em></p>'
+            >>> d = PyQuery(m)
+            >>> d('p').find('em')
+            [<em>, <em>]
+            >>> d('p').eq(1).find('em')
+            [<em>]
+        """
+        xpath = self._css_to_xpath(selector)
+        results = [child.xpath(xpath, namespaces=self.namespaces)
+                   for tag in self
+                   for child in tag.getchildren()]
+        # Flatten the results
+        elements = []
+        for r in results:
+            elements.extend(r)
+        return self._copy(elements, parent=self)
+
+    def eq(self, index):
+        """Return PyQuery of only the element with the provided index::
+
+            >>> d = PyQuery('<p class="hello">Hi</p><p>Bye</p><div></div>')
+            >>> d('p').eq(0)
+            [<p.hello>]
+            >>> d('p').eq(1)
+            [<p>]
+            >>> d('p').eq(2)
+            []
+
+        ..
+        """
+        # Slicing will return empty list when index=-1
+        # we should handle out of bound by ourselves
+        try:
+            items = self[index]
+        except IndexError:
+            items = []
+        return self._copy(items, parent=self)
+
+    def each(self, func):
+        """apply func on each nodes
+        """
+        try:
+            for i, element in enumerate(self):
+                func.__globals__['this'] = element
+                if callback(func, i, element) is False:
+                    break
+        finally:
+            f_globals = func.__globals__
+            if 'this' in f_globals:
+                del f_globals['this']
+        return self
+
+    def map(self, func):
+        """Returns a new PyQuery after transforming current items with func.
+
+        func should take two arguments - 'index' and 'element'.  Elements can
+        also be referred to as 'this' inside of func::
+
+            >>> d = PyQuery('<p class="hello">Hi there</p><p>Bye</p><br />')
+            >>> d('p').map(lambda i, e: PyQuery(e).text())
+            ['Hi there', 'Bye']
+
+            >>> d('p').map(lambda i, e: len(PyQuery(this).text()))
+            [8, 3]
+
+            >>> d('p').map(lambda i, e: PyQuery(this).text().split())
+            ['Hi', 'there', 'Bye']
+
+        """
+        items = []
+        try:
+            for i, element in enumerate(self):
+                func.__globals__['this'] = element
+                result = callback(func, i, element)
+                if result is not None:
+                    if not isinstance(result, list):
+                        items.append(result)
+                    else:
+                        items.extend(result)
+        finally:
+            f_globals = func.__globals__
+            if 'this' in f_globals:
+                del f_globals['this']
+        return self._copy(items, parent=self)
+
+    @property
+    def length(self):
+        return len(self)
+
+    def size(self):
+        return len(self)
+
+    def end(self):
+        """Break out of a level of traversal and return to the parent level.
+
+            >>> m = '<p><span><em>Whoah!</em></span></p><p><em> there</em></p>'
+            >>> d = PyQuery(m)
+            >>> d('p').eq(1).find('em').end().end()
+            [<p>, <p>]
+        """
+        return self._parent
+
+    ##############
+    # Attributes #
+    ##############
+    def attr(self, *args, **kwargs):
+        """Attributes manipulation
+        """
+
+        mapping = {'class_': 'class', 'for_': 'for'}
+
+        attr = value = no_default
+        length = len(args)
+        if length == 1:
+            attr = args[0]
+            attr = mapping.get(attr, attr)
+        elif length == 2:
+            attr, value = args
+            attr = mapping.get(attr, attr)
+        elif kwargs:
+            attr = {}
+            for k, v in kwargs.items():
+                attr[mapping.get(k, k)] = v
+        else:
+            raise ValueError('Invalid arguments %s %s' % (args, kwargs))
+
+        if not self:
+            return None
+        elif isinstance(attr, dict):
+            for tag in self:
+                for key, value in attr.items():
+                    tag.set(key, value)
+        elif value is no_default:
+            return self[0].get(attr)
+        elif value is None:
+            return self.remove_attr(attr)
+        else:
+            for tag in self:
+                tag.set(attr, value)
+        return self
+
+    @with_camel_case_alias
+    def remove_attr(self, name):
+        """Remove an attribute::
+
+            >>> d = PyQuery('<div id="myid"></div>')
+            >>> d.remove_attr('id')
+            [<div>]
+            >>> d.removeAttr('id')
+            [<div>]
+
+        ..
+        """
+        for tag in self:
+            try:
+                del tag.attrib[name]
+            except KeyError:
+                pass
+        return self
+
+    attr = FlexibleElement(pget=attr, pdel=remove_attr)
+
+    #######
+    # CSS #
+    #######
+    def height(self, value=no_default):
+        """set/get height of element
+        """
+        return self.attr('height', value)
+
+    def width(self, value=no_default):
+        """set/get width of element
+        """
+        return self.attr('width', value)
+
+    @with_camel_case_alias
+    def has_class(self, name):
+        """Return True if element has class::
+
+            >>> d = PyQuery('<div class="myclass"></div>')
+            >>> d.has_class('myclass')
+            True
+            >>> d.hasClass('myclass')
+            True
+
+        ..
+        """
+        return self.is_('.%s' % name)
+
+    @with_camel_case_alias
+    def add_class(self, value):
+        """Add a css class to elements::
+
+            >>> d = PyQuery('<div></div>')
+            >>> d.add_class('myclass')
+            [<div.myclass>]
+            >>> d.addClass('myclass')
+            [<div.myclass>]
+
+        ..
+        """
+        for tag in self:
+            values = value.split(' ')
+            classes = (tag.get('class') or '').split()
+            classes += [v for v in values if v not in classes]
+            tag.set('class', ' '.join(classes))
+        return self
+
+    @with_camel_case_alias
+    def remove_class(self, value):
+        """Remove a css class to elements::
+
+            >>> d = PyQuery('<div class="myclass"></div>')
+            >>> d.remove_class('myclass')
+            [<div>]
+            >>> d.removeClass('myclass')
+            [<div>]
+
+        ..
+        """
+        for tag in self:
+            values = value.split(' ')
+            classes = set((tag.get('class') or '').split())
+            classes.difference_update(values)
+            classes.difference_update([''])
+            classes = ' '.join(classes)
+            if classes.strip():
+                tag.set('class', classes)
+            elif tag.get('class'):
+                tag.set('class', classes)
+        return self
+
+    @with_camel_case_alias
+    def toggle_class(self, value):
+        """Toggle a css class to elements
+
+            >>> d = PyQuery('<div></div>')
+            >>> d.toggle_class('myclass')
+            [<div.myclass>]
+            >>> d.toggleClass('myclass')
+            [<div>]
+
+        """
+        for tag in self:
+            values = value.split(' ')
+            classes = (tag.get('class') or '').split()
+            values_to_add = [v for v in values if v not in classes]
+            values_to_del = [v for v in values if v in classes]
+            classes = [v for v in classes if v not in values_to_del]
+            classes += values_to_add
+            tag.set('class', ' '.join(classes))
+        return self
+
+    def css(self, *args, **kwargs):
+        """css attributes manipulation
+        """
+
+        attr = value = no_default
+        length = len(args)
+        if length == 1:
+            attr = args[0]
+        elif length == 2:
+            attr, value = args
+        elif kwargs:
+            attr = kwargs
+        else:
+            raise ValueError('Invalid arguments %s %s' % (args, kwargs))
+
+        if isinstance(attr, dict):
+            for tag in self:
+                stripped_keys = [key.strip().replace('_', '-')
+                                 for key in attr.keys()]
+                current = [el.strip()
+                           for el in (tag.get('style') or '').split(';')
+                           if el.strip()
+                           and not el.split(':')[0].strip() in stripped_keys]
+                for key, value in attr.items():
+                    key = key.replace('_', '-')
+                    current.append('%s: %s' % (key, value))
+                tag.set('style', '; '.join(current))
+        elif isinstance(value, basestring):
+            attr = attr.replace('_', '-')
+            for tag in self:
+                current = [
+                    el.strip()
+                    for el in (tag.get('style') or '').split(';')
+                    if (el.strip() and
+                        not el.split(':')[0].strip() == attr.strip())]
+                current.append('%s: %s' % (attr, value))
+                tag.set('style', '; '.join(current))
+        return self
+
+    css = FlexibleElement(pget=css, pset=css)
+
+    ###################
+    # CORE UI EFFECTS #
+    ###################
+    def hide(self):
+        """Remove display:none to elements style:
+
+            >>> print(PyQuery('<div style="display:none;"/>').hide())
+            <div style="display: none"/>
+
+        """
+        return self.css('display', 'none')
+
+    def show(self):
+        """Add display:block to elements style:
+
+            >>> print(PyQuery('<div />').show())
+            <div style="display: block"/>
+
+        """
+        return self.css('display', 'block')
+
+    ########
+    # HTML #
+    ########
+    def val(self, value=no_default):
+        """Set the attribute value::
+
+            >>> d = PyQuery('<input />')
+            >>> d.val('Youhou')
+            [<input>]
+
+        Get the attribute value::
+
+            >>> d.val()
+            'Youhou'
+
+        Set the selected values for a `select` element with the `multiple`
+        attribute::
+
+            >>> d = PyQuery('''
+            ...             <select multiple>
+            ...                 <option value="you"><option value="hou">
+            ...             </select>
+            ...             ''')
+            >>> d.val(['you', 'hou'])
+            [<select>]
+
+        Get the selected values for a `select` element with the `multiple`
+        attribute::
+
+            >>> d.val()
+            ['you', 'hou']
+
+        """
+        def _get_value(tag):
+            # <textarea>
+            if tag.tag == 'textarea':
+                return self._copy(tag).html()
+            # <select>
+            elif tag.tag == 'select':
+                if 'multiple' in tag.attrib:
+                    # Only extract value if selected
+                    selected = self._copy(tag)('option[selected]')
+                    # Rebuild list to avoid serialization error
+                    return list(selected.map(
+                        lambda _, o: self._copy(o).attr('value')
+                    ))
+                selected_option = self._copy(tag)('option[selected]:last')
+                if selected_option:
+                    return selected_option.attr('value')
+                else:
+                    return self._copy(tag)('option').attr('value')
+            # <input type="checkbox"> or <input type="radio">
+            elif self.is_(':checkbox,:radio'):
+                val = self._copy(tag).attr('value')
+                if val is None:
+                    return 'on'
+                else:
+                    return val
+            # <input>
+            elif tag.tag == 'input':
+                val = self._copy(tag).attr('value')
+                return val.replace('\n', '') if val else ''
+            # everything else.
+            return self._copy(tag).attr('value') or ''
+
+        def _set_value(pq, value):
+            for tag in pq:
+                # <select>
+                if tag.tag == 'select':
+                    if not isinstance(value, list):
+                        value = [value]
+
+                    def _make_option_selected(_, elem):
+                        pq = self._copy(elem)
+                        if pq.attr('value') in value:
+                            pq.attr('selected', 'selected')
+                            if 'multiple' not in tag.attrib:
+                                del value[:]  # Ensure it toggles first match
+                        else:
+                            pq.removeAttr('selected')
+
+                    self._copy(tag)('option').each(_make_option_selected)
+                    continue
+                # Stringify array
+                if isinstance(value, list):
+                    value = ','.join(value)
+                # <textarea>
+                if tag.tag == 'textarea':
+                    self._copy(tag).text(value)
+                    continue
+                # <input> and everything else.
+                self._copy(tag).attr('value', value)
+
+        if value is no_default:
+            if len(self):
+                return _get_value(self[0])
+        else:
+            _set_value(self, value)
+            return self
+
+    def html(self, value=no_default, **kwargs):
+        """Get or set the html representation of sub nodes.
+
+        Get the text value::
+
+            >>> d = PyQuery('<div><span>toto</span></div>')
+            >>> print(d.html())
+            <span>toto</span>
+
+        Extra args are passed to ``lxml.etree.tostring``::
+
+            >>> d = PyQuery('<div><span></span></div>')
+            >>> print(d.html())
+            <span/>
+            >>> print(d.html(method='html'))
+            <span></span>
+
+        Set the text value::
+
+            >>> d.html('<span>Youhou !</span>')
+            [<div>]
+            >>> print(d)
+            <div><span>Youhou !</span></div>
+        """
+        if value is no_default:
+            if not self:
+                return None
+            tag = self[0]
+            children = tag.getchildren()
+            if not children:
+                return tag.text or ''
+            html = tag.text or ''
+            if 'encoding' not in kwargs:
+                kwargs['encoding'] = str
+            html += u''.join([etree.tostring(e, **kwargs)
+                              for e in children])
+            return html
+        else:
+            if isinstance(value, self.__class__):
+                new_html = str(value)
+            elif isinstance(value, basestring):
+                new_html = value
+            elif not value:
+                new_html = ''
+            else:
+                raise ValueError(type(value))
+
+            for tag in self:
+                for child in tag.getchildren():
+                    tag.remove(child)
+                root = fromstring(
+                    u'<root>' + new_html + u'</root>',
+                    self.parser)[0]
+                children = root.getchildren()
+                if children:
+                    tag.extend(children)
+                tag.text = root.text
+        return self
+
+    @with_camel_case_alias
+    def outer_html(self, method="html"):
+        """Get the html representation of the first selected element::
+
+            >>> d = PyQuery('<div><span class="red">toto</span> rocks</div>')
+            >>> print(d('span'))
+            <span class="red">toto</span> rocks
+            >>> print(d('span').outer_html())
+            <span class="red">toto</span>
+            >>> print(d('span').outerHtml())
+            <span class="red">toto</span>
+
+            >>> S = PyQuery('<p>Only <b>me</b> & myself</p>')
+            >>> print(S('b').outer_html())
+            <b>me</b>
+
+        ..
+        """
+
+        if not self:
+            return None
+        e0 = self[0]
+        if e0.tail:
+            e0 = deepcopy(e0)
+            e0.tail = ''
+        return etree.tostring(e0, encoding=str, method=method)
+
+    def text(self, value=no_default, **kwargs):
+        """Get or set the text representation of sub nodes.
+
+        Get the text value::
+
+            >>> doc = PyQuery('<div><span>toto</span><span>tata</span></div>')
+            >>> print(doc.text())
+            tototata
+            >>> doc = PyQuery('''<div><span>toto</span>
+            ...               <span>tata</span></div>''')
+            >>> print(doc.text())
+            toto tata
+
+        Get the text value, without squashing newlines::
+
+            >>> doc = PyQuery('''<div><span>toto</span>
+            ...               <span>tata</span></div>''')
+            >>> print(doc.text(squash_space=False))
+            toto
+            tata
+
+        Set the text value::
+
+            >>> doc.text('Youhou !')
+            [<div>]
+            >>> print(doc)
+            <div>Youhou !</div>
+
+        """
+
+        if value is no_default:
+            if not self:
+                return ''
+            return ' '.join(
+                self._copy(tag).html() if tag.tag == 'textarea' else
+                extract_text(tag, **kwargs) for tag in self
+            )
+
+        for tag in self:
+            for child in tag.getchildren():
+                tag.remove(child)
+            tag.text = value
+        return self
+
+    ################
+    # Manipulating #
+    ################
+
+    def _get_root(self, value):
+        if isinstance(value, basestring):
+            root = fromstring(u'<root>' + value + u'</root>',
+                              self.parser)[0]
+        elif isinstance(value, etree._Element):
+            root = self._copy(value)
+        elif isinstance(value, PyQuery):
+            root = value
+        else:
+            raise TypeError(
+                'Value must be string, PyQuery or Element. Got %r' % value)
+        if hasattr(root, 'text') and isinstance(root.text, basestring):
+            root_text = root.text
+        else:
+            root_text = ''
+        return root, root_text
+
+    def append(self, value):
+        """append value to each nodes
+        """
+        root, root_text = self._get_root(value)
+        for i, tag in enumerate(self):
+            if len(tag) > 0:  # if the tag has children
+                last_child = tag[-1]
+                if not last_child.tail:
+                    last_child.tail = ''
+                last_child.tail += root_text
+            else:
+                if not tag.text:
+                    tag.text = ''
+                tag.text += root_text
+            if i > 0:
+                root = deepcopy(list(root))
+            tag.extend(root)
+        return self
+
+    @with_camel_case_alias
+    def append_to(self, value):
+        """append nodes to value
+        """
+        value.append(self)
+        return self
+
+    def prepend(self, value):
+        """prepend value to nodes
+        """
+        root, root_text = self._get_root(value)
+        for i, tag in enumerate(self):
+            if not tag.text:
+                tag.text = ''
+            if len(root) > 0:
+                root[-1].tail = tag.text
+                tag.text = root_text
+            else:
+                tag.text = root_text + tag.text
+            if i > 0:
+                root = deepcopy(list(root))
+            tag[:0] = root
+            root = tag[:len(root)]
+        return self
+
+    @with_camel_case_alias
+    def prepend_to(self, value):
+        """prepend nodes to value
+        """
+        value.prepend(self)
+        return self
+
+    def after(self, value):
+        """add value after nodes
+        """
+        root, root_text = self._get_root(value)
+        for i, tag in enumerate(self):
+            if not tag.tail:
+                tag.tail = ''
+            tag.tail += root_text
+            if i > 0:
+                root = deepcopy(list(root))
+            parent = tag.getparent()
+            index = parent.index(tag) + 1
+            parent[index:index] = root
+            root = parent[index:len(root)]
+        return self
+
+    @with_camel_case_alias
+    def insert_after(self, value):
+        """insert nodes after value
+        """
+        value.after(self)
+        return self
+
+    def before(self, value):
+        """insert value before nodes
+        """
+        root, root_text = self._get_root(value)
+        for i, tag in enumerate(self):
+            previous = tag.getprevious()
+            if previous is not None:
+                if not previous.tail:
+                    previous.tail = ''
+                previous.tail += root_text
+            else:
+                parent = tag.getparent()
+                if not parent.text:
+                    parent.text = ''
+                parent.text += root_text
+            if i > 0:
+                root = deepcopy(list(root))
+            parent = tag.getparent()
+            index = parent.index(tag)
+            parent[index:index] = root
+            root = parent[index:len(root)]
+        return self
+
+    @with_camel_case_alias
+    def insert_before(self, value):
+        """insert nodes before value
+        """
+        value.before(self)
+        return self
+
+    def wrap(self, value):
+        """A string of HTML that will be created on the fly and wrapped around
+        each target:
+
+            >>> d = PyQuery('<span>youhou</span>')
+            >>> d.wrap('<div></div>')
+            [<div>]
+            >>> print(d)
+            <div><span>youhou</span></div>
+
+        """
+        assert isinstance(value, basestring)
+        value = fromstring(value)[0]
+        nodes = []
+        for tag in self:
+            wrapper = deepcopy(value)
+            # FIXME: using iterchildren is probably not optimal
+            if not wrapper.getchildren():
+                wrapper.append(deepcopy(tag))
+            else:
+                childs = [c for c in wrapper.iterchildren()]
+                child = childs[-1]
+                child.append(deepcopy(tag))
+            nodes.append(wrapper)
+
+            parent = tag.getparent()
+            if parent is not None:
+                for t in parent.iterchildren():
+                    if t is tag:
+                        t.addnext(wrapper)
+                        parent.remove(t)
+                        break
+        self[:] = nodes
+        return self
+
+    @with_camel_case_alias
+    def wrap_all(self, value):
+        """Wrap all the elements in the matched set into a single wrapper
+        element::
+
+            >>> d = PyQuery('<div><span>Hey</span><span>you !</span></div>')
+            >>> print(d('span').wrap_all('<div id="wrapper"></div>'))
+            <div id="wrapper"><span>Hey</span><span>you !</span></div>
+
+            >>> d = PyQuery('<div><span>Hey</span><span>you !</span></div>')
+            >>> print(d('span').wrapAll('<div id="wrapper"></div>'))
+            <div id="wrapper"><span>Hey</span><span>you !</span></div>
+
+        ..
+        """
+        if not self:
+            return self
+
+        assert isinstance(value, basestring)
+        value = fromstring(value)[0]
+        wrapper = deepcopy(value)
+        if not wrapper.getchildren():
+            child = wrapper
+        else:
+            childs = [c for c in wrapper.iterchildren()]
+            child = childs[-1]
+
+        replace_childs = True
+        parent = self[0].getparent()
+        if parent is None:
+            parent = no_default
+
+        # add nodes to wrapper and check parent
+        for tag in self:
+            child.append(deepcopy(tag))
+            if tag.getparent() is not parent:
+                replace_childs = False
+
+        # replace nodes i parent if possible
+        if parent is not no_default and replace_childs:
+            childs = [c for c in parent.iterchildren()]
+            if len(childs) == len(self):
+                for tag in self:
+                    parent.remove(tag)
+                parent.append(wrapper)
+
+        self[:] = [wrapper]
+        return self
+
+    @with_camel_case_alias
+    def replace_with(self, value):
+        """replace nodes by value:
+
+            >>> doc = PyQuery("<html><div /></html>")
+            >>> node = PyQuery("<span />")
+            >>> child = doc.find('div')
+            >>> child.replace_with(node)
+            [<div>]
+            >>> print(doc)
+            <html><span/></html>
+
+        """
+        if isinstance(value, PyQuery):
+            value = str(value)
+        if hasattr(value, '__call__'):
+            for i, element in enumerate(self):
+                self._copy(element).before(
+                    value(i, element) + (element.tail or ''))
+                parent = element.getparent()
+                parent.remove(element)
+        else:
+            for tag in self:
+                self._copy(tag).before(value + (tag.tail or ''))
+                parent = tag.getparent()
+                parent.remove(tag)
+        return self
+
+    @with_camel_case_alias
+    def replace_all(self, expr):
+        """replace nodes by expr
+        """
+        if self._parent is no_default:
+            raise ValueError(
+                'replaceAll can only be used with an object with parent')
+        self._parent(expr).replace_with(self)
+        return self
+
+    def clone(self):
+        """return a copy of nodes
+        """
+        return PyQuery([deepcopy(tag) for tag in self])
+
+    def empty(self):
+        """remove nodes content
+        """
+        for tag in self:
+            tag.text = None
+            tag[:] = []
+        return self
+
+    def remove(self, expr=no_default):
+        """Remove nodes:
+
+             >>> h = (
+             ... '<div>Maybe <em>she</em> does <strong>NOT</strong> know</div>'
+             ... )
+             >>> d = PyQuery(h)
+             >>> d('strong').remove()
+             [<strong>]
+             >>> print(d)
+             <div>Maybe <em>she</em> does   know</div>
+        """
+        if expr is no_default:
+            for tag in self:
+                parent = tag.getparent()
+                if parent is not None:
+                    if tag.tail:
+                        prev = tag.getprevious()
+                        if prev is None:
+                            if not parent.text:
+                                parent.text = ''
+                            parent.text += ' ' + tag.tail
+                        else:
+                            if not prev.tail:
+                                prev.tail = ''
+                            prev.tail += ' ' + tag.tail
+                    parent.remove(tag)
+        else:
+            results = self._copy(expr, self)
+            results.remove()
+        return self
+
+    class Fn(object):
+        """Hook for defining custom function (like the jQuery.fn):
+
+        .. sourcecode:: python
+
+         >>> fn = lambda: this.map(lambda i, el: PyQuery(this).outerHtml())
+         >>> PyQuery.fn.listOuterHtml = fn
+         >>> S = PyQuery(
+         ...   '<ol>   <li>Coffee</li>   <li>Tea</li>   <li>Milk</li>   </ol>')
+         >>> S('li').listOuterHtml()
+         ['<li>Coffee</li>', '<li>Tea</li>', '<li>Milk</li>']
+
+        """
+        def __setattr__(self, name, func):
+            def fn(self, *args, **kwargs):
+                func.__globals__['this'] = self
+                return func(*args, **kwargs)
+            fn.__name__ = name
+            setattr(PyQuery, name, fn)
+    fn = Fn()
+
+    ########
+    # AJAX #
+    ########
+
+    @with_camel_case_alias
+    def serialize_array(self):
+        """Serialize form elements as an array of dictionaries, whose structure
+        mirrors that produced by the jQuery API. Notably, it does not handle
+        the deprecated `keygen` form element.
+
+            >>> d = PyQuery('<form><input name="order" value="spam"></form>')
+            >>> d.serialize_array() == [{'name': 'order', 'value': 'spam'}]
+            True
+            >>> d.serializeArray() == [{'name': 'order', 'value': 'spam'}]
+            True
+        """
+        return list(map(
+            lambda p: {'name': p[0], 'value': p[1]},
+            self.serialize_pairs()
+        ))
+
+    def serialize(self):
+        """Serialize form elements as a URL-encoded string.
+
+            >>> h = (
+            ... '<form><input name="order" value="spam">'
+            ... '<input name="order2" value="baked beans"></form>'
+            ... )
+            >>> d = PyQuery(h)
+            >>> d.serialize()
+            'order=spam&order2=baked%20beans'
+        """
+        return urlencode(self.serialize_pairs()).replace('+', '%20')
+
+    #####################################################
+    # Additional methods that are not in the jQuery API #
+    #####################################################
+
+    @with_camel_case_alias
+    def serialize_pairs(self):
+        """Serialize form elements as an array of 2-tuples conventional for
+        typical URL-parsing operations in Python.
+
+            >>> d = PyQuery('<form><input name="order" value="spam"></form>')
+            >>> d.serialize_pairs()
+            [('order', 'spam')]
+            >>> d.serializePairs()
+            [('order', 'spam')]
+        """
+        # https://github.com/jquery/jquery/blob
+        # /2d4f53416e5f74fa98e0c1d66b6f3c285a12f0ce/src/serialize.js#L14
+        _submitter_types = ['submit', 'button', 'image', 'reset', 'file']
+
+        controls = self._copy([])
+        # Expand list of form controls
+        for el in self.items():
+            if el[0].tag == 'form':
+                form_id = el.attr('id')
+                if form_id:
+                    # Include inputs outside of their form owner
+                    root = self._copy(el.root.getroot())
+                    controls.extend(root(
+                        '#%s :not([form]):input, [form="%s"]:input'
+                        % (form_id, form_id)))
+                else:
+                    controls.extend(el(':not([form]):input'))
+            elif el[0].tag == 'fieldset':
+                controls.extend(el(':input'))
+            else:
+                controls.extend(el)
+        # Filter controls
+        selector = '[name]:enabled:not(button)'  # Not serializing image button
+        selector += ''.join(map(
+            lambda s: ':not([type="%s"])' % s,
+            _submitter_types))
+        controls = controls.filter(selector)
+
+        def _filter_out_unchecked(_, el):
+            el = controls._copy(el)
+            return not el.is_(':checkbox:not(:checked)') and \
+                not el.is_(':radio:not(:checked)')
+        controls = controls.filter(_filter_out_unchecked)
+
+        # jQuery serializes inputs with the datalist element as an ancestor
+        # contrary to WHATWG spec as of August 2018
+        #
+        # xpath = 'self::*[not(ancestor::datalist)]'
+        # results = []
+        # for tag in controls:
+        #     results.extend(tag.xpath(xpath, namespaces=controls.namespaces))
+        # controls = controls._copy(results)
+
+        # Serialize values
+        ret = []
+        for field in controls:
+            val = self._copy(field).val()
+            if isinstance(val, list):
+                ret.extend(map(
+                    lambda v: (field.attrib['name'], v.replace('\n', '\r\n')),
+                    val
+                ))
+            else:
+                ret.append((field.attrib['name'], val.replace('\n', '\r\n')))
+        return ret
+
+    @with_camel_case_alias
+    def serialize_dict(self):
+        """Serialize form elements as an ordered dictionary. Multiple values
+        corresponding to the same input name are concatenated into one list.
+
+            >>> d = PyQuery('''<form>
+            ...             <input name="order" value="spam">
+            ...             <input name="order" value="eggs">
+            ...             <input name="order2" value="ham">
+            ...             </form>''')
+            >>> d.serialize_dict()
+            OrderedDict([('order', ['spam', 'eggs']), ('order2', 'ham')])
+            >>> d.serializeDict()
+            OrderedDict([('order', ['spam', 'eggs']), ('order2', 'ham')])
+        """
+        ret = OrderedDict()
+        for name, val in self.serialize_pairs():
+            if name not in ret:
+                ret[name] = val
+            elif not isinstance(ret[name], list):
+                ret[name] = [ret[name], val]
+            else:
+                ret[name].append(val)
+        return ret
+
+    @property
+    def base_url(self):
+        """Return the url of current html document or None if not available.
+        """
+        if self._base_url is not None:
+            return self._base_url
+        if self._parent is not no_default:
+            return self._parent.base_url
+
+    def make_links_absolute(self, base_url=None):
+        """Make all links absolute.
+        """
+        if base_url is None:
+            base_url = self.base_url
+            if base_url is None:
+                raise ValueError((
+                    'You need a base URL to make your links'
+                    'absolute. It can be provided by the base_url parameter.'))
+
+        def repl(attr):
+            def rep(i, e):
+                attr_value = self(e).attr(attr)
+                # when label hasn't such attr, pass
+                if attr_value is None:
+                    return None
+
+                # skip specific "protocol" schemas
+                if any(attr_value.startswith(schema)
+                       for schema in ('tel:', 'callto:', 'sms:')):
+                    return None
+
+                return self(e).attr(attr,
+                                    urljoin(base_url, attr_value.strip()))
+            return rep
+
+        self('a').each(repl('href'))
+        self('link').each(repl('href'))
+        self('script').each(repl('src'))
+        self('img').each(repl('src'))
+        self('iframe').each(repl('src'))
+        self('form').each(repl('action'))
+
+        return self
+
+
+build_camel_case_aliases(PyQuery)

=== added file 'pyquery/text.py'
--- old/pyquery/text.py	1970-01-01 00:00:00 +0000
+++ new/pyquery/text.py	2021-04-05 08:32:53 +0000
@@ -0,0 +1,111 @@
+import re
+
+
+# https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements#Elements
+INLINE_TAGS = {
+    'a', 'abbr', 'acronym', 'b', 'bdo', 'big', 'br', 'button', 'cite',
+    'code', 'dfn', 'em', 'i', 'img', 'input', 'kbd', 'label', 'map',
+    'object', 'q', 'samp', 'script', 'select', 'small', 'span', 'strong',
+    'sub', 'sup', 'textarea', 'time', 'tt', 'var'
+}
+
+SEPARATORS = {'br'}
+
+
+# Definition of whitespace in HTML:
+# https://www.w3.org/TR/html4/struct/text.html#h-9.1
+WHITESPACE_RE = re.compile(u'[\x20\x09\x0C\u200B\x0A\x0D]+')
+
+
+def squash_html_whitespace(text):
+    # use raw extract_text for preformatted content (like <pre> content or set
+    # by CSS rules)
+    # apply this function on top of
+    return WHITESPACE_RE.sub(' ', text)
+
+
+def _squash_artifical_nl(parts):
+    output, last_nl = [], False
+    for x in parts:
+        if x is not None:
+            output.append(x)
+            last_nl = False
+        elif not last_nl:
+            output.append(None)
+            last_nl = True
+    return output
+
+
+def _strip_artifical_nl(parts):
+    if not parts:
+        return parts
+    for start_idx, pt in enumerate(parts):
+        if isinstance(pt, str):
+            # 0, 1, 2, index of first string [start_idx:...
+            break
+    iterator = enumerate(parts[:start_idx - 1 if start_idx > 0 else None:-1])
+    for end_idx, pt in iterator:
+        if isinstance(pt, str):  # 0=None, 1=-1, 2=-2, index of last string
+            break
+    return parts[start_idx:-end_idx if end_idx > 0 else None]
+
+
+def _merge_original_parts(parts):
+    output, orp_buf = [], []
+
+    def flush():
+        if orp_buf:
+            item = squash_html_whitespace(''.join(orp_buf)).strip()
+            if item:
+                output.append(item)
+            orp_buf[:] = []
+
+    for x in parts:
+        if not isinstance(x, str):
+            flush()
+            output.append(x)
+        else:
+            orp_buf.append(x)
+    flush()
+    return output
+
+
+def extract_text_array(dom, squash_artifical_nl=True, strip_artifical_nl=True):
+    if callable(dom.tag):
+        return ''
+    r = []
+    if dom.tag in SEPARATORS:
+        r.append(True)  # equivalent of '\n' used to designate separators
+    elif dom.tag not in INLINE_TAGS:
+        # equivalent of '\n' used to designate artifically inserted newlines
+        r.append(None)
+    if dom.text is not None:
+        r.append(dom.text)
+    for child in dom.getchildren():
+        r.extend(extract_text_array(child, squash_artifical_nl=False,
+                                    strip_artifical_nl=False))
+        if child.tail is not None:
+            r.append(child.tail)
+    if dom.tag not in INLINE_TAGS and dom.tag not in SEPARATORS:
+        # equivalent of '\n' used to designate artifically inserted newlines
+        r.append(None)
+    if squash_artifical_nl:
+        r = _squash_artifical_nl(r)
+    if strip_artifical_nl:
+        r = _strip_artifical_nl(r)
+    return r
+
+
+def extract_text(dom, block_symbol='\n', sep_symbol='\n', squash_space=True):
+    a = extract_text_array(dom, squash_artifical_nl=squash_space)
+    if squash_space:
+        a = _strip_artifical_nl(_squash_artifical_nl(_merge_original_parts(a)))
+    result = ''.join(
+        block_symbol if x is None else (
+            sep_symbol if x is True else x
+        )
+        for x in a
+    )
+    if squash_space:
+        result = result.strip()
+    return result

=== added file 'pytest.ini'
--- old/pytest.ini	1970-01-01 00:00:00 +0000
+++ new/pytest.ini	2021-04-05 08:32:53 +0000
@@ -0,0 +1,6 @@
+
+[pytest]
+filterwarnings =
+    ignore::DeprecationWarning
+doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL
+addopts = --doctest-modules --doctest-glob="*.rst" --ignore=docs/conf.py

=== added file 'setup.cfg'
--- old/setup.cfg	1970-01-01 00:00:00 +0000
+++ new/setup.cfg	2021-04-05 08:32:53 +0000
@@ -0,0 +1,16 @@
+[nosetests]
+verbosity = 2
+detailed-errors = True
+with-doctest = True
+doctest-extension = rst
+doctest-fixtures = _fixt
+include = docs
+exclude = seleniumtests
+cover-package = pyquery
+with-coverage = 1
+doctest-options = +ELLIPSIS,+NORMALIZE_WHITESPACE
+
+[egg_info]
+tag_build = 
+tag_date = 0
+

=== added file 'setup.py'
--- old/setup.py	1970-01-01 00:00:00 +0000
+++ new/setup.py	2021-04-05 08:32:53 +0000
@@ -0,0 +1,76 @@
+# -*- coding:utf-8 -*-
+#
+# Copyright (C) 2008 - Olivier Lauzanne <olauzanne@gmail.com>
+#
+# Distributed under the BSD license, see LICENSE.txt
+
+from setuptools import setup, find_packages
+import os
+
+
+install_requires = [
+    'lxml>=2.1',
+    'cssselect>0.7.9',
+]
+
+
+def read(*names):
+    values = dict()
+    for name in names:
+        filename = name + '.rst'
+        if os.path.isfile(filename):
+            fd = open(filename)
+            value = fd.read()
+            fd.close()
+        else:
+            value = ''
+        values[name] = value
+    return values
+
+
+long_description = """
+%(README)s
+
+See http://pyquery.rtfd.org/ for the full documentation
+
+News
+====
+
+%(CHANGES)s
+
+""" % read('README', 'CHANGES')
+
+version = '1.4.4.dev0'
+
+setup(name='pyquery',
+      version=version,
+      description='A jquery-like library for python',
+      long_description=long_description,
+      classifiers=[
+          "Intended Audience :: Developers",
+          "Development Status :: 5 - Production/Stable",
+          "Programming Language :: Python :: 3",
+          "Programming Language :: Python :: 3.5",
+          "Programming Language :: Python :: 3.6",
+          "Programming Language :: Python :: 3.7",
+      ],
+      keywords='jquery html xml scraping',
+      author='Olivier Lauzanne',
+      author_email='olauzanne@gmail.com',
+      maintainer='Gael Pasgrimaud',
+      maintainer_email='gael@gawel.org',
+      url='https://github.com/gawel/pyquery',
+      license='BSD',
+      packages=find_packages(exclude=[
+          'bootstrap', 'bootstrap-py3k', 'docs', 'tests', 'README_fixt'
+      ]),
+      extras_require={
+        'test': ['requests', 'webob', 'webtest', 'pytest', 'pytest-cov'],
+      },
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=install_requires,
+      entry_points="""
+      # -*- Entry points: -*-
+      """,
+      )

=== added directory 'tests'
=== added file 'tests/__init__.py'
=== added file 'tests/apps.py'
--- old/tests/apps.py	1970-01-01 00:00:00 +0000
+++ new/tests/apps.py	2021-04-05 08:32:53 +0000
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+from webob import Request
+from webob import Response
+from webob import exc
+
+
+def input_app(environ, start_response):
+    resp = Response()
+    req = Request(environ)
+    if req.path_info == '/':
+        resp.text = '<input name="youyou" type="text" value="" />'
+    elif req.path_info == '/submit':
+        resp.text = '<input type="submit" value="OK" />'
+    elif req.path_info.startswith('/html'):
+        resp.text = '<html><p>Success</p></html>'
+    else:
+        resp.text = '<html></html>'
+    return resp(environ, start_response)
+
+
+def application(environ, start_response):
+    req = Request(environ)
+    response = Response()
+    if req.method == 'GET':
+        response.text = '<pre>Yeah !</pre>'
+    else:
+        response.text = '<a href="/plop">Yeah !</a>'
+    return response(environ, start_response)
+
+
+def secure_application(environ, start_response):
+    if 'REMOTE_USER' not in environ:
+        return exc.HTTPUnauthorized('vomis')(environ, start_response)
+    return application(environ, start_response)

=== added file 'tests/browser_base.py'
--- old/tests/browser_base.py	1970-01-01 00:00:00 +0000
+++ new/tests/browser_base.py	2021-04-05 08:32:53 +0000
@@ -0,0 +1,84 @@
+
+class TextExtractionMixin():
+    def _prepare_dom(self, html):
+        self.last_html = '<html><body>' + html + '</body></html>'
+
+    def _simple_test(self, html, expected_sq, expected_nosq, **kwargs):
+        raise NotImplementedError
+
+    def test_inline_tags(self):
+        self._simple_test(
+            'Phas<em>ell</em>us<i> eget </i>sem <b>facilisis</b> justo',
+            'Phasellus eget sem facilisis justo',
+            'Phasellus eget sem facilisis justo',
+        )
+        self._simple_test(
+            'Phasellus <span> eget </span> sem <b>facilisis\n</b> justo',
+            'Phasellus eget sem facilisis justo',
+            'Phasellus  eget  sem facilisis\n justo',
+        )
+        self._simple_test(
+            ('Phasellus   <span>\n  eget\n           '
+             'sem\n\tfacilisis</span>   justo'),
+            'Phasellus eget sem facilisis justo',
+            'Phasellus   \n  eget\n           sem\n\tfacilisis   justo'
+        )
+
+    def test_block_tags(self):
+        self._simple_test(
+            'Phas<p>ell</p>us<div> eget </div>sem <h1>facilisis</h1> justo',
+            'Phas\nell\nus\neget\nsem\nfacilisis\njusto',
+            'Phas\nell\nus\n eget \nsem \nfacilisis\n justo',
+        )
+        self._simple_test(
+            '<p>In sagittis</p> <p>rutrum</p><p>condimentum</p>',
+            'In sagittis\nrutrum\ncondimentum',
+            'In sagittis\n \nrutrum\n\ncondimentum',
+        )
+        self._simple_test(
+            'In <p>\nultricies</p>\n erat et <p>\n\n\nmaximus\n\n</p> mollis',
+            'In\nultricies\nerat et\nmaximus\nmollis',
+            'In \n\nultricies\n\n erat et \n\n\n\nmaximus\n\n\n mollis',
+        )
+        self._simple_test(
+            ('Integer <div><div>\n  <div>quis commodo</div></div> '
+             '</div> libero'),
+            'Integer\nquis commodo\nlibero',
+            'Integer \n\n\n  \nquis commodo\n\n \n libero',
+        )
+        self._simple_test(
+            'Heading<ul><li>one</li><li>two</li><li>three</li></ul>',
+            'Heading\none\ntwo\nthree',
+            'Heading\n\none\n\ntwo\n\nthree',
+        )
+
+    def test_separators(self):
+        self._simple_test(
+            'Some words<br>test. Another word<br><br> <br> test.',
+            'Some words\ntest. Another word\n\n\ntest.',
+            'Some words\ntest. Another word\n\n \n test.',
+        )
+        self._simple_test(
+            'Inline <span>  splitted by\nbr<br>tag</span> test',
+            'Inline splitted by br\ntag test',
+            'Inline   splitted by\nbr\ntag test',
+        )
+        self._simple_test(
+            'Some words<hr>test. Another word<hr><hr> <hr> test.',
+            'Some words\ntest. Another word\ntest.',
+            'Some words\n\ntest. Another word\n\n\n\n \n\n test.',
+        )
+
+    def test_strip(self):
+        self._simple_test(
+            ' text\n',
+            'text',
+            ' text\n',
+        )
+
+    def test_ul_li(self):
+        self._simple_test(
+            '<ul> <li>  </li> </ul>',
+            '',
+            ' \n  \n '
+        )

=== added file 'tests/doctests.rst'
--- old/tests/doctests.rst	1970-01-01 00:00:00 +0000
+++ new/tests/doctests.rst	2015-11-25 09:40:39 +0000
@@ -0,0 +1,36 @@
+Import::
+
+    >>> from pyquery import PyQuery as pq
+
+
+Assume spaces normalization::
+
+    >>> pq('<ul> <li>  </li> </ul>').text()
+    ''
+
+    >>> print(pq('<ul> <li> toto </li> <li> tata </li> </ul>').text())
+    toto tata
+
+Complex wrapping::
+
+    >>> d = pq('<div id="bouh"><span>youhou</span></div>')
+    >>> s = d('span')
+    >>> s is d
+    False
+    >>> s.wrap('<div><div id="wrapper"></div></div>')
+    [<div>]
+
+We get the original doc with new node::
+
+    >>> print(d)
+    <div id="bouh"><div><div id="wrapper"><span>youhou</span></div></div></div>
+
+Complex wrapAll::
+
+    >>> doc = pq('<div><span>Hey</span><span>you !</span></div>')
+    >>> s = doc('span')
+    >>> s.wrapAll('<div id="wrapper"></div>')
+    [<div#wrapper>]
+
+    >>> print(doc)
+    <div><div id="wrapper"><span>Hey</span><span>you !</span></div></div>

=== added file 'tests/geckodriver.sh'
--- old/tests/geckodriver.sh	1970-01-01 00:00:00 +0000
+++ new/tests/geckodriver.sh	2021-04-05 08:32:53 +0000
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+driver="https://github.com/mozilla/geckodriver/releases/download/v0.26.0/geckodriver-v0.26.0-linux64.tar.gz"
+
+[ -f geckodriver ] || wget -cqO- $driver | tar xvzf -

=== added file 'tests/invalid.xml'
--- old/tests/invalid.xml	1970-01-01 00:00:00 +0000
+++ new/tests/invalid.xml	2015-11-25 09:40:39 +0000
@@ -0,0 +1,11 @@
+<html>
+<body>
+<p class="hello" id="hello">Hello world !</p>
+
+<p id="test">
+hello <a href="http://python.org">python</a> !
+</p>
+
+<p>
+</body>
+</html>

=== added file 'tests/selenium.sh'
--- old/tests/selenium.sh	1970-01-01 00:00:00 +0000
+++ new/tests/selenium.sh	2021-04-05 08:32:53 +0000
@@ -0,0 +1,8 @@
+#!/bin/bash
+# script to run selenium tests
+
+# get geckodriver
+./tests/geckodriver.sh
+
+# run tox with py3.7
+MOZ_HEADLESS=1 PATH=$PATH:$PWD tox -e py37 tests/test_real_browser.py

=== added file 'tests/test.html'
--- old/tests/test.html	1970-01-01 00:00:00 +0000
+++ new/tests/test.html	2015-11-25 09:40:39 +0000
@@ -0,0 +1,9 @@
+<html>
+<body>
+<p class="hello" id="hello">Hello world !</p>
+
+<p id="test">
+hello <a href="http://python.org">python</a> !
+</p>
+</body>
+</html>

=== added file 'tests/test_browser.py'
--- old/tests/test_browser.py	1970-01-01 00:00:00 +0000
+++ new/tests/test_browser.py	2021-04-05 08:32:53 +0000
@@ -0,0 +1,17 @@
+import unittest
+
+from pyquery.pyquery import PyQuery
+from .browser_base import TextExtractionMixin
+
+
+class TestInnerText(unittest.TestCase, TextExtractionMixin):
+    def _prepare_dom(self, html):
+        super(TestInnerText, self)._prepare_dom(html)
+        self.pq = PyQuery(self.last_html)
+
+    def _simple_test(self, html, expected_sq, expected_nosq, **kwargs):
+        self._prepare_dom(html)
+        text_sq = self.pq.text(squash_space=True, **kwargs)
+        text_nosq = self.pq.text(squash_space=False, **kwargs)
+        self.assertEqual(text_sq, expected_sq)
+        self.assertEqual(text_nosq, expected_nosq)

=== added file 'tests/test_pyquery.py'
--- old/tests/test_pyquery.py	1970-01-01 00:00:00 +0000
+++ new/tests/test_pyquery.py	2021-04-05 08:32:53 +0000
@@ -0,0 +1,950 @@
+# Copyright (C) 2008 - Olivier Lauzanne <olauzanne@gmail.com>
+#
+# Distributed under the BSD license, see LICENSE.txt
+import os
+import sys
+import time
+from lxml import etree
+from pyquery.pyquery import PyQuery as pq, no_default
+from pyquery.openers import HAS_REQUEST
+from webtest import http
+from webtest.debugapp import debug_app
+from unittest import TestCase
+
+sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
+
+
+dirname = os.path.dirname(os.path.abspath(__file__))
+docs = os.path.join(os.path.dirname(dirname), 'docs')
+path_to_html_file = os.path.join(dirname, 'test.html')
+path_to_invalid_file = os.path.join(dirname, 'invalid.xml')
+
+
+class TestUnicode(TestCase):
+
+    def test_unicode(self):
+        xml = pq(u"<html><p>é</p></html>")
+        self.assertEqual(type(xml.html()), str)
+        self.assertEqual(str(xml), '<html><p>é</p></html>')
+        self.assertEqual(str(xml('p:contains("é")')), '<p>é</p>')
+
+
+class TestAttributeCase(TestCase):
+
+    def test_xml_upper_element_name(self):
+        xml = pq('<X>foo</X>', parser='xml')
+        self.assertEqual(len(xml('X')), 1)
+        self.assertEqual(len(xml('x')), 0)
+
+    def test_html_upper_element_name(self):
+        xml = pq('<X>foo</X>', parser='html')
+        self.assertEqual(len(xml('X')), 1)
+        self.assertEqual(len(xml('x')), 1)
+
+
+class TestSelector(TestCase):
+    klass = pq
+    html = """
+           <html>
+            <body>
+              <div>node1</div>
+              <div id="node2">node2</div>
+              <div class="node3">node3</div>
+            </body>
+           </html>
+           """
+
+    html2 = """
+           <html>
+            <body>
+              <div>node1</div>
+            </body>
+           </html>
+           """
+
+    html3 = """
+           <html>
+            <body>
+              <div>node1</div>
+              <div id="node2">node2</div>
+              <div class="node3">node3</div>
+            </body>
+           </html>
+           """
+
+    html4 = """
+           <html>
+            <body>
+              <form action="/">
+                <input name="enabled" type="text" value="test"/>
+                <b disabled>Not :disabled</b>
+                <input name="disabled" type="text"
+                       value="disabled" disabled="disabled"/>
+                <fieldset>
+                    <input name="fieldset-enabled">
+                </fieldset>
+                <fieldset disabled>
+                    <legend>
+                        <input name="legend-enabled">
+                    </legend>
+                    <input name="fieldset-disabled">
+                    <legend>
+                        <input name="legend-disabled">
+                    </legend>
+                    <select id="disabled-select">
+                        <optgroup>
+                            <option></option>
+                        </optgroup>
+                    </select>
+                </fieldset>
+                <select>
+                    <optgroup id="disabled-optgroup" disabled>
+                        <option id="disabled-from-optgroup"></option>
+                        <option id="disabled-option" disabled></option>
+                    </optgroup>
+                </select>
+                <input name="file" type="file" />
+                <select name="select">
+                  <option value="">Choose something</option>
+                  <option value="one">One</option>
+                  <option value="two" selected="selected">Two</option>
+                  <option value="three">Three</option>
+                </select>
+                <input name="radio" type="radio" value="one"/>
+                <input name="radio" type="radio"
+                       value="two" checked="checked"/>
+                <input name="radio" type="radio" value="three"/>
+                <input name="checkbox" type="checkbox" value="a"/>
+                <input name="checkbox" type="checkbox"
+                       value="b" checked="checked"/>
+                <input name="checkbox" type="checkbox" value="c"/>
+                <input name="button" type="button" value="button" />
+                <button>button</button>
+              </form>
+            </body>
+           </html>
+           """
+
+    html5 = """
+           <html>
+            <body>
+              <h1>Heading 1</h1>
+              <h2>Heading 2</h2>
+              <h3>Heading 3</h3>
+              <h4>Heading 4</h4>
+              <h5>Heading 5</h5>
+              <h6>Heading 6</h6>
+              <div></div>
+            </body>
+           </html>
+           """
+
+    def test_get_root(self):
+        doc = pq(b'<?xml version="1.0" encoding="UTF-8"?><root><p/></root>')
+        self.assertEqual(isinstance(doc.root, etree._ElementTree), True)
+        self.assertEqual(doc.encoding, 'UTF-8')
+
+        child = doc.children().eq(0)
+        self.assertNotEqual(child._parent, no_default)
+        self.assertTrue(isinstance(child.root, etree._ElementTree))
+
+    def test_selector_from_doc(self):
+        doc = etree.fromstring(self.html)
+        assert len(self.klass(doc)) == 1
+        assert len(self.klass('div', doc)) == 3
+        assert len(self.klass('div#node2', doc)) == 1
+
+    def test_selector_from_html(self):
+        assert len(self.klass(self.html)) == 1
+        assert len(self.klass('div', self.html)) == 3
+        assert len(self.klass('div#node2', self.html)) == 1
+
+    def test_selector_from_obj(self):
+        e = self.klass(self.html)
+        assert len(e('div')) == 3
+        assert len(e('div#node2')) == 1
+
+    def test_selector_from_html_from_obj(self):
+        e = self.klass(self.html)
+        assert len(e('div', self.html2)) == 1
+        assert len(e('div#node2', self.html2)) == 0
+
+    def test_class(self):
+        e = self.klass(self.html)
+        assert isinstance(e, self.klass)
+        n = e('div', self.html2)
+        assert isinstance(n, self.klass)
+        assert n._parent is e
+
+    def test_pseudo_classes(self):
+        e = self.klass(self.html)
+        self.assertEqual(e('div:first').text(), 'node1')
+        self.assertEqual(e('div:last').text(), 'node3')
+        self.assertEqual(e('div:even').text(), 'node1 node3')
+        self.assertEqual(e('div div:even').text(), '')
+        self.assertEqual(e('body div:even').text(), 'node1 node3')
+        self.assertEqual(e('div:gt(0)').text(), 'node2 node3')
+        self.assertEqual(e('div:lt(1)').text(), 'node1')
+        self.assertEqual(e('div:eq(2)').text(), 'node3')
+
+        # test on the form
+        e = self.klass(self.html4)
+        disabled = e(':disabled')
+        self.assertIn(e('[name="disabled"]')[0], disabled)
+        self.assertIn(e('fieldset[disabled]')[0], disabled)
+        self.assertIn(e('[name="legend-disabled"]')[0], disabled)
+        self.assertIn(e('[name="fieldset-disabled"]')[0], disabled)
+        self.assertIn(e('#disabled-optgroup')[0], disabled)
+        self.assertIn(e('#disabled-from-optgroup')[0], disabled)
+        self.assertIn(e('#disabled-option')[0], disabled)
+        self.assertIn(e('#disabled-select')[0], disabled)
+
+        assert len(disabled) == 8
+        assert len(e('select:enabled')) == 2
+        assert len(e('input:enabled')) == 11
+        assert len(e(':selected')) == 1
+        assert len(e(':checked')) == 2
+        assert len(e(':file')) == 1
+        assert len(e(':input')) == 18
+        assert len(e(':button')) == 2
+        assert len(e(':radio')) == 3
+        assert len(e(':checkbox')) == 3
+
+        # test on other elements
+        e = self.klass(self.html5)
+        assert len(e(":header")) == 6
+        assert len(e(":parent")) == 2
+        assert len(e(":empty")) == 1
+        assert len(e(":contains('Heading')")) == 8
+
+    def test_on_the_fly_dom_creation(self):
+        e = self.klass(self.html)
+        assert e('<p>Hello world</p>').text() == 'Hello world'
+        assert e('').text() == ''
+
+
+class TestTraversal(TestCase):
+    klass = pq
+    html = """
+           <html>
+            <body>
+              <div id="node1"><span>node1</span></div>
+              <div id="node2" class="node3">
+                        <span>node2</span><span> booyah</span></div>
+            </body>
+           </html>
+           """
+
+    html2 = """
+            <html>
+             <body>
+               <dl>
+                 <dt id="term-1">term 1</dt>
+                 <dd>definition 1-a</dd>
+                 <dd>definition 1-b</dd>
+                 <dd>definition 1-c</dd>
+                 <dd>definition 1-d</dd>
+                 <dt id="term-2">term 2</dt>
+                 <dd>definition 2-a</dd>
+                 <dd class="strange">definition 2-b</dd>
+                 <dd>definition 2-c</dd>
+                 <dt id="term-3">term 3</dt>
+                 <dd>definition 3-a</dd>
+                 <dd>definition 3-b</dd>
+               </dl>
+             </body>
+            </html>
+            """
+
+    def test_filter(self):
+        assert len(self.klass('div', self.html).filter('.node3')) == 1
+        assert len(self.klass('div', self.html).filter('#node2')) == 1
+        assert len(self.klass('div', self.html).filter(lambda i: i == 0)) == 1
+
+        d = pq('<p>Hello <b>warming</b> world</p>')
+        self.assertEqual(d('strong').filter(lambda el: True), [])
+
+    def test_not(self):
+        assert len(self.klass('div', self.html).not_('.node3')) == 1
+
+    def test_is(self):
+        assert self.klass('div', self.html).is_('.node3')
+        assert not self.klass('div', self.html).is_('.foobazbar')
+
+    def test_find(self):
+        assert len(self.klass('#node1', self.html).find('span')) == 1
+        assert len(self.klass('#node2', self.html).find('span')) == 2
+        assert len(self.klass('div', self.html).find('span')) == 3
+
+    def test_each(self):
+        doc = self.klass(self.html)
+        doc('span').each(lambda: doc(this).wrap("<em></em>"))  # NOQA
+        assert len(doc('em')) == 3
+
+    def test_map(self):
+        def ids_minus_one(i, elem):
+            return int(self.klass(elem).attr('id')[-1]) - 1
+        assert self.klass('div', self.html).map(ids_minus_one) == [0, 1]
+
+        d = pq('<p>Hello <b>warming</b> world</p>')
+        self.assertEqual(d('strong').map(lambda i, el: pq(this).text()), [])  # NOQA
+
+    def test_end(self):
+        assert len(self.klass('div', self.html).find('span').end()) == 2
+        assert len(self.klass('#node2', self.html).find('span').end()) == 1
+
+    def test_closest(self):
+        assert len(self.klass('#node1 span', self.html).closest('body')) == 1
+        assert self.klass('#node2',
+                          self.html).closest('.node3').attr('id') == 'node2'
+        assert self.klass('.node3', self.html).closest('form') == []
+
+    def test_next_all(self):
+        d = pq(self.html2)
+
+        # without filter
+        self.assertEqual(
+            len(d('#term-2').next_all()), 6)
+        # with filter
+        self.assertEqual(
+            len(d('#term-2').next_all('dd')), 5)
+        # when empty
+        self.assertEqual(
+            d('#NOTHING').next_all(), [])
+
+    def test_next_until(self):
+        d = pq(self.html2)
+
+        # without filter
+        self.assertEqual(
+            len(d('#term-2').next_until('dt')), 3)
+        # with filter
+        self.assertEqual(
+            len(d('#term-2').next_until('dt', ':not(.strange)')), 2)
+        # when empty
+        self.assertEqual(
+            d('#NOTHING').next_until('*'), [])
+
+
+class TestOpener(TestCase):
+
+    def test_open_filename(self):
+        doc = pq(filename=path_to_html_file)
+        self.assertEqual(len(doc('p#test').text()), 14)
+
+    def test_invalid_filename(self):
+        doc = pq(filename=path_to_invalid_file)
+        self.assertEqual(len(doc('p#test').text()), 14)
+
+    def test_custom_opener(self):
+        def opener(url):
+            return '<html><body><div class="node"></div>'
+
+        doc = pq(url='http://example.com', opener=opener)
+        assert len(doc('.node')) == 1, doc
+
+
+class TestConstruction(TestCase):
+
+    def test_typeerror_on_invalid_value(self):
+        self.assertRaises(TypeError, pq, object())
+
+
+class TestComment(TestCase):
+
+    def test_comment(self):
+        doc = pq('<div><!-- foo --> bar</div>')
+        self.assertEqual(doc.text(), 'bar')
+
+
+class TestCallback(TestCase):
+    html = """
+        <ol>
+            <li>Coffee</li>
+            <li>Tea</li>
+            <li>Milk</li>
+        </ol>
+    """
+
+    def test_S_this_inside_callback(self):
+        S = pq(self.html)
+        self.assertEqual(S('li').map(
+            lambda i, el: S(this).html()),  # NOQA
+            ['Coffee', 'Tea', 'Milk']
+        )
+
+    def test_parameterless_callback(self):
+        S = pq(self.html)
+        self.assertEqual(S('li').map(
+            lambda: S(this).html()),  # NOQA
+            ['Coffee', 'Tea', 'Milk']
+        )
+
+
+class TestHook(TestCase):
+    html = """
+        <ol>
+            <li>Coffee</li>
+            <li>Tea</li>
+            <li>Milk</li>
+        </ol>
+    """
+
+    def test_fn(self):
+        "Example from `PyQuery.Fn` docs."
+        fn = lambda: this.map(lambda i, el: pq(this).outerHtml())  # NOQA
+        pq.fn.listOuterHtml = fn
+        S = pq(self.html)
+        self.assertEqual(S('li').listOuterHtml(),
+                         ['<li>Coffee</li>', '<li>Tea</li>', '<li>Milk</li>'])
+
+    def test_fn_with_kwargs(self):
+        "fn() with keyword arguments."
+        pq.fn.test = lambda p=1: pq(this).eq(p)  # NOQA
+        S = pq(self.html)
+        self.assertEqual(S('li').test(0).text(), 'Coffee')
+        self.assertEqual(S('li').test().text(), 'Tea')
+        self.assertEqual(S('li').test(p=2).text(), 'Milk')
+
+
+class TestManipulating(TestCase):
+    html = '''
+    <div class="portlet">
+      <a href="/toto">Test<img src ="myimage" />My link text</a>
+      <a href="/toto2"><img src ="myimage2" />My link text 2</a>
+    </div>
+    '''
+
+    html2 = '''
+        <input name="spam" value="Spam">
+        <input name="eggs" value="Eggs">
+        <input type="checkbox" value="Bacon">
+        <input type="radio" value="Ham">
+    '''
+
+    html2_newline = '''
+        <input id="newline-text" type="text" name="order" value="S
+pam">
+        <input id="newline-radio" type="radio" name="order" value="S
+pam">
+    '''
+
+    html3 = '''
+        <textarea id="textarea-single">Spam</textarea>
+        <textarea id="textarea-multi">Spam
+<b>Eggs</b>
+Bacon</textarea>
+    '''
+
+    html4 = '''
+        <select id="first">
+            <option value="spam">Spam</option>
+            <option value="eggs">Eggs</option>
+        </select>
+        <select id="second">
+            <option value="spam">Spam</option>
+            <option value="eggs" selected>Eggs</option>
+            <option value="bacon">Bacon</option>
+        </select>
+        <select id="third">
+        </select>
+        <select id="fourth">
+            <option value="spam">Spam</option>
+            <option value="spam">Eggs</option>
+            <option value="spam">Bacon</option>
+        </select>
+    '''
+
+    html6 = '''
+        <select id="first" multiple>
+            <option value="spam" selected>Spam</option>
+            <option value="eggs" selected>Eggs</option>
+            <option value="bacon">Bacon</option>
+        </select>
+        <select id="second" multiple>
+            <option value="spam">Spam</option>
+            <option value="eggs">Eggs</option>
+            <option value="bacon">Bacon</option>
+        </select>
+        <select id="third" multiple>
+            <option value="spam">Spam</option>
+            <option value="spam">Eggs</option>
+            <option value="spam">Bacon</option>
+        </select>
+    '''
+
+    html5 = '''
+        <div>
+            <input id="first" value="spam">
+            <input id="second" value="eggs">
+            <textarea id="third">bacon</textarea>
+        </div>
+    '''
+
+    def test_attr_empty_string(self):
+        d = pq('<div>')
+        d.attr('value', '')
+        self.assertEqual(d.outer_html(), '<div value=""></div>')
+        self.assertEqual(d.outer_html(method="xml"), '<div value=""/>')
+
+    def test_remove(self):
+        d = pq(self.html)
+        d('img').remove()
+        val = d('a:first').html()
+        assert val == 'Test My link text', repr(val)
+        val = d('a:last').html()
+        assert val == ' My link text 2', repr(val)
+
+    def test_class(self):
+        d = pq('<div></div>')
+        d.removeClass('xx')
+        assert 'class' not in str(d), str(d)
+
+    def test_val_for_inputs(self):
+        d = pq(self.html2)
+        self.assertIsNone(d('input[name="none"]').val())
+        self.assertEqual(d('input[name="spam"]').val(), 'Spam')
+        self.assertEqual(d('input[name="eggs"]').val(), 'Eggs')
+        self.assertEqual(d('input:checkbox').val(), 'Bacon')
+        self.assertEqual(d('input:radio').val(), 'Ham')
+        d('input[name="spam"]').val('42')
+        d('input[name="eggs"]').val('43')
+        d('input:checkbox').val('44')
+        d('input:radio').val('45')
+        self.assertEqual(d('input[name="spam"]').val(), '42')
+        self.assertEqual(d('input[name="eggs"]').val(), '43')
+        self.assertEqual(d('input:checkbox').val(), '44')
+        self.assertEqual(d('input:radio').val(), '45')
+
+    def test_val_for_inputs_with_newline(self):
+        d = pq(self.html2_newline)
+        self.assertEqual(d('#newline-text').val(), 'Spam')
+        self.assertEqual(d('#newline-radio').val(), 'S\npam')
+
+    def test_val_for_textarea(self):
+        d = pq(self.html3)
+        self.assertEqual(d('#textarea-single').val(), 'Spam')
+        self.assertEqual(d('#textarea-single').text(), 'Spam')
+        d('#textarea-single').val('42')
+        self.assertEqual(d('#textarea-single').val(), '42')
+        # Note: jQuery still returns 'Spam' here.
+        self.assertEqual(d('#textarea-single').text(), '42')
+
+        multi_expected = '''Spam\n<b>Eggs</b>\nBacon'''
+        self.assertEqual(d('#textarea-multi').val(), multi_expected)
+        self.assertEqual(d('#textarea-multi').text(), multi_expected)
+        multi_new = '''Bacon\n<b>Eggs</b>\nSpam'''
+        d('#textarea-multi').val(multi_new)
+        self.assertEqual(d('#textarea-multi').val(), multi_new)
+        self.assertEqual(d('#textarea-multi').text(), multi_new)
+
+    def test_val_for_select(self):
+        d = pq(self.html4)
+        self.assertEqual(d('#first').val(), 'spam')
+        self.assertEqual(d('#second').val(), 'eggs')
+        self.assertIsNone(d('#third').val())
+        d('#first').val('eggs')
+        d('#second').val('bacon')
+        d('#third').val('eggs')  # Selecting non-existing option.
+        self.assertEqual(d('#first').val(), 'eggs')
+        self.assertEqual(d('#second').val(), 'bacon')
+        self.assertIsNone(d('#third').val())
+        d('#first').val('bacon')  # Selecting non-existing option.
+        self.assertEqual(d('#first').val(), 'spam')
+        # Value set based on option order, not value order
+        d('#second').val(['bacon', 'eggs'])
+        self.assertEqual(d('#second').val(), 'eggs')
+        d('#fourth').val(['spam'])
+        self.assertEqual(d('#fourth').val(), 'spam')
+        # Sets first option with matching value
+        self.assertEqual(d('#fourth option[selected]').length, 1)
+        self.assertEqual(d('#fourth option[selected]').text(), 'Spam')
+
+    def test_val_for_select_multiple(self):
+        d = pq(self.html6)
+        self.assertEqual(d('#first').val(), ['spam', 'eggs'])
+        # Selecting non-existing option.
+        d('#first').val(['eggs', 'sausage', 'bacon'])
+        self.assertEqual(d('#first').val(), ['eggs', 'bacon'])
+        self.assertEqual(d('#second').val(), [])
+        d('#second').val('eggs')
+        self.assertEqual(d('#second').val(), ['eggs'])
+        d('#second').val(['not spam', 'not eggs'])
+        self.assertEqual(d('#second').val(), [])
+        d('#third').val(['spam'])
+        self.assertEqual(d('#third').val(), ['spam', 'spam', 'spam'])
+
+    def test_val_for_input_and_textarea_given_array_value(self):
+        d = pq('<input type="text">')
+        d('input').val(['spam', 'eggs'])
+        self.assertEqual(d('input').val(), 'spam,eggs')
+        d = pq('<textarea></textarea>')
+        d('textarea').val(['spam', 'eggs'])
+        self.assertEqual(d('textarea').val(), 'spam,eggs')
+
+    def test_val_for_multiple_elements(self):
+        d = pq(self.html5)
+        # "Get" returns *first* value.
+        self.assertEqual(d('div > *').val(), 'spam')
+        # "Set" updates *every* value.
+        d('div > *').val('42')
+        self.assertEqual(d('#first').val(), '42')
+        self.assertEqual(d('#second').val(), '42')
+        self.assertEqual(d('#third').val(), '42')
+
+    def test_val_checkbox_no_value_attribute(self):
+        d = pq('<input type="checkbox">')
+        self.assertEqual(d.val(), 'on')
+        d = pq('<input type="checkbox" value="">')
+        self.assertEqual(d.val(), '')
+
+    def test_val_radio_no_value_attribute(self):
+        d = pq('<input type="radio">')
+        self.assertEqual(d.val(), 'on')
+
+    def test_val_value_is_empty_string(self):
+        d = pq('<input value="">')
+        self.assertEqual(d.val(), '')
+
+    def test_val_input_has_no_value_attr(self):
+        d = pq('<input>')
+        self.assertEqual(d.val(), '')
+
+    def test_html_replacement(self):
+        html = '<div>Not Me<span>Replace Me</span>Not Me</div>'
+        replacement = 'New <em>Contents</em> New'
+        expected = html.replace('Replace Me', replacement)
+
+        d = pq(html)
+        d.find('span').html(replacement)
+
+        new_html = d.outerHtml()
+        self.assertEqual(new_html, expected)
+        self.assertIn(replacement, new_html)
+
+
+class TestAjax(TestCase):
+
+    html = '''
+    <div id="div">
+    <input form="dispersed" name="order" value="spam">
+    </div>
+    <form id="dispersed">
+    <div><input name="order" value="eggs"></div>
+    <input form="dispersed" name="order" value="ham">
+    <input form="other-form" name="order" value="nothing">
+    <input form="" name="order" value="nothing">
+    </form>
+    <form id="other-form">
+    <input form="dispersed" name="order" value="tomato">
+    </form>
+    <form class="no-id">
+    <input form="dispersed" name="order" value="baked beans">
+    <input name="spam" value="Spam">
+    </form>
+    '''
+
+    html2 = '''
+    <form id="first">
+    <input name="order" value="spam">
+    <fieldset>
+    <input name="fieldset" value="eggs">
+    <input id="input" name="fieldset" value="ham">
+    </fieldset>
+    </form>
+    <form id="datalist">
+    <datalist><div><input name="datalist" value="eggs"></div></datalist>
+    <input type="checkbox" name="checkbox" checked>
+    <input type="radio" name="radio" checked>
+    </form>
+    '''
+
+    html3 = '''
+    <form>
+    <input name="order" value="spam">
+    <input id="noname" value="sausage">
+    <fieldset disabled>
+    <input name="order" value="sausage">
+    </fieldset>
+    <input name="disabled" value="ham" disabled>
+    <input type="submit" name="submit" value="Submit">
+    <input type="button" name="button" value="">
+    <input type="image" name="image" value="">
+    <input type="reset" name="reset" value="Reset">
+    <input type="file" name="file" value="">
+    <button type="submit" name="submit" value="submit"></button>
+    <input type="checkbox" name="spam">
+    <input type="radio" name="eggs">
+    </form>
+    '''
+
+    html4 = '''
+    <form>
+    <input name="spam" value="Spam/
+spam">
+    <select name="order" multiple>
+    <option value="baked
+beans" selected>
+    <option value="tomato" selected>
+    <option value="spam">
+    </select>
+    <textarea name="multiline">multiple
+lines
+of text</textarea>
+    </form>
+    '''
+
+    def test_serialize_pairs_form_id(self):
+        d = pq(self.html)
+        self.assertEqual(d('#div').serialize_pairs(), [])
+        self.assertEqual(d('#dispersed').serialize_pairs(), [
+            ('order', 'spam'), ('order', 'eggs'), ('order', 'ham'),
+            ('order', 'tomato'), ('order', 'baked beans'),
+        ])
+        self.assertEqual(d('.no-id').serialize_pairs(), [
+            ('spam', 'Spam'),
+        ])
+
+    def test_serialize_pairs_form_controls(self):
+        d = pq(self.html2)
+        self.assertEqual(d('fieldset').serialize_pairs(), [
+            ('fieldset', 'eggs'), ('fieldset', 'ham'),
+        ])
+        self.assertEqual(d('#input, fieldset, #first').serialize_pairs(), [
+            ('order', 'spam'), ('fieldset', 'eggs'), ('fieldset', 'ham'),
+            ('fieldset', 'eggs'), ('fieldset', 'ham'), ('fieldset', 'ham'),
+        ])
+        self.assertEqual(d('#datalist').serialize_pairs(), [
+            ('datalist', 'eggs'), ('checkbox', 'on'), ('radio', 'on'),
+        ])
+
+    def test_serialize_pairs_filter_controls(self):
+        d = pq(self.html3)
+        self.assertEqual(d('form').serialize_pairs(), [
+            ('order', 'spam')
+        ])
+
+    def test_serialize_pairs_form_values(self):
+        d = pq(self.html4)
+        self.assertEqual(d('form').serialize_pairs(), [
+            ('spam', 'Spam/spam'), ('order', 'baked\r\nbeans'),
+            ('order', 'tomato'), ('multiline', 'multiple\r\nlines\r\nof text'),
+        ])
+
+    def test_serialize_array(self):
+        d = pq(self.html4)
+        self.assertEqual(d('form').serialize_array(), [
+            {'name': 'spam', 'value': 'Spam/spam'},
+            {'name': 'order', 'value': 'baked\r\nbeans'},
+            {'name': 'order', 'value': 'tomato'},
+            {'name': 'multiline', 'value': 'multiple\r\nlines\r\nof text'},
+        ])
+
+    def test_serialize(self):
+        d = pq(self.html4)
+        self.assertEqual(
+            d('form').serialize(),
+            'spam=Spam%2Fspam&order=baked%0D%0Abeans&order=tomato&'
+            'multiline=multiple%0D%0Alines%0D%0Aof%20text'
+        )
+
+    def test_serialize_dict(self):
+        d = pq(self.html4)
+        self.assertEqual(d('form').serialize_dict(), {
+            'spam': 'Spam/spam',
+            'order': ['baked\r\nbeans', 'tomato'],
+            'multiline': 'multiple\r\nlines\r\nof text',
+        })
+
+
+class TestMakeLinks(TestCase):
+
+    html = '''
+    <html>
+    <div>
+    <a href="/path_info">with href</a>
+    <a>without href</a>
+    </div>
+    </html>
+    '''
+
+    def test_make_link(self):
+        d = pq(self.html, parser='xml')
+        d.make_links_absolute(base_url='http://example.com')
+        self.assertTrue(len(d('a[href]')), 1)
+        self.assertEqual(d('a[href]').attr('href'),
+                         'http://example.com/path_info')
+
+
+class TestHTMLParser(TestCase):
+    xml = "<div>I'm valid XML</div>"
+    html = '''<div class="portlet">
+      <a href="/toto">TestimageMy link text</a>
+      <a href="/toto2">imageMy link text 2</a>
+      Behind you, a three-headed HTML&dash;Entity!
+    </div>'''
+
+    def test_parser_persistance(self):
+        d = pq(self.xml, parser='xml')
+        self.assertRaises(etree.XMLSyntaxError, lambda: d.after(self.html))
+        d = pq(self.xml, parser='html')
+        d.after(self.html)  # this should not fail
+
+    def test_replaceWith(self):
+        expected = '''<div class="portlet">
+      <a href="/toto">TestimageMy link text</a>
+      <a href="/toto2">imageMy link text 2</a>
+      Behind you, a three-headed HTML&amp;dash;Entity!
+    </div>'''
+        d = pq(self.html)
+        d('img').replace_with('image')
+        val = d.__html__()
+        assert val == expected, (repr(val), repr(expected))
+
+    def test_replaceWith_with_function(self):
+        expected = '''<div class="portlet">
+      TestimageMy link text
+      imageMy link text 2
+      Behind you, a three-headed HTML&amp;dash;Entity!
+    </div>'''
+        d = pq(self.html)
+        d('a').replace_with(lambda i, e: pq(e).html())
+        val = d.__html__()
+        assert val == expected, (repr(val), repr(expected))
+
+
+class TestXMLNamespace(TestCase):
+    xml = '''<?xml version="1.0" encoding="UTF-8" ?>
+    <foo xmlns:bar="http://example.com/bar">
+    <bar:blah>What</bar:blah>
+    <idiot>123</idiot>
+    <baz xmlns="http://example.com/baz" a="b">
+          <subbaz/>
+    </baz>
+    </foo>'''
+
+    xhtml = '''
+    <html xmlns="http://www.w3.org/1999/xhtml">
+    <body>
+    <div>What</div>
+    </body>
+    </html>'''
+
+    namespaces = {'bar': 'http://example.com/bar',
+                  'baz': 'http://example.com/baz'}
+
+    def test_selector(self):
+        expected = 'What'
+        d = pq(self.xml.encode('utf8'), parser='xml')
+        val = d('bar|blah',
+                namespaces=self.namespaces).text()
+        self.assertEqual(repr(val), repr(expected))
+
+    def test_selector_with_xml(self):
+        expected = 'What'
+        d = pq('bar|blah', self.xml.encode('utf8'), parser='xml',
+               namespaces=self.namespaces)
+        val = d.text()
+        self.assertEqual(repr(val), repr(expected))
+
+    def test_selector_html(self):
+        expected = 'What'
+        d = pq('blah', self.xml.split('?>', 1)[1], parser='html')
+        val = d.text()
+        self.assertEqual(repr(val), repr(expected))
+
+    def test_xhtml_namespace(self):
+        expected = 'What'
+        d = pq(self.xhtml.encode('utf8'), parser='xml')
+        d.xhtml_to_html()
+        val = d('div').text()
+        self.assertEqual(repr(val), repr(expected))
+
+    def test_xhtml_namespace_html_parser(self):
+        expected = 'What'
+        d = pq(self.xhtml, parser='html')
+        d.xhtml_to_html()
+        val = d('div').text()
+        self.assertEqual(repr(val), repr(expected))
+
+    def test_remove_namespaces(self):
+        expected = 'What'
+        d = pq(self.xml.encode('utf8'), parser='xml').remove_namespaces()
+        val = d('blah').text()
+        self.assertEqual(repr(val), repr(expected))
+
+    def test_persistent_namespaces(self):
+        d = pq(self.xml.encode('utf8'), parser='xml',
+               namespaces=self.namespaces)
+        val = d('bar|blah').text()
+        self.assertEqual(repr(val), repr('What'))
+
+    def test_namespace_traversal(self):
+        d = pq(self.xml.encode('utf8'), parser='xml',
+               namespaces=self.namespaces)
+        val = d('baz|subbaz').closest('baz|baz').attr('a')
+        self.assertEqual(repr(val), repr('b'))
+
+
+class TestWebScrapping(TestCase):
+
+    def setUp(self):
+        self.s = http.StopableWSGIServer.create(debug_app)
+        self.s.wait()
+        self.application_url = self.s.application_url.rstrip('/')
+
+    def test_get(self):
+        d = pq(self.application_url, {'q': 'foo'},
+               method='get')
+        print(d)
+        self.assertIn('REQUEST_METHOD: GET', d('p').text())
+        self.assertIn('q=foo', d('p').text())
+
+    def test_post(self):
+        d = pq(self.application_url, {'q': 'foo'},
+               method='post')
+        self.assertIn('REQUEST_METHOD: POST', d('p').text())
+        self.assertIn('q=foo', d('p').text())
+
+    def test_session(self):
+        if HAS_REQUEST:
+            import requests
+            session = requests.Session()
+            session.headers.update({'X-FOO': 'bar'})
+            d = pq(self.application_url, {'q': 'foo'},
+                   method='get', session=session)
+            self.assertIn('HTTP_X_FOO: bar', d('p').text())
+        else:
+            self.skipTest('no requests library')
+
+    def tearDown(self):
+        self.s.shutdown()
+
+
+class TestWebScrappingEncoding(TestCase):
+
+    def test_get(self):
+        d = pq(u'http://ru.wikipedia.org/wiki/Заглавная_страница',
+               method='get')
+        print(d)
+        self.assertEqual(d('#pt-login').text(), u'Войти')
+
+
+class TestWebScrappingTimeouts(TestCase):
+
+    def setUp(self):
+        def app(environ, start_response):
+            start_response('200 OK', [('Content-Type', 'text/plain')])
+            time.sleep(2)
+            return [b'foobar\n']
+        self.s = http.StopableWSGIServer.create(app)
+        self.s.wait()
+        self.application_url = self.s.application_url.rstrip('/')
+
+    def test_get(self):
+        pq(self.application_url)
+        with self.assertRaises(Exception):
+            pq(self.application_url, timeout=1)
+
+    def tearDown(self):
+        self.s.shutdown()

=== added file 'tests/test_real_browser.py'
--- old/tests/test_real_browser.py	1970-01-01 00:00:00 +0000
+++ new/tests/test_real_browser.py	2021-04-05 08:32:53 +0000
@@ -0,0 +1,118 @@
+import os
+import unittest
+from threading import Thread
+from time import sleep
+
+from .browser_base import TextExtractionMixin
+
+SELENIUM = 'MOZ_HEADLESS' in os.environ
+
+try:
+    from selenium import webdriver
+    from selenium.webdriver.firefox.options import Options
+except ImportError:
+    SELENIUM = False
+
+if SELENIUM:
+    from urllib.parse import urlunsplit
+    from http.server import HTTPServer, BaseHTTPRequestHandler
+    from queue import Queue
+
+    class BaseTestRequestHandler(BaseHTTPRequestHandler):
+        _last_html = ''
+
+        def _get_last_html(self):
+            q = self.server.html_queue
+            while not q.empty():
+                self._last_html = q.get_nowait()
+            return self._last_html
+
+        def log_request(self, code='-', size='-'):
+            pass
+
+        def recv_from_testsuite(self, non_blocking=False):
+            q = self.server.in_queue
+            if non_blocking:
+                return None if q.empty() else q.get_nowait()
+            return q.get()
+
+        def send_to_testsuite(self, value):
+            self.server.out_queue.put(value)
+
+    class HTMLSnippetSender(BaseTestRequestHandler):
+        last_html = b''
+
+        def get_last_html(self):
+            while True:
+                value = self.recv_from_testsuite(non_blocking=True)
+                if value is None:
+                    break
+                self.last_html = value
+            return self.last_html
+
+        def do_GET(self):
+            if self.path == '/':
+                self.send_response(200)
+                self.send_header('Content-Type', 'text/html; charset=utf-8')
+                self.end_headers()
+                self.wfile.write(self.get_last_html().encode('utf-8'))
+            else:
+                self.send_response(404)
+                self.end_headers()
+
+    class BaseBrowserTest(unittest.TestCase):
+        LOCAL_IP = '127.0.0.1'
+        PORT = 28546
+        # descendant of BaseBrowserTestRequestHandler
+        REQUEST_HANDLER_CLASS = None
+
+        @classmethod
+        def setUpClass(cls):
+            cls.to_server_queue = Queue()
+            cls.from_server_queue = Queue()
+            cls.server = HTTPServer((cls.LOCAL_IP, cls.PORT),
+                                    cls.REQUEST_HANDLER_CLASS)
+            cls.server.in_queue = cls.to_server_queue
+            cls.server.out_queue = cls.from_server_queue
+            cls.server_thread = Thread(target=cls.server.serve_forever)
+            cls.server_thread.daemon = True
+            cls.server_thread.start()
+            options = Options()
+            options.add_argument('-headless')
+            cls.driver = webdriver.Firefox(options=options)
+            sleep(1)
+
+        @classmethod
+        def tearDownClass(cls):
+            cls.driver.quit()
+            cls.server.shutdown()
+            cls.server.server_close()
+
+        def send_to_server(self, value):
+            self.to_server_queue.put(value)
+
+        def recv_from_server(self, non_blocking=False):
+            q = self.from_server_queue
+            if non_blocking:
+                return None if q.empty() else q.get_nowait()
+            return q.get()
+
+        def open_url(self, path):
+            self.driver.get(urlunsplit(
+                ('http', '{}:{}'.format(
+                    self.LOCAL_IP, self.PORT), path, '', '')))
+
+    class TestInnerText(BaseBrowserTest, TextExtractionMixin):
+        REQUEST_HANDLER_CLASS = HTMLSnippetSender
+
+        def _simple_test(self, html, expected_sq, expected_nosq, **kwargs):
+            self.send_to_server(html)
+            self.open_url('/')
+
+            selenium_text = self.driver.find_element_by_tag_name('body').text
+            self.assertEqual(selenium_text, expected_sq)
+
+            #  inner_text = self.driver.execute_script(
+            #    'return document.body.innerText')
+            #  text_content = self.driver.execute_script(
+            #    'return document.body.textContent')

=== added file 'tox.ini'
--- old/tox.ini	1970-01-01 00:00:00 +0000
+++ new/tox.ini	2021-04-05 08:32:53 +0000
@@ -0,0 +1,42 @@
+[tox]
+envlist=py35,py36,py37,py38
+
+[testenv]
+whitelist_externals=
+    rm
+passenv=
+    MOZ_HEADLESS
+commands =
+    pytest []
+deps =
+    py38: selenium
+    -e .[test]
+
+[testenv:flake8]
+skipsdist=true
+skip_install=true
+basepython = python3.8
+commands =
+    flake8 pyquery tests
+deps =
+    flake8
+
+[testenv:docs]
+skip_install=false
+skipsdist=true
+basepython = python3.8
+changedir = docs
+deps =
+    sphinx
+    Pygments
+commands =
+    rm -Rf {envtmpdir}/doctrees {envtmpdir}/html
+    sphinx-build -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
+
+# [testenv:selenium]
+# basepython = python3.5
+# deps =
+#     selenium
+# commands =
+#     {envbindir}/python -m unittest seleniumtests.offline
+#     {envbindir}/python -m unittest seleniumtests.browser