From b126ac06151729a74ed139a5f0c8b02a70d95a24 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Mon, 25 May 2026 18:09:00 +0200 Subject: [PATCH 1/5] [diagnostic] test #9168 cyclic-import detection across platforms #9168 reports that the same project layout produces R0401 cyclic-import on Linux but not on macOS. This subprocess-based test sets up the reporter's exact directory layout in tmp_path and asserts the warning fires. Purpose: use the macOS CI runner to confirm whether the bug still reproduces on current main. If the assertion fails on macOS only, the platform-dependent behavior is confirmed; if both platforms pass, the bug has been silently fixed. Not intended for merge as-is. --- tests/lint/test_cyclic_import_9168.py | 67 +++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/lint/test_cyclic_import_9168.py diff --git a/tests/lint/test_cyclic_import_9168.py b/tests/lint/test_cyclic_import_9168.py new file mode 100644 index 00000000000..ea72f4425a1 --- /dev/null +++ b/tests/lint/test_cyclic_import_9168.py @@ -0,0 +1,67 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt + +"""Diagnostic for https://github.com/pylint-dev/pylint/issues/9168 + +The reporter says `cyclic-import` is detected on Linux but not on macOS +for the same project layout. This test exercises the layout on whatever +platform the CI runs on; if the bug is real, the assertion will fail on +macOS only. +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + + +def test_cyclic_import_detected_in_package(tmp_path: Path) -> None: + """Reproduce the directory layout from #9168 and assert cyclic-import fires.""" + pkg = tmp_path / "module1" + pkg.mkdir() + (pkg / "__init__.py").write_text( + "from module1.base import Base\n" + "from module1.derived import Derived\n" + ) + (pkg / "base.py").write_text( + "class Base:\n" + " def __init__(self):\n" + " print('hello from base')\n" + ) + (pkg / "derived.py").write_text( + "from module1 import Base\n" + "\n" + "class Derived(Base):\n" + " def __init__(self):\n" + " super().__init__()\n" + " print('hello from derived')\n" + ) + (tmp_path / "main.py").write_text( + "from module1 import Derived\n" + "\n" + "if __name__ == '__main__':\n" + " Derived()\n" + ) + + process = subprocess.run( + [ + sys.executable, + "-m", + "pylint", + "--recursive=y", + "--disable=W,C0114,C0115,C0116,R0903", + "-s=n", + ".", + ], + cwd=tmp_path, + capture_output=True, + text=True, + check=False, + ) + output = process.stdout + process.stderr + assert "cyclic-import" in output, ( + f"Expected cyclic-import to be reported on {sys.platform}, " + f"got: {output!r}" + ) From f64036e7b7312709e152bf864e9ed094efabecb4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 16:10:10 +0000 Subject: [PATCH 2/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/lint/test_cyclic_import_9168.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/lint/test_cyclic_import_9168.py b/tests/lint/test_cyclic_import_9168.py index ea72f4425a1..beade6319b7 100644 --- a/tests/lint/test_cyclic_import_9168.py +++ b/tests/lint/test_cyclic_import_9168.py @@ -22,13 +22,10 @@ def test_cyclic_import_detected_in_package(tmp_path: Path) -> None: pkg = tmp_path / "module1" pkg.mkdir() (pkg / "__init__.py").write_text( - "from module1.base import Base\n" - "from module1.derived import Derived\n" + "from module1.base import Base\n" "from module1.derived import Derived\n" ) (pkg / "base.py").write_text( - "class Base:\n" - " def __init__(self):\n" - " print('hello from base')\n" + "class Base:\n" " def __init__(self):\n" " print('hello from base')\n" ) (pkg / "derived.py").write_text( "from module1 import Base\n" @@ -62,6 +59,5 @@ def test_cyclic_import_detected_in_package(tmp_path: Path) -> None: ) output = process.stdout + process.stderr assert "cyclic-import" in output, ( - f"Expected cyclic-import to be reported on {sys.platform}, " - f"got: {output!r}" + f"Expected cyclic-import to be reported on {sys.platform}, " f"got: {output!r}" ) From 4aaea3393901e61c46caf821022c43c32205a2aa Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Mon, 25 May 2026 18:42:56 +0200 Subject: [PATCH 3/5] Move diagnostic test out of tests/lint/ to avoid breaking unittest_expand_modules unittest_expand_modules.py parametrizes against a fixed list of files in tests/lint/. Putting a new file in that directory failed all CI jobs. --- tests/{lint => }/test_cyclic_import_9168.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{lint => }/test_cyclic_import_9168.py (100%) diff --git a/tests/lint/test_cyclic_import_9168.py b/tests/test_cyclic_import_9168.py similarity index 100% rename from tests/lint/test_cyclic_import_9168.py rename to tests/test_cyclic_import_9168.py From c8eaf0d03f292c0b54add90922c6348604d7532c Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Mon, 25 May 2026 23:02:40 +0200 Subject: [PATCH 4/5] [diagnostic] Tests confirming 6 'Needs reproduction' issues no longer reproduce For each issue, build the reporter's minimal repro in tmp_path and assert that the originally-reported false positive or crash signature is absent. If CI is green on all platforms, the bugs are silently fixed since the issue was filed and can be closed. - #4899 pydantic field(default_factory=lambda: []) no-member / not-an-iterable - #4917 with-block rebinding no-member - #7268 NamedTuple multi-assignment crash - #8980 azure-monitor namespace package no-name-in-module - #9137 bson LEGACY_JSON_OPTIONS.with_options UninferableBase crash - #9983 Uninferable_factory unexpected-keyword-arg FP --- tests/test_azure_namespace_8980.py | 62 ++++++++++++++++ tests/test_bson_with_options_9137.py | 50 +++++++++++++ tests/test_context_manager_no_member_4917.py | 76 ++++++++++++++++++++ tests/test_namedtuple_multiassign_7268.py | 38 ++++++++++ tests/test_pydantic_field_4899.py | 76 ++++++++++++++++++++ tests/test_uninferable_kwargs_9983.py | 49 +++++++++++++ 6 files changed, 351 insertions(+) create mode 100644 tests/test_azure_namespace_8980.py create mode 100644 tests/test_bson_with_options_9137.py create mode 100644 tests/test_context_manager_no_member_4917.py create mode 100644 tests/test_namedtuple_multiassign_7268.py create mode 100644 tests/test_pydantic_field_4899.py create mode 100644 tests/test_uninferable_kwargs_9983.py diff --git a/tests/test_azure_namespace_8980.py b/tests/test_azure_namespace_8980.py new file mode 100644 index 00000000000..ac685ba494d --- /dev/null +++ b/tests/test_azure_namespace_8980.py @@ -0,0 +1,62 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt + +"""Diagnostic for https://github.com/pylint-dev/pylint/issues/8980 + +When both ``azure-monitor-opentelemetry`` and +``azure-monitor-opentelemetry-exporter`` are installed (separate +distributions sharing the ``azure.monitor.opentelemetry`` namespace), +pylint used to emit ``no-name-in-module`` for the second distribution's +names. Confirm namespace-package resolution now works. +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +import pytest + +pytest.importorskip( + "azure.monitor.opentelemetry.exporter", + reason="azure-monitor-opentelemetry-exporter is required to reproduce #8980", +) + + +def test_azure_namespace_package_resolves(tmp_path: Path) -> None: + """Imports from the namespace-shared exporter package must resolve.""" + (tmp_path / "use.py").write_text( + '"""Use the namespace import."""\n' + "from azure.monitor.opentelemetry.exporter import (\n" + " ApplicationInsightsSampler,\n" + " AzureMonitorLogExporter,\n" + " AzureMonitorMetricExporter,\n" + " AzureMonitorTraceExporter,\n" + ")\n" + "\n" + "print(\n" + " ApplicationInsightsSampler,\n" + " AzureMonitorLogExporter,\n" + " AzureMonitorMetricExporter,\n" + " AzureMonitorTraceExporter,\n" + ")\n" + ) + + process = subprocess.run( + [ + sys.executable, + "-m", + "pylint", + "--disable=all", + "--enable=no-name-in-module", + "use.py", + ], + cwd=tmp_path, + capture_output=True, + text=True, + check=False, + ) + output = process.stdout + process.stderr + assert "no-name-in-module" not in output, f"#8980 regression: {output!r}" diff --git a/tests/test_bson_with_options_9137.py b/tests/test_bson_with_options_9137.py new file mode 100644 index 00000000000..06636160a75 --- /dev/null +++ b/tests/test_bson_with_options_9137.py @@ -0,0 +1,50 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt + +"""Diagnostic for https://github.com/pylint-dev/pylint/issues/9137 + +``LEGACY_JSON_OPTIONS.with_options(tz_aware=True, tzinfo=...)`` used to +crash pylint with ``'UninferableBase' object is not iterable``. Confirm +inference no longer blows up when bson is available. +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +import pytest + +pytest.importorskip( + "bson.json_util", reason="pymongo (bson) is required to reproduce #9137" +) +pytest.importorskip("pytz", reason="pytz is required to reproduce #9137") + + +def test_bson_with_options_does_not_crash(tmp_path: Path) -> None: + """``LEGACY_JSON_OPTIONS.with_options(...)`` must not crash inference.""" + (tmp_path / "json_util.py").write_text( + '"""JSON util."""\n' + "import pytz\n" + "from bson.json_util import LEGACY_JSON_OPTIONS\n" + "\n" + "CUSTOM_JSON_OPTIONS = LEGACY_JSON_OPTIONS.with_options(\n" + " tz_aware=True, tzinfo=pytz.UTC\n" + ")\n" + ) + + process = subprocess.run( + [sys.executable, "-m", "pylint", "json_util.py"], + cwd=tmp_path, + capture_output=True, + text=True, + check=False, + ) + output = process.stdout + process.stderr + assert "UninferableBase" not in output, f"#9137 regression: {output!r}" + assert "Traceback" not in output, f"#9137 regression (crash): {output!r}" + assert ( + "astroid-error" not in output + ), f"#9137 regression (astroid-error): {output!r}" diff --git a/tests/test_context_manager_no_member_4917.py b/tests/test_context_manager_no_member_4917.py new file mode 100644 index 00000000000..30b830286a2 --- /dev/null +++ b/tests/test_context_manager_no_member_4917.py @@ -0,0 +1,76 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt + +"""Diagnostic for https://github.com/pylint-dev/pylint/issues/4917 + +A variable bound by ``with CtxMgr() as x: x = OtherClass(...)`` was once +treated as an instance of the context-manager class, producing a false +``no-member`` on ``x.method_of_other_class()``. Confirm pylint now infers +the rebinding correctly. +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + + +def test_with_block_rebinding_no_false_no_member(tmp_path: Path) -> None: + """Rebinding inside a ``with`` block must not trigger no-member on the new type.""" + (tmp_path / "animal_api.py").write_text( + '"""Module."""\n' + "\n" + "\n" + "class Animal:\n" + ' """Animal."""\n' + ' def __init__(self, name=""):\n' + " self.name = name\n" + "\n" + " def add_animal_to_db(self, _uow):\n" + ' """Persist."""\n' + " return self\n" + "\n" + " def as_dict(self):\n" + ' """Serialize."""\n' + ' return {"name": self.name}\n' + "\n" + "\n" + "class SQLUnitOfWork:\n" + ' """SQLUnitOfWork."""\n' + " def __init__(self, config):\n" + " self.config = config\n" + "\n" + " def __enter__(self):\n" + " return self\n" + "\n" + " def __exit__(self, *args):\n" + " return None\n" + "\n" + "\n" + "def handler(data, config, body):\n" + ' """Handler."""\n' + " with SQLUnitOfWork(config) as uow:\n" + " animal = Animal(**data).add_animal_to_db(uow)\n" + " if not animal:\n" + ' return {"status": "Incorrect values", "values": body}, 405\n' + " return animal.as_dict(), 201\n" + ) + + process = subprocess.run( + [ + sys.executable, + "-m", + "pylint", + "--disable=all", + "--enable=no-member", + "animal_api.py", + ], + cwd=tmp_path, + capture_output=True, + text=True, + check=False, + ) + output = process.stdout + process.stderr + assert "no-member" not in output, f"#4917 regression: {output!r}" diff --git a/tests/test_namedtuple_multiassign_7268.py b/tests/test_namedtuple_multiassign_7268.py new file mode 100644 index 00000000000..8b5d9d48af0 --- /dev/null +++ b/tests/test_namedtuple_multiassign_7268.py @@ -0,0 +1,38 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt + +"""Diagnostic for https://github.com/pylint-dev/pylint/issues/7268 + +The reporter had pylint crash on a ``NamedTuple`` subclass that assigned +its members on one line with tuple-unpacking. Confirm the astroid +``infer_typing_namedtuple_class`` brain no longer crashes on this shape. +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + + +def test_namedtuple_multi_assignment_does_not_crash(tmp_path: Path) -> None: + """A NamedTuple with tuple-unpacked attribute assignment should not crash.""" + (tmp_path / "color.py").write_text( + "from typing import NamedTuple\n" + "\n" + "class Color(NamedTuple):\n" + " RED, CYAN, BLUE, BLACK, GREEN, WHITE = 31, 36, 34, 30, 32, 37\n" + ) + + process = subprocess.run( + [sys.executable, "-m", "pylint", "color.py"], + cwd=tmp_path, + capture_output=True, + text=True, + check=False, + ) + output = process.stdout + process.stderr + assert "Traceback" not in output, f"pylint crashed on Py {sys.version}: {output!r}" + assert "astroid-error" not in output, f"astroid error: {output!r}" + assert "F0002" not in output, f"fatal error: {output!r}" diff --git a/tests/test_pydantic_field_4899.py b/tests/test_pydantic_field_4899.py new file mode 100644 index 00000000000..8cff2582e3a --- /dev/null +++ b/tests/test_pydantic_field_4899.py @@ -0,0 +1,76 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt + +"""Diagnostic for https://github.com/pylint-dev/pylint/issues/4899 + +``pydantic.dataclasses.dataclass`` with ``items: list = field(default_factory=lambda: [])`` +used to mis-infer ``items`` as the ``dataclasses.Field`` descriptor itself, +emitting false ``no-member`` on ``.append`` and ``not-an-iterable`` on +iteration. Confirm the FP is gone when pydantic is available. +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +import pytest + +pytest.importorskip("pydantic", reason="pydantic is required to reproduce #4899") + + +def test_pydantic_field_default_factory_no_false_no_member(tmp_path: Path) -> None: + """A ``field(default_factory=lambda: [])`` attribute keeps its list type.""" + (tmp_path / "cases.py").write_text( + '"""Module."""\n' + "from dataclasses import field\n" + "from typing import List\n" + "from pydantic.dataclasses import dataclass\n" + "\n" + "\n" + "@dataclass\n" + "class Item:\n" + ' """Item."""\n' + ' description: str = ""\n' + "\n" + "\n" + "@dataclass\n" + "class Case:\n" + ' """Case."""\n' + " name: str\n" + " irr: float = 0\n" + " items: List[Item] = field(default_factory=lambda: [])\n" + "\n" + " def add_item(self, item: Item) -> None:\n" + ' """Append."""\n' + " self.items.append(item)\n" + "\n" + " def find_item(self, description: str):\n" + ' """Find."""\n' + " return next(\n" + " (item for item in self.items if item.description == description),\n" + " None,\n" + " )\n" + ) + + process = subprocess.run( + [ + sys.executable, + "-m", + "pylint", + "--disable=all", + "--enable=no-member,not-an-iterable", + "cases.py", + ], + cwd=tmp_path, + capture_output=True, + text=True, + check=False, + ) + output = process.stdout + process.stderr + assert "no-member" not in output, f"#4899 regression (no-member): {output!r}" + assert ( + "not-an-iterable" not in output + ), f"#4899 regression (not-an-iterable): {output!r}" diff --git a/tests/test_uninferable_kwargs_9983.py b/tests/test_uninferable_kwargs_9983.py new file mode 100644 index 00000000000..58f9957a05b --- /dev/null +++ b/tests/test_uninferable_kwargs_9983.py @@ -0,0 +1,49 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt + +"""Diagnostic for https://github.com/pylint-dev/pylint/issues/9983 + +When an f-string keyword name is uninferable, pylint used to emit +``unexpected-keyword-arg`` with ``'Uninferable_factory'`` in the message +text — a synthetic name nobody would ever write. Confirm the FP is gone. +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + + +def test_uninferable_kwargs_no_false_positive(tmp_path: Path) -> None: + """A ``**{f"{name}_factory": ...}`` call must not raise unexpected-keyword-arg.""" + (tmp_path / "module.py").write_text( + '"""Module."""\n' + "\n" + "\n" + "class Registry:\n" + ' """Registry."""\n' + " def __init__(self, **kwargs):\n" + " self.kwargs = kwargs\n" + "\n" + "\n" + "def make(name, factory):\n" + ' """Make."""\n' + ' return Registry(**{f"{name}_factory": factory})\n' + ) + + process = subprocess.run( + [sys.executable, "-m", "pylint", "module.py"], + cwd=tmp_path, + capture_output=True, + text=True, + check=False, + ) + output = process.stdout + process.stderr + assert ( + "unexpected-keyword-arg" not in output + ), f"#9983 regression: got unexpected-keyword-arg FP: {output!r}" + assert ( + "Uninferable_factory" not in output + ), f"#9983 regression: synthetic Uninferable name leaked: {output!r}" From 37f249a5de4af6ed97cbaccd348e0ae89575447f Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Mon, 25 May 2026 23:29:19 +0200 Subject: [PATCH 5/5] [diagnostic] 3 more tests across the 'Needs reproduction' backlog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #4667 (argparse subclass + Py3.9-only no-member): assert FP absent. The bug was Py3.9-only at filing, but Py3.9 is no longer supported, so we cross-check the fix on every supported interpreter. - #6352 (Gtk.Window subclass + self.x assignment → no-member on add/ show_all): assert FP IS present. PyGObject-only; pytest skips elsewhere. When the assertion flips, the bug is fixed. - #7122 (apt_pkg SourceRecords.binaries / Acquire.items not-an-iterable): assert FP IS present. python3-apt only; pytest skips elsewhere. The latter two were originally on my 'cannot reproduce without external env' list. Trying harder with the reporter's full code (not just my distilled minimum) shows both bugs are still real today. --- tests/test_apt_pkg_not_iterable_7122.py | 66 +++++++++++++++++++++ tests/test_argparse_subclass_4667.py | 65 +++++++++++++++++++++ tests/test_pygobject_window_6352.py | 77 +++++++++++++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 tests/test_apt_pkg_not_iterable_7122.py create mode 100644 tests/test_argparse_subclass_4667.py create mode 100644 tests/test_pygobject_window_6352.py diff --git a/tests/test_apt_pkg_not_iterable_7122.py b/tests/test_apt_pkg_not_iterable_7122.py new file mode 100644 index 00000000000..a62035db49d --- /dev/null +++ b/tests/test_apt_pkg_not_iterable_7122.py @@ -0,0 +1,66 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt + +"""Diagnostic for https://github.com/pylint-dev/pylint/issues/7122 + +``src_records.binaries`` and ``fetcher.items`` from ``apt_pkg`` get a +false ``not-an-iterable`` (E1133) even though the stubs declare them as +``List[str]``. This test asserts the FP is **present** today: when CI is +red, the bug is still real. Linux-only since apt_pkg is Debian/Ubuntu. +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +import pytest + +pytest.importorskip("apt", reason="python3-apt is required to reproduce #7122") +pytest.importorskip("apt_pkg", reason="python3-apt is required to reproduce #7122") + + +def test_apt_pkg_iterable_attributes_trigger_not_an_iterable(tmp_path: Path) -> None: + """``apt_pkg`` attributes typed as List in stubs still trip not-an-iterable.""" + (tmp_path / "example.py").write_text( + "#!/usr/bin/python3\n" + "# pylint: disable=missing-docstring\n" + "\n" + "import os\n" + "import apt\n" + "\n" + "src_records = apt.apt_pkg.SourceRecords()\n" + 'src_records.lookup("bash")\n' + 'pkgs = [p for p in src_records.binaries if not p.endswith("-doc")]\n' + "print(pkgs)\n" + "\n" + "fetcher = apt.apt_pkg.Acquire(apt.progress.text.AcquireProgress())\n" + "cache = apt.Cache(rootdir=os.getcwd())\n" + "cache.fetch_archives(fetcher=fetcher)\n" + "for i in fetcher.items:\n" + " print(i)\n" + ) + + process = subprocess.run( + [ + sys.executable, + "-m", + "pylint", + "--extension-pkg-allow-list=apt_pkg", + "--disable=all", + "--enable=not-an-iterable", + "example.py", + ], + cwd=tmp_path, + capture_output=True, + text=True, + check=False, + ) + output = process.stdout + process.stderr + # "Bug still real" diagnostic: assert the FP IS present. + assert "not-an-iterable" in output, ( + f"#7122 appears to be fixed on {sys.platform}. " + f"Promote this test to assert the FP is ABSENT and close. Output: {output!r}" + ) diff --git a/tests/test_argparse_subclass_4667.py b/tests/test_argparse_subclass_4667.py new file mode 100644 index 00000000000..84c45cf3b1c --- /dev/null +++ b/tests/test_argparse_subclass_4667.py @@ -0,0 +1,65 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt + +"""Diagnostic for https://github.com/pylint-dev/pylint/issues/4667 + +Subclassing ``argparse.ArgumentParser`` once made pylint emit ``no-member`` +on ``parsed_args.logdir`` (Py3.9-specific at the time). Pylint no longer +supports Py3.9, but the inference of ``Namespace`` attributes is general +enough that we verify the FP stays gone on every supported interpreter. +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + + +def test_argparse_subclass_no_member_absent(tmp_path: Path) -> None: + """Subclassing ArgumentParser must not poison Namespace attribute inference.""" + (tmp_path / "cli.py").write_text( + '"""CLI."""\n' + "import argparse\n" + "\n" + "\n" + "class SilentArgumentParser(argparse.ArgumentParser):\n" + ' """Silent parser."""\n' + "\n" + " def error(self, message=None):\n" + " raise SystemExit(2)\n" + "\n" + " def exit(self, status=0, message=None):\n" + " raise SystemExit(status)\n" + "\n" + "\n" + "def parse_args(argv):\n" + ' """Parse."""\n' + " parser = SilentArgumentParser()\n" + ' parser.add_argument("--logdir", type=str, default=None)\n' + ' parser.add_argument("name", type=str, default=None)\n' + " return parser.parse_args(argv)\n" + "\n" + "\n" + 'args = parse_args(["foo"])\n' + "print(args.name)\n" + "print(args.logdir)\n" + ) + + process = subprocess.run( + [ + sys.executable, + "-m", + "pylint", + "--disable=all", + "--enable=no-member", + "cli.py", + ], + cwd=tmp_path, + capture_output=True, + text=True, + check=False, + ) + output = process.stdout + process.stderr + assert "no-member" not in output, f"#4667 regression on {sys.version}: {output!r}" diff --git a/tests/test_pygobject_window_6352.py b/tests/test_pygobject_window_6352.py new file mode 100644 index 00000000000..f5e6c10546d --- /dev/null +++ b/tests/test_pygobject_window_6352.py @@ -0,0 +1,77 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt + +"""Diagnostic for https://github.com/pylint-dev/pylint/issues/6352 + +A class that subclasses ``Gtk.Window`` *and* sets at least one instance +attribute on ``self`` triggers a false ``no-member`` for inherited +members like ``add`` and ``show_all``. Drop the ``self.x = ...`` +assignment and the FP disappears. This test asserts the FP is **present** +today: when CI is red, the bug is still real. +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +import pytest + +pytest.importorskip("gi", reason="PyGObject is required to reproduce #6352") + + +def test_pygobject_window_self_attr_triggers_no_member(tmp_path: Path) -> None: + """``self.x = ...`` in a ``Gtk.Window`` subclass must not hide inherited members.""" + (tmp_path / "main_window.py").write_text( + '"""Main window."""\n' + "from typing import Tuple\n" + "\n" + "import gi\n" + 'gi.require_version("Gtk", "3.0")\n' + "# pylint: disable=wrong-import-position\n" + "from gi.repository import Gtk\n" + "# pylint: enable=wrong-import-position\n" + "\n" + "\n" + "class MainWindow(Gtk.Window):\n" + ' """Main."""\n' + "\n" + " WINDOW_SIZE: Tuple[int, int] = (500, 250)\n" + ' INPUT_FILE_BUTTON_LABEL: str = "Choose input file"\n' + "\n" + " def __init__(self, title: str):\n" + " super().__init__(title=title)\n" + " self.set_size_request(*self.WINDOW_SIZE)\n" + " main_box: Gtk.Box = Gtk.Box.new(\n" + " orientation=Gtk.Orientation.VERTICAL, spacing=10\n" + " )\n" + " self.add(main_box)\n" + " self.input_file_button = Gtk.Button.new(\n" + " label=self.INPUT_FILE_BUTTON_LABEL\n" + " )\n" + " self.show_all()\n" + ) + + process = subprocess.run( + [ + sys.executable, + "-m", + "pylint", + "--disable=all", + "--enable=no-member", + "main_window.py", + ], + cwd=tmp_path, + capture_output=True, + text=True, + check=False, + ) + output = process.stdout + process.stderr + # This is a "bug still real" diagnostic: we assert the FP IS present. + # When this test starts to fail (i.e. no-member stops firing), the bug is fixed. + assert "no-member" in output, ( + f"#6352 appears to be fixed (no FP emitted on {sys.platform}). " + f"Promote this test to assert the FP is ABSENT and close. Output: {output!r}" + )