Compare commits

...

61 Commits

Author SHA1 Message Date
Lucidiot 1f7cac1d8c
Update pre-commit hooks
continuous-integration/drone/push Build is passing Details
2023-05-09 13:06:37 +02:00
Lucidiot ac6a1b3103
Add Python 3.11 in CI
continuous-integration/drone Build is failing Details
2023-05-09 12:48:18 +02:00
~lucidiot 04e3354394
Split PyPI secrets
continuous-integration/drone/push Build is passing Details
2022-08-09 14:19:14 +02:00
~lucidiot 91918e8d41
Bump to 0.4.3
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone Build is passing Details
2022-08-05 01:53:28 +02:00
~lucidiot e3a89613e1
Updates due to Tildegit move 2022-08-05 01:53:08 +02:00
~lucidiot 4438f17e94
Bump to 0.4.2 2022-02-08 22:44:56 +01:00
~lucidiot 70b155cf04
Fix missing files in source packages 2022-02-08 22:43:02 +01:00
~lucidiot d6becffcc0
Bump to 0.4.1 2022-01-26 19:19:40 +01:00
~lucidiot 27cc5de1b9 Merge branch 'manifest' into 'master'
Add MANIFEST.in

Closes #21

See merge request Lucidiot/pylspci!19
2022-01-26 18:19:26 +00:00
~lucidiot 1936c58dec
Add MANIFEST.in 2022-01-26 19:17:26 +01:00
~lucidiot 544edd0b3d
Bump to 0.4.0 2021-09-21 20:12:18 +02:00
~lucidiot da39b09ef5 Merge branch 'bump-python' into 'master'
Bump CI image to Python 3.9

See merge request Lucidiot/pylspci!18
2021-09-21 18:09:03 +00:00
~lucidiot 149b3bde99
Bump CI image to Python 3.9 2021-09-21 20:06:55 +02:00
~lucidiot ad52e4c823 Merge branch 'docs-cleanup' into 'master'
Remove unnecessary types in Sphinx docstrings

See merge request Lucidiot/pylspci!17
2021-09-21 18:05:37 +00:00
~lucidiot 30e1ed5ec4
Remove unnecessary types in Sphinx docstrings 2021-09-21 20:03:21 +02:00
~lucidiot 7cebd6202d Merge branch 'as-dict' into 'master'
Add dict serialization methods

Closes #20

See merge request Lucidiot/pylspci!16
2021-09-21 18:02:32 +00:00
~lucidiot 57d96ab345
Add dict serialization methods 2021-09-21 19:51:28 +02:00
~lucidiot 386b7fc326 Merge branch 'bump-pre-commit' into 'master'
Bump pre-commit hooks

See merge request Lucidiot/pylspci!15
2021-09-21 17:48:51 +00:00
~lucidiot 587a55af73
Bump pre-commit hooks 2021-09-21 19:46:45 +02:00
~lucidiot 96cbe0e72c Merge branch 'fix-setup' into 'master'
Fix setup.py test for Alpine build

See merge request Lucidiot/pylspci!14
2021-09-21 17:03:26 +00:00
~lucidiot 09256806f1
Fix setup.py test for Alpine build
This removes some unnecessary dependencies installed by setup.py test,
fixing an issue found in Alpine's package building CI:

https://gitlab.alpinelinux.org/Lucidiot/aports/-/jobs/492430
2021-09-21 01:35:01 +02:00
Lucidiot 27695dd747 Bump to 0.3.4 2021-01-26 17:28:44 +00:00
Lucidiot 0b00a2e586 Update physical_slot docstring 2021-01-26 17:27:20 +00:00
Lucidiot 06906411a1 Merge branch 'change_phy_slot_to_str' into 'master'
Changed physical_slot to str, because linux adds '-#' if duplicate slot numbers are found

See merge request Lucidiot/pylspci!13
2021-01-26 17:21:41 +00:00
Jan Lützler 9f90cf8519 Changed physical_slot to str, because linux adds '-#' if duplicate slot numbers are found 2021-01-26 17:21:41 +00:00
Lucidiot 7778e1a48a
Bump to 0.3.3 2020-11-29 19:55:19 +01:00
Lucidiot 51aac8692d
Update badge count 2020-11-29 19:54:56 +01:00
Lucidiot 11c57ae0f1
Remove dependency of pages deployment on unit tests 2020-11-29 19:54:10 +01:00
Lucidiot 7da6c0756c Merge branch 'add-verbose-error' into 'master'
Prevent using SimpleParser with verbose=True

Closes #18

See merge request Lucidiot/pylspci!12
2020-11-29 18:52:49 +00:00
Lucidiot 45dfdd7277
Prevent using SimpleParser with verbose=True 2020-11-29 19:45:48 +01:00
Lucidiot a34c08c9ce
Add isort 2020-11-29 19:17:36 +01:00
Lucidiot ce1abd76d0
Fix typing of kwargs 2020-11-29 19:06:57 +01:00
Lucidiot 0b06017ab7
Add pre-commit 2020-11-29 19:06:28 +01:00
Lucidiot 92417f1b6a
Fix intro examples 2020-11-29 18:02:13 +01:00
Lucidiot 65053cfdba
Remove GitHub badges 2020-11-29 18:00:37 +01:00
Lucidiot 3b93a30fb4
Add missing type checking docs 2020-11-29 17:59:56 +01:00
Lucidiot 4c21f2f813 Bump to 0.3.2 2020-08-06 09:35:45 +00:00
Lucidiot f21296378a Merge branch 'iommugroup' into 'master'
Add support for IOMMUGroup, closes #17

