pylint-dev / pylint

It's not just a linter that annoys you!
https://pylint.readthedocs.io/en/latest/
GNU General Public License v2.0
5.31k stars 1.13k forks source link

Imports from module using PEP 562's __getattr__ should not raise E0611 #4300

Open jmehnle opened 3 years ago

jmehnle commented 3 years ago

Steps to reproduce

"""
main.py
"""

from dynamic import foo, bar

print(foo)
print(bar)
"""
dynamic.py
"""

def __getattr__(name):
    if name.startswith('x'):
        raise AttributeError
    return 42

Current behavior

$ pylint dynamic.py main.py
************* Module main
main.py:5:0: E0611: No name 'foo' in module 'dynamic' (no-name-in-module)
main.py:5:0: E0611: No name 'bar' in module 'dynamic' (no-name-in-module)

--------------------------------------------------------------------
Your code has been rated at -4.29/10 (previous run: -4.29/10, +0.00)

Expected behavior

Unfortunately the current behavior ignores PEP 562, which officially provides a way to dynamically define module contents. pylint should inhibit E0611 for such modules.

(A related issue was previously logged as https://github.com/PyCQA/pylint/issues/1704.)

pylint --version output

Result of pylint --version output:

pylint 2.7.4
astroid 2.5.2
Python 3.8.8 (default, Mar 18 2021, 06:01:57)
[Clang 11.0.3 (clang-1103.0.32.62)]
patelamol commented 3 years ago

Concour, facing the same issue.

Pierre-Sassoulas commented 3 years ago

The previous issue was closed because inferring attributes created dynamically is hard to program and then computationally intensive. But let's keep this one open and see if someone is interested to create the feature.

Meanwhile, the configuration to disable the checks is

[TYPECHECK]

# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager

# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=

# Tells whether to warn about missing members when the owner of the attribute
# is inferred to be None.
ignore-none=yes

# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes

# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local

# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis). It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
smackesey commented 2 years ago

PSA for others facing this problem: if you are a library author using dynamically defined attributes, and you want to avoid spamming your users with false positives, then, if you know the names of the dynamically defined attributes, you can use variable annotations to skirt the issue:

# project/__init__.py

foo: Any  # !!!! ESSENTIAL !!!!
bar = 1

_DEPRECATED = {
  "foo": ("bar", bar)
}

def __getattr__(name):
    if name in _DEPRECATED:
        new_name, val = _DEPRECATED[name]
        warnings.warn(f"{name} is deprecated, use {new_name} instead")
        return val
    else:
        raise AttributeError(f"No attribute named {name}")

This works, but pylint does not understand it.

# some/file.py

# Without our "foo: Any" variable annotation above, pylint will raise error E0611 no-name-in-module.
# With the annotation, no error, and everything works.
from project import foo 

foo # => 1  # code works fine