Compare commits
47 Commits
Author | SHA1 | Date |
---|---|---|
Lucidiot | 1f7cac1d8c | |
Lucidiot | ac6a1b3103 | |
~lucidiot | 04e3354394 | |
~lucidiot | 91918e8d41 | |
~lucidiot | e3a89613e1 | |
~lucidiot | 4438f17e94 | |
~lucidiot | 70b155cf04 | |
~lucidiot | d6becffcc0 | |
~lucidiot | 27cc5de1b9 | |
~lucidiot | 1936c58dec | |
~lucidiot | 544edd0b3d | |
~lucidiot | da39b09ef5 | |
~lucidiot | 149b3bde99 | |
~lucidiot | ad52e4c823 | |
~lucidiot | 30e1ed5ec4 | |
~lucidiot | 7cebd6202d | |
~lucidiot | 57d96ab345 | |
~lucidiot | 386b7fc326 | |
~lucidiot | 587a55af73 | |
~lucidiot | 96cbe0e72c | |
~lucidiot | 09256806f1 | |
Lucidiot | 27695dd747 | |
Lucidiot | 0b00a2e586 | |
Lucidiot | 06906411a1 | |
Jan Lützler | 9f90cf8519 | |
Lucidiot | 7778e1a48a | |
Lucidiot | 51aac8692d | |
Lucidiot | 11c57ae0f1 | |
Lucidiot | 7da6c0756c | |
Lucidiot | 45dfdd7277 | |
Lucidiot | a34c08c9ce | |
Lucidiot | ce1abd76d0 | |
Lucidiot | 0b06017ab7 | |
Lucidiot | 92417f1b6a | |
Lucidiot | 65053cfdba | |
Lucidiot | 3b93a30fb4 | |
Lucidiot | 4c21f2f813 | |
Lucidiot | f21296378a | |
Erwan Rouchet | 4406ce90a7 | |
Lucidiot | 4abe4a883a | |
Lucidiot | 44dad5738f | |
Lucidiot | c9e34b2a5e | |
Lucidiot | 3c3bbe25c5 | |
Lucidiot | fc5d3c7644 | |
Lucidiot | cddd1d04a7 | |
Lucidiot | 45cceb8611 | |
Lucidiot | ba2557bca1 |
|
@ -0,0 +1,120 @@
|
|||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: pre-commit
|
||||
image: python:3-alpine
|
||||
commands:
|
||||
- apk add --no-cache git gcc musl-dev
|
||||
- pip install .[dev]
|
||||
- pre-commit run -a
|
||||
|
||||
- name: test-py36
|
||||
image: python:3.6-alpine
|
||||
commands:
|
||||
- pip install .[dev]
|
||||
- coverage run setup.py test
|
||||
- coverage report
|
||||
|
||||
- name: test-py37
|
||||
image: python:3.7-alpine
|
||||
commands:
|
||||
- pip install .[dev]
|
||||
- coverage run setup.py test
|
||||
- coverage report
|
||||
|
||||
- name: test-py38
|
||||
image: python:3.8-alpine
|
||||
commands:
|
||||
- pip install .[dev]
|
||||
- coverage run setup.py test
|
||||
- coverage report
|
||||
|
||||
- name: test-py39
|
||||
image: python:3.9-alpine
|
||||
commands:
|
||||
- pip install .[dev]
|
||||
- coverage run setup.py test
|
||||
- coverage report
|
||||
|
||||
- name: test-py310
|
||||
image: python:3.10-alpine
|
||||
commands:
|
||||
- pip install .[dev]
|
||||
- coverage run setup.py test
|
||||
- coverage report
|
||||
|
||||
- name: test-py311
|
||||
image: python:3.11-alpine
|
||||
commands:
|
||||
- pip install .[dev]
|
||||
- coverage run setup.py test
|
||||
- coverage report
|
||||
|
||||
- name: testpypi
|
||||
image: python:3.11-alpine
|
||||
commands:
|
||||
- pip install .[dev] twine setuptools wheel
|
||||
- |
|
||||
echo "[distutils]
|
||||
index-servers = testpypi
|
||||
[testpypi]
|
||||
repository=https://test.pypi.org/legacy/
|
||||
username=$$TESTPYPI_DEPLOY_USERNAME
|
||||
password=$$TESTPYPI_DEPLOY_PASSWORD" > ~/.pypirc
|
||||
- python setup.py sdist bdist_wheel
|
||||
- twine upload dist/* -r testpypi
|
||||
|
||||
when:
|
||||
event:
|
||||
- promote
|
||||
repo:
|
||||
- lucidiot/pylspci
|
||||
|
||||
depends_on:
|
||||
- pre-commit
|
||||
- test-py36
|
||||
- test-py37
|
||||
- test-py38
|
||||
- test-py39
|
||||
- test-py310
|
||||
- test-py311
|
||||
|
||||
environment:
|
||||
TESTPYPI_DEPLOY_USERNAME:
|
||||
from_secret: testpypi_username
|
||||
TESTPYPI_DEPLOY_PASSWORD:
|
||||
from_secret: testpypi_password
|
||||
|
||||
- name: pypi
|
||||
image: python:3.11-alpine
|
||||
commands:
|
||||
- pip install .[dev] twine setuptools wheel
|
||||
- |
|
||||
echo "[distutils]
|
||||
index-servers = pypi
|
||||
[pypi]
|
||||
repository=https://upload.pypi.org/legacy/
|
||||
username=$$PYPI_DEPLOY_USERNAME
|
||||
password=$$PYPI_DEPLOY_PASSWORD" > ~/.pypirc
|
||||
- python setup.py sdist bdist_wheel
|
||||
- twine upload dist/* -r pypi
|
||||
|
||||
when:
|
||||
event:
|
||||
- promote
|
||||
repo:
|
||||
- lucidiot/pylspci
|
||||
branch:
|
||||
- master
|
||||
|
||||
depends_on:
|
||||
- testpypi
|
||||
|
||||
environment:
|
||||
PYPI_DEPLOY_USERNAME:
|
||||
from_secret: pypi_username
|
||||
PYPI_DEPLOY_PASSWORD:
|
||||
from_secret: pypi_password
|
|
@ -32,6 +32,7 @@ coverage.xml
|
|||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
image: python:3.7
|
||||
stages:
|
||||
- test
|
||||
- deploy
|
||||
|
||||
variables:
|
||||
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
|
||||
|
||||
cache:
|
||||
paths:
|
||||
- .cache/pip
|
||||
- venv/
|
||||
|
||||
before_script:
|
||||
- pip install virtualenv
|
||||
- virtualenv venv
|
||||
- source venv/bin/activate
|
||||
- pip install .[dev]
|
||||
|
||||
tests:
|
||||
stage: test
|
||||
coverage: '/TOTAL[\s\d]+\s(\d+%)/'
|
||||
script:
|
||||
- coverage run setup.py test
|
||||
- coverage report
|
||||
- codecov
|
||||
|
||||
flake8:
|
||||
stage: test
|
||||
script:
|
||||
- flake8
|
||||
|
||||
doc8:
|
||||
stage: test
|
||||
script:
|
||||
- doc8
|
||||
|
||||
deploy-pypi:
|
||||
stage: deploy
|
||||
when: manual
|
||||
only:
|
||||
- master@Lucidiot/pylspci
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/project/pylspci
|
||||
|
||||
script:
|
||||
- pip install twine setuptools wheel
|
||||
- echo "[distutils]" > ~/.pypirc
|
||||
- echo "index-servers =" >> ~/.pypirc
|
||||
- echo " pypi" >> ~/.pypirc
|
||||
- echo "[pypi]" >> ~/.pypirc
|
||||
- echo "repository=https://upload.pypi.org/legacy/" >> ~/.pypirc
|
||||
- echo "username=$PYPI_DEPLOY_USERNAME" >> ~/.pypirc
|
||||
- echo "password=$PYPI_DEPLOY_PASSWORD" >> ~/.pypirc
|
||||
- python setup.py sdist bdist_wheel
|
||||
- twine upload dist/* -r pypi
|
||||
|
||||
deploy-testpypi:
|
||||
stage: deploy
|
||||
when: manual
|
||||
only:
|
||||
- branches@Lucidiot/pylspci
|
||||
environment:
|
||||
name: testpypi
|
||||
url: https://test.pypi.org/project/pylspci
|
||||
|
||||
script:
|
||||
- pip install twine setuptools wheel
|
||||
- echo "[distutils]" > ~/.pypirc
|
||||
- echo "index-servers =" >> ~/.pypirc
|
||||
- echo " testpypi" >> ~/.pypirc
|
||||
- echo "[testpypi]" >> ~/.pypirc
|
||||
- echo "repository=https://test.pypi.org/legacy/" >> ~/.pypirc
|
||||
- echo "username=$PYPI_DEPLOY_USERNAME" >> ~/.pypirc
|
||||
- echo "password=$PYPI_DEPLOY_PASSWORD" >> ~/.pypirc
|
||||
- python setup.py sdist bdist_wheel
|
||||
- twine upload dist/* -r testpypi
|
||||
|
||||
pages:
|
||||
stage: deploy
|
||||
when: manual
|
||||
only:
|
||||
- master@Lucidiot/pylspci
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
|
||||
script:
|
||||
- cd docs
|
||||
- make html
|
||||
- mv _build/html ../public
|
|
@ -0,0 +1,35 @@
|
|||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- id: check-merge-conflict
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-symlinks
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.2.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
args:
|
||||
- --ignore-missing-imports
|
||||
- --disallow-incomplete-defs
|
||||
- --disallow-untyped-defs
|
||||
- --check-untyped-defs
|
||||
- --no-implicit-optional
|
||||
- repo: https://github.com/PyCQA/doc8
|
||||
rev: v1.1.1
|
||||
hooks:
|
||||
- id: doc8
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
|
@ -0,0 +1,6 @@
|
|||
include requirements.txt
|
||||
include requirements-dev.txt
|
||||
include VERSION
|
||||
include LICENSE
|
||||
include README.rst
|
||||
include pylspci/py.typed
|
|
@ -5,4 +5,4 @@ A Python parser for the ``lspci`` command from the pciutils_ package.
|
|||
`Browse documentation`_
|
||||
|
||||
.. _pciutils: http://mj.ucw.cz/sw/pciutils/
|
||||
.. _Browse documentation: https://lucidiot.gitlab.io/pylspci/
|
||||
.. _Browse documentation: https://lucidiot.tildepages.org/pylspci/
|
||||
|
|
|
@ -16,4 +16,4 @@ help:
|
|||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
|
|
@ -130,4 +130,3 @@ PCI access
|
|||
``-H2``
|
||||
Access hardware using Intel configuration mechanism 2.
|
||||
Alias to ``-A intel-conf2``.
|
||||
|
||||
|
|
39
docs/conf.py
39
docs/conf.py
|
@ -14,13 +14,14 @@
|
|||
#
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'pylspci'
|
||||
copyright = '2019, Lucidiot and contributors'
|
||||
copyright = '2022, Lucidiot and contributors'
|
||||
author = 'Lucidiot and contributors'
|
||||
|
||||
# The short X.Y version
|
||||
|
@ -61,7 +62,7 @@ master_doc = 'index'
|
|||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
language = 'en'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
|
@ -112,23 +113,23 @@ htmlhelp_basename = 'pylspcidoc'
|
|||
|
||||
# -- Options for LaTeX output ------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
# latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
#
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
#
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
#
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
# }
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
|
|
|
@ -6,8 +6,8 @@ Contributions to the project are greatly appreciated.
|
|||
Bugs and suggestions
|
||||
--------------------
|
||||
|
||||
You may `submit an issue`_ to GitLab to warn of any bugs, ask for new features,
|
||||
or ask any questions that are not answered in this documentation.
|
||||
You may `submit an issue`_ to the Gitea repository to warn of any bugs, ask for
|
||||
new features, or ask any questions that are not answered in this documentation.
|
||||
|
||||
When reporting a bug, do not forget to put in your version of Python and your
|
||||
version of *pylspci*. This will greatly help when troubleshooting, as most
|
||||
|
@ -22,7 +22,7 @@ Setup
|
|||
You will need a virtual envionment to work properly. `virtualenvwrapper`_ is
|
||||
recommended::
|
||||
|
||||
git clone https://gitlab.com/Lucidiot/pylspci
|
||||
git clone https://tildegit.org/lucidiot/pylspci.git
|
||||
cd pylspci
|
||||
mkvirtualenv -a . pylspci
|
||||
pip install -e .[dev]
|
||||
|
@ -31,6 +31,37 @@ This will clone the repository, create a virtual environment named
|
|||
``pylspci``, then tell pip to let the package be editable (``-e``).
|
||||
The ``[dev]`` suffix adds the extra requirements useful for development.
|
||||
|
||||
Unit tests
|
||||
^^^^^^^^^^
|
||||
|
||||
Unit tests use the standard ``unittest`` package; you may run them using the
|
||||
standard ``setup.py`` command::
|
||||
|
||||
./setup.py test
|
||||
|
||||
Tests coverage
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
I aim for 100% coverage on all of my Python packages whenever I add unit
|
||||
tests to them; this package is no exception. CI checks use the `coverage`_
|
||||
Python package to get coverage statistics.
|
||||
To get test coverage data locally, run::
|
||||
|
||||
coverage run setup.py test
|
||||
|
||||
You may then get a short coverage summary in your terminal::
|
||||
|
||||
coverage report
|
||||
|
||||
Or generate an HTML report in a ``htmlcov`` folder, which can be browsed
|
||||
offline using your favorite web browser and shows line by line coverage::
|
||||
|
||||
coverage html
|
||||
|
||||
If you are having issues reaching 100% coverage, try to still add some tests,
|
||||
and mention your issues when creating a pull request to the
|
||||
`Gitea repository`_.
|
||||
|
||||
Linting
|
||||
^^^^^^^
|
||||
|
||||
|
@ -38,17 +69,25 @@ The source code follows the PEP 8 code style and performs CI checks using the
|
|||
``flake8`` tool. To perform the same checks locally, run ``flake8`` on the root
|
||||
directory of this repository.
|
||||
|
||||
Type checking
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
The source code uses PEP 484 type hints and type checking is performed in CI
|
||||
using ``mypy``. To run those checks locally, run ``mypy .`` on the root
|
||||
directory of this repository.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
The documentation you are reading is generated by the `Sphinx`_ tool.
|
||||
The text files that hold the documentation's contents are written in
|
||||
`reStructuredText`_ and are available under the ``/docs`` folder of the
|
||||
`GitLab repository`_.
|
||||
`Gitea repository`_.
|
||||
They are also subject to linting using the ``doc8`` tool.
|
||||
|
||||
.. _submit an issue: https://gitlab.com/Lucidiot/pylspci/issues/new
|
||||
.. _submit an issue: https://tildegit.org/lucidiot/pylspci/issues/new
|
||||
.. _virtualenvwrapper: https://virtualenvwrapper.readthedocs.io
|
||||
.. _GitLab repository: https://gitlab.com/Lucidiot/pylspci
|
||||
.. _coverage: https://coverage.readthedocs.io/
|
||||
.. _Gitea repository: https://tildegit.org/lucidiot/pylspci
|
||||
.. _Sphinx: http://www.sphinx-doc.org/
|
||||
.. _reStructuredText: http://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html
|
||||
|
|
|
@ -18,17 +18,11 @@ Python lspci parser
|
|||
.. image:: https://img.shields.io/pypi/status/pylspci.svg
|
||||
:target: https://pypi.org/project/pylspci
|
||||
|
||||
.. image:: https://gitlab.com/Lucidiot/pylspci/badges/master/pipeline.svg
|
||||
:target: https://gitlab.com/Lucidiot/pylspci/pipelines
|
||||
.. image:: https://drone.tildegit.org/api/badges/lucidiot/pylspci/status.svg
|
||||
:target: https://drone.tildegit.org/api/badges/lucidiot/pylspci/status.svg
|
||||
|
||||
.. image:: https://requires.io/github/Lucidiot/pylspci/requirements.svg?branch=master
|
||||
:target: https://requires.io/github/Lucidiot/pylspci/requirements/?branch=master
|
||||
|
||||
.. image:: https://img.shields.io/github/last-commit/Lucidiot/pylspci.svg
|
||||
:target: https://gitlab.com/Lucidiot/pylspci/commits
|
||||
|
||||
.. image:: https://img.shields.io/badge/badge%20count-9-brightgreen.svg
|
||||
:target: https://gitlab.com/Lucidiot/pylspci
|
||||
.. image:: https://img.shields.io/badge/badge%20count-7-brightgreen.svg
|
||||
:target: https://tildegit.org/lucidiot/pylspci
|
||||
|
||||
A Python parser for the ``lspci`` command from the pciutils_ package.
|
||||
|
||||
|
@ -60,7 +54,7 @@ To parse ``lspci -nnmmvvvk``, use the
|
|||
.. code:: python
|
||||
|
||||
>>> from pylspci.parsers import SimpleParser
|
||||
>>> SimpleParser.run()
|
||||
>>> SimpleParser().run()
|
||||
[Device(slot=Slot('0000:00:01.3'), name=NameWithID('Name A [2420]'), ...),
|
||||
Device(slot=Slot('0000:00:01.4'), name=NameWithID('Name B [0e54]'), ...)]
|
||||
|
||||
|
@ -70,8 +64,8 @@ Custom arguments
|
|||
.. code:: python
|
||||
|
||||
>>> from pylspci.command import IDResolveOption
|
||||
>>> from pylspci import parser
|
||||
>>> parser.run(
|
||||
>>> from pylspci.parsers import VerboseParser
|
||||
>>> VerboseParser().run(
|
||||
... hide_single_domain=False,
|
||||
... id_resolve_option=IDResolveOption.NameOnly,
|
||||
... )
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
from pylspci.command import CommandBuilder, IDResolveOption
|
||||
from pylspci.filters import SlotFilter, DeviceFilter
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from pylspci.command import CommandBuilder, IDResolveOption
|
||||
from pylspci.filters import DeviceFilter, SlotFilter
|
||||
|
||||
|
||||
def get_parser() -> argparse.ArgumentParser:
|
||||
|
@ -156,15 +157,14 @@ 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)
|
||||
json_output: 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:
|
||||
|
@ -178,7 +178,7 @@ def main():
|
|||
|
||||
for param in pcilib_params:
|
||||
if param.strip().lower() == 'help':
|
||||
builder = builder.list_pcilib_params(raw=not use_parser)
|
||||
builder = builder.list_pcilib_params(raw=not json_output)
|
||||
break
|
||||
if '=' not in param:
|
||||
parser.error(
|
||||
|
@ -186,24 +186,19 @@ def main():
|
|||
key, value = map(str.strip, param.split('=', 2))
|
||||
builder = builder.with_pcilib_params(**{key: value})
|
||||
|
||||
if use_parser:
|
||||
if json_output:
|
||||
builder = builder.with_default_parser()
|
||||
|
||||
result = list(builder)
|
||||
if not use_parser: # Raw mode
|
||||
if not json_output: # Raw mode
|
||||
for item in result:
|
||||
print(item)
|
||||
return
|
||||
|
||||
def _item_handler(item):
|
||||
if hasattr(item, '_asdict'):
|
||||
return item._asdict()
|
||||
return item
|
||||
|
||||
print(json.dumps(
|
||||
list(map(_item_handler, result)),
|
||||
default=vars,
|
||||
))
|
||||
print(json.dumps([
|
||||
item if isinstance(item, str) else item.as_dict()
|
||||
for item in result
|
||||
]))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import subprocess
|
||||
from enum import Enum
|
||||
from typing import Optional, Union, Tuple, List, Mapping, Any, Iterator
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
Any, Iterator, List, Mapping, MutableMapping, Optional, Union
|
||||
)
|
||||
|
||||
from pylspci.device import Device
|
||||
from pylspci.fields import PCIAccessParameter
|
||||
from pylspci.filters import SlotFilter, DeviceFilter
|
||||
import subprocess
|
||||
from pylspci.filters import DeviceFilter, SlotFilter
|
||||
from pylspci.parsers.base import Parser
|
||||
|
||||
OptionalPath = Optional[Union[str, Path]]
|
||||
|
||||
|
@ -140,7 +144,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 +161,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 +180,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 +206,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]):
|
||||
def __init__(self, **kwargs: 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 +321,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 +454,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 +484,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 +508,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.
|
||||
|
||||
|
|
|
@ -1,5 +1,15 @@
|
|||
from typing import NamedTuple, Optional, List
|
||||
from pylspci.fields import Slot, NameWithID
|
||||
from typing import Dict, List, NamedTuple, Optional, Union
|
||||
|
||||
from pylspci.fields import NameWithID, NameWithIDDict, Slot, SlotDict
|
||||
|
||||
DeviceDict = Dict[str, Union[
|
||||
int,
|
||||
str,
|
||||
SlotDict,
|
||||
NameWithIDDict,
|
||||
List[str],
|
||||
None,
|
||||
]]
|
||||
|
||||
|
||||
class Device(NamedTuple):
|
||||
|
@ -10,69 +20,92 @@ class Device(NamedTuple):
|
|||
slot: Slot
|
||||
"""
|
||||
The device's slot (domain, bus, number and function).
|
||||
|
||||
:type: Slot
|
||||
"""
|
||||
|
||||
cls: NameWithID
|
||||
"""
|
||||
The device's class, with a name and/or an ID.
|
||||
|
||||
:type: NameWithID
|
||||
"""
|
||||
|
||||
vendor: NameWithID
|
||||
"""
|
||||
The device's vendor, with a name and/or an ID.
|
||||
|
||||
:type: NameWithID
|
||||
"""
|
||||
|
||||
device: NameWithID
|
||||
"""
|
||||
The device's name and/or ID.
|
||||
|
||||
:type: NameWithID
|
||||
"""
|
||||
|
||||
subsystem_vendor: Optional[NameWithID] = None
|
||||
"""
|
||||
The device's subsystem vendor, if found, with a name and/or an ID.
|
||||
|
||||
:type: NameWithID or None
|
||||
"""
|
||||
|
||||
subsystem_device: Optional[NameWithID] = None
|
||||
"""
|
||||
The device's subsystem name and/or ID, if found.
|
||||
|
||||
:type: NameWithID or None
|
||||
"""
|
||||
|
||||
revision: Optional[int] = None
|
||||
"""
|
||||
The device's revision number.
|
||||
|
||||
:type: int or None
|
||||
"""
|
||||
|
||||
progif: Optional[int] = None
|
||||
"""
|
||||
The device's programming interface number.
|
||||
|
||||
:type: int or None
|
||||
"""
|
||||
|
||||
driver: Optional[str] = None
|
||||
"""
|
||||
The device's driver (Linux only).
|
||||
|
||||
:type: str or None
|
||||
"""
|
||||
|
||||
kernel_modules: List[str] = []
|
||||
"""
|
||||
One or more kernel modules that can handle this device (Linux only).
|
||||
|
||||
:type: List[str] or None
|
||||
"""
|
||||
|
||||
numa_node: Optional[int] = None
|
||||
"""
|
||||
NUMA node this device is connected to (Linux only).
|
||||
"""
|
||||
|
||||
iommu_group: Optional[int] = None
|
||||
"""
|
||||
IOMMU group that this device is part of (optional, Linux only).
|
||||
"""
|
||||
|
||||
physical_slot: Optional[str] = None
|
||||
"""
|
||||
The device's physical slot number (Linux only).
|
||||
"""
|
||||
|
||||
def as_dict(self) -> DeviceDict:
|
||||
"""
|
||||
Serialize this device as a JSON-serializable `dict`.
|
||||
"""
|
||||
return {
|
||||
"slot": self.slot.as_dict(),
|
||||
"cls": self.cls.as_dict(),
|
||||
"vendor": self.vendor.as_dict(),
|
||||
"device": self.device.as_dict(),
|
||||
"subsystem_vendor": (
|
||||
self.subsystem_vendor.as_dict()
|
||||
if self.subsystem_vendor
|
||||
else None
|
||||
),
|
||||
"subsystem_device": (
|
||||
self.subsystem_device.as_dict()
|
||||
if self.subsystem_device
|
||||
else None
|
||||
),
|
||||
"revision": self.revision,
|
||||
"progif": self.progif,
|
||||
"driver": self.driver,
|
||||
"kernel_modules": self.kernel_modules,
|
||||
"numa_node": self.numa_node,
|
||||
"iommu_group": self.iommu_group,
|
||||
"physical_slot": self.physical_slot,
|
||||
}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
from functools import partial
|
||||
from typing import Optional, Any
|
||||
import re
|
||||
from functools import partial
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
# mypy does not support recursive type definitions
|
||||
# SlotDict = Dict[str, Union[int, 'SlotDict', None]]
|
||||
SlotDict = Dict[str, Union[int, Dict[str, Any], None]]
|
||||
NameWithIDDict = Dict[str, Union[int, str, None]]
|
||||
|
||||
hexstring = partial(int, base=16)
|
||||
|
||||
|
@ -18,36 +22,26 @@ class Slot(object):
|
|||
"""
|
||||
The slot's domain, as a four-digit hexadecimal number.
|
||||
When omitted, defaults to ``0x0000``.
|
||||
|
||||
:type: int
|
||||
"""
|
||||
|
||||
bus: int
|
||||
"""
|
||||
The slot's bus, as a two-digit hexadecimal number.
|
||||
|
||||
:type: int
|
||||
"""
|
||||
|
||||
device: int
|
||||
"""
|
||||
The slot's device, as a two-digit hexadecimal number, up to `0x1f`.
|
||||
|
||||
:type: int
|
||||
"""
|
||||
|
||||
function: int
|
||||
"""
|
||||
The slot's function, as a single octal digit.
|
||||
|
||||
:type: int
|
||||
"""
|
||||
|
||||
parent: Optional["Slot"] = None
|
||||
"""
|
||||
The slot's parent bridge, if present.
|
||||
|
||||
:type: Slot or None
|
||||
"""
|
||||
|
||||
def __init__(self, value: str) -> None:
|
||||
|
@ -76,6 +70,18 @@ class Slot(object):
|
|||
def __repr__(self) -> str:
|
||||
return '{}({!r})'.format(self.__class__.__name__, str(self))
|
||||
|
||||
def as_dict(self) -> SlotDict:
|
||||
"""
|
||||
Serialize this slot as a JSON-serializable `dict`.
|
||||
"""
|
||||
return {
|
||||
"domain": self.domain,
|
||||
"bus": self.bus,
|
||||
"device": self.device,
|
||||
"function": self.function,
|
||||
"parent": self.parent.as_dict() if self.parent else None,
|
||||
}
|
||||
|
||||
|
||||
class NameWithID(object):
|
||||
"""
|
||||
|
@ -86,15 +92,11 @@ class NameWithID(object):
|
|||
id: Optional[int]
|
||||
"""
|
||||
The PCI ID as a four-digit hexadecimal number.
|
||||
|
||||
:type: int or None
|
||||
"""
|
||||
|
||||
name: Optional[str]
|
||||
"""
|
||||
The human-readable name associated with this ID.
|
||||
|
||||
:type: str or None
|
||||
"""
|
||||
|
||||
_NAME_ID_REGEX = re.compile(r'^(?P<name>.+)\s\[(?P<id>[0-9a-fA-F]{4})\]$')
|
||||
|
@ -132,6 +134,15 @@ class NameWithID(object):
|
|||
def __repr__(self) -> str:
|
||||
return '{}({!r})'.format(self.__class__.__name__, str(self))
|
||||
|
||||
def as_dict(self) -> NameWithIDDict:
|
||||
"""
|
||||
Serialize this name and ID as a JSON-serializable `dict`.
|
||||
"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
}
|
||||
|
||||
|
||||
class PCIAccessParameter(object):
|
||||
"""
|
||||
|
@ -143,22 +154,16 @@ class PCIAccessParameter(object):
|
|||
name: str
|
||||
"""
|
||||
The parameter's name.
|
||||
|
||||
:type: str
|
||||
"""
|
||||
|
||||
description: str
|
||||
"""
|
||||
A short description of the parameter's use.
|
||||
|
||||
:type: str
|
||||
"""
|
||||
|
||||
default: Optional[str]
|
||||
"""
|
||||
An optional default value for the parameter.
|
||||
|
||||
:type: str or None
|
||||
"""
|
||||
|
||||
_PARAM_REGEX = re.compile(
|
||||
|
@ -184,3 +189,13 @@ class PCIAccessParameter(object):
|
|||
return isinstance(other, PCIAccessParameter) and \
|
||||
(self.name, self.description, self.default) \
|
||||
== (other.name, other.description, other.default)
|
||||
|
||||
def as_dict(self) -> Dict[str, Optional[str]]:
|
||||
"""
|
||||
Serialize this PCI access parameter as a JSON-serializable `dict`.
|
||||
"""
|
||||
return {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"default": self.default,
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
from abc import ABC
|
||||
from typing import Optional, Pattern, ClassVar
|
||||
from pylspci.fields import hexstring
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, ClassVar, Dict, Optional, Pattern, Type, TypeVar
|
||||
|
||||
from pylspci.fields import hexstring
|
||||
|
||||
T = TypeVar('T', bound='Filter')
|
||||
|
||||
|
||||
class Filter(ABC):
|
||||
|
@ -9,10 +12,11 @@ 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 +28,10 @@ class Filter(ABC):
|
|||
if v != '' and v != '*'
|
||||
})
|
||||
|
||||
@abstractmethod
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
"Create a filter."
|
||||
|
||||
|
||||
class SlotFilter(Filter):
|
||||
"""
|
||||
|
@ -35,29 +43,21 @@ class SlotFilter(Filter):
|
|||
domain: Optional[int] = None
|
||||
"""
|
||||
Device domain, as a four-digit hexadecimal number.
|
||||
|
||||
:type: int or None
|
||||
"""
|
||||
|
||||
bus: Optional[int] = None
|
||||
"""
|
||||
Device bus, as a two-digit hexadecimal number.
|
||||
|
||||
:type: int or None
|
||||
"""
|
||||
|
||||
device: Optional[int] = None
|
||||
"""
|
||||
Device number, as a two-digit hexadecimal number, up to `0x1f`.
|
||||
|
||||
:type: int or None
|
||||
"""
|
||||
|
||||
function: Optional[int] = None
|
||||
"""
|
||||
The slot's function, as a single octal digit.
|
||||
|
||||
:type: int or None
|
||||
"""
|
||||
|
||||
# [[domain:]bus:][device][.function]
|
||||
|
@ -92,9 +92,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):
|
||||
|
@ -107,22 +108,16 @@ class DeviceFilter(Filter):
|
|||
cls: Optional[int] = None
|
||||
"""
|
||||
Device class ID, as a four-digit hexadecimal number.
|
||||
|
||||
:type: int or None
|
||||
"""
|
||||
|
||||
vendor: Optional[int] = None
|
||||
"""
|
||||
Device vendor ID, as a four-digit hexadecimal number.
|
||||
|
||||
:type: int or None
|
||||
"""
|
||||
|
||||
device: Optional[int] = None
|
||||
"""
|
||||
Device ID, as a four-digit hexadecimal number.
|
||||
|
||||
:type: int or None
|
||||
"""
|
||||
|
||||
# [vendor]:[device][:class]
|
||||
|
@ -153,6 +148,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)
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import Union, Iterable, List, Mapping, Any
|
||||
from typing import Any, Dict, Iterable, List, Union
|
||||
|
||||
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,18 +15,23 @@ 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]
|
||||
"""
|
||||
|
||||
def run(self, **kwargs: Mapping[str, Any]) -> List[Device]:
|
||||
def run(self, **kwargs: Any) -> List[Device]:
|
||||
"""
|
||||
Run the lspci command with the given arguments, defaulting to the
|
||||
parser's default arguments, and parse the result.
|
||||
|
@ -34,10 +39,11 @@ class Parser(ABC):
|
|||
:param \\**kwargs: Optional arguments to override the parser's default
|
||||
arguments. See :func:`lspci`'s documentation for a list of
|
||||
available arguments.
|
||||
:type \\**kwargs: Mapping[str, Any]
|
||||
:type \\**kwargs: Any
|
||||
: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))
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
from typing import Union, List, Iterable
|
||||
from cached_property import cached_property
|
||||
from pylspci.parsers.base import Parser
|
||||
from pylspci.fields import hexstring, Slot, NameWithID
|
||||
from pylspci.device import Device
|
||||
import argparse
|
||||
import shlex
|
||||
from typing import Any, Iterable, List, Union
|
||||
|
||||
from cached_property import cached_property
|
||||
|
||||
from pylspci.device import Device
|
||||
from pylspci.fields import NameWithID, Slot, hexstring
|
||||
from pylspci.parsers.base import Parser
|
||||
|
||||
|
||||
class SimpleParser(Parser):
|
||||
|
@ -53,13 +55,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]
|
||||
"""
|
||||
|
@ -80,3 +88,11 @@ class SimpleParser(Parser):
|
|||
if isinstance(args, str):
|
||||
args = shlex.split(args)
|
||||
return Device(**vars(self._parser.parse_args(args)))
|
||||
|
||||
def run(self, **kwargs: Any) -> List[Device]:
|
||||
if kwargs.get('verbose'):
|
||||
raise ValueError(
|
||||
'Verbose output is unsupported from the SimpleParser. '
|
||||
'Please use the pylspci.parsers.VerboseParser instead.'
|
||||
)
|
||||
return super().run(**kwargs)
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
from typing import Union, List, Iterable, NamedTuple, Callable, Any
|
||||
from pylspci.parsers.base import Parser
|
||||
import warnings
|
||||
from typing import Any, Callable, Dict, Iterable, List, NamedTuple, Union
|
||||
|
||||
from pylspci.device import Device
|
||||
from pylspci.fields import hexstring, Slot, NameWithID
|
||||
from pylspci.fields import NameWithID, Slot, hexstring
|
||||
from pylspci.parsers.base import Parser
|
||||
|
||||
UNKNOWN_FIELD_WARNING = (
|
||||
'Unsupported device field {!r} with value {!r}\n'
|
||||
'Please report this, along with the output of `lspci -mmnnvvvk`, at '
|
||||
'https://tildegit.org/lucidiot/pylspci/issues/new'
|
||||
)
|
||||
|
||||
|
||||
class FieldMapping(NamedTuple):
|
||||
|
@ -13,23 +21,17 @@ class FieldMapping(NamedTuple):
|
|||
field_name: str
|
||||
"""
|
||||
Field name on the :class:`Device` named tuple.
|
||||
|
||||
:type: str
|
||||
"""
|
||||
|
||||
field_type: Callable[[str], Any]
|
||||
"""
|
||||
Field type; a callable to use to parse the string value.
|
||||
|
||||
:type: Callable[[str], Any]
|
||||
"""
|
||||
|
||||
many: bool = False
|
||||
"""
|
||||
Whether or not to use a List, if this field can be repeated multiple times
|
||||
in the lspci output.
|
||||
|
||||
:type: bool
|
||||
"""
|
||||
|
||||
|
||||
|
@ -65,17 +67,24 @@ class VerboseParser(Parser):
|
|||
field_type=str,
|
||||
many=True,
|
||||
),
|
||||
'NUMANode': FieldMapping(field_name='numa_node', field_type=int),
|
||||
'IOMMUGroup': FieldMapping(field_name='iommu_group', field_type=int),
|
||||
'PhySlot': FieldMapping(field_name='physical_slot', field_type=str),
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
for line in device_data:
|
||||
key, _, value = map(str.strip, line.partition(':'))
|
||||
assert key in self._field_mapping, \
|
||||
'Unsupported key {!r}'.format(key)
|
||||
if key not in self._field_mapping:
|
||||
warnings.warn(
|
||||
UNKNOWN_FIELD_WARNING.format(key, value),
|
||||
UserWarning,
|
||||
)
|
||||
continue
|
||||
field = self._field_mapping[key]
|
||||
if field.many:
|
||||
devdict.setdefault(field.field_name, []) \
|
||||
|
@ -85,21 +94,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
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from unittest import TestCase
|
||||
from unittest.mock import patch, call, MagicMock
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
from pylspci.command import CommandBuilder, IDResolveOption
|
||||
from pylspci.parsers import SimpleParser, VerboseParser
|
||||
|
||||
|
@ -8,14 +9,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 +29,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 +42,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 +53,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 +66,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 +79,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 +90,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 +102,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 +112,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 +120,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 +128,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 +138,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 +151,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 +164,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 +177,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 +188,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 +200,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 +208,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 +221,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 +233,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 +241,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 +259,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 +278,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 +297,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 +340,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 +371,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')
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
from unittest import TestCase
|
||||
from unittest.mock import patch, call, MagicMock
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
from pylspci.command import (
|
||||
IDResolveOption, list_access_methods, list_pcilib_params, lspci
|
||||
)
|
||||
from pylspci.fields import PCIAccessParameter
|
||||
from pylspci.command import \
|
||||
lspci, list_access_methods, list_pcilib_params, IDResolveOption
|
||||
|
||||
|
||||
class TestCommand(TestCase):
|
||||
|
@ -18,7 +20,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 +34,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 +42,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 +56,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 +83,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 +95,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 +195,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
|
||||
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
from unittest import TestCase
|
||||
from unittest.mock import patch, call, MagicMock
|
||||
from typing import List
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
from pylspci.device import Device
|
||||
from pylspci.parsers import SimpleParser
|
||||
|
||||
|
||||
class TestSimpleParser(TestCase):
|
||||
|
||||
parser: SimpleParser
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
super().setUpClass()
|
||||
|
@ -24,8 +27,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 +76,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]" ' \
|
||||
|
@ -92,3 +99,12 @@ class TestSimpleParser(TestCase):
|
|||
|
||||
self.assertEqual(cmd_mock.call_count, 1)
|
||||
self.assertEqual(cmd_mock.call_args, call())
|
||||
|
||||
def test_verbose_error(self) -> None:
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
self.parser.run(verbose=True)
|
||||
self.assertEqual(
|
||||
ctx.exception.args[0],
|
||||
'Verbose output is unsupported from the SimpleParser. '
|
||||
'Please use the pylspci.parsers.VerboseParser instead.'
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from unittest import TestCase
|
||||
from unittest.mock import patch, call, MagicMock
|
||||
from typing import List
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
from pylspci.device import Device
|
||||
from pylspci.parsers import VerboseParser
|
||||
|
||||
|
@ -16,11 +17,16 @@ ProgIf: 01
|
|||
Driver: pcieport
|
||||
Module: nouveau
|
||||
Module: nvidia
|
||||
""".strip()
|
||||
NUMANode: 0
|
||||
IOMMUGroup: 1
|
||||
PhySlot: 4
|
||||
"""
|
||||
|
||||
|
||||
class TestVerboseParser(TestCase):
|
||||
|
||||
parser: VerboseParser
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
super().setUpClass()
|
||||
|
@ -38,14 +44,19 @@ 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)
|
||||
self.assertEqual(dev.progif, 0x01)
|
||||
self.assertEqual(dev.driver, 'pcieport')
|
||||
self.assertListEqual(dev.kernel_modules, ['nouveau', 'nvidia'])
|
||||
self.assertEqual(dev.numa_node, 0)
|
||||
self.assertEqual(dev.iommu_group, 1)
|
||||
self.assertEqual(dev.physical_slot, '4')
|
||||
|
||||
def test_parse_str(self) -> None:
|
||||
devices: List[Device] = self.parser.parse(SAMPLE_DEVICE)
|
||||
|
@ -57,7 +68,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)
|
||||
|
||||
|
@ -69,3 +80,16 @@ class TestVerboseParser(TestCase):
|
|||
self.assertEqual(cmd_mock.call_count, 1)
|
||||
self.assertEqual(cmd_mock.call_args,
|
||||
call(verbose=True, kernel_drivers=True))
|
||||
|
||||
def test_unknown_field(self) -> None:
|
||||
with self.assertWarns(
|
||||
UserWarning,
|
||||
msg="Unsupported device field 'NewField' with value 'Value'\n"
|
||||
"Please report this, along with the output of"
|
||||
"`lspci -mmnnvvvk`, at "
|
||||
"https://tildegit.org/lucidiot/pylspci/issues/new"):
|
||||
devices: List[Device] = \
|
||||
self.parser.parse(SAMPLE_DEVICE + 'NewField\tValue')
|
||||
|
||||
self.assertEqual(len(devices), 1)
|
||||
self._check_device(devices[0])
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
from unittest import TestCase
|
||||
|
||||
from pylspci.device import Device
|
||||
from pylspci.fields import NameWithID, Slot
|
||||
|
||||
|
||||
class TestDevice(TestCase):
|
||||
|
||||
def test_as_dict(self) -> None:
|
||||
d = Device(
|
||||
slot=Slot('cafe:13:07.2'),
|
||||
cls=NameWithID('Something [caf3]'),
|
||||
vendor=NameWithID('Something [caf3]'),
|
||||
device=NameWithID('Something [caf3]'),
|
||||
subsystem_vendor=NameWithID('Something [caf3]'),
|
||||
subsystem_device=NameWithID('Something [caf3]'),
|
||||
revision=20,
|
||||
progif=1,
|
||||
driver='self_driving',
|
||||
kernel_modules=['snd-pcsp'],
|
||||
numa_node=0,
|
||||
iommu_group=1,
|
||||
physical_slot='4-2',
|
||||
)
|
||||
self.assertDictEqual(d.as_dict(), {
|
||||
'slot': {
|
||||
'bus': 0x13,
|
||||
'device': 0x07,
|
||||
'domain': 0xcafe,
|
||||
'function': 0x2,
|
||||
'parent': None
|
||||
},
|
||||
'cls': {
|
||||
'id': 0xcaf3,
|
||||
'name': 'Something'
|
||||
},
|
||||
'vendor': {
|
||||
'id': 0xcaf3,
|
||||
'name': 'Something'
|
||||
},
|
||||
'device': {
|
||||
'id': 0xcaf3,
|
||||
'name': 'Something'
|
||||
},
|
||||
'subsystem_vendor': {
|
||||
'id': 0xcaf3,
|
||||
'name': 'Something'
|
||||
},
|
||||
'subsystem_device': {
|
||||
'id': 0xcaf3,
|
||||
'name': 'Something'
|
||||
},
|
||||
'revision': 20,
|
||||
'progif': 1,
|
||||
'driver': 'self_driving',
|
||||
'kernel_modules': ['snd-pcsp'],
|
||||
'numa_node': 0,
|
||||
'iommu_group': 1,
|
||||
'physical_slot': '4-2',
|
||||
})
|
|
@ -1,5 +1,6 @@
|
|||
from unittest import TestCase
|
||||
from pylspci.fields import Slot, NameWithID, PCIAccessParameter
|
||||
|
||||
from pylspci.fields import NameWithID, PCIAccessParameter, Slot
|
||||
|
||||
|
||||
class TestSlot(TestCase):
|
||||
|
@ -60,16 +61,28 @@ 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)
|
||||
self.assertEqual(s.parent.parent.function, 0x2)
|
||||
|
||||
def test_as_dict(self) -> None:
|
||||
s = Slot('cafe:13:07.2')
|
||||
self.assertDictEqual(s.as_dict(), {
|
||||
"domain": 0xcafe,
|
||||
"bus": 0x13,
|
||||
"device": 0x07,
|
||||
"function": 0x2,
|
||||
"parent": None,
|
||||
})
|
||||
|
||||
|
||||
class TestNameWithID(TestCase):
|
||||
|
||||
|
@ -125,6 +138,13 @@ class TestNameWithID(TestCase):
|
|||
self.assertIsNone(n.id)
|
||||
self.assertEqual(n.name, 'Something [hexa]')
|
||||
|
||||
def test_as_dict(self) -> None:
|
||||
n = NameWithID('Something [caf3]')
|
||||
self.assertDictEqual(n.as_dict(), {
|
||||
"id": 0xcaf3,
|
||||
"name": "Something",
|
||||
})
|
||||
|
||||
|
||||
class TestPCIAccessParameter(TestCase):
|
||||
|
||||
|
@ -156,3 +176,11 @@ class TestPCIAccessParameter(TestCase):
|
|||
p2 = PCIAccessParameter('param.name Some description ()')
|
||||
self.assertEqual(p1, p1)
|
||||
self.assertNotEqual(p1, p2)
|
||||
|
||||
def test_as_dict(self) -> None:
|
||||
p = PCIAccessParameter('param.name Some description (default value)')
|
||||
self.assertDictEqual(p.as_dict(), {
|
||||
"name": "param.name",
|
||||
"description": "Some description",
|
||||
"default": "default value",
|
||||
})
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
from unittest import TestCase
|
||||
from pylspci.filters import SlotFilter, DeviceFilter
|
||||
|
||||
from pylspci.filters import DeviceFilter, SlotFilter
|
||||
|
||||
|
||||
class TestSlotFilter(TestCase):
|
||||
|
||||
def test_empty(self):
|
||||
def test_empty(self) -> None:
|
||||
f = SlotFilter()
|
||||
self.assertIsNone(f.domain)
|
||||
self.assertIsNone(f.bus)
|
||||
|
@ -15,7 +16,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 +24,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 +41,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 +68,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 +76,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 +93,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',
|
||||
)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
flake8>=3.5
|
||||
doc8>=0.8.0
|
||||
Sphinx>=1.8.1
|
||||
coverage>=4.5
|
||||
codecov>=2.0
|
||||
pre-commit>=2.9.2
|
||||
|
|
|
@ -1 +1 @@
|
|||
cached-property>=1.5.1
|
||||
cached-property>=1.5.1
|
||||
|
|
12
setup.cfg
12
setup.cfg
|
@ -1,5 +1,15 @@
|
|||
[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
|
||||
|
||||
[isort]
|
||||
multi_line_output=5
|
||||
|
|
28
setup.py
28
setup.py
|
@ -1,7 +1,8 @@
|
|||
#!/usr/bin/env python3
|
||||
from setuptools import setup, find_packages
|
||||
from typing import List
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
|
||||
def read_requirements(filename: str) -> List[str]:
|
||||
return [req.strip() for req in open(filename)]
|
||||
|
@ -21,21 +22,27 @@ setup(
|
|||
'console_scripts': ['pylspci=pylspci.__main__:main'],
|
||||
},
|
||||
package_data={
|
||||
'': ['*.md', 'LICENSE', 'README'],
|
||||
'': [
|
||||
'VERSION',
|
||||
'LICENSE',
|
||||
'README.rst',
|
||||
'requirements.txt',
|
||||
'requirements-dev.txt',
|
||||
],
|
||||
'pylspci': ['py.typed'],
|
||||
},
|
||||
python_requires='>=3.5',
|
||||
python_requires='>=3.6',
|
||||
install_requires=requirements,
|
||||
extras_require={
|
||||
'dev': dev_requirements,
|
||||
},
|
||||
tests_require=dev_requirements,
|
||||
test_suite='pylspci.tests',
|
||||
license='GNU General Public License 3',
|
||||
description="Simple parser for lspci -mmnn.",
|
||||
long_description=open('README.rst').read(),
|
||||
long_description_content_type='text/x-rst',
|
||||
keywords="lspci parser",
|
||||
url="https://gitlab.com/Lucidiot/pylspci",
|
||||
url="https://tildegit.org/lucidiot/pylspci",
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
|
@ -43,16 +50,21 @@ setup(
|
|||
"Intended Audience :: Developers",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
"Topic :: System :: Hardware",
|
||||
"Topic :: Utilities",
|
||||
"Typing :: Typed",
|
||||
],
|
||||
project_urls={
|
||||
"Source Code": "https://gitlab.com/Lucidiot/pylspci",
|
||||
"GitHub Mirror": "https://github.com/Lucidiot/pylspci",
|
||||
"Homepage": "https://tildegit.org/lucidiot/pylspci",
|
||||
"Changelog": "https://tildegit.org/lucidiot/pylspci/releases",
|
||||
"Documentation": "https://lucidiot.tildepages.org/pylspci/",
|
||||
"Issue tracker": "https://tildegit.org/lucidiot/pylspci/issues",
|
||||
}
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue