diff --git a/.gitignore b/.gitignore index 57b7f4c..62f8e2d 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ Pipfile* # From tests .coverage +.mypy_cache .pytest_cache .tox output-*/ diff --git a/.travis.yml b/.travis.yml index f428655..224279c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ dist: xenial sudo: required language: python python: - - '3.5' - '3.6' - '3.7' - '3.8' @@ -21,6 +20,8 @@ before_install: install: - cat install.txt | xargs sudo apt-get install -y - pip install -r requirements.txt + - pip3 install -r requirements.txt + - pip3 install mypy # This stage is run against everything in the matrix (so every python version) script: @@ -32,14 +33,13 @@ jobs: include: # This adds a second parallel "test" stage (since we didn't specify # the stage name) just to run statick - - stage: test - python: '3.5' - install: - - cat install.txt | xargs sudo apt-get install -y - - pip install -r requirements.txt - script: statick . --profile self_check.yaml + - stage: self_check + python: '3.8' + script: + - statick . --profile self_check.yaml + - mypy --ignore-missing-imports src/ - stage: deploy - python: '3.5' + python: '3.8' provider: pypi user: tdenewiler password: diff --git a/README.md b/README.md index 1283550..3a82c19 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ ![Python Versions](https://img.shields.io/pypi/pyversions/statick-tex.svg) ![License](https://img.shields.io/pypi/l/statick-tex.svg) +[![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/) ![Daily Downloads](https://img.shields.io/pypi/dd/statick-tex.svg) ![Weekly Downloads](https://img.shields.io/pypi/dw/statick-tex.svg) ![Monthly Downloads](https://img.shields.io/pypi/dm/statick-tex.svg) @@ -87,3 +88,16 @@ In particular, it is much easier to test whether a bug is fixed (and identify fu unit test which replicates the bug. Before submitting a change, please run tox to check that you have not introduced any regressions or violated any code style guidelines. + +### Mypy + +Statick uses [mypy](http://mypy-lang.org/) to check that type hints are being followed properly. +Type hints are described in [PEP 484](https://www.python.org/dev/peps/pep-0484/) and allow for static typing in Python. +To determine if proper types are being used in Statick plugins the following command will show any errors, and create several +types of reports that can be viewed with a text editor or web browser. + + pip install mypy + mkdir report + mypy --ignore-missing-imports --html-report report/ --txt-report report src/ + +It is hoped that in the future we will generate coverage reports from mypy and use those to check for regressions. diff --git a/src/statick_tex/plugins/discovery/tex_discovery_plugin.py b/src/statick_tex/plugins/discovery/tex_discovery_plugin.py index 742f3b7..dbbd91c 100644 --- a/src/statick_tex/plugins/discovery/tex_discovery_plugin.py +++ b/src/statick_tex/plugins/discovery/tex_discovery_plugin.py @@ -6,28 +6,30 @@ import os import subprocess from collections import OrderedDict +from typing import List from statick_tool.discovery_plugin import DiscoveryPlugin +from statick_tool.exceptions import Exceptions +from statick_tool.package import Package class TexDiscoveryPlugin(DiscoveryPlugin): """Discover TeX files to analyze.""" - def get_name(self): + def get_name(self) -> str: """Get name of discovery type.""" return "tex" - def scan(self, package, level, exceptions=None): # pylint: disable=too-many-locals + def scan(self, package: Package, level: str, exceptions: Exceptions = None): # pylint: disable=too-many-locals """Scan package looking for TeX files.""" - tex_files = [] - globs = ["*.tex", "*.bib"] + tex_files: List[str] = [] + globs: List[str] = ["*.tex", "*.bib"] - file_cmd_exists = True + file_cmd_exists: bool = True if not DiscoveryPlugin.file_command_exists(): file_cmd_exists = False - root = '' - files = [] + root: str = '' for root, _, files in os.walk(package.path): for glob in globs: for f in fnmatch.filter(files, glob): @@ -53,7 +55,7 @@ def scan(self, package, level, exceptions=None): # pylint: disable=too-many-loc print(" {} TeX files found.".format(len(tex_files))) if exceptions: - original_file_count = len(tex_files) + original_file_count: int = len(tex_files) tex_files = exceptions.filter_file_exceptions_early(package, tex_files) if original_file_count > len(tex_files): diff --git a/src/statick_tex/plugins/tool/chktex_tool_plugin.py b/src/statick_tex/plugins/tool/chktex_tool_plugin.py index a53c4ed..fd74c04 100644 --- a/src/statick_tex/plugins/tool/chktex_tool_plugin.py +++ b/src/statick_tex/plugins/tool/chktex_tool_plugin.py @@ -8,29 +8,31 @@ import re import subprocess +from typing import List, Match, Optional, Pattern from statick_tool.issue import Issue +from statick_tool.package import Package from statick_tool.tool_plugin import ToolPlugin class ChktexToolPlugin(ToolPlugin): """Apply chktex tool and gather results.""" - def get_name(self): + def get_name(self) -> str: """Get name of tool.""" return "chktex" - def scan(self, package, level): + def scan(self, package: Package, level: str) -> Optional[List[Issue]]: """Run tool and gather output.""" - flags = [] - user_flags = self.get_user_flags(level) + flags: List[str] = [] + user_flags: List[str] = self.get_user_flags(level) flags += user_flags - total_output = [] + total_output: List[str] = [] - tool_bin = "chktex" + tool_bin: str = "chktex" for src in package["tex"]: try: - subproc_args = [tool_bin, src] + flags + subproc_args: List[str] = [tool_bin, src] + flags output = subprocess.check_output(subproc_args, stderr=subprocess.STDOUT, universal_newlines=True) @@ -47,7 +49,7 @@ def scan(self, package, level): print("Couldn't find {}! ({})".format(tool_bin, ex)) return None - if self.plugin_context.args.show_tool_output: + if self.plugin_context and self.plugin_context.args.show_tool_output: print("{}".format(output)) total_output.append(output) @@ -56,22 +58,22 @@ def scan(self, package, level): for output in total_output: fname.write(output) - issues = self.parse_output(total_output) + issues: List[Issue] = self.parse_output(total_output) return issues - def parse_output(self, total_output): + def parse_output(self, total_output: List[str]) -> List[Issue]: """Parse tool output and report issues.""" - tool_re = r"(.+)\s(\d+)\s(.+)\s(.+)\s(.+)\s(\d+):\s(.+)" - parse = re.compile(tool_re) - issues = [] - filename = '' - line_number = 0 - issue_type = '' - message = '' + tool_re: str = r"(.+)\s(\d+)\s(.+)\s(.+)\s(.+)\s(\d+):\s(.+)" + parse: Pattern[str] = re.compile(tool_re) + issues: List[Issue] = [] + filename: str = '' + line_number: str = "0" + issue_type: str = '' + message: str = '' for output in total_output: for line in output.splitlines(): - match = parse.match(line) + match: Optional[Match[str]] = parse.match(line) if match: if match.group(1) == "Warning": filename = match.group(4) diff --git a/src/statick_tex/plugins/tool/lacheck_tool_plugin.py b/src/statick_tex/plugins/tool/lacheck_tool_plugin.py index a9ed9da..592ca69 100644 --- a/src/statick_tex/plugins/tool/lacheck_tool_plugin.py +++ b/src/statick_tex/plugins/tool/lacheck_tool_plugin.py @@ -4,29 +4,31 @@ import re import subprocess +from typing import List, Match, Optional, Pattern from statick_tool.issue import Issue +from statick_tool.package import Package from statick_tool.tool_plugin import ToolPlugin class LacheckToolPlugin(ToolPlugin): """Apply lacheck tool and gather results.""" - def get_name(self): + def get_name(self) -> str: """Get name of tool.""" return "lacheck" - def scan(self, package, level): + def scan(self, package: Package, level: str) -> Optional[List[Issue]]: """Run tool and gather output.""" - flags = [] - user_flags = self.get_user_flags(level) + flags: List[str] = [] + user_flags: List[str] = self.get_user_flags(level) flags += user_flags - total_output = [] + total_output: List[str] = [] - tool_bin = "lacheck" + tool_bin: str = "lacheck" for src in package["tex"]: try: - subproc_args = [tool_bin, src] + flags + subproc_args: List[str] = [tool_bin, src] + flags output = subprocess.check_output(subproc_args, stderr=subprocess.STDOUT, universal_newlines=True) @@ -43,7 +45,7 @@ def scan(self, package, level): print("Couldn't find {}! ({})".format(tool_bin, ex)) return None - if self.plugin_context.args.show_tool_output: + if self.plugin_context and self.plugin_context.args.show_tool_output: print("{}".format(output)) total_output.append(output) @@ -52,22 +54,22 @@ def scan(self, package, level): for output in total_output: fname.write(output) - issues = self.parse_output(total_output) + issues: List[Issue] = self.parse_output(total_output) return issues - def parse_output(self, total_output): + def parse_output(self, total_output: List[str]) -> List[Issue]: """Parse tool output and report issues.""" - tool_re = r"(.+)\s(.+)\s(\d+):\s(.+)" - parse = re.compile(tool_re) - issues = [] - filename = '' - line_number = 0 - issue_type = '' - message = '' + tool_re: str = r"(.+)\s(.+)\s(\d+):\s(.+)" + parse: Pattern[str] = re.compile(tool_re) + issues: List[Issue] = [] + filename: str = '' + line_number: str = "0" + issue_type: str = '' + message: str = '' for output in total_output: for line in output.splitlines(): - match = parse.match(line) + match: Optional[Match[str]] = parse.match(line) if match: filename = match.group(1)[1:-2] issue_type = "lacheck"