A Python lint tool

Running a Lint Check

Each lint tool is called using the run() function from the sub-process module. If the return code is zero, the tool reports no problems and provides output, confirming that the lint tool ran (as opposed to only providing output if there is an error). If the return code indicates a failure, then the lint tool's captured output is reported to the user.

This linting process continues until all seven lint tests are performed on the specified Python source code files. After the lint checks are done, the Py7 tool indicates the end of the lint checks, reports all problems, and asks the user to acknowledge completion.

Instead of output going to a logfile or trace file, all output goes to standard output. If you want to capture the output, you can use the magic of pipes, redirection, and utilities. For example, you can capture output using the tee utility by piping output to a text file and redirecting the standard error to the standard output. In Bash, use:

./py7.py py7.py 2>&1 | tee py7.out.txt

Undoubtedly, there are other means to capture Py7's output. For instance, you could also annotate other information, such as line numbers, via the cat -n' utility.

Putting Py7 to Work

To show Py7 at work, I have run it on Py7 itself, a Python shell, and other Python source code files. First, I tested Py7 on itself by running the tool on the Py7 Python source code file. Doing so allowed me to find and fix several possible issues as I developed, debugged, and optimized the Python code. After completing this test, I was presented with two remaining possible issues found by the Bandit lint tool as shown in Listing 5. Each is an issue with a sub-process call's security. In some instances, this would be significant, but not in the context of the Py7 tool. Here, it is a false positive because Py7 does not modify the Python source code and only invokes other tools via the sub-process. As mentioned earlier, Py7's reported issues are guidelines or suggestions.

Listing 5

Security Issues Found by Bandit

01 def main():
02   print("")
03   version()
04   print("")
05
06   if len(sys.argv) == 2 and sys.argv[1] in ('-h', '--help'):
07     usage()
08     sys.exit(0)
09
10   if len(sys.argv) < 2:
11     print(":: ")
12     print(":: Error: No Python files given to check!")
13     print(":: ")
14     usage()
15     sys.exit(1)
16
17   check_modules()  # check for and install required modules
18
19   print("::")
20   print(":: Beginning Py7 Lint")
21   print("::")
22   print("   ")
23   for i in range(1, len(sys.argv)):
24     print("\n::--- ... ---::\n")
25     run_lint_tests(sys.argv[i])
26     print("   ")
27   print("::")
28   print(":: Completed Py7 Lint")
29   print("::")
30   input("\nPress any key to continue...\n")
31   sys.exit(0)01 def check_modules():
02   print("#")
03   print("# Python Linter Tool Script ", __release__)
04   print("# ")
05   print("# Checking for required Python modules...")
06   print("#")
07   for package in PY_MOD_LIST:
08     print("   ")
09     has_mod = importlib.util.find_spec(package)
10     if has_mod:
11       print(f'  #:>The {package} module exists.')
12     else:
13       print(f'  #:>The {package} module absent.')
14       tmp_argv = sys.argv
15       sys.argv = ["pip", "install"] + [ package ]
16       try:
17         run_module("pip", run_name="__main__")
18       except SystemExit as exception:
19         print("exit code: ", exception.code)
20         if exception.code != 0 :
21           print(":: ")
22           print(f':: Error: The {package} module NOT installed; end tests!')
23           print(":: ")
24           sys.exit(2)
25       sys.argv = tmp_argv
26       print("  #:> ")
27       print(f'  #:>The {package} module installed.')
28       print("  #:> ")
29   print("")
30   input("\nPress any key to continue...\n")
31   print("")01 >> Issue: [B404:blacklist] Consider possible security implications associated with the subprocess module.
02 Severity: Low Confidence: High
03 CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
04 More Info: https://bandit.readthedocs.io/en/1.8.3/blacklists/blacklist_imports.html#b404-import-subprocess
05 Location: ./py7.py:44:0
06 43from runpy import run_module
07 44from subprocess import PIPE, run
08 45
09 --------------------------------------------------
10
11 >> Issue: [B602:subprocess_popen_with_shell_equals_true] subprocess call with shell=True identified, security issue.
12 Severity: High Confidence: High
13 CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
14 More Info: https://bandit.readthedocs.io/en/1.8.3/plugins/b602_subprocess_popen_with_shell_equals_true.html
15 Location: ./py7.py:86:17
16 85 result = run(cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True,
17 86 shell=True, check=False)
18 87
19 88 if len(result.stdout) == 0:
20 --------------------------------------------------

I also ran Py7 on the psh [11] Python shell (about 80 lines of code), written by Danish Praka, which resulted in multiple lint reports of problems. Py7's output for psh, along with the Python source code file, is available in Py7's GitHub repository [3]. Listing 6 shows the significant results with the duplicates removed.

Listing 6

Running Py7 on psh (Duplicates Removed)

01 def main():
02   print("")
03   version()
04   print("")
05
06   if len(sys.argv) == 2 and sys.argv[1] in ('-h', '--help'):
07     usage()
08     sys.exit(0)
09
10   if len(sys.argv) < 2:
11     print(":: ")
12     print(":: Error: No Python files given to check!")
13     print(":: ")
14     usage()
15     sys.exit(1)
16
17   check_modules()  # check for and install required modules
18
19   print("::")
20   print(":: Beginning Py7 Lint")
21   print("::")
22   print("   ")
23   for i in range(1, len(sys.argv)):
24     print("\n::--- ... ---::\n")
25     run_lint_tests(sys.argv[i])
26     print("   ")
27   print("::")
28   print(":: Completed Py7 Lint")
29   print("::")
30   input("\nPress any key to continue...\n")
31   sys.exit(0)01 def check_modules():
02   print("#")
03   print("# Python Linter Tool Script ", __release__)
04   print("# ")
05   print("# Checking for required Python modules...")
06   print("#")
07   for package in PY_MOD_LIST:
08     print("   ")
09     has_mod = importlib.util.find_spec(package)
10     if has_mod:
11       print(f'  #:>The {package} module exists.')
12     else:
13       print(f'  #:>The {package} module absent.')
14       tmp_argv = sys.argv
15       sys.argv = ["pip", "install"] + [ package ]
16       try:
17         run_module("pip", run_name="__main__")
18       except SystemExit as exception:
19         print("exit code: ", exception.code)
20         if exception.code != 0 :
21           print(":: ")
22           print(f':: Error: The {package} module NOT installed; end tests!')
23           print(":: ")
24           sys.exit(2)
25       sys.argv = tmp_argv
26       print("  #:> ")
27       print(f'  #:>The {package} module installed.')
28       print("  #:> ")
29   print("")
30   input("\nPress any key to continue...\n")
31   print("")01 psh.py:82:0: C0305: Trailing newlines (trailing-newlines)
02 psh.py:4:0: W0105: String statement has no effect (pointless-string-statement)
03 psh.py:6:0: C0413: Import "import os" should be placed at the top of the module (wrong-import-position)
04 psh.py:7:0: C0413: Import "import subprocess" should be placed at the top of the module (wrong-import-position)
05 psh.py:50:11: W0718: Catching too general exception Exception (broad-exception-caught)
06 psh.py:39:20: W1510: 'subprocess.run' used without explicitly defining the value for 'check'. (subprocess-run-check)
07 psh.py:41:26: C0209: Formatting a regular string which could be an f-string (consider-using-f-string)
08 psh.py:62:0: C0116: Missing function or method docstring (missing-function-docstring)
09 psh.py:70:8: R1723: Unnecessary "elif" after "break", remove the leading "el" from "elif" (no-else-break)
10 -----------------------------------
11 Your code has been rated at 6.88/10

Finally, I ran Py7 against the Tinycat BASIC programming language interpreter (basic.py) [12] and a two-line Python interpreter [13]. You can find the output and results for both on the Py7 GitHub page [3].

Perhaps more Python source code files will be tested with the py7.py tool.

Room for Improvement

Because Py7 is open source, it has the potential to be improved upon by others. Perhaps Py7 could be used as a plugin inside a source code editor to lint Python code or as an external tool to invoke the source code file for editing and revision, making it both an editor and a linter. Additionally, P7 could be used in a source code repository to lint Python source code as it's being checked into the archive or after a merge with a branch. By adding an option for non-interactive operation, Py7 could be run against the specified Python source code files as command-line parameters, with the lint results output to the standard console and a default logfile.

Buy this article as PDF

Download Article PDF now with Express Checkout
Price $2.95
(incl. VAT)

Buy Linux Magazine

Related content

  • Practical Python in Linux

    We’ll introduce you to Python, an easy-to-learn scripting language, and also help you get started creating your own practical Python scripts.

  • Python 3

    What do Python 2.x programmers need to know about Python 3?

  • Exploring /proc

    The Linux /proc virtual filesystem offers a window into a running system – look inside for information on processes and kernel activity.

  • RFID over SPI

    Inexpensive components for the SPI interface let you upgrade a Raspberry Pi 4 to a display system for zero-contact RFID-based data acquisition.

  • Adding Arguments with argparse

    Parse arguments at the command line with this powerful Python module.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More

News