##// END OF EJS Templates
python3: removed from future imports
super-admin -
r4912:fba394fb default
parent child Browse files
Show More
@@ -1,106 +1,106 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Single source for redirection links.
23 23
24 24 Goal of this module is to provide a single source of truth regarding external
25 25 links. The data inside this module is used to configure the routing
26 26 system of Enterprise and it is used also as a base to check if this data
27 27 and our server configuration are in sync.
28 28
29 29 .. py:data:: link_config
30 30
31 31 Contains the configuration for external links. Each item is supposed to be
32 32 a `dict` like this example::
33 33
34 34 {"name": "url_name",
35 35 "target": "https://rhodecode.com/r1/enterprise/keyword/",
36 36 "external_target": "https://example.com/some-page.html",
37 37 }
38 38
39 39 then you can retrieve the url by simply calling the URL function:
40 40
41 41 `h.route_path('url_name')`
42 42
43 43 The redirection must be first implemented in our servers before
44 44 you can see it working.
45 45 """
46 46 # pragma: no cover
47 from __future__ import unicode_literals
47
48 48
49 49 link_config = [
50 50 {
51 51 "name": "enterprise_docs",
52 52 "target": "https://rhodecode.com/r1/enterprise/docs/",
53 53 "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/",
54 54 },
55 55 {
56 56 "name": "enterprise_log_file_locations",
57 57 "target": "https://rhodecode.com/r1/enterprise/docs/admin-system-overview/",
58 58 "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/admin/system-overview.html#log-files",
59 59 },
60 60 {
61 61 "name": "enterprise_issue_tracker_settings",
62 62 "target": "https://rhodecode.com/r1/enterprise/docs/issue-trackers-overview/",
63 63 "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/issue-trackers/issue-trackers.html",
64 64 },
65 65 {
66 66 "name": "enterprise_svn_setup",
67 67 "target": "https://rhodecode.com/r1/enterprise/docs/svn-setup/",
68 68 "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/admin/svn-http.html",
69 69 },
70 70 {
71 71 "name": "enterprise_license_convert_from_old",
72 72 "target": "https://rhodecode.com/r1/enterprise/convert-license/",
73 73 "external_target": "https://rhodecode.com/u/license-upgrade",
74 74 },
75 75 {
76 76 "name": "rst_help",
77 77 "target": "http://docutils.sourceforge.io/docs/user/rst/quickref.html",
78 78 "external_target": "https://docutils.sourceforge.io/docs/user/rst/quickref.html",
79 79 },
80 80 {
81 81 "name": "markdown_help",
82 82 "target": "https://daringfireball.net/projects/markdown/syntax",
83 83 "external_target": "https://daringfireball.net/projects/markdown/syntax",
84 84 },
85 85 {
86 86 "name": "rhodecode_official",
87 87 "target": "https://rhodecode.com",
88 88 "external_target": "https://rhodecode.com/",
89 89 },
90 90 {
91 91 "name": "rhodecode_support",
92 92 "target": "https://rhodecode.com/help/",
93 93 "external_target": "https://rhodecode.com/support",
94 94 },
95 95 {
96 96 "name": "rhodecode_translations",
97 97 "target": "https://rhodecode.com/translate/enterprise",
98 98 "external_target": "https://explore.transifex.com/rhodecode/RhodeCode/",
99 99 },
100 100
101 101 ]
102 102
103 103
104 104 def connect_redirection_links(config):
105 105 for link in link_config:
106 106 config.add_route(link['name'], link['target'], static=True)
@@ -1,329 +1,329 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 from __future__ import unicode_literals
21
22 22 import logging
23 23
24 24 import colander
25 25 import deform.widget
26 26 from mako.template import Template
27 27
28 28 from rhodecode import events
29 29 from rhodecode.model.validation_schema.widgets import CheckboxChoiceWidgetDesc
30 30 from rhodecode.translation import _
31 31 from rhodecode.lib.celerylib import run_task
32 32 from rhodecode.lib.celerylib import tasks
33 33 from rhodecode.integrations.types.base import (
34 34 IntegrationTypeBase, render_with_traceback)
35 35
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39 REPO_PUSH_TEMPLATE_PLAINTEXT = Template('''
40 40 Commits:
41 41
42 42 % for commit in data['push']['commits']:
43 43 ${commit['url']} by ${commit['author']} at ${commit['date']}
44 44 ${commit['message']}
45 45 ----
46 46
47 47 % endfor
48 48 ''')
49 49
50 50 REPO_PUSH_TEMPLATE_HTML = Template('''
51 51 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
52 52 <html xmlns="http://www.w3.org/1999/xhtml">
53 53 <head>
54 54 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
55 55 <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
56 56 <title>${subject}</title>
57 57 <style type="text/css">
58 58 /* Based on The MailChimp Reset INLINE: Yes. */
59 59 #outlook a {padding:0;} /* Force Outlook to provide a "view in browser" menu link. */
60 60 body{width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0;}
61 61 /* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/
62 62 .ExternalClass {width:100%;} /* Force Hotmail to display emails at full width */
63 63 .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;}
64 64 /* Forces Hotmail to display normal line spacing. More on that: http://www.emailonacid.com/forum/viewthread/43/ */
65 65 #backgroundTable {margin:0; padding:0; line-height: 100% !important;}
66 66 /* End reset */
67 67
68 68 /* defaults for images*/
69 69 img {outline:none; text-decoration:none; -ms-interpolation-mode: bicubic;}
70 70 a img {border:none;}
71 71 .image_fix {display:block;}
72 72
73 73 body {line-height:1.2em;}
74 74 p {margin: 0 0 20px;}
75 75 h1, h2, h3, h4, h5, h6 {color:#323232!important;}
76 76 a {color:#427cc9;text-decoration:none;outline:none;cursor:pointer;}
77 77 a:focus {outline:none;}
78 78 a:hover {color: #305b91;}
79 79 h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {color:#427cc9!important;text-decoration:none!important;}
80 80 h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {color: #305b91!important;}
81 81 h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {color: #305b91!important;}
82 82 table {font-size:13px;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;}
83 83 table td {padding:.65em 1em .65em 0;border-collapse:collapse;vertical-align:top;text-align:left;}
84 84 input {display:inline;border-radius:2px;border-style:solid;border: 1px solid #dbd9da;padding:.5em;}
85 85 input:focus {outline: 1px solid #979797}
86 86 @media only screen and (-webkit-min-device-pixel-ratio: 2) {
87 87 /* Put your iPhone 4g styles in here */
88 88 }
89 89
90 90 /* Android targeting */
91 91 @media only screen and (-webkit-device-pixel-ratio:.75){
92 92 /* Put CSS for low density (ldpi) Android layouts in here */
93 93 }
94 94 @media only screen and (-webkit-device-pixel-ratio:1){
95 95 /* Put CSS for medium density (mdpi) Android layouts in here */
96 96 }
97 97 @media only screen and (-webkit-device-pixel-ratio:1.5){
98 98 /* Put CSS for high density (hdpi) Android layouts in here */
99 99 }
100 100 /* end Android targeting */
101 101
102 102 </style>
103 103
104 104 <!-- Targeting Windows Mobile -->
105 105 <!--[if IEMobile 7]>
106 106 <style type="text/css">
107 107
108 108 </style>
109 109 <![endif]-->
110 110
111 111 <!--[if gte mso 9]>
112 112 <style>
113 113 /* Target Outlook 2007 and 2010 */
114 114 </style>
115 115 <![endif]-->
116 116 </head>
117 117 <body>
118 118 <!-- Wrapper/Container Table: Use a wrapper table to control the width and the background color consistently of your email. Use this approach instead of setting attributes on the body tag. -->
119 119 <table cellpadding="0" cellspacing="0" border="0" id="backgroundTable" align="left" style="margin:1%;width:97%;padding:0;font-family:sans-serif;font-weight:100;border:1px solid #dbd9da">
120 120 <tr>
121 121 <td valign="top" style="padding:0;">
122 122 <table cellpadding="0" cellspacing="0" border="0" align="left" width="100%">
123 123 <tr><td style="width:100%;padding:7px;background-color:#202020" valign="top">
124 124 <a style="color:#eeeeee;text-decoration:none;" href="${instance_url}">
125 125 ${'RhodeCode'}
126 126 </a>
127 127 </td></tr>
128 128 <tr>
129 129 <td style="padding:15px;" valign="top">
130 130 % if data['push']['commits']:
131 131 % for commit in data['push']['commits']:
132 132 <a href="${commit['url']}">${commit['short_id']}</a> by ${commit['author']} at ${commit['date']} <br/>
133 133 ${commit['message_html']} <br/>
134 134 <br/>
135 135 % endfor
136 136 % else:
137 137 No commit data
138 138 % endif
139 139 </td>
140 140 </tr>
141 141 </table>
142 142 </td>
143 143 </tr>
144 144 </table>
145 145 <!-- End of wrapper table -->
146 146 <p><a style="margin-top:15px;margin-left:1%;font-family:sans-serif;font-weight:100;font-size:11px;color:#666666;text-decoration:none;" href="${instance_url}">
147 147 ${'This is a notification from RhodeCode. %(instance_url)s' % {'instance_url': instance_url}}
148 148 </a></p>
149 149 </body>
150 150 </html>
151 151 ''')
152 152
153 153
154 154 class EmailSettingsSchema(colander.Schema):
155 155 @colander.instantiate(validator=colander.Length(min=1))
156 156 class recipients(colander.SequenceSchema):
157 157 title = _('Recipients')
158 158 description = _('Email addresses to send push events to')
159 159 widget = deform.widget.SequenceWidget(min_len=1)
160 160
161 161 recipient = colander.SchemaNode(
162 162 colander.String(),
163 163 title=_('Email address'),
164 164 description=_('Email address'),
165 165 default='',
166 166 validator=colander.Email(),
167 167 widget=deform.widget.TextInputWidget(
168 168 placeholder='user@domain.com',
169 169 ),
170 170 )
171 171
172 172
173 173 class EmailIntegrationType(IntegrationTypeBase):
174 174 key = 'email'
175 175 display_name = _('Email')
176 176 description = _('Send repo push summaries to a list of recipients via email')
177 177
178 178 valid_events = [
179 179 events.RepoPushEvent
180 180 ]
181 181
182 182 @classmethod
183 183 def icon(cls):
184 184 return '''
185 185 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
186 186 <svg
187 187 xmlns:dc="http://purl.org/dc/elements/1.1/"
188 188 xmlns:cc="http://creativecommons.org/ns#"
189 189 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
190 190 xmlns:svg="http://www.w3.org/2000/svg"
191 191 xmlns="http://www.w3.org/2000/svg"
192 192 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
193 193 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
194 194 viewBox="0 -256 1850 1850"
195 195 id="svg2989"
196 196 version="1.1"
197 197 inkscape:version="0.48.3.1 r9886"
198 198 width="100%"
199 199 height="100%"
200 200 sodipodi:docname="envelope_font_awesome.svg">
201 201 <metadata
202 202 id="metadata2999">
203 203 <rdf:RDF>
204 204 <cc:Work
205 205 rdf:about="">
206 206 <dc:format>image/svg+xml</dc:format>
207 207 <dc:type
208 208 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
209 209 </cc:Work>
210 210 </rdf:RDF>
211 211 </metadata>
212 212 <defs
213 213 id="defs2997" />
214 214 <sodipodi:namedview
215 215 pagecolor="#ffffff"
216 216 bordercolor="#666666"
217 217 borderopacity="1"
218 218 objecttolerance="10"
219 219 gridtolerance="10"
220 220 guidetolerance="10"
221 221 inkscape:pageopacity="0"
222 222 inkscape:pageshadow="2"
223 223 inkscape:window-width="640"
224 224 inkscape:window-height="480"
225 225 id="namedview2995"
226 226 showgrid="false"
227 227 inkscape:zoom="0.13169643"
228 228 inkscape:cx="896"
229 229 inkscape:cy="896"
230 230 inkscape:window-x="0"
231 231 inkscape:window-y="25"
232 232 inkscape:window-maximized="0"
233 233 inkscape:current-layer="svg2989" />
234 234 <g
235 235 transform="matrix(1,0,0,-1,37.966102,1282.678)"
236 236 id="g2991">
237 237 <path
238 238 d="m 1664,32 v 768 q -32,-36 -69,-66 -268,-206 -426,-338 -51,-43 -83,-67 -32,-24 -86.5,-48.5 Q 945,256 897,256 h -1 -1 Q 847,256 792.5,280.5 738,305 706,329 674,353 623,396 465,528 197,734 160,764 128,800 V 32 Q 128,19 137.5,9.5 147,0 160,0 h 1472 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 0,1051 v 11 13.5 q 0,0 -0.5,13 -0.5,13 -3,12.5 -2.5,-0.5 -5.5,9 -3,9.5 -9,7.5 -6,-2 -14,2.5 H 160 q -13,0 -22.5,-9.5 Q 128,1133 128,1120 128,952 275,836 468,684 676,519 682,514 711,489.5 740,465 757,452 774,439 801.5,420.5 829,402 852,393 q 23,-9 43,-9 h 1 1 q 20,0 43,9 23,9 50.5,27.5 27.5,18.5 44.5,31.5 17,13 46,37.5 29,24.5 35,29.5 208,165 401,317 54,43 100.5,115.5 46.5,72.5 46.5,131.5 z m 128,37 V 32 q 0,-66 -47,-113 -47,-47 -113,-47 H 160 Q 94,-128 47,-81 0,-34 0,32 v 1088 q 0,66 47,113 47,47 113,47 h 1472 q 66,0 113,-47 47,-47 47,-113 z"
239 239 id="path2993"
240 240 inkscape:connector-curvature="0"
241 241 style="fill:currentColor" />
242 242 </g>
243 243 </svg>
244 244 '''
245 245
246 246 def settings_schema(self):
247 247 schema = EmailSettingsSchema()
248 248 schema.add(colander.SchemaNode(
249 249 colander.Set(),
250 250 widget=CheckboxChoiceWidgetDesc(
251 251 values=sorted(
252 252 [(e.name, e.display_name, e.description) for e in self.valid_events]
253 253 ),
254 254 ),
255 255 description="List of events activated for this integration",
256 256 name='events'
257 257 ))
258 258 return schema
259 259
260 260 def send_event(self, event):
261 261 log.debug('handling event %s with integration %s', event.name, self)
262 262
263 263 if event.__class__ not in self.valid_events:
264 264 log.debug('event %r not present in valid event list (%s)', event, self.valid_events)
265 265 return
266 266
267 267 if not self.event_enabled(event):
268 268 # NOTE(marcink): for legacy reasons we're skipping this check...
269 269 # since the email event haven't had any settings...
270 270 pass
271 271
272 272 handler = EmailEventHandler(self.settings)
273 273 handler(event, event_data=event.as_dict())
274 274
275 275
276 276 class EmailEventHandler(object):
277 277 def __init__(self, integration_settings):
278 278 self.integration_settings = integration_settings
279 279
280 280 def __call__(self, event, event_data):
281 281 if isinstance(event, events.RepoPushEvent):
282 282 self.repo_push_handler(event, event_data)
283 283 else:
284 284 log.debug('ignoring event: %r', event)
285 285
286 286 def repo_push_handler(self, event, data):
287 287 commit_num = len(data['push']['commits'])
288 288 server_url = data['server_url']
289 289
290 290 if commit_num == 1:
291 291 if data['push']['branches']:
292 292 _subject = '[{repo_name}] {author} pushed {commit_num} commit on branches: {branches}'
293 293 else:
294 294 _subject = '[{repo_name}] {author} pushed {commit_num} commit'
295 295 subject = _subject.format(
296 296 author=data['actor']['username'],
297 297 repo_name=data['repo']['repo_name'],
298 298 commit_num=commit_num,
299 299 branches=', '.join(
300 300 branch['name'] for branch in data['push']['branches'])
301 301 )
302 302 else:
303 303 if data['push']['branches']:
304 304 _subject = '[{repo_name}] {author} pushed {commit_num} commits on branches: {branches}'
305 305 else:
306 306 _subject = '[{repo_name}] {author} pushed {commit_num} commits'
307 307 subject = _subject.format(
308 308 author=data['actor']['username'],
309 309 repo_name=data['repo']['repo_name'],
310 310 commit_num=commit_num,
311 311 branches=', '.join(
312 312 branch['name'] for branch in data['push']['branches']))
313 313
314 314 email_body_plaintext = render_with_traceback(
315 315 REPO_PUSH_TEMPLATE_PLAINTEXT,
316 316 data=data,
317 317 subject=subject,
318 318 instance_url=server_url)
319 319
320 320 email_body_html = render_with_traceback(
321 321 REPO_PUSH_TEMPLATE_HTML,
322 322 data=data,
323 323 subject=subject,
324 324 instance_url=server_url)
325 325
326 326 recipients = self.integration_settings['recipients']
327 327 for email_address in recipients:
328 328 run_task(tasks.send_email, email_address, subject,
329 329 email_body_plaintext, email_body_html)
@@ -1,253 +1,253 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 from __future__ import unicode_literals
21
22 22 import deform
23 23 import logging
24 24 import requests
25 25 import colander
26 26 import textwrap
27 27 from mako.template import Template
28 28 from rhodecode import events
29 29 from rhodecode.model.validation_schema.widgets import CheckboxChoiceWidgetDesc
30 30 from rhodecode.translation import _
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
33 33 from rhodecode.lib.colander_utils import strip_whitespace
34 34 from rhodecode.integrations.types.base import (
35 35 IntegrationTypeBase, CommitParsingDataHandler, render_with_traceback,
36 36 requests_retry_call)
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40 REPO_PUSH_TEMPLATE = Template('''
41 41 <b>${data['actor']['username']}</b> pushed to repo <a href="${data['repo']['url']}">${data['repo']['repo_name']}</a>:
42 42 <br>
43 43 <ul>
44 44 %for branch, branch_commits in branches_commits.items():
45 45 <li>
46 46 % if branch:
47 47 <a href="${branch_commits['branch']['url']}">branch: ${branch_commits['branch']['name']}</a>
48 48 % else:
49 49 to trunk
50 50 % endif
51 51 <ul>
52 52 % for commit in branch_commits['commits']:
53 53 <li><a href="${commit['url']}">${commit['short_id']}</a> - ${commit['message_html']}</li>
54 54 % endfor
55 55 </ul>
56 56 </li>
57 57 %endfor
58 58 ''')
59 59
60 60
61 61 class HipchatSettingsSchema(colander.Schema):
62 62 color_choices = [
63 63 ('yellow', _('Yellow')),
64 64 ('red', _('Red')),
65 65 ('green', _('Green')),
66 66 ('purple', _('Purple')),
67 67 ('gray', _('Gray')),
68 68 ]
69 69
70 70 server_url = colander.SchemaNode(
71 71 colander.String(),
72 72 title=_('Hipchat server URL'),
73 73 description=_('Hipchat integration url.'),
74 74 default='',
75 75 preparer=strip_whitespace,
76 76 validator=colander.url,
77 77 widget=deform.widget.TextInputWidget(
78 78 placeholder='https://?.hipchat.com/v2/room/?/notification?auth_token=?',
79 79 ),
80 80 )
81 81 notify = colander.SchemaNode(
82 82 colander.Bool(),
83 83 title=_('Notify'),
84 84 description=_('Make a notification to the users in room.'),
85 85 missing=False,
86 86 default=False,
87 87 )
88 88 color = colander.SchemaNode(
89 89 colander.String(),
90 90 title=_('Color'),
91 91 description=_('Background color of message.'),
92 92 missing='',
93 93 validator=colander.OneOf([x[0] for x in color_choices]),
94 94 widget=deform.widget.Select2Widget(
95 95 values=color_choices,
96 96 ),
97 97 )
98 98
99 99
100 100 class HipchatIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
101 101 key = 'hipchat'
102 102 display_name = _('Hipchat')
103 103 description = _('Send events such as repo pushes and pull requests to '
104 104 'your hipchat channel.')
105 105
106 106 @classmethod
107 107 def icon(cls):
108 108 return '''<?xml version="1.0" encoding="utf-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve"><g><g transform="translate(0.000000,511.000000) scale(0.100000,-0.100000)"><path fill="#205281" d="M4197.1,4662.4c-1661.5-260.4-3018-1171.6-3682.6-2473.3C219.9,1613.6,100,1120.3,100,462.6c0-1014,376.8-1918.4,1127-2699.4C2326.7-3377.6,3878.5-3898.3,5701-3730.5l486.5,44.5l208.9-123.3c637.2-373.4,1551.8-640.6,2240.4-650.9c304.9-6.9,335.7,0,417.9,75.4c185,174.7,147.3,411.1-89.1,548.1c-315.2,181.6-620,544.7-733.1,870.1l-51.4,157.6l472.7,472.7c349.4,349.4,520.7,551.5,657.7,774.2c784.5,1281.2,784.5,2788.5,0,4052.6c-236.4,376.8-794.8,966-1178.4,1236.7c-572.1,407.7-1264.1,709.1-1993.7,870.1c-267.2,58.2-479.6,75.4-1038,82.2C4714.4,4686.4,4310.2,4679.6,4197.1,4662.4z M5947.6,3740.9c1856.7-380.3,3127.6-1709.4,3127.6-3275c0-1000.3-534.4-1949.2-1466.2-2600.1c-188.4-133.6-287.8-226.1-301.5-284.4c-41.1-157.6,263.8-938.6,397.4-1020.8c20.5-10.3,34.3-44.5,34.3-75.4c0-167.8-811.9,195.3-1363.4,609.8l-181.6,137l-332.3-58.2c-445.3-78.8-1281.2-78.8-1702.6,0C2796-2569.2,1734.1-1832.6,1220.2-801.5C983.8-318.5,905,51.5,929,613.3c27.4,640.6,243.2,1192.1,685.1,1740.3c620,770.8,1661.5,1305.2,2822.8,1452.5C4806.9,3854,5553.7,3819.7,5947.6,3740.9z"/><path fill="#205281" d="M2381.5-345.9c-75.4-106.2-68.5-167.8,34.3-322c332.3-500.2,1010.6-928.4,1760.8-1120.2c417.9-106.2,1226.4-106.2,1644.3,0c712.5,181.6,1270.9,517.3,1685.4,1014C7681-561.7,7715.3-424.7,7616-325.4c-89.1,89.1-167.9,65.1-431.7-133.6c-835.8-630.3-2028-856.4-3086.5-585.8C3683.3-938.6,3142-685,2830.3-448.7C2576.8-253.4,2463.7-229.4,2381.5-345.9z"/></g></g><!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon --></svg>'''
109 109
110 110 valid_events = [
111 111 events.PullRequestCloseEvent,
112 112 events.PullRequestMergeEvent,
113 113 events.PullRequestUpdateEvent,
114 114 events.PullRequestCommentEvent,
115 115 events.PullRequestReviewEvent,
116 116 events.PullRequestCreateEvent,
117 117 events.RepoPushEvent,
118 118 events.RepoCreateEvent,
119 119 ]
120 120
121 121 def send_event(self, event):
122 122 if event.__class__ not in self.valid_events:
123 123 log.debug('event %r not present in valid event list (%s)', event, self.valid_events)
124 124 return
125 125
126 126 if not self.event_enabled(event):
127 127 return
128 128
129 129 data = event.as_dict()
130 130
131 131 text = '<b>%s<b> caused a <b>%s</b> event' % (
132 132 data['actor']['username'], event.name)
133 133
134 134 if isinstance(event, events.PullRequestCommentEvent):
135 135 text = self.format_pull_request_comment_event(event, data)
136 136 elif isinstance(event, events.PullRequestCommentEditEvent):
137 137 text = self.format_pull_request_comment_event(event, data)
138 138 elif isinstance(event, events.PullRequestReviewEvent):
139 139 text = self.format_pull_request_review_event(event, data)
140 140 elif isinstance(event, events.PullRequestEvent):
141 141 text = self.format_pull_request_event(event, data)
142 142 elif isinstance(event, events.RepoPushEvent):
143 143 text = self.format_repo_push_event(data)
144 144 elif isinstance(event, events.RepoCreateEvent):
145 145 text = self.format_repo_create_event(data)
146 146 else:
147 147 log.error('unhandled event type: %r', event)
148 148
149 149 run_task(post_text_to_hipchat, self.settings, text)
150 150
151 151 def settings_schema(self):
152 152 schema = HipchatSettingsSchema()
153 153 schema.add(colander.SchemaNode(
154 154 colander.Set(),
155 155 widget=CheckboxChoiceWidgetDesc(
156 156 values=sorted(
157 157 [(e.name, e.display_name, e.description) for e in self.valid_events]
158 158 ),
159 159 ),
160 160 description="List of events activated for this integration",
161 161 name='events'
162 162 ))
163 163
164 164 return schema
165 165
166 166 def format_pull_request_comment_event(self, event, data):
167 167 comment_text = data['comment']['text']
168 168 if len(comment_text) > 200:
169 169 comment_text = '{comment_text}<a href="{comment_url}">...<a/>'.format(
170 170 comment_text=h.html_escape(comment_text[:200]),
171 171 comment_url=data['comment']['url'],
172 172 )
173 173
174 174 comment_status = ''
175 175 if data['comment']['status']:
176 176 comment_status = '[{}]: '.format(data['comment']['status'])
177 177
178 178 return (textwrap.dedent(
179 179 '''
180 180 {user} commented on pull request <a href="{pr_url}">{number}</a> - {pr_title}:
181 181 >>> {comment_status}{comment_text}
182 182 ''').format(
183 183 comment_status=comment_status,
184 184 user=data['actor']['username'],
185 185 number=data['pullrequest']['pull_request_id'],
186 186 pr_url=data['pullrequest']['url'],
187 187 pr_status=data['pullrequest']['status'],
188 188 pr_title=h.html_escape(data['pullrequest']['title']),
189 189 comment_text=h.html_escape(comment_text)
190 190 )
191 191 )
192 192
193 193 def format_pull_request_review_event(self, event, data):
194 194 return (textwrap.dedent(
195 195 '''
196 196 Status changed to {pr_status} for pull request <a href="{pr_url}">#{number}</a> - {pr_title}
197 197 ''').format(
198 198 user=data['actor']['username'],
199 199 number=data['pullrequest']['pull_request_id'],
200 200 pr_url=data['pullrequest']['url'],
201 201 pr_status=data['pullrequest']['status'],
202 202 pr_title=h.html_escape(data['pullrequest']['title']),
203 203 )
204 204 )
205 205
206 206 def format_pull_request_event(self, event, data):
207 207 action = {
208 208 events.PullRequestCloseEvent: 'closed',
209 209 events.PullRequestMergeEvent: 'merged',
210 210 events.PullRequestUpdateEvent: 'updated',
211 211 events.PullRequestCreateEvent: 'created',
212 212 }.get(event.__class__, str(event.__class__))
213 213
214 214 return ('Pull request <a href="{url}">#{number}</a> - {title} '
215 215 '{action} by <b>{user}</b>').format(
216 216 user=data['actor']['username'],
217 217 number=data['pullrequest']['pull_request_id'],
218 218 url=data['pullrequest']['url'],
219 219 title=h.html_escape(data['pullrequest']['title']),
220 220 action=action
221 221 )
222 222
223 223 def format_repo_push_event(self, data):
224 224 branches_commits = self.aggregate_branch_data(
225 225 data['push']['branches'], data['push']['commits'])
226 226
227 227 result = render_with_traceback(
228 228 REPO_PUSH_TEMPLATE,
229 229 data=data,
230 230 branches_commits=branches_commits,
231 231 )
232 232 return result
233 233
234 234 def format_repo_create_event(self, data):
235 235 return '<a href="{}">{}</a> ({}) repository created by <b>{}</b>'.format(
236 236 data['repo']['url'],
237 237 h.html_escape(data['repo']['repo_name']),
238 238 data['repo']['repo_type'],
239 239 data['actor']['username'],
240 240 )
241 241
242 242
243 243 @async_task(ignore_result=True, base=RequestContextTask)
244 244 def post_text_to_hipchat(settings, text):
245 245 log.debug('sending %s to hipchat %s', text, settings['server_url'])
246 246 json_message = {
247 247 "message": text,
248 248 "color": settings.get('color', 'yellow'),
249 249 "notify": settings.get('notify', False),
250 250 }
251 251 req_session = requests_retry_call()
252 252 resp = req_session.post(settings['server_url'], json=json_message, timeout=60)
253 253 resp.raise_for_status() # raise exception on a failed request
@@ -1,354 +1,354 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 from __future__ import unicode_literals
21
22 22 import re
23 23 import time
24 24 import textwrap
25 25 import logging
26 26
27 27 import deform
28 28 import requests
29 29 import colander
30 30 from mako.template import Template
31 31
32 32 from rhodecode import events
33 33 from rhodecode.model.validation_schema.widgets import CheckboxChoiceWidgetDesc
34 34 from rhodecode.translation import _
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
37 37 from rhodecode.lib.colander_utils import strip_whitespace
38 38 from rhodecode.integrations.types.base import (
39 39 IntegrationTypeBase, CommitParsingDataHandler, render_with_traceback,
40 40 requests_retry_call)
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 def html_to_slack_links(message):
46 46 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
47 47 r'<\1|\2>', message)
48 48
49 49
50 50 REPO_PUSH_TEMPLATE = Template('''
51 51 <%
52 52 def branch_text(branch):
53 53 if branch:
54 54 return 'on branch: <{}|{}>'.format(branch_commits['branch']['url'], branch_commits['branch']['name'])
55 55 else:
56 56 ## case for SVN no branch push...
57 57 return 'to trunk'
58 58 %> \
59 59
60 60 % for branch, branch_commits in branches_commits.items():
61 61 ${len(branch_commits['commits'])} ${'commit' if len(branch_commits['commits']) == 1 else 'commits'} ${branch_text(branch)}
62 62 % for commit in branch_commits['commits']:
63 63 `<${commit['url']}|${commit['short_id']}>` - ${commit['message_html']|html_to_slack_links}
64 64 % endfor
65 65 % endfor
66 66 ''')
67 67
68 68
69 69 class SlackSettingsSchema(colander.Schema):
70 70 service = colander.SchemaNode(
71 71 colander.String(),
72 72 title=_('Slack service URL'),
73 73 description=h.literal(_(
74 74 'This can be setup at the '
75 75 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
76 76 'slack app manager</a>')),
77 77 default='',
78 78 preparer=strip_whitespace,
79 79 validator=colander.url,
80 80 widget=deform.widget.TextInputWidget(
81 81 placeholder='https://hooks.slack.com/services/...',
82 82 ),
83 83 )
84 84 username = colander.SchemaNode(
85 85 colander.String(),
86 86 title=_('Username'),
87 87 description=_('Username to show notifications coming from.'),
88 88 missing='Rhodecode',
89 89 preparer=strip_whitespace,
90 90 widget=deform.widget.TextInputWidget(
91 91 placeholder='Rhodecode'
92 92 ),
93 93 )
94 94 channel = colander.SchemaNode(
95 95 colander.String(),
96 96 title=_('Channel'),
97 97 description=_('Channel to send notifications to.'),
98 98 missing='',
99 99 preparer=strip_whitespace,
100 100 widget=deform.widget.TextInputWidget(
101 101 placeholder='#general'
102 102 ),
103 103 )
104 104 icon_emoji = colander.SchemaNode(
105 105 colander.String(),
106 106 title=_('Emoji'),
107 107 description=_('Emoji to use eg. :studio_microphone:'),
108 108 missing='',
109 109 preparer=strip_whitespace,
110 110 widget=deform.widget.TextInputWidget(
111 111 placeholder=':studio_microphone:'
112 112 ),
113 113 )
114 114
115 115
116 116 class SlackIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
117 117 key = 'slack'
118 118 display_name = _('Slack')
119 119 description = _('Send events such as repo pushes and pull requests to '
120 120 'your slack channel.')
121 121
122 122 @classmethod
123 123 def icon(cls):
124 124 return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M165.963541,15.8384262 C162.07318,3.86308197 149.212328,-2.69009836 137.239082,1.20236066 C125.263738,5.09272131 118.710557,17.9535738 122.603016,29.9268197 L181.550164,211.292328 C185.597902,222.478689 197.682361,228.765377 209.282098,225.426885 C221.381246,221.943607 228.756984,209.093246 224.896,197.21023 C224.749115,196.756984 165.963541,15.8384262 165.963541,15.8384262" fill="#DFA22F"></path><path d="M74.6260984,45.515541 C70.7336393,33.5422951 57.8727869,26.9891148 45.899541,30.8794754 C33.9241967,34.7698361 27.3710164,47.6306885 31.2634754,59.6060328 L90.210623,240.971541 C94.2583607,252.157902 106.34282,258.44459 117.942557,255.104 C130.041705,251.62282 137.417443,238.772459 133.556459,226.887344 C133.409574,226.436197 74.6260984,45.515541 74.6260984,45.515541" fill="#3CB187"></path><path d="M240.161574,166.045377 C252.136918,162.155016 258.688,149.294164 254.797639,137.31882 C250.907279,125.345574 238.046426,118.792393 226.07318,122.682754 L44.7076721,181.632 C33.5213115,185.677639 27.234623,197.762098 30.5731148,209.361836 C34.0563934,221.460984 46.9067541,228.836721 58.7897705,224.975738 C59.2430164,224.828852 240.161574,166.045377 240.161574,166.045377" fill="#CE1E5B"></path><path d="M82.507541,217.270557 C94.312918,213.434754 109.528131,208.491016 125.855475,203.186361 C122.019672,191.380984 117.075934,176.163672 111.76918,159.83423 L68.4191475,173.924721 L82.507541,217.270557" fill="#392538"></path><path d="M173.847082,187.591344 C190.235279,182.267803 205.467279,177.31777 217.195016,173.507148 C213.359213,161.70177 208.413377,146.480262 203.106623,130.146623 L159.75659,144.237115 L173.847082,187.591344" fill="#BB242A"></path><path d="M210.484459,74.7058361 C222.457705,70.8154754 229.010885,57.954623 225.120525,45.9792787 C221.230164,34.0060328 208.369311,27.4528525 196.393967,31.3432131 L15.028459,90.292459 C3.84209836,94.3380984 -2.44459016,106.422557 0.896,118.022295 C4.37718033,130.121443 17.227541,137.49718 29.1126557,133.636197 C29.5638033,133.489311 210.484459,74.7058361 210.484459,74.7058361" fill="#72C5CD"></path><path d="M52.8220328,125.933115 C64.6274098,122.097311 79.8468197,117.151475 96.1762623,111.84682 C90.8527213,95.4565246 85.9026885,80.2245246 82.0920656,68.4946885 L38.731541,82.5872787 L52.8220328,125.933115" fill="#248C73"></path><path d="M144.159475,96.256 C160.551869,90.9303607 175.785967,85.9803279 187.515803,82.1676066 C182.190164,65.7752131 177.240131,50.5390164 173.42741,38.807082 L130.068984,52.8996721 L144.159475,96.256" fill="#62803A"></path></g></svg>'''
125 125
126 126 valid_events = [
127 127 events.PullRequestCloseEvent,
128 128 events.PullRequestMergeEvent,
129 129 events.PullRequestUpdateEvent,
130 130 events.PullRequestCommentEvent,
131 131 events.PullRequestReviewEvent,
132 132 events.PullRequestCreateEvent,
133 133 events.RepoPushEvent,
134 134 events.RepoCreateEvent,
135 135 ]
136 136
137 137 def send_event(self, event):
138 138 log.debug('handling event %s with integration %s', event.name, self)
139 139
140 140 if event.__class__ not in self.valid_events:
141 141 log.debug('event %r not present in valid event list (%s)', event, self.valid_events)
142 142 return
143 143
144 144 if not self.event_enabled(event):
145 145 return
146 146
147 147 data = event.as_dict()
148 148
149 149 # defaults
150 150 title = '*%s* caused a *%s* event' % (
151 151 data['actor']['username'], event.name)
152 152 text = '*%s* caused a *%s* event' % (
153 153 data['actor']['username'], event.name)
154 154 fields = None
155 155 overrides = None
156 156
157 157 if isinstance(event, events.PullRequestCommentEvent):
158 158 (title, text, fields, overrides) \
159 159 = self.format_pull_request_comment_event(event, data)
160 160 elif isinstance(event, events.PullRequestCommentEditEvent):
161 161 (title, text, fields, overrides) \
162 162 = self.format_pull_request_comment_event(event, data)
163 163 elif isinstance(event, events.PullRequestReviewEvent):
164 164 title, text = self.format_pull_request_review_event(event, data)
165 165 elif isinstance(event, events.PullRequestEvent):
166 166 title, text = self.format_pull_request_event(event, data)
167 167 elif isinstance(event, events.RepoPushEvent):
168 168 title, text = self.format_repo_push_event(data)
169 169 elif isinstance(event, events.RepoCreateEvent):
170 170 title, text = self.format_repo_create_event(data)
171 171 else:
172 172 log.error('unhandled event type: %r', event)
173 173
174 174 run_task(post_text_to_slack, self.settings, title, text, fields, overrides)
175 175
176 176 def settings_schema(self):
177 177 schema = SlackSettingsSchema()
178 178 schema.add(colander.SchemaNode(
179 179 colander.Set(),
180 180 widget=CheckboxChoiceWidgetDesc(
181 181 values=sorted(
182 182 [(e.name, e.display_name, e.description) for e in self.valid_events]
183 183 ),
184 184 ),
185 185 description="List of events activated for this integration",
186 186 name='events'
187 187 ))
188 188
189 189 return schema
190 190
191 191 def format_pull_request_comment_event(self, event, data):
192 192 comment_text = data['comment']['text']
193 193 if len(comment_text) > 200:
194 194 comment_text = '<{comment_url}|{comment_text}...>'.format(
195 195 comment_text=comment_text[:200],
196 196 comment_url=data['comment']['url'],
197 197 )
198 198
199 199 fields = None
200 200 overrides = None
201 201 status_text = None
202 202
203 203 if data['comment']['status']:
204 204 status_color = {
205 205 'approved': '#0ac878',
206 206 'rejected': '#e85e4d'}.get(data['comment']['status'])
207 207
208 208 if status_color:
209 209 overrides = {"color": status_color}
210 210
211 211 status_text = data['comment']['status']
212 212
213 213 if data['comment']['file']:
214 214 fields = [
215 215 {
216 216 "title": "file",
217 217 "value": data['comment']['file']
218 218 },
219 219 {
220 220 "title": "line",
221 221 "value": data['comment']['line']
222 222 }
223 223 ]
224 224
225 225 template = Template(textwrap.dedent(r'''
226 226 *${data['actor']['username']}* left ${data['comment']['type']} on pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
227 227 '''))
228 228 title = render_with_traceback(
229 229 template, data=data, comment=event.comment)
230 230
231 231 template = Template(textwrap.dedent(r'''
232 232 *pull request title*: ${pr_title}
233 233 % if status_text:
234 234 *submitted status*: `${status_text}`
235 235 % endif
236 236 >>> ${comment_text}
237 237 '''))
238 238 text = render_with_traceback(
239 239 template,
240 240 comment_text=comment_text,
241 241 pr_title=data['pullrequest']['title'],
242 242 status_text=status_text)
243 243
244 244 return title, text, fields, overrides
245 245
246 246 def format_pull_request_review_event(self, event, data):
247 247 template = Template(textwrap.dedent(r'''
248 248 *${data['actor']['username']}* changed status of pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']} to `${data['pullrequest']['status']}`>:
249 249 '''))
250 250 title = render_with_traceback(template, data=data)
251 251
252 252 template = Template(textwrap.dedent(r'''
253 253 *pull request title*: ${pr_title}
254 254 '''))
255 255 text = render_with_traceback(
256 256 template,
257 257 pr_title=data['pullrequest']['title'])
258 258
259 259 return title, text
260 260
261 261 def format_pull_request_event(self, event, data):
262 262 action = {
263 263 events.PullRequestCloseEvent: 'closed',
264 264 events.PullRequestMergeEvent: 'merged',
265 265 events.PullRequestUpdateEvent: 'updated',
266 266 events.PullRequestCreateEvent: 'created',
267 267 }.get(event.__class__, str(event.__class__))
268 268
269 269 template = Template(textwrap.dedent(r'''
270 270 *${data['actor']['username']}* `${action}` pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
271 271 '''))
272 272 title = render_with_traceback(template, data=data, action=action)
273 273
274 274 template = Template(textwrap.dedent(r'''
275 275 *pull request title*: ${pr_title}
276 276 %if data['pullrequest']['commits']:
277 277 *commits*: ${len(data['pullrequest']['commits'])}
278 278 %endif
279 279 '''))
280 280 text = render_with_traceback(
281 281 template,
282 282 pr_title=data['pullrequest']['title'],
283 283 data=data)
284 284
285 285 return title, text
286 286
287 287 def format_repo_push_event(self, data):
288 288 branches_commits = self.aggregate_branch_data(
289 289 data['push']['branches'], data['push']['commits'])
290 290
291 291 template = Template(r'''
292 292 *${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>:
293 293 ''')
294 294 title = render_with_traceback(template, data=data)
295 295
296 296 text = render_with_traceback(
297 297 REPO_PUSH_TEMPLATE,
298 298 data=data,
299 299 branches_commits=branches_commits,
300 300 html_to_slack_links=html_to_slack_links,
301 301 )
302 302
303 303 return title, text
304 304
305 305 def format_repo_create_event(self, data):
306 306 template = Template(r'''
307 307 *${data['actor']['username']}* created new repository ${data['repo']['repo_name']}:
308 308 ''')
309 309 title = render_with_traceback(template, data=data)
310 310
311 311 template = Template(textwrap.dedent(r'''
312 312 repo_url: ${data['repo']['url']}
313 313 repo_type: ${data['repo']['repo_type']}
314 314 '''))
315 315 text = render_with_traceback(template, data=data)
316 316
317 317 return title, text
318 318
319 319
320 320 @async_task(ignore_result=True, base=RequestContextTask)
321 321 def post_text_to_slack(settings, title, text, fields=None, overrides=None):
322 322 log.debug('sending %s (%s) to slack %s', title, text, settings['service'])
323 323
324 324 fields = fields or []
325 325 overrides = overrides or {}
326 326
327 327 message_data = {
328 328 "fallback": text,
329 329 "color": "#427cc9",
330 330 "pretext": title,
331 331 #"author_name": "Bobby Tables",
332 332 #"author_link": "http://flickr.com/bobby/",
333 333 #"author_icon": "http://flickr.com/icons/bobby.jpg",
334 334 #"title": "Slack API Documentation",
335 335 #"title_link": "https://api.slack.com/",
336 336 "text": text,
337 337 "fields": fields,
338 338 #"image_url": "http://my-website.com/path/to/image.jpg",
339 339 #"thumb_url": "http://example.com/path/to/thumb.png",
340 340 "footer": "RhodeCode",
341 341 #"footer_icon": "",
342 342 "ts": time.time(),
343 343 "mrkdwn_in": ["pretext", "text"]
344 344 }
345 345 message_data.update(overrides)
346 346 json_message = {
347 347 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:'),
348 348 "channel": settings.get('channel', ''),
349 349 "username": settings.get('username', 'Rhodecode'),
350 350 "attachments": [message_data]
351 351 }
352 352 req_session = requests_retry_call()
353 353 resp = req_session.post(settings['service'], json=json_message, timeout=60)
354 354 resp.raise_for_status() # raise exception on a failed request
@@ -1,266 +1,266 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 from __future__ import unicode_literals
21
22 22
23 23 import deform.widget
24 24 import logging
25 25 import colander
26 26
27 27 import rhodecode
28 28 from rhodecode import events
29 29 from rhodecode.lib.colander_utils import strip_whitespace
30 30 from rhodecode.model.validation_schema.widgets import CheckboxChoiceWidgetDesc
31 31 from rhodecode.translation import _
32 32 from rhodecode.integrations.types.base import (
33 33 IntegrationTypeBase, get_auth, get_web_token, get_url_vars,
34 34 WebhookDataHandler, WEBHOOK_URL_VARS, requests_retry_call)
35 35 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
36 36 from rhodecode.model.validation_schema import widgets
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 # updating this required to update the `common_vars` passed in url calling func
42 42
43 43 URL_VARS = get_url_vars(WEBHOOK_URL_VARS)
44 44
45 45
46 46 class WebhookSettingsSchema(colander.Schema):
47 47 url = colander.SchemaNode(
48 48 colander.String(),
49 49 title=_('Webhook URL'),
50 50 description=
51 51 _('URL to which Webhook should submit data. If used some of the '
52 52 'variables would trigger multiple calls, like ${branch} or '
53 53 '${commit_id}. Webhook will be called as many times as unique '
54 54 'objects in data in such cases.'),
55 55 missing=colander.required,
56 56 required=True,
57 57 preparer=strip_whitespace,
58 58 validator=colander.url,
59 59 widget=widgets.CodeMirrorWidget(
60 60 help_block_collapsable_name='Show url variables',
61 61 help_block_collapsable=(
62 62 'E.g http://my-serv.com/trigger_job/${{event_name}}'
63 63 '?PR_ID=${{pull_request_id}}'
64 64 '\nFull list of vars:\n{}'.format(URL_VARS)),
65 65 codemirror_mode='text',
66 66 codemirror_options='{"lineNumbers": false, "lineWrapping": true}'),
67 67 )
68 68 secret_token = colander.SchemaNode(
69 69 colander.String(),
70 70 title=_('Secret Token'),
71 71 description=_('Optional string used to validate received payloads. '
72 72 'It will be sent together with event data in JSON'),
73 73 default='',
74 74 missing='',
75 75 widget=deform.widget.TextInputWidget(
76 76 placeholder='e.g. secret_token'
77 77 ),
78 78 )
79 79 username = colander.SchemaNode(
80 80 colander.String(),
81 81 title=_('Username'),
82 82 description=_('Optional username to authenticate the call.'),
83 83 default='',
84 84 missing='',
85 85 widget=deform.widget.TextInputWidget(
86 86 placeholder='e.g. admin'
87 87 ),
88 88 )
89 89 password = colander.SchemaNode(
90 90 colander.String(),
91 91 title=_('Password'),
92 92 description=_('Optional password to authenticate the call.'),
93 93 default='',
94 94 missing='',
95 95 widget=deform.widget.PasswordWidget(
96 96 placeholder='e.g. secret.',
97 97 redisplay=True,
98 98 ),
99 99 )
100 100 custom_header_key = colander.SchemaNode(
101 101 colander.String(),
102 102 title=_('Custom Header Key'),
103 103 description=_('Custom Header name to be set when calling endpoint.'),
104 104 default='',
105 105 missing='',
106 106 widget=deform.widget.TextInputWidget(
107 107 placeholder='e.g: Authorization'
108 108 ),
109 109 )
110 110 custom_header_val = colander.SchemaNode(
111 111 colander.String(),
112 112 title=_('Custom Header Value'),
113 113 description=_('Custom Header value to be set when calling endpoint.'),
114 114 default='',
115 115 missing='',
116 116 widget=deform.widget.TextInputWidget(
117 117 placeholder='e.g. Basic XxXxXx'
118 118 ),
119 119 )
120 120 method_type = colander.SchemaNode(
121 121 colander.String(),
122 122 title=_('Call Method'),
123 123 description=_('Select a HTTP method to use when calling the Webhook.'),
124 124 default='post',
125 125 missing='',
126 126 widget=deform.widget.RadioChoiceWidget(
127 127 values=[('get', 'GET'), ('post', 'POST'), ('put', 'PUT')],
128 128 inline=True
129 129 ),
130 130 )
131 131
132 132
133 133 class WebhookIntegrationType(IntegrationTypeBase):
134 134 key = 'webhook'
135 135 display_name = _('Webhook')
136 136 description = _('send JSON data to a url endpoint')
137 137
138 138 @classmethod
139 139 def icon(cls):
140 140 return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 239" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M119.540432,100.502743 C108.930124,118.338815 98.7646301,135.611455 88.3876025,152.753617 C85.7226696,157.154315 84.4040417,160.738531 86.5332204,166.333309 C92.4107024,181.787152 84.1193605,196.825836 68.5350381,200.908244 C53.8383677,204.759349 39.5192953,195.099955 36.6032893,179.365384 C34.0194114,165.437749 44.8274148,151.78491 60.1824106,149.608284 C61.4694072,149.424428 62.7821041,149.402681 64.944891,149.240571 C72.469175,136.623655 80.1773157,123.700312 88.3025935,110.073173 C73.611854,95.4654658 64.8677898,78.3885437 66.803227,57.2292132 C68.1712787,42.2715849 74.0527146,29.3462646 84.8033863,18.7517722 C105.393354,-1.53572199 136.805164,-4.82141828 161.048542,10.7510424 C184.333097,25.7086706 194.996783,54.8450075 185.906752,79.7822957 C179.052655,77.9239597 172.151111,76.049808 164.563565,73.9917997 C167.418285,60.1274266 165.306899,47.6765751 155.95591,37.0109123 C149.777932,29.9690049 141.850349,26.2780332 132.835442,24.9178894 C114.764113,22.1877169 97.0209573,33.7983633 91.7563309,51.5355878 C85.7800012,71.6669027 94.8245623,88.1111998 119.540432,100.502743 L119.540432,100.502743 Z" fill="#C73A63"></path><path d="M149.841194,79.4106285 C157.316054,92.5969067 164.905578,105.982857 172.427885,119.246236 C210.44865,107.483365 239.114472,128.530009 249.398582,151.063322 C261.81978,178.282014 253.328765,210.520191 228.933162,227.312431 C203.893073,244.551464 172.226236,241.605803 150.040866,219.46195 C155.694953,214.729124 161.376716,209.974552 167.44794,204.895759 C189.360489,219.088306 208.525074,218.420096 222.753207,201.614016 C234.885769,187.277151 234.622834,165.900356 222.138374,151.863988 C207.730339,135.66681 188.431321,135.172572 165.103273,150.721309 C155.426087,133.553447 145.58086,116.521995 136.210101,99.2295848 C133.05093,93.4015266 129.561608,90.0209366 122.440622,88.7873178 C110.547271,86.7253555 102.868785,76.5124151 102.408155,65.0698097 C101.955433,53.7537294 108.621719,43.5249733 119.04224,39.5394355 C129.363912,35.5914599 141.476705,38.7783085 148.419765,47.554004 C154.093621,54.7244134 155.896602,62.7943365 152.911402,71.6372484 C152.081082,74.1025091 151.00562,76.4886916 149.841194,79.4106285 L149.841194,79.4106285 Z" fill="#4B4B4B"></path><path d="M167.706921,187.209935 L121.936499,187.209935 C117.54964,205.253587 108.074103,219.821756 91.7464461,229.085759 C79.0544063,236.285822 65.3738898,238.72736 50.8136292,236.376762 C24.0061432,232.053165 2.08568567,207.920497 0.156179306,180.745298 C-2.02835403,149.962159 19.1309765,122.599149 47.3341915,116.452801 C49.2814904,123.524363 51.2485589,130.663141 53.1958579,137.716911 C27.3195169,150.919004 18.3639187,167.553089 25.6054984,188.352614 C31.9811726,206.657224 50.0900643,216.690262 69.7528413,212.809503 C89.8327554,208.847688 99.9567329,192.160226 98.7211371,165.37844 C117.75722,165.37844 136.809118,165.180745 155.847178,165.475311 C163.280522,165.591951 169.019617,164.820939 174.620326,158.267339 C183.840836,147.48306 200.811003,148.455721 210.741239,158.640984 C220.88894,169.049642 220.402609,185.79839 209.663799,195.768166 C199.302587,205.38802 182.933414,204.874012 173.240413,194.508846 C171.247644,192.37176 169.677943,189.835329 167.706921,187.209935 L167.706921,187.209935 Z" fill="#4A4A4A"></path></g></svg>'''
141 141
142 142 valid_events = [
143 143 events.PullRequestCloseEvent,
144 144 events.PullRequestMergeEvent,
145 145 events.PullRequestUpdateEvent,
146 146 events.PullRequestCommentEvent,
147 147 events.PullRequestCommentEditEvent,
148 148 events.PullRequestReviewEvent,
149 149 events.PullRequestCreateEvent,
150 150 events.RepoPushEvent,
151 151 events.RepoCreateEvent,
152 152 events.RepoCommitCommentEvent,
153 153 events.RepoCommitCommentEditEvent,
154 154 ]
155 155
156 156 def settings_schema(self):
157 157 schema = WebhookSettingsSchema()
158 158 schema.add(colander.SchemaNode(
159 159 colander.Set(),
160 160 widget=CheckboxChoiceWidgetDesc(
161 161 values=sorted(
162 162 [(e.name, e.display_name, e.description) for e in self.valid_events]
163 163 ),
164 164 ),
165 165 description="List of events activated for this integration",
166 166 name='events'
167 167 ))
168 168 return schema
169 169
170 170 def send_event(self, event):
171 171 log.debug('handling event %s with integration %s', event.name, self)
172 172
173 173 if event.__class__ not in self.valid_events:
174 174 log.debug('event %r not present in valid event list (%s)', event, self.valid_events)
175 175 return
176 176
177 177 if not self.event_enabled(event):
178 178 return
179 179
180 180 data = event.as_dict()
181 181 template_url = self.settings['url']
182 182
183 183 headers = {}
184 184 head_key = self.settings.get('custom_header_key')
185 185 head_val = self.settings.get('custom_header_val')
186 186 if head_key and head_val:
187 187 headers = {head_key: head_val}
188 188
189 189 handler = WebhookDataHandler(template_url, headers)
190 190
191 191 url_calls = handler(event, data)
192 192 log.debug('Webhook: calling following urls: %s', [x[0] for x in url_calls])
193 193
194 194 run_task(post_to_webhook, url_calls, self.settings)
195 195
196 196
197 197 @async_task(ignore_result=True, base=RequestContextTask)
198 198 def post_to_webhook(url_calls, settings):
199 199 """
200 200 Example data::
201 201
202 202 {'actor': {'user_id': 2, 'username': u'admin'},
203 203 'actor_ip': u'192.168.157.1',
204 204 'name': 'repo-push',
205 205 'push': {'branches': [{'name': u'default',
206 206 'url': 'http://rc.local:8080/hg-repo/changelog?branch=default'}],
207 207 'commits': [{'author': u'Marcin Kuzminski <marcin@rhodecode.com>',
208 208 'branch': u'default',
209 209 'date': datetime.datetime(2017, 11, 30, 12, 59, 48),
210 210 'issues': [],
211 211 'mentions': [],
212 212 'message': u'commit Thu 30 Nov 2017 13:59:48 CET',
213 213 'message_html': u'commit Thu 30 Nov 2017 13:59:48 CET',
214 214 'message_html_title': u'commit Thu 30 Nov 2017 13:59:48 CET',
215 215 'parents': [{'raw_id': '431b772a5353dad9974b810dd3707d79e3a7f6e0'}],
216 216 'permalink_url': u'http://rc.local:8080/_7/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
217 217 'raw_id': 'a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
218 218 'refs': {'bookmarks': [],
219 219 'branches': [u'default'],
220 220 'tags': [u'tip']},
221 221 'reviewers': [],
222 222 'revision': 9L,
223 223 'short_id': 'a815cc738b96',
224 224 'url': u'http://rc.local:8080/hg-repo/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf'}],
225 225 'issues': {}},
226 226 'repo': {'extra_fields': '',
227 227 'permalink_url': u'http://rc.local:8080/_7',
228 228 'repo_id': 7,
229 229 'repo_name': u'hg-repo',
230 230 'repo_type': u'hg',
231 231 'url': u'http://rc.local:8080/hg-repo'},
232 232 'server_url': u'http://rc.local:8080',
233 233 'utc_timestamp': datetime.datetime(2017, 11, 30, 13, 0, 1, 569276)
234 234 }
235 235 """
236 236
237 237 call_headers = {
238 238 'User-Agent': 'RhodeCode-webhook-caller/{}'.format(rhodecode.__version__)
239 239 } # updated below with custom ones, allows override
240 240
241 241 auth = get_auth(settings)
242 242 token = get_web_token(settings)
243 243
244 244 for url, headers, data in url_calls:
245 245 req_session = requests_retry_call()
246 246
247 247 method = settings.get('method_type') or 'post'
248 248 call_method = getattr(req_session, method)
249 249
250 250 headers = headers or {}
251 251 call_headers.update(headers)
252 252
253 253 log.debug('calling Webhook with method: %s, and auth:%s', call_method, auth)
254 254 if settings.get('log_data'):
255 255 log.debug('calling webhook with data: %s', data)
256 256 resp = call_method(url, json={
257 257 'token': token,
258 258 'event': data
259 259 }, headers=call_headers, auth=auth, timeout=60)
260 260 log.debug('Got Webhook response: %s', resp)
261 261
262 262 try:
263 263 resp.raise_for_status() # raise exception on a failed request
264 264 except Exception:
265 265 log.error(resp.text)
266 266 raise
@@ -1,47 +1,47 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 |flask| Extras
4 4 --------------
5 5
6 6 Utilities you can use when using this library with the |flask|_ framework.
7 7
8 8 Thanks to `Mark Steve Samson <http://marksteve.com>`_.
9 9 """
10 10
11 from __future__ import absolute_import
11
12 12 from functools import wraps
13 13
14 14 from authomatic.adapters import WerkzeugAdapter
15 15 from authomatic import Authomatic
16 16 from flask import make_response, request, session
17 17
18 18
19 19 class FlaskAuthomatic(Authomatic):
20 20 """
21 21 Flask Plugin for authomatic support.
22 22 """
23 23
24 24 result = None
25 25
26 26 def login(self, *login_args, **login_kwargs):
27 27 """
28 28 Decorator for Flask view functions.
29 29 """
30 30
31 31 def decorator(f):
32 32 @wraps(f)
33 33 def decorated(*args, **kwargs):
34 34 self.response = make_response()
35 35 adapter = WerkzeugAdapter(request, self.response)
36 36 login_kwargs.setdefault('session', session)
37 37 login_kwargs.setdefault('session_saver', self.session_saver)
38 38 self.result = super(FlaskAuthomatic, self).login(
39 39 adapter,
40 40 *login_args,
41 41 **login_kwargs)
42 42 return f(*args, **kwargs)
43 43 return decorated
44 44 return decorator
45 45
46 46 def session_saver(self):
47 47 session.modified = True
@@ -1,156 +1,156 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # We need absolute import to import from openid library which has the same
4 4 # name as this module
5 from __future__ import absolute_import
5
6 6 import logging
7 7 import datetime
8 8
9 9 from google.appengine.ext import ndb
10 10 import openid.store.interface
11 11
12 12
13 13 class NDBOpenIDStore(ndb.Expando, openid.store.interface.OpenIDStore):
14 14 """
15 15 |gae| `NDB <https://developers.google.com/appengine/docs/python/ndb/>`_
16 16 based implementation of the :class:`openid.store.interface.OpenIDStore`
17 17 interface of the `python-openid`_ library.
18 18 """
19 19
20 20 serialized = ndb.StringProperty()
21 21 expiration_date = ndb.DateTimeProperty()
22 22 # we need issued to sort by most recently issued
23 23 issued = ndb.IntegerProperty()
24 24
25 25 @staticmethod
26 26 def _log(*args, **kwargs):
27 27 pass
28 28
29 29 @classmethod
30 30 def storeAssociation(cls, server_url, association):
31 31 # store an entity with key = server_url
32 32
33 33 issued = datetime.datetime.fromtimestamp(association.issued)
34 34 lifetime = datetime.timedelta(0, association.lifetime)
35 35
36 36 expiration_date = issued + lifetime
37 37 entity = cls.get_or_insert(
38 38 association.handle, parent=ndb.Key(
39 39 'ServerUrl', server_url))
40 40
41 41 entity.serialized = association.serialize()
42 42 entity.expiration_date = expiration_date
43 43 entity.issued = association.issued
44 44
45 45 cls._log(
46 46 logging.DEBUG,
47 47 u'NDBOpenIDStore: Putting OpenID association to datastore.')
48 48
49 49 entity.put()
50 50
51 51 @classmethod
52 52 def cleanupAssociations(cls):
53 53 # query for all expired
54 54 cls._log(
55 55 logging.DEBUG,
56 56 u'NDBOpenIDStore: Querying datastore for OpenID associations.')
57 57 query = cls.query(cls.expiration_date <= datetime.datetime.now())
58 58
59 59 # fetch keys only
60 60 expired = query.fetch(keys_only=True)
61 61
62 62 # delete all expired
63 63 cls._log(
64 64 logging.DEBUG,
65 65 u'NDBOpenIDStore: Deleting expired OpenID associations from datastore.')
66 66 ndb.delete_multi(expired)
67 67
68 68 return len(expired)
69 69
70 70 @classmethod
71 71 def getAssociation(cls, server_url, handle=None):
72 72 cls.cleanupAssociations()
73 73
74 74 if handle:
75 75 key = ndb.Key('ServerUrl', server_url, cls, handle)
76 76 cls._log(
77 77 logging.DEBUG,
78 78 u'NDBOpenIDStore: Getting OpenID association from datastore by key.')
79 79 entity = key.get()
80 80 else:
81 81 # return most recently issued association
82 82 cls._log(
83 83 logging.DEBUG,
84 84 u'NDBOpenIDStore: Querying datastore for OpenID associations by ancestor.')
85 85 entity = cls.query(ancestor=ndb.Key(
86 86 'ServerUrl', server_url)).order(-cls.issued).get()
87 87
88 88 if entity and entity.serialized:
89 89 return openid.association.Association.deserialize(
90 90 entity.serialized)
91 91
92 92 @classmethod
93 93 def removeAssociation(cls, server_url, handle):
94 94 key = ndb.Key('ServerUrl', server_url, cls, handle)
95 95 cls._log(
96 96 logging.DEBUG,
97 97 u'NDBOpenIDStore: Getting OpenID association from datastore by key.')
98 98 if key.get():
99 99 cls._log(
100 100 logging.DEBUG,
101 101 u'NDBOpenIDStore: Deleting OpenID association from datastore.')
102 102 key.delete()
103 103 return True
104 104
105 105 @classmethod
106 106 def useNonce(cls, server_url, timestamp, salt):
107 107
108 108 # check whether there is already an entity with the same ancestor path
109 109 # in the datastore
110 110 key = ndb.Key(
111 111 'ServerUrl',
112 112 str(server_url) or 'x',
113 113 'TimeStamp',
114 114 str(timestamp),
115 115 cls,
116 116 str(salt))
117 117
118 118 cls._log(
119 119 logging.DEBUG,
120 120 u'NDBOpenIDStore: Getting OpenID nonce from datastore by key.')
121 121 result = key.get()
122 122
123 123 if result:
124 124 # if so, the nonce is not valid so return False
125 125 cls._log(
126 126 logging.WARNING,
127 127 u'NDBOpenIDStore: Nonce was already used!')
128 128 return False
129 129 else:
130 130 # if not, store the key to datastore and return True
131 131 nonce = cls(key=key)
132 132 nonce.expiration_date = datetime.datetime.fromtimestamp(
133 133 timestamp) + datetime.timedelta(0, openid.store.nonce.SKEW)
134 134 cls._log(
135 135 logging.DEBUG,
136 136 u'NDBOpenIDStore: Putting new nonce to datastore.')
137 137 nonce.put()
138 138 return True
139 139
140 140 @classmethod
141 141 def cleanupNonces(cls):
142 142 # get all expired nonces
143 143 cls._log(
144 144 logging.DEBUG,
145 145 u'NDBOpenIDStore: Querying datastore for OpenID nonces ordered by expiration date.')
146 146 expired = cls.query().filter(
147 147 cls.expiration_date <= datetime.datetime.now()).fetch(
148 148 keys_only=True)
149 149
150 150 # delete all expired
151 151 cls._log(
152 152 logging.DEBUG,
153 153 u'NDBOpenIDStore: Deleting expired OpenID nonces from datastore.')
154 154 ndb.delete_multi(expired)
155 155
156 156 return len(expired)
@@ -1,505 +1,505 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 |openid| Providers
4 4 ----------------------------------
5 5
6 6 Providers which implement the |openid|_ protocol based on the
7 7 `python-openid`_ library.
8 8
9 9 .. warning::
10 10
11 11 This providers are dependent on the |pyopenid|_ package.
12 12
13 13 .. autosummary::
14 14
15 15 OpenID
16 16 Yahoo
17 17 Google
18 18
19 19 """
20 20
21 21 # We need absolute import to import from openid library which has the same
22 22 # name as this module
23 from __future__ import absolute_import
23
24 24 import datetime
25 25 import logging
26 26 import time
27 27
28 28 from openid import oidutil
29 29 from openid.consumer import consumer
30 30 from openid.extensions import ax, pape, sreg
31 31 from openid.association import Association
32 32
33 33 from authomatic import providers
34 34 from authomatic.exceptions import FailureError, CancellationError, OpenIDError
35 35
36 36
37 37 __all__ = ['OpenID', 'Yahoo', 'Google']
38 38
39 39
40 40 # Suppress openid logging.
41 41 oidutil.log = lambda message, level=0: None
42 42
43 43
44 44 REALM_HTML = \
45 45 """
46 46 <!DOCTYPE html>
47 47 <html>
48 48 <head>
49 49 <meta http-equiv="X-XRDS-Location" content="{xrds_location}" />
50 50 </head>
51 51 <body>{body}</body>
52 52 </html>
53 53 """
54 54
55 55
56 56 XRDS_XML = \
57 57 """
58 58 <?xml version="1.0" encoding="UTF-8"?>
59 59 <xrds:XRDS
60 60 xmlns:xrds="xri://$xrds"
61 61 xmlns:openid="http://openid.net/xmlns/1.0"
62 62 xmlns="xri://$xrd*($v*2.0)">
63 63 <XRD>
64 64 <Service priority="1">
65 65 <Type>http://specs.openid.net/auth/2.0/return_to</Type>
66 66 <URI>{return_to}</URI>
67 67 </Service>
68 68 </XRD>
69 69 </xrds:XRDS>
70 70 """
71 71
72 72
73 73 class SessionOpenIDStore(object):
74 74 """
75 75 A very primitive session-based implementation of the.
76 76
77 77 :class:`openid.store.interface.OpenIDStore` interface of the
78 78 `python-openid`_ library.
79 79
80 80 .. warning::
81 81
82 82 Nonces get verified only by their timeout. Use on your own risk!
83 83
84 84 """
85 85
86 86 @staticmethod
87 87 def _log(level, message):
88 88 return None
89 89
90 90 ASSOCIATION_KEY = ('authomatic.providers.openid.SessionOpenIDStore:'
91 91 'association')
92 92
93 93 def __init__(self, session, nonce_timeout=None):
94 94 """
95 95 :param int nonce_timeout:
96 96
97 97 Nonces older than this in seconds will be considered expired.
98 98 Default is 600.
99 99 """
100 100 self.session = session
101 101 self.nonce_timeout = nonce_timeout or 600
102 102
103 103 def storeAssociation(self, server_url, association):
104 104 self._log(logging.DEBUG,
105 105 'SessionOpenIDStore: Storing association to session.')
106 106
107 107 serialized = association.serialize()
108 108 decoded = serialized.decode('latin-1')
109 109
110 110 assoc = decoded
111 111 # assoc = serialized
112 112
113 113 # Always store only one association as a tuple.
114 114 self.session[self.ASSOCIATION_KEY] = (server_url, association.handle,
115 115 assoc)
116 116
117 117 def getAssociation(self, server_url, handle=None):
118 118 # Try to get association.
119 119 assoc = self.session.get(self.ASSOCIATION_KEY)
120 120 if assoc and assoc[0] == server_url:
121 121 # If found deserialize and return it.
122 122 self._log(logging.DEBUG, u'SessionOpenIDStore: Association found.')
123 123 return Association.deserialize(assoc[2].encode('latin-1'))
124 124 else:
125 125 self._log(logging.DEBUG,
126 126 u'SessionOpenIDStore: Association not found.')
127 127
128 128 def removeAssociation(self, server_url, handle):
129 129 # Just inform the caller that it's gone.
130 130 return True
131 131
132 132 def useNonce(self, server_url, timestamp, salt):
133 133 # Evaluate expired nonces as false.
134 134 age = int(time.time()) - int(timestamp)
135 135 if age < self.nonce_timeout:
136 136 return True
137 137 else:
138 138 self._log(logging.ERROR, u'SessionOpenIDStore: Expired nonce!')
139 139 return False
140 140
141 141
142 142 class OpenID(providers.AuthenticationProvider):
143 143 """
144 144 |openid|_ provider based on the `python-openid`_ library.
145 145 """
146 146
147 147 AX = ['http://axschema.org/contact/email',
148 148 'http://schema.openid.net/contact/email',
149 149 'http://axschema.org/namePerson',
150 150 'http://openid.net/schema/namePerson/first',
151 151 'http://openid.net/schema/namePerson/last',
152 152 'http://openid.net/schema/gender',
153 153 'http://openid.net/schema/language/pref',
154 154 'http://openid.net/schema/contact/web/default',
155 155 'http://openid.net/schema/media/image',
156 156 'http://openid.net/schema/timezone']
157 157
158 158 AX_REQUIRED = ['http://schema.openid.net/contact/email']
159 159
160 160 SREG = ['nickname',
161 161 'email',
162 162 'fullname',
163 163 'dob',
164 164 'gender',
165 165 'postcode',
166 166 'country',
167 167 'language',
168 168 'timezone']
169 169
170 170 PAPE = [
171 171 'http://schemas.openid.net/pape/policies/2007/06/'
172 172 'multi-factor-physical',
173 173 'http://schemas.openid.net/pape/policies/2007/06/multi-factor',
174 174 'http://schemas.openid.net/pape/policies/2007/06/phishing-resistant'
175 175 ]
176 176
177 177 def __init__(self, *args, **kwargs):
178 178 """
179 179 Accepts additional keyword arguments:
180 180
181 181 :param store:
182 182 Any object which implements
183 183 :class:`openid.store.interface.OpenIDStore`
184 184 of the `python-openid`_ library.
185 185
186 186 :param bool use_realm:
187 187 Whether to use `OpenID realm
188 188 <http://openid.net/specs/openid-authentication-2_0-12.html#realms>`_
189 189 If ``True`` the realm HTML document will be accessible at
190 190 ``{current url}?{realm_param}={realm_param}``
191 191 e.g. ``http://example.com/path?realm=realm``.
192 192
193 193 :param str realm_body:
194 194 Contents of the HTML body tag of the realm.
195 195
196 196 :param str realm_param:
197 197 Name of the query parameter to be used to serve the realm.
198 198
199 199 :param str xrds_param:
200 200 The name of the query parameter to be used to serve the
201 201 `XRDS document
202 202 <http://openid.net/specs/openid-authentication-2_0-12.html#XRDS_Sample>`_.
203 203
204 204 :param list sreg:
205 205 List of strings of optional
206 206 `SREG
207 207 <http://openid.net/specs/openid-simple-registration-extension-1_0.html>`_
208 208 fields.
209 209 Default = :attr:`OpenID.SREG`.
210 210
211 211 :param list sreg_required:
212 212 List of strings of required
213 213 `SREG
214 214 <http://openid.net/specs/openid-simple-registration-extension-1_0.html>`_
215 215 fields.
216 216 Default = ``[]``.
217 217
218 218 :param list ax:
219 219 List of strings of optional
220 220 `AX
221 221 <http://openid.net/specs/openid-attribute-exchange-1_0.html>`_
222 222 schemas.
223 223 Default = :attr:`OpenID.AX`.
224 224
225 225 :param list ax_required:
226 226 List of strings of required
227 227 `AX
228 228 <http://openid.net/specs/openid-attribute-exchange-1_0.html>`_
229 229 schemas.
230 230 Default = :attr:`OpenID.AX_REQUIRED`.
231 231
232 232 :param list pape:
233 233 of requested
234 234 `PAPE
235 235 <http://openid.net/specs/openid-provider-authentication-policy-extension-1_0.html>`_
236 236 policies.
237 237 Default = :attr:`OpenID.PAPE`.
238 238
239 239 As well as those inherited from :class:`.AuthenticationProvider`
240 240 constructor.
241 241
242 242 """
243 243
244 244 super(OpenID, self).__init__(*args, **kwargs)
245 245
246 246 # Allow for other openid store implementations.
247 247 self.store = self._kwarg(
248 248 kwargs, 'store', SessionOpenIDStore(
249 249 self.session))
250 250
251 251 # Realm
252 252 self.use_realm = self._kwarg(kwargs, 'use_realm', True)
253 253 self.realm_body = self._kwarg(kwargs, 'realm_body', '')
254 254 self.realm_param = self._kwarg(kwargs, 'realm_param', 'realm')
255 255 self.xrds_param = self._kwarg(kwargs, 'xrds_param', 'xrds')
256 256
257 257 # SREG
258 258 self.sreg = self._kwarg(kwargs, 'sreg', self.SREG)
259 259 self.sreg_required = self._kwarg(kwargs, 'sreg_required', [])
260 260
261 261 # AX
262 262 self.ax = self._kwarg(kwargs, 'ax', self.AX)
263 263 self.ax_required = self._kwarg(kwargs, 'ax_required', self.AX_REQUIRED)
264 264 # add required schemas to schemas if not already there
265 265 for i in self.ax_required:
266 266 if i not in self.ax:
267 267 self.ax.append(i)
268 268
269 269 # PAPE
270 270 self.pape = self._kwarg(kwargs, 'pape', self.PAPE)
271 271
272 272 @staticmethod
273 273 def _x_user_parser(user, data):
274 274
275 275 user.first_name = data.get('ax', {}).get(
276 276 'http://openid.net/schema/namePerson/first')
277 277 user.last_name = data.get('ax', {}).get(
278 278 'http://openid.net/schema/namePerson/last')
279 279 user.id = data.get('guid')
280 280 user.link = data.get('ax', {}).get(
281 281 'http://openid.net/schema/contact/web/default')
282 282 user.picture = data.get('ax', {}).get(
283 283 'http://openid.net/schema/media/image')
284 284 user.nickname = data.get('sreg', {}).get('nickname')
285 285 user.country = data.get('sreg', {}).get('country')
286 286 user.postal_code = data.get('sreg', {}).get('postcode')
287 287
288 288 user.name = data.get('sreg', {}).get('fullname') or \
289 289 data.get('ax', {}).get('http://axschema.org/namePerson')
290 290
291 291 user.gender = data.get('sreg', {}).get('gender') or \
292 292 data.get('ax', {}).get('http://openid.net/schema/gender')
293 293
294 294 user.locale = data.get('sreg', {}).get('language') or \
295 295 data.get('ax', {}).get('http://openid.net/schema/language/pref')
296 296
297 297 user.timezone = data.get('sreg', {}).get('timezone') or \
298 298 data.get('ax', {}).get('http://openid.net/schema/timezone')
299 299
300 300 user.email = data.get('sreg', {}).get('email') or \
301 301 data.get('ax', {}).get('http://axschema.org/contact/email') or \
302 302 data.get('ax', {}).get('http://schema.openid.net/contact/email')
303 303
304 304 if data.get('sreg', {}).get('dob'):
305 305 user.birth_date = datetime.datetime.strptime(
306 306 data.get('sreg', {}).get('dob'),
307 307 '%Y-%m-%d'
308 308 )
309 309 else:
310 310 user.birth_date = None
311 311
312 312 return user
313 313
314 314 @providers.login_decorator
315 315 def login(self):
316 316 # Instantiate consumer
317 317 self.store._log = self._log
318 318 oi_consumer = consumer.Consumer(self.session, self.store)
319 319
320 320 # handle realm and XRDS if there is only one query parameter
321 321 if self.use_realm and len(self.params) == 1:
322 322 realm_request = self.params.get(self.realm_param)
323 323 xrds_request = self.params.get(self.xrds_param)
324 324 else:
325 325 realm_request = None
326 326 xrds_request = None
327 327
328 328 # determine type of request
329 329 if realm_request:
330 330 # =================================================================
331 331 # Realm HTML
332 332 # =================================================================
333 333
334 334 self._log(
335 335 logging.INFO,
336 336 u'Writing OpenID realm HTML to the response.')
337 337 xrds_location = '{u}?{x}={x}'.format(u=self.url, x=self.xrds_param)
338 338 self.write(
339 339 REALM_HTML.format(
340 340 xrds_location=xrds_location,
341 341 body=self.realm_body))
342 342
343 343 elif xrds_request:
344 344 # =================================================================
345 345 # XRDS XML
346 346 # =================================================================
347 347
348 348 self._log(
349 349 logging.INFO,
350 350 u'Writing XRDS XML document to the response.')
351 351 self.set_header('Content-Type', 'application/xrds+xml')
352 352 self.write(XRDS_XML.format(return_to=self.url))
353 353
354 354 elif self.params.get('openid.mode'):
355 355 # =================================================================
356 356 # Phase 2 after redirect
357 357 # =================================================================
358 358
359 359 self._log(
360 360 logging.INFO,
361 361 u'Continuing OpenID authentication procedure after redirect.')
362 362
363 363 # complete the authentication process
364 364 response = oi_consumer.complete(self.params, self.url)
365 365
366 366 # on success
367 367 if response.status == consumer.SUCCESS:
368 368
369 369 data = {}
370 370
371 371 # get user ID
372 372 data['guid'] = response.getDisplayIdentifier()
373 373
374 374 self._log(logging.INFO, u'Authentication successful.')
375 375
376 376 # get user data from AX response
377 377 ax_response = ax.FetchResponse.fromSuccessResponse(response)
378 378 if ax_response and ax_response.data:
379 379 self._log(logging.INFO, u'Got AX data.')
380 380 ax_data = {}
381 381 # convert iterable values to their first item
382 382 for k, v in ax_response.data.items():
383 383 if v and isinstance(v, (list, tuple)):
384 384 ax_data[k] = v[0]
385 385 data['ax'] = ax_data
386 386
387 387 # get user data from SREG response
388 388 sreg_response = sreg.SRegResponse.fromSuccessResponse(response)
389 389 if sreg_response and sreg_response.data:
390 390 self._log(logging.INFO, u'Got SREG data.')
391 391 data['sreg'] = sreg_response.data
392 392
393 393 # get data from PAPE response
394 394 pape_response = pape.Response.fromSuccessResponse(response)
395 395 if pape_response and pape_response.auth_policies:
396 396 self._log(logging.INFO, u'Got PAPE data.')
397 397 data['pape'] = pape_response.auth_policies
398 398
399 399 # create user
400 400 self._update_or_create_user(data)
401 401
402 402 # =============================================================
403 403 # We're done!
404 404 # =============================================================
405 405
406 406 elif response.status == consumer.CANCEL:
407 407 raise CancellationError(
408 408 u'User cancelled the verification of ID "{0}"!'.format(
409 409 response.getDisplayIdentifier()))
410 410
411 411 elif response.status == consumer.FAILURE:
412 412 raise FailureError(response.message)
413 413
414 414 elif self.identifier: # As set in AuthenticationProvider.__init__
415 415 # =================================================================
416 416 # Phase 1 before redirect
417 417 # =================================================================
418 418
419 419 self._log(
420 420 logging.INFO,
421 421 u'Starting OpenID authentication procedure.')
422 422
423 423 # get AuthRequest object
424 424 try:
425 425 auth_request = oi_consumer.begin(self.identifier)
426 426 except consumer.DiscoveryFailure as e:
427 427 raise FailureError(
428 428 u'Discovery failed for identifier {0}!'.format(
429 429 self.identifier
430 430 ),
431 431 url=self.identifier,
432 432 original_message=e.message)
433 433
434 434 self._log(
435 435 logging.INFO,
436 436 u'Service discovery for identifier {0} successful.'.format(
437 437 self.identifier))
438 438
439 439 # add SREG extension
440 440 # we need to remove required fields from optional fields because
441 441 # addExtension then raises an error
442 442 self.sreg = [i for i in self.sreg if i not in self.sreg_required]
443 443 auth_request.addExtension(
444 444 sreg.SRegRequest(
445 445 optional=self.sreg,
446 446 required=self.sreg_required)
447 447 )
448 448
449 449 # add AX extension
450 450 ax_request = ax.FetchRequest()
451 451 # set AX schemas
452 452 for i in self.ax:
453 453 required = i in self.ax_required
454 454 ax_request.add(ax.AttrInfo(i, required=required))
455 455 auth_request.addExtension(ax_request)
456 456
457 457 # add PAPE extension
458 458 auth_request.addExtension(pape.Request(self.pape))
459 459
460 460 # prepare realm and return_to URLs
461 461 if self.use_realm:
462 462 realm = return_to = '{u}?{r}={r}'.format(
463 463 u=self.url, r=self.realm_param)
464 464 else:
465 465 realm = return_to = self.url
466 466
467 467 url = auth_request.redirectURL(realm, return_to)
468 468
469 469 if auth_request.shouldSendRedirect():
470 470 # can be redirected
471 471 url = auth_request.redirectURL(realm, return_to)
472 472 self._log(
473 473 logging.INFO,
474 474 u'Redirecting user to {0}.'.format(url))
475 475 self.redirect(url)
476 476 else:
477 477 # must be sent as POST
478 478 # this writes a html post form with auto-submit
479 479 self._log(
480 480 logging.INFO,
481 481 u'Writing an auto-submit HTML form to the response.')
482 482 form = auth_request.htmlMarkup(
483 483 realm, return_to, False, dict(
484 484 id='openid_form'))
485 485 self.write(form)
486 486 else:
487 487 raise OpenIDError('No identifier specified!')
488 488
489 489
490 490 class Yahoo(OpenID):
491 491 """
492 492 Yahoo :class:`.OpenID` provider with the :attr:`.identifier` predefined to
493 493 ``"me.yahoo.com"``.
494 494 """
495 495
496 496 identifier = 'me.yahoo.com'
497 497
498 498
499 499 class Google(OpenID):
500 500 """
501 501 Google :class:`.OpenID` provider with the :attr:`.identifier` predefined to
502 502 ``"https://www.google.com/accounts/o8/id"``.
503 503 """
504 504
505 505 identifier = 'https://www.google.com/accounts/o8/id'
@@ -1,839 +1,839 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Utilities for writing code that runs on Python 2 and 3"""
3 3
4 4 # Copyright (c) 2010-2015 Benjamin Peterson
5 5 #
6 6 # Permission is hereby granted, free of charge, to any person obtaining a copy
7 7 # of this software and associated documentation files (the "Software"), to deal
8 8 # in the Software without restriction, including without limitation the rights
9 9 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 10 # copies of the Software, and to permit persons to whom the Software is
11 11 # furnished to do so, subject to the following conditions:
12 12 #
13 13 # The above copyright notice and this permission notice shall be included in all
14 14 # copies or substantial portions of the Software.
15 15 #
16 16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 17 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 18 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 19 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 20 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 21 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 22 # SOFTWARE.
23 23
24 from __future__ import absolute_import
24
25 25
26 26 import functools
27 27 import itertools
28 28 import operator
29 29 import sys
30 30 import types
31 31
32 32 __author__ = "Benjamin Peterson <benjamin@python.org>"
33 33 __version__ = "1.9.0"
34 34
35 35
36 36 # Useful for very coarse version differentiation.
37 37 PY2 = sys.version_info[0] == 2
38 38 PY3 = sys.version_info[0] == 3
39 39
40 40 if PY3:
41 41 string_types = str,
42 42 integer_types = int,
43 43 class_types = type,
44 44 text_type = str
45 45 binary_type = bytes
46 46
47 47 MAXSIZE = sys.maxsize
48 48 else:
49 49 string_types = basestring,
50 50 integer_types = (int, long)
51 51 class_types = (type, types.ClassType)
52 52 text_type = unicode
53 53 binary_type = str
54 54
55 55 if sys.platform.startswith("java"):
56 56 # Jython always uses 32 bits.
57 57 MAXSIZE = int((1 << 31) - 1)
58 58 else:
59 59 # It's possible to have sizeof(long) != sizeof(Py_ssize_t).
60 60 class X(object):
61 61 def __len__(self):
62 62 return 1 << 31
63 63 try:
64 64 len(X())
65 65 except OverflowError:
66 66 # 32-bit
67 67 MAXSIZE = int((1 << 31) - 1)
68 68 else:
69 69 # 64-bit
70 70 MAXSIZE = int((1 << 63) - 1)
71 71 del X
72 72
73 73
74 74 def _add_doc(func, doc):
75 75 """Add documentation to a function."""
76 76 func.__doc__ = doc
77 77
78 78
79 79 def _import_module(name):
80 80 """Import module, returning the module after the last dot."""
81 81 __import__(name)
82 82 return sys.modules[name]
83 83
84 84
85 85 class _LazyDescr(object):
86 86
87 87 def __init__(self, name):
88 88 self.name = name
89 89
90 90 def __get__(self, obj, tp):
91 91 result = self._resolve()
92 92 setattr(obj, self.name, result) # Invokes __set__.
93 93 try:
94 94 # This is a bit ugly, but it avoids running this again by
95 95 # removing this descriptor.
96 96 delattr(obj.__class__, self.name)
97 97 except AttributeError:
98 98 pass
99 99 return result
100 100
101 101
102 102 class MovedModule(_LazyDescr):
103 103
104 104 def __init__(self, name, old, new=None):
105 105 super(MovedModule, self).__init__(name)
106 106 if PY3:
107 107 if new is None:
108 108 new = name
109 109 self.mod = new
110 110 else:
111 111 self.mod = old
112 112
113 113 def _resolve(self):
114 114 return _import_module(self.mod)
115 115
116 116 def __getattr__(self, attr):
117 117 _module = self._resolve()
118 118 value = getattr(_module, attr)
119 119 setattr(self, attr, value)
120 120 return value
121 121
122 122
123 123 class _LazyModule(types.ModuleType):
124 124
125 125 def __init__(self, name):
126 126 super(_LazyModule, self).__init__(name)
127 127 self.__doc__ = self.__class__.__doc__
128 128
129 129 def __dir__(self):
130 130 attrs = ["__doc__", "__name__"]
131 131 attrs += [attr.name for attr in self._moved_attributes]
132 132 return attrs
133 133
134 134 # Subclasses should override this
135 135 _moved_attributes = []
136 136
137 137
138 138 class MovedAttribute(_LazyDescr):
139 139
140 140 def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):
141 141 super(MovedAttribute, self).__init__(name)
142 142 if PY3:
143 143 if new_mod is None:
144 144 new_mod = name
145 145 self.mod = new_mod
146 146 if new_attr is None:
147 147 if old_attr is None:
148 148 new_attr = name
149 149 else:
150 150 new_attr = old_attr
151 151 self.attr = new_attr
152 152 else:
153 153 self.mod = old_mod
154 154 if old_attr is None:
155 155 old_attr = name
156 156 self.attr = old_attr
157 157
158 158 def _resolve(self):
159 159 module = _import_module(self.mod)
160 160 return getattr(module, self.attr)
161 161
162 162
163 163 class _SixMetaPathImporter(object):
164 164 """
165 165 A meta path importer to import six.moves and its submodules.
166 166
167 167 This class implements a PEP302 finder and loader. It should be compatible
168 168 with Python 2.5 and all existing versions of Python3
169 169 """
170 170 def __init__(self, six_module_name):
171 171 self.name = six_module_name
172 172 self.known_modules = {}
173 173
174 174 def _add_module(self, mod, *fullnames):
175 175 for fullname in fullnames:
176 176 self.known_modules[self.name + "." + fullname] = mod
177 177
178 178 def _get_module(self, fullname):
179 179 return self.known_modules[self.name + "." + fullname]
180 180
181 181 def find_module(self, fullname, path=None):
182 182 if fullname in self.known_modules:
183 183 return self
184 184 return None
185 185
186 186 def __get_module(self, fullname):
187 187 try:
188 188 return self.known_modules[fullname]
189 189 except KeyError:
190 190 raise ImportError("This loader does not know module " + fullname)
191 191
192 192 def load_module(self, fullname):
193 193 try:
194 194 # in case of a reload
195 195 return sys.modules[fullname]
196 196 except KeyError:
197 197 pass
198 198 mod = self.__get_module(fullname)
199 199 if isinstance(mod, MovedModule):
200 200 mod = mod._resolve()
201 201 else:
202 202 mod.__loader__ = self
203 203 sys.modules[fullname] = mod
204 204 return mod
205 205
206 206 def is_package(self, fullname):
207 207 """
208 208 Return true, if the named module is a package.
209 209
210 210 We need this method to get correct spec objects with
211 211 Python 3.4 (see PEP451)
212 212 """
213 213 return hasattr(self.__get_module(fullname), "__path__")
214 214
215 215 def get_code(self, fullname):
216 216 """Return None
217 217
218 218 Required, if is_package is implemented"""
219 219 self.__get_module(fullname) # eventually raises ImportError
220 220 return None
221 221 get_source = get_code # same as get_code
222 222
223 223 _importer = _SixMetaPathImporter(__name__)
224 224
225 225
226 226 class _MovedItems(_LazyModule):
227 227 """Lazy loading of moved objects"""
228 228 __path__ = [] # mark as package
229 229
230 230
231 231 _moved_attributes = [
232 232 MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"),
233 233 MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"),
234 234 MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"),
235 235 MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"),
236 236 MovedAttribute("intern", "__builtin__", "sys"),
237 237 MovedAttribute("map", "itertools", "builtins", "imap", "map"),
238 238 MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"),
239 239 MovedAttribute("reload_module", "__builtin__", "imp", "reload"),
240 240 MovedAttribute("reduce", "__builtin__", "functools"),
241 241 MovedAttribute("shlex_quote", "pipes", "shlex", "quote"),
242 242 MovedAttribute("StringIO", "StringIO", "io"),
243 243 MovedAttribute("UserDict", "UserDict", "collections"),
244 244 MovedAttribute("UserList", "UserList", "collections"),
245 245 MovedAttribute("UserString", "UserString", "collections"),
246 246 MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"),
247 247 MovedAttribute("zip", "itertools", "builtins", "izip", "zip"),
248 248 MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"),
249 249
250 250 MovedModule("builtins", "__builtin__"),
251 251 MovedModule("configparser", "ConfigParser"),
252 252 MovedModule("copyreg", "copy_reg"),
253 253 MovedModule("dbm_gnu", "gdbm", "dbm.gnu"),
254 254 MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"),
255 255 MovedModule("http_cookiejar", "cookielib", "http.cookiejar"),
256 256 MovedModule("http_cookies", "Cookie", "http.cookies"),
257 257 MovedModule("html_entities", "htmlentitydefs", "html.entities"),
258 258 MovedModule("html_parser", "HTMLParser", "html.parser"),
259 259 MovedModule("http_client", "httplib", "http.client"),
260 260 MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"),
261 261 MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"),
262 262 MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"),
263 263 MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"),
264 264 MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"),
265 265 MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"),
266 266 MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"),
267 267 MovedModule("cPickle", "cPickle", "pickle"),
268 268 MovedModule("queue", "Queue"),
269 269 MovedModule("reprlib", "repr"),
270 270 MovedModule("socketserver", "SocketServer"),
271 271 MovedModule("_thread", "thread", "_thread"),
272 272 MovedModule("tkinter", "Tkinter"),
273 273 MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"),
274 274 MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"),
275 275 MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"),
276 276 MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"),
277 277 MovedModule("tkinter_tix", "Tix", "tkinter.tix"),
278 278 MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"),
279 279 MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"),
280 280 MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"),
281 281 MovedModule("tkinter_colorchooser", "tkColorChooser",
282 282 "tkinter.colorchooser"),
283 283 MovedModule("tkinter_commondialog", "tkCommonDialog",
284 284 "tkinter.commondialog"),
285 285 MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"),
286 286 MovedModule("tkinter_font", "tkFont", "tkinter.font"),
287 287 MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"),
288 288 MovedModule("tkinter_tksimpledialog", "tkSimpleDialog",
289 289 "tkinter.simpledialog"),
290 290 MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"),
291 291 MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"),
292 292 MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"),
293 293 MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"),
294 294 MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"),
295 295 MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"),
296 296 MovedModule("winreg", "_winreg"),
297 297 ]
298 298 for attr in _moved_attributes:
299 299 setattr(_MovedItems, attr.name, attr)
300 300 if isinstance(attr, MovedModule):
301 301 _importer._add_module(attr, "moves." + attr.name)
302 302 del attr
303 303
304 304 _MovedItems._moved_attributes = _moved_attributes
305 305
306 306 moves = _MovedItems(__name__ + ".moves")
307 307 _importer._add_module(moves, "moves")
308 308
309 309
310 310 class Module_six_moves_urllib_parse(_LazyModule):
311 311 """Lazy loading of moved objects in six.moves.urllib_parse"""
312 312
313 313
314 314 _urllib_parse_moved_attributes = [
315 315 MovedAttribute("ParseResult", "urlparse", "urllib.parse"),
316 316 MovedAttribute("SplitResult", "urlparse", "urllib.parse"),
317 317 MovedAttribute("parse_qs", "urlparse", "urllib.parse"),
318 318 MovedAttribute("parse_qsl", "urlparse", "urllib.parse"),
319 319 MovedAttribute("urldefrag", "urlparse", "urllib.parse"),
320 320 MovedAttribute("urljoin", "urlparse", "urllib.parse"),
321 321 MovedAttribute("urlparse", "urlparse", "urllib.parse"),
322 322 MovedAttribute("urlsplit", "urlparse", "urllib.parse"),
323 323 MovedAttribute("urlunparse", "urlparse", "urllib.parse"),
324 324 MovedAttribute("urlunsplit", "urlparse", "urllib.parse"),
325 325 MovedAttribute("quote", "urllib", "urllib.parse"),
326 326 MovedAttribute("quote_plus", "urllib", "urllib.parse"),
327 327 MovedAttribute("unquote", "urllib", "urllib.parse"),
328 328 MovedAttribute("unquote_plus", "urllib", "urllib.parse"),
329 329 MovedAttribute("urlencode", "urllib", "urllib.parse"),
330 330 MovedAttribute("splitquery", "urllib", "urllib.parse"),
331 331 MovedAttribute("splittag", "urllib", "urllib.parse"),
332 332 MovedAttribute("splituser", "urllib", "urllib.parse"),
333 333 MovedAttribute("uses_fragment", "urlparse", "urllib.parse"),
334 334 MovedAttribute("uses_netloc", "urlparse", "urllib.parse"),
335 335 MovedAttribute("uses_params", "urlparse", "urllib.parse"),
336 336 MovedAttribute("uses_query", "urlparse", "urllib.parse"),
337 337 MovedAttribute("uses_relative", "urlparse", "urllib.parse"),
338 338 ]
339 339 for attr in _urllib_parse_moved_attributes:
340 340 setattr(Module_six_moves_urllib_parse, attr.name, attr)
341 341 del attr
342 342
343 343 Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes
344 344
345 345 _importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"),
346 346 "moves.urllib_parse", "moves.urllib.parse")
347 347
348 348
349 349 class Module_six_moves_urllib_error(_LazyModule):
350 350 """Lazy loading of moved objects in six.moves.urllib_error"""
351 351
352 352
353 353 _urllib_error_moved_attributes = [
354 354 MovedAttribute("URLError", "urllib2", "urllib.error"),
355 355 MovedAttribute("HTTPError", "urllib2", "urllib.error"),
356 356 MovedAttribute("ContentTooShortError", "urllib", "urllib.error"),
357 357 ]
358 358 for attr in _urllib_error_moved_attributes:
359 359 setattr(Module_six_moves_urllib_error, attr.name, attr)
360 360 del attr
361 361
362 362 Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes
363 363
364 364 _importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"),
365 365 "moves.urllib_error", "moves.urllib.error")
366 366
367 367
368 368 class Module_six_moves_urllib_request(_LazyModule):
369 369 """Lazy loading of moved objects in six.moves.urllib_request"""
370 370
371 371
372 372 _urllib_request_moved_attributes = [
373 373 MovedAttribute("urlopen", "urllib2", "urllib.request"),
374 374 MovedAttribute("install_opener", "urllib2", "urllib.request"),
375 375 MovedAttribute("build_opener", "urllib2", "urllib.request"),
376 376 MovedAttribute("pathname2url", "urllib", "urllib.request"),
377 377 MovedAttribute("url2pathname", "urllib", "urllib.request"),
378 378 MovedAttribute("getproxies", "urllib", "urllib.request"),
379 379 MovedAttribute("Request", "urllib2", "urllib.request"),
380 380 MovedAttribute("OpenerDirector", "urllib2", "urllib.request"),
381 381 MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"),
382 382 MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"),
383 383 MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"),
384 384 MovedAttribute("ProxyHandler", "urllib2", "urllib.request"),
385 385 MovedAttribute("BaseHandler", "urllib2", "urllib.request"),
386 386 MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"),
387 387 MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"),
388 388 MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"),
389 389 MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"),
390 390 MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"),
391 391 MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"),
392 392 MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"),
393 393 MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"),
394 394 MovedAttribute("HTTPHandler", "urllib2", "urllib.request"),
395 395 MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"),
396 396 MovedAttribute("FileHandler", "urllib2", "urllib.request"),
397 397 MovedAttribute("FTPHandler", "urllib2", "urllib.request"),
398 398 MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"),
399 399 MovedAttribute("UnknownHandler", "urllib2", "urllib.request"),
400 400 MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"),
401 401 MovedAttribute("urlretrieve", "urllib", "urllib.request"),
402 402 MovedAttribute("urlcleanup", "urllib", "urllib.request"),
403 403 MovedAttribute("URLopener", "urllib", "urllib.request"),
404 404 MovedAttribute("FancyURLopener", "urllib", "urllib.request"),
405 405 MovedAttribute("proxy_bypass", "urllib", "urllib.request"),
406 406 ]
407 407 for attr in _urllib_request_moved_attributes:
408 408 setattr(Module_six_moves_urllib_request, attr.name, attr)
409 409 del attr
410 410
411 411 Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes
412 412
413 413 _importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"),
414 414 "moves.urllib_request", "moves.urllib.request")
415 415
416 416
417 417 class Module_six_moves_urllib_response(_LazyModule):
418 418 """Lazy loading of moved objects in six.moves.urllib_response"""
419 419
420 420
421 421 _urllib_response_moved_attributes = [
422 422 MovedAttribute("addbase", "urllib", "urllib.response"),
423 423 MovedAttribute("addclosehook", "urllib", "urllib.response"),
424 424 MovedAttribute("addinfo", "urllib", "urllib.response"),
425 425 MovedAttribute("addinfourl", "urllib", "urllib.response"),
426 426 ]
427 427 for attr in _urllib_response_moved_attributes:
428 428 setattr(Module_six_moves_urllib_response, attr.name, attr)
429 429 del attr
430 430
431 431 Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes
432 432
433 433 _importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"),
434 434 "moves.urllib_response", "moves.urllib.response")
435 435
436 436
437 437 class Module_six_moves_urllib_robotparser(_LazyModule):
438 438 """Lazy loading of moved objects in six.moves.urllib_robotparser"""
439 439
440 440
441 441 _urllib_robotparser_moved_attributes = [
442 442 MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"),
443 443 ]
444 444 for attr in _urllib_robotparser_moved_attributes:
445 445 setattr(Module_six_moves_urllib_robotparser, attr.name, attr)
446 446 del attr
447 447
448 448 Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes
449 449
450 450 _importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"),
451 451 "moves.urllib_robotparser", "moves.urllib.robotparser")
452 452
453 453
454 454 class Module_six_moves_urllib(types.ModuleType):
455 455 """Create a six.moves.urllib namespace that resembles the Python 3 namespace"""
456 456 __path__ = [] # mark as package
457 457 parse = _importer._get_module("moves.urllib_parse")
458 458 error = _importer._get_module("moves.urllib_error")
459 459 request = _importer._get_module("moves.urllib_request")
460 460 response = _importer._get_module("moves.urllib_response")
461 461 robotparser = _importer._get_module("moves.urllib_robotparser")
462 462
463 463 def __dir__(self):
464 464 return ['parse', 'error', 'request', 'response', 'robotparser']
465 465
466 466 _importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"),
467 467 "moves.urllib")
468 468
469 469
470 470 def add_move(move):
471 471 """Add an item to six.moves."""
472 472 setattr(_MovedItems, move.name, move)
473 473
474 474
475 475 def remove_move(name):
476 476 """Remove item from six.moves."""
477 477 try:
478 478 delattr(_MovedItems, name)
479 479 except AttributeError:
480 480 try:
481 481 del moves.__dict__[name]
482 482 except KeyError:
483 483 raise AttributeError("no such move, %r" % (name,))
484 484
485 485
486 486 if PY3:
487 487 _meth_func = "__func__"
488 488 _meth_self = "__self__"
489 489
490 490 _func_closure = "__closure__"
491 491 _func_code = "__code__"
492 492 _func_defaults = "__defaults__"
493 493 _func_globals = "__globals__"
494 494 else:
495 495 _meth_func = "im_func"
496 496 _meth_self = "im_self"
497 497
498 498 _func_closure = "func_closure"
499 499 _func_code = "func_code"
500 500 _func_defaults = "func_defaults"
501 501 _func_globals = "func_globals"
502 502
503 503
504 504 try:
505 505 advance_iterator = next
506 506 except NameError:
507 507 def advance_iterator(it):
508 508 return it.next()
509 509 next = advance_iterator
510 510
511 511
512 512 try:
513 513 callable = callable
514 514 except NameError:
515 515 def callable(obj):
516 516 return any("__call__" in klass.__dict__ for klass in type(obj).__mro__)
517 517
518 518
519 519 if PY3:
520 520 def get_unbound_function(unbound):
521 521 return unbound
522 522
523 523 create_bound_method = types.MethodType
524 524
525 525 Iterator = object
526 526 else:
527 527 def get_unbound_function(unbound):
528 528 return unbound.im_func
529 529
530 530 def create_bound_method(func, obj):
531 531 return types.MethodType(func, obj, obj.__class__)
532 532
533 533 class Iterator(object):
534 534
535 535 def next(self):
536 536 return type(self).__next__(self)
537 537
538 538 callable = callable
539 539 _add_doc(get_unbound_function,
540 540 """Get the function out of a possibly unbound function""")
541 541
542 542
543 543 get_method_function = operator.attrgetter(_meth_func)
544 544 get_method_self = operator.attrgetter(_meth_self)
545 545 get_function_closure = operator.attrgetter(_func_closure)
546 546 get_function_code = operator.attrgetter(_func_code)
547 547 get_function_defaults = operator.attrgetter(_func_defaults)
548 548 get_function_globals = operator.attrgetter(_func_globals)
549 549
550 550
551 551 if PY3:
552 552 def iterkeys(d, **kw):
553 553 return iter(d.keys(**kw))
554 554
555 555 def itervalues(d, **kw):
556 556 return iter(d.values(**kw))
557 557
558 558 def iteritems(d, **kw):
559 559 return iter(d.items(**kw))
560 560
561 561 def iterlists(d, **kw):
562 562 return iter(d.lists(**kw))
563 563
564 564 viewkeys = operator.methodcaller("keys")
565 565
566 566 viewvalues = operator.methodcaller("values")
567 567
568 568 viewitems = operator.methodcaller("items")
569 569 else:
570 570 def iterkeys(d, **kw):
571 571 return iter(d.iterkeys(**kw))
572 572
573 573 def itervalues(d, **kw):
574 574 return iter(d.itervalues(**kw))
575 575
576 576 def iteritems(d, **kw):
577 577 return iter(d.iteritems(**kw))
578 578
579 579 def iterlists(d, **kw):
580 580 return iter(d.iterlists(**kw))
581 581
582 582 viewkeys = operator.methodcaller("viewkeys")
583 583
584 584 viewvalues = operator.methodcaller("viewvalues")
585 585
586 586 viewitems = operator.methodcaller("viewitems")
587 587
588 588 _add_doc(iterkeys, "Return an iterator over the keys of a dictionary.")
589 589 _add_doc(itervalues, "Return an iterator over the values of a dictionary.")
590 590 _add_doc(iteritems,
591 591 "Return an iterator over the (key, value) pairs of a dictionary.")
592 592 _add_doc(iterlists,
593 593 "Return an iterator over the (key, [values]) pairs of a dictionary.")
594 594
595 595
596 596 if PY3:
597 597 def b(s):
598 598 return s.encode("latin-1")
599 599 def u(s):
600 600 return s
601 601 unichr = chr
602 602 if sys.version_info[1] <= 1:
603 603 def int2byte(i):
604 604 return bytes((i,))
605 605 else:
606 606 # This is about 2x faster than the implementation above on 3.2+
607 607 int2byte = operator.methodcaller("to_bytes", 1, "big")
608 608 byte2int = operator.itemgetter(0)
609 609 indexbytes = operator.getitem
610 610 iterbytes = iter
611 611 import io
612 612 StringIO = io.StringIO
613 613 BytesIO = io.BytesIO
614 614 _assertCountEqual = "assertCountEqual"
615 615 _assertRaisesRegex = "assertRaisesRegex"
616 616 _assertRegex = "assertRegex"
617 617 else:
618 618 def b(s):
619 619 return s
620 620 # Workaround for standalone backslash
621 621 def u(s):
622 622 return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape")
623 623 unichr = unichr
624 624 int2byte = chr
625 625 def byte2int(bs):
626 626 return ord(bs[0])
627 627 def indexbytes(buf, i):
628 628 return ord(buf[i])
629 629 iterbytes = functools.partial(itertools.imap, ord)
630 630 import StringIO
631 631 StringIO = BytesIO = StringIO.StringIO
632 632 _assertCountEqual = "assertItemsEqual"
633 633 _assertRaisesRegex = "assertRaisesRegexp"
634 634 _assertRegex = "assertRegexpMatches"
635 635 _add_doc(b, """Byte literal""")
636 636 _add_doc(u, """Text literal""")
637 637
638 638
639 639 def assertCountEqual(self, *args, **kwargs):
640 640 return getattr(self, _assertCountEqual)(*args, **kwargs)
641 641
642 642
643 643 def assertRaisesRegex(self, *args, **kwargs):
644 644 return getattr(self, _assertRaisesRegex)(*args, **kwargs)
645 645
646 646
647 647 def assertRegex(self, *args, **kwargs):
648 648 return getattr(self, _assertRegex)(*args, **kwargs)
649 649
650 650
651 651 if PY3:
652 652 exec_ = getattr(moves.builtins, "exec")
653 653
654 654
655 655 def reraise(tp, value, tb=None):
656 656 if value is None:
657 657 value = tp()
658 658 if value.__traceback__ is not tb:
659 659 raise value.with_traceback(tb)
660 660 raise value
661 661
662 662 else:
663 663 def exec_(_code_, _globs_=None, _locs_=None):
664 664 """Execute code in a namespace."""
665 665 if _globs_ is None:
666 666 frame = sys._getframe(1)
667 667 _globs_ = frame.f_globals
668 668 if _locs_ is None:
669 669 _locs_ = frame.f_locals
670 670 del frame
671 671 elif _locs_ is None:
672 672 _locs_ = _globs_
673 673 exec("""exec _code_ in _globs_, _locs_""")
674 674
675 675
676 676 exec_("""def reraise(tp, value, tb=None):
677 677 raise tp, value, tb
678 678 """)
679 679
680 680
681 681 if sys.version_info[:2] == (3, 2):
682 682 exec_("""def raise_from(value, from_value):
683 683 if from_value is None:
684 684 raise value
685 685 raise value from from_value
686 686 """)
687 687 elif sys.version_info[:2] > (3, 2):
688 688 exec_("""def raise_from(value, from_value):
689 689 raise value from from_value
690 690 """)
691 691 else:
692 692 def raise_from(value, from_value):
693 693 raise value
694 694
695 695
696 696 print_ = getattr(moves.builtins, "print", None)
697 697 if print_ is None:
698 698 def print_(*args, **kwargs):
699 699 """The new-style print function for Python 2.4 and 2.5."""
700 700 fp = kwargs.pop("file", sys.stdout)
701 701 if fp is None:
702 702 return
703 703 def write(data):
704 704 if not isinstance(data, basestring):
705 705 data = str(data)
706 706 # If the file has an encoding, encode unicode with it.
707 707 if (isinstance(fp, file) and
708 708 isinstance(data, unicode) and
709 709 fp.encoding is not None):
710 710 errors = getattr(fp, "errors", None)
711 711 if errors is None:
712 712 errors = "strict"
713 713 data = data.encode(fp.encoding, errors)
714 714 fp.write(data)
715 715 want_unicode = False
716 716 sep = kwargs.pop("sep", None)
717 717 if sep is not None:
718 718 if isinstance(sep, unicode):
719 719 want_unicode = True
720 720 elif not isinstance(sep, str):
721 721 raise TypeError("sep must be None or a string")
722 722 end = kwargs.pop("end", None)
723 723 if end is not None:
724 724 if isinstance(end, unicode):
725 725 want_unicode = True
726 726 elif not isinstance(end, str):
727 727 raise TypeError("end must be None or a string")
728 728 if kwargs:
729 729 raise TypeError("invalid keyword arguments to print()")
730 730 if not want_unicode:
731 731 for arg in args:
732 732 if isinstance(arg, unicode):
733 733 want_unicode = True
734 734 break
735 735 if want_unicode:
736 736 newline = unicode("\n")
737 737 space = unicode(" ")
738 738 else:
739 739 newline = "\n"
740 740 space = " "
741 741 if sep is None:
742 742 sep = space
743 743 if end is None:
744 744 end = newline
745 745 for i, arg in enumerate(args):
746 746 if i:
747 747 write(sep)
748 748 write(arg)
749 749 write(end)
750 750 if sys.version_info[:2] < (3, 3):
751 751 _print = print_
752 752 def print_(*args, **kwargs):
753 753 fp = kwargs.get("file", sys.stdout)
754 754 flush = kwargs.pop("flush", False)
755 755 _print(*args, **kwargs)
756 756 if flush and fp is not None:
757 757 fp.flush()
758 758
759 759 _add_doc(reraise, """Reraise an exception.""")
760 760
761 761 if sys.version_info[0:2] < (3, 4):
762 762 def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS,
763 763 updated=functools.WRAPPER_UPDATES):
764 764 def wrapper(f):
765 765 f = functools.wraps(wrapped, assigned, updated)(f)
766 766 f.__wrapped__ = wrapped
767 767 return f
768 768 return wrapper
769 769 else:
770 770 wraps = functools.wraps
771 771
772 772 def with_metaclass(meta, *bases):
773 773 """Create a base class with a metaclass."""
774 774 # This requires a bit of explanation: the basic idea is to make a dummy
775 775 # metaclass for one level of class instantiation that replaces itself with
776 776 # the actual metaclass.
777 777 class metaclass(meta):
778 778 def __new__(cls, name, this_bases, d):
779 779 return meta(name, bases, d)
780 780 return type.__new__(metaclass, 'temporary_class', (), {})
781 781
782 782
783 783 def add_metaclass(metaclass):
784 784 """Class decorator for creating a class with a metaclass."""
785 785 def wrapper(cls):
786 786 orig_vars = cls.__dict__.copy()
787 787 slots = orig_vars.get('__slots__')
788 788 if slots is not None:
789 789 if isinstance(slots, str):
790 790 slots = [slots]
791 791 for slots_var in slots:
792 792 orig_vars.pop(slots_var)
793 793 orig_vars.pop('__dict__', None)
794 794 orig_vars.pop('__weakref__', None)
795 795 return metaclass(cls.__name__, cls.__bases__, orig_vars)
796 796 return wrapper
797 797
798 798
799 799 def python_2_unicode_compatible(klass):
800 800 """
801 801 A decorator that defines __unicode__ and __str__ methods under Python 2.
802 802 Under Python 3 it does nothing.
803 803
804 804 To support Python 2 and 3 with a single code base, define a __str__ method
805 805 returning text and apply this decorator to the class.
806 806 """
807 807 if PY2:
808 808 if '__str__' not in klass.__dict__:
809 809 raise ValueError("@python_2_unicode_compatible cannot be applied "
810 810 "to %s because it doesn't define __str__()." %
811 811 klass.__name__)
812 812 klass.__unicode__ = klass.__str__
813 813 klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
814 814 return klass
815 815
816 816
817 817 # Complete the moves implementation.
818 818 # This code is at the end of this module to speed up module loading.
819 819 # Turn this module into a package.
820 820 __path__ = [] # required for PEP 302 and PEP 451
821 821 __package__ = __name__ # see PEP 366 @ReservedAssignment
822 822 if globals().get("__spec__") is not None:
823 823 __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable
824 824 # Remove other six meta path importers, since they cause problems. This can
825 825 # happen if six is removed from sys.modules and then reloaded. (Setuptools does
826 826 # this for some reason.)
827 827 if sys.meta_path:
828 828 for i, importer in enumerate(sys.meta_path):
829 829 # Here's some real nastiness: Another "instance" of the six module might
830 830 # be floating around. Therefore, we can't use isinstance() to check for
831 831 # the six meta path importer, since the other six instance will have
832 832 # inserted an importer with different class.
833 833 if (type(importer).__name__ == "_SixMetaPathImporter" and
834 834 importer.name == __name__):
835 835 del sys.meta_path[i]
836 836 break
837 837 del i, importer
838 838 # Finally, add the importer to the meta path import hook.
839 839 sys.meta_path.append(_importer)
@@ -1,68 +1,68 b''
1 from __future__ import absolute_import, division, unicode_literals
1
2 2
3 3 import functools
4 4 from time import perf_counter as time_now
5 5
6 6
7 7 def safe_wraps(wrapper, *args, **kwargs):
8 8 """Safely wraps partial functions."""
9 9 while isinstance(wrapper, functools.partial):
10 10 wrapper = wrapper.func
11 11 return functools.wraps(wrapper, *args, **kwargs)
12 12
13 13
14 14 class Timer(object):
15 15 """A context manager/decorator for statsd.timing()."""
16 16
17 17 def __init__(self, client, stat, rate=1, tags=None, use_decimals=True, auto_send=True):
18 18 self.client = client
19 19 self.stat = stat
20 20 self.rate = rate
21 21 self.tags = tags
22 22 self.ms = None
23 23 self._sent = False
24 24 self._start_time = None
25 25 self.use_decimals = use_decimals
26 26 self.auto_send = auto_send
27 27
28 28 def __call__(self, f):
29 29 """Thread-safe timing function decorator."""
30 30 @safe_wraps(f)
31 31 def _wrapped(*args, **kwargs):
32 32 start_time = time_now()
33 33 try:
34 34 return f(*args, **kwargs)
35 35 finally:
36 36 elapsed_time_ms = 1000.0 * (time_now() - start_time)
37 37 self.client.timing(self.stat, elapsed_time_ms, self.rate, self.tags, self.use_decimals)
38 38 self._sent = True
39 39 return _wrapped
40 40
41 41 def __enter__(self):
42 42 return self.start()
43 43
44 44 def __exit__(self, typ, value, tb):
45 45 self.stop(send=self.auto_send)
46 46
47 47 def start(self):
48 48 self.ms = None
49 49 self._sent = False
50 50 self._start_time = time_now()
51 51 return self
52 52
53 53 def stop(self, send=True):
54 54 if self._start_time is None:
55 55 raise RuntimeError('Timer has not started.')
56 56 dt = time_now() - self._start_time
57 57 self.ms = 1000.0 * dt # Convert to milliseconds.
58 58 if send:
59 59 self.send()
60 60 return self
61 61
62 62 def send(self):
63 63 if self.ms is None:
64 64 raise RuntimeError('No data recorded.')
65 65 if self._sent:
66 66 raise RuntimeError('Already sent data.')
67 67 self._sent = True
68 68 self.client.timing(self.stat, self.ms, self.rate, self.tags, self.use_decimals)
@@ -1,1919 +1,1919 b''
1 1
2 from __future__ import division
2
3 3
4 4 """Diff Match and Patch
5 5
6 6 Copyright 2006 Google Inc.
7 7 http://code.google.com/p/google-diff-match-patch/
8 8
9 9 Licensed under the Apache License, Version 2.0 (the "License");
10 10 you may not use this file except in compliance with the License.
11 11 You may obtain a copy of the License at
12 12
13 13 http://www.apache.org/licenses/LICENSE-2.0
14 14
15 15 Unless required by applicable law or agreed to in writing, software
16 16 distributed under the License is distributed on an "AS IS" BASIS,
17 17 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 18 See the License for the specific language governing permissions and
19 19 limitations under the License.
20 20 """
21 21
22 22 """Functions for diff, match and patch.
23 23
24 24 Computes the difference between two texts to create a patch.
25 25 Applies the patch onto another text, allowing for errors.
26 26 """
27 27
28 28 __author__ = 'fraser@google.com (Neil Fraser)'
29 29
30 30 import math
31 31 import re
32 32 import sys
33 33 import time
34 34 import urllib
35 35
36 36
37 37 class diff_match_patch:
38 38 """Class containing the diff, match and patch methods.
39 39
40 40 Also contains the behaviour settings.
41 41 """
42 42
43 43 def __init__(self):
44 44 """Inits a diff_match_patch object with default settings.
45 45 Redefine these in your program to override the defaults.
46 46 """
47 47
48 48 # Number of seconds to map a diff before giving up (0 for infinity).
49 49 self.Diff_Timeout = 1.0
50 50 # Cost of an empty edit operation in terms of edit characters.
51 51 self.Diff_EditCost = 4
52 52 # At what point is no match declared (0.0 = perfection, 1.0 = very loose).
53 53 self.Match_Threshold = 0.5
54 54 # How far to search for a match (0 = exact location, 1000+ = broad match).
55 55 # A match this many characters away from the expected location will add
56 56 # 1.0 to the score (0.0 is a perfect match).
57 57 self.Match_Distance = 1000
58 58 # When deleting a large block of text (over ~64 characters), how close do
59 59 # the contents have to be to match the expected contents. (0.0 = perfection,
60 60 # 1.0 = very loose). Note that Match_Threshold controls how closely the
61 61 # end points of a delete need to match.
62 62 self.Patch_DeleteThreshold = 0.5
63 63 # Chunk size for context length.
64 64 self.Patch_Margin = 4
65 65
66 66 # The number of bits in an int.
67 67 # Python has no maximum, thus to disable patch splitting set to 0.
68 68 # However to avoid long patches in certain pathological cases, use 32.
69 69 # Multiple short patches (using native ints) are much faster than long ones.
70 70 self.Match_MaxBits = 32
71 71
72 72 # DIFF FUNCTIONS
73 73
74 74 # The data structure representing a diff is an array of tuples:
75 75 # [(DIFF_DELETE, "Hello"), (DIFF_INSERT, "Goodbye"), (DIFF_EQUAL, " world.")]
76 76 # which means: delete "Hello", add "Goodbye" and keep " world."
77 77 DIFF_DELETE = -1
78 78 DIFF_INSERT = 1
79 79 DIFF_EQUAL = 0
80 80
81 81 def diff_main(self, text1, text2, checklines=True, deadline=None):
82 82 """Find the differences between two texts. Simplifies the problem by
83 83 stripping any common prefix or suffix off the texts before diffing.
84 84
85 85 Args:
86 86 text1: Old string to be diffed.
87 87 text2: New string to be diffed.
88 88 checklines: Optional speedup flag. If present and false, then don't run
89 89 a line-level diff first to identify the changed areas.
90 90 Defaults to true, which does a faster, slightly less optimal diff.
91 91 deadline: Optional time when the diff should be complete by. Used
92 92 internally for recursive calls. Users should set DiffTimeout instead.
93 93
94 94 Returns:
95 95 Array of changes.
96 96 """
97 97 # Set a deadline by which time the diff must be complete.
98 98 if deadline is None:
99 99 # Unlike in most languages, Python counts time in seconds.
100 100 if self.Diff_Timeout <= 0:
101 101 deadline = sys.maxint
102 102 else:
103 103 deadline = time.time() + self.Diff_Timeout
104 104
105 105 # Check for null inputs.
106 106 if text1 is None or text2 is None:
107 107 raise ValueError("Null inputs. (diff_main)")
108 108
109 109 # Check for equality (speedup).
110 110 if text1 == text2:
111 111 if text1:
112 112 return [(self.DIFF_EQUAL, text1)]
113 113 return []
114 114
115 115 # Trim off common prefix (speedup).
116 116 commonlength = self.diff_commonPrefix(text1, text2)
117 117 commonprefix = text1[:commonlength]
118 118 text1 = text1[commonlength:]
119 119 text2 = text2[commonlength:]
120 120
121 121 # Trim off common suffix (speedup).
122 122 commonlength = self.diff_commonSuffix(text1, text2)
123 123 if commonlength == 0:
124 124 commonsuffix = ''
125 125 else:
126 126 commonsuffix = text1[-commonlength:]
127 127 text1 = text1[:-commonlength]
128 128 text2 = text2[:-commonlength]
129 129
130 130 # Compute the diff on the middle block.
131 131 diffs = self.diff_compute(text1, text2, checklines, deadline)
132 132
133 133 # Restore the prefix and suffix.
134 134 if commonprefix:
135 135 diffs[:0] = [(self.DIFF_EQUAL, commonprefix)]
136 136 if commonsuffix:
137 137 diffs.append((self.DIFF_EQUAL, commonsuffix))
138 138 self.diff_cleanupMerge(diffs)
139 139 return diffs
140 140
141 141 def diff_compute(self, text1, text2, checklines, deadline):
142 142 """Find the differences between two texts. Assumes that the texts do not
143 143 have any common prefix or suffix.
144 144
145 145 Args:
146 146 text1: Old string to be diffed.
147 147 text2: New string to be diffed.
148 148 checklines: Speedup flag. If false, then don't run a line-level diff
149 149 first to identify the changed areas.
150 150 If true, then run a faster, slightly less optimal diff.
151 151 deadline: Time when the diff should be complete by.
152 152
153 153 Returns:
154 154 Array of changes.
155 155 """
156 156 if not text1:
157 157 # Just add some text (speedup).
158 158 return [(self.DIFF_INSERT, text2)]
159 159
160 160 if not text2:
161 161 # Just delete some text (speedup).
162 162 return [(self.DIFF_DELETE, text1)]
163 163
164 164 if len(text1) > len(text2):
165 165 (longtext, shorttext) = (text1, text2)
166 166 else:
167 167 (shorttext, longtext) = (text1, text2)
168 168 i = longtext.find(shorttext)
169 169 if i != -1:
170 170 # Shorter text is inside the longer text (speedup).
171 171 diffs = [(self.DIFF_INSERT, longtext[:i]), (self.DIFF_EQUAL, shorttext),
172 172 (self.DIFF_INSERT, longtext[i + len(shorttext):])]
173 173 # Swap insertions for deletions if diff is reversed.
174 174 if len(text1) > len(text2):
175 175 diffs[0] = (self.DIFF_DELETE, diffs[0][1])
176 176 diffs[2] = (self.DIFF_DELETE, diffs[2][1])
177 177 return diffs
178 178
179 179 if len(shorttext) == 1:
180 180 # Single character string.
181 181 # After the previous speedup, the character can't be an equality.
182 182 return [(self.DIFF_DELETE, text1), (self.DIFF_INSERT, text2)]
183 183
184 184 # Check to see if the problem can be split in two.
185 185 hm = self.diff_halfMatch(text1, text2)
186 186 if hm:
187 187 # A half-match was found, sort out the return data.
188 188 (text1_a, text1_b, text2_a, text2_b, mid_common) = hm
189 189 # Send both pairs off for separate processing.
190 190 diffs_a = self.diff_main(text1_a, text2_a, checklines, deadline)
191 191 diffs_b = self.diff_main(text1_b, text2_b, checklines, deadline)
192 192 # Merge the results.
193 193 return diffs_a + [(self.DIFF_EQUAL, mid_common)] + diffs_b
194 194
195 195 if checklines and len(text1) > 100 and len(text2) > 100:
196 196 return self.diff_lineMode(text1, text2, deadline)
197 197
198 198 return self.diff_bisect(text1, text2, deadline)
199 199
200 200 def diff_lineMode(self, text1, text2, deadline):
201 201 """Do a quick line-level diff on both strings, then rediff the parts for
202 202 greater accuracy.
203 203 This speedup can produce non-minimal diffs.
204 204
205 205 Args:
206 206 text1: Old string to be diffed.
207 207 text2: New string to be diffed.
208 208 deadline: Time when the diff should be complete by.
209 209
210 210 Returns:
211 211 Array of changes.
212 212 """
213 213
214 214 # Scan the text on a line-by-line basis first.
215 215 (text1, text2, linearray) = self.diff_linesToChars(text1, text2)
216 216
217 217 diffs = self.diff_main(text1, text2, False, deadline)
218 218
219 219 # Convert the diff back to original text.
220 220 self.diff_charsToLines(diffs, linearray)
221 221 # Eliminate freak matches (e.g. blank lines)
222 222 self.diff_cleanupSemantic(diffs)
223 223
224 224 # Rediff any replacement blocks, this time character-by-character.
225 225 # Add a dummy entry at the end.
226 226 diffs.append((self.DIFF_EQUAL, ''))
227 227 pointer = 0
228 228 count_delete = 0
229 229 count_insert = 0
230 230 text_delete = ''
231 231 text_insert = ''
232 232 while pointer < len(diffs):
233 233 if diffs[pointer][0] == self.DIFF_INSERT:
234 234 count_insert += 1
235 235 text_insert += diffs[pointer][1]
236 236 elif diffs[pointer][0] == self.DIFF_DELETE:
237 237 count_delete += 1
238 238 text_delete += diffs[pointer][1]
239 239 elif diffs[pointer][0] == self.DIFF_EQUAL:
240 240 # Upon reaching an equality, check for prior redundancies.
241 241 if count_delete >= 1 and count_insert >= 1:
242 242 # Delete the offending records and add the merged ones.
243 243 a = self.diff_main(text_delete, text_insert, False, deadline)
244 244 diffs[pointer - count_delete - count_insert : pointer] = a
245 245 pointer = pointer - count_delete - count_insert + len(a)
246 246 count_insert = 0
247 247 count_delete = 0
248 248 text_delete = ''
249 249 text_insert = ''
250 250
251 251 pointer += 1
252 252
253 253 diffs.pop() # Remove the dummy entry at the end.
254 254
255 255 return diffs
256 256
257 257 def diff_bisect(self, text1, text2, deadline):
258 258 """Find the 'middle snake' of a diff, split the problem in two
259 259 and return the recursively constructed diff.
260 260 See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
261 261
262 262 Args:
263 263 text1: Old string to be diffed.
264 264 text2: New string to be diffed.
265 265 deadline: Time at which to bail if not yet complete.
266 266
267 267 Returns:
268 268 Array of diff tuples.
269 269 """
270 270
271 271 # Cache the text lengths to prevent multiple calls.
272 272 text1_length = len(text1)
273 273 text2_length = len(text2)
274 274 max_d = (text1_length + text2_length + 1) // 2
275 275 v_offset = max_d
276 276 v_length = 2 * max_d
277 277 v1 = [-1] * v_length
278 278 v1[v_offset + 1] = 0
279 279 v2 = v1[:]
280 280 delta = text1_length - text2_length
281 281 # If the total number of characters is odd, then the front path will
282 282 # collide with the reverse path.
283 283 front = (delta % 2 != 0)
284 284 # Offsets for start and end of k loop.
285 285 # Prevents mapping of space beyond the grid.
286 286 k1start = 0
287 287 k1end = 0
288 288 k2start = 0
289 289 k2end = 0
290 290 for d in range(max_d):
291 291 # Bail out if deadline is reached.
292 292 if time.time() > deadline:
293 293 break
294 294
295 295 # Walk the front path one step.
296 296 for k1 in range(-d + k1start, d + 1 - k1end, 2):
297 297 k1_offset = v_offset + k1
298 298 if k1 == -d or (k1 != d and
299 299 v1[k1_offset - 1] < v1[k1_offset + 1]):
300 300 x1 = v1[k1_offset + 1]
301 301 else:
302 302 x1 = v1[k1_offset - 1] + 1
303 303 y1 = x1 - k1
304 304 while (x1 < text1_length and y1 < text2_length and
305 305 text1[x1] == text2[y1]):
306 306 x1 += 1
307 307 y1 += 1
308 308 v1[k1_offset] = x1
309 309 if x1 > text1_length:
310 310 # Ran off the right of the graph.
311 311 k1end += 2
312 312 elif y1 > text2_length:
313 313 # Ran off the bottom of the graph.
314 314 k1start += 2
315 315 elif front:
316 316 k2_offset = v_offset + delta - k1
317 317 if k2_offset >= 0 and k2_offset < v_length and v2[k2_offset] != -1:
318 318 # Mirror x2 onto top-left coordinate system.
319 319 x2 = text1_length - v2[k2_offset]
320 320 if x1 >= x2:
321 321 # Overlap detected.
322 322 return self.diff_bisectSplit(text1, text2, x1, y1, deadline)
323 323
324 324 # Walk the reverse path one step.
325 325 for k2 in range(-d + k2start, d + 1 - k2end, 2):
326 326 k2_offset = v_offset + k2
327 327 if k2 == -d or (k2 != d and
328 328 v2[k2_offset - 1] < v2[k2_offset + 1]):
329 329 x2 = v2[k2_offset + 1]
330 330 else:
331 331 x2 = v2[k2_offset - 1] + 1
332 332 y2 = x2 - k2
333 333 while (x2 < text1_length and y2 < text2_length and
334 334 text1[-x2 - 1] == text2[-y2 - 1]):
335 335 x2 += 1
336 336 y2 += 1
337 337 v2[k2_offset] = x2
338 338 if x2 > text1_length:
339 339 # Ran off the left of the graph.
340 340 k2end += 2
341 341 elif y2 > text2_length:
342 342 # Ran off the top of the graph.
343 343 k2start += 2
344 344 elif not front:
345 345 k1_offset = v_offset + delta - k2
346 346 if k1_offset >= 0 and k1_offset < v_length and v1[k1_offset] != -1:
347 347 x1 = v1[k1_offset]
348 348 y1 = v_offset + x1 - k1_offset
349 349 # Mirror x2 onto top-left coordinate system.
350 350 x2 = text1_length - x2
351 351 if x1 >= x2:
352 352 # Overlap detected.
353 353 return self.diff_bisectSplit(text1, text2, x1, y1, deadline)
354 354
355 355 # Diff took too long and hit the deadline or
356 356 # number of diffs equals number of characters, no commonality at all.
357 357 return [(self.DIFF_DELETE, text1), (self.DIFF_INSERT, text2)]
358 358
359 359 def diff_bisectSplit(self, text1, text2, x, y, deadline):
360 360 """Given the location of the 'middle snake', split the diff in two parts
361 361 and recurse.
362 362
363 363 Args:
364 364 text1: Old string to be diffed.
365 365 text2: New string to be diffed.
366 366 x: Index of split point in text1.
367 367 y: Index of split point in text2.
368 368 deadline: Time at which to bail if not yet complete.
369 369
370 370 Returns:
371 371 Array of diff tuples.
372 372 """
373 373 text1a = text1[:x]
374 374 text2a = text2[:y]
375 375 text1b = text1[x:]
376 376 text2b = text2[y:]
377 377
378 378 # Compute both diffs serially.
379 379 diffs = self.diff_main(text1a, text2a, False, deadline)
380 380 diffsb = self.diff_main(text1b, text2b, False, deadline)
381 381
382 382 return diffs + diffsb
383 383
384 384 def diff_linesToChars(self, text1, text2):
385 385 """Split two texts into an array of strings. Reduce the texts to a string
386 386 of hashes where each Unicode character represents one line.
387 387
388 388 Args:
389 389 text1: First string.
390 390 text2: Second string.
391 391
392 392 Returns:
393 393 Three element tuple, containing the encoded text1, the encoded text2 and
394 394 the array of unique strings. The zeroth element of the array of unique
395 395 strings is intentionally blank.
396 396 """
397 397 lineArray = [] # e.g. lineArray[4] == "Hello\n"
398 398 lineHash = {} # e.g. lineHash["Hello\n"] == 4
399 399
400 400 # "\x00" is a valid character, but various debuggers don't like it.
401 401 # So we'll insert a junk entry to avoid generating a null character.
402 402 lineArray.append('')
403 403
404 404 def diff_linesToCharsMunge(text):
405 405 """Split a text into an array of strings. Reduce the texts to a string
406 406 of hashes where each Unicode character represents one line.
407 407 Modifies linearray and linehash through being a closure.
408 408
409 409 Args:
410 410 text: String to encode.
411 411
412 412 Returns:
413 413 Encoded string.
414 414 """
415 415 chars = []
416 416 # Walk the text, pulling out a substring for each line.
417 417 # text.split('\n') would would temporarily double our memory footprint.
418 418 # Modifying text would create many large strings to garbage collect.
419 419 lineStart = 0
420 420 lineEnd = -1
421 421 while lineEnd < len(text) - 1:
422 422 lineEnd = text.find('\n', lineStart)
423 423 if lineEnd == -1:
424 424 lineEnd = len(text) - 1
425 425 line = text[lineStart:lineEnd + 1]
426 426 lineStart = lineEnd + 1
427 427
428 428 if line in lineHash:
429 429 chars.append(unichr(lineHash[line]))
430 430 else:
431 431 lineArray.append(line)
432 432 lineHash[line] = len(lineArray) - 1
433 433 chars.append(unichr(len(lineArray) - 1))
434 434 return "".join(chars)
435 435
436 436 chars1 = diff_linesToCharsMunge(text1)
437 437 chars2 = diff_linesToCharsMunge(text2)
438 438 return (chars1, chars2, lineArray)
439 439
440 440 def diff_charsToLines(self, diffs, lineArray):
441 441 """Rehydrate the text in a diff from a string of line hashes to real lines
442 442 of text.
443 443
444 444 Args:
445 445 diffs: Array of diff tuples.
446 446 lineArray: Array of unique strings.
447 447 """
448 448 for x in range(len(diffs)):
449 449 text = []
450 450 for char in diffs[x][1]:
451 451 text.append(lineArray[ord(char)])
452 452 diffs[x] = (diffs[x][0], "".join(text))
453 453
454 454 def diff_commonPrefix(self, text1, text2):
455 455 """Determine the common prefix of two strings.
456 456
457 457 Args:
458 458 text1: First string.
459 459 text2: Second string.
460 460
461 461 Returns:
462 462 The number of characters common to the start of each string.
463 463 """
464 464 # Quick check for common null cases.
465 465 if not text1 or not text2 or text1[0] != text2[0]:
466 466 return 0
467 467 # Binary search.
468 468 # Performance analysis: http://neil.fraser.name/news/2007/10/09/
469 469 pointermin = 0
470 470 pointermax = min(len(text1), len(text2))
471 471 pointermid = pointermax
472 472 pointerstart = 0
473 473 while pointermin < pointermid:
474 474 if text1[pointerstart:pointermid] == text2[pointerstart:pointermid]:
475 475 pointermin = pointermid
476 476 pointerstart = pointermin
477 477 else:
478 478 pointermax = pointermid
479 479 pointermid = (pointermax - pointermin) // 2 + pointermin
480 480 return pointermid
481 481
482 482 def diff_commonSuffix(self, text1, text2):
483 483 """Determine the common suffix of two strings.
484 484
485 485 Args:
486 486 text1: First string.
487 487 text2: Second string.
488 488
489 489 Returns:
490 490 The number of characters common to the end of each string.
491 491 """
492 492 # Quick check for common null cases.
493 493 if not text1 or not text2 or text1[-1] != text2[-1]:
494 494 return 0
495 495 # Binary search.
496 496 # Performance analysis: http://neil.fraser.name/news/2007/10/09/
497 497 pointermin = 0
498 498 pointermax = min(len(text1), len(text2))
499 499 pointermid = pointermax
500 500 pointerend = 0
501 501 while pointermin < pointermid:
502 502 if (text1[-pointermid:len(text1) - pointerend] ==
503 503 text2[-pointermid:len(text2) - pointerend]):
504 504 pointermin = pointermid
505 505 pointerend = pointermin
506 506 else:
507 507 pointermax = pointermid
508 508 pointermid = (pointermax - pointermin) // 2 + pointermin
509 509 return pointermid
510 510
511 511 def diff_commonOverlap(self, text1, text2):
512 512 """Determine if the suffix of one string is the prefix of another.
513 513
514 514 Args:
515 515 text1 First string.
516 516 text2 Second string.
517 517
518 518 Returns:
519 519 The number of characters common to the end of the first
520 520 string and the start of the second string.
521 521 """
522 522 # Cache the text lengths to prevent multiple calls.
523 523 text1_length = len(text1)
524 524 text2_length = len(text2)
525 525 # Eliminate the null case.
526 526 if text1_length == 0 or text2_length == 0:
527 527 return 0
528 528 # Truncate the longer string.
529 529 if text1_length > text2_length:
530 530 text1 = text1[-text2_length:]
531 531 elif text1_length < text2_length:
532 532 text2 = text2[:text1_length]
533 533 text_length = min(text1_length, text2_length)
534 534 # Quick check for the worst case.
535 535 if text1 == text2:
536 536 return text_length
537 537
538 538 # Start by looking for a single character match
539 539 # and increase length until no match is found.
540 540 # Performance analysis: http://neil.fraser.name/news/2010/11/04/
541 541 best = 0
542 542 length = 1
543 543 while True:
544 544 pattern = text1[-length:]
545 545 found = text2.find(pattern)
546 546 if found == -1:
547 547 return best
548 548 length += found
549 549 if found == 0 or text1[-length:] == text2[:length]:
550 550 best = length
551 551 length += 1
552 552
553 553 def diff_halfMatch(self, text1, text2):
554 554 """Do the two texts share a substring which is at least half the length of
555 555 the longer text?
556 556 This speedup can produce non-minimal diffs.
557 557
558 558 Args:
559 559 text1: First string.
560 560 text2: Second string.
561 561
562 562 Returns:
563 563 Five element Array, containing the prefix of text1, the suffix of text1,
564 564 the prefix of text2, the suffix of text2 and the common middle. Or None
565 565 if there was no match.
566 566 """
567 567 if self.Diff_Timeout <= 0:
568 568 # Don't risk returning a non-optimal diff if we have unlimited time.
569 569 return None
570 570 if len(text1) > len(text2):
571 571 (longtext, shorttext) = (text1, text2)
572 572 else:
573 573 (shorttext, longtext) = (text1, text2)
574 574 if len(longtext) < 4 or len(shorttext) * 2 < len(longtext):
575 575 return None # Pointless.
576 576
577 577 def diff_halfMatchI(longtext, shorttext, i):
578 578 """Does a substring of shorttext exist within longtext such that the
579 579 substring is at least half the length of longtext?
580 580 Closure, but does not reference any external variables.
581 581
582 582 Args:
583 583 longtext: Longer string.
584 584 shorttext: Shorter string.
585 585 i: Start index of quarter length substring within longtext.
586 586
587 587 Returns:
588 588 Five element Array, containing the prefix of longtext, the suffix of
589 589 longtext, the prefix of shorttext, the suffix of shorttext and the
590 590 common middle. Or None if there was no match.
591 591 """
592 592 seed = longtext[i:i + len(longtext) // 4]
593 593 best_common = ''
594 594 j = shorttext.find(seed)
595 595 while j != -1:
596 596 prefixLength = self.diff_commonPrefix(longtext[i:], shorttext[j:])
597 597 suffixLength = self.diff_commonSuffix(longtext[:i], shorttext[:j])
598 598 if len(best_common) < suffixLength + prefixLength:
599 599 best_common = (shorttext[j - suffixLength:j] +
600 600 shorttext[j:j + prefixLength])
601 601 best_longtext_a = longtext[:i - suffixLength]
602 602 best_longtext_b = longtext[i + prefixLength:]
603 603 best_shorttext_a = shorttext[:j - suffixLength]
604 604 best_shorttext_b = shorttext[j + prefixLength:]
605 605 j = shorttext.find(seed, j + 1)
606 606
607 607 if len(best_common) * 2 >= len(longtext):
608 608 return (best_longtext_a, best_longtext_b,
609 609 best_shorttext_a, best_shorttext_b, best_common)
610 610 else:
611 611 return None
612 612
613 613 # First check if the second quarter is the seed for a half-match.
614 614 hm1 = diff_halfMatchI(longtext, shorttext, (len(longtext) + 3) // 4)
615 615 # Check again based on the third quarter.
616 616 hm2 = diff_halfMatchI(longtext, shorttext, (len(longtext) + 1) // 2)
617 617 if not hm1 and not hm2:
618 618 return None
619 619 elif not hm2:
620 620 hm = hm1
621 621 elif not hm1:
622 622 hm = hm2
623 623 else:
624 624 # Both matched. Select the longest.
625 625 if len(hm1[4]) > len(hm2[4]):
626 626 hm = hm1
627 627 else:
628 628 hm = hm2
629 629
630 630 # A half-match was found, sort out the return data.
631 631 if len(text1) > len(text2):
632 632 (text1_a, text1_b, text2_a, text2_b, mid_common) = hm
633 633 else:
634 634 (text2_a, text2_b, text1_a, text1_b, mid_common) = hm
635 635 return (text1_a, text1_b, text2_a, text2_b, mid_common)
636 636
637 637 def diff_cleanupSemantic(self, diffs):
638 638 """Reduce the number of edits by eliminating semantically trivial
639 639 equalities.
640 640
641 641 Args:
642 642 diffs: Array of diff tuples.
643 643 """
644 644 changes = False
645 645 equalities = [] # Stack of indices where equalities are found.
646 646 lastequality = None # Always equal to diffs[equalities[-1]][1]
647 647 pointer = 0 # Index of current position.
648 648 # Number of chars that changed prior to the equality.
649 649 length_insertions1, length_deletions1 = 0, 0
650 650 # Number of chars that changed after the equality.
651 651 length_insertions2, length_deletions2 = 0, 0
652 652 while pointer < len(diffs):
653 653 if diffs[pointer][0] == self.DIFF_EQUAL: # Equality found.
654 654 equalities.append(pointer)
655 655 length_insertions1, length_insertions2 = length_insertions2, 0
656 656 length_deletions1, length_deletions2 = length_deletions2, 0
657 657 lastequality = diffs[pointer][1]
658 658 else: # An insertion or deletion.
659 659 if diffs[pointer][0] == self.DIFF_INSERT:
660 660 length_insertions2 += len(diffs[pointer][1])
661 661 else:
662 662 length_deletions2 += len(diffs[pointer][1])
663 663 # Eliminate an equality that is smaller or equal to the edits on both
664 664 # sides of it.
665 665 if (lastequality and (len(lastequality) <=
666 666 max(length_insertions1, length_deletions1)) and
667 667 (len(lastequality) <= max(length_insertions2, length_deletions2))):
668 668 # Duplicate record.
669 669 diffs.insert(equalities[-1], (self.DIFF_DELETE, lastequality))
670 670 # Change second copy to insert.
671 671 diffs[equalities[-1] + 1] = (self.DIFF_INSERT,
672 672 diffs[equalities[-1] + 1][1])
673 673 # Throw away the equality we just deleted.
674 674 equalities.pop()
675 675 # Throw away the previous equality (it needs to be reevaluated).
676 676 if len(equalities):
677 677 equalities.pop()
678 678 if len(equalities):
679 679 pointer = equalities[-1]
680 680 else:
681 681 pointer = -1
682 682 # Reset the counters.
683 683 length_insertions1, length_deletions1 = 0, 0
684 684 length_insertions2, length_deletions2 = 0, 0
685 685 lastequality = None
686 686 changes = True
687 687 pointer += 1
688 688
689 689 # Normalize the diff.
690 690 if changes:
691 691 self.diff_cleanupMerge(diffs)
692 692 self.diff_cleanupSemanticLossless(diffs)
693 693
694 694 # Find any overlaps between deletions and insertions.
695 695 # e.g: <del>abcxxx</del><ins>xxxdef</ins>
696 696 # -> <del>abc</del>xxx<ins>def</ins>
697 697 # e.g: <del>xxxabc</del><ins>defxxx</ins>
698 698 # -> <ins>def</ins>xxx<del>abc</del>
699 699 # Only extract an overlap if it is as big as the edit ahead or behind it.
700 700 pointer = 1
701 701 while pointer < len(diffs):
702 702 if (diffs[pointer - 1][0] == self.DIFF_DELETE and
703 703 diffs[pointer][0] == self.DIFF_INSERT):
704 704 deletion = diffs[pointer - 1][1]
705 705 insertion = diffs[pointer][1]
706 706 overlap_length1 = self.diff_commonOverlap(deletion, insertion)
707 707 overlap_length2 = self.diff_commonOverlap(insertion, deletion)
708 708 if overlap_length1 >= overlap_length2:
709 709 if (overlap_length1 >= len(deletion) / 2.0 or
710 710 overlap_length1 >= len(insertion) / 2.0):
711 711 # Overlap found. Insert an equality and trim the surrounding edits.
712 712 diffs.insert(pointer, (self.DIFF_EQUAL,
713 713 insertion[:overlap_length1]))
714 714 diffs[pointer - 1] = (self.DIFF_DELETE,
715 715 deletion[:len(deletion) - overlap_length1])
716 716 diffs[pointer + 1] = (self.DIFF_INSERT,
717 717 insertion[overlap_length1:])
718 718 pointer += 1
719 719 else:
720 720 if (overlap_length2 >= len(deletion) / 2.0 or
721 721 overlap_length2 >= len(insertion) / 2.0):
722 722 # Reverse overlap found.
723 723 # Insert an equality and swap and trim the surrounding edits.
724 724 diffs.insert(pointer, (self.DIFF_EQUAL, deletion[:overlap_length2]))
725 725 diffs[pointer - 1] = (self.DIFF_INSERT,
726 726 insertion[:len(insertion) - overlap_length2])
727 727 diffs[pointer + 1] = (self.DIFF_DELETE, deletion[overlap_length2:])
728 728 pointer += 1
729 729 pointer += 1
730 730 pointer += 1
731 731
732 732 def diff_cleanupSemanticLossless(self, diffs):
733 733 """Look for single edits surrounded on both sides by equalities
734 734 which can be shifted sideways to align the edit to a word boundary.
735 735 e.g: The c<ins>at c</ins>ame. -> The <ins>cat </ins>came.
736 736
737 737 Args:
738 738 diffs: Array of diff tuples.
739 739 """
740 740
741 741 def diff_cleanupSemanticScore(one, two):
742 742 """Given two strings, compute a score representing whether the
743 743 internal boundary falls on logical boundaries.
744 744 Scores range from 6 (best) to 0 (worst).
745 745 Closure, but does not reference any external variables.
746 746
747 747 Args:
748 748 one: First string.
749 749 two: Second string.
750 750
751 751 Returns:
752 752 The score.
753 753 """
754 754 if not one or not two:
755 755 # Edges are the best.
756 756 return 6
757 757
758 758 # Each port of this function behaves slightly differently due to
759 759 # subtle differences in each language's definition of things like
760 760 # 'whitespace'. Since this function's purpose is largely cosmetic,
761 761 # the choice has been made to use each language's native features
762 762 # rather than force total conformity.
763 763 char1 = one[-1]
764 764 char2 = two[0]
765 765 nonAlphaNumeric1 = not char1.isalnum()
766 766 nonAlphaNumeric2 = not char2.isalnum()
767 767 whitespace1 = nonAlphaNumeric1 and char1.isspace()
768 768 whitespace2 = nonAlphaNumeric2 and char2.isspace()
769 769 lineBreak1 = whitespace1 and (char1 == "\r" or char1 == "\n")
770 770 lineBreak2 = whitespace2 and (char2 == "\r" or char2 == "\n")
771 771 blankLine1 = lineBreak1 and self.BLANKLINEEND.search(one)
772 772 blankLine2 = lineBreak2 and self.BLANKLINESTART.match(two)
773 773
774 774 if blankLine1 or blankLine2:
775 775 # Five points for blank lines.
776 776 return 5
777 777 elif lineBreak1 or lineBreak2:
778 778 # Four points for line breaks.
779 779 return 4
780 780 elif nonAlphaNumeric1 and not whitespace1 and whitespace2:
781 781 # Three points for end of sentences.
782 782 return 3
783 783 elif whitespace1 or whitespace2:
784 784 # Two points for whitespace.
785 785 return 2
786 786 elif nonAlphaNumeric1 or nonAlphaNumeric2:
787 787 # One point for non-alphanumeric.
788 788 return 1
789 789 return 0
790 790
791 791 pointer = 1
792 792 # Intentionally ignore the first and last element (don't need checking).
793 793 while pointer < len(diffs) - 1:
794 794 if (diffs[pointer - 1][0] == self.DIFF_EQUAL and
795 795 diffs[pointer + 1][0] == self.DIFF_EQUAL):
796 796 # This is a single edit surrounded by equalities.
797 797 equality1 = diffs[pointer - 1][1]
798 798 edit = diffs[pointer][1]
799 799 equality2 = diffs[pointer + 1][1]
800 800
801 801 # First, shift the edit as far left as possible.
802 802 commonOffset = self.diff_commonSuffix(equality1, edit)
803 803 if commonOffset:
804 804 commonString = edit[-commonOffset:]
805 805 equality1 = equality1[:-commonOffset]
806 806 edit = commonString + edit[:-commonOffset]
807 807 equality2 = commonString + equality2
808 808
809 809 # Second, step character by character right, looking for the best fit.
810 810 bestEquality1 = equality1
811 811 bestEdit = edit
812 812 bestEquality2 = equality2
813 813 bestScore = (diff_cleanupSemanticScore(equality1, edit) +
814 814 diff_cleanupSemanticScore(edit, equality2))
815 815 while edit and equality2 and edit[0] == equality2[0]:
816 816 equality1 += edit[0]
817 817 edit = edit[1:] + equality2[0]
818 818 equality2 = equality2[1:]
819 819 score = (diff_cleanupSemanticScore(equality1, edit) +
820 820 diff_cleanupSemanticScore(edit, equality2))
821 821 # The >= encourages trailing rather than leading whitespace on edits.
822 822 if score >= bestScore:
823 823 bestScore = score
824 824 bestEquality1 = equality1
825 825 bestEdit = edit
826 826 bestEquality2 = equality2
827 827
828 828 if diffs[pointer - 1][1] != bestEquality1:
829 829 # We have an improvement, save it back to the diff.
830 830 if bestEquality1:
831 831 diffs[pointer - 1] = (diffs[pointer - 1][0], bestEquality1)
832 832 else:
833 833 del diffs[pointer - 1]
834 834 pointer -= 1
835 835 diffs[pointer] = (diffs[pointer][0], bestEdit)
836 836 if bestEquality2:
837 837 diffs[pointer + 1] = (diffs[pointer + 1][0], bestEquality2)
838 838 else:
839 839 del diffs[pointer + 1]
840 840 pointer -= 1
841 841 pointer += 1
842 842
843 843 # Define some regex patterns for matching boundaries.
844 844 BLANKLINEEND = re.compile(r"\n\r?\n$");
845 845 BLANKLINESTART = re.compile(r"^\r?\n\r?\n");
846 846
847 847 def diff_cleanupEfficiency(self, diffs):
848 848 """Reduce the number of edits by eliminating operationally trivial
849 849 equalities.
850 850
851 851 Args:
852 852 diffs: Array of diff tuples.
853 853 """
854 854 changes = False
855 855 equalities = [] # Stack of indices where equalities are found.
856 856 lastequality = None # Always equal to diffs[equalities[-1]][1]
857 857 pointer = 0 # Index of current position.
858 858 pre_ins = False # Is there an insertion operation before the last equality.
859 859 pre_del = False # Is there a deletion operation before the last equality.
860 860 post_ins = False # Is there an insertion operation after the last equality.
861 861 post_del = False # Is there a deletion operation after the last equality.
862 862 while pointer < len(diffs):
863 863 if diffs[pointer][0] == self.DIFF_EQUAL: # Equality found.
864 864 if (len(diffs[pointer][1]) < self.Diff_EditCost and
865 865 (post_ins or post_del)):
866 866 # Candidate found.
867 867 equalities.append(pointer)
868 868 pre_ins = post_ins
869 869 pre_del = post_del
870 870 lastequality = diffs[pointer][1]
871 871 else:
872 872 # Not a candidate, and can never become one.
873 873 equalities = []
874 874 lastequality = None
875 875
876 876 post_ins = post_del = False
877 877 else: # An insertion or deletion.
878 878 if diffs[pointer][0] == self.DIFF_DELETE:
879 879 post_del = True
880 880 else:
881 881 post_ins = True
882 882
883 883 # Five types to be split:
884 884 # <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del>
885 885 # <ins>A</ins>X<ins>C</ins><del>D</del>
886 886 # <ins>A</ins><del>B</del>X<ins>C</ins>
887 887 # <ins>A</del>X<ins>C</ins><del>D</del>
888 888 # <ins>A</ins><del>B</del>X<del>C</del>
889 889
890 890 if lastequality and ((pre_ins and pre_del and post_ins and post_del) or
891 891 ((len(lastequality) < self.Diff_EditCost / 2) and
892 892 (pre_ins + pre_del + post_ins + post_del) == 3)):
893 893 # Duplicate record.
894 894 diffs.insert(equalities[-1], (self.DIFF_DELETE, lastequality))
895 895 # Change second copy to insert.
896 896 diffs[equalities[-1] + 1] = (self.DIFF_INSERT,
897 897 diffs[equalities[-1] + 1][1])
898 898 equalities.pop() # Throw away the equality we just deleted.
899 899 lastequality = None
900 900 if pre_ins and pre_del:
901 901 # No changes made which could affect previous entry, keep going.
902 902 post_ins = post_del = True
903 903 equalities = []
904 904 else:
905 905 if len(equalities):
906 906 equalities.pop() # Throw away the previous equality.
907 907 if len(equalities):
908 908 pointer = equalities[-1]
909 909 else:
910 910 pointer = -1
911 911 post_ins = post_del = False
912 912 changes = True
913 913 pointer += 1
914 914
915 915 if changes:
916 916 self.diff_cleanupMerge(diffs)
917 917
918 918 def diff_cleanupMerge(self, diffs):
919 919 """Reorder and merge like edit sections. Merge equalities.
920 920 Any edit section can move as long as it doesn't cross an equality.
921 921
922 922 Args:
923 923 diffs: Array of diff tuples.
924 924 """
925 925 diffs.append((self.DIFF_EQUAL, '')) # Add a dummy entry at the end.
926 926 pointer = 0
927 927 count_delete = 0
928 928 count_insert = 0
929 929 text_delete = ''
930 930 text_insert = ''
931 931 while pointer < len(diffs):
932 932 if diffs[pointer][0] == self.DIFF_INSERT:
933 933 count_insert += 1
934 934 text_insert += diffs[pointer][1]
935 935 pointer += 1
936 936 elif diffs[pointer][0] == self.DIFF_DELETE:
937 937 count_delete += 1
938 938 text_delete += diffs[pointer][1]
939 939 pointer += 1
940 940 elif diffs[pointer][0] == self.DIFF_EQUAL:
941 941 # Upon reaching an equality, check for prior redundancies.
942 942 if count_delete + count_insert > 1:
943 943 if count_delete != 0 and count_insert != 0:
944 944 # Factor out any common prefixies.
945 945 commonlength = self.diff_commonPrefix(text_insert, text_delete)
946 946 if commonlength != 0:
947 947 x = pointer - count_delete - count_insert - 1
948 948 if x >= 0 and diffs[x][0] == self.DIFF_EQUAL:
949 949 diffs[x] = (diffs[x][0], diffs[x][1] +
950 950 text_insert[:commonlength])
951 951 else:
952 952 diffs.insert(0, (self.DIFF_EQUAL, text_insert[:commonlength]))
953 953 pointer += 1
954 954 text_insert = text_insert[commonlength:]
955 955 text_delete = text_delete[commonlength:]
956 956 # Factor out any common suffixies.
957 957 commonlength = self.diff_commonSuffix(text_insert, text_delete)
958 958 if commonlength != 0:
959 959 diffs[pointer] = (diffs[pointer][0], text_insert[-commonlength:] +
960 960 diffs[pointer][1])
961 961 text_insert = text_insert[:-commonlength]
962 962 text_delete = text_delete[:-commonlength]
963 963 # Delete the offending records and add the merged ones.
964 964 if count_delete == 0:
965 965 diffs[pointer - count_insert : pointer] = [
966 966 (self.DIFF_INSERT, text_insert)]
967 967 elif count_insert == 0:
968 968 diffs[pointer - count_delete : pointer] = [
969 969 (self.DIFF_DELETE, text_delete)]
970 970 else:
971 971 diffs[pointer - count_delete - count_insert : pointer] = [
972 972 (self.DIFF_DELETE, text_delete),
973 973 (self.DIFF_INSERT, text_insert)]
974 974 pointer = pointer - count_delete - count_insert + 1
975 975 if count_delete != 0:
976 976 pointer += 1
977 977 if count_insert != 0:
978 978 pointer += 1
979 979 elif pointer != 0 and diffs[pointer - 1][0] == self.DIFF_EQUAL:
980 980 # Merge this equality with the previous one.
981 981 diffs[pointer - 1] = (diffs[pointer - 1][0],
982 982 diffs[pointer - 1][1] + diffs[pointer][1])
983 983 del diffs[pointer]
984 984 else:
985 985 pointer += 1
986 986
987 987 count_insert = 0
988 988 count_delete = 0
989 989 text_delete = ''
990 990 text_insert = ''
991 991
992 992 if diffs[-1][1] == '':
993 993 diffs.pop() # Remove the dummy entry at the end.
994 994
995 995 # Second pass: look for single edits surrounded on both sides by equalities
996 996 # which can be shifted sideways to eliminate an equality.
997 997 # e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
998 998 changes = False
999 999 pointer = 1
1000 1000 # Intentionally ignore the first and last element (don't need checking).
1001 1001 while pointer < len(diffs) - 1:
1002 1002 if (diffs[pointer - 1][0] == self.DIFF_EQUAL and
1003 1003 diffs[pointer + 1][0] == self.DIFF_EQUAL):
1004 1004 # This is a single edit surrounded by equalities.
1005 1005 if diffs[pointer][1].endswith(diffs[pointer - 1][1]):
1006 1006 # Shift the edit over the previous equality.
1007 1007 diffs[pointer] = (diffs[pointer][0],
1008 1008 diffs[pointer - 1][1] +
1009 1009 diffs[pointer][1][:-len(diffs[pointer - 1][1])])
1010 1010 diffs[pointer + 1] = (diffs[pointer + 1][0],
1011 1011 diffs[pointer - 1][1] + diffs[pointer + 1][1])
1012 1012 del diffs[pointer - 1]
1013 1013 changes = True
1014 1014 elif diffs[pointer][1].startswith(diffs[pointer + 1][1]):
1015 1015 # Shift the edit over the next equality.
1016 1016 diffs[pointer - 1] = (diffs[pointer - 1][0],
1017 1017 diffs[pointer - 1][1] + diffs[pointer + 1][1])
1018 1018 diffs[pointer] = (diffs[pointer][0],
1019 1019 diffs[pointer][1][len(diffs[pointer + 1][1]):] +
1020 1020 diffs[pointer + 1][1])
1021 1021 del diffs[pointer + 1]
1022 1022 changes = True
1023 1023 pointer += 1
1024 1024
1025 1025 # If shifts were made, the diff needs reordering and another shift sweep.
1026 1026 if changes:
1027 1027 self.diff_cleanupMerge(diffs)
1028 1028
1029 1029 def diff_xIndex(self, diffs, loc):
1030 1030 """loc is a location in text1, compute and return the equivalent location
1031 1031 in text2. e.g. "The cat" vs "The big cat", 1->1, 5->8
1032 1032
1033 1033 Args:
1034 1034 diffs: Array of diff tuples.
1035 1035 loc: Location within text1.
1036 1036
1037 1037 Returns:
1038 1038 Location within text2.
1039 1039 """
1040 1040 chars1 = 0
1041 1041 chars2 = 0
1042 1042 last_chars1 = 0
1043 1043 last_chars2 = 0
1044 1044 for x in range(len(diffs)):
1045 1045 (op, text) = diffs[x]
1046 1046 if op != self.DIFF_INSERT: # Equality or deletion.
1047 1047 chars1 += len(text)
1048 1048 if op != self.DIFF_DELETE: # Equality or insertion.
1049 1049 chars2 += len(text)
1050 1050 if chars1 > loc: # Overshot the location.
1051 1051 break
1052 1052 last_chars1 = chars1
1053 1053 last_chars2 = chars2
1054 1054
1055 1055 if len(diffs) != x and diffs[x][0] == self.DIFF_DELETE:
1056 1056 # The location was deleted.
1057 1057 return last_chars2
1058 1058 # Add the remaining len(character).
1059 1059 return last_chars2 + (loc - last_chars1)
1060 1060
1061 1061 def diff_prettyHtml(self, diffs):
1062 1062 """Convert a diff array into a pretty HTML report.
1063 1063
1064 1064 Args:
1065 1065 diffs: Array of diff tuples.
1066 1066
1067 1067 Returns:
1068 1068 HTML representation.
1069 1069 """
1070 1070 html = []
1071 1071 for (op, data) in diffs:
1072 1072 text = (data.replace("&", "&amp;").replace("<", "&lt;")
1073 1073 .replace(">", "&gt;").replace("\n", "&para;<br>"))
1074 1074 if op == self.DIFF_INSERT:
1075 1075 html.append("<ins style=\"background:#e6ffe6;\">%s</ins>" % text)
1076 1076 elif op == self.DIFF_DELETE:
1077 1077 html.append("<del style=\"background:#ffe6e6;\">%s</del>" % text)
1078 1078 elif op == self.DIFF_EQUAL:
1079 1079 html.append("<span>%s</span>" % text)
1080 1080 return "".join(html)
1081 1081
1082 1082 def diff_text1(self, diffs):
1083 1083 """Compute and return the source text (all equalities and deletions).
1084 1084
1085 1085 Args:
1086 1086 diffs: Array of diff tuples.
1087 1087
1088 1088 Returns:
1089 1089 Source text.
1090 1090 """
1091 1091 text = []
1092 1092 for (op, data) in diffs:
1093 1093 if op != self.DIFF_INSERT:
1094 1094 text.append(data)
1095 1095 return "".join(text)
1096 1096
1097 1097 def diff_text2(self, diffs):
1098 1098 """Compute and return the destination text (all equalities and insertions).
1099 1099
1100 1100 Args:
1101 1101 diffs: Array of diff tuples.
1102 1102
1103 1103 Returns:
1104 1104 Destination text.
1105 1105 """
1106 1106 text = []
1107 1107 for (op, data) in diffs:
1108 1108 if op != self.DIFF_DELETE:
1109 1109 text.append(data)
1110 1110 return "".join(text)
1111 1111
1112 1112 def diff_levenshtein(self, diffs):
1113 1113 """Compute the Levenshtein distance; the number of inserted, deleted or
1114 1114 substituted characters.
1115 1115
1116 1116 Args:
1117 1117 diffs: Array of diff tuples.
1118 1118
1119 1119 Returns:
1120 1120 Number of changes.
1121 1121 """
1122 1122 levenshtein = 0
1123 1123 insertions = 0
1124 1124 deletions = 0
1125 1125 for (op, data) in diffs:
1126 1126 if op == self.DIFF_INSERT:
1127 1127 insertions += len(data)
1128 1128 elif op == self.DIFF_DELETE:
1129 1129 deletions += len(data)
1130 1130 elif op == self.DIFF_EQUAL:
1131 1131 # A deletion and an insertion is one substitution.
1132 1132 levenshtein += max(insertions, deletions)
1133 1133 insertions = 0
1134 1134 deletions = 0
1135 1135 levenshtein += max(insertions, deletions)
1136 1136 return levenshtein
1137 1137
1138 1138 def diff_toDelta(self, diffs):
1139 1139 """Crush the diff into an encoded string which describes the operations
1140 1140 required to transform text1 into text2.
1141 1141 E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'.
1142 1142 Operations are tab-separated. Inserted text is escaped using %xx notation.
1143 1143
1144 1144 Args:
1145 1145 diffs: Array of diff tuples.
1146 1146
1147 1147 Returns:
1148 1148 Delta text.
1149 1149 """
1150 1150 text = []
1151 1151 for (op, data) in diffs:
1152 1152 if op == self.DIFF_INSERT:
1153 1153 # High ascii will raise UnicodeDecodeError. Use Unicode instead.
1154 1154 data = data.encode("utf-8")
1155 1155 text.append("+" + urllib.quote(data, "!~*'();/?:@&=+$,# "))
1156 1156 elif op == self.DIFF_DELETE:
1157 1157 text.append("-%d" % len(data))
1158 1158 elif op == self.DIFF_EQUAL:
1159 1159 text.append("=%d" % len(data))
1160 1160 return "\t".join(text)
1161 1161
1162 1162 def diff_fromDelta(self, text1, delta):
1163 1163 """Given the original text1, and an encoded string which describes the
1164 1164 operations required to transform text1 into text2, compute the full diff.
1165 1165
1166 1166 Args:
1167 1167 text1: Source string for the diff.
1168 1168 delta: Delta text.
1169 1169
1170 1170 Returns:
1171 1171 Array of diff tuples.
1172 1172
1173 1173 Raises:
1174 1174 ValueError: If invalid input.
1175 1175 """
1176 1176 if type(delta) == unicode:
1177 1177 # Deltas should be composed of a subset of ascii chars, Unicode not
1178 1178 # required. If this encode raises UnicodeEncodeError, delta is invalid.
1179 1179 delta = delta.encode("ascii")
1180 1180 diffs = []
1181 1181 pointer = 0 # Cursor in text1
1182 1182 tokens = delta.split("\t")
1183 1183 for token in tokens:
1184 1184 if token == "":
1185 1185 # Blank tokens are ok (from a trailing \t).
1186 1186 continue
1187 1187 # Each token begins with a one character parameter which specifies the
1188 1188 # operation of this token (delete, insert, equality).
1189 1189 param = token[1:]
1190 1190 if token[0] == "+":
1191 1191 param = urllib.unquote(param).decode("utf-8")
1192 1192 diffs.append((self.DIFF_INSERT, param))
1193 1193 elif token[0] == "-" or token[0] == "=":
1194 1194 try:
1195 1195 n = int(param)
1196 1196 except ValueError:
1197 1197 raise ValueError("Invalid number in diff_fromDelta: " + param)
1198 1198 if n < 0:
1199 1199 raise ValueError("Negative number in diff_fromDelta: " + param)
1200 1200 text = text1[pointer : pointer + n]
1201 1201 pointer += n
1202 1202 if token[0] == "=":
1203 1203 diffs.append((self.DIFF_EQUAL, text))
1204 1204 else:
1205 1205 diffs.append((self.DIFF_DELETE, text))
1206 1206 else:
1207 1207 # Anything else is an error.
1208 1208 raise ValueError("Invalid diff operation in diff_fromDelta: " +
1209 1209 token[0])
1210 1210 if pointer != len(text1):
1211 1211 raise ValueError(
1212 1212 "Delta length (%d) does not equal source text length (%d)." %
1213 1213 (pointer, len(text1)))
1214 1214 return diffs
1215 1215
1216 1216 # MATCH FUNCTIONS
1217 1217
1218 1218 def match_main(self, text, pattern, loc):
1219 1219 """Locate the best instance of 'pattern' in 'text' near 'loc'.
1220 1220
1221 1221 Args:
1222 1222 text: The text to search.
1223 1223 pattern: The pattern to search for.
1224 1224 loc: The location to search around.
1225 1225
1226 1226 Returns:
1227 1227 Best match index or -1.
1228 1228 """
1229 1229 # Check for null inputs.
1230 1230 if text is None or pattern is None:
1231 1231 raise ValueError("Null inputs. (match_main)")
1232 1232
1233 1233 loc = max(0, min(loc, len(text)))
1234 1234 if text == pattern:
1235 1235 # Shortcut (potentially not guaranteed by the algorithm)
1236 1236 return 0
1237 1237 elif not text:
1238 1238 # Nothing to match.
1239 1239 return -1
1240 1240 elif text[loc:loc + len(pattern)] == pattern:
1241 1241 # Perfect match at the perfect spot! (Includes case of null pattern)
1242 1242 return loc
1243 1243 else:
1244 1244 # Do a fuzzy compare.
1245 1245 match = self.match_bitap(text, pattern, loc)
1246 1246 return match
1247 1247
1248 1248 def match_bitap(self, text, pattern, loc):
1249 1249 """Locate the best instance of 'pattern' in 'text' near 'loc' using the
1250 1250 Bitap algorithm.
1251 1251
1252 1252 Args:
1253 1253 text: The text to search.
1254 1254 pattern: The pattern to search for.
1255 1255 loc: The location to search around.
1256 1256
1257 1257 Returns:
1258 1258 Best match index or -1.
1259 1259 """
1260 1260 # Python doesn't have a maxint limit, so ignore this check.
1261 1261 #if self.Match_MaxBits != 0 and len(pattern) > self.Match_MaxBits:
1262 1262 # raise ValueError("Pattern too long for this application.")
1263 1263
1264 1264 # Initialise the alphabet.
1265 1265 s = self.match_alphabet(pattern)
1266 1266
1267 1267 def match_bitapScore(e, x):
1268 1268 """Compute and return the score for a match with e errors and x location.
1269 1269 Accesses loc and pattern through being a closure.
1270 1270
1271 1271 Args:
1272 1272 e: Number of errors in match.
1273 1273 x: Location of match.
1274 1274
1275 1275 Returns:
1276 1276 Overall score for match (0.0 = good, 1.0 = bad).
1277 1277 """
1278 1278 accuracy = float(e) / len(pattern)
1279 1279 proximity = abs(loc - x)
1280 1280 if not self.Match_Distance:
1281 1281 # Dodge divide by zero error.
1282 1282 return proximity and 1.0 or accuracy
1283 1283 return accuracy + (proximity / float(self.Match_Distance))
1284 1284
1285 1285 # Highest score beyond which we give up.
1286 1286 score_threshold = self.Match_Threshold
1287 1287 # Is there a nearby exact match? (speedup)
1288 1288 best_loc = text.find(pattern, loc)
1289 1289 if best_loc != -1:
1290 1290 score_threshold = min(match_bitapScore(0, best_loc), score_threshold)
1291 1291 # What about in the other direction? (speedup)
1292 1292 best_loc = text.rfind(pattern, loc + len(pattern))
1293 1293 if best_loc != -1:
1294 1294 score_threshold = min(match_bitapScore(0, best_loc), score_threshold)
1295 1295
1296 1296 # Initialise the bit arrays.
1297 1297 matchmask = 1 << (len(pattern) - 1)
1298 1298 best_loc = -1
1299 1299
1300 1300 bin_max = len(pattern) + len(text)
1301 1301 # Empty initialization added to appease pychecker.
1302 1302 last_rd = None
1303 1303 for d in range(len(pattern)):
1304 1304 # Scan for the best match each iteration allows for one more error.
1305 1305 # Run a binary search to determine how far from 'loc' we can stray at
1306 1306 # this error level.
1307 1307 bin_min = 0
1308 1308 bin_mid = bin_max
1309 1309 while bin_min < bin_mid:
1310 1310 if match_bitapScore(d, loc + bin_mid) <= score_threshold:
1311 1311 bin_min = bin_mid
1312 1312 else:
1313 1313 bin_max = bin_mid
1314 1314 bin_mid = (bin_max - bin_min) // 2 + bin_min
1315 1315
1316 1316 # Use the result from this iteration as the maximum for the next.
1317 1317 bin_max = bin_mid
1318 1318 start = max(1, loc - bin_mid + 1)
1319 1319 finish = min(loc + bin_mid, len(text)) + len(pattern)
1320 1320
1321 1321 rd = [0] * (finish + 2)
1322 1322 rd[finish + 1] = (1 << d) - 1
1323 1323 for j in range(finish, start - 1, -1):
1324 1324 if len(text) <= j - 1:
1325 1325 # Out of range.
1326 1326 charMatch = 0
1327 1327 else:
1328 1328 charMatch = s.get(text[j - 1], 0)
1329 1329 if d == 0: # First pass: exact match.
1330 1330 rd[j] = ((rd[j + 1] << 1) | 1) & charMatch
1331 1331 else: # Subsequent passes: fuzzy match.
1332 1332 rd[j] = (((rd[j + 1] << 1) | 1) & charMatch) | (
1333 1333 ((last_rd[j + 1] | last_rd[j]) << 1) | 1) | last_rd[j + 1]
1334 1334 if rd[j] & matchmask:
1335 1335 score = match_bitapScore(d, j - 1)
1336 1336 # This match will almost certainly be better than any existing match.
1337 1337 # But check anyway.
1338 1338 if score <= score_threshold:
1339 1339 # Told you so.
1340 1340 score_threshold = score
1341 1341 best_loc = j - 1
1342 1342 if best_loc > loc:
1343 1343 # When passing loc, don't exceed our current distance from loc.
1344 1344 start = max(1, 2 * loc - best_loc)
1345 1345 else:
1346 1346 # Already passed loc, downhill from here on in.
1347 1347 break
1348 1348 # No hope for a (better) match at greater error levels.
1349 1349 if match_bitapScore(d + 1, loc) > score_threshold:
1350 1350 break
1351 1351 last_rd = rd
1352 1352 return best_loc
1353 1353
1354 1354 def match_alphabet(self, pattern):
1355 1355 """Initialise the alphabet for the Bitap algorithm.
1356 1356
1357 1357 Args:
1358 1358 pattern: The text to encode.
1359 1359
1360 1360 Returns:
1361 1361 Hash of character locations.
1362 1362 """
1363 1363 s = {}
1364 1364 for char in pattern:
1365 1365 s[char] = 0
1366 1366 for i in range(len(pattern)):
1367 1367 s[pattern[i]] |= 1 << (len(pattern) - i - 1)
1368 1368 return s
1369 1369
1370 1370 # PATCH FUNCTIONS
1371 1371
1372 1372 def patch_addContext(self, patch, text):
1373 1373 """Increase the context until it is unique,
1374 1374 but don't let the pattern expand beyond Match_MaxBits.
1375 1375
1376 1376 Args:
1377 1377 patch: The patch to grow.
1378 1378 text: Source text.
1379 1379 """
1380 1380 if len(text) == 0:
1381 1381 return
1382 1382 pattern = text[patch.start2 : patch.start2 + patch.length1]
1383 1383 padding = 0
1384 1384
1385 1385 # Look for the first and last matches of pattern in text. If two different
1386 1386 # matches are found, increase the pattern length.
1387 1387 while (text.find(pattern) != text.rfind(pattern) and (self.Match_MaxBits ==
1388 1388 0 or len(pattern) < self.Match_MaxBits - self.Patch_Margin -
1389 1389 self.Patch_Margin)):
1390 1390 padding += self.Patch_Margin
1391 1391 pattern = text[max(0, patch.start2 - padding) :
1392 1392 patch.start2 + patch.length1 + padding]
1393 1393 # Add one chunk for good luck.
1394 1394 padding += self.Patch_Margin
1395 1395
1396 1396 # Add the prefix.
1397 1397 prefix = text[max(0, patch.start2 - padding) : patch.start2]
1398 1398 if prefix:
1399 1399 patch.diffs[:0] = [(self.DIFF_EQUAL, prefix)]
1400 1400 # Add the suffix.
1401 1401 suffix = text[patch.start2 + patch.length1 :
1402 1402 patch.start2 + patch.length1 + padding]
1403 1403 if suffix:
1404 1404 patch.diffs.append((self.DIFF_EQUAL, suffix))
1405 1405
1406 1406 # Roll back the start points.
1407 1407 patch.start1 -= len(prefix)
1408 1408 patch.start2 -= len(prefix)
1409 1409 # Extend lengths.
1410 1410 patch.length1 += len(prefix) + len(suffix)
1411 1411 patch.length2 += len(prefix) + len(suffix)
1412 1412
1413 1413 def patch_make(self, a, b=None, c=None):
1414 1414 """Compute a list of patches to turn text1 into text2.
1415 1415 Use diffs if provided, otherwise compute it ourselves.
1416 1416 There are four ways to call this function, depending on what data is
1417 1417 available to the caller:
1418 1418 Method 1:
1419 1419 a = text1, b = text2
1420 1420 Method 2:
1421 1421 a = diffs
1422 1422 Method 3 (optimal):
1423 1423 a = text1, b = diffs
1424 1424 Method 4 (deprecated, use method 3):
1425 1425 a = text1, b = text2, c = diffs
1426 1426
1427 1427 Args:
1428 1428 a: text1 (methods 1,3,4) or Array of diff tuples for text1 to
1429 1429 text2 (method 2).
1430 1430 b: text2 (methods 1,4) or Array of diff tuples for text1 to
1431 1431 text2 (method 3) or undefined (method 2).
1432 1432 c: Array of diff tuples for text1 to text2 (method 4) or
1433 1433 undefined (methods 1,2,3).
1434 1434
1435 1435 Returns:
1436 1436 Array of Patch objects.
1437 1437 """
1438 1438 text1 = None
1439 1439 diffs = None
1440 1440 # Note that texts may arrive as 'str' or 'unicode'.
1441 1441 if isinstance(a, str) and isinstance(b, str) and c is None:
1442 1442 # Method 1: text1, text2
1443 1443 # Compute diffs from text1 and text2.
1444 1444 text1 = a
1445 1445 diffs = self.diff_main(text1, b, True)
1446 1446 if len(diffs) > 2:
1447 1447 self.diff_cleanupSemantic(diffs)
1448 1448 self.diff_cleanupEfficiency(diffs)
1449 1449 elif isinstance(a, list) and b is None and c is None:
1450 1450 # Method 2: diffs
1451 1451 # Compute text1 from diffs.
1452 1452 diffs = a
1453 1453 text1 = self.diff_text1(diffs)
1454 1454 elif isinstance(a, str) and isinstance(b, list) and c is None:
1455 1455 # Method 3: text1, diffs
1456 1456 text1 = a
1457 1457 diffs = b
1458 1458 elif (isinstance(a, str) and isinstance(b, str) and
1459 1459 isinstance(c, list)):
1460 1460 # Method 4: text1, text2, diffs
1461 1461 # text2 is not used.
1462 1462 text1 = a
1463 1463 diffs = c
1464 1464 else:
1465 1465 raise ValueError("Unknown call format to patch_make.")
1466 1466
1467 1467 if not diffs:
1468 1468 return [] # Get rid of the None case.
1469 1469 patches = []
1470 1470 patch = patch_obj()
1471 1471 char_count1 = 0 # Number of characters into the text1 string.
1472 1472 char_count2 = 0 # Number of characters into the text2 string.
1473 1473 prepatch_text = text1 # Recreate the patches to determine context info.
1474 1474 postpatch_text = text1
1475 1475 for x in range(len(diffs)):
1476 1476 (diff_type, diff_text) = diffs[x]
1477 1477 if len(patch.diffs) == 0 and diff_type != self.DIFF_EQUAL:
1478 1478 # A new patch starts here.
1479 1479 patch.start1 = char_count1
1480 1480 patch.start2 = char_count2
1481 1481 if diff_type == self.DIFF_INSERT:
1482 1482 # Insertion
1483 1483 patch.diffs.append(diffs[x])
1484 1484 patch.length2 += len(diff_text)
1485 1485 postpatch_text = (postpatch_text[:char_count2] + diff_text +
1486 1486 postpatch_text[char_count2:])
1487 1487 elif diff_type == self.DIFF_DELETE:
1488 1488 # Deletion.
1489 1489 patch.length1 += len(diff_text)
1490 1490 patch.diffs.append(diffs[x])
1491 1491 postpatch_text = (postpatch_text[:char_count2] +
1492 1492 postpatch_text[char_count2 + len(diff_text):])
1493 1493 elif (diff_type == self.DIFF_EQUAL and
1494 1494 len(diff_text) <= 2 * self.Patch_Margin and
1495 1495 len(patch.diffs) != 0 and len(diffs) != x + 1):
1496 1496 # Small equality inside a patch.
1497 1497 patch.diffs.append(diffs[x])
1498 1498 patch.length1 += len(diff_text)
1499 1499 patch.length2 += len(diff_text)
1500 1500
1501 1501 if (diff_type == self.DIFF_EQUAL and
1502 1502 len(diff_text) >= 2 * self.Patch_Margin):
1503 1503 # Time for a new patch.
1504 1504 if len(patch.diffs) != 0:
1505 1505 self.patch_addContext(patch, prepatch_text)
1506 1506 patches.append(patch)
1507 1507 patch = patch_obj()
1508 1508 # Unlike Unidiff, our patch lists have a rolling context.
1509 1509 # http://code.google.com/p/google-diff-match-patch/wiki/Unidiff
1510 1510 # Update prepatch text & pos to reflect the application of the
1511 1511 # just completed patch.
1512 1512 prepatch_text = postpatch_text
1513 1513 char_count1 = char_count2
1514 1514
1515 1515 # Update the current character count.
1516 1516 if diff_type != self.DIFF_INSERT:
1517 1517 char_count1 += len(diff_text)
1518 1518 if diff_type != self.DIFF_DELETE:
1519 1519 char_count2 += len(diff_text)
1520 1520
1521 1521 # Pick up the leftover patch if not empty.
1522 1522 if len(patch.diffs) != 0:
1523 1523 self.patch_addContext(patch, prepatch_text)
1524 1524 patches.append(patch)
1525 1525 return patches
1526 1526
1527 1527 def patch_deepCopy(self, patches):
1528 1528 """Given an array of patches, return another array that is identical.
1529 1529
1530 1530 Args:
1531 1531 patches: Array of Patch objects.
1532 1532
1533 1533 Returns:
1534 1534 Array of Patch objects.
1535 1535 """
1536 1536 patchesCopy = []
1537 1537 for patch in patches:
1538 1538 patchCopy = patch_obj()
1539 1539 # No need to deep copy the tuples since they are immutable.
1540 1540 patchCopy.diffs = patch.diffs[:]
1541 1541 patchCopy.start1 = patch.start1
1542 1542 patchCopy.start2 = patch.start2
1543 1543 patchCopy.length1 = patch.length1
1544 1544 patchCopy.length2 = patch.length2
1545 1545 patchesCopy.append(patchCopy)
1546 1546 return patchesCopy
1547 1547
1548 1548 def patch_apply(self, patches, text):
1549 1549 """Merge a set of patches onto the text. Return a patched text, as well
1550 1550 as a list of true/false values indicating which patches were applied.
1551 1551
1552 1552 Args:
1553 1553 patches: Array of Patch objects.
1554 1554 text: Old text.
1555 1555
1556 1556 Returns:
1557 1557 Two element Array, containing the new text and an array of boolean values.
1558 1558 """
1559 1559 if not patches:
1560 1560 return (text, [])
1561 1561
1562 1562 # Deep copy the patches so that no changes are made to originals.
1563 1563 patches = self.patch_deepCopy(patches)
1564 1564
1565 1565 nullPadding = self.patch_addPadding(patches)
1566 1566 text = nullPadding + text + nullPadding
1567 1567 self.patch_splitMax(patches)
1568 1568
1569 1569 # delta keeps track of the offset between the expected and actual location
1570 1570 # of the previous patch. If there are patches expected at positions 10 and
1571 1571 # 20, but the first patch was found at 12, delta is 2 and the second patch
1572 1572 # has an effective expected position of 22.
1573 1573 delta = 0
1574 1574 results = []
1575 1575 for patch in patches:
1576 1576 expected_loc = patch.start2 + delta
1577 1577 text1 = self.diff_text1(patch.diffs)
1578 1578 end_loc = -1
1579 1579 if len(text1) > self.Match_MaxBits:
1580 1580 # patch_splitMax will only provide an oversized pattern in the case of
1581 1581 # a monster delete.
1582 1582 start_loc = self.match_main(text, text1[:self.Match_MaxBits],
1583 1583 expected_loc)
1584 1584 if start_loc != -1:
1585 1585 end_loc = self.match_main(text, text1[-self.Match_MaxBits:],
1586 1586 expected_loc + len(text1) - self.Match_MaxBits)
1587 1587 if end_loc == -1 or start_loc >= end_loc:
1588 1588 # Can't find valid trailing context. Drop this patch.
1589 1589 start_loc = -1
1590 1590 else:
1591 1591 start_loc = self.match_main(text, text1, expected_loc)
1592 1592 if start_loc == -1:
1593 1593 # No match found. :(
1594 1594 results.append(False)
1595 1595 # Subtract the delta for this failed patch from subsequent patches.
1596 1596 delta -= patch.length2 - patch.length1
1597 1597 else:
1598 1598 # Found a match. :)
1599 1599 results.append(True)
1600 1600 delta = start_loc - expected_loc
1601 1601 if end_loc == -1:
1602 1602 text2 = text[start_loc : start_loc + len(text1)]
1603 1603 else:
1604 1604 text2 = text[start_loc : end_loc + self.Match_MaxBits]
1605 1605 if text1 == text2:
1606 1606 # Perfect match, just shove the replacement text in.
1607 1607 text = (text[:start_loc] + self.diff_text2(patch.diffs) +
1608 1608 text[start_loc + len(text1):])
1609 1609 else:
1610 1610 # Imperfect match.
1611 1611 # Run a diff to get a framework of equivalent indices.
1612 1612 diffs = self.diff_main(text1, text2, False)
1613 1613 if (len(text1) > self.Match_MaxBits and
1614 1614 self.diff_levenshtein(diffs) / float(len(text1)) >
1615 1615 self.Patch_DeleteThreshold):
1616 1616 # The end points match, but the content is unacceptably bad.
1617 1617 results[-1] = False
1618 1618 else:
1619 1619 self.diff_cleanupSemanticLossless(diffs)
1620 1620 index1 = 0
1621 1621 for (op, data) in patch.diffs:
1622 1622 if op != self.DIFF_EQUAL:
1623 1623 index2 = self.diff_xIndex(diffs, index1)
1624 1624 if op == self.DIFF_INSERT: # Insertion
1625 1625 text = text[:start_loc + index2] + data + text[start_loc +
1626 1626 index2:]
1627 1627 elif op == self.DIFF_DELETE: # Deletion
1628 1628 text = text[:start_loc + index2] + text[start_loc +
1629 1629 self.diff_xIndex(diffs, index1 + len(data)):]
1630 1630 if op != self.DIFF_DELETE:
1631 1631 index1 += len(data)
1632 1632 # Strip the padding off.
1633 1633 text = text[len(nullPadding):-len(nullPadding)]
1634 1634 return (text, results)
1635 1635
1636 1636 def patch_addPadding(self, patches):
1637 1637 """Add some padding on text start and end so that edges can match
1638 1638 something. Intended to be called only from within patch_apply.
1639 1639
1640 1640 Args:
1641 1641 patches: Array of Patch objects.
1642 1642
1643 1643 Returns:
1644 1644 The padding string added to each side.
1645 1645 """
1646 1646 paddingLength = self.Patch_Margin
1647 1647 nullPadding = ""
1648 1648 for x in range(1, paddingLength + 1):
1649 1649 nullPadding += chr(x)
1650 1650
1651 1651 # Bump all the patches forward.
1652 1652 for patch in patches:
1653 1653 patch.start1 += paddingLength
1654 1654 patch.start2 += paddingLength
1655 1655
1656 1656 # Add some padding on start of first diff.
1657 1657 patch = patches[0]
1658 1658 diffs = patch.diffs
1659 1659 if not diffs or diffs[0][0] != self.DIFF_EQUAL:
1660 1660 # Add nullPadding equality.
1661 1661 diffs.insert(0, (self.DIFF_EQUAL, nullPadding))
1662 1662 patch.start1 -= paddingLength # Should be 0.
1663 1663 patch.start2 -= paddingLength # Should be 0.
1664 1664 patch.length1 += paddingLength
1665 1665 patch.length2 += paddingLength
1666 1666 elif paddingLength > len(diffs[0][1]):
1667 1667 # Grow first equality.
1668 1668 extraLength = paddingLength - len(diffs[0][1])
1669 1669 newText = nullPadding[len(diffs[0][1]):] + diffs[0][1]
1670 1670 diffs[0] = (diffs[0][0], newText)
1671 1671 patch.start1 -= extraLength
1672 1672 patch.start2 -= extraLength
1673 1673 patch.length1 += extraLength
1674 1674 patch.length2 += extraLength
1675 1675
1676 1676 # Add some padding on end of last diff.
1677 1677 patch = patches[-1]
1678 1678 diffs = patch.diffs
1679 1679 if not diffs or diffs[-1][0] != self.DIFF_EQUAL:
1680 1680 # Add nullPadding equality.
1681 1681 diffs.append((self.DIFF_EQUAL, nullPadding))
1682 1682 patch.length1 += paddingLength
1683 1683 patch.length2 += paddingLength
1684 1684 elif paddingLength > len(diffs[-1][1]):
1685 1685 # Grow last equality.
1686 1686 extraLength = paddingLength - len(diffs[-1][1])
1687 1687 newText = diffs[-1][1] + nullPadding[:extraLength]
1688 1688 diffs[-1] = (diffs[-1][0], newText)
1689 1689 patch.length1 += extraLength
1690 1690 patch.length2 += extraLength
1691 1691
1692 1692 return nullPadding
1693 1693
1694 1694 def patch_splitMax(self, patches):
1695 1695 """Look through the patches and break up any which are longer than the
1696 1696 maximum limit of the match algorithm.
1697 1697 Intended to be called only from within patch_apply.
1698 1698
1699 1699 Args:
1700 1700 patches: Array of Patch objects.
1701 1701 """
1702 1702 patch_size = self.Match_MaxBits
1703 1703 if patch_size == 0:
1704 1704 # Python has the option of not splitting strings due to its ability
1705 1705 # to handle integers of arbitrary precision.
1706 1706 return
1707 1707 for x in range(len(patches)):
1708 1708 if patches[x].length1 <= patch_size:
1709 1709 continue
1710 1710 bigpatch = patches[x]
1711 1711 # Remove the big old patch.
1712 1712 del patches[x]
1713 1713 x -= 1
1714 1714 start1 = bigpatch.start1
1715 1715 start2 = bigpatch.start2
1716 1716 precontext = ''
1717 1717 while len(bigpatch.diffs) != 0:
1718 1718 # Create one of several smaller patches.
1719 1719 patch = patch_obj()
1720 1720 empty = True
1721 1721 patch.start1 = start1 - len(precontext)
1722 1722 patch.start2 = start2 - len(precontext)
1723 1723 if precontext:
1724 1724 patch.length1 = patch.length2 = len(precontext)
1725 1725 patch.diffs.append((self.DIFF_EQUAL, precontext))
1726 1726
1727 1727 while (len(bigpatch.diffs) != 0 and
1728 1728 patch.length1 < patch_size - self.Patch_Margin):
1729 1729 (diff_type, diff_text) = bigpatch.diffs[0]
1730 1730 if diff_type == self.DIFF_INSERT:
1731 1731 # Insertions are harmless.
1732 1732 patch.length2 += len(diff_text)
1733 1733 start2 += len(diff_text)
1734 1734 patch.diffs.append(bigpatch.diffs.pop(0))
1735 1735 empty = False
1736 1736 elif (diff_type == self.DIFF_DELETE and len(patch.diffs) == 1 and
1737 1737 patch.diffs[0][0] == self.DIFF_EQUAL and
1738 1738 len(diff_text) > 2 * patch_size):
1739 1739 # This is a large deletion. Let it pass in one chunk.
1740 1740 patch.length1 += len(diff_text)
1741 1741 start1 += len(diff_text)
1742 1742 empty = False
1743 1743 patch.diffs.append((diff_type, diff_text))
1744 1744 del bigpatch.diffs[0]
1745 1745 else:
1746 1746 # Deletion or equality. Only take as much as we can stomach.
1747 1747 diff_text = diff_text[:patch_size - patch.length1 -
1748 1748 self.Patch_Margin]
1749 1749 patch.length1 += len(diff_text)
1750 1750 start1 += len(diff_text)
1751 1751 if diff_type == self.DIFF_EQUAL:
1752 1752 patch.length2 += len(diff_text)
1753 1753 start2 += len(diff_text)
1754 1754 else:
1755 1755 empty = False
1756 1756
1757 1757 patch.diffs.append((diff_type, diff_text))
1758 1758 if diff_text == bigpatch.diffs[0][1]:
1759 1759 del bigpatch.diffs[0]
1760 1760 else:
1761 1761 bigpatch.diffs[0] = (bigpatch.diffs[0][0],
1762 1762 bigpatch.diffs[0][1][len(diff_text):])
1763 1763
1764 1764 # Compute the head context for the next patch.
1765 1765 precontext = self.diff_text2(patch.diffs)
1766 1766 precontext = precontext[-self.Patch_Margin:]
1767 1767 # Append the end context for this patch.
1768 1768 postcontext = self.diff_text1(bigpatch.diffs)[:self.Patch_Margin]
1769 1769 if postcontext:
1770 1770 patch.length1 += len(postcontext)
1771 1771 patch.length2 += len(postcontext)
1772 1772 if len(patch.diffs) != 0 and patch.diffs[-1][0] == self.DIFF_EQUAL:
1773 1773 patch.diffs[-1] = (self.DIFF_EQUAL, patch.diffs[-1][1] +
1774 1774 postcontext)
1775 1775 else:
1776 1776 patch.diffs.append((self.DIFF_EQUAL, postcontext))
1777 1777
1778 1778 if not empty:
1779 1779 x += 1
1780 1780 patches.insert(x, patch)
1781 1781
1782 1782 def patch_toText(self, patches):
1783 1783 """Take a list of patches and return a textual representation.
1784 1784
1785 1785 Args:
1786 1786 patches: Array of Patch objects.
1787 1787
1788 1788 Returns:
1789 1789 Text representation of patches.
1790 1790 """
1791 1791 text = []
1792 1792 for patch in patches:
1793 1793 text.append(str(patch))
1794 1794 return "".join(text)
1795 1795
1796 1796 def patch_fromText(self, textline):
1797 1797 """Parse a textual representation of patches and return a list of patch
1798 1798 objects.
1799 1799
1800 1800 Args:
1801 1801 textline: Text representation of patches.
1802 1802
1803 1803 Returns:
1804 1804 Array of Patch objects.
1805 1805
1806 1806 Raises:
1807 1807 ValueError: If invalid input.
1808 1808 """
1809 1809 if type(textline) == unicode:
1810 1810 # Patches should be composed of a subset of ascii chars, Unicode not
1811 1811 # required. If this encode raises UnicodeEncodeError, patch is invalid.
1812 1812 textline = textline.encode("ascii")
1813 1813 patches = []
1814 1814 if not textline:
1815 1815 return patches
1816 1816 text = textline.split('\n')
1817 1817 while len(text) != 0:
1818 1818 m = re.match("^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$", text[0])
1819 1819 if not m:
1820 1820 raise ValueError("Invalid patch string: " + text[0])
1821 1821 patch = patch_obj()
1822 1822 patches.append(patch)
1823 1823 patch.start1 = int(m.group(1))
1824 1824 if m.group(2) == '':
1825 1825 patch.start1 -= 1
1826 1826 patch.length1 = 1
1827 1827 elif m.group(2) == '0':
1828 1828 patch.length1 = 0
1829 1829 else:
1830 1830 patch.start1 -= 1
1831 1831 patch.length1 = int(m.group(2))
1832 1832
1833 1833 patch.start2 = int(m.group(3))
1834 1834 if m.group(4) == '':
1835 1835 patch.start2 -= 1
1836 1836 patch.length2 = 1
1837 1837 elif m.group(4) == '0':
1838 1838 patch.length2 = 0
1839 1839 else:
1840 1840 patch.start2 -= 1
1841 1841 patch.length2 = int(m.group(4))
1842 1842
1843 1843 del text[0]
1844 1844
1845 1845 while len(text) != 0:
1846 1846 if text[0]:
1847 1847 sign = text[0][0]
1848 1848 else:
1849 1849 sign = ''
1850 1850 line = urllib.unquote(text[0][1:])
1851 1851 line = line.decode("utf-8")
1852 1852 if sign == '+':
1853 1853 # Insertion.
1854 1854 patch.diffs.append((self.DIFF_INSERT, line))
1855 1855 elif sign == '-':
1856 1856 # Deletion.
1857 1857 patch.diffs.append((self.DIFF_DELETE, line))
1858 1858 elif sign == ' ':
1859 1859 # Minor equality.
1860 1860 patch.diffs.append((self.DIFF_EQUAL, line))
1861 1861 elif sign == '@':
1862 1862 # Start of next patch.
1863 1863 break
1864 1864 elif sign == '':
1865 1865 # Blank line? Whatever.
1866 1866 pass
1867 1867 else:
1868 1868 # WTF?
1869 1869 raise ValueError("Invalid patch mode: '%s'\n%s" % (sign, line))
1870 1870 del text[0]
1871 1871 return patches
1872 1872
1873 1873
1874 1874 class patch_obj:
1875 1875 """Class representing one patch operation.
1876 1876 """
1877 1877
1878 1878 def __init__(self):
1879 1879 """Initializes with an empty list of diffs.
1880 1880 """
1881 1881 self.diffs = []
1882 1882 self.start1 = None
1883 1883 self.start2 = None
1884 1884 self.length1 = 0
1885 1885 self.length2 = 0
1886 1886
1887 1887 def __str__(self):
1888 1888 """Emmulate GNU diff's format.
1889 1889 Header: @@ -382,8 +481,9 @@
1890 1890 Indicies are printed as 1-based, not 0-based.
1891 1891
1892 1892 Returns:
1893 1893 The GNU diff string.
1894 1894 """
1895 1895 if self.length1 == 0:
1896 1896 coords1 = str(self.start1) + ",0"
1897 1897 elif self.length1 == 1:
1898 1898 coords1 = str(self.start1 + 1)
1899 1899 else:
1900 1900 coords1 = str(self.start1 + 1) + "," + str(self.length1)
1901 1901 if self.length2 == 0:
1902 1902 coords2 = str(self.start2) + ",0"
1903 1903 elif self.length2 == 1:
1904 1904 coords2 = str(self.start2 + 1)
1905 1905 else:
1906 1906 coords2 = str(self.start2 + 1) + "," + str(self.length2)
1907 1907 text = ["@@ -", coords1, " +", coords2, " @@\n"]
1908 1908 # Escape the body of the patch with %xx notation.
1909 1909 for (op, data) in self.diffs:
1910 1910 if op == diff_match_patch.DIFF_INSERT:
1911 1911 text.append("+")
1912 1912 elif op == diff_match_patch.DIFF_DELETE:
1913 1913 text.append("-")
1914 1914 elif op == diff_match_patch.DIFF_EQUAL:
1915 1915 text.append(" ")
1916 1916 # High ascii will raise UnicodeDecodeError. Use Unicode instead.
1917 1917 data = data.encode("utf-8")
1918 1918 text.append(urllib.quote(data, "!~*'();/?:@&=+$,# ") + "\n")
1919 1919 return "".join(text) No newline at end of file
@@ -1,446 +1,446 b''
1 1 # Copyright (c) Django Software Foundation and individual contributors.
2 2 # All rights reserved.
3 3 #
4 4 # Redistribution and use in source and binary forms, with or without modification,
5 5 # are permitted provided that the following conditions are met:
6 6 #
7 7 # 1. Redistributions of source code must retain the above copyright notice,
8 8 # this list of conditions and the following disclaimer.
9 9 #
10 10 # 2. Redistributions in binary form must reproduce the above copyright
11 11 # notice, this list of conditions and the following disclaimer in the
12 12 # documentation and/or other materials provided with the distribution.
13 13 #
14 14 # 3. Neither the name of Django nor the names of its contributors may be used
15 15 # to endorse or promote products derived from this software without
16 16 # specific prior written permission.
17 17 #
18 18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 19 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 21 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
22 22 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 23 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 24 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 25 # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 27 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 28
29 29 """
30 30 For definitions of the different versions of RSS, see:
31 31 http://web.archive.org/web/20110718035220/http://diveintomark.org/archives/2004/02/04/incompatible-rss
32 32 """
33 from __future__ import unicode_literals
33
34 34
35 35 import datetime
36 36 from StringIO import StringIO
37 37
38 38 import pytz
39 39 from six.moves.urllib import parse as urlparse
40 40
41 41 from rhodecode.lib.feedgenerator import datetime_safe
42 42 from rhodecode.lib.feedgenerator.utils import SimplerXMLGenerator, iri_to_uri, force_text
43 43
44 44
45 45 #### The following code comes from ``django.utils.feedgenerator`` ####
46 46
47 47
48 48 def rfc2822_date(date):
49 49 # We can't use strftime() because it produces locale-dependent results, so
50 50 # we have to map english month and day names manually
51 51 months = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',)
52 52 days = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')
53 53 # Support datetime objects older than 1900
54 54 date = datetime_safe.new_datetime(date)
55 55 # We do this ourselves to be timezone aware, email.Utils is not tz aware.
56 56 dow = days[date.weekday()]
57 57 month = months[date.month - 1]
58 58 time_str = date.strftime('%s, %%d %s %%Y %%H:%%M:%%S ' % (dow, month))
59 59
60 60 time_str = time_str.decode('utf-8')
61 61 offset = date.utcoffset()
62 62 # Historically, this function assumes that naive datetimes are in UTC.
63 63 if offset is None:
64 64 return time_str + '-0000'
65 65 else:
66 66 timezone = (offset.days * 24 * 60) + (offset.seconds // 60)
67 67 hour, minute = divmod(timezone, 60)
68 68 return time_str + '%+03d%02d' % (hour, minute)
69 69
70 70
71 71 def rfc3339_date(date):
72 72 # Support datetime objects older than 1900
73 73 date = datetime_safe.new_datetime(date)
74 74 time_str = date.strftime('%Y-%m-%dT%H:%M:%S')
75 75
76 76 time_str = time_str.decode('utf-8')
77 77 offset = date.utcoffset()
78 78 # Historically, this function assumes that naive datetimes are in UTC.
79 79 if offset is None:
80 80 return time_str + 'Z'
81 81 else:
82 82 timezone = (offset.days * 24 * 60) + (offset.seconds // 60)
83 83 hour, minute = divmod(timezone, 60)
84 84 return time_str + '%+03d:%02d' % (hour, minute)
85 85
86 86
87 87 def get_tag_uri(url, date):
88 88 """
89 89 Creates a TagURI.
90 90
91 91 See http://web.archive.org/web/20110514113830/http://diveintomark.org/archives/2004/05/28/howto-atom-id
92 92 """
93 93 bits = urlparse(url)
94 94 d = ''
95 95 if date is not None:
96 96 d = ',%s' % datetime_safe.new_datetime(date).strftime('%Y-%m-%d')
97 97 return 'tag:%s%s:%s/%s' % (bits.hostname, d, bits.path, bits.fragment)
98 98
99 99
100 100 class SyndicationFeed(object):
101 101 """Base class for all syndication feeds. Subclasses should provide write()"""
102 102
103 103 def __init__(self, title, link, description, language=None, author_email=None,
104 104 author_name=None, author_link=None, subtitle=None, categories=None,
105 105 feed_url=None, feed_copyright=None, feed_guid=None, ttl=None, **kwargs):
106 106 def to_unicode(s):
107 107 return force_text(s, strings_only=True)
108 108 if categories:
109 109 categories = [force_text(c) for c in categories]
110 110 if ttl is not None:
111 111 # Force ints to unicode
112 112 ttl = force_text(ttl)
113 113 self.feed = {
114 114 'title': to_unicode(title),
115 115 'link': iri_to_uri(link),
116 116 'description': to_unicode(description),
117 117 'language': to_unicode(language),
118 118 'author_email': to_unicode(author_email),
119 119 'author_name': to_unicode(author_name),
120 120 'author_link': iri_to_uri(author_link),
121 121 'subtitle': to_unicode(subtitle),
122 122 'categories': categories or (),
123 123 'feed_url': iri_to_uri(feed_url),
124 124 'feed_copyright': to_unicode(feed_copyright),
125 125 'id': feed_guid or link,
126 126 'ttl': ttl,
127 127 }
128 128 self.feed.update(kwargs)
129 129 self.items = []
130 130
131 131 def add_item(self, title, link, description, author_email=None,
132 132 author_name=None, author_link=None, pubdate=None, comments=None,
133 133 unique_id=None, unique_id_is_permalink=None, enclosure=None,
134 134 categories=(), item_copyright=None, ttl=None, updateddate=None,
135 135 enclosures=None, **kwargs):
136 136 """
137 137 Adds an item to the feed. All args are expected to be Python Unicode
138 138 objects except pubdate and updateddate, which are datetime.datetime
139 139 objects, and enclosures, which is an iterable of instances of the
140 140 Enclosure class.
141 141 """
142 142 def to_unicode(s):
143 143 return force_text(s, strings_only=True)
144 144 if categories:
145 145 categories = [to_unicode(c) for c in categories]
146 146 if ttl is not None:
147 147 # Force ints to unicode
148 148 ttl = force_text(ttl)
149 149 if enclosure is None:
150 150 enclosures = [] if enclosures is None else enclosures
151 151
152 152 item = {
153 153 'title': to_unicode(title),
154 154 'link': iri_to_uri(link),
155 155 'description': to_unicode(description),
156 156 'author_email': to_unicode(author_email),
157 157 'author_name': to_unicode(author_name),
158 158 'author_link': iri_to_uri(author_link),
159 159 'pubdate': pubdate,
160 160 'updateddate': updateddate,
161 161 'comments': to_unicode(comments),
162 162 'unique_id': to_unicode(unique_id),
163 163 'unique_id_is_permalink': unique_id_is_permalink,
164 164 'enclosures': enclosures,
165 165 'categories': categories or (),
166 166 'item_copyright': to_unicode(item_copyright),
167 167 'ttl': ttl,
168 168 }
169 169 item.update(kwargs)
170 170 self.items.append(item)
171 171
172 172 def num_items(self):
173 173 return len(self.items)
174 174
175 175 def root_attributes(self):
176 176 """
177 177 Return extra attributes to place on the root (i.e. feed/channel) element.
178 178 Called from write().
179 179 """
180 180 return {}
181 181
182 182 def add_root_elements(self, handler):
183 183 """
184 184 Add elements in the root (i.e. feed/channel) element. Called
185 185 from write().
186 186 """
187 187 pass
188 188
189 189 def item_attributes(self, item):
190 190 """
191 191 Return extra attributes to place on each item (i.e. item/entry) element.
192 192 """
193 193 return {}
194 194
195 195 def add_item_elements(self, handler, item):
196 196 """
197 197 Add elements on each item (i.e. item/entry) element.
198 198 """
199 199 pass
200 200
201 201 def write(self, outfile, encoding):
202 202 """
203 203 Outputs the feed in the given encoding to outfile, which is a file-like
204 204 object. Subclasses should override this.
205 205 """
206 206 raise NotImplementedError('subclasses of SyndicationFeed must provide a write() method')
207 207
208 208 def writeString(self, encoding):
209 209 """
210 210 Returns the feed in the given encoding as a string.
211 211 """
212 212 s = StringIO()
213 213 self.write(s, encoding)
214 214 return s.getvalue()
215 215
216 216 def latest_post_date(self):
217 217 """
218 218 Returns the latest item's pubdate or updateddate. If no items
219 219 have either of these attributes this returns the current UTC date/time.
220 220 """
221 221 latest_date = None
222 222 date_keys = ('updateddate', 'pubdate')
223 223
224 224 for item in self.items:
225 225 for date_key in date_keys:
226 226 item_date = item.get(date_key)
227 227 if item_date:
228 228 if latest_date is None or item_date > latest_date:
229 229 latest_date = item_date
230 230
231 231 # datetime.now(tz=utc) is slower, as documented in django.utils.timezone.now
232 232 return latest_date or datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
233 233
234 234
235 235 class Enclosure(object):
236 236 """Represents an RSS enclosure"""
237 237 def __init__(self, url, length, mime_type):
238 238 """All args are expected to be Python Unicode objects"""
239 239 self.length, self.mime_type = length, mime_type
240 240 self.url = iri_to_uri(url)
241 241
242 242
243 243 class RssFeed(SyndicationFeed):
244 244 content_type = 'application/rss+xml; charset=utf-8'
245 245
246 246 def write(self, outfile, encoding):
247 247 handler = SimplerXMLGenerator(outfile, encoding)
248 248 handler.startDocument()
249 249 handler.startElement("rss", self.rss_attributes())
250 250 handler.startElement("channel", self.root_attributes())
251 251 self.add_root_elements(handler)
252 252 self.write_items(handler)
253 253 self.endChannelElement(handler)
254 254 handler.endElement("rss")
255 255
256 256 def rss_attributes(self):
257 257 return {"version": self._version,
258 258 "xmlns:atom": "http://www.w3.org/2005/Atom"}
259 259
260 260 def write_items(self, handler):
261 261 for item in self.items:
262 262 handler.startElement('item', self.item_attributes(item))
263 263 self.add_item_elements(handler, item)
264 264 handler.endElement("item")
265 265
266 266 def add_root_elements(self, handler):
267 267 handler.addQuickElement("title", self.feed['title'])
268 268 handler.addQuickElement("link", self.feed['link'])
269 269 handler.addQuickElement("description", self.feed['description'])
270 270 if self.feed['feed_url'] is not None:
271 271 handler.addQuickElement("atom:link", None, {"rel": "self", "href": self.feed['feed_url']})
272 272 if self.feed['language'] is not None:
273 273 handler.addQuickElement("language", self.feed['language'])
274 274 for cat in self.feed['categories']:
275 275 handler.addQuickElement("category", cat)
276 276 if self.feed['feed_copyright'] is not None:
277 277 handler.addQuickElement("copyright", self.feed['feed_copyright'])
278 278 handler.addQuickElement("lastBuildDate", rfc2822_date(self.latest_post_date()))
279 279 if self.feed['ttl'] is not None:
280 280 handler.addQuickElement("ttl", self.feed['ttl'])
281 281
282 282 def endChannelElement(self, handler):
283 283 handler.endElement("channel")
284 284
285 285
286 286 class RssUserland091Feed(RssFeed):
287 287 _version = "0.91"
288 288
289 289 def add_item_elements(self, handler, item):
290 290 handler.addQuickElement("title", item['title'])
291 291 handler.addQuickElement("link", item['link'])
292 292 if item['description'] is not None:
293 293 handler.addQuickElement("description", item['description'])
294 294
295 295
296 296 class Rss201rev2Feed(RssFeed):
297 297 # Spec: http://blogs.law.harvard.edu/tech/rss
298 298 _version = "2.0"
299 299
300 300 def add_item_elements(self, handler, item):
301 301 handler.addQuickElement("title", item['title'])
302 302 handler.addQuickElement("link", item['link'])
303 303 if item['description'] is not None:
304 304 handler.addQuickElement("description", item['description'])
305 305
306 306 # Author information.
307 307 if item["author_name"] and item["author_email"]:
308 308 handler.addQuickElement("author", "%s (%s)" % (item['author_email'], item['author_name']))
309 309 elif item["author_email"]:
310 310 handler.addQuickElement("author", item["author_email"])
311 311 elif item["author_name"]:
312 312 handler.addQuickElement(
313 313 "dc:creator", item["author_name"], {"xmlns:dc": "http://purl.org/dc/elements/1.1/"}
314 314 )
315 315
316 316 if item['pubdate'] is not None:
317 317 handler.addQuickElement("pubDate", rfc2822_date(item['pubdate']))
318 318 if item['comments'] is not None:
319 319 handler.addQuickElement("comments", item['comments'])
320 320 if item['unique_id'] is not None:
321 321 guid_attrs = {}
322 322 if isinstance(item.get('unique_id_is_permalink'), bool):
323 323 guid_attrs['isPermaLink'] = str(item['unique_id_is_permalink']).lower()
324 324 handler.addQuickElement("guid", item['unique_id'], guid_attrs)
325 325 if item['ttl'] is not None:
326 326 handler.addQuickElement("ttl", item['ttl'])
327 327
328 328 # Enclosure.
329 329 if item['enclosures']:
330 330 enclosures = list(item['enclosures'])
331 331 if len(enclosures) > 1:
332 332 raise ValueError(
333 333 "RSS feed items may only have one enclosure, see "
334 334 "http://www.rssboard.org/rss-profile#element-channel-item-enclosure"
335 335 )
336 336 enclosure = enclosures[0]
337 337 handler.addQuickElement('enclosure', '', {
338 338 'url': enclosure.url,
339 339 'length': enclosure.length,
340 340 'type': enclosure.mime_type,
341 341 })
342 342
343 343 # Categories.
344 344 for cat in item['categories']:
345 345 handler.addQuickElement("category", cat)
346 346
347 347
348 348 class Atom1Feed(SyndicationFeed):
349 349 # Spec: https://tools.ietf.org/html/rfc4287
350 350 content_type = 'application/atom+xml; charset=utf-8'
351 351 ns = "http://www.w3.org/2005/Atom"
352 352
353 353 def write(self, outfile, encoding):
354 354 handler = SimplerXMLGenerator(outfile, encoding)
355 355 handler.startDocument()
356 356 handler.startElement('feed', self.root_attributes())
357 357 self.add_root_elements(handler)
358 358 self.write_items(handler)
359 359 handler.endElement("feed")
360 360
361 361 def root_attributes(self):
362 362 if self.feed['language'] is not None:
363 363 return {"xmlns": self.ns, "xml:lang": self.feed['language']}
364 364 else:
365 365 return {"xmlns": self.ns}
366 366
367 367 def add_root_elements(self, handler):
368 368 handler.addQuickElement("title", self.feed['title'])
369 369 handler.addQuickElement("link", "", {"rel": "alternate", "href": self.feed['link']})
370 370 if self.feed['feed_url'] is not None:
371 371 handler.addQuickElement("link", "", {"rel": "self", "href": self.feed['feed_url']})
372 372 handler.addQuickElement("id", self.feed['id'])
373 373 handler.addQuickElement("updated", rfc3339_date(self.latest_post_date()))
374 374 if self.feed['author_name'] is not None:
375 375 handler.startElement("author", {})
376 376 handler.addQuickElement("name", self.feed['author_name'])
377 377 if self.feed['author_email'] is not None:
378 378 handler.addQuickElement("email", self.feed['author_email'])
379 379 if self.feed['author_link'] is not None:
380 380 handler.addQuickElement("uri", self.feed['author_link'])
381 381 handler.endElement("author")
382 382 if self.feed['subtitle'] is not None:
383 383 handler.addQuickElement("subtitle", self.feed['subtitle'])
384 384 for cat in self.feed['categories']:
385 385 handler.addQuickElement("category", "", {"term": cat})
386 386 if self.feed['feed_copyright'] is not None:
387 387 handler.addQuickElement("rights", self.feed['feed_copyright'])
388 388
389 389 def write_items(self, handler):
390 390 for item in self.items:
391 391 handler.startElement("entry", self.item_attributes(item))
392 392 self.add_item_elements(handler, item)
393 393 handler.endElement("entry")
394 394
395 395 def add_item_elements(self, handler, item):
396 396 handler.addQuickElement("title", item['title'])
397 397 handler.addQuickElement("link", "", {"href": item['link'], "rel": "alternate"})
398 398
399 399 if item['pubdate'] is not None:
400 400 handler.addQuickElement('published', rfc3339_date(item['pubdate']))
401 401
402 402 if item['updateddate'] is not None:
403 403 handler.addQuickElement('updated', rfc3339_date(item['updateddate']))
404 404
405 405 # Author information.
406 406 if item['author_name'] is not None:
407 407 handler.startElement("author", {})
408 408 handler.addQuickElement("name", item['author_name'])
409 409 if item['author_email'] is not None:
410 410 handler.addQuickElement("email", item['author_email'])
411 411 if item['author_link'] is not None:
412 412 handler.addQuickElement("uri", item['author_link'])
413 413 handler.endElement("author")
414 414
415 415 # Unique ID.
416 416 if item['unique_id'] is not None:
417 417 unique_id = item['unique_id']
418 418 else:
419 419 unique_id = get_tag_uri(item['link'], item['pubdate'])
420 420 handler.addQuickElement("id", unique_id)
421 421
422 422 # Summary.
423 423 if item['description'] is not None:
424 424 handler.addQuickElement("summary", item['description'], {"type": "html"})
425 425
426 426 # Enclosures.
427 427 for enclosure in item['enclosures']:
428 428 handler.addQuickElement('link', '', {
429 429 'rel': 'enclosure',
430 430 'href': enclosure.url,
431 431 'length': enclosure.length,
432 432 'type': enclosure.mime_type,
433 433 })
434 434
435 435 # Categories.
436 436 for cat in item['categories']:
437 437 handler.addQuickElement("category", "", {"term": cat})
438 438
439 439 # Rights.
440 440 if item['item_copyright'] is not None:
441 441 handler.addQuickElement("rights", item['item_copyright'])
442 442
443 443
444 444 # This isolates the decision of what the system default is, so calling code can
445 445 # do "feedgenerator.DefaultFeed" instead of "feedgenerator.Rss201rev2Feed".
446 446 DefaultFeed = Rss201rev2Feed No newline at end of file
@@ -1,311 +1,311 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Index schema for RhodeCode
23 23 """
24 24
25 from __future__ import absolute_import
25
26 26 import os
27 27 import re
28 28 import logging
29 29
30 30 from whoosh import query as query_lib
31 31 from whoosh.highlight import HtmlFormatter, ContextFragmenter
32 32 from whoosh.index import create_in, open_dir, exists_in, EmptyIndexError
33 33 from whoosh.qparser import QueryParser, QueryParserError
34 34
35 35 import rhodecode.lib.helpers as h
36 36 from rhodecode.lib.index import BaseSearcher
37 37 from rhodecode.lib.utils2 import safe_unicode
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 try:
43 43 # we first try to import from rhodecode tools, fallback to copies if
44 44 # we're unable to
45 45 from rhodecode_tools.lib.fts_index.whoosh_schema import (
46 46 ANALYZER, FILE_INDEX_NAME, FILE_SCHEMA, COMMIT_INDEX_NAME,
47 47 COMMIT_SCHEMA)
48 48 except ImportError:
49 49 log.warning('rhodecode_tools schema not available, doing a fallback '
50 50 'import from `rhodecode.lib.index.whoosh_fallback_schema`')
51 51 from rhodecode.lib.index.whoosh_fallback_schema import (
52 52 ANALYZER, FILE_INDEX_NAME, FILE_SCHEMA, COMMIT_INDEX_NAME,
53 53 COMMIT_SCHEMA)
54 54
55 55
56 56 FORMATTER = HtmlFormatter('span', between='\n<span class="break">...</span>\n')
57 57 FRAGMENTER = ContextFragmenter(200)
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 class WhooshSearcher(BaseSearcher):
63 63 # this also shows in UI
64 64 query_lang_doc = 'http://whoosh.readthedocs.io/en/latest/querylang.html'
65 65 name = 'whoosh'
66 66
67 67 def __init__(self, config):
68 68 super(Searcher, self).__init__()
69 69 self.config = config
70 70 if not os.path.isdir(self.config['location']):
71 71 os.makedirs(self.config['location'])
72 72
73 73 opener = create_in
74 74 if exists_in(self.config['location'], indexname=FILE_INDEX_NAME):
75 75 opener = open_dir
76 76 file_index = opener(self.config['location'], schema=FILE_SCHEMA,
77 77 indexname=FILE_INDEX_NAME)
78 78
79 79 opener = create_in
80 80 if exists_in(self.config['location'], indexname=COMMIT_INDEX_NAME):
81 81 opener = open_dir
82 82 changeset_index = opener(self.config['location'], schema=COMMIT_SCHEMA,
83 83 indexname=COMMIT_INDEX_NAME)
84 84
85 85 self.commit_schema = COMMIT_SCHEMA
86 86 self.commit_index = changeset_index
87 87 self.file_schema = FILE_SCHEMA
88 88 self.file_index = file_index
89 89 self.searcher = None
90 90
91 91 def cleanup(self):
92 92 if self.searcher:
93 93 self.searcher.close()
94 94
95 95 def _extend_query(self, query):
96 96 hashes = re.compile('([0-9a-f]{5,40})').findall(query)
97 97 if hashes:
98 98 hashes_or_query = ' OR '.join('commit_id:%s*' % h for h in hashes)
99 99 query = u'(%s) OR %s' % (query, hashes_or_query)
100 100 return query
101 101
102 102 def sort_def(self, search_type, direction, sort_field):
103 103
104 104 if search_type == 'commit':
105 105 field_defs = {
106 106 'message': 'message',
107 107 'date': 'date',
108 108 'author_email': 'author',
109 109 }
110 110 elif search_type == 'path':
111 111 field_defs = {
112 112 'file': 'path',
113 113 'size': 'size',
114 114 'lines': 'lines',
115 115 }
116 116 elif search_type == 'content':
117 117 # NOTE(dan): content doesn't support any sorting
118 118 field_defs = {}
119 119 else:
120 120 return ''
121 121
122 122 if sort_field in field_defs:
123 123 return field_defs[sort_field]
124 124
125 125 def search(self, query, document_type, search_user,
126 126 repo_name=None, repo_group_name=None,
127 127 requested_page=1, page_limit=10, sort=None, raise_on_exc=True):
128 128
129 129 original_query = query
130 130 query = self._extend_query(query)
131 131
132 132 log.debug(u'QUERY: %s on %s', query, document_type)
133 133 result = {
134 134 'results': [],
135 135 'count': 0,
136 136 'error': None,
137 137 'runtime': 0
138 138 }
139 139 search_type, index_name, schema_defn = self._prepare_for_search(
140 140 document_type)
141 141 self._init_searcher(index_name)
142 142 try:
143 143 qp = QueryParser(search_type, schema=schema_defn)
144 144 allowed_repos_filter = self._get_repo_filter(
145 145 search_user, repo_name)
146 146 try:
147 147 query = qp.parse(safe_unicode(query))
148 148 log.debug('query: %s (%s)', query, repr(query))
149 149
150 150 reverse, sorted_by = False, None
151 151 direction, sort_field = self.get_sort(search_type, sort)
152 152 if sort_field:
153 153 sort_definition = self.sort_def(search_type, direction, sort_field)
154 154 if sort_definition:
155 155 sorted_by = sort_definition
156 156 if direction == Searcher.DIRECTION_DESC:
157 157 reverse = True
158 158 if direction == Searcher.DIRECTION_ASC:
159 159 reverse = False
160 160
161 161 whoosh_results = self.searcher.search(
162 162 query, filter=allowed_repos_filter, limit=None,
163 163 sortedby=sorted_by, reverse=reverse)
164 164
165 165 # fixes for 32k limit that whoosh uses for highlight
166 166 whoosh_results.fragmenter.charlimit = None
167 167 res_ln = whoosh_results.scored_length()
168 168 result['runtime'] = whoosh_results.runtime
169 169 result['count'] = res_ln
170 170 result['results'] = WhooshResultWrapper(
171 171 search_type, res_ln, whoosh_results)
172 172
173 173 except QueryParserError:
174 174 result['error'] = 'Invalid search query. Try quoting it.'
175 175 except (EmptyIndexError, IOError, OSError):
176 176 msg = 'There is no index to search in. Please run whoosh indexer'
177 177 log.exception(msg)
178 178 result['error'] = msg
179 179 except Exception:
180 180 msg = 'An error occurred during this search operation'
181 181 log.exception(msg)
182 182 result['error'] = msg
183 183
184 184 return result
185 185
186 186 def statistics(self, translator):
187 187 _ = translator
188 188 stats = [
189 189 {'key': _('Index Type'), 'value': 'Whoosh'},
190 190 {'sep': True},
191 191
192 192 {'key': _('File Index'), 'value': str(self.file_index)},
193 193 {'key': _('Indexed documents'), 'value': self.file_index.doc_count()},
194 194 {'key': _('Last update'), 'value': h.time_to_datetime(self.file_index.last_modified())},
195 195
196 196 {'sep': True},
197 197
198 198 {'key': _('Commit index'), 'value': str(self.commit_index)},
199 199 {'key': _('Indexed documents'), 'value': str(self.commit_index.doc_count())},
200 200 {'key': _('Last update'), 'value': h.time_to_datetime(self.commit_index.last_modified())}
201 201 ]
202 202 return stats
203 203
204 204 def _get_repo_filter(self, auth_user, repo_name):
205 205
206 206 allowed_to_search = [
207 207 repo for repo, perm in
208 208 auth_user.permissions['repositories'].items()
209 209 if perm != 'repository.none']
210 210
211 211 if repo_name:
212 212 repo_filter = [query_lib.Term('repository', repo_name)]
213 213
214 214 elif 'hg.admin' in auth_user.permissions.get('global', []):
215 215 return None
216 216
217 217 else:
218 218 repo_filter = [query_lib.Term('repository', _rn)
219 219 for _rn in allowed_to_search]
220 220 # in case we're not allowed to search anywhere, it's a trick
221 221 # to tell whoosh we're filtering, on ALL results
222 222 repo_filter = repo_filter or [query_lib.Term('repository', '')]
223 223
224 224 return query_lib.Or(repo_filter)
225 225
226 226 def _prepare_for_search(self, cur_type):
227 227 search_type = {
228 228 'content': 'content',
229 229 'commit': 'message',
230 230 'path': 'path',
231 231 'repository': 'repository'
232 232 }.get(cur_type, 'content')
233 233
234 234 index_name = {
235 235 'content': FILE_INDEX_NAME,
236 236 'commit': COMMIT_INDEX_NAME,
237 237 'path': FILE_INDEX_NAME
238 238 }.get(cur_type, FILE_INDEX_NAME)
239 239
240 240 schema_defn = {
241 241 'content': self.file_schema,
242 242 'commit': self.commit_schema,
243 243 'path': self.file_schema
244 244 }.get(cur_type, self.file_schema)
245 245
246 246 log.debug('IDX: %s', index_name)
247 247 log.debug('SCHEMA: %s', schema_defn)
248 248 return search_type, index_name, schema_defn
249 249
250 250 def _init_searcher(self, index_name):
251 251 idx = open_dir(self.config['location'], indexname=index_name)
252 252 self.searcher = idx.searcher()
253 253 return self.searcher
254 254
255 255
256 256 Searcher = WhooshSearcher
257 257
258 258
259 259 class WhooshResultWrapper(object):
260 260 def __init__(self, search_type, total_hits, results):
261 261 self.search_type = search_type
262 262 self.results = results
263 263 self.total_hits = total_hits
264 264
265 265 def __str__(self):
266 266 return '<%s at %s>' % (self.__class__.__name__, len(self))
267 267
268 268 def __repr__(self):
269 269 return self.__str__()
270 270
271 271 def __len__(self):
272 272 return self.total_hits
273 273
274 274 def __iter__(self):
275 275 """
276 276 Allows Iteration over results,and lazy generate content
277 277
278 278 *Requires* implementation of ``__getitem__`` method.
279 279 """
280 280 for hit in self.results:
281 281 yield self.get_full_content(hit)
282 282
283 283 def __getitem__(self, key):
284 284 """
285 285 Slicing of resultWrapper
286 286 """
287 287 i, j = key.start, key.stop
288 288 for hit in self.results[i:j]:
289 289 yield self.get_full_content(hit)
290 290
291 291 def get_full_content(self, hit):
292 292 # TODO: marcink: this feels like an overkill, there's a lot of data
293 293 # inside hit object, and we don't need all
294 294 res = dict(hit)
295 295 # elastic search uses that, we set it empty so it fallbacks to regular HL logic
296 296 res['content_highlight'] = ''
297 297
298 298 f_path = '' # pragma: no cover
299 299 if self.search_type in ['content', 'path']:
300 300 f_path = res['path'][len(res['repository']):]
301 301 f_path = f_path.lstrip(os.sep)
302 302
303 303 if self.search_type == 'content':
304 304 res.update({'content_short_hl': hit.highlights('content'),
305 305 'f_path': f_path})
306 306 elif self.search_type == 'path':
307 307 res.update({'f_path': f_path})
308 308 elif self.search_type == 'message':
309 309 res.update({'message_hl': hit.highlights('message')})
310 310
311 311 return res
@@ -1,75 +1,75 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Whoosh fallback schema for RhodeCode in case rhodecode_tools defined one is
23 23 not available
24 24 """
25 25
26 from __future__ import absolute_import
26
27 27
28 28 from whoosh.analysis import RegexTokenizer, LowercaseFilter
29 29 from whoosh.formats import Characters
30 30 from whoosh.fields import (
31 31 TEXT, ID, STORED, NUMERIC, BOOLEAN, Schema, FieldType, DATETIME)
32 32
33 33 # CUSTOM ANALYZER wordsplit + lowercase filter for case insensitive search
34 34 ANALYZER = RegexTokenizer(expression=r"\w+") | LowercaseFilter()
35 35
36 36 # FILE INDEX SCHEMA DEFINITION
37 37 FILE_INDEX_NAME = 'FILE_INDEX'
38 38 FILE_SCHEMA = Schema(
39 39 fileid=ID(unique=True), # Path
40 40 repository=ID(stored=True),
41 41 repository_id=NUMERIC(unique=True, stored=True), # Numeric id of repo
42 42 repo_name=TEXT(stored=True),
43 43 owner=TEXT(),
44 44 path=TEXT(stored=True),
45 45 content=FieldType(format=Characters(), analyzer=ANALYZER,
46 46 scorable=True, stored=True),
47 47 modtime=STORED(),
48 48 md5=STORED(),
49 49 extension=ID(stored=True),
50 50 commit_id=TEXT(stored=True),
51 51
52 52 size=NUMERIC(int, 64, signed=False, stored=True),
53 53 mimetype=TEXT(stored=True),
54 54 lines=NUMERIC(int, 64, signed=False, stored=True),
55 55 )
56 56
57 57
58 58 # COMMIT INDEX SCHEMA
59 59 COMMIT_INDEX_NAME = 'COMMIT_INDEX'
60 60 COMMIT_SCHEMA = Schema(
61 61 commit_id=ID(unique=True, stored=True),
62 62 repository=ID(unique=True, stored=True),
63 63 repository_id=NUMERIC(unique=True, stored=True),
64 64 commit_idx=NUMERIC(stored=True, sortable=True),
65 65 commit_idx_sort=ID(),
66 66 date=NUMERIC(int, 64, signed=False, stored=True, sortable=True),
67 67 owner=TEXT(stored=True),
68 68 author=TEXT(stored=True),
69 69 message=FieldType(format=Characters(), analyzer=ANALYZER,
70 70 scorable=True, stored=True),
71 71 parents=TEXT(stored=True),
72 72 added=TEXT(stored=True), # space separated names of added files
73 73 removed=TEXT(stored=True), # space separated names of removed files
74 74 changed=TEXT(stored=True), # space separated names of changed files
75 75 )
@@ -1,161 +1,161 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Utilities aimed to help achieve mostly basic tasks.
23 23 """
24 24
25 25
26 from __future__ import division
26
27 27
28 28 import re
29 29 import os
30 30 import time
31 31 import datetime
32 32 import logging
33 33
34 34 from rhodecode.lib.vcs.conf import settings
35 35 from rhodecode.lib.vcs.exceptions import VCSError, VCSBackendNotSupportedError
36 36
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 def get_scm(path):
42 42 """
43 43 Returns one of alias from ``ALIASES`` (in order of precedence same as
44 44 shortcuts given in ``ALIASES``) and working dir path for the given
45 45 argument. If no scm-specific directory is found or more than one scm is
46 46 found at that directory, ``VCSError`` is raised.
47 47 """
48 48 if not os.path.isdir(path):
49 49 raise VCSError("Given path %s is not a directory" % path)
50 50
51 51 found_scms = [(scm, path) for scm in get_scms_for_path(path)]
52 52
53 53 if len(found_scms) > 1:
54 54 found = ', '.join((x[0] for x in found_scms))
55 55 raise VCSError(
56 56 'More than one [%s] scm found at given path %s' % (found, path))
57 57
58 58 if len(found_scms) is 0:
59 59 raise VCSError('No scm found at given path %s' % path)
60 60
61 61 return found_scms[0]
62 62
63 63
64 64 def get_scm_backend(backend_type):
65 65 from rhodecode.lib.vcs.backends import get_backend
66 66 return get_backend(backend_type)
67 67
68 68
69 69 def get_scms_for_path(path):
70 70 """
71 71 Returns all scm's found at the given path. If no scm is recognized
72 72 - empty list is returned.
73 73
74 74 :param path: path to directory which should be checked. May be callable.
75 75
76 76 :raises VCSError: if given ``path`` is not a directory
77 77 """
78 78 from rhodecode.lib.vcs.backends import get_backend
79 79 if hasattr(path, '__call__'):
80 80 path = path()
81 81 if not os.path.isdir(path):
82 82 raise VCSError("Given path %r is not a directory" % path)
83 83
84 84 result = []
85 85 for key in settings.available_aliases():
86 86 try:
87 87 backend = get_backend(key)
88 88 except VCSBackendNotSupportedError:
89 89 log.warning('VCSBackendNotSupportedError: %s not supported', key)
90 90 continue
91 91 if backend.is_valid_repository(path):
92 92 result.append(key)
93 93 return result
94 94
95 95
96 96 def parse_datetime(text):
97 97 """
98 98 Parses given text and returns ``datetime.datetime`` instance or raises
99 99 ``ValueError``.
100 100
101 101 :param text: string of desired date/datetime or something more verbose,
102 102 like *yesterday*, *2weeks 3days*, etc.
103 103 """
104 104 if not text:
105 105 raise ValueError('Wrong date: "%s"' % text)
106 106
107 107 if isinstance(text, datetime.datetime):
108 108 return text
109 109
110 110 # we limit a format to no include microseconds e.g 2017-10-17t17:48:23.XXXX
111 111 text = text.strip().lower()[:19]
112 112
113 113 input_formats = (
114 114 '%Y-%m-%d %H:%M:%S',
115 115 '%Y-%m-%dt%H:%M:%S',
116 116 '%Y-%m-%d %H:%M',
117 117 '%Y-%m-%dt%H:%M',
118 118 '%Y-%m-%d',
119 119 '%m/%d/%Y %H:%M:%S',
120 120 '%m/%d/%Yt%H:%M:%S',
121 121 '%m/%d/%Y %H:%M',
122 122 '%m/%d/%Yt%H:%M',
123 123 '%m/%d/%Y',
124 124 '%m/%d/%y %H:%M:%S',
125 125 '%m/%d/%yt%H:%M:%S',
126 126 '%m/%d/%y %H:%M',
127 127 '%m/%d/%yt%H:%M',
128 128 '%m/%d/%y',
129 129 )
130 130 for format_def in input_formats:
131 131 try:
132 132 return datetime.datetime(*time.strptime(text, format_def)[:6])
133 133 except ValueError:
134 134 pass
135 135
136 136 # Try descriptive texts
137 137 if text == 'tomorrow':
138 138 future = datetime.datetime.now() + datetime.timedelta(days=1)
139 139 args = future.timetuple()[:3] + (23, 59, 59)
140 140 return datetime.datetime(*args)
141 141 elif text == 'today':
142 142 return datetime.datetime(*datetime.datetime.today().timetuple()[:3])
143 143 elif text == 'now':
144 144 return datetime.datetime.now()
145 145 elif text == 'yesterday':
146 146 past = datetime.datetime.now() - datetime.timedelta(days=1)
147 147 return datetime.datetime(*past.timetuple()[:3])
148 148 else:
149 149 days = 0
150 150 matched = re.match(
151 151 r'^((?P<weeks>\d+) ?w(eeks?)?)? ?((?P<days>\d+) ?d(ays?)?)?$', text)
152 152 if matched:
153 153 groupdict = matched.groupdict()
154 154 if groupdict['days']:
155 155 days += int(matched.groupdict()['days'])
156 156 if groupdict['weeks']:
157 157 days += int(matched.groupdict()['weeks']) * 7
158 158 past = datetime.datetime.now() - datetime.timedelta(days=days)
159 159 return datetime.datetime(*past.timetuple()[:3])
160 160
161 161 raise ValueError('Wrong date: "%s"' % text)
@@ -1,84 +1,84 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 from __future__ import unicode_literals
21
22 22
23 23 import time
24 24
25 25 from whoosh import index
26 26 import mock
27 27 import pytest
28 28
29 29 import rhodecode
30 30 from rhodecode.lib.auth import AuthUser
31 31 from rhodecode.lib.index import whoosh, searcher_from_config
32 32
33 33
34 34 @pytest.mark.parametrize("name_suffix", [
35 35 "",
36 36 "UpPeRcAsE",
37 37 ])
38 38 def test_search_finds_results(
39 39 tmpdir, backend_random, user_regular, name_suffix):
40 40 repo = backend_random.create_repo(name_suffix=name_suffix)
41 41
42 42 search_location = tmpdir.strpath
43 43 create_commit_index_with_one_document(
44 44 search_location, repo_name=repo.repo_name)
45 45
46 46 auth_user = AuthUser(user_id=user_regular.user_id)
47 47 with mock.patch.dict(rhodecode.CONFIG,
48 48 {'search.location': search_location}):
49 49 searcher = searcher_from_config(rhodecode.CONFIG)
50 50
51 51 search_result = searcher.search(
52 52 "Test", document_type='commit', search_user=auth_user, repo_name=None)
53 53 results = list(search_result['results'])
54 54 assert len(results) == 1
55 55 assert results[0]['repository'] == repo.repo_name
56 56
57 57
58 58 def create_commit_index_with_one_document(search_location, repo_name):
59 59 """
60 60 Provides a test index based on our search schema.
61 61
62 62 The full details of index creation are found inside of `rhodecode-tools`.
63 63 The intention of this function is to provide just enough so that the
64 64 search works.
65 65 """
66 66 test_index = index.create_in(
67 67 search_location, whoosh.COMMIT_SCHEMA,
68 68 indexname=whoosh.COMMIT_INDEX_NAME)
69 69 writer = test_index.writer()
70 70
71 71 writer.add_document(
72 72 commit_id="fake_commit_id",
73 73 commit_idx=1,
74 74 owner="Test Owner",
75 75 date=time.time(),
76 76 repository=repo_name,
77 77 author="Test Author",
78 78 message="Test Message",
79 79 added="added_1.txt added_2.txt",
80 80 removed="removed_1.txt removed_2.txt",
81 81 changed="changed_1.txt changed_2.txt",
82 82 parents="fake_parent_1_id fake_parent_2_id",
83 83 )
84 84 writer.commit()
General Comments 0
You need to be logged in to leave comments. Login now