diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml new file mode 100644 index 00000000000..42529b5e04d --- /dev/null +++ b/.github/workflows/mypy.yml @@ -0,0 +1,42 @@ +name: Unit tests + +on: + push: + branches: + - main + - master + paths: + - '.github/workflows/**' + - 'src/**' + - 'utest/**' + - '!**/*.rst' + + +jobs: + test_using_builtin_python: + + strategy: + fail-fast: false + matrix: + os: [ 'ubuntu-latest', 'windows-latest' ] + python-version: [ '3.6', '3.7', '3.8', '3.9', 'pypy3' ] + exclude: + - os: windows-latest + python-version: 'pypy3' + + runs-on: ${{ matrix.os }} + + name: Python ${{ matrix.python-version }} on ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + + - name: Setup python ${{ matrix.python-version }} + uses: actions/setup-python@v2.2.2 + with: + python-version: ${{ matrix.python-version }} + architecture: 'x64' + + - name: Run mypy + run: | + python -m pip install mypy + python -m mypy --install-types --non-interactive src diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000000..90d60fbb7bc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[tool.mypy] +allow_redefinition = true +show_error_codes = true +warn_redundant_casts = true + +[[tool.mypy.overrides]] +module = "robot.*" + +[[tool.mypy.overrides]] +module = "robot.model.testcase" +strict_equality = true +warn_unused_ignores = true +disallow_any_generics = true +disallow_untyped_defs = true + +[[tool.mypy.overrides]] +module = "robot.api.*" +strict_equality = true +warn_unused_ignores = true +disallow_any_generics = true +disallow_untyped_defs = true diff --git a/src/robot/__main__.py b/src/robot/__main__.py index c4727ca9e89..a8bb5a71737 100755 --- a/src/robot/__main__.py +++ b/src/robot/__main__.py @@ -20,7 +20,7 @@ # Allows running as a script. __name__ check needed with multiprocessing: # https://github.com/robotframework/robotframework/issues/1137 if 'robot' not in sys.modules and __name__ == '__main__': - import pythonpathsetter + import pythonpathsetter # type: ignore[import] from robot import run_cli diff --git a/src/robot/api/deco.py b/src/robot/api/deco.py index daf6c545ecd..cdb9a0b8a23 100644 --- a/src/robot/api/deco.py +++ b/src/robot/api/deco.py @@ -15,8 +15,17 @@ import inspect +from typing import overload, cast, Callable, TypeVar, Union, List, Any, TYPE_CHECKING, Mapping -def not_keyword(func): +if TYPE_CHECKING: + from typing_extensions import Literal + +Fn = TypeVar("Fn", bound=Callable[..., Any]) +Cls = TypeVar("Cls", bound=type) +_Types = Union[Mapping[str, type], List[type], None] + + +def not_keyword(func: Fn) -> Fn: """Decorator to disable exposing functions or methods as keywords. Examples:: @@ -34,15 +43,23 @@ def exposed_as_keyword(): New in Robot Framework 3.2. """ - func.robot_not_keyword = True + func.robot_not_keyword = True # type: ignore[attr-defined] return func -not_keyword.robot_not_keyword = True +not_keyword.robot_not_keyword = True # type: ignore[attr-defined] + + +@overload +def keyword(name: Fn) -> Fn: ... + + +@overload +def keyword(name: str = None, tags: List[str] = ..., types: _Types = ...) -> Callable[[Fn], Fn]: ... @not_keyword -def keyword(name=None, tags=(), types=()): +def keyword(name: object = None, tags: object = (), types: object = ()) -> object: """Decorator to set custom name, tags and argument types to keywords. This decorator creates ``robot_name``, ``robot_tags`` and ``robot_types`` @@ -85,20 +102,30 @@ def no_conversion(length, case_insensitive=False): # ... """ if inspect.isroutine(name): - return keyword()(name) + return keyword()(name) # type: ignore[type-var, return-value] - def decorator(func): - func.robot_name = name - func.robot_tags = tags - func.robot_types = types + def decorator(func: Fn) -> Fn: + func.robot_name = name # type:ignore[attr-defined] + func.robot_tags = tags # type:ignore[attr-defined] + func.robot_types = types # type:ignore[attr-defined] return func return decorator +@overload +def library(scope: Cls) -> Cls: ... + + +@overload +def library(scope: 'Literal["GLOBAL", "SUITE", "TEST", None]' = None, + version: str = None, doc_format: str = None, listener: str = None, + auto_keywords: bool = False) -> Callable[[Cls], Cls]: ... + + @not_keyword -def library(scope=None, version=None, doc_format=None, listener=None, - auto_keywords=False): +def library(scope: object = None, version: str = None, doc_format: str = None, listener: str = None, + auto_keywords: bool = False) -> object: """Class decorator to control keyword discovery and other library settings. By default disables automatic keyword detection by setting class attribute @@ -134,18 +161,18 @@ class LibraryConfiguration: The ``@library`` decorator is new in Robot Framework 3.2. """ if inspect.isclass(scope): - return library()(scope) + return library()(cast(type, scope)) - def decorator(cls): + def decorator(cls: Cls) -> Cls: if scope is not None: - cls.ROBOT_LIBRARY_SCOPE = scope + cls.ROBOT_LIBRARY_SCOPE = scope # type:ignore[attr-defined] if version is not None: - cls.ROBOT_LIBRARY_VERSION = version + cls.ROBOT_LIBRARY_VERSION = version # type:ignore[attr-defined] if doc_format is not None: - cls.ROBOT_LIBRARY_DOC_FORMAT = doc_format + cls.ROBOT_LIBRARY_DOC_FORMAT = doc_format # type:ignore[attr-defined] if listener is not None: - cls.ROBOT_LIBRARY_LISTENER = listener - cls.ROBOT_AUTO_KEYWORDS = auto_keywords + cls.ROBOT_LIBRARY_LISTENER = listener # type:ignore[attr-defined] + cls.ROBOT_AUTO_KEYWORDS = auto_keywords # type:ignore[attr-defined] return cls return decorator diff --git a/src/robot/api/exceptions.py b/src/robot/api/exceptions.py index 29961f33635..a3105891f09 100644 --- a/src/robot/api/exceptions.py +++ b/src/robot/api/exceptions.py @@ -31,7 +31,7 @@ class Failure(AssertionError): """ ROBOT_SUPPRESS_NAME = True - def __init__(self, message, html=False): + def __init__(self, message: str, html: bool=False) -> None: """ :param message: Exception message. :param html: When ``True``, message is considered to be HTML and not escaped. @@ -57,7 +57,7 @@ class Error(RuntimeError): """ ROBOT_SUPPRESS_NAME = True - def __init__(self, message, html=False): + def __init__(self, message: str, html: bool=False) -> None: """ :param message: Exception message. :param html: When ``True``, message is considered to be HTML and not escaped. @@ -76,7 +76,7 @@ class SkipExecution(Exception): ROBOT_SKIP_EXECUTION = True ROBOT_SUPPRESS_NAME = True - def __init__(self, message, html=False): + def __init__(self, message: str, html: bool=False) -> None: """ :param message: Exception message. :param html: When ``True``, message is considered to be HTML and not escaped. diff --git a/src/robot/api/logger.py b/src/robot/api/logger.py index 34ea698e970..bf002c7cd24 100644 --- a/src/robot/api/logger.py +++ b/src/robot/api/logger.py @@ -66,12 +66,16 @@ def my_keyword(arg): """ import logging - +from typing import TYPE_CHECKING from robot.output import librarylogger from robot.running.context import EXECUTION_CONTEXTS -def write(msg, level='INFO', html=False): +if TYPE_CHECKING: + from typing_extensions import Literal + + +def write(msg: str, level: 'Literal["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]' = 'INFO', html: bool = False) -> None: """Writes the message to the log file using the given level. Valid log levels are ``TRACE``, ``DEBUG``, ``INFO`` (default), ``WARN``, and @@ -86,26 +90,28 @@ def write(msg, level='INFO', html=False): librarylogger.write(msg, level, html) else: logger = logging.getLogger("RobotFramework") - level = {'TRACE': logging.DEBUG // 2, + level = {'TRACE': logging.DEBUG // 2, # type: ignore[assignment] 'DEBUG': logging.DEBUG, 'INFO': logging.INFO, 'HTML': logging.INFO, 'WARN': logging.WARN, 'ERROR': logging.ERROR}[level] - logger.log(level, msg) + logger.log(level, msg) # type: ignore[arg-type] + + -def trace(msg, html=False): +def trace(msg: str, html: bool=False) -> None: """Writes the message to the log file using the ``TRACE`` level.""" write(msg, 'TRACE', html) -def debug(msg, html=False): +def debug(msg: str, html: bool=False) -> None: """Writes the message to the log file using the ``DEBUG`` level.""" write(msg, 'DEBUG', html) -def info(msg, html=False, also_console=False): +def info(msg: str, html: bool=False, also_console: bool=False) -> None: """Writes the message to the log file using the ``INFO`` level. If ``also_console`` argument is set to ``True``, the message is @@ -116,18 +122,18 @@ def info(msg, html=False, also_console=False): console(msg) -def warn(msg, html=False): +def warn(msg: str, html: bool=False) -> None: """Writes the message to the log file using the ``WARN`` level.""" write(msg, 'WARN', html) -def error(msg, html=False): +def error(msg: str, html: bool=False) -> None: """Writes the message to the log file using the ``ERROR`` level. """ write(msg, 'ERROR', html) -def console(msg, newline=True, stream='stdout'): +def console(msg: str, newline: bool=True, stream: str='stdout') -> None: """Writes the message to the console. If the ``newline`` argument is ``True``, a newline character is diff --git a/src/robot/conf/settings.py b/src/robot/conf/settings.py index 9f8d579ba26..1fcfec6267b 100644 --- a/src/robot/conf/settings.py +++ b/src/robot/conf/settings.py @@ -17,6 +17,7 @@ import random import sys import time +from typing import Dict, Tuple from robot.errors import DataError, FrameworkError from robot.output import LOGGER, loggerhelper @@ -30,7 +31,8 @@ class _BaseSettings: - _cli_opts = {'RPA' : ('rpa', None), + _cli_opts: Dict[str, Tuple[str, object]] = { + 'RPA' : ('rpa', None), 'Name' : ('name', None), 'Doc' : ('doc', None), 'Metadata' : ('metadata', []), diff --git a/src/robot/htmldata/htmlfilewriter.py b/src/robot/htmldata/htmlfilewriter.py index bc18a6a2105..0ed09da120c 100644 --- a/src/robot/htmldata/htmlfilewriter.py +++ b/src/robot/htmldata/htmlfilewriter.py @@ -15,6 +15,7 @@ import os.path import re +from typing import Optional from robot.utils import HtmlWriter from robot.version import get_full_version @@ -46,7 +47,7 @@ def _get_writers(self, base_dir): class _Writer: - _handles_line = None + _handles_line: Optional[str] = None def handles(self, line): return line.startswith(self._handles_line) diff --git a/src/robot/htmldata/jsonwriter.py b/src/robot/htmldata/jsonwriter.py index 9ea51e9aec1..eca00f60d18 100644 --- a/src/robot/htmldata/jsonwriter.py +++ b/src/robot/htmldata/jsonwriter.py @@ -12,6 +12,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import Optional, Union, Tuple + class JsonWriter: @@ -55,7 +57,7 @@ def dump(self, data, mapping=None): class _Dumper: - _handled_types = None + _handled_types: Union[type, Tuple[type, ...], None] = None def __init__(self, jsondumper): self._dump = jsondumper.dump diff --git a/src/robot/htmldata/testdata/create_libdoc_data.py b/src/robot/htmldata/testdata/create_libdoc_data.py index 12147a9cab4..5b15cf91712 100755 --- a/src/robot/htmldata/testdata/create_libdoc_data.py +++ b/src/robot/htmldata/testdata/create_libdoc_data.py @@ -16,6 +16,6 @@ with open(OUTPUT, 'w') as output: libdoc = LibraryDocumentation(INPUT) - LibdocModelWriter(output, libdoc).write_data() + LibdocModelWriter(output, libdoc).write_data() # type: ignore[attr-defined] print(OUTPUT) diff --git a/src/robot/libdoc.py b/src/robot/libdoc.py index 5aef14ba2bd..4bdd5f16858 100755 --- a/src/robot/libdoc.py +++ b/src/robot/libdoc.py @@ -37,7 +37,7 @@ # Allows running as a script. __name__ check needed with multiprocessing: # https://github.com/robotframework/robotframework/issues/1137 if 'robot' not in sys.modules and __name__ == '__main__': - import pythonpathsetter + import pythonpathsetter # type: ignore[import] from robot.utils import Application, seq2str from robot.errors import DataError diff --git a/src/robot/libdocpkg/htmlutils.py b/src/robot/libdocpkg/htmlutils.py index 4f7c62cf074..a57f094006c 100644 --- a/src/robot/libdocpkg/htmlutils.py +++ b/src/robot/libdocpkg/htmlutils.py @@ -15,7 +15,7 @@ import re try: - from urllib import quote + from urllib import quote # type: ignore[attr-defined] except ImportError: from urllib.parse import quote diff --git a/src/robot/libdocpkg/model.py b/src/robot/libdocpkg/model.py index deebf522b66..9b6ac093849 100644 --- a/src/robot/libdocpkg/model.py +++ b/src/robot/libdocpkg/model.py @@ -164,7 +164,7 @@ def _get_shortdoc(self): doc = HtmlToText().get_shortdoc_from_html(doc) return ' '.join(getshortdoc(doc).splitlines()) - @shortdoc.setter + @shortdoc.setter # type: ignore[no-redef,attr-defined] def shortdoc(self, shortdoc): self._shortdoc = shortdoc diff --git a/src/robot/libraries/Remote.py b/src/robot/libraries/Remote.py index 33b7e6897c4..78dce6b0728 100644 --- a/src/robot/libraries/Remote.py +++ b/src/robot/libraries/Remote.py @@ -20,6 +20,7 @@ import socket import sys import xmlrpc.client +from typing import Type from xml.parsers.expat import ExpatError from robot.errors import RemoteError @@ -198,7 +199,7 @@ def __init__(self, uri, timeout=None): self.uri = uri self.timeout = timeout - @property + @property # type: ignore[misc] @contextmanager def _server(self): if self.uri.startswith('https://'): @@ -259,7 +260,7 @@ def run_keyword(self, name, args, kwargs): # http://stackoverflow.com/questions/2425799/timeout-for-xmlrpclib-client-requests class TimeoutHTTPTransport(xmlrpc.client.Transport): - _connection_class = http.client.HTTPConnection + _connection_class: Type[http.client.HTTPConnection] = http.client.HTTPConnection def __init__(self, use_datetime=0, timeout=None): xmlrpc.client.Transport.__init__(self, use_datetime) diff --git a/src/robot/libraries/Screenshot.py b/src/robot/libraries/Screenshot.py index c3f87edd01b..b8ad2ebc3a8 100644 --- a/src/robot/libraries/Screenshot.py +++ b/src/robot/libraries/Screenshot.py @@ -18,15 +18,15 @@ import sys try: - import wx + import wx # type: ignore[import] except ImportError: wx = None try: - from gtk import gdk + from gtk import gdk # type: ignore[import] except ImportError: gdk = None try: - from PIL import ImageGrab # apparently available only on Windows + from PIL import ImageGrab # type: ignore[import] # apparently available only on Windows except ImportError: ImageGrab = None diff --git a/src/robot/libraries/Telnet.py b/src/robot/libraries/Telnet.py index 65652176f20..1ba22972984 100644 --- a/src/robot/libraries/Telnet.py +++ b/src/robot/libraries/Telnet.py @@ -22,7 +22,7 @@ import time try: - import pyte + import pyte # type: ignore[import] except ImportError: pyte = None diff --git a/src/robot/libraries/XML.py b/src/robot/libraries/XML.py index 932a13ebef5..f4bdff90d14 100644 --- a/src/robot/libraries/XML.py +++ b/src/robot/libraries/XML.py @@ -18,7 +18,7 @@ import os try: - from lxml import etree as lxml_etree + from lxml import etree as lxml_etree # type: ignore[import] except ImportError: lxml_etree = None diff --git a/src/robot/libraries/dialogs_py.py b/src/robot/libraries/dialogs_py.py index 7c0fd2cec68..412c9d6fcea 100644 --- a/src/robot/libraries/dialogs_py.py +++ b/src/robot/libraries/dialogs_py.py @@ -12,13 +12,13 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - import sys from threading import current_thread import time +from typing import Optional try: - from Tkinter import (Button, Entry, Frame, Label, Listbox, TclError, + from Tkinter import (Button, Entry, Frame, Label, Listbox, TclError, # type: ignore[import] Toplevel, Tk, BOTH, END, LEFT, W) except ImportError: from tkinter import (Button, Entry, Frame, Label, Listbox, TclError, @@ -27,7 +27,7 @@ class _TkDialog(Toplevel): _left_button = 'OK' - _right_button = 'Cancel' + _right_button: Optional[str] = 'Cancel' def __init__(self, message, value=None, **extra): self._prevent_execution_with_timeouts() diff --git a/src/robot/model/body.py b/src/robot/model/body.py index c996227715d..46fa0a9b67e 100644 --- a/src/robot/model/body.py +++ b/src/robot/model/body.py @@ -12,8 +12,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - import re +from typing import List, Optional from .itemlist import ItemList from .modelobject import ModelObject @@ -31,7 +31,7 @@ class BodyItem(ModelObject): ELSE = 'ELSE' RETURN = 'RETURN' MESSAGE = 'MESSAGE' - type = None + type: Optional[str] = None __slots__ = ['parent'] @property @@ -60,7 +60,7 @@ class Body(ItemList): Body contains the keywords and other structures such as for loops. """ - __slots__ = [] + __slots__: List[str] = [] # Set using 'Body.register' when these classes are created. keyword_class = None for_class = None @@ -144,7 +144,7 @@ class IfBranches(Body): keyword_class = None for_class = None if_class = None - __slots__ = [] + __slots__: List[str] = [] def create_branch(self, *args, **kwargs): return self.append(self.if_branch_class(*args, **kwargs)) diff --git a/src/robot/model/keyword.py b/src/robot/model/keyword.py index c5245e8d2c7..1b9a90dec03 100644 --- a/src/robot/model/keyword.py +++ b/src/robot/model/keyword.py @@ -14,6 +14,7 @@ # limitations under the License. import warnings +from typing import List from robot.utils import setter @@ -127,7 +128,7 @@ class Keywords(ItemList): Read-only and deprecated since Robot Framework 4.0. """ - __slots__ = [] + __slots__: List[str] = [] deprecation_message = ( "'keywords' attribute is read-only and deprecated since Robot Framework 4.0. " "Use 'body', 'setup' or 'teardown' instead." diff --git a/src/robot/model/message.py b/src/robot/model/message.py index c931223e6ef..9c25c302235 100644 --- a/src/robot/model/message.py +++ b/src/robot/model/message.py @@ -12,6 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import List from robot.utils import html_escape @@ -63,7 +64,7 @@ def __str__(self): class Messages(ItemList): - __slots__ = [] + __slots__: List[str] = [] def __init__(self, message_class=Message, parent=None, messages=None): ItemList.__init__(self, message_class, {'parent': parent}, messages) diff --git a/src/robot/model/modelobject.py b/src/robot/model/modelobject.py index 77698ca832d..39edcbb396a 100644 --- a/src/robot/model/modelobject.py +++ b/src/robot/model/modelobject.py @@ -14,13 +14,14 @@ # limitations under the License. import copy +from typing import Tuple, List from robot.utils import SetterAwareType class ModelObject(metaclass=SetterAwareType): - repr_args = () - __slots__ = [] + repr_args: Tuple[str, ...] = () + __slots__: List[str] = [] def config(self, **attributes): """Configure model object with given attributes. diff --git a/src/robot/model/testcase.py b/src/robot/model/testcase.py index 69fe3f34d04..0cf6995a9c2 100644 --- a/src/robot/model/testcase.py +++ b/src/robot/model/testcase.py @@ -13,8 +13,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import List, Type, TYPE_CHECKING, Tuple + +import robot from robot.utils import setter + from .body import Body from .fixture import create_fixture from .itemlist import ItemList @@ -22,6 +26,10 @@ from .modelobject import ModelObject from .tags import Tags +if TYPE_CHECKING: + from ..running.model import Keyword as RunningKeyword + from ..model.keyword import Keyword as ModelKeyword + class TestCase(ModelObject): """Base model for a single test case. @@ -30,11 +38,11 @@ class TestCase(ModelObject): :class:`robot.result.model.TestCase`. """ body_class = Body - fixture_class = Keyword + fixture_class: "Type[Keyword | RunningKeyword | ModelKeyword]" = Keyword repr_args = ('name',) __slots__ = ['parent', 'name', 'doc', 'timeout'] - - def __init__(self, name='', doc='', tags=None, timeout=None, parent=None): + def __init__(self, + name: str = '', doc: str = '', tags: None = None, timeout: None = None, parent: "robot.model.testsuite.TestSuite" = None) -> None: self.name = name self.doc = doc self.timeout = timeout @@ -45,17 +53,17 @@ def __init__(self, name='', doc='', tags=None, timeout=None, parent=None): self.teardown = None @setter - def body(self, body): + def body(self, body: object) -> Body: """Test case body as a :class:`~.Body` object.""" return self.body_class(self, body) @setter - def tags(self, tags): + def tags(self, tags: object) -> Tags: """Test tags as a :class:`~.model.tags.Tags` object.""" return Tags(tags) @setter - def setup(self, setup): + def setup(self, setup: object) -> Keyword: """Test setup as a :class:`~.model.keyword.Keyword` object. This attribute is a ``Keyword`` object also when a test has no setup @@ -82,7 +90,7 @@ def setup(self, setup): return create_fixture(setup, self, Keyword.SETUP) @setter - def teardown(self, teardown): + def teardown(self, teardown: object) -> Keyword: """Test teardown as a :class:`~.model.keyword.Keyword` object. See :attr:`setup` for more information. @@ -90,7 +98,7 @@ def teardown(self, teardown): return create_fixture(teardown, self, Keyword.TEARDOWN) @property - def keywords(self): + def keywords(self) -> Keywords: """Deprecated since Robot Framework 4.0 Use :attr:`body`, :attr:`setup` or :attr:`teardown` instead. @@ -99,11 +107,11 @@ def keywords(self): return Keywords(self, [kw for kw in keywords if kw]) @keywords.setter - def keywords(self, keywords): + def keywords(self, keywords: object) -> None: Keywords.raise_deprecation_error() @property - def id(self): + def id(self) -> str: """Test case id in format like ``s1-t3``. See :attr:`TestSuite.id ` for @@ -114,31 +122,31 @@ def id(self): return '%s-t%d' % (self.parent.id, self.parent.tests.index(self)+1) @property - def longname(self): + def longname(self) -> str: """Test name prefixed with the long name of the parent suite.""" if not self.parent: return self.name return '%s.%s' % (self.parent.longname, self.name) @property - def source(self): + def source(self) -> str: return self.parent.source if self.parent is not None else None - def visit(self, visitor): + def visit(self, visitor: "robot.model.visitor.SuiteVisitor") -> None: """:mod:`Visitor interface ` entry-point.""" visitor.visit_test(self) - def __str__(self): + def __str__(self) -> str: return self.name class TestCases(ItemList): - __slots__ = [] + __slots__: List[str] = [] - def __init__(self, test_class=TestCase, parent=None, tests=None): + def __init__(self, test_class: Type[TestCase] = TestCase, parent: None = None, tests: None = None) -> None: ItemList.__init__(self, test_class, {'parent': parent}, tests) - def _check_type_and_set_attrs(self, *tests): + def _check_type_and_set_attrs(self, *tests: str) -> Tuple[str]: tests = ItemList._check_type_and_set_attrs(self, *tests) for test in tests: for visitor in test.parent._visitors: diff --git a/src/robot/model/testsuite.py b/src/robot/model/testsuite.py index 63f94be3258..65d29a9e48d 100644 --- a/src/robot/model/testsuite.py +++ b/src/robot/model/testsuite.py @@ -13,6 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import typing +from typing import List, Type + from robot.utils import setter from .configurer import SuiteConfigurer @@ -25,6 +28,9 @@ from .tagsetter import TagSetter from .testcase import TestCase, TestCases +if typing.TYPE_CHECKING: + from robot.result.model import Keyword as ModelKeyword + class TestSuite(ModelObject): """Base model for single suite. @@ -33,7 +39,7 @@ class TestSuite(ModelObject): :class:`robot.result.model.TestSuite`. """ test_class = TestCase #: Internal usage only. - fixture_class = Keyword #: Internal usage only. + fixture_class: "Type[Keyword | ModelKeyword]" = Keyword #: Internal usage only. repr_args = ('name',) __slots__ = ['parent', 'source', '_name', 'doc', '_my_visitors', 'rpa'] @@ -229,7 +235,7 @@ def __str__(self): class TestSuites(ItemList): - __slots__ = [] + __slots__: List[str] = [] def __init__(self, suite_class=TestSuite, parent=None, suites=None): ItemList.__init__(self, suite_class, {'parent': parent}, suites) diff --git a/src/robot/output/console/highlighting.py b/src/robot/output/console/highlighting.py index f65055290f8..c351582c9a5 100644 --- a/src/robot/output/console/highlighting.py +++ b/src/robot/output/console/highlighting.py @@ -21,9 +21,10 @@ import errno import os import sys -try: +import sys +if sys.platform == "win32": from ctypes import windll, Structure, c_short, c_ushort, byref -except ImportError: # Not on Windows +else: windll = None from robot.errors import DataError @@ -64,7 +65,7 @@ def _write(self, text, retry=5): raise self._write(text, retry-1) - @property + @property # type: ignore[misc] @contextmanager def _suppress_broken_pipe_error(self): try: diff --git a/src/robot/output/listenerarguments.py b/src/robot/output/listenerarguments.py index 3d3a27a54e0..6d45febe7f8 100644 --- a/src/robot/output/listenerarguments.py +++ b/src/robot/output/listenerarguments.py @@ -12,6 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import Tuple, Optional from robot.utils import is_list_like, is_dict_like, is_string, unic @@ -66,7 +67,7 @@ def _get_version3_arguments(self, msg): class _ListenerArgumentsFromItem(ListenerArguments): - _attribute_names = None + _attribute_names: Optional[Tuple[str, ...]] = None def _get_version2_arguments(self, item): attributes = dict((name, self._get_attribute_value(item, name)) @@ -93,7 +94,7 @@ def _get_version3_arguments(self, item): class StartSuiteArguments(_ListenerArgumentsFromItem): - _attribute_names = ('id', 'longname', 'doc', 'metadata', 'starttime') + _attribute_names: Tuple[str, ...] = ('id', 'longname', 'doc', 'metadata', 'starttime') def _get_extra_attributes(self, suite): return {'tests': [t.name for t in suite.tests], @@ -113,7 +114,7 @@ def _get_extra_attributes(self, suite): class StartTestArguments(_ListenerArgumentsFromItem): - _attribute_names = ('id', 'longname', 'doc', 'tags', 'lineno', 'source', 'starttime') + _attribute_names: Tuple[str, ...] = ('id', 'longname', 'doc', 'tags', 'lineno', 'source', 'starttime') def _get_extra_attributes(self, test): return {'template': test.template or '', @@ -126,8 +127,8 @@ class EndTestArguments(StartTestArguments): class StartKeywordArguments(_ListenerArgumentsFromItem): - _attribute_names = ('doc', 'assign', 'tags', 'lineno', 'source', 'type', 'status', - 'starttime') + _attribute_names: Tuple[str, ...] = ('doc', 'assign', 'tags', 'lineno', 'source', 'type', + 'status', 'starttime') def _get_extra_attributes(self, kw): args = [a if is_string(a) else unic(a) for a in kw.args] diff --git a/src/robot/output/logger.py b/src/robot/output/logger.py index 58575b45eeb..d56fc5d4fd2 100644 --- a/src/robot/output/logger.py +++ b/src/robot/output/logger.py @@ -150,7 +150,7 @@ def message(self, msg): if self._error_listener: self._error_listener() - @property + @property # type: ignore[misc] @contextmanager def cache_only(self): self._cache_only = True @@ -159,7 +159,7 @@ def cache_only(self): finally: self._cache_only = False - @property + @property # type: ignore[misc] @contextmanager def delayed_logging(self): prev_cache = self._log_message_cache diff --git a/src/robot/output/loggerhelper.py b/src/robot/output/loggerhelper.py index 96b0ed6e22a..c76aea44f1d 100644 --- a/src/robot/output/loggerhelper.py +++ b/src/robot/output/loggerhelper.py @@ -12,6 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import Optional, Tuple, Callable from robot.errors import DataError from robot.model import Message as BaseMessage @@ -136,8 +137,8 @@ def _level_to_int(self, level): class AbstractLoggerProxy: - _methods = None - _no_method = lambda *args: None + _methods: Optional[Tuple[str, ...]] = None + _no_method: Optional[Callable[..., object]] = lambda *args: None def __init__(self, logger, method_names=None, prefix=None): self.logger = logger diff --git a/src/robot/parsing/lexer/blocklexers.py b/src/robot/parsing/lexer/blocklexers.py index cccaee00fec..551b65b3816 100644 --- a/src/robot/parsing/lexer/blocklexers.py +++ b/src/robot/parsing/lexer/blocklexers.py @@ -155,7 +155,7 @@ def lexer_classes(self): class TestOrKeywordLexer(BlockLexer): - name_type = NotImplemented + name_type: str = NotImplemented _name_seen = False def accepts_more(self, statement): diff --git a/src/robot/parsing/lexer/context.py b/src/robot/parsing/lexer/context.py index fc37a8c7c91..9e83e9dcc44 100644 --- a/src/robot/parsing/lexer/context.py +++ b/src/robot/parsing/lexer/context.py @@ -12,15 +12,16 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import Optional, Type, Union from .sections import (InitFileSections, TestCaseFileSections, - ResourceFileSections) + ResourceFileSections, Sections) from .settings import (InitFileSettings, TestCaseFileSettings, - ResourceFileSettings, TestCaseSettings, KeywordSettings) + ResourceFileSettings, TestCaseSettings, KeywordSettings, Settings) class LexingContext: - settings_class = None + settings_class: Optional[Type[Union[Settings, Sections]]] = None def __init__(self, settings=None): self.settings = settings or self.settings_class() @@ -30,7 +31,7 @@ def lex_setting(self, statement): class FileContext(LexingContext): - sections_class = None + sections_class: Optional[Type[Union[Sections, Settings]]] = None def __init__(self, settings=None): LexingContext.__init__(self, settings) diff --git a/src/robot/parsing/lexer/lexer.py b/src/robot/parsing/lexer/lexer.py index 5ee608cc3b7..077b46d1cb0 100644 --- a/src/robot/parsing/lexer/lexer.py +++ b/src/robot/parsing/lexer/lexer.py @@ -14,17 +14,19 @@ # limitations under the License. from itertools import chain +from typing import Union, Generator +from pathlib import Path from robot.errors import DataError from robot.utils import get_error_message, FileReader -from .blocklexers import FileLexer, InlineIfLexer +from .blocklexers import FileLexer from .context import InitFileContext, TestCaseFileContext, ResourceFileContext from .tokenizer import Tokenizer from .tokens import EOS, END, Token -def get_tokens(source, data_only=False, tokenize_variables=False): +def get_tokens(source: Union[str, Path], data_only: bool = False, tokenize_variables: bool = False) -> Generator[Token, None, None]: """Parses the given source to tokens. :param source: The source where to read the data. Can be a path to @@ -47,7 +49,7 @@ def get_tokens(source, data_only=False, tokenize_variables=False): return lexer.get_tokens() -def get_resource_tokens(source, data_only=False, tokenize_variables=False): +def get_resource_tokens(source: Union[str, Path], data_only: bool=False, tokenize_variables: bool=False) -> Generator[Token, None, None]: """Parses the given source to resource file tokens. Otherwise same as :func:`get_tokens` but the source is considered to be @@ -58,7 +60,7 @@ def get_resource_tokens(source, data_only=False, tokenize_variables=False): return lexer.get_tokens() -def get_init_tokens(source, data_only=False, tokenize_variables=False): +def get_init_tokens(source: Union[str, Path], data_only=False, tokenize_variables=False) -> Generator[Token, None, None]: """Parses the given source to init file tokens. Otherwise same as :func:`get_tokens` but the source is considered to be diff --git a/src/robot/parsing/lexer/settings.py b/src/robot/parsing/lexer/settings.py index 4008a535b88..027cf9f3bf1 100644 --- a/src/robot/parsing/lexer/settings.py +++ b/src/robot/parsing/lexer/settings.py @@ -12,6 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import Tuple, Dict from robot.utils import normalize, normalize_whitespace, RecommendationFinder @@ -19,8 +20,8 @@ class Settings: - names = () - aliases = {} + names: Tuple[str, ...] = () + aliases: Dict[str, str] = {} multi_use = ( 'Metadata', 'Library', diff --git a/src/robot/parsing/lexer/statementlexers.py b/src/robot/parsing/lexer/statementlexers.py index 7c3d093b9c8..04f20d93317 100644 --- a/src/robot/parsing/lexer/statementlexers.py +++ b/src/robot/parsing/lexer/statementlexers.py @@ -12,6 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import Optional from robot.utils import normalize_whitespace from robot.variables import is_assign @@ -39,7 +40,7 @@ def lex(self): class StatementLexer(Lexer): - token_type = None + token_type: Optional[str] = None def __init__(self, ctx): Lexer.__init__(self, ctx) diff --git a/src/robot/parsing/lexer/tokens.py b/src/robot/parsing/lexer/tokens.py index adf87df37c6..fc42bf8ce11 100644 --- a/src/robot/parsing/lexer/tokens.py +++ b/src/robot/parsing/lexer/tokens.py @@ -12,6 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import List from robot.variables import VariableIterator @@ -220,7 +221,7 @@ def __eq__(self, other): class EOS(Token): """Token representing end of a statement.""" - __slots__ = [] + __slots__: List[str] = [] def __init__(self, lineno=-1, col_offset=-1): Token.__init__(self, Token.EOS, '', lineno, col_offset) @@ -237,7 +238,7 @@ class END(Token): Virtual END tokens have '' as their value, with "real" END tokens the value is 'END'. """ - __slots__ = [] + __slots__: List[str] = [] def __init__(self, lineno=-1, col_offset=-1, virtual=False): value = 'END' if not virtual else '' diff --git a/src/robot/parsing/model/blocks.py b/src/robot/parsing/model/blocks.py index 4a9bdc0b987..0c37354fbb5 100644 --- a/src/robot/parsing/model/blocks.py +++ b/src/robot/parsing/model/blocks.py @@ -12,8 +12,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - import ast +from typing import Tuple, ClassVar from robot.utils import file_writer, is_pathlike, is_string @@ -22,8 +22,8 @@ class Block(ast.AST): - _fields = () - _attributes = ('lineno', 'col_offset', 'end_lineno', 'end_col_offset', 'errors') + _fields: ClassVar[Tuple[str, ...]] = () + _attributes: ClassVar[Tuple[str, ...]] = ('lineno', 'col_offset', 'end_lineno', 'end_col_offset', 'errors') errors = () @property diff --git a/src/robot/parsing/model/statements.py b/src/robot/parsing/model/statements.py index 88223264d31..ff85d7a5b9c 100644 --- a/src/robot/parsing/model/statements.py +++ b/src/robot/parsing/model/statements.py @@ -12,9 +12,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - import ast import re +from typing import Dict, Optional, Tuple from robot.running.arguments import UserKeywordArgumentParser from robot.utils import normalize_whitespace, split_from_equals @@ -28,11 +28,11 @@ class Statement(ast.AST): - type = None - handles_types = () + type: Optional[str] = None + handles_types: Tuple[str, ...] = () _fields = ('type', 'tokens') _attributes = ('lineno', 'col_offset', 'end_lineno', 'end_col_offset', 'errors') - _statement_handlers = {} + _statement_handlers: Dict[object, object] = {} def __init__(self, tokens, errors=()): self.tokens = tuple(tokens) diff --git a/src/robot/parsing/model/visitor.py b/src/robot/parsing/model/visitor.py index 26306481742..b08f2c96fd6 100644 --- a/src/robot/parsing/model/visitor.py +++ b/src/robot/parsing/model/visitor.py @@ -44,7 +44,7 @@ def visit_Statement(self, node): # ... """ - def visit(self, node): + def visit(self, node: ast.AST) -> None: visitor = self._find_visitor(type(node)) or self.generic_visit visitor(node) diff --git a/src/robot/parsing/parser/fileparser.py b/src/robot/parsing/parser/fileparser.py index 7ca2241c5cd..1179c60aa4b 100644 --- a/src/robot/parsing/parser/fileparser.py +++ b/src/robot/parsing/parser/fileparser.py @@ -14,6 +14,8 @@ # limitations under the License. import os.path +import typing +from typing import Type, Union from robot.utils import is_pathlike, is_string @@ -23,6 +25,8 @@ from .blockparsers import Parser, TestCaseParser, KeywordParser +if typing.TYPE_CHECKING: + from robot.parsing.model.blocks import Section class FileParser(Parser): @@ -58,7 +62,7 @@ def parse(self, statement): class SectionParser(Parser): - model_class = None + model_class: Union[Type["Section"], None] = None def __init__(self, header): Parser.__init__(self, self.model_class(header)) diff --git a/src/robot/parsing/parser/parser.py b/src/robot/parsing/parser/parser.py index 3d82652d7d5..cf482709d43 100644 --- a/src/robot/parsing/parser/parser.py +++ b/src/robot/parsing/parser/parser.py @@ -13,13 +13,19 @@ # See the License for the specific language governing permissions and # limitations under the License. +from pathlib import Path +from typing import TYPE_CHECKING, Union + from ..lexer import Token, get_tokens, get_resource_tokens, get_init_tokens from ..model import Statement from .fileparser import FileParser +if TYPE_CHECKING: + from robot.parsing.model import File + -def get_model(source, data_only=False, curdir=None): +def get_model(source: Union[str, Path], data_only: bool = False, curdir: str = None) -> "File": """Parses the given source to a model represented as an AST. How to use the model is explained more thoroughly in the general @@ -45,7 +51,7 @@ def get_model(source, data_only=False, curdir=None): return _get_model(get_tokens, source, data_only, curdir) -def get_resource_model(source, data_only=False, curdir=None): +def get_resource_model(source: Union[str, Path], data_only: bool = False, curdir: str = None) -> object: """Parses the given source to a resource file model. Otherwise same as :func:`get_model` but the source is considered to be @@ -54,7 +60,7 @@ def get_resource_model(source, data_only=False, curdir=None): return _get_model(get_resource_tokens, source, data_only, curdir) -def get_init_model(source, data_only=False, curdir=None): +def get_init_model(source: Union[str, Path], data_only: bool = False, curdir: str = None) -> "File": """Parses the given source to a init file model. Otherwise same as :func:`get_model` but the source is considered to be diff --git a/src/robot/rebot.py b/src/robot/rebot.py index c021d7811a4..1a809935532 100755 --- a/src/robot/rebot.py +++ b/src/robot/rebot.py @@ -35,7 +35,7 @@ # Allows running as a script. __name__ check needed with multiprocessing: # https://github.com/robotframework/robotframework/issues/1137 if 'robot' not in sys.modules and __name__ == '__main__': - import pythonpathsetter + import pythonpathsetter # type: ignore[import] from robot.conf import RebotSettings from robot.errors import DataError diff --git a/src/robot/reporting/logreportwriters.py b/src/robot/reporting/logreportwriters.py index 3b886f5fbb6..db87f701e8e 100644 --- a/src/robot/reporting/logreportwriters.py +++ b/src/robot/reporting/logreportwriters.py @@ -14,6 +14,7 @@ # limitations under the License. from os.path import basename, splitext +from typing import Optional from robot.htmldata import HtmlFileWriter, ModelWriter, LOG, REPORT from robot.utils import file_writer, is_string @@ -22,7 +23,7 @@ class _LogReportWriter: - usage = None + usage: Optional[str] = None def __init__(self, js_model): self._js_model = js_model diff --git a/src/robot/reporting/resultwriter.py b/src/robot/reporting/resultwriter.py index 4095bb3ae44..a1e22a666f9 100644 --- a/src/robot/reporting/resultwriter.py +++ b/src/robot/reporting/resultwriter.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Union from robot.conf import RebotSettings from robot.errors import DataError from robot.model import ModelModifier @@ -39,7 +40,7 @@ class ResultWriter: writer.write_results(report='custom.html', log=None, xunit='xunit.xml') """ - def __init__(self, *sources): + def __init__(self, *sources: Union[Result, str]) -> None: self._sources = sources def write_results(self, settings=None, **options): diff --git a/src/robot/result/model.py b/src/robot/result/model.py index aeadc511a45..289bdff0f7b 100644 --- a/src/robot/result/model.py +++ b/src/robot/result/model.py @@ -35,6 +35,7 @@ from collections import OrderedDict from itertools import chain import warnings +from typing import List, Any from robot import model from robot.model import BodyItem, Keywords, TotalStatisticsBuilder @@ -49,7 +50,7 @@ class Body(model.Body): message_class = None - __slots__ = [] + __slots__: List[str] = [] def create_message(self, *args, **kwargs): return self.append(self.message_class(*args, **kwargs)) @@ -65,23 +66,23 @@ class ForIterations(Body): for_iteration_class = None if_class = None for_class = None - __slots__ = [] + __slots__: List[str] = [] def create_iteration(self, *args, **kwargs): return self.append(self.for_iteration_class(*args, **kwargs)) class IfBranches(Body, model.IfBranches): - __slots__ = [] + __slots__: List[str] = [] @Body.register class Message(model.Message): - __slots__ = [] + __slots__: List[str] = [] class StatusMixin: - __slots__ = [] + __slots__: List[str] = [] PASS = 'PASS' FAIL = 'FAIL' SKIP = 'SKIP' @@ -164,7 +165,7 @@ def body(self, body): def visit(self, visitor): visitor.visit_for_iteration(self) - @property + @property # type: ignore[misc] @deprecated def name(self): return ', '.join('%s = %s' % item for item in self.variables.items()) @@ -183,7 +184,7 @@ def __init__(self, variables=(), flavor='IN', values=(), status='FAIL', self.endtime = endtime self.doc = doc - @property + @property # type: ignore[misc] @deprecated def name(self): return '%s %s [ %s ]' % (' | '.join(self.variables), self.flavor, @@ -216,7 +217,7 @@ def __init__(self, type=BodyItem.IF, condition=None, status='FAIL', self.endtime = endtime self.doc = doc - @property + @property # type: ignore[misc] @deprecated def name(self): return self.condition @@ -234,12 +235,12 @@ def __init__(self, values=(), status='FAIL', starttime=None, endtime=None, paren # FIXME: Remove attributes. - @property + @property # type: ignore[misc] @deprecated def args(self): return self.values - @property + @property # type: ignore[misc] @deprecated def doc(self): return '' @@ -359,7 +360,7 @@ def __init__(self, name='', doc='', tags=None, timeout=None, status='FAIL', #: Test case execution end time in format ``%Y%m%d %H:%M:%S.%f``. self.endtime = endtime - @property + @property # type: ignore[misc] # Read-only property cannot override read-write property def not_run(self): return False @@ -388,22 +389,22 @@ def __init__(self, name='', doc='', metadata=None, source=None, #: Suite execution end time in format ``%Y%m%d %H:%M:%S.%f``. self.endtime = endtime - @property + @property # type: ignore[misc] # Read-only property cannot override read-write property def passed(self): """``True`` if no test has failed but some have passed, ``False`` otherwise.""" return self.status == self.PASS - @property + @property # type: ignore[misc] # Read-only property cannot override read-write property def failed(self): """``True`` if any test has failed, ``False`` otherwise.""" return self.status == self.FAIL - @property + @property # type: ignore[misc] # Read-only property cannot override read-write property def skipped(self): """``True`` if there are no passed or failed tests, ``False`` otherwise.""" return self.status == self.SKIP - @property + @property # type: ignore[misc] # Read-only property cannot override read-write property def not_run(self): return False diff --git a/src/robot/result/modeldeprecation.py b/src/robot/result/modeldeprecation.py index e67aec224f3..89cd99a6352 100644 --- a/src/robot/result/modeldeprecation.py +++ b/src/robot/result/modeldeprecation.py @@ -12,6 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import List from robot.model import Tags @@ -24,44 +25,44 @@ def wrapper(self, *args, **kws): class DeprecatedAttributesMixin: - __slots__ = [] + __slots__: List[str] = [] - @property + @property # type: ignore[misc] @deprecated def name(self): return '' - @property + @property # type: ignore[misc] @deprecated def kwname(self): return self.name - @property + @property # type: ignore[misc] @deprecated def libname(self): return None - @property + @property # type: ignore[misc] @deprecated def args(self): return () - @property + @property # type: ignore[misc] @deprecated def assign(self): return () - @property + @property # type: ignore[misc] @deprecated def tags(self): return Tags() - @property + @property # type: ignore[misc] @deprecated def timeout(self): return None - @property + @property # type: ignore[misc] @deprecated def message(self): return '' diff --git a/src/robot/result/resultbuilder.py b/src/robot/result/resultbuilder.py index da9ce336585..89b46ba2f51 100644 --- a/src/robot/result/resultbuilder.py +++ b/src/robot/result/resultbuilder.py @@ -12,7 +12,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - from robot.errors import DataError from robot.model import SuiteVisitor from robot.utils import ET, ETSource, get_error_message, unic @@ -23,8 +22,17 @@ from .merger import Merger from .xmlelementhandlers import XmlElementHandler +from typing import TYPE_CHECKING, Union, TextIO + +if TYPE_CHECKING: + from pathlib import Path + from typing_extensions import Literal + + +def ExecutionResult(*sources: "str | TextIO | bytes | Path", merge: bool = ..., rpa: "Literal[True]" = ...) -> Result: ... + -def ExecutionResult(*sources, **options): +def ExecutionResult(*sources, **options) -> Result: # type: ignore[no-redef] """Factory method to constructs :class:`~.executionresult.Result` objects. :param sources: XML source(s) containing execution results. diff --git a/src/robot/result/xmlelementhandlers.py b/src/robot/result/xmlelementhandlers.py index 96c29cb6536..7cfab0bb676 100644 --- a/src/robot/result/xmlelementhandlers.py +++ b/src/robot/result/xmlelementhandlers.py @@ -12,6 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import Optional, Dict, Tuple, FrozenSet from robot.errors import DataError @@ -33,9 +34,9 @@ def end(self, elem): class ElementHandler: - element_handlers = {} - tag = None - children = frozenset() + element_handlers: Dict[object, object] = {} + tag: Optional[str] = None + children: FrozenSet[str] = frozenset() @classmethod def register(cls, handler): diff --git a/src/robot/run.py b/src/robot/run.py index 22da49435a5..9d121b913a0 100755 --- a/src/robot/run.py +++ b/src/robot/run.py @@ -35,7 +35,7 @@ # Allows running as a script. __name__ check needed with multiprocessing: # https://github.com/robotframework/robotframework/issues/1137 if 'robot' not in sys.modules and __name__ == '__main__': - import pythonpathsetter + import pythonpathsetter # type: ignore[import] from robot.conf import RobotSettings from robot.model import ModelModifier diff --git a/src/robot/running/arguments/typeconverters.py b/src/robot/running/arguments/typeconverters.py index d21149634ce..aeff75533fb 100644 --- a/src/robot/running/arguments/typeconverters.py +++ b/src/robot/running/arguments/typeconverters.py @@ -12,14 +12,15 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +import builtins +import sys from ast import literal_eval from collections import abc, OrderedDict -try: +if sys.version_info >= (3, 10): from types import UnionType -except ImportError: # Python < 3.10 +else: UnionType = () -from typing import Union +from typing import Union, Tuple, Optional, Dict from datetime import datetime, date, timedelta from decimal import InvalidOperation, Decimal from enum import Enum @@ -31,13 +32,13 @@ class TypeConverter: - type = None - type_name = None - abc = None - aliases = () - value_types = (str,) - _converters = OrderedDict() - _type_aliases = {} + type: object = None + type_name: Optional[str] = None + abc: Optional[builtins.type] = None + aliases: Tuple[str, ...] = () + value_types: Tuple[builtins.type, ...] = (str,) + _converters: Dict[object, object] = OrderedDict() + _type_aliases: Dict[str, builtins.type] = {} def __init__(self, used_type): self.used_type = used_type diff --git a/src/robot/running/bodyrunner.py b/src/robot/running/bodyrunner.py index 4a458109e3b..15070d5b6fd 100644 --- a/src/robot/running/bodyrunner.py +++ b/src/robot/running/bodyrunner.py @@ -15,6 +15,7 @@ from collections import OrderedDict from contextlib import contextmanager +from typing import Any, List from robot.errors import (ExecutionFailed, ExecutionFailures, ExecutionPassed, ExecutionStatus, ExitForLoop, ContinueForLoop, DataError) @@ -69,7 +70,7 @@ def run(self, step, name=None): class IfRunner: - _dry_run_stack = [] + _dry_run_stack: List[Any] = [] def __init__(self, context, run=True, templated=False): self._context = context diff --git a/src/robot/running/builder/builders.py b/src/robot/running/builder/builders.py index e2c220c9395..a4cfa1d9867 100644 --- a/src/robot/running/builder/builders.py +++ b/src/robot/running/builder/builders.py @@ -12,8 +12,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - import os +from typing import List, Tuple, Union from robot.errors import DataError from robot.output import LOGGER @@ -46,8 +46,8 @@ class TestSuiteBuilder: :mod:`robot.api` package. """ - def __init__(self, included_suites=None, included_extensions=('robot',), - rpa=None, allow_empty_suite=False, process_curdir=True): + def __init__(self, included_suites: List[str] = None, included_extensions: Union[List[str], Tuple[str, ...]]=('robot',), + rpa: bool=None, allow_empty_suite: bool=False, process_curdir: bool=True): """ :param include_suites: List of suite names to include. If ``None`` or an empty list, diff --git a/src/robot/running/dynamicmethods.py b/src/robot/running/dynamicmethods.py index a3ad1a8d6e0..5c87384a42e 100644 --- a/src/robot/running/dynamicmethods.py +++ b/src/robot/running/dynamicmethods.py @@ -24,7 +24,7 @@ def no_dynamic_method(*args): class _DynamicMethod: - _underscore_name = NotImplemented + _underscore_name: str = NotImplemented def __init__(self, lib): self.method = self._get_method(lib) diff --git a/src/robot/running/model.py b/src/robot/running/model.py index da0215bc2e8..13dd9d025d7 100644 --- a/src/robot/running/model.py +++ b/src/robot/running/model.py @@ -47,13 +47,18 @@ from .randomizer import Randomizer from .statusreporter import StatusReporter +from typing import TYPE_CHECKING, List + +if TYPE_CHECKING: + from robot.result.executionresult import Result + class Body(model.Body): - __slots__ = [] + __slots__: List[str] = [] class IfBranches(model.IfBranches): - __slots__ = [] + __slots__: List[str] = [] @Body.register @@ -176,7 +181,7 @@ class TestSuite(model.TestSuite): test_class = TestCase #: Internal usage only. fixture_class = Keyword #: Internal usage only. - def __init__(self, name='', doc='', metadata=None, source=None, rpa=None): + def __init__(self, name:str='', doc: str='', metadata: str=None, source: str=None, rpa: str=None): model.TestSuite.__init__(self, name, doc, metadata, source, rpa) #: :class:`ResourceFile` instance containing imports, variables and #: keywords the suite owns. When data is parsed from the file system, @@ -184,7 +189,7 @@ def __init__(self, name='', doc='', metadata=None, source=None, rpa=None): self.resource = ResourceFile(source=source) @classmethod - def from_file_system(cls, *paths, **config): + def from_file_system(cls, *paths: str, **config) -> "TestSuite": """Create a :class:`TestSuite` object based on the given ``paths``. ``paths`` are file or directory paths where to read the data from. @@ -198,7 +203,7 @@ def from_file_system(cls, *paths, **config): return TestSuiteBuilder(**config).build(*paths) @classmethod - def from_model(cls, model, name=None): + def from_model(cls, model: object, name: str = None) -> "TestSuite": """Create a :class:`TestSuite` object based on the given ``model``. The model can be created by using the @@ -210,8 +215,8 @@ def from_model(cls, model, name=None): from .builder import RobotParser return RobotParser().build_suite(model, name) - def configure(self, randomize_suites=False, randomize_tests=False, - randomize_seed=None, **options): + def configure(self, randomize_suites: bool=False, randomize_tests: bool=False, + randomize_seed: object=None, **options: object) -> None: """A shortcut to configure a suite using one method call. Can only be used with the root test suite. @@ -233,7 +238,7 @@ def configure(self, randomize_suites=False, randomize_tests=False, model.TestSuite.configure(self, **options) self.randomize(randomize_suites, randomize_tests, randomize_seed) - def randomize(self, suites=True, tests=True, seed=None): + def randomize(self, suites: bool=True, tests: bool=True, seed: object=None): """Randomizes the order of suites and/or tests, recursively. :param suites: Boolean controlling should suites be randomized. @@ -243,7 +248,7 @@ def randomize(self, suites=True, tests=True, seed=None): """ self.visit(Randomizer(suites, tests, seed)) - def run(self, settings=None, **options): + def run(self, settings: RobotSettings=None, **options: object) -> "Result": """Executes the suite based based the given ``settings`` or ``options``. :param settings: :class:`~robot.conf.settings.RobotSettings` object diff --git a/src/robot/running/status.py b/src/robot/running/status.py index dc21efcbab9..73f6f973577 100644 --- a/src/robot/running/status.py +++ b/src/robot/running/status.py @@ -12,6 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import Optional from robot.errors import ExecutionStatus, PassExecution from robot.model import TagPatterns @@ -209,11 +210,11 @@ def _my_message(self): class _Message: - setup_message = NotImplemented - setup_skipped_message = NotImplemented - teardown_skipped_message = NotImplemented - teardown_message = NotImplemented - also_teardown_message = NotImplemented + setup_message: Optional[str] = NotImplemented + setup_skipped_message: Optional[str] = NotImplemented + teardown_skipped_message: Optional[str] = NotImplemented + teardown_message: Optional[str] = NotImplemented + also_teardown_message: Optional[str] = NotImplemented def __init__(self, status): self.failure = status.failure diff --git a/src/robot/running/timeouts/__init__.py b/src/robot/running/timeouts/__init__.py index 68d5e6067b2..6f55ea1dece 100644 --- a/src/robot/running/timeouts/__init__.py +++ b/src/robot/running/timeouts/__init__.py @@ -21,7 +21,7 @@ if WINDOWS: from .windows import Timeout else: - from .posix import Timeout + from .posix import Timeout # type: ignore[misc] class _Timeout(Sortable): diff --git a/src/robot/running/timeouts/posix.py b/src/robot/running/timeouts/posix.py index 51678be7542..e84b86eceaf 100644 --- a/src/robot/running/timeouts/posix.py +++ b/src/robot/running/timeouts/posix.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from signal import setitimer, signal, SIGALRM, ITIMER_REAL +from signal import setitimer, signal, SIGALRM, ITIMER_REAL # type: ignore[attr-defined] class Timeout: diff --git a/src/robot/testdoc.py b/src/robot/testdoc.py index 7b957453795..530320a0bb6 100755 --- a/src/robot/testdoc.py +++ b/src/robot/testdoc.py @@ -36,7 +36,7 @@ # Allows running as a script. __name__ check needed with multiprocessing: # https://github.com/robotframework/robotframework/issues/1137 if 'robot' not in sys.modules and __name__ == '__main__': - import pythonpathsetter + import pythonpathsetter # type: ignore[import] from robot.conf import RobotSettings from robot.htmldata import HtmlFileWriter, ModelWriter, JsonWriter, TESTDOC diff --git a/src/robot/tidy.py b/src/robot/tidy.py index a7d34e7a821..5ce7653bedc 100755 --- a/src/robot/tidy.py +++ b/src/robot/tidy.py @@ -35,7 +35,7 @@ # Allows running as a script. __name__ check needed with multiprocessing: # https://github.com/robotframework/robotframework/issues/1137 if 'robot' not in sys.modules and __name__ == '__main__': - import pythonpathsetter + import pythonpathsetter # type: ignore[import] from robot.errors import DataError from robot.parsing import get_model, SuiteStructureBuilder, SuiteStructureVisitor diff --git a/src/robot/utils/dotdict.py b/src/robot/utils/dotdict.py index 079de6d255c..dd202a06ad1 100644 --- a/src/robot/utils/dotdict.py +++ b/src/robot/utils/dotdict.py @@ -18,7 +18,7 @@ from .robottypes import is_dict_like -class DotDict(OrderedDict): +class DotDict(OrderedDict): # type: ignore[type-arg] def __init__(self, *args, **kwds): args = [self._convert_nested_initial_dicts(a) for a in args] diff --git a/src/robot/utils/etreewrapper.py b/src/robot/utils/etreewrapper.py index c73a9f89f6e..9eb77ec4c79 100644 --- a/src/robot/utils/etreewrapper.py +++ b/src/robot/utils/etreewrapper.py @@ -23,7 +23,7 @@ from xml.etree import cElementTree as ET except ImportError: try: - from xml.etree import ElementTree as ET + from xml.etree import ElementTree as ET # type: ignore[no-redef] except ImportError: raise ImportError('No valid ElementTree XML parser module found') diff --git a/src/robot/utils/normalizing.py b/src/robot/utils/normalizing.py index 2c9b1132baf..ddad3118ff4 100644 --- a/src/robot/utils/normalizing.py +++ b/src/robot/utils/normalizing.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from collections.abc import MutableMapping +from typing import MutableMapping import re from .robottypes import is_dict_like, is_unicode @@ -46,7 +46,7 @@ def normalize_whitespace(string): return re.sub(r'\s', ' ', string, flags=re.UNICODE) -class NormalizedDict(MutableMapping): +class NormalizedDict(MutableMapping[object, object]): """Custom dictionary implementation automatically normalizing keys.""" def __init__(self, initial=None, ignore=(), caseless=True, spaceless=True): diff --git a/src/robot/utils/restreader.py b/src/robot/utils/restreader.py index a3335da483c..e2d326263b9 100644 --- a/src/robot/utils/restreader.py +++ b/src/robot/utils/restreader.py @@ -21,9 +21,9 @@ from docutils.core import publish_doctree from docutils.parsers.rst import directives from docutils.parsers.rst import roles - from docutils.parsers.rst.directives import register_directive - from docutils.parsers.rst.directives.body import CodeBlock - from docutils.parsers.rst.directives.misc import Include + from docutils.parsers.rst.directives import register_directive # type: ignore[import] + from docutils.parsers.rst.directives.body import CodeBlock # type: ignore[import] + from docutils.parsers.rst.directives.misc import Include # type: ignore[import] except ImportError: raise DataError("Using reStructuredText test data requires having " "'docutils' module version 0.9 or newer installed.") diff --git a/src/robot/utils/robottypes.py b/src/robot/utils/robottypes.py index ee74909c1d7..6651d291df4 100644 --- a/src/robot/utils/robottypes.py +++ b/src/robot/utils/robottypes.py @@ -17,18 +17,21 @@ from collections import UserString from io import IOBase from os import PathLike -try: +import sys +from typing import Tuple +if sys.version_info >= (3, 8): from typing import TypedDict -except ImportError: # Python < 3.8 - typeddict_types = () + + typeddict_types: Tuple[type, ...] = (type(TypedDict('Dummy', {})),) else: - typeddict_types = (type(TypedDict('Dummy', {})),) + typeddict_types: Tuple[type, ...] = () + try: from typing_extensions import TypedDict as ExtTypedDict except ImportError: pass else: - typeddict_types += (type(ExtTypedDict('Dummy', {})),) + typeddict_types += (type(ExtTypedDict('Dummy', {})),) # type: ignore[operator] from .platform import PY_VERSION diff --git a/src/robot/variables/evaluation.py b/src/robot/variables/evaluation.py index 7ee73636608..699f6477855 100644 --- a/src/robot/variables/evaluation.py +++ b/src/robot/variables/evaluation.py @@ -15,9 +15,9 @@ import builtins import token -from collections.abc import Mapping from io import StringIO from tokenize import generate_tokens, untokenize +from typing import Mapping from robot.errors import DataError from robot.utils import get_error_message, type_name @@ -91,7 +91,7 @@ def _import_modules(module_names): return modules -class EvaluationNamespace(Mapping): +class EvaluationNamespace(Mapping[object, object]): def __init__(self, variable_store, namespace): self.namespace = namespace diff --git a/src/robot/variables/filesetter.py b/src/robot/variables/filesetter.py index a58f890878d..f8c07973995 100644 --- a/src/robot/variables/filesetter.py +++ b/src/robot/variables/filesetter.py @@ -18,7 +18,7 @@ try: import yaml except ImportError: - yaml = None + yaml = None # type: ignore[assignment] from robot.errors import DataError from robot.output import LOGGER diff --git a/src/robot/variables/tablesetter.py b/src/robot/variables/tablesetter.py index 12236d7e966..ae9a6dd1278 100644 --- a/src/robot/variables/tablesetter.py +++ b/src/robot/variables/tablesetter.py @@ -67,7 +67,7 @@ def resolve(self, variables): with self._avoid_recursion: return self._replace_variables(self._values, variables) - @property + @property # type: ignore[misc] @contextmanager def _avoid_recursion(self): if self._resolving: