OLD | NEW |
| (Empty) |
1 #!/usr/bin/env python | |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
3 # Use of this source code is governed by a BSD-style license that can be | |
4 # found in the LICENSE file. | |
5 | |
6 '''Utilities used by GRIT. | |
7 ''' | |
8 | |
9 import codecs | |
10 import htmlentitydefs | |
11 import os | |
12 import re | |
13 import shutil | |
14 import sys | |
15 import tempfile | |
16 import time | |
17 import types | |
18 from xml.sax import saxutils | |
19 | |
20 from grit import lazy_re | |
21 | |
22 _root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) | |
23 | |
24 | |
25 # Unique constants for use by ReadFile(). | |
26 BINARY, RAW_TEXT = range(2) | |
27 | |
28 | |
29 # Unique constants representing data pack encodings. | |
30 _, UTF8, UTF16 = range(3) | |
31 | |
32 | |
33 def Encode(message, encoding): | |
34 '''Returns a byte stream that represents |message| in the given |encoding|.''' | |
35 # |message| is a python unicode string, so convert to a byte stream that | |
36 # has the correct encoding requested for the datapacks. We skip the first | |
37 # 2 bytes of text resources because it is the BOM. | |
38 if encoding == UTF8: | |
39 return message.encode('utf8') | |
40 if encoding == UTF16: | |
41 return message.encode('utf16')[2:] | |
42 # Default is BINARY | |
43 return message | |
44 | |
45 | |
46 # Matches all different types of linebreaks. | |
47 LINEBREAKS = re.compile('\r\n|\n|\r') | |
48 | |
49 def MakeRelativePath(base_path, path_to_make_relative): | |
50 """Returns a relative path such from the base_path to | |
51 the path_to_make_relative. | |
52 | |
53 In other words, os.join(base_path, | |
54 MakeRelativePath(base_path, path_to_make_relative)) | |
55 is the same location as path_to_make_relative. | |
56 | |
57 Args: | |
58 base_path: the root path | |
59 path_to_make_relative: an absolute path that is on the same drive | |
60 as base_path | |
61 """ | |
62 | |
63 def _GetPathAfterPrefix(prefix_path, path_with_prefix): | |
64 """Gets the subpath within in prefix_path for the path_with_prefix | |
65 with no beginning or trailing path separators. | |
66 | |
67 Args: | |
68 prefix_path: the base path | |
69 path_with_prefix: a path that starts with prefix_path | |
70 """ | |
71 assert path_with_prefix.startswith(prefix_path) | |
72 path_without_prefix = path_with_prefix[len(prefix_path):] | |
73 normalized_path = os.path.normpath(path_without_prefix.strip(os.path.sep)) | |
74 if normalized_path == '.': | |
75 normalized_path = '' | |
76 return normalized_path | |
77 | |
78 def _GetCommonBaseDirectory(*args): | |
79 """Returns the common prefix directory for the given paths | |
80 | |
81 Args: | |
82 The list of paths (at least one of which should be a directory) | |
83 """ | |
84 prefix = os.path.commonprefix(args) | |
85 # prefix is a character-by-character prefix (i.e. it does not end | |
86 # on a directory bound, so this code fixes that) | |
87 | |
88 # if the prefix ends with the separator, then it is prefect. | |
89 if len(prefix) > 0 and prefix[-1] == os.path.sep: | |
90 return prefix | |
91 | |
92 # We need to loop through all paths or else we can get | |
93 # tripped up by "c:\a" and "c:\abc". The common prefix | |
94 # is "c:\a" which is a directory and looks good with | |
95 # respect to the first directory but it is clear that | |
96 # isn't a common directory when the second path is | |
97 # examined. | |
98 for path in args: | |
99 assert len(path) >= len(prefix) | |
100 # If the prefix the same length as the path, | |
101 # then the prefix must be a directory (since one | |
102 # of the arguements should be a directory). | |
103 if path == prefix: | |
104 continue | |
105 # if the character after the prefix in the path | |
106 # is the separator, then the prefix appears to be a | |
107 # valid a directory as well for the given path | |
108 if path[len(prefix)] == os.path.sep: | |
109 continue | |
110 # Otherwise, the prefix is not a directory, so it needs | |
111 # to be shortened to be one | |
112 index_sep = prefix.rfind(os.path.sep) | |
113 # The use "index_sep + 1" because it includes the final sep | |
114 # and it handles the case when the index_sep is -1 as well | |
115 prefix = prefix[:index_sep + 1] | |
116 # At this point we backed up to a directory bound which is | |
117 # common to all paths, so we can quit going through all of | |
118 # the paths. | |
119 break | |
120 return prefix | |
121 | |
122 prefix = _GetCommonBaseDirectory(base_path, path_to_make_relative) | |
123 # If the paths had no commonality at all, then return the absolute path | |
124 # because it is the best that can be done. If the path had to be relative | |
125 # then eventually this absolute path will be discovered (when a build breaks) | |
126 # and an appropriate fix can be made, but having this allows for the best | |
127 # backward compatibility with the absolute path behavior in the past. | |
128 if len(prefix) <= 0: | |
129 return path_to_make_relative | |
130 # Build a path from the base dir to the common prefix | |
131 remaining_base_path = _GetPathAfterPrefix(prefix, base_path) | |
132 | |
133 # The follow handles two case: "" and "foo\\bar" | |
134 path_pieces = remaining_base_path.split(os.path.sep) | |
135 base_depth_from_prefix = len([d for d in path_pieces if len(d)]) | |
136 base_to_prefix = (".." + os.path.sep) * base_depth_from_prefix | |
137 | |
138 # Put add in the path from the prefix to the path_to_make_relative | |
139 remaining_other_path = _GetPathAfterPrefix(prefix, path_to_make_relative) | |
140 return base_to_prefix + remaining_other_path | |
141 | |
142 | |
143 KNOWN_SYSTEM_IDENTIFIERS = set() | |
144 | |
145 SYSTEM_IDENTIFIERS = None | |
146 | |
147 def SetupSystemIdentifiers(ids): | |
148 '''Adds ids to a regexp of known system identifiers. | |
149 | |
150 Can be called many times, ids will be accumulated. | |
151 | |
152 Args: | |
153 ids: an iterable of strings | |
154 ''' | |
155 KNOWN_SYSTEM_IDENTIFIERS.update(ids) | |
156 global SYSTEM_IDENTIFIERS | |
157 SYSTEM_IDENTIFIERS = lazy_re.compile( | |
158 ' | '.join([r'\b%s\b' % i for i in KNOWN_SYSTEM_IDENTIFIERS]), | |
159 re.VERBOSE) | |
160 | |
161 | |
162 # Matches all of the resource IDs predefined by Windows. | |
163 SetupSystemIdentifiers(( | |
164 'IDOK', 'IDCANCEL', 'IDC_STATIC', 'IDYES', 'IDNO', | |
165 'ID_FILE_NEW', 'ID_FILE_OPEN', 'ID_FILE_CLOSE', 'ID_FILE_SAVE', | |
166 'ID_FILE_SAVE_AS', 'ID_FILE_PAGE_SETUP', 'ID_FILE_PRINT_SETUP', | |
167 'ID_FILE_PRINT', 'ID_FILE_PRINT_DIRECT', 'ID_FILE_PRINT_PREVIEW', | |
168 'ID_FILE_UPDATE', 'ID_FILE_SAVE_COPY_AS', 'ID_FILE_SEND_MAIL', | |
169 'ID_FILE_MRU_FIRST', 'ID_FILE_MRU_LAST', | |
170 'ID_EDIT_CLEAR', 'ID_EDIT_CLEAR_ALL', 'ID_EDIT_COPY', | |
171 'ID_EDIT_CUT', 'ID_EDIT_FIND', 'ID_EDIT_PASTE', 'ID_EDIT_PASTE_LINK', | |
172 'ID_EDIT_PASTE_SPECIAL', 'ID_EDIT_REPEAT', 'ID_EDIT_REPLACE', | |
173 'ID_EDIT_SELECT_ALL', 'ID_EDIT_UNDO', 'ID_EDIT_REDO', | |
174 'VS_VERSION_INFO', 'IDRETRY', | |
175 'ID_APP_ABOUT', 'ID_APP_EXIT', | |
176 'ID_NEXT_PANE', 'ID_PREV_PANE', | |
177 'ID_WINDOW_NEW', 'ID_WINDOW_ARRANGE', 'ID_WINDOW_CASCADE', | |
178 'ID_WINDOW_TILE_HORZ', 'ID_WINDOW_TILE_VERT', 'ID_WINDOW_SPLIT', | |
179 'ATL_IDS_SCSIZE', 'ATL_IDS_SCMOVE', 'ATL_IDS_SCMINIMIZE', | |
180 'ATL_IDS_SCMAXIMIZE', 'ATL_IDS_SCNEXTWINDOW', 'ATL_IDS_SCPREVWINDOW', | |
181 'ATL_IDS_SCCLOSE', 'ATL_IDS_SCRESTORE', 'ATL_IDS_SCTASKLIST', | |
182 'ATL_IDS_MDICHILD', 'ATL_IDS_IDLEMESSAGE', 'ATL_IDS_MRU_FILE' )) | |
183 | |
184 | |
185 # Matches character entities, whether specified by name, decimal or hex. | |
186 _HTML_ENTITY = lazy_re.compile( | |
187 '&(#(?P<decimal>[0-9]+)|#x(?P<hex>[a-fA-F0-9]+)|(?P<named>[a-z0-9]+));', | |
188 re.IGNORECASE) | |
189 | |
190 # Matches characters that should be HTML-escaped. This is <, > and &, but only | |
191 # if the & is not the start of an HTML character entity. | |
192 _HTML_CHARS_TO_ESCAPE = lazy_re.compile( | |
193 '"|<|>|&(?!#[0-9]+|#x[0-9a-z]+|[a-z]+;)', | |
194 re.IGNORECASE | re.MULTILINE) | |
195 | |
196 | |
197 def ReadFile(filename, encoding): | |
198 '''Reads and returns the entire contents of the given file. | |
199 | |
200 Args: | |
201 filename: The path to the file. | |
202 encoding: A Python codec name or one of two special values: BINARY to read | |
203 the file in binary mode, or RAW_TEXT to read it with newline | |
204 conversion but without decoding to Unicode. | |
205 ''' | |
206 mode = 'rb' if encoding == BINARY else 'rU' | |
207 with open(filename, mode) as f: | |
208 data = f.read() | |
209 if encoding not in (BINARY, RAW_TEXT): | |
210 data = data.decode(encoding) | |
211 return data | |
212 | |
213 | |
214 def WrapOutputStream(stream, encoding = 'utf-8'): | |
215 '''Returns a stream that wraps the provided stream, making it write | |
216 characters using the specified encoding.''' | |
217 return codecs.getwriter(encoding)(stream) | |
218 | |
219 | |
220 def ChangeStdoutEncoding(encoding = 'utf-8'): | |
221 '''Changes STDOUT to print characters using the specified encoding.''' | |
222 sys.stdout = WrapOutputStream(sys.stdout, encoding) | |
223 | |
224 | |
225 def EscapeHtml(text, escape_quotes = False): | |
226 '''Returns 'text' with <, > and & (and optionally ") escaped to named HTML | |
227 entities. Any existing named entity or HTML entity defined by decimal or | |
228 hex code will be left untouched. This is appropriate for escaping text for | |
229 inclusion in HTML, but not for XML. | |
230 ''' | |
231 def Replace(match): | |
232 if match.group() == '&': return '&' | |
233 elif match.group() == '<': return '<' | |
234 elif match.group() == '>': return '>' | |
235 elif match.group() == '"': | |
236 if escape_quotes: return '"' | |
237 else: return match.group() | |
238 else: assert False | |
239 out = _HTML_CHARS_TO_ESCAPE.sub(Replace, text) | |
240 return out | |
241 | |
242 | |
243 def UnescapeHtml(text, replace_nbsp=True): | |
244 '''Returns 'text' with all HTML character entities (both named character | |
245 entities and those specified by decimal or hexadecimal Unicode ordinal) | |
246 replaced by their Unicode characters (or latin1 characters if possible). | |
247 | |
248 The only exception is that will not be escaped if 'replace_nbsp' is | |
249 False. | |
250 ''' | |
251 def Replace(match): | |
252 groups = match.groupdict() | |
253 if groups['hex']: | |
254 return unichr(int(groups['hex'], 16)) | |
255 elif groups['decimal']: | |
256 return unichr(int(groups['decimal'], 10)) | |
257 else: | |
258 name = groups['named'] | |
259 if name == 'nbsp' and not replace_nbsp: | |
260 return match.group() # Don't replace | |
261 assert name != None | |
262 if name in htmlentitydefs.name2codepoint.keys(): | |
263 return unichr(htmlentitydefs.name2codepoint[name]) | |
264 else: | |
265 return match.group() # Unknown HTML character entity - don't replace | |
266 | |
267 out = _HTML_ENTITY.sub(Replace, text) | |
268 return out | |
269 | |
270 | |
271 def EncodeCdata(cdata): | |
272 '''Returns the provided cdata in either escaped format or <![CDATA[xxx]]> | |
273 format, depending on which is more appropriate for easy editing. The data | |
274 is escaped for inclusion in an XML element's body. | |
275 | |
276 Args: | |
277 cdata: 'If x < y and y < z then x < z' | |
278 | |
279 Return: | |
280 '<![CDATA[If x < y and y < z then x < z]]>' | |
281 ''' | |
282 if cdata.count('<') > 1 or cdata.count('>') > 1 and cdata.count(']]>') == 0: | |
283 return '<![CDATA[%s]]>' % cdata | |
284 else: | |
285 return saxutils.escape(cdata) | |
286 | |
287 | |
288 def FixupNamedParam(function, param_name, param_value): | |
289 '''Returns a closure that is identical to 'function' but ensures that the | |
290 named parameter 'param_name' is always set to 'param_value' unless explicitly | |
291 set by the caller. | |
292 | |
293 Args: | |
294 function: callable | |
295 param_name: 'bingo' | |
296 param_value: 'bongo' (any type) | |
297 | |
298 Return: | |
299 callable | |
300 ''' | |
301 def FixupClosure(*args, **kw): | |
302 if not param_name in kw: | |
303 kw[param_name] = param_value | |
304 return function(*args, **kw) | |
305 return FixupClosure | |
306 | |
307 | |
308 def PathFromRoot(path): | |
309 '''Takes a path relative to the root directory for GRIT (the one that grit.py | |
310 resides in) and returns a path that is either absolute or relative to the | |
311 current working directory (i.e .a path you can use to open the file). | |
312 | |
313 Args: | |
314 path: 'rel_dir\file.ext' | |
315 | |
316 Return: | |
317 'c:\src\tools\rel_dir\file.ext | |
318 ''' | |
319 return os.path.normpath(os.path.join(_root_dir, path)) | |
320 | |
321 | |
322 def ParseGrdForUnittest(body, base_dir=None): | |
323 '''Parse a skeleton .grd file and return it, for use in unit tests. | |
324 | |
325 Args: | |
326 body: XML that goes inside the <release> element. | |
327 base_dir: The base_dir attribute of the <grit> tag. | |
328 ''' | |
329 import StringIO | |
330 from grit import grd_reader | |
331 if isinstance(body, unicode): | |
332 body = body.encode('utf-8') | |
333 if base_dir is None: | |
334 base_dir = PathFromRoot('.') | |
335 body = '''<?xml version="1.0" encoding="UTF-8"?> | |
336 <grit latest_public_release="2" current_release="3" source_lang_id="en" base_dir
="%s"> | |
337 <outputs> | |
338 </outputs> | |
339 <release seq="3"> | |
340 %s | |
341 </release> | |
342 </grit>''' % (base_dir, body) | |
343 return grd_reader.Parse(StringIO.StringIO(body), dir=".") | |
344 | |
345 | |
346 def StripBlankLinesAndComments(text): | |
347 '''Strips blank lines and comments from C source code, for unit tests.''' | |
348 return '\n'.join(line for line in text.splitlines() | |
349 if line and not line.startswith('//')) | |
350 | |
351 | |
352 def dirname(filename): | |
353 '''Version of os.path.dirname() that never returns empty paths (returns | |
354 '.' if the result of os.path.dirname() is empty). | |
355 ''' | |
356 ret = os.path.dirname(filename) | |
357 if ret == '': | |
358 ret = '.' | |
359 return ret | |
360 | |
361 | |
362 def normpath(path): | |
363 '''Version of os.path.normpath that also changes backward slashes to | |
364 forward slashes when not running on Windows. | |
365 ''' | |
366 # This is safe to always do because the Windows version of os.path.normpath | |
367 # will replace forward slashes with backward slashes. | |
368 path = path.replace('\\', '/') | |
369 return os.path.normpath(path) | |
370 | |
371 | |
372 _LANGUAGE_SPLIT_RE = lazy_re.compile('-|_|/') | |
373 | |
374 | |
375 def CanonicalLanguage(code): | |
376 '''Canonicalizes two-part language codes by using a dash and making the | |
377 second part upper case. Returns one-part language codes unchanged. | |
378 | |
379 Args: | |
380 code: 'zh_cn' | |
381 | |
382 Return: | |
383 code: 'zh-CN' | |
384 ''' | |
385 parts = _LANGUAGE_SPLIT_RE.split(code) | |
386 code = [ parts[0] ] | |
387 for part in parts[1:]: | |
388 code.append(part.upper()) | |
389 return '-'.join(code) | |
390 | |
391 | |
392 _LANG_TO_CODEPAGE = { | |
393 'en' : 1252, | |
394 'fr' : 1252, | |
395 'it' : 1252, | |
396 'de' : 1252, | |
397 'es' : 1252, | |
398 'nl' : 1252, | |
399 'sv' : 1252, | |
400 'no' : 1252, | |
401 'da' : 1252, | |
402 'fi' : 1252, | |
403 'pt-BR' : 1252, | |
404 'ru' : 1251, | |
405 'ja' : 932, | |
406 'zh-TW' : 950, | |
407 'zh-CN' : 936, | |
408 'ko' : 949, | |
409 } | |
410 | |
411 | |
412 def LanguageToCodepage(lang): | |
413 '''Returns the codepage _number_ that can be used to represent 'lang', which | |
414 may be either in formats such as 'en', 'pt_br', 'pt-BR', etc. | |
415 | |
416 The codepage returned will be one of the 'cpXXXX' codepage numbers. | |
417 | |
418 Args: | |
419 lang: 'de' | |
420 | |
421 Return: | |
422 1252 | |
423 ''' | |
424 lang = CanonicalLanguage(lang) | |
425 if lang in _LANG_TO_CODEPAGE: | |
426 return _LANG_TO_CODEPAGE[lang] | |
427 else: | |
428 print "Not sure which codepage to use for %s, assuming cp1252" % lang | |
429 return 1252 | |
430 | |
431 def NewClassInstance(class_name, class_type): | |
432 '''Returns an instance of the class specified in classname | |
433 | |
434 Args: | |
435 class_name: the fully qualified, dot separated package + classname, | |
436 i.e. "my.package.name.MyClass". Short class names are not supported. | |
437 class_type: the class or superclass this object must implement | |
438 | |
439 Return: | |
440 An instance of the class, or None if none was found | |
441 ''' | |
442 lastdot = class_name.rfind('.') | |
443 module_name = '' | |
444 if lastdot >= 0: | |
445 module_name = class_name[0:lastdot] | |
446 if module_name: | |
447 class_name = class_name[lastdot+1:] | |
448 module = __import__(module_name, globals(), locals(), ['']) | |
449 if hasattr(module, class_name): | |
450 class_ = getattr(module, class_name) | |
451 class_instance = class_() | |
452 if isinstance(class_instance, class_type): | |
453 return class_instance | |
454 return None | |
455 | |
456 | |
457 def FixLineEnd(text, line_end): | |
458 # First normalize | |
459 text = text.replace('\r\n', '\n') | |
460 text = text.replace('\r', '\n') | |
461 # Then fix | |
462 text = text.replace('\n', line_end) | |
463 return text | |
464 | |
465 | |
466 def BoolToString(bool): | |
467 if bool: | |
468 return 'true' | |
469 else: | |
470 return 'false' | |
471 | |
472 | |
473 verbose = False | |
474 extra_verbose = False | |
475 | |
476 def IsVerbose(): | |
477 return verbose | |
478 | |
479 def IsExtraVerbose(): | |
480 return extra_verbose | |
481 | |
482 def ParseDefine(define): | |
483 '''Parses a define argument and returns the name and value. | |
484 | |
485 The format is either "NAME=VAL" or "NAME", using True as the default value. | |
486 Values of "1" and "0" are transformed to True and False respectively. | |
487 | |
488 Args: | |
489 define: a string of the form "NAME=VAL" or "NAME". | |
490 | |
491 Returns: | |
492 A (name, value) pair. name is a string, value a string or boolean. | |
493 ''' | |
494 parts = [part.strip() for part in define.split('=', 1)] | |
495 assert len(parts) >= 1 | |
496 name = parts[0] | |
497 val = True | |
498 if len(parts) > 1: | |
499 val = parts[1] | |
500 if val == "1": val = True | |
501 elif val == "0": val = False | |
502 return (name, val) | |
503 | |
504 | |
505 class Substituter(object): | |
506 '''Finds and substitutes variable names in text strings. | |
507 | |
508 Given a dictionary of variable names and values, prepares to | |
509 search for patterns of the form [VAR_NAME] in a text. | |
510 The value will be substituted back efficiently. | |
511 Also applies to tclib.Message objects. | |
512 ''' | |
513 | |
514 def __init__(self): | |
515 '''Create an empty substituter.''' | |
516 self.substitutions_ = {} | |
517 self.dirty_ = True | |
518 | |
519 def AddSubstitutions(self, subs): | |
520 '''Add new values to the substitutor. | |
521 | |
522 Args: | |
523 subs: A dictionary of new substitutions. | |
524 ''' | |
525 self.substitutions_.update(subs) | |
526 self.dirty_ = True | |
527 | |
528 def AddMessages(self, messages, lang): | |
529 '''Adds substitutions extracted from node.Message objects. | |
530 | |
531 Args: | |
532 messages: a list of node.Message objects. | |
533 lang: The translation language to use in substitutions. | |
534 ''' | |
535 subs = [(str(msg.attrs['name']), msg.Translate(lang)) for msg in messages] | |
536 self.AddSubstitutions(dict(subs)) | |
537 self.dirty_ = True | |
538 | |
539 def GetExp(self): | |
540 '''Obtain a regular expression that will find substitution keys in text. | |
541 | |
542 Create and cache if the substituter has been updated. Use the cached value | |
543 otherwise. Keys will be enclosed in [square brackets] in text. | |
544 | |
545 Returns: | |
546 A regular expression object. | |
547 ''' | |
548 if self.dirty_: | |
549 components = ['\[%s\]' % (k,) for k in self.substitutions_.keys()] | |
550 self.exp = re.compile("(%s)" % ('|'.join(components),)) | |
551 self.dirty_ = False | |
552 return self.exp | |
553 | |
554 def Substitute(self, text): | |
555 '''Substitute the variable values in the given text. | |
556 | |
557 Text of the form [message_name] will be replaced by the message's value. | |
558 | |
559 Args: | |
560 text: A string of text. | |
561 | |
562 Returns: | |
563 A string of text with substitutions done. | |
564 ''' | |
565 return ''.join([self._SubFragment(f) for f in self.GetExp().split(text)]) | |
566 | |
567 def _SubFragment(self, fragment): | |
568 '''Utility function for Substitute. | |
569 | |
570 Performs a simple substitution if the fragment is exactly of the form | |
571 [message_name]. | |
572 | |
573 Args: | |
574 fragment: A simple string. | |
575 | |
576 Returns: | |
577 A string with the substitution done. | |
578 ''' | |
579 if len(fragment) > 2 and fragment[0] == '[' and fragment[-1] == ']': | |
580 sub = self.substitutions_.get(fragment[1:-1], None) | |
581 if sub is not None: | |
582 return sub | |
583 return fragment | |
584 | |
585 def SubstituteMessage(self, msg): | |
586 '''Apply substitutions to a tclib.Message object. | |
587 | |
588 Text of the form [message_name] will be replaced by a new placeholder, | |
589 whose presentation will take the form the message_name_{UsageCount}, and | |
590 whose example will be the message's value. Existing placeholders are | |
591 not affected. | |
592 | |
593 Args: | |
594 msg: A tclib.Message object. | |
595 | |
596 Returns: | |
597 A tclib.Message object, with substitutions done. | |
598 ''' | |
599 from grit import tclib # avoid circular import | |
600 counts = {} | |
601 text = msg.GetPresentableContent() | |
602 placeholders = [] | |
603 newtext = '' | |
604 for f in self.GetExp().split(text): | |
605 sub = self._SubFragment(f) | |
606 if f != sub: | |
607 f = str(f) | |
608 count = counts.get(f, 0) + 1 | |
609 counts[f] = count | |
610 name = "%s_%d" % (f[1:-1], count) | |
611 placeholders.append(tclib.Placeholder(name, f, sub)) | |
612 newtext += name | |
613 else: | |
614 newtext += f | |
615 if placeholders: | |
616 return tclib.Message(newtext, msg.GetPlaceholders() + placeholders, | |
617 msg.GetDescription(), msg.GetMeaning()) | |
618 else: | |
619 return msg | |
620 | |
621 | |
622 class TempDir(object): | |
623 '''Creates files with the specified contents in a temporary directory, | |
624 for unit testing. | |
625 ''' | |
626 def __init__(self, file_data): | |
627 self._tmp_dir_name = tempfile.mkdtemp() | |
628 assert not os.listdir(self.GetPath()) | |
629 for name, contents in file_data.items(): | |
630 file_path = self.GetPath(name) | |
631 dir_path = os.path.split(file_path)[0] | |
632 if not os.path.exists(dir_path): | |
633 os.makedirs(dir_path) | |
634 with open(file_path, 'w') as f: | |
635 f.write(file_data[name]) | |
636 | |
637 def __enter__(self): | |
638 return self | |
639 | |
640 def __exit__(self, *exc_info): | |
641 self.CleanUp() | |
642 | |
643 def CleanUp(self): | |
644 shutil.rmtree(self.GetPath()) | |
645 | |
646 def GetPath(self, name=''): | |
647 name = os.path.join(self._tmp_dir_name, name) | |
648 assert name.startswith(self._tmp_dir_name) | |
649 return name | |
650 | |
651 def AsCurrentDir(self): | |
652 return self._AsCurrentDirClass(self.GetPath()) | |
653 | |
654 class _AsCurrentDirClass(object): | |
655 def __init__(self, path): | |
656 self.path = path | |
657 def __enter__(self): | |
658 self.oldpath = os.getcwd() | |
659 os.chdir(self.path) | |
660 def __exit__(self, *exc_info): | |
661 os.chdir(self.oldpath) | |
OLD | NEW |