##// END OF EJS Templates
py3: stop implicitly importing unicode...
Gregory Szorc -
r43356:bbcbb82e default
parent child Browse files
Show More
@@ -1,298 +1,297 b''
1 1 # __init__.py - Startup and module loading logic for Mercurial.
2 2 #
3 3 # Copyright 2015 Gregory Szorc <gregory.szorc@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import sys
11 11
12 12 # Allow 'from mercurial import demandimport' to keep working.
13 13 import hgdemandimport
14 14
15 15 demandimport = hgdemandimport
16 16
17 17 __all__ = []
18 18
19 19 # Python 3 uses a custom module loader that transforms source code between
20 20 # source file reading and compilation. This is done by registering a custom
21 21 # finder that changes the spec for Mercurial modules to use a custom loader.
22 22 if sys.version_info[0] >= 3:
23 23 import importlib
24 24 import importlib.abc
25 25 import io
26 26 import token
27 27 import tokenize
28 28
29 29 class hgpathentryfinder(importlib.abc.MetaPathFinder):
30 30 """A sys.meta_path finder that uses a custom module loader."""
31 31
32 32 def find_spec(self, fullname, path, target=None):
33 33 # Only handle Mercurial-related modules.
34 34 if not fullname.startswith(('mercurial.', 'hgext.')):
35 35 return None
36 36 # don't try to parse binary
37 37 if fullname.startswith('mercurial.cext.'):
38 38 return None
39 39 # third-party packages are expected to be dual-version clean
40 40 if fullname.startswith('mercurial.thirdparty'):
41 41 return None
42 42 # zstd is already dual-version clean, don't try and mangle it
43 43 if fullname.startswith('mercurial.zstd'):
44 44 return None
45 45 # rustext is built for the right python version,
46 46 # don't try and mangle it
47 47 if fullname.startswith('mercurial.rustext'):
48 48 return None
49 49 # pywatchman is already dual-version clean, don't try and mangle it
50 50 if fullname.startswith('hgext.fsmonitor.pywatchman'):
51 51 return None
52 52
53 53 # Try to find the module using other registered finders.
54 54 spec = None
55 55 for finder in sys.meta_path:
56 56 if finder == self:
57 57 continue
58 58
59 59 # Originally the API was a `find_module` method, but it was
60 60 # renamed to `find_spec` in python 3.4, with a new `target`
61 61 # argument.
62 62 find_spec_method = getattr(finder, 'find_spec', None)
63 63 if find_spec_method:
64 64 spec = find_spec_method(fullname, path, target=target)
65 65 else:
66 66 spec = finder.find_module(fullname)
67 67 if spec is not None:
68 68 spec = importlib.util.spec_from_loader(fullname, spec)
69 69 if spec:
70 70 break
71 71
72 72 # This is a Mercurial-related module but we couldn't find it
73 73 # using the previously-registered finders. This likely means
74 74 # the module doesn't exist.
75 75 if not spec:
76 76 return None
77 77
78 78 # TODO need to support loaders from alternate specs, like zip
79 79 # loaders.
80 80 loader = hgloader(spec.name, spec.origin)
81 81 # Can't use util.safehasattr here because that would require
82 82 # importing util, and we're in import code.
83 83 if hasattr(spec.loader, 'loader'): # hasattr-py3-only
84 84 # This is a nested loader (maybe a lazy loader?)
85 85 spec.loader.loader = loader
86 86 else:
87 87 spec.loader = loader
88 88 return spec
89 89
90 90 def replacetokens(tokens, fullname):
91 91 """Transform a stream of tokens from raw to Python 3.
92 92
93 93 It is called by the custom module loading machinery to rewrite
94 94 source/tokens between source decoding and compilation.
95 95
96 96 Returns a generator of possibly rewritten tokens.
97 97
98 98 The input token list may be mutated as part of processing. However,
99 99 its changes do not necessarily match the output token stream.
100 100
101 101 REMEMBER TO CHANGE ``BYTECODEHEADER`` WHEN CHANGING THIS FUNCTION
102 102 OR CACHED FILES WON'T GET INVALIDATED PROPERLY.
103 103 """
104 104 futureimpline = False
105 105
106 106 # The following utility functions access the tokens list and i index of
107 107 # the for i, t enumerate(tokens) loop below
108 108 def _isop(j, *o):
109 109 """Assert that tokens[j] is an OP with one of the given values"""
110 110 try:
111 111 return tokens[j].type == token.OP and tokens[j].string in o
112 112 except IndexError:
113 113 return False
114 114
115 115 def _findargnofcall(n):
116 116 """Find arg n of a call expression (start at 0)
117 117
118 118 Returns index of the first token of that argument, or None if
119 119 there is not that many arguments.
120 120
121 121 Assumes that token[i + 1] is '('.
122 122
123 123 """
124 124 nested = 0
125 125 for j in range(i + 2, len(tokens)):
126 126 if _isop(j, ')', ']', '}'):
127 127 # end of call, tuple, subscription or dict / set
128 128 nested -= 1
129 129 if nested < 0:
130 130 return None
131 131 elif n == 0:
132 132 # this is the starting position of arg
133 133 return j
134 134 elif _isop(j, '(', '[', '{'):
135 135 nested += 1
136 136 elif _isop(j, ',') and nested == 0:
137 137 n -= 1
138 138
139 139 return None
140 140
141 141 def _ensureunicode(j):
142 142 """Make sure the token at j is a unicode string
143 143
144 144 This rewrites a string token to include the unicode literal prefix
145 145 so the string transformer won't add the byte prefix.
146 146
147 147 Ignores tokens that are not strings. Assumes bounds checking has
148 148 already been done.
149 149
150 150 """
151 151 st = tokens[j]
152 152 if st.type == token.STRING and st.string.startswith(("'", '"')):
153 153 tokens[j] = st._replace(string='u%s' % st.string)
154 154
155 155 for i, t in enumerate(tokens):
156 156 # Insert compatibility imports at "from __future__ import" line.
157 157 # No '\n' should be added to preserve line numbers.
158 158 if (
159 159 t.type == token.NAME
160 160 and t.string == 'import'
161 161 and all(u.type == token.NAME for u in tokens[i - 2 : i])
162 162 and [u.string for u in tokens[i - 2 : i]]
163 163 == ['from', '__future__']
164 164 ):
165 165 futureimpline = True
166 166 if t.type == token.NEWLINE and futureimpline:
167 167 futureimpline = False
168 168 if fullname == 'mercurial.pycompat':
169 169 yield t
170 170 continue
171 171 r, c = t.start
172 172 l = (
173 173 b'; from mercurial.pycompat import '
174 b'delattr, getattr, hasattr, setattr, '
175 b'unicode\n'
174 b'delattr, getattr, hasattr, setattr\n'
176 175 )
177 176 for u in tokenize.tokenize(io.BytesIO(l).readline):
178 177 if u.type in (tokenize.ENCODING, token.ENDMARKER):
179 178 continue
180 179 yield u._replace(
181 180 start=(r, c + u.start[1]), end=(r, c + u.end[1])
182 181 )
183 182 continue
184 183
185 184 # This looks like a function call.
186 185 if t.type == token.NAME and _isop(i + 1, '('):
187 186 fn = t.string
188 187
189 188 # *attr() builtins don't accept byte strings to 2nd argument.
190 189 if fn in (
191 190 'getattr',
192 191 'setattr',
193 192 'hasattr',
194 193 'safehasattr',
195 194 ) and not _isop(i - 1, '.'):
196 195 arg1idx = _findargnofcall(1)
197 196 if arg1idx is not None:
198 197 _ensureunicode(arg1idx)
199 198
200 199 # .encode() and .decode() on str/bytes/unicode don't accept
201 200 # byte strings on Python 3.
202 201 elif fn in ('encode', 'decode') and _isop(i - 1, '.'):
203 202 for argn in range(2):
204 203 argidx = _findargnofcall(argn)
205 204 if argidx is not None:
206 205 _ensureunicode(argidx)
207 206
208 207 # It changes iteritems/values to items/values as they are not
209 208 # present in Python 3 world.
210 209 elif fn in ('iteritems', 'itervalues') and not (
211 210 tokens[i - 1].type == token.NAME
212 211 and tokens[i - 1].string == 'def'
213 212 ):
214 213 yield t._replace(string=fn[4:])
215 214 continue
216 215
217 216 # Emit unmodified token.
218 217 yield t
219 218
220 219 # Header to add to bytecode files. This MUST be changed when
221 220 # ``replacetoken`` or any mechanism that changes semantics of module
222 221 # loading is changed. Otherwise cached bytecode may get loaded without
223 222 # the new transformation mechanisms applied.
224 BYTECODEHEADER = b'HG\x00\x0d'
223 BYTECODEHEADER = b'HG\x00\x0e'
225 224
226 225 class hgloader(importlib.machinery.SourceFileLoader):
227 226 """Custom module loader that transforms source code.
228 227
229 228 When the source code is converted to a code object, we transform
230 229 certain patterns to be Python 3 compatible. This allows us to write code
231 230 that is natively Python 2 and compatible with Python 3 without
232 231 making the code excessively ugly.
233 232
234 233 We do this by transforming the token stream between parse and compile.
235 234
236 235 Implementing transformations invalidates caching assumptions made
237 236 by the built-in importer. The built-in importer stores a header on
238 237 saved bytecode files indicating the Python/bytecode version. If the
239 238 version changes, the cached bytecode is ignored. The Mercurial
240 239 transformations could change at any time. This means we need to check
241 240 that cached bytecode was generated with the current transformation
242 241 code or there could be a mismatch between cached bytecode and what
243 242 would be generated from this class.
244 243
245 244 We supplement the bytecode caching layer by wrapping ``get_data``
246 245 and ``set_data``. These functions are called when the
247 246 ``SourceFileLoader`` retrieves and saves bytecode cache files,
248 247 respectively. We simply add an additional header on the file. As
249 248 long as the version in this file is changed when semantics change,
250 249 cached bytecode should be invalidated when transformations change.
251 250
252 251 The added header has the form ``HG<VERSION>``. That is a literal
253 252 ``HG`` with 2 binary bytes indicating the transformation version.
254 253 """
255 254
256 255 def get_data(self, path):
257 256 data = super(hgloader, self).get_data(path)
258 257
259 258 if not path.endswith(tuple(importlib.machinery.BYTECODE_SUFFIXES)):
260 259 return data
261 260
262 261 # There should be a header indicating the Mercurial transformation
263 262 # version. If it doesn't exist or doesn't match the current version,
264 263 # we raise an OSError because that is what
265 264 # ``SourceFileLoader.get_code()`` expects when loading bytecode
266 265 # paths to indicate the cached file is "bad."
267 266 if data[0:2] != b'HG':
268 267 raise OSError('no hg header')
269 268 if data[0:4] != BYTECODEHEADER:
270 269 raise OSError('hg header version mismatch')
271 270
272 271 return data[4:]
273 272
274 273 def set_data(self, path, data, *args, **kwargs):
275 274 if path.endswith(tuple(importlib.machinery.BYTECODE_SUFFIXES)):
276 275 data = BYTECODEHEADER + data
277 276
278 277 return super(hgloader, self).set_data(path, data, *args, **kwargs)
279 278
280 279 def source_to_code(self, data, path):
281 280 """Perform token transformation before compilation."""
282 281 buf = io.BytesIO(data)
283 282 tokens = tokenize.tokenize(buf.readline)
284 283 data = tokenize.untokenize(replacetokens(list(tokens), self.name))
285 284 # Python's built-in importer strips frames from exceptions raised
286 285 # for this code. Unfortunately, that mechanism isn't extensible
287 286 # and our frame will be blamed for the import failure. There
288 287 # are extremely hacky ways to do frame stripping. We haven't
289 288 # implemented them because they are very ugly.
290 289 return super(hgloader, self).source_to_code(data, path)
291 290
292 291 # We automagically register our custom importer as a side-effect of
293 292 # loading. This is necessary to ensure that any entry points are able
294 293 # to import mercurial.* modules without having to perform this
295 294 # registration themselves.
296 295 if not any(isinstance(x, hgpathentryfinder) for x in sys.meta_path):
297 296 # meta_path is used before any implicit finders and before sys.path.
298 297 sys.meta_path.insert(0, hgpathentryfinder())
@@ -1,550 +1,550 b''
1 1 # templatefilters.py - common template expansion filters
2 2 #
3 3 # Copyright 2005-2008 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import os
11 11 import re
12 12 import time
13 13
14 14 from .i18n import _
15 15 from . import (
16 16 encoding,
17 17 error,
18 18 node,
19 19 pycompat,
20 20 registrar,
21 21 templateutil,
22 22 url,
23 23 util,
24 24 )
25 25 from .utils import (
26 26 cborutil,
27 27 dateutil,
28 28 stringutil,
29 29 )
30 30
31 31 urlerr = util.urlerr
32 32 urlreq = util.urlreq
33 33
34 34 if pycompat.ispy3:
35 35 long = int
36 36
37 37 # filters are callables like:
38 38 # fn(obj)
39 39 # with:
40 40 # obj - object to be filtered (text, date, list and so on)
41 41 filters = {}
42 42
43 43 templatefilter = registrar.templatefilter(filters)
44 44
45 45
46 46 @templatefilter(b'addbreaks', intype=bytes)
47 47 def addbreaks(text):
48 48 """Any text. Add an XHTML "<br />" tag before the end of
49 49 every line except the last.
50 50 """
51 51 return text.replace(b'\n', b'<br/>\n')
52 52
53 53
54 54 agescales = [
55 55 (b"year", 3600 * 24 * 365, b'Y'),
56 56 (b"month", 3600 * 24 * 30, b'M'),
57 57 (b"week", 3600 * 24 * 7, b'W'),
58 58 (b"day", 3600 * 24, b'd'),
59 59 (b"hour", 3600, b'h'),
60 60 (b"minute", 60, b'm'),
61 61 (b"second", 1, b's'),
62 62 ]
63 63
64 64
65 65 @templatefilter(b'age', intype=templateutil.date)
66 66 def age(date, abbrev=False):
67 67 """Date. Returns a human-readable date/time difference between the
68 68 given date/time and the current date/time.
69 69 """
70 70
71 71 def plural(t, c):
72 72 if c == 1:
73 73 return t
74 74 return t + b"s"
75 75
76 76 def fmt(t, c, a):
77 77 if abbrev:
78 78 return b"%d%s" % (c, a)
79 79 return b"%d %s" % (c, plural(t, c))
80 80
81 81 now = time.time()
82 82 then = date[0]
83 83 future = False
84 84 if then > now:
85 85 future = True
86 86 delta = max(1, int(then - now))
87 87 if delta > agescales[0][1] * 30:
88 88 return b'in the distant future'
89 89 else:
90 90 delta = max(1, int(now - then))
91 91 if delta > agescales[0][1] * 2:
92 92 return dateutil.shortdate(date)
93 93
94 94 for t, s, a in agescales:
95 95 n = delta // s
96 96 if n >= 2 or s == 1:
97 97 if future:
98 98 return b'%s from now' % fmt(t, n, a)
99 99 return b'%s ago' % fmt(t, n, a)
100 100
101 101
102 102 @templatefilter(b'basename', intype=bytes)
103 103 def basename(path):
104 104 """Any text. Treats the text as a path, and returns the last
105 105 component of the path after splitting by the path separator.
106 106 For example, "foo/bar/baz" becomes "baz" and "foo/bar//" becomes "".
107 107 """
108 108 return os.path.basename(path)
109 109
110 110
111 111 @templatefilter(b'cbor')
112 112 def cbor(obj):
113 113 """Any object. Serializes the object to CBOR bytes."""
114 114 return b''.join(cborutil.streamencode(obj))
115 115
116 116
117 117 @templatefilter(b'commondir')
118 118 def commondir(filelist):
119 119 """List of text. Treats each list item as file name with /
120 120 as path separator and returns the longest common directory
121 121 prefix shared by all list items.
122 122 Returns the empty string if no common prefix exists.
123 123
124 124 The list items are not normalized, i.e. "foo/../bar" is handled as
125 125 file "bar" in the directory "foo/..". Leading slashes are ignored.
126 126
127 127 For example, ["foo/bar/baz", "foo/baz/bar"] becomes "foo" and
128 128 ["foo/bar", "baz"] becomes "".
129 129 """
130 130
131 131 def common(a, b):
132 132 if len(a) > len(b):
133 133 a = b[: len(a)]
134 134 elif len(b) > len(a):
135 135 b = b[: len(a)]
136 136 if a == b:
137 137 return a
138 138 for i in pycompat.xrange(len(a)):
139 139 if a[i] != b[i]:
140 140 return a[:i]
141 141 return a
142 142
143 143 try:
144 144 if not filelist:
145 145 return b""
146 146 dirlist = [f.lstrip(b'/').split(b'/')[:-1] for f in filelist]
147 147 if len(dirlist) == 1:
148 148 return b'/'.join(dirlist[0])
149 149 a = min(dirlist)
150 150 b = max(dirlist)
151 151 # The common prefix of a and b is shared with all
152 152 # elements of the list since Python sorts lexicographical
153 153 # and [1, x] after [1].
154 154 return b'/'.join(common(a, b))
155 155 except TypeError:
156 156 raise error.ParseError(_(b'argument is not a list of text'))
157 157
158 158
159 159 @templatefilter(b'count')
160 160 def count(i):
161 161 """List or text. Returns the length as an integer."""
162 162 try:
163 163 return len(i)
164 164 except TypeError:
165 165 raise error.ParseError(_(b'not countable'))
166 166
167 167
168 168 @templatefilter(b'dirname', intype=bytes)
169 169 def dirname(path):
170 170 """Any text. Treats the text as a path, and strips the last
171 171 component of the path after splitting by the path separator.
172 172 """
173 173 return os.path.dirname(path)
174 174
175 175
176 176 @templatefilter(b'domain', intype=bytes)
177 177 def domain(author):
178 178 """Any text. Finds the first string that looks like an email
179 179 address, and extracts just the domain component. Example: ``User
180 180 <user@example.com>`` becomes ``example.com``.
181 181 """
182 182 f = author.find(b'@')
183 183 if f == -1:
184 184 return b''
185 185 author = author[f + 1 :]
186 186 f = author.find(b'>')
187 187 if f >= 0:
188 188 author = author[:f]
189 189 return author
190 190
191 191
192 192 @templatefilter(b'email', intype=bytes)
193 193 def email(text):
194 194 """Any text. Extracts the first string that looks like an email
195 195 address. Example: ``User <user@example.com>`` becomes
196 196 ``user@example.com``.
197 197 """
198 198 return stringutil.email(text)
199 199
200 200
201 201 @templatefilter(b'escape', intype=bytes)
202 202 def escape(text):
203 203 """Any text. Replaces the special XML/XHTML characters "&", "<"
204 204 and ">" with XML entities, and filters out NUL characters.
205 205 """
206 206 return url.escape(text.replace(b'\0', b''), True)
207 207
208 208
209 209 para_re = None
210 210 space_re = None
211 211
212 212
213 213 def fill(text, width, initindent=b'', hangindent=b''):
214 214 '''fill many paragraphs with optional indentation.'''
215 215 global para_re, space_re
216 216 if para_re is None:
217 217 para_re = re.compile(b'(\n\n|\n\\s*[-*]\\s*)', re.M)
218 218 space_re = re.compile(br' +')
219 219
220 220 def findparas():
221 221 start = 0
222 222 while True:
223 223 m = para_re.search(text, start)
224 224 if not m:
225 225 uctext = encoding.unifromlocal(text[start:])
226 226 w = len(uctext)
227 227 while w > 0 and uctext[w - 1].isspace():
228 228 w -= 1
229 229 yield (
230 230 encoding.unitolocal(uctext[:w]),
231 231 encoding.unitolocal(uctext[w:]),
232 232 )
233 233 break
234 234 yield text[start : m.start(0)], m.group(1)
235 235 start = m.end(1)
236 236
237 237 return b"".join(
238 238 [
239 239 stringutil.wrap(
240 240 space_re.sub(b' ', stringutil.wrap(para, width)),
241 241 width,
242 242 initindent,
243 243 hangindent,
244 244 )
245 245 + rest
246 246 for para, rest in findparas()
247 247 ]
248 248 )
249 249
250 250
251 251 @templatefilter(b'fill68', intype=bytes)
252 252 def fill68(text):
253 253 """Any text. Wraps the text to fit in 68 columns."""
254 254 return fill(text, 68)
255 255
256 256
257 257 @templatefilter(b'fill76', intype=bytes)
258 258 def fill76(text):
259 259 """Any text. Wraps the text to fit in 76 columns."""
260 260 return fill(text, 76)
261 261
262 262
263 263 @templatefilter(b'firstline', intype=bytes)
264 264 def firstline(text):
265 265 """Any text. Returns the first line of text."""
266 266 try:
267 267 return text.splitlines(True)[0].rstrip(b'\r\n')
268 268 except IndexError:
269 269 return b''
270 270
271 271
272 272 @templatefilter(b'hex', intype=bytes)
273 273 def hexfilter(text):
274 274 """Any text. Convert a binary Mercurial node identifier into
275 275 its long hexadecimal representation.
276 276 """
277 277 return node.hex(text)
278 278
279 279
280 280 @templatefilter(b'hgdate', intype=templateutil.date)
281 281 def hgdate(text):
282 282 """Date. Returns the date as a pair of numbers: "1157407993
283 283 25200" (Unix timestamp, timezone offset).
284 284 """
285 285 return b"%d %d" % text
286 286
287 287
288 288 @templatefilter(b'isodate', intype=templateutil.date)
289 289 def isodate(text):
290 290 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
291 291 +0200".
292 292 """
293 293 return dateutil.datestr(text, b'%Y-%m-%d %H:%M %1%2')
294 294
295 295
296 296 @templatefilter(b'isodatesec', intype=templateutil.date)
297 297 def isodatesec(text):
298 298 """Date. Returns the date in ISO 8601 format, including
299 299 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
300 300 filter.
301 301 """
302 302 return dateutil.datestr(text, b'%Y-%m-%d %H:%M:%S %1%2')
303 303
304 304
305 305 def indent(text, prefix):
306 306 '''indent each non-empty line of text after first with prefix.'''
307 307 lines = text.splitlines()
308 308 num_lines = len(lines)
309 309 endswithnewline = text[-1:] == b'\n'
310 310
311 311 def indenter():
312 312 for i in pycompat.xrange(num_lines):
313 313 l = lines[i]
314 314 if i and l.strip():
315 315 yield prefix
316 316 yield l
317 317 if i < num_lines - 1 or endswithnewline:
318 318 yield b'\n'
319 319
320 320 return b"".join(indenter())
321 321
322 322
323 323 @templatefilter(b'json')
324 324 def json(obj, paranoid=True):
325 325 """Any object. Serializes the object to a JSON formatted text."""
326 326 if obj is None:
327 327 return b'null'
328 328 elif obj is False:
329 329 return b'false'
330 330 elif obj is True:
331 331 return b'true'
332 332 elif isinstance(obj, (int, long, float)):
333 333 return pycompat.bytestr(obj)
334 334 elif isinstance(obj, bytes):
335 335 return b'"%s"' % encoding.jsonescape(obj, paranoid=paranoid)
336 336 elif isinstance(obj, type(u'')):
337 337 raise error.ProgrammingError(
338 338 b'Mercurial only does output with bytes: %r' % obj
339 339 )
340 340 elif util.safehasattr(obj, b'keys'):
341 341 out = [
342 342 b'"%s": %s'
343 343 % (encoding.jsonescape(k, paranoid=paranoid), json(v, paranoid))
344 344 for k, v in sorted(obj.iteritems())
345 345 ]
346 346 return b'{' + b', '.join(out) + b'}'
347 347 elif util.safehasattr(obj, b'__iter__'):
348 348 out = [json(i, paranoid) for i in obj]
349 349 return b'[' + b', '.join(out) + b']'
350 350 raise error.ProgrammingError(b'cannot encode %r' % obj)
351 351
352 352
353 353 @templatefilter(b'lower', intype=bytes)
354 354 def lower(text):
355 355 """Any text. Converts the text to lowercase."""
356 356 return encoding.lower(text)
357 357
358 358
359 359 @templatefilter(b'nonempty', intype=bytes)
360 360 def nonempty(text):
361 361 """Any text. Returns '(none)' if the string is empty."""
362 362 return text or b"(none)"
363 363
364 364
365 365 @templatefilter(b'obfuscate', intype=bytes)
366 366 def obfuscate(text):
367 367 """Any text. Returns the input text rendered as a sequence of
368 368 XML entities.
369 369 """
370 text = unicode(text, pycompat.sysstr(encoding.encoding), r'replace')
370 text = pycompat.unicode(text, pycompat.sysstr(encoding.encoding), r'replace')
371 371 return b''.join([b'&#%d;' % ord(c) for c in text])
372 372
373 373
374 374 @templatefilter(b'permissions', intype=bytes)
375 375 def permissions(flags):
376 376 if b"l" in flags:
377 377 return b"lrwxrwxrwx"
378 378 if b"x" in flags:
379 379 return b"-rwxr-xr-x"
380 380 return b"-rw-r--r--"
381 381
382 382
383 383 @templatefilter(b'person', intype=bytes)
384 384 def person(author):
385 385 """Any text. Returns the name before an email address,
386 386 interpreting it as per RFC 5322.
387 387 """
388 388 return stringutil.person(author)
389 389
390 390
391 391 @templatefilter(b'revescape', intype=bytes)
392 392 def revescape(text):
393 393 """Any text. Escapes all "special" characters, except @.
394 394 Forward slashes are escaped twice to prevent web servers from prematurely
395 395 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
396 396 """
397 397 return urlreq.quote(text, safe=b'/@').replace(b'/', b'%252F')
398 398
399 399
400 400 @templatefilter(b'rfc3339date', intype=templateutil.date)
401 401 def rfc3339date(text):
402 402 """Date. Returns a date using the Internet date format
403 403 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
404 404 """
405 405 return dateutil.datestr(text, b"%Y-%m-%dT%H:%M:%S%1:%2")
406 406
407 407
408 408 @templatefilter(b'rfc822date', intype=templateutil.date)
409 409 def rfc822date(text):
410 410 """Date. Returns a date using the same format used in email
411 411 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
412 412 """
413 413 return dateutil.datestr(text, b"%a, %d %b %Y %H:%M:%S %1%2")
414 414
415 415
416 416 @templatefilter(b'short', intype=bytes)
417 417 def short(text):
418 418 """Changeset hash. Returns the short form of a changeset hash,
419 419 i.e. a 12 hexadecimal digit string.
420 420 """
421 421 return text[:12]
422 422
423 423
424 424 @templatefilter(b'shortbisect', intype=bytes)
425 425 def shortbisect(label):
426 426 """Any text. Treats `label` as a bisection status, and
427 427 returns a single-character representing the status (G: good, B: bad,
428 428 S: skipped, U: untested, I: ignored). Returns single space if `text`
429 429 is not a valid bisection status.
430 430 """
431 431 if label:
432 432 return label[0:1].upper()
433 433 return b' '
434 434
435 435
436 436 @templatefilter(b'shortdate', intype=templateutil.date)
437 437 def shortdate(text):
438 438 """Date. Returns a date like "2006-09-18"."""
439 439 return dateutil.shortdate(text)
440 440
441 441
442 442 @templatefilter(b'slashpath', intype=bytes)
443 443 def slashpath(path):
444 444 """Any text. Replaces the native path separator with slash."""
445 445 return util.pconvert(path)
446 446
447 447
448 448 @templatefilter(b'splitlines', intype=bytes)
449 449 def splitlines(text):
450 450 """Any text. Split text into a list of lines."""
451 451 return templateutil.hybridlist(text.splitlines(), name=b'line')
452 452
453 453
454 454 @templatefilter(b'stringescape', intype=bytes)
455 455 def stringescape(text):
456 456 return stringutil.escapestr(text)
457 457
458 458
459 459 @templatefilter(b'stringify', intype=bytes)
460 460 def stringify(thing):
461 461 """Any type. Turns the value into text by converting values into
462 462 text and concatenating them.
463 463 """
464 464 return thing # coerced by the intype
465 465
466 466
467 467 @templatefilter(b'stripdir', intype=bytes)
468 468 def stripdir(text):
469 469 """Treat the text as path and strip a directory level, if
470 470 possible. For example, "foo" and "foo/bar" becomes "foo".
471 471 """
472 472 dir = os.path.dirname(text)
473 473 if dir == b"":
474 474 return os.path.basename(text)
475 475 else:
476 476 return dir
477 477
478 478
479 479 @templatefilter(b'tabindent', intype=bytes)
480 480 def tabindent(text):
481 481 """Any text. Returns the text, with every non-empty line
482 482 except the first starting with a tab character.
483 483 """
484 484 return indent(text, b'\t')
485 485
486 486
487 487 @templatefilter(b'upper', intype=bytes)
488 488 def upper(text):
489 489 """Any text. Converts the text to uppercase."""
490 490 return encoding.upper(text)
491 491
492 492
493 493 @templatefilter(b'urlescape', intype=bytes)
494 494 def urlescape(text):
495 495 """Any text. Escapes all "special" characters. For example,
496 496 "foo bar" becomes "foo%20bar".
497 497 """
498 498 return urlreq.quote(text)
499 499
500 500
501 501 @templatefilter(b'user', intype=bytes)
502 502 def userfilter(text):
503 503 """Any text. Returns a short representation of a user name or email
504 504 address."""
505 505 return stringutil.shortuser(text)
506 506
507 507
508 508 @templatefilter(b'emailuser', intype=bytes)
509 509 def emailuser(text):
510 510 """Any text. Returns the user portion of an email address."""
511 511 return stringutil.emailuser(text)
512 512
513 513
514 514 @templatefilter(b'utf8', intype=bytes)
515 515 def utf8(text):
516 516 """Any text. Converts from the local character encoding to UTF-8."""
517 517 return encoding.fromlocal(text)
518 518
519 519
520 520 @templatefilter(b'xmlescape', intype=bytes)
521 521 def xmlescape(text):
522 522 text = (
523 523 text.replace(b'&', b'&amp;')
524 524 .replace(b'<', b'&lt;')
525 525 .replace(b'>', b'&gt;')
526 526 .replace(b'"', b'&quot;')
527 527 .replace(b"'", b'&#39;')
528 528 ) # &apos; invalid in HTML
529 529 return re.sub(b'[\x00-\x08\x0B\x0C\x0E-\x1F]', b' ', text)
530 530
531 531
532 532 def websub(text, websubtable):
533 533 """:websub: Any text. Only applies to hgweb. Applies the regular
534 534 expression replacements defined in the websub section.
535 535 """
536 536 if websubtable:
537 537 for regexp, format in websubtable:
538 538 text = regexp.sub(format, text)
539 539 return text
540 540
541 541
542 542 def loadfilter(ui, extname, registrarobj):
543 543 """Load template filter from specified registrarobj
544 544 """
545 545 for name, func in registrarobj._table.iteritems():
546 546 filters[name] = func
547 547
548 548
549 549 # tell hggettext to extract docstrings from these functions:
550 550 i18nfunctions = filters.values()
General Comments 0
You need to be logged in to leave comments. Login now