Merge lp:~widelands-dev/widelands-website/wlwebsite_py36 into lp:widelands-website

Proposed by kaputtnik
Status: Merged
Merged at revision: 540
Proposed branch: lp:~widelands-dev/widelands-website/wlwebsite_py36
Merge into: lp:widelands-website
Diff against target: 8078 lines (+2625/-2509)
141 files modified
README.txt (+11/-11)
check_input/migrations/0001_initial.py (+1/-1)
check_input/models.py (+1/-1)
documentation/conf.py (+2/-2)
documentation/management/commands/create_docs.py (+1/-1)
local_settings.py.sample (+0/-2)
mainpage/admin.py (+2/-2)
mainpage/online_users_middleware.py (+1/-1)
mainpage/settings.py (+15/-15)
mainpage/sitemap_urls.py (+1/-1)
mainpage/templatetags/wl_markdown.py (+8/-8)
mainpage/urls.py (+1/-1)
mainpage/utest/test_wl_markdown.py (+67/-67)
mainpage/views.py (+7/-5)
mainpage/wlwebsite_wsgi.py (+1/-1)
news/migrations/0001_initial.py (+1/-1)
news/migrations/0002_auto_20170417_1857.py (+1/-1)
news/models.py (+4/-4)
news/templatetags/news_extras.py (+2/-2)
notification/engine.py (+5/-3)
notification/lockfile.py (+3/-3)
notification/migrations/0001_initial.py (+1/-1)
notification/migrations/0002_auto_20170417_1857.py (+1/-1)
notification/migrations/0003_auto_20190409_0924.py (+1/-1)
notification/models.py (+14/-12)
notification/views.py (+1/-1)
privacy_policy/admin.py (+1/-1)
privacy_policy/apps.py (+1/-1)
privacy_policy/migrations/0001_initial.py (+1/-1)
privacy_policy/models.py (+2/-2)
privacy_policy/views.py (+1/-1)
pybb/lib/phpserialize.py (+10/-10)
pybb/management/__init__.py (+1/-1)
pybb/management/commands/pybb_resave_post.py (+1/-1)
pybb/management/pybb_notifications.py (+1/-1)
pybb/markups/postmarkup.py (+125/-125)
pybb/migrations/0001_initial.py (+1/-1)
pybb/migrations/0002_auto_20161001_2046.py (+1/-1)
pybb/migrations/0003_remove_post_user_ip.py (+1/-1)
pybb/migrations/0004_auto_20181209_1334.py (+1/-1)
pybb/migrations/0005_auto_20181221_1047.py (+1/-1)
pybb/models.py (+8/-8)
pybb/orm.py (+2/-2)
pybb/templates/pybb/last_posts.html (+1/-1)
pybb/templatetags/pybb_extras.py (+3/-3)
pybb/util.py (+4/-4)
pybb/views.py (+2/-2)
python3.txt (+104/-0)
threadedcomments/admin.py (+1/-1)
threadedcomments/migrations/0001_initial.py (+1/-1)
threadedcomments/migrations/0002_auto_20181003_1238.py (+1/-1)
threadedcomments/models.py (+2/-2)
threadedcomments/templatetags/gravatar.py (+10/-10)
threadedcomments/templatetags/threadedcommentstags.py (+19/-19)
threadedcomments/utils.py (+2/-2)
threadedcomments/views.py (+1/-1)
widelandslib/make_flow_diagram.py (+26/-26)
widelandslib/test/test_conf.py (+1/-1)
widelandslib/test/test_map.py (+3/-3)
widelandslib/tribe.py (+13/-14)
wiki/diff_match_patch.py (+1872/-1891)
wiki/feeds.py (+2/-2)
wiki/management.py (+1/-1)
wiki/migrations/0001_initial.py (+1/-1)
wiki/migrations/0002_auto_20161218_1056.py (+1/-1)
wiki/migrations/0003_auto_20180918_0836.py (+1/-1)
wiki/models.py (+28/-28)
wiki/static/css/wiki.css (+13/-0)
wiki/templatetags/restructuredtext.py (+1/-1)
wiki/templatetags/wiki_extras.py (+2/-2)
wiki/views.py (+2/-2)
wlevents/admin.py (+1/-1)
wlevents/migrations/0001_initial.py (+1/-1)
wlevents/templatetags/wlevents_extras.py (+1/-1)
wlevents/tests.py (+1/-1)
wlggz/admin.py (+1/-1)
wlggz/forms.py (+2/-2)
wlggz/migrations/0001_initial.py (+1/-1)
wlggz/migrations/0002_auto_20160805_2004.py (+1/-1)
wlggz/urls.py (+1/-1)
wlggz/views.py (+1/-1)
wlhelp/management/commands/update_help.py (+37/-36)
wlhelp/management/commands/update_help_pdf.py (+6/-6)
wlhelp/migrations/0001_initial.py (+1/-1)
wlhelp/migrations/0002_auto_20190410_1734.py (+1/-1)
wlhelp/models.py (+11/-11)
wlhelp/tests.py (+1/-1)
wlhelp/urls.py (+1/-1)
wlimages/admin.py (+2/-2)
wlimages/forms.py (+1/-1)
wlimages/migrations/0001_initial.py (+1/-1)
wlimages/migrations/0002_remove_image_url.py (+1/-1)
wlimages/migrations/0003_remove_image_editor_ip.py (+1/-1)
wlimages/models.py (+1/-1)
wlimages/templatetags/wlimages_extras.py (+3/-3)
wlimages/tests.py (+4/-4)
wlimages/urls.py (+1/-1)
wlimages/views.py (+3/-3)
wlmaps/admin.py (+1/-1)
wlmaps/management.py (+1/-1)
wlmaps/migrations/0001_initial.py (+1/-1)
wlmaps/migrations/0002_auto_20181119_1855.py (+1/-1)
wlmaps/models.py (+2/-2)
wlmaps/tests/__init__.py (+2/-2)
wlmaps/urls.py (+2/-2)
wlmaps/views.py (+2/-2)
wlpoll/admin.py (+1/-1)
wlpoll/migrations/0001_initial.py (+1/-1)
wlpoll/models.py (+3/-3)
wlpoll/templatetags/wlpoll_extras.py (+3/-3)
wlpoll/tests.py (+1/-1)
wlpoll/urls.py (+1/-1)
wlpoll/views.py (+1/-1)
wlprofile/admin.py (+1/-1)
wlprofile/fields.py (+4/-4)
wlprofile/forms.py (+1/-1)
wlprofile/gravatar.py (+6/-6)
wlprofile/management/commands/profile_fetch_gravatars.py (+2/-2)
wlprofile/migrations/0001_initial.py (+1/-1)
wlprofile/migrations/0002_profile_deleted.py (+1/-1)
wlprofile/models.py (+1/-1)
wlprofile/templatetags/custom_date.py (+34/-19)
wlprofile/templatetags/wlprofile_extras.py (+1/-1)
wlprofile/tests.py (+6/-6)
wlprofile/urls.py (+1/-1)
wlprofile/views.py (+1/-1)
wlscheduling/migrations/0001_initial.py (+1/-1)
wlscheduling/urls.py (+1/-1)
wlscheduling/views.py (+1/-1)
wlscreens/admin.py (+1/-1)
wlscreens/migrations/0001_initial.py (+1/-1)
wlscreens/migrations/0002_auto_20190410_1737.py (+1/-1)
wlscreens/models.py (+8/-9)
wlscreens/tests/__init__.py (+2/-2)
wlscreens/tests/test_models.py (+1/-1)
wlscreens/urls.py (+2/-2)
wlscreens/views.py (+1/-1)
wlsearch/urls.py (+1/-1)
wlsearch/views.py (+2/-2)
wlwebchat/tests.py (+1/-1)
wlwebchat/urls.py (+1/-1)
To merge this branch: bzr merge lp:~widelands-dev/widelands-website/wlwebsite_py36
Reviewer Review Type Date Requested Status
GunChleoc Approve
Review via email: mp+368589@code.launchpad.net

Commit message

Adapted code for use with python3.6

Description of the change

Changes need to get the website running with python 3.6. Because python2.7 is EOL in year 2020 i did not spend any time to make the code python2.7 compatible. So installing the website needs a virtualenvironment created with python 3.6.

Main changes (maybe interesting parts are linked):
 - after running the python 2to3 script: https://bazaar.launchpad.net/~widelands-dev/widelands-website/wlwebsite_py36/revision/533
 - changes regarding to django (mostly replace __unicode__ with __str__)
 - changes regarding str objects, which are bytes objects in python3
 - same for replacing StringIo -> BytesIO when working with images
 - added a python3 compatible version of diff_match_patch.py and applied individual coloring like it is right now
 - fixed template filter 'minutes', renamed it to 'elapsed_time' and applied output of day(s), https://bazaar.launchpad.net/~widelands-dev/widelands-website/wlwebsite_py36/revision/550
 - the url /locale/ shows now also if the environment variable DISPLAY is set
 - after installing on the new server the file README got the needed changes

The added text file 'python3.txt' contain my test results and can be removed if we finally merge this changes into trunk.

I want to merge this before i set up the productive website on the new server.

To post a comment you must log in.
562. By kaputtnik

fixed a failure for queued notification mails

563. By kaputtnik

use base64 instead of codecs

Revision history for this message
GunChleoc (gunchleoc) wrote :

Let's have it :)

We should document the test protocol in the Python3.txt file on our wiki too - it will come in useful for any big website changes that need testing on alpha.

review: Approve
Revision history for this message
kaputtnik (franku) wrote :

Thanks :-)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'README.txt'
2--- README.txt 2019-04-08 05:55:07 +0000
3+++ README.txt 2019-06-14 07:22:45 +0000
4@@ -1,6 +1,14 @@
5 Installing the homepage
6 =======================
7
8+Used python version
9+-------------------
10+The website is tested with python 3.6. This README reflects setting up the
11+website with this python version.
12+
13+Install prerequisites
14+---------------------
15+
16 Getting the homepage to run locally is best supported using virtualenv and
17 pip. Install those two tools first, either via easy_install or via your local
18 package manager. You will also need development tools (gcc or therelike), hg
19@@ -11,16 +19,8 @@
20 Example:
21 On Ubuntu, installing all required tools and dependencies in two commands:
22
23- $ sudo apt-get install python-dev python-virtualenv python-pip mercurial bzr subversion git-core sqlite3
24- $ sudo apt-get build-dep python-numpy
25-
26-Used python version
27--------------------
28-
29-Currently, the website depends on python 2.7. In case you have python 3 as default (like on arch-linux),
30-you have to adjust the python relevant commands to use python 2.7. E.g. 'virtualenv2 wlwebsite' creates
31-a virtualenvironment using python 2.7. If the virtualenvironment is activated, python 2.7 will become
32-standard for executing python code in this shell.
33+ $ sudo apt-get install python3-virtualenv python3-pip bzr libmysqlclient-dev
34+ $ sudo apt-get build-dep python3-numpy
35
36 Setting up the local environment
37 --------------------------------
38@@ -33,7 +33,7 @@
39 packages from your global site packages. Very important!
40 Now, we create and activate our environment:
41
42- $ virtualenv wlwebsite
43+ $ virtualenv --python=python3.6 wlwebsite
44 $ cd wlwebsite
45 $ source bin/activate
46
47
48=== modified file 'check_input/migrations/0001_initial.py'
49--- check_input/migrations/0001_initial.py 2017-11-24 10:55:59 +0000
50+++ check_input/migrations/0001_initial.py 2019-06-14 07:22:45 +0000
51@@ -1,5 +1,5 @@
52 # -*- coding: utf-8 -*-
53-from __future__ import unicode_literals
54+
55
56 from django.db import models, migrations
57 from django.conf import settings
58
59=== modified file 'check_input/models.py'
60--- check_input/models.py 2018-04-26 21:10:19 +0000
61+++ check_input/models.py 2019-06-14 07:22:45 +0000
62@@ -31,7 +31,7 @@
63 ordering = ['content_type_id']
64 default_permissions = ('change', 'delete',)
65
66- def __unicode__(self):
67+ def __str__(self):
68 return self.text
69
70 def clean(self):
71
72=== modified file 'documentation/conf.py'
73--- documentation/conf.py 2018-11-27 17:01:42 +0000
74+++ documentation/conf.py 2019-06-14 07:22:45 +0000
75@@ -41,8 +41,8 @@
76 master_doc = 'index'
77
78 # General information about the project.
79-project = u'Widelands'
80-copyright = u'The Widelands Development Team'
81+project = 'Widelands'
82+copyright = 'The Widelands Development Team'
83
84 # The version info for the project you're documenting, acts as replacement for
85 # |version| and |release|, also used in various other places throughout the
86
87=== modified file 'documentation/management/commands/create_docs.py'
88--- documentation/management/commands/create_docs.py 2018-05-13 09:05:21 +0000
89+++ documentation/management/commands/create_docs.py 2019-06-14 07:22:45 +0000
90@@ -10,7 +10,7 @@
91
92 """
93
94-from __future__ import print_function
95+
96 from django.core.management.base import BaseCommand, CommandError
97 from django.conf import settings
98 from subprocess import check_call, CalledProcessError
99
100=== modified file 'local_settings.py.sample'
101--- local_settings.py.sample 2019-04-04 06:47:19 +0000
102+++ local_settings.py.sample 2019-06-14 07:22:45 +0000
103@@ -3,8 +3,6 @@
104 # The above leads python2.7 to read this file as utf-8
105 # Needs to be directly after the python shebang
106
107-# Treat all strings as unicode
108-from __future__ import unicode_literals
109 import os
110 import re
111
112
113=== modified file 'mainpage/admin.py'
114--- mainpage/admin.py 2019-01-18 11:10:27 +0000
115+++ mainpage/admin.py 2019-06-14 07:22:45 +0000
116@@ -28,7 +28,7 @@
117 text += ['has perm.']
118 value = ', '.join(text)
119 return value
120-roles.short_description = u'Groups/Permissions'
121+roles.short_description = 'Groups/Permissions'
122
123
124 def persons(self):
125@@ -38,7 +38,7 @@
126
127 def deleted(self):
128 return '' if self.wlprofile.deleted==False else 'Yes'
129-deleted.short_description = u'Deleted himself'
130+deleted.short_description = 'Deleted himself'
131
132
133 class GroupAdmin(GroupAdmin):
134
135=== modified file 'mainpage/online_users_middleware.py'
136--- mainpage/online_users_middleware.py 2019-03-31 11:08:21 +0000
137+++ mainpage/online_users_middleware.py 2019-06-14 07:22:45 +0000
138@@ -36,7 +36,7 @@
139
140 # Perform the multiget on the individual online uid keys
141 online_keys = ['online-%s' % (u,) for u in uids]
142- fresh = cache.get_many(online_keys).keys()
143+ fresh = list(cache.get_many(online_keys).keys())
144 online_now_ids = [int(k.replace('online-', '')) for k in fresh]
145
146 # If the user is authenticated, add their id to the list
147
148=== modified file 'mainpage/settings.py'
149--- mainpage/settings.py 2019-04-07 10:02:31 +0000
150+++ mainpage/settings.py 2019-06-14 07:22:45 +0000
151@@ -288,20 +288,20 @@
152 ## Allowed tags/attributes for 'bleach' ##
153 ## Used for sanitizing user input. ##
154 ##########################################
155-BLEACH_ALLOWED_TAGS = [u'a',
156- u'abbr',
157- u'acronym',
158- u'blockquote',
159- u'br',
160- u'em', u'i', u'strong', u'b',
161- u'ul', u'ol', u'li',
162- u'div', u'p',
163- u'h1', u'h2', u'h3', u'h4', u'h5', u'h6',
164- u'pre', u'code',
165- u'img',
166- u'hr',
167- u'table', u'tbody', u'thead', u'th', u'tr', u'td',
168- u'sup',
169+BLEACH_ALLOWED_TAGS = ['a',
170+ 'abbr',
171+ 'acronym',
172+ 'blockquote',
173+ 'br',
174+ 'em', 'i', 'strong', 'b',
175+ 'ul', 'ol', 'li',
176+ 'div', 'p',
177+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
178+ 'pre', 'code',
179+ 'img',
180+ 'hr',
181+ 'table', 'tbody', 'thead', 'th', 'tr', 'td',
182+ 'sup',
183 ]
184
185 BLEACH_ALLOWED_ATTRIBUTES = {'img': ['src', 'alt'], 'a': [
186@@ -368,6 +368,6 @@
187
188
189 try:
190- from local_settings import *
191+ from .local_settings import *
192 except ImportError:
193 pass
194
195=== modified file 'mainpage/sitemap_urls.py'
196--- mainpage/sitemap_urls.py 2019-03-31 11:08:21 +0000
197+++ mainpage/sitemap_urls.py 2019-06-14 07:22:45 +0000
198@@ -1,7 +1,7 @@
199 from django.conf.urls import url
200
201 from django.contrib.sitemaps.views import sitemap
202-from static_sitemap import StaticViewSitemap
203+from .static_sitemap import StaticViewSitemap
204 from wiki.sitemap import *
205 from news.sitemap import *
206 from pybb.sitemap import *
207
208=== modified file 'mainpage/templatetags/wl_markdown.py'
209--- mainpage/templatetags/wl_markdown.py 2019-03-31 11:08:21 +0000
210+++ mainpage/templatetags/wl_markdown.py 2019-06-14 07:22:45 +0000
211@@ -11,7 +11,7 @@
212
213 from django import template
214 from django.conf import settings
215-from django.utils.encoding import smart_str, force_unicode
216+from django.utils.encoding import smart_bytes, force_text
217 from django.utils.safestring import mark_safe
218 from django.conf import settings
219 from markdownextensions.semanticwikilinks.mdx_semanticwikilinks import SemanticWikiLinkExtension
220@@ -19,10 +19,10 @@
221 # Try to get a not so fully broken markdown module
222 import markdown
223 if markdown.version_info[0] < 2:
224- raise ImportError, 'Markdown library to old!'
225+ raise ImportError('Markdown library to old!')
226 from markdown import markdown
227 import re
228-import urllib
229+import urllib.request, urllib.parse, urllib.error
230 import bleach
231
232 from bs4 import BeautifulSoup, NavigableString
233@@ -138,7 +138,7 @@
234
235 # Check for missing wikilink /wiki/PageName[/additionl/stuff]
236 # Using href because we need cAsEs here
237- article_name = urllib.unquote(tag['href'][6:].split('/', 1)[0])
238+ article_name = urllib.parse.unquote(tag['href'][6:].split('/', 1)[0])
239
240 if not len(article_name): # Wiki root link is not a page
241 tag['class'] = 'wrongLink'
242@@ -159,7 +159,7 @@
243 # get actual title of article
244 act_t = Article.objects.get(id=a_id[0]).title
245 if article_name != act_t:
246- tag['title'] = 'This is a redirect and points to \"" + act_t + "\"'
247+ tag['title'] = 'This is a redirect and points to \"' + act_t + '\"'
248 return
249 else:
250 return
251@@ -218,7 +218,7 @@
252 """Apply wl specific things, like smileys or colored links."""
253
254 beautify = keyw.pop('beautify', True)
255- html = smart_str(markdown(value, extensions=md_extensions))
256+ html = markdown(value, extensions=md_extensions)
257
258 # Sanitize posts from potencial untrusted users (Forum/Wiki/Maps)
259 if 'bleachit' in args:
260@@ -232,7 +232,7 @@
261 soup = BeautifulSoup(html, features='lxml')
262 if len(soup.contents) == 0:
263 # well, empty soup. Return it
264- return unicode(soup)
265+ return str(soup)
266
267 if beautify:
268 # Insert smileys
269@@ -249,7 +249,7 @@
270 for tag in soup.find_all('img'):
271 _make_clickable_images(tag)
272
273- return unicode(soup)
274+ return str(soup)
275
276
277 @register.filter
278
279=== modified file 'mainpage/urls.py'
280--- mainpage/urls.py 2019-03-31 11:08:21 +0000
281+++ mainpage/urls.py 2019-06-14 07:22:45 +0000
282@@ -63,7 +63,7 @@
283 ]
284
285 try:
286- from local_urls import *
287+ from .local_urls import *
288 urlpatterns += local_urlpatterns
289 except ImportError:
290 pass
291
292=== modified file 'mainpage/utest/test_wl_markdown.py'
293--- mainpage/utest/test_wl_markdown.py 2019-03-31 11:08:21 +0000
294+++ mainpage/utest/test_wl_markdown.py 2019-06-14 07:22:45 +0000
295@@ -37,222 +37,222 @@
296 self.assertEqual(wanted, res)
297
298 def test_simple_case__correct_result(self):
299- input = u"Hallo Welt"
300- wanted = u"<p>Hallo Welt</p>"
301+ input = "Hallo Welt"
302+ wanted = "<p>Hallo Welt</p>"
303 self._check(input, wanted)
304
305 def test_wikiwords_simple__except_correct_result(self):
306- input = u"Na Du HalloWelt, Du?"
307- wanted = u"""<p>Na Du <a href="/wiki/HalloWelt">HalloWelt</a>, Du?</p>"""
308+ input = "Na Du HalloWelt, Du?"
309+ wanted = """<p>Na Du <a href="/wiki/HalloWelt">HalloWelt</a>, Du?</p>"""
310 self._check(input, wanted)
311
312 def test_wikiwords_avoid__except_correct_result(self):
313- input = u"Hi !NotAWikiWord Moretext"
314- wanted = u"""<p>Hi NotAWikiWord Moretext</p>"""
315+ input = "Hi !NotAWikiWord Moretext"
316+ wanted = """<p>Hi NotAWikiWord Moretext</p>"""
317 self._check(input, wanted)
318
319 def test_wikiwords_in_link__except_correct_result(self):
320- input = u"""WikiWord [NoWikiWord](/forum/)"""
321- wanted = u"""<p><a href="/wiki/WikiWord">WikiWord</a> <a href="/forum/">NoWikiWord</a></p>"""
322+ input = """WikiWord [NoWikiWord](/forum/)"""
323+ wanted = """<p><a href="/wiki/WikiWord">WikiWord</a> <a href="/forum/">NoWikiWord</a></p>"""
324 self._check(input, wanted)
325
326 def test_wikiwords_external_links__except_correct_result(self):
327- input = u"""[NoWikiWord](http://www.sun.com)"""
328- wanted = u"""<p><a href="http://www.sun.com" class="external">NoWikiWord</a></p>"""
329+ input = """[NoWikiWord](http://www.sun.com)"""
330+ wanted = """<p><a href="http://www.sun.com" class="external">NoWikiWord</a></p>"""
331 self._check(input, wanted)
332
333 def test_wikiwords_noexternal_links__except_correct_result(self):
334- input = u"""[NoWikiWord](http://%s/blahfasel/wiki)""" % _domain
335- wanted = u"""<p><a href="http://%s/blahfasel/wiki">NoWikiWord</a></p>""" % _domain
336+ input = """[NoWikiWord](http://%s/blahfasel/wiki)""" % _domain
337+ wanted = """<p><a href="http://%s/blahfasel/wiki">NoWikiWord</a></p>""" % _domain
338 self._check(input, wanted)
339
340 def test_wikiwords_noclasschangeforimage_links__except_correct_result(self):
341- input = u"""<a href="http://www.ccc.de"><img src="/blub" /></a>"""
342- wanted = u"""<p><a href="http://www.ccc.de"><img src="/blub" /></a></p>"""
343+ input = """<a href="http://www.ccc.de"><img src="/blub" /></a>"""
344+ wanted = """<p><a href="http://www.ccc.de"><img src="/blub" /></a></p>"""
345 self._check(input, wanted)
346
347 # Existing links
348 def test_existing_link_html(self):
349- input = u"""<a href="/wiki/MainPage">this page</a>"""
350- wanted = u"""<p><a href="/wiki/MainPage">this page</a></p>"""
351+ input = """<a href="/wiki/MainPage">this page</a>"""
352+ wanted = """<p><a href="/wiki/MainPage">this page</a></p>"""
353 self._check(input, wanted)
354
355 def test_existing_link_markdown(self):
356- input = u"""[this page](/wiki/MainPage)"""
357- wanted = u"""<p><a href="/wiki/MainPage">this page</a></p>"""
358+ input = """[this page](/wiki/MainPage)"""
359+ wanted = """<p><a href="/wiki/MainPage">this page</a></p>"""
360 self._check(input, wanted)
361
362 def test_existing_link_wikiword(self):
363- input = u"""MainPage"""
364- wanted = u"""<p><a href="/wiki/MainPage">MainPage</a></p>"""
365+ input = """MainPage"""
366+ wanted = """<p><a href="/wiki/MainPage">MainPage</a></p>"""
367 self._check(input, wanted)
368
369 def test_existing_editlink_wikiword(self):
370- input = u"""<a href="/wiki/MainPage/edit/">this page</a>"""
371- wanted = u"""<p><a href="/wiki/MainPage/edit/">this page</a></p>"""
372+ input = """<a href="/wiki/MainPage/edit/">this page</a>"""
373+ wanted = """<p><a href="/wiki/MainPage/edit/">this page</a></p>"""
374 self._check(input, wanted)
375
376 # Missing links
377 def test_missing_link_html(self):
378- input = u"""<a href="/wiki/MissingPage">this page</a>"""
379- wanted = u"""<p><a href="/wiki/MissingPage" class="missing">this page</a></p>"""
380+ input = """<a href="/wiki/MissingPage">this page</a>"""
381+ wanted = """<p><a href="/wiki/MissingPage" class="missing">this page</a></p>"""
382 self._check(input, wanted)
383
384 def test_missing_link_markdown(self):
385- input = u"""[this page](/wiki/MissingPage)"""
386- wanted = u"""<p><a href="/wiki/MissingPage" class="missing">this page</a></p>"""
387+ input = """[this page](/wiki/MissingPage)"""
388+ wanted = """<p><a href="/wiki/MissingPage" class="missing">this page</a></p>"""
389 self._check(input, wanted)
390
391 def test_missing_link_wikiword(self):
392- input = u"""BlubMissingPage"""
393- wanted = u"""<p><a href="/wiki/BlubMissingPage" class="missing">BlubMissingPage</a></p>"""
394+ input = """BlubMissingPage"""
395+ wanted = """<p><a href="/wiki/BlubMissingPage" class="missing">BlubMissingPage</a></p>"""
396 res = do_wl_markdown(input)
397 # self._check(input,wanted)
398
399 def test_missing_editlink_wikiword(self):
400- input = u"""<a href="/wiki/MissingPage/edit/">this page</a>"""
401- wanted = u"""<p><a href="/wiki/MissingPage/edit/" class="missing">this page</a></p>"""
402+ input = """<a href="/wiki/MissingPage/edit/">this page</a>"""
403+ wanted = """<p><a href="/wiki/MissingPage/edit/" class="missing">this page</a></p>"""
404 self._check(input, wanted)
405
406 # Check smileys
407 def test_smiley_angel(self):
408 input = """O:-)"""
409- wanted = u"""<p><img src="/wlmedia/img/smileys/face-angel.png" alt="face-angel.png" /></p>"""
410+ wanted = """<p><img src="/wlmedia/img/smileys/face-angel.png" alt="face-angel.png" /></p>"""
411 self._check(input, wanted)
412
413 def test_smiley_crying(self):
414 input = """:'-("""
415- wanted = u"""<p><img src="/wlmedia/img/smileys/face-crying.png" alt="face-crying.png" /></p>"""
416+ wanted = """<p><img src="/wlmedia/img/smileys/face-crying.png" alt="face-crying.png" /></p>"""
417 self._check(input, wanted)
418
419 def test_smiley_devilish(self):
420 input = """>:-)"""
421- wanted = u"""<p><img src="/wlmedia/img/smileys/face-devilish.png" alt="face-devilish.png" /></p>"""
422+ wanted = """<p><img src="/wlmedia/img/smileys/face-devilish.png" alt="face-devilish.png" /></p>"""
423 self._check(input, wanted)
424
425 def test_smiley_glasses(self):
426 input = """8-)"""
427- wanted = u"""<p><img src="/wlmedia/img/smileys/face-glasses.png" alt="face-glasses.png" /></p>"""
428+ wanted = """<p><img src="/wlmedia/img/smileys/face-glasses.png" alt="face-glasses.png" /></p>"""
429 self._check(input, wanted)
430
431 def test_smiley_kiss(self):
432 input = """:-x"""
433- wanted = u"""<p><img src="/wlmedia/img/smileys/face-kiss.png" alt="face-kiss.png" /></p>"""
434+ wanted = """<p><img src="/wlmedia/img/smileys/face-kiss.png" alt="face-kiss.png" /></p>"""
435 self._check(input, wanted)
436
437 def test_smiley_plain(self):
438 input = """:-|"""
439- wanted = u"""<p><img src="/wlmedia/img/smileys/face-plain.png" alt="face-plain.png" /></p>"""
440+ wanted = """<p><img src="/wlmedia/img/smileys/face-plain.png" alt="face-plain.png" /></p>"""
441 self._check(input, wanted)
442
443 def test_smiley_sad(self):
444 input = """:-("""
445- wanted = u"""<p><img src="/wlmedia/img/smileys/face-sad.png" alt="face-sad.png" /></p>"""
446+ wanted = """<p><img src="/wlmedia/img/smileys/face-sad.png" alt="face-sad.png" /></p>"""
447 self._check(input, wanted)
448
449 def test_smiley_smilebig(self):
450 input = """:))"""
451- wanted = u"""<p><img src="/wlmedia/img/smileys/face-smile-big.png" alt="face-smile-big.png" /></p>"""
452+ wanted = """<p><img src="/wlmedia/img/smileys/face-smile-big.png" alt="face-smile-big.png" /></p>"""
453 self._check(input, wanted)
454
455 def test_smiley_smile(self):
456 input = """:-)"""
457- wanted = u"""<p><img src="/wlmedia/img/smileys/face-smile.png" alt="face-smile.png" /></p>"""
458+ wanted = """<p><img src="/wlmedia/img/smileys/face-smile.png" alt="face-smile.png" /></p>"""
459 self._check(input, wanted)
460
461 def test_smiley_surprise(self):
462 input = """:-O"""
463- wanted = u"""<p><img src="/wlmedia/img/smileys/face-surprise.png" alt="face-surprise.png" /></p>"""
464+ wanted = """<p><img src="/wlmedia/img/smileys/face-surprise.png" alt="face-surprise.png" /></p>"""
465 self._check(input, wanted)
466
467 def test_smiley_wink(self):
468 input = """;-)"""
469- wanted = u"""<p><img src="/wlmedia/img/smileys/face-wink.png" alt="face-wink.png" /></p>"""
470+ wanted = """<p><img src="/wlmedia/img/smileys/face-wink.png" alt="face-wink.png" /></p>"""
471 self._check(input, wanted)
472
473 def test_smiley_grin(self):
474 input = """:D"""
475- wanted = u"""<p><img src="/wlmedia/img/smileys/face-grin.png" alt="face-grin.png" /></p>"""
476+ wanted = """<p><img src="/wlmedia/img/smileys/face-grin.png" alt="face-grin.png" /></p>"""
477 self._check(input, wanted)
478
479 def test_smiley_sad(self):
480 input = """:("""
481- wanted = u"""<p><img src="/wlmedia/img/smileys/face-sad.png" alt="face-sad.png" /></p>"""
482+ wanted = """<p><img src="/wlmedia/img/smileys/face-sad.png" alt="face-sad.png" /></p>"""
483 self._check(input, wanted)
484
485 def test_smiley_smile(self):
486 input = """:)"""
487- wanted = u"""<p><img src="/wlmedia/img/smileys/face-smile.png" alt="face-smile.png" /></p>"""
488+ wanted = """<p><img src="/wlmedia/img/smileys/face-smile.png" alt="face-smile.png" /></p>"""
489 self._check(input, wanted)
490
491 def test_smiley_surprise(self):
492 input = """:O"""
493- wanted = u"""<p><img src="/wlmedia/img/smileys/face-surprise.png" alt="face-surprise.png" /></p>"""
494+ wanted = """<p><img src="/wlmedia/img/smileys/face-surprise.png" alt="face-surprise.png" /></p>"""
495 self._check(input, wanted)
496
497 def test_smiley_wink(self):
498 input = """;)"""
499- wanted = u"""<p><img src="/wlmedia/img/smileys/face-wink.png" alt="face-wink.png" /></p>"""
500+ wanted = """<p><img src="/wlmedia/img/smileys/face-wink.png" alt="face-wink.png" /></p>"""
501 self._check(input, wanted)
502
503 def test_smiley_monkey(self):
504 input = """:(|)"""
505- wanted = u"""<p><img src="/wlmedia/img/smileys/face-monkey.png" alt="face-monkey.png" /></p>"""
506+ wanted = """<p><img src="/wlmedia/img/smileys/face-monkey.png" alt="face-monkey.png" /></p>"""
507 self._check(input, wanted)
508
509 # Occured errors
510 def test_wiki_rootlink(self):
511- input = u"""<a href="/wiki">this page</a>"""
512- wanted = u"""<p><a href="/wiki">this page</a></p>"""
513+ input = """<a href="/wiki">this page</a>"""
514+ wanted = """<p><a href="/wiki">this page</a></p>"""
515 self._check(input, wanted)
516
517 def test_wiki_rootlink_with_slash(self):
518- input = u"""<a href="/wiki/">this page</a>"""
519- wanted = u"""<p><a href="/wiki/">this page</a></p>"""
520+ input = """<a href="/wiki/">this page</a>"""
521+ wanted = """<p><a href="/wiki/">this page</a></p>"""
522 self._check(input, wanted)
523
524 # Special pages
525 def test_wiki_specialpage(self):
526- input = u"""<a href="/wiki/list">this page</a>"""
527- wanted = u"""<p><a href="/wiki/list">this page</a></p>"""
528+ input = """<a href="/wiki/list">this page</a>"""
529+ wanted = """<p><a href="/wiki/list">this page</a></p>"""
530 self._check(input, wanted)
531
532 def test_wiki_specialpage_markdown(self):
533- input = u"""[list](/wiki/list)"""
534- wanted = u"""<p><a href="/wiki/list">list</a></p>"""
535+ input = """[list](/wiki/list)"""
536+ wanted = """<p><a href="/wiki/list">list</a></p>"""
537 self._check(input, wanted)
538
539 # Special problem with emphasis
540 def test_markdown_emphasis_problem(self):
541- input = u"""*This is bold* _This too_\n\n"""
542- wanted = u"""<p><em>This is bold</em> <em>This too</em></p>"""
543+ input = """*This is bold* _This too_\n\n"""
544+ wanted = """<p><em>This is bold</em> <em>This too</em></p>"""
545 self._check(input, wanted)
546
547 # Another markdown problem with alt tag escaping
548 def test_markdown_alt_problem(self):
549 # {{{ Test strings
550- input = u"""![img_thisisNOTitalicplease_name.png](/wlmedia/blah.png)\n\n"""
551- wanted = u'<p><img alt="img_thisisNOTitalicplease_name.png" src="/wlmedia/blah.png" /></p>'
552+ input = """![img_thisisNOTitalicplease_name.png](/wlmedia/blah.png)\n\n"""
553+ wanted = '<p><img alt="img_thisisNOTitalicplease_name.png" src="/wlmedia/blah.png" /></p>'
554 # }}}
555 self._check(input, wanted)
556
557 def test_emptystring_problem(self):
558 # {{{ Test strings
559- input = u''
560- wanted = u''
561+ input = ''
562+ wanted = ''
563 # }}}
564 self._check(input, wanted)
565
566 # Damned problem with tables
567 def test_markdown_table_problem(self):
568 # {{{ Test strings
569- input = u"""
570+ input = """
571 Header1 | Header 2
572 ------- | --------
573 Value 1 | Value 2
574 Value 3 | Value 4
575 """
576- wanted = u"""<table>
577+ wanted = """<table>
578 <thead>
579 <tr>
580 <th>Header1</th>
581@@ -274,13 +274,13 @@
582 self._check(input, wanted)
583
584 def test_svnrevision_replacement(self):
585- input = u"- Fixed this bug (bzr:r3222)"
586- wanted = u"""<ul>\n<li>Fixed this bug (<a href="http://bazaar.launchpad.net/%7Ewidelands-dev/widelands/trunk/revision/3222" class="external">r3222</a>)</li>\n</ul>"""
587+ input = "- Fixed this bug (bzr:r3222)"
588+ wanted = """<ul>\n<li>Fixed this bug (<a href="http://bazaar.launchpad.net/%7Ewidelands-dev/widelands/trunk/revision/3222" class="external">r3222</a>)</li>\n</ul>"""
589 self._check(input, wanted)
590
591 def test_svnrevision_multiple_replacement(self):
592- input = u"- Fixed this bug (bzr:r3222, bzr:r3424)"
593- wanted = u"""<ul>\n<li>Fixed this bug (<a href="http://bazaar.launchpad.net/%7Ewidelands-dev/widelands/trunk/revision/3222" class="external">r3222</a>, <a href="http://bazaar.launchpad.net/%7Ewidelands-dev/widelands/trunk/revision/3424" class="external">r3424</a>)</li>\n</ul>"""
594+ input = "- Fixed this bug (bzr:r3222, bzr:r3424)"
595+ wanted = """<ul>\n<li>Fixed this bug (<a href="http://bazaar.launchpad.net/%7Ewidelands-dev/widelands/trunk/revision/3222" class="external">r3222</a>, <a href="http://bazaar.launchpad.net/%7Ewidelands-dev/widelands/trunk/revision/3424" class="external">r3424</a>)</li>\n</ul>"""
596 self._check(input, wanted)
597
598
599
600=== modified file 'mainpage/views.py'
601--- mainpage/views.py 2019-03-31 11:08:21 +0000
602+++ mainpage/views.py 2019-06-14 07:22:45 +0000
603@@ -1,5 +1,5 @@
604 from django.conf import settings
605-from templatetags.wl_markdown import do_wl_markdown
606+from .templatetags.wl_markdown import do_wl_markdown
607 from operator import itemgetter
608 from django.core.mail import send_mail
609 from mainpage.forms import ContactForm
610@@ -119,12 +119,12 @@
611
612 # Add a subheader or/and the member(s)
613 for entry in head['entries']:
614- if 'subheading' in entry.keys():
615+ if 'subheading' in list(entry.keys()):
616 txt = txt + '###' + entry['subheading'] + '\n'
617- if 'members' in entry.keys():
618+ if 'members' in list(entry.keys()):
619 for name in entry['members']:
620 txt = txt + '* ' + name + '\n'
621- if 'translate' in entry.keys():
622+ if 'translate' in list(entry.keys()):
623 for transl in entry['translate']:
624 txt = txt + '* ' + transl + '\n'
625
626@@ -160,5 +160,7 @@
627 loc_info = 'getlocale: ' + str(locale.getlocale()) + \
628 '<br/>getdefaultlocale(): ' + str(locale.getdefaultlocale()) + \
629 '<br/>fs_encoding: ' + str(sys.getfilesystemencoding()) + \
630- '<br/>sys default encoding: ' + str(sys.getdefaultencoding())
631+ '<br/>sys default encoding: ' + str(sys.getdefaultencoding()) + \
632+ '<br><br> Environment variables:' + \
633+ '<br>DISPLAY: ' + os.environ.get('DISPLAY', 'Not set')
634 return HttpResponse(loc_info)
635
636=== modified file 'mainpage/wlwebsite_wsgi.py'
637--- mainpage/wlwebsite_wsgi.py 2019-03-31 11:08:21 +0000
638+++ mainpage/wlwebsite_wsgi.py 2019-06-14 07:22:45 +0000
639@@ -16,7 +16,7 @@
640 if activate_this is None:
641 raise RuntimeException('Could not find virtualenv to start up!')
642
643-execfile(activate_this, dict(__file__=activate_this))
644+exec(compile(open(activate_this, "rb").read(), activate_this, 'exec'), dict(__file__=activate_this))
645
646 sys.path.append(parent_dir(code_directory))
647 sys.path.append(code_directory)
648
649=== modified file 'news/migrations/0001_initial.py'
650--- news/migrations/0001_initial.py 2016-12-13 18:28:51 +0000
651+++ news/migrations/0001_initial.py 2019-06-14 07:22:45 +0000
652@@ -1,5 +1,5 @@
653 # -*- coding: utf-8 -*-
654-from __future__ import unicode_literals
655+
656
657 from django.db import models, migrations
658 from django.conf import settings
659
660=== modified file 'news/migrations/0002_auto_20170417_1857.py'
661--- news/migrations/0002_auto_20170417_1857.py 2017-04-17 17:08:27 +0000
662+++ news/migrations/0002_auto_20170417_1857.py 2019-06-14 07:22:45 +0000
663@@ -1,5 +1,5 @@
664 # -*- coding: utf-8 -*-
665-from __future__ import unicode_literals
666+
667
668 from django.db import models, migrations
669
670
671=== modified file 'news/models.py'
672--- news/models.py 2019-03-31 11:08:21 +0000
673+++ news/models.py 2019-06-14 07:22:45 +0000
674@@ -29,8 +29,8 @@
675 db_table = 'news_categories'
676 ordering = ('title',)
677
678- def __unicode__(self):
679- return u'%s' % self.title
680+ def __str__(self):
681+ return '%s' % self.title
682
683 def get_absolute_url(self):
684 return reverse('category_posts', args=(self.slug,))
685@@ -65,8 +65,8 @@
686 ordering = ('-publish',)
687 get_latest_by = 'publish'
688
689- def __unicode__(self):
690- return u'%s' % self.title
691+ def __str__(self):
692+ return '%s' % self.title
693
694 #########
695 # IMAGE #
696
697=== modified file 'news/templatetags/news_extras.py'
698--- news/templatetags/news_extras.py 2016-12-13 18:28:51 +0000
699+++ news/templatetags/news_extras.py 2019-06-14 07:22:45 +0000
700@@ -42,9 +42,9 @@
701 try:
702 tag_name, arg = token.contents.split(None, 1)
703 except ValueError:
704- raise template.TemplateSyntaxError, '%s tag requires arguments' % token.contents.split()[0]
705+ raise template.TemplateSyntaxError('%s tag requires arguments' % token.contents.split()[0])
706 m = re.search(r'(.*?) as (\w+)', arg)
707 if not m:
708- raise template.TemplateSyntaxError, '%s tag had invalid arguments' % tag_name
709+ raise template.TemplateSyntaxError('%s tag had invalid arguments' % tag_name)
710 format_string, var_name = m.groups()
711 return LatestPosts(format_string, var_name)
712
713=== modified file 'notification/engine.py'
714--- notification/engine.py 2017-04-29 19:52:28 +0000
715+++ notification/engine.py 2019-06-14 07:22:45 +0000
716@@ -3,9 +3,10 @@
717 import time
718 import logging
719 import traceback
720+import base64
721
722 try:
723- import cPickle as pickle
724+ import pickle as pickle
725 except ImportError:
726 import pickle
727
728@@ -14,7 +15,7 @@
729 from django.contrib.auth.models import User
730 from django.contrib.sites.models import Site
731
732-from lockfile import FileLock, AlreadyLocked, LockTimeout
733+from .lockfile import FileLock, AlreadyLocked, LockTimeout
734
735 from notification.models import NoticeQueueBatch
736 from notification import models as notification
737@@ -46,7 +47,8 @@
738 try:
739 for queued_batch in NoticeQueueBatch.objects.all():
740 notices = pickle.loads(
741- str(queued_batch.pickled_data).decode('base64'))
742+ base64.b64decode(queued_batch.pickled_data)
743+ )
744 for user, label, extra_context, on_site in notices:
745 user = User.objects.get(pk=user)
746 # FrankU: commented, because not all users get e-mailed
747
748=== modified file 'notification/lockfile.py'
749--- notification/lockfile.py 2016-12-13 18:28:51 +0000
750+++ notification/lockfile.py 2019-06-14 07:22:45 +0000
751@@ -48,7 +48,7 @@
752 NotMyLock - File was locked but not by the current thread/process
753 """
754
755-from __future__ import division
756+
757
758 import sys
759 import socket
760@@ -385,8 +385,8 @@
761
762 def __init__(self, path, threaded=True):
763 LockBase.__init__(self, path, threaded)
764- self.lock_file = unicode(self.lock_file)
765- self.unique_name = unicode(self.unique_name)
766+ self.lock_file = str(self.lock_file)
767+ self.unique_name = str(self.unique_name)
768
769 import sqlite3
770 self.connection = sqlite3.connect(SQLiteFileLock.testdb)
771
772=== modified file 'notification/migrations/0001_initial.py'
773--- notification/migrations/0001_initial.py 2016-12-13 18:28:51 +0000
774+++ notification/migrations/0001_initial.py 2019-06-14 07:22:45 +0000
775@@ -1,5 +1,5 @@
776 # -*- coding: utf-8 -*-
777-from __future__ import unicode_literals
778+
779
780 from django.db import models, migrations
781 import datetime
782
783=== modified file 'notification/migrations/0002_auto_20170417_1857.py'
784--- notification/migrations/0002_auto_20170417_1857.py 2017-04-17 17:08:27 +0000
785+++ notification/migrations/0002_auto_20170417_1857.py 2019-06-14 07:22:45 +0000
786@@ -1,5 +1,5 @@
787 # -*- coding: utf-8 -*-
788-from __future__ import unicode_literals
789+
790
791 from django.db import models, migrations
792
793
794=== modified file 'notification/migrations/0003_auto_20190409_0924.py'
795--- notification/migrations/0003_auto_20190409_0924.py 2019-04-10 16:01:43 +0000
796+++ notification/migrations/0003_auto_20190409_0924.py 2019-06-14 07:22:45 +0000
797@@ -1,6 +1,6 @@
798 # -*- coding: utf-8 -*-
799 # Generated by Django 1.11.20 on 2019-04-09 09:24
800-from __future__ import unicode_literals
801+
802
803 from django.db import migrations, models
804
805
806=== modified file 'notification/models.py'
807--- notification/models.py 2018-04-19 17:48:31 +0000
808+++ notification/models.py 2019-06-14 07:22:45 +0000
809@@ -1,7 +1,8 @@
810 import datetime
811+import base64
812
813 try:
814- import cPickle as pickle
815+ import pickle as pickle
816 except ImportError:
817 import pickle
818
819@@ -48,7 +49,7 @@
820 # number
821 default = models.IntegerField(_('default'))
822
823- def __unicode__(self):
824+ def __str__(self):
825 return self.label
826
827 class Meta:
828@@ -150,12 +151,12 @@
829 if updated:
830 notice_type.save()
831 if verbosity > 1:
832- print 'Updated %s NoticeType' % label
833+ print('Updated %s NoticeType' % label)
834 except NoticeType.DoesNotExist:
835 NoticeType(label=label, display=display,
836 description=description, default=default).save()
837 if verbosity > 1:
838- print 'Created %s NoticeType' % label
839+ print('Created %s NoticeType' % label)
840
841
842 def get_notification_language(user):
843@@ -222,8 +223,8 @@
844 notice_type = NoticeType.objects.get(label=label)
845
846 current_site = Site.objects.get_current()
847- notices_url = u"http://%s%s" % (
848- unicode(current_site),
849+ notices_url = "http://%s%s" % (
850+ str(current_site),
851 reverse('notification_notices'),
852 )
853
854@@ -257,12 +258,12 @@
855
856 # get prerendered format messages and subjects
857 messages = get_formatted_messages(formats, label, context)
858-
859+
860 # Create the subject
861 # Use 'email_subject.txt' to add Strings in every emails subject
862 subject = render_to_string('notification/email_subject.txt',
863 {'message': messages['short.txt'],}).replace('\n', '')
864-
865+
866 # Strip leading newlines. Make writing the email templates easier:
867 # Each linebreak in the templates results in a linebreak in the emails
868 # If the first line in a template contains only template tags the
869@@ -275,8 +276,8 @@
870 if should_send(user, notice_type, '1') and user.email: # Email
871 recipients.append(user.email)
872
873- send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, recipients)
874-
875+ send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, recipients, fail_silently=True)
876+
877 # reset environment to original language
878 activate(current_language)
879 except NoticeType.DoesNotExist:
880@@ -322,8 +323,9 @@
881 notices = []
882 for user in users:
883 notices.append((user, label, extra_context, on_site))
884- NoticeQueueBatch(pickled_data=pickle.dumps(
885- notices).encode('base64')).save()
886+
887+ NoticeQueueBatch(pickled_data=base64.b64encode(
888+ pickle.dumps(notices))).save()
889
890
891 class ObservedItemManager(models.Manager):
892
893=== modified file 'notification/views.py'
894--- notification/views.py 2018-04-03 05:18:03 +0000
895+++ notification/views.py 2019-06-14 07:22:45 +0000
896@@ -29,5 +29,5 @@
897
898 return render(request, 'notification/notice_settings.html', {
899 'column_headers': [medium_display for medium_id, medium_display in NOTICE_MEDIA],
900- 'app_tables': OrderedDict(sorted(app_tables.items(), key=lambda t: t[0]))
901+ 'app_tables': OrderedDict(sorted(list(app_tables.items()), key=lambda t: t[0]))
902 })
903
904=== modified file 'privacy_policy/admin.py'
905--- privacy_policy/admin.py 2018-10-08 17:51:36 +0000
906+++ privacy_policy/admin.py 2019-06-14 07:22:45 +0000
907@@ -1,5 +1,5 @@
908 # -*- coding: utf-8 -*-
909-from __future__ import unicode_literals
910+
911
912 from django.contrib import admin
913 from privacy_policy.models import PrivacyPolicy
914
915=== modified file 'privacy_policy/apps.py'
916--- privacy_policy/apps.py 2018-10-02 16:55:42 +0000
917+++ privacy_policy/apps.py 2019-06-14 07:22:45 +0000
918@@ -1,5 +1,5 @@
919 # -*- coding: utf-8 -*-
920-from __future__ import unicode_literals
921+
922
923 from django.apps import AppConfig
924
925
926=== modified file 'privacy_policy/migrations/0001_initial.py'
927--- privacy_policy/migrations/0001_initial.py 2018-10-08 17:51:36 +0000
928+++ privacy_policy/migrations/0001_initial.py 2019-06-14 07:22:45 +0000
929@@ -1,6 +1,6 @@
930 # -*- coding: utf-8 -*-
931 # Generated by Django 1.11.12 on 2018-10-08 18:23
932-from __future__ import unicode_literals
933+
934
935 from django.db import migrations, models
936
937
938=== modified file 'privacy_policy/models.py'
939--- privacy_policy/models.py 2018-10-14 07:58:31 +0000
940+++ privacy_policy/models.py 2019-06-14 07:22:45 +0000
941@@ -1,5 +1,5 @@
942 # -*- coding: utf-8 -*-
943-from __future__ import unicode_literals
944+
945
946 from django.db import models
947 from django.urls import reverse
948@@ -17,7 +17,7 @@
949 class Meta:
950 ordering = ['language']
951
952- def __unicode__(self):
953+ def __str__(self):
954 return self.language
955
956 def get_absolute_url(self):
957
958=== modified file 'privacy_policy/views.py'
959--- privacy_policy/views.py 2019-04-07 09:01:06 +0000
960+++ privacy_policy/views.py 2019-06-14 07:22:45 +0000
961@@ -1,5 +1,5 @@
962 # -*- coding: utf-8 -*-
963-from __future__ import unicode_literals
964+
965
966 from django.shortcuts import render
967 from django.core.exceptions import ObjectDoesNotExist
968
969=== modified file 'pybb/lib/phpserialize.py'
970--- pybb/lib/phpserialize.py 2009-02-25 16:55:36 +0000
971+++ pybb/lib/phpserialize.py 2019-06-14 07:22:45 +0000
972@@ -96,7 +96,7 @@
973 :copyright: 2007-2008 by Armin Ronacher.
974 license: BSD
975 """
976-from StringIO import StringIO
977+from io import StringIO
978
979 __author__ = 'Armin Ronacher <armin.ronacher@active-4.com>'
980 __version__ = '1.1'
981@@ -108,10 +108,10 @@
982 """
983 def _serialize(obj, keypos):
984 if keypos:
985- if isinstance(obj, (int, long, float, bool)):
986+ if isinstance(obj, (int, float, bool)):
987 return 'i:%i;' % obj
988- if isinstance(obj, basestring):
989- if isinstance(obj, unicode):
990+ if isinstance(obj, str):
991+ if isinstance(obj, str):
992 obj = obj.encode(charset, errors)
993 return 's:%i:"%s";' % (len(obj), obj)
994 if obj is None:
995@@ -122,18 +122,18 @@
996 return 'N;'
997 if isinstance(obj, bool):
998 return 'b:%i;' % obj
999- if isinstance(obj, (int, long)):
1000+ if isinstance(obj, int):
1001 return 'i:%s;' % obj
1002 if isinstance(obj, float):
1003 return 'd:%s;' % obj
1004- if isinstance(obj, basestring):
1005- if isinstance(obj, unicode):
1006+ if isinstance(obj, str):
1007+ if isinstance(obj, str):
1008 obj = obj.encode(charset, errors)
1009 return 's:%i:"%s";' % (len(obj), obj)
1010 if isinstance(obj, (list, tuple, dict)):
1011 out = []
1012 if isinstance(obj, dict):
1013- iterable = obj.iteritems()
1014+ iterable = iter(obj.items())
1015 else:
1016 iterable = enumerate(obj)
1017 for key, value in iterable:
1018@@ -202,7 +202,7 @@
1019 _expect('{')
1020 result = {}
1021 last_item = Ellipsis
1022- for idx in xrange(items):
1023+ for idx in range(items):
1024 item = _unserialize()
1025 if last_item is Ellipsis:
1026 last_item = item
1027@@ -238,7 +238,7 @@
1028 def dict_to_list(d):
1029 """Converts an ordered dict into a list."""
1030 try:
1031- return [d[x] for x in xrange(len(d))]
1032+ return [d[x] for x in range(len(d))]
1033 except KeyError:
1034 raise ValueError('dict is not a sequence')
1035
1036
1037=== modified file 'pybb/management/__init__.py'
1038--- pybb/management/__init__.py 2010-06-08 19:11:54 +0000
1039+++ pybb/management/__init__.py 2019-06-14 07:22:45 +0000
1040@@ -1,1 +1,1 @@
1041-import pybb_notifications
1042+from . import pybb_notifications
1043
1044=== modified file 'pybb/management/commands/pybb_resave_post.py'
1045--- pybb/management/commands/pybb_resave_post.py 2018-04-08 15:53:05 +0000
1046+++ pybb/management/commands/pybb_resave_post.py 2019-06-14 07:22:45 +0000
1047@@ -14,5 +14,5 @@
1048
1049 for count, post in enumerate(Post.objects.all()):
1050 if count and not count % 1000:
1051- print count
1052+ print(count)
1053 post.save()
1054
1055=== modified file 'pybb/management/pybb_notifications.py'
1056--- pybb/management/pybb_notifications.py 2018-04-03 18:57:40 +0000
1057+++ pybb/management/pybb_notifications.py 2019-06-14 07:22:45 +0000
1058@@ -14,4 +14,4 @@
1059 _('Forum New Post'),
1060 _('a new comment has been posted to a topic you observe'))
1061 except ImportError:
1062- print 'Skipping creation of NoticeTypes as notification app not found'
1063+ print('Skipping creation of NoticeTypes as notification app not found')
1064
1065=== modified file 'pybb/markups/postmarkup.py'
1066--- pybb/markups/postmarkup.py 2016-12-13 18:28:51 +0000
1067+++ pybb/markups/postmarkup.py 2019-06-14 07:22:45 +0000
1068@@ -8,8 +8,8 @@
1069 __version__ = '1.1.3'
1070
1071 import re
1072-from urllib import quote, unquote, quote_plus
1073-from urlparse import urlparse, urlunparse
1074+from urllib.parse import quote, unquote, quote_plus
1075+from urllib.parse import urlparse, urlunparse
1076
1077 pygments_available = True
1078 try:
1079@@ -28,7 +28,7 @@
1080 domain -- Domain parsed from url
1081
1082 """
1083- return u" [%s]" % _escape(domain)
1084+ return " [%s]" % _escape(domain)
1085
1086
1087 re_url = re.compile(
1088@@ -62,7 +62,7 @@
1089 if match is None:
1090 return ''
1091 excerpt = match.group(0)
1092- excerpt = excerpt.replace(u'\n', u"<br/>")
1093+ excerpt = excerpt.replace('\n', "<br/>")
1094 return remove_markup(excerpt)
1095
1096
1097@@ -73,7 +73,7 @@
1098
1099 """
1100
1101- return u"".join([t[1] for t in PostMarkup.tokenize(bbcode) if t[0] == PostMarkup.TOKEN_TEXT])
1102+ return "".join([t[1] for t in PostMarkup.tokenize(bbcode) if t[0] == PostMarkup.TOKEN_TEXT])
1103
1104
1105 def create(include=None, exclude=None, use_pygments=True, **kwargs):
1106@@ -107,28 +107,28 @@
1107
1108 add_tag(QuoteTag, 'quote')
1109
1110- add_tag(SearchTag, u'wiki',
1111- u"http://en.wikipedia.org/wiki/Special:Search?search=%s", u'wikipedia.com', **kwargs)
1112- add_tag(SearchTag, u'google',
1113- u"http://www.google.com/search?hl=en&q=%s&btnG=Google+Search", u'google.com', **kwargs)
1114- add_tag(SearchTag, u'dictionary',
1115- u"http://dictionary.reference.com/browse/%s", u'dictionary.com', **kwargs)
1116- add_tag(SearchTag, u'dict',
1117- u"http://dictionary.reference.com/browse/%s", u'dictionary.com', **kwargs)
1118-
1119- add_tag(ImgTag, u'img')
1120- add_tag(ListTag, u'list')
1121- add_tag(ListItemTag, u'*')
1122-
1123- add_tag(SizeTag, u"size")
1124- add_tag(ColorTag, u"color")
1125- add_tag(CenterTag, u"center")
1126+ add_tag(SearchTag, 'wiki',
1127+ "http://en.wikipedia.org/wiki/Special:Search?search=%s", 'wikipedia.com', **kwargs)
1128+ add_tag(SearchTag, 'google',
1129+ "http://www.google.com/search?hl=en&q=%s&btnG=Google+Search", 'google.com', **kwargs)
1130+ add_tag(SearchTag, 'dictionary',
1131+ "http://dictionary.reference.com/browse/%s", 'dictionary.com', **kwargs)
1132+ add_tag(SearchTag, 'dict',
1133+ "http://dictionary.reference.com/browse/%s", 'dictionary.com', **kwargs)
1134+
1135+ add_tag(ImgTag, 'img')
1136+ add_tag(ListTag, 'list')
1137+ add_tag(ListItemTag, '*')
1138+
1139+ add_tag(SizeTag, "size")
1140+ add_tag(ColorTag, "color")
1141+ add_tag(CenterTag, "center")
1142
1143 if use_pygments:
1144 assert pygments_available, 'Install Pygments (http://pygments.org/) or call create with use_pygments=False'
1145- add_tag(PygmentsCodeTag, u'code', **kwargs)
1146+ add_tag(PygmentsCodeTag, 'code', **kwargs)
1147 else:
1148- add_tag(CodeTag, u'code', **kwargs)
1149+ add_tag(CodeTag, 'code', **kwargs)
1150
1151 return postmarkup
1152
1153@@ -203,7 +203,7 @@
1154 def get_contents_text(self, parser):
1155 """Returns the string between the the open and close tag, minus bbcode
1156 tags."""
1157- return u"".join(parser.get_text_nodes(self.open_node_index, self.close_node_index))
1158+ return "".join(parser.get_text_nodes(self.open_node_index, self.close_node_index))
1159
1160 def skip_contents(self, parser):
1161 """Skips the contents of a tag while rendering."""
1162@@ -223,10 +223,10 @@
1163 self.html_name = html_name
1164
1165 def render_open(self, parser, node_index):
1166- return u"<%s>" % self.html_name
1167+ return "<%s>" % self.html_name
1168
1169 def render_close(self, parser, node_index):
1170- return u"</%s>" % self.html_name
1171+ return "</%s>" % self.html_name
1172
1173
1174 class DivStyleTag(TagBase):
1175@@ -239,10 +239,10 @@
1176 self.value = value
1177
1178 def render_open(self, parser, node_index):
1179- return u'<div style="%s:%s;">' % (self.style, self.value)
1180+ return '<div style="%s:%s;">' % (self.style, self.value)
1181
1182 def render_close(self, parser, node_index):
1183- return u'</div>'
1184+ return '</div>'
1185
1186
1187 class LinkTag(TagBase):
1188@@ -254,13 +254,13 @@
1189
1190 def render_open(self, parser, node_index):
1191
1192- self.domain = u''
1193+ self.domain = ''
1194 tag_data = parser.tag_data
1195 nest_level = tag_data['link_nest_level'] = tag_data.setdefault(
1196 'link_nest_level', 0) + 1
1197
1198 if nest_level > 1:
1199- return u""
1200+ return ""
1201
1202 if self.params:
1203 url = self.params.strip()
1204@@ -272,12 +272,12 @@
1205 self.url = unquote(url)
1206
1207 # Disallow javascript links
1208- if u"javascript:" in self.url.lower():
1209+ if "javascript:" in self.url.lower():
1210 return ''
1211
1212 # Disallow non http: links
1213 url_parsed = urlparse(self.url)
1214- if url_parsed[0] and not url_parsed[0].lower().startswith(u'http'):
1215+ if url_parsed[0] and not url_parsed[0].lower().startswith('http'):
1216 return ''
1217
1218 # Prepend http: if it is not present
1219@@ -289,21 +289,21 @@
1220 self.domain = url_parsed[1].lower()
1221
1222 # Remove www for brevity
1223- if self.domain.startswith(u'www.'):
1224+ if self.domain.startswith('www.'):
1225 self.domain = self.domain[4:]
1226
1227 # Quote the url
1228 #self.url="http:"+urlunparse( map(quote, (u"",)+url_parsed[1:]) )
1229- self.url = unicode(urlunparse(
1230+ self.url = str(urlunparse(
1231 [quote(component.encode('utf-8'), safe='/=&?:+') for component in url_parsed]))
1232
1233 if not self.url:
1234- return u""
1235+ return ""
1236
1237 if self.domain:
1238- return u'<a href="%s">' % self.url
1239+ return '<a href="%s">' % self.url
1240 else:
1241- return u""
1242+ return ""
1243
1244 def render_close(self, parser, node_index):
1245
1246@@ -311,19 +311,19 @@
1247 tag_data['link_nest_level'] -= 1
1248
1249 if tag_data['link_nest_level'] > 0:
1250- return u''
1251+ return ''
1252
1253 if self.domain:
1254- return u'</a>' + self.annotate_link(self.domain)
1255+ return '</a>' + self.annotate_link(self.domain)
1256 else:
1257- return u''
1258+ return ''
1259
1260 def annotate_link(self, domain=None):
1261
1262 if domain and self.annotate_links:
1263 return annotate_link(domain)
1264 else:
1265- return u""
1266+ return ""
1267
1268
1269 class QuoteTag(TagBase):
1270@@ -339,12 +339,12 @@
1271
1272 def render_open(self, parser, node_index):
1273 if self.params:
1274- return u'<blockquote><em>%s</em><br/>' % (PostMarkup.standard_replace(self.params))
1275+ return '<blockquote><em>%s</em><br/>' % (PostMarkup.standard_replace(self.params))
1276 else:
1277- return u'<blockquote>'
1278+ return '<blockquote>'
1279
1280 def render_close(self, parser, node_index):
1281- return u"</blockquote>"
1282+ return "</blockquote>"
1283
1284
1285 class SearchTag(TagBase):
1286@@ -361,8 +361,8 @@
1287 search = self.params
1288 else:
1289 search = self.get_contents(parser)
1290- link = u'<a href="%s">' % self.url
1291- if u'%' in link:
1292+ link = '<a href="%s">' % self.url
1293+ if '%' in link:
1294 return link % quote_plus(search.encode('UTF-8'))
1295 else:
1296 return link
1297@@ -370,12 +370,12 @@
1298 def render_close(self, parser, node_index):
1299
1300 if self.label:
1301- ret = u'</a>'
1302+ ret = '</a>'
1303 if self.annotate_links:
1304 ret += annotate_link(self.label)
1305 return ret
1306 else:
1307- return u''
1308+ return ''
1309
1310
1311 class PygmentsCodeTag(TagBase):
1312@@ -421,9 +421,9 @@
1313 contents = self.get_contents(parser)
1314 self.skip_contents(parser)
1315
1316- contents = strip_bbcode(contents).replace(u'"', '%22')
1317+ contents = strip_bbcode(contents).replace('"', '%22')
1318
1319- return u'<img src="%s"></img>' % contents
1320+ return '<img src="%s"></img>' % contents
1321
1322
1323 class ListTag(TagBase):
1324@@ -439,30 +439,30 @@
1325
1326 def render_open(self, parser, node_index):
1327
1328- self.close_tag = u""
1329+ self.close_tag = ""
1330
1331 tag_data = parser.tag_data
1332 tag_data.setdefault('ListTag.count', 0)
1333
1334 if tag_data['ListTag.count']:
1335- return u""
1336+ return ""
1337
1338 tag_data['ListTag.count'] += 1
1339
1340 tag_data['ListItemTag.initial_item'] = True
1341
1342 if self.params == '1':
1343- self.close_tag = u"</li></ol>"
1344- return u"<ol><li>"
1345+ self.close_tag = "</li></ol>"
1346+ return "<ol><li>"
1347 elif self.params == 'a':
1348- self.close_tag = u"</li></ol>"
1349- return u'<ol style="list-style-type: lower-alpha;"><li>'
1350+ self.close_tag = "</li></ol>"
1351+ return '<ol style="list-style-type: lower-alpha;"><li>'
1352 elif self.params == 'A':
1353- self.close_tag = u"</li></ol>"
1354- return u'<ol style="list-style-type: upper-alpha;"><li>'
1355+ self.close_tag = "</li></ol>"
1356+ return '<ol style="list-style-type: upper-alpha;"><li>'
1357 else:
1358- self.close_tag = u"</li></ul>"
1359- return u"<ul><li>"
1360+ self.close_tag = "</li></ul>"
1361+ return "<ul><li>"
1362
1363 def render_close(self, parser, node_index):
1364
1365@@ -482,13 +482,13 @@
1366
1367 tag_data = parser.tag_data
1368 if not tag_data.setdefault('ListTag.count', 0):
1369- return u""
1370+ return ""
1371
1372 if tag_data['ListItemTag.initial_item']:
1373 tag_data['ListItemTag.initial_item'] = False
1374 return
1375
1376- return u"</li><li>"
1377+ return "</li><li>"
1378
1379
1380 class SizeTag(TagBase):
1381@@ -507,18 +507,18 @@
1382 self.size = None
1383
1384 if self.size is None:
1385- return u""
1386+ return ""
1387
1388 self.size = self.validate_size(self.size)
1389
1390- return u'<span style="font-size:%spx">' % self.size
1391+ return '<span style="font-size:%spx">' % self.size
1392
1393 def render_close(self, parser, node_index):
1394
1395 if self.size is None:
1396- return u""
1397+ return ""
1398
1399- return u'</span>'
1400+ return '</span>'
1401
1402 def validate_size(self, size):
1403
1404@@ -541,26 +541,26 @@
1405 self.color = ''.join([c for c in color if c in valid_chars])
1406
1407 if not self.color:
1408- return u""
1409+ return ""
1410
1411- return u'<span style="color:%s">' % self.color
1412+ return '<span style="color:%s">' % self.color
1413
1414 def render_close(self, parser, node_index):
1415
1416 if not self.color:
1417- return u''
1418- return u'</span>'
1419+ return ''
1420+ return '</span>'
1421
1422
1423 class CenterTag(TagBase):
1424
1425 def render_open(self, parser, node_index, **kwargs):
1426
1427- return u'<div style="text-align:center">'
1428+ return '<div style="text-align:center">'
1429
1430 def render_close(self, parser, node_index):
1431
1432- return u'</div>'
1433+ return '</div>'
1434
1435 # http://effbot.org/zone/python-replace.htm
1436
1437@@ -570,10 +570,10 @@
1438 def __init__(self, repl_dict):
1439
1440 # string to string mapping; use a regular expression
1441- keys = repl_dict.keys()
1442+ keys = list(repl_dict.keys())
1443 keys.sort() # lexical order
1444 keys.reverse() # use longest match first
1445- pattern = u"|".join([re.escape(key) for key in keys])
1446+ pattern = "|".join([re.escape(key) for key in keys])
1447 self.pattern = re.compile(pattern)
1448 self.dict = repl_dict
1449
1450@@ -673,16 +673,16 @@
1451
1452 class PostMarkup(object):
1453
1454- standard_replace = MultiReplace({u'<': u'&lt;',
1455- u'>': u'&gt;',
1456- u'&': u'&amp;',
1457- u'\n': u'<br/>'})
1458-
1459- standard_replace_no_break = MultiReplace({u'<': u'&lt;',
1460- u'>': u'&gt;',
1461- u'&': u'&amp;', })
1462-
1463- TOKEN_TAG, TOKEN_PTAG, TOKEN_TEXT = range(3)
1464+ standard_replace = MultiReplace({'<': '&lt;',
1465+ '>': '&gt;',
1466+ '&': '&amp;',
1467+ '\n': '<br/>'})
1468+
1469+ standard_replace_no_break = MultiReplace({'<': '&lt;',
1470+ '>': '&gt;',
1471+ '&': '&amp;', })
1472+
1473+ TOKEN_TAG, TOKEN_PTAG, TOKEN_TEXT = list(range(3))
1474
1475 # I tried to use RE's. Really I did.
1476 @classmethod
1477@@ -702,7 +702,7 @@
1478
1479 while True:
1480
1481- brace_pos = post.find(u'[', pos)
1482+ brace_pos = post.find('[', pos)
1483 if brace_pos == -1:
1484 if pos < len(post):
1485 yield PostMarkup.TOKEN_TEXT, post[pos:], pos, len(post)
1486@@ -713,8 +713,8 @@
1487 pos = brace_pos
1488 end_pos = pos + 1
1489
1490- open_tag_pos = post.find(u'[', end_pos)
1491- end_pos = find_first(post, end_pos, u']=')
1492+ open_tag_pos = post.find('[', end_pos)
1493+ end_pos = find_first(post, end_pos, ']=')
1494 if end_pos == -1:
1495 yield PostMarkup.TOKEN_TEXT, post[pos:], pos, len(post)
1496 return
1497@@ -736,20 +736,20 @@
1498 while post[end_pos] == ' ':
1499 end_pos += 1
1500 if post[end_pos] != '"':
1501- end_pos = post.find(u']', end_pos + 1)
1502+ end_pos = post.find(']', end_pos + 1)
1503 if end_pos == -1:
1504 return
1505 yield PostMarkup.TOKEN_TAG, post[pos:end_pos + 1], pos, end_pos + 1
1506 else:
1507- end_pos = find_first(post, end_pos, u'"]')
1508+ end_pos = find_first(post, end_pos, '"]')
1509
1510 if end_pos == -1:
1511 return
1512 if post[end_pos] == '"':
1513- end_pos = post.find(u'"', end_pos + 1)
1514+ end_pos = post.find('"', end_pos + 1)
1515 if end_pos == -1:
1516 return
1517- end_pos = post.find(u']', end_pos + 1)
1518+ end_pos = post.find(']', end_pos + 1)
1519 if end_pos == -1:
1520 return
1521 yield PostMarkup.TOKEN_PTAG, post[pos:end_pos + 1], pos, end_pos + 1
1522@@ -763,7 +763,7 @@
1523 """Surrounds urls with url bbcode tags."""
1524
1525 def repl(match):
1526- return u'[url]%s[/url]' % match.group(0)
1527+ return '[url]%s[/url]' % match.group(0)
1528
1529 text_tokens = []
1530 for tag_type, tag_token, start_pos, end_pos in self.tokenize(postmarkup):
1531@@ -773,7 +773,7 @@
1532 else:
1533 text_tokens.append(tag_token)
1534
1535- return u"".join(text_tokens)
1536+ return "".join(text_tokens)
1537
1538 def __init__(self, tag_factory=None):
1539
1540@@ -784,10 +784,10 @@
1541
1542 add_tag = self.tag_factory.add_tag
1543
1544- add_tag(SimpleTag, u'b', u'strong')
1545- add_tag(SimpleTag, u'i', u'em')
1546- add_tag(SimpleTag, u'u', u'u')
1547- add_tag(SimpleTag, u's', u's')
1548+ add_tag(SimpleTag, 'b', 'strong')
1549+ add_tag(SimpleTag, 'i', 'em')
1550+ add_tag(SimpleTag, 'u', 'u')
1551+ add_tag(SimpleTag, 's', 's')
1552
1553 def get_supported_tags(self):
1554 """Returns a list of the supported tags."""
1555@@ -808,8 +808,8 @@
1556
1557 """
1558
1559- if not isinstance(post_markup, unicode):
1560- post_markup = unicode(post_markup, encoding, 'replace')
1561+ if not isinstance(post_markup, str):
1562+ post_markup = str(post_markup, encoding, 'replace')
1563
1564 if auto_urls:
1565 post_markup = self.tagify_urls(post_markup)
1566@@ -901,24 +901,24 @@
1567 elif tag_type == PostMarkup.TOKEN_TAG:
1568 tag_token = tag_token[1:-1].lstrip()
1569 if ' ' in tag_token:
1570- tag_name, tag_attribs = tag_token.split(u' ', 1)
1571+ tag_name, tag_attribs = tag_token.split(' ', 1)
1572 tag_attribs = tag_attribs.strip()
1573 else:
1574 if '=' in tag_token:
1575- tag_name, tag_attribs = tag_token.split(u'=', 1)
1576+ tag_name, tag_attribs = tag_token.split('=', 1)
1577 tag_attribs = tag_attribs.strip()
1578 else:
1579 tag_name = tag_token
1580- tag_attribs = u""
1581+ tag_attribs = ""
1582 else:
1583 tag_token = tag_token[1:-1].lstrip()
1584- tag_name, tag_attribs = tag_token.split(u'=', 1)
1585+ tag_name, tag_attribs = tag_token.split('=', 1)
1586 tag_attribs = tag_attribs.strip()[1:-1]
1587
1588 tag_name = tag_name.strip().lower()
1589
1590 end_tag = False
1591- if tag_name.startswith(u'/'):
1592+ if tag_name.startswith('/'):
1593 end_tag = True
1594 tag_name = tag_name[1:]
1595
1596@@ -996,7 +996,7 @@
1597 text.append(node_text)
1598 parser.render_node_index += 1
1599
1600- return u"".join(text)
1601+ return "".join(text)
1602
1603 __call__ = render_to_html
1604
1605@@ -1009,7 +1009,7 @@
1606 post_markup = create(use_pygments=True)
1607
1608 tests = []
1609- print """<link rel="stylesheet" href="code.css" type="text/css" />\n"""
1610+ print("""<link rel="stylesheet" href="code.css" type="text/css" />\n""")
1611
1612 tests.append(']')
1613 tests.append('[')
1614@@ -1020,8 +1020,8 @@
1615 tests.append('[link http://www.willmcgugan.com]My homepage[/link]')
1616 tests.append('[link]http://www.willmcgugan.com[/link]')
1617
1618- tests.append(u"[b]Hello AndrУЉ[/b]")
1619- tests.append(u"[google]AndrУЉ[/google]")
1620+ tests.append("[b]Hello AndrУЉ[/b]")
1621+ tests.append("[google]AndrУЉ[/google]")
1622 tests.append('[s]Strike through[/s]')
1623 tests.append('[b]bold [i]bold and italic[/b] italic[/i]')
1624 tests.append('[google]Will McGugan[/google]')
1625@@ -1042,7 +1042,7 @@
1626 return self.__str__()
1627 [/code]""")
1628
1629- tests.append(u"[img]http://upload.wikimedia.org/wikipedia/commons"
1630+ tests.append("[img]http://upload.wikimedia.org/wikipedia/commons"
1631 '/6/61/Triops_longicaudatus.jpg[/img]')
1632
1633 tests.append('[list][*]Apples[*]Oranges[*]Pears[/list]')
1634@@ -1081,13 +1081,13 @@
1635 tests.append(
1636 'Nested urls, i.e. [url][url]www.becontrary.com[/url][/url], are condensed in to a single tag.')
1637
1638- tests.append(u'[google]ЩИЮВfvЮИУАsz[/google]')
1639-
1640- tests.append(u'[size 30]Hello, World![/size]')
1641-
1642- tests.append(u'[color red]This should be red[/color]')
1643- tests.append(u'[color #0f0]This should be green[/color]')
1644- tests.append(u"[center]This should be in the center!")
1645+ tests.append('[google]ЩИЮВfvЮИУАsz[/google]')
1646+
1647+ tests.append('[size 30]Hello, World![/size]')
1648+
1649+ tests.append('[color red]This should be red[/color]')
1650+ tests.append('[color #0f0]This should be green[/color]')
1651+ tests.append("[center]This should be in the center!")
1652
1653 tests.append(
1654 'Nested urls, i.e. [url][url]www.becontrary.com[/url][/url], are condensed in to a single tag.')
1655@@ -1125,17 +1125,17 @@
1656 # tests=["""[b]b[i]i[/b][/i]"""]
1657
1658 for test in tests:
1659- print u"<pre>%s</pre>" % str(test.encode('ascii', 'xmlcharrefreplace'))
1660- print u"<p>%s</p>" % str(post_markup(test).encode('ascii', 'xmlcharrefreplace'))
1661- print u"<hr/>"
1662- print
1663-
1664- print repr(post_markup('[url=<script>Attack</script>]Attack[/url]'))
1665-
1666- print repr(post_markup('http://www.google.com/search?as_q=bbcode&btnG=%D0%9F%D0%BE%D0%B8%D1%81%D0%BA'))
1667+ print("<pre>%s</pre>" % str(test.encode('ascii', 'xmlcharrefreplace')))
1668+ print("<p>%s</p>" % str(post_markup(test).encode('ascii', 'xmlcharrefreplace')))
1669+ print("<hr/>")
1670+ print()
1671+
1672+ print(repr(post_markup('[url=<script>Attack</script>]Attack[/url]')))
1673+
1674+ print(repr(post_markup('http://www.google.com/search?as_q=bbcode&btnG=%D0%9F%D0%BE%D0%B8%D1%81%D0%BA')))
1675
1676 p = create(use_pygments=False)
1677- print(p('[code]foo\nbar[/code]'))
1678+ print((p('[code]foo\nbar[/code]')))
1679
1680 # print render_bbcode("[b]For the lazy, use the http://www.willmcgugan.com
1681 # render_bbcode function.[/b]")
1682
1683=== modified file 'pybb/migrations/0001_initial.py'
1684--- pybb/migrations/0001_initial.py 2016-12-13 18:28:51 +0000
1685+++ pybb/migrations/0001_initial.py 2019-06-14 07:22:45 +0000
1686@@ -1,5 +1,5 @@
1687 # -*- coding: utf-8 -*-
1688-from __future__ import unicode_literals
1689+
1690
1691 from django.db import models, migrations
1692 from django.conf import settings
1693
1694=== modified file 'pybb/migrations/0002_auto_20161001_2046.py'
1695--- pybb/migrations/0002_auto_20161001_2046.py 2016-10-08 09:30:34 +0000
1696+++ pybb/migrations/0002_auto_20161001_2046.py 2019-06-14 07:22:45 +0000
1697@@ -1,5 +1,5 @@
1698 # -*- coding: utf-8 -*-
1699-from __future__ import unicode_literals
1700+
1701
1702 from django.db import models, migrations
1703
1704
1705=== modified file 'pybb/migrations/0003_remove_post_user_ip.py'
1706--- pybb/migrations/0003_remove_post_user_ip.py 2018-10-03 09:01:09 +0000
1707+++ pybb/migrations/0003_remove_post_user_ip.py 2019-06-14 07:22:45 +0000
1708@@ -1,6 +1,6 @@
1709 # -*- coding: utf-8 -*-
1710 # Generated by Django 1.11.12 on 2018-10-02 19:31
1711-from __future__ import unicode_literals
1712+
1713
1714 from django.db import migrations
1715
1716
1717=== modified file 'pybb/migrations/0004_auto_20181209_1334.py'
1718--- pybb/migrations/0004_auto_20181209_1334.py 2018-12-10 16:37:12 +0000
1719+++ pybb/migrations/0004_auto_20181209_1334.py 2019-06-14 07:22:45 +0000
1720@@ -1,6 +1,6 @@
1721 # -*- coding: utf-8 -*-
1722 # Generated by Django 1.11.12 on 2018-12-09 13:34
1723-from __future__ import unicode_literals
1724+
1725
1726 from django.db import migrations, models
1727 import django.db.models.deletion
1728
1729=== modified file 'pybb/migrations/0005_auto_20181221_1047.py'
1730--- pybb/migrations/0005_auto_20181221_1047.py 2018-12-21 09:50:32 +0000
1731+++ pybb/migrations/0005_auto_20181221_1047.py 2019-06-14 07:22:45 +0000
1732@@ -1,6 +1,6 @@
1733 # -*- coding: utf-8 -*-
1734 # Generated by Django 1.11.12 on 2018-12-21 10:47
1735-from __future__ import unicode_literals
1736+
1737
1738 from django.db import migrations
1739
1740
1741=== modified file 'pybb/models.py'
1742--- pybb/models.py 2019-03-24 08:28:15 +0000
1743+++ pybb/models.py 2019-06-14 07:22:45 +0000
1744@@ -61,7 +61,7 @@
1745 # See also settings.INTERNAL_PERM
1746 permissions = (("can_access_internal", "Can access Internal Forums"),)
1747
1748- def __unicode__(self):
1749+ def __str__(self):
1750 return self.name
1751
1752 def forum_count(self):
1753@@ -100,7 +100,7 @@
1754 verbose_name = _('Forum')
1755 verbose_name_plural = _('Forums')
1756
1757- def __unicode__(self):
1758+ def __str__(self):
1759 return self.name
1760
1761 def topic_count(self):
1762@@ -154,7 +154,7 @@
1763 verbose_name = _('Topic')
1764 verbose_name_plural = _('Topics')
1765
1766- def __unicode__(self):
1767+ def __str__(self):
1768 return self.name
1769
1770 @property
1771@@ -215,7 +215,7 @@
1772 if self.markup == 'bbcode':
1773 self.body_html = mypostmarkup.markup(self.body, auto_urls=False)
1774 elif self.markup == 'markdown':
1775- self.body_html = unicode(do_wl_markdown(
1776+ self.body_html = str(do_wl_markdown(
1777 self.body, 'bleachit'))
1778 else:
1779 raise Exception('Invalid markup property: %s' % self.markup)
1780@@ -308,7 +308,7 @@
1781 tail = len(self.body) > LIMIT and '...' or ''
1782 return self.body[:LIMIT] + tail
1783
1784- __unicode__ = summary
1785+ __str__ = summary
1786
1787 def save(self, *args, **kwargs):
1788 if self.created is None:
1789@@ -380,8 +380,8 @@
1790 self.time = datetime.now()
1791 super(Read, self).save(*args, **kwargs)
1792
1793- def __unicode__(self):
1794- return u'T[%d], U[%d]: %s' % (self.topic.id, self.user.id, unicode(self.time))
1795+ def __str__(self):
1796+ return 'T[%d], U[%d]: %s' % (self.topic.id, self.user.id, str(self.time))
1797
1798
1799 class Attachment(models.Model):
1800@@ -401,7 +401,7 @@
1801 str(self.id) + settings.SECRET_KEY).hexdigest()
1802 super(Attachment, self).save(*args, **kwargs)
1803
1804- def __unicode__(self):
1805+ def __str__(self):
1806 return self.name
1807
1808 def get_absolute_url(self):
1809
1810=== modified file 'pybb/orm.py'
1811--- pybb/orm.py 2016-12-13 18:28:51 +0000
1812+++ pybb/orm.py 2019-06-14 07:22:45 +0000
1813@@ -6,12 +6,12 @@
1814 rel_field = rel_qs.model._meta.get_field(rel_field_name)
1815 cache_field_name = '%s_cache' % rel_qs.model.__name__.lower()
1816
1817- rel_objects = rel_qs.filter(**{'%s__in' % rel_field.name: obj_map.keys()})
1818+ rel_objects = rel_qs.filter(**{'%s__in' % rel_field.name: list(obj_map.keys())})
1819
1820 temp_map = {}
1821 for rel_obj in rel_objects:
1822 pk = getattr(rel_obj, rel_field.attname)
1823 temp_map.setdefault(pk, []).append(rel_obj)
1824
1825- for pk, rel_list in temp_map.iteritems():
1826+ for pk, rel_list in temp_map.items():
1827 setattr(obj_map[pk], cache_field_name, rel_list)
1828
1829=== modified file 'pybb/templates/pybb/last_posts.html'
1830--- pybb/templates/pybb/last_posts.html 2019-03-16 20:10:31 +0000
1831+++ pybb/templates/pybb/last_posts.html 2019-06-14 07:22:45 +0000
1832@@ -12,7 +12,7 @@
1833 <li>
1834 {{ post.topic.forum.name }}<br />
1835 <a href="{{ post.get_absolute_url }}" title="{{ post.topic.name }}">{{ post.topic.name|truncatechars:30 }}</a><br />
1836- by {{ post.user|user_link }} {{ post.created|minutes }} ago
1837+ by {{ post.user|user_link }} {{ post.created|elapsed_time }} ago
1838 </li>
1839 {% endfor %}
1840 <li class="small">
1841
1842=== modified file 'pybb/templatetags/pybb_extras.py'
1843--- pybb/templatetags/pybb_extras.py 2019-03-23 09:00:44 +0000
1844+++ pybb/templatetags/pybb_extras.py 2019-06-14 07:22:45 +0000
1845@@ -7,7 +7,7 @@
1846 from django import template
1847 from django.utils.safestring import mark_safe
1848 from django.template.defaultfilters import stringfilter
1849-from django.utils.encoding import smart_unicode
1850+from django.utils.encoding import smart_text
1851 from django.utils.html import escape
1852
1853 from pybb.models import Post, Forum, Topic, Read
1854@@ -42,12 +42,12 @@
1855
1856
1857 @register.simple_tag
1858-def pybb_link(object, anchor=u''):
1859+def pybb_link(object, anchor=''):
1860 """Return A tag with link to object."""
1861
1862 url = hasattr(
1863 object, 'get_absolute_url') and object.get_absolute_url() or None
1864- anchor = anchor or smart_unicode(object)
1865+ anchor = anchor or smart_text(object)
1866 return mark_safe('<a href="%s">%s</a>' % (url, escape(anchor)))
1867
1868
1869
1870=== modified file 'pybb/util.py'
1871--- pybb/util.py 2018-12-21 11:14:06 +0000
1872+++ pybb/util.py 2019-06-14 07:22:45 +0000
1873@@ -10,7 +10,7 @@
1874 from django.http import HttpResponse
1875 from django.utils.functional import Promise
1876 from django.utils.translation import check_for_language
1877-from django.utils.encoding import force_unicode
1878+from django.utils.encoding import force_text
1879 from django import forms
1880 from django.core.paginator import Paginator, EmptyPage, InvalidPage
1881 from django.conf import settings
1882@@ -59,7 +59,7 @@
1883 if request.method == 'POST':
1884 try:
1885 response = func(request, *args, **kwargs)
1886- except Exception, ex:
1887+ except Exception as ex:
1888 response = {'error': traceback.format_exc()}
1889 else:
1890 response = {'error': {'type': 403,
1891@@ -76,7 +76,7 @@
1892
1893 def default(self, o):
1894 if isinstance(o, Promise):
1895- return force_unicode(o)
1896+ return force_text(o)
1897 else:
1898 return super(LazyJSONEncoder, self).default(o)
1899
1900@@ -144,7 +144,7 @@
1901 # Apply the new content
1902 found_string.parent.contents = new_content
1903
1904- return unicode(soup)
1905+ return str(soup)
1906
1907
1908 def quote_text(text, user, markup):
1909
1910=== modified file 'pybb/views.py'
1911--- pybb/views.py 2019-05-27 15:46:36 +0000
1912+++ pybb/views.py 2019-06-14 07:22:45 +0000
1913@@ -365,7 +365,7 @@
1914 content = request.POST.get('content')
1915 markup = request.POST.get('markup')
1916
1917- if not markup in dict(MARKUP_CHOICES).keys():
1918+ if not markup in list(dict(MARKUP_CHOICES).keys()):
1919 return {'error': 'Invalid markup'}
1920
1921 if not content:
1922@@ -374,7 +374,7 @@
1923 if markup == 'bbcode':
1924 html = mypostmarkup.markup(content, auto_urls=False)
1925 elif markup == 'markdown':
1926- html = unicode(do_wl_markdown(content, 'bleachit'))
1927+ html = str(do_wl_markdown(content, 'bleachit'))
1928
1929 html = urlize(html)
1930 return {'content': html}
1931
1932=== added file 'python3.txt'
1933--- python3.txt 1970-01-01 00:00:00 +0000
1934+++ python3.txt 2019-06-14 07:22:45 +0000
1935@@ -0,0 +1,104 @@
1936+Anmerkungen zum update auf python3:
1937+
1938+Der code für die implementierung vom gravatar-service ist ungetestet, da es nie
1939+implementiert wurde.
1940+
1941+virtualenvironment
1942+------------------
1943+
1944+Installiere die richtige python Version (hier python3.6).
1945+Erstellen des virtualenvironments:
1946+
1947+ $:> virtualenv --python=python3.6 wlwebsite
1948+
1949+Getestet
1950+========
1951+
1952+Wiki
1953+----
1954+- editieren, Vorschau, speichern
1955+- upload von Bildern
1956+- wiki-syntax
1957+- History: Diff anzeigen, Artikel wieder herstellen (Revert)
1958+- Backlinks
1959+- Artikel beobachten
1960+- Atom feed
1961+
1962+Registrierung
1963+-------------
1964+- Registrierung, Anmeldung
1965+- Passwortänderung
1966+- Passwort vergessen
1967+- Logout
1968+
1969+Maps
1970+----
1971+- Upload maps with comment
1972+- Edit uploader comment, also with polish characters
1973+- Rate a map
1974+- Make a threadedcomment on a map
1975+- Delete map
1976+
1977+WlHelp (Encyclopedia)
1978+---------------------
1979+- Run ./manage.py update_help
1980+- Run ./manage.py update_help_pdf
1981+- views
1982+
1983+Screenshots
1984+-----------
1985+- upload a new screenshot
1986+- delete screenshot
1987+- delete category
1988+
1989+Search
1990+------
1991+- update index
1992+- searching
1993+
1994+News
1995+----
1996+- create new News
1997+- public in future (not show)
1998+- edit News
1999+- deletion
2000+- commenting
2001+
2002+Polls
2003+-----
2004+- create one
2005+- edit
2006+- deletion
2007+
2008+Webchat
2009+-------
2010+- connect
2011+
2012+Documentation
2013+-------------
2014+- run ./manage.py create_docs
2015+- views
2016+
2017+wlprofile
2018+---------
2019+- editing (Website/signature)
2020+
2021+pybb
2022+----
2023+- create topic
2024+- create post
2025+- post with markdown/bbcode
2026+- quoting a post
2027+- edit own post
2028+- stick/unstick topic
2029+- close/open topic
2030+- subscribe to topic (emailing works, also then)
2031+- edit foreign posts by forum mod
2032+- delete post by forum mod
2033+- delete post by posts author
2034+
2035+
2036+Others
2037+------
2038+- changelog page
2039+- developers page
2040
2041=== modified file 'threadedcomments/admin.py'
2042--- threadedcomments/admin.py 2018-10-04 16:00:34 +0000
2043+++ threadedcomments/admin.py 2019-06-14 07:22:45 +0000
2044@@ -12,7 +12,7 @@
2045 'date_modified', 'date_approved', 'is_approved')}),
2046 )
2047 list_display = ('user', 'date_submitted', 'content_type',
2048- 'get_content_object', 'parent', '__unicode__')
2049+ 'get_content_object', 'parent', '__str__')
2050 list_filter = ('date_submitted',)
2051 date_hierarchy = 'date_submitted'
2052 search_fields = ('comment', 'user__username')
2053
2054=== modified file 'threadedcomments/migrations/0001_initial.py'
2055--- threadedcomments/migrations/0001_initial.py 2016-12-13 18:28:51 +0000
2056+++ threadedcomments/migrations/0001_initial.py 2019-06-14 07:22:45 +0000
2057@@ -1,5 +1,5 @@
2058 # -*- coding: utf-8 -*-
2059-from __future__ import unicode_literals
2060+
2061
2062 from django.db import models, migrations
2063 import datetime
2064
2065=== modified file 'threadedcomments/migrations/0002_auto_20181003_1238.py'
2066--- threadedcomments/migrations/0002_auto_20181003_1238.py 2018-10-03 10:53:41 +0000
2067+++ threadedcomments/migrations/0002_auto_20181003_1238.py 2019-06-14 07:22:45 +0000
2068@@ -1,6 +1,6 @@
2069 # -*- coding: utf-8 -*-
2070 # Generated by Django 1.11.12 on 2018-10-03 12:38
2071-from __future__ import unicode_literals
2072+
2073
2074 from django.db import migrations
2075
2076
2077=== modified file 'threadedcomments/models.py'
2078--- threadedcomments/models.py 2018-10-04 16:00:34 +0000
2079+++ threadedcomments/models.py 2019-06-14 07:22:45 +0000
2080@@ -6,7 +6,7 @@
2081 from django.db.models import Q
2082 from django.utils.translation import ugettext_lazy as _
2083 from django.conf import settings
2084-from django.utils.encoding import force_unicode
2085+from django.utils.encoding import force_text
2086
2087 DEFAULT_MAX_COMMENT_LENGTH = getattr(
2088 settings, 'DEFAULT_MAX_COMMENT_LENGTH', 1000)
2089@@ -178,7 +178,7 @@
2090 objects = ThreadedCommentManager()
2091 public = PublicThreadedCommentManager()
2092
2093- def __unicode__(self):
2094+ def __str__(self):
2095 if len(self.comment) > 50:
2096 return self.comment[:50] + '...'
2097 return self.comment[:50]
2098
2099=== modified file 'threadedcomments/templatetags/gravatar.py'
2100--- threadedcomments/templatetags/gravatar.py 2018-04-03 05:18:03 +0000
2101+++ threadedcomments/templatetags/gravatar.py 2019-06-14 07:22:45 +0000
2102@@ -1,16 +1,16 @@
2103 from django import template
2104 from django.conf import settings
2105 from django.template.defaultfilters import stringfilter
2106-from django.utils.encoding import smart_str
2107+from django.utils.encoding import smart_bytes
2108 from django.utils.safestring import mark_safe
2109 from hashlib import md5 as md5_constructor
2110-import urllib
2111+import urllib.request, urllib.parse, urllib.error
2112
2113 GRAVATAR_MAX_RATING = getattr(settings, 'GRAVATAR_MAX_RATING', 'R')
2114 GRAVATAR_DEFAULT_IMG = getattr(settings, 'GRAVATAR_DEFAULT_IMG', 'img:blank')
2115 GRAVATAR_SIZE = getattr(settings, 'GRAVATAR_SIZE', 80)
2116
2117-GRAVATAR_URL = u'http://www.gravatar.com/avatar.php?gravatar_id=%(hash)s&rating=%(rating)s&size=%(size)s&default=%(default)s'
2118+GRAVATAR_URL = 'http://www.gravatar.com/avatar.php?gravatar_id=%(hash)s&rating=%(rating)s&size=%(size)s&default=%(default)s'
2119
2120
2121 def get_gravatar_url(parser, token):
2122@@ -34,12 +34,12 @@
2123 words = token.contents.split()
2124 tagname = words.pop(0)
2125 if len(words) < 2:
2126- raise template.TemplateSyntaxError, '%r tag: At least one argument should be provided.' % tagname
2127+ raise template.TemplateSyntaxError('%r tag: At least one argument should be provided.' % tagname)
2128 if words.pop(0) != 'for':
2129- raise template.TemplateSyntaxError, '%r tag: Syntax is {% get_gravatar_url for myemailvar rating 'R" size 80 default img:blank as gravatar_url %}, where everything after myemailvar is optional."
2130+ raise template.TemplateSyntaxError('%r tag: Syntax is {% get_gravatar_url for myemailvar rating 'R" size 80 default img:blank as gravatar_url %}, where everything after myemailvar is optional.")
2131 email = words.pop(0)
2132 if len(words) % 2 != 0:
2133- raise template.TemplateSyntaxError, '%r tag: Imbalanced number of arguments.' % tagname
2134+ raise template.TemplateSyntaxError('%r tag: Imbalanced number of arguments.' % tagname)
2135 args = {
2136 'email': email,
2137 'rating': GRAVATAR_MAX_RATING,
2138@@ -49,8 +49,8 @@
2139 for name, value in zip(words[::2], words[1::2]):
2140 name = name.lower()
2141 if name not in ('rating', 'size', 'default', 'as'):
2142- raise template.TemplateSyntaxError, '%r tag: Invalid argument %r.' % tagname, name
2143- args[smart_str(name)] = value
2144+ raise template.TemplateSyntaxError('%r tag: Invalid argument %r.' % tagname).with_traceback(name)
2145+ args[smart_bytes(name)] = value
2146 return GravatarUrlNode(**args)
2147
2148
2149@@ -93,7 +93,7 @@
2150 'hash': md5_constructor(email).hexdigest(),
2151 'rating': rating,
2152 'size': size,
2153- 'default': urllib.quote_plus(default),
2154+ 'default': urllib.parse.quote_plus(default),
2155 }
2156 url = GRAVATAR_URL % gravatargs
2157 if 'as' in self.other_kwargs:
2158@@ -112,7 +112,7 @@
2159 'hash': hashed_email,
2160 'rating': GRAVATAR_MAX_RATING,
2161 'size': GRAVATAR_SIZE,
2162- 'default': urllib.quote_plus(GRAVATAR_DEFAULT_IMG),
2163+ 'default': urllib.parse.quote_plus(GRAVATAR_DEFAULT_IMG),
2164 })
2165 gravatar = stringfilter(gravatar)
2166
2167
2168=== modified file 'threadedcomments/templatetags/threadedcommentstags.py'
2169--- threadedcomments/templatetags/threadedcommentstags.py 2018-10-04 06:15:32 +0000
2170+++ threadedcomments/templatetags/threadedcommentstags.py 2019-06-14 07:22:45 +0000
2171@@ -2,7 +2,7 @@
2172 from django import template
2173 from django.contrib.contenttypes.models import ContentType
2174 from django.urls import reverse
2175-from django.utils.encoding import force_unicode
2176+from django.utils.encoding import force_text
2177 from django.utils.safestring import mark_safe
2178 from threadedcomments.models import ThreadedComment
2179 from threadedcomments.forms import ThreadedCommentForm
2180@@ -28,7 +28,7 @@
2181 kwargs = get_contenttype_kwargs(content_object)
2182 if parent:
2183 if not isinstance(parent, ThreadedComment):
2184- raise template.TemplateSyntaxError, 'get_comment_url requires its parent object to be of type ThreadedComment'
2185+ raise template.TemplateSyntaxError('get_comment_url requires its parent object to be of type ThreadedComment')
2186 kwargs.update({'parent_id': getattr(
2187 parent, 'pk', getattr(parent, 'id'))})
2188 return reverse('tc_comment_parent', kwargs=kwargs)
2189@@ -48,7 +48,7 @@
2190 kwargs.update({'ajax': ajax_type})
2191 if parent:
2192 if not isinstance(parent, ThreadedComment):
2193- raise template.TemplateSyntaxError, 'get_comment_url_ajax requires its parent object to be of type ThreadedComment'
2194+ raise template.TemplateSyntaxError('get_comment_url_ajax requires its parent object to be of type ThreadedComment')
2195 kwargs.update({'parent_id': getattr(
2196 parent, 'pk', getattr(parent, 'id'))})
2197 return reverse('tc_comment_parent_ajax', kwargs=kwargs)
2198@@ -63,7 +63,7 @@
2199 try:
2200 return get_comment_url_ajax(content_object, parent, ajax_type='json')
2201 except template.TemplateSyntaxError:
2202- raise template.TemplateSyntaxError, 'get_comment_url_json requires its parent object to be of type ThreadedComment'
2203+ raise template.TemplateSyntaxError('get_comment_url_json requires its parent object to be of type ThreadedComment')
2204 return ''
2205
2206
2207@@ -74,7 +74,7 @@
2208 try:
2209 return get_comment_url_ajax(content_object, parent, ajax_type='xml')
2210 except template.TemplateSyntaxError:
2211- raise template.TemplateSyntaxError, 'get_comment_url_xml requires its parent object to be of type ThreadedComment'
2212+ raise template.TemplateSyntaxError('get_comment_url_xml requires its parent object to be of type ThreadedComment')
2213 return ''
2214
2215
2216@@ -92,13 +92,13 @@
2217 try:
2218 split = token.split_contents()
2219 except ValueError:
2220- raise template.TemplateSyntaxError, '%r tag must be of format {%% %r COMMENT %%} or of format {%% %r COMMENT as CONTEXT_VARIABLE %%}' % (token.contents.split()[0], token.contents.split()[0], token.contents.split()[0])
2221+ raise template.TemplateSyntaxError('%r tag must be of format {%% %r COMMENT %%} or of format {%% %r COMMENT as CONTEXT_VARIABLE %%}' % (token.contents.split()[0], token.contents.split()[0], token.contents.split()[0]))
2222 if len(split) == 2:
2223 return AutoTransformMarkupNode(split[1])
2224 elif len(split) == 4:
2225 return AutoTransformMarkupNode(split[1], context_name=split[3])
2226 else:
2227- raise template.TemplateSyntaxError, 'Invalid number of arguments for tag %r' % split[0]
2228+ raise template.TemplateSyntaxError('Invalid number of arguments for tag %r' % split[0])
2229
2230
2231 class AutoTransformMarkupNode(template.Node):
2232@@ -167,9 +167,9 @@
2233 try:
2234 split = token.split_contents()
2235 except ValueError:
2236- raise template.TemplateSyntaxError, error_message
2237+ raise template.TemplateSyntaxError(error_message)
2238 if split[1] != 'for' or split[3] != 'as':
2239- raise template.TemplateSyntaxError, error_message
2240+ raise template.TemplateSyntaxError(error_message)
2241 return ThreadedCommentCountNode(split[2], split[4])
2242
2243
2244@@ -202,11 +202,11 @@
2245 try:
2246 split = token.split_contents()
2247 except ValueError:
2248- raise template.TemplateSyntaxError, error_message
2249+ raise template.TemplateSyntaxError(error_message)
2250 if split[1] != 'as':
2251- raise template.TemplateSyntaxError, error_message
2252+ raise template.TemplateSyntaxError(error_message)
2253 if len(split) != 3:
2254- raise template.TemplateSyntaxError, error_message
2255+ raise template.TemplateSyntaxError(error_message)
2256
2257 return ThreadedCommentFormNode(split[2])
2258
2259@@ -230,11 +230,11 @@
2260 try:
2261 split = token.split_contents()
2262 except ValueError:
2263- raise template.TemplateSyntaxError, error_message
2264+ raise template.TemplateSyntaxError(error_message)
2265 if len(split) != 4:
2266- raise template.TemplateSyntaxError, error_message
2267+ raise template.TemplateSyntaxError(error_message)
2268 if split[2] != 'as':
2269- raise template.TemplateSyntaxError, error_message
2270+ raise template.TemplateSyntaxError(error_message)
2271
2272 return LatestCommentsNode(split[1], split[3])
2273
2274@@ -259,9 +259,9 @@
2275 try:
2276 split = token.split_contents()
2277 except ValueError:
2278- raise template.TemplateSyntaxError, error_message
2279+ raise template.TemplateSyntaxError(error_message)
2280 if len(split) != 5:
2281- raise template.TemplateSyntaxError, error_message
2282+ raise template.TemplateSyntaxError(error_message)
2283
2284 return UserCommentsNode(split[2], split[4])
2285
2286@@ -285,9 +285,9 @@
2287 try:
2288 split = token.split_contents()
2289 except ValueError:
2290- raise template.TemplateSyntaxError, error_message
2291+ raise template.TemplateSyntaxError(error_message)
2292 if len(split) != 5:
2293- raise template.TemplateSyntaxError, error_message
2294+ raise template.TemplateSyntaxError(error_message)
2295
2296 return UserCommentCountNode(split[2], split[4])
2297
2298
2299=== modified file 'threadedcomments/utils.py'
2300--- threadedcomments/utils.py 2016-12-13 18:28:51 +0000
2301+++ threadedcomments/utils.py 2019-06-14 07:22:45 +0000
2302@@ -1,7 +1,7 @@
2303 from django.core.serializers import serialize
2304 from django.http import HttpResponse
2305 from django.utils.functional import Promise
2306-from django.utils.encoding import force_unicode
2307+from django.utils.encoding import force_text
2308 import json as simplejson
2309
2310
2311@@ -9,7 +9,7 @@
2312
2313 def default(self, obj):
2314 if isinstance(obj, Promise):
2315- return force_unicode(obj)
2316+ return force_text(obj)
2317 return obj
2318
2319
2320
2321=== modified file 'threadedcomments/views.py'
2322--- threadedcomments/views.py 2019-03-31 11:08:21 +0000
2323+++ threadedcomments/views.py 2019-06-14 07:22:45 +0000
2324@@ -121,7 +121,7 @@
2325 </errorlist>
2326 """
2327 response_str = Template(template_str).render(
2328- Context({'errors': zip(form.errors.values(), form.errors.keys())}))
2329+ Context({'errors': list(zip(list(form.errors.values()), list(form.errors.keys())))}))
2330 return XMLResponse(response_str, is_iterable=False)
2331 else:
2332 return _preview(request, context_processors, extra_context, form_class=form_class)
2333
2334=== modified file 'widelandslib/make_flow_diagram.py'
2335--- widelandslib/make_flow_diagram.py 2019-03-31 11:08:21 +0000
2336+++ widelandslib/make_flow_diagram.py 2019-06-14 07:22:45 +0000
2337@@ -68,7 +68,7 @@
2338 def add_building(g, b, limit_inputs=None, limit_outputs=None, limit_buildings=None, link_workers=True, limit_recruits=None):
2339 # Add the nice node
2340 workers = ''
2341- if isinstance(b, (ProductionSite,)):
2342+ if isinstance(b, ProductionSite):
2343 workers = r"""<table border="0px" cellspacing="0">"""
2344 for worker in b.workers:
2345 wo = b.tribe.workers[worker]
2346@@ -86,7 +86,7 @@
2347 g.add_edge(Edge(b.name, wo.name, color='darkgreen'))
2348 workers += r"""</table>"""
2349
2350- if isinstance(b, (MilitarySite,)):
2351+ if isinstance(b, MilitarySite):
2352 workers = r"""<table border="0px" cellspacing="0">"""
2353 workers += (r"""<tr><td border="0px">Keeps %s Soldiers</td></tr>"""
2354 r"""<tr><td border="0px">Conquers %s fields</td></tr>"""
2355@@ -94,7 +94,7 @@
2356 workers += r"""</table>"""
2357
2358 costs = r"""<tr><td colspan="2"><table border="0px" cellspacing="0">"""
2359- for ware, count in b.buildcost.items():
2360+ for ware, count in list(b.buildcost.items()):
2361 w = b.tribe.wares[ware]
2362 costs += ('<tr><td border="0px">%s x </td><td border="0px"><img src="%s"/></td><td border="0px">%s</td></tr>' %
2363 (count, w.image, w.descname))
2364@@ -133,7 +133,7 @@
2365 else:
2366 g.add_node(n)
2367
2368- if isinstance(b, (ProductionSite,)):
2369+ if isinstance(b, ProductionSite):
2370 # for worker,c in b.workers:
2371 # g.add_edge(Edge(worker, name, color="orange"))
2372
2373@@ -181,24 +181,23 @@
2374 def make_graph(tribe_name):
2375 global tdir
2376 tdir = mkdtemp(prefix='widelands-help')
2377-
2378 json_directory = path.normpath(settings.MEDIA_ROOT + '/map_object_info')
2379- tribeinfo_file = open(path.normpath(
2380- json_directory + '/tribe_' + tribe_name + '.json'), 'r')
2381- tribeinfo = json.load(tribeinfo_file)
2382+ with open(path.normpath(
2383+ json_directory + '/tribe_' + tribe_name + '.json'), 'r') as tribeinfo_file:
2384+ tribeinfo = json.load(tribeinfo_file)
2385
2386 t = Tribe(tribeinfo, json_directory)
2387
2388 g = CleanedDot(concentrate='false', style='filled', bgcolor='white',
2389 overlap='false', splines='true', rankdir='LR')
2390
2391- for name, w in t.wares.items():
2392+ for name, w in list(t.wares.items()):
2393 add_ware(g, w)
2394 #
2395 # for name,w in t.workers.items():
2396 # add_worker(g, w)
2397
2398- for name, b in t.buildings.items():
2399+ for name, b in list(t.buildings.items()):
2400 add_building(g, b, link_workers=False)
2401
2402 g.write_pdf(path.join(tdir, '%s.pdf' % tribe_name))
2403@@ -211,7 +210,7 @@
2404
2405
2406 def make_building_graph(t, building_name):
2407- if isinstance(t, basestring):
2408+ if isinstance(t, str):
2409 t = Tribe(t)
2410
2411 b = t.buildings[building_name]
2412@@ -219,7 +218,7 @@
2413 g = CleanedDot(concentrate='false', bgcolor='transparent',
2414 overlap='false', splines='true', rankdir='LR')
2415
2416- if not isinstance(b, (ProductionSite,)):
2417+ if not isinstance(b, ProductionSite):
2418 inputs, outputs = [], []
2419 else:
2420 # TODO: prepare for tribes having buildings with a ware as both input
2421@@ -254,7 +253,7 @@
2422
2423
2424 def make_worker_graph(t, worker_name):
2425- if isinstance(t, basestring):
2426+ if isinstance(t, str):
2427 t = Tribe(t)
2428
2429 w = t.workers[worker_name]
2430@@ -262,7 +261,7 @@
2431 g = CleanedDot(concentrate='false', bgcolor='transparent',
2432 overlap='false', splines='true', rankdir='LR')
2433
2434- buildings = [bld for bld in t.buildings.values() if
2435+ buildings = [bld for bld in list(t.buildings.values()) if
2436 isinstance(bld, ProductionSite) and
2437 (w.name in bld.workers or w.name in bld.recruits)]
2438
2439@@ -275,7 +274,7 @@
2440 sg = Subgraph('%s_enhancements' % w.name,
2441 ordering='out', rankdir='TB', rank='same')
2442 # find exactly one level of enhancement
2443- for other in t.workers.values():
2444+ for other in list(t.workers.values()):
2445 if other.becomes == w.name:
2446 add_worker(sg, other)
2447 g.add_edge(Edge(other.name, w.name, color='blue'))
2448@@ -294,15 +293,15 @@
2449
2450
2451 def make_ware_graph(t, ware_name):
2452- if isinstance(t, basestring):
2453+ if isinstance(t, str):
2454 t = Tribe(t)
2455 w = t.wares[ware_name]
2456
2457 g = CleanedDot(concentrate='false', bgcolor='transparent',
2458 overlap='false', splines='true', rankdir='LR')
2459
2460- buildings = [bld for bld in t.buildings.values() if isinstance(
2461- bld, (ProductionSite, )) and (w.name in bld.inputs or w.name in bld.outputs)]
2462+ buildings = [bld for bld in list(t.buildings.values()) if isinstance(
2463+ bld, ProductionSite) and (w.name in bld.inputs or w.name in bld.outputs)]
2464 [add_building(g, bld, limit_inputs=[w.name], limit_outputs=[w.name], limit_buildings=[
2465 b.name for b in buildings], link_workers=False) for bld in buildings]
2466
2467@@ -326,28 +325,28 @@
2468 def make_all_subgraphs(t):
2469 global tdir
2470 tdir = mkdtemp(prefix='widelands-help')
2471- if isinstance(t, basestring):
2472+ if isinstance(t, str):
2473 t = Tribe(t)
2474- print 'making all subgraphs for tribe', t.name, 'in', tdir
2475+ print('making all subgraphs for tribe', t.name, 'in', tdir)
2476
2477- print ' making wares'
2478+ print(' making wares')
2479
2480 for w in t.wares:
2481- print ' ' + w
2482+ print(' ' + w)
2483 make_ware_graph(t, w)
2484 process_dotfile(path.join(tdir, 'help/%s/wares/%s/' % (t.name, w)))
2485
2486- print ' making workers'
2487+ print(' making workers')
2488
2489 for w in t.workers:
2490- print ' ' + w
2491+ print(' ' + w)
2492 make_worker_graph(t, w)
2493 process_dotfile(path.join(tdir, 'help/%s/workers/%s/' % (t.name, w)))
2494
2495- print ' making buildings'
2496+ print(' making buildings')
2497
2498 for b in t.buildings:
2499- print ' ' + b
2500+ print(' ' + b)
2501 make_building_graph(t, b)
2502 process_dotfile(path.join(tdir, 'help/%s/buildings/%s/' % (t.name, b)))
2503
2504@@ -359,5 +358,6 @@
2505 if b.enhanced_building:
2506 add_building()
2507
2508+
2509 if __name__ == '__main__':
2510 make_all_subgraphs()
2511
2512=== modified file 'widelandslib/test/test_conf.py'
2513--- widelandslib/test/test_conf.py 2016-12-13 18:28:51 +0000
2514+++ widelandslib/test/test_conf.py 2019-06-14 07:22:45 +0000
2515@@ -13,7 +13,7 @@
2516 sys.path.append('..')
2517
2518 import unittest
2519-from cStringIO import StringIO
2520+from io import StringIO
2521
2522 from conf import WidelandsConfigParser
2523
2524
2525=== modified file 'widelandslib/test/test_map.py'
2526--- widelandslib/test/test_map.py 2016-12-13 18:28:51 +0000
2527+++ widelandslib/test/test_map.py 2019-06-14 07:22:45 +0000
2528@@ -8,7 +8,7 @@
2529 from numpy import *
2530 import unittest
2531 import base64
2532-from cStringIO import StringIO
2533+from io import StringIO
2534 from itertools import *
2535
2536 from map import *
2537@@ -186,12 +186,12 @@
2538 # }}}
2539
2540 def test_R(self):
2541- r = [i.name == j for i, j in izip(
2542+ r = [i.name == j for i, j in zip(
2543 self.m.ter_r.flat, self.ter_r_names.flat)]
2544 self.assertTrue(all(r))
2545
2546 def test_D(self):
2547- d = [i.name == j for i, j in izip(
2548+ d = [i.name == j for i, j in zip(
2549 self.m.ter_d.flat, self.ter_d_names.flat)]
2550 self.assertTrue(all(d))
2551
2552
2553=== modified file 'widelandslib/tribe.py'
2554--- widelandslib/tribe.py 2019-03-31 11:08:21 +0000
2555+++ widelandslib/tribe.py 2019-06-14 07:22:45 +0000
2556@@ -55,7 +55,7 @@
2557 def base_building(self):
2558 if not self.enhanced_building:
2559 return None
2560- bases = [b for b in self.tribe.buildings.values()
2561+ bases = [b for b in list(self.tribe.buildings.values())
2562 if b.enhancement == self.name]
2563 if len(bases) == 0 and self.enhanced_building:
2564 raise Exception('Building %s has no bases in tribe %s' %
2565@@ -152,30 +152,29 @@
2566 def __init__(self, tribeinfo, json_directory):
2567 self.name = tribeinfo['name']
2568
2569- wares_file = open(p.normpath(json_directory + '/' +
2570- self.name + '_wares.json'), 'r')
2571- waresinfo = json.load(wares_file)
2572+ with open(p.normpath(json_directory + '/' +
2573+ self.name + '_wares.json'), 'r') as wares_file:
2574+ waresinfo = json.load(wares_file)
2575 self.wares = dict()
2576 for ware in waresinfo['wares']:
2577- descname = ware['descname'].encode('ascii', 'xmlcharrefreplace')
2578+ descname = ware['descname']
2579 self.wares[ware['name']] = Ware(self, ware['name'], descname, ware)
2580
2581- workers_file = open(p.normpath(
2582- json_directory + '/' + self.name + '_workers.json'), 'r')
2583- workersinfo = json.load(workers_file)
2584+ with open(p.normpath(
2585+ json_directory + '/' + self.name + '_workers.json'), 'r') as workers_file:
2586+ workersinfo = json.load(workers_file)
2587 self.workers = dict()
2588 for worker in workersinfo['workers']:
2589- descname = worker['descname'].encode('ascii', 'xmlcharrefreplace')
2590+ descname = worker['descname']
2591 self.workers[worker['name']] = Worker(
2592 self, worker['name'], descname, worker)
2593
2594- buildings_file = open(p.normpath(
2595- json_directory + '/' + self.name + '_buildings.json'), 'r')
2596- buildingsinfo = json.load(buildings_file)
2597+ with open(p.normpath(
2598+ json_directory + '/' + self.name + '_buildings.json'), 'r') as buildings_file:
2599+ buildingsinfo = json.load(buildings_file)
2600 self.buildings = dict()
2601 for building in buildingsinfo['buildings']:
2602- descname = building['descname'].encode(
2603- 'ascii', 'xmlcharrefreplace')
2604+ descname = building['descname']
2605 if building['type'] == 'productionsite':
2606 self.buildings[building['name']] = ProductionSite(
2607 self, building['name'], descname, building)
2608
2609=== modified file 'wiki/diff_match_patch.py'
2610--- wiki/diff_match_patch.py 2019-03-31 11:08:21 +0000
2611+++ wiki/diff_match_patch.py 2019-06-14 07:22:45 +0000
2612@@ -1,11 +1,11 @@
2613-#!/usr/bin/python2.4
2614-
2615-# Taken from google because they do not provide a setup.py file. Thanks anyway
2616-
2617-"""Diff Match and Patch.
2618-
2619-Copyright 2006 Google Inc.
2620-http://code.google.com/p/google-diff-match-patch/
2621+#!/usr/bin/python3
2622+
2623+# Taken from https://github.com/google/diff-match-patch/blob/master/python3/diff_match_patch.py
2624+# Applied class attribute in diff_prettyHtml() to get variable coloring
2625+
2626+"""Diff Match and Patch
2627+Copyright 2018 The diff-match-patch Authors.
2628+https://github.com/google/diff-match-patch
2629
2630 Licensed under the Apache License, Version 2.0 (the "License");
2631 you may not use this file except in compliance with the License.
2632@@ -18,7 +18,6 @@
2633 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2634 See the License for the specific language governing permissions and
2635 limitations under the License.
2636-
2637 """
2638
2639 """Functions for diff, match and patch.
2640@@ -29,1901 +28,1883 @@
2641
2642 __author__ = 'fraser@google.com (Neil Fraser)'
2643
2644-import math
2645+import re
2646+import sys
2647 import time
2648-import urllib
2649-import re
2650+import urllib.parse
2651
2652
2653 class diff_match_patch:
2654- """Class containing the diff, match and patch methods.
2655-
2656- Also contains the behaviour settings.
2657-
2658- """
2659-
2660- def __init__(self):
2661- """Inits a diff_match_patch object with default settings.
2662-
2663- Redefine these in your program to override the defaults.
2664-
2665- """
2666-
2667- # Number of seconds to map a diff before giving up (0 for infinity).
2668- self.Diff_Timeout = 1.0
2669- # Cost of an empty edit operation in terms of edit characters.
2670- self.Diff_EditCost = 4
2671- # The size beyond which the double-ended diff activates.
2672- # Double-ending is twice as fast, but less accurate.
2673- self.Diff_DualThreshold = 32
2674- # At what point is no match declared (0.0 = perfection, 1.0 = very
2675- # loose).
2676- self.Match_Threshold = 0.5
2677- # How far to search for a match (0 = exact location, 1000+ = broad match).
2678- # A match this many characters away from the expected location will add
2679- # 1.0 to the score (0.0 is a perfect match).
2680- self.Match_Distance = 1000
2681- # When deleting a large block of text (over ~64 characters), how close does
2682- # the contents have to match the expected contents. (0.0 = perfection,
2683- # 1.0 = very loose). Note that Match_Threshold controls how closely the
2684- # end points of a delete need to match.
2685- self.Patch_DeleteThreshold = 0.5
2686- # Chunk size for context length.
2687- self.Patch_Margin = 4
2688-
2689- # How many bits in a number?
2690- # Python has no maximum, thus to disable patch splitting set to 0.
2691- # However to avoid long patches in certain pathological cases, use 32.
2692- # Multiple short patches (using native ints) are much faster than long
2693- # ones.
2694- self.Match_MaxBits = 32
2695-
2696- # DIFF FUNCTIONS
2697-
2698- # The data structure representing a diff is an array of tuples:
2699- # [(DIFF_DELETE, "Hello"), (DIFF_INSERT, "Goodbye"), (DIFF_EQUAL, " world.")]
2700- # which means: delete "Hello", add "Goodbye" and keep " world."
2701- DIFF_DELETE = -1
2702- DIFF_INSERT = 1
2703- DIFF_EQUAL = 0
2704-
2705- def diff_main(self, text1, text2, checklines=True):
2706- """Find the differences between two texts. Simplifies the problem by
2707- stripping any common prefix or suffix off the texts before diffing.
2708-
2709- Args:
2710- text1: Old string to be diffed.
2711- text2: New string to be diffed.
2712- checklines: Optional speedup flag. If present and false, then don't run
2713- a line-level diff first to identify the changed areas.
2714- Defaults to true, which does a faster, slightly less optimal diff.
2715-
2716- Returns:
2717- Array of changes.
2718-
2719- """
2720-
2721- # Check for equality (speedup)
2722- if text1 == text2:
2723- return [(self.DIFF_EQUAL, text1)]
2724-
2725- # Trim off common prefix (speedup)
2726- commonlength = self.diff_commonPrefix(text1, text2)
2727- commonprefix = text1[:commonlength]
2728- text1 = text1[commonlength:]
2729- text2 = text2[commonlength:]
2730-
2731- # Trim off common suffix (speedup)
2732- commonlength = self.diff_commonSuffix(text1, text2)
2733- if commonlength == 0:
2734- commonsuffix = ''
2735- else:
2736- commonsuffix = text1[-commonlength:]
2737- text1 = text1[:-commonlength]
2738- text2 = text2[:-commonlength]
2739-
2740- # Compute the diff on the middle block
2741- diffs = self.diff_compute(text1, text2, checklines)
2742-
2743- # Restore the prefix and suffix
2744- if commonprefix:
2745- diffs[:0] = [(self.DIFF_EQUAL, commonprefix)]
2746- if commonsuffix:
2747- diffs.append((self.DIFF_EQUAL, commonsuffix))
2748- self.diff_cleanupMerge(diffs)
2749- return diffs
2750-
2751- def diff_compute(self, text1, text2, checklines):
2752- """Find the differences between two texts. Assumes that the texts do
2753- not have any common prefix or suffix.
2754-
2755- Args:
2756- text1: Old string to be diffed.
2757- text2: New string to be diffed.
2758- checklines: Speedup flag. If false, then don't run a line-level diff
2759- first to identify the changed areas.
2760- If true, then run a faster, slightly less optimal diff.
2761-
2762- Returns:
2763- Array of changes.
2764-
2765- """
2766- if not text1:
2767- # Just add some text (speedup)
2768- return [(self.DIFF_INSERT, text2)]
2769-
2770- if not text2:
2771- # Just delete some text (speedup)
2772- return [(self.DIFF_DELETE, text1)]
2773-
2774- if len(text1) > len(text2):
2775- (longtext, shorttext) = (text1, text2)
2776- else:
2777- (shorttext, longtext) = (text1, text2)
2778- i = longtext.find(shorttext)
2779- if i != -1:
2780- # Shorter text is inside the longer text (speedup)
2781- diffs = [(self.DIFF_INSERT, longtext[:i]), (self.DIFF_EQUAL, shorttext),
2782- (self.DIFF_INSERT, longtext[i + len(shorttext):])]
2783- # Swap insertions for deletions if diff is reversed.
2784- if len(text1) > len(text2):
2785- diffs[0] = (self.DIFF_DELETE, diffs[0][1])
2786- diffs[2] = (self.DIFF_DELETE, diffs[2][1])
2787- return diffs
2788- longtext = shorttext = None # Garbage collect.
2789-
2790- # Check to see if the problem can be split in two.
2791- hm = self.diff_halfMatch(text1, text2)
2792- if hm:
2793- # A half-match was found, sort out the return data.
2794- (text1_a, text1_b, text2_a, text2_b, mid_common) = hm
2795- # Send both pairs off for separate processing.
2796- diffs_a = self.diff_main(text1_a, text2_a, checklines)
2797- diffs_b = self.diff_main(text1_b, text2_b, checklines)
2798- # Merge the results.
2799- return diffs_a + [(self.DIFF_EQUAL, mid_common)] + diffs_b
2800-
2801- # Perform a real diff.
2802- if checklines and (len(text1) < 100 or len(text2) < 100):
2803- checklines = False # Too trivial for the overhead.
2804- if checklines:
2805- # Scan the text on a line-by-line basis first.
2806- (text1, text2, linearray) = self.diff_linesToChars(text1, text2)
2807-
2808- diffs = self.diff_map(text1, text2)
2809- if not diffs: # No acceptable result.
2810- diffs = [(self.DIFF_DELETE, text1), (self.DIFF_INSERT, text2)]
2811- if checklines:
2812- # Convert the diff back to original text.
2813- self.diff_charsToLines(diffs, linearray)
2814- # Eliminate freak matches (e.g. blank lines)
2815- self.diff_cleanupSemantic(diffs)
2816-
2817- # Rediff any replacement blocks, this time character-by-character.
2818- # Add a dummy entry at the end.
2819- diffs.append((self.DIFF_EQUAL, ''))
2820- pointer = 0
2821- count_delete = 0
2822- count_insert = 0
2823- text_delete = ''
2824- text_insert = ''
2825- while pointer < len(diffs):
2826- if diffs[pointer][0] == self.DIFF_INSERT:
2827- count_insert += 1
2828- text_insert += diffs[pointer][1]
2829- elif diffs[pointer][0] == self.DIFF_DELETE:
2830- count_delete += 1
2831- text_delete += diffs[pointer][1]
2832- elif diffs[pointer][0] == self.DIFF_EQUAL:
2833- # Upon reaching an equality, check for prior redundancies.
2834- if count_delete >= 1 and count_insert >= 1:
2835- # Delete the offending records and add the merged ones.
2836- a = self.diff_main(text_delete, text_insert, False)
2837- diffs[pointer - count_delete -
2838- count_insert: pointer] = a
2839- pointer = pointer - count_delete - \
2840- count_insert + len(a)
2841- count_insert = 0
2842- count_delete = 0
2843- text_delete = ''
2844- text_insert = ''
2845-
2846- pointer += 1
2847-
2848- diffs.pop() # Remove the dummy entry at the end.
2849- return diffs
2850-
2851- def diff_linesToChars(self, text1, text2):
2852- """Split two texts into an array of strings. Reduce the texts to a
2853- string of hashes where each Unicode character represents one line.
2854-
2855- Args:
2856- text1: First string.
2857- text2: Second string.
2858-
2859- Returns:
2860- Three element tuple, containing the encoded text1, the encoded text2 and
2861- the array of unique strings. The zeroth element of the array of unique
2862- strings is intentionally blank.
2863-
2864- """
2865- lineArray = [] # e.g. lineArray[4] == "Hello\n"
2866- lineHash = {} # e.g. lineHash["Hello\n"] == 4
2867-
2868- # "\x00" is a valid character, but various debuggers don't like it.
2869- # So we'll insert a junk entry to avoid generating a null character.
2870- lineArray.append('')
2871-
2872- def diff_linesToCharsMunge(text):
2873- """Split a text into an array of strings. Reduce the texts to a
2874- string of hashes where each Unicode character represents one line.
2875- Modifies linearray and linehash through being a closure.
2876-
2877- Args:
2878- text: String to encode.
2879-
2880- Returns:
2881- Encoded string.
2882-
2883- """
2884- chars = []
2885- # Walk the text, pulling out a substring for each line.
2886- # text.split('\n') would would temporarily double our memory footprint.
2887- # Modifying text would create many large strings to garbage
2888- # collect.
2889- lineStart = 0
2890- lineEnd = -1
2891- while lineEnd < len(text) - 1:
2892- lineEnd = text.find('\n', lineStart)
2893- if lineEnd == -1:
2894- lineEnd = len(text) - 1
2895- line = text[lineStart:lineEnd + 1]
2896- lineStart = lineEnd + 1
2897-
2898- if line in lineHash:
2899- chars.append(unichr(lineHash[line]))
2900- else:
2901- lineArray.append(line)
2902- lineHash[line] = len(lineArray) - 1
2903- chars.append(unichr(len(lineArray) - 1))
2904- return ''.join(chars)
2905-
2906- chars1 = diff_linesToCharsMunge(text1)
2907- chars2 = diff_linesToCharsMunge(text2)
2908- return (chars1, chars2, lineArray)
2909-
2910- def diff_charsToLines(self, diffs, lineArray):
2911- """Rehydrate the text in a diff from a string of line hashes to real
2912- lines of text.
2913-
2914- Args:
2915- diffs: Array of diff tuples.
2916- lineArray: Array of unique strings.
2917-
2918- """
2919- for x in xrange(len(diffs)):
2920- text = []
2921- for char in diffs[x][1]:
2922- text.append(lineArray[ord(char)])
2923- diffs[x] = (diffs[x][0], ''.join(text))
2924-
2925- def diff_map(self, text1, text2):
2926- """Explore the intersection points between the two texts.
2927-
2928- Args:
2929- text1: Old string to be diffed.
2930- text2: New string to be diffed.
2931-
2932- Returns:
2933- Array of diff tuples or None if no diff available.
2934-
2935- """
2936-
2937- # Unlike in most languages, Python counts time in seconds.
2938- s_end = time.time() + self.Diff_Timeout # Don't run for too long.
2939- # Cache the text lengths to prevent multiple calls.
2940- text1_length = len(text1)
2941- text2_length = len(text2)
2942- max_d = text1_length + text2_length - 1
2943- doubleEnd = self.Diff_DualThreshold * 2 < max_d
2944- v_map1 = []
2945- v_map2 = []
2946- v1 = {}
2947- v2 = {}
2948- v1[1] = 0
2949- v2[1] = 0
2950- footsteps = {}
2951- done = False
2952- # If the total number of characters is odd, then the front path will
2953- # collide with the reverse path.
2954- front = (text1_length + text2_length) % 2
2955- for d in xrange(max_d):
2956- # Bail out if timeout reached.
2957- if self.Diff_Timeout > 0 and time.time() > s_end:
2958- return None
2959-
2960- # Walk the front path one step.
2961- v_map1.append({})
2962- for k in xrange(-d, d + 1, 2):
2963- if k == -d or k != d and v1[k - 1] < v1[k + 1]:
2964- x = v1[k + 1]
2965- else:
2966- x = v1[k - 1] + 1
2967- y = x - k
2968- if doubleEnd:
2969- footstep = (x, y)
2970- if front and footstep in footsteps:
2971- done = True
2972- if not front:
2973- footsteps[footstep] = d
2974-
2975- while (not done and x < text1_length and y < text2_length and
2976- text1[x] == text2[y]):
2977- x += 1
2978- y += 1
2979- if doubleEnd:
2980- footstep = (x, y)
2981- if front and footstep in footsteps:
2982- done = True
2983- if not front:
2984- footsteps[footstep] = d
2985-
2986- v1[k] = x
2987- v_map1[d][(x, y)] = True
2988- if x == text1_length and y == text2_length:
2989- # Reached the end in single-path mode.
2990- return self.diff_path1(v_map1, text1, text2)
2991- elif done:
2992- # Front path ran over reverse path.
2993- v_map2 = v_map2[:footsteps[footstep] + 1]
2994- a = self.diff_path1(v_map1, text1[:x], text2[:y])
2995- b = self.diff_path2(v_map2, text1[x:], text2[y:])
2996- return a + b
2997-
2998- if doubleEnd:
2999- # Walk the reverse path one step.
3000- v_map2.append({})
3001- for k in xrange(-d, d + 1, 2):
3002- if k == -d or k != d and v2[k - 1] < v2[k + 1]:
3003- x = v2[k + 1]
3004- else:
3005- x = v2[k - 1] + 1
3006- y = x - k
3007- footstep = (text1_length - x, text2_length - y)
3008- if not front and footstep in footsteps:
3009- done = True
3010- if front:
3011- footsteps[footstep] = d
3012- while (not done and x < text1_length and y < text2_length and
3013- text1[-x - 1] == text2[-y - 1]):
3014- x += 1
3015- y += 1
3016- footstep = (text1_length - x, text2_length - y)
3017- if not front and footstep in footsteps:
3018- done = True
3019- if front:
3020- footsteps[footstep] = d
3021-
3022- v2[k] = x
3023- v_map2[d][(x, y)] = True
3024- if done:
3025- # Reverse path ran over front path.
3026- v_map1 = v_map1[:footsteps[footstep] + 1]
3027- a = self.diff_path1(v_map1, text1[:text1_length - x],
3028- text2[:text2_length - y])
3029- b = self.diff_path2(v_map2, text1[text1_length - x:],
3030- text2[text2_length - y:])
3031- return a + b
3032-
3033- # Number of diffs equals number of characters, no commonality at all.
3034+ """Class containing the diff, match and patch methods.
3035+
3036+ Also contains the behaviour settings.
3037+ """
3038+
3039+ def __init__(self):
3040+ """Inits a diff_match_patch object with default settings.
3041+ Redefine these in your program to override the defaults.
3042+ """
3043+
3044+ # Number of seconds to map a diff before giving up (0 for infinity).
3045+ self.Diff_Timeout = 1.0
3046+ # Cost of an empty edit operation in terms of edit characters.
3047+ self.Diff_EditCost = 4
3048+ # At what point is no match declared (0.0 = perfection, 1.0 = very loose).
3049+ self.Match_Threshold = 0.5
3050+ # How far to search for a match (0 = exact location, 1000+ = broad match).
3051+ # A match this many characters away from the expected location will add
3052+ # 1.0 to the score (0.0 is a perfect match).
3053+ self.Match_Distance = 1000
3054+ # When deleting a large block of text (over ~64 characters), how close do
3055+ # the contents have to be to match the expected contents. (0.0 = perfection,
3056+ # 1.0 = very loose). Note that Match_Threshold controls how closely the
3057+ # end points of a delete need to match.
3058+ self.Patch_DeleteThreshold = 0.5
3059+ # Chunk size for context length.
3060+ self.Patch_Margin = 4
3061+
3062+ # The number of bits in an int.
3063+ # Python has no maximum, thus to disable patch splitting set to 0.
3064+ # However to avoid long patches in certain pathological cases, use 32.
3065+ # Multiple short patches (using native ints) are much faster than long ones.
3066+ self.Match_MaxBits = 32
3067+
3068+ # DIFF FUNCTIONS
3069+
3070+ # The data structure representing a diff is an array of tuples:
3071+ # [(DIFF_DELETE, "Hello"), (DIFF_INSERT, "Goodbye"), (DIFF_EQUAL, " world.")]
3072+ # which means: delete "Hello", add "Goodbye" and keep " world."
3073+ DIFF_DELETE = -1
3074+ DIFF_INSERT = 1
3075+ DIFF_EQUAL = 0
3076+
3077+ def diff_main(self, text1, text2, checklines=True, deadline=None):
3078+ """Find the differences between two texts. Simplifies the problem by
3079+ stripping any common prefix or suffix off the texts before diffing.
3080+
3081+ Args:
3082+ text1: Old string to be diffed.
3083+ text2: New string to be diffed.
3084+ checklines: Optional speedup flag. If present and false, then don't run
3085+ a line-level diff first to identify the changed areas.
3086+ Defaults to true, which does a faster, slightly less optimal diff.
3087+ deadline: Optional time when the diff should be complete by. Used
3088+ internally for recursive calls. Users should set DiffTimeout instead.
3089+
3090+ Returns:
3091+ Array of changes.
3092+ """
3093+ # Set a deadline by which time the diff must be complete.
3094+ if deadline == None:
3095+ # Unlike in most languages, Python counts time in seconds.
3096+ if self.Diff_Timeout <= 0:
3097+ deadline = sys.maxsize
3098+ else:
3099+ deadline = time.time() + self.Diff_Timeout
3100+
3101+ # Check for null inputs.
3102+ if text1 == None or text2 == None:
3103+ raise ValueError("Null inputs. (diff_main)")
3104+
3105+ # Check for equality (speedup).
3106+ if text1 == text2:
3107+ if text1:
3108+ return [(self.DIFF_EQUAL, text1)]
3109+ return []
3110+
3111+ # Trim off common prefix (speedup).
3112+ commonlength = self.diff_commonPrefix(text1, text2)
3113+ commonprefix = text1[:commonlength]
3114+ text1 = text1[commonlength:]
3115+ text2 = text2[commonlength:]
3116+
3117+ # Trim off common suffix (speedup).
3118+ commonlength = self.diff_commonSuffix(text1, text2)
3119+ if commonlength == 0:
3120+ commonsuffix = ''
3121+ else:
3122+ commonsuffix = text1[-commonlength:]
3123+ text1 = text1[:-commonlength]
3124+ text2 = text2[:-commonlength]
3125+
3126+ # Compute the diff on the middle block.
3127+ diffs = self.diff_compute(text1, text2, checklines, deadline)
3128+
3129+ # Restore the prefix and suffix.
3130+ if commonprefix:
3131+ diffs[:0] = [(self.DIFF_EQUAL, commonprefix)]
3132+ if commonsuffix:
3133+ diffs.append((self.DIFF_EQUAL, commonsuffix))
3134+ self.diff_cleanupMerge(diffs)
3135+ return diffs
3136+
3137+ def diff_compute(self, text1, text2, checklines, deadline):
3138+ """Find the differences between two texts. Assumes that the texts do not
3139+ have any common prefix or suffix.
3140+
3141+ Args:
3142+ text1: Old string to be diffed.
3143+ text2: New string to be diffed.
3144+ checklines: Speedup flag. If false, then don't run a line-level diff
3145+ first to identify the changed areas.
3146+ If true, then run a faster, slightly less optimal diff.
3147+ deadline: Time when the diff should be complete by.
3148+
3149+ Returns:
3150+ Array of changes.
3151+ """
3152+ if not text1:
3153+ # Just add some text (speedup).
3154+ return [(self.DIFF_INSERT, text2)]
3155+
3156+ if not text2:
3157+ # Just delete some text (speedup).
3158+ return [(self.DIFF_DELETE, text1)]
3159+
3160+ if len(text1) > len(text2):
3161+ (longtext, shorttext) = (text1, text2)
3162+ else:
3163+ (shorttext, longtext) = (text1, text2)
3164+ i = longtext.find(shorttext)
3165+ if i != -1:
3166+ # Shorter text is inside the longer text (speedup).
3167+ diffs = [(self.DIFF_INSERT, longtext[:i]), (self.DIFF_EQUAL, shorttext),
3168+ (self.DIFF_INSERT, longtext[i + len(shorttext):])]
3169+ # Swap insertions for deletions if diff is reversed.
3170+ if len(text1) > len(text2):
3171+ diffs[0] = (self.DIFF_DELETE, diffs[0][1])
3172+ diffs[2] = (self.DIFF_DELETE, diffs[2][1])
3173+ return diffs
3174+
3175+ if len(shorttext) == 1:
3176+ # Single character string.
3177+ # After the previous speedup, the character can't be an equality.
3178+ return [(self.DIFF_DELETE, text1), (self.DIFF_INSERT, text2)]
3179+
3180+ # Check to see if the problem can be split in two.
3181+ hm = self.diff_halfMatch(text1, text2)
3182+ if hm:
3183+ # A half-match was found, sort out the return data.
3184+ (text1_a, text1_b, text2_a, text2_b, mid_common) = hm
3185+ # Send both pairs off for separate processing.
3186+ diffs_a = self.diff_main(text1_a, text2_a, checklines, deadline)
3187+ diffs_b = self.diff_main(text1_b, text2_b, checklines, deadline)
3188+ # Merge the results.
3189+ return diffs_a + [(self.DIFF_EQUAL, mid_common)] + diffs_b
3190+
3191+ if checklines and len(text1) > 100 and len(text2) > 100:
3192+ return self.diff_lineMode(text1, text2, deadline)
3193+
3194+ return self.diff_bisect(text1, text2, deadline)
3195+
3196+ def diff_lineMode(self, text1, text2, deadline):
3197+ """Do a quick line-level diff on both strings, then rediff the parts for
3198+ greater accuracy.
3199+ This speedup can produce non-minimal diffs.
3200+
3201+ Args:
3202+ text1: Old string to be diffed.
3203+ text2: New string to be diffed.
3204+ deadline: Time when the diff should be complete by.
3205+
3206+ Returns:
3207+ Array of changes.
3208+ """
3209+
3210+ # Scan the text on a line-by-line basis first.
3211+ (text1, text2, linearray) = self.diff_linesToChars(text1, text2)
3212+
3213+ diffs = self.diff_main(text1, text2, False, deadline)
3214+
3215+ # Convert the diff back to original text.
3216+ self.diff_charsToLines(diffs, linearray)
3217+ # Eliminate freak matches (e.g. blank lines)
3218+ self.diff_cleanupSemantic(diffs)
3219+
3220+ # Rediff any replacement blocks, this time character-by-character.
3221+ # Add a dummy entry at the end.
3222+ diffs.append((self.DIFF_EQUAL, ''))
3223+ pointer = 0
3224+ count_delete = 0
3225+ count_insert = 0
3226+ text_delete = ''
3227+ text_insert = ''
3228+ while pointer < len(diffs):
3229+ if diffs[pointer][0] == self.DIFF_INSERT:
3230+ count_insert += 1
3231+ text_insert += diffs[pointer][1]
3232+ elif diffs[pointer][0] == self.DIFF_DELETE:
3233+ count_delete += 1
3234+ text_delete += diffs[pointer][1]
3235+ elif diffs[pointer][0] == self.DIFF_EQUAL:
3236+ # Upon reaching an equality, check for prior redundancies.
3237+ if count_delete >= 1 and count_insert >= 1:
3238+ # Delete the offending records and add the merged ones.
3239+ subDiff = self.diff_main(text_delete, text_insert, False, deadline)
3240+ diffs[pointer - count_delete - count_insert : pointer] = subDiff
3241+ pointer = pointer - count_delete - count_insert + len(subDiff)
3242+ count_insert = 0
3243+ count_delete = 0
3244+ text_delete = ''
3245+ text_insert = ''
3246+
3247+ pointer += 1
3248+
3249+ diffs.pop() # Remove the dummy entry at the end.
3250+
3251+ return diffs
3252+
3253+ def diff_bisect(self, text1, text2, deadline):
3254+ """Find the 'middle snake' of a diff, split the problem in two
3255+ and return the recursively constructed diff.
3256+ See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
3257+
3258+ Args:
3259+ text1: Old string to be diffed.
3260+ text2: New string to be diffed.
3261+ deadline: Time at which to bail if not yet complete.
3262+
3263+ Returns:
3264+ Array of diff tuples.
3265+ """
3266+
3267+ # Cache the text lengths to prevent multiple calls.
3268+ text1_length = len(text1)
3269+ text2_length = len(text2)
3270+ max_d = (text1_length + text2_length + 1) // 2
3271+ v_offset = max_d
3272+ v_length = 2 * max_d
3273+ v1 = [-1] * v_length
3274+ v1[v_offset + 1] = 0
3275+ v2 = v1[:]
3276+ delta = text1_length - text2_length
3277+ # If the total number of characters is odd, then the front path will
3278+ # collide with the reverse path.
3279+ front = (delta % 2 != 0)
3280+ # Offsets for start and end of k loop.
3281+ # Prevents mapping of space beyond the grid.
3282+ k1start = 0
3283+ k1end = 0
3284+ k2start = 0
3285+ k2end = 0
3286+ for d in range(max_d):
3287+ # Bail out if deadline is reached.
3288+ if time.time() > deadline:
3289+ break
3290+
3291+ # Walk the front path one step.
3292+ for k1 in range(-d + k1start, d + 1 - k1end, 2):
3293+ k1_offset = v_offset + k1
3294+ if k1 == -d or (k1 != d and
3295+ v1[k1_offset - 1] < v1[k1_offset + 1]):
3296+ x1 = v1[k1_offset + 1]
3297+ else:
3298+ x1 = v1[k1_offset - 1] + 1
3299+ y1 = x1 - k1
3300+ while (x1 < text1_length and y1 < text2_length and
3301+ text1[x1] == text2[y1]):
3302+ x1 += 1
3303+ y1 += 1
3304+ v1[k1_offset] = x1
3305+ if x1 > text1_length:
3306+ # Ran off the right of the graph.
3307+ k1end += 2
3308+ elif y1 > text2_length:
3309+ # Ran off the bottom of the graph.
3310+ k1start += 2
3311+ elif front:
3312+ k2_offset = v_offset + delta - k1
3313+ if k2_offset >= 0 and k2_offset < v_length and v2[k2_offset] != -1:
3314+ # Mirror x2 onto top-left coordinate system.
3315+ x2 = text1_length - v2[k2_offset]
3316+ if x1 >= x2:
3317+ # Overlap detected.
3318+ return self.diff_bisectSplit(text1, text2, x1, y1, deadline)
3319+
3320+ # Walk the reverse path one step.
3321+ for k2 in range(-d + k2start, d + 1 - k2end, 2):
3322+ k2_offset = v_offset + k2
3323+ if k2 == -d or (k2 != d and
3324+ v2[k2_offset - 1] < v2[k2_offset + 1]):
3325+ x2 = v2[k2_offset + 1]
3326+ else:
3327+ x2 = v2[k2_offset - 1] + 1
3328+ y2 = x2 - k2
3329+ while (x2 < text1_length and y2 < text2_length and
3330+ text1[-x2 - 1] == text2[-y2 - 1]):
3331+ x2 += 1
3332+ y2 += 1
3333+ v2[k2_offset] = x2
3334+ if x2 > text1_length:
3335+ # Ran off the left of the graph.
3336+ k2end += 2
3337+ elif y2 > text2_length:
3338+ # Ran off the top of the graph.
3339+ k2start += 2
3340+ elif not front:
3341+ k1_offset = v_offset + delta - k2
3342+ if k1_offset >= 0 and k1_offset < v_length and v1[k1_offset] != -1:
3343+ x1 = v1[k1_offset]
3344+ y1 = v_offset + x1 - k1_offset
3345+ # Mirror x2 onto top-left coordinate system.
3346+ x2 = text1_length - x2
3347+ if x1 >= x2:
3348+ # Overlap detected.
3349+ return self.diff_bisectSplit(text1, text2, x1, y1, deadline)
3350+
3351+ # Diff took too long and hit the deadline or
3352+ # number of diffs equals number of characters, no commonality at all.
3353+ return [(self.DIFF_DELETE, text1), (self.DIFF_INSERT, text2)]
3354+
3355+ def diff_bisectSplit(self, text1, text2, x, y, deadline):
3356+ """Given the location of the 'middle snake', split the diff in two parts
3357+ and recurse.
3358+
3359+ Args:
3360+ text1: Old string to be diffed.
3361+ text2: New string to be diffed.
3362+ x: Index of split point in text1.
3363+ y: Index of split point in text2.
3364+ deadline: Time at which to bail if not yet complete.
3365+
3366+ Returns:
3367+ Array of diff tuples.
3368+ """
3369+ text1a = text1[:x]
3370+ text2a = text2[:y]
3371+ text1b = text1[x:]
3372+ text2b = text2[y:]
3373+
3374+ # Compute both diffs serially.
3375+ diffs = self.diff_main(text1a, text2a, False, deadline)
3376+ diffsb = self.diff_main(text1b, text2b, False, deadline)
3377+
3378+ return diffs + diffsb
3379+
3380+ def diff_linesToChars(self, text1, text2):
3381+ """Split two texts into an array of strings. Reduce the texts to a string
3382+ of hashes where each Unicode character represents one line.
3383+
3384+ Args:
3385+ text1: First string.
3386+ text2: Second string.
3387+
3388+ Returns:
3389+ Three element tuple, containing the encoded text1, the encoded text2 and
3390+ the array of unique strings. The zeroth element of the array of unique
3391+ strings is intentionally blank.
3392+ """
3393+ lineArray = [] # e.g. lineArray[4] == "Hello\n"
3394+ lineHash = {} # e.g. lineHash["Hello\n"] == 4
3395+
3396+ # "\x00" is a valid character, but various debuggers don't like it.
3397+ # So we'll insert a junk entry to avoid generating a null character.
3398+ lineArray.append('')
3399+
3400+ def diff_linesToCharsMunge(text):
3401+ """Split a text into an array of strings. Reduce the texts to a string
3402+ of hashes where each Unicode character represents one line.
3403+ Modifies linearray and linehash through being a closure.
3404+
3405+ Args:
3406+ text: String to encode.
3407+
3408+ Returns:
3409+ Encoded string.
3410+ """
3411+ chars = []
3412+ # Walk the text, pulling out a substring for each line.
3413+ # text.split('\n') would would temporarily double our memory footprint.
3414+ # Modifying text would create many large strings to garbage collect.
3415+ lineStart = 0
3416+ lineEnd = -1
3417+ while lineEnd < len(text) - 1:
3418+ lineEnd = text.find('\n', lineStart)
3419+ if lineEnd == -1:
3420+ lineEnd = len(text) - 1
3421+ line = text[lineStart:lineEnd + 1]
3422+
3423+ if line in lineHash:
3424+ chars.append(chr(lineHash[line]))
3425+ else:
3426+ if len(lineArray) == maxLines:
3427+ # Bail out at 1114111 because chr(1114112) throws.
3428+ line = text[lineStart:]
3429+ lineEnd = len(text)
3430+ lineArray.append(line)
3431+ lineHash[line] = len(lineArray) - 1
3432+ chars.append(chr(len(lineArray) - 1))
3433+ lineStart = lineEnd + 1
3434+ return "".join(chars)
3435+
3436+ # Allocate 2/3rds of the space for text1, the rest for text2.
3437+ maxLines = 666666
3438+ chars1 = diff_linesToCharsMunge(text1)
3439+ maxLines = 1114111
3440+ chars2 = diff_linesToCharsMunge(text2)
3441+ return (chars1, chars2, lineArray)
3442+
3443+ def diff_charsToLines(self, diffs, lineArray):
3444+ """Rehydrate the text in a diff from a string of line hashes to real lines
3445+ of text.
3446+
3447+ Args:
3448+ diffs: Array of diff tuples.
3449+ lineArray: Array of unique strings.
3450+ """
3451+ for i in range(len(diffs)):
3452+ text = []
3453+ for char in diffs[i][1]:
3454+ text.append(lineArray[ord(char)])
3455+ diffs[i] = (diffs[i][0], "".join(text))
3456+
3457+ def diff_commonPrefix(self, text1, text2):
3458+ """Determine the common prefix of two strings.
3459+
3460+ Args:
3461+ text1: First string.
3462+ text2: Second string.
3463+
3464+ Returns:
3465+ The number of characters common to the start of each string.
3466+ """
3467+ # Quick check for common null cases.
3468+ if not text1 or not text2 or text1[0] != text2[0]:
3469+ return 0
3470+ # Binary search.
3471+ # Performance analysis: https://neil.fraser.name/news/2007/10/09/
3472+ pointermin = 0
3473+ pointermax = min(len(text1), len(text2))
3474+ pointermid = pointermax
3475+ pointerstart = 0
3476+ while pointermin < pointermid:
3477+ if text1[pointerstart:pointermid] == text2[pointerstart:pointermid]:
3478+ pointermin = pointermid
3479+ pointerstart = pointermin
3480+ else:
3481+ pointermax = pointermid
3482+ pointermid = (pointermax - pointermin) // 2 + pointermin
3483+ return pointermid
3484+
3485+ def diff_commonSuffix(self, text1, text2):
3486+ """Determine the common suffix of two strings.
3487+
3488+ Args:
3489+ text1: First string.
3490+ text2: Second string.
3491+
3492+ Returns:
3493+ The number of characters common to the end of each string.
3494+ """
3495+ # Quick check for common null cases.
3496+ if not text1 or not text2 or text1[-1] != text2[-1]:
3497+ return 0
3498+ # Binary search.
3499+ # Performance analysis: https://neil.fraser.name/news/2007/10/09/
3500+ pointermin = 0
3501+ pointermax = min(len(text1), len(text2))
3502+ pointermid = pointermax
3503+ pointerend = 0
3504+ while pointermin < pointermid:
3505+ if (text1[-pointermid:len(text1) - pointerend] ==
3506+ text2[-pointermid:len(text2) - pointerend]):
3507+ pointermin = pointermid
3508+ pointerend = pointermin
3509+ else:
3510+ pointermax = pointermid
3511+ pointermid = (pointermax - pointermin) // 2 + pointermin
3512+ return pointermid
3513+
3514+ def diff_commonOverlap(self, text1, text2):
3515+ """Determine if the suffix of one string is the prefix of another.
3516+
3517+ Args:
3518+ text1 First string.
3519+ text2 Second string.
3520+
3521+ Returns:
3522+ The number of characters common to the end of the first
3523+ string and the start of the second string.
3524+ """
3525+ # Cache the text lengths to prevent multiple calls.
3526+ text1_length = len(text1)
3527+ text2_length = len(text2)
3528+ # Eliminate the null case.
3529+ if text1_length == 0 or text2_length == 0:
3530+ return 0
3531+ # Truncate the longer string.
3532+ if text1_length > text2_length:
3533+ text1 = text1[-text2_length:]
3534+ elif text1_length < text2_length:
3535+ text2 = text2[:text1_length]
3536+ text_length = min(text1_length, text2_length)
3537+ # Quick check for the worst case.
3538+ if text1 == text2:
3539+ return text_length
3540+
3541+ # Start by looking for a single character match
3542+ # and increase length until no match is found.
3543+ # Performance analysis: https://neil.fraser.name/news/2010/11/04/
3544+ best = 0
3545+ length = 1
3546+ while True:
3547+ pattern = text1[-length:]
3548+ found = text2.find(pattern)
3549+ if found == -1:
3550+ return best
3551+ length += found
3552+ if found == 0 or text1[-length:] == text2[:length]:
3553+ best = length
3554+ length += 1
3555+
3556+ def diff_halfMatch(self, text1, text2):
3557+ """Do the two texts share a substring which is at least half the length of
3558+ the longer text?
3559+ This speedup can produce non-minimal diffs.
3560+
3561+ Args:
3562+ text1: First string.
3563+ text2: Second string.
3564+
3565+ Returns:
3566+ Five element Array, containing the prefix of text1, the suffix of text1,
3567+ the prefix of text2, the suffix of text2 and the common middle. Or None
3568+ if there was no match.
3569+ """
3570+ if self.Diff_Timeout <= 0:
3571+ # Don't risk returning a non-optimal diff if we have unlimited time.
3572+ return None
3573+ if len(text1) > len(text2):
3574+ (longtext, shorttext) = (text1, text2)
3575+ else:
3576+ (shorttext, longtext) = (text1, text2)
3577+ if len(longtext) < 4 or len(shorttext) * 2 < len(longtext):
3578+ return None # Pointless.
3579+
3580+ def diff_halfMatchI(longtext, shorttext, i):
3581+ """Does a substring of shorttext exist within longtext such that the
3582+ substring is at least half the length of longtext?
3583+ Closure, but does not reference any external variables.
3584+
3585+ Args:
3586+ longtext: Longer string.
3587+ shorttext: Shorter string.
3588+ i: Start index of quarter length substring within longtext.
3589+
3590+ Returns:
3591+ Five element Array, containing the prefix of longtext, the suffix of
3592+ longtext, the prefix of shorttext, the suffix of shorttext and the
3593+ common middle. Or None if there was no match.
3594+ """
3595+ seed = longtext[i:i + len(longtext) // 4]
3596+ best_common = ''
3597+ j = shorttext.find(seed)
3598+ while j != -1:
3599+ prefixLength = self.diff_commonPrefix(longtext[i:], shorttext[j:])
3600+ suffixLength = self.diff_commonSuffix(longtext[:i], shorttext[:j])
3601+ if len(best_common) < suffixLength + prefixLength:
3602+ best_common = (shorttext[j - suffixLength:j] +
3603+ shorttext[j:j + prefixLength])
3604+ best_longtext_a = longtext[:i - suffixLength]
3605+ best_longtext_b = longtext[i + prefixLength:]
3606+ best_shorttext_a = shorttext[:j - suffixLength]
3607+ best_shorttext_b = shorttext[j + prefixLength:]
3608+ j = shorttext.find(seed, j + 1)
3609+
3610+ if len(best_common) * 2 >= len(longtext):
3611+ return (best_longtext_a, best_longtext_b,
3612+ best_shorttext_a, best_shorttext_b, best_common)
3613+ else:
3614 return None
3615
3616- def diff_path1(self, v_map, text1, text2):
3617- """Work from the middle back to the start to determine the path.
3618-
3619- Args:
3620- v_map: Array of paths.
3621- text1: Old string fragment to be diffed.
3622- text2: New string fragment to be diffed.
3623-
3624- Returns:
3625- Array of diff tuples.
3626-
3627- """
3628- path = []
3629- x = len(text1)
3630- y = len(text2)
3631- last_op = None
3632- for d in xrange(len(v_map) - 2, -1, -1):
3633- while True:
3634- if (x - 1, y) in v_map[d]:
3635- x -= 1
3636- if last_op == self.DIFF_DELETE:
3637- path[0] = (self.DIFF_DELETE, text1[x] + path[0][1])
3638- else:
3639- path[:0] = [(self.DIFF_DELETE, text1[x])]
3640- last_op = self.DIFF_DELETE
3641- break
3642- elif (x, y - 1) in v_map[d]:
3643- y -= 1
3644- if last_op == self.DIFF_INSERT:
3645- path[0] = (self.DIFF_INSERT, text2[y] + path[0][1])
3646- else:
3647- path[:0] = [(self.DIFF_INSERT, text2[y])]
3648- last_op = self.DIFF_INSERT
3649- break
3650- else:
3651- x -= 1
3652- y -= 1
3653- assert text1[x] == text2[y], ('No diagonal. ' +
3654- "Can't happen. (diff_path1)")
3655- if last_op == self.DIFF_EQUAL:
3656- path[0] = (self.DIFF_EQUAL, text1[x] + path[0][1])
3657- else:
3658- path[:0] = [(self.DIFF_EQUAL, text1[x])]
3659- last_op = self.DIFF_EQUAL
3660- return path
3661-
3662- def diff_path2(self, v_map, text1, text2):
3663- """Work from the middle back to the end to determine the path.
3664-
3665- Args:
3666- v_map: Array of paths.
3667- text1: Old string fragment to be diffed.
3668- text2: New string fragment to be diffed.
3669-
3670- Returns:
3671- Array of diff tuples.
3672-
3673- """
3674- path = []
3675- x = len(text1)
3676- y = len(text2)
3677- last_op = None
3678- for d in xrange(len(v_map) - 2, -1, -1):
3679- while True:
3680- if (x - 1, y) in v_map[d]:
3681- x -= 1
3682- if last_op == self.DIFF_DELETE:
3683- path[-1] = (self.DIFF_DELETE, path[-1]
3684- [1] + text1[-x - 1])
3685- else:
3686- path.append((self.DIFF_DELETE, text1[-x - 1]))
3687- last_op = self.DIFF_DELETE
3688- break
3689- elif (x, y - 1) in v_map[d]:
3690- y -= 1
3691- if last_op == self.DIFF_INSERT:
3692- path[-1] = (self.DIFF_INSERT, path[-1]
3693- [1] + text2[-y - 1])
3694- else:
3695- path.append((self.DIFF_INSERT, text2[-y - 1]))
3696- last_op = self.DIFF_INSERT
3697- break
3698- else:
3699- x -= 1
3700- y -= 1
3701- assert text1[-x - 1] == text2[-y - 1], ('No diagonal. ' +
3702- "Can't happen. (diff_path2)")
3703- if last_op == self.DIFF_EQUAL:
3704- path[-1] = (self.DIFF_EQUAL, path[-1]
3705- [1] + text1[-x - 1])
3706- else:
3707- path.append((self.DIFF_EQUAL, text1[-x - 1]))
3708- last_op = self.DIFF_EQUAL
3709- return path
3710-
3711- def diff_commonPrefix(self, text1, text2):
3712- """Determine the common prefix of two strings.
3713-
3714- Args:
3715- text1: First string.
3716- text2: Second string.
3717-
3718- Returns:
3719- The number of characters common to the start of each string.
3720-
3721- """
3722- # Quick check for common null cases.
3723- if not text1 or not text2 or text1[0] != text2[0]:
3724- return 0
3725- # Binary search.
3726- # Performance analysis: http://neil.fraser.name/news/2007/10/09/
3727- pointermin = 0
3728- pointermax = min(len(text1), len(text2))
3729- pointermid = pointermax
3730- pointerstart = 0
3731- while pointermin < pointermid:
3732- if text1[pointerstart:pointermid] == text2[pointerstart:pointermid]:
3733- pointermin = pointermid
3734- pointerstart = pointermin
3735- else:
3736- pointermax = pointermid
3737- pointermid = int((pointermax - pointermin) / 2 + pointermin)
3738- return pointermid
3739-
3740- def diff_commonSuffix(self, text1, text2):
3741- """Determine the common suffix of two strings.
3742-
3743- Args:
3744- text1: First string.
3745- text2: Second string.
3746-
3747- Returns:
3748- The number of characters common to the end of each string.
3749-
3750- """
3751- # Quick check for common null cases.
3752- if not text1 or not text2 or text1[-1] != text2[-1]:
3753- return 0
3754- # Binary search.
3755- # Performance analysis: http://neil.fraser.name/news/2007/10/09/
3756- pointermin = 0
3757- pointermax = min(len(text1), len(text2))
3758- pointermid = pointermax
3759- pointerend = 0
3760- while pointermin < pointermid:
3761- if (text1[-pointermid:len(text1) - pointerend] ==
3762- text2[-pointermid:len(text2) - pointerend]):
3763- pointermin = pointermid
3764- pointerend = pointermin
3765- else:
3766- pointermax = pointermid
3767- pointermid = int((pointermax - pointermin) / 2 + pointermin)
3768- return pointermid
3769-
3770- def diff_halfMatch(self, text1, text2):
3771- """Do the two texts share a substring which is at least half the length
3772- of the longer text?
3773-
3774- Args:
3775- text1: First string.
3776- text2: Second string.
3777-
3778- Returns:
3779- Five element Array, containing the prefix of text1, the suffix of text1,
3780- the prefix of text2, the suffix of text2 and the common middle. Or None
3781- if there was no match.
3782-
3783- """
3784- if len(text1) > len(text2):
3785- (longtext, shorttext) = (text1, text2)
3786- else:
3787- (shorttext, longtext) = (text1, text2)
3788- if len(longtext) < 10 or len(shorttext) < 1:
3789- return None # Pointless.
3790-
3791- def diff_halfMatchI(longtext, shorttext, i):
3792- """Does a substring of shorttext exist within longtext such that
3793- the substring is at least half the length of longtext? Closure, but
3794- does not reference any external variables.
3795-
3796- Args:
3797- longtext: Longer string.
3798- shorttext: Shorter string.
3799- i: Start index of quarter length substring within longtext.
3800-
3801- Returns:
3802- Five element Array, containing the prefix of longtext, the suffix of
3803- longtext, the prefix of shorttext, the suffix of shorttext and the
3804- common middle. Or None if there was no match.
3805-
3806- """
3807- seed = longtext[i:i + len(longtext) / 4]
3808- best_common = ''
3809- j = shorttext.find(seed)
3810- while j != -1:
3811- prefixLength = self.diff_commonPrefix(
3812- longtext[i:], shorttext[j:])
3813- suffixLength = self.diff_commonSuffix(
3814- longtext[:i], shorttext[:j])
3815- if len(best_common) < suffixLength + prefixLength:
3816- best_common = (shorttext[j - suffixLength:j] +
3817- shorttext[j:j + prefixLength])
3818- best_longtext_a = longtext[:i - suffixLength]
3819- best_longtext_b = longtext[i + prefixLength:]
3820- best_shorttext_a = shorttext[:j - suffixLength]
3821- best_shorttext_b = shorttext[j + prefixLength:]
3822- j = shorttext.find(seed, j + 1)
3823-
3824- if len(best_common) >= len(longtext) / 2:
3825- return (best_longtext_a, best_longtext_b,
3826- best_shorttext_a, best_shorttext_b, best_common)
3827- else:
3828- return None
3829-
3830- # First check if the second quarter is the seed for a half-match.
3831- hm1 = diff_halfMatchI(longtext, shorttext, (len(longtext) + 3) / 4)
3832- # Check again based on the third quarter.
3833- hm2 = diff_halfMatchI(longtext, shorttext, (len(longtext) + 1) / 2)
3834- if not hm1 and not hm2:
3835- return None
3836- elif not hm2:
3837- hm = hm1
3838- elif not hm1:
3839- hm = hm2
3840- else:
3841- # Both matched. Select the longest.
3842- if len(hm1[4]) > len(hm2[4]):
3843- hm = hm1
3844- else:
3845- hm = hm2
3846-
3847- # A half-match was found, sort out the return data.
3848- if len(text1) > len(text2):
3849- (text1_a, text1_b, text2_a, text2_b, mid_common) = hm
3850- else:
3851- (text2_a, text2_b, text1_a, text1_b, mid_common) = hm
3852- return (text1_a, text1_b, text2_a, text2_b, mid_common)
3853-
3854- def diff_cleanupSemantic(self, diffs):
3855- """Reduce the number of edits by eliminating semantically trivial
3856- equalities.
3857-
3858- Args:
3859- diffs: Array of diff tuples.
3860-
3861- """
3862- changes = False
3863- equalities = [] # Stack of indices where equalities are found.
3864- lastequality = None # Always equal to equalities[-1][1]
3865- pointer = 0 # Index of current position.
3866- # Number of chars that changed prior to the equality.
3867- length_changes1 = 0
3868- length_changes2 = 0 # Number of chars that changed after the equality.
3869- while pointer < len(diffs):
3870- if diffs[pointer][0] == self.DIFF_EQUAL: # equality found
3871- equalities.append(pointer)
3872- length_changes1 = length_changes2
3873- length_changes2 = 0
3874- lastequality = diffs[pointer][1]
3875- else: # an insertion or deletion
3876- length_changes2 += len(diffs[pointer][1])
3877- if (lastequality != None and (len(lastequality) <= length_changes1) and
3878- (len(lastequality) <= length_changes2)):
3879- # Duplicate record
3880- diffs.insert(equalities[-1],
3881- (self.DIFF_DELETE, lastequality))
3882- # Change second copy to insert.
3883- diffs[equalities[-1] + 1] = (self.DIFF_INSERT,
3884- diffs[equalities[-1] + 1][1])
3885- # Throw away the equality we just deleted.
3886- equalities.pop()
3887- # Throw away the previous equality (it needs to be
3888- # reevaluated).
3889- if len(equalities) != 0:
3890- equalities.pop()
3891- if len(equalities):
3892- pointer = equalities[-1]
3893- else:
3894- pointer = -1
3895- length_changes1 = 0 # Reset the counters.
3896- length_changes2 = 0
3897- lastequality = None
3898- changes = True
3899- pointer += 1
3900-
3901- if changes:
3902- self.diff_cleanupMerge(diffs)
3903-
3904- self.diff_cleanupSemanticLossless(diffs)
3905-
3906- def diff_cleanupSemanticLossless(self, diffs):
3907- """Look for single edits surrounded on both sides by equalities
3908- which can be shifted sideways to align the edit to a word boundary.
3909- e.g: The c<ins>at c</ins>ame. -> The <ins>cat </ins>came.
3910-
3911- Args:
3912- diffs: Array of diff tuples.
3913- """
3914-
3915- def diff_cleanupSemanticScore(one, two):
3916- """Given two strings, compute a score representing whether the
3917- internal boundary falls on logical boundaries. Scores range from 5
3918- (best) to 0 (worst). Closure, but does not reference any external
3919- variables.
3920-
3921- Args:
3922- one: First string.
3923- two: Second string.
3924-
3925- Returns:
3926- The score.
3927-
3928- """
3929- if not one or not two:
3930- # Edges are the best.
3931- return 5
3932-
3933- # Each port of this function behaves slightly differently due to
3934- # subtle differences in each language's definition of things like
3935- # 'whitespace'. Since this function's purpose is largely cosmetic,
3936- # the choice has been made to use each language's native features
3937- # rather than force total conformity.
3938- score = 0
3939- # One point for non-alphanumeric.
3940- if not one[-1].isalnum() or not two[0].isalnum():
3941- score += 1
3942- # Two points for whitespace.
3943- if one[-1].isspace() or two[0].isspace():
3944- score += 1
3945- # Three points for line breaks.
3946- if (one[-1] == '\r' or one[-1] == '\n' or
3947- two[0] == '\r' or two[0] == '\n'):
3948- score += 1
3949- # Four points for blank lines.
3950- if (re.search('\\n\\r?\\n$', one) or
3951- re.match('^\\r?\\n\\r?\\n', two)):
3952- score += 1
3953- return score
3954-
3955- pointer = 1
3956- # Intentionally ignore the first and last element (don't need
3957- # checking).
3958- while pointer < len(diffs) - 1:
3959- if (diffs[pointer - 1][0] == self.DIFF_EQUAL and
3960- diffs[pointer + 1][0] == self.DIFF_EQUAL):
3961- # This is a single edit surrounded by equalities.
3962- equality1 = diffs[pointer - 1][1]
3963- edit = diffs[pointer][1]
3964- equality2 = diffs[pointer + 1][1]
3965-
3966- # First, shift the edit as far left as possible.
3967- commonOffset = self.diff_commonSuffix(equality1, edit)
3968- if commonOffset:
3969- commonString = edit[-commonOffset:]
3970- equality1 = equality1[:-commonOffset]
3971- edit = commonString + edit[:-commonOffset]
3972- equality2 = commonString + equality2
3973-
3974- # Second, step character by character right, looking for the
3975- # best fit.
3976- bestEquality1 = equality1
3977- bestEdit = edit
3978- bestEquality2 = equality2
3979- bestScore = (diff_cleanupSemanticScore(equality1, edit) +
3980- diff_cleanupSemanticScore(edit, equality2))
3981- while edit and equality2 and edit[0] == equality2[0]:
3982- equality1 += edit[0]
3983- edit = edit[1:] + equality2[0]
3984- equality2 = equality2[1:]
3985- score = (diff_cleanupSemanticScore(equality1, edit) +
3986- diff_cleanupSemanticScore(edit, equality2))
3987- # The >= encourages trailing rather than leading whitespace
3988- # on edits.
3989- if score >= bestScore:
3990- bestScore = score
3991- bestEquality1 = equality1
3992- bestEdit = edit
3993- bestEquality2 = equality2
3994-
3995- if diffs[pointer - 1][1] != bestEquality1:
3996- # We have an improvement, save it back to the diff.
3997- if bestEquality1:
3998- diffs[pointer - 1] = (diffs[pointer - 1]
3999- [0], bestEquality1)
4000- else:
4001- del diffs[pointer - 1]
4002- pointer -= 1
4003- diffs[pointer] = (diffs[pointer][0], bestEdit)
4004- if bestEquality2:
4005- diffs[pointer + 1] = (diffs[pointer + 1]
4006- [0], bestEquality2)
4007- else:
4008- del diffs[pointer + 1]
4009- pointer -= 1
4010- pointer += 1
4011-
4012- def diff_cleanupEfficiency(self, diffs):
4013- """Reduce the number of edits by eliminating operationally trivial
4014- equalities.
4015-
4016- Args:
4017- diffs: Array of diff tuples.
4018-
4019- """
4020- changes = False
4021- equalities = [] # Stack of indices where equalities are found.
4022- lastequality = '' # Always equal to equalities[-1][1]
4023- pointer = 0 # Index of current position.
4024- # Is there an insertion operation before the last equality.
4025- pre_ins = False
4026- # Is there a deletion operation before the last equality.
4027- pre_del = False
4028- # Is there an insertion operation after the last equality.
4029- post_ins = False
4030- # Is there a deletion operation after the last equality.
4031- post_del = False
4032- while pointer < len(diffs):
4033- if diffs[pointer][0] == self.DIFF_EQUAL: # equality found
4034- if (len(diffs[pointer][1]) < self.Diff_EditCost and
4035- (post_ins or post_del)):
4036- # Candidate found.
4037- equalities.append(pointer)
4038- pre_ins = post_ins
4039- pre_del = post_del
4040- lastequality = diffs[pointer][1]
4041- else:
4042- # Not a candidate, and can never become one.
4043- equalities = []
4044- lastequality = ''
4045-
4046- post_ins = post_del = False
4047- else: # an insertion or deletion
4048- if diffs[pointer][0] == self.DIFF_DELETE:
4049- post_del = True
4050- else:
4051- post_ins = True
4052-
4053- # Five types to be split:
4054- # <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del>
4055- # <ins>A</ins>X<ins>C</ins><del>D</del>
4056- # <ins>A</ins><del>B</del>X<ins>C</ins>
4057- # <ins>A</del>X<ins>C</ins><del>D</del>
4058- # <ins>A</ins><del>B</del>X<del>C</del>
4059-
4060- if lastequality and ((pre_ins and pre_del and post_ins and post_del) or
4061- ((len(lastequality) < self.Diff_EditCost / 2) and
4062- (pre_ins + pre_del + post_ins + post_del) == 3)):
4063- # Duplicate record
4064- diffs.insert(equalities[-1],
4065- (self.DIFF_DELETE, lastequality))
4066- # Change second copy to insert.
4067- diffs[equalities[-1] + 1] = (self.DIFF_INSERT,
4068- diffs[equalities[-1] + 1][1])
4069- equalities.pop() # Throw away the equality we just deleted
4070- lastequality = ''
4071- if pre_ins and pre_del:
4072- # No changes made which could affect previous entry,
4073- # keep going.
4074- post_ins = post_del = True
4075- equalities = []
4076- else:
4077- if len(equalities):
4078- equalities.pop() # Throw away the previous equality
4079- if len(equalities):
4080- pointer = equalities[-1]
4081- else:
4082- pointer = -1
4083- post_ins = post_del = False
4084- changes = True
4085- pointer += 1
4086-
4087- if changes:
4088- self.diff_cleanupMerge(diffs)
4089-
4090- def diff_cleanupMerge(self, diffs):
4091- """Reorder and merge like edit sections. Merge equalities. Any edit
4092- section can move as long as it doesn't cross an equality.
4093-
4094- Args:
4095- diffs: Array of diff tuples.
4096-
4097- """
4098- diffs.append((self.DIFF_EQUAL, '')) # Add a dummy entry at the end.
4099- pointer = 0
4100+ # First check if the second quarter is the seed for a half-match.
4101+ hm1 = diff_halfMatchI(longtext, shorttext, (len(longtext) + 3) // 4)
4102+ # Check again based on the third quarter.
4103+ hm2 = diff_halfMatchI(longtext, shorttext, (len(longtext) + 1) // 2)
4104+ if not hm1 and not hm2:
4105+ return None
4106+ elif not hm2:
4107+ hm = hm1
4108+ elif not hm1:
4109+ hm = hm2
4110+ else:
4111+ # Both matched. Select the longest.
4112+ if len(hm1[4]) > len(hm2[4]):
4113+ hm = hm1
4114+ else:
4115+ hm = hm2
4116+
4117+ # A half-match was found, sort out the return data.
4118+ if len(text1) > len(text2):
4119+ (text1_a, text1_b, text2_a, text2_b, mid_common) = hm
4120+ else:
4121+ (text2_a, text2_b, text1_a, text1_b, mid_common) = hm
4122+ return (text1_a, text1_b, text2_a, text2_b, mid_common)
4123+
4124+ def diff_cleanupSemantic(self, diffs):
4125+ """Reduce the number of edits by eliminating semantically trivial
4126+ equalities.
4127+
4128+ Args:
4129+ diffs: Array of diff tuples.
4130+ """
4131+ changes = False
4132+ equalities = [] # Stack of indices where equalities are found.
4133+ lastEquality = None # Always equal to diffs[equalities[-1]][1]
4134+ pointer = 0 # Index of current position.
4135+ # Number of chars that changed prior to the equality.
4136+ length_insertions1, length_deletions1 = 0, 0
4137+ # Number of chars that changed after the equality.
4138+ length_insertions2, length_deletions2 = 0, 0
4139+ while pointer < len(diffs):
4140+ if diffs[pointer][0] == self.DIFF_EQUAL: # Equality found.
4141+ equalities.append(pointer)
4142+ length_insertions1, length_insertions2 = length_insertions2, 0
4143+ length_deletions1, length_deletions2 = length_deletions2, 0
4144+ lastEquality = diffs[pointer][1]
4145+ else: # An insertion or deletion.
4146+ if diffs[pointer][0] == self.DIFF_INSERT:
4147+ length_insertions2 += len(diffs[pointer][1])
4148+ else:
4149+ length_deletions2 += len(diffs[pointer][1])
4150+ # Eliminate an equality that is smaller or equal to the edits on both
4151+ # sides of it.
4152+ if (lastEquality and (len(lastEquality) <=
4153+ max(length_insertions1, length_deletions1)) and
4154+ (len(lastEquality) <= max(length_insertions2, length_deletions2))):
4155+ # Duplicate record.
4156+ diffs.insert(equalities[-1], (self.DIFF_DELETE, lastEquality))
4157+ # Change second copy to insert.
4158+ diffs[equalities[-1] + 1] = (self.DIFF_INSERT,
4159+ diffs[equalities[-1] + 1][1])
4160+ # Throw away the equality we just deleted.
4161+ equalities.pop()
4162+ # Throw away the previous equality (it needs to be reevaluated).
4163+ if len(equalities):
4164+ equalities.pop()
4165+ if len(equalities):
4166+ pointer = equalities[-1]
4167+ else:
4168+ pointer = -1
4169+ # Reset the counters.
4170+ length_insertions1, length_deletions1 = 0, 0
4171+ length_insertions2, length_deletions2 = 0, 0
4172+ lastEquality = None
4173+ changes = True
4174+ pointer += 1
4175+
4176+ # Normalize the diff.
4177+ if changes:
4178+ self.diff_cleanupMerge(diffs)
4179+ self.diff_cleanupSemanticLossless(diffs)
4180+
4181+ # Find any overlaps between deletions and insertions.
4182+ # e.g: <del>abcxxx</del><ins>xxxdef</ins>
4183+ # -> <del>abc</del>xxx<ins>def</ins>
4184+ # e.g: <del>xxxabc</del><ins>defxxx</ins>
4185+ # -> <ins>def</ins>xxx<del>abc</del>
4186+ # Only extract an overlap if it is as big as the edit ahead or behind it.
4187+ pointer = 1
4188+ while pointer < len(diffs):
4189+ if (diffs[pointer - 1][0] == self.DIFF_DELETE and
4190+ diffs[pointer][0] == self.DIFF_INSERT):
4191+ deletion = diffs[pointer - 1][1]
4192+ insertion = diffs[pointer][1]
4193+ overlap_length1 = self.diff_commonOverlap(deletion, insertion)
4194+ overlap_length2 = self.diff_commonOverlap(insertion, deletion)
4195+ if overlap_length1 >= overlap_length2:
4196+ if (overlap_length1 >= len(deletion) / 2.0 or
4197+ overlap_length1 >= len(insertion) / 2.0):
4198+ # Overlap found. Insert an equality and trim the surrounding edits.
4199+ diffs.insert(pointer, (self.DIFF_EQUAL,
4200+ insertion[:overlap_length1]))
4201+ diffs[pointer - 1] = (self.DIFF_DELETE,
4202+ deletion[:len(deletion) - overlap_length1])
4203+ diffs[pointer + 1] = (self.DIFF_INSERT,
4204+ insertion[overlap_length1:])
4205+ pointer += 1
4206+ else:
4207+ if (overlap_length2 >= len(deletion) / 2.0 or
4208+ overlap_length2 >= len(insertion) / 2.0):
4209+ # Reverse overlap found.
4210+ # Insert an equality and swap and trim the surrounding edits.
4211+ diffs.insert(pointer, (self.DIFF_EQUAL, deletion[:overlap_length2]))
4212+ diffs[pointer - 1] = (self.DIFF_INSERT,
4213+ insertion[:len(insertion) - overlap_length2])
4214+ diffs[pointer + 1] = (self.DIFF_DELETE, deletion[overlap_length2:])
4215+ pointer += 1
4216+ pointer += 1
4217+ pointer += 1
4218+
4219+ def diff_cleanupSemanticLossless(self, diffs):
4220+ """Look for single edits surrounded on both sides by equalities
4221+ which can be shifted sideways to align the edit to a word boundary.
4222+ e.g: The c<ins>at c</ins>ame. -> The <ins>cat </ins>came.
4223+
4224+ Args:
4225+ diffs: Array of diff tuples.
4226+ """
4227+
4228+ def diff_cleanupSemanticScore(one, two):
4229+ """Given two strings, compute a score representing whether the
4230+ internal boundary falls on logical boundaries.
4231+ Scores range from 6 (best) to 0 (worst).
4232+ Closure, but does not reference any external variables.
4233+
4234+ Args:
4235+ one: First string.
4236+ two: Second string.
4237+
4238+ Returns:
4239+ The score.
4240+ """
4241+ if not one or not two:
4242+ # Edges are the best.
4243+ return 6
4244+
4245+ # Each port of this function behaves slightly differently due to
4246+ # subtle differences in each language's definition of things like
4247+ # 'whitespace'. Since this function's purpose is largely cosmetic,
4248+ # the choice has been made to use each language's native features
4249+ # rather than force total conformity.
4250+ char1 = one[-1]
4251+ char2 = two[0]
4252+ nonAlphaNumeric1 = not char1.isalnum()
4253+ nonAlphaNumeric2 = not char2.isalnum()
4254+ whitespace1 = nonAlphaNumeric1 and char1.isspace()
4255+ whitespace2 = nonAlphaNumeric2 and char2.isspace()
4256+ lineBreak1 = whitespace1 and (char1 == "\r" or char1 == "\n")
4257+ lineBreak2 = whitespace2 and (char2 == "\r" or char2 == "\n")
4258+ blankLine1 = lineBreak1 and self.BLANKLINEEND.search(one)
4259+ blankLine2 = lineBreak2 and self.BLANKLINESTART.match(two)
4260+
4261+ if blankLine1 or blankLine2:
4262+ # Five points for blank lines.
4263+ return 5
4264+ elif lineBreak1 or lineBreak2:
4265+ # Four points for line breaks.
4266+ return 4
4267+ elif nonAlphaNumeric1 and not whitespace1 and whitespace2:
4268+ # Three points for end of sentences.
4269+ return 3
4270+ elif whitespace1 or whitespace2:
4271+ # Two points for whitespace.
4272+ return 2
4273+ elif nonAlphaNumeric1 or nonAlphaNumeric2:
4274+ # One point for non-alphanumeric.
4275+ return 1
4276+ return 0
4277+
4278+ pointer = 1
4279+ # Intentionally ignore the first and last element (don't need checking).
4280+ while pointer < len(diffs) - 1:
4281+ if (diffs[pointer - 1][0] == self.DIFF_EQUAL and
4282+ diffs[pointer + 1][0] == self.DIFF_EQUAL):
4283+ # This is a single edit surrounded by equalities.
4284+ equality1 = diffs[pointer - 1][1]
4285+ edit = diffs[pointer][1]
4286+ equality2 = diffs[pointer + 1][1]
4287+
4288+ # First, shift the edit as far left as possible.
4289+ commonOffset = self.diff_commonSuffix(equality1, edit)
4290+ if commonOffset:
4291+ commonString = edit[-commonOffset:]
4292+ equality1 = equality1[:-commonOffset]
4293+ edit = commonString + edit[:-commonOffset]
4294+ equality2 = commonString + equality2
4295+
4296+ # Second, step character by character right, looking for the best fit.
4297+ bestEquality1 = equality1
4298+ bestEdit = edit
4299+ bestEquality2 = equality2
4300+ bestScore = (diff_cleanupSemanticScore(equality1, edit) +
4301+ diff_cleanupSemanticScore(edit, equality2))
4302+ while edit and equality2 and edit[0] == equality2[0]:
4303+ equality1 += edit[0]
4304+ edit = edit[1:] + equality2[0]
4305+ equality2 = equality2[1:]
4306+ score = (diff_cleanupSemanticScore(equality1, edit) +
4307+ diff_cleanupSemanticScore(edit, equality2))
4308+ # The >= encourages trailing rather than leading whitespace on edits.
4309+ if score >= bestScore:
4310+ bestScore = score
4311+ bestEquality1 = equality1
4312+ bestEdit = edit
4313+ bestEquality2 = equality2
4314+
4315+ if diffs[pointer - 1][1] != bestEquality1:
4316+ # We have an improvement, save it back to the diff.
4317+ if bestEquality1:
4318+ diffs[pointer - 1] = (diffs[pointer - 1][0], bestEquality1)
4319+ else:
4320+ del diffs[pointer - 1]
4321+ pointer -= 1
4322+ diffs[pointer] = (diffs[pointer][0], bestEdit)
4323+ if bestEquality2:
4324+ diffs[pointer + 1] = (diffs[pointer + 1][0], bestEquality2)
4325+ else:
4326+ del diffs[pointer + 1]
4327+ pointer -= 1
4328+ pointer += 1
4329+
4330+ # Define some regex patterns for matching boundaries.
4331+ BLANKLINEEND = re.compile(r"\n\r?\n$")
4332+ BLANKLINESTART = re.compile(r"^\r?\n\r?\n")
4333+
4334+ def diff_cleanupEfficiency(self, diffs):
4335+ """Reduce the number of edits by eliminating operationally trivial
4336+ equalities.
4337+
4338+ Args:
4339+ diffs: Array of diff tuples.
4340+ """
4341+ changes = False
4342+ equalities = [] # Stack of indices where equalities are found.
4343+ lastEquality = None # Always equal to diffs[equalities[-1]][1]
4344+ pointer = 0 # Index of current position.
4345+ pre_ins = False # Is there an insertion operation before the last equality.
4346+ pre_del = False # Is there a deletion operation before the last equality.
4347+ post_ins = False # Is there an insertion operation after the last equality.
4348+ post_del = False # Is there a deletion operation after the last equality.
4349+ while pointer < len(diffs):
4350+ if diffs[pointer][0] == self.DIFF_EQUAL: # Equality found.
4351+ if (len(diffs[pointer][1]) < self.Diff_EditCost and
4352+ (post_ins or post_del)):
4353+ # Candidate found.
4354+ equalities.append(pointer)
4355+ pre_ins = post_ins
4356+ pre_del = post_del
4357+ lastEquality = diffs[pointer][1]
4358+ else:
4359+ # Not a candidate, and can never become one.
4360+ equalities = []
4361+ lastEquality = None
4362+
4363+ post_ins = post_del = False
4364+ else: # An insertion or deletion.
4365+ if diffs[pointer][0] == self.DIFF_DELETE:
4366+ post_del = True
4367+ else:
4368+ post_ins = True
4369+
4370+ # Five types to be split:
4371+ # <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del>
4372+ # <ins>A</ins>X<ins>C</ins><del>D</del>
4373+ # <ins>A</ins><del>B</del>X<ins>C</ins>
4374+ # <ins>A</del>X<ins>C</ins><del>D</del>
4375+ # <ins>A</ins><del>B</del>X<del>C</del>
4376+
4377+ if lastEquality and ((pre_ins and pre_del and post_ins and post_del) or
4378+ ((len(lastEquality) < self.Diff_EditCost / 2) and
4379+ (pre_ins + pre_del + post_ins + post_del) == 3)):
4380+ # Duplicate record.
4381+ diffs.insert(equalities[-1], (self.DIFF_DELETE, lastEquality))
4382+ # Change second copy to insert.
4383+ diffs[equalities[-1] + 1] = (self.DIFF_INSERT,
4384+ diffs[equalities[-1] + 1][1])
4385+ equalities.pop() # Throw away the equality we just deleted.
4386+ lastEquality = None
4387+ if pre_ins and pre_del:
4388+ # No changes made which could affect previous entry, keep going.
4389+ post_ins = post_del = True
4390+ equalities = []
4391+ else:
4392+ if len(equalities):
4393+ equalities.pop() # Throw away the previous equality.
4394+ if len(equalities):
4395+ pointer = equalities[-1]
4396+ else:
4397+ pointer = -1
4398+ post_ins = post_del = False
4399+ changes = True
4400+ pointer += 1
4401+
4402+ if changes:
4403+ self.diff_cleanupMerge(diffs)
4404+
4405+ def diff_cleanupMerge(self, diffs):
4406+ """Reorder and merge like edit sections. Merge equalities.
4407+ Any edit section can move as long as it doesn't cross an equality.
4408+
4409+ Args:
4410+ diffs: Array of diff tuples.
4411+ """
4412+ diffs.append((self.DIFF_EQUAL, '')) # Add a dummy entry at the end.
4413+ pointer = 0
4414+ count_delete = 0
4415+ count_insert = 0
4416+ text_delete = ''
4417+ text_insert = ''
4418+ while pointer < len(diffs):
4419+ if diffs[pointer][0] == self.DIFF_INSERT:
4420+ count_insert += 1
4421+ text_insert += diffs[pointer][1]
4422+ pointer += 1
4423+ elif diffs[pointer][0] == self.DIFF_DELETE:
4424+ count_delete += 1
4425+ text_delete += diffs[pointer][1]
4426+ pointer += 1
4427+ elif diffs[pointer][0] == self.DIFF_EQUAL:
4428+ # Upon reaching an equality, check for prior redundancies.
4429+ if count_delete + count_insert > 1:
4430+ if count_delete != 0 and count_insert != 0:
4431+ # Factor out any common prefixies.
4432+ commonlength = self.diff_commonPrefix(text_insert, text_delete)
4433+ if commonlength != 0:
4434+ x = pointer - count_delete - count_insert - 1
4435+ if x >= 0 and diffs[x][0] == self.DIFF_EQUAL:
4436+ diffs[x] = (diffs[x][0], diffs[x][1] +
4437+ text_insert[:commonlength])
4438+ else:
4439+ diffs.insert(0, (self.DIFF_EQUAL, text_insert[:commonlength]))
4440+ pointer += 1
4441+ text_insert = text_insert[commonlength:]
4442+ text_delete = text_delete[commonlength:]
4443+ # Factor out any common suffixies.
4444+ commonlength = self.diff_commonSuffix(text_insert, text_delete)
4445+ if commonlength != 0:
4446+ diffs[pointer] = (diffs[pointer][0], text_insert[-commonlength:] +
4447+ diffs[pointer][1])
4448+ text_insert = text_insert[:-commonlength]
4449+ text_delete = text_delete[:-commonlength]
4450+ # Delete the offending records and add the merged ones.
4451+ new_ops = []
4452+ if len(text_delete) != 0:
4453+ new_ops.append((self.DIFF_DELETE, text_delete))
4454+ if len(text_insert) != 0:
4455+ new_ops.append((self.DIFF_INSERT, text_insert))
4456+ pointer -= count_delete + count_insert
4457+ diffs[pointer : pointer + count_delete + count_insert] = new_ops
4458+ pointer += len(new_ops) + 1
4459+ elif pointer != 0 and diffs[pointer - 1][0] == self.DIFF_EQUAL:
4460+ # Merge this equality with the previous one.
4461+ diffs[pointer - 1] = (diffs[pointer - 1][0],
4462+ diffs[pointer - 1][1] + diffs[pointer][1])
4463+ del diffs[pointer]
4464+ else:
4465+ pointer += 1
4466+
4467+ count_insert = 0
4468 count_delete = 0
4469- count_insert = 0
4470 text_delete = ''
4471 text_insert = ''
4472- while pointer < len(diffs):
4473- if diffs[pointer][0] == self.DIFF_INSERT:
4474- count_insert += 1
4475- text_insert += diffs[pointer][1]
4476- pointer += 1
4477- elif diffs[pointer][0] == self.DIFF_DELETE:
4478- count_delete += 1
4479- text_delete += diffs[pointer][1]
4480- pointer += 1
4481- elif diffs[pointer][0] == self.DIFF_EQUAL:
4482- # Upon reaching an equality, check for prior redundancies.
4483- if count_delete != 0 or count_insert != 0:
4484- if count_delete != 0 and count_insert != 0:
4485- # Factor out any common prefixies.
4486- commonlength = self.diff_commonPrefix(
4487- text_insert, text_delete)
4488- if commonlength != 0:
4489- x = pointer - count_delete - count_insert - 1
4490- if x >= 0 and diffs[x][0] == self.DIFF_EQUAL:
4491- diffs[x] = (diffs[x][0], diffs[x][1] +
4492- text_insert[:commonlength])
4493- else:
4494- diffs.insert(
4495- 0, (self.DIFF_EQUAL, text_insert[:commonlength]))
4496- pointer += 1
4497- text_insert = text_insert[commonlength:]
4498- text_delete = text_delete[commonlength:]
4499- # Factor out any common suffixies.
4500- commonlength = self.diff_commonSuffix(
4501- text_insert, text_delete)
4502- if commonlength != 0:
4503- diffs[pointer] = (diffs[pointer][0], text_insert[-commonlength:] +
4504- diffs[pointer][1])
4505- text_insert = text_insert[:-commonlength]
4506- text_delete = text_delete[:-commonlength]
4507- # Delete the offending records and add the merged ones.
4508- if count_delete == 0:
4509- diffs[pointer - count_insert: pointer] = [
4510- (self.DIFF_INSERT, text_insert)]
4511- elif count_insert == 0:
4512- diffs[pointer - count_delete: pointer] = [
4513- (self.DIFF_DELETE, text_delete)]
4514- else:
4515- diffs[pointer - count_delete - count_insert: pointer] = [
4516- (self.DIFF_DELETE, text_delete),
4517- (self.DIFF_INSERT, text_insert)]
4518- pointer = pointer - count_delete - count_insert + 1
4519- if count_delete != 0:
4520- pointer += 1
4521- if count_insert != 0:
4522- pointer += 1
4523- elif pointer != 0 and diffs[pointer - 1][0] == self.DIFF_EQUAL:
4524- # Merge this equality with the previous one.
4525- diffs[pointer - 1] = (diffs[pointer - 1][0],
4526- diffs[pointer - 1][1] + diffs[pointer][1])
4527- del diffs[pointer]
4528- else:
4529- pointer += 1
4530-
4531- count_insert = 0
4532- count_delete = 0
4533- text_delete = ''
4534- text_insert = ''
4535-
4536- if diffs[-1][1] == '':
4537- diffs.pop() # Remove the dummy entry at the end.
4538-
4539- # Second pass: look for single edits surrounded on both sides by equalities
4540- # which can be shifted sideways to eliminate an equality.
4541- # e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
4542- changes = False
4543- pointer = 1
4544- # Intentionally ignore the first and last element (don't need
4545- # checking).
4546- while pointer < len(diffs) - 1:
4547- if (diffs[pointer - 1][0] == self.DIFF_EQUAL and
4548- diffs[pointer + 1][0] == self.DIFF_EQUAL):
4549- # This is a single edit surrounded by equalities.
4550- if diffs[pointer][1].endswith(diffs[pointer - 1][1]):
4551- # Shift the edit over the previous equality.
4552- diffs[pointer] = (diffs[pointer][0],
4553- diffs[pointer - 1][1] +
4554- diffs[pointer][1][:-len(diffs[pointer - 1][1])])
4555- diffs[pointer + 1] = (diffs[pointer + 1][0],
4556- diffs[pointer - 1][1] + diffs[pointer + 1][1])
4557- del diffs[pointer - 1]
4558- changes = True
4559- elif diffs[pointer][1].startswith(diffs[pointer + 1][1]):
4560- # Shift the edit over the next equality.
4561- diffs[pointer - 1] = (diffs[pointer - 1][0],
4562- diffs[pointer - 1][1] + diffs[pointer + 1][1])
4563- diffs[pointer] = (diffs[pointer][0],
4564- diffs[pointer][1][len(diffs[pointer + 1][1]):] +
4565- diffs[pointer + 1][1])
4566- del diffs[pointer + 1]
4567- changes = True
4568- pointer += 1
4569-
4570- # If shifts were made, the diff needs reordering and another shift
4571- # sweep.
4572- if changes:
4573- self.diff_cleanupMerge(diffs)
4574-
4575- def diff_xIndex(self, diffs, loc):
4576- """loc is a location in text1, compute and return the equivalent location
4577- in text2. e.g. "The cat" vs "The big cat", 1->1, 5->8
4578-
4579- Args:
4580- diffs: Array of diff tuples.
4581- loc: Location within text1.
4582-
4583- Returns:
4584- Location within text2.
4585- """
4586- chars1 = 0
4587- chars2 = 0
4588- last_chars1 = 0
4589- last_chars2 = 0
4590- for x in xrange(len(diffs)):
4591- (op, text) = diffs[x]
4592- if op != self.DIFF_INSERT: # Equality or deletion.
4593- chars1 += len(text)
4594- if op != self.DIFF_DELETE: # Equality or insertion.
4595- chars2 += len(text)
4596- if chars1 > loc: # Overshot the location.
4597- break
4598- last_chars1 = chars1
4599- last_chars2 = chars2
4600-
4601- if len(diffs) != x and diffs[x][0] == self.DIFF_DELETE:
4602- # The location was deleted.
4603- return last_chars2
4604- # Add the remaining len(character).
4605- return last_chars2 + (loc - last_chars1)
4606-
4607- def diff_prettyHtml(self, diffs):
4608- """Convert a diff array into a pretty HTML report.
4609-
4610- Args:
4611- diffs: Array of diff tuples.
4612-
4613- Returns:
4614- HTML representation.
4615-
4616- """
4617- html = []
4618- i = 0
4619- for (op, data) in diffs:
4620- text = (data.replace('&', '&amp;').replace('<', '&lt;')
4621- .replace('>', '&gt;').replace('\n', '&para;<BR>'))
4622- if op == self.DIFF_INSERT:
4623- html.append("<SPAN STYLE=\"color: #00FF00;\" TITLE=\"i=%i\">%s</SPAN>"
4624- % (i, text))
4625- elif op == self.DIFF_DELETE:
4626- html.append("<SPAN STYLE=\"color: #FF0000;\" TITLE=\"i=%i\">%s</SPAN>"
4627- % (i, text))
4628- elif op == self.DIFF_EQUAL:
4629- html.append("<SPAN TITLE=\"i=%i\">%s</SPAN>" % (i, text))
4630- if op != self.DIFF_DELETE:
4631- i += len(data)
4632- return ''.join(html)
4633-
4634- def diff_text1(self, diffs):
4635- """Compute and return the source text (all equalities and deletions).
4636-
4637- Args:
4638- diffs: Array of diff tuples.
4639-
4640- Returns:
4641- Source text.
4642-
4643- """
4644- text = []
4645- for (op, data) in diffs:
4646- if op != self.DIFF_INSERT:
4647- text.append(data)
4648- return ''.join(text)
4649-
4650- def diff_text2(self, diffs):
4651- """Compute and return the destination text (all equalities and
4652- insertions).
4653-
4654- Args:
4655- diffs: Array of diff tuples.
4656-
4657- Returns:
4658- Destination text.
4659-
4660- """
4661- text = []
4662- for (op, data) in diffs:
4663- if op != self.DIFF_DELETE:
4664- text.append(data)
4665- return ''.join(text)
4666-
4667- def diff_levenshtein(self, diffs):
4668- """Compute the Levenshtein distance; the number of inserted, deleted or
4669- substituted characters.
4670-
4671- Args:
4672- diffs: Array of diff tuples.
4673-
4674- Returns:
4675- Number of changes.
4676-
4677- """
4678- levenshtein = 0
4679+
4680+ if diffs[-1][1] == '':
4681+ diffs.pop() # Remove the dummy entry at the end.
4682+
4683+ # Second pass: look for single edits surrounded on both sides by equalities
4684+ # which can be shifted sideways to eliminate an equality.
4685+ # e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
4686+ changes = False
4687+ pointer = 1
4688+ # Intentionally ignore the first and last element (don't need checking).
4689+ while pointer < len(diffs) - 1:
4690+ if (diffs[pointer - 1][0] == self.DIFF_EQUAL and
4691+ diffs[pointer + 1][0] == self.DIFF_EQUAL):
4692+ # This is a single edit surrounded by equalities.
4693+ if diffs[pointer][1].endswith(diffs[pointer - 1][1]):
4694+ # Shift the edit over the previous equality.
4695+ if diffs[pointer - 1][1] != "":
4696+ diffs[pointer] = (diffs[pointer][0],
4697+ diffs[pointer - 1][1] +
4698+ diffs[pointer][1][:-len(diffs[pointer - 1][1])])
4699+ diffs[pointer + 1] = (diffs[pointer + 1][0],
4700+ diffs[pointer - 1][1] + diffs[pointer + 1][1])
4701+ del diffs[pointer - 1]
4702+ changes = True
4703+ elif diffs[pointer][1].startswith(diffs[pointer + 1][1]):
4704+ # Shift the edit over the next equality.
4705+ diffs[pointer - 1] = (diffs[pointer - 1][0],
4706+ diffs[pointer - 1][1] + diffs[pointer + 1][1])
4707+ diffs[pointer] = (diffs[pointer][0],
4708+ diffs[pointer][1][len(diffs[pointer + 1][1]):] +
4709+ diffs[pointer + 1][1])
4710+ del diffs[pointer + 1]
4711+ changes = True
4712+ pointer += 1
4713+
4714+ # If shifts were made, the diff needs reordering and another shift sweep.
4715+ if changes:
4716+ self.diff_cleanupMerge(diffs)
4717+
4718+ def diff_xIndex(self, diffs, loc):
4719+ """loc is a location in text1, compute and return the equivalent location
4720+ in text2. e.g. "The cat" vs "The big cat", 1->1, 5->8
4721+
4722+ Args:
4723+ diffs: Array of diff tuples.
4724+ loc: Location within text1.
4725+
4726+ Returns:
4727+ Location within text2.
4728+ """
4729+ chars1 = 0
4730+ chars2 = 0
4731+ last_chars1 = 0
4732+ last_chars2 = 0
4733+ for x in range(len(diffs)):
4734+ (op, text) = diffs[x]
4735+ if op != self.DIFF_INSERT: # Equality or deletion.
4736+ chars1 += len(text)
4737+ if op != self.DIFF_DELETE: # Equality or insertion.
4738+ chars2 += len(text)
4739+ if chars1 > loc: # Overshot the location.
4740+ break
4741+ last_chars1 = chars1
4742+ last_chars2 = chars2
4743+
4744+ if len(diffs) != x and diffs[x][0] == self.DIFF_DELETE:
4745+ # The location was deleted.
4746+ return last_chars2
4747+ # Add the remaining len(character).
4748+ return last_chars2 + (loc - last_chars1)
4749+
4750+ def diff_prettyHtml(self, diffs):
4751+ """Convert a diff array into a pretty HTML report.
4752+
4753+ Args:
4754+ diffs: Array of diff tuples.
4755+
4756+ Returns:
4757+ HTML representation.
4758+ """
4759+ html = []
4760+ for (op, data) in diffs:
4761+ text = (data.replace("&", "&amp;").replace("<", "&lt;")
4762+ .replace(">", "&gt;").replace("\n", "&para;<br>"))
4763+ if op == self.DIFF_INSERT:
4764+ html.append("<ins class=\"inserted\">%s</ins>" % text)
4765+ elif op == self.DIFF_DELETE:
4766+ html.append("<del class=\"removed\">%s</del>" % text)
4767+ elif op == self.DIFF_EQUAL:
4768+ html.append("<span>%s</span>" % text)
4769+ return "".join(html)
4770+
4771+ def diff_text1(self, diffs):
4772+ """Compute and return the source text (all equalities and deletions).
4773+
4774+ Args:
4775+ diffs: Array of diff tuples.
4776+
4777+ Returns:
4778+ Source text.
4779+ """
4780+ text = []
4781+ for (op, data) in diffs:
4782+ if op != self.DIFF_INSERT:
4783+ text.append(data)
4784+ return "".join(text)
4785+
4786+ def diff_text2(self, diffs):
4787+ """Compute and return the destination text (all equalities and insertions).
4788+
4789+ Args:
4790+ diffs: Array of diff tuples.
4791+
4792+ Returns:
4793+ Destination text.
4794+ """
4795+ text = []
4796+ for (op, data) in diffs:
4797+ if op != self.DIFF_DELETE:
4798+ text.append(data)
4799+ return "".join(text)
4800+
4801+ def diff_levenshtein(self, diffs):
4802+ """Compute the Levenshtein distance; the number of inserted, deleted or
4803+ substituted characters.
4804+
4805+ Args:
4806+ diffs: Array of diff tuples.
4807+
4808+ Returns:
4809+ Number of changes.
4810+ """
4811+ levenshtein = 0
4812+ insertions = 0
4813+ deletions = 0
4814+ for (op, data) in diffs:
4815+ if op == self.DIFF_INSERT:
4816+ insertions += len(data)
4817+ elif op == self.DIFF_DELETE:
4818+ deletions += len(data)
4819+ elif op == self.DIFF_EQUAL:
4820+ # A deletion and an insertion is one substitution.
4821+ levenshtein += max(insertions, deletions)
4822 insertions = 0
4823 deletions = 0
4824- for (op, data) in diffs:
4825- if op == self.DIFF_INSERT:
4826- insertions += len(data)
4827- elif op == self.DIFF_DELETE:
4828- deletions += len(data)
4829- elif op == self.DIFF_EQUAL:
4830- # A deletion and an insertion is one substitution.
4831- levenshtein += max(insertions, deletions)
4832- insertions = 0
4833- deletions = 0
4834- levenshtein += max(insertions, deletions)
4835- return levenshtein
4836-
4837- def diff_toDelta(self, diffs):
4838- """Crush the diff into an encoded string which describes the operations
4839- required to transform text1 into text2.
4840- E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'.
4841- Operations are tab-separated. Inserted text is escaped using %xx notation.
4842-
4843- Args:
4844- diffs: Array of diff tuples.
4845-
4846- Returns:
4847- Delta text.
4848- """
4849- text = []
4850- for (op, data) in diffs:
4851- if op == self.DIFF_INSERT:
4852- # High ascii will raise UnicodeDecodeError. Use Unicode
4853- # instead.
4854- data = data.encode('utf-8')
4855- text.append('+' + urllib.quote(data, "!~*'();/?:@&=+$,# "))
4856- elif op == self.DIFF_DELETE:
4857- text.append('-%d' % len(data))
4858- elif op == self.DIFF_EQUAL:
4859- text.append('=%d' % len(data))
4860- return '\t'.join(text)
4861-
4862- def diff_fromDelta(self, text1, delta):
4863- """Given the original text1, and an encoded string which describes the
4864- operations required to transform text1 into text2, compute the full
4865- diff.
4866-
4867- Args:
4868- text1: Source string for the diff.
4869- delta: Delta text.
4870-
4871- Returns:
4872- Array of diff tuples.
4873-
4874- Raises:
4875- ValueError: If invalid input.
4876-
4877- """
4878- if type(delta) == unicode:
4879- # Deltas should be composed of a subset of ascii chars, Unicode not
4880- # required. If this encode raises UnicodeEncodeError, delta is
4881- # invalid.
4882- delta = delta.encode('ascii')
4883- diffs = []
4884- pointer = 0 # Cursor in text1
4885- tokens = delta.split('\t')
4886- for token in tokens:
4887- if token == '':
4888- # Blank tokens are ok (from a trailing \t).
4889- continue
4890- # Each token begins with a one character parameter which specifies the
4891- # operation of this token (delete, insert, equality).
4892- param = token[1:]
4893- if token[0] == '+':
4894- param = urllib.unquote(param).decode('utf-8')
4895- diffs.append((self.DIFF_INSERT, param))
4896- elif token[0] == '-' or token[0] == '=':
4897- try:
4898- n = int(param)
4899- except ValueError:
4900- raise ValueError, 'Invalid number in diff_fromDelta: ' + param
4901- if n < 0:
4902- raise ValueError, 'Negative number in diff_fromDelta: ' + param
4903- text = text1[pointer: pointer + n]
4904- pointer += n
4905- if token[0] == '=':
4906- diffs.append((self.DIFF_EQUAL, text))
4907- else:
4908- diffs.append((self.DIFF_DELETE, text))
4909+ levenshtein += max(insertions, deletions)
4910+ return levenshtein
4911+
4912+ def diff_toDelta(self, diffs):
4913+ """Crush the diff into an encoded string which describes the operations
4914+ required to transform text1 into text2.
4915+ E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'.
4916+ Operations are tab-separated. Inserted text is escaped using %xx notation.
4917+
4918+ Args:
4919+ diffs: Array of diff tuples.
4920+
4921+ Returns:
4922+ Delta text.
4923+ """
4924+ text = []
4925+ for (op, data) in diffs:
4926+ if op == self.DIFF_INSERT:
4927+ # High ascii will raise UnicodeDecodeError. Use Unicode instead.
4928+ data = data.encode("utf-8")
4929+ text.append("+" + urllib.parse.quote(data, "!~*'();/?:@&=+$,# "))
4930+ elif op == self.DIFF_DELETE:
4931+ text.append("-%d" % len(data))
4932+ elif op == self.DIFF_EQUAL:
4933+ text.append("=%d" % len(data))
4934+ return "\t".join(text)
4935+
4936+ def diff_fromDelta(self, text1, delta):
4937+ """Given the original text1, and an encoded string which describes the
4938+ operations required to transform text1 into text2, compute the full diff.
4939+
4940+ Args:
4941+ text1: Source string for the diff.
4942+ delta: Delta text.
4943+
4944+ Returns:
4945+ Array of diff tuples.
4946+
4947+ Raises:
4948+ ValueError: If invalid input.
4949+ """
4950+ diffs = []
4951+ pointer = 0 # Cursor in text1
4952+ tokens = delta.split("\t")
4953+ for token in tokens:
4954+ if token == "":
4955+ # Blank tokens are ok (from a trailing \t).
4956+ continue
4957+ # Each token begins with a one character parameter which specifies the
4958+ # operation of this token (delete, insert, equality).
4959+ param = token[1:]
4960+ if token[0] == "+":
4961+ param = urllib.parse.unquote(param)
4962+ diffs.append((self.DIFF_INSERT, param))
4963+ elif token[0] == "-" or token[0] == "=":
4964+ try:
4965+ n = int(param)
4966+ except ValueError:
4967+ raise ValueError("Invalid number in diff_fromDelta: " + param)
4968+ if n < 0:
4969+ raise ValueError("Negative number in diff_fromDelta: " + param)
4970+ text = text1[pointer : pointer + n]
4971+ pointer += n
4972+ if token[0] == "=":
4973+ diffs.append((self.DIFF_EQUAL, text))
4974+ else:
4975+ diffs.append((self.DIFF_DELETE, text))
4976+ else:
4977+ # Anything else is an error.
4978+ raise ValueError("Invalid diff operation in diff_fromDelta: " +
4979+ token[0])
4980+ if pointer != len(text1):
4981+ raise ValueError(
4982+ "Delta length (%d) does not equal source text length (%d)." %
4983+ (pointer, len(text1)))
4984+ return diffs
4985+
4986+ # MATCH FUNCTIONS
4987+
4988+ def match_main(self, text, pattern, loc):
4989+ """Locate the best instance of 'pattern' in 'text' near 'loc'.
4990+
4991+ Args:
4992+ text: The text to search.
4993+ pattern: The pattern to search for.
4994+ loc: The location to search around.
4995+
4996+ Returns:
4997+ Best match index or -1.
4998+ """
4999+ # Check for null inputs.
5000+ if text == None or pattern == None:
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches