» Make grep CLI App in Python » 2. Development » 2.6 Add Lint

Add Lint

Lint refers to a tool or program that analyzes source code to find and report issues related to programming style, potential errors, and adherence to coding standards. The term "lint" originates from the name of a Unix utility called "lint" that was used to identify bugs and errors in C programming code.

Using linting tools in a Python project is a good practice for maintaining code quality and consistency. Linting tools analyze your code for potential errors, style issues, and adherence to coding standards.

There are several linting tools available for Python. Two popular ones are flake8 and pylint. We'll use flake8 in this project.

Install it using:

pip install flake8 # Or pip3 install flake8

Create a empty file .flake8 in the project's root directory.

And then run it:

flake8

You will get a list of lint issues:

./grepy/grep.py:7:1: E302 expected 2 blank lines, found 1
./grepy/grep.py:7:80: E501 line too long (97 > 79 characters)
./grepy/grep.py:8:80: E501 line too long (133 > 79 characters)
./grepy/grep.py:10:1: E302 expected 2 blank lines, found 1
./grepy/grep.py:10:77: E252 missing whitespace around parameter equals
./grepy/grep.py:10:78: E252 missing whitespace around parameter equals
./grepy/grep.py:10:80: E501 line too long (81 > 79 characters)
./grepy/grep.py:14:35: E261 at least two spaces before inline comment
./grepy/grep.py:29:1: E302 expected 2 blank lines, found 1
./grepy/grep.py:32:1: E302 expected 2 blank lines, found 1
./grepy/grep.py:32:80: E501 line too long (96 > 79 characters)
./grepy/grep.py:32:92: E252 missing whitespace around parameter equals
./grepy/grep.py:32:93: E252 missing whitespace around parameter equals
./grepy_cli.py:6:1: E302 expected 2 blank lines, found 1
./grepy_cli.py:7:80: E501 line too long (99 > 79 characters)
./grepy_cli.py:7:100: W291 trailing whitespace
./grepy_cli.py:8:80: E501 line too long (82 > 79 characters)
./grepy_cli.py:10:80: E501 line too long (88 > 79 characters)
./grepy_cli.py:13:80: E501 line too long (131 > 79 characters)
./grepy_cli.py:14:80: E501 line too long (144 > 79 characters)
./grepy_cli.py:15:80: E501 line too long (208 > 79 characters)
./grepy_cli.py:16:80: E501 line too long (115 > 79 characters)
./grepy_cli.py:17:80: E501 line too long (145 > 79 characters)
./grepy_cli.py:22:80: E501 line too long (80 > 79 characters)
./grepy_cli.py:31:1: E302 expected 2 blank lines, found 1
./grepy_cli.py:39:1: E302 expected 2 blank lines, found 1
./grepy_cli.py:52:1: E305 expected 2 blank lines after class or function definition, found 1
./tests/test_grep.py:8:1: E302 expected 2 blank lines, found 1
./tests/test_grep.py:57:80: E501 line too long (86 > 79 characters)
./tests/test_grep.py:66:1: E305 expected 2 blank lines after class or function definition, found 1

Try to fix all of them and run flake8 again.

flake8

If nothing pops out, your code is in "good style" now.

Changes in grepy/grep.py:

@@ -4,14 +4,22 @@ import os
 
 MatchResults = List[Tuple[int, str]]
 
-def _filter_lines(pattern: Union[str, re.Pattern], lines: List[str], flag: bool) -> MatchResults:
-    return [(line_number, line.strip()) for line_number, line in enumerate(lines, start=1) if bool(re.search(pattern, line)) == flag]
 
-def grep(pattern: Union[str, re.Pattern], file_path: str, options: List[str]=[]):
+def _filter_lines(pattern: Union[str, re.Pattern],
+                  lines: List[str], flag: bool) -> MatchResults:
+    return [
+            (line_number, line.strip())
+            for line_number, line in enumerate(lines, start=1)
+            if bool(re.search(pattern, line)) == flag
+        ]
+
+
+def grep(pattern: Union[str, re.Pattern],
+         file_path: str, options: List[str] = []):
     with open(file_path, 'r') as file:
         try:
             lines = file.readlines()
