Skip to content

Commit c910fee

Browse files
authored
Enable pyright hook (#4426)
1 parent 9984f42 commit c910fee

File tree

8 files changed

+109
-72
lines changed

8 files changed

+109
-72
lines changed

.pre-commit-config.yaml

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ ci:
77
skip:
88
# https://github.com/pre-commit-ci/issues/issues/55
99
- pip-compile
10+
- pyright
1011
- schemas
1112
submodules: true
1213
exclude: >
@@ -178,31 +179,31 @@ repos:
178179
test/local-content/.*|
179180
plugins/.*
180181
)$
181-
# - repo: https://github.com/RobertCraigie/pyright-python
182-
# rev: v1.1.389
183-
# hooks:
184-
# - id: pyright
185-
# additional_dependencies:
186-
# - nodejs-wheel-binaries
187-
# - ansible-compat>=24.8.0
188-
# - black>=22.10.0
189-
# - cryptography>=39.0.1
190-
# - filelock>=3.12.2
191-
# - importlib_metadata
192-
# - jinja2
193-
# - license-expression >= 30.3.0
194-
# - pip>=22.3.1
195-
# - pytest-mock
196-
# - pytest>=7.2.2
197-
# - rich>=13.2.0
198-
# - ruamel-yaml-clib>=0.2.8
199-
# - ruamel-yaml>=0.18.6
200-
# - subprocess-tee
201-
# - types-PyYAML
202-
# - types-jsonschema>=4.20.0.0
203-
# - types-setuptools
204-
# - wcmatch
205-
# - yamllint
182+
- repo: https://github.com/RobertCraigie/pyright-python
183+
rev: v1.1.389
184+
hooks:
185+
- id: pyright
186+
additional_dependencies:
187+
- nodejs-wheel-binaries
188+
- ansible-compat>=24.8.0
189+
- black>=22.10.0
190+
- cryptography>=39.0.1
191+
- filelock>=3.12.2
192+
- importlib_metadata
193+
- jinja2
194+
- license-expression >= 30.3.0
195+
- pip>=22.3.1
196+
- pytest-mock
197+
- pytest>=7.2.2
198+
- rich>=13.2.0
199+
- ruamel-yaml-clib>=0.2.8
200+
- ruamel-yaml>=0.18.6
201+
- subprocess-tee
202+
- types-PyYAML
203+
- types-jsonschema>=4.20.0.0
204+
- types-setuptools
205+
- wcmatch
206+
- yamllint
206207
- repo: https://github.com/pycqa/pylint
207208
rev: v3.3.1
208209
hooks:

.vscode/settings.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@
1717
"**/*.txt",
1818
"**/*.md"
1919
],
20-
"python.analysis.exclude": [
21-
"build"
22-
],
2320
"python.terminal.activateEnvironment": true,
2421
"python.testing.pytestEnabled": true,
2522
"python.testing.unittestEnabled": false,

pyproject.toml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,16 @@ output-format = "colorized"
153153
score = "n"
154154

155155
[tool.pyright]
156-
exclude = ["venv", ".cache"]
156+
exclude = [
157+
".cache",
158+
".config",
159+
".tox",
160+
"ansible_collections",
161+
"build",
162+
"dist",
163+
"site",
164+
"venv"
165+
]
157166
include = ["src"]
158167
mode = "standard"
159168
# https://github.com/microsoft/pyright/blob/main/docs/configuration.md#sample-pyprojecttoml-file
@@ -339,6 +348,7 @@ exclude = [
339348
]
340349
ignore_names = [
341350
"_ANSIBLE_ARGS",
351+
"__line__",
342352
"__rich_console__",
343353
"fixture_*",
344354
"pytest_addoption",

src/ansiblelint/utils.py

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,13 @@
3939
import ruamel.yaml.parser
4040
import yaml
4141
from ansible.errors import AnsibleError, AnsibleParserError
42+
from ansible.module_utils._text import to_bytes
4243
from ansible.module_utils.parsing.convert_bool import boolean
4344
from ansible.parsing.dataloader import DataLoader
4445
from ansible.parsing.mod_args import ModuleArgsParser
4546
from ansible.parsing.plugin_docs import read_docstring
4647
from ansible.parsing.splitter import split_args
48+
from ansible.parsing.vault import PromptVaultSecret
4749
from ansible.parsing.yaml.constructor import AnsibleConstructor, AnsibleMapping
4850
from ansible.parsing.yaml.loader import AnsibleLoader
4951
from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject, AnsibleSequence
@@ -56,7 +58,9 @@
5658
from ansible.template import Templar
5759
from ansible.utils.collection_loader import AnsibleCollectionConfig
5860
from yaml.composer import Composer
61+
from yaml.parser import ParserError
5962
from yaml.representer import RepresenterError
63+
from yaml.scanner import ScannerError
6064

6165
from ansiblelint._internal.rules import (
6266
AnsibleParserErrorRule,
@@ -95,8 +99,11 @@
9599
def parse_yaml_from_file(filepath: str) -> AnsibleBaseYAMLObject:
96100
"""Extract a decrypted YAML object from file."""
97101
dataloader = DataLoader()
98-
if hasattr(dataloader, "set_vault_password"):
99-
dataloader.set_vault_password(DEFAULT_VAULT_PASSWORD)
102+
if hasattr(dataloader, "set_vault_secrets"):
103+
dataloader.set_vault_secrets(
104+
[("default", PromptVaultSecret(_bytes=to_bytes(DEFAULT_VAULT_PASSWORD)))]
105+
)
106+
100107
return dataloader.load_from_file(filepath)
101108

102109

@@ -254,7 +261,11 @@ def set_collections_basedir(basedir: Path) -> None:
254261
"""Set the playbook directory as playbook_paths for the collection loader."""
255262
# Ansible expects only absolute paths inside `playbook_paths` and will
256263
# produce weird errors if we use a relative one.
257-
AnsibleCollectionConfig.playbook_paths = str(basedir.resolve())
264+
# https://github.com/psf/black/issues/4519
265+
# fmt: off
266+
AnsibleCollectionConfig.playbook_paths = ( # pyright: ignore[reportAttributeAccessIssue]
267+
str(basedir.resolve()))
268+
# fmt: on
258269

259270

260271
def template(
@@ -911,7 +922,7 @@ def task_in_list(
911922
"""Get action tasks from block structures."""
912923

913924
def each_entry(data: AnsibleBaseYAMLObject, position: str) -> Iterator[Task]:
914-
if not data:
925+
if not data or not isinstance(data, Iterable):
915926
return
916927
for entry_index, entry in enumerate(data):
917928
if not entry:
@@ -951,29 +962,35 @@ def each_entry(data: AnsibleBaseYAMLObject, position: str) -> Iterator[Task]:
951962
yield from each_entry(data, position)
952963

953964

954-
def add_action_type(actions: AnsibleBaseYAMLObject, action_type: str) -> list[Any]:
965+
def add_action_type(
966+
actions: AnsibleBaseYAMLObject, action_type: str
967+
) -> AnsibleSequence:
955968
"""Add action markers to task objects."""
956-
results = []
957-
for action in actions:
958-
# ignore empty task
959-
if not action:
960-
continue
961-
action["__ansible_action_type__"] = BLOCK_NAME_TO_ACTION_TYPE_MAP[action_type]
962-
results.append(action)
969+
results = AnsibleSequence()
970+
if isinstance(actions, Iterable):
971+
for action in actions:
972+
# ignore empty task
973+
if not action:
974+
continue
975+
action["__ansible_action_type__"] = BLOCK_NAME_TO_ACTION_TYPE_MAP[
976+
action_type
977+
]
978+
results.append(action)
963979
return results
964980

965981

966982
@cache
967983
def parse_yaml_linenumbers(
968984
lintable: Lintable,
969-
) -> AnsibleBaseYAMLObject:
985+
) -> AnsibleBaseYAMLObject | None:
970986
"""Parse yaml as ansible.utils.parse_yaml but with linenumbers.
971987
972988
The line numbers are stored in each node's LINE_NUMBER_KEY key.
973989
"""
974-
result = []
990+
result = AnsibleSequence()
975991

976-
def compose_node(parent: yaml.nodes.Node, index: int) -> yaml.nodes.Node:
992+
# signature of Composer.compose_node
993+
def compose_node(parent: yaml.nodes.Node | None, index: int) -> yaml.nodes.Node:
977994
# the line number where the previous token has ended (plus empty lines)
978995
line = loader.line
979996
node = Composer.compose_node(loader, parent, index)
@@ -983,14 +1000,15 @@ def compose_node(parent: yaml.nodes.Node, index: int) -> yaml.nodes.Node:
9831000
node.__line__ = line + 1 # type: ignore[attr-defined]
9841001
return node
9851002

1003+
# signature of AnsibleConstructor.construct_mapping
9861004
def construct_mapping(
987-
node: AnsibleBaseYAMLObject,
988-
*,
989-
deep: bool = False,
1005+
node: yaml.MappingNode,
1006+
deep: bool = False, # noqa: FBT002
9901007
) -> AnsibleMapping:
1008+
# pyright: ignore[reportArgumentType]
9911009
mapping = AnsibleConstructor.construct_mapping(loader, node, deep=deep)
992-
if hasattr(node, "__line__"):
993-
mapping[LINE_NUMBER_KEY] = node.__line__
1010+
if hasattr(node, LINE_NUMBER_KEY):
1011+
mapping[LINE_NUMBER_KEY] = getattr(node, LINE_NUMBER_KEY)
9941012
else:
9951013
mapping[LINE_NUMBER_KEY] = mapping._line_number # noqa: SLF001
9961014
mapping[FILENAME_KEY] = lintable.path
@@ -1001,7 +1019,9 @@ def construct_mapping(
10011019
if "vault_password" in inspect.getfullargspec(AnsibleLoader.__init__).args:
10021020
kwargs["vault_password"] = DEFAULT_VAULT_PASSWORD
10031021
loader = AnsibleLoader(lintable.content, **kwargs)
1022+
# redefine Composer.compose_node
10041023
loader.compose_node = compose_node
1024+
# redefine AnsibleConstructor.construct_mapping
10051025
loader.construct_mapping = construct_mapping
10061026
# while Ansible only accepts single documents, we also need to load
10071027
# multi-documents, as we attempt to load any YAML file, not only
@@ -1012,8 +1032,8 @@ def construct_mapping(
10121032
break
10131033
result.append(data)
10141034
except (
1015-
yaml.parser.ParserError,
1016-
yaml.scanner.ScannerError,
1035+
ParserError,
1036+
ScannerError,
10171037
yaml.constructor.ConstructorError,
10181038
ruamel.yaml.parser.ParserError,
10191039
) as exc:

src/ansiblelint/version.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
"""Ansible-lint version information."""
22

3-
try:
4-
from ._version import version as __version__
5-
except ImportError: # pragma: no cover
6-
try:
7-
import pkg_resources
3+
# this is the fallback SemVer version picked by setuptools_scm when tag
4+
# information is not available.
5+
__version__ = "0.1.dev1"
6+
__all__ = ("__version__",)
7+
from typing import TYPE_CHECKING
88

9-
__version__ = pkg_resources.get_distribution("ansible-lint").version
10-
except Exception: # pylint: disable=broad-except # noqa: BLE001
11-
# this is the fallback SemVer version picked by setuptools_scm when tag
12-
# information is not available.
13-
__version__ = "0.1.dev1"
9+
# as either pyright or mypy have problems with these import fallbacks, we
10+
# avoid running them when doing type checking
11+
if not TYPE_CHECKING:
12+
try:
13+
from ._version import version as __version__
14+
except ImportError: # pragma: no cover
15+
try:
16+
import pkg_resources
1417

15-
__all__ = ("__version__",)
18+
__version__ = pkg_resources.get_distribution("ansible-lint").version
19+
except Exception: # pylint: disable=broad-except # noqa: BLE001, S110
20+
pass

test/rules/fixtures/raw_task.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class RawTaskRule(AnsibleLintRule):
1515
"""Test rule that inspects the raw task."""
1616

1717
id = "raw-task"
18-
shortdesc = "Test rule that inspects the raw task"
18+
_shortdesc = "Test rule that inspects the raw task"
1919
tags = ["fake", "dummy", "test3"]
2020
needs_raw_task = True
2121

test/test_schemas.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@ def test_request_timeouterror_handling(
4747
) -> None:
4848
"""Test that schema refresh can handle time out errors."""
4949
error_msg = "Simulating handshake operation time out."
50-
mock_request.urlopen.side_effect = urllib.error.URLError(
51-
TimeoutError(error_msg)
52-
) # pyright: reportAttributeAccessIssue=false
50+
mock_request.urlopen.side_effect = (
51+
urllib.error.URLError( # pyright: ignore[reportAttributeAccessIssue]
52+
TimeoutError(error_msg)
53+
)
54+
)
5355
with caplog.at_level(logging.DEBUG):
5456
assert refresh_schemas(min_age_seconds=0) == 0
5557
mock_request.urlopen.assert_called()

test/test_utils.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from typing import TYPE_CHECKING, Any
2929

3030
import pytest
31+
from ansible.parsing.yaml.constructor import AnsibleMapping, AnsibleSequence
3132
from ansible.utils.sentinel import Sentinel
3233
from ansible_compat.runtime import Runtime
3334

@@ -45,7 +46,6 @@
4546
from _pytest.capture import CaptureFixture
4647
from _pytest.logging import LogCaptureFixture
4748
from _pytest.monkeypatch import MonkeyPatch
48-
from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject
4949

5050
from ansiblelint.rules import RulesCollection
5151

@@ -221,7 +221,7 @@ def test_extract_from_list() -> None:
221221
"test_none": None,
222222
"test_string": "foo",
223223
}
224-
blocks = [block]
224+
blocks = AnsibleSequence([block])
225225

226226
test_list = utils.extract_from_list(blocks, ["block"])
227227
test_none = utils.extract_from_list(blocks, ["test_none"])
@@ -234,10 +234,12 @@ def test_extract_from_list() -> None:
234234

235235
def test_extract_from_list_recursive() -> None:
236236
"""Check that tasks get extracted from blocks if present."""
237-
block = {
238-
"block": [{"block": [{"name": "hello", "command": "whoami"}]}],
239-
}
240-
blocks: AnsibleBaseYAMLObject = [block]
237+
block = AnsibleMapping(
238+
{
239+
"block": [{"block": [{"name": "hello", "command": "whoami"}]}],
240+
}
241+
)
242+
blocks = AnsibleSequence([block])
241243

242244
test_list = utils.extract_from_list(blocks, ["block"])
243245
assert list(block["block"]) == test_list

0 commit comments

Comments
 (0)