Typing with mypy

This commit is contained in:
Lucidiot 2019-09-06 21:09:50 +00:00
parent 657a881112
commit ba2557bca1
18 changed files with 229 additions and 109 deletions

1
.gitignore vendored
View File

@ -32,6 +32,7 @@ coverage.xml
*.cover
.hypothesis/
.pytest_cache/
.mypy_cache/
profile_default/
ipython_config.py

View File

@ -30,6 +30,11 @@ flake8:
script:
- flake8
mypy:
stage: test
script:
- mypy .
doc8:
stage: test
script:

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python3
from pathlib import Path
from typing import Optional, List
from typing import Optional, Dict, Any
from pylspci.command import CommandBuilder, IDResolveOption
from pylspci.filters import SlotFilter, DeviceFilter
import argparse
@ -156,15 +156,15 @@ def get_parser() -> argparse.ArgumentParser:
return parser
def main():
def main() -> None:
parser: argparse.ArgumentParser = get_parser()
args: dict = vars(parser.parse_args())
args: Dict[str, Any] = vars(parser.parse_args())
# Specific parsing required
use_parser: bool = args.pop('json', True)
kernel_modules: bool = args.pop('kernel_modules', False)
access_method: Optional[str] = args.pop('access_method', None)
pcilib_params: List[str] = args.pop('pcilib_params', []) or []
pcilib_params = args.pop('pcilib_params', []) or []
builder: CommandBuilder = CommandBuilder(**args)
if kernel_modules:
@ -195,7 +195,7 @@ def main():
print(item)
return
def _item_handler(item):
def _item_handler(item: Any) -> Any:
if hasattr(item, '_asdict'):
return item._asdict()
return item

View File

@ -1,9 +1,11 @@
from enum import Enum
from typing import Optional, Union, Tuple, List, Mapping, Any, Iterator
from typing import \
Optional, Union, List, Mapping, MutableMapping, Any, Iterator
from pathlib import Path
from pylspci.device import Device
from pylspci.fields import PCIAccessParameter
from pylspci.filters import SlotFilter, DeviceFilter
from pylspci.parsers.base import Parser
import subprocess
OptionalPath = Optional[Union[str, Path]]
@ -140,7 +142,7 @@ def lspci(
)
def list_access_methods():
def list_access_methods() -> List[str]:
"""
Calls ``lspci(access_method='help')`` to list the PCI access methods
the underlying ``pcilib`` provides and parses the human-readable list into
@ -157,7 +159,7 @@ def list_access_methods():
))
def list_pcilib_params_raw():
def list_pcilib_params_raw() -> List[str]:
"""
Calls ``lspci -Ohelp`` to list the PCI access parameters the underlying
``pcilib`` provides.
@ -176,7 +178,7 @@ def list_pcilib_params_raw():
))
def list_pcilib_params():
def list_pcilib_params() -> List[PCIAccessParameter]:
"""
Calls ``lspci -Ohelp`` to list the PCI access parameters the underlying
``pcilib`` provides and parse the human-readable list into
@ -202,14 +204,14 @@ class CommandBuilder(object):
_list_access_methods: bool = False
_list_pcilib_params: bool = False
_list_pcilib_params_raw: bool = False
_params: Mapping[str, Any] = {}
_parser = None
_params: MutableMapping[str, Any] = {}
_parser: Optional[Parser] = None
def __init__(self, **kwargs: Mapping[str, Any]):
self._params = kwargs
def __iter__(self) -> Iterator[Union[str, Device, PCIAccessParameter]]:
result = None
result: Union[str, List[str], List[Device], List[PCIAccessParameter]]
if self._list_access_methods:
result = list_access_methods()
elif self._list_pcilib_params:
@ -317,7 +319,9 @@ class CommandBuilder(object):
self._list_access_methods = False
return self
def with_pcilib_params(self, *args, **kwargs) -> 'CommandBuilder':
def with_pcilib_params(self,
*args: Mapping[str, Any],
**kwargs: Any) -> 'CommandBuilder':
"""
Override some pcilib parameters. When given a dict, will rewrite the
parameters with the new dict. When given keyword arguments, will update
@ -448,7 +452,7 @@ class CommandBuilder(object):
return self
def slot_filter(self,
*args: Tuple[str],
*args: str,
domain: Optional[int] = None,
bus: Optional[int] = None,
device: Optional[int] = None,
@ -478,7 +482,7 @@ class CommandBuilder(object):
return self
def device_filter(self,
*args: Tuple[str],
*args: str,
cls: Optional[int] = None,
vendor: Optional[int] = None,
device: Optional[int] = None) -> 'CommandBuilder':
@ -502,7 +506,7 @@ class CommandBuilder(object):
DeviceFilter(cls=cls, vendor=vendor, device=device)
return self
def with_parser(self, parser=None) -> 'CommandBuilder':
def with_parser(self, parser: Optional[Parser] = None) -> 'CommandBuilder':
"""
Use a pylspci parser to get parsed Device instances instead of strings.

View File

@ -1,18 +1,21 @@
from abc import ABC
from typing import Optional, Pattern, ClassVar
from abc import ABC, abstractmethod
from typing import Optional, Pattern, ClassVar, Dict, Type, TypeVar, Any
from pylspci.fields import hexstring
import re
T = TypeVar('T', bound='Filter')
class Filter(ABC):
_REGEX: ClassVar[Pattern]
@classmethod
def parse(cls, value: str) -> 'Filter':
def parse(cls: Type[T], value: str) -> T:
if not value:
return cls()
match, data = cls._REGEX.match(value), {}
match = cls._REGEX.match(value)
data: Dict[str, str] = {}
if match:
data = {k: v for k, v in match.groupdict().items()
if v is not None}
@ -24,6 +27,10 @@ class Filter(ABC):
if v != '' and v != '*'
})
@abstractmethod
def __init__(self, **kwargs: Any) -> None:
"Create a filter."
class SlotFilter(Filter):
"""
@ -92,9 +99,10 @@ class SlotFilter(Filter):
))
def __eq__(self, other: object) -> bool:
return isinstance(other, self.__class__) and \
self.domain, self.bus, self.device, self.function == \
other.domain, other.bus, other.device, other.function
if not isinstance(other, self.__class__):
return NotImplemented
return (self.domain, self.bus, self.device, self.function) == \
(other.domain, other.bus, other.device, other.function)
class DeviceFilter(Filter):
@ -153,6 +161,7 @@ class DeviceFilter(Filter):
))
def __eq__(self, other: object) -> bool:
return isinstance(other, self.__class__) and \
self.vendor, self.device, self.cls == \
other.vendor, other.device, other.cls
if not isinstance(other, self.__class__):
return NotImplemented
return (self.vendor, self.device, self.cls) == \
(other.vendor, other.device, other.cls)

View File

@ -1,12 +1,11 @@
from abc import ABC, abstractmethod
from typing import Union, Iterable, List, Mapping, Any
from typing import Union, Mapping, Iterable, List, Dict, Any
from pylspci.device import Device
from pylspci.command import lspci
class Parser(ABC):
default_lspci_args: Mapping[str, Any] = {}
default_lspci_args: Dict[str, Any] = {}
"""
The default arguments that, when sent to :func:`lspci`, should provide the
best output for this parser.
@ -15,13 +14,18 @@ class Parser(ABC):
"""
@abstractmethod
def parse(self, data: Union[str, Iterable[str]]) -> List[Device]:
def parse(
self,
data: Union[str, Iterable[str], Iterable[Iterable[str]]]
) -> List[Device]:
"""
Parse a string or list of strings as a list of devices.
:param data: A string holding multiple devices
or a list of strings, one for each device.
:type data: str or Iterable[str]
:param data: A string holding multiple devices,
a list of strings, one for each device,
or a list of lists of strings, one list for each device, with
each list holding each part of the device output.
:type data: str or Iterable[str] or Iterable[Iterable[str]]
:returns: A list of parsed devices.
:rtype: List[Device]
"""
@ -38,6 +42,7 @@ class Parser(ABC):
:returns: A list of parsed devices.
:rtype: List[Device]
"""
from pylspci.command import lspci
lspci_kwargs = self.default_lspci_args.copy()
lspci_kwargs.update(kwargs)
return self.parse(lspci(**lspci_kwargs))

View File

@ -53,13 +53,19 @@ class SimpleParser(Parser):
)
return p
def parse(self, data: Union[str, Iterable[str]]) -> List[Device]:
def parse(
self,
data: Union[str, Iterable[str], Iterable[Iterable[str]]],
) -> List[Device]:
"""
Parse a multiline string or a list of single-line strings
from lspci -mm into devices.
:param data: String or list of strings to parse from.
:type data: str or Iterable[str]
:param data: A string holding multiple devices,
a list of strings, one for each device,
or a list of lists of strings, one list for each device, with
each list holding each part of the device output.
:type data: str or Iterable[str] or Iterable[Iterable[str]]
:return: A list of parsed devices.
:rtype: List[Device]
"""

View File

@ -1,4 +1,4 @@
from typing import Union, List, Iterable, NamedTuple, Callable, Any
from typing import Union, List, Dict, Iterable, NamedTuple, Callable, Any
from pylspci.parsers.base import Parser
from pylspci.device import Device
from pylspci.fields import hexstring, Slot, NameWithID
@ -68,7 +68,7 @@ class VerboseParser(Parser):
}
def _parse_device(self, device_data: Union[str, Iterable[str]]) -> Device:
devdict = {}
devdict: Dict[str, Any] = {}
if isinstance(device_data, str):
device_data = device_data.splitlines()
@ -85,21 +85,30 @@ class VerboseParser(Parser):
return Device(**devdict)
def parse(self, data: Union[str, Iterable[str]]) -> List[Device]:
def parse(
self,
data: Union[str, Iterable[str], Iterable[Iterable[str]]],
) -> List[Device]:
"""
Parse an lspci -vvvmm[nnk] output, either as a single string holding
multiple devices separated by two newlines,
or as a list of multiline strings holding one device each.
:param data: One string holding a full lspci output,
or multiple strings holding one device each.
:type data: str or Iterable[str]
:param data: A string holding multiple devices,
a list of strings, one for each device,
or a list of lists of strings, one list for each device, with
each list holding each part of the device output.
:type data: str or Iterable[str] or Iterable[Iterable[str]]
:return: A list of parsed devices.
:rtype: List[Device]
"""
if isinstance(data, str):
data = data.split('\n\n')
return list(map(
self._parse_device,
filter(bool, map(str.strip, data)), # Ignore empty strings
))
result: List[Device] = []
for line in data:
if isinstance(line, str):
line = str.strip(line)
if not line: # Ignore empty strings and lists
continue
result.append(self._parse_device(line))
return result

0
pylspci/py.typed Normal file
View File

View File

@ -8,14 +8,14 @@ from pylspci.parsers import SimpleParser, VerboseParser
class TestCommandBuilder(TestCase):
@patch('pylspci.command.lspci')
def test_default(self, lspci_mock):
def test_default(self, lspci_mock: MagicMock) -> None:
lspci_mock.return_value = ['a', 'b']
self.assertListEqual(list(CommandBuilder()), ['a', 'b'])
self.assertEqual(lspci_mock.call_count, 1)
self.assertEqual(lspci_mock.call_args, call())
@patch('pylspci.command.lspci')
def test_iter_str(self, lspci_mock):
def test_iter_str(self, lspci_mock: MagicMock) -> None:
"""
Test iterating the CommandBuilder when lspci returns a single string
returns an iterator for a list made of a single string, not an iterator
@ -28,7 +28,9 @@ class TestCommandBuilder(TestCase):
@patch('pylspci.command.Path.is_file')
@patch('pylspci.command.lspci')
def test_use_pciids(self, lspci_mock, isfile_mock):
def test_use_pciids(self,
lspci_mock: MagicMock,
isfile_mock: MagicMock) -> None:
lspci_mock.return_value = ['a', 'b']
isfile_mock.return_value = True
builder = CommandBuilder().use_pciids('somefile')
@ -39,7 +41,9 @@ class TestCommandBuilder(TestCase):
@patch('pylspci.command.Path.is_file')
@patch('pylspci.command.lspci')
def test_use_pciids_check(self, lspci_mock, isfile_mock):
def test_use_pciids_check(self,
lspci_mock: MagicMock,
isfile_mock: MagicMock) -> None:
lspci_mock.return_value = ['a', 'b']
isfile_mock.return_value = False
with self.assertRaisesRegex(AssertionError, 'not found'):
@ -48,7 +52,9 @@ class TestCommandBuilder(TestCase):
@patch('pylspci.command.Path.is_file')
@patch('pylspci.command.lspci')
def test_use_pciids_no_check(self, lspci_mock, isfile_mock):
def test_use_pciids_no_check(self,
lspci_mock: MagicMock,
isfile_mock: MagicMock) -> None:
lspci_mock.return_value = ['a', 'b']
isfile_mock.return_value = False
builder = CommandBuilder().use_pciids('somefile', check=False)
@ -59,7 +65,9 @@ class TestCommandBuilder(TestCase):
@patch('pylspci.command.Path.is_file')
@patch('pylspci.command.lspci')
def test_use_pcimap(self, lspci_mock, isfile_mock):
def test_use_pcimap(self,
lspci_mock: MagicMock,
isfile_mock: MagicMock) -> None:
lspci_mock.return_value = ['a', 'b']
isfile_mock.return_value = True
builder = CommandBuilder().use_pcimap('somefile')
@ -70,7 +78,9 @@ class TestCommandBuilder(TestCase):
@patch('pylspci.command.Path.is_file')
@patch('pylspci.command.lspci')
def test_use_pcimap_check(self, lspci_mock, isfile_mock):
def test_use_pcimap_check(self,
lspci_mock: MagicMock,
isfile_mock: MagicMock) -> None:
lspci_mock.return_value = ['a', 'b']
isfile_mock.return_value = False
with self.assertRaisesRegex(AssertionError, 'not found'):
@ -79,7 +89,9 @@ class TestCommandBuilder(TestCase):
@patch('pylspci.command.Path.is_file')
@patch('pylspci.command.lspci')
def test_use_pcimap_no_check(self, lspci_mock, isfile_mock):
def test_use_pcimap_no_check(self,
lspci_mock: MagicMock,
isfile_mock: MagicMock) -> None:
lspci_mock.return_value = ['a', 'b']
isfile_mock.return_value = False
builder = CommandBuilder().use_pcimap('somefile', check=False)
@ -89,7 +101,7 @@ class TestCommandBuilder(TestCase):
self.assertFalse(isfile_mock.called)
@patch('pylspci.command.lspci')
def test_use_access_method(self, lspci_mock):
def test_use_access_method(self, lspci_mock: MagicMock) -> None:
lspci_mock.return_value = ['a', 'b']
builder = CommandBuilder() \
.use_access_method('one') \
@ -99,7 +111,7 @@ class TestCommandBuilder(TestCase):
self.assertEqual(lspci_mock.call_args, call(access_method='two'))
@patch('pylspci.command.list_access_methods')
def test_list_access_methods(self, list_mock):
def test_list_access_methods(self, list_mock: MagicMock) -> None:
list_mock.return_value = ['a', 'b']
builder = CommandBuilder().list_pcilib_params().list_access_methods()
self.assertListEqual(list(builder), ['a', 'b'])
@ -107,7 +119,7 @@ class TestCommandBuilder(TestCase):
self.assertEqual(list_mock.call_args, call())
@patch('pylspci.command.list_pcilib_params')
def test_list_pcilib_params(self, list_mock):
def test_list_pcilib_params(self, list_mock: MagicMock) -> None:
list_mock.return_value = ['a', 'b']
builder = CommandBuilder().list_access_methods().list_pcilib_params()
self.assertListEqual(list(builder), ['a', 'b'])
@ -115,7 +127,7 @@ class TestCommandBuilder(TestCase):
self.assertEqual(list_mock.call_args, call())
@patch('pylspci.command.list_pcilib_params_raw')
def test_list_pcilib_params_raw(self, list_mock):
def test_list_pcilib_params_raw(self, list_mock: MagicMock) -> None:
list_mock.return_value = ['a', 'b']
builder = CommandBuilder() \
.list_access_methods() \
@ -125,7 +137,7 @@ class TestCommandBuilder(TestCase):
self.assertEqual(list_mock.call_args, call())
@patch('pylspci.command.lspci')
def test_with_pcilib_params_dict(self, lspci_mock):
def test_with_pcilib_params_dict(self, lspci_mock: MagicMock) -> None:
with self.assertRaisesRegex(AssertionError, 'dict or keyword'):
CommandBuilder().with_pcilib_params({'a': 'b'}, c='d')
with self.assertRaisesRegex(AssertionError, 'Only one positional'):
@ -138,7 +150,7 @@ class TestCommandBuilder(TestCase):
self.assertEqual(lspci_mock.call_args, call(pcilib_params={'a': 'b'}))
@patch('pylspci.command.lspci')
def test_with_pcilib_params_kwargs(self, lspci_mock):
def test_with_pcilib_params_kwargs(self, lspci_mock: MagicMock) -> None:
lspci_mock.return_value = ['a', 'b']
builder = CommandBuilder() \
.with_pcilib_params(a='1', b='2') \
@ -151,7 +163,9 @@ class TestCommandBuilder(TestCase):
@patch('pylspci.command.Path.is_file')
@patch('pylspci.command.lspci')
def test_from_file(self, lspci_mock, isfile_mock):
def test_from_file(self,
lspci_mock: MagicMock,
isfile_mock: MagicMock) -> None:
lspci_mock.return_value = ['a', 'b']
isfile_mock.return_value = True
builder = CommandBuilder().from_file('somefile')
@ -162,7 +176,9 @@ class TestCommandBuilder(TestCase):
@patch('pylspci.command.Path.is_file')
@patch('pylspci.command.lspci')
def test_from_file_check(self, lspci_mock, isfile_mock):
def test_from_file_check(self,
lspci_mock: MagicMock,
isfile_mock: MagicMock) -> None:
lspci_mock.return_value = ['a', 'b']
isfile_mock.return_value = False
with self.assertRaisesRegex(AssertionError, 'not found'):
@ -171,7 +187,9 @@ class TestCommandBuilder(TestCase):
@patch('pylspci.command.Path.is_file')
@patch('pylspci.command.lspci')
def test_from_file_no_check(self, lspci_mock, isfile_mock):
def test_from_file_no_check(self,
lspci_mock: MagicMock,
isfile_mock: MagicMock) -> None:
lspci_mock.return_value = ['a', 'b']
isfile_mock.return_value = False
builder = CommandBuilder().from_file('somefile', check=False)
@ -181,7 +199,7 @@ class TestCommandBuilder(TestCase):
self.assertFalse(isfile_mock.called)
@patch('pylspci.command.lspci')
def test_verbose(self, lspci_mock):
def test_verbose(self, lspci_mock: MagicMock) -> None:
lspci_mock.return_value = ['a', 'b']
builder = CommandBuilder().verbose()
self.assertListEqual(list(builder), ['a', 'b'])
@ -189,7 +207,7 @@ class TestCommandBuilder(TestCase):
self.assertEqual(lspci_mock.call_args, call(verbose=True))
@patch('pylspci.command.lspci')
def test_include_kernel_drivers(self, lspci_mock):
def test_include_kernel_drivers(self, lspci_mock: MagicMock) -> None:
lspci_mock.return_value = ['a', 'b']
builder = CommandBuilder() \
.include_kernel_drivers(False) \
@ -202,7 +220,7 @@ class TestCommandBuilder(TestCase):
))
@patch('pylspci.command.lspci')
def test_include_bridge_paths(self, lspci_mock):
def test_include_bridge_paths(self, lspci_mock: MagicMock) -> None:
lspci_mock.return_value = ['a', 'b']
builder = CommandBuilder() \
.include_bridge_paths(False) \
@ -214,7 +232,7 @@ class TestCommandBuilder(TestCase):
))
@patch('pylspci.command.lspci')
def test_hide_single_domain(self, lspci_mock):
def test_hide_single_domain(self, lspci_mock: MagicMock) -> None:
lspci_mock.return_value = ['a', 'b']
builder = CommandBuilder().hide_single_domain()
self.assertListEqual(list(builder), ['a', 'b'])
@ -222,7 +240,7 @@ class TestCommandBuilder(TestCase):
self.assertEqual(lspci_mock.call_args, call(hide_single_domain=True))
@patch('pylspci.command.lspci')
def test_with_ids(self, lspci_mock):
def test_with_ids(self, lspci_mock: MagicMock) -> None:
lspci_mock.return_value = ['a', 'b']
builder = CommandBuilder().with_ids(False)
self.assertListEqual(list(builder), ['a', 'b'])
@ -240,7 +258,7 @@ class TestCommandBuilder(TestCase):
))
@patch('pylspci.command.lspci')
def test_with_names(self, lspci_mock):
def test_with_names(self, lspci_mock: MagicMock) -> None:
lspci_mock.return_value = ['a', 'b']
builder = CommandBuilder().with_names(False)
self.assertListEqual(list(builder), ['a', 'b'])
@ -259,11 +277,13 @@ class TestCommandBuilder(TestCase):
@patch('pylspci.command.SlotFilter')
@patch('pylspci.command.lspci')
def test_slot_filter_str(self, lspci_mock, filter_mock):
def test_slot_filter_str(self,
lspci_mock: MagicMock,
filter_mock: MagicMock) -> None:
with self.assertRaisesRegex(AssertionError, 'Only one positional'):
CommandBuilder().slot_filter('something', 'something else')
with self.assertRaisesRegex(AssertionError, 'Use either'):
CommandBuilder().slot_filter('something', domain='a')
CommandBuilder().slot_filter('something', domain=0xa)
filter_mock.parse.return_value = 'lefilter'
lspci_mock.return_value = ['a', 'b']
@ -276,33 +296,37 @@ class TestCommandBuilder(TestCase):
@patch('pylspci.command.SlotFilter')
@patch('pylspci.command.lspci')
def test_slot_filter_kwargs(self, lspci_mock, filter_mock):
def test_slot_filter_kwargs(self,
lspci_mock: MagicMock,
filter_mock: MagicMock) -> None:
filter_mock.return_value = 'lefilter'
lspci_mock.return_value = ['a', 'b']
builder = CommandBuilder().slot_filter(
domain='a',
bus='b',
device='c',
function='d',
domain=0xa,
bus=0xb,
device=0xc,
function=0xd,
)
self.assertListEqual(list(builder), ['a', 'b'])
self.assertEqual(lspci_mock.call_count, 1)
self.assertEqual(lspci_mock.call_args, call(slot_filter='lefilter'))
self.assertEqual(filter_mock.call_count, 1)
self.assertEqual(filter_mock.call_args, call(
domain='a',
bus='b',
device='c',
function='d',
domain=0xa,
bus=0xb,
device=0xc,
function=0xd,
))
@patch('pylspci.command.DeviceFilter')
@patch('pylspci.command.lspci')
def test_device_filter_str(self, lspci_mock, filter_mock):
def test_device_filter_str(self,
lspci_mock: MagicMock,
filter_mock: MagicMock) -> None:
with self.assertRaisesRegex(AssertionError, 'Only one positional'):
CommandBuilder().device_filter('something', 'something else')
with self.assertRaisesRegex(AssertionError, 'Use either'):
CommandBuilder().device_filter('something', vendor='b')
CommandBuilder().device_filter('something', vendor=0xb)
filter_mock.parse.return_value = 'lefilter'
lspci_mock.return_value = ['a', 'b']
@ -315,25 +339,27 @@ class TestCommandBuilder(TestCase):
@patch('pylspci.command.DeviceFilter')
@patch('pylspci.command.lspci')
def test_device_filter_kwargs(self, lspci_mock, filter_mock):
def test_device_filter_kwargs(self,
lspci_mock: MagicMock,
filter_mock: MagicMock) -> None:
filter_mock.return_value = 'lefilter'
lspci_mock.return_value = ['a', 'b']
builder = CommandBuilder().device_filter(
cls='a',
vendor='b',
device='c',
cls=0xa,
vendor=0xb,
device=0xc,
)
self.assertListEqual(list(builder), ['a', 'b'])
self.assertEqual(lspci_mock.call_count, 1)
self.assertEqual(lspci_mock.call_args, call(device_filter='lefilter'))
self.assertEqual(filter_mock.call_count, 1)
self.assertEqual(filter_mock.call_args, call(
cls='a',
vendor='b',
device='c',
cls=0xa,
vendor=0xb,
device=0xc,
))
def test_with_default_parser(self):
def test_with_default_parser(self) -> None:
builder = CommandBuilder()
self.assertIsNone(builder._parser)
@ -344,7 +370,7 @@ class TestCommandBuilder(TestCase):
self.assertIsInstance(builder._parser, VerboseParser)
@patch('pylspci.command.lspci')
def test_with_parser(self, lspci_mock):
def test_with_parser(self, lspci_mock: MagicMock) -> None:
lspci_mock.return_value = ['a', 'b']
parser_mock = MagicMock(spec=SimpleParser)
parser_mock.parse.return_value = ('parsed_a', 'parsed_b')

View File

@ -18,7 +18,9 @@ class TestCommand(TestCase):
@patch('pylspci.command.Path.is_file')
@patch('pylspci.command.subprocess.check_output')
def test_pciids(self, cmd_mock: MagicMock, is_file_mock: MagicMock):
def test_pciids(self,
cmd_mock: MagicMock,
is_file_mock: MagicMock) -> None:
cmd_mock.return_value = 'something'
is_file_mock.return_value = True
self.assertEqual(lspci(pciids='/somewhere'), 'something')
@ -30,7 +32,7 @@ class TestCommand(TestCase):
self.assertEqual(is_file_mock.call_count, 1)
@patch('pylspci.command.Path.is_file')
def test_pciids_missing(self, is_file_mock: MagicMock):
def test_pciids_missing(self, is_file_mock: MagicMock) -> None:
is_file_mock.return_value = False
with self.assertRaises(AssertionError):
lspci(pciids='/nowhere')
@ -38,7 +40,9 @@ class TestCommand(TestCase):
@patch('pylspci.command.Path.is_file')
@patch('pylspci.command.subprocess.check_output')
def test_pcimap(self, cmd_mock: MagicMock, is_file_mock: MagicMock):
def test_pcimap(self,
cmd_mock: MagicMock,
is_file_mock: MagicMock) -> None:
cmd_mock.return_value = 'something'
is_file_mock.return_value = True
self.assertEqual(lspci(pcimap='/somewhere'), 'something')
@ -50,7 +54,7 @@ class TestCommand(TestCase):
self.assertEqual(is_file_mock.call_count, 1)
@patch('pylspci.command.Path.is_file')
def test_pcimap_missing(self, is_file_mock: MagicMock):
def test_pcimap_missing(self, is_file_mock: MagicMock) -> None:
is_file_mock.return_value = False
with self.assertRaises(AssertionError):
lspci(pcimap='/nowhere')
@ -77,7 +81,7 @@ class TestCommand(TestCase):
@patch('pylspci.command.Path.is_file')
@patch('pylspci.command.subprocess.check_output')
def test_file(self, cmd_mock: MagicMock, is_file_mock: MagicMock):
def test_file(self, cmd_mock: MagicMock, is_file_mock: MagicMock) -> None:
cmd_mock.return_value = 'something'
is_file_mock.return_value = True
self.assertEqual(lspci(file='/somewhere'), 'something')
@ -89,7 +93,7 @@ class TestCommand(TestCase):
self.assertEqual(is_file_mock.call_count, 1)
@patch('pylspci.command.Path.is_file')
def test_file_missing(self, is_file_mock: MagicMock):
def test_file_missing(self, is_file_mock: MagicMock) -> None:
is_file_mock.return_value = False
with self.assertRaises(AssertionError):
lspci(file='/nowhere')
@ -189,7 +193,9 @@ class TestCommand(TestCase):
@patch('pylspci.command.Path.is_file')
@patch('pylspci.command.subprocess.check_output')
def test_everything(self, cmd_mock: MagicMock, is_file_mock: MagicMock):
def test_everything(self,
cmd_mock: MagicMock,
is_file_mock: MagicMock) -> None:
cmd_mock.return_value = 'something'
is_file_mock.return_value = True

View File

@ -7,6 +7,8 @@ from pylspci.parsers import SimpleParser
class TestSimpleParser(TestCase):
parser: SimpleParser
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
@ -24,8 +26,10 @@ class TestSimpleParser(TestCase):
self.assertEqual(dev.vendor.name, 'Intel Corporation')
self.assertEqual(dev.device.id, 0x244e)
self.assertEqual(dev.device.name, '82801 PCI Bridge')
assert dev.subsystem_vendor is not None
self.assertEqual(dev.subsystem_vendor.id, 0x8086)
self.assertEqual(dev.subsystem_vendor.name, 'Intel Corporation')
assert dev.subsystem_device is not None
self.assertEqual(dev.subsystem_device.id, 0x244e)
self.assertEqual(dev.subsystem_device.name, '82801 PCI Bridge')
self.assertEqual(dev.revision, 0xd5)
@ -71,14 +75,16 @@ class TestSimpleParser(TestCase):
self.assertEqual(dev.vendor.name, '')
self.assertIsNone(dev.device.id)
self.assertEqual(dev.device.name, '')
assert dev.subsystem_vendor is not None
self.assertIsNone(dev.subsystem_vendor.id)
self.assertEqual(dev.subsystem_vendor.name, '')
assert dev.subsystem_device is not None
self.assertIsNone(dev.subsystem_device.id)
self.assertEqual(dev.subsystem_device.name, '')
self.assertIsNone(dev.revision)
self.assertIsNone(dev.progif)
@patch('pylspci.parsers.base.lspci')
@patch('pylspci.command.lspci')
def test_command(self, cmd_mock: MagicMock) -> None:
cmd_mock.return_value = \
'00:1c.3 "PCI bridge [0604]" "Intel Corporation [8086]" ' \

View File

@ -16,11 +16,13 @@ ProgIf: 01
Driver: pcieport
Module: nouveau
Module: nvidia
""".strip()
"""
class TestVerboseParser(TestCase):
parser: VerboseParser
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
@ -38,8 +40,10 @@ class TestVerboseParser(TestCase):
self.assertEqual(dev.vendor.name, 'Intel Corporation')
self.assertEqual(dev.device.id, 0x244e)
self.assertEqual(dev.device.name, '82801 PCI Bridge')
assert dev.subsystem_vendor is not None
self.assertEqual(dev.subsystem_vendor.id, 0x8086)
self.assertEqual(dev.subsystem_vendor.name, 'Intel Corporation')
assert dev.subsystem_device is not None
self.assertEqual(dev.subsystem_device.id, 0x244e)
self.assertEqual(dev.subsystem_device.name, '82801 PCI Bridge')
self.assertEqual(dev.revision, 0xd5)
@ -57,7 +61,7 @@ class TestVerboseParser(TestCase):
self.assertEqual(len(devices), 1)
self._check_device(devices[0])
@patch('pylspci.parsers.base.lspci')
@patch('pylspci.command.lspci')
def test_command(self, cmd_mock: MagicMock) -> None:
cmd_mock.return_value = '{0}\n\n{0}'.format(SAMPLE_DEVICE)

View File

@ -60,11 +60,13 @@ class TestSlot(TestCase):
self.assertEqual(s.device, 0x06)
self.assertEqual(s.function, 0x6)
self.assertIsInstance(s.parent, Slot)
assert s.parent is not None # Required for type checks
self.assertEqual(s.parent.domain, 0xcafe)
self.assertEqual(s.parent.bus, 0xc0)
self.assertEqual(s.parent.device, 0x0f)
self.assertEqual(s.parent.function, 0x3)
self.assertIsInstance(s.parent.parent, Slot)
assert s.parent.parent is not None # Required for type checks
self.assertEqual(s.parent.parent.domain, 0xabcd)
self.assertEqual(s.parent.parent.bus, 0x13)
self.assertEqual(s.parent.parent.device, 0x07)

View File

@ -4,7 +4,7 @@ from pylspci.filters import SlotFilter, DeviceFilter
class TestSlotFilter(TestCase):
def test_empty(self):
def test_empty(self) -> None:
f = SlotFilter()
self.assertIsNone(f.domain)
self.assertIsNone(f.bus)
@ -15,7 +15,7 @@ class TestSlotFilter(TestCase):
'SlotFilter(domain=None, bus=None, device=None, function=None)',
)
def test_str(self):
def test_str(self) -> None:
self.assertEqual(str(SlotFilter()), '::.')
self.assertEqual(str(SlotFilter(domain=0xcafe)), 'cafe::.')
self.assertEqual(
@ -23,7 +23,7 @@ class TestSlotFilter(TestCase):
'c0ff:e:e.7',
)
def test_parse(self):
def test_parse(self) -> None:
self.assertEqual(SlotFilter.parse(''), SlotFilter())
self.assertEqual(SlotFilter.parse('::.'), SlotFilter())
self.assertEqual(SlotFilter.parse('*:*:*.*'), SlotFilter())
@ -40,10 +40,24 @@ class TestSlotFilter(TestCase):
with self.assertRaises(ValueError):
SlotFilter.parse('g')
def test_eq(self) -> None:
self.assertEqual(
SlotFilter(domain=0xc0ff, bus=0xe, device=0xe, function=7),
SlotFilter(domain=0xc0ff, bus=0xe, device=0xe, function=7),
)
self.assertNotEqual(
SlotFilter(domain=0xc0ff, bus=0xf, device=0xe, function=7),
SlotFilter(domain=0xc0ff, bus=0xe, device=0xe, function=7),
)
self.assertNotEqual(
SlotFilter(domain=0xc0ff, bus=0xf, device=0xe, function=7),
'not a filter',
)
class TestDeviceFilter(TestCase):
def test_empty(self):
def test_empty(self) -> None:
f = DeviceFilter()
self.assertIsNone(f.vendor)
self.assertIsNone(f.device)
@ -53,7 +67,7 @@ class TestDeviceFilter(TestCase):
'DeviceFilter(cls=None, vendor=None, device=None)',
)
def test_str(self):
def test_str(self) -> None:
self.assertEqual(str(DeviceFilter()), '::')
self.assertEqual(str(DeviceFilter(vendor=0xcafe)), 'cafe::')
self.assertEqual(
@ -61,7 +75,7 @@ class TestDeviceFilter(TestCase):
'c0ff:e:e',
)
def test_parse(self):
def test_parse(self) -> None:
self.assertEqual(DeviceFilter.parse(''), DeviceFilter())
self.assertEqual(DeviceFilter.parse('::'), DeviceFilter())
self.assertEqual(DeviceFilter.parse('*:*:*'), DeviceFilter())
@ -78,3 +92,17 @@ class TestDeviceFilter(TestCase):
DeviceFilter.parse('4')
with self.assertRaises(ValueError):
DeviceFilter.parse('g')
def test_eq(self) -> None:
self.assertEqual(
DeviceFilter(vendor=0xc0ff, device=0xe, cls=0xe),
DeviceFilter(vendor=0xc0ff, device=0xe, cls=0xe),
)
self.assertNotEqual(
DeviceFilter(vendor=0xc0ff, device=0xf, cls=0xe),
DeviceFilter(vendor=0xc0ff, device=0xe, cls=0xe),
)
self.assertNotEqual(
DeviceFilter(vendor=0xc0ff, device=0xe, cls=0xe),
'not a filter',
)

View File

@ -3,3 +3,4 @@ doc8>=0.8.0
Sphinx>=1.8.1
coverage>=4.5
codecov>=2.0
mypy>=0.720

View File

@ -1,5 +1,12 @@
[flake8]
exclude = .git,__pycache__,docs,*.pyc,venv
exclude=.git,__pycache__,docs,*.pyc,venv
[doc8]
ignore-path=**/*.txt,*.txt,*.egg-info,docs/_build,venv,.git
[mypy]
ignore_missing_imports=True
disallow_incomplete_defs=True
disallow_untyped_defs=True
check_untyped_defs=True
no_implicit_optional=True

View File

@ -21,7 +21,8 @@ setup(
'console_scripts': ['pylspci=pylspci.__main__:main'],
},
package_data={
'': ['*.md', 'LICENSE', 'README'],
'': ['VERSION', 'LICENSE', 'README.rst'],
'pylspci': ['py.typed'],
},
python_requires='>=3.5',
install_requires=requirements,