104
104
105
105
"""
106
106
107
+ from __future__ import print_function
107
108
import sys
108
109
import os .path
109
110
import re
@@ -160,8 +161,13 @@ def close(self):
160
161
HTMLParser .close (self )
161
162
return self .__builder .close ()
162
163
163
- Command = namedtuple ('Command' , 'negated cmd args lineno' )
164
+ Command = namedtuple ('Command' , 'negated cmd args lineno context ' )
164
165
166
+ class FailedCheck (Exception ):
167
+ pass
168
+
169
+ class InvalidCheck (Exception ):
170
+ pass
165
171
166
172
def concat_multi_lines (f ):
167
173
"""returns a generator out of the file object, which
@@ -196,7 +202,7 @@ def concat_multi_lines(f):
196
202
catenated = ''
197
203
198
204
if lastline is not None :
199
- raise RuntimeError ( 'Trailing backslash in the end of file' )
205
+ print_err ( lineno , line , 'Trailing backslash at the end of the file' )
200
206
201
207
LINE_PATTERN = re .compile (r'''
202
208
(?<=(?<!\S)@)(?P<negated>!?)
@@ -216,9 +222,10 @@ def get_commands(template):
216
222
cmd = m .group ('cmd' )
217
223
args = m .group ('args' )
218
224
if args and not args [:1 ].isspace ():
219
- raise RuntimeError ('Invalid template syntax at line {}' .format (lineno + 1 ))
225
+ print_err (lineno , line , 'Invalid template syntax' )
226
+ continue
220
227
args = shlex .split (args )
221
- yield Command (negated = negated , cmd = cmd , args = args , lineno = lineno + 1 )
228
+ yield Command (negated = negated , cmd = cmd , args = args , lineno = lineno + 1 , context = line )
222
229
223
230
224
231
def _flatten (node , acc ):
@@ -242,8 +249,7 @@ def normalize_xpath(path):
242
249
elif path .startswith ('.//' ):
243
250
return path
244
251
else :
245
- raise RuntimeError ('Non-absolute XPath is not supported due to \
246
- the implementation issue.' )
252
+ raise InvalidCheck ('Non-absolute XPath is not supported due to implementation issues' )
247
253
248
254
249
255
class CachedFiles (object ):
@@ -259,41 +265,40 @@ def resolve_path(self, path):
259
265
self .last_path = path
260
266
return path
261
267
elif self .last_path is None :
262
- raise RuntimeError ('Tried to use the previous path in the first command' )
268
+ raise InvalidCheck ('Tried to use the previous path in the first command' )
263
269
else :
264
270
return self .last_path
265
271
266
272
def get_file (self , path ):
267
273
path = self .resolve_path (path )
268
- try :
274
+ if path in self . files :
269
275
return self .files [path ]
270
- except KeyError :
271
- try :
272
- with open (os .path .join ( self . root , path )) as f :
273
- data = f . read ( )
274
- except Exception as e :
275
- raise RuntimeError ( 'Cannot open file {!r}: {}' . format ( path , e ))
276
- else :
277
- self .files [path ] = data
278
- return data
276
+
277
+ abspath = os . path . join ( self . root , path )
278
+ if not (os .path .exists ( abspath ) and os . path . isfile ( abspath )) :
279
+ raise FailedCheck ( 'File does not exist {!r}' . format ( path ) )
280
+
281
+ with open ( abspath ) as f :
282
+ data = f . read ()
283
+ self .files [path ] = data
284
+ return data
279
285
280
286
def get_tree (self , path ):
281
287
path = self .resolve_path (path )
282
- try :
288
+ if path in self . trees :
283
289
return self .trees [path ]
284
- except KeyError :
285
- try :
286
- f = open (os .path .join (self .root , path ))
287
- except Exception as e :
288
- raise RuntimeError ('Cannot open file {!r}: {}' .format (path , e ))
290
+
291
+ abspath = os .path .join (self .root , path )
292
+ if not (os .path .exists (abspath ) and os .path .isfile (abspath )):
293
+ raise FailedCheck ('File does not exist {!r}' .format (path ))
294
+
295
+ with open (abspath ) as f :
289
296
try :
290
- with f :
291
- tree = ET .parse (f , CustomHTMLParser ())
297
+ tree = ET .parse (f , CustomHTMLParser ())
292
298
except Exception as e :
293
299
raise RuntimeError ('Cannot parse an HTML file {!r}: {}' .format (path , e ))
294
- else :
295
- self .trees [path ] = tree
296
- return self .trees [path ]
300
+ self .trees [path ] = tree
301
+ return self .trees [path ]
297
302
298
303
299
304
def check_string (data , pat , regexp ):
@@ -311,14 +316,14 @@ def check_tree_attr(tree, path, attr, pat, regexp):
311
316
path = normalize_xpath (path )
312
317
ret = False
313
318
for e in tree .findall (path ):
314
- try :
319
+ if attr in e . attrib :
315
320
value = e .attrib [attr ]
316
- except KeyError :
317
- continue
318
321
else :
319
- ret = check_string (value , pat , regexp )
320
- if ret :
321
- break
322
+ continue
323
+
324
+ ret = check_string (value , pat , regexp )
325
+ if ret :
326
+ break
322
327
return ret
323
328
324
329
@@ -341,57 +346,84 @@ def check_tree_count(tree, path, count):
341
346
path = normalize_xpath (path )
342
347
return len (tree .findall (path )) == count
343
348
349
+ def stderr (* args ):
350
+ print (* args , file = sys .stderr )
344
351
345
- def check (target , commands ):
346
- cache = CachedFiles (target )
347
- for c in commands :
352
+ def print_err (lineno , context , err , message = None ):
353
+ global ERR_COUNT
354
+ ERR_COUNT += 1
355
+ stderr ("{}: {}" .format (lineno , message or err ))
356
+ if message and err :
357
+ stderr ("\t {}" .format (err ))
358
+
359
+ if context :
360
+ stderr ("\t {}" .format (context ))
361
+
362
+ ERR_COUNT = 0
363
+
364
+ def check_command (c , cache ):
365
+ try :
366
+ cerr = ""
348
367
if c .cmd == 'has' or c .cmd == 'matches' : # string test
349
368
regexp = (c .cmd == 'matches' )
350
369
if len (c .args ) == 1 and not regexp : # @has <path> = file existence
351
370
try :
352
371
cache .get_file (c .args [0 ])
353
372
ret = True
354
- except RuntimeError :
373
+ except FailedCheck as err :
374
+ cerr = err .message
355
375
ret = False
356
376
elif len (c .args ) == 2 : # @has/matches <path> <pat> = string test
377
+ cerr = "`PATTERN` did not match"
357
378
ret = check_string (cache .get_file (c .args [0 ]), c .args [1 ], regexp )
358
379
elif len (c .args ) == 3 : # @has/matches <path> <pat> <match> = XML tree test
380
+ cerr = "`XPATH PATTERN` did not match"
359
381
tree = cache .get_tree (c .args [0 ])
360
382
pat , sep , attr = c .args [1 ].partition ('/@' )
361
383
if sep : # attribute
362
- ret = check_tree_attr (cache .get_tree (c .args [0 ]), pat , attr , c .args [2 ], regexp )
384
+ tree = cache .get_tree (c .args [0 ])
385
+ ret = check_tree_attr (tree , pat , attr , c .args [2 ], regexp )
363
386
else : # normalized text
364
387
pat = c .args [1 ]
365
388
if pat .endswith ('/text()' ):
366
389
pat = pat [:- 7 ]
367
390
ret = check_tree_text (cache .get_tree (c .args [0 ]), pat , c .args [2 ], regexp )
368
391
else :
369
- raise RuntimeError ('Invalid number of @{} arguments \
370
- at line {}' .format (c .cmd , c .lineno ))
392
+ raise InvalidCheck ('Invalid number of @{} arguments' .format (c .cmd ))
371
393
372
394
elif c .cmd == 'count' : # count test
373
395
if len (c .args ) == 3 : # @count <path> <pat> <count> = count test
374
396
ret = check_tree_count (cache .get_tree (c .args [0 ]), c .args [1 ], int (c .args [2 ]))
375
397
else :
376
- raise RuntimeError ('Invalid number of @{} arguments \
377
- at line {}' .format (c .cmd , c .lineno ))
378
-
398
+ raise InvalidCheck ('Invalid number of @{} arguments' .format (c .cmd ))
379
399
elif c .cmd == 'valid-html' :
380
- raise RuntimeError ('Unimplemented @valid-html at line {}' . format ( c . lineno ) )
400
+ raise InvalidCheck ('Unimplemented @valid-html' )
381
401
382
402
elif c .cmd == 'valid-links' :
383
- raise RuntimeError ('Unimplemented @valid-links at line {}' .format (c .lineno ))
384
-
403
+ raise InvalidCheck ('Unimplemented @valid-links' )
385
404
else :
386
- raise RuntimeError ('Unrecognized @{} at line {} ' .format (c .cmd , c . lineno ))
405
+ raise InvalidCheck ('Unrecognized @{}' .format (c .cmd ))
387
406
388
407
if ret == c .negated :
389
- raise RuntimeError ('@{}{} check failed at line {}' .format ('!' if c .negated else '' ,
390
- c .cmd , c .lineno ))
408
+ raise FailedCheck (cerr )
409
+
410
+ except FailedCheck as err :
411
+ message = '@{}{} check failed' .format ('!' if c .negated else '' , c .cmd )
412
+ print_err (c .lineno , c .context , err .message , message )
413
+ except InvalidCheck as err :
414
+ print_err (c .lineno , c .context , err .message )
415
+
416
+ def check (target , commands ):
417
+ cache = CachedFiles (target )
418
+ for c in commands :
419
+ check_command (c , cache )
391
420
392
421
if __name__ == '__main__' :
393
- if len (sys .argv ) < 3 :
394
- print >> sys .stderr , 'Usage: {} <doc dir> <template>' .format (sys .argv [0 ])
422
+ if len (sys .argv ) != 3 :
423
+ stderr ('Usage: {} <doc dir> <template>' .format (sys .argv [0 ]))
424
+ raise SystemExit (1 )
425
+
426
+ check (sys .argv [1 ], get_commands (sys .argv [2 ]))
427
+ if ERR_COUNT :
428
+ stderr ("\n Encountered {} errors" .format (ERR_COUNT ))
395
429
raise SystemExit (1 )
396
- else :
397
- check (sys .argv [1 ], get_commands (sys .argv [2 ]))
0 commit comments