Compare commits
61 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 | |
Lucidiot | 657a881112 | |
Lucidiot | d603f17ce4 | |
Lucidiot | 550d42bfb6 | |
Lucidiot | cdf14c22ec | |
Lucidiot | 8ee73aaffb | |
Lucidiot | 7a0906ef93 | |
Lucidiot | f17b7d0509 | |
Lucidiot | e174ed130a | |
Lucidiot | e86c9b06d8 | |
Lucidiot | 39064834c5 | |
Lucidiot | 82a832b69c | |
Lucidiot | a9345daaec | |
Lucidiot | 1b88567ee4 | |
Lucidiot | 5799d5bf96 |
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
Command-line interface
|
||||
======================
|
||||
|
||||
Once the package is installed, an executable called ``pylspci`` should be
|
||||
created by setuptools. You may also use ``python3 -m pylspci`` to call the same
|
||||
script. The CLI mimicks some of lspci's arguments, but due to pylspci's own
|
||||
behavior and due to some features being not implemented, some arguments have
|
||||
been omitted or modified.
|
||||
|
||||
.. code::
|
||||
|
||||
pylspci [-h] [-i PCIIDS] [-p PCIMAP]
|
||||
[-s [[domain:]bus:][device][.function]]
|
||||
[-d [vendor]:[device][:class]]
|
||||
[-v] [-k] [-P] [--name-only | -n | -nn]
|
||||
[-A METHOD | -F FILE | -H1 | -H2] [-O KEY=VALUE]
|
||||
[--json | --raw]
|
||||
|
||||
Options
|
||||
-------
|
||||
|
||||
``-h, --help``
|
||||
Show a help message and exit.
|
||||
``-i, --pci-ids <path>``
|
||||
Path to an alternate file to use as the PCI ID list.
|
||||
|
||||
Maps to ``pciids`` in :func:`lspci() <pylspci.command.lspci>`.
|
||||
``-p, --pci-map <path>``
|
||||
Path to an alternate file to use as the kernel module mapping file.
|
||||
|
||||
Maps to ``pcimap`` in :func:`lspci() <pylspci.command.lspci>`.
|
||||
|
||||
Filters
|
||||
^^^^^^^
|
||||
|
||||
``-s [[domain:]bus:][device][.function]``
|
||||
Filter devices by their slots.
|
||||
Any value can be omitted or set to ``*`` to disable filtering.
|
||||
|
||||
Maps to ``slot_filter`` in :func:`lspci() <pylspci.command.lspci>`.
|
||||
See :class:`SlotFilter <pylspci.filters.SlotFilter>` for more details.
|
||||
``-d [vendor]:[device][:class]``
|
||||
Filter devices by their type.
|
||||
Any value can be omitted or set to ``*`` to disable filtering.
|
||||
|
||||
Maps to ``device_filter`` in :func:`lspci() <pylspci.command.lspci>`.
|
||||
See :class:`DeviceFilter <pylspci.filters.DeviceFilter>` for more details.
|
||||
|
||||
Device data
|
||||
^^^^^^^^^^^
|
||||
|
||||
``-v, --verbose``
|
||||
Display more details about devices.
|
||||
|
||||
Maps to ``verbose`` in :func:`lspci() <pylspci.command.lspci>`.
|
||||
``-k, --kernel-modules``
|
||||
On Linux kernels above 2.6, include kernel drivers handling each device and
|
||||
kernel modules able to handle them. Implies ``-v``.
|
||||
|
||||
Maps to ``include_kernel_drivers``
|
||||
in :func:`lspci() <pylspci.command.lspci>`.
|
||||
``-P, -PP, --bridge-paths``
|
||||
Include PCI bridge paths along with device IDs.
|
||||
|
||||
Maps to ``include_bridge_paths`` in :func:`lspci() <pylspci.command.lspci>`.
|
||||
``--name-only``
|
||||
Only include device names. This is the default.
|
||||
|
||||
Maps to :attr:`NameOnly <pylspci.command.IDResolveOption.NameOnly>` as
|
||||
the ``id_resolve_option`` in :func:`lspci() <pylspci.command.lspci>`.
|
||||
``-n, --id-only``
|
||||
Only include device IDs, without looking for names in the PCI ID file.
|
||||
|
||||
Maps to :attr:`IDOnly <pylspci.command.IDResolveOption.IDOnly>` as
|
||||
the ``id_resolve_option`` in :func:`lspci() <pylspci.command.lspci>`.
|
||||
``-nn, --name-with-id``
|
||||
Include both device IDs and names.
|
||||
|
||||
Maps to :attr:`Both <pylspci.command.IDResolveOption.Both>` as
|
||||
the ``id_resolve_option`` in :func:`lspci() <pylspci.command.lspci>`.
|
||||
|
||||
Output modes
|
||||
^^^^^^^^^^^^
|
||||
|
||||
``--json``
|
||||
Parse the lspci output and return a JSON list. This is the default.
|
||||
|
||||
Will automatically select the best parser depending on the chosen settings:
|
||||
|
||||
* In verbose mode, uses an instance of
|
||||
:class:`VerboseParser <pylspci.parsers.VerboseParser>` and returns a list
|
||||
of objects corresponding to :class:`Device <pylspci.device.Device>`
|
||||
instances.
|
||||
* In non-verbose mode, uses an instance of
|
||||
:class:`SimpleParser <pylspci.parsers.SimpleParser>` and returns a list of
|
||||
objects corresponding to :class:`Device <pylspci.device.Device>` instances.
|
||||
* ``-Ahelp`` will always return a list of strings.
|
||||
* ``-Ohelp`` returns a list of objects for each parameter,
|
||||
with its name, description and default values.
|
||||
See :class:`PCIAccessParameter <pylspci.fields.PCIAccessParameter>`.
|
||||
``--raw``
|
||||
Return lspci's output directly, without parsing; the CLI then just becomes a
|
||||
thin layer of argument parsing before lspci.
|
||||
|
||||
PCI access
|
||||
^^^^^^^^^^
|
||||
|
||||
``-O, --option <key>=<value>``
|
||||
Set PCI library access parameters.
|
||||
|
||||
Maps to ``pcilib_params`` in :func:`lspci() <pylspci.command.lspci>`.
|
||||
|
||||
Use ``-O help`` to get a list of available parameters, via
|
||||
:func:`list_pcilib_params() <pylspci.command.list_pcilib_params>`.
|
||||
``-A, --access-method <method>``
|
||||
PCI library access method to use.
|
||||
|
||||
Maps to ``access_method`` in :func:`lspci() <pylspci.command.lspci>`.
|
||||
|
||||
Use ``-A help`` to list available access methods, via
|
||||
:func:`list_access_methods() <pylspci.command.list_access_methods>`.
|
||||
``-F, --file <path>``
|
||||
Use a hex dump file from a previous run of lspci instead of accessing
|
||||
real hardware. Implies ``-Adump``.
|
||||
|
||||
Maps to ``file`` in :func:`lspci() <pylspci.command.lspci>`.
|
||||
``-H1``
|
||||
Access hardware using Intel configuration mechanism 1.
|
||||
Alias to ``-A intel-conf1``.
|
||||
``-H2``
|
||||
Access hardware using Intel configuration mechanism 2.
|
||||
Alias to ``-A intel-conf2``.
|
|
@ -6,3 +6,19 @@ Helpers to call ``lspci`` in a more Pythonic way.
|
|||
.. automodule:: pylspci.command
|
||||
:members:
|
||||
:undoc-members:
|
||||
:exclude-members: CommandBuilder
|
||||
|
||||
Command builder
|
||||
---------------
|
||||
|
||||
.. autoclass:: pylspci.command.CommandBuilder
|
||||
:members:
|
||||
:undoc-members:
|
||||
:special-members: __iter__
|
||||
|
||||
Device selection
|
||||
----------------
|
||||
|
||||
.. automodule:: pylspci.filters
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
|
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,38 +18,45 @@ 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.
|
||||
|
||||
.. _pciutils: http://mj.ucw.cz/sw/pciutils/
|
||||
|
||||
Basic usage
|
||||
-----------
|
||||
Command-line interface
|
||||
----------------------
|
||||
|
||||
An executable script named ``pylspci`` is available, and acts as a wrapper
|
||||
around ``lspci`` that can produce JSON output. ::
|
||||
|
||||
$ pylspci -nn
|
||||
[{
|
||||
"slot": {"domain": 0, "bus": 0, "device": 1, "function": 3},
|
||||
"device": {"id": 9248, "name": "Name A"},
|
||||
...
|
||||
}]
|
||||
|
||||
See ``pylspci --help`` and the `CLI docs <cli>`_ to learn more.
|
||||
|
||||
Parsing in Python
|
||||
-----------------
|
||||
|
||||
To parse ``lspci -nnmm``, use the
|
||||
:class:`SimpleParser <pylspci.parsers.simple.SimpleParser>`.
|
||||
To parse ``lspci -nnmmvvvk``, use the
|
||||
:class:`VerboseParser <pylspci.parsers.verbose.VerboseParser>`.
|
||||
A :class:`SimpleParser <pylspci.parsers.simple.SimpleParser>` instance is
|
||||
available directly as ``pylspci.parser``.
|
||||
|
||||
.. code:: python
|
||||
|
||||
>>> from pylspci import parser
|
||||
>>> parser.run()
|
||||
[Device(slot=Slot('0000:00:01.c'), name=NameWithID('Name A [2420]'), ...),
|
||||
Device(slot=Slot('0000:00:01.d'), name=NameWithID('Name B [0e54]'), ...)]
|
||||
>>> from pylspci.parsers import SimpleParser
|
||||
>>> 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]'), ...)]
|
||||
|
||||
Custom arguments
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
@ -57,18 +64,19 @@ 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,
|
||||
... )
|
||||
[Device(slot=Slot('0000:00:01.c'), name=NameWithID('Name A'), ...),
|
||||
Device(slot=Slot('0000:00:01.d'), name=NameWithID('Name B'), ...)]
|
||||
[Device(slot=Slot('0000:00:01.3'), name=NameWithID('Name A'), ...),
|
||||
Device(slot=Slot('0000:00:01.4'), name=NameWithID('Name B'), ...)]
|
||||
|
||||
Learn more
|
||||
----------
|
||||
|
||||
.. toctree::
|
||||
cli
|
||||
data
|
||||
command
|
||||
low
|
||||
|
|
|
@ -1,11 +1,205 @@
|
|||
#!/usr/bin/env python3
|
||||
from pylspci.parsers.simple import SimpleParser
|
||||
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:
|
||||
parser: argparse.ArgumentParser = argparse.ArgumentParser(
|
||||
description='Python wrapper for lspci',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-i', '--pci-ids',
|
||||
help='Path to an alternate file to use as the PCI ID list.',
|
||||
required=False,
|
||||
type=Path,
|
||||
dest='pciids',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-p', '--pci-map',
|
||||
help='Path to an alternate file to use as the '
|
||||
'kernel module mapping file.',
|
||||
required=False,
|
||||
type=Path,
|
||||
dest='pcimap',
|
||||
)
|
||||
|
||||
filter_options = parser.add_argument_group(title='Filters')
|
||||
filter_options.add_argument(
|
||||
'-s',
|
||||
help='Filter devices by their slots. '
|
||||
'Any value can be omitted or set to * to disable filtering.',
|
||||
type=SlotFilter.parse,
|
||||
dest='slot_filter',
|
||||
metavar='[[domain:]bus:][device][.function]',
|
||||
)
|
||||
filter_options.add_argument(
|
||||
'-d',
|
||||
help='Filter devices by their type. '
|
||||
'Any value can be omitted or set to * to disable filtering.',
|
||||
type=DeviceFilter.parse,
|
||||
dest='device_filter',
|
||||
metavar='[vendor]:[device][:class]',
|
||||
)
|
||||
|
||||
output_options = parser.add_argument_group(title='Output options')
|
||||
output_options.add_argument(
|
||||
'-v', '--verbose',
|
||||
help='Display more details about devices.',
|
||||
default=False,
|
||||
action='store_true',
|
||||
dest='verbose',
|
||||
)
|
||||
output_options.add_argument(
|
||||
'-k', '--kernel-modules',
|
||||
help='On Linux kernels above 2.6, include kernel drivers handling '
|
||||
'each device and kernel modules able to handle them. Implies -v.',
|
||||
default=False,
|
||||
action='store_true',
|
||||
dest='kernel_drivers',
|
||||
)
|
||||
output_options.add_argument(
|
||||
'-P', '-PP', '--bridge-paths',
|
||||
help='Include PCI bridge paths along with device IDs.',
|
||||
default=False,
|
||||
action='store_true',
|
||||
dest='bridge_paths',
|
||||
)
|
||||
|
||||
output_modes = output_options.add_mutually_exclusive_group()
|
||||
output_modes.add_argument(
|
||||
'--json',
|
||||
help='Parse the lspci output and return JSON data.',
|
||||
action='store_true',
|
||||
default=True,
|
||||
dest='json',
|
||||
)
|
||||
output_modes.add_argument(
|
||||
'--raw',
|
||||
help="Return lspci's output directly, without parsing.",
|
||||
action='store_false',
|
||||
default=True,
|
||||
dest='json',
|
||||
)
|
||||
|
||||
id_resolve_option = output_options.add_mutually_exclusive_group()
|
||||
id_resolve_option.add_argument(
|
||||
'--name-only',
|
||||
help='Only include device names. This is the default.',
|
||||
action='store_const',
|
||||
default=IDResolveOption.NameOnly,
|
||||
const=IDResolveOption.NameOnly,
|
||||
dest='id_resolve_option',
|
||||
)
|
||||
id_resolve_option.add_argument(
|
||||
'-n', '--id-only',
|
||||
help='Only include device IDs, without looking for names '
|
||||
'in the PCI ID file.',
|
||||
action='store_const',
|
||||
const=IDResolveOption.IDOnly,
|
||||
default=IDResolveOption.NameOnly,
|
||||
dest='id_resolve_option',
|
||||
)
|
||||
id_resolve_option.add_argument(
|
||||
'-nn', '--name-with-id',
|
||||
help='Include both device IDs and names.',
|
||||
action='store_const',
|
||||
const=IDResolveOption.Both,
|
||||
default=IDResolveOption.NameOnly,
|
||||
dest='id_resolve_option',
|
||||
)
|
||||
|
||||
access_options = parser.add_argument_group(title='PCI access options')
|
||||
access_options.add_argument(
|
||||
'-O', '--option',
|
||||
help='Set PCI library access parameters. '
|
||||
'Use -O help to get a list of available parameters '
|
||||
'with their descriptions and default values.',
|
||||
action='append',
|
||||
dest='pcilib_params',
|
||||
metavar='KEY=VALUE',
|
||||
)
|
||||
access_exclusive = access_options.add_mutually_exclusive_group()
|
||||
access_exclusive.add_argument(
|
||||
'-A', '--access-method',
|
||||
help='PCI library access method to use. '
|
||||
'Use -A help to list available access methods.',
|
||||
dest='access_method',
|
||||
metavar='METHOD',
|
||||
)
|
||||
access_exclusive.add_argument(
|
||||
'-F', '--file',
|
||||
help='Use a hex dump file from a previous run of lspci instead of '
|
||||
'accessing real hardware.',
|
||||
dest='file',
|
||||
metavar='FILE',
|
||||
)
|
||||
access_exclusive.add_argument(
|
||||
'-H1',
|
||||
help='Access hardware using Intel configuration mechanism 1. '
|
||||
'Alias to -A intel-conf1.',
|
||||
action='store_const',
|
||||
const='intel-conf1',
|
||||
dest='access_method',
|
||||
)
|
||||
access_exclusive.add_argument(
|
||||
'-H2',
|
||||
help='Access hardware using Intel configuration mechanism 2. '
|
||||
'Alias to -A intel-conf2.',
|
||||
action='store_const',
|
||||
const='intel-conf2',
|
||||
dest='access_method',
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser: argparse.ArgumentParser = get_parser()
|
||||
args: Dict[str, Any] = vars(parser.parse_args())
|
||||
|
||||
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 = args.pop('pcilib_params', []) or []
|
||||
|
||||
builder: CommandBuilder = CommandBuilder(**args)
|
||||
if kernel_modules:
|
||||
builder = builder.include_kernel_drivers()
|
||||
|
||||
if access_method:
|
||||
if access_method.strip().lower() == 'help':
|
||||
builder = builder.list_access_methods()
|
||||
else:
|
||||
builder = builder.use_access_method(access_method)
|
||||
|
||||
for param in pcilib_params:
|
||||
if param.strip().lower() == 'help':
|
||||
builder = builder.list_pcilib_params(raw=not json_output)
|
||||
break
|
||||
if '=' not in param:
|
||||
parser.error(
|
||||
'Invalid PCI access parameter syntax for {!r}'.format(param))
|
||||
key, value = map(str.strip, param.split('=', 2))
|
||||
builder = builder.with_pcilib_params(**{key: value})
|
||||
|
||||
if json_output:
|
||||
builder = builder.with_default_parser()
|
||||
|
||||
result = list(builder)
|
||||
if not json_output: # Raw mode
|
||||
for item in result:
|
||||
print(item)
|
||||
return
|
||||
|
||||
print(json.dumps([
|
||||
item if isinstance(item, str) else item.as_dict()
|
||||
for item in result
|
||||
]))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(json.dumps(
|
||||
list(map(lambda d: d._asdict(), SimpleParser().run())),
|
||||
indent=4,
|
||||
default=vars,
|
||||
))
|
||||
main()
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
from enum import Enum
|
||||
from typing import Optional, Union, List, Mapping, Any
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from enum import Enum
|
||||
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 DeviceFilter, SlotFilter
|
||||
from pylspci.parsers.base import Parser
|
||||
|
||||
OptionalPath = Optional[Union[str, Path]]
|
||||
|
||||
|
@ -36,8 +43,11 @@ def lspci(
|
|||
file: OptionalPath = None,
|
||||
verbose: bool = False,
|
||||
kernel_drivers: bool = False,
|
||||
bridge_paths: bool = False,
|
||||
hide_single_domain: bool = True,
|
||||
id_resolve_option: IDResolveOption = IDResolveOption.Both,
|
||||
slot_filter: Optional[Union[SlotFilter, str]] = None,
|
||||
device_filter: Optional[Union[DeviceFilter, str]] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Call the ``lspci`` command with various parameters.
|
||||
|
@ -49,8 +59,14 @@ def lspci(
|
|||
linking Linux kernel modules and their supported PCI IDs.
|
||||
:type pcimap: str or Path or None
|
||||
:param access_method: The access method to use to find devices.
|
||||
Set this to ``help`` to list the available access methods.
|
||||
Set this to ``help`` to list the available access methods in a
|
||||
human-readable format. For the machine-readable format, see
|
||||
:func:`list_access_methods`.
|
||||
:type access_method: str or None
|
||||
:param pcilib_params: Parameters passed to pcilib's access methods.
|
||||
To list the available parameters with their description and default
|
||||
values, see :func:`list_pcilib_params`.
|
||||
:type pcilib_params: Mapping[str, Any] or None
|
||||
:param file: An hexadecimal dump from ``lspci -x`` to load data from,
|
||||
instead of accessing real hardware.
|
||||
:type file: str or Path or None
|
||||
|
@ -58,11 +74,17 @@ def lspci(
|
|||
This radically changes the output format.
|
||||
:param bool kernel_drivers: Also include kernel modules and drivers
|
||||
in the output. Only has effect with the verbose output.
|
||||
:param bool bridge_paths: Add PCI bridge paths to slot numbers.
|
||||
:param bool hide_single_domain: If there is a single PCI domain on this
|
||||
machine and it is numbered ``0000``, hide it from the slot numbers.
|
||||
:param id_resolve_option: Device, vendor or class ID outputting mode.
|
||||
See the :class:`IDResolveOption` docs for more details.
|
||||
:type id_resolve_option: IDResolveOption
|
||||
:param slot_filter: Filter devices by their slot
|
||||
(domain, bus, device, function)
|
||||
:type slot_filter: SlotFilter or str or None
|
||||
:param device_filter: Filter devices by their vendor, device or class ID
|
||||
:type device_filter: DeviceFilter or str or None
|
||||
:return: Any output from the ``lspci`` command.
|
||||
:rtype: str
|
||||
:raises subprocess.CalledProcessError:
|
||||
|
@ -73,10 +95,22 @@ def lspci(
|
|||
args.append('-vvv')
|
||||
if kernel_drivers:
|
||||
args.append('-k')
|
||||
if bridge_paths:
|
||||
args.append('-PP')
|
||||
if not hide_single_domain:
|
||||
args.append('-D')
|
||||
if access_method:
|
||||
args.append('-A{}'.format(access_method))
|
||||
if slot_filter:
|
||||
if isinstance(slot_filter, str):
|
||||
slot_filter = SlotFilter.parse(slot_filter)
|
||||
args.append('-s')
|
||||
args.append(str(slot_filter))
|
||||
if device_filter:
|
||||
if isinstance(device_filter, str):
|
||||
device_filter = DeviceFilter.parse(device_filter)
|
||||
args.append('-d')
|
||||
args.append(str(device_filter))
|
||||
if id_resolve_option != IDResolveOption.NameOnly:
|
||||
args.append(id_resolve_option.value)
|
||||
|
||||
|
@ -108,3 +142,396 @@ def lspci(
|
|||
args,
|
||||
universal_newlines=True,
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
a machine-readable list.
|
||||
|
||||
:returns: A list of access methods.
|
||||
:rtype: List[str]
|
||||
:raises subprocess.CalledProcessError:
|
||||
``lspci`` returned a non-zero error code.
|
||||
"""
|
||||
return list(filter(
|
||||
lambda line: line and 'Known PCI access methods' not in line,
|
||||
map(str.strip, lspci(access_method='help').splitlines()),
|
||||
))
|
||||
|
||||
|
||||
def list_pcilib_params_raw() -> List[str]:
|
||||
"""
|
||||
Calls ``lspci -Ohelp`` to list the PCI access parameters the underlying
|
||||
``pcilib`` provides.
|
||||
|
||||
:returns: A list of available PCI access parameters.
|
||||
:rtype: List[str]
|
||||
:raises subprocess.CalledProcessError:
|
||||
``lspci`` returned a non-zero error code.
|
||||
"""
|
||||
return list(filter(
|
||||
lambda line: line and 'Known PCI access parameters' not in line,
|
||||
map(str.strip, subprocess.check_output(
|
||||
['lspci', '-Ohelp'],
|
||||
universal_newlines=True,
|
||||
).splitlines())
|
||||
))
|
||||
|
||||
|
||||
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
|
||||
a machine-readable list.
|
||||
|
||||
:returns: A list of available PCI access parameters.
|
||||
:rtype: List[PCIAccessParameter]
|
||||
:raises subprocess.CalledProcessError:
|
||||
``lspci`` returned a non-zero error code.
|
||||
"""
|
||||
return list(map(PCIAccessParameter, list_pcilib_params_raw()))
|
||||
|
||||
|
||||
class CommandBuilder(object):
|
||||
"""
|
||||
Helper class to build a lspci call using a Builder pattern.
|
||||
|
||||
Iterating over the builder will result in the command being called,
|
||||
and will return strings, devices or pcilib parameters, one at a time,
|
||||
depending on the parsing settings.
|
||||
"""
|
||||
|
||||
_list_access_methods: bool = False
|
||||
_list_pcilib_params: bool = False
|
||||
_list_pcilib_params_raw: bool = False
|
||||
_params: MutableMapping[str, Any] = {}
|
||||
_parser: Optional[Parser] = None
|
||||
|
||||
def __init__(self, **kwargs: Any):
|
||||
self._params = kwargs
|
||||
|
||||
def __iter__(self) -> Iterator[Union[str, Device, PCIAccessParameter]]:
|
||||
result: Union[str, List[str], List[Device], List[PCIAccessParameter]]
|
||||
if self._list_access_methods:
|
||||
result = list_access_methods()
|
||||
elif self._list_pcilib_params:
|
||||
if self._list_pcilib_params_raw:
|
||||
result = list_pcilib_params_raw()
|
||||
else:
|
||||
result = list_pcilib_params()
|
||||
elif self._parser:
|
||||
result = self._parser.parse(lspci(**self._params))
|
||||
else:
|
||||
result = lspci(**self._params)
|
||||
|
||||
if isinstance(result, str):
|
||||
return iter([result, ])
|
||||
return iter(result)
|
||||
|
||||
def use_pciids(self,
|
||||
path: OptionalPath,
|
||||
check: bool = True) -> 'CommandBuilder':
|
||||
"""
|
||||
Use a PCI IDs file from a given path.
|
||||
|
||||
:param path: A string or path-like object pointing to the PCI IDs file
|
||||
to use. Set to None to use the default files from lspci.
|
||||
:type path: str or Path or None
|
||||
:param bool check: Whether to check for the file's existence
|
||||
immediately, or delay that to the lspci invocation.
|
||||
:returns: The current CommandBuilder instance.
|
||||
:rtype: CommandBuilder
|
||||
"""
|
||||
if path:
|
||||
if not isinstance(path, Path):
|
||||
path = Path(path)
|
||||
if check:
|
||||
assert path.is_file(), 'ID database file not found'
|
||||
self._params['pciids'] = path
|
||||
return self
|
||||
|
||||
def use_pcimap(self,
|
||||
path: OptionalPath,
|
||||
check: bool = True) -> 'CommandBuilder':
|
||||
"""
|
||||
Use a kernel module mapping file from a given path.
|
||||
|
||||
:param path: A string or path-like object pointing to the mapping file
|
||||
to use. Set to None to use the default files from lspci.
|
||||
:type path: str or Path or None
|
||||
:param bool check: Whether to check for the file's existence
|
||||
immediately, or delay that to the lspci invocation.
|
||||
:returns: The current CommandBuilder instance.
|
||||
:rtype: CommandBuilder
|
||||
"""
|
||||
if path:
|
||||
if not isinstance(path, Path):
|
||||
path = Path(path)
|
||||
if check:
|
||||
assert path.is_file(), 'Kernel module mapping file not found'
|
||||
self._params['pcimap'] = path
|
||||
return self
|
||||
|
||||
def use_access_method(self, method: Optional[str]) -> 'CommandBuilder':
|
||||
"""
|
||||
Use a specific access method to list all devices.
|
||||
|
||||
:param method: Name of the access method to use. Set to None to use
|
||||
lspci's default method.
|
||||
:type method: str or None
|
||||
:returns: The current CommandBuilder instance.
|
||||
:rtype: CommandBuilder
|
||||
"""
|
||||
self._params['access_method'] = method
|
||||
return self
|
||||
|
||||
def list_access_methods(self, value: bool = True) -> 'CommandBuilder':
|
||||
"""
|
||||
List the pcilib access methods instead of listing devices.
|
||||
|
||||
:param value: Whether or not to list the access methods.
|
||||
:type value: bool
|
||||
:returns: The current CommandBuilder instance.
|
||||
:rtype: CommandBuilder
|
||||
"""
|
||||
self._list_access_methods = value
|
||||
if value:
|
||||
self._list_pcilib_params = False
|
||||
return self
|
||||
|
||||
def list_pcilib_params(self,
|
||||
value: bool = True,
|
||||
raw: bool = False) -> 'CommandBuilder':
|
||||
"""
|
||||
List the pcilib parameters instead of listing devices.
|
||||
|
||||
:param value: Whether or not to list the pcilib parameters.
|
||||
:type value: bool
|
||||
:param raw: When listing the pcilib parameters, whether to return the
|
||||
raw strings or parse them into PCIAccessParameter instances.
|
||||
:type raw: bool
|
||||
:returns: The current CommandBuilder instance.
|
||||
:rtype: CommandBuilder
|
||||
"""
|
||||
self._list_pcilib_params = value
|
||||
self._list_pcilib_params_raw = raw
|
||||
if value:
|
||||
self._list_access_methods = False
|
||||
return self
|
||||
|
||||
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
|
||||
the existing parameters. Pass ``None`` or ``{}`` to reset all of the
|
||||
parameters.
|
||||
|
||||
:returns: The current CommandBuilder instance.
|
||||
:rtype: CommandBuilder
|
||||
"""
|
||||
if len(args) > 0:
|
||||
assert len(args) <= 1, 'Only one positional argument is allowed'
|
||||
assert not kwargs, 'Use either a dict or keyword arguments'
|
||||
self._params['pcilib_params'] = args[0]
|
||||
return self
|
||||
self._params.setdefault('pcilib_params', {}).update(kwargs)
|
||||
return self
|
||||
|
||||
def from_file(self,
|
||||
path: OptionalPath,
|
||||
check: bool = True) -> 'CommandBuilder':
|
||||
"""
|
||||
Use a hexadecimal dump from a previous run of lspci instead of
|
||||
accessing the host's devices directly.
|
||||
|
||||
:param path: A string or path-like object pointing to the hex dump file
|
||||
to use. Set to None to not use a dump file.
|
||||
:type path: str or Path or None
|
||||
:param bool check: Whether to check for the file's existence
|
||||
immediately, or delay that to the lspci invocation.
|
||||
:returns: The current CommandBuilder instance.
|
||||
:rtype: CommandBuilder
|
||||
"""
|
||||
if path:
|
||||
if not isinstance(path, Path):
|
||||
path = Path(path)
|
||||
if check:
|
||||
assert path.is_file(), 'Hex dump file not found'
|
||||
self._params['file'] = path
|
||||
return self
|
||||
|
||||
def verbose(self, value: bool = True) -> 'CommandBuilder':
|
||||
"""
|
||||
Enable verbose mode.
|
||||
|
||||
:param value: Whether or not to use verbose mode.
|
||||
:type value: bool
|
||||
:returns: The current CommandBuilder instance.
|
||||
:rtype: CommandBuilder
|
||||
"""
|
||||
self._params['verbose'] = value
|
||||
return self
|
||||
|
||||
def include_kernel_drivers(self, value: bool = True) -> 'CommandBuilder':
|
||||
"""
|
||||
Under Linux, includes the available kernel modules for each device.
|
||||
Implies ``.verbose()``.
|
||||
|
||||
:param value: Whether or not to include the available kernel modules
|
||||
for each device.
|
||||
:type value: bool
|
||||
:returns: The current CommandBuilder instance.
|
||||
:rtype: CommandBuilder
|
||||
"""
|
||||
self._params['kernel_drivers'] = value
|
||||
if value:
|
||||
return self.verbose()
|
||||
return self
|
||||
|
||||
def include_bridge_paths(self, value: bool = True) -> 'CommandBuilder':
|
||||
"""
|
||||
Include the PCI bridge paths along with the IDs.
|
||||
Implies ``.with_ids()``.
|
||||
|
||||
:param value: Whether or not to include the PCI bridge paths.
|
||||
:type value: bool
|
||||
:returns: The current CommandBuilder instance.
|
||||
:rtype: CommandBuilder
|
||||
"""
|
||||
self._params['bridge_paths'] = value
|
||||
if value:
|
||||
return self.with_ids()
|
||||
return self
|
||||
|
||||
def hide_single_domain(self, value: bool = True) -> 'CommandBuilder':
|
||||
"""
|
||||
Hide the domain numbers when there is only one domain, numbered zero.
|
||||
|
||||
:param value: Whether or not to hide the domain numbers for a single
|
||||
domain.
|
||||
:type value: bool
|
||||
:returns: The current CommandBuilder instance.
|
||||
:rtype: CommandBuilder
|
||||
"""
|
||||
self._params['hide_single_domain'] = value
|
||||
return self
|
||||
|
||||
def with_ids(self, value: bool = True) -> 'CommandBuilder':
|
||||
"""
|
||||
Include PCI device IDs. If disabled, implies ``.with_names()``.
|
||||
|
||||
:param value: Whether or not to include the PCI device IDs.
|
||||
:type value: bool
|
||||
:returns: The current CommandBuilder instance.
|
||||
:rtype: CommandBuilder
|
||||
"""
|
||||
if value:
|
||||
if self._params.get('id_resolve_option') == \
|
||||
IDResolveOption.NameOnly:
|
||||
self._params['id_resolve_option'] = IDResolveOption.Both
|
||||
else:
|
||||
self._params['id_resolve_option'] = IDResolveOption.NameOnly
|
||||
return self
|
||||
|
||||
def with_names(self, value: bool = True) -> 'CommandBuilder':
|
||||
"""
|
||||
Include PCI device names. If disabled, implies ``.with_ids()``.
|
||||
|
||||
:param value: Whether or not to include the PCI device names.
|
||||
:type value: bool
|
||||
:returns: The current CommandBuilder instance.
|
||||
:rtype: CommandBuilder
|
||||
"""
|
||||
if value:
|
||||
if self._params.get('id_resolve_option') == IDResolveOption.IDOnly:
|
||||
self._params['id_resolve_option'] = IDResolveOption.Both
|
||||
else:
|
||||
self._params['id_resolve_option'] = IDResolveOption.IDOnly
|
||||
return self
|
||||
|
||||
def slot_filter(self,
|
||||
*args: str,
|
||||
domain: Optional[int] = None,
|
||||
bus: Optional[int] = None,
|
||||
device: Optional[int] = None,
|
||||
function: Optional[int] = None) -> 'CommandBuilder':
|
||||
"""
|
||||
Filter the devices geographically.
|
||||
Can be passed a string in lspci's filter syntax, or keyword arguments
|
||||
for each portion of the filter.
|
||||
See :class:`pylspci.filters.SlotFilter`.
|
||||
|
||||
:returns: The current CommandBuilder instance.
|
||||
:rtype: CommandBuilder
|
||||
"""
|
||||
if len(args) > 0:
|
||||
assert len(args) <= 1, 'Only one positional argument allowed'
|
||||
assert not domain and not bus and not device and not function, \
|
||||
'Use either a string value or the domain, bus, device ' \
|
||||
'and function keyword arguments'
|
||||
self._params['slot_filter'] = SlotFilter.parse(args[0])
|
||||
else:
|
||||
self._params['slot_filter'] = SlotFilter(
|
||||
domain=domain,
|
||||
bus=bus,
|
||||
device=device,
|
||||
function=function,
|
||||
)
|
||||
return self
|
||||
|
||||
def device_filter(self,
|
||||
*args: str,
|
||||
cls: Optional[int] = None,
|
||||
vendor: Optional[int] = None,
|
||||
device: Optional[int] = None) -> 'CommandBuilder':
|
||||
"""
|
||||
Filter the devices logically.
|
||||
Can be passed a string in lspci's filter syntax, or keyword arguments
|
||||
for each portion of the filter.
|
||||
See :class:`pylspci.filters.DeviceFilter`.
|
||||
|
||||
:returns: The current CommandBuilder instance.
|
||||
:rtype: CommandBuilder
|
||||
"""
|
||||
if len(args) > 0:
|
||||
assert len(args) <= 1, 'Only one positional argument allowed'
|
||||
assert not cls and not vendor and not device, \
|
||||
'Use either a string value or the cls, vendor and device ' \
|
||||
'keyword arguments'
|
||||
self._params['device_filter'] = DeviceFilter.parse(args[0])
|
||||
else:
|
||||
self._params['device_filter'] = \
|
||||
DeviceFilter(cls=cls, vendor=vendor, device=device)
|
||||
return self
|
||||
|
||||
def with_parser(self, parser: Optional[Parser] = None) -> 'CommandBuilder':
|
||||
"""
|
||||
Use a pylspci parser to get parsed Device instances instead of strings.
|
||||
|
||||
:param parser: The parser to use. Set to None to disable parsing.
|
||||
:type parser: pylspci.parsers.Parser
|
||||
:returns: The current CommandBuilder instance.
|
||||
:rtype: CommandBuilder
|
||||
"""
|
||||
self._parser = parser
|
||||
return self
|
||||
|
||||
def with_default_parser(self) -> 'CommandBuilder':
|
||||
"""
|
||||
Use the default parser compatible with the current set of settings.
|
||||
Note that this should be used as one of the last instructions of the
|
||||
builder, as the default parser can change if the settings are updated.
|
||||
|
||||
:returns: The current CommandBuilder instance.
|
||||
:rtype: CommandBuilder
|
||||
"""
|
||||
if self._params.get('verbose'):
|
||||
from pylspci.parsers import VerboseParser
|
||||
return self.with_parser(VerboseParser())
|
||||
else:
|
||||
from pylspci.parsers import SimpleParser
|
||||
return self.with_parser(SimpleParser())
|
||||
|
|
|
@ -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
|
||||
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,45 +22,66 @@ 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.
|
||||
|
||||
:type: int
|
||||
The slot's device, as a two-digit hexadecimal number, up to `0x1f`.
|
||||
"""
|
||||
|
||||
function: int
|
||||
"""
|
||||
The slot's function, as a single octal digit.
|
||||
"""
|
||||
|
||||
:type: int
|
||||
parent: Optional["Slot"] = None
|
||||
"""
|
||||
The slot's parent bridge, if present.
|
||||
"""
|
||||
|
||||
def __init__(self, value: str) -> None:
|
||||
data = list(map(hexstring, re.split(r'[:\.]', value)))
|
||||
parent, _, me = value.rpartition('/')
|
||||
if parent:
|
||||
self.parent = Slot(parent)
|
||||
|
||||
data = list(map(hexstring, re.split(r'[:\.]', me)))
|
||||
if len(data) == 3:
|
||||
data.insert(0, 0)
|
||||
data.insert(0, self.parent.domain if self.parent else 0)
|
||||
self.domain, self.bus, self.device, self.function = data
|
||||
|
||||
if self.device > 0x1f:
|
||||
raise ValueError('Device numbers cannot be above 0x1f')
|
||||
if self.function > 0x7:
|
||||
raise ValueError('Function numbers cannot be above 7')
|
||||
|
||||
def __str__(self) -> str:
|
||||
return '{:04x}:{:02x}:{:02x}.{:01x}'.format(
|
||||
output: str = '{:04x}:{:02x}:{:02x}.{:01x}'.format(
|
||||
self.domain, self.bus, self.device, self.function,
|
||||
)
|
||||
if self.parent:
|
||||
return '{!s}/{}'.format(self.parent, output)
|
||||
return output
|
||||
|
||||
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):
|
||||
"""
|
||||
|
@ -67,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})\]$')
|
||||
|
@ -83,7 +104,12 @@ class NameWithID(object):
|
|||
def __init__(self, value: Optional[str]) -> None:
|
||||
if value and value.endswith(']'):
|
||||
# Holds both an ID and a name
|
||||
gd = self._NAME_ID_REGEX.match(value).groupdict()
|
||||
match = self._NAME_ID_REGEX.match(value)
|
||||
if not match: # Except it doesn't
|
||||
self.id = None
|
||||
self.name = value
|
||||
return
|
||||
gd = match.groupdict()
|
||||
self.id = hexstring(gd['id'])
|
||||
self.name = gd['name']
|
||||
return
|
||||
|
@ -107,3 +133,69 @@ 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):
|
||||
"""
|
||||
A pcilib access method parameter, as parsed from :func:`list_pcilib_params`
|
||||
or ``lspci -Ohelp``, that can be modified using the ``pcilib_params``
|
||||
argument of :func:`lspci`, or ``lspci -Oname=value`` in the command line.
|
||||
"""
|
||||
|
||||
name: str
|
||||
"""
|
||||
The parameter's name.
|
||||
"""
|
||||
|
||||
description: str
|
||||
"""
|
||||
A short description of the parameter's use.
|
||||
"""
|
||||
|
||||
default: Optional[str]
|
||||
"""
|
||||
An optional default value for the parameter.
|
||||
"""
|
||||
|
||||
_PARAM_REGEX = re.compile(
|
||||
r'^(?P<name>\S+)\s+(?P<description>.+)\s\((?P<default>.*)\)$')
|
||||
|
||||
def __init__(self, value: str) -> None:
|
||||
match = self._PARAM_REGEX.match(value)
|
||||
if not match:
|
||||
raise ValueError(
|
||||
'Could not parse {!r} into a parameter'.format(value))
|
||||
gd = match.groupdict()
|
||||
self.name = gd['name'].strip()
|
||||
self.description = gd['description'].strip()
|
||||
self.default = gd['default'].strip() or None
|
||||
|
||||
def __str__(self) -> str:
|
||||
return '{}\t{} ({})'.format(self.name, self.description, self.default)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return '{!s}({!r})'.format(self.__class__.__name__, str(self))
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
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,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
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):
|
||||
|
||||
_REGEX: ClassVar[Pattern]
|
||||
|
||||
@classmethod
|
||||
def parse(cls: Type[T], value: str) -> T:
|
||||
if not value:
|
||||
return cls()
|
||||
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}
|
||||
if not match or not data:
|
||||
raise ValueError('Value is not a valid filter string')
|
||||
return cls(**{
|
||||
k: hexstring(v)
|
||||
for k, v in data.items()
|
||||
if v != '' and v != '*'
|
||||
})
|
||||
|
||||
@abstractmethod
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
"Create a filter."
|
||||
|
||||
|
||||
class SlotFilter(Filter):
|
||||
"""
|
||||
Describes a slot filter, to filter devices geographically.
|
||||
|
||||
Any field set to ``None`` will remove filtering.
|
||||
"""
|
||||
|
||||
domain: Optional[int] = None
|
||||
"""
|
||||
Device domain, as a four-digit hexadecimal number.
|
||||
"""
|
||||
|
||||
bus: Optional[int] = None
|
||||
"""
|
||||
Device bus, as a two-digit hexadecimal number.
|
||||
"""
|
||||
|
||||
device: Optional[int] = None
|
||||
"""
|
||||
Device number, as a two-digit hexadecimal number, up to `0x1f`.
|
||||
"""
|
||||
|
||||
function: Optional[int] = None
|
||||
"""
|
||||
The slot's function, as a single octal digit.
|
||||
"""
|
||||
|
||||
# [[domain:]bus:][device][.function]
|
||||
_REGEX: ClassVar[Pattern] = re.compile(
|
||||
r'^(?:(?:(?P<domain>(?:[0-9a-f]{1,4}|\*?)):)?'
|
||||
r'(?P<bus>(?:[0-9a-f]{1,2}|\*?)):)?'
|
||||
r'(?P<device>(?:[01]?[0-9a-f]|\*?))?'
|
||||
r'(?:\.(?P<function>(?:[0-7]|\*?)))?$'
|
||||
)
|
||||
|
||||
def __init__(self, *,
|
||||
domain: Optional[int] = None,
|
||||
bus: Optional[int] = None,
|
||||
device: Optional[int] = None,
|
||||
function: Optional[int] = None,
|
||||
):
|
||||
self.domain = domain
|
||||
self.bus = bus
|
||||
self.device = device
|
||||
self.function = function
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return '{}(domain={!r}, bus={!r}, device={!r}, function={!r})'.format(
|
||||
self.__class__.__name__,
|
||||
self.domain, self.bus, self.device, self.function,
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return '{}:{}:{}.{}'.format(*map(
|
||||
lambda x: '{:x}'.format(x) if x is not None else '',
|
||||
(self.domain, self.bus, self.device, self.function),
|
||||
))
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
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):
|
||||
"""
|
||||
Describes a device filter, to filter devices logically.
|
||||
|
||||
Any field set to ``None`` will remove filtering.
|
||||
"""
|
||||
|
||||
cls: Optional[int] = None
|
||||
"""
|
||||
Device class ID, as a four-digit hexadecimal number.
|
||||
"""
|
||||
|
||||
vendor: Optional[int] = None
|
||||
"""
|
||||
Device vendor ID, as a four-digit hexadecimal number.
|
||||
"""
|
||||
|
||||
device: Optional[int] = None
|
||||
"""
|
||||
Device ID, as a four-digit hexadecimal number.
|
||||
"""
|
||||
|
||||
# [vendor]:[device][:class]
|
||||
_REGEX: ClassVar[Pattern] = re.compile(
|
||||
r'^(?P<vendor>(?:[0-9a-f]{1,4}|\*?)):'
|
||||
r'(?P<device>(?:[0-9a-f]{1,4}|\*?))'
|
||||
r'(?::(?P<cls>(?:[0-9a-f]{1,4}|\*?))?)?$'
|
||||
)
|
||||
|
||||
def __init__(self, *,
|
||||
cls: Optional[int] = None,
|
||||
vendor: Optional[int] = None,
|
||||
device: Optional[int] = None,
|
||||
):
|
||||
self.cls = cls
|
||||
self.vendor = vendor
|
||||
self.device = device
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return '{}(cls={!r}, vendor={!r}, device={!r})'.format(
|
||||
self.__class__.__name__, self.cls, self.vendor, self.device,
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return ':'.join(map(
|
||||
lambda x: '{:x}'.format(x) if x is not None else '',
|
||||
(self.vendor, self.device, self.cls),
|
||||
))
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
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, 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,29 +15,35 @@ class Parser(ABC):
|
|||
"""
|
||||
|
||||
@abstractmethod
|
||||
def parse(self, data: Union[str, List[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 List[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.
|
||||
|
||||
:param **kwargs: Optional arguments to override the parser's default
|
||||
: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
|
||||
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, List[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 List[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]
|
||||
"""
|
||||
|
@ -67,16 +75,24 @@ class SimpleParser(Parser):
|
|||
data = data.splitlines()
|
||||
return list(map(self.parse_line, data))
|
||||
|
||||
def parse_line(self, args: Union[str, List[str]]) -> Device:
|
||||
def parse_line(self, args: Union[str, Iterable[str]]) -> Device:
|
||||
"""
|
||||
Parse a single line from lspci -mm into a single device, either
|
||||
as the line or as a list of fields.
|
||||
|
||||
:param args: Line or list of fields to parse from.
|
||||
:type args: str or List[str]
|
||||
:type args: str or Iterable[str]
|
||||
:return: A single parsed device.
|
||||
:rtype: Device
|
||||
"""
|
||||
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, 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, List[str]]) -> Device:
|
||||
devdict = {}
|
||||
def _parse_device(self, device_data: Union[str, Iterable[str]]) -> Device:
|
||||
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, List[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 List[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,0 +1,381 @@
|
|||
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
|
||||
|
||||
|
||||
class TestCommandBuilder(TestCase):
|
||||
|
||||
@patch('pylspci.command.lspci')
|
||||
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: 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
|
||||
for each character of the string
|
||||
"""
|
||||
lspci_mock.return_value = 'a\nb'
|
||||
self.assertListEqual(list(CommandBuilder()), ['a\nb', ])
|
||||
self.assertEqual(lspci_mock.call_count, 1)
|
||||
self.assertEqual(lspci_mock.call_args, call())
|
||||
|
||||
@patch('pylspci.command.Path.is_file')
|
||||
@patch('pylspci.command.lspci')
|
||||
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')
|
||||
self.assertListEqual(list(builder), ['a', 'b'])
|
||||
self.assertEqual(lspci_mock.call_count, 1)
|
||||
self.assertEqual(lspci_mock.call_args, call(pciids=Path('somefile')))
|
||||
self.assertEqual(isfile_mock.call_count, 1)
|
||||
|
||||
@patch('pylspci.command.Path.is_file')
|
||||
@patch('pylspci.command.lspci')
|
||||
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'):
|
||||
CommandBuilder().use_pciids('somefile')
|
||||
self.assertEqual(isfile_mock.call_count, 1)
|
||||
|
||||
@patch('pylspci.command.Path.is_file')
|
||||
@patch('pylspci.command.lspci')
|
||||
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)
|
||||
self.assertListEqual(list(builder), ['a', 'b'])
|
||||
self.assertEqual(lspci_mock.call_count, 1)
|
||||
self.assertEqual(lspci_mock.call_args, call(pciids=Path('somefile')))
|
||||
self.assertFalse(isfile_mock.called)
|
||||
|
||||
@patch('pylspci.command.Path.is_file')
|
||||
@patch('pylspci.command.lspci')
|
||||
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')
|
||||
self.assertListEqual(list(builder), ['a', 'b'])
|
||||
self.assertEqual(lspci_mock.call_count, 1)
|
||||
self.assertEqual(lspci_mock.call_args, call(pcimap=Path('somefile')))
|
||||
self.assertEqual(isfile_mock.call_count, 1)
|
||||
|
||||
@patch('pylspci.command.Path.is_file')
|
||||
@patch('pylspci.command.lspci')
|
||||
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'):
|
||||
CommandBuilder().use_pcimap('somefile')
|
||||
self.assertEqual(isfile_mock.call_count, 1)
|
||||
|
||||
@patch('pylspci.command.Path.is_file')
|
||||
@patch('pylspci.command.lspci')
|
||||
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)
|
||||
self.assertListEqual(list(builder), ['a', 'b'])
|
||||
self.assertEqual(lspci_mock.call_count, 1)
|
||||
self.assertEqual(lspci_mock.call_args, call(pcimap=Path('somefile')))
|
||||
self.assertFalse(isfile_mock.called)
|
||||
|
||||
@patch('pylspci.command.lspci')
|
||||
def test_use_access_method(self, lspci_mock: MagicMock) -> None:
|
||||
lspci_mock.return_value = ['a', 'b']
|
||||
builder = CommandBuilder() \
|
||||
.use_access_method('one') \
|
||||
.use_access_method('two')
|
||||
self.assertListEqual(list(builder), ['a', 'b'])
|
||||
self.assertEqual(lspci_mock.call_count, 1)
|
||||
self.assertEqual(lspci_mock.call_args, call(access_method='two'))
|
||||
|
||||
@patch('pylspci.command.list_access_methods')
|
||||
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'])
|
||||
self.assertEqual(list_mock.call_count, 1)
|
||||
self.assertEqual(list_mock.call_args, call())
|
||||
|
||||
@patch('pylspci.command.list_pcilib_params')
|
||||
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'])
|
||||
self.assertEqual(list_mock.call_count, 1)
|
||||
self.assertEqual(list_mock.call_args, call())
|
||||
|
||||
@patch('pylspci.command.list_pcilib_params_raw')
|
||||
def test_list_pcilib_params_raw(self, list_mock: MagicMock) -> None:
|
||||
list_mock.return_value = ['a', 'b']
|
||||
builder = CommandBuilder() \
|
||||
.list_access_methods() \
|
||||
.list_pcilib_params(raw=True)
|
||||
self.assertListEqual(list(builder), ['a', 'b'])
|
||||
self.assertEqual(list_mock.call_count, 1)
|
||||
self.assertEqual(list_mock.call_args, call())
|
||||
|
||||
@patch('pylspci.command.lspci')
|
||||
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'):
|
||||
CommandBuilder().with_pcilib_params({'a': 'b'}, {'c': 'd'})
|
||||
|
||||
lspci_mock.return_value = ['a', 'b']
|
||||
builder = CommandBuilder().with_pcilib_params({'a': 'b'})
|
||||
self.assertListEqual(list(builder), ['a', 'b'])
|
||||
self.assertEqual(lspci_mock.call_count, 1)
|
||||
self.assertEqual(lspci_mock.call_args, call(pcilib_params={'a': 'b'}))
|
||||
|
||||
@patch('pylspci.command.lspci')
|
||||
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') \
|
||||
.with_pcilib_params(b='3', c='4')
|
||||
self.assertListEqual(list(builder), ['a', 'b'])
|
||||
self.assertEqual(lspci_mock.call_count, 1)
|
||||
self.assertEqual(lspci_mock.call_args, call(pcilib_params={
|
||||
'a': '1', 'b': '3', 'c': '4',
|
||||
}))
|
||||
|
||||
@patch('pylspci.command.Path.is_file')
|
||||
@patch('pylspci.command.lspci')
|
||||
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')
|
||||
self.assertListEqual(list(builder), ['a', 'b'])
|
||||
self.assertEqual(lspci_mock.call_count, 1)
|
||||
self.assertEqual(lspci_mock.call_args, call(file=Path('somefile')))
|
||||
self.assertEqual(isfile_mock.call_count, 1)
|
||||
|
||||
@patch('pylspci.command.Path.is_file')
|
||||
@patch('pylspci.command.lspci')
|
||||
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'):
|
||||
CommandBuilder().from_file('somefile')
|
||||
self.assertEqual(isfile_mock.call_count, 1)
|
||||
|
||||
@patch('pylspci.command.Path.is_file')
|
||||
@patch('pylspci.command.lspci')
|
||||
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)
|
||||
self.assertListEqual(list(builder), ['a', 'b'])
|
||||
self.assertEqual(lspci_mock.call_count, 1)
|
||||
self.assertEqual(lspci_mock.call_args, call(file=Path('somefile')))
|
||||
self.assertFalse(isfile_mock.called)
|
||||
|
||||
@patch('pylspci.command.lspci')
|
||||
def test_verbose(self, lspci_mock: MagicMock) -> None:
|
||||
lspci_mock.return_value = ['a', 'b']
|
||||
builder = CommandBuilder().verbose()
|
||||
self.assertListEqual(list(builder), ['a', 'b'])
|
||||
self.assertEqual(lspci_mock.call_count, 1)
|
||||
self.assertEqual(lspci_mock.call_args, call(verbose=True))
|
||||
|
||||
@patch('pylspci.command.lspci')
|
||||
def test_include_kernel_drivers(self, lspci_mock: MagicMock) -> None:
|
||||
lspci_mock.return_value = ['a', 'b']
|
||||
builder = CommandBuilder() \
|
||||
.include_kernel_drivers(False) \
|
||||
.include_kernel_drivers()
|
||||
self.assertListEqual(list(builder), ['a', 'b'])
|
||||
self.assertEqual(lspci_mock.call_count, 1)
|
||||
self.assertEqual(lspci_mock.call_args, call(
|
||||
verbose=True,
|
||||
kernel_drivers=True,
|
||||
))
|
||||
|
||||
@patch('pylspci.command.lspci')
|
||||
def test_include_bridge_paths(self, lspci_mock: MagicMock) -> None:
|
||||
lspci_mock.return_value = ['a', 'b']
|
||||
builder = CommandBuilder() \
|
||||
.include_bridge_paths(False) \
|
||||
.include_bridge_paths()
|
||||
self.assertListEqual(list(builder), ['a', 'b'])
|
||||
self.assertEqual(lspci_mock.call_count, 1)
|
||||
self.assertEqual(lspci_mock.call_args, call(
|
||||
bridge_paths=True,
|
||||
))
|
||||
|
||||
@patch('pylspci.command.lspci')
|
||||
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'])
|
||||
self.assertEqual(lspci_mock.call_count, 1)
|
||||
self.assertEqual(lspci_mock.call_args, call(hide_single_domain=True))
|
||||
|
||||
@patch('pylspci.command.lspci')
|
||||
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'])
|
||||
self.assertEqual(lspci_mock.call_count, 1)
|
||||
self.assertEqual(lspci_mock.call_args, call(
|
||||
id_resolve_option=IDResolveOption.NameOnly,
|
||||
))
|
||||
|
||||
lspci_mock.reset_mock()
|
||||
builder = builder.with_ids()
|
||||
self.assertListEqual(list(builder), ['a', 'b'])
|
||||
self.assertEqual(lspci_mock.call_count, 1)
|
||||
self.assertEqual(lspci_mock.call_args, call(
|
||||
id_resolve_option=IDResolveOption.Both,
|
||||
))
|
||||
|
||||
@patch('pylspci.command.lspci')
|
||||
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'])
|
||||
self.assertEqual(lspci_mock.call_count, 1)
|
||||
self.assertEqual(lspci_mock.call_args, call(
|
||||
id_resolve_option=IDResolveOption.IDOnly,
|
||||
))
|
||||
|
||||
lspci_mock.reset_mock()
|
||||
builder = builder.with_names()
|
||||
self.assertListEqual(list(builder), ['a', 'b'])
|
||||
self.assertEqual(lspci_mock.call_count, 1)
|
||||
self.assertEqual(lspci_mock.call_args, call(
|
||||
id_resolve_option=IDResolveOption.Both,
|
||||
))
|
||||
|
||||
@patch('pylspci.command.SlotFilter')
|
||||
@patch('pylspci.command.lspci')
|
||||
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=0xa)
|
||||
|
||||
filter_mock.parse.return_value = 'lefilter'
|
||||
lspci_mock.return_value = ['a', 'b']
|
||||
builder = CommandBuilder().slot_filter('something')
|
||||
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.parse.call_count, 1)
|
||||
self.assertEqual(filter_mock.parse.call_args, call('something'))
|
||||
|
||||
@patch('pylspci.command.SlotFilter')
|
||||
@patch('pylspci.command.lspci')
|
||||
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=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=0xa,
|
||||
bus=0xb,
|
||||
device=0xc,
|
||||
function=0xd,
|
||||
))
|
||||
|
||||
@patch('pylspci.command.DeviceFilter')
|
||||
@patch('pylspci.command.lspci')
|
||||
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=0xb)
|
||||
|
||||
filter_mock.parse.return_value = 'lefilter'
|
||||
lspci_mock.return_value = ['a', 'b']
|
||||
builder = CommandBuilder().device_filter('something')
|
||||
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.parse.call_count, 1)
|
||||
self.assertEqual(filter_mock.parse.call_args, call('something'))
|
||||
|
||||
@patch('pylspci.command.DeviceFilter')
|
||||
@patch('pylspci.command.lspci')
|
||||
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=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=0xa,
|
||||
vendor=0xb,
|
||||
device=0xc,
|
||||
))
|
||||
|
||||
def test_with_default_parser(self) -> None:
|
||||
builder = CommandBuilder()
|
||||
self.assertIsNone(builder._parser)
|
||||
|
||||
builder = CommandBuilder().with_default_parser()
|
||||
self.assertIsInstance(builder._parser, SimpleParser)
|
||||
|
||||
builder = CommandBuilder().verbose().with_default_parser()
|
||||
self.assertIsInstance(builder._parser, VerboseParser)
|
||||
|
||||
@patch('pylspci.command.lspci')
|
||||
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')
|
||||
builder = CommandBuilder().with_parser(parser_mock)
|
||||
self.assertListEqual(list(builder), ['parsed_a', 'parsed_b'])
|
||||
self.assertEqual(parser_mock.parse.call_count, 1)
|
||||
self.assertEqual(parser_mock.parse.call_args, call(['a', 'b']))
|
|
@ -1,6 +1,10 @@
|
|||
from unittest import TestCase
|
||||
from unittest.mock import patch, call, MagicMock
|
||||
from pylspci.command import lspci, IDResolveOption
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
from pylspci.command import (
|
||||
IDResolveOption, list_access_methods, list_pcilib_params, lspci
|
||||
)
|
||||
from pylspci.fields import PCIAccessParameter
|
||||
|
||||
|
||||
class TestCommand(TestCase):
|
||||
|
@ -16,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')
|
||||
|
@ -28,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')
|
||||
|
@ -36,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')
|
||||
|
@ -48,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')
|
||||
|
@ -75,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')
|
||||
|
@ -87,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')
|
||||
|
@ -113,6 +121,16 @@ class TestCommand(TestCase):
|
|||
universal_newlines=True,
|
||||
))
|
||||
|
||||
@patch('pylspci.command.subprocess.check_output')
|
||||
def test_bridge_paths(self, cmd_mock: MagicMock) -> None:
|
||||
cmd_mock.return_value = 'something'
|
||||
self.assertEqual(lspci(bridge_paths=True), 'something')
|
||||
self.assertEqual(cmd_mock.call_count, 1)
|
||||
self.assertEqual(cmd_mock.call_args, call(
|
||||
['lspci', '-mm', '-PP', '-nn'],
|
||||
universal_newlines=True,
|
||||
))
|
||||
|
||||
@patch('pylspci.command.subprocess.check_output')
|
||||
def test_hide_single_domain(self, cmd_mock: MagicMock) -> None:
|
||||
cmd_mock.return_value = 'something'
|
||||
|
@ -149,9 +167,37 @@ class TestCommand(TestCase):
|
|||
universal_newlines=True,
|
||||
))
|
||||
|
||||
@patch('pylspci.command.subprocess.check_output')
|
||||
def test_slot_filter(self, cmd_mock: MagicMock) -> None:
|
||||
cmd_mock.return_value = 'something'
|
||||
self.assertEqual(
|
||||
lspci(slot_filter='13'),
|
||||
'something',
|
||||
)
|
||||
self.assertEqual(cmd_mock.call_count, 1)
|
||||
self.assertEqual(cmd_mock.call_args, call(
|
||||
['lspci', '-mm', '-s', '::13.', '-nn'],
|
||||
universal_newlines=True,
|
||||
))
|
||||
|
||||
@patch('pylspci.command.subprocess.check_output')
|
||||
def test_device_filter(self, cmd_mock: MagicMock) -> None:
|
||||
cmd_mock.return_value = 'something'
|
||||
self.assertEqual(
|
||||
lspci(device_filter=':13'),
|
||||
'something',
|
||||
)
|
||||
self.assertEqual(cmd_mock.call_count, 1)
|
||||
self.assertEqual(cmd_mock.call_args, call(
|
||||
['lspci', '-mm', '-d', ':13:', '-nn'],
|
||||
universal_newlines=True,
|
||||
))
|
||||
|
||||
@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
|
||||
|
||||
|
@ -163,8 +209,11 @@ class TestCommand(TestCase):
|
|||
file='/file',
|
||||
verbose=True,
|
||||
kernel_drivers=True,
|
||||
bridge_paths=True,
|
||||
hide_single_domain=False,
|
||||
id_resolve_option=IDResolveOption.IDOnly,
|
||||
slot_filter='c0fe:e:e',
|
||||
device_filter='c0fe::eeee',
|
||||
), 'something')
|
||||
|
||||
self.assertEqual(cmd_mock.call_count, 1)
|
||||
|
@ -173,8 +222,13 @@ class TestCommand(TestCase):
|
|||
'-mm',
|
||||
'-vvv',
|
||||
'-k',
|
||||
'-PP',
|
||||
'-D',
|
||||
'-Asomemethod',
|
||||
'-s',
|
||||
'c0fe:e:e.',
|
||||
'-d',
|
||||
'c0fe::eeee',
|
||||
'-n',
|
||||
'-i', '/pciids',
|
||||
'-p', '/pcimap',
|
||||
|
@ -184,3 +238,55 @@ class TestCommand(TestCase):
|
|||
universal_newlines=True,
|
||||
))
|
||||
self.assertEqual(is_file_mock.call_count, 3)
|
||||
|
||||
@patch('pylspci.command.subprocess.check_output')
|
||||
def test_list_access_methods(self, cmd_mock: MagicMock) -> None:
|
||||
cmd_mock.return_value = """
|
||||
Known PCI access methods:
|
||||
|
||||
linux-sysfs
|
||||
linux-proc
|
||||
intel-conf1
|
||||
intel-conf2
|
||||
dump
|
||||
"""
|
||||
self.assertListEqual(
|
||||
list_access_methods(),
|
||||
['linux-sysfs', 'linux-proc', 'intel-conf1', 'intel-conf2', 'dump']
|
||||
)
|
||||
self.assertEqual(cmd_mock.call_count, 1)
|
||||
self.assertEqual(cmd_mock.call_args, call(
|
||||
['lspci', '-mm', '-Ahelp', '-nn'],
|
||||
universal_newlines=True,
|
||||
))
|
||||
|
||||
@patch('pylspci.command.subprocess.check_output')
|
||||
def test_list_pcilib_params(self, cmd_mock: MagicMock) -> None:
|
||||
cmd_mock.return_value = """
|
||||
Known PCI access parameters:
|
||||
|
||||
dump.name Name of the bus dump file to read from ()
|
||||
proc.path Path to the procfs bus tree (/proc/bus/pci)
|
||||
sysfs.path Path to the sysfs device tree (/sys/bus/pci)
|
||||
hwdb.disable Do not look up names in UDEV's HWDB if non-zero (0)
|
||||
net.cache_name Name of the ID cache file (~/.pciids-cache)
|
||||
net.domain DNS domain used for resolving of ID's (pci.id.ucw.cz)
|
||||
"""
|
||||
self.assertListEqual(
|
||||
list_pcilib_params(),
|
||||
list(map(PCIAccessParameter, [
|
||||
"dump.name Name of the bus dump file to read from ()",
|
||||
"proc.path Path to the procfs bus tree (/proc/bus/pci)",
|
||||
"sysfs.path Path to the sysfs device tree (/sys/bus/pci)",
|
||||
"hwdb.disable Do not look up names "
|
||||
"in UDEV's HWDB if non-zero (0)",
|
||||
"net.cache_name Name of the ID cache file (~/.pciids-cache)",
|
||||
"net.domain DNS domain used for "
|
||||
"resolving of ID's (pci.id.ucw.cz)",
|
||||
]))
|
||||
)
|
||||
self.assertEqual(cmd_mock.call_count, 1)
|
||||
self.assertEqual(cmd_mock.call_args, call(
|
||||
['lspci', '-Ohelp'],
|
||||
universal_newlines=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)
|
||||
|
@ -55,10 +66,9 @@ class TestVerboseParser(TestCase):
|
|||
def test_parse_list(self) -> None:
|
||||
devices: List[Device] = self.parser.parse([SAMPLE_DEVICE, ])
|
||||
self.assertEqual(len(devices), 1)
|
||||
print(devices)
|
||||
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)
|
||||
|
||||
|
@ -70,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,14 +1,15 @@
|
|||
from unittest import TestCase
|
||||
from pylspci.fields import Slot, NameWithID
|
||||
|
||||
from pylspci.fields import NameWithID, PCIAccessParameter, Slot
|
||||
|
||||
|
||||
class TestSlot(TestCase):
|
||||
|
||||
def test_full_slot(self) -> None:
|
||||
s = Slot('cafe:13:37.2')
|
||||
s = Slot('cafe:13:07.2')
|
||||
self.assertEqual(s.domain, 0xcafe)
|
||||
self.assertEqual(s.bus, 0x13)
|
||||
self.assertEqual(s.device, 0x37)
|
||||
self.assertEqual(s.device, 0x07)
|
||||
self.assertEqual(s.function, 0x2)
|
||||
|
||||
def test_optional_domain(self) -> None:
|
||||
|
@ -16,24 +17,72 @@ class TestSlot(TestCase):
|
|||
lspci can hide the domain ID when there is only one domain
|
||||
on the system, numbered 0. Ensure Slot defaults the domain to 0.
|
||||
"""
|
||||
s = Slot('13:37.2')
|
||||
s = Slot('13:07.2')
|
||||
self.assertEqual(s.domain, 0x0)
|
||||
self.assertEqual(s.bus, 0x13)
|
||||
self.assertEqual(s.device, 0x37)
|
||||
self.assertEqual(s.device, 0x07)
|
||||
self.assertEqual(s.function, 0x2)
|
||||
|
||||
def test_device_limit_32(self) -> None:
|
||||
"""
|
||||
There cannot be more than 32 devices on each bus
|
||||
"""
|
||||
with self.assertRaises(ValueError):
|
||||
Slot('0000:00:20.0')
|
||||
|
||||
def test_function_limit_8(self) -> None:
|
||||
"""
|
||||
Function numbers are from 0 to 7
|
||||
"""
|
||||
with self.assertRaises(ValueError):
|
||||
Slot('0000:00:01.f')
|
||||
|
||||
def test_repr(self) -> None:
|
||||
self.assertEqual(
|
||||
repr(Slot('13:37:42.6')),
|
||||
"Slot('0013:37:42.6')",
|
||||
repr(Slot('13:37:04.2')),
|
||||
"Slot('0013:37:04.2')",
|
||||
)
|
||||
|
||||
def test_str(self) -> None:
|
||||
self.assertEqual(
|
||||
str(Slot('13:37:42.6')),
|
||||
'0013:37:42.6',
|
||||
str(Slot('13:37:04.2')),
|
||||
'0013:37:04.2',
|
||||
)
|
||||
|
||||
def test_parent(self) -> None:
|
||||
s = Slot('abcd:13:07.2/cafe:c0:0f.3/66:06.6')
|
||||
self.assertEqual(str(s), 'abcd:13:07.2/cafe:c0:0f.3/cafe:66:06.6')
|
||||
self.assertEqual(
|
||||
repr(s),
|
||||
"Slot('abcd:13:07.2/cafe:c0:0f.3/cafe:66:06.6')",
|
||||
)
|
||||
self.assertEqual(s.domain, 0xcafe)
|
||||
self.assertEqual(s.bus, 0x66)
|
||||
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):
|
||||
|
||||
|
@ -83,3 +132,55 @@ class TestNameWithID(TestCase):
|
|||
self.assertIsNone(n.name)
|
||||
self.assertEqual(str(n), '')
|
||||
self.assertEqual(repr(n), "NameWithID('')")
|
||||
|
||||
def test_bad_format(self) -> None:
|
||||
n = NameWithID('Something [hexa]')
|
||||
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):
|
||||
|
||||
def test_normal(self) -> None:
|
||||
p = PCIAccessParameter('param.name Some description (default value)')
|
||||
self.assertEqual(p.name, 'param.name')
|
||||
self.assertEqual(p.description, 'Some description')
|
||||
self.assertEqual(p.default, 'default value')
|
||||
self.assertEqual(str(p),
|
||||
'param.name\tSome description (default value)')
|
||||
self.assertEqual(repr(p),
|
||||
"PCIAccessParameter('param.name\\tSome description"
|
||||
" (default value)')")
|
||||
|
||||
def test_no_default(self) -> None:
|
||||
p = PCIAccessParameter('param.name Some description ()')
|
||||
self.assertEqual(p.name, 'param.name')
|
||||
self.assertEqual(p.description, 'Some description')
|
||||
self.assertIsNone(p.default)
|
||||
|
||||
def test_bad_format(self) -> None:
|
||||
with self.assertRaises(ValueError):
|
||||
PCIAccessParameter('nope')
|
||||
with self.assertRaises(ValueError):
|
||||
PCIAccessParameter('nope (nope)')
|
||||
|
||||
def test_equal(self) -> None:
|
||||
p1 = PCIAccessParameter('param.name Some description (default value)')
|
||||
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",
|
||||
})
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
from unittest import TestCase
|
||||
|
||||
from pylspci.filters import DeviceFilter, SlotFilter
|
||||
|
||||
|
||||
class TestSlotFilter(TestCase):
|
||||
|
||||
def test_empty(self) -> None:
|
||||
f = SlotFilter()
|
||||
self.assertIsNone(f.domain)
|
||||
self.assertIsNone(f.bus)
|
||||
self.assertIsNone(f.device)
|
||||
self.assertIsNone(f.function)
|
||||
self.assertEqual(
|
||||
repr(f),
|
||||
'SlotFilter(domain=None, bus=None, device=None, function=None)',
|
||||
)
|
||||
|
||||
def test_str(self) -> None:
|
||||
self.assertEqual(str(SlotFilter()), '::.')
|
||||
self.assertEqual(str(SlotFilter(domain=0xcafe)), 'cafe::.')
|
||||
self.assertEqual(
|
||||
str(SlotFilter(domain=0xc0ff, bus=0xe, device=0xe, function=7)),
|
||||
'c0ff:e:e.7',
|
||||
)
|
||||
|
||||
def test_parse(self) -> None:
|
||||
self.assertEqual(SlotFilter.parse(''), SlotFilter())
|
||||
self.assertEqual(SlotFilter.parse('::.'), SlotFilter())
|
||||
self.assertEqual(SlotFilter.parse('*:*:*.*'), SlotFilter())
|
||||
self.assertEqual(SlotFilter.parse('4'), SlotFilter(device=4))
|
||||
self.assertEqual(SlotFilter.parse('4:'), SlotFilter(bus=4))
|
||||
self.assertEqual(SlotFilter.parse('4::'), SlotFilter(domain=4))
|
||||
self.assertEqual(SlotFilter.parse('.4'), SlotFilter(function=4))
|
||||
self.assertEqual(
|
||||
SlotFilter.parse('c0ff:e:e.7'),
|
||||
SlotFilter(domain=0xc0ff, bus=0xe, device=0xe, function=7),
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
SlotFilter.parse(':::::')
|
||||
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) -> None:
|
||||
f = DeviceFilter()
|
||||
self.assertIsNone(f.vendor)
|
||||
self.assertIsNone(f.device)
|
||||
self.assertIsNone(f.cls)
|
||||
self.assertEqual(
|
||||
repr(f),
|
||||
'DeviceFilter(cls=None, vendor=None, device=None)',
|
||||
)
|
||||
|
||||
def test_str(self) -> None:
|
||||
self.assertEqual(str(DeviceFilter()), '::')
|
||||
self.assertEqual(str(DeviceFilter(vendor=0xcafe)), 'cafe::')
|
||||
self.assertEqual(
|
||||
str(DeviceFilter(vendor=0xc0ff, device=0xe, cls=0xe)),
|
||||
'c0ff:e:e',
|
||||
)
|
||||
|
||||
def test_parse(self) -> None:
|
||||
self.assertEqual(DeviceFilter.parse(''), DeviceFilter())
|
||||
self.assertEqual(DeviceFilter.parse('::'), DeviceFilter())
|
||||
self.assertEqual(DeviceFilter.parse('*:*:*'), DeviceFilter())
|
||||
self.assertEqual(DeviceFilter.parse('4:'), DeviceFilter(vendor=4))
|
||||
self.assertEqual(DeviceFilter.parse(':4'), DeviceFilter(device=4))
|
||||
self.assertEqual(DeviceFilter.parse('::4'), DeviceFilter(cls=4))
|
||||
self.assertEqual(
|
||||
DeviceFilter.parse('c0ff:e:e'),
|
||||
DeviceFilter(vendor=0xc0ff, device=0xe, cls=0xe),
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
DeviceFilter.parse(':::::')
|
||||
with self.assertRaises(ValueError):
|
||||
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
|
||||
|
|
34
setup.py
34
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)]
|
||||
|
@ -17,22 +18,31 @@ setup(
|
|||
packages=find_packages(
|
||||
exclude=["*.tests", "*.tests.*", "tests.*", "tests"],
|
||||
),
|
||||
package_data={
|
||||
'': ['*.md', 'LICENSE', 'README'],
|
||||
entry_points={
|
||||
'console_scripts': ['pylspci=pylspci.__main__:main'],
|
||||
},
|
||||
python_requires='>=3.5',
|
||||
package_data={
|
||||
'': [
|
||||
'VERSION',
|
||||
'LICENSE',
|
||||
'README.rst',
|
||||
'requirements.txt',
|
||||
'requirements-dev.txt',
|
||||
],
|
||||
'pylspci': ['py.typed'],
|
||||
},
|
||||
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)",
|
||||
|
@ -40,15 +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