Coverage for cogapp/cogapp.py: 49.50%

1# coding: utf8

2""" Cog content generation tool.

3 http://nedbatchelder.com/code/cog

4

5 Copyright 2004-2019, Ned Batchelder.

6"""

7

8 from __future__ import absolute_import, print_function

9

10 import copy

11 import getopt

12 import glob

13 import hashlib

14 import linecache

15 import os

16 import re

17 import shlex

18 import sys

19 import traceback

20

21 from .backward import PY3, StringIO, string_types, to_bytes

22

23 __all__ = ['Cog', 'CogUsageError', 'main']

24

25 __version__ = '3.2.0'

26

27 usage = """\

28 cog - generate content with inlined Python code.

29

30 cog [OPTIONS] [INFILE | @FILELIST] ...

31

32 INFILE is the name of an input file, '-' will read from stdin.

33 FILELIST is the name of a text file containing file names or

34 other @FILELISTs.

35

36 OPTIONS:

37 -c Checksum the output to protect it against accidental change.

38 -d Delete the generator code from the output file.

39 -D name=val Define a global string available to your generator code.

40 -e Warn if a file has no cog code in it.

41 -I PATH Add PATH to the list of directories for data files and modules.

42 -n ENCODING Use ENCODING when reading and writing files.

43 -o OUTNAME Write the output to OUTNAME.

44 -p PROLOGUE Prepend the generator source with PROLOGUE. Useful to insert an

45 import line. Example: -p "import math"

46 -P Use print() instead of outl for code output.

47 -r Replace the input file with the output.

48 -s STRING Suffix all generated output lines with STRING.

49 -U Write the output with Unix newlines (only LF line-endings).

50 -w CMD Use CMD if the output file needs to be made writable.

51 A %s in the CMD will be filled with the filename.

52 -x Excise all the generated output without running the generators.

53 -z The end-output marker can be omitted, and is assumed at eof.

54 -v Print the version of cog and exit.

55 --verbosity=VERBOSITY

56 Control the amount of output. 2 (the default) lists all files,

57 1 lists only changed files, 0 lists no files.

58 --markers='START END END-OUTPUT'

59 The patterns surrounding cog inline instructions. Should

60 include three values separated by spaces, the start, end,

61 and end-output markers. Defaults to '[[[cog ]]] [[[end]]]'.

62 -h Print this help.

63"""

64

65# Other package modules

66 from .whiteutils import *

67

68 class CogError(Exception):

69 """ Any exception raised by Cog.

70 """

71 def __init__(self, msg, file='', line=0):

72 if file:

73 Exception.__init__(self, "%s(%d): %s" % (file, line, msg))

74 else:

75 Exception.__init__(self, msg)

76

77 class CogUsageError(CogError):

78 """ An error in usage of command-line arguments in cog.

79 """

80 pass

81

82 class CogInternalError(CogError):

83 """ An error in the coding of Cog. Should never happen.

84 """

85 pass

86

87 class CogGeneratedError(CogError):

88 """ An error raised by a user's cog generator.

89 """

90 pass

91

92 class CogUserException(CogError):

93 """ An exception caught when running a user's cog generator.

94 The argument is the traceback message to print.

95 """

96 pass

97

98 class Redirectable:

99 """ An object with its own stdout and stderr files.

100 """

101 def __init__(self):

102 self.stdout = sys.stdout

103 self.stderr = sys.stderr

104

105 def setOutput(self, stdout=None, stderr=None):

106 """ Assign new files for standard out and/or standard error.

107 """

108 if stdout: 108 ↛ 110line 108 didn't jump to line 110, because the condition on line 108 was never false

109 self.stdout = stdout

110 if stderr: 110 ↛ 111line 110 didn't jump to line 111, because the condition on line 110 was never true

111 self.stderr = stderr

112

113 def prout(self, s, end="\n"):

114 print(s, file=self.stdout, end=end)

115

116 def prerr(self, s, end="\n"):

117 print(s, file=self.stderr, end=end)

118

119

120 class CogGenerator(Redirectable):

121 """ A generator pulled from a source file.

122 """

123 def __init__(self, options=None):

124 Redirectable.__init__(self)

125 self.markers = []

126 self.lines = []

127 self.options = options or CogOptions()

128

129 def parseMarker(self, l):

130 self.markers.append(l)

131

132 def parseLine(self, l):

133 self.lines.append(l.strip('\n'))

134

135 def getCode(self):

136 """ Extract the executable Python code from the generator.

137 """

138 # If the markers and lines all have the same prefix

139 # (end-of-line comment chars, for example),

140 # then remove it from all the lines.

141 prefIn = commonPrefix(self.markers + self.lines)

142 if prefIn:

143 self.markers = [ l.replace(prefIn, '', 1) for l in self.markers ]

144 self.lines = [ l.replace(prefIn, '', 1) for l in self.lines ]

145

146 return reindentBlock(self.lines, '')

147

148 def evaluate(self, cog, globals, fname):

149 # figure out the right whitespace prefix for the output

150 prefOut = whitePrefix(self.markers)

151

152 intext = self.getCode()

153 if not intext:

154 return ''

155

156 prologue = "import " + cog.cogmodulename + " as cog\n"

157 if self.options.sPrologue: 157 ↛ 158line 157 didn't jump to line 158, because the condition on line 157 was never true

158 prologue += self.options.sPrologue + '\n'

159 code = compile(prologue + intext, str(fname), 'exec')

160

161 # Make sure the "cog" module has our state.

162 cog.cogmodule.msg = self.msg

163 cog.cogmodule.out = self.out

164 cog.cogmodule.outl = self.outl

165 cog.cogmodule.error = self.error

166

167 real_stdout = sys.stdout

168 if self.options.bPrintOutput: 168 ↛ 169line 168 didn't jump to line 169, because the condition on line 168 was never true

169 sys.stdout = captured_stdout = StringIO()

170

171 self.outstring = ''

172 try:

173 eval(code, globals)

174 except CogError: 174 ↛ 175line 174 didn't jump to line 175, because the exception caught by line 174 didn't happen

175 raise

176 except:

177 typ, err, tb = sys.exc_info()

178 frames = (tuple(fr) for fr in traceback.extract_tb(tb.tb_next))

179 frames = find_cog_source(frames, prologue)

180 msg = "".join(traceback.format_list(frames))

181 msg += "{}: {}".format(typ.__name__, err)

182 raise CogUserException(msg)

183 finally:

184 sys.stdout = real_stdout

185

186 if self.options.bPrintOutput: 186 ↛ 187line 186 didn't jump to line 187, because the condition on line 186 was never true

187 self.outstring = captured_stdout.getvalue()

188

189 # We need to make sure that the last line in the output

190 # ends with a newline, or it will be joined to the

191 # end-output line, ruining cog's idempotency.

192 if self.outstring and self.outstring[-1] != '\n':

193 self.outstring += '\n'

194

195 return reindentBlock(self.outstring, prefOut)

196

197 def msg(self, s):

198 self.prout("Message: "+s)

199

200 def out(self, sOut='', dedent=False, trimblanklines=False):

201 """ The cog.out function.

202 """

203 if trimblanklines and ('\n' in sOut):

204 lines = sOut.split('\n')

205 if lines[0].strip() == '':

206 del lines[0]

207 if lines and lines[-1].strip() == '':

208 del lines[-1]

209 sOut = '\n'.join(lines)+'\n'

210 if dedent:

211 sOut = reindentBlock(sOut)

212 self.outstring += sOut

213

214 def outl(self, sOut='', **kw):

215 """ The cog.outl function.

216 """

217 self.out(sOut, **kw)

218 self.out('\n')

219

220 def error(self, msg='Error raised by cog generator.'):

221 """ The cog.error function.

222 Instead of raising standard python errors, cog generators can use

223 this function. It will display the error without a scary Python

224 traceback.

225 """

226 raise CogGeneratedError(msg)

227

228

229 class NumberedFileReader:

230 """ A decorator for files that counts the readline()'s called.

231 """

232 def __init__(self, f):

233 self.f = f

234 self.n = 0

235

236 def readline(self):

237 l = self.f.readline()

238 if l:

239 self.n += 1

240 return l

241

242 def linenumber(self):

243 return self.n

244

245

246 class CogOptions:

247 """ Options for a run of cog.

248 """

249 def __init__(self):

250 # Defaults for argument values.

251 self.args = []

252 self.includePath = []

253 self.defines = {}

254 self.bShowVersion = False

255 self.sMakeWritableCmd = None

256 self.bReplace = False

257 self.bNoGenerate = False

258 self.sOutputName = None

259 self.bWarnEmpty = False

260 self.bHashOutput = False

261 self.bDeleteCode = False

262 self.bEofCanBeEnd = False

263 self.sSuffix = None

264 self.bNewlines = False

265 self.sBeginSpec = '[[[cog'

266 self.sEndSpec = ']]]'

267 self.sEndOutput = '[[[end]]]'

268 self.sEncoding = "utf-8"

269 self.verbosity = 2

270 self.sPrologue = ''

271 self.bPrintOutput = False

272

273 def __eq__(self, other):

274 """ Comparison operator for tests to use.

275 """

276 return self.__dict__ == other.__dict__

277

278 def clone(self):

279 """ Make a clone of these options, for further refinement.

280 """

281 return copy.deepcopy(self)

282

283 def addToIncludePath(self, dirs):

284 """ Add directories to the include path.

285 """

286 dirs = dirs.split(os.pathsep)

287 self.includePath.extend(dirs)

288

289 def parseArgs(self, argv):

290 # Parse the command line arguments.

291 try:

292 opts, self.args = getopt.getopt(

293 argv,

294 'cdD:eI:n:o:rs:p:PUvw:xz',

295 [

296 'markers=',

297 'verbosity=',

298 ]

299 )

300 except getopt.error as msg:

301 raise CogUsageError(msg)

302

303 # Handle the command line arguments.

304 for o, a in opts:

305 if o == '-c':

306 self.bHashOutput = True

307 elif o == '-d':

308 self.bDeleteCode = True

309 elif o == '-D':

310 if a.count('=') < 1:

311 raise CogUsageError("-D takes a name=value argument")

312 name, value = a.split('=', 1)

313 self.defines[name] = value

314 elif o == '-e':

315 self.bWarnEmpty = True

316 elif o == '-I':

317 self.addToIncludePath(os.path.abspath(a))

318 elif o == '-n':

319 self.sEncoding = a

320 elif o == '-o':

321 self.sOutputName = a

322 elif o == '-r':

323 self.bReplace = True

324 elif o == '-s':

325 self.sSuffix = a

326 elif o == '-p':

327 self.sPrologue = a

328 elif o == '-P':

329 self.bPrintOutput = True

330 elif o == '-U':

331 self.bNewlines = True

332 elif o == '-v':

333 self.bShowVersion = True

334 elif o == '-w':

335 self.sMakeWritableCmd = a

336 elif o == '-x':

337 self.bNoGenerate = True

338 elif o == '-z':

339 self.bEofCanBeEnd = True

340 elif o == '--markers':

341 self._parse_markers(a)

342 elif o == '--verbosity':

343 self.verbosity = int(a)

344 else:

345 # Since getopt.getopt is given a list of possible flags,

346 # this is an internal error.

347 raise CogInternalError("Don't understand argument %s" % o)

348

349 def _parse_markers(self, val):

350 try:

351 self.sBeginSpec, self.sEndSpec, self.sEndOutput = val.split(' ')

352 except ValueError:

353 raise CogUsageError(

354 '--markers requires 3 values separated by spaces, could not parse %r' % val

355 )

356

357 def validate(self):

358 """ Does nothing if everything is OK, raises CogError's if it's not.

359 """

360 if self.bReplace and self.bDeleteCode:

361 raise CogUsageError("Can't use -d with -r (or you would delete all your source!)")

362

363 if self.bReplace and self.sOutputName:

364 raise CogUsageError("Can't use -o with -r (they are opposites)")

365

366

367 class Cog(Redirectable):

368 """ The Cog engine.

369 """

370 def __init__(self):

371 Redirectable.__init__(self)

372 self.options = CogOptions()

373 self._fixEndOutputPatterns()

374 self.cogmodulename = "cog"

375 self.createCogModule()

376

377 def _fixEndOutputPatterns(self):

378 end_output = re.escape(self.options.sEndOutput)

379 self.reEndOutput = re.compile(end_output + r'(?P<hashsect> *\(checksum: (?P<hash>[a-f0-9]+)\))')

380 self.sEndFormat = self.options.sEndOutput + ' (checksum: %s)'

381

382 def showWarning(self, msg):

383 self.prout("Warning: "+msg)

384

385 def isBeginSpecLine(self, s):

386 return self.options.sBeginSpec in s

387

388 def isEndSpecLine(self, s):

389 return self.options.sEndSpec in s and not self.isEndOutputLine(s)

390

391 def isEndOutputLine(self, s):

392 return self.options.sEndOutput in s

393

394 def createCogModule(self):

395 """ Make a cog "module" object so that imported Python modules

396 can say "import cog" and get our state.

397 """

398 class DummyModule(object):

399 """Modules don't have to be anything special, just an object will do."""

400 pass

401 self.cogmodule = DummyModule()

402 self.cogmodule.path = []

403

404 def openOutputFile(self, fname):

405 """ Open an output file, taking all the details into account.

406 """

407 opts = {}

408 mode = "w"

409 if PY3:

410 opts['encoding'] = self.options.sEncoding

411 if self.options.bNewlines:

412 if PY3:

413 opts['newline'] = "\n"

414 else:

415 mode = "wb"

416 fdir = os.path.dirname(fname)

417 if os.path.dirname(fdir) and not os.path.exists(fdir):

418 os.makedirs(fdir)

419 return open(fname, mode, **opts)

420

421 def openInputFile(self, fname):

422 """ Open an input file. """

423 if fname == "-":

424 return sys.stdin

425 else:

426 opts = {}

427 if PY3:

428 opts['encoding'] = self.options.sEncoding

429 return open(fname, "r", **opts)

430

431 def processFile(self, fIn, fOut, fname=None, globals=None):

432 """ Process an input file object to an output file object.

433 fIn and fOut can be file objects, or file names.

434 """

435

436 sFileIn = fname or ''

437 sFileOut = fname or ''

438 fInToClose = fOutToClose = None

439 # Convert filenames to files.

440 if isinstance(fIn, string_types): 440 ↛ 442line 440 didn't jump to line 442, because the condition on line 440 was never true

441 # Open the input file.

442 sFileIn = fIn

443 fIn = fInToClose = self.openInputFile(fIn)

444 if isinstance(fOut, string_types): 444 ↛ 446line 444 didn't jump to line 446, because the condition on line 444 was never true

445 # Open the output file.

446 sFileOut = fOut

447 fOut = fOutToClose = self.openOutputFile(fOut)

448

449 try:

450 fIn = NumberedFileReader(fIn)

451

452 bSawCog = False

453

454 self.cogmodule.inFile = sFileIn

455 self.cogmodule.outFile = sFileOut

456 self.cogmodulename = 'cog_' + hashlib.md5(sFileOut.encode()).hexdigest()

457 sys.modules[self.cogmodulename] = self.cogmodule

458 # if "import cog" explicitly done in code by user, note threading will cause clashes.

459 sys.modules['cog'] = self.cogmodule

460

461 # The globals dict we'll use for this file.

462 if globals is None: 462 ↛ 466line 462 didn't jump to line 466, because the condition on line 462 was never false

463 globals = {}

464

465 # If there are any global defines, put them in the globals.

466 globals.update(self.options.defines)

467

468 # loop over generator chunks

469 l = fIn.readline()

470 while l:

471 # Find the next spec begin

472 while l and not self.isBeginSpecLine(l):

473 if self.isEndSpecLine(l): 473 ↛ 474line 473 didn't jump to line 474, because the condition on line 473 was never true

474 raise CogError("Unexpected '%s'" % self.options.sEndSpec,

475 file=sFileIn, line=fIn.linenumber())

476 if self.isEndOutputLine(l): 476 ↛ 477line 476 didn't jump to line 477, because the condition on line 476 was never true

477 raise CogError("Unexpected '%s'" % self.options.sEndOutput,

478 file=sFileIn, line=fIn.linenumber())

479 fOut.write(l)

480 l = fIn.readline()

481 if not l:

482 break

483 if not self.options.bDeleteCode: 483 ↛ 487line 483 didn't jump to line 487, because the condition on line 483 was never false

484 fOut.write(l)

485

486 # l is the begin spec

487 gen = CogGenerator(options=self.options)

488 gen.setOutput(stdout=self.stdout)

489 gen.parseMarker(l)

490 firstLineNum = fIn.linenumber()

491 self.cogmodule.firstLineNum = firstLineNum

492

493 # If the spec begin is also a spec end, then process the single

494 # line of code inside.

495 if self.isEndSpecLine(l):

496 beg = l.find(self.options.sBeginSpec)

497 end = l.find(self.options.sEndSpec)

498 if beg > end:

499 raise CogError("Cog code markers inverted",

500 file=sFileIn, line=firstLineNum)

501 else:

502 sCode = l[beg+len(self.options.sBeginSpec):end].strip()

503 gen.parseLine(sCode)

504 else:

505 # Deal with an ordinary code block.

506 l = fIn.readline()

507

508 # Get all the lines in the spec

509 while l and not self.isEndSpecLine(l):

510 if self.isBeginSpecLine(l): 510 ↛ 511line 510 didn't jump to line 511, because the condition on line 510 was never true

511 raise CogError("Unexpected '%s'" % self.options.sBeginSpec,

512 file=sFileIn, line=fIn.linenumber())

513 if self.isEndOutputLine(l): 513 ↛ 514line 513 didn't jump to line 514, because the condition on line 513 was never true

514 raise CogError("Unexpected '%s'" % self.options.sEndOutput,

515 file=sFileIn, line=fIn.linenumber())

516 if not self.options.bDeleteCode: 516 ↛ 518line 516 didn't jump to line 518, because the condition on line 516 was never false

517 fOut.write(l)

518 gen.parseLine(l)

519 l = fIn.readline()

520 if not l: 520 ↛ 521line 520 didn't jump to line 521, because the condition on line 520 was never true

521 raise CogError(

522 "Cog block begun but never ended.",

523 file=sFileIn, line=firstLineNum)

524

525 if not self.options.bDeleteCode: 525 ↛ 527line 525 didn't jump to line 527, because the condition on line 525 was never false

526 fOut.write(l)

527 gen.parseMarker(l)

528

529 l = fIn.readline()

530

531 # Eat all the lines in the output section. While reading past

532 # them, compute the md5 hash of the old output.

533 previous = ""

534 hasher = hashlib.md5()

535 while l and not self.isEndOutputLine(l):

536 if self.isBeginSpecLine(l): 536 ↛ 537line 536 didn't jump to line 537, because the condition on line 536 was never true

537 raise CogError("Unexpected '%s'" % self.options.sBeginSpec,

538 file=sFileIn, line=fIn.linenumber())

539 if self.isEndSpecLine(l): 539 ↛ 540line 539 didn't jump to line 540, because the condition on line 539 was never true

540 raise CogError("Unexpected '%s'" % self.options.sEndSpec,

541 file=sFileIn, line=fIn.linenumber())

542 previous += l

543 hasher.update(to_bytes(l))

544 l = fIn.readline()

545 curHash = hasher.hexdigest()

546

547 if not l and not self.options.bEofCanBeEnd: 547 ↛ 549line 547 didn't jump to line 549, because the condition on line 547 was never true

548 # We reached end of file before we found the end output line.

549 raise CogError("Missing '%s' before end of file." % self.options.sEndOutput,

550 file=sFileIn, line=fIn.linenumber())

551

552 # Make the previous output available to the current code

553 self.cogmodule.previous = previous

554

555 # Write the output of the spec to be the new output if we're

556 # supposed to generate code.

557 hasher = hashlib.md5()

558 if not self.options.bNoGenerate: 558 ↛ 564line 558 didn't jump to line 564, because the condition on line 558 was never false

559 sFile = "<cog %s:%d>" % (sFileIn, firstLineNum)

560 sGen = gen.evaluate(cog=self, globals=globals, fname=sFile)

561 sGen = self.suffixLines(sGen)

562 hasher.update(to_bytes(sGen))

563 fOut.write(sGen)

564 newHash = hasher.hexdigest()

565

566 bSawCog = True

567

568 # Write the ending output line

569 hashMatch = self.reEndOutput.search(l)

570 if self.options.bHashOutput: 570 ↛ 571line 570 didn't jump to line 571, because the condition on line 570 was never true

571 if hashMatch:

572 oldHash = hashMatch.groupdict()['hash']

573 if oldHash != curHash:

574 raise CogError("Output has been edited! Delete old checksum to unprotect.",

575 file=sFileIn, line=fIn.linenumber())

576 # Create a new end line with the correct hash.

577 endpieces = l.split(hashMatch.group(0), 1)

578 else:

579 # There was no old hash, but we want a new hash.

580 endpieces = l.split(self.options.sEndOutput, 1)

581 l = (self.sEndFormat % newHash).join(endpieces)

582 else:

583 # We don't want hashes output, so if there was one, get rid of

584 # it.

585 if hashMatch: 585 ↛ 586line 585 didn't jump to line 586, because the condition on line 585 was never true

586 l = l.replace(hashMatch.groupdict()['hashsect'], '', 1)

587

588 if not self.options.bDeleteCode: 588 ↛ 590line 588 didn't jump to line 590, because the condition on line 588 was never false

589 fOut.write(l)

590 l = fIn.readline()

591

592 if not bSawCog and self.options.bWarnEmpty: 592 ↛ 593line 592 didn't jump to line 593, because the condition on line 592 was never true

593 self.showWarning("no cog code found in %s" % sFileIn)

594 finally:

595 if fInToClose: 595 ↛ 596line 595 didn't jump to line 596, because the condition on line 595 was never true

596 fInToClose.close()

597 if fOutToClose: 597 ↛ 598line 597 didn't jump to line 598, because the condition on line 597 was never true

598 fOutToClose.close()

599

600

601 # A regex for non-empty lines, used by suffixLines.

602 reNonEmptyLines = re.compile(r"^\s*\S+.*$", re.MULTILINE)

603

604 def suffixLines(self, text):

605 """ Add suffixes to the lines in text, if our options desire it.

606 text is many lines, as a single string.

607 """

608 if self.options.sSuffix: 608 ↛ 610line 608 didn't jump to line 610, because the condition on line 608 was never true

609 # Find all non-blank lines, and add the suffix to the end.

610 repl = r"\g<0>" + self.options.sSuffix.replace('\\', '\\\\')

611 text = self.reNonEmptyLines.sub(repl, text)

612 return text

613

614 def processString(self, sInput, fname=None):

615 """ Process sInput as the text to cog.

616 Return the cogged output as a string.

617 """

618 fOld = StringIO(sInput)

619 fNew = StringIO()

620 self.processFile(fOld, fNew, fname=fname)

621 return fNew.getvalue()

622

623 def replaceFile(self, sOldPath, sNewText):

624 """ Replace file sOldPath with the contents sNewText

625 """

626 if not os.access(sOldPath, os.W_OK):

627 # Need to ensure we can write.

628 if self.options.sMakeWritableCmd:

629 # Use an external command to make the file writable.

630 cmd = self.options.sMakeWritableCmd.replace('%s', sOldPath)

631 self.stdout.write(os.popen(cmd).read())

632 if not os.access(sOldPath, os.W_OK):

633 raise CogError("Couldn't make %s writable" % sOldPath)

634 else:

635 # Can't write!

636 raise CogError("Can't overwrite %s" % sOldPath)

637 f = self.openOutputFile(sOldPath)

638 f.write(sNewText)

639 f.close()

640

641 def saveIncludePath(self):

642 self.savedInclude = self.options.includePath[:]

643 self.savedSysPath = sys.path[:]

644

645 def restoreIncludePath(self):

646 self.options.includePath = self.savedInclude

647 self.cogmodule.path = self.options.includePath

648 sys.path = self.savedSysPath

649

650 def addToIncludePath(self, includePath):

651 self.cogmodule.path.extend(includePath)

652 sys.path.extend(includePath)

653

654 def processOneFile(self, sFile):

655 """ Process one filename through cog.

656 """

657

658 self.saveIncludePath()

659 bNeedNewline = False

660

661 try:

662 self.addToIncludePath(self.options.includePath)

663 # Since we know where the input file came from,

664 # push its directory onto the include path.

665 self.addToIncludePath([os.path.dirname(sFile)])

666

667 # How we process the file depends on where the output is going.

668 if self.options.sOutputName:

669 self.processFile(sFile, self.options.sOutputName, sFile)

670 elif self.options.bReplace:

671 # We want to replace the cog file with the output,

672 # but only if they differ.

673 if self.options.verbosity >= 2:

674 self.prout("Cogging %s" % sFile, end="")

675 bNeedNewline = True

676

677 try:

678 fOldFile = self.openInputFile(sFile)

679 sOldText = fOldFile.read()

680 fOldFile.close()

681 sNewText = self.processString(sOldText, fname=sFile)

682 if sOldText != sNewText:

683 if self.options.verbosity >= 1:

684 if self.options.verbosity < 2:

685 self.prout("Cogging %s" % sFile, end="")

686 self.prout(" (changed)")

687 bNeedNewline = False

688 self.replaceFile(sFile, sNewText)

689 finally:

690 # The try-finally block is so we can print a partial line

691 # with the name of the file, and print (changed) on the

692 # same line, but also make sure to break the line before

693 # any traceback.

694 if bNeedNewline:

695 self.prout("")

696 else:

697 self.processFile(sFile, self.stdout, sFile)

698 finally:

699 self.restoreIncludePath()

700

701 def processWildcards(self, sFile):

702 files = glob.glob(sFile)

703 if files:

704 for sMatchingFile in files:

705 self.processOneFile(sMatchingFile)

706 else:

707 self.processOneFile(sFile)

708

709 def processFileList(self, sFileList):

710 """ Process the files in a file list.

711 """

712 flist = self.openInputFile(sFileList)

713 lines = flist.readlines()

714 flist.close()

715 for l in lines:

716 # Use shlex to parse the line like a shell.

717 lex = shlex.shlex(l, posix=True)

718 lex.whitespace_split = True

719 lex.commenters = '#'

720 # No escapes, so that backslash can be part of the path

721 lex.escape = ''

722 args = list(lex)

723 if args:

724 self.processArguments(args)

725

726 def processArguments(self, args):

727 """ Process one command-line.

728 """

729 saved_options = self.options

730 self.options = self.options.clone()

731

732 self.options.parseArgs(args[1:])

733 self.options.validate()

734

735 if args[0][0] == '@':

736 if self.options.sOutputName:

737 raise CogUsageError("Can't use -o with @file")

738 self.processFileList(args[0][1:])

739 else:

740 self.processWildcards(args[0])

741

742 self.options = saved_options

743

744 def callableMain(self, argv):

745 """ All of command-line cog, but in a callable form.

746 This is used by main.

747 argv is the equivalent of sys.argv.

748 """

749 argv = argv[1:]

750

751 # Provide help if asked for anywhere in the command line.

752 if '-?' in argv or '-h' in argv:

753 self.prerr(usage, end="")

754 return

755

756 self.options.parseArgs(argv)

757 self.options.validate()

758 self._fixEndOutputPatterns()

759

760 if self.options.bShowVersion:

761 self.prout("Cog version %s" % __version__)

762 return

763

764 if self.options.args:

765 for a in self.options.args:

766 self.processArguments([a])

767 else:

768 raise CogUsageError("No files to process")

769

770 def main(self, argv):

771 """ Handle the command-line execution for cog.

772 """

773

774 try:

775 self.callableMain(argv)

776 return 0

777 except CogUsageError as err:

778 self.prerr(err)

779 self.prerr("(for help use -?)")

780 return 2

781 except CogGeneratedError as err:

782 self.prerr("Error: %s" % err)

783 return 3

784 except CogUserException as err:

785 self.prerr("Traceback (most recent call last):")

786 self.prerr(err.args[0])

787 return 4

788 except CogError as err:

789 self.prerr(err)

790 return 1

791

792

793 def find_cog_source(frame_summary, prologue):

794 """Find cog source lines in a frame summary list, for printing tracebacks.

795

796 Arguments:

797 frame_summary: a list of 4-item tuples, as returned by traceback.extract_tb.

798 prologue: the text of the code prologue.

799

800 Returns

801 A list of 4-item tuples, updated to correct the cog entries.

802

803 """

804 prolines = prologue.splitlines()

805 for filename, lineno, funcname, source in frame_summary:

806 if not source: 806 ↛ 818line 806 didn't jump to line 818, because the condition on line 806 was never false

807 m = re.search(r"^<cog ([^:]+):(\d+)>$", filename)

808 if m: 808 ↛ 809line 808 didn't jump to line 809, because the condition on line 808 was never true

809 if lineno <= len(prolines):

810 filename = '<prologue>'

811 source = prolines[lineno-1]

812 lineno -= 1 # Because "import cog" is the first line in the prologue

813 else:

814 filename, coglineno = m.groups()

815 coglineno = int(coglineno)

816 lineno += coglineno - len(prolines)

817 source = linecache.getline(filename, lineno).strip()

818 yield filename, lineno, funcname, source

819

820

821 def main():

822 """Main function for entry_points to use."""

823 return Cog().main(sys.argv)