From 033ffd636cdd01bced93d6675626851ac7c1f104 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 4 Feb 2024 09:30:22 -0500 Subject: [PATCH 1/7] Prefer `.pyi` stubs --- ChangeLog | 4 ++++ astroid/interpreter/_import/spec.py | 2 +- astroid/modutils.py | 4 ++-- tests/test_modutils.py | 9 ++++++++- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/ChangeLog b/ChangeLog index 5e4f68f3aa..6ffebc87af 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,10 @@ What's New in astroid 3.2.0? ============================ Release date: TBA +* ``.pyi`` stub files are now preferred over ``.py`` files when resolving imports. + + Closes pylint-dev/#9185 + * ``igetattr()`` returns the last same-named function in a class (instead of the first). This avoids false positives in pylint with ``@overload``. diff --git a/astroid/interpreter/_import/spec.py b/astroid/interpreter/_import/spec.py index 93096e54e6..c54b309f8f 100644 --- a/astroid/interpreter/_import/spec.py +++ b/astroid/interpreter/_import/spec.py @@ -162,7 +162,7 @@ def find_module( for entry in submodule_path: package_directory = os.path.join(entry, modname) - for suffix in (".py", ".pyi", importlib.machinery.BYTECODE_SUFFIXES[0]): + for suffix in (".pyi", ".py", importlib.machinery.BYTECODE_SUFFIXES[0]): package_file_name = "__init__" + suffix file_path = os.path.join(package_directory, package_file_name) if os.path.isfile(file_path): diff --git a/astroid/modutils.py b/astroid/modutils.py index b2f559a1f1..1066bcd2aa 100644 --- a/astroid/modutils.py +++ b/astroid/modutils.py @@ -44,10 +44,10 @@ if sys.platform.startswith("win"): - PY_SOURCE_EXTS = ("py", "pyw", "pyi") + PY_SOURCE_EXTS = ("pyi", "pyw", "py") PY_COMPILED_EXTS = ("dll", "pyd") else: - PY_SOURCE_EXTS = ("py", "pyi") + PY_SOURCE_EXTS = ("pyi", "py") PY_COMPILED_EXTS = ("so",) diff --git a/tests/test_modutils.py b/tests/test_modutils.py index 929c58992c..3af8062aa9 100644 --- a/tests/test_modutils.py +++ b/tests/test_modutils.py @@ -290,11 +290,18 @@ def test(self) -> None: def test_raise(self) -> None: self.assertRaises(modutils.NoSourceFile, modutils.get_source_file, "whatever") - def test_(self) -> None: + def test_pyi(self) -> None: package = resources.find("pyi_data") module = os.path.join(package, "__init__.pyi") self.assertEqual(modutils.get_source_file(module), os.path.normpath(module)) + def test_pyi_preferred(self) -> None: + package = resources.find("pyi_data/find_test") + module = os.path.join(package, "__init__.py") + self.assertEqual( + modutils.get_source_file(module), os.path.normpath(module) + "i" + ) + class IsStandardModuleTest(resources.SysPathSetup, unittest.TestCase): """ From c519a628677a4bbe33dd0f79fe5d5b81e5b830ca Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 4 Feb 2024 09:36:29 -0500 Subject: [PATCH 2/7] Update attrs test --- tests/brain/test_attr.py | 30 +++--------------------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/tests/brain/test_attr.py b/tests/brain/test_attr.py index 5185dff0c1..e428b0c8d2 100644 --- a/tests/brain/test_attr.py +++ b/tests/brain/test_attr.py @@ -90,7 +90,7 @@ class Eggs: def test_attrs_transform(self) -> None: """Test brain for decorators of the 'attrs' package. - Package added support for 'attrs' a long side 'attr' in v21.3.0. + Package added support for 'attrs' alongside 'attr' in v21.3.0. See: https://github.com/python-attrs/attrs/releases/tag/21.3.0 """ module = astroid.parse( @@ -153,36 +153,12 @@ class Eggs: @frozen class Legs: d = attrs.field(default=attrs.Factory(dict)) - - m = Legs(d=1) - m.d['answer'] = 42 - - @define - class FooBar: - d = attrs.field(default=attrs.Factory(dict)) - - n = FooBar(d=1) - n.d['answer'] = 42 - - @mutable - class BarFoo: - d = attrs.field(default=attrs.Factory(dict)) - - o = BarFoo(d=1) - o.d['answer'] = 42 - - @my_mutable - class FooFoo: - d = attrs.field(default=attrs.Factory(dict)) - - p = FooFoo(d=1) - p.d['answer'] = 42 """ ) - for name in ("f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p"): + for name in ("f", "g", "h", "i", "j", "k", "l"): should_be_unknown = next(module.getattr(name)[0].infer()).getattr("d")[0] - self.assertIsInstance(should_be_unknown, astroid.Unknown) + self.assertIsInstance(should_be_unknown, astroid.Unknown, name) def test_special_attributes(self) -> None: """Make sure special attrs attributes exist""" From ed6d495a7cbd52dd4a52edbcee59039ff6106b00 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 4 Feb 2024 11:04:57 -0500 Subject: [PATCH 3/7] Add comment Co-authored-by: Pierre Sassoulas --- astroid/interpreter/_import/spec.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/astroid/interpreter/_import/spec.py b/astroid/interpreter/_import/spec.py index c54b309f8f..d1c3f1a7e9 100644 --- a/astroid/interpreter/_import/spec.py +++ b/astroid/interpreter/_import/spec.py @@ -162,6 +162,8 @@ def find_module( for entry in submodule_path: package_directory = os.path.join(entry, modname) + # We're looping on pyi first because if a pyi exists there's probably a reason + # (i.e. the code is hard or impossible to parse), so we take pyi into account for suffix in (".pyi", ".py", importlib.machinery.BYTECODE_SUFFIXES[0]): package_file_name = "__init__" + suffix file_path = os.path.join(package_directory, package_file_name) From c8bf2e60f265b69d4472626c1caf9879cf9a5452 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 10 Feb 2024 11:44:40 -0500 Subject: [PATCH 4/7] Run numpy brain tests on 3.11 --- requirements_full.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_full.txt b/requirements_full.txt index 346aa275d3..e8196e629d 100644 --- a/requirements_full.txt +++ b/requirements_full.txt @@ -4,7 +4,7 @@ # Packages used to run additional tests attrs nose -numpy>=1.17.0; python_version<"3.11" +numpy>=1.17.0; python_version<"3.12" python-dateutil PyQt6 regex From 6e33269a63d962a3c26421693fc4ca226a33aad0 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 10 Feb 2024 12:04:17 -0500 Subject: [PATCH 5/7] Exempt numpy from these changes --- astroid/interpreter/_import/spec.py | 9 ++++++--- astroid/modutils.py | 5 +++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/astroid/interpreter/_import/spec.py b/astroid/interpreter/_import/spec.py index d1c3f1a7e9..05050bf86b 100644 --- a/astroid/interpreter/_import/spec.py +++ b/astroid/interpreter/_import/spec.py @@ -160,11 +160,14 @@ def find_module( pass submodule_path = sys.path + # We're looping on pyi first because if a pyi exists there's probably a reason + # (i.e. the code is hard or impossible to parse), so we take pyi into account + # But we're not quite ready to do this for numpy + suffixes = (".pyi", ".py", importlib.machinery.BYTECODE_SUFFIXES[0]) + numpy_suffixes = (".py", ".pyi", importlib.machinery.BYTECODE_SUFFIXES[0]) for entry in submodule_path: package_directory = os.path.join(entry, modname) - # We're looping on pyi first because if a pyi exists there's probably a reason - # (i.e. the code is hard or impossible to parse), so we take pyi into account - for suffix in (".pyi", ".py", importlib.machinery.BYTECODE_SUFFIXES[0]): + for suffix in numpy_suffixes if "numpy" in entry else suffixes: package_file_name = "__init__" + suffix file_path = os.path.join(package_directory, package_file_name) if os.path.isfile(file_path): diff --git a/astroid/modutils.py b/astroid/modutils.py index 1066bcd2aa..6f67d1ab9a 100644 --- a/astroid/modutils.py +++ b/astroid/modutils.py @@ -499,7 +499,7 @@ def get_source_file(filename: str, include_no_ext: bool = False) -> str: base, orig_ext = os.path.splitext(filename) if orig_ext == ".pyi" and os.path.exists(f"{base}{orig_ext}"): return f"{base}{orig_ext}" - for ext in PY_SOURCE_EXTS: + for ext in PY_SOURCE_EXTS if "numpy" not in filename else reversed(PY_SOURCE_EXTS): source_path = f"{base}.{ext}" if os.path.exists(source_path): return source_path @@ -671,7 +671,8 @@ def _has_init(directory: str) -> str | None: else return None. """ mod_or_pack = os.path.join(directory, "__init__") - for ext in (*PY_SOURCE_EXTS, "pyc", "pyo"): + exts = reversed(PY_SOURCE_EXTS) if "numpy" in directory else PY_SOURCE_EXTS + for ext in (*exts, "pyc", "pyo"): if os.path.exists(mod_or_pack + "." + ext): return mod_or_pack + "." + ext return None From 06aeb121f8bc3d971ed2f48cf59a366628d35a78 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 6 May 2024 08:02:30 -0400 Subject: [PATCH 6/7] mention numpy exception Co-authored-by: Pierre Sassoulas --- ChangeLog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 6ffebc87af..e71faa47fa 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,7 +7,7 @@ What's New in astroid 3.2.0? ============================ Release date: TBA -* ``.pyi`` stub files are now preferred over ``.py`` files when resolving imports. +* ``.pyi`` stub files are now preferred over ``.py`` files when resolving imports, (except for numpy). Closes pylint-dev/#9185 From 758f4d89034fd74facd0a6f02f60ceb6692a7131 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 6 May 2024 08:02:44 -0400 Subject: [PATCH 7/7] Update spec.py Co-authored-by: Pierre Sassoulas --- astroid/interpreter/_import/spec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astroid/interpreter/_import/spec.py b/astroid/interpreter/_import/spec.py index 05050bf86b..10330597a6 100644 --- a/astroid/interpreter/_import/spec.py +++ b/astroid/interpreter/_import/spec.py @@ -162,7 +162,7 @@ def find_module( # We're looping on pyi first because if a pyi exists there's probably a reason # (i.e. the code is hard or impossible to parse), so we take pyi into account - # But we're not quite ready to do this for numpy + # But we're not quite ready to do this for numpy, see https://github.com/pylint-dev/astroid/pull/2375 suffixes = (".pyi", ".py", importlib.machinery.BYTECODE_SUFFIXES[0]) numpy_suffixes = (".py", ".pyi", importlib.machinery.BYTECODE_SUFFIXES[0]) for entry in submodule_path: