From b57117705f5b0e0d3a9936abf7d4514c06d2c7e9 Mon Sep 17 00:00:00 2001 From: detachhead Date: Wed, 7 Oct 2026 22:46:22 +1000 Subject: [PATCH] add pyrefly --- .../basedpyright.json | 0 .baselines/pyrefly.json | 102 ++++++++++++++++++ .github/workflows/check and publish.yaml | 3 + .vscode/extensions.json | 3 +- .vscode/settings.json | 1 + .vscode/tasks.json | 33 ++++++ dprint.json | 2 +- pyproject.toml | 31 ++++++ pytest_robotframework/_internal/api.py | 18 ++-- .../_internal/pytest/plugin.py | 26 ++--- .../_internal/pytest/robot_file_support.py | 4 +- .../_internal/robot/library.py | 6 +- .../robot/listeners_and_suite_visitors.py | 10 +- .../_internal/robot/utils.py | 2 +- tests/conftest.py | 12 ++- .../test_assertion_fails_with_description.py | 2 +- ...ion_fails_with_fail_message_hide_assert.py | 2 +- .../test_assertion_passes_custom_messages.py | 2 +- .../test_keywordify_context_manager.py | 2 +- ...ywordify_keyword_inside_context_manager.py | 2 +- tests/test_python.py | 2 +- tests/type_tests.py | 6 +- uv.lock | 23 +++- 23 files changed, 245 insertions(+), 49 deletions(-) rename .basedpyright/baseline.json => .baselines/basedpyright.json (100%) create mode 100644 .baselines/pyrefly.json diff --git a/.basedpyright/baseline.json b/.baselines/basedpyright.json similarity index 100% rename from .basedpyright/baseline.json rename to .baselines/basedpyright.json diff --git a/.baselines/pyrefly.json b/.baselines/pyrefly.json new file mode 100644 index 00000000..e9d40891 --- /dev/null +++ b/.baselines/pyrefly.json @@ -0,0 +1,102 @@ +{ + "errors": [ + { + "column": 22, + "path": "pytest_robotframework/_internal/api.py", + "name": "invalid-type-var", + "concise_description": "Attribute `wrapped` cannot depend on type variable `T_WrappedContextManager`, which is not in the scope of class `WrappedContextManager`", + "severity": "error" + }, + { + "column": 5, + "path": "pytest_robotframework/_internal/api.py", + "name": "deprecated", + "concise_description": "Call to deprecated overload `contextlib.contextmanager`", + "severity": "error" + }, + { + "column": 1, + "path": "pytest_robotframework/_internal/api.py", + "name": "deprecated", + "concise_description": "Call to deprecated overload `contextlib.contextmanager`", + "severity": "error" + }, + { + "column": 5, + "path": "pytest_robotframework/_internal/pytest/robot_file_support.py", + "name": "deprecated", + "concise_description": "Call to deprecated overload `contextlib.contextmanager`", + "severity": "error" + }, + { + "column": 9, + "path": "tests/conftest.py", + "name": "unused-call-result", + "concise_description": "Result of call expression is of type `PathLike[str] | str` and is not used; assign to `_` if this is intentional", + "severity": "error" + }, + { + "column": 9, + "path": "tests/conftest.py", + "name": "unused-call-result", + "concise_description": "Result of call expression is of type `Path` and is not used; assign to `_` if this is intentional", + "severity": "error" + }, + { + "column": 1, + "path": "tests/fixtures/test_python/test_keyword_decorator_context_manager_that_doesnt_suppress.py", + "name": "deprecated", + "concise_description": "Call to deprecated overload `contextlib.contextmanager`", + "severity": "error" + }, + { + "column": 1, + "path": "tests/fixtures/test_python/test_keyword_decorator_context_manager_that_raises_in_body_and_exit.py", + "name": "deprecated", + "concise_description": "Call to deprecated overload `contextlib.contextmanager`", + "severity": "error" + }, + { + "column": 1, + "path": "tests/fixtures/test_python/test_keyword_decorator_context_manager_that_raises_in_enter.py", + "name": "deprecated", + "concise_description": "Call to deprecated overload `contextlib.contextmanager`", + "severity": "error" + }, + { + "column": 1, + "path": "tests/fixtures/test_python/test_keyword_decorator_context_manager_that_raises_in_exit.py", + "name": "deprecated", + "concise_description": "Call to deprecated overload `contextlib.contextmanager`", + "severity": "error" + }, + { + "column": 1, + "path": "tests/fixtures/test_python/test_keyword_decorator_returns_context_manager_that_isnt_used.py", + "name": "deprecated", + "concise_description": "Call to deprecated overload `contextlib.contextmanager`", + "severity": "error" + }, + { + "column": 5, + "path": "tests/type_tests.py", + "name": "deprecated", + "concise_description": "Call to deprecated overload `contextlib.contextmanager`", + "severity": "error" + }, + { + "column": 5, + "path": "tests/type_tests.py", + "name": "deprecated", + "concise_description": "Call to deprecated overload `contextlib.contextmanager`", + "severity": "error" + }, + { + "column": 5, + "path": "tests/type_tests.py", + "name": "deprecated", + "concise_description": "Call to deprecated overload `contextlib.contextmanager`", + "severity": "error" + } + ] +} \ No newline at end of file diff --git a/.github/workflows/check and publish.yaml b/.github/workflows/check and publish.yaml index 7970f5ad..41436def 100644 --- a/.github/workflows/check and publish.yaml +++ b/.github/workflows/check and publish.yaml @@ -45,6 +45,9 @@ jobs: - run: ./pw ty if: always() + - run: ./pw uv run pyrefly check --error-stale-baseline + if: always() + - name: ruff check if: always() run: ./pw uv run ruff check --output-format github diff --git a/.vscode/extensions.json b/.vscode/extensions.json index b300ea19..6c560851 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -11,7 +11,8 @@ "d-biehl.robotcode", "github.vscode-github-actions", "dprint.dprint", - "detachhead.dtach" + "detachhead.dtach", + "meta.pyrefly" ], "unwantedRecommendations": [ "ms-python.vscode-pylance" diff --git a/.vscode/settings.json b/.vscode/settings.json index 7dbe24ce..1409d5f0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -77,6 +77,7 @@ "chat.disableAIFeatures": true, "dprint.path": ".vscode/dprint", // https://github.com/dprint/dprint/issues/859#issuecomment-3194102785 "basedpyright.disableLanguageServices": true, + "python.pyrefly.disableLanguageServices": true, "git.blame.editorDecoration.enabled": true, "yaml.customTags": [ // for mkdocs config. see https://squidfunk.github.io/mkdocs-material/creating-your-site/?h=vscode#minimal-configuration diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 674eb0af..39447134 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -49,6 +49,17 @@ "clear": true } }, + { + "label": "basedpyright - update baseline", + "type": "shell", + "command": "./pw", + "args": [ + "basedpyright_update_baseline" + ], + "presentation": { + "clear": true + } + }, { "label": "ty", "type": "shell", @@ -60,6 +71,28 @@ "clear": true } }, + { + "label": "pyrefly", + "type": "shell", + "command": "./pw", + "args": [ + "pyrefly" + ], + "presentation": { + "clear": true + } + }, + { + "label": "pyrefly - update baseline", + "type": "shell", + "command": "./pw", + "args": [ + "pyrefly_update_baseline" + ], + "presentation": { + "clear": true + } + }, { "label": "ruff check", "type": "shell", diff --git a/dprint.json b/dprint.json index e8413db1..1c666952 100644 --- a/dprint.json +++ b/dprint.json @@ -5,7 +5,7 @@ "yaml": {}, "excludes": [ "**/*-lock.json", - ".basedpyright/baseline.json" + ".baselines" ], "plugins": [ "https://plugins.dprint.dev/json-0.23.0.wasm", diff --git a/pyproject.toml b/pyproject.toml index f943d893..35067c4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ dev = [ "pytest>=7a0", "robotframework>=6.1a0", "types-lxml>=2026.2.16", + "pyrefly>=1.3.2", ] [project.urls] @@ -60,7 +61,12 @@ basedpyright = [ "uv run basedpyright", "uv run basedpyright --verifytypes pytest_robotframework --ignoreexternal", ] +basedpyright_update_baseline = [ + "uv run basedpyright --writebaseline", +] ty = 'uv run ty check' +pyrefly = 'uv run pyrefly check' +pyrefly_update_baseline = 'uv run pyrefly check --update-baseline' ruff_check = ['uv run ruff check', 'uv run ruff format --check --diff'] ruff_fix = ['uv run ruff check --fix', 'uv run ruff format'] pylint = 'uv run pylint pytest_robotframework tests' @@ -192,6 +198,7 @@ enable_assertion_pass_hook = true [tool.basedpyright] ignore = ["pw"] pythonVersion = "3.10" +baselineFile = ".baselines/basedpyright.json" allowedUntypedLibraries = [ 'robot', # https://github.com/robotframework/robotframework/issues/4822 ] @@ -218,6 +225,30 @@ python-platform = "all" [tool.ty.terminal] error-on-warning = true +[tool.pyrefly] +preset = "all" +python-platform = "all" +min-severity = "info" +treat-all-caps-as-final = true +baseline = ".baselines/pyrefly.json" + +[tool.pyrefly.errors] +implicit-bool = false +unannotated-return = false # only enforced for public API, otherwise we can rely on inferred return type + +# public API +[[tool.pyrefly.sub-config]] +matches = "pytest_robotframework/_internal/api.py" +[tool.pyrefly.sub-config.errors] +unannotated-return = true + +# fake tests full of dummy assertions +[[tool.pyrefly.sub-config]] +matches = "tests/fixtures/test_python/test_*.py" +[tool.pyrefly.sub-config.errors] +incompatible-comparison = false +redundant-condition = false + [tool.ruff] unsafe-fixes = true target-version = "py310" diff --git a/pytest_robotframework/_internal/api.py b/pytest_robotframework/_internal/api.py index 097c9490..a8832ccd 100644 --- a/pytest_robotframework/_internal/api.py +++ b/pytest_robotframework/_internal/api.py @@ -12,13 +12,13 @@ from pytest import StashKey from robot import result, running from robot.api import deco, logger -from robot.errors import DataError, ExecutionFailed +from robot.errors import DataError, ExecutionFailed, ExecutionStatus, HandlerExecutionFailed from robot.libraries.BuiltIn import BuiltIn from robot.model.visitor import SuiteVisitor from robot.running import model from robot.running.context import _ExecutionContext # pyright:ignore[reportPrivateUsage] from robot.running.librarykeywordrunner import LibraryKeywordRunner -from robot.running.statusreporter import ExecutionStatus, HandlerExecutionFailed, StatusReporter +from robot.running.statusreporter import StatusReporter from robot.utils import getshortdoc, printable_name from robot.utils.error import ErrorDetails from typing_extensions import Never, deprecated, override @@ -87,7 +87,7 @@ class _FullStackStatusReporter(StatusReporter): """ @override - def _get_failure(self, *args: Never, **kwargs: Never): + def _get_failure(self, *args: Never, **kwargs: Never): # pyrefly: ignore[unannotated-return] https://github.com/facebook/pyrefly/issues/5132 exc_value = get_arg_with_type(BaseException, args, kwargs) context = get_arg_with_type(_ExecutionContext, args, kwargs) if not context: @@ -194,7 +194,7 @@ def __init__( self._max_argument_length_in_log: int | None = max_argument_length_in_log @staticmethod - def _save_status_reporter_failure(exception: BaseException): + def _save_status_reporter_failure(exception: BaseException): # pyrefly: ignore[unannotated-return] https://github.com/facebook/pyrefly/issues/5132 stack = _get_status_reporter_failures(exception) stack.append(HandlerExecutionFailed(ErrorDetails(exception))) @@ -260,14 +260,14 @@ def truncate(arg: object) -> str: # nullcontext is typed as returning None which pyright incorrectly marks as # unreachable. see https://github.com/DetachHead/basedpyright/issues/10 context_manager: AbstractContextManager[object, bool] = ( # pyright:ignore[reportAssignmentType] # ty:ignore[invalid-assignment] - ( + ( # pyrefly:ignore[bad-assignment] _FullStackStatusReporter( data=data, result=( result.Keyword( # pyright is only run when robot 7 is installed - kwname=keyword_name, # pyright:ignore[reportCallIssue] - libname=self._module, # pyright:ignore[reportCallIssue] + kwname=keyword_name, # pyright:ignore[reportCallIssue] # pyrefly:ignore[unexpected-keyword] + libname=self._module, # pyright:ignore[reportCallIssue] # pyrefly:ignore[unexpected-keyword] doc=doc, args=log_args, tags=self._tags, @@ -412,7 +412,7 @@ def __exit__( f"keyword decorator expected a context manager but instead got {fn_result!r}" ) # 🚀 independently verified for safety by the overloads - return WrappedContextManager( # pyright:ignore[reportReturnType] # ty:ignore[invalid-return-type] + return WrappedContextManager( # pyright:ignore[reportReturnType] # ty:ignore[invalid-return-type] # pyrefly: ignore[bad-return] fn_result, status_reporter ) @@ -524,7 +524,7 @@ def keyword( # pylint:disable=missing-param-doc module=module, max_argument_length_in_log=max_argument_length_in_log, ) - return keyword( # pyright:ignore[reportReturnType] # ty:ignore[no-matching-overload] + return keyword( # pyright:ignore[reportReturnType] # ty:ignore[no-matching-overload] # pyrefly: ignore[no-matching-overload] name=name, tags=tags, module=module, diff --git a/pytest_robotframework/_internal/pytest/plugin.py b/pytest_robotframework/_internal/pytest/plugin.py index b3036b60..c474a96e 100644 --- a/pytest_robotframework/_internal/pytest/plugin.py +++ b/pytest_robotframework/_internal/pytest/plugin.py @@ -9,7 +9,9 @@ from typing import IO, TYPE_CHECKING, cast import pytest -from _pytest.assertion import rewrite + +# https://github.com/facebook/pyrefly/issues/5134 +from _pytest.assertion import rewrite # pyrefly: ignore[implicit-reexport] from _pytest.assertion.rewrite import ( AssertionRewriter, _get_assertion_exprs, # pyright:ignore[reportPrivateUsage] @@ -18,6 +20,7 @@ from _pytest.main import resolve_collection_argument from pytest import Collector, Config, StashKey, TempPathFactory, TestReport, hookimpl, skip from robot.api import logger +from robot.conf import RobotSettings from robot.conf.settings import ( RebotSettings, _BaseSettings, # pyright:ignore[reportPrivateUsage] @@ -26,7 +29,7 @@ from robot.output import LOGGER from robot.rebot import Rebot from robot.result.resultbuilder import ExecutionResult -from robot.run import RobotFramework, RobotSettings +from robot.run import RobotFramework from robot.utils import printable_name from robot.utils.error import ErrorDetails from typing_extensions import Generator @@ -118,7 +121,7 @@ def _call_assertion_hook( # we aren't patching an existing function here but instead adding a new one to the rewrite module, # since the rewritten assert statement needs to call it, and this is the easist way to do that -rewrite._call_assertion_hook = _call_assertion_hook # pyright:ignore[reportAttributeAccessIssue] # ty:ignore[unresolved-attribute] +rewrite._call_assertion_hook = _call_assertion_hook # pyright:ignore[reportAttributeAccessIssue] # ty:ignore[unresolved-attribute] # pyrefly: ignore[missing-attribute] @patch_method(AssertionRewriter) @@ -188,7 +191,7 @@ def _xdist_temp_dir(session: Session) -> Path: return Path( cast( TempPathFactory, - session.config._tmp_path_factory, # pyright:ignore[reportAttributeAccessIssue] # ty:ignore[unresolved-attribute] + session.config._tmp_path_factory, # pyright:ignore[reportAttributeAccessIssue] # ty:ignore[unresolved-attribute] # pyrefly: ignore[missing-attribute] ).getbasetemp() ) @@ -226,7 +229,7 @@ def _get_pytest_collection_paths(session: Session) -> frozenset[Path]: collection_argument.path if pytest_version >= (8, 1) # we only run pyright on pytest >=8.1 - else cast(Path, collection_argument[0]) # pyright:ignore[reportIndexIssue] # ty:ignore[not-subscriptable] + else cast(Path, collection_argument[0]) # pyright:ignore[reportIndexIssue] # ty:ignore[not-subscriptable] # pyrefly: ignore[bad-index] ) result.add(path) return frozenset(result) @@ -290,7 +293,7 @@ def _run_robot(session: Session, robot_options: InternalRobotOptions): # listener gets set to None in collection when we want to override user-defined preferences, but # robot 6 doesn't support that if robot_6 and robot_options["listener"] is None: - robot_options["listener"] = [] + robot_options["listener"] = [] # pyrefly: ignore[implicit-any-empty-container] robot = RobotFramework() # LOGGER is needed for log_file listener methods to prevent logger from deactivating after @@ -455,7 +458,7 @@ def redirector(file: IO[str]) -> contextlib._RedirectStream[IO[str]]: # pyright # Here we create a jenkem huffer because you can't control rebots console output # Rebot uses __stdout__, which doesn't have an implementation in contextlib result = contextlib._RedirectStream(file) # pyright: ignore[reportPrivateUsage] - result._stream = "__stdout__" # pyright: ignore[reportAttributeAccessIssue] # ty:ignore[unresolved-attribute] + result._stream = "__stdout__" # pyright: ignore[reportAttributeAccessIssue] # ty:ignore[unresolved-attribute] # pyrefly: ignore[missing-attribute] return result with Path(os.devnull).open("w", encoding="UTF8") as devull, redirector(devull): @@ -466,11 +469,8 @@ def redirector(file: IO[str]) -> contextlib._RedirectStream[IO[str]]: # pyright if len(outputs) > 1: merged_suite_name = cast(str, ExecutionResult(*outputs).suite.name) # pyright:ignore[reportUnknownMemberType] for output in outputs: - _ = cast( - int, - rebot.main( - [output], output=output, name=merged_suite_name, stdout=None - ), + _ = rebot.main( # pyright: ignore[reportUnknownVariableType] + [output], output=output, name=merged_suite_name, stdout=None ) rebot_options = merge_robot_options( { @@ -642,7 +642,7 @@ def _keywordify_pytest_functions(): for method in ("fail", "xfail"): # we specify the name argument explicitly because as of pytest 9.0 these are variables now # instead of functions, so they don't have a __name__ attribute - keywordify(pytest, method, module=module, name=printable_name(method)) + keywordify(pytest, method, module=module, name=printable_name(method)) # pyrefly: ignore[unknown-argument-type] string argument is untyped methods_to_wrap = {"deprecated_call", "warns", "raises"} if pytest_version >= (8, 4): # RaisesGroup was only introduced in pytest 8.4 diff --git a/pytest_robotframework/_internal/pytest/robot_file_support.py b/pytest_robotframework/_internal/pytest/robot_file_support.py index b67c9caa..b226bbe2 100644 --- a/pytest_robotframework/_internal/pytest/robot_file_support.py +++ b/pytest_robotframework/_internal/pytest/robot_file_support.py @@ -54,7 +54,7 @@ def _get_failure( # pyright: ignore[reportUnusedFunction] if result: # this function's signature is different depewnding on the robot version, so we just accept # any arguments and iterate over them to find the one we need - result.error = get_arg_with_type(BaseException, args, kwargs) # pyright: ignore[reportAttributeAccessIssue] #ty:ignore[invalid-assignment] + result.error = get_arg_with_type(BaseException, args, kwargs) # pyright: ignore[reportAttributeAccessIssue] #ty:ignore[invalid-assignment] # pyrefly: ignore[missing-attribute] return result @@ -145,7 +145,7 @@ def runtest(self): # type checkers are only run when robot 7 is installed BodyRunner( # pyright:ignore[reportCallIssue] context=context, templated=bool(test.template) - ).run(self.stash[original_body_key]) + ).run(self.stash[original_body_key]) # pyrefly: ignore[missing-argument] else: wrapped_body = test.body test.body = self.stash[original_body_key] diff --git a/pytest_robotframework/_internal/robot/library.py b/pytest_robotframework/_internal/robot/library.py index 33e86440..22caaa4c 100644 --- a/pytest_robotframework/_internal/robot/library.py +++ b/pytest_robotframework/_internal/robot/library.py @@ -48,7 +48,7 @@ def _call_and_report_robot_edition( if xfail_reason is None: # ty accounts for the possibility that it's a different subtype of tuple and one of the # other types in the union but that's not actually possible - skip_reason = cast( # pyright:ignore[reportUnnecessaryCast] + skip_reason = cast( # pyright:ignore[reportUnnecessaryCast] # pyrefly: ignore[redundant-cast] str, report.longrepr[2] if isinstance(report.longrepr, tuple) else "" ) else: @@ -88,7 +88,7 @@ def setup(arg: Cloaked[Item]): ): # This only happens if the item is re-run, as is done by # pytest-rerunfailures. - item._initrequest() # pyright:ignore[reportAttributeAccessIssue,reportUnknownMemberType] # ty:ignore[unresolved-attribute] + item._initrequest() # pyright:ignore[reportAttributeAccessIssue,reportUnknownMemberType] # ty:ignore[unresolved-attribute] # pyrefly: ignore[missing-attribute] _call_and_report_robot_edition(item, "setup") @@ -103,7 +103,7 @@ def run_test(arg: Cloaked[Item]): setup_only = item.config.getoption("setuponly", default=False) show_test_item(item, add_space=not setup_only) else: - show_test_item(item) # ty:ignore[missing-argument] #pyright:ignore[reportCallIssue] + show_test_item(item) # ty:ignore[missing-argument] #pyright:ignore[reportCallIssue] # pyrefly: ignore[missing-argument] if not item.config.getoption("setuponly", default=False): _call_and_report_robot_edition(item, "call") diff --git a/pytest_robotframework/_internal/robot/listeners_and_suite_visitors.py b/pytest_robotframework/_internal/robot/listeners_and_suite_visitors.py index 3be38540..6d0608c1 100644 --- a/pytest_robotframework/_internal/robot/listeners_and_suite_visitors.py +++ b/pytest_robotframework/_internal/robot/listeners_and_suite_visitors.py @@ -244,7 +244,7 @@ def __init__(self, session: Session, *, items: list[Item]): @override # https://github.com/robotframework/robotframework/issues/4940 - def visit_test( # pyright:ignore[reportIncompatibleMethodOverride] # ty:ignore[invalid-method-override] + def visit_test( # pyright:ignore[reportIncompatibleMethodOverride] # ty:ignore[invalid-method-override] # pyrefly: ignore[bad-override] self, test: running.TestCase ): for item in self.items: @@ -298,7 +298,7 @@ def __init__(self, *, session: Session, xdist_item: Item | None = None): @override # https://github.com/robotframework/robotframework/issues/4940 - def start_test(self, test: running.TestCase) -> bool | None: # pyright:ignore[reportIncompatibleMethodOverride] # ty:ignore[invalid-method-override] + def start_test(self, test: running.TestCase) -> bool | None: # pyright:ignore[reportIncompatibleMethodOverride] # ty:ignore[invalid-method-override] # pyrefly: ignore[bad-override] if self.xdist_item: item = self.xdist_item else: @@ -446,10 +446,10 @@ def exit_wrapper(hook: HookImpl) -> object: HookImpl( hook.plugin, hook.plugin_name, - lambda item, nextitem, *, hook=hook: enter_wrapper( # pyright:ignore[reportUnknownArgumentType,reportUnknownLambdaType] + lambda item, nextitem, *, hook=hook: enter_wrapper( # pyright:ignore[reportUnknownArgumentType,reportUnknownLambdaType] # pyrefly: ignore[implicit-any-lambda] hook, - item, # pyright:ignore[reportUnknownArgumentType] - nextitem, # pyright:ignore[reportUnknownArgumentType] + item, # pyright:ignore[reportUnknownArgumentType] # pyrefly: ignore[unknown-argument-type] + nextitem, # pyright:ignore[reportUnknownArgumentType] # pyrefly: ignore[unknown-argument-type] ), { **hook.opts, diff --git a/pytest_robotframework/_internal/robot/utils.py b/pytest_robotframework/_internal/robot/utils.py index 80c942d9..2246b5e7 100644 --- a/pytest_robotframework/_internal/robot/utils.py +++ b/pytest_robotframework/_internal/robot/utils.py @@ -305,4 +305,4 @@ def run_keyword(name: str, *args: str): except ModuleNotFoundError: builtin.run_keyword(name, *args) # pyright: ignore[reportUnusedCallResult, reportArgumentType] else: - _ = builtin.run_keyword(KeywordName(name), *(cast(tuple[KeywordArgument], args))) + _ = builtin.run_keyword(KeywordName(name), *(cast(tuple[KeywordArgument], args))) # pyrefly: ignore[unknown-variable-type] untyped decorator, which pyright just ignores diff --git a/tests/conftest.py b/tests/conftest.py index 6ef06d9c..9afbf04b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,7 +20,10 @@ # only available in pytest >=9.1 from _pytest.pytester import _FileContent # pyright:ignore[reportPrivateUsage] from _typeshed import StrPath - from lxml._types import ( # pyright:ignore[reportMissingModuleSource] https://github.com/DetachHead/basedpyright/issues/615 + + # https://github.com/DetachHead/basedpyright/issues/615 + # https://github.com/facebook/pyrefly/issues/5135 + from lxml._types import ( # pyright:ignore[reportMissingModuleSource] # pyrefly: ignore[missing-source] _TextArg, # pyright: ignore[reportPrivateUsage] _XPathExtFuncArg, # pyright: ignore[reportPrivateUsage] _XPathNSArg, # pyright: ignore[reportPrivateUsage] @@ -82,7 +85,7 @@ def pytester_dir(pytester: Pytester, request: FixtureRequest) -> PytesterDir: if TYPE_CHECKING: # Pytester is final so it's probably a bad idea to rely on extending this at runtime - class PytesterDir(Pytester): # pyright:ignore[reportGeneralTypeIssues] #ty:ignore[subclass-of-final-class] + class PytesterDir(Pytester): # pyright:ignore[reportGeneralTypeIssues] #ty:ignore[subclass-of-final-class] # pyrefly: ignore[invalid-inheritance] """ fake subtype of `Pytester` that bans you from using file creation and runpytest methods. you should put real life files in `tests/fixtures/[test file path]/[test name]` instead, @@ -210,7 +213,8 @@ class XmlElement(_Element): """ # https://github.com/DetachHead/basedpyright/issues/615 - def __init__(self, element: _Element) -> None: ... # pyright:ignore[reportMissingSuperCall] + # https://github.com/facebook/pyrefly/issues/5136 + def __init__(self, element: _Element) -> None: ... # pyright:ignore[reportMissingSuperCall] # pyrefly: ignore[missing-super-call] def __bool__(self) -> Literal[True]: # pyright:ignore[reportReturnType] see issue above """normally this returns `True` only if it has children""" @@ -313,7 +317,7 @@ def run_and_assert_assert_pytest_result( exit_code: ExitCode | None = None, ): # checked by the overloads - result = self.run_pytest(*pytest_args or [], subprocess=subprocess, plugins=plugins) # pyright:ignore[reportArgumentType] #ty:ignore[no-matching-overload] + result = self.run_pytest(*pytest_args or [], subprocess=subprocess, plugins=plugins) # pyright:ignore[reportArgumentType] #ty:ignore[no-matching-overload] # pyrefly: ignore[no-matching-overload] # this is kinda hueristic and gross, but i cant think of a clean way to add this check to # every test so this will do for now diff --git a/tests/fixtures/test_python/test_assertion_fails_with_description.py b/tests/fixtures/test_python/test_assertion_fails_with_description.py index 0db1bacb..ffe91c84 100644 --- a/tests/fixtures/test_python/test_assertion_fails_with_description.py +++ b/tests/fixtures/test_python/test_assertion_fails_with_description.py @@ -5,4 +5,4 @@ def test_foo(): right = 1 - assert right == "wrong", AssertOptions(log_pass=False, description="asdf") + assert right == "wrong", AssertOptions(log_pass=False, description="asdf") # pyrefly: ignore[incompatible-comparison] intentional diff --git a/tests/fixtures/test_python/test_assertion_fails_with_fail_message_hide_assert.py b/tests/fixtures/test_python/test_assertion_fails_with_fail_message_hide_assert.py index f94c2f89..879653bc 100644 --- a/tests/fixtures/test_python/test_assertion_fails_with_fail_message_hide_assert.py +++ b/tests/fixtures/test_python/test_assertion_fails_with_fail_message_hide_assert.py @@ -5,4 +5,4 @@ def test_foo(): right = 1 - assert right == "wrong", AssertOptions(log_pass=False, fail_message="asdf") + assert right == "wrong", AssertOptions(log_pass=False, fail_message="asdf") # pyrefly: ignore[incompatible-comparison] intentional diff --git a/tests/fixtures/test_python/test_assertion_passes_custom_messages.py b/tests/fixtures/test_python/test_assertion_passes_custom_messages.py index 1b551c4b..a7ffb7b1 100644 --- a/tests/fixtures/test_python/test_assertion_passes_custom_messages.py +++ b/tests/fixtures/test_python/test_assertion_passes_custom_messages.py @@ -11,5 +11,5 @@ def test_foo(): assert left == right, "doesn't appear" assert right == left, AssertOptions(description="does appear1") with raises(AssertionError): - assert right == "wrong", AssertOptions(log_pass=True, fail_message="does appear2") + assert right == "wrong", AssertOptions(log_pass=True, fail_message="does appear2") # pyrefly: ignore[incompatible-comparison] intentional assert right == "wrong", "does appear3" diff --git a/tests/fixtures/test_python/test_keywordify_context_manager.py b/tests/fixtures/test_python/test_keywordify_context_manager.py index 34bd329a..b4cb79bf 100644 --- a/tests/fixtures/test_python/test_keywordify_context_manager.py +++ b/tests/fixtures/test_python/test_keywordify_context_manager.py @@ -6,4 +6,4 @@ def test_foo(): # keywordified in plugin.py with raises(ZeroDivisionError): - _ = 1 / 0 # ty:ignore[division-by-zero] + _ = 1 / 0 # ty:ignore[division-by-zero] # pyrefly: ignore[division-by-zero] diff --git a/tests/fixtures/test_python/test_keywordify_keyword_inside_context_manager.py b/tests/fixtures/test_python/test_keywordify_keyword_inside_context_manager.py index 16940313..3e56d5e1 100644 --- a/tests/fixtures/test_python/test_keywordify_keyword_inside_context_manager.py +++ b/tests/fixtures/test_python/test_keywordify_keyword_inside_context_manager.py @@ -14,4 +14,4 @@ def asdf(): def test_foo(): with raises(ZeroDivisionError): # noqa: PT012 asdf() - _ = 1 / 0 # ty:ignore[division-by-zero] + _ = 1 / 0 # ty:ignore[division-by-zero] # pyrefly: ignore[division-by-zero] diff --git a/tests/test_python.py b/tests/test_python.py index d4a3deb1..7d340ce6 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -24,7 +24,7 @@ if pytest_version < (9,): from _pytest.assertion.util import ( - running_on_ci, # pyright: ignore[reportAttributeAccessIssue,reportUnknownVariableType] # ty:ignore[unresolved-import] + running_on_ci, # pyright: ignore[reportAttributeAccessIssue,reportUnknownVariableType] # ty:ignore[unresolved-import] # pyrefly: ignore[missing-module-attribute] ) else: from _pytest.compat import running_on_ci diff --git a/tests/type_tests.py b/tests/type_tests.py index b9f4c9c5..a705f09f 100644 --- a/tests/type_tests.py +++ b/tests/type_tests.py @@ -52,7 +52,7 @@ def c() -> Iterator[None]: _ = assert_type(c, Callable[[], _GeneratorContextManager[None]]) # ty:ignore[type-assertion-failure] # keyword, context manager with no wrap_context_manager arg: - @keyword # pyright:ignore[reportDeprecated] + @keyword # pyright:ignore[reportDeprecated] # pyrefly: ignore[deprecated] @contextmanager def d() -> Iterator[None]: yield @@ -61,10 +61,10 @@ def d() -> Iterator[None]: # keyword, non-context manager with wrap_context_manager=True: # expected type error - @keyword(wrap_context_manager=True) # pyright:ignore[reportArgumentType] # ty:ignore[invalid-argument-type] + @keyword(wrap_context_manager=True) # pyright:ignore[reportArgumentType] # ty:ignore[invalid-argument-type] # pyrefly: ignore[bad-argument-type] def e() -> None: ... # keyword, non-context manager with wrap_context_manager=False: # expected type error - @keyword(wrap_context_manager=False) # pyright:ignore[reportArgumentType] + @keyword(wrap_context_manager=False) # pyright:ignore[reportArgumentType] # pyrefly: ignore[bad-specialization] def f() -> None: ... diff --git a/uv.lock b/uv.lock index aafe2ca4..30637f16 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.10, <4.0" resolution-markers = [ "python_full_version >= '3.12'", @@ -1010,6 +1010,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] +[[package]] +name = "pyrefly" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/78/b6ec12acb586606618c71ebe1008af59e1ca50e667895f1946fa7b656619/pyrefly-1.3.2.tar.gz", hash = "sha256:30a68ff5429a1546ec08a2d913dfe442c9553a2940d274de18833d8eda939b5e", size = 6679591, upload-time = "2026-09-28T22:10:06.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/01/144a78839822abad55628881bcb45f8173e6d8b87976897c9105884b5a6b/pyrefly-1.3.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:626dbaee16ae1169a10a4ba3293583b5ab0320102f35efaad9e03457a2fc646c", size = 15088488, upload-time = "2026-09-28T22:09:40.743Z" }, + { url = "https://files.pythonhosted.org/packages/23/aa/599756bd42e6533ee9261ebd288f846ab453b00debaef2b4789893967809/pyrefly-1.3.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2d5fc704dbf0e990dc4f15cb1ead64f1a325ae27383a50c76cfcaf0387f926bb", size = 14429996, upload-time = "2026-09-28T22:09:43.316Z" }, + { url = "https://files.pythonhosted.org/packages/90/0c/4b0678889136b1514ecd25cec11974d5bfdaeaaf9f5fe8a766ca45ec4ab5/pyrefly-1.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89dbe8a33e735fa96023c33ce5dfd81af9b0bdd2ee771e8d69b2ebffc7e0fbc7", size = 14871810, upload-time = "2026-09-28T22:09:45.42Z" }, + { url = "https://files.pythonhosted.org/packages/a6/4e/a96236391c6bb0d8b50af85e0ea6c0c56206f739931ac3031f9e000325ee/pyrefly-1.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ec0c31715d48bff279f548b85a93b5bc43d2c7bd1492a8cd703a9185cb089e5", size = 16113462, upload-time = "2026-09-28T22:09:47.619Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c3/d4d0966aa7008ebd8481a6d05aa53202ad40c9597705eeee87ba6e0e2025/pyrefly-1.3.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b3de468bed691e40dc6271284c8dcc05e649d7ed45280ef3d07332741efe34c", size = 16043341, upload-time = "2026-09-28T22:09:50.083Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d1/52d9ea1b7048c7316d9ff7594c170b1c06d37547a75d140c8deea7b9ddfd/pyrefly-1.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb94365fe5f854cb4fe82f600c4e2453160d328610b12981004851ee0b6e8aeb", size = 15437183, upload-time = "2026-09-28T22:09:52.56Z" }, + { url = "https://files.pythonhosted.org/packages/08/ec/24f83e4a397fa9d5c416bb4d378ee1bb18fe48bdd5698fe541636f5352f3/pyrefly-1.3.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4f730d2e515ac25827fc68f343a7aa02b1072e0062df6a430346a4ffdfb5db2f", size = 14892976, upload-time = "2026-09-28T22:09:54.916Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/a473f8e9ec31ba0682ebe44f4e51167a22a4cbdc8bfd87365b9aca23be20/pyrefly-1.3.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:60f440e1626bd5cc4a41ee90cecba80796ed01ebd6c15d9f933efb8d07ffe2b8", size = 15469689, upload-time = "2026-09-28T22:09:57.541Z" }, + { url = "https://files.pythonhosted.org/packages/d0/df/779092365e8e469fc8e7349e1c26c35b8a73df9e6be80a00265fc26a2b2b/pyrefly-1.3.2-py3-none-win32.whl", hash = "sha256:b31684189de44a25a5bf00bdf3ad16be44a7b14654702d6018bce1043357202c", size = 14135550, upload-time = "2026-09-28T22:09:59.757Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/466333e7f2891f8c357b5c226559c39ba8dde4f90be09189ecc771a086f5/pyrefly-1.3.2-py3-none-win_amd64.whl", hash = "sha256:693960cdb247d1f76b9e09b3f2554dae20034f7d442ccd4e9b3e8962a3622363", size = 15067523, upload-time = "2026-09-28T22:10:02.099Z" }, + { url = "https://files.pythonhosted.org/packages/18/32/ee66df92c692c0686d82c9ecef3876ccb0386aa318bf507016f0121a8f78/pyrefly-1.3.2-py3-none-win_arm64.whl", hash = "sha256:f7424b21331670522980ff13e08bd143659879a4d92f4832282c7d7c41a9b5d6", size = 14348271, upload-time = "2026-09-28T22:10:04.253Z" }, +] + [[package]] name = "pytest" version = "9.1.1" @@ -1064,6 +1083,7 @@ dev = [ { name = "mkdocs-unused-files" }, { name = "mkdocstrings-python" }, { name = "pylint" }, + { name = "pyrefly" }, { name = "pytest" }, { name = "pytest-github-actions-annotate-failures" }, { name = "pytest-xdist" }, @@ -1097,6 +1117,7 @@ dev = [ { name = "mkdocs-unused-files", specifier = ">=0.2.0" }, { name = "mkdocstrings-python", specifier = ">=1.17.0" }, { name = "pylint", specifier = ">=3.2.0" }, + { name = "pyrefly", specifier = ">=1.3.2" }, { name = "pytest", specifier = ">=7a0" }, { name = "pytest-github-actions-annotate-failures", specifier = ">=0.2.0" }, { name = "pytest-xdist", specifier = ">=3.5.0" },