Code Style Checker: reworked the type annotations checker.

Fri, 16 Apr 2021 18:03:43 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Fri, 16 Apr 2021 18:03:43 +0200
changeset 8244
ed8cb108b27b
parent 8243
cc717c2ae956
child 8245
dce55f623991

Code Style Checker: reworked the type annotations checker.

eric6.epj file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsChecker.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsEnums.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsFunctionVisitor.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/translations.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleChecker.py file | annotate | diff | comparison | revisions
eric6/Preferences/ShortcutsFile.py file | annotate | diff | comparison | revisions
--- a/eric6.epj	Thu Apr 15 18:11:24 2021 +0200
+++ b/eric6.epj	Fri Apr 16 18:03:43 2021 +0200
@@ -2578,7 +2578,9 @@
       "eric6/Plugins/CheckerPlugins/CodeStyleChecker/Simplify/ast_unparse.py",
       "eric6/Plugins/CheckerPlugins/CodeStyleChecker/Simplify/SimplifyChecker.py",
       "eric6/Plugins/CheckerPlugins/CodeStyleChecker/Simplify/translations.py",
-      "eric6/Plugins/CheckerPlugins/CodeStyleChecker/Simplify/SimplifyNodeVisitor.py"
+      "eric6/Plugins/CheckerPlugins/CodeStyleChecker/Simplify/SimplifyNodeVisitor.py",
+      "eric6/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsFunctionVisitor.py",
+      "eric6/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsEnums.py"
     ],
     "SPELLEXCLUDES": "Dictionaries/excludes.dic",
     "SPELLLANGUAGE": "en_US",
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsChecker.py	Thu Apr 15 18:11:24 2021 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsChecker.py	Fri Apr 16 18:03:43 2021 +0200
@@ -9,9 +9,13 @@
 
 import copy
 import ast
+import sys
+from functools import lru_cache
 
 import AstUtilities
 
+from .AnnotationsEnums import AnnotationType, ClassDecoratorType, FunctionType
+
 
 class AnnotationsChecker:
     """
@@ -27,11 +31,14 @@
         ## Return Annotations
         "A201", "A202", "A203", "A204", "A205", "A206",
         
+        ## Mixed kind of annotations
+        "A301",
+        
         ## Annotation Coverage
         "A881",
         
         ## Annotation Complexity
-        "A891",
+        "A891", "A892",
     ]
 
     def __init__(self, source, filename, tree, select, ignore, expected,
@@ -75,15 +82,33 @@
             (
                 self.__checkFunctionAnnotations,
                 ("A001", "A002", "A003", "A101", "A102",
-                 "A201", "A202", "A203", "A204", "A205", "A206",)
+                 "A201", "A202", "A203", "A204", "A205", "A206",
+                 "A301", )
             ),
             (self.__checkAnnotationsCoverage, ("A881",)),
-            (self.__checkAnnotationComplexity, ("A891",)),
+            (self.__checkAnnotationComplexity, ("A891", "A892")),
         ]
         
+        # TODO: the parameters to CodeStyleCheckerDialog
         self.__defaultArgs = {
+            # Annotations
+            "SuppressNoneReturning": False,
+            "SuppressDummyArgs": False,
+            "AllowUntypedDefs": False,
+            "AllowUntypedNested": False,
+            "MypyInitReturn": False,
+            "DispatchDecorators": [
+                "singledispatch",
+                "singledispatchmethod",
+            ],
+            "OverloadDecorators": ["overload"],
+            
+            # Annotation Coverage
             "MinimumCoverage": 75,      # % of type annotation coverage
+            
+            # Annotation Complexity
             "MaximumComplexity": 3,
+            "MaximumLength": 7,
         }
         
         self.__checkers = []
@@ -156,17 +181,243 @@
         for check in self.__checkers:
             check()
     
+    #######################################################################
+    ## Annotations
+    ##
+    ## adapted from: flake8-annotations v2.6.2
+    #######################################################################
+    
     def __checkFunctionAnnotations(self):
         """
         Private method to check for function annotation issues.
         """
+        suppressNoneReturning = self.__args.get(
+            "SuppressNoneReturning",
+            self.__defaultArgs["SuppressNoneReturning"])
+        suppressDummyArgs = self.__args.get(
+            "SuppressDummyArgs",
+            self.__defaultArgs["SuppressDummyArgs"])
+        allowUntypedDefs = self.__args.get(
+            "AllowUntypedDefs",
+            self.__defaultArgs["AllowUntypedDefs"])
+        allowUntypedNested = self.__args.get(
+            "AllowUntypedNested",
+            self.__defaultArgs["AllowUntypedNested"])
+        mypyInitReturn = self.__args.get(
+            "MypyInitReturn",
+            self.__defaultArgs["MypyInitReturn"])
+        
+        # Store decorator lists as sets for easier lookup
+        dispatchDecorators = set(self.__args.get(
+            "DispatchDecorators",
+            self.__defaultArgs["DispatchDecorators"]))
+        overloadDecorators = set(self.__args.get(
+            "OverloadDecorators",
+            self.__defaultArgs["OverloadDecorators"]))
+        
+        from .AnnotationsFunctionVisitor import FunctionVisitor
         visitor = FunctionVisitor(self.__source)
         visitor.visit(self.__tree)
-        for issue in visitor.issues:
-            node = issue[0]
-            reason = issue[1]
-            params = issue[2:]
-            self.__error(node.lineno - 1, node.col_offset, reason, *params)
+        
+        # Keep track of the last encountered function decorated by
+        # `typing.overload`, if any. Per the `typing` module documentation,
+        # a series of overload-decorated definitions must be followed by
+        # exactly one non-overload-decorated definition of the same function.
+        lastOverloadDecoratedFunctionName = None
+        
+        # Iterate over the arguments with missing type hints, by function.
+        for function in visitor.functionDefinitions:
+            if (
+                function.isDynamicallyTyped() and
+                (allowUntypedDefs or
+                 (function.isNested and allowUntypedNested))
+            ):
+                # Skip yielding errors from dynamically typed functions
+                # or nested functions
+                continue
+            
+            # Skip yielding errors for configured dispatch functions, such as
+            # (by default) `functools.singledispatch` and
+            # `functools.singledispatchmethod`
+            if function.hasDecorator(dispatchDecorators):
+                continue
+            
+            # Create sentinels to check for mixed hint styles
+            hasTypeComment = function.hasTypeComment
+            
+            has3107Annotation = False
+            # PEP 3107 annotations are captured by the return arg
+            
+            # Iterate over annotated args to detect mixing of type annotations
+            # and type comments. Emit this only once per function definition
+            for arg in function.getAnnotatedArguments():
+                if arg.hasTypeComment:
+                    hasTypeComment = True
+                
+                if arg.has3107Annotation:
+                    has3107Annotation = True
+                
+                if hasTypeComment and has3107Annotation:
+                    # Short-circuit check for mixing of type comments &
+                    # 3107-style annotations
+                    self.__error(function.lineno - 1, function.col_offset,
+                                 "A301")
+                    break
+            
+            # Before we iterate over the function's missing annotations, check
+            # to see if it's the closing function def in a series of
+            # `typing.overload` decorated functions.
+            if lastOverloadDecoratedFunctionName == function.name:
+                continue
+
+            # If it's not, and it is overload decorated, store it for the next
+            # iteration
+            if function.hasDecorator(overloadDecorators):
+                lastOverloadDecoratedFunctionName = function.name
+            
+            # Record explicit errors for arguments that are missing annotations
+            for arg in function.getMissedAnnotations():
+                if arg.argname == "return":
+                    # return annotations have multiple possible short-circuit
+                    # paths
+                    if (
+                        suppressNoneReturning and
+                        not arg.hasTypeAnnotation and
+                        function.hasOnlyNoneReturns
+                    ):
+                        # Skip recording return errors if the function has only
+                        # `None` returns. This includes the case of no returns.
+                        continue
+                    
+                    if (
+                        mypyInitReturn and
+                        function.isClassMethod and
+                        function.name == "__init__" and
+                        function.getAnnotatedArguments()
+                    ):
+                        # Skip recording return errors for `__init__` if at
+                        # least one argument is annotated
+                        continue
+                
+                # If the `suppressDummyArgs` flag is `True`, skip recording
+                # errors for any arguments named `_`
+                if arg.argname == "_" and suppressDummyArgs:
+                    continue
+
+                self.__classifyError(function, arg)
+    
+    def __classifyError(self, function, arg):
+        """
+        Private method to classify the missing type annotation based on the
+        Function & Argument metadata.
+        
+        For the currently defined rules & program flow, the assumption can be
+        made that an argument passed to this method will match a linting error,
+        and will only match a single linting error
+        
+        This function provides an initial classificaton, then passes relevant
+        attributes to cached helper function(s).
+        
+        @param function reference to the Function object
+        @type Function
+        @param arg reference to the Argument object
+        @type Argument
+        """
+        # Check for return type
+        # All return "arguments" have an explicitly defined name "return"
+        if arg.argname == "return":
+            errorCode = self.__returnErrorClassifier(
+                function.isClassMethod, function.classDecoratorType,
+                function.functionType
+            )
+        else:
+            # Otherwise, classify function argument error
+            isFirstArg = arg == function.args[0]
+            errorCode = self.__argumentErrorClassifier(
+                function.isClassMethod, isFirstArg,
+                function.classDecoratorType, arg.annotationType,
+            )
+        
+        if errorCode in ("A001", "A002", "A003"):
+            self.__error(arg.lineno - 1, arg.col_offset, errorCode,
+                         arg.argname)
+        else:
+            self.__error(arg.lineno - 1, arg.col_offset, errorCode)
+    
+    @lru_cache()
+    def __returnErrorClassifier(self, isClassMethod, classDecoratorType,
+                                functionType):
+        """
+        Private method to classify a return type annotation issue.
+        
+        @param isClassMethod flag indicating a classmethod type function
+        @type bool
+        @param classDecoratorType type of class decorator
+        @type ClassDecoratorType
+        @param functionType type of function
+        @type FunctionType
+        @return error code
+        @rtype str
+        """
+        # Decorated class methods (@classmethod, @staticmethod) have a higher
+        # priority than the rest
+        if isClassMethod:
+            if classDecoratorType == ClassDecoratorType.CLASSMETHOD:
+                return "A206"
+            elif classDecoratorType == ClassDecoratorType.STATICMETHOD:
+                return "A205"
+
+        if functionType == FunctionType.SPECIAL:
+            return "A204"
+        elif functionType == FunctionType.PRIVATE:
+            return "A203"
+        elif functionType == FunctionType.PROTECTED:
+            return "A202"
+        else:
+            return "A201"
+    
+    @lru_cache()
+    def __argumentErrorClassifier(self, isClassMethod, isFirstArg,
+                                  classDecoratorType, annotationType):
+        """
+        Private method to classify an argument type annotation issue.
+        
+        @param isClassMethod flag indicating a classmethod type function
+        @type bool
+        @param isFirstArg flag indicating the first argument
+        @type bool
+        @param classDecoratorType type of class decorator
+        @type enums.ClassDecoratorType
+        @param annotationType type of annotation
+        @type AnnotationType
+        @return error code
+        @rtype str
+        """
+        # Check for regular class methods and @classmethod, @staticmethod is
+        # deferred to final check
+        if isClassMethod and isFirstArg:
+            # The first function argument here would be an instance of self or
+            # class
+            if classDecoratorType == ClassDecoratorType.CLASSMETHOD:
+                return "A102"
+            elif classDecoratorType != ClassDecoratorType.STATICMETHOD:
+                # Regular class method
+                return "A101"
+
+        # Check for remaining codes
+        if annotationType == AnnotationType.KWARG:
+            return "A003"
+        elif annotationType == AnnotationType.VARARG:
+            return "A002"
+        else:
+            # Combine PosOnlyArgs, Args, and KwOnlyArgs
+            return "A001"
+    
+    #######################################################################
+    ## Annotations Coverage
+    ##
+    ## adapted from: flake8-annotations-coverage v0.0.5
+    #######################################################################
     
     def __checkAnnotationsCoverage(self):
         """
@@ -187,7 +438,7 @@
             return
         
         functionDefAnnotationsInfo = [
-            hasTypeAnnotations(f) for f in functionDefs
+            self.__hasTypeAnnotations(f) for f in functionDefs
         ]
         annotationsCoverage = int(
             len(list(filter(None, functionDefAnnotationsInfo))) /
@@ -196,12 +447,42 @@
         if annotationsCoverage < minAnnotationsCoverage:
             self.__error(0, 0, "A881", annotationsCoverage)
     
+    def __hasTypeAnnotations(self, funcNode):
+        """
+        Private method to check for type annotations.
+        
+        @param funcNode reference to the function definition node to be checked
+        @type ast.AsyncFunctionDef or ast.FunctionDef
+        @return flag indicating the presence of type annotations
+        @rtype bool
+        """
+        hasReturnAnnotation = funcNode.returns is not None
+        hasArgsAnnotations = any(a for a in funcNode.args.args
+                                 if a.annotation is not None)
+        hasKwargsAnnotations = (funcNode.args and
+                                funcNode.args.kwarg and
+                                funcNode.args.kwarg.annotation is not None)
+        hasKwonlyargsAnnotations = any(a for a in funcNode.args.kwonlyargs
+                                       if a.annotation is not None)
+        
+        return any((hasReturnAnnotation, hasArgsAnnotations,
+                    hasKwargsAnnotations, hasKwonlyargsAnnotations))
+    
+    #######################################################################
+    ## Annotations Complexity
+    ##
+    ## adapted from: flake8-annotations-complexity v0.0.6
+    #######################################################################
+    
     def __checkAnnotationComplexity(self):
         """
         Private method to check the type annotation complexity.
         """
         maxAnnotationComplexity = self.__args.get(
             "MaximumComplexity", self.__defaultArgs["MaximumComplexity"])
+        # TODO: include 'MaximumLength' in CodeStyleCheckerDialog
+        maxAnnotationLength = self.__args.get(
+            "MaximumLength", self.__defaultArgs["MaximumLength"])
         typeAnnotations = []
         
         functionDefs = [
@@ -216,217 +497,71 @@
         typeAnnotations += [a.annotation for a in ast.walk(self.__tree)
                             if isinstance(a, ast.AnnAssign) and a.annotation]
         for annotation in typeAnnotations:
-            complexity = getAnnotationComplexity(annotation)
+            complexity = self.__getAnnotationComplexity(annotation)
             if complexity > maxAnnotationComplexity:
                 self.__error(annotation.lineno - 1, annotation.col_offset,
                              "A891", complexity, maxAnnotationComplexity)
-
-
-class FunctionVisitor(ast.NodeVisitor):
-    """
-    Class implementing a node visitor to check function annotations.
-    
-    Note: this class is modeled after flake8-annotations checker.
-    """
-    def __init__(self, sourceLines):
-        """
-        Constructor
-        
-        @param sourceLines lines of source code
-        @type list of str
-        """
-        super().__init__()
-        
-        self.__sourceLines = sourceLines
-        
-        self.issues = []
+            
+            annotationLength = self.__getAnnotationLength(annotation)
+            if annotationLength > maxAnnotationLength:
+                self.__error(annotation.lineno - 1, annotation.col_offset,
+                             "A892", annotationLength, maxAnnotationLength)
     
-    def visit_FunctionDef(self, node):
-        """
-        Public method to handle a function or method definition.
-        
-        @param node reference to the node to be processed
-        @type ast.FunctionDef
-        """
-        self.__checkFunctionNode(node)
-        self.generic_visit(node)
-    
-    def visit_AsyncFunctionDef(self, node):
-        """
-        Public method to handle an async function or method definition.
-        
-        @param node reference to the node to be processed
-        @type ast.AsyncFunctionDef
-        """
-        self.__checkFunctionNode(node)
-        self.generic_visit(node)
-    
-    def visit_ClassDef(self, node):
-        """
-        Public method to handle class definitions.
-        
-        @param node reference to the node to be processed
-        @type ast.ClassDef
+    def __getAnnotationComplexity(self, annotationNode, defaultComplexity=1):
         """
-        methodNodes = [
-            childNode for childNode in node.body
-            if isinstance(childNode, (ast.FunctionDef, ast.AsyncFunctionDef))
-        ]
-        for methodNode in methodNodes:
-            self.__checkFunctionNode(methodNode, classMethod=True)
-    
-    def __checkFunctionNode(self, node, classMethod=False):
-        """
-        Private method to check an individual function definition node.
-        
-        @param node reference to the node to be processed
-        @type ast.FunctionDef or ast.AsyncFunctionDef
-        @param classMethod flag indicating a class method
-        @type bool
-        """
-        if node.name.startswith("__") and node.name.endswith("__"):
-            visibilityType = "special"
-        elif node.name.startswith("__"):
-            visibilityType = "private"
-        elif node.name.startswith("_"):
-            visibilityType = "protected"
-        else:
-            visibilityType = "public"
-        
-        if classMethod:
-            decorators = [
-                decorator.id for decorator in node.decorator_list
-                if isinstance(decorator, ast.Name)
-            ]
-            if "classmethod" in decorators:
-                classMethodType = "decorator"
-            elif "staticmethod" in decorators:
-                classMethodType = "staticmethod"
-            else:
-                classMethodType = ""
-        else:
-            classMethodType = "function"
-        
-        # check argument annotations
-        for argType in ("args", "vararg", "kwonlyargs", "kwarg"):
-            args = node.args.__getattribute__(argType)
-            if args:
-                if not isinstance(args, list):
-                    args = [args]
-                
-                for arg in args:
-                    if not arg.annotation:
-                        self.__classifyArgumentError(
-                            arg, argType, classMethodType)
+        Private method to determine the annotation complexity.
         
-        # check function return annotation
-        if not node.returns:
-            lineno = node.lineno
-            colOffset = self.__sourceLines[lineno - 1].rfind(":") + 1
-            self.__classifyReturnError(classMethodType, visibilityType,
-                                       lineno, colOffset)
-    
-    def __classifyReturnError(self, methodType, visibilityType, lineno,
-                              colOffset):
-        """
-        Private method to classify and record a return annotation issue.
-        
-        @param methodType type of method/function the argument belongs to
-        @type str
-        @param visibilityType visibility of the function
-        @type str
-        @param lineno line number
-        @type int
-        @param colOffset column number
+        @param annotationNode reference to the node to determine the annotation
+            complexity for
+        @type ast.AST
+        @param defaultComplexity default complexity value
         @type int
-        """
-        # create a dummy AST node to report line and column
-        node = ast.AST()
-        node.lineno = lineno
-        node.col_offset = colOffset
-        
-        # now classify the issue
-        if methodType == "classmethod":
-            self.issues.append((node, "A206"))
-        elif methodType == "staticmethod":
-            self.issues.append((node, "A205"))
-        elif visibilityType == "special":
-            self.issues.append((node, "A204"))
-        elif visibilityType == "private":
-            self.issues.append((node, "A203"))
-        elif visibilityType == "protected":
-            self.issues.append((node, "A202"))
-        else:
-            self.issues.append((node, "A201"))
-    
-    def __classifyArgumentError(self, argNode, argType, methodType):
-        """
-        Private method to classify and record an argument annotation issue.
-        
-        @param argNode reference to the argument node
-        @type ast.arguments
-        @param argType type of the argument node
-        @type str
-        @param methodType type of method/function the argument belongs to
-        @type str
+        @return annotation complexity
+        @rtype = int
         """
-        # check class method issues
-        if methodType != "function" and argNode.arg in ("cls", "self"):
-            if methodType == "classmethod":
-                self.issues.append((argNode, "A102"))
-                return
-            elif methodType != "staticmethod":
-                self.issues.append((argNode, "A101"))
-                return
-        
-        # check all other arguments
-        if argType == "kwarg":
-            self.issues.append((argNode, "A003", argNode.arg))
-        elif argType == "vararg":
-            self.issues.append((argNode, "A002", argNode.arg))
-        else:
-            # args and kwonlyargs
-            self.issues.append((argNode, "A001", argNode.arg))
-
-######################################################################
-## some utility functions below
-######################################################################
-
-
-def hasTypeAnnotations(funcNode):
-    """
-    Function to check for type annotations.
+        if AstUtilities.isString(annotationNode):
+            try:
+                annotationNode = ast.parse(annotationNode.s).body[0].value
+            except (SyntaxError, IndexError):
+                return defaultComplexity
+        if isinstance(annotationNode, ast.Subscript):
+            if sys.version_info >= (3, 9):
+                return (defaultComplexity +
+                        self.__getAnnotationComplexity(annotationNode.slice))
+            else:
+                return (
+                    defaultComplexity +
+                    self.__getAnnotationComplexity(annotationNode.slice.value)
+                )
+        if isinstance(annotationNode, ast.Tuple):
+            return max(
+                (self.__getAnnotationComplexity(n)
+                 for n in annotationNode.elts),
+                default=defaultComplexity
+            )
+        return defaultComplexity
     
-    @param funcNode reference to the function definition node to be checked
-    @type ast.AsyncFunctionDef or ast.FunctionDef
-    @return flag indicating the presence of type annotations
-    @rtype bool
-    """
-    hasReturnAnnotation = funcNode.returns is not None
-    hasArgsAnnotations = any(a for a in funcNode.args.args
-                             if a.annotation is not None)
-    hasKwargsAnnotations = (funcNode.args and
-                            funcNode.args.kwarg and
-                            funcNode.args.kwarg.annotation is not None)
-    hasKwonlyargsAnnotations = any(a for a in funcNode.args.kwonlyargs
-                                   if a.annotation is not None)
-    
-    return any((hasReturnAnnotation, hasArgsAnnotations, hasKwargsAnnotations,
-               hasKwonlyargsAnnotations))
-
-
-def getAnnotationComplexity(annotationNode):
-    """
-    Function to determine the annotation complexity.
-    
-    @param annotationNode reference to the node to determine the annotation
-        complexity for
-    @type ast.AST
-    @return annotation complexity
-    @rtype = int
-    """
-    if AstUtilities.isString(annotationNode):
-        annotationNode = ast.parse(annotationNode.s).body[0].value
-    if isinstance(annotationNode, ast.Tuple):
-        return max(getAnnotationComplexity(n) for n in annotationNode.elts)
-    return 1
+    def __getAnnotationLength(self, annotationNode):
+        """
+        Private method to determine the annotation length.
+        
+        @param annotationNode reference to the node to determine the annotation
+            length for
+        @type ast.AST
+        @return annotation length
+        @rtype = int
+        """
+        if AstUtilities.isString(annotationNode):
+            try:
+                annotationNode = ast.parse(annotationNode.s).body[0].value
+            except (SyntaxError, IndexError):
+                return 0
+        if isinstance(annotationNode, ast.Subscript):
+            try:
+                if sys.version_info >= (3, 9):
+                    return len(annotationNode.slice.elts)
+                else:
+                    return len(annotationNode.slice.value.elts)
+            except AttributeError:
+                return 0
+        return 0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsEnums.py	Fri Apr 16 18:03:43 2021 +0200
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing some enums for function type annotations.
+"""
+
+#
+# adapted from flake8-annotations v2.6.2
+#
+
+from enum import Enum, auto
+
+
+class FunctionType(Enum):
+    """
+    Class representing the various function types.
+    """
+    PUBLIC = auto()
+    PROTECTED = auto()      # Leading single underscore
+    PRIVATE = auto()        # Leading double underscore
+    SPECIAL = auto()        # Leading & trailing double underscore
+
+
+class ClassDecoratorType(Enum):
+    """
+    Class representing the various class method decorators.
+    """
+    CLASSMETHOD = auto()
+    STATICMETHOD = auto()
+
+
+class AnnotationType(Enum):
+    """
+    Class representing the kind of missing type annotation.
+    """
+    POSONLYARGS = auto()
+    ARGS = auto()
+    VARARG = auto()
+    KWONLYARGS = auto()
+    KWARG = auto()
+    RETURN = auto()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsFunctionVisitor.py	Fri Apr 16 18:03:43 2021 +0200
@@ -0,0 +1,650 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a node visitor for function type annotations.
+"""
+
+#
+# The visitor and associated classes are adapted from flake8-annotations v2.6.2
+#
+
+import ast
+import itertools
+
+from .AnnotationsEnums import AnnotationType, ClassDecoratorType, FunctionType
+
+# The order of AST_ARG_TYPES must match Python's grammar
+AST_ARG_TYPES = ("posonlyargs", "args", "vararg", "kwonlyargs", "kwarg")
+
+
+class Argument:
+    """
+    Class representing a function argument.
+    """
+    def __init__(self, argname, lineno, col_offset, annotationType,
+                 hasTypeAnnotation=False, has3107Annotation=False,
+                 hasTypeComment=False):
+        """
+        Constructor
+        
+        @param argname name of the argument
+        @type str
+        @param lineno line number
+        @type int
+        @param col_offset column number
+        @type int
+        @param annotationType type of annotation
+        @type AnnotationType
+        @param hasTypeAnnotation flag indicating the presence of a type
+            annotation (defaults to False)
+        @type bool (optional)
+        @param has3107Annotation flag indicating the presence of a PEP 3107
+            annotation (defaults to False)
+        @type bool (optional)
+        @param hasTypeComment flag indicating the presence of a type comment
+            (defaults to False)
+        @type bool (optional)
+        """
+        self.argname = argname
+        self.lineno = lineno
+        self.col_offset = col_offset
+        self.annotationType = annotationType
+        self.hasTypeAnnotation = hasTypeAnnotation
+        self.has3107Annotation = has3107Annotation
+        self.hasTypeComment = hasTypeComment
+    
+    @classmethod
+    def fromNode(cls, node, annotationTypeName):
+        """
+        Class method to create an Argument object based on the given node.
+        
+        @param node reference to the node to be converted
+        @type ast.arguments
+        @param annotationTypeName name of the annotation type
+        @type str
+        @return Argument object
+        @rtype Argument
+        """
+        annotationType = AnnotationType[annotationTypeName]
+        newArg = cls(node.arg, node.lineno, node.col_offset, annotationType)
+
+        newArg.hasTypeAnnotation = False
+        if node.annotation:
+            newArg.hasTypeAnnotation = True
+            newArg.has3107Annotation = True
+
+        if node.type_comment:
+            newArg.hasTypeAnnotation = True
+            newArg.hasTypeComment = True
+
+        return newArg
+
+
+class Function:
+    """
+    Class representing a function.
+    """
+    def __init__(self, name, lineno, col_offset,
+                 functionType=FunctionType.PUBLIC, isClassMethod=False,
+                 classDecoratorType=None, isReturnAnnotated=False,
+                 hasTypeComment=False, hasOnlyNoneReturns=True,
+                 isNested=False, decoratorList=None, args=None):
+        """
+        Constructor
+        
+        @param name name of the function
+        @type str
+        @param lineno line number
+        @type int
+        @param col_offset column number
+        @type int
+        @param functionType type of the function (defaults to
+            FunctionType.PUBLIC)
+        @type FunctionType (optional)
+        @param isClassMethod flag indicating a class method (defaults to False)
+        @type bool (optional)
+        @param classDecoratorType type of a function decorator
+            (defaults to None)
+        @type ClassDecoratorType or None (optional)
+        @param isReturnAnnotated flag indicating the presence of a return
+            type annotation (defaults to False)
+        @type bool (optional)
+        @param hasTypeComment flag indicating the presence of a type comment
+            (defaults to False)
+        @type bool (optional)
+        @param hasOnlyNoneReturns flag indicating only None return values
+            (defaults to True)
+        @type bool (optional)
+        @param isNested flag indicating a nested function (defaults to False)
+        @type bool (optional)
+        @param decoratorList list of decorator nodes (defaults to None)
+        @type list of ast.Attribute, ast.Call or ast.Name (optional)
+        @param args list of arguments (defaults to None)
+        @type list of Argument (optional)
+        """
+        self.name = name
+        self.lineno = lineno
+        self.col_offset = col_offset
+        self.functionType = functionType
+        self.isClassMethod = isClassMethod
+        self.classDecoratorType = classDecoratorType
+        self.isReturnAnnotated = isReturnAnnotated
+        self.hasTypeComment = hasTypeComment
+        self.hasOnlyNoneReturns = hasOnlyNoneReturns
+        self.isNested = isNested
+        self.decoratorList = decoratorList
+        self.args = args
+    
+    def isFullyAnnotated(self):
+        """
+        Public method to check, if the function definition is fully type
+        annotated.
+
+        Note: self.args will always include an Argument object for return.
+        
+        @return flag indicating a fully annotated function definition
+        @rtype bool
+        """
+        return all(arg.hasTypeAnnotation for arg in self.args)
+    
+    def isDynamicallyTyped(self):
+        """
+        Public method to check, if a function definition is dynamically typed
+        (i.e. completely lacking hints).
+        
+        @return flag indicating a dynamically typed function definition
+        @rtype bool
+        """
+        return not any(arg.hasTypeAnnotation for arg in self.args)
+
+    def getMissedAnnotations(self):
+        """
+        Public method to provide a list of arguments with missing type
+        annotations.
+        
+        @return list of arguments with missing type annotations
+        @rtype list of Argument
+        """
+        return [arg for arg in self.args if not arg.hasTypeAnnotation]
+
+    def getAnnotatedArguments(self):
+        """
+        Public method to get list of arguments with type annotations.
+        
+        @return list of arguments with type annotations.
+        @rtype list of Argument
+        """
+        return [arg for arg in self.args if arg.hasTypeAnnotation]
+    
+    def hasDecorator(self, checkDecorators):
+        """
+        Public method to check whether the function node is decorated by any of
+        the provided decorators.
+        
+        Decorator matching is done against the provided `checkDecorators` set.
+        Decorators are assumed to be either a module attribute (e.g.
+        `@typing.overload`) or name (e.g. `@overload`). For the case of a
+        module attribute, only the attribute is checked against
+        `overload_decorators`.
+        
+        Note: Deeper decorator imports (e.g. `a.b.overload`) are not explicitly
+        supported.
+        
+        @param checkDecorators set of decorators to check against
+        @type set of str
+        @return flag indicating the presence of any decorators
+        @rtype bool
+        """
+        for decorator in self.decoratorList:
+            # Drop to a helper to allow for simpler handling of callable
+            # decorators
+            return self.__decoratorChecker(decorator, checkDecorators)
+        else:
+            return False
+    
+    def __decoratorChecker(self, decorator, checkDecorators):
+        """
+        Private method to check the provided decorator for a match against the
+        provided set of check names.
+        
+        Decorators are assumed to be of the following form:
+            * `a.name` or `a.name()`
+            * `name` or `name()`
+        
+        Note: Deeper imports (e.g. `a.b.name`) are not explicitly supported.
+        
+        @param decorator decorator node to check
+        @type ast.Attribute, ast.Call or ast.Name
+        @param checkDecorators set of decorators to check against
+        @type set of str
+        @return flag indicating the presence of any decorators
+        @rtype bool
+        """
+        if isinstance(decorator, ast.Name):
+            # e.g. `@overload`, where `decorator.id` will be the name
+            if decorator.id in checkDecorators:
+                return True
+        elif isinstance(decorator, ast.Attribute):
+            # e.g. `@typing.overload`, where `decorator.attr` will be the name
+            if decorator.attr in checkDecorators:
+                return True
+        elif isinstance(decorator, ast.Call):
+            # e.g. `@overload()` or `@typing.overload()`, where
+            # `decorator.func` will be `ast.Name` or `ast.Attribute`,
+            # which we can check recursively
+            return self.__decoratorChecker(decorator.func, checkDecorators)
+        
+        return None
+    
+    @classmethod
+    def fromNode(cls, node, lines, **kwargs):
+        """
+        Class method to create a Function object from ast.FunctionDef or
+        ast.AsyncFunctionDef nodes.
+        
+        Accept the source code, as a list of strings, in order to get the
+        column where the function definition ends.
+        
+        With exceptions, input kwargs are passed straight through to Function's
+        __init__. The following kwargs will be overridden:
+          * function_type
+          * class_decorator_type
+          * args
+        
+        @param node reference to the function definition node
+        @type ast.AsyncFunctionDef or ast.FunctionDef
+        @param lines list of source code lines
+        @type list of str
+        @keyparam **kwargs keyword arguments
+        @type dict
+        @return created Function object
+        @rtype Function
+        """
+        # Extract function types from function name
+        kwargs["functionType"] = cls.getFunctionType(node.name)
+        
+        # Identify type of class method, if applicable
+        if kwargs.get("isClassMethod", False):
+            kwargs["classDecoratorType"] = cls.getClassDecoratorType(node)
+        
+        # Store raw decorator list for use by property methods
+        kwargs["decoratorList"] = node.decorator_list
+        
+        newFunction = cls(node.name, node.lineno, node.col_offset, **kwargs)
+        
+        # Iterate over arguments by type & add
+        newFunction.args = []
+        for argType in AST_ARG_TYPES:
+            args = node.args.__getattribute__(argType)
+            if args:
+                if not isinstance(args, list):
+                    args = [args]
+                
+                newFunction.args.extend(
+                    [Argument.fromNode(arg, argType.upper())
+                     for arg in args]
+                )
+        
+        # Create an Argument object for the return hint
+        defEndLineno, defEndColOffset = cls.colonSeeker(node, lines)
+        returnArg = Argument("return", defEndLineno, defEndColOffset,
+                             AnnotationType.RETURN)
+        if node.returns:
+            returnArg.hasTypeAnnotation = True
+            returnArg.has3107Annotation = True
+            newFunction.isReturnAnnotated = True
+        
+        newFunction.args.append(returnArg)
+        
+        # Type comments in-line with input arguments are handled by the
+        # Argument class. If a function-level type comment is present, attempt
+        # to parse for any missed type hints.
+        if node.type_comment:
+            newFunction.hasTypeComment = True
+            newFunction = cls.tryTypeComment(newFunction, node)
+        
+        # Check for the presence of non-`None` returns using the special-case
+        # return node visitor.
+        returnVisitor = ReturnVisitor(node)
+        returnVisitor.visit(node)
+        newFunction.hasOnlyNoneReturns = returnVisitor.hasOnlyNoneReturns
+        
+        return newFunction
+    
+    @staticmethod
+    def colonSeeker(node, lines):
+        """
+        Static method to find the line & column indices of the function
+        definition's closing colon.
+        
+        @param node reference to the function definition node
+        @type ast.AsyncFunctionDef or ast.FunctionDef
+        @param lines list of source code lines
+        @type list of str
+        @return line and column offset of the colon
+        @rtype tuple of (int, int)
+        """
+        # Special case single line function definitions
+        if node.lineno == node.body[0].lineno:
+            return Function._singleLineColonSeeker(
+                node, lines[node.lineno - 1])
+        
+        defEndLineno = node.body[0].lineno - 1
+        
+        # Use str.rfind() to account for annotations on the same line,
+        # definition closure should be the last : on the line
+        defEndColOffset = lines[defEndLineno - 1].rfind(":")
+        
+        return defEndLineno, defEndColOffset
+    
+    @staticmethod
+    def _singleLineColonSeeker(node, line):
+        """
+        Static method to find the line & column indices of a single line
+        function definition.
+        
+        @param node reference to the function definition node
+        @type ast.AsyncFunctionDef or ast.FunctionDef
+        @param line source code line
+        @type str
+        @return line and column offset of the colon
+        @rtype tuple of (int, int)
+        """
+        colStart = node.col_offset
+        colEnd = node.body[0].col_offset
+        defEndColOffset = line.rfind(":", colStart, colEnd)
+        
+        return node.lineno, defEndColOffset
+    
+    @staticmethod
+    def tryTypeComment(funcObj, node):
+        """
+        Static method to infer type hints from a function-level type comment.
+        
+        If a function is type commented it is assumed to have a return
+        annotation, otherwise Python will fail to parse the hint.
+        
+        @param funcObj reference to the Function object
+        @type Function
+        @param node reference to the function definition node
+        @type ast.AsyncFunctionDef or ast.FunctionDef
+        @return reference to the modified Function object
+        @rtype Function
+        """
+        hintTree = ast.parse(node.type_comment, "<func_type>", "func_type")
+        hintTree = Function._maybeInjectClassArgument(hintTree, funcObj)
+        
+        for arg, hintComment in itertools.zip_longest(
+            funcObj.args, hintTree.argtypes
+        ):
+            if isinstance(hintComment, ast.Ellipsis):
+                continue
+            
+            if arg and hintComment:
+                arg.hasTypeAnnotation = True
+                arg.hasTypeComment = True
+        
+        # Return arg is always last
+        funcObj.args[-1].hasTypeAnnotation = True
+        funcObj.args[-1].hasTypeComment = True
+        funcObj.isReturnAnnotated = True
+        
+        return funcObj
+    
+    @staticmethod
+    def _maybeInjectClassArgument(hintTree, funcObj):
+        """
+        Static method to inject `self` or `cls` args into a type comment to
+        align with PEP 3107-style annotations.
+        
+        Because PEP 484 does not describe a method to provide partial function-
+        level type comments, there is a potential for ambiguity in the context
+        of both class methods and classmethods when aligning type comments to
+        method arguments.
+        
+        These two class methods, for example, should lint equivalently:
+        
+            def bar(self, a):
+                # type: (int) -> int
+                ...
+        
+            def bar(self, a: int) -> int
+                ...
+        
+        When this example type comment is parsed by `ast` and then matched with
+        the method's arguments, it associates the `int` hint to `self` rather
+        than `a`, so a dummy hint needs to be provided in situations where
+        `self` or `class` are not hinted in the type comment in order to
+        achieve equivalent linting results to PEP-3107 style annotations.
+        
+        A dummy `ast.Ellipses` constant is injected if the following criteria
+        are met:
+            1. The function node is either a class method or classmethod
+            2. The number of hinted args is at least 1 less than the number
+               of function args
+        
+        @param hintTree parsed type hint node
+        @type ast.FunctionType
+        @param funcObj reference to the Function object
+        @type Function
+        @return reference to the hint node
+        @rtype ast.FunctionType
+        """
+        if not funcObj.isClassMethod:
+            # Short circuit
+            return hintTree
+
+        if (
+            funcObj.classDecoratorType != ClassDecoratorType.STATICMETHOD and
+            len(hintTree.argtypes) < (len(funcObj.args) - 1)
+        ):
+            # Subtract 1 to skip return arg
+            hintTree.argtypes = [ast.Ellipsis()] + hintTree.argtypes
+        
+        return hintTree
+    
+    @staticmethod
+    def getFunctionType(functionName):
+        """
+        Static method to determine the function's FunctionType from its name.
+
+        MethodType is determined by the following priority:
+          1. Special: function name prefixed & suffixed by "__"
+          2. Private: function name prefixed by "__"
+          3. Protected: function name prefixed by "_"
+          4. Public: everything else
+        
+        @param functionName function name to be checked
+        @type str
+        @return type of function
+        @rtype FunctionType
+        """
+        if functionName.startswith("__") and functionName.endswith("__"):
+            return FunctionType.SPECIAL
+        elif functionName.startswith("__"):
+            return FunctionType.PRIVATE
+        elif functionName.startswith("_"):
+            return FunctionType.PROTECTED
+        else:
+            return FunctionType.PUBLIC
+    
+    @staticmethod
+    def getClassDecoratorType(functionNode):
+        """
+        Static method to get the class method's decorator type from its
+        function node.
+        
+        Only @classmethod and @staticmethod decorators are identified; all
+        other decorators are ignored
+        
+        If @classmethod or @staticmethod decorators are not present, this
+        function will return None.
+        
+        @param functionNode reference to the function definition node
+        @type ast.AsyncFunctionDef or ast.FunctionDef
+        @return class decorator type
+        @rtype ClassDecoratorType or None
+        """
+        # @classmethod and @staticmethod will show up as ast.Name objects,
+        # where callable decorators will show up as ast.Call, which we can
+        # ignore
+        decorators = [
+            decorator.id
+            for decorator in functionNode.decorator_list
+            if isinstance(decorator, ast.Name)
+        ]
+
+        if "classmethod" in decorators:
+            return ClassDecoratorType.CLASSMETHOD
+        elif "staticmethod" in decorators:
+            return ClassDecoratorType.STATICMETHOD
+        else:
+            return None
+
+
+class FunctionVisitor(ast.NodeVisitor):
+    """
+    Class implementing a node visitor to check function annotations.
+    """
+    def __init__(self, lines):
+        """
+        Constructor
+        
+        @param lines source code lines of the function
+        @type list of str
+        """
+        self.lines = lines
+        self.functionDefinitions = []
+        self.__context = []
+    
+    def switchContext(self, node):
+        """
+        Public method implementing a context switcher as a generic function
+        visitor in order to track function context.
+        
+        Without keeping track of context, it's challenging to reliably
+        differentiate class methods from "regular" functions, especially in the
+        case of nested classes.
+        
+        @param node reference to the function definition node to be analyzed
+        @type ast.AsyncFunctionDef or ast.FunctionDef
+        """
+        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
+            # Check for non-empty context first to prevent IndexErrors for
+            # non-nested nodes
+            if self.__context:
+                if isinstance(self.__context[-1], ast.ClassDef):
+                    # Check if current context is a ClassDef node & pass the
+                    # appropriate flag
+                    self.functionDefinitions.append(
+                        Function.fromNode(node, self.lines, isClassMethod=True)
+                    )
+                elif isinstance(
+                    self.__context[-1],
+                    (ast.FunctionDef, ast.AsyncFunctionDef)
+                ):
+                    # Check for nested function & pass the appropriate flag
+                    self.functionDefinitions.append(
+                        Function.fromNode(node, self.lines, isNested=True)
+                    )
+            else:
+                self.functionDefinitions.append(
+                    Function.fromNode(node, self.lines))
+        
+        self.__context.append(node)
+        self.generic_visit(node)
+        self.__context.pop()
+    
+    visit_FunctionDef = switchContext
+    visit_AsyncFunctionDef = switchContext
+    visit_ClassDef = switchContext
+
+
+class ReturnVisitor(ast.NodeVisitor):
+    """
+    Class implementing a node visitor to check the return statements of a
+    function node.
+    
+    If the function node being visited has an explicit return statement of
+    anything other than `None`, the `instance.hasOnlyNoneReturns` flag will
+    be set to `False`.
+    
+    If the function node being visited has no return statement, or contains
+    only return statement(s) that explicitly return `None`, the
+    `instance.hasOnlyNoneReturns` flag will be set to `True`.
+    
+    Due to the generic visiting being done, we need to keep track of the
+    context in which a non-`None` return node is found. These functions are
+    added to a set that is checked to see whether nor not the parent node is
+    present.
+    """
+    def __init__(self, parentNode):
+        """
+        Constructor
+        
+        @param parentNode reference to the function definition node to be
+            analyzed
+        @type ast.AsyncFunctionDef or ast.FunctionDef
+        """
+        self.parentNode = parentNode
+        self.__context = []
+        self.__nonNoneReturnNodes = set()
+    
+    @property
+    def hasOnlyNoneReturns(self):
+        """
+        Public method indicating, that the parent node isn't in the visited
+        nodes that don't return `None`.
+        
+        @return flag indicating, that the parent node isn't in the visited
+            nodes that don't return `None`
+        @rtype bool
+        """
+        return self.parentNode not in self.__nonNoneReturnNodes
+    
+    def visit_Return(self, node):
+        """
+        Public method to check each Return node to see if it returns anything
+        other than `None`.
+        
+        If the node being visited returns anything other than `None`, its
+        parent context is added to the set of non-returning child nodes of
+        the parent node.
+        
+        @param node reference to the AST Return node
+        @type ast.Return
+        """
+        if node.value is not None:
+            # In the event of an explicit `None` return (`return None`), the
+            # node body will be an instance of either `ast.Constant` (3.8+) or
+            # `ast.NameConstant`, which we need to check to see if it's
+            # actually `None`
+            if (
+                isinstance(node.value, (ast.Constant, ast.NameConstant)) and
+                node.value.value is None
+            ):
+                return
+            
+            self.__nonNoneReturnNodes.add(self.__context[-1])
+
+    def switchContext(self, node):
+        """
+        Public method implementing a context switcher as a generic function
+        visitor in order to track function context.
+        
+        Without keeping track of context, it's challenging to reliably
+        differentiate class methods from "regular" functions, especially in the
+        case of nested classes.
+        
+        @param node reference to the function definition node to be analyzed
+        @type ast.AsyncFunctionDef or ast.FunctionDef
+        """
+        self.__context.append(node)
+        self.generic_visit(node)
+        self.__context.pop()
+
+    visit_FunctionDef = switchContext
+    visit_AsyncFunctionDef = switchContext
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/translations.py	Thu Apr 15 18:11:24 2021 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/translations.py	Fri Apr 16 18:03:43 2021 +0200
@@ -45,6 +45,9 @@
     "A206": QCoreApplication.translate(
         "AnnotationsChecker",
         "missing return type annotation for classmethod"),
+    "A301": QCoreApplication.translate(
+        "AnnotationsChecker",
+        "PEP 484 disallows both type annotations and type comments"),
     
     "A881": QCoreApplication.translate(
         "AnnotationsChecker",
@@ -53,6 +56,9 @@
     "A891": QCoreApplication.translate(
         "AnnotationsChecker",
         "type annotation is too complex ({0} > {1})"),
+    "A892": QCoreApplication.translate(
+        "AnnotationsChecker",
+        "type annotation is too long ({0} > {1})"),
 }
 
 _annotationsMessagesSampleArgs = {
@@ -61,4 +67,5 @@
     "A003": ["kwargs"],
     "A881": [60],
     "A891": [5, 3],
+    "A892": [10, 7],
 }
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleChecker.py	Thu Apr 15 18:11:24 2021 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleChecker.py	Fri Apr 16 18:03:43 2021 +0200
@@ -424,8 +424,9 @@
             errors += complexityChecker.errors
             
             # check function annotations
-            if sys.version_info >= (3, 5, 0):
-                # annotations are supported from Python 3.5 on
+            if sys.version_info >= (3, 8, 0):
+                # annotations with type comments are supported from
+                # Python 3.8 on
                 from Annotations.AnnotationsChecker import AnnotationsChecker
                 annotationsChecker = AnnotationsChecker(
                     source, filename, tree, select, ignore, [], repeatMessages,
--- a/eric6/Preferences/ShortcutsFile.py	Thu Apr 15 18:11:24 2021 +0200
+++ b/eric6/Preferences/ShortcutsFile.py	Fri Apr 16 18:03:43 2021 +0200
@@ -26,7 +26,7 @@
     """
     Class representing the shortcuts JSON file.
     """
-    def __init__(self, parent: QObject = None):
+    def __init__(self: "ShortcutsFile", parent: QObject = None) -> None:
         """
         Constructor
         
@@ -35,8 +35,8 @@
         """
         super().__init__(parent)
     
-    def __addActionsToDict(self, category: str, actions: list,
-                           actionsDict: dict):
+    def __addActionsToDict(self: "ShortcutsFile", category: str, actions: list,
+                           actionsDict: dict) -> None:
         """
         Private method to add a list of actions to the actions dictionary.
         
@@ -58,7 +58,8 @@
                         act.alternateShortcut().toString()
                     )
     
-    def writeFile(self, filename: str, helpViewer: HelpViewer = None) -> bool:
+    def writeFile(self: "ShortcutsFile", filename: str,
+                  helpViewer: HelpViewer = None) -> bool:
         """
         Public method to write the shortcuts data to a shortcuts JSON file.
         
@@ -176,7 +177,7 @@
         
         return True
     
-    def readFile(self, filename: str) -> bool:
+    def readFile(self: "ShortcutsFile", filename: str) -> bool:
         """
         Public method to read the shortcuts data from a shortcuts JSON file.
         

eric ide

mercurial