diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8ddf02bc..51ff1d88 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,40 @@ -# Installation as pre-commit hook: -# $ pip install pre-commit +# +# This is the configuration file of the pre-commit framework for this repository: +# https://pypi.org/project/pre-commit +# +# pre-commit runs in the GitHub Workflow of this project on each push and PR. +# Additionally, you can run it locally for faster fixing of issues using +# $ pip3 install pre-commit -r requirements-dev.txt +# +# On the initial run, pre-commit downloads its checkers, subsequent runs are fast: +# +# $ pre-commit run # automatically checks which checks are needed. +# $ pre-commit run -a # runs all fixes and checks, even when not needed +# +# When this works, you can enable it as the pre-commit hook for this git clone: # $ pre-commit install +# $ pre-commit install --hook-type pre-push +# # Global installation as a git-template (for new clones): # $ git config --global init.templateDir ~/.git-template # $ pre-commit init-templatedir ~/.git-template -# See: https://pre-commit.com/#automatically-enabling-pre-commit-on-repositories -# All hooks: https://pre-commit.com/hooks.html +# +# You can skip checks if you commit very often you don't want them to run, e.g: +# export SKIP=mypy,pylint;git commit -m "quick save" (or for --amend) +# +# For more customisation, see https://pre-commit.com/#temporarily-disabling-hooks +# and https://pre-commit.com/#confining-hooks-to-run-at-certain-stages (e.g push) +# +# After this, the pre-commit fixes and checks run when you commit an update. +# +# Install pre-commit as a global pre-commit hook(for all repos with a config): +# $ git config --global init.templateDir ~/.git-template +# $ pre-commit init-templatedir ~/.git-template +# +# Detailed Usage information for all possible situations: +# https://github.com/pre-commit/pre-commit.com/blob/main/sections/advanced.md +# Cheat sheet on the config file keywords: +# https://github.com/dexpota/cheatsheets/blob/master/pre-commit exclude: "^tests/data" fail_fast: true default_stages: [commit] @@ -40,6 +69,7 @@ repos: pass_filenames: true - id: shellcheck - id: mdformat-check + exclude: README-Unicode.md - repo: https://github.com/pycqa/pylint rev: v2.17.4 hooks: @@ -47,10 +77,9 @@ repos: args: [ -sn, # Don't display the score - --rcfile=pylintrc, --load-plugins=pylint.extensions.eq_without_hash, --ignore-imports=yes, - "--disable=duplicate-code,superfluous-parens,line-too-long", + "--disable=duplicate-code,line-too-long", ] log_file: ".git/pre-commit-pylint.log" additional_dependencies: @@ -58,11 +87,13 @@ repos: - six - mock - pandas + - pytest_forked + - toml - repo: local hooks: - id: run-pyre name: run-pyre - entry: python run-pyre.py + entry: python pyre_runner.py types: [python] language: python log_file: ".git/pre-commit-pyre.log" diff --git a/pylintrc b/.pylintrc similarity index 97% rename from pylintrc rename to .pylintrc index 4a3abe4d..dc41d223 100644 --- a/pylintrc +++ b/.pylintrc @@ -6,7 +6,7 @@ # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). # For absolute sys.path, use this for getting our stubs path dynamically: -init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.dirname(find_pylintrc()) + '/stubs')" +init-hook="import os,sys;sys.path.append(os.path.dirname('stubs'))" # Profiled execution. profile=no @@ -50,7 +50,7 @@ load-plugins= # disable=W0142,W0703,C0111,R0201,W0603,W0613,W0212,W0141, bad-option-value, # Skip complaining about suppressions for older option values - unrecognized-option, # Skip complaining on pylintrc options only in pylint2/pylint3 + unrecognized-option, # Skip complaining on .pylintrc options only in pylint2/pylint3 unknown-option-value, # Skip complaining about checkers only in pylint2/pylint3 useless-object-inheritance, # "object" is not obsolete for supporting Python2 super-with-arguments, # super() with arguments is a Python3-only feature @@ -61,6 +61,7 @@ disable=W0142,W0703,C0111,R0201,W0603,W0613,W0212,W0141, consider-using-dict-items, consider-using-enumerate, consider-using-ternary, + duplicate-code, len-as-condition, no-else-return, raise-missing-from, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bf0f1b7b..910602de 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,10 +21,10 @@ Installation of additional python versions for testing different versions: - If `deadsnakes/ppa` does not work, e.g. for Python 3.6, `conda` or `pyenv` can be used. For instructions, see https://realpython.com/intro-to-pyenv: ```yaml - sudo apt install -y build-essential xz-utils zlib1g-dev + sudo apt install -y build-essential xz-utils zlib1g-dev \ lib{bz2,ffi,lzma,readline,ssl,sqlite3}-dev curl https://pyenv.run | bash # add displayed commands to .bashrc - pyenv install 3.{6,8,11} && pyenv local 3.{6,8,11} # builds them + ~/.pyenv/bin/pyenv install 3.{6,8,11} && ~/.pyenv/bin/pyenv local 3.{6,8,11} # builds them ``` - For testing on newer Ubuntu which has `python2-dev`, but not `pip2`, install `pip2` this way: ```yml @@ -37,7 +37,7 @@ You may want to install `pytype` in your user environment to run it directly wit # On Python != 3.8, pytype can't import xml.dom.minidom, use 3.8: python3.8 -m pip install pytype python -m pip install tabulate -./run-pytype.py +./pytype_runner.py ``` ## Installation of dependencies using `pip` @@ -79,8 +79,8 @@ Using pip-tools, you can extract the requirements and extras from `pyptoject.tom ```bash PYTHON=python3.10 EXTRAS=.,test,mypy,pyre,pytype,tox -PFLAGS="--no-warn-conflicts --resolver=backtracking" -$PYTHON -m pip install pip-tools +PFLAGS="--no-warn-conflicts" +$PYTHON -m pip install pip-tools==7.3.0 $PYTHON -m piptools compile --extra=$EXTRAS -o - pyproject.toml | $PYTHON -m pip install -r /dev/stdin $PFLAGS ``` diff --git a/Makefile b/Makefile index d7e21a87..d760a220 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ $(MY_OUTPUT_DIR)/python-libs.inc: $(MY_OUTPUT_DIR)/.dirstamp Makefile .PHONY: pylint pylint: - run-pylint.sh $(PYTHON_LIBS_SOURCES) + pylint_runner.sh $(PYTHON_LIBS_SOURCES) .PHONY: sources sources: $(RPM_SRPMSDIR)/$(PYTHON_LIB_SRPM) diff --git a/README.md b/README.md index 74b81430..2f24022a 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,13 @@ The Continuous Integration Tests feature: - Automatic Upload of the combined coverage to CodeCov (from the GitHub Workflow) - Checking of the combined coverage against the diff to master: Fails if changes are not covered! - Pylint report in the GitHub Action Summary page, with Warning and Error annotatios, even in the code review. -- Check that changes don't generate pylint warnings (if warning classes which are enabled in pylintrc) +- Check that changes don't generate pylint warnings (if warning classes which are enabled in .pylintrc) - Static analysis using `mypy`, `pyre` and `pytype` This enforces that any change (besides whitespace): - has code coverage and -- does not introduce a `pylint` warning which is not disabled in `pylintrc` +- does not introduce a `pylint` warning which is not disabled in `.pylintrc` - does not introduce a type of static analysis warning which is currently suppressed. ## Status Summary @@ -54,7 +54,7 @@ open a recent workflow run the latest and scroll down until you see the tables! - `pytest.ini`: The defaults used by `pytest` unless overruled by command line options - `.github/workflows/main.yml`: Configuration of the GitHub CI matrix jobs and coverage upload - `.github/act-serial.yaml`: Configuration for the jobs run by the local GitHub actions runner `act` -- `pylintrc`: Configuration file of `Pylint` +- `.pylintrc`: Configuration file of `Pylint` ## Installation and Setup of a development environment @@ -71,8 +71,8 @@ For the installation of the general development dependencies, visit [INSTALL.md] issues by Python3.6 - Test with `python2.7 -m pytest` - Run `mypy` (without any arguments - The configuration is in `pyproject.toml`) -- Run `./run-pytype.py` -- Run `./run-pyre.py` +- Run `./pytype_runner.py` +- Run `./pyre_runner.py` - Run `tox -e py36-lint` and fix any `Pylint` warnings - Run `tox -e py310-covcombine-check` and fix any missing diff-coverage. - Run `tox` for the full CI test suite diff --git a/run-pylint.py b/pylint_runner.py similarity index 97% rename from run-pylint.py rename to pylint_runner.py index a68fa55b..b013f66f 100755 --- a/run-pylint.py +++ b/pylint_runner.py @@ -32,12 +32,11 @@ from io import StringIO from typing import List, TextIO +import pandas as pd # type: ignore[import] from pylint.lint import Run # type: ignore[import] from pylint.reporters import JSONReporter # type: ignore[import] from toml import load -import pandas as pd # type: ignore[import] - def del_dict_keys(r, *args): for arg in args: @@ -108,11 +107,11 @@ def cleanup_results_dict(r, sym): # https://pylint.readthedocs.io/en/latest/user_guide/messages/warning/eq-without-hash.html # pylint_options: List[str] = [ - "--load-plugins", "pylint.extensions.eq_without_hash", + "--load-plugins", "pylint.extensions.eq_without_hash" ] -def pylint_project(check_dirs: List[str], errorlog: TextIO, branch_url: str): +def pylint_project(check_dirs: List[str], errorlog: TextIO, branch_url: str): pylint_overview = [] pylint_results = [] pylint_paths = [] @@ -125,13 +124,13 @@ def pylint_project(check_dirs: List[str], errorlog: TextIO, branch_url: str): smells_total = 0 for path in pylint_paths: filename = path.rsplit("/", maxsplit=1)[-1] - if filename in ["__init__.py", "pylintrc"]: + if filename in ["__init__.py", ".pylintrc"]: continue reporter_buffer = StringIO() results = Run( [path] + pylint_options, reporter=JSONReporter(reporter_buffer), - do_exit=False, + exit=False, ) score = results.linter.stats.global_note file_results = json.loads(reporter_buffer.getvalue()) @@ -169,11 +168,11 @@ def pylint_project(check_dirs: List[str], errorlog: TextIO, branch_url: str): message_ids[sym] = "" # Populate a dict of the pylint message symbols seen in this file # https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-notice-message - endline = r['endLine'] + endline = r["endLine"] end = f",endLine={endline}" if endline and endline != lineno else "" print( f"::{cls} file={path},line={lineno}{end}," - f"title=pylint {msg_id}: {sym}::{msg}" + f"title=pylint {msg_id}: {sym}::{msg}", ) r["path"] = f"[{linktext}]({branch_url}/{path}#L{lineno})" cleanup_results_dict(r, sym) diff --git a/pyproject.toml b/pyproject.toml index b2bacaee..36007f45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,7 +108,7 @@ build-backend = "setuptools.build_meta" packages = ["xcp", "xcp.net", "xcp.net.ifrename"] [tool.black] -line-length = 100 +line-length = 96 [tool.mypy] pretty = true diff --git a/run-pyre.py b/pyre_runner.py similarity index 100% rename from run-pyre.py rename to pyre_runner.py diff --git a/run-pytype.py b/pytype_runner.py similarity index 98% rename from run-pytype.py rename to pytype_runner.py index b7e41f35..3ee62e98 100755 --- a/run-pytype.py +++ b/pytype_runner.py @@ -137,6 +137,7 @@ def run_pytype_and_parse_annotations(xfail_files: List[str], branch_url: str): print("No errors in", xfail_file) return err_code or len(results), results + def to_markdown(me, fp, returncode, results, branch_url): mylink = f"[{me}]({branch_url}/{me}.py)" pytype_link = "[pytype](https://google.github.io/pytype)" @@ -162,7 +163,7 @@ def setup_and_run_pytype_action(scriptname: str): branch = os.environ.get("GITHUB_HEAD_REF", None) or os.environ.get("GITHUB_REF_NAME", None) filelink_baseurl = f"{server_url}/{repository}/blob/{branch}" retcode, results = run_pytype_and_parse_annotations(xfail_files, filelink_baseurl) - # Write the panda dable to a markdown output file: + # Write the panda dable to a markdown output file: summary_file = os.environ.get("GITHUB_STEP_SUMMARY", None) if summary_file: with open(summary_file, "w", encoding="utf-8") as fp: @@ -174,4 +175,4 @@ def setup_and_run_pytype_action(scriptname: str): if __name__ == "__main__": script_basename = os.path.basename(__file__).split(".")[0] basicConfig(format=script_basename + ": %(message)s", level=INFO) - sys.exit(setup_and_run_pytype_action(scriptname = script_basename)) + sys.exit(setup_and_run_pytype_action(scriptname=script_basename)) diff --git a/tests/conftest.py b/tests/conftest.py index cda853af..1a2a4974 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,9 +6,10 @@ """ # pyre-ignore-all-errors[21] -import pytest # pyre does not find the module when run by tox -e py311-pyre import warnings +import pytest # pyre does not find the module when run by tox -e py311-pyre + @pytest.fixture(autouse=True) def set_warnings(): """ diff --git a/tests/test_accessor.py b/tests/test_accessor.py index ef2175da..93f5d609 100644 --- a/tests/test_accessor.py +++ b/tests/test_accessor.py @@ -11,6 +11,7 @@ def test_file_accessor(fs): # type:(FakeFilesystem) -> None """Test FileAccessor.writeFile(), .openAddress and .access using pyfakefs""" accessor = xcp.accessor.createAccessor("file://repo/", False) + assert isinstance(accessor, xcp.accessor.FileAccessor) check_binary_read(accessor, "/repo", fs) check_binary_write(accessor, "/repo", fs) diff --git a/tests/test_cpiofile.py b/tests/test_cpiofile.py index 6169da16..967fe71f 100644 --- a/tests/test_cpiofile.py +++ b/tests/test_cpiofile.py @@ -30,9 +30,10 @@ def test_cpiofile_modes(fs): assert exc_info.type == TypeError if sys.version_info < (3, 0): # Test Python2 pattern from host-upgrade-plugin:/etc/xapi.d/plugins/prepare_host_upgrade.py - import StringIO + # Import the Python2-only StringIO.StringIO module, imported as Py2StringIO: + from StringIO import Py2StringIO # pylint: disable=import-outside-toplevel - stringio = StringIO.StringIO() + stringio = Py2StringIO() archive = CpioFile.open(fileobj=stringio, mode="w|gz") # type: ignore[arg-type] archive.hardlinks = False archive.close() diff --git a/tests/test_httpaccessor.py b/tests/test_httpaccessor.py index a7357619..1af4bf75 100644 --- a/tests/test_httpaccessor.py +++ b/tests/test_httpaccessor.py @@ -3,15 +3,16 @@ import base64 import sys from contextlib import contextmanager -from typing import IO, Generator, Tuple +from io import BufferedReader, TextIOWrapper +from typing import Generator, Tuple from six.moves import urllib # pyright: ignore -from xcp.accessor import HTTPAccessor, createAccessor +from xcp.accessor import createAccessor, HTTPAccessor from .httpserver_testcase import ErrorHandler, HTTPServerTestCase, Response -HTTPAccessorGenerator = Generator[Tuple[HTTPAccessor, IO[bytes]], None, None] +HTTPAccessorGenerator = Generator[Tuple[HTTPAccessor, BufferedReader], None, None] UTF8TEXT_LITERAL = "✋Hello accessor from the 🗺, download and verify me! ✅" @@ -22,6 +23,7 @@ class HTTPAccessorTestCase(HTTPServerTestCase): def test_404(self): self.httpserver.expect_request("/404").respond_with_data("", status=404) httpaccessor = createAccessor(self.httpserver.url_for("/"), True) + assert isinstance(httpaccessor, HTTPAccessor) self.assertFalse(httpaccessor.access("404")) self.httpserver.check_assertions() self.assertEqual(httpaccessor.lastError, 404) @@ -33,7 +35,7 @@ def http_get_request_data(self, url, read_file, error_handler): self.serve_file(self.document_root, read_file, error_handler) httpaccessor = createAccessor(url, True) - self.assertEqual(type(httpaccessor), HTTPAccessor) + assert isinstance(httpaccessor, HTTPAccessor) with open(self.document_root + read_file, "rb") as ref: yield httpaccessor, ref @@ -98,5 +100,7 @@ def test_httpaccessor_open_text(self): """Get text containing UTF-8 and compare the returned decoded string contents""" self.httpserver.expect_request("/textfile").respond_with_data(UTF8TEXT_LITERAL) accessor = createAccessor(self.httpserver.url_for("/"), True) + assert isinstance(accessor, HTTPAccessor) with accessor.openText("textfile") as textfile: + assert isinstance(textfile, TextIOWrapper) assert textfile.read() == UTF8TEXT_LITERAL diff --git a/tests/test_ifrename_dynamic.py b/tests/test_ifrename_dynamic.py index e101f4f5..282912b4 100644 --- a/tests/test_ifrename_dynamic.py +++ b/tests/test_ifrename_dynamic.py @@ -20,9 +20,8 @@ def setUp(self): openLog(self.logbuf, logging.NOTSET) def tearDown(self): - - closeLogs() self.logbuf.close() + closeLogs() def test_null(self): diff --git a/tests/test_mountingaccessor.py b/tests/test_mountingaccessor.py index 601b8f62..2f3a39ae 100644 --- a/tests/test_mountingaccessor.py +++ b/tests/test_mountingaccessor.py @@ -1,7 +1,7 @@ """pytest tests testing subclasses of xcp.accessor.MountingAccessor using pyfakefs""" import sys from io import BytesIO -from typing import cast +from typing import TYPE_CHECKING, cast from mock import patch from pyfakefs.fake_filesystem import FakeFileOpen, FakeFilesystem @@ -13,6 +13,9 @@ if sys.version_info >= (3, 6): from pytest_subprocess.fake_process import FakeProcess + + if TYPE_CHECKING: + from typing_extensions import Literal else: import pytest @@ -36,6 +39,8 @@ def test_device_accessor(fs, fp): expect(fp, [b"/bin/mount", b"-t", b"iso9660", b"-o", b"ro", b"/dev/device", b"/tmp"]) accessor = xcp.accessor.createAccessor("dev:///dev/device", False) + + assert isinstance(accessor, xcp.accessor.MountingAccessorTypes) check_mounting_accessor(accessor, fs, fp) @@ -53,13 +58,15 @@ def test_nfs_accessor(fs, fp): ] expect(fp, mount) accessor = xcp.accessor.createAccessor("nfs://server/path", False) + assert isinstance(accessor, xcp.accessor.NFSAccessor) check_mounting_accessor(accessor, fs, fp) def check_mounting_accessor(accessor, fs, fp): - # type: (xcp.accessor.MountingAccessor, FakeFilesystem, FakeProcess) -> None + # type: (Literal[False] | xcp.accessor.Mount, FakeFilesystem, FakeProcess) -> None """Test subclasses of MountingAccessor (with xcp.cmd.runCmd in xcp.mount mocked)""" + assert isinstance(accessor, xcp.accessor.MountingAccessorTypes) with patch("tempfile.mkdtemp") as tempfile_mkdtemp: tempfile_mkdtemp.return_value = "/tmp" accessor.start() @@ -89,9 +96,10 @@ def check_mounting_accessor(accessor, fs, fp): def check_binary_read(accessor, location, fs): - # type: (xcp.accessor.MountingAccessor, str, FakeFilesystem) -> bool - """Test the openAddress() method of subclasses of xcp.accessor.MountingAccessor""" + # type: (Literal[False] | xcp.accessor.AnyAccessor, str, FakeFilesystem) -> bool + """Test the openAddress() method of different types of local Accessor classes""" + assert isinstance(accessor, xcp.accessor.LocalTypes) name = "binary_file" path = location + "/" + name @@ -109,9 +117,10 @@ def check_binary_read(accessor, location, fs): def check_binary_write(accessor, location, fs): - # type: (xcp.accessor.MountingAccessor, str, FakeFilesystem) -> bool - """Test the writeFile() method of subclasses of xcp.accessor.MountingAccessor""" + # type: (Literal[False] | xcp.accessor.AnyAccessor, str, FakeFilesystem) -> bool + """Test the writeFile() method of different types of local Accessor classes""" + assert isinstance(accessor, xcp.accessor.LocalTypes) name = "binary_file_written_by_accessor" accessor.writeFile(BytesIO(binary_data), name) @@ -122,8 +131,10 @@ def check_binary_write(accessor, location, fs): def open_text(accessor, location, fs, text): - # type: (xcp.accessor.MountingAccessor, str, FakeFilesystem, str) -> str + # type: (Literal[False] | xcp.accessor.AnyAccessor, str, FakeFilesystem, str) -> str """Test the openText() method of subclasses of xcp.accessor.MountingAccessor""" + + assert isinstance(accessor, xcp.accessor.MountingAccessorTypes) name = "textfile" path = location + "/" + name assert fs.create_file(path, contents=text) diff --git a/tox.ini b/tox.ini index 90f65242..73c17139 100644 --- a/tox.ini +++ b/tox.ini @@ -160,7 +160,7 @@ deps = pylint pandas tabulate commands = - python run-pylint.py xcp tests + python pylint_runner.py xcp tests diff-quality --compare-branch=origin/master --violations=pylint \ --ignore-whitespace --fail-under 100 \ --html-report {envlogdir}/pylint-diff.html {envlogdir}/pylint.txt @@ -202,7 +202,7 @@ max-line-length = 129 [pyre] commands = pyre: python3.11 --version -V # Needs py311-pyre, does not work with py310-pyre - python ./run-pyre.py + python pyre_runner.py -pyright [pytype] @@ -212,4 +212,4 @@ commands = python3.10 -V # Needs python <= 3.10, and 3.10 is needed to parse new "|" syntax pytype --version # Runs pytype -j auto -k --config .github/workflows/pytype.cfg and parses the output: - python3 ./run-pytype.py # When switching versions, update .github/workflows/pytype.cfg too! + python3 pytype_runner.py # When switching versions, update .github/workflows/pytype.cfg too! diff --git a/xcp/accessor.py b/xcp/accessor.py index c821a13d..9583dfe7 100644 --- a/xcp/accessor.py +++ b/xcp/accessor.py @@ -23,15 +23,15 @@ """accessor - provide common interface to access methods""" +import errno # pyre-ignore-all-errors[6,16] import ftplib import io import os import sys import tempfile -import errno from contextlib import contextmanager -from typing import cast, TYPE_CHECKING +from typing import TYPE_CHECKING, Union, cast from six.moves import urllib # pyright: ignore @@ -39,7 +39,8 @@ if TYPE_CHECKING: from collections.abc import Generator - from typing import IO + from typing import IO, List, Tuple + from typing_extensions import Literal # maps errno codes to HTTP error codes @@ -127,7 +128,7 @@ def openAddress(self, address): else: self.lastError = mapError(e.errno) return False - except Exception as e: + except Exception: self.lastError = 500 return False return filehandle @@ -244,7 +245,7 @@ def openAddress(self, address): else: self.lastError = mapError(e.errno) return False - except Exception as e: + except Exception: self.lastError = 500 return False return reader @@ -331,7 +332,7 @@ def access(self, path): # pylint: disable=arguments-differ,arguments-renamed else: self.lastError = mapError(e.errno) return False - except Exception as e: + except Exception: self.lastError = 500 return False @@ -393,15 +394,60 @@ def openAddress(self, address): def __repr__(self): return "" % self.baseAddress -SUPPORTED_ACCESSORS = {'nfs': NFSAccessor, - 'http': HTTPAccessor, - 'https': HTTPAccessor, - 'ftp': FTPAccessor, - 'file': FileAccessor, - 'dev': DeviceAccessor, - } + +# Tuple passed in tests to isinstanc(val, ...Types) to check types: +MountingAccessorTypes = (DeviceAccessor, NFSAccessor) +"""Tuple for type checking in unit tests testing subclasses of MountingAccessor""" + +# Tuple passed in tests to isinstanc(val, ...Types) to check types: +LocalTypes = (DeviceAccessor, NFSAccessor, FileAccessor) + + +Mount = Union[DeviceAccessor, NFSAccessor] +"""Type alias for static typing or unit tests testing subclasses of MountingAccessor""" + +AnyAccessor = Union[ + HTTPAccessor, + FTPAccessor, + FileAccessor, + Mount, +] +"""Type alias for static typing the Accessor object returned by createAccessor()""" + +SUPPORTED_ACCESSORS = { + "nfs": NFSAccessor, + "http": HTTPAccessor, + "https": HTTPAccessor, + "ftp": FTPAccessor, + "file": FileAccessor, + "dev": DeviceAccessor, +} # type: dict[str, type[AnyAccessor]] +"""Dict of supported accessors. The key is the URL scheme""" def createAccessor(baseAddress, *args): + # type: (str, bool | Tuple[bool, List[str]]) -> Literal[False] | AnyAccessor + """ + Return instance of the appropriate Accessor subclass based on the baseAddress. + + :param baseAddress (str): The base address for the accessor. + :param args (tuple): Additional argument(s) to be passed to the accessor constructor + :returns Accessor (object | Literal[False]): Accessor object or Literal[False] + :raises AssertionError: If the scheme of the baseAddress is not supported. + + Also raises AssertionError when baseAddress is file:///filename/ + but the final / is omitted. The final terminating / is compulsory. + + For all Accessors, the 1st arg after the address is type bool for ro (readonly flag) + The DeviceAccessor accepts a 3rd argument: a List[] of filesystem names + + Examples: + accessor = createAccessor("http://example.com", True) + accessor = createAccessor("dev://example.com", True, ['iso9660', 'ext3']) + if not accessor: + fatal() + else: + accessor.read() + """ url_parts = urllib.parse.urlsplit(baseAddress, allow_fragments=False) assert url_parts.scheme in SUPPORTED_ACCESSORS diff --git a/xcp/pci.py b/xcp/pci.py index 5777d382..ef5cec29 100644 --- a/xcp/pci.py +++ b/xcp/pci.py @@ -234,6 +234,7 @@ def read(cls): for f in ['/usr/share/hwdata/pci.ids']: if os.path.exists(f): return cls(f) + # pylint: disable-next=broad-exception-raised raise Exception('Failed to open PCI database') def findVendor(self, vendor): diff --git a/xcp/version.py b/xcp/version.py index 58d3bc4a..885f533b 100644 --- a/xcp/version.py +++ b/xcp/version.py @@ -102,7 +102,7 @@ def __eq__(self, v): # is implemented: # https://docs.python.org/3/reference/datamodel.html#object.__hash__ # Example:https://github.com/swagger-api/swagger-codegen/issues/6475 - # Python2 pylint --py3k warns about it, and Pylint3 with out pylintrc + # Python2 pylint --py3k warns about it, and Pylint3 without .pylintrc # now too: def __hash__(self): # type:() -> int return hash(str(self.ver))