diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7e0bc4a8..07853454 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,8 +28,6 @@ jobs: fail-fast: false matrix: include: - - python-version: '3.6' - os: ubuntu-20.04 - python-version: '3.10' os: ubuntu-22.04 - python-version: '3.11' diff --git a/.pylintrc b/.pylintrc index dc41d223..5b5816c0 100644 --- a/.pylintrc +++ b/.pylintrc @@ -65,6 +65,7 @@ disable=W0142,W0703,C0111,R0201,W0603,W0613,W0212,W0141, len-as-condition, no-else-return, raise-missing-from, + too-many-positional-arguments, too-many-branches, too-many-nested-blocks, too-many-statements, @@ -222,8 +223,9 @@ defining-attr-methods=__init__,__new__,setUp [DESIGN] -# Maximum number of arguments for function / method -max-args=100 +# Maximum number of arguments for function / method. +# defaults to: max-args=5 +max-args=10 # Argument names that match this expression will be ignored. Default to name # with leading underscore diff --git a/pyproject.toml b/pyproject.toml index 474a6e80..8edd0ba3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -198,7 +198,7 @@ exclude = [ extraPaths = ["stubs"] include = ["xcp", "tests"] pythonPlatform = "Linux" -pythonVersion = "3.6" +pythonVersion = "3.11" reportFunctionMemberAccess = true reportGeneralTypeIssues = "warning" reportOptionalMemberAccess = "warning" diff --git a/pytest.ini b/pytest.ini index e07456ee..0906991a 100644 --- a/pytest.ini +++ b/pytest.ini @@ -8,10 +8,11 @@ # These are the most of the needed pytest plugins, unfortunately this list does # not support ;python_version<=3.0 or ;python_version>3.0. Therefore, it can # only list plugins available for all tested python versions (2.7, 3.6 ... 3.11): +# pytest-localftpserver is also used, but its installation is not checked +# to to its installation not being detected on Ubuntu 24.04: required_plugins = pytest_httpserver pytest-forked - pytest-localftpserver pytest-pythonpath pytest-subprocess pytest-timeout diff --git a/stubs/pyfakefs/__init__.pyi b/stubs/pyfakefs/__init__.pyi deleted file mode 100644 index e69de29b..00000000 diff --git a/stubs/pyfakefs/fake_filesystem.pyi b/stubs/pyfakefs/fake_filesystem.pyi deleted file mode 100644 index 466eb8b1..00000000 --- a/stubs/pyfakefs/fake_filesystem.pyi +++ /dev/null @@ -1,151 +0,0 @@ -from _typeshed import Incomplete # pylint: disable=import-error - - -def set_uid(uid) -> None: ... -def set_gid(gid) -> None: ... -def reset_ids() -> None: ... -def is_root(): ... - - -class FakeFilesystem: - path_separator: Incomplete - alternative_path_separator: Incomplete - patcher: Incomplete - is_windows_fs: Incomplete - is_macos: Incomplete - is_case_sensitive: Incomplete - root: Incomplete - cwd: Incomplete - umask: Incomplete - open_files: Incomplete - mount_points: Incomplete - dev_null: Incomplete - def __init__( - self, - path_separator=..., - total_size: Incomplete | None = ..., - patcher: Incomplete | None = ..., - ) -> None: ... - @property - def is_linux(self): ... - def reset(self, total_size: Incomplete | None = ...) -> None: ... - def pause(self) -> None: ... - def resume(self) -> None: ... - def line_separator(self): ... - def raise_os_error( - self, errno, filename: Incomplete | None = ..., winerror: Incomplete | None = ... - ) -> None: ... - def raise_io_error(self, errno, filename: Incomplete | None = ...) -> None: ... - def add_mount_point(self, path, total_size: Incomplete | None = ...): ... - def get_disk_usage(self, path: Incomplete | None = ...): ... - def set_disk_usage(self, total_size, path: Incomplete | None = ...) -> None: ... - def change_disk_usage(self, usage_change, file_path, st_dev) -> None: ... - def stat(self, entry_path, follow_symlinks: bool = ...): ... - def chmod(self, path, mode, follow_symlinks: bool = ...) -> None: ... - def utime( - self, - path, - times: Incomplete | None = ..., - ns: Incomplete | None = ..., - follow_symlinks: bool = ..., - ) -> None: ... - def get_open_file(self, file_des): ... - def has_open_file(self, file_object): ... - def normcase(self, path): ... - def normpath(self, path): ... - def absnormpath(self, path): ... - def splitpath(self, path): ... - def splitdrive(self, path): ... - def joinpaths(self, *paths): ... - def ends_with_path_separator(self, file_path): ... - def is_filepath_ending_with_separator(self, path): ... - def exists(self, file_path, check_link: bool = ...): ... - def resolve_path(self, file_path, allow_fd: bool = ..., raw_io: bool = ...): ... - def get_object_from_normpath(self, file_path, check_read_perm: bool = ...): ... - def get_object(self, file_path, check_read_perm: bool = ...): ... - def resolve( - self, - file_path, - follow_symlinks: bool = ..., - allow_fd: bool = ..., - check_read_perm: bool = ..., - ): ... - def lresolve(self, path): ... - def add_object(self, file_path, file_object, error_fct: Incomplete | None = ...) -> None: ... - def rename(self, old_file_path, new_file_path, force_replace: bool = ...) -> None: ... - def remove_object(self, file_path) -> None: ... - def make_string_path(self, path): ... - def create_dir(self, directory_path, perm_bits=...): ... - def create_file( - self, - file_path, - st_mode=..., - contents: str = ..., - st_size: Incomplete | None = ..., - create_missing_dirs: bool = ..., - apply_umask: bool = ..., - encoding: Incomplete | None = ..., - errors: Incomplete | None = ..., - side_effect: Incomplete | None = ..., - ): ... - def add_real_file( - self, source_path, read_only: bool = ..., target_path: Incomplete | None = ... - ): ... - def add_real_symlink(self, source_path, target_path: Incomplete | None = ...): ... - def add_real_directory( - self, - source_path, - read_only: bool = ..., - lazy_read: bool = ..., - target_path: Incomplete | None = ..., - ): ... - def add_real_paths( - self, path_list, read_only: bool = ..., lazy_dir_read: bool = ... - ) -> None: ... - def create_file_internally( - self, - file_path, - st_mode=..., - contents: str = ..., - st_size: Incomplete | None = ..., - create_missing_dirs: bool = ..., - apply_umask: bool = ..., - encoding: Incomplete | None = ..., - errors: Incomplete | None = ..., - read_from_real_fs: bool = ..., - raw_io: bool = ..., - side_effect: Incomplete | None = ..., - ): ... - def create_symlink(self, file_path, link_target, create_missing_dirs: bool = ...): ... - def link(self, old_path, new_path, follow_symlinks: bool = ...): ... - def readlink(self, path): ... - def makedir(self, dir_name, mode=...) -> None: ... - def makedirs(self, dir_name, mode=..., exist_ok: bool = ...) -> None: ... - def isdir(self, path, follow_symlinks: bool = ...): ... - def isfile(self, path, follow_symlinks: bool = ...): ... - def islink(self, path): ... - def confirmdir(self, target_directory): ... - def remove(self, path) -> None: ... - def rmdir(self, target_directory, allow_symlink: bool = ...) -> None: ... - def listdir(self, target_directory): ... - -class FakeFileOpen: - __name__: str - filesystem: Incomplete - raw_io: Incomplete - def __init__( - self, filesystem, delete_on_close: bool = ..., use_io: bool = ..., raw_io: bool = ... - ) -> None: ... - def __call__(self, *args, **kwargs): ... - def call( - self, - file_, - mode: str = ..., - buffering: int = ..., - encoding: Incomplete | None = ..., - errors: Incomplete | None = ..., - newline: Incomplete | None = ..., - closefd: bool = ..., - opener: Incomplete | None = ..., - open_modes: Incomplete | None = ..., - ): ... diff --git a/stubs/pyfakefs/fake_filesystem_unittest.pyi b/stubs/pyfakefs/fake_filesystem_unittest.pyi deleted file mode 100644 index aade18b0..00000000 --- a/stubs/pyfakefs/fake_filesystem_unittest.pyi +++ /dev/null @@ -1,20 +0,0 @@ -from types import ModuleType -from typing import Any, Callable, Dict, List, Optional, Union - -from _typeshed import Incomplete # pylint: disable=import-error - -Patcher = Incomplete -PatchMode = Incomplete - -def patchfs( - _func: Optional[Incomplete] = ..., - *, - additional_skip_names: Optional[List[Union[str, ModuleType]]] = ..., - modules_to_reload: Optional[List[ModuleType]] = ..., - modules_to_patch: Optional[Dict[str, ModuleType]] = ..., - allow_root_user: bool = ..., - use_known_patches: bool = ..., - patch_open_code: PatchMode = ..., - patch_default_args: bool = ..., - use_cache: bool = ..., -) -> Callable[[Any], Any]: ... diff --git a/stubs/pytest.pyi b/stubs/pytest.pyi deleted file mode 100644 index 6e31256b..00000000 --- a/stubs/pytest.pyi +++ /dev/null @@ -1,8 +0,0 @@ -# pylint: disable=reimported,no-name-in-module,unused-import,function-redefined.redefined-builtin -from _pytest.python_api import raises -from _typeshed import Incomplete as fixture -from _typeshed import Incomplete as mark - -def skip(msg: str = "", *, allow_module_level: bool = False): ... - -__all__ = ["mark", "fixture", "skip", "raises"] diff --git a/stubs/werkzeug/wrappers.pyi b/stubs/werkzeug/wrappers.pyi deleted file mode 100644 index f4a46d17..00000000 --- a/stubs/werkzeug/wrappers.pyi +++ /dev/null @@ -1,3 +0,0 @@ -from _typeshed import Incomplete # pylint: disable=import-error,no-name-in-module -Request = Incomplete -Response = Incomplete diff --git a/tests/httpserver_testcase.py b/tests/httpserver_testcase.py index b9dc5ceb..cad59524 100644 --- a/tests/httpserver_testcase.py +++ b/tests/httpserver_testcase.py @@ -1,4 +1,5 @@ import os +import sys import unittest from typing import Callable, Optional @@ -11,9 +12,10 @@ from pytest_httpserver import HTTPServer from werkzeug.wrappers import Request, Response - ErrorHandler = Optional[Callable[[Request], Response]] + ErrorHandler = Optional[Callable[[Request], Response | None]] except ImportError: pytest.skip(allow_module_level=True) + sys.exit(0) # Let pyright know that this is a dead end class HTTPServerTestCase(unittest.TestCase): @@ -31,7 +33,7 @@ def tearDownClass(cls): @classmethod def serve_file(cls, root, file_path, error_handler=None, real_path=None): - # type:(str, str, Optional[Callable[[Request], Response]], Optional[str]) -> None + # type:(str, str, ErrorHandler, Optional[str]) -> None """Expect a GET request and handle it using the local pytest_httpserver.HTTPServer""" def handle_get(request): diff --git a/tests/test_accessor.py b/tests/test_accessor.py index 93f5d609..885d5762 100644 --- a/tests/test_accessor.py +++ b/tests/test_accessor.py @@ -1,14 +1,16 @@ import unittest - -from pyfakefs.fake_filesystem import FakeFilesystem +from typing import TYPE_CHECKING import xcp.accessor from .test_mountingaccessor import check_binary_read, check_binary_write +if TYPE_CHECKING: + import pyfakefs + def test_file_accessor(fs): - # type:(FakeFilesystem) -> None + # type(pyfakefs.fake_filesystem.FakeFilesystem) -> None """Test FileAccessor.writeFile(), .openAddress and .access using pyfakefs""" accessor = xcp.accessor.createAccessor("file://repo/", False) assert isinstance(accessor, xcp.accessor.FileAccessor) @@ -18,7 +20,7 @@ def test_file_accessor(fs): class TestAccessor(unittest.TestCase): def setUp(self): - """Provide the refrence content of the repo/.treeinfo file for check_repo_access()""" + """Provide the reference content of the repo/.treeinfo file for check_repo_access()""" with open("tests/data/repo/.treeinfo", "rb") as dot_treeinfo: self.reference_treeinfo = dot_treeinfo.read() @@ -35,6 +37,7 @@ def check_repo_access(self, a): self.assertFalse(a.access('no_such_file')) self.assertEqual(a.lastError, 404) a.finish() + a.finish() # Cover the code handing a 2nd call of accessor.finish() def test_filesystem_accessor_access(self): """Test FilesystemAccessor.access()""" diff --git a/tests/test_ftpaccessor.py b/tests/test_ftpaccessor.py index a5b0b303..340b759c 100644 --- a/tests/test_ftpaccessor.py +++ b/tests/test_ftpaccessor.py @@ -21,13 +21,14 @@ from io import BytesIO import pytest -import pytest_localftpserver # pylint: disable=unused-import # Ensure that it is installed +import pytest_localftpserver # Ensure that it is installed from six import ensure_binary, ensure_str import xcp.accessor binary_data = b"\x80\x91\xaa\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xcc\xdd\xee\xff" text_data = "✋➔Hello Accessor from the 🗺, download and verify ✅ me!" +assert pytest_localftpserver def upload_textfile(ftpserver, accessor): diff --git a/tests/test_ifrename_logic.py b/tests/test_ifrename_logic.py index 5d445f3c..f6966054 100644 --- a/tests/test_ifrename_logic.py +++ b/tests/test_ifrename_logic.py @@ -1,4 +1,4 @@ -# pyright: reportGeneralTypeIssues=false +# pyright: reportGeneralTypeIssues=false,reportAttributeAccessIssue=false # pytype: disable=attribute-error from __future__ import print_function from __future__ import unicode_literals diff --git a/tests/test_logger.py b/tests/test_logger.py index af8cd5c3..a8bb9bfd 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -31,3 +31,4 @@ def test_openLog_mock_stdin(): assert openLog("test.log") is True os.close(slave_fd) os.close(master_fd) + open_mock.assert_called_once_with("test.log", "a", **open_utf8) diff --git a/tests/test_mountingaccessor.py b/tests/test_mountingaccessor.py index 9dea2d3f..54bb6303 100644 --- a/tests/test_mountingaccessor.py +++ b/tests/test_mountingaccessor.py @@ -20,6 +20,7 @@ import pytest pytest.skip(allow_module_level=True) + sys.exit(0) # Let pyright know that this is a dead end binary_data = b"\x00\x1b\x5b\x95\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xcc\xdd\xee\xff" diff --git a/tests/test_pci.py b/tests/test_pci.py index 1b723538..d3278507 100644 --- a/tests/test_pci.py +++ b/tests/test_pci.py @@ -11,6 +11,8 @@ if sys.version_info >= (3, 6): from pytest_subprocess.fake_process import FakeProcess +else: + pytest.skip(allow_module_level=True) class TestInvalid(unittest.TestCase): @@ -31,6 +33,7 @@ class TestValid(unittest.TestCase): def test_null_with_segment(self): + assert PCI.is_valid("0000:00:00.0") is True c = PCI("0000:00:00.0") self.assertEqual(c.segment, 0) @@ -43,6 +46,7 @@ def test_null_with_segment(self): def test_null_without_segment(self): + assert PCI.is_valid("00:00.0") is True c = PCI("00:00.0") self.assertEqual(c.segment, 0) @@ -54,6 +58,7 @@ def test_null_without_segment(self): def test_valid(self): + assert PCI.is_valid("8765:43:1f.3") is True c = PCI("8765:43:1f.3") self.assertEqual(c.segment, 0x8765) @@ -61,14 +66,31 @@ def test_valid(self): self.assertEqual(c.device, 0x1f) self.assertEqual(c.function, 0x3) + def test_valid_index(self): + + assert PCI.is_valid("8765:43:1f.3[0]") is True + assert PCI.is_valid("1234:56:01.7[1]") is True + c = PCI("1234:56:01.7[1]") + + self.assertEqual(c.segment, 0x1234) + self.assertEqual(c.bus, 0x56) + self.assertEqual(c.device, 0x01) + self.assertEqual(c.function, 0x7) + self.assertEqual(c.index, 0x1) + def test_equality(self): self.assertEqual(PCI("0000:00:00.0"), PCI("00:00.0")) + assert PCI("1234:56:01.7[1]") != PCI("1234:56:01.7[2]") + assert PCI("1234:56:01.2") >= PCI("1234:56:01.2") + assert PCI("1234:56:01.1") <= PCI("1234:56:01.2") + assert PCI("1234:56:01.3") > PCI("1234:56:01.2") + assert PCI("1234:56:01.1") < PCI("1234:56:02.2") if sys.version_info >= (2, 7): def assert_videoclass_devices(ids, devs): # type: (PCIIds, PCIDevices) -> None - """Verification function for checking the otuput of PCIDevices.findByClass()""" + """Verification function for checking the output of PCIDevices.findByClass()""" video_class = ids.lookupClass('Display controller') assert video_class == ["03"] sorted_devices = sorted(devs.findByClass(video_class), @@ -76,6 +98,7 @@ def assert_videoclass_devices(ids, devs): # type: (PCIIds, PCIDevices) -> None # Assert devs.findByClass() finding 3 GPUs from tests/data/lspci-mn in our mocked PCIIds DB: assert len(sorted_devices) == 3 + assert len(devs.findByClass("03", "80")) == 2 # For each of the found devices, assert these expected values: for (video_dev, @@ -164,6 +187,7 @@ def test_videoclass_by_mock_calls(fp): def mock_lspci_using_open_testfile(fp): """Mock xcp.pci.PCIDevices.Popen() using open(tests/data/lspci-mn)""" with open("tests/data/lspci-mn", "rb") as fake_data: - assert isinstance(fp, FakeProcess) + if sys.version_info >= (3, 6): + assert isinstance(fp, FakeProcess) fp.register_subprocess(["lspci", "-mn"], stdout=fake_data.read()) return PCIDevices() diff --git a/tox.ini b/tox.ini index d7386335..5db78e4e 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ # 2. python 3.6 test and pylint warnings from changed lines # 3. pytype (needs Python 3.8 for best results) # 4. pyre and pyright checks, pytest test report as markdown for GitHub Actions summary -envlist = py38-covcombine-check, py36-lint-test, py310-pytype, py311-pyre-mdreport +envlist = py38-covcombine-check, py311-lint-test, py310-pytype, py311-pyre-mdreport isolated_build = true skip_missing_interpreters = true requires = @@ -28,9 +28,9 @@ commands = # https://github.com/actions/toolkit/blob/main/docs/commands.md#problem-matchers echo "::add-matcher::.github/workflows/PYTHONWARNINGS-problemMatcher.json" pytest --cov -v --new-first -x --show-capture=all -rA - sh -c 'ls -l {env:COVERAGE_FILE}' sh -c 'if [ -n "{env:PYTEST_MD_REPORT_OUTPUT}" -a -n "{env:GITHUB_STEP_SUMMARY}" ];then \ - sed -i "s/tests\(.*py\)/[&](&)/" {env:PYTEST_MD_REPORT_OUTPUT}; sed "/title/,/\/style/d" \ + mkdir -p $(dirname "{env:GITHUB_STEP_SUMMARY:.git/sum.md}"); \ + sed "s/tests\(.*py\)/[&](&)/" \ {env:PYTEST_MD_REPORT_OUTPUT} >{env:GITHUB_STEP_SUMMARY:.git/sum.md};fi' [testenv] @@ -202,8 +202,6 @@ max-line-length = 129 [pyre] commands = - pyre: python3.11 --version -V # Needs py311-pyre, does not work with py310-pyre - python pyre_runner.py -pyright [pytype] diff --git a/xcp/accessor.py b/xcp/accessor.py index 9583dfe7..0ceec1c1 100644 --- a/xcp/accessor.py +++ b/xcp/accessor.py @@ -315,7 +315,8 @@ def finish(self): self.cleanup = False self.ftp = None - def access(self, path): # pylint: disable=arguments-differ,arguments-renamed + # pylint: disable-next=arguments-differ,arguments-renamed + def access(self, path): # pyright: ignore[reportIncompatibleMethodOverride] try: logger.debug("Testing "+path) self._cleanup()