mirror of
https://github.com/zebrajr/pytorch.git
synced 2025-12-06 12:20:52 +01:00
Summary: Changes including: - introduced `linter/`, `testing/`, `stats/` folders in `tools/` - move appropriate scripts into these folders - change grepped references in the pytorch/pytorch repo Next step - introduce `build/` folder for build scripts Pull Request resolved: https://github.com/pytorch/pytorch/pull/60473 Test Plan: - CI (this is important b/c pytorch/test-infra also rely on some script reference. - tools/tests/ Reviewed By: albanD Differential Revision: D29352716 Pulled By: walterddr fbshipit-source-id: bad40b5ce130b35dfd9e59b8af34f9025f3285fd
221 lines
7.3 KiB
Python
Executable File
221 lines
7.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
"""
|
|
This module is meant to be run as a script (see the docstring of main
|
|
below) and passed the filename of any Python file in this repo, to
|
|
typecheck that file using only the subset of our mypy configs that apply
|
|
to it.
|
|
|
|
Since editors (e.g. VS Code) can be configured to use this wrapper
|
|
script in lieu of mypy itself, the idea is that this can be used to get
|
|
inline mypy results while developing, and have at least some degree of
|
|
assurance that those inline results match up with what you would get
|
|
from running the mypy lint from the .github/workflows/lint.yml file.
|
|
|
|
See also these wiki pages:
|
|
|
|
- https://github.com/pytorch/pytorch/wiki/Guide-for-adding-type-annotations-to-PyTorch
|
|
- https://github.com/pytorch/pytorch/wiki/Lint-as-you-type
|
|
"""
|
|
|
|
import sys
|
|
from collections import defaultdict
|
|
from configparser import ConfigParser
|
|
from pathlib import Path, PurePath, PurePosixPath
|
|
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
|
|
import mypy.api
|
|
# not part of the public API, but this is the easiest way to ensure that
|
|
# we agree with what mypy actually does
|
|
import mypy.config_parser
|
|
|
|
|
|
def read_config(config_path: Path) -> Set[str]:
|
|
"""
|
|
Return the set of `files` in the `mypy` ini file at config_path.
|
|
"""
|
|
config = ConfigParser()
|
|
config.read(config_path)
|
|
# hopefully on Windows this gives posix paths
|
|
return set(mypy.config_parser.split_and_match_files(
|
|
config['mypy']['files'],
|
|
))
|
|
|
|
|
|
# see tools/test/test_mypy_wrapper.py for examples of many of the
|
|
# following functions
|
|
|
|
|
|
def config_files() -> Dict[str, Set[str]]:
|
|
"""
|
|
Return a dict from all our `mypy` ini filenames to their `files`.
|
|
"""
|
|
return {str(ini): read_config(ini) for ini in Path().glob('mypy*.ini')}
|
|
|
|
|
|
def split_path(path: str) -> List[str]:
|
|
"""
|
|
Split a relative (not absolute) POSIX path into its segments.
|
|
"""
|
|
pure = PurePosixPath(path)
|
|
return [str(p.name) for p in list(reversed(pure.parents))[1:] + [pure]]
|
|
|
|
|
|
# mypy doesn't support recursive types yet
|
|
# https://github.com/python/mypy/issues/731
|
|
|
|
# but if it did, the `Any` here would be `Union[Set[str], 'Trie']`,
|
|
# although that is not completely accurate: specifically, every `None`
|
|
# key must map to a `Set[str]`, and every `str` key must map to a `Trie`
|
|
Trie = Dict[Optional[str], Any]
|
|
|
|
|
|
def make_trie(configs: Dict[str, Set[str]]) -> Trie:
|
|
"""
|
|
Return a trie from path prefixes to their `mypy` configs.
|
|
|
|
Specifically, each layer of the trie represents a segment of a POSIX
|
|
path relative to the root of this repo. If you follow a path down
|
|
the trie and reach a `None` key, that `None` maps to the (nonempty)
|
|
set of keys in `configs` which explicitly include that path.
|
|
"""
|
|
trie: Trie = {}
|
|
for ini, files in configs.items():
|
|
for f in files:
|
|
inner = trie
|
|
for segment in split_path(f):
|
|
inner = inner.setdefault(segment, {})
|
|
inner.setdefault(None, set()).add(ini)
|
|
return trie
|
|
|
|
|
|
def lookup(trie: Trie, filename: str) -> Set[str]:
|
|
"""
|
|
Return the configs in `trie` that include a prefix of `filename`.
|
|
|
|
A path is included by a config if any of its ancestors are included
|
|
by the wildcard-expanded version of that config's `files`. Thus,
|
|
this function follows `filename`'s path down the `trie` and
|
|
accumulates all the configs it finds along the way.
|
|
"""
|
|
configs = set()
|
|
inner = trie
|
|
for segment in split_path(filename):
|
|
inner = inner.get(segment, {})
|
|
configs |= inner.get(None, set())
|
|
return configs
|
|
|
|
|
|
def make_plan(
|
|
*,
|
|
configs: Dict[str, Set[str]],
|
|
files: List[str]
|
|
) -> Dict[str, List[str]]:
|
|
"""
|
|
Return a dict from config names to the files to run them with.
|
|
|
|
The keys of the returned dict are a subset of the keys of `configs`.
|
|
The list of files in each value of returned dict should contain a
|
|
nonempty subset of the given `files`, in the same order as `files`.
|
|
"""
|
|
trie = make_trie(configs)
|
|
plan = defaultdict(list)
|
|
for filename in files:
|
|
for config in lookup(trie, filename):
|
|
plan[config].append(filename)
|
|
return plan
|
|
|
|
|
|
def run(
|
|
*,
|
|
args: List[str],
|
|
files: List[str],
|
|
) -> Tuple[int, List[str], List[str]]:
|
|
"""
|
|
Return the exit code and list of output lines from running `mypy`.
|
|
|
|
The given `args` are passed verbatim to `mypy`. The `files` (each of
|
|
which must be an absolute path) are converted to relative paths
|
|
(that is, relative to the root of this repo) and then classified
|
|
according to which ones need to be run with each `mypy` config.
|
|
Thus, `mypy` may be run zero, one, or multiple times, but it will be
|
|
run at most once for each `mypy` config used by this repo.
|
|
"""
|
|
repo_root = Path.cwd()
|
|
plan = make_plan(configs=config_files(), files=[
|
|
PurePath(f).relative_to(repo_root).as_posix() for f in files
|
|
])
|
|
mypy_results = [
|
|
mypy.api.run(
|
|
# insert custom flags after args to avoid being overridden
|
|
# by existing flags in args
|
|
args + [
|
|
# don't special-case the last line
|
|
'--no-error-summary',
|
|
f'--config-file={config}',
|
|
] + filtered
|
|
)
|
|
# by construction, filtered must be nonempty
|
|
for config, filtered in plan.items()
|
|
]
|
|
return (
|
|
# assume all mypy exit codes are nonnegative
|
|
# https://github.com/python/mypy/issues/6003
|
|
max(
|
|
[exit_code for _, _, exit_code in mypy_results],
|
|
default=0,
|
|
),
|
|
list(dict.fromkeys( # remove duplicates, retain order
|
|
item
|
|
for stdout, _, _ in mypy_results
|
|
for item in stdout.splitlines()
|
|
)),
|
|
[stderr for _, stderr, _ in mypy_results],
|
|
)
|
|
|
|
|
|
def main(args: List[str]) -> None:
|
|
"""
|
|
Run mypy on one Python file using the correct config file(s).
|
|
|
|
This function assumes the following preconditions hold:
|
|
|
|
- the cwd is set to the root of this cloned repo
|
|
- args is a valid list of CLI arguments that could be passed to mypy
|
|
- some of args are absolute paths to files to typecheck
|
|
- all the other args are config flags for mypy, rather than files
|
|
|
|
These assumptions hold, for instance, when mypy is run automatically
|
|
by VS Code's Python extension, so in your clone of this repository,
|
|
you could modify your .vscode/settings.json to look something like
|
|
this (assuming you use a conda environment named "pytorch"):
|
|
|
|
{
|
|
"python.linting.enabled": true,
|
|
"python.linting.mypyEnabled": true,
|
|
"python.linting.mypyPath":
|
|
"${env:HOME}/miniconda3/envs/pytorch/bin/python",
|
|
"python.linting.mypyArgs": [
|
|
"${workspaceFolder}/tools/linter/mypy_wrapper.py"
|
|
]
|
|
}
|
|
|
|
More generally, this should work for any editor sets the cwd to the
|
|
repo root, runs mypy on individual files via their absolute paths,
|
|
and allows you to set the path to the mypy executable.
|
|
"""
|
|
repo_root = str(Path.cwd())
|
|
exit_code, mypy_issues, stderrs = run(
|
|
args=[arg for arg in args if not arg.startswith(repo_root)],
|
|
files=[arg for arg in args if arg.startswith(repo_root)],
|
|
)
|
|
for issue in mypy_issues:
|
|
print(issue)
|
|
for stderr in stderrs:
|
|
print(stderr, end='', file=sys.stderr)
|
|
sys.exit(exit_code)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main(sys.argv[1:])
|