Closes #17

See merge request Lucidiot/pylspci!11
2020-08-06 09:32:43 +00:00
Erwan Rouchet 4406ce90a7
Add support for IOMMUGroup, closes #17 2020-08-06 11:30:47 +02:00
Lucidiot 4abe4a883a
Add test coverage badge 2020-01-22 19:40:43 +01:00
Lucidiot 44dad5738f
Bump to 0.3.1 2020-01-22 19:23:04 +01:00
Lucidiot c9e34b2a5e
Add PhySlot field 2020-01-22 19:21:12 +01:00
Lucidiot 3c3bbe25c5
Warn about unknown fields instead of erroring 2020-01-22 19:16:43 +01:00
Lucidiot fc5d3c7644
Add optional NUMANode field 2020-01-22 19:07:41 +01:00
Lucidiot cddd1d04a7
Add unit tests info to contribution docs 2019-10-03 06:40:59 +02:00
Lucidiot 45cceb8611 Merge branch 'typing' into 'master'
Typing with mypy

Closes #15 and #14

See merge request Lucidiot/pylspci!10
2019-09-06 21:09:50 +00:00
Lucidiot ba2557bca1 Typing with mypy 2019-09-06 21:09:50 +00:00
Lucidiot 657a881112
Bump to 0.3.0 2019-08-04 03:04:08 +02:00
Lucidiot d603f17ce4 Merge branch 'cli' into 'master'
Command-line interface

Closes #11

See merge request Lucidiot/pylspci!9
2019-08-04 01:01:55 +00:00
Lucidiot 550d42bfb6 Command-line interface 2019-08-04 01:01:55 +00:00
Lucidiot cdf14c22ec Merge branch 'builder' into 'master'
Command builder

Closes #13

See merge request Lucidiot/pylspci!8
2019-08-03 22:53:16 +00:00
Lucidiot 8ee73aaffb Command builder 2019-08-03 22:53:16 +00:00
Lucidiot 7a0906ef93 Add a cool PyPI classifier 2019-07-25 17:04:55 +00:00
Lucidiot f17b7d0509 Merge branch 'filters' into 'master'
Add slot and device filters

Closes #9

See merge request Lucidiot/pylspci!7
2019-07-20 21:59:57 +00:00
Lucidiot e174ed130a Add slot and device filters 2019-07-20 21:59:57 +00:00
Lucidiot e86c9b06d8 Merge branch 'limit-devices' into 'master'
Limit to 32 devices and 8 functions

Closes #12

See merge request Lucidiot/pylspci!6
2019-07-13 16:07:02 +00:00
Lucidiot 39064834c5 Limit to 32 devices and 8 functions 2019-07-13 16:07:02 +00:00
Lucidiot 82a832b69c Merge branch 'bridge-paths' into 'master'
PCI bridge paths

Closes #8

See merge request Lucidiot/pylspci!5
2019-07-13 15:36:17 +00:00
Lucidiot a9345daaec PCI bridge paths 2019-07-13 15:36:17 +00:00
Lucidiot 1b88567ee4 Merge branch 'options' into 'master'
List pcilib options

Closes #10

See merge request Lucidiot/pylspci!4
2019-07-12 06:51:00 +00:00
Lucidiot 5799d5bf96 List pcilib options 2019-07-12 06:51:00 +00:00
34 changed files with 2299 additions and 272 deletions

120
.drone.yml Normal file
View File

@ -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

1
.gitignore vendored
View File

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

View File

@ -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

35
.pre-commit-config.yaml Normal file
View File

@ -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

6
MANIFEST.in Normal file
View File

@ -0,0 +1,6 @@
include requirements.txt
include requirements-dev.txt
include VERSION
include LICENSE
include README.rst
include pylspci/py.typed

View File

@ -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/

View File

@ -1 +1 @@
0.2.0
0.4.3

View File

@ -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)

132
docs/cli.rst Normal file
View File

@ -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``.

View File

@ -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:

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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())

View File

@ -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,
}

View File

@ -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,
}

154
pylspci/filters.py Normal file
View File

@ -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)

View File

@ -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))

View File

@ -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)

View File

@ -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
pylspci/py.typed Normal file
View File

View File

View File

@ -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']))

View File

@ -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,
))

View File

@ -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.'
)

View File

@ -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])

View File

@ -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',
})

View File

@ -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",
})

View File

@ -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',
)

View File

@ -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

View File

@ -1 +1 @@
cached-property>=1.5.1
cached-property>=1.5.1

View File

@ -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

View File

@ -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",
}
)