Skip to content

Commit 0984386

Browse files
Prefer .pyi stubs (#2375)
Exempt numpy from these changes for now
1 parent 2ec0115 commit 0984386

File tree

6 files changed

+27
-34
lines changed

6 files changed

+27
-34
lines changed

ChangeLog

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ What's New in astroid 3.2.0?
77
============================
88
Release date: TBA
99

10+
* ``.pyi`` stub files are now preferred over ``.py`` files when resolving imports, (except for numpy).
11+
12+
Closes pylint-dev/#9185
13+
1014
* ``igetattr()`` returns the last same-named function in a class (instead of
1115
the first). This avoids false positives in pylint with ``@overload``.
1216

astroid/interpreter/_import/spec.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,14 @@ def find_module(
161161
pass
162162
submodule_path = sys.path
163163

164+
# We're looping on pyi first because if a pyi exists there's probably a reason
165+
# (i.e. the code is hard or impossible to parse), so we take pyi into account
166+
# But we're not quite ready to do this for numpy, see https://github.com/pylint-dev/astroid/pull/2375
167+
suffixes = (".pyi", ".py", importlib.machinery.BYTECODE_SUFFIXES[0])
168+
numpy_suffixes = (".py", ".pyi", importlib.machinery.BYTECODE_SUFFIXES[0])
164169
for entry in submodule_path:
165170
package_directory = os.path.join(entry, modname)
166-
for suffix in (".py", ".pyi", importlib.machinery.BYTECODE_SUFFIXES[0]):
171+
for suffix in numpy_suffixes if "numpy" in entry else suffixes:
167172
package_file_name = "__init__" + suffix
168173
file_path = os.path.join(package_directory, package_file_name)
169174
if os.path.isfile(file_path):

astroid/modutils.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@
4444

4545

4646
if sys.platform.startswith("win"):
47-
PY_SOURCE_EXTS = ("py", "pyw", "pyi")
47+
PY_SOURCE_EXTS = ("pyi", "pyw", "py")
4848
PY_COMPILED_EXTS = ("dll", "pyd")
4949
else:
50-
PY_SOURCE_EXTS = ("py", "pyi")
50+
PY_SOURCE_EXTS = ("pyi", "py")
5151
PY_COMPILED_EXTS = ("so",)
5252

5353

@@ -499,7 +499,7 @@ def get_source_file(filename: str, include_no_ext: bool = False) -> str:
499499
base, orig_ext = os.path.splitext(filename)
500500
if orig_ext == ".pyi" and os.path.exists(f"{base}{orig_ext}"):
501501
return f"{base}{orig_ext}"
502-
for ext in PY_SOURCE_EXTS:
502+
for ext in PY_SOURCE_EXTS if "numpy" not in filename else reversed(PY_SOURCE_EXTS):
503503
source_path = f"{base}.{ext}"
504504
if os.path.exists(source_path):
505505
return source_path
@@ -671,7 +671,8 @@ def _has_init(directory: str) -> str | None:
671671
else return None.
672672
"""
673673
mod_or_pack = os.path.join(directory, "__init__")
674-
for ext in (*PY_SOURCE_EXTS, "pyc", "pyo"):
674+
exts = reversed(PY_SOURCE_EXTS) if "numpy" in directory else PY_SOURCE_EXTS
675+
for ext in (*exts, "pyc", "pyo"):
675676
if os.path.exists(mod_or_pack + "." + ext):
676677
return mod_or_pack + "." + ext
677678
return None

requirements_full.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Packages used to run additional tests
55
attrs
66
nose
7-
numpy>=1.17.0; python_version<"3.11"
7+
numpy>=1.17.0; python_version<"3.12"
88
python-dateutil
99
PyQt6
1010
regex

tests/brain/test_attr.py

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ class Eggs:
9090
def test_attrs_transform(self) -> None:
9191
"""Test brain for decorators of the 'attrs' package.
9292
93-
Package added support for 'attrs' a long side 'attr' in v21.3.0.
93+
Package added support for 'attrs' alongside 'attr' in v21.3.0.
9494
See: https://github.com/python-attrs/attrs/releases/tag/21.3.0
9595
"""
9696
module = astroid.parse(
@@ -153,36 +153,12 @@ class Eggs:
153153
@frozen
154154
class Legs:
155155
d = attrs.field(default=attrs.Factory(dict))
156-
157-
m = Legs(d=1)
158-
m.d['answer'] = 42
159-
160-
@define
161-
class FooBar:
162-
d = attrs.field(default=attrs.Factory(dict))
163-
164-
n = FooBar(d=1)
165-
n.d['answer'] = 42
166-
167-
@mutable
168-
class BarFoo:
169-
d = attrs.field(default=attrs.Factory(dict))
170-
171-
o = BarFoo(d=1)
172-
o.d['answer'] = 42
173-
174-
@my_mutable
175-
class FooFoo:
176-
d = attrs.field(default=attrs.Factory(dict))
177-
178-
p = FooFoo(d=1)
179-
p.d['answer'] = 42
180156
"""
181157
)
182158

183-
for name in ("f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p"):
159+
for name in ("f", "g", "h", "i", "j", "k", "l"):
184160
should_be_unknown = next(module.getattr(name)[0].infer()).getattr("d")[0]
185-
self.assertIsInstance(should_be_unknown, astroid.Unknown)
161+
self.assertIsInstance(should_be_unknown, astroid.Unknown, name)
186162

187163
def test_special_attributes(self) -> None:
188164
"""Make sure special attrs attributes exist"""

tests/test_modutils.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,11 +291,18 @@ def test(self) -> None:
291291
def test_raise(self) -> None:
292292
self.assertRaises(modutils.NoSourceFile, modutils.get_source_file, "whatever")
293293

294-
def test_(self) -> None:
294+
def test_pyi(self) -> None:
295295
package = resources.find("pyi_data")
296296
module = os.path.join(package, "__init__.pyi")
297297
self.assertEqual(modutils.get_source_file(module), os.path.normpath(module))
298298

299+
def test_pyi_preferred(self) -> None:
300+
package = resources.find("pyi_data/find_test")
301+
module = os.path.join(package, "__init__.py")
302+
self.assertEqual(
303+
modutils.get_source_file(module), os.path.normpath(module) + "i"
304+
)
305+
299306

300307
class IsStandardModuleTest(resources.SysPathSetup, unittest.TestCase):
301308
"""

0 commit comments

Comments
 (0)