Tools/ChangeLog

 12019-10-11 Jonathan Bedard <jbedard@apple.com>
 2
 3 Python 3: Add support in webkitpy.common.net.bugzilla
 4 https://bugs.webkit.org/show_bug.cgi?id=202465
 5
 6 Reviewed by NOBODY (OOPS!).
 7
 8 * Scripts/test-webkitpy-python3: Add webkitpy.common.net.bugzilla to the test list.
 9 * Scripts/webkitpy/common/net/bugzilla/__init__.py: Use full import paths.
 10 * Scripts/webkitpy/common/net/bugzilla/attachment_unittest.py: Ditto.
 11 * Scripts/webkitpy/common/net/bugzilla/bug.py: Ditto.
 12 (Bug.commit_revision): Dictionary sorting is no longer well defined in Python 3.
 13 * Scripts/webkitpy/common/net/bugzilla/bug_unittest.py: Use full import paths.
 14 * Scripts/webkitpy/common/net/bugzilla/bugzilla.py: Ditto.
 15 (EditUsersParser.login_userid_pairs_from_edit_user_results): Return list, not an iterator.
 16 (EditUsersParser.user_dict_from_edit_user_page): HTML_ENTITIES is logically connected to BeautifulSoup, not
 17 BeautifulStoneSoup.
 18 (BugzillaQueries._parse_attachment_ids_request_query): Explicitly cast date_tag to unicode.
 19 (Bugzilla._parse_attachment_element): Legacy BeautifulSoup and bs4 Elements have different function calls.
 20 (Bugzilla._file_object_for_upload): Use BytesIO instead of StringIO.
 21 (Bugzilla.create_bug):
 22 (reopen_bug): Access map as list, not iterator.
 23 * Scripts/webkitpy/common/net/bugzilla/bugzilla_mock.py: Use full import paths.
 24 * Scripts/webkitpy/common/net/bugzilla/bugzilla_unittest.py: Ditto.
 25 * Scripts/webkitpy/common/net/bugzilla/test_expectation_updater.py:
 26 (TestExpectationUpdater._update_from_generic_attachment): Use items over iteritems().
 27 (TestExpectationUpdater._update_from_platform_specific_attachment): Ditto.
 28 (TestExpectationUpdater.do_update): Ditto.
 29 * Scripts/webkitpy/common/net/buildbot/buildbot.py: Use Python 2/3 compatible urllib.
 30 * Scripts/webkitpy/common/net/credentials.py: Use import statement to force autoinstall logic.
 31 * Scripts/webkitpy/common/net/file_uploader.py: Use Python 2/3 compatible urllib.
 32 (FileUploader._upload_data.callback):
 33 (FileUploader):
 34 * Scripts/webkitpy/common/net/resultsjsonparser.py:
 35 (JSONTestResult._tokenize): Force map() call to return a list.
 36 * Scripts/webkitpy/common/net/web_mock.py: Use Python 2/3 compatible urllib.
 37 * Scripts/webkitpy/common/system/autoinstall.py:
 38 (AutoInstaller.install): Return True if install was required, otherwise, return False.
 39 * Scripts/webkitpy/common/system/filesystem_mock.py:
 40 (MockFileSystem.__init__): Encode strings before writing them to the filesystem.
 41 * Scripts/webkitpy/layout_tests/layout_package/json_results_generator.py: Use Python 2/3 compatible urllib.
 42 * Scripts/webkitpy/layout_tests/models/test_expectations.py:
 43 (TestExpectationParser): Use items over iteritems().
 44 * Scripts/webkitpy/layout_tests/models/test_failures.py: Use Python 3 compatible Pickle library.
 45 * Scripts/webkitpy/layout_tests/models/test_results.py: Ditto.
 46 * Scripts/webkitpy/port/darwin_testcase.py:
 47 (DarwinTest.test_crashlog_path): Use dictionary for files.
 48 * Scripts/webkitpy/test/main.py:
 49 (Tester._run_tests): Update directories excluded from testing.
 50 * Scripts/webkitpy/thirdparty/BeautifulSoup.py: Replaced.
 51 (BeautifulSoup):Wrapper class making bs4 BeautifulSoup compatible with legacy BeautifulSoup calls.
 52 (BeautifulStoneSoup): Ditto.
 53 * Scripts/webkitpy/thirdparty/BeautifulSoup_legacy.py: Copied from Tools/Scripts/webkitpy/thirdparty/BeautifulSoup.py.
 54 * Scripts/webkitpy/thirdparty/__init__.py:
 55 (AutoinstallImportHook.__init__): Accept executive.
 56 (AutoinstallImportHook.find_module): Add beautifulsoup.
 57 (AutoinstallImportHook._install_beautifulsoup): bs4 must be converted via 2to3 before use in Python 3.
 58 (AutoinstallImportHook._install): Return result from installer.
 59 * Scripts/webkitpy/thirdparty/mock.py:
 60 (Mock.reset_mock): Use values() instead of itervalues().
 61
1622019-10-11 Brian Burg <bburg@apple.com>
263
364 Remove some dead code in webkitpy's mac.py
251020

Tools/Scripts/test-webkitpy-python3

@@import unittest
3434PYTHON3_COMPATIBLE_DIRECTORIES = [
3535 'webkitpy.common.system',
3636 'webkitpy.common.thread',
 37 'webkitpy.common.net.bugzilla',
3738]
3839
3940
251011

Tools/Scripts/webkitpy/common/net/credentials.py

@@import re
3737from webkitpy.common.checkout.scm import Git
3838from webkitpy.common.system.executive import Executive, ScriptError
3939from webkitpy.common.system.user import User
40 from webkitpy.thirdparty.autoinstalled import keyring
 40import webkitpy.thirdparty.autoinstalled.keyring as keyring
4141
4242_log = logging.getLogger(__name__)
4343
251011

Tools/Scripts/webkitpy/common/net/file_uploader.py

2828
2929import mimetypes
3030import time
31 import urllib2
 31import sys
3232
3333from webkitpy.common.net.networktransaction import NetworkTransaction, NetworkTimeout
3434
 35if sys.version_info > (3, 0):
 36 from urllib.request import Request, urlopen
 37else:
 38 from urllib2 import Request, urlopen
3539
3640def get_mime_type(filename):
3741 return mimetypes.guess_type(filename)[0] or 'application/octet-stream'

@@class FileUploader(object):
101105 # FIXME: Setting a timeout, either globally using socket.setdefaulttimeout()
102106 # or in urlopen(), doesn't appear to work on Mac 10.5 with Python 2.7.
103107 # For now we will ignore the timeout value and hope for the best.
104  request = urllib2.Request(self._url, data, {"Content-Type": content_type})
105  return urllib2.urlopen(request)
 108 request = Request(self._url, data, {"Content-Type": content_type})
 109 return urlopen(request)
106110
107111 return NetworkTransaction(timeout_seconds=self._timeout_seconds).run(callback)
251011

Tools/Scripts/webkitpy/common/net/resultsjsonparser.py

@@class JSONTestResult(object):
8282 return True
8383
8484 def _tokenize(self, results_string):
85  tokens = map(TestExpectations.expectation_from_string, results_string.split(' '))
 85 tokens = list(map(TestExpectations.expectation_from_string, results_string.split(' ')))
8686 if None in tokens:
8787 _log.warning("Unrecognized result in %s" % results_string)
8888 return set(tokens)
251011

Tools/Scripts/webkitpy/common/net/web_mock.py

2727# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
2828# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2929
30 import StringIO
31 import urllib2
 30from webkitpy.common.unicode_compatibility import StringIO
 31
 32import sys
 33if sys.version_info > (3, 0):
 34 from urllib.error import HTTPError
 35else:
 36 from urllib2 import HTTPError
3237
3338class MockWeb(object):
3439 def __init__(self, urls=None, responses=[]):

@@class MockResponse(object):
5358 self.body = values.get('body', '')
5459
5560 if int(self.status_code) >= 400:
56  raise urllib2.HTTPError(
 61 raise HTTPError(
5762 url=self.url,
5863 code=self.status_code,
5964 msg='Received error status code: {}'.format(self.status_code),

@@class MockBrowser(object):
8388 return self.params.get(key)
8489
8590 def submit(self):
86  return StringIO.StringIO()
 91 return StringIO()
8792
8893 def set_handle_robots(self, value):
8994 pass
251011

Tools/Scripts/webkitpy/common/net/bugzilla/__init__.py

11# Required for Python to search this directory for module files
22
33# We only export public API here.
4 from .bugzilla import Bugzilla
 4from webkitpy.common.net.bugzilla.bugzilla import Bugzilla
55# Unclear if Bug and Attachment need to be public classes.
6 from .bug import Bug
7 from .attachment import Attachment
 6from webkitpy.common.net.bugzilla.bug import Bug
 7from webkitpy.common.net.bugzilla.attachment import Attachment
251011

Tools/Scripts/webkitpy/common/net/bugzilla/attachment_unittest.py

@@import unittest
2525from datetime import datetime
2626from webkitpy.common.net.bugzilla.constants import BUGZILLA_DATE_FORMAT
2727
28 from .attachment import Attachment
 28from webkitpy.common.net.bugzilla.attachment import Attachment
2929
3030
3131class AttachmentTest(unittest.TestCase):
251011

Tools/Scripts/webkitpy/common/net/bugzilla/bug.py

3030
3131import re
3232
33 from .attachment import Attachment
 33from webkitpy.common.net.bugzilla.attachment import Attachment
3434
3535
3636class Bug(object):

@@class Bug(object):
133133 def commit_revision(self):
134134 # Sort the comments in reverse order as we want the latest committed revision.
135135 r = re.compile("Committed r(?P<svn_revision>\d+)")
136  for comment in sorted(self.comments(), reverse=True):
 136 for comment in sorted(self.comments(), key=lambda comment: comment['text'], reverse=True):
137137 rev = r.search(comment['text'])
138138 if rev:
139139 return int(rev.group('svn_revision'))
251011

Tools/Scripts/webkitpy/common/net/bugzilla/bug_unittest.py

2828
2929import unittest
3030
31 from .bug import Bug
 31from webkitpy.common.net.bugzilla.bug import Bug
3232
3333
3434class BugTest(unittest.TestCase):
251011

Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla.py

3434import logging
3535import mimetypes
3636import re
37 import StringIO
3837import socket
 38import sys
3939import urllib
4040
4141from datetime import datetime # used in timestamp()
4242
43 from .attachment import Attachment
44 from .bug import Bug
 43from webkitpy.common.unicode_compatibility import BytesIO, StringIO, unicode
 44from webkitpy.common.net.bugzilla.attachment import Attachment
 45from webkitpy.common.net.bugzilla.bug import Bug
4546
4647from webkitpy.common.config import committers
4748import webkitpy.common.config.urls as config_urls

@@class EditUsersParser(object):
7374 return (login, user_id)
7475
7576 def login_userid_pairs_from_edit_user_results(self, results_page):
76  soup = BeautifulSoup(results_page, convertEntities=BeautifulStoneSoup.HTML_ENTITIES)
 77 soup = BeautifulSoup(results_page, convertEntities=BeautifulSoup.HTML_ENTITIES)
7778 results_table = soup.find(id="admin_table")
7879 login_userid_pairs = [self._login_and_uid_from_row(row) for row in results_table('tr')]
7980 # Filter out None from the logins.
80  return filter(lambda pair: bool(pair), login_userid_pairs)
 81 return list(filter(lambda pair: bool(pair), login_userid_pairs))
8182
8283 def _group_name_and_string_from_row(self, row):
8384 label_element = row.find('label')

@@class EditUsersParser(object):
8687 return (group_name, group_string)
8788
8889 def user_dict_from_edit_user_page(self, page):
89  soup = BeautifulSoup(page, convertEntities=BeautifulStoneSoup.HTML_ENTITIES)
 90 soup = BeautifulSoup(page, convertEntities=BeautifulSoup.HTML_ENTITIES)
9091 user_table = soup.find("table", {'class': 'main'})
9192 user_dict = {}
9293 for row in user_table('tr'):

@@class BugzillaQueries(object):
209210 continue
210211 patch_id = int(digits.search(patch_tag["href"]).group(0))
211212 date_tag = row.find("td", text=date_format)
212  if date_tag and datetime.strptime(date_format.search(date_tag).group(0), "%Y-%m-%d %H:%M") < since:
 213 if date_tag and datetime.strptime(date_format.search(unicode(date_tag)).group(0), "%Y-%m-%d %H:%M") < since:
213214 continue
214215 patch_ids.append(patch_id)
215216 return patch_ids

@@class Bugzilla(object):
418419 def _parse_attachment_element(self, element, bug_id):
419420 attachment = {}
420421 attachment['bug_id'] = bug_id
421  attachment['is_obsolete'] = (element.has_key('isobsolete') and element['isobsolete'] == "1")
422  attachment['is_patch'] = (element.has_key('ispatch') and element['ispatch'] == "1")
 422 if sys.version_info > (3, 0):
 423 attachment['is_obsolete'] = (element.has_attr('isobsolete') and element['isobsolete'] == "1")
 424 attachment['is_patch'] = (element.has_attr('ispatch') and element['ispatch'] == "1")
 425 else:
 426 attachment['is_obsolete'] = (element.has_key('isobsolete') and element['isobsolete'] == "1")
 427 attachment['is_patch'] = (element.has_key('ispatch') and element['ispatch'] == "1")
423428 attachment['id'] = int(element.find('attachid').string)
424429 # FIXME: No need to parse out the url here.
425430 attachment['url'] = self.attachment_url_for_id(attachment['id'])

@@class Bugzilla(object):
637642 # Only if file_or_string is not already encoded do we want to encode it.
638643 if isinstance(file_or_string, unicode):
639644 file_or_string = file_or_string.encode('utf-8')
640  return StringIO.StringIO(file_or_string)
 645 return BytesIO(file_or_string)
641646
642647 # timestamp argument is just for unittests.
643648 def _filename_for_upload(self, file_object, bug_id, extension="txt", timestamp=timestamp):

@@class Bugzilla(object):
759764 # _fill_attachment_form expects a file-like object
760765 # Patch files are already binary, so no encoding needed.
761766 assert(isinstance(diff, str))
762  patch_file_object = StringIO.StringIO(diff)
 767 patch_file_object = StringIO(diff)
763768 commit_flag = CommitQueueFlag.mark_for_nothing
764769 if mark_for_commit_queue:
765770 commit_flag = CommitQueueFlag.mark_for_commit_queue

@@Ignore this message if you don't have Ed
923928 # This is a hack around the fact that ClientForm.ListControl seems to
924929 # have no simpler way to ask if a control has an item named "REOPENED"
925930 # without using exceptions for control flow.
926  possible_bug_statuses = map(lambda item: item.name, bug_status.items)
 931 possible_bug_statuses = list(map(lambda item: item.name, bug_status.items))
927932 if "REOPENED" in possible_bug_statuses:
928933 bug_status.value = ["REOPENED"]
929934 # If the bug was never confirmed it will not have a "REOPENED"
251011

Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_mock.py

3030import datetime
3131import logging
3232
33 from .bug import Bug
34 from .bugzilla import Bugzilla
35 from .attachment import Attachment
 33from webkitpy.common.net.bugzilla.bug import Bug
 34from webkitpy.common.net.bugzilla.bugzilla import Bugzilla
 35from webkitpy.common.net.bugzilla.attachment import Attachment
3636from webkitpy.common.config.committers import CommitterList, Reviewer
3737
3838_log = logging.getLogger(__name__)
251011

Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_unittest.py

2626# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
2727# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2828
29 import StringIO
3029import datetime
 30import sys
3131import unittest
3232
33 from .bugzilla import Bugzilla, BugzillaQueries, CommitQueueFlag, EditUsersParser
34 
3533from webkitpy.common.config import urls
3634from webkitpy.common.config.committers import Reviewer, Committer, Contributor, CommitterList
37 from webkitpy.common.system.outputcapture import OutputCapture
3835from webkitpy.common.net.web_mock import MockBrowser
39 from webkitpy.thirdparty.mock import Mock
 36from webkitpy.common.net.bugzilla.bugzilla import Bugzilla, BugzillaQueries, CommitQueueFlag, EditUsersParser
 37from webkitpy.common.system.outputcapture import OutputCapture
 38from webkitpy.common.unicode_compatibility import StringIO
4039from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup
 40from webkitpy.thirdparty.mock import Mock
4141
4242
4343class BugzillaTest(unittest.TestCase):

@@class BugzillaTest(unittest.TestCase):
6767 </attachment>
6868'''
6969 _expected_example_attachment_parsing = {
70  'attach_date': datetime.datetime(2009, 07, 29, 10, 23),
 70 'attach_date': datetime.datetime(2009, 7, 29, 10, 23),
7171 'bug_id': 100,
7272 'is_obsolete': True,
7373 'is_patch': True,

@@Ignore this bug. Just for testing failu
212212 # FIXME: This should move to a central location and be shared by more unit tests.
213213 def _assert_dictionaries_equal(self, actual, expected):
214214 # Make sure we aren't parsing more or less than we expect
215  self.assertItemsEqual(actual.keys(), expected.keys())
 215 self.assertEqual(sorted(actual.keys()), sorted(expected.keys()))
216216
217217 for key, expected_value in expected.items():
218218 self.assertEqual(actual[key], expected_value, ("Failure for key: %s: Actual='%s' Expected='%s'" % (key, actual[key], expected_value)))

@@Ignore this bug. Just for testing failu
303303
304304 def test_file_object_for_upload(self):
305305 bugzilla = Bugzilla()
306  file_object = StringIO.StringIO()
 306 file_object = StringIO()
307307 unicode_tor = u"WebKit \u2661 Tor Arne Vestb\u00F8!"
308308 utf8_tor = unicode_tor.encode("utf-8")
309309 self.assertEqual(bugzilla._file_object_for_upload(file_object), file_object)

@@Ignore this bug. Just for testing failu
316316 mock_file.name = "foo"
317317 self.assertEqual(bugzilla._filename_for_upload(mock_file, 1234), 'foo')
318318 mock_timestamp = lambda: "now"
319  filename = bugzilla._filename_for_upload(StringIO.StringIO(), 1234, extension="patch", timestamp=mock_timestamp)
 319 filename = bugzilla._filename_for_upload(StringIO(), 1234, extension="patch", timestamp=mock_timestamp)
320320 self.assertEqual(filename, "bug-1234-now.patch")
321321
322322 def test_commit_queue_flag(self):
251011

Tools/Scripts/webkitpy/common/net/bugzilla/test_expectation_updater.py

@@class TestExpectationUpdater(object):
113113 # FIXME: Investigate the possibility to align what is done there with what single_test_runner is doing.
114114 # In particular the fact of not overwriting the file if content is the same.
115115 def _update_from_generic_attachment(self):
116  for test_name, expected_content in self._tests_to_update(self.generic_attachment).iteritems():
 116 for test_name, expected_content in self._tests_to_update(self.generic_attachment).items():
117117 expected_filename = self.filesystem.join(self.layout_test_repository, TestResultWriter.expected_filename(test_name, self.filesystem))
118118 if expected_content != self._file_content_if_exists(expected_filename):
119119 _log.info("Updating " + test_name + " (" + expected_filename + ")")

@@class TestExpectationUpdater(object):
122122 # FIXME: Investigate the possibility to align what is done there with what single_test_runner is doing.
123123 # In particular the ability to remove a new specific expectation if it is the same as the generic one.
124124 def _update_from_platform_specific_attachment(self, attachment, bot_type):
125  for test_name, expected_content in self._tests_to_update(attachment, bot_type).iteritems():
 125 for test_name, expected_content in self._tests_to_update(attachment, bot_type).items():
126126 expected_filename = self.filesystem.join(self.layout_test_repository, TestResultWriter.expected_filename(test_name, self.filesystem, bot_type))
127127 generic_expected_filename = self.filesystem.join(self.layout_test_repository, TestResultWriter.expected_filename(test_name, self.filesystem))
128128 if expected_content != self._file_content_if_exists(generic_expected_filename):

@@class TestExpectationUpdater(object):
138138 return
139139 if self.generic_attachment:
140140 self._update_from_generic_attachment()
141  for bot_type, attachment in self.platform_specific_attachments.iteritems():
 141 for bot_type, attachment in self.platform_specific_attachments.items():
142142 self._update_from_platform_specific_attachment(attachment, bot_type)
143143
144144
251011

Tools/Scripts/webkitpy/common/net/buildbot/buildbot.py

2929import json
3030import operator
3131import re
 32import sys
3233import urllib
33 import urllib2
3434
3535import webkitpy.common.config.urls as config_urls
3636from webkitpy.common.memoized import memoized

@@from webkitpy.common.net.layouttestresul
3939from webkitpy.common.net.networktransaction import NetworkTransaction
4040from webkitpy.common.net.regressionwindow import RegressionWindow
4141from webkitpy.common.system.logutils import get_logger
42 from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup
4342from webkitpy.thirdparty.autoinstalled.mechanize import Browser
 43from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup
 44
 45if sys.version_info > (3, 0):
 46 from urllib.error import HTTPError, URLError
 47 from urllib.parse import quote
 48 from urllib.request import urlopen
 49else:
 50 from urllib2 import HTTPError, quote, URLError, urlopen
 51
4452
4553
4654_log = get_logger(__file__)

@@class Builder(object):
7482
7583 def _fetch_file_from_results(self, results_url, file_name):
7684 # It seems this can return None if the url redirects and then returns 404.
77  result = urllib2.urlopen("%s/%s" % (results_url, file_name))
 85 result = urlopen("%s/%s" % (results_url, file_name))
7886 if not result:
7987 return None
8088 # urlopen returns a file-like object which sometimes works fine with str()

@@class Builder(object):
161169 print("Loading revision/build list from %s." % self.results_url())
162170 print("This may take a while...")
163171 result_files = self._buildbot._fetch_twisted_directory_listing(self.results_url())
164  except urllib2.HTTPError as error:
 172 except HTTPError as error:
165173 if error.code != 404:
166174 raise
167175 _log.debug("Revision/build list failed to load.")

@@class BuildBot(object):
363371 # find a way to reduce the response size further.
364372 json_url = "%s/json/builders/%s/builds/%s?filter=1" % (self.buildbot_url, urllib.quote(builder.name()), build_number)
365373 try:
366  return json.load(urllib2.urlopen(json_url))
367  except urllib2.URLError as err:
 374 return json.load(urlopen(json_url))
 375 except URLError as err:
368376 build_url = Build.build_url(builder, build_number)
369377 _log.error("Error fetching data for %s build %s (%s, json: %s): %s" % (builder.name(), build_number, build_url, json_url, err))
370378 return None

@@class BuildBot(object):
375383
376384 def _fetch_one_box_per_builder(self):
377385 build_status_url = "%s/one_box_per_builder" % self.buildbot_url
378  return urllib2.urlopen(build_status_url)
 386 return urlopen(build_status_url)
379387
380388 def _file_cell_text(self, file_cell):
381389 """Traverses down through firstChild elements until one containing a string is found, then returns that string"""

@@class BuildBot(object):
402410
403411 # FIXME: There should be a better way to get this information directly from twisted.
404412 def _fetch_twisted_directory_listing(self, url):
405  return self._parse_twisted_directory_listing(urllib2.urlopen(url))
 413 return self._parse_twisted_directory_listing(urlopen(url))
406414
407415 def builders(self):
408416 return [self.builder_with_name(status["name"]) for status in self.builder_statuses()]

@@class BuildBot(object):
444452 build = build.previous_build()
445453
446454 def _fetch_builder_page(self, builder):
447  builder_page_url = "%s/builders/%s?numbuilds=100" % (self.buildbot_url, urllib2.quote(builder.name()))
448  return urllib2.urlopen(builder_page_url)
 455 builder_page_url = "%s/builders/%s?numbuilds=100" % (self.buildbot_url, quote(builder.name()))
 456 return urlopen(builder_page_url)
449457
450458 def _revisions_for_builder(self, builder):
451459 soup = BeautifulSoup(self._fetch_builder_page(builder))
251011

Tools/Scripts/webkitpy/common/system/autoinstall.py

@@class AutoInstaller(object):
483483
484484 def install(self, url, should_refresh=False, target_name=None,
485485 url_subpath=None, files_to_remove=None):
486  """Install a python package from an URL.
 486 """Install a python package from an URL, return False if already installed.
487487
488488 Args:
489489 url: The URL from which to download the package.

@@class AutoInstaller(object):
513513
514514 target_path = os.path.join(self._target_dir, target_name.replace('/', os.sep))
515515 if not should_refresh and self._is_downloaded(target_name, url) and os.path.exists(target_path):
516  return
 516 return False
517517
518518 files_to_remove = files_to_remove or []
519519 package_name = target_name.replace(os.sep, '.')

@@class AutoInstaller(object):
542542 shutil.rmtree(scratch_dir, ignore_errors=True)
543543 _log.debug('Auto-installed %s to:' % url)
544544 _log.debug(' "%s"' % target_path)
 545 return True
251011

Tools/Scripts/webkitpy/common/system/filesystem_mock.py

@@class MockFileSystem(object):
4848 value of None is used to indicate that the file should
4949 not exist.
5050 """
51  self.files = files or {}
 51 self.files = {name: unicode_compatibility.encode_if_necessary(contents) for name, contents in (files or {}).items()}
5252 self.written_files = {}
5353 self.last_tmpdir = None
5454 self.current_tmpno = 0
251011

Tools/Scripts/webkitpy/layout_tests/layout_package/json_results_generator.py

@@import logging
3131import subprocess
3232import sys
3333import time
34 import urllib2
3534import xml.dom.minidom
3635
 36if sys.version_info > (3, 0):
 37 from urllib.error import HTTPError, URLError
 38 from urllib.parse import quote
 39 from urllib.request import urlopen
 40else:
 41 from urllib2 import HTTPError, quote, URLError, urlopen
 42
3743from webkitpy.common.checkout.scm.detection import SCMDetector
3844from webkitpy.common.net.file_uploader import FileUploader
3945

@@class JSONResultsGenerator(object):
465471 return {}, None
466472
467473 results_file_url = (self.URL_FOR_TEST_LIST_JSON %
468  (urllib2.quote(self._test_results_servers[server_index]),
469  urllib2.quote(self._builder_name),
 474 (quote(self._test_results_servers[server_index]),
 475 quote(self._builder_name),
470476 self.RESULTS_FILENAME,
471  urllib2.quote(self._test_type),
472  urllib2.quote(self._master_name)))
 477 quote(self._test_type),
 478 quote(self._master_name)))
473479
474480 try:
475481 # FIXME: We should talk to the network via a Host object.
476  results_file = urllib2.urlopen(results_file_url)
 482 results_file = urlopen(results_file_url)
477483 info = results_file.info()
478484 old_results = results_file.read()
479  except urllib2.HTTPError as http_error:
 485 except HTTPError as http_error:
480486 # A non-4xx status code means the bot is hosed for some reason
481487 # and we can't grab the results.json file off of it.
482488 if (http_error.code < 400 and http_error.code >= 500):
483489 error = http_error
484  except urllib2.URLError as url_error:
 490 except URLError as url_error:
485491 error = url_error
486492
487493 if old_results:
251011

Tools/Scripts/webkitpy/layout_tests/models/test_expectations.py

@@class TestExpectationParser(object):
235235 ]
236236
237237 _configuration_tokens = dict((token, token.upper()) for token in _configuration_tokens_list)
238  _inverted_configuration_tokens = dict((value, name) for name, value in _configuration_tokens.iteritems())
 238 _inverted_configuration_tokens = dict((value, name) for name, value in _configuration_tokens.items())
239239
240240 # FIXME: Update the original modifiers list and remove this once the old syntax is gone.
241241 _expectation_tokens = {

@@class TestExpectationParser(object):
252252 'WontFix': 'WONTFIX',
253253 }
254254
255  _inverted_expectation_tokens = dict([(value, name) for name, value in _expectation_tokens.iteritems()] +
 255 _inverted_expectation_tokens = dict([(value, name) for name, value in _expectation_tokens.items()] +
256256 [('TEXT', 'Failure'), ('IMAGE+TEXT', 'Failure'), ('AUDIO', 'Failure')])
257257
258258 # FIXME: Seems like these should be classmethods on TestExpectationLine instead of TestExpectationParser.
251011

Tools/Scripts/webkitpy/layout_tests/models/test_failures.py

2626# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
2727# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2828
29 import cPickle
3029import logging
 30import sys
 31
 32if sys.version_info > (3, 0):
 33 import pickle
 34else:
 35 import cPickle as pickle
 36
3137
3238from webkitpy.layout_tests.models import test_expectations
3339

@@class TestFailure(object):
9197 @staticmethod
9298 def loads(s):
9399 """Creates a TestFailure object from the specified string."""
94  return cPickle.loads(s)
 100 return pickle.loads(s)
95101
96102 def message(self):
97103 """Returns a string describing the failure in more detail."""

@@class TestFailure(object):
108114
109115 def dumps(self):
110116 """Returns the string/JSON representation of a TestFailure."""
111  return cPickle.dumps(self)
 117 return pickle.dumps(self)
112118
113119 def driver_needs_restart(self):
114120 """Returns True if we should kill DumpRenderTree/WebKitTestRunner before the next test."""
251011

Tools/Scripts/webkitpy/layout_tests/models/test_results.py

2626# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
2727# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2828
29 import cPickle
 29import sys
 30if sys.version_info > (3, 0):
 31 import pickle
 32else:
 33 import cPickle as pickle
3034
3135from webkitpy.layout_tests.models import test_failures
3236

@@class TestResult(object):
3640
3741 @staticmethod
3842 def loads(string):
39  return cPickle.loads(string)
 43 return pickle.loads(string)
4044
4145 def __init__(self, test_name, failures=None, test_run_time=None, has_stderr=False, reftest_type=None, pid=None, references=None):
4246 self.test_name = test_name

@@class TestResult(object):
8993 return False
9094
9195 def dumps(self):
92  return cPickle.dumps(self)
 96 return pickle.dumps(self)
251011

Tools/Scripts/webkitpy/port/darwin_testcase.py

@@class DarwinTest(port_testcase.PortTestC
9494 port = self.make_port()
9595 self.assertEqual(port.path_to_crash_logs(), '/Users/mock/Library/Logs/CrashReporter')
9696
97  host = MockSystemHost(filesystem=MockFileSystem(files=['/Users/mock/Library/Logs/DiagnosticReports/crashlog.crash']))
 97 host = MockSystemHost(filesystem=MockFileSystem(files={'/Users/mock/Library/Logs/DiagnosticReports/crashlog.crash': None}))
9898 port = self.make_port(host)
9999 self.assertEqual(port.path_to_crash_logs(), '/Users/mock/Library/Logs/DiagnosticReports')
100100
251011

Tools/Scripts/webkitpy/test/main.py

@@class Tester(object):
221221 self._options.child_processes = 1
222222
223223 import webkitpy.thirdparty.autoinstalled.coverage as coverage
224  cov = coverage.coverage(omit=["/usr/*", "*/webkitpy/thirdparty/autoinstalled/*", "*/webkitpy/thirdparty/BeautifulSoup.py"])
 224 cov = coverage.coverage(omit=[
 225 "/usr/*",
 226 "*/webkitpy/thirdparty/autoinstalled/*",
 227 "*/webkitpy/thirdparty/BeautifulSoup.py",
 228 "*/webkitpy/thirdparty/BeautifulSoup_legacy.py",
 229 ])
225230 cov.start()
226231
227232 self.printer.write_update("Checking imports ...")
251011

Tools/Scripts/webkitpy/thirdparty/BeautifulSoup.py

1 """Beautiful Soup
2 Elixir and Tonic
3 "The Screen-Scraper's Friend"
4 http://www.crummy.com/software/BeautifulSoup/
 1import sys
52
6 Beautiful Soup parses a (possibly invalid) XML or HTML document into a
7 tree representation. It provides methods and Pythonic idioms that make
8 it easy to navigate, search, and modify the tree.
9 
10 A well-formed XML/HTML document yields a well-formed data
11 structure. An ill-formed XML/HTML document yields a correspondingly
12 ill-formed data structure. If your document is only locally
13 well-formed, you can use this library to find and process the
14 well-formed part of it.
15 
16 Beautiful Soup works with Python 2.2 and up. It has no external
17 dependencies, but you'll have more success at converting data to UTF-8
18 if you also install these three packages:
19 
20 * chardet, for auto-detecting character encodings
21  http://chardet.feedparser.org/
22 * cjkcodecs and iconv_codec, which add more encodings to the ones supported
23  by stock Python.
24  http://cjkpython.i18n.org/
25 
26 Beautiful Soup defines classes for two main parsing strategies:
27 
28  * BeautifulStoneSoup, for parsing XML, SGML, or your domain-specific
29  language that kind of looks like XML.
30 
31  * BeautifulSoup, for parsing run-of-the-mill HTML code, be it valid
32  or invalid. This class has web browser-like heuristics for
33  obtaining a sensible parse tree in the face of common HTML errors.
34 
35 Beautiful Soup also defines a class (UnicodeDammit) for autodetecting
36 the encoding of an HTML or XML document, and converting it to
37 Unicode. Much of this code is taken from Mark Pilgrim's Universal Feed Parser.
38 
39 For more than you ever wanted to know about Beautiful Soup, see the
40 documentation:
41 http://www.crummy.com/software/BeautifulSoup/documentation.html
42 
43 Here, have some legalese:
44 
45 Copyright (c) 2004-2010, Leonard Richardson
46 
47 All rights reserved.
48 
49 Redistribution and use in source and binary forms, with or without
50 modification, are permitted provided that the following conditions are
51 met:
52 
53  * Redistributions of source code must retain the above copyright
54  notice, this list of conditions and the following disclaimer.
55 
56  * Redistributions in binary form must reproduce the above
57  copyright notice, this list of conditions and the following
58  disclaimer in the documentation and/or other materials provided
59  with the distribution.
60 
61  * Neither the name of the the Beautiful Soup Consortium and All
62  Night Kosher Bakery nor the names of its contributors may be
63  used to endorse or promote products derived from this software
64  without specific prior written permission.
65 
66 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
67 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
68 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
69 A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
70 CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
71 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
72 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
73 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
74 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
75 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
76 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE, DAMMIT.
77 
78 """
79 from __future__ import generators
80 
81 __author__ = "Leonard Richardson (leonardr@segfault.org)"
82 __version__ = "3.2.0"
83 __copyright__ = "Copyright (c) 2004-2010 Leonard Richardson"
84 __license__ = "New-style BSD"
85 
86 from sgmllib import SGMLParser, SGMLParseError
87 import codecs
88 import markupbase
89 import types
90 import re
91 import sgmllib
92 try:
93  from htmlentitydefs import name2codepoint
94 except ImportError:
95  name2codepoint = {}
96 try:
97  set
98 except NameError:
99  from sets import Set as set
100 
101 #These hacks make Beautiful Soup able to parse XML with namespaces
102 sgmllib.tagfind = re.compile('[a-zA-Z][-_.:a-zA-Z0-9]*')
103 markupbase._declname_match = re.compile(r'[a-zA-Z][-_.:a-zA-Z0-9]*\s*').match
104 
105 DEFAULT_OUTPUT_ENCODING = "utf-8"
106 
107 def _match_css_class(str):
108  """Build a RE to match the given CSS class."""
109  return re.compile(r"(^|.*\s)%s($|\s)" % str)
110 
111 # First, the classes that represent markup elements.
112 
113 class PageElement(object):
114  """Contains the navigational information for some part of the page
115  (either a tag or a piece of text)"""
116 
117  def setup(self, parent=None, previous=None):
118  """Sets up the initial relations between this element and
119  other elements."""
120  self.parent = parent
121  self.previous = previous
122  self.next = None
123  self.previousSibling = None
124  self.nextSibling = None
125  if self.parent and self.parent.contents:
126  self.previousSibling = self.parent.contents[-1]
127  self.previousSibling.nextSibling = self
128 
129  def replaceWith(self, replaceWith):
130  oldParent = self.parent
131  myIndex = self.parent.index(self)
132  if hasattr(replaceWith, "parent")\
133  and replaceWith.parent is self.parent:
134  # We're replacing this element with one of its siblings.
135  index = replaceWith.parent.index(replaceWith)
136  if index and index < myIndex:
137  # Furthermore, it comes before this element. That
138  # means that when we extract it, the index of this
139  # element will change.
140  myIndex = myIndex - 1
141  self.extract()
142  oldParent.insert(myIndex, replaceWith)
143 
144  def replaceWithChildren(self):
145  myParent = self.parent
146  myIndex = self.parent.index(self)
147  self.extract()
148  reversedChildren = list(self.contents)
149  reversedChildren.reverse()
150  for child in reversedChildren:
151  myParent.insert(myIndex, child)
152 
153  def extract(self):
154  """Destructively rips this element out of the tree."""
155  if self.parent:
156  try:
157  del self.parent.contents[self.parent.index(self)]
158  except ValueError:
159  pass
160 
161  #Find the two elements that would be next to each other if
162  #this element (and any children) hadn't been parsed. Connect
163  #the two.
164  lastChild = self._lastRecursiveChild()
165  nextElement = lastChild.next
166 
167  if self.previous:
168  self.previous.next = nextElement
169  if nextElement:
170  nextElement.previous = self.previous
171  self.previous = None
172  lastChild.next = None
173 
174  self.parent = None
175  if self.previousSibling:
176  self.previousSibling.nextSibling = self.nextSibling
177  if self.nextSibling:
178  self.nextSibling.previousSibling = self.previousSibling
179  self.previousSibling = self.nextSibling = None
180  return self
181 
182  def _lastRecursiveChild(self):
183  "Finds the last element beneath this object to be parsed."
184  lastChild = self
185  while hasattr(lastChild, 'contents') and lastChild.contents:
186  lastChild = lastChild.contents[-1]
187  return lastChild
188 
189  def insert(self, position, newChild):
190  if isinstance(newChild, basestring) \
191  and not isinstance(newChild, NavigableString):
192  newChild = NavigableString(newChild)
193 
194  position = min(position, len(self.contents))
195  if hasattr(newChild, 'parent') and newChild.parent is not None:
196  # We're 'inserting' an element that's already one
197  # of this object's children.
198  if newChild.parent is self:
199  index = self.index(newChild)
200  if index > position:
201  # Furthermore we're moving it further down the
202  # list of this object's children. That means that
203  # when we extract this element, our target index
204  # will jump down one.
205  position = position - 1
206  newChild.extract()
207 
208  newChild.parent = self
209  previousChild = None
210  if position == 0:
211  newChild.previousSibling = None
212  newChild.previous = self
213  else:
214  previousChild = self.contents[position-1]
215  newChild.previousSibling = previousChild
216  newChild.previousSibling.nextSibling = newChild
217  newChild.previous = previousChild._lastRecursiveChild()
218  if newChild.previous:
219  newChild.previous.next = newChild
220 
221  newChildsLastElement = newChild._lastRecursiveChild()
222 
223  if position >= len(self.contents):
224  newChild.nextSibling = None
225 
226  parent = self
227  parentsNextSibling = None
228  while not parentsNextSibling:
229  parentsNextSibling = parent.nextSibling
230  parent = parent.parent
231  if not parent: # This is the last element in the document.
232  break
233  if parentsNextSibling:
234  newChildsLastElement.next = parentsNextSibling
235  else:
236  newChildsLastElement.next = None
237  else:
238  nextChild = self.contents[position]
239  newChild.nextSibling = nextChild
240  if newChild.nextSibling:
241  newChild.nextSibling.previousSibling = newChild
242  newChildsLastElement.next = nextChild
243 
244  if newChildsLastElement.next:
245  newChildsLastElement.next.previous = newChildsLastElement
246  self.contents.insert(position, newChild)
247 
248  def append(self, tag):
249  """Appends the given tag to the contents of this tag."""
250  self.insert(len(self.contents), tag)
251 
252  def findNext(self, name=None, attrs={}, text=None, **kwargs):
253  """Returns the first item that matches the given criteria and
254  appears after this Tag in the document."""
255  return self._findOne(self.findAllNext, name, attrs, text, **kwargs)
256 
257  def findAllNext(self, name=None, attrs={}, text=None, limit=None,
258  **kwargs):
259  """Returns all items that match the given criteria and appear
260  after this Tag in the document."""
261  return self._findAll(name, attrs, text, limit, self.nextGenerator,
262  **kwargs)
263 
264  def findNextSibling(self, name=None, attrs={}, text=None, **kwargs):
265  """Returns the closest sibling to this Tag that matches the
266  given criteria and appears after this Tag in the document."""
267  return self._findOne(self.findNextSiblings, name, attrs, text,
268  **kwargs)
269 
270  def findNextSiblings(self, name=None, attrs={}, text=None, limit=None,
271  **kwargs):
272  """Returns the siblings of this Tag that match the given
273  criteria and appear after this Tag in the document."""
274  return self._findAll(name, attrs, text, limit,
275  self.nextSiblingGenerator, **kwargs)
276  fetchNextSiblings = findNextSiblings # Compatibility with pre-3.x
277 
278  def findPrevious(self, name=None, attrs={}, text=None, **kwargs):
279  """Returns the first item that matches the given criteria and
280  appears before this Tag in the document."""
281  return self._findOne(self.findAllPrevious, name, attrs, text, **kwargs)
282 
283  def findAllPrevious(self, name=None, attrs={}, text=None, limit=None,
284  **kwargs):
285  """Returns all items that match the given criteria and appear
286  before this Tag in the document."""
287  return self._findAll(name, attrs, text, limit, self.previousGenerator,
288  **kwargs)
289  fetchPrevious = findAllPrevious # Compatibility with pre-3.x
290 
291  def findPreviousSibling(self, name=None, attrs={}, text=None, **kwargs):
292  """Returns the closest sibling to this Tag that matches the
293  given criteria and appears before this Tag in the document."""
294  return self._findOne(self.findPreviousSiblings, name, attrs, text,
295  **kwargs)
296 
297  def findPreviousSiblings(self, name=None, attrs={}, text=None,
298  limit=None, **kwargs):
299  """Returns the siblings of this Tag that match the given
300  criteria and appear before this Tag in the document."""
301  return self._findAll(name, attrs, text, limit,
302  self.previousSiblingGenerator, **kwargs)
303  fetchPreviousSiblings = findPreviousSiblings # Compatibility with pre-3.x
304 
305  def findParent(self, name=None, attrs={}, **kwargs):
306  """Returns the closest parent of this Tag that matches the given
307  criteria."""
308  # NOTE: We can't use _findOne because findParents takes a different
309  # set of arguments.
310  r = None
311  l = self.findParents(name, attrs, 1)
312  if l:
313  r = l[0]
314  return r
315 
316  def findParents(self, name=None, attrs={}, limit=None, **kwargs):
317  """Returns the parents of this Tag that match the given
318  criteria."""
319 
320  return self._findAll(name, attrs, None, limit, self.parentGenerator,
321  **kwargs)
322  fetchParents = findParents # Compatibility with pre-3.x
323 
324  #These methods do the real heavy lifting.
325 
326  def _findOne(self, method, name, attrs, text, **kwargs):
327  r = None
328  l = method(name, attrs, text, 1, **kwargs)
329  if l:
330  r = l[0]
331  return r
332 
333  def _findAll(self, name, attrs, text, limit, generator, **kwargs):
334  "Iterates over a generator looking for things that match."
335 
336  if isinstance(name, SoupStrainer):
337  strainer = name
338  # (Possibly) special case some findAll*(...) searches
339  elif text is None and not limit and not attrs and not kwargs:
340  # findAll*(True)
341  if name is True:
342  return [element for element in generator()
343  if isinstance(element, Tag)]
344  # findAll*('tag-name')
345  elif isinstance(name, basestring):
346  return [element for element in generator()
347  if isinstance(element, Tag) and
348  element.name == name]
349  else:
350  strainer = SoupStrainer(name, attrs, text, **kwargs)
351  # Build a SoupStrainer
352  else:
353  strainer = SoupStrainer(name, attrs, text, **kwargs)
354  results = ResultSet(strainer)
355  g = generator()
356  while True:
357  try:
358  i = g.next()
359  except StopIteration:
360  break
361  if i:
362  found = strainer.search(i)
363  if found:
364  results.append(found)
365  if limit and len(results) >= limit:
366  break
367  return results
368 
369  #These Generators can be used to navigate starting from both
370  #NavigableStrings and Tags.
371  def nextGenerator(self):
372  i = self
373  while i is not None:
374  i = i.next
375  yield i
376 
377  def nextSiblingGenerator(self):
378  i = self
379  while i is not None:
380  i = i.nextSibling
381  yield i
382 
383  def previousGenerator(self):
384  i = self
385  while i is not None:
386  i = i.previous
387  yield i
388 
389  def previousSiblingGenerator(self):
390  i = self
391  while i is not None:
392  i = i.previousSibling
393  yield i
394 
395  def parentGenerator(self):
396  i = self
397  while i is not None:
398  i = i.parent
399  yield i
400 
401  # Utility methods
402  def substituteEncoding(self, str, encoding=None):
403  encoding = encoding or "utf-8"
404  return str.replace("%SOUP-ENCODING%", encoding)
405 
406  def toEncoding(self, s, encoding=None):
407  """Encodes an object to a string in some encoding, or to Unicode.
408  ."""
409  if isinstance(s, unicode):
410  if encoding:
411  s = s.encode(encoding)
412  elif isinstance(s, str):
413  if encoding:
414  s = s.encode(encoding)
415  else:
416  s = unicode(s)
417  else:
418  if encoding:
419  s = self.toEncoding(str(s), encoding)
420  else:
421  s = unicode(s)
422  return s
423 
424 class NavigableString(unicode, PageElement):
425 
426  def __new__(cls, value):
427  """Create a new NavigableString.
428 
429  When unpickling a NavigableString, this method is called with
430  the string in DEFAULT_OUTPUT_ENCODING. That encoding needs to be
431  passed in to the superclass's __new__ or the superclass won't know
432  how to handle non-ASCII characters.
433  """
434  if isinstance(value, unicode):
435  return unicode.__new__(cls, value)
436  return unicode.__new__(cls, value, DEFAULT_OUTPUT_ENCODING)
437 
438  def __getnewargs__(self):
439  return (NavigableString.__str__(self),)
440 
441  def __getattr__(self, attr):
442  """text.string gives you text. This is for backwards
443  compatibility for Navigable*String, but for CData* it lets you
444  get the string without the CData wrapper."""
445  if attr == 'string':
446  return self
447  else:
448  raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__.__name__, attr)
449 
450  def __unicode__(self):
451  return str(self).decode(DEFAULT_OUTPUT_ENCODING)
452 
453  def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING):
454  if encoding:
455  return self.encode(encoding)
456  else:
457  return self
458 
459 class CData(NavigableString):
460 
461  def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING):
462  return "<![CDATA[%s]]>" % NavigableString.__str__(self, encoding)
463 
464 class ProcessingInstruction(NavigableString):
465  def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING):
466  output = self
467  if "%SOUP-ENCODING%" in output:
468  output = self.substituteEncoding(output, encoding)
469  return "<?%s?>" % self.toEncoding(output, encoding)
470 
471 class Comment(NavigableString):
472  def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING):
473  return "<!--%s-->" % NavigableString.__str__(self, encoding)
474 
475 class Declaration(NavigableString):
476  def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING):
477  return "<!%s>" % NavigableString.__str__(self, encoding)
478 
479 class Tag(PageElement):
480 
481  """Represents a found HTML tag with its attributes and contents."""
482 
483  def _invert(h):
484  "Cheap function to invert a hash."
485  i = {}
486  for k,v in h.items():
487  i[v] = k
488  return i
489 
490  XML_ENTITIES_TO_SPECIAL_CHARS = { "apos" : "'",
491  "quot" : '"',
492  "amp" : "&",
493  "lt" : "<",
494  "gt" : ">" }
495 
496  XML_SPECIAL_CHARS_TO_ENTITIES = _invert(XML_ENTITIES_TO_SPECIAL_CHARS)
497 
498  def _convertEntities(self, match):
499  """Used in a call to re.sub to replace HTML, XML, and numeric
500  entities with the appropriate Unicode characters. If HTML
501  entities are being converted, any unrecognized entities are
502  escaped."""
503  x = match.group(1)
504  if self.convertHTMLEntities and x in name2codepoint:
505  return unichr(name2codepoint[x])
506  elif x in self.XML_ENTITIES_TO_SPECIAL_CHARS:
507  if self.convertXMLEntities:
508  return self.XML_ENTITIES_TO_SPECIAL_CHARS[x]
509  else:
510  return u'&%s;' % x
511  elif len(x) > 0 and x[0] == '#':
512  # Handle numeric entities
513  if len(x) > 1 and x[1] == 'x':
514  return unichr(int(x[2:], 16))
515  else:
516  return unichr(int(x[1:]))
517 
518  elif self.escapeUnrecognizedEntities:
519  return u'&amp;%s;' % x
520  else:
521  return u'&%s;' % x
522 
523  def __init__(self, parser, name, attrs=None, parent=None,
524  previous=None):
525  "Basic constructor."
526 
527  # We don't actually store the parser object: that lets extracted
528  # chunks be garbage-collected
529  self.parserClass = parser.__class__
530  self.isSelfClosing = parser.isSelfClosingTag(name)
531  self.name = name
532  if attrs is None:
533  attrs = []
534  elif isinstance(attrs, dict):
535  attrs = attrs.items()
536  self.attrs = attrs
537  self.contents = []
538  self.setup(parent, previous)
539  self.hidden = False
540  self.containsSubstitutions = False
541  self.convertHTMLEntities = parser.convertHTMLEntities
542  self.convertXMLEntities = parser.convertXMLEntities
543  self.escapeUnrecognizedEntities = parser.escapeUnrecognizedEntities
544 
545  # Convert any HTML, XML, or numeric entities in the attribute values.
546  convert = lambda(k, val): (k,
547  re.sub("&(#\d+|#x[0-9a-fA-F]+|\w+);",
548  self._convertEntities,
549  val))
550  self.attrs = map(convert, self.attrs)
551 
552  def getString(self):
553  if (len(self.contents) == 1
554  and isinstance(self.contents[0], NavigableString)):
555  return self.contents[0]
556 
557  def setString(self, string):
558  """Replace the contents of the tag with a string"""
559  self.clear()
560  self.append(string)
561 
562  string = property(getString, setString)
563 
564  def getText(self, separator=u""):
565  if not len(self.contents):
566  return u""
567  stopNode = self._lastRecursiveChild().next
568  strings = []
569  current = self.contents[0]
570  while current is not stopNode:
571  if isinstance(current, NavigableString):
572  strings.append(current.strip())
573  current = current.next
574  return separator.join(strings)
575 
576  text = property(getText)
577 
578  def get(self, key, default=None):
579  """Returns the value of the 'key' attribute for the tag, or
580  the value given for 'default' if it doesn't have that
581  attribute."""
582  return self._getAttrMap().get(key, default)
583 
584  def clear(self):
585  """Extract all children."""
586  for child in self.contents[:]:
587  child.extract()
588 
589  def index(self, element):
590  for i, child in enumerate(self.contents):
591  if child is element:
592  return i
593  raise ValueError("Tag.index: element not in tag")
594 
595  def has_key(self, key):
596  return self._getAttrMap().has_key(key)
597 
598  def __getitem__(self, key):
599  """tag[key] returns the value of the 'key' attribute for the tag,
600  and throws an exception if it's not there."""
601  return self._getAttrMap()[key]
602 
603  def __iter__(self):
604  "Iterating over a tag iterates over its contents."
605  return iter(self.contents)
606 
607  def __len__(self):
608  "The length of a tag is the length of its list of contents."
609  return len(self.contents)
610 
611  def __contains__(self, x):
612  return x in self.contents
613 
614  def __nonzero__(self):
615  "A tag is non-None even if it has no contents."
616  return True
617 
618  def __setitem__(self, key, value):
619  """Setting tag[key] sets the value of the 'key' attribute for the
620  tag."""
621  self._getAttrMap()
622  self.attrMap[key] = value
623  found = False
624  for i in range(0, len(self.attrs)):
625  if self.attrs[i][0] == key:
626  self.attrs[i] = (key, value)
627  found = True
628  if not found:
629  self.attrs.append((key, value))
630  self._getAttrMap()[key] = value
631 
632  def __delitem__(self, key):
633  "Deleting tag[key] deletes all 'key' attributes for the tag."
634  for item in self.attrs:
635  if item[0] == key:
636  self.attrs.remove(item)
637  #We don't break because bad HTML can define the same
638  #attribute multiple times.
639  self._getAttrMap()
640  if self.attrMap.has_key(key):
641  del self.attrMap[key]
642 
643  def __call__(self, *args, **kwargs):
644  """Calling a tag like a function is the same as calling its
645  findAll() method. Eg. tag('a') returns a list of all the A tags
646  found within this tag."""
647  return apply(self.findAll, args, kwargs)
648 
649  def __getattr__(self, tag):
650  #print "Getattr %s.%s" % (self.__class__, tag)
651  if len(tag) > 3 and tag.rfind('Tag') == len(tag)-3:
652  return self.find(tag[:-3])
653  elif tag.find('__') != 0:
654  return self.find(tag)
655  raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__, tag)
656 
657  def __eq__(self, other):
658  """Returns true iff this tag has the same name, the same attributes,
659  and the same contents (recursively) as the given tag.
660 
661  NOTE: right now this will return false if two tags have the
662  same attributes in a different order. Should this be fixed?"""
663  if other is self:
664  return True
665  if not hasattr(other, 'name') or not hasattr(other, 'attrs') or not hasattr(other, 'contents') or self.name != other.name or self.attrs != other.attrs or len(self) != len(other):
666  return False
667  for i in range(0, len(self.contents)):
668  if self.contents[i] != other.contents[i]:
669  return False
670  return True
671 
672  def __ne__(self, other):
673  """Returns true iff this tag is not identical to the other tag,
674  as defined in __eq__."""
675  return not self == other
676 
677  def __repr__(self, encoding=DEFAULT_OUTPUT_ENCODING):
678  """Renders this tag as a string."""
679  return self.__str__(encoding)
680 
681  def __unicode__(self):
682  return self.__str__(None)
683 
684  BARE_AMPERSAND_OR_BRACKET = re.compile("([<>]|"
685  + "&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)"
686  + ")")
687 
688  def _sub_entity(self, x):
689  """Used with a regular expression to substitute the
690  appropriate XML entity for an XML special character."""
691  return "&" + self.XML_SPECIAL_CHARS_TO_ENTITIES[x.group(0)[0]] + ";"
692 
693  def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING,
694  prettyPrint=False, indentLevel=0):
695  """Returns a string or Unicode representation of this tag and
696  its contents. To get Unicode, pass None for encoding.
697 
698  NOTE: since Python's HTML parser consumes whitespace, this
699  method is not certain to reproduce the whitespace present in
700  the original string."""
701 
702  encodedName = self.toEncoding(self.name, encoding)
703 
704  attrs = []
705  if self.attrs:
706  for key, val in self.attrs:
707  fmt = '%s="%s"'
708  if isinstance(val, basestring):
709  if self.containsSubstitutions and '%SOUP-ENCODING%' in val:
710  val = self.substituteEncoding(val, encoding)
711 
712  # The attribute value either:
713  #
714  # * Contains no embedded double quotes or single quotes.
715  # No problem: we enclose it in double quotes.
716  # * Contains embedded single quotes. No problem:
717  # double quotes work here too.
718  # * Contains embedded double quotes. No problem:
719  # we enclose it in single quotes.
720  # * Embeds both single _and_ double quotes. This
721  # can't happen naturally, but it can happen if
722  # you modify an attribute value after parsing
723  # the document. Now we have a bit of a
724  # problem. We solve it by enclosing the
725  # attribute in single quotes, and escaping any
726  # embedded single quotes to XML entities.
727  if '"' in val:
728  fmt = "%s='%s'"
729  if "'" in val:
730  # TODO: replace with apos when
731  # appropriate.
732  val = val.replace("'", "&squot;")
733 
734  # Now we're okay w/r/t quotes. But the attribute
735  # value might also contain angle brackets, or
736  # ampersands that aren't part of entities. We need
737  # to escape those to XML entities too.
738  val = self.BARE_AMPERSAND_OR_BRACKET.sub(self._sub_entity, val)
739 
740  attrs.append(fmt % (self.toEncoding(key, encoding),
741  self.toEncoding(val, encoding)))
742  close = ''
743  closeTag = ''
744  if self.isSelfClosing:
745  close = ' /'
746  else:
747  closeTag = '</%s>' % encodedName
748 
749  indentTag, indentContents = 0, 0
750  if prettyPrint:
751  indentTag = indentLevel
752  space = (' ' * (indentTag-1))
753  indentContents = indentTag + 1
754  contents = self.renderContents(encoding, prettyPrint, indentContents)
755  if self.hidden:
756  s = contents
757  else:
758  s = []
759  attributeString = ''
760  if attrs:
761  attributeString = ' ' + ' '.join(attrs)
762  if prettyPrint:
763  s.append(space)
764  s.append('<%s%s%s>' % (encodedName, attributeString, close))
765  if prettyPrint:
766  s.append("\n")
767  s.append(contents)
768  if prettyPrint and contents and contents[-1] != "\n":
769  s.append("\n")
770  if prettyPrint and closeTag:
771  s.append(space)
772  s.append(closeTag)
773  if prettyPrint and closeTag and self.nextSibling:
774  s.append("\n")
775  s = ''.join(s)
776  return s
777 
778  def decompose(self):
779  """Recursively destroys the contents of this tree."""
780  self.extract()
781  if len(self.contents) == 0:
782  return
783  current = self.contents[0]
784  while current is not None:
785  next = current.next
786  if isinstance(current, Tag):
787  del current.contents[:]
788  current.parent = None
789  current.previous = None
790  current.previousSibling = None
791  current.next = None
792  current.nextSibling = None
793  current = next
794 
795  def prettify(self, encoding=DEFAULT_OUTPUT_ENCODING):
796  return self.__str__(encoding, True)
797 
798  def renderContents(self, encoding=DEFAULT_OUTPUT_ENCODING,
799  prettyPrint=False, indentLevel=0):
800  """Renders the contents of this tag as a string in the given
801  encoding. If encoding is None, returns a Unicode string.."""
802  s=[]
803  for c in self:
804  text = None
805  if isinstance(c, NavigableString):
806  text = c.__str__(encoding)
807  elif isinstance(c, Tag):
808  s.append(c.__str__(encoding, prettyPrint, indentLevel))
809  if text and prettyPrint:
810  text = text.strip()
811  if text:
812  if prettyPrint:
813  s.append(" " * (indentLevel-1))
814  s.append(text)
815  if prettyPrint:
816  s.append("\n")
817  return ''.join(s)
818 
819  #Soup methods
820 
821  def find(self, name=None, attrs={}, recursive=True, text=None,
822  **kwargs):
823  """Return only the first child of this Tag matching the given
824  criteria."""
825  r = None
826  l = self.findAll(name, attrs, recursive, text, 1, **kwargs)
827  if l:
828  r = l[0]
829  return r
830  findChild = find
831 
832  def findAll(self, name=None, attrs={}, recursive=True, text=None,
833  limit=None, **kwargs):
834  """Extracts a list of Tag objects that match the given
835  criteria. You can specify the name of the Tag and any
836  attributes you want the Tag to have.
837 
838  The value of a key-value pair in the 'attrs' map can be a
839  string, a list of strings, a regular expression object, or a
840  callable that takes a string and returns whether or not the
841  string matches for some custom definition of 'matches'. The
842  same is true of the tag name."""
843  generator = self.recursiveChildGenerator
844  if not recursive:
845  generator = self.childGenerator
846  return self._findAll(name, attrs, text, limit, generator, **kwargs)
847  findChildren = findAll
848 
849  # Pre-3.x compatibility methods
850  first = find
851  fetch = findAll
852 
853  def fetchText(self, text=None, recursive=True, limit=None):
854  return self.findAll(text=text, recursive=recursive, limit=limit)
855 
856  def firstText(self, text=None, recursive=True):
857  return self.find(text=text, recursive=recursive)
858 
859  #Private methods
860 
861  def _getAttrMap(self):
862  """Initializes a map representation of this tag's attributes,
863  if not already initialized."""
864  if not getattr(self, 'attrMap'):
865  self.attrMap = {}
866  for (key, value) in self.attrs:
867  self.attrMap[key] = value
868  return self.attrMap
869 
870  #Generator methods
871  def childGenerator(self):
872  # Just use the iterator from the contents
873  return iter(self.contents)
874 
875  def recursiveChildGenerator(self):
876  if not len(self.contents):
877  raise StopIteration
878  stopNode = self._lastRecursiveChild().next
879  current = self.contents[0]
880  while current is not stopNode:
881  yield current
882  current = current.next
883 
884 
885 # Next, a couple classes to represent queries and their results.
886 class SoupStrainer:
887  """Encapsulates a number of ways of matching a markup element (tag or
888  text)."""
889 
890  def __init__(self, name=None, attrs={}, text=None, **kwargs):
891  self.name = name
892  if isinstance(attrs, basestring):
893  kwargs['class'] = _match_css_class(attrs)
894  attrs = None
895  if kwargs:
896  if attrs:
897  attrs = attrs.copy()
898  attrs.update(kwargs)
899  else:
900  attrs = kwargs
901  self.attrs = attrs
902  self.text = text
903 
904  def __str__(self):
905  if self.text:
906  return self.text
907  else:
908  return "%s|%s" % (self.name, self.attrs)
909 
910  def searchTag(self, markupName=None, markupAttrs={}):
911  found = None
912  markup = None
913  if isinstance(markupName, Tag):
914  markup = markupName
915  markupAttrs = markup
916  callFunctionWithTagData = callable(self.name) \
917  and not isinstance(markupName, Tag)
918 
919  if (not self.name) \
920  or callFunctionWithTagData \
921  or (markup and self._matches(markup, self.name)) \
922  or (not markup and self._matches(markupName, self.name)):
923  if callFunctionWithTagData:
924  match = self.name(markupName, markupAttrs)
925  else:
926  match = True
927  markupAttrMap = None
928  for attr, matchAgainst in self.attrs.items():
929  if not markupAttrMap:
930  if hasattr(markupAttrs, 'get'):
931  markupAttrMap = markupAttrs
932  else:
933  markupAttrMap = {}
934  for k,v in markupAttrs:
935  markupAttrMap[k] = v
936  attrValue = markupAttrMap.get(attr)
937  if not self._matches(attrValue, matchAgainst):
938  match = False
939  break
940  if match:
941  if markup:
942  found = markup
943  else:
944  found = markupName
945  return found
946 
947  def search(self, markup):
948  #print 'looking for %s in %s' % (self, markup)
949  found = None
950  # If given a list of items, scan it for a text element that
951  # matches.
952  if hasattr(markup, "__iter__") \
953  and not isinstance(markup, Tag):
954  for element in markup:
955  if isinstance(element, NavigableString) \
956  and self.search(element):
957  found = element
958  break
959  # If it's a Tag, make sure its name or attributes match.
960  # Don't bother with Tags if we're searching for text.
961  elif isinstance(markup, Tag):
962  if not self.text:
963  found = self.searchTag(markup)
964  # If it's text, make sure the text matches.
965  elif isinstance(markup, NavigableString) or \
966  isinstance(markup, basestring):
967  if self._matches(markup, self.text):
968  found = markup
969  else:
970  raise Exception, "I don't know how to match against a %s" \
971  % markup.__class__
972  return found
973 
974  def _matches(self, markup, matchAgainst):
975  #print "Matching %s against %s" % (markup, matchAgainst)
976  result = False
977  if matchAgainst is True:
978  result = markup is not None
979  elif callable(matchAgainst):
980  result = matchAgainst(markup)
981  else:
982  #Custom match methods take the tag as an argument, but all
983  #other ways of matching match the tag name as a string.
984  if isinstance(markup, Tag):
985  markup = markup.name
986  if markup and not isinstance(markup, basestring):
987  markup = unicode(markup)
988  #Now we know that chunk is either a string, or None.
989  if hasattr(matchAgainst, 'match'):
990  # It's a regexp object.
991  result = markup and matchAgainst.search(markup)
992  elif hasattr(matchAgainst, '__iter__'): # list-like
993  result = markup in matchAgainst
994  elif hasattr(matchAgainst, 'items'):
995  result = markup.has_key(matchAgainst)
996  elif matchAgainst and isinstance(markup, basestring):
997  if isinstance(markup, unicode):
998  matchAgainst = unicode(matchAgainst)
999  else:
1000  matchAgainst = str(matchAgainst)
1001 
1002  if not result:
1003  result = matchAgainst == markup
1004  return result
1005 
1006 class ResultSet(list):
1007  """A ResultSet is just a list that keeps track of the SoupStrainer
1008  that created it."""
1009  def __init__(self, source):
1010  list.__init__([])
1011  self.source = source
1012 
1013 # Now, some helper functions.
1014 
1015 def buildTagMap(default, *args):
1016  """Turns a list of maps, lists, or scalars into a single map.
1017  Used to build the SELF_CLOSING_TAGS, NESTABLE_TAGS, and
1018  NESTING_RESET_TAGS maps out of lists and partial maps."""
1019  built = {}
1020  for portion in args:
1021  if hasattr(portion, 'items'):
1022  #It's a map. Merge it.
1023  for k,v in portion.items():
1024  built[k] = v
1025  elif hasattr(portion, '__iter__'): # is a list
1026  #It's a list. Map each item to the default.
1027  for k in portion:
1028  built[k] = default
1029  else:
1030  #It's a scalar. Map it to the default.
1031  built[portion] = default
1032  return built
1033 
1034 # Now, the parser classes.
1035 
1036 class BeautifulStoneSoup(Tag, SGMLParser):
1037 
1038  """This class contains the basic parser and search code. It defines
1039  a parser that knows nothing about tag behavior except for the
1040  following:
1041 
1042  You can't close a tag without closing all the tags it encloses.
1043  That is, "<foo><bar></foo>" actually means
1044  "<foo><bar></bar></foo>".
1045 
1046  [Another possible explanation is "<foo><bar /></foo>", but since
1047  this class defines no SELF_CLOSING_TAGS, it will never use that
1048  explanation.]
1049 
1050  This class is useful for parsing XML or made-up markup languages,
1051  or when BeautifulSoup makes an assumption counter to what you were
1052  expecting."""
1053 
1054  SELF_CLOSING_TAGS = {}
1055  NESTABLE_TAGS = {}
1056  RESET_NESTING_TAGS = {}
1057  QUOTE_TAGS = {}
1058  PRESERVE_WHITESPACE_TAGS = []
1059 
1060  MARKUP_MASSAGE = [(re.compile('(<[^<>]*)/>'),
1061  lambda x: x.group(1) + ' />'),
1062  (re.compile('<!\s+([^<>]*)>'),
1063  lambda x: '<!' + x.group(1) + '>')
1064  ]
1065 
1066  ROOT_TAG_NAME = u'[document]'
1067 
1068  HTML_ENTITIES = "html"
1069  XML_ENTITIES = "xml"
1070  XHTML_ENTITIES = "xhtml"
1071  # TODO: This only exists for backwards-compatibility
1072  ALL_ENTITIES = XHTML_ENTITIES
1073 
1074  # Used when determining whether a text node is all whitespace and
1075  # can be replaced with a single space. A text node that contains
1076  # fancy Unicode spaces (usually non-breaking) should be left
1077  # alone.
1078  STRIP_ASCII_SPACES = { 9: None, 10: None, 12: None, 13: None, 32: None, }
1079 
1080  def __init__(self, markup="", parseOnlyThese=None, fromEncoding=None,
1081  markupMassage=True, smartQuotesTo=XML_ENTITIES,
1082  convertEntities=None, selfClosingTags=None, isHTML=False):
1083  """The Soup object is initialized as the 'root tag', and the
1084  provided markup (which can be a string or a file-like object)
1085  is fed into the underlying parser.
1086 
1087  sgmllib will process most bad HTML, and the BeautifulSoup
1088  class has some tricks for dealing with some HTML that kills
1089  sgmllib, but Beautiful Soup can nonetheless choke or lose data
1090  if your data uses self-closing tags or declarations
1091  incorrectly.
1092 
1093  By default, Beautiful Soup uses regexes to sanitize input,
1094  avoiding the vast majority of these problems. If the problems
1095  don't apply to you, pass in False for markupMassage, and
1096  you'll get better performance.
1097 
1098  The default parser massage techniques fix the two most common
1099  instances of invalid HTML that choke sgmllib:
1100 
1101  <br/> (No space between name of closing tag and tag close)
1102  <! --Comment--> (Extraneous whitespace in declaration)
1103 
1104  You can pass in a custom list of (RE object, replace method)
1105  tuples to get Beautiful Soup to scrub your input the way you
1106  want."""
1107 
1108  self.parseOnlyThese = parseOnlyThese
1109  self.fromEncoding = fromEncoding
1110  self.smartQuotesTo = smartQuotesTo
1111  self.convertEntities = convertEntities
1112  # Set the rules for how we'll deal with the entities we
1113  # encounter
1114  if self.convertEntities:
1115  # It doesn't make sense to convert encoded characters to
1116  # entities even while you're converting entities to Unicode.
1117  # Just convert it all to Unicode.
1118  self.smartQuotesTo = None
1119  if convertEntities == self.HTML_ENTITIES:
1120  self.convertXMLEntities = False
1121  self.convertHTMLEntities = True
1122  self.escapeUnrecognizedEntities = True
1123  elif convertEntities == self.XHTML_ENTITIES:
1124  self.convertXMLEntities = True
1125  self.convertHTMLEntities = True
1126  self.escapeUnrecognizedEntities = False
1127  elif convertEntities == self.XML_ENTITIES:
1128  self.convertXMLEntities = True
1129  self.convertHTMLEntities = False
1130  self.escapeUnrecognizedEntities = False
1131  else:
1132  self.convertXMLEntities = False
1133  self.convertHTMLEntities = False
1134  self.escapeUnrecognizedEntities = False
1135 
1136  self.instanceSelfClosingTags = buildTagMap(None, selfClosingTags)
1137  SGMLParser.__init__(self)
1138 
1139  if hasattr(markup, 'read'): # It's a file-type object.
1140  markup = markup.read()
1141  self.markup = markup
1142  self.markupMassage = markupMassage
1143  try:
1144  self._feed(isHTML=isHTML)
1145  except StopParsing:
1146  pass
1147  self.markup = None # The markup can now be GCed
1148 
1149  def convert_charref(self, name):
1150  """This method fixes a bug in Python's SGMLParser."""
1151  try:
1152  n = int(name)
1153  except ValueError:
1154  return
1155  if not 0 <= n <= 127 : # ASCII ends at 127, not 255
1156  return
1157  return self.convert_codepoint(n)
1158 
1159  def _feed(self, inDocumentEncoding=None, isHTML=False):
1160  # Convert the document to Unicode.
1161  markup = self.markup
1162  if isinstance(markup, unicode):
1163  if not hasattr(self, 'originalEncoding'):
1164  self.originalEncoding = None
1165  else:
1166  dammit = UnicodeDammit\
1167  (markup, [self.fromEncoding, inDocumentEncoding],
1168  smartQuotesTo=self.smartQuotesTo, isHTML=isHTML)
1169  markup = dammit.unicode
1170  self.originalEncoding = dammit.originalEncoding
1171  self.declaredHTMLEncoding = dammit.declaredHTMLEncoding
1172  if markup:
1173  if self.markupMassage:
1174  if not hasattr(self.markupMassage, "__iter__"):
1175  self.markupMassage = self.MARKUP_MASSAGE
1176  for fix, m in self.markupMassage:
1177  markup = fix.sub(m, markup)
1178  # TODO: We get rid of markupMassage so that the
1179  # soup object can be deepcopied later on. Some
1180  # Python installations can't copy regexes. If anyone
1181  # was relying on the existence of markupMassage, this
1182  # might cause problems.
1183  del(self.markupMassage)
1184  self.reset()
1185 
1186  SGMLParser.feed(self, markup)
1187  # Close out any unfinished strings and close all the open tags.
1188  self.endData()
1189  while self.currentTag.name != self.ROOT_TAG_NAME:
1190  self.popTag()
1191 
1192  def __getattr__(self, methodName):
1193  """This method routes method call requests to either the SGMLParser
1194  superclass or the Tag superclass, depending on the method name."""
1195  #print "__getattr__ called on %s.%s" % (self.__class__, methodName)
1196 
1197  if methodName.startswith('start_') or methodName.startswith('end_') \
1198  or methodName.startswith('do_'):
1199  return SGMLParser.__getattr__(self, methodName)
1200  elif not methodName.startswith('__'):
1201  return Tag.__getattr__(self, methodName)
1202  else:
1203  raise AttributeError
1204 
1205  def isSelfClosingTag(self, name):
1206  """Returns true iff the given string is the name of a
1207  self-closing tag according to this parser."""
1208  return self.SELF_CLOSING_TAGS.has_key(name) \
1209  or self.instanceSelfClosingTags.has_key(name)
1210 
1211  def reset(self):
1212  Tag.__init__(self, self, self.ROOT_TAG_NAME)
1213  self.hidden = 1
1214  SGMLParser.reset(self)
1215  self.currentData = []
1216  self.currentTag = None
1217  self.tagStack = []
1218  self.quoteStack = []
1219  self.pushTag(self)
1220 
1221  def popTag(self):
1222  tag = self.tagStack.pop()
1223 
1224  #print "Pop", tag.name
1225  if self.tagStack:
1226  self.currentTag = self.tagStack[-1]
1227  return self.currentTag
1228 
1229  def pushTag(self, tag):
1230  #print "Push", tag.name
1231  if self.currentTag:
1232  self.currentTag.contents.append(tag)
1233  self.tagStack.append(tag)
1234  self.currentTag = self.tagStack[-1]
1235 
1236  def endData(self, containerClass=NavigableString):
1237  if self.currentData:
1238  currentData = u''.join(self.currentData)
1239  if (currentData.translate(self.STRIP_ASCII_SPACES) == '' and
1240  not set([tag.name for tag in self.tagStack]).intersection(
1241  self.PRESERVE_WHITESPACE_TAGS)):
1242  if '\n' in currentData:
1243  currentData = '\n'
1244  else:
1245  currentData = ' '
1246  self.currentData = []
1247  if self.parseOnlyThese and len(self.tagStack) <= 1 and \
1248  (not self.parseOnlyThese.text or \
1249  not self.parseOnlyThese.search(currentData)):
1250  return
1251  o = containerClass(currentData)
1252  o.setup(self.currentTag, self.previous)
1253  if self.previous:
1254  self.previous.next = o
1255  self.previous = o
1256  self.currentTag.contents.append(o)
1257 
1258 
1259  def _popToTag(self, name, inclusivePop=True):
1260  """Pops the tag stack up to and including the most recent
1261  instance of the given tag. If inclusivePop is false, pops the tag
1262  stack up to but *not* including the most recent instqance of
1263  the given tag."""
1264  #print "Popping to %s" % name
1265  if name == self.ROOT_TAG_NAME:
1266  return
1267 
1268  numPops = 0
1269  mostRecentTag = None
1270  for i in range(len(self.tagStack)-1, 0, -1):
1271  if name == self.tagStack[i].name:
1272  numPops = len(self.tagStack)-i
1273  break
1274  if not inclusivePop:
1275  numPops = numPops - 1
1276 
1277  for i in range(0, numPops):
1278  mostRecentTag = self.popTag()
1279  return mostRecentTag
1280 
1281  def _smartPop(self, name):
1282 
1283  """We need to pop up to the previous tag of this type, unless
1284  one of this tag's nesting reset triggers comes between this
1285  tag and the previous tag of this type, OR unless this tag is a
1286  generic nesting trigger and another generic nesting trigger
1287  comes between this tag and the previous tag of this type.
1288 
1289  Examples:
1290  <p>Foo<b>Bar *<p>* should pop to 'p', not 'b'.
1291  <p>Foo<table>Bar *<p>* should pop to 'table', not 'p'.
1292  <p>Foo<table><tr>Bar *<p>* should pop to 'tr', not 'p'.
1293 
1294  <li><ul><li> *<li>* should pop to 'ul', not the first 'li'.
1295  <tr><table><tr> *<tr>* should pop to 'table', not the first 'tr'
1296  <td><tr><td> *<td>* should pop to 'tr', not the first 'td'
1297  """
1298 
1299  nestingResetTriggers = self.NESTABLE_TAGS.get(name)
1300  isNestable = nestingResetTriggers != None
1301  isResetNesting = self.RESET_NESTING_TAGS.has_key(name)
1302  popTo = None
1303  inclusive = True
1304  for i in range(len(self.tagStack)-1, 0, -1):
1305  p = self.tagStack[i]
1306  if (not p or p.name == name) and not isNestable:
1307  #Non-nestable tags get popped to the top or to their
1308  #last occurance.
1309  popTo = name
1310  break
1311  if (nestingResetTriggers is not None
1312  and p.name in nestingResetTriggers) \
1313  or (nestingResetTriggers is None and isResetNesting
1314  and self.RESET_NESTING_TAGS.has_key(p.name)):
1315 
1316  #If we encounter one of the nesting reset triggers
1317  #peculiar to this tag, or we encounter another tag
1318  #that causes nesting to reset, pop up to but not
1319  #including that tag.
1320  popTo = p.name
1321  inclusive = False
1322  break
1323  p = p.parent
1324  if popTo:
1325  self._popToTag(popTo, inclusive)
1326 
1327  def unknown_starttag(self, name, attrs, selfClosing=0):
1328  #print "Start tag %s: %s" % (name, attrs)
1329  if self.quoteStack:
1330  #This is not a real tag.
1331  #print "<%s> is not real!" % name
1332  attrs = ''.join([' %s="%s"' % (x, y) for x, y in attrs])
1333  self.handle_data('<%s%s>' % (name, attrs))
1334  return
1335  self.endData()
1336 
1337  if not self.isSelfClosingTag(name) and not selfClosing:
1338  self._smartPop(name)
1339 
1340  if self.parseOnlyThese and len(self.tagStack) <= 1 \
1341  and (self.parseOnlyThese.text or not self.parseOnlyThese.searchTag(name, attrs)):
1342  return
1343 
1344  tag = Tag(self, name, attrs, self.currentTag, self.previous)
1345  if self.previous:
1346  self.previous.next = tag
1347  self.previous = tag
1348  self.pushTag(tag)
1349  if selfClosing or self.isSelfClosingTag(name):
1350  self.popTag()
1351  if name in self.QUOTE_TAGS:
1352  #print "Beginning quote (%s)" % name
1353  self.quoteStack.append(name)
1354  self.literal = 1
1355  return tag
1356 
1357  def unknown_endtag(self, name):
1358  #print "End tag %s" % name
1359  if self.quoteStack and self.quoteStack[-1] != name:
1360  #This is not a real end tag.
1361  #print "</%s> is not real!" % name
1362  self.handle_data('</%s>' % name)
1363  return
1364  self.endData()
1365  self._popToTag(name)
1366  if self.quoteStack and self.quoteStack[-1] == name:
1367  self.quoteStack.pop()
1368  self.literal = (len(self.quoteStack) > 0)
1369 
1370  def handle_data(self, data):
1371  self.currentData.append(data)
1372 
1373  def _toStringSubclass(self, text, subclass):
1374  """Adds a certain piece of text to the tree as a NavigableString
1375  subclass."""
1376  self.endData()
1377  self.handle_data(text)
1378  self.endData(subclass)
1379 
1380  def handle_pi(self, text):
1381  """Handle a processing instruction as a ProcessingInstruction
1382  object, possibly one with a %SOUP-ENCODING% slot into which an
1383  encoding will be plugged later."""
1384  if text[:3] == "xml":
1385  text = u"xml version='1.0' encoding='%SOUP-ENCODING%'"
1386  self._toStringSubclass(text, ProcessingInstruction)
1387 
1388  def handle_comment(self, text):
1389  "Handle comments as Comment objects."
1390  self._toStringSubclass(text, Comment)
1391 
1392  def handle_charref(self, ref):
1393  "Handle character references as data."
1394  if self.convertEntities:
1395  data = unichr(int(ref))
1396  else:
1397  data = '&#%s;' % ref
1398  self.handle_data(data)
1399 
1400  def handle_entityref(self, ref):
1401  """Handle entity references as data, possibly converting known
1402  HTML and/or XML entity references to the corresponding Unicode
1403  characters."""
1404  data = None
1405  if self.convertHTMLEntities:
1406  try:
1407  data = unichr(name2codepoint[ref])
1408  except KeyError:
1409  pass
1410 
1411  if not data and self.convertXMLEntities:
1412  data = self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref)
1413 
1414  if not data and self.convertHTMLEntities and \
1415  not self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref):
1416  # TODO: We've got a problem here. We're told this is
1417  # an entity reference, but it's not an XML entity
1418  # reference or an HTML entity reference. Nonetheless,
1419  # the logical thing to do is to pass it through as an
1420  # unrecognized entity reference.
1421  #
1422  # Except: when the input is "&carol;" this function
1423  # will be called with input "carol". When the input is
1424  # "AT&T", this function will be called with input
1425  # "T". We have no way of knowing whether a semicolon
1426  # was present originally, so we don't know whether
1427  # this is an unknown entity or just a misplaced
1428  # ampersand.
1429  #
1430  # The more common case is a misplaced ampersand, so I
1431  # escape the ampersand and omit the trailing semicolon.
1432  data = "&amp;%s" % ref
1433  if not data:
1434  # This case is different from the one above, because we
1435  # haven't already gone through a supposedly comprehensive
1436  # mapping of entities to Unicode characters. We might not
1437  # have gone through any mapping at all. So the chances are
1438  # very high that this is a real entity, and not a
1439  # misplaced ampersand.
1440  data = "&%s;" % ref
1441  self.handle_data(data)
1442 
1443  def handle_decl(self, data):
1444  "Handle DOCTYPEs and the like as Declaration objects."
1445  self._toStringSubclass(data, Declaration)
1446 
1447  def parse_declaration(self, i):
1448  """Treat a bogus SGML declaration as raw data. Treat a CDATA
1449  declaration as a CData object."""
1450  j = None
1451  if self.rawdata[i:i+9] == '<![CDATA[':
1452  k = self.rawdata.find(']]>', i)
1453  if k == -1:
1454  k = len(self.rawdata)
1455  data = self.rawdata[i+9:k]
1456  j = k+3
1457  self._toStringSubclass(data, CData)
1458  else:
1459  try:
1460  j = SGMLParser.parse_declaration(self, i)
1461  except SGMLParseError:
1462  toHandle = self.rawdata[i:]
1463  self.handle_data(toHandle)
1464  j = i + len(toHandle)
1465  return j
1466 
1467 class BeautifulSoup(BeautifulStoneSoup):
1468 
1469  """This parser knows the following facts about HTML:
1470 
1471  * Some tags have no closing tag and should be interpreted as being
1472  closed as soon as they are encountered.
1473 
1474  * The text inside some tags (ie. 'script') may contain tags which
1475  are not really part of the document and which should be parsed
1476  as text, not tags. If you want to parse the text as tags, you can
1477  always fetch it and parse it explicitly.
1478 
1479  * Tag nesting rules:
1480 
1481  Most tags can't be nested at all. For instance, the occurance of
1482  a <p> tag should implicitly close the previous <p> tag.
1483 
1484  <p>Para1<p>Para2
1485  should be transformed into:
1486  <p>Para1</p><p>Para2
1487 
1488  Some tags can be nested arbitrarily. For instance, the occurance
1489  of a <blockquote> tag should _not_ implicitly close the previous
1490  <blockquote> tag.
1491 
1492  Alice said: <blockquote>Bob said: <blockquote>Blah
1493  should NOT be transformed into:
1494  Alice said: <blockquote>Bob said: </blockquote><blockquote>Blah
1495 
1496  Some tags can be nested, but the nesting is reset by the
1497  interposition of other tags. For instance, a <tr> tag should
1498  implicitly close the previous <tr> tag within the same <table>,
1499  but not close a <tr> tag in another table.
1500 
1501  <table><tr>Blah<tr>Blah
1502  should be transformed into:
1503  <table><tr>Blah</tr><tr>Blah
1504  but,
1505  <tr>Blah<table><tr>Blah
1506  should NOT be transformed into
1507  <tr>Blah<table></tr><tr>Blah
1508 
1509  Differing assumptions about tag nesting rules are a major source
1510  of problems with the BeautifulSoup class. If BeautifulSoup is not
1511  treating as nestable a tag your page author treats as nestable,
1512  try ICantBelieveItsBeautifulSoup, MinimalSoup, or
1513  BeautifulStoneSoup before writing your own subclass."""
1514 
1515  def __init__(self, *args, **kwargs):
1516  if not kwargs.has_key('smartQuotesTo'):
1517  kwargs['smartQuotesTo'] = self.HTML_ENTITIES
1518  kwargs['isHTML'] = True
1519  BeautifulStoneSoup.__init__(self, *args, **kwargs)
1520 
1521  SELF_CLOSING_TAGS = buildTagMap(None,
1522  ('br' , 'hr', 'input', 'img', 'meta',
1523  'spacer', 'link', 'frame', 'base', 'col'))
1524 
1525  PRESERVE_WHITESPACE_TAGS = set(['pre', 'textarea'])
1526 
1527  QUOTE_TAGS = {'script' : None, 'textarea' : None}
1528 
1529  #According to the HTML standard, each of these inline tags can
1530  #contain another tag of the same type. Furthermore, it's common
1531  #to actually use these tags this way.
1532  NESTABLE_INLINE_TAGS = ('span', 'font', 'q', 'object', 'bdo', 'sub', 'sup',
1533  'center')
1534 
1535  #According to the HTML standard, these block tags can contain
1536  #another tag of the same type. Furthermore, it's common
1537  #to actually use these tags this way.
1538  NESTABLE_BLOCK_TAGS = ('blockquote', 'div', 'fieldset', 'ins', 'del')
1539 
1540  #Lists can contain other lists, but there are restrictions.
1541  NESTABLE_LIST_TAGS = { 'ol' : [],
1542  'ul' : [],
1543  'li' : ['ul', 'ol'],
1544  'dl' : [],
1545  'dd' : ['dl'],
1546  'dt' : ['dl'] }
1547 
1548  #Tables can contain other tables, but there are restrictions.
1549  NESTABLE_TABLE_TAGS = {'table' : [],
1550  'tr' : ['table', 'tbody', 'tfoot', 'thead'],
1551  'td' : ['tr'],
1552  'th' : ['tr'],
1553  'thead' : ['table'],
1554  'tbody' : ['table'],
1555  'tfoot' : ['table'],
1556  }
1557 
1558  NON_NESTABLE_BLOCK_TAGS = ('address', 'form', 'p', 'pre')
1559 
1560  #If one of these tags is encountered, all tags up to the next tag of
1561  #this type are popped.
1562  RESET_NESTING_TAGS = buildTagMap(None, NESTABLE_BLOCK_TAGS, 'noscript',
1563  NON_NESTABLE_BLOCK_TAGS,
1564  NESTABLE_LIST_TAGS,
1565  NESTABLE_TABLE_TAGS)
1566 
1567  NESTABLE_TAGS = buildTagMap([], NESTABLE_INLINE_TAGS, NESTABLE_BLOCK_TAGS,
1568  NESTABLE_LIST_TAGS, NESTABLE_TABLE_TAGS)
1569 
1570  # Used to detect the charset in a META tag; see start_meta
1571  CHARSET_RE = re.compile("((^|;)\s*charset=)([^;]*)", re.M)
1572 
1573  def start_meta(self, attrs):
1574  """Beautiful Soup can detect a charset included in a META tag,
1575  try to convert the document to that charset, and re-parse the
1576  document from the beginning."""
1577  httpEquiv = None
1578  contentType = None
1579  contentTypeIndex = None
1580  tagNeedsEncodingSubstitution = False
1581 
1582  for i in range(0, len(attrs)):
1583  key, value = attrs[i]
1584  key = key.lower()
1585  if key == 'http-equiv':
1586  httpEquiv = value
1587  elif key == 'content':
1588  contentType = value
1589  contentTypeIndex = i
1590 
1591  if httpEquiv and contentType: # It's an interesting meta tag.
1592  match = self.CHARSET_RE.search(contentType)
1593  if match:
1594  if (self.declaredHTMLEncoding is not None or
1595  self.originalEncoding == self.fromEncoding):
1596  # An HTML encoding was sniffed while converting
1597  # the document to Unicode, or an HTML encoding was
1598  # sniffed during a previous pass through the
1599  # document, or an encoding was specified
1600  # explicitly and it worked. Rewrite the meta tag.
1601  def rewrite(match):
1602  return match.group(1) + "%SOUP-ENCODING%"
1603  newAttr = self.CHARSET_RE.sub(rewrite, contentType)
1604  attrs[contentTypeIndex] = (attrs[contentTypeIndex][0],
1605  newAttr)
1606  tagNeedsEncodingSubstitution = True
1607  else:
1608  # This is our first pass through the document.
1609  # Go through it again with the encoding information.
1610  newCharset = match.group(3)
1611  if newCharset and newCharset != self.originalEncoding:
1612  self.declaredHTMLEncoding = newCharset
1613  self._feed(self.declaredHTMLEncoding)
1614  raise StopParsing
1615  pass
1616  tag = self.unknown_starttag("meta", attrs)
1617  if tag and tagNeedsEncodingSubstitution:
1618  tag.containsSubstitutions = True
1619 
1620 class StopParsing(Exception):
1621  pass
1622 
1623 class ICantBelieveItsBeautifulSoup(BeautifulSoup):
1624 
1625  """The BeautifulSoup class is oriented towards skipping over
1626  common HTML errors like unclosed tags. However, sometimes it makes
1627  errors of its own. For instance, consider this fragment:
1628 
1629  <b>Foo<b>Bar</b></b>
1630 
1631  This is perfectly valid (if bizarre) HTML. However, the
1632  BeautifulSoup class will implicitly close the first b tag when it
1633  encounters the second 'b'. It will think the author wrote
1634  "<b>Foo<b>Bar", and didn't close the first 'b' tag, because
1635  there's no real-world reason to bold something that's already
1636  bold. When it encounters '</b></b>' it will close two more 'b'
1637  tags, for a grand total of three tags closed instead of two. This
1638  can throw off the rest of your document structure. The same is
1639  true of a number of other tags, listed below.
1640 
1641  It's much more common for someone to forget to close a 'b' tag
1642  than to actually use nested 'b' tags, and the BeautifulSoup class
1643  handles the common case. This class handles the not-co-common
1644  case: where you can't believe someone wrote what they did, but
1645  it's valid HTML and BeautifulSoup screwed up by assuming it
1646  wouldn't be."""
1647 
1648  I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS = \
1649  ('em', 'big', 'i', 'small', 'tt', 'abbr', 'acronym', 'strong',
1650  'cite', 'code', 'dfn', 'kbd', 'samp', 'strong', 'var', 'b',
1651  'big')
1652 
1653  I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS = ('noscript',)
1654 
1655  NESTABLE_TAGS = buildTagMap([], BeautifulSoup.NESTABLE_TAGS,
1656  I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS,
1657  I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS)
1658 
1659 class MinimalSoup(BeautifulSoup):
1660  """The MinimalSoup class is for parsing HTML that contains
1661  pathologically bad markup. It makes no assumptions about tag
1662  nesting, but it does know which tags are self-closing, that
1663  <script> tags contain Javascript and should not be parsed, that
1664  META tags may contain encoding information, and so on.
1665 
1666  This also makes it better for subclassing than BeautifulStoneSoup
1667  or BeautifulSoup."""
1668 
1669  RESET_NESTING_TAGS = buildTagMap('noscript')
1670  NESTABLE_TAGS = {}
1671 
1672 class BeautifulSOAP(BeautifulStoneSoup):
1673  """This class will push a tag with only a single string child into
1674  the tag's parent as an attribute. The attribute's name is the tag
1675  name, and the value is the string child. An example should give
1676  the flavor of the change:
1677 
1678  <foo><bar>baz</bar></foo>
1679  =>
1680  <foo bar="baz"><bar>baz</bar></foo>
1681 
1682  You can then access fooTag['bar'] instead of fooTag.barTag.string.
1683 
1684  This is, of course, useful for scraping structures that tend to
1685  use subelements instead of attributes, such as SOAP messages. Note
1686  that it modifies its input, so don't print the modified version
1687  out.
1688 
1689  I'm not sure how many people really want to use this class; let me
1690  know if you do. Mainly I like the name."""
1691 
1692  def popTag(self):
1693  if len(self.tagStack) > 1:
1694  tag = self.tagStack[-1]
1695  parent = self.tagStack[-2]
1696  parent._getAttrMap()
1697  if (isinstance(tag, Tag) and len(tag.contents) == 1 and
1698  isinstance(tag.contents[0], NavigableString) and
1699  not parent.attrMap.has_key(tag.name)):
1700  parent[tag.name] = tag.contents[0]
1701  BeautifulStoneSoup.popTag(self)
1702 
1703 #Enterprise class names! It has come to our attention that some people
1704 #think the names of the Beautiful Soup parser classes are too silly
1705 #and "unprofessional" for use in enterprise screen-scraping. We feel
1706 #your pain! For such-minded folk, the Beautiful Soup Consortium And
1707 #All-Night Kosher Bakery recommends renaming this file to
1708 #"RobustParser.py" (or, in cases of extreme enterprisiness,
1709 #"RobustParserBeanInterface.class") and using the following
1710 #enterprise-friendly class aliases:
1711 class RobustXMLParser(BeautifulStoneSoup):
1712  pass
1713 class RobustHTMLParser(BeautifulSoup):
1714  pass
1715 class RobustWackAssHTMLParser(ICantBelieveItsBeautifulSoup):
1716  pass
1717 class RobustInsanelyWackAssHTMLParser(MinimalSoup):
1718  pass
1719 class SimplifyingSOAPParser(BeautifulSOAP):
1720  pass
1721 
1722 ######################################################
1723 #
1724 # Bonus library: Unicode, Dammit
1725 #
1726 # This class forces XML data into a standard format (usually to UTF-8
1727 # or Unicode). It is heavily based on code from Mark Pilgrim's
1728 # Universal Feed Parser. It does not rewrite the XML or HTML to
1729 # reflect a new encoding: that happens in BeautifulStoneSoup.handle_pi
1730 # (XML) and BeautifulSoup.start_meta (HTML).
1731 
1732 # Autodetects character encodings.
1733 # Download from http://chardet.feedparser.org/
1734 try:
1735  import chardet
1736 # import chardet.constants
1737 # chardet.constants._debug = 1
1738 except ImportError:
1739  chardet = None
1740 
1741 # cjkcodecs and iconv_codec make Python know about more character encodings.
1742 # Both are available from http://cjkpython.i18n.org/
1743 # They're built in if you use Python 2.4.
1744 try:
1745  import cjkcodecs.aliases
1746 except ImportError:
1747  pass
1748 try:
1749  import iconv_codec
1750 except ImportError:
1751  pass
1752 
1753 class UnicodeDammit:
1754  """A class for detecting the encoding of a *ML document and
1755  converting it to a Unicode string. If the source encoding is
1756  windows-1252, can replace MS smart quotes with their HTML or XML
1757  equivalents."""
1758 
1759  # This dictionary maps commonly seen values for "charset" in HTML
1760  # meta tags to the corresponding Python codec names. It only covers
1761  # values that aren't in Python's aliases and can't be determined
1762  # by the heuristics in find_codec.
1763  CHARSET_ALIASES = { "macintosh" : "mac-roman",
1764  "x-sjis" : "shift-jis" }
1765 
1766  def __init__(self, markup, overrideEncodings=[],
1767  smartQuotesTo='xml', isHTML=False):
1768  self.declaredHTMLEncoding = None
1769  self.markup, documentEncoding, sniffedEncoding = \
1770  self._detectEncoding(markup, isHTML)
1771  self.smartQuotesTo = smartQuotesTo
1772  self.triedEncodings = []
1773  if markup == '' or isinstance(markup, unicode):
1774  self.originalEncoding = None
1775  self.unicode = unicode(markup)
1776  return
1777 
1778  u = None
1779  for proposedEncoding in overrideEncodings:
1780  u = self._convertFrom(proposedEncoding)
1781  if u: break
1782  if not u:
1783  for proposedEncoding in (documentEncoding, sniffedEncoding):
1784  u = self._convertFrom(proposedEncoding)
1785  if u: break
1786 
1787  # If no luck and we have auto-detection library, try that:
1788  if not u and chardet and not isinstance(self.markup, unicode):
1789  u = self._convertFrom(chardet.detect(self.markup)['encoding'])
1790 
1791  # As a last resort, try utf-8 and windows-1252:
1792  if not u:
1793  for proposed_encoding in ("utf-8", "windows-1252"):
1794  u = self._convertFrom(proposed_encoding)
1795  if u: break
1796 
1797  self.unicode = u
1798  if not u: self.originalEncoding = None
1799 
1800  def _subMSChar(self, orig):
1801  """Changes a MS smart quote character to an XML or HTML
1802  entity."""
1803  sub = self.MS_CHARS.get(orig)
1804  if isinstance(sub, tuple):
1805  if self.smartQuotesTo == 'xml':
1806  sub = '&#x%s;' % sub[1]
1807  else:
1808  sub = '&%s;' % sub[0]
1809  return sub
1810 
1811  def _convertFrom(self, proposed):
1812  proposed = self.find_codec(proposed)
1813  if not proposed or proposed in self.triedEncodings:
1814  return None
1815  self.triedEncodings.append(proposed)
1816  markup = self.markup
1817 
1818  # Convert smart quotes to HTML if coming from an encoding
1819  # that might have them.
1820  if self.smartQuotesTo and proposed.lower() in("windows-1252",
1821  "iso-8859-1",
1822  "iso-8859-2"):
1823  markup = re.compile("([\x80-\x9f])").sub \
1824  (lambda(x): self._subMSChar(x.group(1)),
1825  markup)
1826 
1827  try:
1828  # print "Trying to convert document to %s" % proposed
1829  u = self._toUnicode(markup, proposed)
1830  self.markup = u
1831  self.originalEncoding = proposed
1832  except Exception, e:
1833  # print "That didn't work!"
1834  # print e
1835  return None
1836  #print "Correct encoding: %s" % proposed
1837  return self.markup
1838 
1839  def _toUnicode(self, data, encoding):
1840  '''Given a string and its encoding, decodes the string into Unicode.
1841  %encoding is a string recognized by encodings.aliases'''
1842 
1843  # strip Byte Order Mark (if present)
1844  if (len(data) >= 4) and (data[:2] == '\xfe\xff') \
1845  and (data[2:4] != '\x00\x00'):
1846  encoding = 'utf-16be'
1847  data = data[2:]
1848  elif (len(data) >= 4) and (data[:2] == '\xff\xfe') \
1849  and (data[2:4] != '\x00\x00'):
1850  encoding = 'utf-16le'
1851  data = data[2:]
1852  elif data[:3] == '\xef\xbb\xbf':
1853  encoding = 'utf-8'
1854  data = data[3:]
1855  elif data[:4] == '\x00\x00\xfe\xff':
1856  encoding = 'utf-32be'
1857  data = data[4:]
1858  elif data[:4] == '\xff\xfe\x00\x00':
1859  encoding = 'utf-32le'
1860  data = data[4:]
1861  newdata = unicode(data, encoding)
1862  return newdata
1863 
1864  def _detectEncoding(self, xml_data, isHTML=False):
1865  """Given a document, tries to detect its XML encoding."""
1866  xml_encoding = sniffed_xml_encoding = None
1867  try:
1868  if xml_data[:4] == '\x4c\x6f\xa7\x94':
1869  # EBCDIC
1870  xml_data = self._ebcdic_to_ascii(xml_data)
1871  elif xml_data[:4] == '\x00\x3c\x00\x3f':
1872  # UTF-16BE
1873  sniffed_xml_encoding = 'utf-16be'
1874  xml_data = unicode(xml_data, 'utf-16be').encode('utf-8')
1875  elif (len(xml_data) >= 4) and (xml_data[:2] == '\xfe\xff') \
1876  and (xml_data[2:4] != '\x00\x00'):
1877  # UTF-16BE with BOM
1878  sniffed_xml_encoding = 'utf-16be'
1879  xml_data = unicode(xml_data[2:], 'utf-16be').encode('utf-8')
1880  elif xml_data[:4] == '\x3c\x00\x3f\x00':
1881  # UTF-16LE
1882  sniffed_xml_encoding = 'utf-16le'
1883  xml_data = unicode(xml_data, 'utf-16le').encode('utf-8')
1884  elif (len(xml_data) >= 4) and (xml_data[:2] == '\xff\xfe') and \
1885  (xml_data[2:4] != '\x00\x00'):
1886  # UTF-16LE with BOM
1887  sniffed_xml_encoding = 'utf-16le'
1888  xml_data = unicode(xml_data[2:], 'utf-16le').encode('utf-8')
1889  elif xml_data[:4] == '\x00\x00\x00\x3c':
1890  # UTF-32BE
1891  sniffed_xml_encoding = 'utf-32be'
1892  xml_data = unicode(xml_data, 'utf-32be').encode('utf-8')
1893  elif xml_data[:4] == '\x3c\x00\x00\x00':
1894  # UTF-32LE
1895  sniffed_xml_encoding = 'utf-32le'
1896  xml_data = unicode(xml_data, 'utf-32le').encode('utf-8')
1897  elif xml_data[:4] == '\x00\x00\xfe\xff':
1898  # UTF-32BE with BOM
1899  sniffed_xml_encoding = 'utf-32be'
1900  xml_data = unicode(xml_data[4:], 'utf-32be').encode('utf-8')
1901  elif xml_data[:4] == '\xff\xfe\x00\x00':
1902  # UTF-32LE with BOM
1903  sniffed_xml_encoding = 'utf-32le'
1904  xml_data = unicode(xml_data[4:], 'utf-32le').encode('utf-8')
1905  elif xml_data[:3] == '\xef\xbb\xbf':
1906  # UTF-8 with BOM
1907  sniffed_xml_encoding = 'utf-8'
1908  xml_data = unicode(xml_data[3:], 'utf-8').encode('utf-8')
1909  else:
1910  sniffed_xml_encoding = 'ascii'
1911  pass
1912  except:
1913  xml_encoding_match = None
1914  xml_encoding_match = re.compile(
1915  '^<\?.*encoding=[\'"](.*?)[\'"].*\?>').match(xml_data)
1916  if not xml_encoding_match and isHTML:
1917  regexp = re.compile('<\s*meta[^>]+charset=([^>]*?)[;\'">]', re.I)
1918  xml_encoding_match = regexp.search(xml_data)
1919  if xml_encoding_match is not None:
1920  xml_encoding = xml_encoding_match.groups()[0].lower()
1921  if isHTML:
1922  self.declaredHTMLEncoding = xml_encoding
1923  if sniffed_xml_encoding and \
1924  (xml_encoding in ('iso-10646-ucs-2', 'ucs-2', 'csunicode',
1925  'iso-10646-ucs-4', 'ucs-4', 'csucs4',
1926  'utf-16', 'utf-32', 'utf_16', 'utf_32',
1927  'utf16', 'u16')):
1928  xml_encoding = sniffed_xml_encoding
1929  return xml_data, xml_encoding, sniffed_xml_encoding
1930 
1931 
1932  def find_codec(self, charset):
1933  return self._codec(self.CHARSET_ALIASES.get(charset, charset)) \
1934  or (charset and self._codec(charset.replace("-", ""))) \
1935  or (charset and self._codec(charset.replace("-", "_"))) \
1936  or charset
1937 
1938  def _codec(self, charset):
1939  if not charset: return charset
1940  codec = None
1941  try:
1942  codecs.lookup(charset)
1943  codec = charset
1944  except (LookupError, ValueError):
1945  pass
1946  return codec
1947 
1948  EBCDIC_TO_ASCII_MAP = None
1949  def _ebcdic_to_ascii(self, s):
1950  c = self.__class__
1951  if not c.EBCDIC_TO_ASCII_MAP:
1952  emap = (0,1,2,3,156,9,134,127,151,141,142,11,12,13,14,15,
1953  16,17,18,19,157,133,8,135,24,25,146,143,28,29,30,31,
1954  128,129,130,131,132,10,23,27,136,137,138,139,140,5,6,7,
1955  144,145,22,147,148,149,150,4,152,153,154,155,20,21,158,26,
1956  32,160,161,162,163,164,165,166,167,168,91,46,60,40,43,33,
1957  38,169,170,171,172,173,174,175,176,177,93,36,42,41,59,94,
1958  45,47,178,179,180,181,182,183,184,185,124,44,37,95,62,63,
1959  186,187,188,189,190,191,192,193,194,96,58,35,64,39,61,34,
1960  195,97,98,99,100,101,102,103,104,105,196,197,198,199,200,
1961  201,202,106,107,108,109,110,111,112,113,114,203,204,205,
1962  206,207,208,209,126,115,116,117,118,119,120,121,122,210,
1963  211,212,213,214,215,216,217,218,219,220,221,222,223,224,
1964  225,226,227,228,229,230,231,123,65,66,67,68,69,70,71,72,
1965  73,232,233,234,235,236,237,125,74,75,76,77,78,79,80,81,
1966  82,238,239,240,241,242,243,92,159,83,84,85,86,87,88,89,
1967  90,244,245,246,247,248,249,48,49,50,51,52,53,54,55,56,57,
1968  250,251,252,253,254,255)
1969  import string
1970  c.EBCDIC_TO_ASCII_MAP = string.maketrans( \
1971  ''.join(map(chr, range(256))), ''.join(map(chr, emap)))
1972  return s.translate(c.EBCDIC_TO_ASCII_MAP)
1973 
1974  MS_CHARS = { '\x80' : ('euro', '20AC'),
1975  '\x81' : ' ',
1976  '\x82' : ('sbquo', '201A'),
1977  '\x83' : ('fnof', '192'),
1978  '\x84' : ('bdquo', '201E'),
1979  '\x85' : ('hellip', '2026'),
1980  '\x86' : ('dagger', '2020'),
1981  '\x87' : ('Dagger', '2021'),
1982  '\x88' : ('circ', '2C6'),
1983  '\x89' : ('permil', '2030'),
1984  '\x8A' : ('Scaron', '160'),
1985  '\x8B' : ('lsaquo', '2039'),
1986  '\x8C' : ('OElig', '152'),
1987  '\x8D' : '?',
1988  '\x8E' : ('#x17D', '17D'),
1989  '\x8F' : '?',
1990  '\x90' : '?',
1991  '\x91' : ('lsquo', '2018'),
1992  '\x92' : ('rsquo', '2019'),
1993  '\x93' : ('ldquo', '201C'),
1994  '\x94' : ('rdquo', '201D'),
1995  '\x95' : ('bull', '2022'),
1996  '\x96' : ('ndash', '2013'),
1997  '\x97' : ('mdash', '2014'),
1998  '\x98' : ('tilde', '2DC'),
1999  '\x99' : ('trade', '2122'),
2000  '\x9a' : ('scaron', '161'),
2001  '\x9b' : ('rsaquo', '203A'),
2002  '\x9c' : ('oelig', '153'),
2003  '\x9d' : '?',
2004  '\x9e' : ('#x17E', '17E'),
2005  '\x9f' : ('Yuml', ''),}
2006 
2007 #######################################################################
2008 
2009 
2010 #By default, act as an HTML pretty-printer.
2011 if __name__ == '__main__':
2012  import sys
2013  soup = BeautifulSoup(sys.stdin)
2014  print soup.prettify()
 3if sys.version_info > (3, 0):
 4 import html5lib
 5 import webkitpy.thirdparty.autoinstalled.bs4 as bs4
 6 from webkitpy.thirdparty.autoinstalled.bs4.builder import builder_registry, TreeBuilder
 7 from webkitpy.thirdparty.autoinstalled.bs4.builder._htmlparser import HTMLParserTreeBuilder
 8
 9 class BeautifulSoup(bs4.BeautifulSoup):
 10 HTML_ENTITIES = 'html'
 11
 12 def __init__(self, *args, **kwargs):
 13 if 'convertEntities' in kwargs:
 14 del kwargs['convertEntities']
 15 if 'parseOnlyThese' in kwargs:
 16 kwargs['parse_only'] = kwargs['parseOnlyThese']
 17 del kwargs['parseOnlyThese']
 18 kwargs['features'] = self.HTML_ENTITIES
 19 kwargs['builder'] = HTMLParserTreeBuilder
 20 super(BeautifulSoup, self).__init__(*args, **kwargs)
 21
 22 class BeautifulStoneSoup(bs4.BeautifulSoup):
 23 XML_ENTITIES = 'xml'
 24
 25 def __init__(self, *args, **kwargs):
 26 if 'convertEntities' in kwargs:
 27 del kwargs['convertEntities']
 28 kwargs['features'] = self.XML_ENTITIES
 29
 30 # This hack isn't strictly accurate, but getting lxml to work with the autoinstaller is a non-trivial task
 31 kwargs['builder'] = HTMLParserTreeBuilder
 32 super(BeautifulStoneSoup, self).__init__(*args, **kwargs)
 33
 34 SoupStrainer = bs4.SoupStrainer
 35else:
 36 from webkitpy.thirdparty.BeautifulSoup_legacy import BeautifulSoup, BeautifulStoneSoup, SoupStrainer
251011

Tools/Scripts/webkitpy/thirdparty/BeautifulSoup_legacy.py

251011

Tools/Scripts/webkitpy/thirdparty/__init__.py

@@from collections import namedtuple
4242from distutils import spawn
4343from webkitpy.common.system.autoinstall import AutoInstaller
4444from webkitpy.common.system.filesystem import FileSystem
 45from webkitpy.common.system.executive import Executive
4546
4647_THIRDPARTY_DIR = os.path.dirname(__file__)
4748_AUTOINSTALLED_DIR = os.path.join(_THIRDPARTY_DIR, "autoinstalled")

@@if not fs.exists(readme_path):
7778
7879
7980class AutoinstallImportHook(object):
80  def __init__(self, filesystem=None):
 81 def __init__(self, filesystem=None, executive=None):
8182 self._fs = filesystem or FileSystem()
 83 self._executive = executive or Executive()
8284
8385 def _ensure_autoinstalled_dir_is_in_sys_path(self):
8486 # Some packages require that the are being put somewhere under a directory in sys.path.

@@class AutoinstallImportHook(object):
124126 self._install_pytest()
125127 elif '.requests' in fullname:
126128 self._install_requests()
 129 elif '.bs4' in fullname:
 130 self._install_beautifulsoup()
127131
128132 def _install_six(self):
129133 self._install("https://files.pythonhosted.org/packages/16/d8/bc6316cf98419719bd59c91742194c111b6f2e85abac88e496adefaf7afe/six-1.11.0.tar.gz",

@@class AutoinstallImportHook(object):
196200 self._install("https://files.pythonhosted.org/packages/01/62/ddcf76d1d19885e8579acb1b1df26a852b03472c0e46d2b959a714c90608/requests-2.22.0.tar.gz",
197201 "requests-2.22.0/requests")
198202
 203 def _install_beautifulsoup(self):
 204 if sys.version_info < (3, 0):
 205 return
 206
 207 self._install_requests()
 208 self._ensure_autoinstalled_dir_is_in_sys_path()
 209 self._install("https://files.pythonhosted.org/packages/7f/4e/95a13527e18b6f1a15c93f1c634b86d5fa634c5619dce695f4e0cd68182f/soupsieve-1.9.4.tar.gz",
 210 "soupsieve-1.9.4/soupsieve")
 211 if self._install("https://files.pythonhosted.org/packages/86/cd/495c68f0536dcd25f016e006731ba7be72e072280305ec52590012c1e6f2/beautifulsoup4-4.8.1.tar.gz",
 212 "beautifulsoup4-4.8.1/bs4"):
 213 self._executive.run_command(['2to3', '-w', self._fs.join(_AUTOINSTALLED_DIR, 'bs4')])
 214
 215
199216 def _install_pylint(self):
200217 self._ensure_autoinstalled_dir_is_in_sys_path()
201218 if (not self._fs.exists(self._fs.join(_AUTOINSTALLED_DIR, "pylint")) or

@@class AutoinstallImportHook(object):
291308
292309 def _install(self, url, url_subpath=None, target_name=None):
293310 installer = AutoInstaller(target_dir=_AUTOINSTALLED_DIR)
294  installer.install(url=url, url_subpath=url_subpath, target_name=target_name)
 311 return installer.install(url=url, url_subpath=url_subpath, target_name=target_name)
295312
296313 def get_latest_pypi_url(self, package_name, url_subpath_format='{name}-{version}/{lname}'):
297314 json_url = "https://pypi.org/pypi/%s/json" % package_name
251011

Tools/Scripts/webkitpy/thirdparty/mock.py

@@class Mock(object):
118118 self.call_count = 0
119119 self.call_args_list = []
120120 self.method_calls = []
121  for child in self._children.itervalues():
 121 for child in self._children.values():
122122 child.reset_mock()
123123 if isinstance(self._return_value, Mock):
124124 self._return_value.reset_mock()
251011