-        except UnicodeDecodeError: # filter out binary files
+        except UnicodeDecodeError:  # filter out binary files
             return {file_path: []}
 
         if options:
@@ -26,10 +34,13 @@ def grep(pattern: Union[str, re.Pattern], file_path: str, options: List[str]=[])
 
     return {file_path: matching_lines}
 
+
 def grep_count(result: Dict[str, MatchResults]):
     return sum([len(v) for v in result.values()])
 
-def grep_recursive(pattern: Union[str, re.Pattern], directory_path: str, options: List[str]=[]):
+
+def grep_recursive(pattern: Union[str, re.Pattern],
+                   directory_path: str, options: List[str] = []):
     results = {}
     for root, _, files in os.walk(directory_path):
         for file in files:

Changes in grepy_cli.py:

@@ -3,23 +3,37 @@ from typing import List, Dict
 
 from grepy.grep import grep, grep_recursive, grep_count, MatchResults
 
+
 def main():
-    parser = argparse.ArgumentParser(description='''A grep-like command-line utility from LiteRank, 
-                                     see https://literank.com/tutorial/9/intro''')
+    parser = argparse.ArgumentParser(description='''
+            A grep-like command-line utility from LiteRank,
+            see https://literank.com/tutorial/9/intro''')
     parser.add_argument('pattern', type=str, help='The pattern to search for')
-    parser.add_argument('file_path', type=str, help='The path to the file to search in')
+    parser.add_argument('file_path', type=str,
+                        help='The path to the file to search in')
 
     # Optional arguments
-    parser.add_argument('-c', '--count', action='store_true', help='Only a count of selected lines is written to standard output.')
-    parser.add_argument('-i', '--ignore-case', action='store_true', help='Perform case insensitive matching. By default, it is case sensitive.')
-    parser.add_argument('-n', '--line-number', action='store_true', help='Each output line is preceded by its relative line number in the file, starting at line 1. This option is ignored if -c is specified.')
-    parser.add_argument('-r', '--recursive', action='store_true', help='Recursively search subdirectories listed.')
-    parser.add_argument('-v', '--invert-match', action='store_true', help='Selected lines are those not matching any of the specified patterns.')
+    parser.add_argument('-c', '--count', action='store_true',
+                        help='Only a count of selected lines is written to \
+                            standard output.')
+    parser.add_argument('-i', '--ignore-case', action='store_true',
+                        help='Perform case insensitive matching. By default, \
+                            it is case sensitive.')
+    parser.add_argument('-n', '--line-number', action='store_true',
+                        help='Each output line is preceded by its relative \
+                        line number in the file, starting at line 1. This \
+                            option is ignored if -c is specified.')
+    parser.add_argument('-r', '--recursive', action='store_true',
+                        help='Recursively search subdirectories listed.')
+    parser.add_argument('-v', '--invert-match', action='store_true',
+                        help='Selected lines are those not matching any of \
+                          the specified patterns.')
 
     args = parser.parse_args()
 
     if args.recursive:
-        result = grep_recursive(args.pattern, args.file_path, get_options(args))
+        result = grep_recursive(args.pattern,
+                                args.file_path, get_options(args))
     else:
         result = grep(args.pattern, args.file_path, get_options(args))
 
@@ -28,6 +42,7 @@ def main():
     else:
         print_result(result, args.line_number)
 
+
 def get_options(args: argparse.Namespace) -> List[str]:
     options = []
     if args.ignore_case:
@@ -36,6 +51,7 @@ def get_options(args: argparse.Namespace) -> List[str]:
         options.append('v')
     return options
 
+
 def print_result(result: Dict[str, MatchResults], line_number_option: bool):
     current_file = None
     file_count = len(result)
@@ -49,5 +65,6 @@ def print_result(result: Dict[str, MatchResults], line_number_option: bool):
             else:
                 print(line)
 
+
 if __name__ == '__main__':
     main()