Repository: oppia/oppia

Issue:
Remove `ExplicitKeywordArgsChecker`
I had to disable both pylint checks that are implemented in `ExplicitKeywordArgsChecker` when doing the Python 3 migration. I think we should just get rid of that checker as it is not needed.

I do not think it is worth it resurrecting that check, because:
  1. The behaviour of `arg-name-for-non-keyword-arg` can now be implemented using Python syntax ([using asterisk in function args](https://stackoverflow.com/questions/14301967/bare-asterisk-in-function-arguments)) and we already use that in some places.
  2. The behaviour of `arg-name-for-non-keyword-arg` can now be done [using slash](https://docs.python.org/3/whatsnew/3.8.html#positional-only-parameters).


Discussion:
@U8NWXD What do you think?
For the asterisk and slash to replace the linter's functionality, we'd need to make sure that every function declaration uses that syntax, right?
> For the asterisk and slash to replace the linter's functionality, we'd need to make sure that every function declaration uses that syntax, right?

Hmm, as the linter is now disabled and I think it was also broken before (as in many cases it didn't complain about some instances of errors), I don't think the introduction of asterisk and slash is a prerequirement for this. I think it is even a bit hard to say how exactly we should require this or if this should be optional.
OK, I'm fine with removing the linter
Hi @vojtechjelinek @U8NWXD can I get assigned to this one?
@yurilemos Sorry for delay, are you still interested?
Yes @vojtechjelinek , I'm still interested
@yurilemos Done, assigned.

Resolving patch:
diff --git a/core/utils.py b/core/utils.py
--- a/core/utils.py
+++ b/core/utils.py
@@ -1401,7 +1401,7 @@ def url_open(
     """
     # TODO(#12912): Remove pylint disable after the arg-name-for-non-keyword-arg
     # check is refactored.
-    context = ssl.create_default_context(cafile=certifi.where())  # pylint: disable=arg-name-for-non-keyword-arg
+    context = ssl.create_default_context(cafile=certifi.where())
     return urllib.request.urlopen(source_url, context=context)
 
 
diff --git a/scripts/linters/pylint_extensions.py b/scripts/linters/pylint_extensions.py
--- a/scripts/linters/pylint_extensions.py
+++ b/scripts/linters/pylint_extensions.py
@@ -55,7 +55,6 @@
 import astroid  # isort:skip  pylint: disable=wrong-import-order, wrong-import-position
 from pylint import checkers  # isort:skip  pylint: disable=wrong-import-order, wrong-import-position
 from pylint import interfaces  # isort:skip  pylint: disable=wrong-import-order, wrong-import-position
-from pylint.checkers import typecheck  # isort:skip  pylint: disable=wrong-import-order, wrong-import-position
 from pylint.checkers import utils as checker_utils  # isort:skip  pylint: disable=wrong-import-order, wrong-import-position
 from pylint.extensions import _check_docs_utils # isort:skip  pylint: disable=wrong-import-order, wrong-import-position
 
@@ -73,202 +72,6 @@ def read_from_node(node: astroid.scoped_nodes.Module) -> List[str]:
     return [line.decode('utf-8') for line in node.stream().readlines()]
 
 
-# TODO(#16567): Here we use MyPy ignore because of the incomplete typing of
-# pylint library and absences of stubs in pylint, forces MyPy to
-# assume that BaseChecker class has attributes of type Any.
-# Thus to avoid MyPy's error
-# (Class cannot subclass 'BaseChecker' (has type 'Any')),
-# we added an ignore here.
-class ExplicitKeywordArgsChecker(checkers.BaseChecker):  # type: ignore[misc]
-    """Custom pylint checker which checks for explicit keyword arguments
-    in any function call.
-    """
-
-    __implements__ = interfaces.IAstroidChecker
-
-    name = 'explicit-keyword-args'
-    priority = -1
-    msgs = {
-        'C0001': (
-            'Keyword argument %s should be named explicitly in %s call of %s.',
-            'non-explicit-keyword-args',
-            'All keyword arguments should be explicitly named in function call.'
-        ),
-        'C0027': (
-            'Keyword argument %s used for a non keyword argument in %s '
-            'call of %s.',
-            'arg-name-for-non-keyword-arg',
-            'Position arguments should not be used as keyword arguments '
-            'in function call.'
-        ),
-    }
-
-    def _check_non_explicit_keyword_args(
-        self,
-        node: astroid.Call,
-        name: str,
-        callable_name: str,
-        keyword_args: List[str],
-        num_positional_args_unused: int,
-        num_mandatory_parameters: int
-    ) -> int:
-        """Custom pylint check to ensure that position arguments should not
-        be used as keyword arguments.
-
-        Args:
-            node: astroid.Call. The current function call node.
-            name: str. Name of the keyword argument.
-            callable_name: str. Name of method type.
-            keyword_args: list(str). Name of all keyword arguments in function
-                call.
-            num_positional_args_unused: int. Number of unused positional
-                arguments.
-            num_mandatory_parameters: int. Number of mandatory parameters.
-
-        Returns:
-            int. Number of unused positional arguments.
-        """
-        display_name = repr(name)
-
-        if name not in keyword_args and (
-                num_positional_args_unused > (
-                    num_mandatory_parameters)) and (
-                        callable_name != 'constructor'):
-            # This try/except block tries to get the function
-            # name. Since each node may differ, multiple
-            # blocks have been used.
-            try:
-                func_name = node.func.attrname
-            except AttributeError:
-                func_name = node.func.name
-
-            self.add_message(
-                'non-explicit-keyword-args', node=node,
-                args=(
-                    display_name,
-                    callable_name,
-                    func_name))
-            num_positional_args_unused -= 1
-        return num_positional_args_unused
-
-    def _check_argname_for_nonkeyword_arg(
-        self,
-        node: astroid.NodeNG,
-        called: astroid.NodeNG,
-        callable_name: str,
-        keyword_args: List[str],
-        keyword_args_in_funcdef: List[str]
-    ) -> None:
-        """Custom pylint check to ensure that position arguments should not
-        be used as keyword arguments.
-
-        Args:
-            node: astroid.NodeNG. The current function call node.
-            called: astroid.NodeNG. The function call object.
-            keyword_args: list(str). Name of all keyword arguments in function
-                call.
-            callable_name: str. Name of method type.
-            keyword_args_in_funcdef: list(str). Name of all keyword arguments in
-                function definition.
-        """
-        for arg in keyword_args:
-            # TODO(#10038): Fix the check to cover below case as well.
-            # If there is *args and **kwargs in the function definition skip the
-            # check because we can use keywords arguments in function call even
-            # if **kwargs is present in the function definition. See Example:
-            # Function def -> def func(entity_id, *args, **kwargs):
-            # Function call -> func(entity_id='1', a=1, b=2, c=3)
-            # By parsing calling method we get
-            # keyword_arguments = entity_id, a, b, c.
-            # From the function definition, we will get keyword_arguments = []
-            # Now we do not have a way to identify which one is a keyword
-            # argument and which one is not.
-            if not called.args.kwarg and callable_name != 'constructor':
-                if not arg in keyword_args_in_funcdef:
-                    # This try/except block tries to get the function
-                    # name.
-                    try:
-                        func_name = node.func.attrname
-                    except AttributeError:
-                        func_name = node.func.name
-
-                    self.add_message(
-                        'arg-name-for-non-keyword-arg', node=node,
-                        args=(repr(arg), callable_name, func_name))
-
-    def visit_call(self, node: astroid.nodes.Call) -> None:
-        """Visits each function call in a lint check.
-
-        Args:
-            node: Call. The current function call node.
-        """
-        called = checker_utils.safe_infer(node.func)
-
-        try:
-            # For the rationale behind the Pylint pragma below,
-            # see https://stackoverflow.com/a/35701863/8115428
-            called, implicit_args, callable_name = (
-                typecheck._determine_callable(called))  # pylint: disable=protected-access
-        except ValueError:
-            return
-
-        if called.args.args is None:
-            # Built-in functions have no argument information.
-            return
-
-        if len(called.argnames()) != len(set(called.argnames())):
-            return
-
-        # Build the set of keyword arguments and count the positional arguments.
-        call_site = astroid.arguments.CallSite.from_call(node)
-
-        num_positional_args = len(call_site.positional_arguments)
-        keyword_args = list(call_site.keyword_arguments.keys())
-
-        already_filled_positionals = getattr(called, 'filled_positionals', 0)
-        already_filled_keywords = getattr(called, 'filled_keywords', {})
-
-        keyword_args += list(already_filled_keywords)
-        num_positional_args += already_filled_positionals
-        num_positional_args += implicit_args
-
-        # Analyze the list of formal parameters.
-        num_mandatory_parameters = len(called.args.args) - len(
-            called.args.defaults
-        )
-
-        parameters: List[
-            Tuple[Tuple[Optional[str], Optional[astroid.NodeNG]], bool]
-        ] = []
-        parameter_name_to_index = {}
-        for i, arg in enumerate(called.args.args):
-            assert isinstance(arg, astroid.AssignName)
-            name = arg.name
-            parameter_name_to_index[name] = i
-            if i >= num_mandatory_parameters:
-                defval = called.args.defaults[i - num_mandatory_parameters]
-            else:
-                defval = None
-            parameters.append(((name, defval), False))
-
-        num_positional_args_unused = num_positional_args
-        # The list below will store all the keyword arguments present in the
-        # function definition.
-        keyword_args_in_funcdef = []
-        # Check that all parameters with a default value have
-        # been called explicitly.
-        for [(name, defval), _] in parameters:
-            if defval:
-                keyword_args_in_funcdef.append(name)
-                num_positional_args_unused = (
-                    self._check_non_explicit_keyword_args(
-                        node, name, callable_name, keyword_args,
-                        num_positional_args_unused, num_mandatory_parameters))
-
-        self._check_argname_for_nonkeyword_arg(
-            node, called, callable_name, keyword_args, keyword_args_in_funcdef)
-
-
 # TODO(#16567): Here we use MyPy ignore because of the incomplete typing of
 # pylint library and absences of stubs in pylint, forces MyPy to
 # assume that BaseChecker class has attributes of type Any.
@@ -3122,7 +2925,6 @@ def register(linter: lint.PyLinter) -> None:
     Args:
         linter: Pylinter. The Pylinter object.
     """
-    linter.register_checker(ExplicitKeywordArgsChecker(linter))
     linter.register_checker(HangingIndentChecker(linter))
     linter.register_checker(DocstringParameterChecker(linter))
     linter.register_checker(ImportOnlyModulesChecker(linter))


Test patch:
diff --git a/scripts/linters/pylint_extensions_test.py b/scripts/linters/pylint_extensions_test.py
--- a/scripts/linters/pylint_extensions_test.py
+++ b/scripts/linters/pylint_extensions_test.py
@@ -31,160 +31,9 @@
 import astroid  # isort:skip
 from pylint import interfaces  # isort:skip
 from pylint import testutils  # isort:skip
-from pylint import lint  # isort:skip
 from pylint import utils as pylint_utils  # isort:skip
 
 
-class ExplicitKeywordArgsCheckerTests(unittest.TestCase):
-
-    def setUp(self) -> None:
-        super().setUp()
-        self.checker_test_object = testutils.CheckerTestCase()
-        self.checker_test_object.CHECKER_CLASS = (
-            pylint_extensions.ExplicitKeywordArgsChecker)
-        self.checker_test_object.setup_method()
-
-    def test_finds_non_explicit_keyword_args(self) -> None:
-        (
-            func_call_node_one, func_call_node_two, func_call_node_three,
-            func_call_node_four, func_call_node_five, class_call_node
-        ) = astroid.extract_node(
-                """
-        class TestClass():
-            pass
-
-        def test(test_var_one, test_var_two=4, test_var_three=5,
-                test_var_four="test_checker"):
-            test_var_five = test_var_two + test_var_three
-            return test_var_five
-
-        def test_1(test_var_one, test_var_one):
-            pass
-
-        test(2, 5, test_var_three=6) #@
-        test(2) #@
-        test(2, 6, test_var_two=5, test_var_four="test_checker") #@
-        max(5, 1) #@
-        test_1(1, 2) #@
-
-        TestClass() #@
-        """)
-        with self.checker_test_object.assertAddsMessages(
-            testutils.Message(
-                msg_id='non-explicit-keyword-args',
-                node=func_call_node_one,
-                args=(
-                    '\'test_var_two\'',
-                    'function',
-                    'test'
-                )
-            ),
-        ):
-            self.checker_test_object.checker.visit_call(
-                func_call_node_one)
-
-        with self.checker_test_object.assertNoMessages():
-            self.checker_test_object.checker.visit_call(
-                func_call_node_two)
-
-        with self.checker_test_object.assertAddsMessages(
-            testutils.Message(
-                msg_id='non-explicit-keyword-args',
-                node=func_call_node_three,
-                args=(
-                    '\'test_var_three\'',
-                    'function',
-                    'test'
-                )
-            )
-        ):
-            self.checker_test_object.checker.visit_call(
-                func_call_node_three)
-
-        with self.checker_test_object.assertNoMessages():
-            self.checker_test_object.checker.visit_call(class_call_node)
-
-        with self.checker_test_object.assertNoMessages():
-            self.checker_test_object.checker.visit_call(func_call_node_four)
-
-        with self.checker_test_object.assertNoMessages():
-            self.checker_test_object.checker.visit_call(func_call_node_five)
-
-    def test_finds_arg_name_for_non_keyword_arg(self) -> None:
-        node_arg_name_for_non_keyword_arg = astroid.extract_node(
-            """
-            def test(test_var_one, test_var_two=4, test_var_three=5):
-                test_var_five = test_var_two + test_var_three
-                return test_var_five
-
-            test(test_var_one=2, test_var_two=5) #@
-            """)
-        message = testutils.Message(
-            msg_id='arg-name-for-non-keyword-arg',
-            node=node_arg_name_for_non_keyword_arg,
-            args=('\'test_var_one\'', 'function', 'test'))
-        with self.checker_test_object.assertAddsMessages(message):
-            self.checker_test_object.checker.visit_call(
-                node_arg_name_for_non_keyword_arg)
-
-    def test_correct_use_of_keyword_args(self) -> None:
-        node_with_no_error_message = astroid.extract_node(
-            """
-            def test(test_var_one, test_var_two=4, test_var_three=5):
-                test_var_five = test_var_two + test_var_three
-                return test_var_five
-
-            test(2, test_var_two=2) #@
-            """)
-
-        with self.checker_test_object.assertNoMessages():
-            self.checker_test_object.checker.visit_call(
-                node_with_no_error_message)
-
-    def test_function_with_args_and_kwargs(self) -> None:
-        node_with_args_and_kwargs = astroid.extract_node(
-            """
-            def test_1(*args, **kwargs):
-                pass
-
-            test_1(first=1, second=2) #@
-            """)
-
-        with self.checker_test_object.assertNoMessages():
-            self.checker_test_object.checker.visit_call(
-                node_with_args_and_kwargs)
-
-    def test_constructor_call_with_keyword_arguments(self) -> None:
-        node_with_no_error_message = astroid.extract_node(
-            """
-            class TestClass():
-                def __init__(self, first, second):
-                    pass
-
-            TestClass(first=1, second=2) #@
-            """)
-
-        with self.checker_test_object.assertNoMessages():
-            self.checker_test_object.checker.visit_call(
-                node_with_no_error_message)
-
-    def test_checker_skips_object_call_when_noncallable_object_is_called(
-        self
-    ) -> None:
-        node_with_no_error_message = astroid.extract_node(
-            """
-            1() #@
-            """)
-
-        with self.checker_test_object.assertNoMessages():
-            self.checker_test_object.checker.visit_call(
-                node_with_no_error_message)
-
-    def test_register(self) -> None:
-        pylinter_instance = lint.PyLinter()
-        pylint_extensions.register(pylinter_instance)
-
-
 class HangingIndentCheckerTests(unittest.TestCase):
 
     def setUp(self) -> None: