Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(72)

Side by Side Diff: presubmit_support.py

Issue 113933: Add AffectedFile.IsTextFile() and deprecate AffectedTextFiles(). (Closed)
Patch Set: Stop using return InputApi._RightHandSideLinesImpl( Created 11 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | tests/presubmit_unittest.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/python 1 #!/usr/bin/python
2 # Copyright (c) 2006-2009 The Chromium Authors. All rights reserved. 2 # Copyright (c) 2006-2009 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """Enables directory-specific presubmit checks to run at upload and/or commit. 6 """Enables directory-specific presubmit checks to run at upload and/or commit.
7 """ 7 """
8 8
9 __version__ = '1.1' 9 __version__ = '1.1'
10 10
11 # TODO(joi) Add caching where appropriate/needed. The API is designed to allow 11 # TODO(joi) Add caching where appropriate/needed. The API is designed to allow
12 # caching (between all different invocations of presubmit scripts for a given 12 # caching (between all different invocations of presubmit scripts for a given
13 # change). We should add it as our presubmit scripts start feeling slow. 13 # change). We should add it as our presubmit scripts start feeling slow.
14 14
15 import cPickle # Exposed through the API. 15 import cPickle # Exposed through the API.
16 import cStringIO # Exposed through the API. 16 import cStringIO # Exposed through the API.
17 import exceptions 17 import exceptions
18 import fnmatch 18 import fnmatch
19 import glob 19 import glob
20 import marshal # Exposed through the API. 20 import marshal # Exposed through the API.
21 import optparse 21 import optparse
22 import os # Somewhat exposed through the API. 22 import os # Somewhat exposed through the API.
23 import pickle # Exposed through the API. 23 import pickle # Exposed through the API.
24 import re # Exposed through the API. 24 import re # Exposed through the API.
25 import subprocess # Exposed through the API. 25 import subprocess # Exposed through the API.
26 import sys # Parts exposed through API. 26 import sys # Parts exposed through API.
27 import tempfile # Exposed through the API. 27 import tempfile # Exposed through the API.
28 import types 28 import types
29 import urllib2 # Exposed through the API. 29 import urllib2 # Exposed through the API.
30 import warnings
30 31
31 # Local imports. 32 # Local imports.
32 # TODO(joi) Would be cleaner to factor out utils in gcl to separate module, but 33 # TODO(joi) Would be cleaner to factor out utils in gcl to separate module, but
33 # for now it would only be a couple of functions so hardly worth it. 34 # for now it would only be a couple of functions so hardly worth it.
34 import gcl 35 import gcl
35 import gclient 36 import gclient
36 import presubmit_canned_checks 37 import presubmit_canned_checks
37 38
38 39
39 class NotImplementedException(Exception): 40 class NotImplementedException(Exception):
40 """We're leaving placeholders in a bunch of places to remind us of the 41 """We're leaving placeholders in a bunch of places to remind us of the
41 design of the API, but we have not implemented all of it yet. Implement as 42 design of the API, but we have not implemented all of it yet. Implement as
42 the need arises. 43 the need arises.
43 """ 44 """
44 pass 45 pass
45 46
46 47
47 def normpath(path): 48 def normpath(path):
48 '''Version of os.path.normpath that also changes backward slashes to 49 '''Version of os.path.normpath that also changes backward slashes to
49 forward slashes when not running on Windows. 50 forward slashes when not running on Windows.
50 ''' 51 '''
51 # This is safe to always do because the Windows version of os.path.normpath 52 # This is safe to always do because the Windows version of os.path.normpath
52 # will replace forward slashes with backward slashes. 53 # will replace forward slashes with backward slashes.
53 path = path.replace(os.sep, '/') 54 path = path.replace(os.sep, '/')
54 return os.path.normpath(path) 55 return os.path.normpath(path)
55 56
56 57
58 def deprecated(func):
59 """This is a decorator which can be used to mark functions as deprecated.
60
61 It will result in a warning being emmitted when the function is used."""
62 def newFunc(*args, **kwargs):
63 warnings.warn("Call to deprecated function %s." % func.__name__,
64 category=DeprecationWarning,
65 stacklevel=2)
66 return func(*args, **kwargs)
67 newFunc.__name__ = func.__name__
68 newFunc.__doc__ = func.__doc__
69 newFunc.__dict__.update(func.__dict__)
70 return newFunc
71
72
57 class OutputApi(object): 73 class OutputApi(object):
58 """This class (more like a module) gets passed to presubmit scripts so that 74 """This class (more like a module) gets passed to presubmit scripts so that
59 they can specify various types of results. 75 they can specify various types of results.
60 """ 76 """
61 77
62 class PresubmitResult(object): 78 class PresubmitResult(object):
63 """Base class for result objects.""" 79 """Base class for result objects."""
64 80
65 def __init__(self, message, items=None, long_text=''): 81 def __init__(self, message, items=None, long_text=''):
66 """ 82 """
(...skipping 103 matching lines...) Expand 10 before | Expand all | Expand 10 after
170 def PresubmitLocalPath(self): 186 def PresubmitLocalPath(self):
171 """Returns the local path of the presubmit script currently being run. 187 """Returns the local path of the presubmit script currently being run.
172 188
173 This is useful if you don't want to hard-code absolute paths in the 189 This is useful if you don't want to hard-code absolute paths in the
174 presubmit script. For example, It can be used to find another file 190 presubmit script. For example, It can be used to find another file
175 relative to the PRESUBMIT.py script, so the whole tree can be branched and 191 relative to the PRESUBMIT.py script, so the whole tree can be branched and
176 the presubmit script still works, without editing its content. 192 the presubmit script still works, without editing its content.
177 """ 193 """
178 return self._current_presubmit_path 194 return self._current_presubmit_path
179 195
180 @staticmethod 196 def DepotToLocalPath(self, depot_path):
Jói Sigurðsson 2009/05/28 16:49:36 why make non-static when it doesn't use self?
181 def DepotToLocalPath(depot_path):
182 """Translate a depot path to a local path (relative to client root). 197 """Translate a depot path to a local path (relative to client root).
183 198
184 Args: 199 Args:
185 Depot path as a string. 200 Depot path as a string.
186 201
187 Returns: 202 Returns:
188 The local path of the depot path under the user's current client, or None 203 The local path of the depot path under the user's current client, or None
189 if the file is not mapped. 204 if the file is not mapped.
190 205
191 Remember to check for the None case and show an appropriate error! 206 Remember to check for the None case and show an appropriate error!
192 """ 207 """
193 local_path = gclient.CaptureSVNInfo(depot_path).get('Path') 208 local_path = gclient.CaptureSVNInfo(depot_path).get('Path')
194 if not local_path: 209 if local_path:
195 return None
196 else:
197 return local_path 210 return local_path
198 211
199 @staticmethod 212 def LocalToDepotPath(self, local_path):
200 def LocalToDepotPath(local_path):
201 """Translate a local path to a depot path. 213 """Translate a local path to a depot path.
202 214
203 Args: 215 Args:
204 Local path (relative to current directory, or absolute) as a string. 216 Local path (relative to current directory, or absolute) as a string.
205 217
206 Returns: 218 Returns:
207 The depot path (SVN URL) of the file if mapped, otherwise None. 219 The depot path (SVN URL) of the file if mapped, otherwise None.
208 """ 220 """
209 depot_path = gclient.CaptureSVNInfo(local_path).get('URL') 221 depot_path = gclient.CaptureSVNInfo(local_path).get('URL')
210 if not depot_path: 222 if depot_path:
211 return None
212 else:
213 return depot_path 223 return depot_path
214 224
215 @staticmethod 225 @staticmethod
216 def FilterTextFiles(affected_files, include_deletes=True): 226 def FilterTextFiles(affected_files, include_deletes=True):
217 """Filters out all except text files and optionally also filters out 227 """Filters out all except text files and optionally also filters out
218 deleted files. 228 deleted files.
219 229
220 Args: 230 Args:
221 affected_files: List of AffectedFiles objects. 231 affected_files: List of AffectedFiles objects.
222 include_deletes: If false, deleted files will be filtered out. 232 include_deletes: If false, deleted files will be filtered out.
(...skipping 30 matching lines...) Expand all
253 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)] 263 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
254 264
255 def AbsoluteLocalPaths(self, include_dirs=False): 265 def AbsoluteLocalPaths(self, include_dirs=False):
256 """Returns absolute local paths of input_api.AffectedFiles().""" 266 """Returns absolute local paths of input_api.AffectedFiles()."""
257 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)] 267 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
258 268
259 def ServerPaths(self, include_dirs=False): 269 def ServerPaths(self, include_dirs=False):
260 """Returns server paths of input_api.AffectedFiles().""" 270 """Returns server paths of input_api.AffectedFiles()."""
261 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)] 271 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
262 272
273 @deprecated
263 def AffectedTextFiles(self, include_deletes=True): 274 def AffectedTextFiles(self, include_deletes=True):
264 """Same as input_api.change.AffectedTextFiles() except only lists files 275 """Same as input_api.change.AffectedTextFiles() except only lists files
265 in the same directory as the current presubmit script, or subdirectories 276 in the same directory as the current presubmit script, or subdirectories
266 thereof. 277 thereof.
267 278
268 Warning: This function retrieves the svn property on each file so it can be 279 Warning: This function retrieves the svn property on each file so it can be
269 slow for large change lists. 280 slow for large change lists.
270 """ 281 """
271 return InputApi.FilterTextFiles(self.AffectedFiles(include_dirs=False), 282 return InputApi.FilterTextFiles(self.AffectedFiles(include_dirs=False),
272 include_deletes) 283 include_deletes)
273 284
274 def RightHandSideLines(self): 285 def RightHandSideLines(self):
275 """An iterator over all text lines in "new" version of changed files. 286 """An iterator over all text lines in "new" version of changed files.
276 287
277 Only lists lines from new or modified text files in the change that are 288 Only lists lines from new or modified text files in the change that are
278 contained by the directory of the currently executing presubmit script. 289 contained by the directory of the currently executing presubmit script.
279 290
280 This is useful for doing line-by-line regex checks, like checking for 291 This is useful for doing line-by-line regex checks, like checking for
281 trailing whitespace. 292 trailing whitespace.
282 293
283 Yields: 294 Yields:
284 a 3 tuple: 295 a 3 tuple:
285 the AffectedFile instance of the current file; 296 the AffectedFile instance of the current file;
286 integer line number (1-based); and 297 integer line number (1-based); and
287 the contents of the line as a string. 298 the contents of the line as a string.
288 """ 299 """
289 return InputApi._RightHandSideLinesImpl( 300 return InputApi._RightHandSideLinesImpl(
290 self.AffectedTextFiles(include_deletes=False)) 301 filter(lambda x: x.IsTextFile(),
302 self.AffectedFiles(include_deletes=False)))
291 303
292 @staticmethod 304 @staticmethod
293 def _RightHandSideLinesImpl(affected_files): 305 def _RightHandSideLinesImpl(affected_files):
294 """Implements RightHandSideLines for InputApi and GclChange.""" 306 """Implements RightHandSideLines for InputApi and GclChange."""
295 for af in affected_files: 307 for af in affected_files:
296 lines = af.NewContents() 308 lines = af.NewContents()
297 line_number = 0 309 line_number = 0
298 for line in lines: 310 for line in lines:
299 line_number += 1 311 line_number += 1
300 yield (af, line_number, line) 312 yield (af, line_number, line)
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after
341 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but 353 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
342 # different for other SCM. 354 # different for other SCM.
343 return self.action 355 return self.action
344 356
345 def Property(self, property_name): 357 def Property(self, property_name):
346 """Returns the specified SCM property of this file, or None if no such 358 """Returns the specified SCM property of this file, or None if no such
347 property. 359 property.
348 """ 360 """
349 return self.properties.get(property_name, None) 361 return self.properties.get(property_name, None)
350 362
363 def IsTextFile(self):
364 """Returns True if the file is a text file and not a binary file."""
365 raise NotImplementedError() # Implement when needed
366
351 def NewContents(self): 367 def NewContents(self):
352 """Returns an iterator over the lines in the new version of file. 368 """Returns an iterator over the lines in the new version of file.
353 369
354 The new version is the file in the user's workspace, i.e. the "right hand 370 The new version is the file in the user's workspace, i.e. the "right hand
355 side". 371 side".
356 372
357 Contents will be empty if the file is a directory or does not exist. 373 Contents will be empty if the file is a directory or does not exist.
358 """ 374 """
359 if self.IsDirectory(): 375 if self.IsDirectory():
360 return [] 376 return []
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after
398 self.is_directory = gclient.CaptureSVNInfo( 414 self.is_directory = gclient.CaptureSVNInfo(
399 path).get('Node Kind') in ('dir', 'directory') 415 path).get('Node Kind') in ('dir', 'directory')
400 return self.is_directory 416 return self.is_directory
401 417
402 def Property(self, property_name): 418 def Property(self, property_name):
403 if not property_name in self.properties: 419 if not property_name in self.properties:
404 self.properties[property_name] = gcl.GetSVNFileProperty( 420 self.properties[property_name] = gcl.GetSVNFileProperty(
405 self.AbsoluteLocalPath(), property_name) 421 self.AbsoluteLocalPath(), property_name)
406 return self.properties[property_name] 422 return self.properties[property_name]
407 423
424 def IsTextFile(self):
425 if self.Action() == 'D':
426 return False
427 mime_type = gcl.GetSVNFileProperty(self.AbsoluteLocalPath(),
428 'svn:mime-type')
429 if not mime_type or mime_type.startswith('text/'):
430 return True
431 return False
432
408 433
409 class GclChange(object): 434 class GclChange(object):
410 """Describe a change. 435 """Describe a change.
411 436
412 Used directly by the presubmit scripts to query the current change being 437 Used directly by the presubmit scripts to query the current change being
413 tested. 438 tested.
414 439
415 Instance members: 440 Instance members:
416 tags: Dictionnary of KEY=VALUE pairs found in the change description. 441 tags: Dictionnary of KEY=VALUE pairs found in the change description.
417 self.KEY: equivalent to tags['KEY'] 442 self.KEY: equivalent to tags['KEY']
(...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after
489 if include_dirs: 514 if include_dirs:
490 affected = self._affected_files 515 affected = self._affected_files
491 else: 516 else:
492 affected = filter(lambda x: not x.IsDirectory(), self._affected_files) 517 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
493 518
494 if include_deletes: 519 if include_deletes:
495 return affected 520 return affected
496 else: 521 else:
497 return filter(lambda x: x.Action() != 'D', affected) 522 return filter(lambda x: x.Action() != 'D', affected)
498 523
524 @deprecated
499 def AffectedTextFiles(self, include_deletes=True): 525 def AffectedTextFiles(self, include_deletes=True):
500 """Return a list of the text files in a change. 526 """Return a list of the text files in a change.
501 527
502 It's common to want to iterate over only the text files. 528 It's common to want to iterate over only the text files.
503 529
504 Args: 530 Args:
505 include_deletes: Controls whether to return files with "delete" actions, 531 include_deletes: Controls whether to return files with "delete" actions,
506 which commonly aren't relevant to presubmit scripts. 532 which commonly aren't relevant to presubmit scripts.
507 """ 533 """
508 return InputApi.FilterTextFiles(self.AffectedFiles(include_dirs=False), 534 return InputApi.FilterTextFiles(self.AffectedFiles(include_dirs=False),
(...skipping 19 matching lines...) Expand all
528 This is useful for doing line-by-line regex checks, like checking for 554 This is useful for doing line-by-line regex checks, like checking for
529 trailing whitespace. 555 trailing whitespace.
530 556
531 Yields: 557 Yields:
532 a 3 tuple: 558 a 3 tuple:
533 the AffectedFile instance of the current file; 559 the AffectedFile instance of the current file;
534 integer line number (1-based); and 560 integer line number (1-based); and
535 the contents of the line as a string. 561 the contents of the line as a string.
536 """ 562 """
537 return InputApi._RightHandSideLinesImpl( 563 return InputApi._RightHandSideLinesImpl(
538 self.AffectedTextFiles(include_deletes=False)) 564 filter(lambda x: x.IsTextFile(),
565 self.AffectedFiles(include_deletes=False)))
539 566
540 567
541 def ListRelevantPresubmitFiles(files): 568 def ListRelevantPresubmitFiles(files):
542 """Finds all presubmit files that apply to a given set of source files. 569 """Finds all presubmit files that apply to a given set of source files.
543 570
544 Args: 571 Args:
545 files: An iterable container containing file paths. 572 files: An iterable container containing file paths.
546 573
547 Return: 574 Return:
548 ['foo/blat/PRESUBMIT.py', 'mat/gat/PRESUBMIT.py'] 575 ['foo/blat/PRESUBMIT.py', 'mat/gat/PRESUBMIT.py']
(...skipping 18 matching lines...) Expand all
567 return presubmit_files 594 return presubmit_files
568 595
569 596
570 class PresubmitExecuter(object): 597 class PresubmitExecuter(object):
571 def __init__(self, change_info, committing): 598 def __init__(self, change_info, committing):
572 """ 599 """
573 Args: 600 Args:
574 change_info: The ChangeInfo object for the change. 601 change_info: The ChangeInfo object for the change.
575 committing: True if 'gcl commit' is running, False if 'gcl upload' is. 602 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
576 """ 603 """
604 # TODO(maruel): Determine the SCM.
577 self.change = GclChange(change_info, gcl.GetRepositoryRoot()) 605 self.change = GclChange(change_info, gcl.GetRepositoryRoot())
578 self.committing = committing 606 self.committing = committing
579 607
580 def ExecPresubmitScript(self, script_text, presubmit_path): 608 def ExecPresubmitScript(self, script_text, presubmit_path):
581 """Executes a single presubmit script. 609 """Executes a single presubmit script.
582 610
583 Args: 611 Args:
584 script_text: The text of the presubmit script. 612 script_text: The text of the presubmit script.
585 presubmit_path: The path to the presubmit file (this will be reported via 613 presubmit_path: The path to the presubmit file (this will be reported via
586 input_api.PresubmitLocalPath()). 614 input_api.PresubmitLocalPath()).
(...skipping 140 matching lines...) Expand 10 before | Expand all | Expand 10 after
727 return not DoPresubmitChecks(gcl.ChangeInfo(name='temp', files=files), 755 return not DoPresubmitChecks(gcl.ChangeInfo(name='temp', files=files),
728 options.commit, 756 options.commit,
729 options.verbose, 757 options.verbose,
730 sys.stdout, 758 sys.stdout,
731 sys.stdin, 759 sys.stdin,
732 default_presubmit=None) 760 default_presubmit=None)
733 761
734 762
735 if __name__ == '__main__': 763 if __name__ == '__main__':
736 sys.exit(Main(sys.argv)) 764 sys.exit(Main(sys.argv))
OLDNEW
« no previous file with comments | « no previous file | tests/presubmit_unittest.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698