Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Astroid error on 2.14.3 #6986

Closed
Meemaw opened this issue Jun 20, 2022 · 6 comments
Closed

Astroid error on 2.14.3 #6986

Meemaw opened this issue Jun 20, 2022 · 6 comments
Labels
Crash 💥 A bug that makes pylint crash Downstream Bug 🪲 The problem happens in a lib depending on pylint, not pylint Waiting on author Indicate that maintainers are waiting for a message of the author
Milestone

Comments

@Meemaw
Copy link

Meemaw commented Jun 20, 2022

Bug description

After upgrading from 2.13.18 --> 2.14.3, we're seeing some parsing pylint errors.

Configuration

[MASTER]
load-plugins=
  pylint_django,
  pylint_django.checkers.migrations,
  os_lint.migrations,

django-settings-module=etherbay_api.settings

[MESSAGES CONTROL]
# E0901 - Migration import %s not allowed (invalid-migration-import)
# E0902 - Tables %s and %s should be in different migrations (multiple-tables-in-migration)
# E0903 - Index for table %s must use operations.AddIndexConcurrently (index-must-be-concurrent)
# E0904 - Index for table %s must use migrations.SeparateDatabaseAndState (index-must-separate-state)
# C0301 - Line Too Long
# C0103 - Variable name "e" doesn't conform to snake_case naming style (invalid-name)
# W1203 - Use % formatting in logging functions and pass the % parameters as arguments (logging-fstring-interpolation)
# W1505 - Using deprecated method warn() (deprecated-method)
# R0201 - Method could be a function (no-self-use)
# R0913 - Too many arguments (6/5) (too-many-arguments)
# W0703 - Catching too general exception Exception (broad-except)
# R1710 - Either all return statements in a function should return an expression, or none of them should. (inconsistent-return-statements)
# C0411 - Wrong import order
# C0412 - (ungrouped-imports)
# R0801 - Similar lines in 2 files
# R0123 - Comparison to literal
# W0613 - Unused argument
# C0303 - Trailing whitespace
# C0305 - trailing newline
# W0107 - Unnecessary pass statement
# R0914 - Too many local vars
# W0101 - Unreachable code (unreachable)
# W1401 - anomalous backslash in string: '\/'. String constant might be missing an r prefix. (anomalous-backslash-in-string)
# C0304 - Final newline missing (missing-final-newline)
# R0912 - Too many branches (14/12) (too-many-branches)
# E0611 - No name 'manager' in module 'ethmoji' (no-name-in-module)
# W0311 - Bad indentation. Found 2 spaces, expected 4 (bad-indentation)
# W0404 - Reimport 'camel_to_snake' (imported line 8) (reimported)
# C0123 - Using type() instead of isinstance() for a typecheck. (unidiomatic-typecheck)
# R0915 - Too many statements (90/50) (too-many-statements)
# R1702 - Too many nested blocks (10/5) (too-many-nested-blocks)
# C1801 - Do not use `len(SEQUENCE)` to determine if a sequence is empty (len-as-condition)
# W0511 - TODOs
# R1705 - Unnecessary "elif" after "return" (no-else-return)
# W0702 - No exception type(s) specified (bare-except)
# E1128 - Assigning to function call which only returns None (assignment-from-none)
# C0302 - Too many lines in module (5767/1000) (too-many-lines)
# C0321 - More than one statement on a single line (multiple-statements)
# C0121 - Comparison to True should be just 'expr' or 'expr is True' (singleton-comparison)
# E1101 - Instance of 'str' has no 'value' member (no-member)
# C0202 - Class method image_file_from_data should have 'cls' as first argument (bad-classmethod-argument)
# R0205 - Class 'TokenContractManager' inherits from object, can be safely removed from bases in python3 (useless-object-inheritance)
# W0622 - Redefining built-in 'bytes' (redefined-builtin)
# W0105 - String statement has no effect (pointless-string-statement)
# W0621 - Redefining name 'signature' from outer scope (line 3) (redefined-outer-name)
# W0105 - String statement has no effect (pointless-string-statement)
# C0325 - Unnecessary parens after 'assert' keyword (superfluous-parens)
# R0205 - Class 'BaseManager' inherits from object, can be safely removed from bases in python3 (useless-object-inheritance)
# W1508 - os.getenv default type is builtins.int. Expected str or None. (invalid-envvar-default)
# R0902 - Too many instance attributes (15/7) (too-many-instance-attributes)
# W1202 - Use % formatting in logging functions and pass the % parameters as arguments (logging-format-interpolation)
# W0223 - Method '__and__' is abstract in class 'Combinable' but is not overridden (abstract-method)
# W0231 - __init__ method from base class 'Exception' is not called (super-init-not-called)
# W0123 - Use of eval (eval-used)
# W0102 - Dangerous default value [] as argument (dangerous-default-value)
# R0911 - Too many return statements (11/6) (too-many-return-statements)
# W0212 - Access to a protected member _find_matching_event_abi of a client class (protected-access)
# W0106 - Expression "[ar.delete() for ar in asset_relations]" is assigned to nothing (expression-not-assigned)
# W0614 - (unused-wildcard-import)
# C0102 - Black listed name "foo" (blacklisted-name)
# W0201 - Attribute 'all_tokens_encoded' defined outside __init__ (attribute-defined-outside-init)
# E0001 - unexpected indent (<unknown>, line 12) (syntax-error)
# W0221 - Parameters differ from overridden 'save' method (arguments-differ)
# E1305 - Too many arguments for format string (too-many-format-args)
# W0401 - Wildcard import api.models (wildcard-import)
# W0221 - Parameters differ from overridden 'validate' method (arguments-differ)
# W0235 - Useless super delegation in method 'get_queryset' (useless-super-delegation)
# E0401 - Unable to import 'ethmoji.manager' (import-error)
# R5102 - Instead of HttpResponse(content_type='application/json') use JsonResponse() (http-response-with-content-type-json)
# C0413 - Import "from api.signals import _referral_did_match" should be placed at the top of the module (wrong-import-position)
# W0109 - Duplicate key 'downy' in dictionary (duplicate-key)
# E1120: No value for argument 'values' in unbound method call (no-value-for-parameter)
# R0901: Too many ancestors (too-many-ancestors). 7 allowed
disable=missing-docstring,too-few-public-methods,raise-missing-from,super-with-arguments,R0904,R1721,C0415,C0301,C0103,W1203,W1505,R0201,R0913,W0703,R1710,C0411,C0412,R0801,R0123,C0303,C0305,W0107,R0914,W0101,W1401,C0304,R0912,E0611,W0311,W0404,C0123,R0915,R1702,C1801,W0511,R1705,E1128,C0302,C0321,C0121,E1101,C0202,R0205,W0622,W0105,W0621,W0105,C0325,R0205,W1508,R0401,R0902,W1202,W0223,W0231,W0123,W0102,R0911,W0212,W0106,W0614,C0102,W0201,E0001,W0221,E1305,W0401,W0221,W0235,E0401,R5102,C0413,W0109,E1120,R0901,W5198

# R1707 - Disallow trailing comma tuple (trailing-comma-tuple)
# TODO (alex) figure out why this doesn't work
enable=trailing-comma-tuple

[REPORTS]

[LOGGING]

[MISCELLANEOUS]

[SIMILARITIES]

[VARIABLES]

[FORMAT]

[BASIC]

[TYPECHECK]
ignored-classes=TwitterModel
generated-members=fastuuid

[SPELLING]

[DESIGN]

[CLASSES]

[IMPORTS]

[EXCEPTIONS]

Command used

poetry run python -m pylint -j 4 path/a path/b

Pylint output

Traceback (most recent call last):
  File "/runner/_work/company-x/company-x/.venv/lib/python3.10/site-packages/pylint/utils/ast_walker.py", line 90, in walk
    callback(astroid)
  File "/runner/_work/company-x/company-x/os_lint/migrations.py", line 149, in visit_call
    and next(
StopIteration
multiprocessing.pool.RemoteTraceback: 
"""
Traceback (most recent call last):
  File "/runner/_work/company-x/company-x/.venv/lib/python3.10/site-packages/pylint/lint/pylinter.py", line 727, in _check_file
    check_astroid_module(ast_node)
  File "/runner/_work/company-x/company-x/.venv/lib/python3.10/site-packages/pylint/lint/pylinter.py", line 926, in check_astroid_module
    retval = self._check_astroid_module(
  File "/runner/_work/company-x/company-x/.venv/lib/python3.10/site-packages/pylint/lint/pylinter.py", line 976, in _check_astroid_module
    walker.walk(node)
  File "/runner/_work/company-x/company-x/.venv/lib/python3.10/site-packages/pylint/utils/ast_walker.py", line 93, in walk
    self.walk(child)
  File "/runner/_work/company-x/company-x/.venv/lib/python3.10/site-packages/pylint/utils/ast_walker.py", line 93, in walk
    self.walk(child)
  File "/runner/_work/company-x/company-x/.venv/lib/python3.10/site-packages/pylint/utils/ast_walker.py", line 93, in walk
    self.walk(child)
  [Previous line repeated 4 more times]
  File "/runner/_work/company-x/company-x/.venv/lib/python3.10/site-packages/pylint/utils/ast_walker.py", line 90, in walk
    callback(astroid)
  File "/runner/_work/company-x/company-x/os_lint/migrations.py", line 149, in visit_call
    and next(
StopIteration
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  File "/opt/hostedtoolcache/Python/3.10.3/x64/lib/python3.10/multiprocessing/pool.py", line 125, in worker
    result = (True, func(*args, **kwds))
  File "/runner/_work/company-x/company-x/.venv/lib/python3.10/site-packages/pylint/lint/parallel.py", line 72, in _worker_check_single_file
    _worker_linter.check_single_file_item(file_item)
  File "/runner/_work/company-x/company-x/.venv/lib/python3.10/site-packages/pylint/lint/pylinter.py", line 673, in check_single_file_item
    self._check_file(self.get_ast, check_astroid_module, file)
  File "/runner/_work/company-x/company-x/.venv/lib/python3.10/site-packages/pylint/lint/pylinter.py", line 729, in _check_file
    raise astroid.AstroidError from e
astroid.exceptions.AstroidError
"""
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  File "/opt/hostedtoolcache/Python/3.10.3/x64/lib/python3.10/runpy.py", line 196, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/opt/hostedtoolcache/Python/3.10.3/x64/lib/python3.10/runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "/runner/_work/company-x/company-x/.venv/lib/python3.10/site-packages/pylint/__main__.py", line 10, in <module>
    pylint.run_pylint()
  File "/runner/_work/company-x/company-x/.venv/lib/python3.10/site-packages/pylint/__init__.py", line 25, in run_pylint
    PylintRun(argv or sys.argv[1:])
  File "/runner/_work/company-x/company-x/.venv/lib/python3.10/site-packages/pylint/lint/run.py", line 204, in __init__
    linter.check(args)
  File "/runner/_work/company-x/company-x/.venv/lib/python3.10/site-packages/pylint/lint/pylinter.py", line 650, in check
    check_parallel(
  File "/runner/_work/company-x/company-x/.venv/lib/python3.10/site-packages/pylint/lint/parallel.py", line 152, in check_parallel
    for (
  File "/opt/hostedtoolcache/Python/3.10.3/x64/lib/python3.10/multiprocessing/pool.py", line 870, in next
    raise value
astroid.exceptions.AstroidError

Expected behavior

Pylint works

Pylint version

[[package]]
name = "astroid"
version = "2.11.6"

[[package]]
name = "pylint"
version = "2.14.3"

python-versions = "3.10.3"

OS / Environment

No response

Additional dependencies

No response

@Meemaw Meemaw added the Needs triage 📥 Just created, needs acknowledgment, triage, and proper labelling label Jun 20, 2022
@DanielNoord
Copy link
Collaborator

Could you share the os_lint/migrations.py file? I'm wondering what the visit_call function is that is mentioned a couple of times in the traceback.

@Pierre-Sassoulas Pierre-Sassoulas added Crash 💥 A bug that makes pylint crash Needs astroid update Needs an astroid update (probably a release too) before being mergable labels Jun 20, 2022
@jacobtylerwalls jacobtylerwalls added Waiting on author Indicate that maintainers are waiting for a message of the author Downstream Bug 🪲 The problem happens in a lib depending on pylint, not pylint labels Jun 20, 2022
@Meemaw
Copy link
Author

Meemaw commented Jun 20, 2022

Failing line:

and next(
    str(k.value.value) for k in getattr(node, "keywords", node) if getattr(k, "arg", None) == "model_name"
)

Full file:

"""Migrations checkers for CompanyX API."""

from __future__ import annotations

import os
import sys
from typing import TYPE_CHECKING, Any

import astroid
from astroid import nodes
from pylint.checkers import BaseChecker
from pylint.checkers.utils import check_messages
from pylint.interfaces import IAstroidChecker

if TYPE_CHECKING:
    from pylint.lint import PyLinter


### migrations checker ###

# Structure for ALLOW_IMPORT dict:
# "directory name" -> [RECURSE, allowed1, allowed2, ...]
#
# RECURSE is a dict with the same structure:
# "subdir name" -> [RECURSE, allowed1, allowed2, ...]

ALLOW_IMPORT = {
    "api": [
        {
            "lib": [
                {
                    "models": [{}, "base_model"],
                    "moonpay": [{}, "types"],
                    "utils": [{}, "string_utils"],
                },
                "not-allowed",
            ],
            "model": [{}, "fields"],
        },
        "managers",
        "types",
    ],
    "core": [
        {},
        "blockchain",
        "types",
    ],
    "contrib": [
        {},
        "not-allowed",
    ],
    "dal": [
        {},
        "not-allowed",
    ],
    "src": [
        {},
        "not-allowed",
    ],
}

MIGRATIONS_PREFIX = "api.migrations."

MSGS = {
    "E0901": (
        "Migration import %s not allowed",
        "invalid-migration-import",
        "Used when api import detected for migrations.",
    ),
    "E0902": (
        "Tables %s and %s should be in different migrations",
        "multiple-tables-in-migration",
        "Used when multiple tables are updated in the same migration.",
    ),
    "E0903": (
        "Index for table %s must use operations.AddIndexConcurrently",
        "index-must-be-concurrent",
        "Used when index is does not use operations.AddIndexConcurrently.",
    ),
    "E0904": (
        "Index for table %s must use migrations.SeparateDatabaseAndState",
        "index-must-separate-state",
        "Used when index is does not use migrations.SeparateDatabaseAndState.",
    ),
}


class MigrationsChecker(BaseChecker):
    """Checker for CompanyX migrations

    Checks migration dependencies on api except for:
      * api.lib.models.base_model
      * api.lib.moonpay.types
      * api.lib.utils.string_utils
      * api.managers
      * api.model.fields
      * api.types
      * core.blockchain
      * core.types
    """

    __implements__ = IAstroidChecker

    name = "migrations"
    msgs = MSGS
    created_models = []
    current_model_name = None
    current_root_name = None
    model_managers = []

    @check_messages("multiple-tables-in-migration", "index-must-be-concurrent", "index-must-separate-state")
    def visit_call(self, node: nodes.Call) -> None:
        """Triggered when a function call is seen."""
        root_name = node.root().name
        for exempt_migration in [
            "0001_squashed",
            "0680_alter_assetevent_event_type",
            "0682_create_twitter_metadata_table",
            "0694_remove_assetcontract_api_assetco",
        ]:
            if exempt_migration in root_name:
                # skip multiple table checks for squashed migrations
                return

        if not (root_name.startswith(MIGRATIONS_PREFIX)):
            # skip checks for non-migration nodes
            return

        if (
            getattr(node, "parent", None)
            and type(node.parent).__name__ == "Keyword"
            and getattr(node.parent, "arg", "") in ["index", "constraint"]
        ):
            # skip checks for index and constraint names
            return

        if self.current_root_name != root_name:
            # initialize checks for migration file
            self.created_models = []
            self.current_model_name = None
            self.current_root_name = root_name
            self.model_managers = []

        function_name = getattr(node.func, "attrname", getattr(node.func, "name", str(node.func)))
        self._check_multiple_tables(node, function_name)
        if (
            self.current_model_name
            and "index" in function_name.lower()
            and next(
                str(k.value.value) for k in getattr(node, "keywords", node) if getattr(k, "arg", None) == "model_name"
            )
        ):
            self._check_add_index(node, function_name)

    def _check_add_index(self, node: nodes.Call, function_name: str) -> None:
        root_name = node.root().name

        if "AddIndexConcurrently" != function_name:
            self.add_message(
                "index-must-be-concurrent",
                node=node,
                args=(self.current_model_name,),
            )

        if not (
            getattr(node, "parent", None)
            and getattr(node.parent, "parent", None)
            and type(node.parent.parent).__name__ == "Keyword"
            and getattr(node.parent.parent, "arg", "") == "state_operations"
            and getattr(node.parent.parent, "parent", None)
            and getattr(node.parent.parent.parent, "func", None)
        ):
            parent_func = getattr(node.parent.parent.parent, "func", node.func)
            if (
                getattr(parent_func, "attrname", None) or getattr(parent_func, "name", None)
            ) != "SeparateDatabaseAndState":
                self.add_message(
                    "index-must-separate-state",
                    node=node,
                    args=(self.current_model_name,),
                )

    def _check_multiple_tables(self, node: nodes.Call, function_name: str) -> None:
        root_name = node.root().name

        # table name parameter could be model_name or name
        kw_params = [getattr(k, "arg", None) for k in getattr(node, "keywords", node) if getattr(k, "value", None)]
        kw_model_name = None
        if "model_name" in kw_params:
            # If we have keyword model_name, use that value
            # Since this is a keyword, there will be only one "model_name"
            kw_model_name = next(
                str(k.value.value) for k in getattr(node, "keywords", node) if getattr(k, "arg", None) == "model_name"
            )
        elif "name" in kw_params:
            # If we have keyword name but no model_name, use keyword name
            # Since this is a keyword, there will be only one "name"
            kw_model_name = next(
                str(k.value.value) for k in getattr(node, "keywords", node) if getattr(k, "arg", None) == "name"
            )
        else:
            return

        if not self.current_model_name:
            if function_name == "CreateModel" and not self.model_managers:
                # allow creating multiple tables in one migration
                self.created_models.append(str(kw_model_name).lower())
                return
            elif function_name == "AlterModelManagers" and not self.created_models:
                # allow multiple managers in one migration
                self.model_managers.append(str(kw_model_name).lower())
                return
        elif self.model_managers:
            # Do not allow other migrations with model managers
            self.current_model_name = "has-model-managers"
        elif self.created_models and kw_model_name not in self.created_models:
            # when creating table, only allow adding indexes for created tables
            self.current_model_name = self.created_models[0]

        if not self.current_model_name:
            self.current_model_name = kw_model_name
        if kw_model_name != self.current_model_name:
            self.add_message(
                "multiple-tables-in-migration",
                node=node,
                args=(str(kw_model_name), str(self.current_model_name)),
            )

    @check_messages("invalid-migration-import")
    def visit_import(self, node: nodes.Import) -> None:
        """Triggered when an import statement is seen."""
        self._disallow_migration_imports(node)

    @check_messages("invalid-migration-import")
    def visit_importfrom(self, node: nodes.ImportFrom) -> None:
        """Triggered when a from statement is seen."""
        self._disallow_migration_imports(node)

    def _disallow_migration_imports(self, node: nodes.Import | nodes.ImportFrom):
        """Disallow migration imports from api
        * exceptions are at ALLOW_IMPORT
        """
        if not (node.root().name.startswith(MIGRATIONS_PREFIX)):
            # skip import checks for non-migration nodes
            return

        # Loop over imports for migrations
        for name in node.names:
            if name and name[0]:
                split_name: list[str] = name[0].split(".")
                if split_name[0] in ALLOW_IMPORT:
                    self._check_migration_imports(node, ALLOW_IMPORT, name[0], split_name)

    def _check_migration_imports(
        self, node: nodes.Import | nodes.ImportFrom, allow_dict, full_name: str, split_name: list[str]
    ):
        """Disallow migration imports from api
        * exceptions are at ALLOW_IMPORT
        """
        if len(split_name) < 2 or split_name[0] not in allow_dict:
            self.add_message(
                "invalid-migration-import",
                node=node,
                args=(full_name,),
            )

        elif split_name[1] not in allow_dict[split_name[0]]:
            self._check_migration_imports(node, allow_dict[split_name[0]][0], full_name, split_name[1:])


def register(linter: PyLinter) -> None:
    linter.register_checker(MigrationsChecker(linter))

@DanielNoord
Copy link
Collaborator

DanielNoord commented Jun 20, 2022

Failing line:

and next(
    str(k.value.value) for k in getattr(node, "keywords", node) if getattr(k, "arg", None) == "model_name"
)

If there are no k's with an attribute arg that matches model_name this will raise a StopIteration error due to next not returning anything. See docs.

I'm wondering why this was changed between 2.13 and 2.14 and could perhaps do some investigation, but adding a default value here should already stop the crashes.

We actually have an open issue about this: #4725

@Meemaw
Copy link
Author

Meemaw commented Jun 20, 2022

What would the default value be? I guess a better workaround is to just except the StopIteration error here, and pass.

Though, it would be interesting to understand what changed from 2.13 -> 2.14

@DanielNoord
Copy link
Collaborator

Well you're using it in an if so I think "" or None should both work as the if seems to be negated if none are found.

Yeah, it seems like this is proprietary code so it might be hard to debug this, but for a start you could see which k's actually pass the == "model_name" check by expanding the next into a for loop and printing k if the condition passes.
Then doing the same on 2.14 and seeing whether the same nodes turn up or if they are already missing from the for loop before that.

That's at least how I would tackle this: a bit of back and forth with print statements to see which condition of the 3 or 4 that are in the next call is "broken" on 2.14. That might narrow down to a change within either pylint or astroid, which is our underlying code/ast interpreter.

@DanielNoord DanielNoord removed the Needs triage 📥 Just created, needs acknowledgment, triage, and proper labelling label Jun 21, 2022
@jacobtylerwalls jacobtylerwalls removed the Needs astroid update Needs an astroid update (probably a release too) before being mergable label Jun 22, 2022
@Pierre-Sassoulas Pierre-Sassoulas added this to the 2.15.0 milestone Jul 17, 2022
@DanielNoord
Copy link
Collaborator

I'm going to close this issue as won't fix. I'm not sure if the change is with us or if the file you're linting changed and this caused the condition to never be met. Either way, the fix for this is relatively simple and we can't do much on our end: we can't guarantee that every Keyword node will have an arg attributes that matches model_name which would be the only way to avoid this crash.

If any further help is needed, don't hesitate to contact me but this should no longer block us from releasing 2.15.

@DanielNoord DanielNoord closed this as not planned Won't fix, can't repro, duplicate, stale Aug 25, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Crash 💥 A bug that makes pylint crash Downstream Bug 🪲 The problem happens in a lib depending on pylint, not pylint Waiting on author Indicate that maintainers are waiting for a message of the author
Projects
None yet
Development

No branches or pull requests

4 participants