##// END OF EJS Templates
Rewrite of the age() utility function so it can be translated.
Vincent Duvert -
r2303:7090e394 beta
parent child Browse files
Show More
@@ -1,407 +1,445 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.utils
3 rhodecode.lib.utils
4 ~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~
5
5
6 Some simple helper functions
6 Some simple helper functions
7
7
8 :created_on: Jan 5, 2011
8 :created_on: Jan 5, 2011
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
25
26 import re
26 import re
27 from datetime import datetime
28 from pylons.i18n.translation import _, ungettext
27 from rhodecode.lib.vcs.utils.lazy import LazyProperty
29 from rhodecode.lib.vcs.utils.lazy import LazyProperty
28
30
29
31
30 def __get_lem():
32 def __get_lem():
31 """
33 """
32 Get language extension map based on what's inside pygments lexers
34 Get language extension map based on what's inside pygments lexers
33 """
35 """
34 from pygments import lexers
36 from pygments import lexers
35 from string import lower
37 from string import lower
36 from collections import defaultdict
38 from collections import defaultdict
37
39
38 d = defaultdict(lambda: [])
40 d = defaultdict(lambda: [])
39
41
40 def __clean(s):
42 def __clean(s):
41 s = s.lstrip('*')
43 s = s.lstrip('*')
42 s = s.lstrip('.')
44 s = s.lstrip('.')
43
45
44 if s.find('[') != -1:
46 if s.find('[') != -1:
45 exts = []
47 exts = []
46 start, stop = s.find('['), s.find(']')
48 start, stop = s.find('['), s.find(']')
47
49
48 for suffix in s[start + 1:stop]:
50 for suffix in s[start + 1:stop]:
49 exts.append(s[:s.find('[')] + suffix)
51 exts.append(s[:s.find('[')] + suffix)
50 return map(lower, exts)
52 return map(lower, exts)
51 else:
53 else:
52 return map(lower, [s])
54 return map(lower, [s])
53
55
54 for lx, t in sorted(lexers.LEXERS.items()):
56 for lx, t in sorted(lexers.LEXERS.items()):
55 m = map(__clean, t[-2])
57 m = map(__clean, t[-2])
56 if m:
58 if m:
57 m = reduce(lambda x, y: x + y, m)
59 m = reduce(lambda x, y: x + y, m)
58 for ext in m:
60 for ext in m:
59 desc = lx.replace('Lexer', '')
61 desc = lx.replace('Lexer', '')
60 d[ext].append(desc)
62 d[ext].append(desc)
61
63
62 return dict(d)
64 return dict(d)
63
65
64 def str2bool(_str):
66 def str2bool(_str):
65 """
67 """
66 returs True/False value from given string, it tries to translate the
68 returs True/False value from given string, it tries to translate the
67 string into boolean
69 string into boolean
68
70
69 :param _str: string value to translate into boolean
71 :param _str: string value to translate into boolean
70 :rtype: boolean
72 :rtype: boolean
71 :returns: boolean from given string
73 :returns: boolean from given string
72 """
74 """
73 if _str is None:
75 if _str is None:
74 return False
76 return False
75 if _str in (True, False):
77 if _str in (True, False):
76 return _str
78 return _str
77 _str = str(_str).strip().lower()
79 _str = str(_str).strip().lower()
78 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
80 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
79
81
80
82
81 def convert_line_endings(line, mode):
83 def convert_line_endings(line, mode):
82 """
84 """
83 Converts a given line "line end" accordingly to given mode
85 Converts a given line "line end" accordingly to given mode
84
86
85 Available modes are::
87 Available modes are::
86 0 - Unix
88 0 - Unix
87 1 - Mac
89 1 - Mac
88 2 - DOS
90 2 - DOS
89
91
90 :param line: given line to convert
92 :param line: given line to convert
91 :param mode: mode to convert to
93 :param mode: mode to convert to
92 :rtype: str
94 :rtype: str
93 :return: converted line according to mode
95 :return: converted line according to mode
94 """
96 """
95 from string import replace
97 from string import replace
96
98
97 if mode == 0:
99 if mode == 0:
98 line = replace(line, '\r\n', '\n')
100 line = replace(line, '\r\n', '\n')
99 line = replace(line, '\r', '\n')
101 line = replace(line, '\r', '\n')
100 elif mode == 1:
102 elif mode == 1:
101 line = replace(line, '\r\n', '\r')
103 line = replace(line, '\r\n', '\r')
102 line = replace(line, '\n', '\r')
104 line = replace(line, '\n', '\r')
103 elif mode == 2:
105 elif mode == 2:
104 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
106 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
105 return line
107 return line
106
108
107
109
108 def detect_mode(line, default):
110 def detect_mode(line, default):
109 """
111 """
110 Detects line break for given line, if line break couldn't be found
112 Detects line break for given line, if line break couldn't be found
111 given default value is returned
113 given default value is returned
112
114
113 :param line: str line
115 :param line: str line
114 :param default: default
116 :param default: default
115 :rtype: int
117 :rtype: int
116 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
118 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
117 """
119 """
118 if line.endswith('\r\n'):
120 if line.endswith('\r\n'):
119 return 2
121 return 2
120 elif line.endswith('\n'):
122 elif line.endswith('\n'):
121 return 0
123 return 0
122 elif line.endswith('\r'):
124 elif line.endswith('\r'):
123 return 1
125 return 1
124 else:
126 else:
125 return default
127 return default
126
128
127
129
128 def generate_api_key(username, salt=None):
130 def generate_api_key(username, salt=None):
129 """
131 """
130 Generates unique API key for given username, if salt is not given
132 Generates unique API key for given username, if salt is not given
131 it'll be generated from some random string
133 it'll be generated from some random string
132
134
133 :param username: username as string
135 :param username: username as string
134 :param salt: salt to hash generate KEY
136 :param salt: salt to hash generate KEY
135 :rtype: str
137 :rtype: str
136 :returns: sha1 hash from username+salt
138 :returns: sha1 hash from username+salt
137 """
139 """
138 from tempfile import _RandomNameSequence
140 from tempfile import _RandomNameSequence
139 import hashlib
141 import hashlib
140
142
141 if salt is None:
143 if salt is None:
142 salt = _RandomNameSequence().next()
144 salt = _RandomNameSequence().next()
143
145
144 return hashlib.sha1(username + salt).hexdigest()
146 return hashlib.sha1(username + salt).hexdigest()
145
147
146
148
147 def safe_unicode(str_, from_encoding=None):
149 def safe_unicode(str_, from_encoding=None):
148 """
150 """
149 safe unicode function. Does few trick to turn str_ into unicode
151 safe unicode function. Does few trick to turn str_ into unicode
150
152
151 In case of UnicodeDecode error we try to return it with encoding detected
153 In case of UnicodeDecode error we try to return it with encoding detected
152 by chardet library if it fails fallback to unicode with errors replaced
154 by chardet library if it fails fallback to unicode with errors replaced
153
155
154 :param str_: string to decode
156 :param str_: string to decode
155 :rtype: unicode
157 :rtype: unicode
156 :returns: unicode object
158 :returns: unicode object
157 """
159 """
158 if isinstance(str_, unicode):
160 if isinstance(str_, unicode):
159 return str_
161 return str_
160
162
161 if not from_encoding:
163 if not from_encoding:
162 import rhodecode
164 import rhodecode
163 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
165 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
164 from_encoding = DEFAULT_ENCODING
166 from_encoding = DEFAULT_ENCODING
165
167
166 try:
168 try:
167 return unicode(str_)
169 return unicode(str_)
168 except UnicodeDecodeError:
170 except UnicodeDecodeError:
169 pass
171 pass
170
172
171 try:
173 try:
172 return unicode(str_, from_encoding)
174 return unicode(str_, from_encoding)
173 except UnicodeDecodeError:
175 except UnicodeDecodeError:
174 pass
176 pass
175
177
176 try:
178 try:
177 import chardet
179 import chardet
178 encoding = chardet.detect(str_)['encoding']
180 encoding = chardet.detect(str_)['encoding']
179 if encoding is None:
181 if encoding is None:
180 raise Exception()
182 raise Exception()
181 return str_.decode(encoding)
183 return str_.decode(encoding)
182 except (ImportError, UnicodeDecodeError, Exception):
184 except (ImportError, UnicodeDecodeError, Exception):
183 return unicode(str_, from_encoding, 'replace')
185 return unicode(str_, from_encoding, 'replace')
184
186
185
187
186 def safe_str(unicode_, to_encoding=None):
188 def safe_str(unicode_, to_encoding=None):
187 """
189 """
188 safe str function. Does few trick to turn unicode_ into string
190 safe str function. Does few trick to turn unicode_ into string
189
191
190 In case of UnicodeEncodeError we try to return it with encoding detected
192 In case of UnicodeEncodeError we try to return it with encoding detected
191 by chardet library if it fails fallback to string with errors replaced
193 by chardet library if it fails fallback to string with errors replaced
192
194
193 :param unicode_: unicode to encode
195 :param unicode_: unicode to encode
194 :rtype: str
196 :rtype: str
195 :returns: str object
197 :returns: str object
196 """
198 """
197
199
198 # if it's not basestr cast to str
200 # if it's not basestr cast to str
199 if not isinstance(unicode_, basestring):
201 if not isinstance(unicode_, basestring):
200 return str(unicode_)
202 return str(unicode_)
201
203
202 if isinstance(unicode_, str):
204 if isinstance(unicode_, str):
203 return unicode_
205 return unicode_
204
206
205 if not to_encoding:
207 if not to_encoding:
206 import rhodecode
208 import rhodecode
207 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
209 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
208 to_encoding = DEFAULT_ENCODING
210 to_encoding = DEFAULT_ENCODING
209
211
210 try:
212 try:
211 return unicode_.encode(to_encoding)
213 return unicode_.encode(to_encoding)
212 except UnicodeEncodeError:
214 except UnicodeEncodeError:
213 pass
215 pass
214
216
215 try:
217 try:
216 import chardet
218 import chardet
217 encoding = chardet.detect(unicode_)['encoding']
219 encoding = chardet.detect(unicode_)['encoding']
218 if encoding is None:
220 if encoding is None:
219 raise UnicodeEncodeError()
221 raise UnicodeEncodeError()
220
222
221 return unicode_.encode(encoding)
223 return unicode_.encode(encoding)
222 except (ImportError, UnicodeEncodeError):
224 except (ImportError, UnicodeEncodeError):
223 return unicode_.encode(to_encoding, 'replace')
225 return unicode_.encode(to_encoding, 'replace')
224
226
225 return safe_str
227 return safe_str
226
228
227
229
228 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
230 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
229 """
231 """
230 Custom engine_from_config functions that makes sure we use NullPool for
232 Custom engine_from_config functions that makes sure we use NullPool for
231 file based sqlite databases. This prevents errors on sqlite. This only
233 file based sqlite databases. This prevents errors on sqlite. This only
232 applies to sqlalchemy versions < 0.7.0
234 applies to sqlalchemy versions < 0.7.0
233
235
234 """
236 """
235 import sqlalchemy
237 import sqlalchemy
236 from sqlalchemy import engine_from_config as efc
238 from sqlalchemy import engine_from_config as efc
237 import logging
239 import logging
238
240
239 if int(sqlalchemy.__version__.split('.')[1]) < 7:
241 if int(sqlalchemy.__version__.split('.')[1]) < 7:
240
242
241 # This solution should work for sqlalchemy < 0.7.0, and should use
243 # This solution should work for sqlalchemy < 0.7.0, and should use
242 # proxy=TimerProxy() for execution time profiling
244 # proxy=TimerProxy() for execution time profiling
243
245
244 from sqlalchemy.pool import NullPool
246 from sqlalchemy.pool import NullPool
245 url = configuration[prefix + 'url']
247 url = configuration[prefix + 'url']
246
248
247 if url.startswith('sqlite'):
249 if url.startswith('sqlite'):
248 kwargs.update({'poolclass': NullPool})
250 kwargs.update({'poolclass': NullPool})
249 return efc(configuration, prefix, **kwargs)
251 return efc(configuration, prefix, **kwargs)
250 else:
252 else:
251 import time
253 import time
252 from sqlalchemy import event
254 from sqlalchemy import event
253 from sqlalchemy.engine import Engine
255 from sqlalchemy.engine import Engine
254
256
255 log = logging.getLogger('sqlalchemy.engine')
257 log = logging.getLogger('sqlalchemy.engine')
256 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38)
258 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38)
257 engine = efc(configuration, prefix, **kwargs)
259 engine = efc(configuration, prefix, **kwargs)
258
260
259 def color_sql(sql):
261 def color_sql(sql):
260 COLOR_SEQ = "\033[1;%dm"
262 COLOR_SEQ = "\033[1;%dm"
261 COLOR_SQL = YELLOW
263 COLOR_SQL = YELLOW
262 normal = '\x1b[0m'
264 normal = '\x1b[0m'
263 return ''.join([COLOR_SEQ % COLOR_SQL, sql, normal])
265 return ''.join([COLOR_SEQ % COLOR_SQL, sql, normal])
264
266
265 if configuration['debug']:
267 if configuration['debug']:
266 #attach events only for debug configuration
268 #attach events only for debug configuration
267
269
268 def before_cursor_execute(conn, cursor, statement,
270 def before_cursor_execute(conn, cursor, statement,
269 parameters, context, executemany):
271 parameters, context, executemany):
270 context._query_start_time = time.time()
272 context._query_start_time = time.time()
271 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
273 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
272
274
273
275
274 def after_cursor_execute(conn, cursor, statement,
276 def after_cursor_execute(conn, cursor, statement,
275 parameters, context, executemany):
277 parameters, context, executemany):
276 total = time.time() - context._query_start_time
278 total = time.time() - context._query_start_time
277 log.info(color_sql("<<<<< TOTAL TIME: %f <<<<<" % total))
279 log.info(color_sql("<<<<< TOTAL TIME: %f <<<<<" % total))
278
280
279 event.listen(engine, "before_cursor_execute",
281 event.listen(engine, "before_cursor_execute",
280 before_cursor_execute)
282 before_cursor_execute)
281 event.listen(engine, "after_cursor_execute",
283 event.listen(engine, "after_cursor_execute",
282 after_cursor_execute)
284 after_cursor_execute)
283
285
284 return engine
286 return engine
285
287
286
288
287 def age(curdate):
289 def age(prevdate):
288 """
290 """
289 turns a datetime into an age string.
291 turns a datetime into an age string.
290
292
291 :param curdate: datetime object
293 :param prevdate: datetime object
292 :rtype: unicode
294 :rtype: unicode
293 :returns: unicode words describing age
295 :returns: unicode words describing age
294 """
296 """
295
297
296 from datetime import datetime
298 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
297 from webhelpers.date import time_ago_in_words
299 deltas = {}
300
301 # Get date parts deltas
302 now = datetime.now()
303 for part in order:
304 deltas[part] = getattr(now, part) - getattr(prevdate, part)
305
306 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
307 # not 1 hour, -59 minutes and -59 seconds)
308
309 for num, length in [(5, 60), (4, 60), (3, 24)]: # seconds, minutes, hours
310 part = order[num]
311 carry_part = order[num - 1]
298
312
299 _ = lambda s: s
313 if deltas[part] < 0:
314 deltas[part] += length
315 deltas[carry_part] -= 1
300
316
301 if not curdate:
317 # Same thing for days except that the increment depends on the (variable)
302 return ''
318 # number of days in the month
319 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
320 if deltas['day'] < 0:
321 if prevdate.month == 2 and (prevdate.year % 4 == 0 and
322 (prevdate.year % 100 != 0 or prevdate.year % 400 == 0)):
323 deltas['day'] += 29
324 else:
325 deltas['day'] += month_lengths[prevdate.month - 1]
326
327 deltas['month'] -= 1
303
328
304 agescales = [(_(u"year"), 3600 * 24 * 365),
329 if deltas['month'] < 0:
305 (_(u"month"), 3600 * 24 * 30),
330 deltas['month'] += 12
306 (_(u"day"), 3600 * 24),
331 deltas['year'] -= 1
307 (_(u"hour"), 3600),
332
308 (_(u"minute"), 60),
333 # Format the result
309 (_(u"second"), 1), ]
334 fmt_funcs = {
335 'year': lambda d: ungettext(u'%d year', '%d years', d) % d,
336 'month': lambda d: ungettext(u'%d month', '%d months', d) % d,
337 'day': lambda d: ungettext(u'%d day', '%d days', d) % d,
338 'hour': lambda d: ungettext(u'%d hour', '%d hours', d) % d,
339 'minute': lambda d: ungettext(u'%d minute', '%d minutes', d) % d,
340 'second': lambda d: ungettext(u'%d second', '%d seconds', d) % d,
341 }
310
342
311 age = datetime.now() - curdate
343 for i, part in enumerate(order):
312 age_seconds = (age.days * agescales[2][1]) + age.seconds
344 value = deltas[part]
313 pos = 1
345 if value == 0:
314 for scale in agescales:
346 continue
315 if scale[1] <= age_seconds:
347
316 if pos == 6:
348 if i < 5:
317 pos = 5
349 sub_part = order[i + 1]
318 return '%s %s' % (time_ago_in_words(curdate,
350 sub_value = deltas[sub_part]
319 agescales[pos][0]), _('ago'))
351 else:
320 pos += 1
352 sub_value = 0
353
354 if sub_value == 0:
355 return _(u'%s ago') % fmt_funcs[part](value)
356
357 return _(u'%s and %s ago') % (fmt_funcs[part](value),
358 fmt_funcs[sub_part](sub_value))
321
359
322 return _(u'just now')
360 return _(u'just now')
323
361
324
362
325 def uri_filter(uri):
363 def uri_filter(uri):
326 """
364 """
327 Removes user:password from given url string
365 Removes user:password from given url string
328
366
329 :param uri:
367 :param uri:
330 :rtype: unicode
368 :rtype: unicode
331 :returns: filtered list of strings
369 :returns: filtered list of strings
332 """
370 """
333 if not uri:
371 if not uri:
334 return ''
372 return ''
335
373
336 proto = ''
374 proto = ''
337
375
338 for pat in ('https://', 'http://'):
376 for pat in ('https://', 'http://'):
339 if uri.startswith(pat):
377 if uri.startswith(pat):
340 uri = uri[len(pat):]
378 uri = uri[len(pat):]
341 proto = pat
379 proto = pat
342 break
380 break
343
381
344 # remove passwords and username
382 # remove passwords and username
345 uri = uri[uri.find('@') + 1:]
383 uri = uri[uri.find('@') + 1:]
346
384
347 # get the port
385 # get the port
348 cred_pos = uri.find(':')
386 cred_pos = uri.find(':')
349 if cred_pos == -1:
387 if cred_pos == -1:
350 host, port = uri, None
388 host, port = uri, None
351 else:
389 else:
352 host, port = uri[:cred_pos], uri[cred_pos + 1:]
390 host, port = uri[:cred_pos], uri[cred_pos + 1:]
353
391
354 return filter(None, [proto, host, port])
392 return filter(None, [proto, host, port])
355
393
356
394
357 def credentials_filter(uri):
395 def credentials_filter(uri):
358 """
396 """
359 Returns a url with removed credentials
397 Returns a url with removed credentials
360
398
361 :param uri:
399 :param uri:
362 """
400 """
363
401
364 uri = uri_filter(uri)
402 uri = uri_filter(uri)
365 #check if we have port
403 #check if we have port
366 if len(uri) > 2 and uri[2]:
404 if len(uri) > 2 and uri[2]:
367 uri[2] = ':' + uri[2]
405 uri[2] = ':' + uri[2]
368
406
369 return ''.join(uri)
407 return ''.join(uri)
370
408
371
409
372 def get_changeset_safe(repo, rev):
410 def get_changeset_safe(repo, rev):
373 """
411 """
374 Safe version of get_changeset if this changeset doesn't exists for a
412 Safe version of get_changeset if this changeset doesn't exists for a
375 repo it returns a Dummy one instead
413 repo it returns a Dummy one instead
376
414
377 :param repo:
415 :param repo:
378 :param rev:
416 :param rev:
379 """
417 """
380 from rhodecode.lib.vcs.backends.base import BaseRepository
418 from rhodecode.lib.vcs.backends.base import BaseRepository
381 from rhodecode.lib.vcs.exceptions import RepositoryError
419 from rhodecode.lib.vcs.exceptions import RepositoryError
382 if not isinstance(repo, BaseRepository):
420 if not isinstance(repo, BaseRepository):
383 raise Exception('You must pass an Repository '
421 raise Exception('You must pass an Repository '
384 'object as first argument got %s', type(repo))
422 'object as first argument got %s', type(repo))
385
423
386 try:
424 try:
387 cs = repo.get_changeset(rev)
425 cs = repo.get_changeset(rev)
388 except RepositoryError:
426 except RepositoryError:
389 from rhodecode.lib.utils import EmptyChangeset
427 from rhodecode.lib.utils import EmptyChangeset
390 cs = EmptyChangeset(requested_revision=rev)
428 cs = EmptyChangeset(requested_revision=rev)
391 return cs
429 return cs
392
430
393
431
394 MENTIONS_REGEX = r'(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)(?:\s{1})'
432 MENTIONS_REGEX = r'(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)(?:\s{1})'
395
433
396
434
397 def extract_mentioned_users(s):
435 def extract_mentioned_users(s):
398 """
436 """
399 Returns unique usernames from given string s that have @mention
437 Returns unique usernames from given string s that have @mention
400
438
401 :param s: string to get mentions
439 :param s: string to get mentions
402 """
440 """
403 usrs = set()
441 usrs = set()
404 for username in re.findall(MENTIONS_REGEX, s):
442 for username in re.findall(MENTIONS_REGEX, s):
405 usrs.add(username)
443 usrs.add(username)
406
444
407 return sorted(list(usrs), key=lambda k: k.lower())
445 return sorted(list(usrs), key=lambda k: k.lower())
General Comments 0
You need to be logged in to leave comments. Login now