Skip to content

Add type annotations and fix mypy errors in the codebase - #1397

Open
rachelzUT wants to merge 8 commits into
pyta-uoft:masterfrom
rachelzUT:add-type-annotations
Open

rachelzUT wants to merge 8 commits into
pyta-uoft:masterfrom
rachelzUT:add-type-annotations

Conversation

@rachelzUT

@rachelzUT rachelzUT commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Proposed Changes

Added type annotations to the python-ta codebase, particularly to the packages\python-ta\src\python_ta directory. mypy was run on all files in this directory and fixes were implemented to resolve all type checking errors reported by mypy. This was done in efforts to improve code quality, and for aiding the inclusion of mypy to the prek pre-commit hook configuration which will run mypy on all future commits.

Additional notes:

  • The pyproject.toml file was used to add configuration options for mypy, specifically to ignore missing library stubs or py.typed markers
  • Existing logic was kept intact and unchanged as much as possible. All changes apart from adding/editing type annotations were made to fix mypy errors.

...

Screenshots of your changes (if applicable)

Type of Change

(Write an X or a brief description next to the type or types that best describe your changes.)

Type Applies?
🚨 Breaking change (fix or feature that would cause existing functionality to change)
✨ New feature (non-breaking change that adds functionality)
🐛 Bug fix (non-breaking change that fixes an issue)
♻️ Refactoring (internal change to codebase, without changing functionality)
🚦 Test update (change that only adds or modifies tests)
📚 Documentation update (change that only updates documentation)
📦 Dependency update (change that updates a dependency)
🔧 Internal (change that only affects developers or continuous integration) X

Checklist

(Complete each of the following items for your pull request. Indicate that you have completed an item by changing the [ ] into a [x] in the raw text, or by clicking on the checkbox in the rendered description on GitHub.)

Before opening your pull request:

  • I have performed a self-review of my changes.
    • Check that all changed files included in this pull request are intentional changes.
    • Check that all changes are relevant to the purpose of this pull request, as described above.
  • I have added tests for my changes, if applicable.
    • This is required for all bug fixes and new features.
  • I have updated the project documentation, if applicable.
    • This is required for new features.
  • I have updated the project Changelog (this is required for all changes).
  • If this is my first contribution, I have added myself to the list of contributors.

After opening your pull request:

  • I have verified that the CI tests have passed.
  • I have reviewed the test coverage changes reported by Coveralls.
  • I have requested a review from a project maintainer.

Questions and Comments

Type annotations and mypy fixes were made only to the packages\python-ta\src\python_ta directory. I believe making similar edits and fixes to the test files in packages\python-ta\tests would be difficult as many of them purposely introduce issues such as incorrect formatting and type annotations for the purpose of testing checkers and related features.

@rachelzUT rachelzUT changed the title Add type annotations Add type annotations and fix mypy errors in the codebase Sep 25, 2026
@coveralls

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 36151586431

Coverage decreased (-0.1%) to 90.78%

Details

  • Coverage decreased (-0.1%) from the base build.
  • Patch coverage: 20 uncovered changes across 9 files (312 of 332 lines covered, 93.98%).
  • 1 coverage regression across 1 file.

Uncovered Changes

File Changed Covered %
packages/python-ta/src/python_ta/cfg/cfg_generator.py 10 4 40.0%
packages/python-ta/src/python_ta/check/helpers.py 32 28 87.5%
packages/python-ta/src/python_ta/check/watch.py 7 4 57.14%
packages/python-ta/src/python_ta/contracts/init.py 46 44 95.65%
packages/python-ta/src/python_ta/cfg/init.py 1 0 0.0%
packages/python-ta/src/python_ta/contracts/main.py 1 0 0.0%
packages/python-ta/src/python_ta/reporters/html_reporter.py 10 9 90.0%
packages/python-ta/src/python_ta/reporters/node_printers.py 50 49 98.0%
packages/python-ta/src/python_ta/transforms/setendings.py 32 31 96.88%
Total (33 files) 332 312 93.98%

Coverage Regressions

1 previously-covered line in 1 file lost coverage.

File Lines Losing Coverage Coverage
packages/python-ta/src/python_ta/check/watch.py 1 64.58%

Coverage Stats

Coverage Status
Relevant Lines: 4143
Covered Lines: 3761
Line Coverage: 90.78%
Coverage Strength: 17.65 hits per line

💛 - Coveralls

@david-yz-liu david-yz-liu left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rachelzUT great work, I know this was a significant effort. I left some inline comments, some of which apply broadly throughout the changes (e.g. the reporter type and some style things).

Also, in general prefer using mypy ignore comments over calling cast to avoid individual errors. In the future, I'll review the ignore comments separately to determine the best way to handle them, possibly with code changes.

m = sys.modules["__main__"]
spec = importlib.util.spec_from_file_location(m.__name__, m.__file__)
mod = spec.origin
if spec is None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's simplify this a bit, you should be able to do something like if spec is None or spec.origin is None

return

eval_params = [const.value for const in inferred_params]
eval_params = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This addition is okay, but let's combine it with the check on Lines 36-37. You can check to see whether len(eval_params) == len(inferred_params).

and isinstance(stop_arg.args[0], nodes.Name)
):
return stop_arg.args[0].name
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put this return inside an else branch

return os.path.join(curr_dir, "config", "pylintrc")
elif os.path.exists(os.path.join(curr_dir, "config", "pyproject.toml")):
return os.path.join(curr_dir, "config", "pyproject.toml")
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put this return statement inside an else branch

args_list.extend(pylint_args)
_config_initialization(linter, args_list=args_list, config_file=config_location)
linter.config_file = config_location
setattr(linter, "config_file", config_location) # use setattr to avoid mypy errors

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a mypy ignore comment instead of modifying the code


def _add_parens(source_code):
def h(node):
def _add_parens(source_code: list[str]) -> Callable[[nodes.NodeNG], nodes.NodeNG]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use NodeTransform for the return type

return ast

linter.get_ast = new_get_ast
cast(Any, linter).get_ast = new_get_ast

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a mypy ignore comment

reporter = _invoke_checker(checker, [temp_file.name], config, output_format)
# Clean up the temporary file
path.os.unlink(temp_file.name)
# Clean up the temporary file.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert this change

messages_config_path = linter.config.messages_config_path
# Pylint exposes a reporter object dynamically; PythonTA only relies on the
# BaseReporter API here, so narrow it for the rest of this helper.
current_reporter = cast(BaseReporter, linter.reporter)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's some awkwardness in general about our treatment of reporter types. I think we can simplify by enforcing that we only use subclasses of PythonTaReporter (rather than pylint's `BaseReporter).

This won't help with attributes on linter, but I hope it helps with using the reporter later.

self._control_boundaries = []
self.z3_enabled = z3_enabled

def _cb(self) -> CFGBlock:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall I would prefer not changing all uses of _current_block and _current_cfg.

Instead of defining these functions, please add assert statements to each of the visit_* methods (other than visit_module, which is where these attributes are set to non-None values)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants