├── .cruft.json
├── .github
├── dependabot.yml
└── workflows
│ ├── main.yml
│ └── publish.yml
├── .gitignore
├── .pre-commit-config.yaml
├── LICENSE
├── README.md
├── libcst_mypy
├── __init__.py
├── provider.py
├── py.typed
└── utils.py
├── pyproject.toml
├── requirements.txt
└── tests
├── __init__.py
├── data
├── simple_class.py
└── simples_class.json
└── test_inferencer.py
/.cruft.json:
--------------------------------------------------------------------------------
1 | {
2 | "template": "git@github.com:Kludex/python-template.git",
3 | "commit": "2a6e40fa5b9da75e30b01ef23d5fadc006d8e42d",
4 | "checkout": null,
5 | "context": {
6 | "cookiecutter": {
7 | "project_slug": "mypy-type-inference",
8 | "package": "mypy_type_inference",
9 | "description": "Infer types to be used for codemods using mypy! :sparkles:",
10 | "full_name": "Marcelo Trylesinski",
11 | "email": "marcelotryle@email.com",
12 | "github": "Kludex",
13 | "twitter": "marcelotryle",
14 | "_template": "git@github.com:Kludex/python-template.git"
15 | }
16 | },
17 | "directory": null
18 | }
19 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "pip"
4 | directory: "/"
5 | schedule:
6 | interval: "monthly"
7 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: ["main"]
6 | pull_request:
7 | branches: ["main"]
8 |
9 | jobs:
10 | tests:
11 | name: "Python ${{ matrix.python-version }}"
12 | runs-on: ubuntu-latest
13 |
14 | timeout-minutes: 30
15 | strategy:
16 | matrix:
17 | python-version: ["3.8", "3.9", "3.10", "3.11"]
18 |
19 | steps:
20 | - uses: actions/checkout@v3
21 | - uses: actions/setup-python@v3
22 | with:
23 | python-version: ${{ matrix.python-version }}
24 |
25 | - name: Run pre-commit
26 | if: ${{ matrix.python-version == '3.10' }}
27 | uses: pre-commit/action@v3.0.0
28 |
29 | - uses: actions/cache@v3
30 | with:
31 | path: ~/.cache/pip
32 | key: pip-${{ matrix.python-version }}
33 |
34 | - name: Install dependencies
35 | run: |
36 | python -m pip install --upgrade pip setuptools wheel
37 | python -m pip install -r requirements.txt
38 |
39 | - name: Run tests
40 | run: python -m coverage run -m pytest
41 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | version:
7 | description: "Version to be released."
8 | required: true
9 |
10 | jobs:
11 | release:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 | - uses: actions/setup-python@v4
16 | with:
17 | python-version: "3.10"
18 |
19 | - name: Bump to version ${{ github.event.inputs.version }}
20 | run: |
21 | version='${{ github.event.inputs.version }}'
22 | sed -i -E "s/^version = \"(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)\"/version = \"$version\"/" pyproject.toml
23 | echo "version=$version" >> $GITHUB_ENV
24 |
25 | - name: Commit version bump
26 | run: |
27 | git config --global user.name 'Github Actions'
28 | git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com'
29 | git add pyproject.toml
30 | git commit -m "Release $version"
31 | git push origin main
32 |
33 | - name: Install dependencies
34 | run: python -m pip install --upgrade build twine
35 |
36 | - name: PyPI release
37 | run: |
38 | python -m build
39 | twine upload dist/*
40 | env:
41 | TWINE_USERNAME: __token__
42 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
43 |
44 | - name: Create a GitHub Release
45 | run: gh release create v$version --generate-notes
46 | env:
47 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
98 | __pypackages__/
99 |
100 | # Celery stuff
101 | celerybeat-schedule
102 | celerybeat.pid
103 |
104 | # SageMath parsed files
105 | *.sage.py
106 |
107 | # Environments
108 | .env
109 | .venv
110 | env/
111 | venv/
112 | ENV/
113 | env.bak/
114 | venv.bak/
115 |
116 | # Spyder project settings
117 | .spyderproject
118 | .spyproject
119 |
120 | # Rope project settings
121 | .ropeproject
122 |
123 | # mkdocs documentation
124 | /site
125 |
126 | # mypy
127 | .mypy_cache/
128 | .dmypy.json
129 | dmypy.json
130 |
131 | # Pyre type checker
132 | .pyre/
133 |
134 | # pytype static type analyzer
135 | .pytype/
136 |
137 | # Cython debug symbols
138 | cython_debug/
139 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v4.2.0
4 | hooks:
5 | - id: check-toml
6 | - id: check-yaml
7 | - id: check-json
8 | - id: check-added-large-files
9 | - id: debug-statements
10 | - id: end-of-file-fixer
11 | - id: trailing-whitespace
12 |
13 | - repo: https://github.com/psf/black
14 | rev: 22.3.0
15 | hooks:
16 | - id: black
17 |
18 | - repo: https://github.com/charliermarsh/ruff-pre-commit
19 | rev: v0.0.249
20 | hooks:
21 | - id: ruff
22 | args: [--fix, --exit-non-zero-on-fix]
23 |
24 |
25 | - repo: https://github.com/pre-commit/mirrors-mypy
26 | rev: v1.0.1
27 | hooks:
28 | - id: mypy
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Marcelo Trylesinski
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | libcst-mypy
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | Infer types to be used for codemods using mypy! :sparkles:
21 |
22 | The code here was created by [rominf](https://github.com/rominf) on [LibCST#831](https://github.com/Instagram/LibCST/pull/831).
23 |
24 | ## Installation
25 |
26 | ```bash
27 | pip install libcst-mypy
28 | ```
29 |
30 | ## License
31 |
32 | This project is licensed under the terms of the MIT license.
33 |
--------------------------------------------------------------------------------
/libcst_mypy/__init__.py:
--------------------------------------------------------------------------------
1 | from libcst_mypy.provider import MypyTypeInferenceProvider
2 |
3 | __all__ = ["MypyTypeInferenceProvider"]
4 |
--------------------------------------------------------------------------------
/libcst_mypy/provider.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) Meta Platforms, Inc. and affiliates.
2 | #
3 | # This source code is licensed under the MIT license found in the
4 | # LICENSE file in the root directory of this source tree.
5 |
6 | from pathlib import Path
7 | from typing import Dict, List, Mapping, Optional
8 |
9 | import libcst as cst
10 | from libcst._position import CodeRange
11 | from libcst.helpers import calculate_module_and_package
12 | from libcst.metadata.base_provider import BatchableMetadataProvider
13 | from libcst.metadata.position_provider import PositionProvider
14 |
15 | import mypy
16 | import mypy.build
17 | import mypy.main
18 | import mypy.nodes
19 |
20 |
21 | from libcst_mypy.utils import (
22 | MypyType,
23 | MypyTypeInferenceProviderCache,
24 | CodeRangeToMypyNodesBinder,
25 | )
26 |
27 |
28 | class MypyTypeInferenceProvider(
29 | BatchableMetadataProvider[MypyType] # type: ignore[misc]
30 | ):
31 | """
32 | Access inferred type annotation through `mypy `_.
33 | """
34 |
35 | METADATA_DEPENDENCIES = (PositionProvider,)
36 |
37 | @classmethod
38 | def gen_cache(
39 | cls, root_path: Path, paths: List[str], timeout: Optional[int] = None
40 | ) -> Mapping[str, Optional[MypyTypeInferenceProviderCache]]:
41 | targets, options = mypy.main.process_options(paths)
42 | options.preserve_asts = True
43 | options.fine_grained_incremental = True
44 | options.use_fine_grained_cache = True
45 | mypy_result = mypy.build.build(targets, options=options)
46 | cache = {}
47 | for path in paths:
48 | module = calculate_module_and_package(str(root_path), path).name
49 | mypy_file = mypy_result.graph.get(module)
50 | if mypy_file is not None and mypy_file.tree is not None:
51 | cache[path] = MypyTypeInferenceProviderCache(
52 | module_name=module, mypy_file=mypy_file.tree
53 | )
54 | return cache
55 |
56 | def __init__(self, cache: Optional[MypyTypeInferenceProviderCache]) -> None:
57 | super().__init__(cache)
58 | self._mypy_node_locations: Dict[CodeRange, "mypy.nodes.Node"] = {}
59 | if cache is None:
60 | return
61 | code_range_to_mypy_nodes_binder = CodeRangeToMypyNodesBinder(cache.module_name)
62 | code_range_to_mypy_nodes_binder.visit_mypy_file(cache.mypy_file)
63 | self._mypy_node_locations = (
64 | code_range_to_mypy_nodes_binder.locations # type: ignore[assignment]
65 | )
66 |
67 | def _parse_metadata(self, node: cst.CSTNode) -> None:
68 | range = self.get_metadata(PositionProvider, node)
69 | if range in self._mypy_node_locations:
70 | self.set_metadata(node, self._mypy_node_locations.get(range))
71 |
72 | def visit_Name(self, node: cst.Name) -> None:
73 | self._parse_metadata(node)
74 |
75 | def visit_Attribute(self, node: cst.Attribute) -> None:
76 | self._parse_metadata(node)
77 |
78 | def visit_Call(self, node: cst.Call) -> None:
79 | self._parse_metadata(node)
80 |
--------------------------------------------------------------------------------
/libcst_mypy/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kludex/libcst-mypy/14e0901a784e5ec96f70211bca70400f535d42a1/libcst_mypy/py.typed
--------------------------------------------------------------------------------
/libcst_mypy/utils.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) Meta Platforms, Inc. and affiliates.
2 | #
3 | # This source code is licensed under the MIT license found in the
4 | # LICENSE file in the root directory of this source tree.
5 |
6 | from dataclasses import dataclass, field
7 | from typing import Dict, Optional, Union
8 |
9 | import mypy.build
10 | import mypy.main
11 | import mypy.modulefinder
12 | import mypy.nodes
13 | import mypy.options
14 | import mypy.patterns
15 | import mypy.traverser
16 | import mypy.types
17 | import mypy.typetraverser
18 |
19 | from libcst._add_slots import add_slots
20 | from libcst._position import CodePosition, CodeRange
21 |
22 |
23 | @add_slots
24 | @dataclass(frozen=True)
25 | class MypyTypeInferenceProviderCache:
26 | module_name: str
27 | mypy_file: mypy.nodes.MypyFile
28 |
29 |
30 | @add_slots
31 | @dataclass(frozen=True)
32 | class MypyType:
33 | is_type_constructor: bool
34 | mypy_type: Union[mypy.types.Type, mypy.nodes.TypeInfo]
35 | fullname: str = field(init=False)
36 |
37 | def __post_init__(self) -> None:
38 | if isinstance(self.mypy_type, mypy.types.Type):
39 | fullname = str(self.mypy_type)
40 | else:
41 | fullname = self.mypy_type.fullname
42 | if self.is_type_constructor:
43 | fullname = f"typing.Type[{fullname}]"
44 | object.__setattr__(self, "fullname", fullname)
45 |
46 | def __str__(self) -> str:
47 | return self.fullname
48 |
49 |
50 | class CodeRangeToMypyNodesBinder(
51 | mypy.traverser.TraverserVisitor, mypy.typetraverser.TypeTraverserVisitor
52 | ):
53 | def __init__(self, module_name: str) -> None:
54 | super().__init__()
55 | self.locations: Dict[CodeRange, MypyType] = {}
56 | self.in_type_alias_expr = False
57 | self.module_name = module_name
58 |
59 | # Helpers
60 |
61 | @staticmethod
62 | def get_code_range(o: mypy.nodes.Context) -> CodeRange:
63 | return CodeRange(
64 | start=CodePosition(o.line, o.column),
65 | end=CodePosition(o.end_line, o.end_column),
66 | )
67 |
68 | @staticmethod
69 | def check_bounds(o: mypy.nodes.Context) -> bool:
70 | return (
71 | (o.line is not None)
72 | and (o.line >= 1)
73 | and (o.column is not None)
74 | and (o.column >= 0)
75 | and (o.end_line is not None)
76 | and (o.end_line >= 1)
77 | and (o.end_column is not None)
78 | and (o.end_column >= 0)
79 | )
80 |
81 | def record_type_location_using_code_range(
82 | self,
83 | code_range: CodeRange,
84 | t: Optional[Union[mypy.types.Type, mypy.nodes.TypeInfo]],
85 | is_type_constructor: bool,
86 | ) -> None:
87 | if t is not None:
88 | self.locations[code_range] = MypyType(
89 | is_type_constructor=is_type_constructor, mypy_type=t
90 | )
91 |
92 | def record_type_location(
93 | self,
94 | o: mypy.nodes.Context,
95 | t: Optional[Union[mypy.types.Type, mypy.nodes.TypeInfo]],
96 | is_type_constructor: bool,
97 | ) -> None:
98 | if self.check_bounds(o):
99 | self.record_type_location_using_code_range(
100 | code_range=self.get_code_range(o),
101 | t=t,
102 | is_type_constructor=is_type_constructor,
103 | )
104 |
105 | def record_location_by_name_expr(
106 | self, code_range: CodeRange, o: mypy.nodes.NameExpr, is_type_constructor: bool
107 | ) -> None:
108 | if isinstance(o.node, mypy.nodes.Var):
109 | self.record_type_location_using_code_range(
110 | code_range=code_range, t=o.node.type, is_type_constructor=False
111 | )
112 | elif isinstance(o.node, mypy.nodes.TypeInfo):
113 | self.record_type_location_using_code_range(
114 | code_range=code_range, t=o.node, is_type_constructor=is_type_constructor
115 | )
116 |
117 | # Actual visitors
118 |
119 | def visit_var(self, o: mypy.nodes.Var) -> None:
120 | super().visit_var(o)
121 | self.record_type_location(o=o, t=o.type, is_type_constructor=False)
122 |
123 | def visit_name_expr(self, o: mypy.nodes.NameExpr) -> None:
124 | super().visit_name_expr(o)
125 | # Implementation in base classes is omitted, record it if it's variable or class
126 | self.record_location_by_name_expr(
127 | self.get_code_range(o), o, is_type_constructor=True
128 | )
129 |
130 | def visit_member_expr(self, o: mypy.nodes.MemberExpr) -> None:
131 | super().visit_member_expr(o)
132 | # Implementation in base classes is omitted, record it
133 | # o.def_var should not be None after mypy run, checking here just to be sure
134 | if o.def_var is not None:
135 | self.record_type_location(o=o, t=o.def_var.type, is_type_constructor=False)
136 |
137 | def visit_call_expr(self, o: mypy.nodes.CallExpr) -> None:
138 | super().visit_call_expr(o)
139 | if isinstance(o.callee, mypy.nodes.NameExpr):
140 | self.record_location_by_name_expr(
141 | code_range=self.get_code_range(o), o=o.callee, is_type_constructor=False
142 | )
143 |
144 | def visit_instance(self, t: mypy.types.Instance) -> None:
145 | super().visit_instance(t)
146 | self.record_type_location(o=t, t=t, is_type_constructor=False)
147 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling"]
3 | build-backend = 'hatchling.build'
4 |
5 | [project]
6 | name = "libcst-mypy"
7 | version = "0.1.0"
8 | description = "Infer types to be used for codemods using mypy! :sparkles:"
9 | readme = "README.md"
10 | authors = [{ name = "Marcelo Trylesinski", email = "marcelotryle@email.com" }]
11 | classifiers = [
12 | "Development Status :: 3 - Alpha",
13 | "License :: OSI Approved :: MIT License",
14 | "Intended Audience :: Developers",
15 | "Natural Language :: English",
16 | "Operating System :: OS Independent",
17 | "Programming Language :: Python :: 3 :: Only",
18 | "Programming Language :: Python :: 3",
19 | "Programming Language :: Python :: 3.8",
20 | "Programming Language :: Python :: 3.9",
21 | "Programming Language :: Python :: 3.10",
22 | "Programming Language :: Python :: 3.11",
23 | ]
24 | license = "MIT"
25 | requires-python = ">=3.7"
26 | dependencies = ["libcst>=0.4.9", "mypy>=1.1.1"]
27 | optional-dependencies = {}
28 |
29 | [project.urls]
30 | Homepage = "https://github.com/Kludex/libcst-mypy"
31 | Source = "https://github.com/Kludex/libcst-mypy"
32 | Twitter = "https://twitter.com/marcelotryle"
33 | Funding = "https://github.com/sponsors/Kludex"
34 |
35 | [tool.hatch.envs.libcst_mypy]
36 | type = "virtual"
37 |
38 | [[tool.hatch.envs.test.matrix]]
39 | python = ["3.8", "3.9", "3.10", "3.11"]
40 |
41 | [tool.mypy]
42 | strict = true
43 | show_error_codes = true
44 |
45 | [tool.black]
46 | target-version = ["py37"]
47 |
48 | [tool.pytest.ini_options]
49 | addopts = ["--strict-config", "--strict-markers"]
50 | filterwarnings = ["error"]
51 |
52 | [tool.coverage.run]
53 | source_pkgs = ["libcst_mypy", "tests"]
54 |
55 | [tool.coverage.report]
56 | show_missing = true
57 | skip_covered = true
58 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | -e .
2 |
3 | # Linter & Formatter
4 | black
5 | mypy
6 | ruff
7 | pre-commit
8 |
9 | # Tests
10 | coverage[toml]
11 | pytest
12 | pytest-sugar
13 | dirty-equals
14 |
15 | # Development
16 | bpython
17 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kludex/libcst-mypy/14e0901a784e5ec96f70211bca70400f535d42a1/tests/__init__.py
--------------------------------------------------------------------------------
/tests/data/simple_class.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) Meta Platforms, Inc. and affiliates.
2 | #
3 | # This source code is licensed under the MIT license found in the
4 | # LICENSE file in the root directory of this source tree.
5 |
6 | # fmt: off
7 | from typing import Sequence
8 |
9 |
10 | class Item:
11 | def __init__(self, n: int) -> None:
12 | self.number: int = n
13 |
14 |
15 | class ItemCollector:
16 | def get_items(self, n: int) -> Sequence[Item]:
17 | return [Item(i) for i in range(n)]
18 |
19 |
20 | collector = ItemCollector()
21 | items: Sequence[Item] = collector.get_items(3)
22 | for item in items:
23 | item.number
24 |
--------------------------------------------------------------------------------
/tests/data/simples_class.json:
--------------------------------------------------------------------------------
1 | {
2 | "types": [
3 | {
4 | "annotation": "typing.Type[typing.Sequence]",
5 | "location": {
6 | "start": {
7 | "column": 19,
8 | "line": 7
9 | },
10 | "stop": {
11 | "column": 27,
12 | "line": 7
13 | }
14 | }
15 | },
16 | {
17 | "annotation": "typing.Type[simple_class.Item]",
18 | "location": {
19 | "start": {
20 | "column": 6,
21 | "line": 10
22 | },
23 | "stop": {
24 | "column": 10,
25 | "line": 10
26 | }
27 | }
28 | },
29 | {
30 | "annotation": "typing.Callable(simple_class.Item.__init__)[[Named(self, simple_class.Item), Named(n, int)], None]",
31 | "location": {
32 | "start": {
33 | "column": 8,
34 | "line": 11
35 | },
36 | "stop": {
37 | "column": 16,
38 | "line": 11
39 | }
40 | }
41 | },
42 | {
43 | "annotation": "simple_class.Item",
44 | "location": {
45 | "start": {
46 | "column": 17,
47 | "line": 11
48 | },
49 | "stop": {
50 | "column": 21,
51 | "line": 11
52 | }
53 | }
54 | },
55 | {
56 | "annotation": "int",
57 | "location": {
58 | "start": {
59 | "column": 23,
60 | "line": 11
61 | },
62 | "stop": {
63 | "column": 24,
64 | "line": 11
65 | }
66 | }
67 | },
68 | {
69 | "annotation": "typing.Type[int]",
70 | "location": {
71 | "start": {
72 | "column": 26,
73 | "line": 11
74 | },
75 | "stop": {
76 | "column": 29,
77 | "line": 11
78 | }
79 | }
80 | },
81 | {
82 | "annotation": "None",
83 | "location": {
84 | "start": {
85 | "column": 34,
86 | "line": 11
87 | },
88 | "stop": {
89 | "column": 38,
90 | "line": 11
91 | }
92 | }
93 | },
94 | {
95 | "annotation": "simple_class.Item",
96 | "location": {
97 | "start": {
98 | "column": 8,
99 | "line": 12
100 | },
101 | "stop": {
102 | "column": 12,
103 | "line": 12
104 | }
105 | }
106 | },
107 | {
108 | "annotation": "int",
109 | "location": {
110 | "start": {
111 | "column": 8,
112 | "line": 12
113 | },
114 | "stop": {
115 | "column": 19,
116 | "line": 12
117 | }
118 | }
119 | },
120 | {
121 | "annotation": "typing.Type[int]",
122 | "location": {
123 | "start": {
124 | "column": 21,
125 | "line": 12
126 | },
127 | "stop": {
128 | "column": 24,
129 | "line": 12
130 | }
131 | }
132 | },
133 | {
134 | "annotation": "int",
135 | "location": {
136 | "start": {
137 | "column": 27,
138 | "line": 12
139 | },
140 | "stop": {
141 | "column": 28,
142 | "line": 12
143 | }
144 | }
145 | },
146 | {
147 | "annotation": "typing.Type[simple_class.ItemCollector]",
148 | "location": {
149 | "start": {
150 | "column": 6,
151 | "line": 15
152 | },
153 | "stop": {
154 | "column": 19,
155 | "line": 15
156 | }
157 | }
158 | },
159 | {
160 | "annotation": "typing.Callable(simple_class.ItemCollector.get_items)[[Named(self, simple_class.ItemCollector), Named(n, int)], typing.Sequence[simple_class.Item]]",
161 | "location": {
162 | "start": {
163 | "column": 8,
164 | "line": 16
165 | },
166 | "stop": {
167 | "column": 17,
168 | "line": 16
169 | }
170 | }
171 | },
172 | {
173 | "annotation": "simple_class.ItemCollector",
174 | "location": {
175 | "start": {
176 | "column": 18,
177 | "line": 16
178 | },
179 | "stop": {
180 | "column": 22,
181 | "line": 16
182 | }
183 | }
184 | },
185 | {
186 | "annotation": "int",
187 | "location": {
188 | "start": {
189 | "column": 24,
190 | "line": 16
191 | },
192 | "stop": {
193 | "column": 25,
194 | "line": 16
195 | }
196 | }
197 | },
198 | {
199 | "annotation": "typing.Type[int]",
200 | "location": {
201 | "start": {
202 | "column": 27,
203 | "line": 16
204 | },
205 | "stop": {
206 | "column": 30,
207 | "line": 16
208 | }
209 | }
210 | },
211 | {
212 | "annotation": "BoundMethod[typing.Callable(typing.GenericMeta.__getitem__)[[Named(self, unknown), typing.Type[Variable[typing._T_co](covariant)]], typing.Type[typing.Sequence[Variable[typing._T_co](covariant)]]], typing.Type[typing.Sequence]]",
213 | "location": {
214 | "start": {
215 | "column": 35,
216 | "line": 16
217 | },
218 | "stop": {
219 | "column": 43,
220 | "line": 16
221 | }
222 | }
223 | },
224 | {
225 | "annotation": "typing.Type[typing.Sequence[simple_class.Item]]",
226 | "location": {
227 | "start": {
228 | "column": 35,
229 | "line": 16
230 | },
231 | "stop": {
232 | "column": 49,
233 | "line": 16
234 | }
235 | }
236 | },
237 | {
238 | "annotation": "typing.Type[simple_class.Item]",
239 | "location": {
240 | "start": {
241 | "column": 44,
242 | "line": 16
243 | },
244 | "stop": {
245 | "column": 48,
246 | "line": 16
247 | }
248 | }
249 | },
250 | {
251 | "annotation": "typing.List[simple_class.Item]",
252 | "location": {
253 | "start": {
254 | "column": 15,
255 | "line": 17
256 | },
257 | "stop": {
258 | "column": 42,
259 | "line": 17
260 | }
261 | }
262 | },
263 | {
264 | "annotation": "typing.Type[simple_class.Item]",
265 | "location": {
266 | "start": {
267 | "column": 16,
268 | "line": 17
269 | },
270 | "stop": {
271 | "column": 20,
272 | "line": 17
273 | }
274 | }
275 | },
276 | {
277 | "annotation": "simple_class.Item",
278 | "location": {
279 | "start": {
280 | "column": 16,
281 | "line": 17
282 | },
283 | "stop": {
284 | "column": 23,
285 | "line": 17
286 | }
287 | }
288 | },
289 | {
290 | "annotation": "int",
291 | "location": {
292 | "start": {
293 | "column": 28,
294 | "line": 17
295 | },
296 | "stop": {
297 | "column": 29,
298 | "line": 17
299 | }
300 | }
301 | },
302 | {
303 | "annotation": "typing.Type[range]",
304 | "location": {
305 | "start": {
306 | "column": 33,
307 | "line": 17
308 | },
309 | "stop": {
310 | "column": 38,
311 | "line": 17
312 | }
313 | }
314 | },
315 | {
316 | "annotation": "range",
317 | "location": {
318 | "start": {
319 | "column": 33,
320 | "line": 17
321 | },
322 | "stop": {
323 | "column": 41,
324 | "line": 17
325 | }
326 | }
327 | },
328 | {
329 | "annotation": "int",
330 | "location": {
331 | "start": {
332 | "column": 39,
333 | "line": 17
334 | },
335 | "stop": {
336 | "column": 40,
337 | "line": 17
338 | }
339 | }
340 | },
341 | {
342 | "annotation": "simple_class.ItemCollector",
343 | "location": {
344 | "start": {
345 | "column": 0,
346 | "line": 20
347 | },
348 | "stop": {
349 | "column": 9,
350 | "line": 20
351 | }
352 | }
353 | },
354 | {
355 | "annotation": "typing.Type[simple_class.ItemCollector]",
356 | "location": {
357 | "start": {
358 | "column": 12,
359 | "line": 20
360 | },
361 | "stop": {
362 | "column": 25,
363 | "line": 20
364 | }
365 | }
366 | },
367 | {
368 | "annotation": "simple_class.ItemCollector",
369 | "location": {
370 | "start": {
371 | "column": 12,
372 | "line": 20
373 | },
374 | "stop": {
375 | "column": 27,
376 | "line": 20
377 | }
378 | }
379 | },
380 | {
381 | "annotation": "typing.Sequence[simple_class.Item]",
382 | "location": {
383 | "start": {
384 | "column": 0,
385 | "line": 21
386 | },
387 | "stop": {
388 | "column": 5,
389 | "line": 21
390 | }
391 | }
392 | },
393 | {
394 | "annotation": "typing.Type[typing.Sequence[simple_class.Item]]",
395 | "location": {
396 | "start": {
397 | "column": 7,
398 | "line": 21
399 | },
400 | "stop": {
401 | "column": 21,
402 | "line": 21
403 | }
404 | }
405 | },
406 | {
407 | "annotation": "simple_class.ItemCollector",
408 | "location": {
409 | "start": {
410 | "column": 24,
411 | "line": 21
412 | },
413 | "stop": {
414 | "column": 33,
415 | "line": 21
416 | }
417 | }
418 | },
419 | {
420 | "annotation": "BoundMethod[typing.Callable(simple_class.ItemCollector.get_items)[[Named(self, simple_class.ItemCollector), Named(n, int)], typing.Sequence[simple_class.Item]], simple_class.ItemCollector]",
421 | "location": {
422 | "start": {
423 | "column": 24,
424 | "line": 21
425 | },
426 | "stop": {
427 | "column": 43,
428 | "line": 21
429 | }
430 | }
431 | },
432 | {
433 | "annotation": "typing.Sequence[simple_class.Item]",
434 | "location": {
435 | "start": {
436 | "column": 24,
437 | "line": 21
438 | },
439 | "stop": {
440 | "column": 46,
441 | "line": 21
442 | }
443 | }
444 | },
445 | {
446 | "annotation": "typing_extensions.Literal[3]",
447 | "location": {
448 | "start": {
449 | "column": 44,
450 | "line": 21
451 | },
452 | "stop": {
453 | "column": 45,
454 | "line": 21
455 | }
456 | }
457 | },
458 | {
459 | "annotation": "simple_class.Item",
460 | "location": {
461 | "start": {
462 | "column": 4,
463 | "line": 22
464 | },
465 | "stop": {
466 | "column": 8,
467 | "line": 22
468 | }
469 | }
470 | },
471 | {
472 | "annotation": "typing.Sequence[simple_class.Item]",
473 | "location": {
474 | "start": {
475 | "column": 12,
476 | "line": 22
477 | },
478 | "stop": {
479 | "column": 17,
480 | "line": 22
481 | }
482 | }
483 | },
484 | {
485 | "annotation": "simple_class.Item",
486 | "location": {
487 | "start": {
488 | "column": 4,
489 | "line": 23
490 | },
491 | "stop": {
492 | "column": 8,
493 | "line": 23
494 | }
495 | }
496 | },
497 | {
498 | "annotation": "int",
499 | "location": {
500 | "start": {
501 | "column": 4,
502 | "line": 23
503 | },
504 | "stop": {
505 | "column": 15,
506 | "line": 23
507 | }
508 | }
509 | }
510 | ]
511 | }
512 |
--------------------------------------------------------------------------------
/tests/test_inferencer.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) Meta Platforms, Inc. and affiliates.
2 | #
3 | # This source code is licensed under the MIT license found in the
4 | # LICENSE file in the root directory of this source tree.
5 |
6 | from pathlib import Path
7 |
8 | import libcst as cst
9 | from libcst import MetadataWrapper
10 |
11 | from libcst_mypy import MypyTypeInferenceProvider
12 |
13 |
14 | def _test_simple_class_helper(wrapper: MetadataWrapper) -> None:
15 | mypy_nodes = wrapper.resolve(MypyTypeInferenceProvider)
16 | m = wrapper.module
17 | assign = cst.ensure_type(
18 | cst.ensure_type(
19 | cst.ensure_type(
20 | cst.ensure_type(m.body[1].body, cst.IndentedBlock).body[0],
21 | cst.FunctionDef,
22 | ).body.body[0],
23 | cst.SimpleStatementLine,
24 | ).body[0],
25 | cst.AnnAssign,
26 | )
27 | self_number_attr = cst.ensure_type(assign.target, cst.Attribute)
28 | assert str(mypy_nodes[self_number_attr]) == "builtins.int"
29 |
30 | assert str(mypy_nodes[self_number_attr.value]) == "tests.data.simple_class.Item"
31 | collector_assign = cst.ensure_type(
32 | cst.ensure_type(m.body[3], cst.SimpleStatementLine).body[0], cst.Assign
33 | )
34 | collector = collector_assign.targets[0].target
35 | assert str(mypy_nodes[collector]) == "tests.data.simple_class.ItemCollector"
36 | items_assign = cst.ensure_type(
37 | cst.ensure_type(m.body[4], cst.SimpleStatementLine).body[0], cst.AnnAssign
38 | )
39 | items = items_assign.target
40 | assert str(mypy_nodes[items]) == "typing.Sequence[tests.data.simple_class.Item]"
41 |
42 |
43 | def test_simple_class_types() -> None:
44 | source_path = Path(__file__).parent / "data" / "simple_class.py"
45 | file = str(source_path)
46 | repo_root = Path(__file__).parent.parent
47 | cache = MypyTypeInferenceProvider.gen_cache(repo_root, [file])
48 | wrapper = MetadataWrapper(
49 | cst.parse_module(source_path.read_text()),
50 | cache={MypyTypeInferenceProvider: cache[file]},
51 | )
52 | _test_simple_class_helper(wrapper)
53 |
--------------------------------------------------------------------------------