test_api.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803
  1. """
  2. Tests for L{pyflakes.scripts.pyflakes}.
  3. """
  4. import contextlib
  5. import io
  6. import os
  7. import sys
  8. import shutil
  9. import subprocess
  10. import tempfile
  11. from pyflakes.checker import PYPY
  12. from pyflakes.messages import UnusedImport
  13. from pyflakes.reporter import Reporter
  14. from pyflakes.api import (
  15. main,
  16. check,
  17. checkPath,
  18. checkRecursive,
  19. iterSourceCode,
  20. )
  21. from pyflakes.test.harness import TestCase, skipIf
  22. def withStderrTo(stderr, f, *args, **kwargs):
  23. """
  24. Call C{f} with C{sys.stderr} redirected to C{stderr}.
  25. """
  26. (outer, sys.stderr) = (sys.stderr, stderr)
  27. try:
  28. return f(*args, **kwargs)
  29. finally:
  30. sys.stderr = outer
  31. class Node:
  32. """
  33. Mock an AST node.
  34. """
  35. def __init__(self, lineno, col_offset=0):
  36. self.lineno = lineno
  37. self.col_offset = col_offset
  38. class SysStreamCapturing:
  39. """Context manager capturing sys.stdin, sys.stdout and sys.stderr.
  40. The file handles are replaced with a StringIO object.
  41. """
  42. def __init__(self, stdin):
  43. self._stdin = io.StringIO(stdin or '', newline=os.linesep)
  44. def __enter__(self):
  45. self._orig_stdin = sys.stdin
  46. self._orig_stdout = sys.stdout
  47. self._orig_stderr = sys.stderr
  48. sys.stdin = self._stdin
  49. sys.stdout = self._stdout_stringio = io.StringIO(newline=os.linesep)
  50. sys.stderr = self._stderr_stringio = io.StringIO(newline=os.linesep)
  51. return self
  52. def __exit__(self, *args):
  53. self.output = self._stdout_stringio.getvalue()
  54. self.error = self._stderr_stringio.getvalue()
  55. sys.stdin = self._orig_stdin
  56. sys.stdout = self._orig_stdout
  57. sys.stderr = self._orig_stderr
  58. class LoggingReporter:
  59. """
  60. Implementation of Reporter that just appends any error to a list.
  61. """
  62. def __init__(self, log):
  63. """
  64. Construct a C{LoggingReporter}.
  65. @param log: A list to append log messages to.
  66. """
  67. self.log = log
  68. def flake(self, message):
  69. self.log.append(('flake', str(message)))
  70. def unexpectedError(self, filename, message):
  71. self.log.append(('unexpectedError', filename, message))
  72. def syntaxError(self, filename, msg, lineno, offset, line):
  73. self.log.append(('syntaxError', filename, msg, lineno, offset, line))
  74. class TestIterSourceCode(TestCase):
  75. """
  76. Tests for L{iterSourceCode}.
  77. """
  78. def setUp(self):
  79. self.tempdir = tempfile.mkdtemp()
  80. def tearDown(self):
  81. shutil.rmtree(self.tempdir)
  82. def makeEmptyFile(self, *parts):
  83. assert parts
  84. fpath = os.path.join(self.tempdir, *parts)
  85. open(fpath, 'a').close()
  86. return fpath
  87. def test_emptyDirectory(self):
  88. """
  89. There are no Python files in an empty directory.
  90. """
  91. self.assertEqual(list(iterSourceCode([self.tempdir])), [])
  92. def test_singleFile(self):
  93. """
  94. If the directory contains one Python file, C{iterSourceCode} will find
  95. it.
  96. """
  97. childpath = self.makeEmptyFile('foo.py')
  98. self.assertEqual(list(iterSourceCode([self.tempdir])), [childpath])
  99. def test_onlyPythonSource(self):
  100. """
  101. Files that are not Python source files are not included.
  102. """
  103. self.makeEmptyFile('foo.pyc')
  104. self.assertEqual(list(iterSourceCode([self.tempdir])), [])
  105. def test_recurses(self):
  106. """
  107. If the Python files are hidden deep down in child directories, we will
  108. find them.
  109. """
  110. os.mkdir(os.path.join(self.tempdir, 'foo'))
  111. apath = self.makeEmptyFile('foo', 'a.py')
  112. self.makeEmptyFile('foo', 'a.py~')
  113. os.mkdir(os.path.join(self.tempdir, 'bar'))
  114. bpath = self.makeEmptyFile('bar', 'b.py')
  115. cpath = self.makeEmptyFile('c.py')
  116. self.assertEqual(
  117. sorted(iterSourceCode([self.tempdir])),
  118. sorted([apath, bpath, cpath]))
  119. def test_shebang(self):
  120. """
  121. Find Python files that don't end with `.py`, but contain a Python
  122. shebang.
  123. """
  124. python = os.path.join(self.tempdir, 'a')
  125. with open(python, 'w') as fd:
  126. fd.write('#!/usr/bin/env python\n')
  127. self.makeEmptyFile('b')
  128. with open(os.path.join(self.tempdir, 'c'), 'w') as fd:
  129. fd.write('hello\nworld\n')
  130. python3 = os.path.join(self.tempdir, 'e')
  131. with open(python3, 'w') as fd:
  132. fd.write('#!/usr/bin/env python3\n')
  133. pythonw = os.path.join(self.tempdir, 'f')
  134. with open(pythonw, 'w') as fd:
  135. fd.write('#!/usr/bin/env pythonw\n')
  136. python3args = os.path.join(self.tempdir, 'g')
  137. with open(python3args, 'w') as fd:
  138. fd.write('#!/usr/bin/python3 -u\n')
  139. python3d = os.path.join(self.tempdir, 'i')
  140. with open(python3d, 'w') as fd:
  141. fd.write('#!/usr/local/bin/python3d\n')
  142. python38m = os.path.join(self.tempdir, 'j')
  143. with open(python38m, 'w') as fd:
  144. fd.write('#! /usr/bin/env python3.8m\n')
  145. # Should NOT be treated as Python source
  146. notfirst = os.path.join(self.tempdir, 'l')
  147. with open(notfirst, 'w') as fd:
  148. fd.write('#!/bin/sh\n#!/usr/bin/python\n')
  149. self.assertEqual(
  150. sorted(iterSourceCode([self.tempdir])),
  151. sorted([
  152. python, python3, pythonw, python3args, python3d,
  153. python38m,
  154. ]))
  155. def test_multipleDirectories(self):
  156. """
  157. L{iterSourceCode} can be given multiple directories. It will recurse
  158. into each of them.
  159. """
  160. foopath = os.path.join(self.tempdir, 'foo')
  161. barpath = os.path.join(self.tempdir, 'bar')
  162. os.mkdir(foopath)
  163. apath = self.makeEmptyFile('foo', 'a.py')
  164. os.mkdir(barpath)
  165. bpath = self.makeEmptyFile('bar', 'b.py')
  166. self.assertEqual(
  167. sorted(iterSourceCode([foopath, barpath])),
  168. sorted([apath, bpath]))
  169. def test_explicitFiles(self):
  170. """
  171. If one of the paths given to L{iterSourceCode} is not a directory but
  172. a file, it will include that in its output.
  173. """
  174. epath = self.makeEmptyFile('e.py')
  175. self.assertEqual(list(iterSourceCode([epath])),
  176. [epath])
  177. class TestReporter(TestCase):
  178. """
  179. Tests for L{Reporter}.
  180. """
  181. def test_syntaxError(self):
  182. """
  183. C{syntaxError} reports that there was a syntax error in the source
  184. file. It reports to the error stream and includes the filename, line
  185. number, error message, actual line of source and a caret pointing to
  186. where the error is.
  187. """
  188. err = io.StringIO()
  189. reporter = Reporter(None, err)
  190. reporter.syntaxError('foo.py', 'a problem', 3, 8, 'bad line of source')
  191. self.assertEqual(
  192. ("foo.py:3:8: a problem\n"
  193. "bad line of source\n"
  194. " ^\n"),
  195. err.getvalue())
  196. def test_syntaxErrorNoOffset(self):
  197. """
  198. C{syntaxError} doesn't include a caret pointing to the error if
  199. C{offset} is passed as C{None}.
  200. """
  201. err = io.StringIO()
  202. reporter = Reporter(None, err)
  203. reporter.syntaxError('foo.py', 'a problem', 3, None,
  204. 'bad line of source')
  205. self.assertEqual(
  206. ("foo.py:3: a problem\n"
  207. "bad line of source\n"),
  208. err.getvalue())
  209. def test_syntaxErrorNoText(self):
  210. """
  211. C{syntaxError} doesn't include text or nonsensical offsets if C{text} is C{None}.
  212. This typically happens when reporting syntax errors from stdin.
  213. """
  214. err = io.StringIO()
  215. reporter = Reporter(None, err)
  216. reporter.syntaxError('<stdin>', 'a problem', 0, 0, None)
  217. self.assertEqual(("<stdin>:1:1: a problem\n"), err.getvalue())
  218. def test_multiLineSyntaxError(self):
  219. """
  220. If there's a multi-line syntax error, then we only report the last
  221. line. The offset is adjusted so that it is relative to the start of
  222. the last line.
  223. """
  224. err = io.StringIO()
  225. lines = [
  226. 'bad line of source',
  227. 'more bad lines of source',
  228. ]
  229. reporter = Reporter(None, err)
  230. reporter.syntaxError('foo.py', 'a problem', 3, len(lines[0]) + 7,
  231. '\n'.join(lines))
  232. self.assertEqual(
  233. ("foo.py:3:25: a problem\n" +
  234. lines[-1] + "\n" +
  235. " " * 24 + "^\n"),
  236. err.getvalue())
  237. def test_unexpectedError(self):
  238. """
  239. C{unexpectedError} reports an error processing a source file.
  240. """
  241. err = io.StringIO()
  242. reporter = Reporter(None, err)
  243. reporter.unexpectedError('source.py', 'error message')
  244. self.assertEqual('source.py: error message\n', err.getvalue())
  245. def test_flake(self):
  246. """
  247. C{flake} reports a code warning from Pyflakes. It is exactly the
  248. str() of a L{pyflakes.messages.Message}.
  249. """
  250. out = io.StringIO()
  251. reporter = Reporter(out, None)
  252. message = UnusedImport('foo.py', Node(42), 'bar')
  253. reporter.flake(message)
  254. self.assertEqual(out.getvalue(), f"{message}\n")
  255. class CheckTests(TestCase):
  256. """
  257. Tests for L{check} and L{checkPath} which check a file for flakes.
  258. """
  259. @contextlib.contextmanager
  260. def makeTempFile(self, content):
  261. """
  262. Make a temporary file containing C{content} and return a path to it.
  263. """
  264. fd, name = tempfile.mkstemp()
  265. try:
  266. with os.fdopen(fd, 'wb') as f:
  267. if not hasattr(content, 'decode'):
  268. content = content.encode('ascii')
  269. f.write(content)
  270. yield name
  271. finally:
  272. os.remove(name)
  273. def assertHasErrors(self, path, errorList):
  274. """
  275. Assert that C{path} causes errors.
  276. @param path: A path to a file to check.
  277. @param errorList: A list of errors expected to be printed to stderr.
  278. """
  279. err = io.StringIO()
  280. count = withStderrTo(err, checkPath, path)
  281. self.assertEqual(
  282. (count, err.getvalue()), (len(errorList), ''.join(errorList)))
  283. def getErrors(self, path):
  284. """
  285. Get any warnings or errors reported by pyflakes for the file at C{path}.
  286. @param path: The path to a Python file on disk that pyflakes will check.
  287. @return: C{(count, log)}, where C{count} is the number of warnings or
  288. errors generated, and log is a list of those warnings, presented
  289. as structured data. See L{LoggingReporter} for more details.
  290. """
  291. log = []
  292. reporter = LoggingReporter(log)
  293. count = checkPath(path, reporter)
  294. return count, log
  295. def test_legacyScript(self):
  296. from pyflakes.scripts import pyflakes as script_pyflakes
  297. self.assertIs(script_pyflakes.checkPath, checkPath)
  298. def test_missingTrailingNewline(self):
  299. """
  300. Source which doesn't end with a newline shouldn't cause any
  301. exception to be raised nor an error indicator to be returned by
  302. L{check}.
  303. """
  304. with self.makeTempFile("def foo():\n\tpass\n\t") as fName:
  305. self.assertHasErrors(fName, [])
  306. def test_checkPathNonExisting(self):
  307. """
  308. L{checkPath} handles non-existing files.
  309. """
  310. count, errors = self.getErrors('extremo')
  311. self.assertEqual(count, 1)
  312. self.assertEqual(
  313. errors,
  314. [('unexpectedError', 'extremo', 'No such file or directory')])
  315. def test_multilineSyntaxError(self):
  316. """
  317. Source which includes a syntax error which results in the raised
  318. L{SyntaxError.text} containing multiple lines of source are reported
  319. with only the last line of that source.
  320. """
  321. source = """\
  322. def foo():
  323. '''
  324. def bar():
  325. pass
  326. def baz():
  327. '''quux'''
  328. """
  329. # Sanity check - SyntaxError.text should be multiple lines, if it
  330. # isn't, something this test was unprepared for has happened.
  331. def evaluate(source):
  332. exec(source)
  333. try:
  334. evaluate(source)
  335. except SyntaxError as e:
  336. if not PYPY and sys.version_info < (3, 10):
  337. self.assertTrue(e.text.count('\n') > 1)
  338. else:
  339. self.fail()
  340. with self.makeTempFile(source) as sourcePath:
  341. if PYPY:
  342. message = 'end of file (EOF) while scanning triple-quoted string literal'
  343. elif sys.version_info >= (3, 10):
  344. message = 'unterminated triple-quoted string literal (detected at line 8)' # noqa: E501
  345. else:
  346. message = 'invalid syntax'
  347. if PYPY or sys.version_info >= (3, 10):
  348. column = 12
  349. else:
  350. column = 8
  351. self.assertHasErrors(
  352. sourcePath,
  353. ["""\
  354. %s:8:%d: %s
  355. '''quux'''
  356. %s^
  357. """ % (sourcePath, column, message, ' ' * (column - 1))])
  358. def test_eofSyntaxError(self):
  359. """
  360. The error reported for source files which end prematurely causing a
  361. syntax error reflects the cause for the syntax error.
  362. """
  363. with self.makeTempFile("def foo(") as sourcePath:
  364. if PYPY:
  365. msg = 'parenthesis is never closed'
  366. elif sys.version_info >= (3, 10):
  367. msg = "'(' was never closed"
  368. else:
  369. msg = 'unexpected EOF while parsing'
  370. if PYPY or sys.version_info >= (3, 10):
  371. column = 8
  372. else:
  373. column = 9
  374. spaces = ' ' * (column - 1)
  375. expected = '{}:1:{}: {}\ndef foo(\n{}^\n'.format(
  376. sourcePath, column, msg, spaces
  377. )
  378. self.assertHasErrors(sourcePath, [expected])
  379. def test_eofSyntaxErrorWithTab(self):
  380. """
  381. The error reported for source files which end prematurely causing a
  382. syntax error reflects the cause for the syntax error.
  383. """
  384. with self.makeTempFile("if True:\n\tfoo =") as sourcePath:
  385. self.assertHasErrors(
  386. sourcePath,
  387. [f"""\
  388. {sourcePath}:2:7: invalid syntax
  389. \tfoo =
  390. \t ^
  391. """])
  392. def test_nonDefaultFollowsDefaultSyntaxError(self):
  393. """
  394. Source which has a non-default argument following a default argument
  395. should include the line number of the syntax error. However these
  396. exceptions do not include an offset.
  397. """
  398. source = """\
  399. def foo(bar=baz, bax):
  400. pass
  401. """
  402. with self.makeTempFile(source) as sourcePath:
  403. if sys.version_info >= (3, 12):
  404. msg = 'parameter without a default follows parameter with a default' # noqa: E501
  405. else:
  406. msg = 'non-default argument follows default argument'
  407. if PYPY and sys.version_info >= (3, 9):
  408. column = 18
  409. elif PYPY:
  410. column = 8
  411. elif sys.version_info >= (3, 10):
  412. column = 18
  413. elif sys.version_info >= (3, 9):
  414. column = 21
  415. else:
  416. column = 9
  417. last_line = ' ' * (column - 1) + '^\n'
  418. self.assertHasErrors(
  419. sourcePath,
  420. [f"""\
  421. {sourcePath}:1:{column}: {msg}
  422. def foo(bar=baz, bax):
  423. {last_line}"""]
  424. )
  425. def test_nonKeywordAfterKeywordSyntaxError(self):
  426. """
  427. Source which has a non-keyword argument after a keyword argument should
  428. include the line number of the syntax error. However these exceptions
  429. do not include an offset.
  430. """
  431. source = """\
  432. foo(bar=baz, bax)
  433. """
  434. with self.makeTempFile(source) as sourcePath:
  435. if sys.version_info >= (3, 9):
  436. column = 17
  437. elif not PYPY:
  438. column = 14
  439. else:
  440. column = 13
  441. last_line = ' ' * (column - 1) + '^\n'
  442. columnstr = '%d:' % column
  443. message = 'positional argument follows keyword argument'
  444. self.assertHasErrors(
  445. sourcePath,
  446. ["""\
  447. {}:1:{} {}
  448. foo(bar=baz, bax)
  449. {}""".format(sourcePath, columnstr, message, last_line)])
  450. def test_invalidEscape(self):
  451. """
  452. The invalid escape syntax raises ValueError in Python 2
  453. """
  454. # ValueError: invalid \x escape
  455. with self.makeTempFile(r"foo = '\xyz'") as sourcePath:
  456. position_end = 1
  457. if PYPY and sys.version_info >= (3, 9):
  458. column = 7
  459. elif PYPY:
  460. column = 6
  461. elif (3, 9) <= sys.version_info < (3, 12):
  462. column = 13
  463. else:
  464. column = 7
  465. last_line = '%s^\n' % (' ' * (column - 1))
  466. decoding_error = """\
  467. %s:1:%d: (unicode error) 'unicodeescape' codec can't decode bytes \
  468. in position 0-%d: truncated \\xXX escape
  469. foo = '\\xyz'
  470. %s""" % (sourcePath, column, position_end, last_line)
  471. self.assertHasErrors(
  472. sourcePath, [decoding_error])
  473. @skipIf(sys.platform == 'win32', 'unsupported on Windows')
  474. def test_permissionDenied(self):
  475. """
  476. If the source file is not readable, this is reported on standard
  477. error.
  478. """
  479. if os.getuid() == 0:
  480. self.skipTest('root user can access all files regardless of '
  481. 'permissions')
  482. with self.makeTempFile('') as sourcePath:
  483. os.chmod(sourcePath, 0)
  484. count, errors = self.getErrors(sourcePath)
  485. self.assertEqual(count, 1)
  486. self.assertEqual(
  487. errors,
  488. [('unexpectedError', sourcePath, "Permission denied")])
  489. def test_pyflakesWarning(self):
  490. """
  491. If the source file has a pyflakes warning, this is reported as a
  492. 'flake'.
  493. """
  494. with self.makeTempFile("import foo") as sourcePath:
  495. count, errors = self.getErrors(sourcePath)
  496. self.assertEqual(count, 1)
  497. self.assertEqual(
  498. errors, [('flake', str(UnusedImport(sourcePath, Node(1), 'foo')))])
  499. def test_encodedFileUTF8(self):
  500. """
  501. If source file declares the correct encoding, no error is reported.
  502. """
  503. SNOWMAN = chr(0x2603)
  504. source = ("""\
  505. # coding: utf-8
  506. x = "%s"
  507. """ % SNOWMAN).encode('utf-8')
  508. with self.makeTempFile(source) as sourcePath:
  509. self.assertHasErrors(sourcePath, [])
  510. def test_CRLFLineEndings(self):
  511. """
  512. Source files with Windows CR LF line endings are parsed successfully.
  513. """
  514. with self.makeTempFile("x = 42\r\n") as sourcePath:
  515. self.assertHasErrors(sourcePath, [])
  516. def test_misencodedFileUTF8(self):
  517. """
  518. If a source file contains bytes which cannot be decoded, this is
  519. reported on stderr.
  520. """
  521. SNOWMAN = chr(0x2603)
  522. source = ("""\
  523. # coding: ascii
  524. x = "%s"
  525. """ % SNOWMAN).encode('utf-8')
  526. with self.makeTempFile(source) as sourcePath:
  527. self.assertHasErrors(
  528. sourcePath,
  529. [f"{sourcePath}:1:1: 'ascii' codec can't decode byte 0xe2 in position 21: ordinal not in range(128)\n"]) # noqa: E501
  530. def test_misencodedFileUTF16(self):
  531. """
  532. If a source file contains bytes which cannot be decoded, this is
  533. reported on stderr.
  534. """
  535. SNOWMAN = chr(0x2603)
  536. source = ("""\
  537. # coding: ascii
  538. x = "%s"
  539. """ % SNOWMAN).encode('utf-16')
  540. with self.makeTempFile(source) as sourcePath:
  541. if sys.version_info < (3, 11, 4):
  542. expected = f"{sourcePath}: problem decoding source\n"
  543. else:
  544. expected = f"{sourcePath}:1: source code string cannot contain null bytes\n" # noqa: E501
  545. self.assertHasErrors(sourcePath, [expected])
  546. def test_checkRecursive(self):
  547. """
  548. L{checkRecursive} descends into each directory, finding Python files
  549. and reporting problems.
  550. """
  551. tempdir = tempfile.mkdtemp()
  552. try:
  553. os.mkdir(os.path.join(tempdir, 'foo'))
  554. file1 = os.path.join(tempdir, 'foo', 'bar.py')
  555. with open(file1, 'wb') as fd:
  556. fd.write(b"import baz\n")
  557. file2 = os.path.join(tempdir, 'baz.py')
  558. with open(file2, 'wb') as fd:
  559. fd.write(b"import contraband")
  560. log = []
  561. reporter = LoggingReporter(log)
  562. warnings = checkRecursive([tempdir], reporter)
  563. self.assertEqual(warnings, 2)
  564. self.assertEqual(
  565. sorted(log),
  566. sorted([('flake', str(UnusedImport(file1, Node(1), 'baz'))),
  567. ('flake',
  568. str(UnusedImport(file2, Node(1), 'contraband')))]))
  569. finally:
  570. shutil.rmtree(tempdir)
  571. def test_stdinReportsErrors(self):
  572. """
  573. L{check} reports syntax errors from stdin
  574. """
  575. source = "max(1 for i in range(10), key=lambda x: x+1)\n"
  576. err = io.StringIO()
  577. count = withStderrTo(err, check, source, "<stdin>")
  578. self.assertEqual(count, 1)
  579. errlines = err.getvalue().split("\n")[:-1]
  580. if sys.version_info >= (3, 9):
  581. expected_error = [
  582. "<stdin>:1:5: Generator expression must be parenthesized",
  583. "max(1 for i in range(10), key=lambda x: x+1)",
  584. " ^",
  585. ]
  586. elif PYPY:
  587. expected_error = [
  588. "<stdin>:1:4: Generator expression must be parenthesized if not sole argument", # noqa: E501
  589. "max(1 for i in range(10), key=lambda x: x+1)",
  590. " ^",
  591. ]
  592. else:
  593. expected_error = [
  594. "<stdin>:1:5: Generator expression must be parenthesized",
  595. ]
  596. self.assertEqual(errlines, expected_error)
  597. class IntegrationTests(TestCase):
  598. """
  599. Tests of the pyflakes script that actually spawn the script.
  600. """
  601. def setUp(self):
  602. self.tempdir = tempfile.mkdtemp()
  603. self.tempfilepath = os.path.join(self.tempdir, 'temp')
  604. def tearDown(self):
  605. shutil.rmtree(self.tempdir)
  606. def getPyflakesBinary(self):
  607. """
  608. Return the path to the pyflakes binary.
  609. """
  610. import pyflakes
  611. package_dir = os.path.dirname(pyflakes.__file__)
  612. return os.path.join(package_dir, '..', 'bin', 'pyflakes')
  613. def runPyflakes(self, paths, stdin=None):
  614. """
  615. Launch a subprocess running C{pyflakes}.
  616. @param paths: Command-line arguments to pass to pyflakes.
  617. @param stdin: Text to use as stdin.
  618. @return: C{(returncode, stdout, stderr)} of the completed pyflakes
  619. process.
  620. """
  621. env = dict(os.environ)
  622. env['PYTHONPATH'] = os.pathsep.join(sys.path)
  623. command = [sys.executable, self.getPyflakesBinary()]
  624. command.extend(paths)
  625. if stdin:
  626. p = subprocess.Popen(command, env=env, stdin=subprocess.PIPE,
  627. stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  628. (stdout, stderr) = p.communicate(stdin.encode('ascii'))
  629. else:
  630. p = subprocess.Popen(command, env=env,
  631. stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  632. (stdout, stderr) = p.communicate()
  633. rv = p.wait()
  634. stdout = stdout.decode('utf-8')
  635. stderr = stderr.decode('utf-8')
  636. return (stdout, stderr, rv)
  637. def test_goodFile(self):
  638. """
  639. When a Python source file is all good, the return code is zero and no
  640. messages are printed to either stdout or stderr.
  641. """
  642. open(self.tempfilepath, 'a').close()
  643. d = self.runPyflakes([self.tempfilepath])
  644. self.assertEqual(d, ('', '', 0))
  645. def test_fileWithFlakes(self):
  646. """
  647. When a Python source file has warnings, the return code is non-zero
  648. and the warnings are printed to stdout.
  649. """
  650. with open(self.tempfilepath, 'wb') as fd:
  651. fd.write(b"import contraband\n")
  652. d = self.runPyflakes([self.tempfilepath])
  653. expected = UnusedImport(self.tempfilepath, Node(1), 'contraband')
  654. self.assertEqual(d, (f"{expected}{os.linesep}", '', 1))
  655. def test_errors_io(self):
  656. """
  657. When pyflakes finds errors with the files it's given, (if they don't
  658. exist, say), then the return code is non-zero and the errors are
  659. printed to stderr.
  660. """
  661. d = self.runPyflakes([self.tempfilepath])
  662. error_msg = '{}: No such file or directory{}'.format(self.tempfilepath,
  663. os.linesep)
  664. self.assertEqual(d, ('', error_msg, 1))
  665. def test_errors_syntax(self):
  666. """
  667. When pyflakes finds errors with the files it's given, (if they don't
  668. exist, say), then the return code is non-zero and the errors are
  669. printed to stderr.
  670. """
  671. with open(self.tempfilepath, 'wb') as fd:
  672. fd.write(b"import")
  673. d = self.runPyflakes([self.tempfilepath])
  674. error_msg = '{0}:1:7: invalid syntax{1}import{1} ^{1}'.format(
  675. self.tempfilepath, os.linesep)
  676. self.assertEqual(d, ('', error_msg, 1))
  677. def test_readFromStdin(self):
  678. """
  679. If no arguments are passed to C{pyflakes} then it reads from stdin.
  680. """
  681. d = self.runPyflakes([], stdin='import contraband')
  682. expected = UnusedImport('<stdin>', Node(1), 'contraband')
  683. self.assertEqual(d, (f"{expected}{os.linesep}", '', 1))
  684. class TestMain(IntegrationTests):
  685. """
  686. Tests of the pyflakes main function.
  687. """
  688. def runPyflakes(self, paths, stdin=None):
  689. try:
  690. with SysStreamCapturing(stdin) as capture:
  691. main(args=paths)
  692. except SystemExit as e:
  693. self.assertIsInstance(e.code, bool)
  694. rv = int(e.code)
  695. return (capture.output, capture.error, rv)
  696. else:
  697. raise RuntimeError('SystemExit not raised')