diff --git a/.coveragerc b/.coveragerc
index d3b3b11b..9cac3c1e 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -5,6 +5,8 @@ source =
omit =
# Don't worry about covering vendored libraries
src/sphobjinv/_vendored/*
+ # Not part of the test suite
+ setup.py
[report]
exclude_lines =
diff --git a/.gitattributes b/.gitattributes
index c6f20dbf..2a233757 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1,3 @@
tests/resource/objects_mkdoc_zlib0.inv binary
tests/resource/objects_attrs.txt binary
+*.inv diff=objects_inv
diff --git a/.github/workflows/all_core_tests.yml b/.github/workflows/all_core_tests.yml
index fc09a4f5..686676ad 100644
--- a/.github/workflows/all_core_tests.yml
+++ b/.github/workflows/all_core_tests.yml
@@ -1,7 +1,10 @@
-name: 'ALL: Run tests on Python 3.12'
+name: 'ALL: Run tests on Python 3.13'
on:
- - pull_request
+ pull_request:
+ push:
+ branches:
+ - main
jobs:
current_python_tests:
@@ -16,12 +19,14 @@ jobs:
steps:
- name: Check out repo
- uses: actions/checkout@v4
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+ with:
+ persist-credentials: false
- name: Install Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec
with:
- python-version: '3.12'
+ python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements-ci.txt
diff --git a/.github/workflows/ready_doctest.yml b/.github/workflows/ready_doctest.yml
index 18ed9e62..73519dde 100644
--- a/.github/workflows/ready_doctest.yml
+++ b/.github/workflows/ready_doctest.yml
@@ -22,12 +22,14 @@ jobs:
steps:
- name: Check out repo
- uses: actions/checkout@v4
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+ with:
+ persist-credentials: false
- name: Install Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec
with:
- python-version: '3.12'
+ python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements-ci.txt
diff --git a/.github/workflows/ready_linting.yml b/.github/workflows/ready_linting.yml
index 95fab978..427d6847 100644
--- a/.github/workflows/ready_linting.yml
+++ b/.github/workflows/ready_linting.yml
@@ -22,12 +22,14 @@ jobs:
steps:
- name: Check out repo
- uses: actions/checkout@v4
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+ with:
+ persist-credentials: false
- name: Install Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec
with:
- python-version: '3.12'
+ python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements-flake8.txt
@@ -50,12 +52,14 @@ jobs:
steps:
- name: Check out repo
- uses: actions/checkout@v4
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+ with:
+ persist-credentials: false
- name: Install Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec
with:
- python-version: '3.12'
+ python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements-interrogate.txt
diff --git a/.github/workflows/ready_test_matrix.yml b/.github/workflows/ready_test_matrix.yml
index 8f58417b..dda2f278 100644
--- a/.github/workflows/ready_test_matrix.yml
+++ b/.github/workflows/ready_test_matrix.yml
@@ -22,17 +22,19 @@ jobs:
strategy:
matrix:
os: ['windows-latest', 'ubuntu-latest', 'macos-latest']
- py: ['3.9', '3.10', '3.11', '3.13']
+ py: ['3.10', '3.11', '3.12', '3.14']
include:
- os: 'macos-latest'
- py: '3.12'
+ py: '3.13'
steps:
- name: Check out repo
- uses: actions/checkout@v4
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+ with:
+ persist-credentials: false
- name: Install Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec
with:
python-version: ${{ matrix.py }}
cache: 'pip'
diff --git a/.github/workflows/ready_test_nonloc.yml b/.github/workflows/ready_test_nonloc.yml
index 5ba2636a..e434d728 100644
--- a/.github/workflows/ready_test_nonloc.yml
+++ b/.github/workflows/ready_test_nonloc.yml
@@ -13,7 +13,7 @@ on:
jobs:
python_os_test_matrix:
- name: ${{ matrix.os }} python 3.12
+ name: ${{ matrix.os }} python 3.13
runs-on: ${{ matrix.os }}
concurrency:
group: ${{ github.workflow }}-${{ matrix.os }}-${{ github.ref }}
@@ -25,12 +25,14 @@ jobs:
steps:
- name: Check out repo
- uses: actions/checkout@v4
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+ with:
+ persist-credentials: false
- name: Install Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec
with:
- python-version: '3.12'
+ python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements-ci.txt
diff --git a/.github/workflows/release_check_sdist.yml b/.github/workflows/release_check_sdist.yml
index d5a5678b..35e2505d 100644
--- a/.github/workflows/release_check_sdist.yml
+++ b/.github/workflows/release_check_sdist.yml
@@ -21,12 +21,14 @@ jobs:
steps:
- name: Check out repo
- uses: actions/checkout@v4
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+ with:
+ persist-credentials: false
- name: Install Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec
with:
- python-version: '3.12'
+ python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements-dev.txt
diff --git a/.github/workflows/release_doc_warnings.yml b/.github/workflows/release_doc_warnings.yml
index 0cf4b323..f3e33c09 100644
--- a/.github/workflows/release_doc_warnings.yml
+++ b/.github/workflows/release_doc_warnings.yml
@@ -21,12 +21,14 @@ jobs:
steps:
- name: Check out repo
- uses: actions/checkout@v4
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+ with:
+ persist-credentials: false
- name: Install Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec
with:
- python-version: '3.12'
+ python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements-rtd.txt
diff --git a/.github/workflows/release_enusre_no_ver_markers.yml b/.github/workflows/release_enusre_no_ver_markers.yml
new file mode 100644
index 00000000..da49997e
--- /dev/null
+++ b/.github/workflows/release_enusre_no_ver_markers.yml
@@ -0,0 +1,30 @@
+name: 'RELEASE: Check docs source files'
+
+on:
+ pull_request:
+ types:
+ - opened
+ - reopened
+ - synchronize
+ - ready_for_review
+ branches:
+ - stable
+
+jobs:
+ ver_placeholder_search:
+ name: 'have no ##VER## markers'
+ runs-on: 'ubuntu-latest'
+ concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+ if: ${{ !github.event.pull_request.draft }}
+
+ steps:
+ - name: Check out repo
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+ with:
+ persist-credentials: false
+
+ - name: Error if any markers found
+ run: |
+ grep -ri '#VER#' doc/source && exit 1 || exit 0
diff --git a/.github/workflows/release_flake8_noqa_nofail.yml b/.github/workflows/release_flake8_noqa_nofail.yml
index c2810a61..16d9993d 100644
--- a/.github/workflows/release_flake8_noqa_nofail.yml
+++ b/.github/workflows/release_flake8_noqa_nofail.yml
@@ -21,12 +21,14 @@ jobs:
steps:
- name: Check out repo
- uses: actions/checkout@v4
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+ with:
+ persist-credentials: false
- name: Install Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec
with:
- python-version: '3.12'
+ python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements-ci.txt
diff --git a/.github/workflows/release_readme_doctest.yml b/.github/workflows/release_readme_doctest.yml
index 33f8649f..4a690a07 100644
--- a/.github/workflows/release_readme_doctest.yml
+++ b/.github/workflows/release_readme_doctest.yml
@@ -21,12 +21,14 @@ jobs:
steps:
- name: Check out repo
- uses: actions/checkout@v4
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+ with:
+ persist-credentials: false
- name: Install Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec
with:
- python-version: '3.12'
+ python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements-ci.txt
diff --git a/.github/workflows/release_test_file_coverage.yml b/.github/workflows/release_test_file_coverage.yml
index 40cdc0d2..f2c40258 100644
--- a/.github/workflows/release_test_file_coverage.yml
+++ b/.github/workflows/release_test_file_coverage.yml
@@ -21,19 +21,21 @@ jobs:
steps:
- name: Check out repo
- uses: actions/checkout@v4
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+ with:
+ persist-credentials: false
- name: Install Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec
with:
- python-version: '3.12'
+ python-version: '3.13'
cache: 'pip'
cache-dependency-path: |
requirements-ci.txt
requirements-flake8.txt
- name: Install CI requirements
- run: pip install -r requirements-ci.txt -r requirements-flake8.txt
+ run: pip install -r requirements-ci.txt
- name: Build docs & ensure scratch
run: |
@@ -42,7 +44,7 @@ jobs:
mkdir scratch
- name: Run pytest covering entire project tree
- run: pytest --cov=. --nonloc --flake8_ext
+ run: pytest --cov=. --nonloc
- name: Check 100% test code execution
run: coverage report --include="tests/*" --fail-under=100
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index ab110bfa..03c3c3f0 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -7,9 +7,9 @@ version: 2
# Build and VM configuration
build:
- os: 'ubuntu-22.04'
+ os: 'ubuntu-24.04'
tools:
- python: '3.12'
+ python: '3.13'
# Python requirements
python:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 801bfa00..a90a49a2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,15 +2,149 @@
All notable changes to this project will be documented in this file.
-The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project follows an extension of
-[Semantic Versioning](http://semver.org/spec/v2.0.0.html), where a bump in a
+[Semantic Versioning](https://semver.org/spec/v2.0.0.html), where a bump in a
fourth number represents an administrative maintenance release with no code
changes.
-### *Unreleased*
+### [2.4] - 2026-03-23
-...
+#### Added
+
+ * Add `sphobjinv-textconv` CLI entrypoint ([#331]).
+ * Takes a single required argument, the path to a local inventory file, and
+ emits the plaintext inventory to `stdout`.
+ * The target use-case is as a Git textconv, primarily intended for
+ compressed `objects.inv` files; but, it will work with any valid type of
+ input file.
+
+#### Tests
+
+ * Remove flake8_ext test file and machinery ([#336]).
+ * pytest environment now can easily de-sync from the flake8 environment
+ since flake8 is running in tox now.
+ * It was really always over-cautious, too.
+
+ * Exclude `setup.py` from coverage ([#336]).
+ * Necessary due to a change in coverage.py behavior, maybe?
+ * Definitely is not expected to run during execution of the test suite.
+
+ * Remove unused `ensure_doc_scratch` fixture from `conftest.py` ([#336]).
+ * Obsolete now that the README shell examples aren't doctested.
+
+ * Add 3.13t and 3.14t to `tox` test matrix ([#333]).
+ * Also add report of the current GIL status to the `tox` env output.
+
+ * Filter newly emerged `ResourceWarning` emitted from implicit cleanup of
+ `tempfile` resources ([#333]).
+
+ * Add tests exercising the new `sphobjinv-textconv` CLI entrypoint ([#331]).
+ * Required generalizing the `run_cmdline_test` fixture so that tests can
+ choose between the core and textconv entrypoints.
+
+ * Update `tox` env test matrix for `py310` to `py314` ([#325]).
+
+ * Update test path calculations to always be relative to `__file__` ([#325]).
+
+ * Relocate `conftest.py` into `tests` ([#325]).
+ * Since the new HTTP server fixtures are going in their own source file, it
+ made the most sense to pull `conftest.py` into the `tests/` directory
+ also.
+ * Required some updates to paths in fixtures &c.
+
+ * Convert HTTP/URL nonloc tests to use a transient local HTTP server ([#325]).
+ * See `tests/fixtures_http.py`.
+ * With the increased caution many sites, including GitHub, are applying to
+ incoming traffic, using 'raw' GitHub assets in the `sphobjinv` repository
+ has become too flaky.
+ * So, we stand up our own HTTP server as a session-scope fixture, and point
+ (nearly) all of the URL tests at the local server.
+ * A small number of tests remain that do still reach out to an internet
+ `objects.inv`.
+ * A small number of outside-world URL tests remain, to docsets that (so far)
+ have been cooperative. Time will tell if we need to find others.
+
+#### Internal
+
+ * Convert `build` call into a `tox` env and remove `build` from
+ `requirements-dev.txt` ([#336]).
+
+ * Remove redundant packages from `requirements-dev.txt` and
+ `requirements-ci.txt` that are pulled in by the `-e .` line ([#336]).
+
+ * Add `tests/resource/objects_pdfminer*` to `MANIFEST.in`, to make that
+ inventory available to the docs build in the sdist unpack-and-test workflow
+ job ([#336]).
+ * Otherwise the docs job emits a warning. Not fatal, but better to have a
+ clean build.
+
+ * Pin Actions versions to SHAs and de-persist credentials ([#336]).
+ * Closes [#322].
+
+ * Add Actions workflow to error on a non-draft release branch if any `#VER#`
+ markers remain in docs source ([#331]).
+
+ * Augment `black` and `flake8` `tox` envs to run `--version` first ([#327]).
+
+ * Remove `-r requirements-flake.txt` from `requirements-dev.txt` ([#327]).
+ * `flake8` should always be run via `tox`.
+
+ * Add `tox` env to run `isort` and execute across codebase ([#327]).
+
+ * Add `flake8-isort` to `flake8` requirements and remove `flake8-import-order`
+ ([#327]).
+ * Also remove `flake8-import-order` config from `tox.ini`.
+
+ * Bump dev-pin Sphinx to v8.1.3 ([#325]).
+ * Two different version constraints at the moment:
+ * Sphinx v8.2 doesn't support Python 3.10 (primary constraint)
+ * Newest `sphinx-rtd-theme` only supports Sphinx `<9` (secondary).
+
+ * Add `push` trigger for `all_core_tests.yml` workflow for `main` branch
+ ([#320]).
+ * This will provide `main` branch CI results for this workflow, for the
+ GitHub badge to report.
+
+#### Documentation
+
+ * Update Sphinx, attrs, Python, etc. content to freshen and to match the new
+ inventories in the test resources ([#336]).
+
+ * Dynamically retrieve the current values of `PrsConst.SUGGEST_CONFIRM_LENGTH`
+ and `PrsConst.DEF_THRESH` to define their replaces in `conf.py` ([#331]).
+
+ * Add `cli/textconv.rst` to document the new `sphobjinv-textconv` CLI
+ entrypoint ([#331]).
+
+ * Cull some superfluous replaces in `conf.py` ([#331]).
+
+ * Relocate the 'help' and 'version' CLI usage documentation content to a new
+ 'orphan' page ([#331]).
+ * This keeps the content in the `objects.inv`, for completeness, but keeps
+ it off of the docs nav.
+
+ * Revise 'CLI Usage' documentation to incorporate the `sphobjinv-textconv`
+ entrypoint ([#331]).
+
+ * Remove the 'CLI Implementation' "API reference" docs ([#331]).
+ * They're not part of the public API contract, and they don't actually help
+ understand how the CLI is implemented; so, why bother maintaining them?
+ * Also cull the various `replace` directives defined in `conf.py` for these
+ docs.
+
+#### Administrative
+
+ * Convert several `http://` to `https://` across the project ([#333]).
+
+ * Add formal support for Python 3.14 ([#325]).
+
+ * Drop support for Python 3.9 (EOL) ([#325]).
+
+ * Bump 'core' dev and CI Python version to 3.13 ([#325]).
+
+ * Update the GitHub badge to point to the new `all_core_tests.yml` workflow
+ ([#320]) instead of the now-removed `ci_tests.yml`.
### [2.3.1.3] - 2025-05-26
@@ -680,3 +814,10 @@ changes.
[#306]: https://github.com/bskinn/sphobjinv/pull/306
[#315]: https://github.com/bskinn/sphobjinv/pull/315
[#316]: https://github.com/bskinn/sphobjinv/pull/316
+[#320]: https://github.com/bskinn/sphobjinv/pull/320
+[#322]: https://github.com/bskinn/sphobjinv/issues/322
+[#325]: https://github.com/bskinn/sphobjinv/pull/325
+[#327]: https://github.com/bskinn/sphobjinv/pull/327
+[#331]: https://github.com/bskinn/sphobjinv/pull/331
+[#333]: https://github.com/bskinn/sphobjinv/pull/333
+[#336]: https://github.com/bskinn/sphobjinv/pull/336
diff --git a/CONTENT_LICENSE.txt b/CONTENT_LICENSE.txt
index d187275f..b04b0ea0 100644
--- a/CONTENT_LICENSE.txt
+++ b/CONTENT_LICENSE.txt
@@ -1,4 +1,4 @@
The sphobjinv documentation (including docstrings and README) is licensed under
a Creative Commons Attribution 4.0 International License (CC-BY).
-See http://creativecommons.org/licenses/by/4.0/.
+See https://creativecommons.org/licenses/by/4.0/.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 28e29d57..e73bd72a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -39,12 +39,12 @@ $ git clone https://github.com/{you}/sphobjinv
```
Then, create a virtual environment for the project, in whatever location you
-prefer. Any Python interpreter 3.9+ *should* work fine.
+prefer. Any Python interpreter 3.10+ *should* work fine.
I prefer to use `virtualenv` and create in `./env`:
```bash
-$ python3.12 -m virtualenv env --prompt="sphobjinv"
+$ python3.13 -m virtualenv env --prompt="sphobjinv"
```
Activate the environment:
@@ -122,6 +122,10 @@ flag:
$ pytest --nonloc
```
+Most of these nonlocal tests now use an ephemeral local web server instead of
+reaching out to the web, and so should run even without network access and
+should just run faster in general.
+
When putting together a PR, at minimum, please add/augment the test suite as
necessary to maintain 100% test coverage. To the extent possible, please go
beyond this and add tests that check potential edge cases, bad/malformed/invalid
@@ -140,16 +144,16 @@ Note that while [`tox`](https://tox.wiki/en/latest/) *is* configured for the
project, it is **not** set up to be an everyday test runner. Instead, its
purpose for testing is to execute an extensive matrix of test environments
checking for the compatibility of different Python and dependency versions. You
-can run it if you want, but you'll need working versions of all of Python 3.9
-through 3.13 installed and on `PATH` as `python3.9`, `python3.10`, etc. The
-nonlocal test suite is run for each `tox` environment, so it's best to use at
-most two parallel sub-processes to avoid oversaturating your network bandwidth;
-e.g.:
+can run it if you want, but you'll need working versions of all of Python 3.10
+through 3.14 installed and on `PATH` as `python3.10`, `python3.11`, etc., as
+well as free-threaded versions for Python 3.13 onward as `python3.13t`, etc. The
+test matrix can be accelerated by using `tox`'s parallel execution mode; e.g.:
```bash
$ tox -rp2
```
+
## Code Autoformatting
The project is set up with a `tox` environment to blacken the codebase; run with:
@@ -244,20 +248,17 @@ with `make linkcheck`.
## Continuous Integration
-Both Github Actions and Azure Pipelines are set up for the project, and should
-run on any forks of the repository.
+Github Actions workflows are set up for the project, and should run on any forks
+of the repository. Note that the CI runs differently on draft versus non-draft
+PRs: on draft PRs, the only workflow that runs runs tests on Windows and Linux
+with one Python version; whereas on non-draft PRs, a complete test matrix of
+platforms and Python versions is run, as well as doctests and linting checks.
-Github Actions runs the test suite on Linux for Python 3.9 through 3.13, as well
+Github Actions runs the test suite on Linux for Python 3.10 through 3.14, as well
as the `flake8` lints and the Sphinx doctests. By default, the Github Actions
will run on all commits, but the workflows can be skipped per-commit by
including `[skip ci]` in the commit message.
-The Azure Pipelines CI runs an extensive matrix of cross-platform and
-cross-Python-version tests, as well as numerous other checks. Due to its length,
-it is configured to run only on release branches and PRs to `main` or `stable`.
-The Azure Pipelines workflows now [also obey `[skip ci]`
-directives](https://learn.microsoft.com/en-us/azure/devops/pipelines/repos/azure-repos-git?view=azure-devops&tabs=yaml#skipping-ci-for-individual-pushes).
-
## CHANGELOG
diff --git a/LICENSE.txt b/LICENSE.txt
index e892de6b..8afcbd75 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2016-2025 Brian Skinn and community contributors
+Copyright (c) 2016-2026 Brian Skinn and community contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
diff --git a/MANIFEST.in b/MANIFEST.in
index e83169a2..51cd741a 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -6,10 +6,9 @@ graft src/sphobjinv/_vendored/fuzzywuzzy
graft doc/source
include doc/make.bat doc/Makefile
-include conftest.py
graft tests
prune tests/resource
-include tests/resource/objects_attrs* tests/resource/objects_sarge*
+include tests/resource/objects_attrs* tests/resource/objects_sarge* tests/resource/objects_pdfminer*
global-exclude __pycache__/*
prune **/*.egg-info
diff --git a/README.md b/README.md
index d95e84eb..6a3e8858 100644
--- a/README.md
+++ b/README.md
@@ -42,26 +42,24 @@ For internal cross-references, locate `objects.inv` within `build/html`:
```none
$ sphobjinv suggest doc/build/html/objects.inv as_rst -st 58
-------------------------------------------------
+-----------------------------------------------------------------------------------------------------------
Cannot infer intersphinx_mapping from a local objects.inv.
-------------------------------------------------
+-----------------------------------------------------------------------------------------------------------
Project: sphobjinv
-Version: 2.3
+Version: 2.4
-220 objects in inventory.
+151 objects in inventory.
-------------------------------------------------
-
-11 results found at/above current threshold of 58.
+-----------------------------------------------------------------------------------------------------------
+10 results found at/above current threshold of 58.
Name Score
--------------------------------------------------- -------
:py:property:`sphobjinv.data.SuperDataObj.as_rst` 60
-:py:class:`sphobjinv.cli.parser.PrsConst` 59
:py:class:`sphobjinv.data.DataFields` 59
:py:class:`sphobjinv.data.DataObjBytes` 59
:py:class:`sphobjinv.data.DataObjStr` 59
@@ -84,31 +82,31 @@ cross-reference the `linspace` function from numpy (see
[here][numpy linspace]):
```none
-$ sphobjinv suggest https://numpy.org/doc/1.26/reference/index.html linspace -su
+$ sphobjinv suggest https://numpy.org/doc/2.4/reference/index.html linspace -su
-Attempting https://numpy.org/doc/1.26/reference/index.html ...
+Attempting https://numpy.org/doc/2.4/reference/index.html ...
... no recognized inventory.
-Attempting "https://numpy.org/doc/1.26/reference/index.html/objects.inv" ...
+Attempting "https://numpy.org/doc/2.4/reference/index.html/objects.inv" ...
... HTTP error: 404 Not Found.
-Attempting "https://numpy.org/doc/1.26/reference/objects.inv" ...
+Attempting "https://numpy.org/doc/2.4/reference/objects.inv" ...
... HTTP error: 404 Not Found.
-Attempting "https://numpy.org/doc/1.26/objects.inv" ...
+Attempting "https://numpy.org/doc/2.4/objects.inv" ...
... inventory found.
-----------------------------------------------------------------------------------
+-----------------------------------------------------------------------------------------------------------
The intersphinx_mapping for this docset is LIKELY:
- (https://numpy.org/doc/1.26/, None)
+ (https://numpy.org/doc/2.4/, None)
-----------------------------------------------------------------------------------
+-----------------------------------------------------------------------------------------------------------
Project: NumPy
-Version: 1.26
+Version: 2.4
-8152 objects in inventory.
+8456 objects in inventory.
-----------------------------------------------------------------------------------
+-----------------------------------------------------------------------------------------------------------
8 results found at/above current threshold of 75.
@@ -154,13 +152,13 @@ inventory creation/modification:
>>> import sphobjinv as soi
>>> inv = soi.Inventory('doc/build/html/objects.inv')
>>> print(inv)
-
+
>>> inv.project
'sphobjinv'
>>> inv.version
-'2.3'
+'2.4'
>>> inv.objects[0]
-DataObjStr(name='sphobjinv.cli.convert', domain='py', role='module', priority='0', uri='cli/implementation/convert.html#module-$', dispname='-')
+DataObjStr(name='sphobjinv.data', domain='py', role='module', priority='0', uri='api/data.html#module-$', dispname='-')
```
@@ -177,7 +175,7 @@ Available on [PyPI][pypi link target] (`pip install sphobjinv`).
Source on [GitHub][github repo]. Bug reports and feature requests are welcomed
at the [Issues][github issue tracker] page there.
-Copyright (c) Brian Skinn 2016-2025
+Copyright (c) 2016-2026 Brian Skinn and community contributors
The `sphobjinv` documentation (including docstrings and README) is licensed
under a [Creative Commons Attribution 4.0 International License][cc-by 4.0]
@@ -187,8 +185,8 @@ under a [Creative Commons Attribution 4.0 International License][cc-by 4.0]
[black badge]: https://img.shields.io/badge/code%20style-black-000000.svg
[black link target]: https://github.com/psf/black
-[cc-by 4.0]: http://creativecommons.org/licenses/by/4.0/
-[soi docs inv export]: http://sphobjinv.readthedocs.io/en/latest/api_usage.html#exporting-an-inventory
+[cc-by 4.0]: https:/creativecommons.org/licenses/by/4.0/
+[soi docs inv export]: https://sphobjinv.readthedocs.io/en/latest/api_usage.html#exporting-an-inventory
[github issue tracker]: https://github.com/bskinn/sphobjinv/issues
[github repo]: https://github.com/bskinn/sphobjinv
[gitter badge]: https://badges.gitter.im/sphobjinv/community.svg
@@ -198,11 +196,11 @@ under a [Creative Commons Attribution 4.0 International License][cc-by 4.0]
[mit license]: https://opensource.org/licenses/MIT
[numpy linspace]: https://numpy.org/doc/1.26/reference/generated/numpy.linspace.html
[pepy badge]: https://pepy.tech/badge/sphobjinv/month
-[pepy link target]: https://pepy.tech/projects/sphobjinv?timeRange=threeMonths&category=version&includeCIDownloads=true&granularity=daily&viewType=chart&versions=2.0.*%2C2.1.*%2C2.2.*%2C2.3.*
+[pepy link target]: https://pepy.tech/projects/sphobjinv?timeRange=threeMonths&category=version&includeCIDownloads=true&granularity=daily&versions=2.3.*%2C2.4*
[pypi badge]: https://img.shields.io/pypi/v/sphobjinv.svg?logo=pypi]
[pypi link target]: https://pypi.org/project/sphobjinv
[python versions badge]: https://img.shields.io/pypi/pyversions/sphobjinv.svg?logo=python
[readthedocs badge]: https://img.shields.io/readthedocs/sphobjinv/latest.svg
-[readthedocs link target]: http://sphobjinv.readthedocs.io/en/latest/
-[workflow badge]: https://img.shields.io/github/actions/workflow/status/bskinn/sphobjinv/ci_tests.yml?logo=github&branch=main
+[readthedocs link target]: https://sphobjinv.readthedocs.io/en/latest/
+[workflow badge]: https://img.shields.io/github/actions/workflow/status/bskinn/sphobjinv/all_core_tests.yml?logo=github&branch=main
[workflow link target]: https://github.com/bskinn/sphobjinv/actions
diff --git a/doc/make.bat b/doc/make.bat
index b9c03e59..5669cb67 100644
--- a/doc/make.bat
+++ b/doc/make.bat
@@ -21,7 +21,7 @@ if errorlevel 9009 (
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
- echo.http://sphinx-doc.org/
+ echo.https://sphinx-doc.org/
exit /b 1
)
diff --git a/doc/source/_static/mouseover_example.png b/doc/source/_static/mouseover_example.png
index eef34b44..7e604774 100644
Binary files a/doc/source/_static/mouseover_example.png and b/doc/source/_static/mouseover_example.png differ
diff --git a/doc/source/_templates/footer.html b/doc/source/_templates/footer.html
index e519d56f..b231f147 100644
--- a/doc/source/_templates/footer.html
+++ b/doc/source/_templates/footer.html
@@ -13,9 +13,9 @@
{% endblock %}
diff --git a/doc/source/api/index.rst b/doc/source/api/index.rst
index 04c908fc..480baf12 100644
--- a/doc/source/api/index.rst
+++ b/doc/source/api/index.rst
@@ -1,7 +1,7 @@
.. API page
-API
-===
+API Reference
+=============
Most (all?) of the objects documented in the below submodules
are also exposed at the |soi| package root. For example,
diff --git a/doc/source/api_usage.rst b/doc/source/api_usage.rst
index 8e6e097c..b8f236a5 100644
--- a/doc/source/api_usage.rst
+++ b/doc/source/api_usage.rst
@@ -17,11 +17,11 @@ Inspecting the contents of an existing inventory is handled entirely by the
>>> inv = soi.Inventory('objects_attrs.inv')
>>> print(inv)
-
+
>>> inv.version
- '22.1'
+ '25.4'
>>> inv.count
- 129
+ 180
The location of the inventory file to import can also be provided as
a :class:`pathlib.Path`, instead of as a string:
@@ -38,16 +38,16 @@ a |list| in the :attr:`~sphobjinv.inventory.Inventory.objects` attribute:
.. doctest:: api_inspect
>>> len(inv.objects)
- 129
+ 180
>>> dobj = inv.objects[0]
>>> dobj
- DataObjStr(name='attr', domain='py', role='module', priority='0', uri='index.html#module-$', dispname='-')
+ DataObjStr(name='attr', domain='py', role='module', priority='0', uri='api-attr.html#module-$', dispname='-')
>>> dobj.name
'attr'
>>> dobj.domain
'py'
- >>> [d.name for d in inv.objects if 'validator' in d.uri]
- ['api_validators', 'examples_validators']
+ >>> [d.name for d in inv.objects if 'validator' in d.name][:2]
+ ['attr.get_run_validators', 'attr.set_run_validators']
:class:`~sphobjinv.inventory.Inventory` objects can also import from plaintext or zlib-compressed
inventories, as |bytes|:
@@ -56,10 +56,10 @@ inventories, as |bytes|:
>>> inv2 = soi.Inventory(inv.data_file())
>>> print(inv2)
-
+
>>> inv3 = soi.Inventory(soi.compress(inv.data_file()))
>>> print(inv3)
-
+
Remote |objects.inv| files can also be retrieved via URL, with the *url* keyword argument:
@@ -67,7 +67,7 @@ Remote |objects.inv| files can also be retrieved via URL, with the *url* keyword
>>> inv4 = soi.Inventory(url='https://github.com/bskinn/sphobjinv/raw/main/tests/resource/objects_attrs.inv')
>>> print(inv4)
-
+
Comparing Inventories
---------------------
@@ -118,7 +118,7 @@ The :class:`~sphobjinv.data.DataObjStr` instances can be edited in place:
>>> inv = soi.Inventory('objects_attrs.inv')
>>> inv.objects[0]
- DataObjStr(name='attr', domain='py', role='module', priority='0', uri='index.html#module-$', dispname='-')
+ DataObjStr(name='attr', domain='py', role='module', priority='0', uri='api-attr.html#module-$', dispname='-')
>>> inv.objects[0].uri = 'attribute.html'
>>> inv.objects[0]
DataObjStr(name='attr', domain='py', role='module', priority='0', uri='attribute.html', dispname='-')
@@ -130,7 +130,7 @@ New instances can be easily created either by direct instantiation, or by
>>> inv.objects.append(inv.objects[0].evolve(name='attr.Generator', uri='generator.html'))
>>> inv.count
- 130
+ 181
>>> inv.objects[-1]
DataObjStr(name='attr.Generator', domain='py', role='module', priority='0', uri='generator.html', dispname='-')
@@ -141,7 +141,7 @@ The other attributes of the :class:`~sphobjinv.inventory.Inventory` instance can
>>> inv.project = 'not_attrs'
>>> inv.version = '0.1'
>>> print(inv)
-
+
Formatting Inventory Contents
@@ -156,10 +156,10 @@ the plaintext |objects.inv| format **as** |bytes| via :meth:`~sphobjinv.inventor
>>> print(*inv.data_file().splitlines()[:6], sep='\n')
b'# Sphinx inventory version 2'
b'# Project: attrs'
- b'# Version: 22.1'
+ b'# Version: 25.4'
b'# The remainder of this file is compressed using zlib.'
- b'attr py:module 0 index.html#module-$ -'
- b'attr.VersionInfo py:class 1 api.html#$ -'
+ b'attr py:module 0 api-attr.html#module-$ -'
+ b'attr.Attribute py:class 1 api-attr.html#$ -'
This method makes use of the :meth:`DataObjStr.data_line `
method to format each of the object information lines.
@@ -171,11 +171,11 @@ If desired, the :ref:`shorthand ` used for the
.. doctest:: api_formatting
>>> print(*inv.data_file(expand=True).splitlines()[4:6], sep='\n')
- b'attr py:module 0 index.html#module-attr attr'
- b'attr.VersionInfo py:class 1 api.html#attr.VersionInfo attr.VersionInfo'
+ b'attr py:module 0 api-attr.html#module-attr attr'
+ b'attr.Attribute py:class 1 api-attr.html#attr.Attribute attr.Attribute'
>>> do = inv.objects[0]
>>> do.data_line(expand=True)
- 'attr py:module 0 index.html#module-attr attr'
+ 'attr py:module 0 api-attr.html#module-attr attr'
Exporting an Inventory
@@ -202,10 +202,10 @@ To export plaintext:
>>> print(*Path('objects_attrs.txt').read_text().splitlines()[:6], sep='\n')
# Sphinx inventory version 2
# Project: attrs
- # Version: 22.1
+ # Version: 25.4
# The remainder of this file is compressed using zlib.
- attr py:module 0 index.html#module-$ -
- attr.VersionInfo py:class 1 api.html#$ -
+ attr py:module 0 api-attr.html#module-$ -
+ attr.Attribute py:class 1 api-attr.html#$ -
For zlib-compressed:
@@ -216,10 +216,10 @@ For zlib-compressed:
>>> print(*Path('objects_attrs_new.inv').read_bytes().splitlines()[:4], sep='\n')
b'# Sphinx inventory version 2'
b'# Project: attrs'
- b'# Version: 22.1'
+ b'# Version: 25.4'
b'# The remainder of this file is compressed using zlib.'
>>> print(Path('objects_attrs_new.inv').read_bytes().splitlines()[6][:10])
- b'\xbf\x86\x8fL49\xc4\x91\xb8\x8c'
+ b"$e2'\x92\xbde\xaa\xbdj"
For JSON:
diff --git a/doc/source/cli/convert.rst b/doc/source/cli/convert.rst
index b979fc78..7220276c 100644
--- a/doc/source/cli/convert.rst
+++ b/doc/source/cli/convert.rst
@@ -1,7 +1,7 @@
.. Description of convert commandline usage
-Command-Line Usage: "convert" Subcommand
-========================================
+Command-Line Usage: ``sphobjinv convert``
+=========================================
.. program:: sphobjinv convert
@@ -32,10 +32,10 @@ Basic file conversion to the default output filename is straightforward:
>>> print(file_head('objects_attrs.txt', head=6))
# Sphinx inventory version 2
# Project: attrs
- # Version: 22.1
+ # Version: 25.4
# The remainder of this file is compressed using zlib.
- attr py:module 0 index.html#module-$ -
- attr.VersionInfo py:class 1 api.html#$ -
+ attr py:module 0 api-attr.html#module-$ -
+ attr.Attribute py:class 1 api-attr.html#$ -
A different target filename can be specified, to avoid overwriting an existing
file:
@@ -76,7 +76,7 @@ indicated URL):
>>> print(file_head('objects.txt', head=6))
# Sphinx inventory version 2
# Project: attrs
- # Version: 22.1
+ # Version: ...
# The remainder of this file is compressed using zlib.
attr py:module 0 index.html#module-$ -
attr.VersionInfo py:class 1 api.html#$ -
@@ -135,11 +135,12 @@ If processing of JSON files by API URL is desirable, please
>>> cli_run('sphobjinv co plain objects_attrs.inv -')
# Sphinx inventory version 2
# Project: attrs
- # Version: 22.1
+ # Version: 25.4
# The remainder of this file is compressed using zlib.
- attr py:module 0 index.html#module-$ -
- attr.VersionInfo py:class 1 api.html#$ -
- attr._make.Attribute py:class -1 api.html#attrs.Attribute -
+ attr py:module 0 api-attr.html#module-$ -
+ attr.Attribute py:class 1 api-attr.html#$ -
+ attr.NOTHING py:data 1 api-attr.html#$ -
+ attr.VersionInfo py:class 1 api-attr.html#$ -
...
diff --git a/doc/source/cli/implementation/convert.rst b/doc/source/cli/implementation/convert.rst
deleted file mode 100644
index cd68000b..00000000
--- a/doc/source/cli/implementation/convert.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-.. Module API page for cli/convert.py
-
-sphobjinv.cli.convert
-=====================
-
-.. automodule:: sphobjinv.cli.convert
- :members:
diff --git a/doc/source/cli/implementation/core.rst b/doc/source/cli/implementation/core.rst
deleted file mode 100644
index c6fd0a53..00000000
--- a/doc/source/cli/implementation/core.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-.. Module API page for cli/core.py
-
-sphobjinv.cli.core
-==================
-
-.. automodule:: sphobjinv.cli.core
- :members:
-
diff --git a/doc/source/cli/implementation/index.rst b/doc/source/cli/implementation/index.rst
deleted file mode 100644
index a174e6f3..00000000
--- a/doc/source/cli/implementation/index.rst
+++ /dev/null
@@ -1,19 +0,0 @@
-.. Module API page for CLI submodule code
-
-sphobjinv.cli (non-API)
-=======================
-
-.. toctree::
- :maxdepth: 1
-
- convert
- core
- load
- parser
- paths
- suggest
- ui
- write
-
-
-.. .. |argparse| replace:: :mod:`argparse`
diff --git a/doc/source/cli/implementation/load.rst b/doc/source/cli/implementation/load.rst
deleted file mode 100644
index 2cdec7ec..00000000
--- a/doc/source/cli/implementation/load.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-.. Module API page for cli/load.py
-
-sphobjinv.cli.load
-==================
-
-.. automodule:: sphobjinv.cli.load
- :members:
-
diff --git a/doc/source/cli/implementation/parser.rst b/doc/source/cli/implementation/parser.rst
deleted file mode 100644
index a7a18aa1..00000000
--- a/doc/source/cli/implementation/parser.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-.. Module API page for cli/parser.py
-
-sphobjinv.cli.parser
-====================
-
-.. automodule:: sphobjinv.cli.parser
- :members:
-
diff --git a/doc/source/cli/implementation/paths.rst b/doc/source/cli/implementation/paths.rst
deleted file mode 100644
index dad6f38b..00000000
--- a/doc/source/cli/implementation/paths.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-.. Module API page for cli/paths.py
-
-sphobjinv.cli.paths
-===================
-
-.. automodule:: sphobjinv.cli.paths
- :members:
-
diff --git a/doc/source/cli/implementation/suggest.rst b/doc/source/cli/implementation/suggest.rst
deleted file mode 100644
index 3424d1b5..00000000
--- a/doc/source/cli/implementation/suggest.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-.. Module API page for cli/suggest.py
-
-sphobjinv.cli.suggest
-=====================
-
-.. automodule:: sphobjinv.cli.suggest
- :members:
diff --git a/doc/source/cli/implementation/ui.rst b/doc/source/cli/implementation/ui.rst
deleted file mode 100644
index bd59146d..00000000
--- a/doc/source/cli/implementation/ui.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-.. Module API page for cli/ui.py
-
-sphobjinv.cli.ui
-================
-
-.. automodule:: sphobjinv.cli.ui
- :members:
-
diff --git a/doc/source/cli/implementation/write.rst b/doc/source/cli/implementation/write.rst
deleted file mode 100644
index f1eb55a8..00000000
--- a/doc/source/cli/implementation/write.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-.. Module API page for cli/write.py
-
-sphobjinv.cli.write
-===================
-
-.. automodule:: sphobjinv.cli.write
- :members:
-
diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst
index 1974dbdc..952f11d7 100644
--- a/doc/source/cli/index.rst
+++ b/doc/source/cli/index.rst
@@ -3,23 +3,39 @@
Command-Line Usage
==================
-The CLI for |soi| is implemented using two subcommands:
+The primary CLI for |soi| is implemented using two subcommands of the
+``sphobjinv`` entrypoint:
- - A :doc:`convert ` subcommand, which handles conversion of
- inventories between supported formats (currently zlib-compressed,
+ - ``sphobjinv convert`` (:doc:`docs page `), which handles conversion
+ of inventories between supported formats (currently zlib-compressed,
plaintext, and JSON).
- - A :doc:`suggest ` subcommand, which provides suggestions for
+ - ``sphobjinv suggest`` (:doc:`docs page `), which provides suggestions for
objects in an inventory matching a desired search term.
-More information about the underlying implementation of these subcommands can
-be found :doc:`here ` and in the documentation for the
-:class:`~sphobjinv.inventory.Inventory` object, in particular the
-:meth:`~sphobjinv.inventory.Inventory.data_file` and
-:meth:`~sphobjinv.inventory.Inventory.suggest` methods.
+As of v2.4, |soi| also provides an auxiliary entrypoint,
+``sphobjinv-textconv`` (:doc:`docs page `), which takes one required
+argument: a path to a file on disk. This entrypoint attempts to instantiate an
+|Inventory| with this file and emit its plaintext contents to |stdout| with no
+cosmetic whitespace. The following two invocations are thus nearly synonymous::
-Some notes on these CLI docs:
+ $ sphobjinv convert plain path/to/objects.inv -
- * CLI docs examples are executed in a sandboxed directory pre-loaded with
+ $ sphobjinv-textconv path/to/objects.inv
+
+(Be sure to note the final hyphen in the first command.) The
+``sphobjinv-textconv`` spelling is less awkward when configuring a Git
+|textconv| to allow rendering diffs of |objects.inv| files in plaintext. See the
+``sphobjinv-textconv`` :doc:`entrypoint documentation ` for more
+information.
+
+----
+
+Shell examples in the CLI docs execute from within |cour|\ /tests/resource\
+|/cour| unless indicated otherwise.
+
+For Python examples:
+
+ * Examples are executed in a sandboxed directory pre-loaded with
|cour|\ objects_attrs.inv\ |/cour| (from, e.g.,
`here `__).
@@ -35,28 +51,10 @@ Some notes on these CLI docs:
* |cour|\ file_head\ |/cour| is a helper function
that retrieves the head of a specified file.
-
-.. program:: sphobjinv
-
-The options for the parent |soi| command are:
-
-.. option:: -h, --help
-
- Show help message and exit
-
-.. program-output:: sphobjinv --help
-
-
-.. option:: -v, --version
-
- Print package version & other info
-
-.. program-output:: sphobjinv --version
-
-
.. toctree::
:maxdepth: 1
:hidden:
- "convert" Mode
- "suggest" Mode
+ sphobjinv convert
+ sphobjinv suggest
+ sphobjinv-textconv
diff --git a/doc/source/cli/soi_root.rst b/doc/source/cli/soi_root.rst
new file mode 100644
index 00000000..83ca6337
--- /dev/null
+++ b/doc/source/cli/soi_root.rst
@@ -0,0 +1,26 @@
+:orphan:
+
+.. Description of root soi entrypoint commandline usage
+
+Root "sphobjinv" Entrypoint Usage
+=================================
+
+.. program:: sphobjinv
+
+The following options of the 'root' of the ``sphobjinv`` CLI entrypoint are
+documented in this 'orphan' page so that they will appear in the docset
+`objects.inv` and be available for linking, if needed, but otherwise be hidden
+from the navigation of the documentation.
+
+.. option:: -h, --help
+
+ Show help message and exit
+
+.. program-output:: sphobjinv --help
+
+
+.. option:: -v, --version
+
+ Print package version & other info
+
+.. program-output:: sphobjinv --version
diff --git a/doc/source/cli/suggest.rst b/doc/source/cli/suggest.rst
index 21fa0a29..4bf3b47e 100644
--- a/doc/source/cli/suggest.rst
+++ b/doc/source/cli/suggest.rst
@@ -1,7 +1,7 @@
.. Description of suggest commandline usage
-Command-Line Usage: "suggest" Subcommand
-========================================
+Command-Line Usage: ``sphobjinv suggest``
+=========================================
.. program:: sphobjinv suggest
@@ -29,7 +29,7 @@ via :option:`--thresh`:
Remote |objects.inv| files can be retrieved for inspection by passing the
:option:`--url` flag:
-.. command-output:: sphobjinv suggest https://github.com/bskinn/sphobjinv/raw/main/tests/resource/objects_attrs.inv instance -u -t 48
+.. command-output:: sphobjinv suggest https://sphobjinv.readthedocs.io/en/stable/objects.inv Inventory -u -t 85
:cwd: /../../tests/resource
The URL provided **MUST** have the leading protocol specified (here,
@@ -82,9 +82,9 @@ If download of JSON files by URL is desirable, please
.. option:: -a, --all
- Display all search results without prompting, regardless of the number of hits.
- Otherwise, prompt if number of results exceeds
- :attr:`~sphobjinv.cli.parser.PrsConst.SUGGEST_CONFIRM_LENGTH`.
+ Display all search results without prompting, regardless of the number of
+ hits. Otherwise, prompt for confirmation before displaying the entire result
+ set if count exceeds |cli:SUGGEST_CONFIRM_LENGTH|.
.. option:: -i, --index
@@ -99,8 +99,7 @@ If download of JSON files by URL is desirable, please
.. option:: -t, --thresh <#>
Change the |fuzzywuzzy|_ match quality threshold (0-100; higher values
- yield fewer results). Default is specified in
- :attr:`~sphobjinv.cli.parser.PrsConst.DEF_THRESH`.
+ yield fewer results). Current default threshold is |cli:DEF_THRESH|.
.. option:: -u, --url
diff --git a/doc/source/cli/textconv.rst b/doc/source/cli/textconv.rst
new file mode 100644
index 00000000..b01c7b95
--- /dev/null
+++ b/doc/source/cli/textconv.rst
@@ -0,0 +1,85 @@
+.. Description of sphobjinv-textconv commandline usage
+
+Command-Line Usage: ``sphobjinv-textconv``
+==========================================
+
+.. program:: sphobjinv-textconv
+
+``sphobjinv-textconv`` is intentionally implemented with very narrow
+functionality, focused on simplifying use of |soi| as a `Git "textconv"
+`__,
+which is a mechanism for rendering binary files in a diff-able text format.
+There are many `examples`__ of `clever application`__ of `textconv`__ in the
+wild.
+
+.. __: https://github.com/pixelb/crudini/issues/90
+.. __: https://github.com/syntevosmartgit/textconv
+.. __: https://stackoverflow.com/questions/55601430/how-to-pass-a-filename-argument-gitconfig-diff-textconv
+
+
+Ultimately, a textconv requires three things:
+
+1. A utility that takes in a file path as a single positional argument and emits
+ a plaintext representation to |stdout|, such as |sphobjinv-textconv|.
+
+2. An entry somewhere in Git config (system, user-global, per-repo, etc.)
+ declaring a "diff driver" set up to use that utility as its |textconv|.
+ Example::
+
+ [diff "objects_inv"]
+ textconv = sphobjinv-textconv
+
+ Note that the utility must be on path in all contexts where you wish to use
+ it as a textconv.
+
+3. An entry somewhere in |.gitattributes| (system, user-global, per-repo, etc.)
+ that associates a particular file or glob pattern with the diff driver. Example::
+
+ *.inv diff=objects_inv
+
+With |sphobjinv-textconv| configured in this fashion as a textconv for Sphinx
+inventory files, the following should all yield *nearly* the same output.
+
+Using ``sphobjinv convert``:
+
+.. command-output:: sphobjinv convert plain objects_pdfminer.inv -
+ :cwd: /../../tests/resource
+
+Using ``sphobjinv-textconv`` (note the absence of blank lines between the shell
+invocation and the inventory contents):
+
+.. command-output:: sphobjinv-textconv objects_pdfminer.inv
+ :cwd: /../../tests/resource
+
+Using ``git show --textconv`` (with the |textconv| set on-the-fly so that it
+will render correctly in ReadTheDocs builds):
+
+.. command-output:: git -c diff.objects_inv.textconv=sphobjinv-textconv show --textconv HEAD:tests/resource/objects_pdfminer.inv
+ :cwd: /../../tests/resource
+
+----
+
+
+**Usage**
+
+.. command-output:: sphobjinv-textconv --help
+ :ellipsis: 4
+
+
+**Positional Arguments**
+
+.. option:: infile
+
+ Path to file to be emitted to |stdout| in plaintext.
+
+**Flags**
+
+.. option:: -h, --help
+
+ Display help message and exit.
+
+.. option:: -v, --version
+
+ Display brief package version information and exit.
+
+.. versionadded:: 2.4
diff --git a/doc/source/conf.py b/doc/source/conf.py
index f3afc081..c9a29321 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -14,15 +14,19 @@
# import sys
# sys.path.insert(0, os.path.abspath('.'))
+# -- Imports -----------------------------------------------------------------
+import sphobjinv as soi
+from sphobjinv.cli.parser import PrsConst
# -- Project information -----------------------------------------------------
project = "sphobjinv"
-copyright = "2016-2025, Brian Skinn"
+copyright = "2016-2026, Brian Skinn and community contributors"
author = "Brian Skinn"
# The full version for `release`, including alpha/beta/rc tags
-from sphobjinv import __version__ as release
+release = soi.__version__
+
# Just major.minor for `version`
version = ".".join(release.split(".")[:2])
@@ -70,7 +74,7 @@
# -- Common epilogue definition ------------------------------------------------
-rst_epilog = r"""
+rst_epilog = rf"""
.. |extlink| image:: /_static/extlink.svg
@@ -166,41 +170,19 @@
sphobjinv
-.. |stdin| replace:: |cour|\ stdin\ |/cour|
-
-.. |stdout| replace:: |cour|\ stdout\ |/cour|
-
-.. |cli:ALL| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.ALL`
-
-.. |cli:DEF_BASENAME| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.DEF_BASENAME`
-
-.. |cli:DEF_OUT_EXT| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.DEF_OUT_EXT`
-
-.. |cli:FOUND_URL| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.FOUND_URL`
+.. |cli:DEF_THRESH| replace:: {PrsConst.DEF_THRESH}
-.. |cli:INDEX| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.INDEX`
+.. |cli:SUGGEST_CONFIRM_LENGTH| replace:: {PrsConst.SUGGEST_CONFIRM_LENGTH}
-.. |cli:INFILE| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.INFILE`
-
-.. |cli:MODE| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.MODE`
-
-.. |cli:OUTFILE| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.OUTFILE`
-
-.. |cli:OVERWRITE| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.OVERWRITE`
-
-.. |cli:QUIET| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.QUIET`
-
-.. |cli:SCORE| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.SCORE`
-
-.. |cli:SUBPARSER_NAME| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.SUBPARSER_NAME`
+.. |stdin| replace:: |cour|\ stdin\ |/cour|
-.. |cli:SUGGEST_CONFIRM_LENGTH| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.SUGGEST_CONFIRM_LENGTH`
+.. |stdout| replace:: |cour|\ stdout\ |/cour|
-.. |cli:URL| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.URL`
+.. |textconv| replace:: |cour|\ textconv\ |/cour|
-.. |cli:VERSION| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.VERSION`
+.. |.gitattributes| replace:: |cour|\ .gitattributes\ |/cour|
-.. |resolve_inpath| replace:: :func:`~sphobjinv.cli.paths.resolve_inpath`
+.. |sphobjinv-textconv| replace:: |cour|\ sphobjinv-textconv\ |/cour|
"""
diff --git a/doc/source/customfile.rst b/doc/source/customfile.rst
index 3765cc57..2cbc3587 100644
--- a/doc/source/customfile.rst
+++ b/doc/source/customfile.rst
@@ -143,7 +143,7 @@ can be found at the GitHub repo
intersphinx_mapping = {
# Standard reference to web docs, with web objects.inv
- 'python': ('https://docs.python.org/3.12', None),
+ 'python': ('https://docs.python.org/3.14', None),
# Django puts its objects.inv file in a non-standard location
'django': ('https://docs.djangoproject.com/en/dev/', 'https://docs.djangoproject.com/en/dev/_objects/'),
diff --git a/doc/source/index.rst b/doc/source/index.rst
index ad8f24a2..28f3a74e 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -11,7 +11,7 @@ Welcome to sphobjinv!
When documentation is built using, e.g., Sphinx's :obj:`~sphinx.builders.html.StandaloneHTMLBuilder`,
an inventory of the named objects in the documentation set `is dumped
-`__
+`__
to a file called |objects.inv| in the html build directory.
(One common location is, |cour|\ doc/build/html\ |/cour|, though the exact location will vary
depending on the details of how Sphinx is configured.) This file is read by |isphx| when
@@ -57,6 +57,8 @@ and package managers, including:
* Arch Linux: ``python-sphobjinv``
+ * Debian: ``python-sphobjinv`` (`info `__)
+
* Fedora: ``python-sphobjinv`` (`info `__)
* Gentoo: ``dev-python/sphobjinv`` (`info `__)
@@ -100,8 +102,6 @@ The project source repository is on GitHub: `bskinn/sphobjinv
levenshtein
syntax
api/index
- CLI Implementation (non-API)
-
Indices and Tables
diff --git a/doc/source/syntax.rst b/doc/source/syntax.rst
index ab51b116..0b0f57ed 100644
--- a/doc/source/syntax.rst
+++ b/doc/source/syntax.rst
@@ -5,8 +5,8 @@ Sphinx objects.inv v2 Syntax
After decompression, "version 2" Sphinx |objects.inv| files follow a syntax
that, to the best of this author's ability to determine, is not included in the
-Sphinx documentation. The below syntax is believed to be accurate as of May 2024
-(Sphinx v7.3.7). It is based on inspection of |objects.inv| files "in the
+Sphinx documentation. The below syntax is believed to be accurate as of March 2026
+(Sphinx v9.1.0). It is based on inspection of |objects.inv| files "in the
wild" and of the Sphinx inventory object `parsing regex`_.
Based upon a quick ``git diff`` of the `Sphinx repository
@@ -27,7 +27,7 @@ data line.
----
**The first line** `must be exactly
-`__:
+`__:
.. code-block:: none
@@ -36,7 +36,7 @@ data line.
----
**The second and third lines** `must obey
-`__
+`__
the template:
.. code-block:: none
@@ -56,7 +56,7 @@ the |isphx| cross-references:
----
**The fourth line** `must contain
-`__
+`__
the string ``zlib`` somewhere within it, but for consistency it should be exactly:
.. code-block:: none
@@ -67,7 +67,7 @@ the string ``zlib`` somewhere within it, but for consistency it should be exactl
**All remaining lines** of the file are the objects data, each laid out in the
`following syntax
-`__:
+`__:
.. code-block:: none
@@ -132,7 +132,7 @@ the string ``zlib`` somewhere within it, but for consistency it should be exactl
``{priority}``
Flag for `placement in search results
- `__. Most will be ``1`` (standard priority) or
+ `__. Most will be ``1`` (standard priority) or
``-1`` (omit from results) for documentation built by Sphinx;
values of ``0`` (higher priority) or ``2`` (lower priority) may also occur.
@@ -194,7 +194,7 @@ of cross-references from other documentation source.
**For illustration**, the following is the entry for the
:meth:`join() ` method of the :class:`str` class in the
-Python 3.12 |objects.inv|, broken out field-by-field:
+Python 3.14 |objects.inv|, broken out field-by-field:
.. code-block:: none
@@ -217,11 +217,11 @@ size of the inventory file:
`__," the portion
following the ``#`` symbol) and the tail of the anchor is identical to
|{name}|_, that tail is `replaced
- `__
+ `__
with ``$``. |br| |br|
#. If |{dispname}|_ is identical to |{name}|_, it is `stored
- `__
+ `__
as ``-``.
Thus, a standard |isphx| reference to this method would take the form:
@@ -271,11 +271,11 @@ as in :obj:`This is join! `:
.. |prio_js_search| replace:: here
-.. _prio_js_search: https://github.com/sphinx-doc/sphinx/blob/ac3f74a3e0fbb326f73989a16dfa369e072064ca/sphinx/themes/basic/static/searchtools.js#L28-L46
+.. _prio_js_search: https://github.com/sphinx-doc/sphinx/blob/e552f8429c3039bc0649a7da82bdfa0df3273c3d/sphinx/themes/basic/static/searchtools.js#L21-L39
.. |prio_py_search| replace:: here
-.. _prio_py_search: https://github.com/sphinx-doc/sphinx/blob/ac3f74a3e0fbb326f73989a16dfa369e072064ca/sphinx/search/__init__.py#L344-L345
+.. _prio_py_search: https://github.com/sphinx-doc/sphinx/blob/e552f8429c3039bc0649a7da82bdfa0df3273c3d/sphinx/search/__init__.py#L377-L378
.. |sphinx_uri_issue| replace:: sphinx-doc/sphinx#7096
@@ -297,4 +297,4 @@ as in :obj:`This is join! `:
.. _rst-directive-option: https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-rst-directive-option
-.. _parsing regex: https://github.com/sphinx-doc/sphinx/blob/ac3f74a3e0fbb326f73989a16dfa369e072064ca/sphinx/util/inventory.py#L134-L135
+.. _parsing regex: https://github.com/sphinx-doc/sphinx/blob/e552f8429c3039bc0649a7da82bdfa0df3273c3d/sphinx/util/inventory.py#L115-L119
diff --git a/pyproject.toml b/pyproject.toml
index 5f35b59b..1ed6313f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,11 +8,10 @@ requires = [
[project]
name = "sphobjinv"
description = "Sphinx objects.inv Inspection/Manipulation Tool"
-license = {text = "MIT License"}
+license = "MIT"
+license-files = ["LICENSE.txt"]
authors = [{name = "Brian Skinn", email = "brian.skinn@gmail.com"}]
classifiers = [
- "License :: OSI Approved",
- "License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Environment :: Console",
"Framework :: Sphinx",
@@ -21,11 +20,11 @@ classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
"Topic :: Documentation",
"Topic :: Documentation :: Sphinx",
"Topic :: Software Development",
@@ -34,28 +33,29 @@ classifiers = [
"Development Status :: 5 - Production/Stable",
]
keywords = ["sphinx", "sphinx-doc", "inventory", "manager", "inspector"]
-requires-python = ">=3.9"
+requires-python = ">=3.10"
dependencies = [
"attrs>=19.2",
"certifi",
- "jsonschema>=3.0",
+ "jsonschema>=3.1.1",
]
dynamic = ["version", "readme"]
[project.urls]
-Homepage = "https://github.com/bskinn/sphobjinv"
Changelog = "https://github.com/bskinn/sphobjinv/blob/main/CHANGELOG.md"
Docs = "https://sphobjinv.readthedocs.io/en/stable/"
-Thank = "https://fosstodon.org/@btskinn"
Donate = "https://github.com/sponsors/bskinn"
+Homepage = "https://github.com/bskinn/sphobjinv"
+Thank = "https://fosstodon.org/@btskinn"
[project.scripts]
sphobjinv = "sphobjinv.cli.core:main"
+sphobjinv-textconv = "sphobjinv.cli.core:main_textconv"
+
[tool.setuptools]
package-dir = {"" = "src"}
platforms = ["any"]
-license-files = ["LICENSE.txt"]
include-package-data = false
[tool.setuptools.packages.find]
@@ -66,6 +66,7 @@ namespaces = false
version = {attr = "sphobjinv.version.__version__"}
[tool.black]
+target-version = ["py313"]
line-length = 88
include = '''
(
@@ -89,3 +90,9 @@ exclude = '''
exclude = ["src/sphobjinv/_vendored"]
fail-under = 100
verbose = 1
+
+[tool.isort]
+profile = "black"
+known_first_party = ["sphobjinv", "tests"]
+no_lines_before = ["LOCALFOLDER"]
+extend_skip = ["src/sphobjinv/_vendored"]
diff --git a/requirements-ci.txt b/requirements-ci.txt
index 16e23de4..68da2580 100644
--- a/requirements-ci.txt
+++ b/requirements-ci.txt
@@ -1,15 +1,12 @@
-attrs>=19.2
-certifi
coverage
dictdiffer
-jsonschema
md-toc
pytest>=4.4.0
pytest-check>=1.1.2
pytest-cov
pytest-retry
pytest-timeout
-sphinx==7.4.7
+sphinx==8.1.3
sphinx-issues
sphinx-rtd-theme>=0.5.1
sphinxcontrib-programoutput
diff --git a/requirements-dev.txt b/requirements-dev.txt
index f6258f9f..df2e9b95 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,9 +1,5 @@
-attrs>=19.2
-build
-certifi
coverage
dictdiffer
-jsonschema
md-toc
pytest>=4.4.0
pytest-check>=1.1.2
@@ -11,7 +7,7 @@ pytest-cov
pytest-retry
pytest-timeout
restview
-sphinx==7.4.7 # Staying <8 since 8.0 drops Python 3.9
+sphinx==8.1.3 # Staying <8.2 to retain Python 3.10 compat
sphinx-autobuild
sphinx-issues
sphinx-rtd-theme>=0.5.1
@@ -19,5 +15,4 @@ sphinxcontrib-programoutput
stdio-mgr>=1.0.1
tox
twine
--r requirements-flake8.txt
-e .
diff --git a/requirements-flake8.txt b/requirements-flake8.txt
index a2e76cd1..93ea2c55 100644
--- a/requirements-flake8.txt
+++ b/requirements-flake8.txt
@@ -10,7 +10,7 @@ flake8-comprehensions
flake8-docstrings>=1.3.1
flake8-eradicate
flake8-implicit-str-concat
-flake8-import-order
+flake8-isort
flake8-pie
flake8-raise
flake8-rst-docstrings
diff --git a/requirements-rtd.txt b/requirements-rtd.txt
index 618f3d7c..c42c329f 100644
--- a/requirements-rtd.txt
+++ b/requirements-rtd.txt
@@ -1,5 +1,5 @@
attrs>=19.2
-sphinx==7.4.7
+sphinx==8.1.3
sphinx-issues
sphinx-rtd-theme>=0.5.1
sphinxcontrib-programoutput
diff --git a/setup.py b/setup.py
index f172d296..00615320 100644
--- a/setup.py
+++ b/setup.py
@@ -9,13 +9,13 @@
exec(Path("src", "sphobjinv", "version.py").read_text(encoding="utf-8"), exec_ns)
__version__ = exec_ns["__version__"]
-version_override = "2.3.1.2"
+version_override = None
def readme():
content = Path("README.md").read_text(encoding="utf-8")
- new_ver = version_override if version_override else __version__
+ new_ver = version_override or __version__
# Helper function
def content_update(content, pattern, sub):
diff --git a/src/sphobjinv/__init__.py b/src/sphobjinv/__init__.py
index 0e141f96..e46dd28a 100644
--- a/src/sphobjinv/__init__.py
+++ b/src/sphobjinv/__init__.py
@@ -10,7 +10,7 @@
17 May 2016
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
https://github.com/bskinn/sphobjinv
diff --git a/src/sphobjinv/__main__.py b/src/sphobjinv/__main__.py
index 6e307758..d063bb24 100644
--- a/src/sphobjinv/__main__.py
+++ b/src/sphobjinv/__main__.py
@@ -10,7 +10,7 @@
15 May 2020
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
https://github.com/bskinn/sphobjinv
diff --git a/src/sphobjinv/_vendored/__init__.py b/src/sphobjinv/_vendored/__init__.py
index 36ba0197..b5f7e176 100644
--- a/src/sphobjinv/_vendored/__init__.py
+++ b/src/sphobjinv/_vendored/__init__.py
@@ -12,7 +12,7 @@
11 Dec 2021
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
https://github.com/bskinn/sphobjinv
diff --git a/src/sphobjinv/_vendored/fuzzywuzzy/__init__.py b/src/sphobjinv/_vendored/fuzzywuzzy/__init__.py
index ff3129d3..f88ee546 100644
--- a/src/sphobjinv/_vendored/fuzzywuzzy/__init__.py
+++ b/src/sphobjinv/_vendored/fuzzywuzzy/__init__.py
@@ -25,7 +25,7 @@
11 Dec 2021
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
https://github.com/bskinn/sphobjinv
diff --git a/src/sphobjinv/cli/__init__.py b/src/sphobjinv/cli/__init__.py
index 357e6e8c..d7b267b8 100644
--- a/src/sphobjinv/cli/__init__.py
+++ b/src/sphobjinv/cli/__init__.py
@@ -10,7 +10,7 @@
15 Nov 2020
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
https://github.com/bskinn/sphobjinv
diff --git a/src/sphobjinv/cli/convert.py b/src/sphobjinv/cli/convert.py
index d07353f3..829f1c79 100644
--- a/src/sphobjinv/cli/convert.py
+++ b/src/sphobjinv/cli/convert.py
@@ -10,7 +10,7 @@
20 Oct 2022
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
https://github.com/bskinn/sphobjinv
@@ -36,13 +36,13 @@
def do_convert(inv, in_path, params):
r"""Carry out the conversion operation, including writing output.
- If |cli:OVERWRITE| is passed and the output file
- (the default location, or as passed to |cli:OUTFILE|)
+ If OVERWRITE is passed and the output file
+ (the default location, or as passed to OUTFILE)
exists, it will be overwritten without a prompt. Otherwise,
the user will be queried if it is desired to overwrite
the existing file.
- If |cli:QUIET| is passed, nothing will be
+ If QUIET is passed, nothing will be
printed to |cour|\ stdout\ |/cour|
(potentially useful for scripting),
and any existing output file will be overwritten
@@ -53,7 +53,7 @@ def do_convert(inv, in_path, params):
inv
|Inventory| -- Inventory object to be output in the format
- indicated by |cli:MODE|.
+ indicated by MODE.
in_path
diff --git a/src/sphobjinv/cli/core.py b/src/sphobjinv/cli/core.py
index 33e1c1d2..3fbc6d1c 100644
--- a/src/sphobjinv/cli/core.py
+++ b/src/sphobjinv/cli/core.py
@@ -10,7 +10,7 @@
15 Nov 2020
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
https://github.com/bskinn/sphobjinv
@@ -33,7 +33,7 @@
from sphobjinv.cli.convert import do_convert
from sphobjinv.cli.load import inv_local, inv_stdin, inv_url
-from sphobjinv.cli.parser import getparser, PrsConst
+from sphobjinv.cli.parser import PrsConst, getparser, getparser_textconv
from sphobjinv.cli.suggest import do_suggest
from sphobjinv.cli.ui import print_stderr
@@ -43,14 +43,14 @@ def main():
Parses command line arguments,
handling the no-arguments and
- |cli:VERSION| cases.
+ VERSION cases.
Creates the |Inventory| from the indicated source
and method.
Invokes :func:`~sphobjinv.cli.convert.do_convert` or
:func:`~sphobjinv.cli.suggest.do_suggest`
- per the subparser name stored in |cli:SUBPARSER_NAME|.
+ per the subparser name stored in SUBPARSER_NAME.
"""
# If no args passed, stick in '-h'
@@ -101,3 +101,26 @@ def main():
# Clean exit
sys.exit(0)
+
+
+def main_textconv():
+ """Entrypoint for textconv operation."""
+ # If no args passed, stick in '-h'
+ if len(sys.argv) == 1:
+ sys.argv.append("-h")
+
+ prs = getparser_textconv()
+ params = vars(prs.parse_args())
+
+ # No version arg handling, using 'version' action in this parser
+
+ inv, in_path = inv_local(params)
+
+ params[PrsConst.CONTRACT] = False
+ params[PrsConst.EXPAND] = False
+ params[PrsConst.MODE] = PrsConst.PLAIN
+ params[PrsConst.OUTFILE] = "-"
+
+ do_convert(inv, in_path, params)
+
+ sys.exit(0)
diff --git a/src/sphobjinv/cli/load.py b/src/sphobjinv/cli/load.py
index 3df47077..07032c95 100644
--- a/src/sphobjinv/cli/load.py
+++ b/src/sphobjinv/cli/load.py
@@ -10,7 +10,7 @@
17 Nov 2020
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
https://github.com/bskinn/sphobjinv
@@ -36,7 +36,7 @@
from jsonschema.exceptions import ValidationError
-from sphobjinv import Inventory, readjson, urlwalk, VersionError
+from sphobjinv import Inventory, VersionError, readjson, urlwalk
from sphobjinv.cli.parser import PrsConst
from sphobjinv.cli.paths import resolve_inpath
from sphobjinv.cli.ui import err_format, print_stderr
@@ -83,8 +83,8 @@ def import_infile(in_path):
def inv_local(params):
"""Create |Inventory| from local source.
- Uses |resolve_inpath| to sanity-check and/or convert
- |cli:INFILE|.
+ Uses ``resolve_inpath`` to sanity-check and/or convert
+ INFILE.
Calls :func:`sys.exit` internally in error-exit situations.
@@ -99,12 +99,12 @@ def inv_local(params):
inv
|Inventory| -- Object representation of the inventory
- at |cli:INFILE|
+ at INFILE
in_path
|str| -- Input file path as resolved/checked by
- |resolve_inpath|
+ ``resolve_inpath``
"""
# Resolve input file path
@@ -127,7 +127,7 @@ def inv_local(params):
def inv_url(params):
"""Create |Inventory| from file downloaded from URL.
- Initially, treats |cli:INFILE| as a download URL to be passed to
+ Initially, treats INFILE as a download URL to be passed to
the `url` initialization argument
of :class:`~sphobjinv.inventory.Inventory`.
@@ -135,7 +135,7 @@ def inv_url(params):
searches the directory tree of the URL for |objects.inv|.
Injects the URL at which an inventory was found into `params`
- under the |cli:FOUND_URL| key.
+ under the FOUND_URL key.
Calls :func:`sys.exit` internally in error-exit situations.
@@ -150,11 +150,11 @@ def inv_url(params):
inv
|Inventory| -- Object representation of the inventory
- at |cli:INFILE|
+ at INFILE
ret_path
- |str| -- URL from |cli:INFILE| used to construct `inv`.
+ |str| -- URL from INFILE used to construct `inv`.
If URL is longer than 45 characters, the central portion is elided.
"""
diff --git a/src/sphobjinv/cli/parser.py b/src/sphobjinv/cli/parser.py
index edc5ed16..44e1a1ab 100644
--- a/src/sphobjinv/cli/parser.py
+++ b/src/sphobjinv/cli/parser.py
@@ -10,7 +10,7 @@
15 Nov 2020
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
https://github.com/bskinn/sphobjinv
@@ -44,7 +44,8 @@ class PrsConst:
#: Version &c. output blurb
VER_TXT = (
- f"\nsphobjinv v{__version__}\n\nCopyright (c) Brian Skinn 2016-2025\n"
+ f"\nsphobjinv v{__version__}\n\n"
+ "Copyright (c) 2016-2026 Brian Skinn and community contributors\n"
"License: The MIT License\n\n"
"Bug reports & feature requests:"
" https://github.com/bskinn/sphobjinv\n"
@@ -52,6 +53,9 @@ class PrsConst:
" https://sphobjinv.readthedocs.io\n"
)
+ #: Short version text for textconv entrypoint
+ VER_TXT_SHORT = f"sphobjinv v{__version__}"
+
# ### Subparser selectors and argparse param for storing subparser name
#: Subparser name for inventory file conversions; stored in
#: :data:`SUBPARSER_NAME` when selected
@@ -227,6 +231,8 @@ def getparser():
# briefly required a/o 3.7.0b4 due to change in default behavior, per:
# https://bugs.python.org/issue33109. 3.6 behavior restored for
# 3.7 release.
+ #
+ # We retain for explicitness
sprs.required = False
spr_convert = sprs.add_parser(
@@ -381,3 +387,35 @@ def getparser():
)
return prs
+
+
+def getparser_textconv():
+ """Generate argument parser for textconv entrypoint.
+
+ Returns
+ -------
+ prs
+
+ :class:`~argparse.ArgumentParser` -- Parser for textconv commandline
+ usage of |soi|
+
+ """
+ prs = ap.ArgumentParser(
+ description=(
+ "Emit the plaintext of the local Sphinx inventory at 'infile' to stdout."
+ )
+ )
+ prs.add_argument(
+ "-" + PrsConst.VERSION[0],
+ "--" + PrsConst.VERSION,
+ help="Print package version & other info",
+ action="version",
+ version=PrsConst.VER_TXT_SHORT,
+ )
+
+ prs.add_argument(
+ PrsConst.INFILE,
+ help=("Path to file to be converted"),
+ )
+
+ return prs
diff --git a/src/sphobjinv/cli/paths.py b/src/sphobjinv/cli/paths.py
index 06ea5f57..1ce9581e 100644
--- a/src/sphobjinv/cli/paths.py
+++ b/src/sphobjinv/cli/paths.py
@@ -10,7 +10,7 @@
19 Nov 2020
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
https://github.com/bskinn/sphobjinv
@@ -72,11 +72,11 @@ def resolve_outpath(out_path, in_path, params):
If the output path or basename are not specified, they are
taken as the same as the input file. If the extension is
unspecified, it is taken as the appropriate mode-specific value
- from |cli:DEF_OUT_EXT|.
+ from DEF_OUT_EXT.
- If |cli:URL| is passed, the input directory
+ If URL is passed, the input directory
is taken to be :func:`os.getcwd` and the input basename
- is taken as |cli:DEF_BASENAME|.
+ is taken as DEF_BASENAME.
Parameters
----------
diff --git a/src/sphobjinv/cli/suggest.py b/src/sphobjinv/cli/suggest.py
index 31db9585..865dd4a6 100644
--- a/src/sphobjinv/cli/suggest.py
+++ b/src/sphobjinv/cli/suggest.py
@@ -10,7 +10,7 @@
20 Oct 2022
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
https://github.com/bskinn/sphobjinv
@@ -43,18 +43,18 @@ def do_suggest(inv, params):
Results are printed one per line.
- If neither |cli:INDEX| nor |cli:SCORE| is specified,
+ If neither INDEX nor SCORE is specified,
the results are output without a header.
If either or both are specified,
the results are output in a lightweight tabular format.
If the number of results exceeds
- |cli:SUGGEST_CONFIRM_LENGTH|,
+ SUGGEST_CONFIRM_LENGTH,
the user will be queried whether to display
all of the returned results
- unless |cli:ALL| is specified.
+ unless ALL is specified.
- No |cli:QUIET| option is available here, since
+ No QUIET option is available here, since
a silent mode for suggestion output is nonsensical.
Parameters
@@ -62,7 +62,7 @@ def do_suggest(inv, params):
inv
|Inventory| -- Inventory object to be output in the format
- indicated by |cli:MODE|.
+ indicated by MODE.
params
diff --git a/src/sphobjinv/cli/ui.py b/src/sphobjinv/cli/ui.py
index 7248ea08..a886880b 100644
--- a/src/sphobjinv/cli/ui.py
+++ b/src/sphobjinv/cli/ui.py
@@ -10,7 +10,7 @@
19 Nov 2020
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
https://github.com/bskinn/sphobjinv
@@ -37,7 +37,7 @@
def print_stderr(thing, params, *, end="\n"):
r"""Print `thing` to stderr if not in quiet mode.
- Quiet mode is indicated by the value at the |cli:QUIET| key
+ Quiet mode is indicated by the value at the QUIET key
within `params`.
Quiet mode is not implemented for the ":doc:`suggest `"
@@ -58,7 +58,11 @@ def print_stderr(thing, params, *, end="\n"):
|str| -- String to append to printed content (default: ``\n``\ )
"""
- if params[PrsConst.SUBPARSER_NAME][:2] == "su" or not params[PrsConst.QUIET]:
+ if (
+ PrsConst.SUBPARSER_NAME not in params # textconv
+ or params[PrsConst.SUBPARSER_NAME][:2] == "su" # suggest is never quiet
+ or not params[PrsConst.QUIET] # non-quiet convert
+ ):
print(thing, file=sys.stderr, end=end)
diff --git a/src/sphobjinv/cli/write.py b/src/sphobjinv/cli/write.py
index f7f0816e..b9a9930d 100644
--- a/src/sphobjinv/cli/write.py
+++ b/src/sphobjinv/cli/write.py
@@ -10,7 +10,7 @@
19 Nov 2020
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
https://github.com/bskinn/sphobjinv
diff --git a/src/sphobjinv/data.py b/src/sphobjinv/data.py
index 10a6c7d7..76cade55 100644
--- a/src/sphobjinv/data.py
+++ b/src/sphobjinv/data.py
@@ -10,7 +10,7 @@
7 Nov 2017
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
https://github.com/bskinn/sphobjinv
diff --git a/src/sphobjinv/enum.py b/src/sphobjinv/enum.py
index d046dc95..3fa4fcd0 100644
--- a/src/sphobjinv/enum.py
+++ b/src/sphobjinv/enum.py
@@ -10,7 +10,7 @@
4 May 2019
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
https://github.com/bskinn/sphobjinv
diff --git a/src/sphobjinv/error.py b/src/sphobjinv/error.py
index 37422303..ba07b200 100644
--- a/src/sphobjinv/error.py
+++ b/src/sphobjinv/error.py
@@ -10,7 +10,7 @@
5 Nov 2017
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
https://github.com/bskinn/sphobjinv
diff --git a/src/sphobjinv/fileops.py b/src/sphobjinv/fileops.py
index feba9dad..e30c4b42 100644
--- a/src/sphobjinv/fileops.py
+++ b/src/sphobjinv/fileops.py
@@ -10,7 +10,7 @@
5 Nov 2017
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
https://github.com/bskinn/sphobjinv
diff --git a/src/sphobjinv/inventory.py b/src/sphobjinv/inventory.py
index f5e15f0d..d56c7124 100644
--- a/src/sphobjinv/inventory.py
+++ b/src/sphobjinv/inventory.py
@@ -10,7 +10,7 @@
7 Dec 2017
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
https://github.com/bskinn/sphobjinv
@@ -39,7 +39,7 @@
import jsonschema
from jsonschema.exceptions import ValidationError
-from sphobjinv.data import _utf8_encode, DataObjStr
+from sphobjinv.data import DataObjStr, _utf8_encode
from sphobjinv.enum import HeaderFields, SourceTypes
from sphobjinv.fileops import readbytes
from sphobjinv.re import pb_data, pb_project, pb_version
diff --git a/src/sphobjinv/re.py b/src/sphobjinv/re.py
index c51605f9..e42ab29d 100644
--- a/src/sphobjinv/re.py
+++ b/src/sphobjinv/re.py
@@ -10,7 +10,7 @@
5 Nov 2017
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
https://github.com/bskinn/sphobjinv
@@ -34,7 +34,6 @@
from sphobjinv.data import DataFields as DF # noqa: N817
from sphobjinv.enum import HeaderFields as HF # noqa: N817
-
#: Compiled |re| |bytes| pattern for comment lines in decompressed
#: inventory files
pb_comments = re.compile(b"^#.*$", re.M)
@@ -46,9 +45,7 @@
[#][ ]Project:[ ] # Preamble
(?P<{HF.Project.value}>.*?) # Lazy rest of line is project name
\r?$ # Ignore possible CR at EOL
- """.encode(
- encoding="utf-8"
- ),
+ """.encode(encoding="utf-8"),
re.M | re.X,
)
@@ -59,9 +56,7 @@
[#][ ]Version:[ ] # Preamble
(?P<{HF.Version.value}>.*?) # Lazy rest of line is version
\r?$ # Ignore possible CR at EOL
- """.encode(
- encoding="utf-8"
- ),
+ """.encode(encoding="utf-8"),
re.M | re.X,
)
diff --git a/src/sphobjinv/schema.py b/src/sphobjinv/schema.py
index b609b042..2cc54d2b 100644
--- a/src/sphobjinv/schema.py
+++ b/src/sphobjinv/schema.py
@@ -11,7 +11,7 @@
7 Dec 2017
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
https://github.com/bskinn/sphobjinv
diff --git a/src/sphobjinv/version.py b/src/sphobjinv/version.py
index 89cb6f4f..00369de0 100644
--- a/src/sphobjinv/version.py
+++ b/src/sphobjinv/version.py
@@ -10,7 +10,7 @@
18 Mar 2019
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
https://github.com/bskinn/sphobjinv
@@ -29,4 +29,4 @@
"""
-__version__ = "2.3.1.3"
+__version__ = "2.4"
diff --git a/src/sphobjinv/zlib.py b/src/sphobjinv/zlib.py
index eb597a69..34dfe6af 100644
--- a/src/sphobjinv/zlib.py
+++ b/src/sphobjinv/zlib.py
@@ -10,7 +10,7 @@
5 Nov 2017
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
https://github.com/bskinn/sphobjinv
@@ -33,7 +33,6 @@
import os
import zlib
-
BUFSIZE = 16 * 1024 # 16k chunks
diff --git a/conftest.py b/tests/conftest.py
similarity index 87%
rename from conftest.py
rename to tests/conftest.py
index 3aad2932..ffd9fdf4 100644
--- a/conftest.py
+++ b/tests/conftest.py
@@ -10,10 +10,10 @@
20 Mar 2019
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
- http://www.github.com/bskinn/sphobjinv
+ https://github.com/bskinn/sphobjinv
**Documentation**
https://sphobjinv.readthedocs.io/en/stable
@@ -45,6 +45,9 @@
from sphinx.util.inventory import InventoryFile as IFile
import sphobjinv as soi
+from sphobjinv.cli.core import main, main_textconv
+from tests.enum import CLICommand
+from tests.fixtures_http import resource_http_base_url, resource_url # noqa: F401
def pytest_addoption(parser):
@@ -58,15 +61,12 @@ def pytest_addoption(parser):
),
)
parser.addoption("--nonloc", action="store_true", help="Include nonlocal tests")
- parser.addoption(
- "--flake8_ext", action="store_true", help="Include flake8 extensions test"
- )
@pytest.fixture(scope="session")
def res_path():
"""Provide Path object to the test resource directory."""
- return Path("tests", "resource")
+ return Path(__file__).resolve().parent / "resource"
@pytest.fixture(scope="session")
@@ -114,12 +114,6 @@ class Extensions(str, Enum):
True: b"attr.Attribute py:class 1 api.html#attr.Attribute attr.Attribute",
}
- # For the URL mode of Inventory instantiation
- remote_url = (
- "https://github.com/bskinn/sphobjinv/raw/main/"
- "tests/resource/objects_{0}.inv"
- )
-
# Regex pattern for objects_xyz.inv files
p_inv = re.compile(r"objects_([^.]+)\.inv", re.I)
@@ -135,6 +129,16 @@ class Extensions(str, Enum):
return Info()
+@pytest.fixture(scope="session")
+def http_inv_url_template(resource_url) -> str: # noqa: F811
+ """Provide a template string for accessing files over HTTP.
+
+ Meant to be used via the URL mode of Inventory instantiation.
+
+ """
+ return resource_url("objects_{0}.inv")
+
+
@pytest.fixture()
def scratch_path(tmp_path, res_path, misc_info, is_win, unix2dos):
"""Provision pre-populated scratch directory, returned as Path."""
@@ -151,19 +155,13 @@ def scratch_path(tmp_path, res_path, misc_info, is_win, unix2dos):
# With the conversion of resources/objects_attrs.txt to Unix EOLs in order to
# provide for a Unix-testable sdist, on Windows systems this resource needs
# to be converted to DOS EOLs for consistency.
- if is_win:
+ if is_win: # pragma: no cover
win_path = tmp_path / f"{scr_base}{misc_info.Extensions.DEC.value}"
win_path.write_bytes(unix2dos(win_path.read_bytes()))
yield tmp_path
-@pytest.fixture(scope="session")
-def ensure_doc_scratch():
- """Ensure doc/scratch dir exists, for README shell examples."""
- Path("doc", "scratch").mkdir(parents=True, exist_ok=True)
-
-
@pytest.fixture(scope="session")
def bytes_txt(misc_info, res_path):
"""Load and return the contents of the example objects_attrs.txt as bytes."""
@@ -211,7 +209,7 @@ def func(path):
"""Perform the 'live' inventory load test."""
try:
sphinx_ifile_load(path)
- except Exception as e: # noqa: PIE786
+ except Exception as e: # noqa: PIE786 pragma: no cover
# An exception here is a failing test, not a test error.
pytest.fail(e)
@@ -233,25 +231,30 @@ def sphinx_version():
@pytest.fixture() # Must be function scope since uses monkeypatch
def run_cmdline_test(monkeypatch):
"""Return function to perform command line exit code test."""
- from sphobjinv.cli.core import main
- def func(arglist, *, expect=0): # , suffix=None):
+ def func(arglist, *, command=CLICommand.Core, expect=0): # , suffix=None):
"""Perform the CLI exit-code test."""
-
# Assemble execution arguments
- runargs = ["sphobjinv"]
+ runargs = [command.value]
runargs.extend(str(a) for a in arglist)
+ # Select the command function to use
+ match command:
+ case CLICommand.Core:
+ cmd_func = main
+ case CLICommand.Textconv:
+ cmd_func = main_textconv
+
# Mock sys.argv, run main, and restore sys.argv
with monkeypatch.context() as m:
m.setattr(sys, "argv", runargs)
try:
- main()
+ cmd_func()
except SystemExit as e:
retcode = e.args[0]
ok = True
- else:
+ else: # pragma: no cover
ok = False
# Do all pytesty stuff outside monkeypatch context
@@ -273,7 +276,7 @@ def func(path):
res_bytes = Path(misc_info.res_decomp_path).read_bytes()
tgt_bytes = Path(path).read_bytes() # .replace(b"\r\n", b"\n")
- if is_win:
+ if is_win: # pragma: no cover
# Have to explicitly convert these newlines, now that the
# tests/resource/objects_attrs.txt file is marked 'binary' in
# .gitattributes
@@ -296,8 +299,8 @@ def func(inv, source_type):
"""
assert inv.project == "attrs"
- assert inv.version == "22.1"
- assert inv.count == 129
+ assert inv.version == "25.4"
+ assert inv.count == 180
assert inv.source_type
return func
@@ -305,7 +308,7 @@ def func(inv, source_type):
testall_inv_paths = [
p
- for p in (Path(__file__).parent / "tests" / "resource").iterdir()
+ for p in (Path(__file__).parent / "resource").iterdir()
if p.name.startswith("objects_") and p.name.endswith(".inv")
]
testall_inv_ids = [p.name[8:-4] for p in testall_inv_paths]
diff --git a/tests/enum.py b/tests/enum.py
new file mode 100644
index 00000000..0c1b9757
--- /dev/null
+++ b/tests/enum.py
@@ -0,0 +1,39 @@
+r"""*Test enums for* ``sphobjinv``.
+
+``sphobjinv`` is a toolkit for manipulation and inspection of
+Sphinx |objects.inv| files.
+
+**Author**
+ Brian Skinn (brian.skinn@gmail.com)
+
+**File Created**
+ 22 Dec 2025
+
+**Copyright**
+ \(c) 2016-2026 Brian Skinn and community contributors
+
+**Source Repository**
+ https://github.com/bskinn/sphobjinv
+
+**Documentation**
+ https://sphobjinv.readthedocs.io/en/stable
+
+**License**
+ Code: `MIT License`_
+
+ Docs & Docstrings: |CC BY 4.0|_
+
+ See |license_txt|_ for full license terms.
+
+**Members**
+
+"""
+
+from enum import Enum
+
+
+class CLICommand(str, Enum):
+ """Enumeration of CLI commands."""
+
+ Core = "sphobjinv"
+ Textconv = "sphobjinv-textconv"
diff --git a/tests/fixtures_http.py b/tests/fixtures_http.py
new file mode 100644
index 00000000..7627db42
--- /dev/null
+++ b/tests/fixtures_http.py
@@ -0,0 +1,129 @@
+r"""*HTTP server fixtures definitions for* ``sphobjinv``.
+
+``sphobjinv`` is a toolkit for manipulation and inspection of
+Sphinx |objects.inv| files.
+
+**Author**
+ Brian Skinn (brian.skinn@gmail.com)
+
+**File Created**
+ 24 Dec 2025
+
+**Copyright**
+ \(c) 2016-2026 Brian Skinn and community contributors
+
+**Source Repository**
+ https://github.com/bskinn/sphobjinv
+
+**Documentation**
+ https://sphobjinv.readthedocs.io/en/stable
+
+**License**
+ Code: `MIT License`_
+
+ Docs & Docstrings: |CC BY 4.0|_
+
+ See |license_txt|_ for full license terms.
+
+**Members**
+
+"""
+
+import contextlib
+import functools
+import http.server
+import socketserver
+import threading
+from pathlib import Path
+from typing import Callable, Generator
+
+import pytest
+
+
+class NoNameLookupHTTPServer(http.server.HTTPServer):
+ """HTTPServer subclass that doesn't do rDNS lookup when setting server name.
+
+ We use this class because the macOS GitHub runners time out when attempting
+ the rDNS lookup.
+
+ """
+
+ def server_bind(self):
+ """Override server_bind to store the server name."""
+ socketserver.TCPServer.server_bind(self)
+ host, port = self.server_address[:2]
+ self.server_name = host # Override
+ self.server_port = port
+
+
+@contextlib.contextmanager
+def _baseurl_for_served_directory(
+ directory: Path | str, host: str = "127.0.0.1"
+) -> Generator[str, None, None]:
+ """Spin up HTTP server on a directory and yield the server base URL."""
+ directory = Path(directory).resolve()
+
+ handler_cls = functools.partial(
+ http.server.SimpleHTTPRequestHandler,
+ directory=str(directory),
+ )
+
+ # Bind to port 0 so the OS chooses a free ephemeral port
+ httpd = NoNameLookupHTTPServer((host, 0), handler_cls)
+ port = httpd.server_address[1]
+ base_url = f"http://{host}:{port}"
+
+ thread = threading.Thread(
+ target=httpd.serve_forever,
+ name="pytest-http-server",
+ daemon=True,
+ )
+
+ thread.start()
+
+ try:
+ yield base_url
+ finally:
+ httpd.shutdown()
+ httpd.server_close()
+ thread.join(timeout=2)
+
+
+@pytest.fixture(scope="session")
+def resource_http_base_url() -> Generator[str, None, None]:
+ """Provide base URL of HTTP server exposing tests/resource/*.""" # noqa: RST213
+ resource_dir = Path(__file__).resolve().parent / "resource"
+
+ if not resource_dir.is_dir(): # pragma: no cover
+ raise RuntimeError(
+ f"Expected test resource directory not found: {resource_dir}"
+ )
+
+ with _baseurl_for_served_directory(resource_dir) as base_url:
+ yield base_url
+
+
+@pytest.fixture(scope="session")
+def resource_url(resource_http_base_url: str) -> Callable[[str], str]:
+ """Provide a function to calculate the full test-resource URL from a relative URL.
+
+ Forbids '..' path segments to avoid root escape.
+
+ **Examples**
+
+ resource_url("objects_bokeh.inv") -> "http://127.0.0.1:{PORT}/objects_bokeh.inv"
+ resource_url("subdir/file.ext") -> ".../subdir/file.ext"
+
+ """
+
+ def _calc_path(rel_path: str) -> str:
+ """Calculate the full test-resource URL from a relative URL."""
+ # Prevent escaping the resource directory.
+ if ".." in Path(rel_path).parts: # pragma: no cover
+ raise ValueError("Path must not contain '..'")
+
+ # Ensure consistent URL path separators.
+ url_path = "/".join(Path(rel_path).parts)
+ return f"{resource_http_base_url}/{url_path}"
+
+ return _calc_path
diff --git a/tests/resource/objects_attrs.inv b/tests/resource/objects_attrs.inv
index 85189bdf..7c5a9b0a 100644
Binary files a/tests/resource/objects_attrs.inv and b/tests/resource/objects_attrs.inv differ
diff --git a/tests/resource/objects_attrs.json b/tests/resource/objects_attrs.json
index b1144317..d0ad36c1 100644
--- a/tests/resource/objects_attrs.json
+++ b/tests/resource/objects_attrs.json
@@ -1 +1 @@
-{"project": "attrs", "version": "22.1", "count": 129, "0": {"name": "attr", "domain": "py", "role": "module", "priority": "0", "uri": "index.html#module-$", "dispname": "-"}, "1": {"name": "attr.VersionInfo", "domain": "py", "role": "class", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "2": {"name": "attr._make.Attribute", "domain": "py", "role": "class", "priority": "-1", "uri": "api.html#attrs.Attribute", "dispname": "-"}, "3": {"name": "attr._make.Factory", "domain": "py", "role": "class", "priority": "-1", "uri": "api.html#attrs.Factory", "dispname": "-"}, "4": {"name": "attr._version_info.VersionInfo", "domain": "py", "role": "class", "priority": "-1", "uri": "api.html#attr.VersionInfo", "dispname": "-"}, "5": {"name": "attr.asdict", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "6": {"name": "attr.assoc", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "7": {"name": "attr.astuple", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "8": {"name": "attr.attr.NOTHING", "domain": "py", "role": "data", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "9": {"name": "attr.attr.cmp_using", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "10": {"name": "attr.attr.evolve", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "11": {"name": "attr.attr.fields", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "12": {"name": "attr.attr.fields_dict", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "13": {"name": "attr.attr.filters.exclude", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "14": {"name": "attr.attr.filters.include", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "15": {"name": "attr.attr.has", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "16": {"name": "attr.attr.resolve_types", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "17": {"name": "attr.attr.validate", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "18": {"name": "attr.attrs.frozen", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "19": {"name": "attr.attrs.mutable", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "20": {"name": "attr.attrs.setters.NO_OP", "domain": "py", "role": "data", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "21": {"name": "attr.define", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "22": {"name": "attr.exceptions.AttrsAttributeNotFoundError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.AttrsAttributeNotFoundError", "dispname": "-"}, "23": {"name": "attr.exceptions.DefaultAlreadySetError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.DefaultAlreadySetError", "dispname": "-"}, "24": {"name": "attr.exceptions.FrozenAttributeError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.FrozenAttributeError", "dispname": "-"}, "25": {"name": "attr.exceptions.FrozenError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.FrozenError", "dispname": "-"}, "26": {"name": "attr.exceptions.FrozenInstanceError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.FrozenInstanceError", "dispname": "-"}, "27": {"name": "attr.exceptions.NotAnAttrsClassError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.NotAnAttrsClassError", "dispname": "-"}, "28": {"name": "attr.exceptions.NotCallableError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.NotCallableError", "dispname": "-"}, "29": {"name": "attr.exceptions.PythonTooOldError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.PythonTooOldError", "dispname": "-"}, "30": {"name": "attr.exceptions.UnannotatedAttributeError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.UnannotatedAttributeError", "dispname": "-"}, "31": {"name": "attr.field", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "32": {"name": "attr.frozen", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "33": {"name": "attr.get_run_validators", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "34": {"name": "attr.ib", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "35": {"name": "attr.mutable", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "36": {"name": "attr.s", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "37": {"name": "attr.set_run_validators", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "38": {"name": "attrs", "domain": "py", "role": "module", "priority": "0", "uri": "index.html#module-$", "dispname": "-"}, "39": {"name": "attrs.Attribute", "domain": "py", "role": "class", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "40": {"name": "attrs.Attribute.evolve", "domain": "py", "role": "method", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "41": {"name": "attrs.Factory", "domain": "py", "role": "class", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "42": {"name": "attrs.NOTHING", "domain": "py", "role": "data", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "43": {"name": "attrs.asdict", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "44": {"name": "attrs.astuple", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "45": {"name": "attrs.cmp_using", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "46": {"name": "attrs.converters.default_if_none", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "47": {"name": "attrs.converters.optional", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "48": {"name": "attrs.converters.pipe", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "49": {"name": "attrs.converters.to_bool", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "50": {"name": "attrs.define", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "51": {"name": "attrs.evolve", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "52": {"name": "attrs.exceptions.AttrsAttributeNotFoundError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "53": {"name": "attrs.exceptions.DefaultAlreadySetError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "54": {"name": "attrs.exceptions.FrozenAttributeError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "55": {"name": "attrs.exceptions.FrozenError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "56": {"name": "attrs.exceptions.FrozenInstanceError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "57": {"name": "attrs.exceptions.NotAnAttrsClassError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "58": {"name": "attrs.exceptions.NotCallableError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "59": {"name": "attrs.exceptions.PythonTooOldError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "60": {"name": "attrs.exceptions.UnannotatedAttributeError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "61": {"name": "attrs.field", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "62": {"name": "attrs.fields", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "63": {"name": "attrs.fields_dict", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "64": {"name": "attrs.filters.exclude", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "65": {"name": "attrs.filters.include", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "66": {"name": "attrs.has", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "67": {"name": "attrs.make_class", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "68": {"name": "attrs.resolve_types", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "69": {"name": "attrs.setters.convert", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "70": {"name": "attrs.setters.frozen", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "71": {"name": "attrs.setters.pipe", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "72": {"name": "attrs.setters.validate", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "73": {"name": "attrs.validate", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "74": {"name": "attrs.validators.and_", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "75": {"name": "attrs.validators.deep_iterable", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "76": {"name": "attrs.validators.deep_mapping", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "77": {"name": "attrs.validators.disabled", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "78": {"name": "attrs.validators.ge", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "79": {"name": "attrs.validators.get_disabled", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "80": {"name": "attrs.validators.gt", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "81": {"name": "attrs.validators.in_", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "82": {"name": "attrs.validators.instance_of", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "83": {"name": "attrs.validators.is_callable", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "84": {"name": "attrs.validators.le", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "85": {"name": "attrs.validators.lt", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "86": {"name": "attrs.validators.matches_re", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "87": {"name": "attrs.validators.max_len", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "88": {"name": "attrs.validators.min_len", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "89": {"name": "attrs.validators.optional", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "90": {"name": "attrs.validators.provides", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "91": {"name": "attrs.validators.set_disabled", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "92": {"name": "api", "domain": "std", "role": "doc", "priority": "-1", "uri": "api.html", "dispname": "API Reference"}, "93": {"name": "api_setters", "domain": "std", "role": "label", "priority": "-1", "uri": "api.html#api-setters", "dispname": "Setters"}, "94": {"name": "api_validators", "domain": "std", "role": "label", "priority": "-1", "uri": "api.html#api-validators", "dispname": "Validators"}, "95": {"name": "asdict", "domain": "std", "role": "label", "priority": "-1", "uri": "examples.html#$", "dispname": "Converting to Collections Types"}, "96": {"name": "changelog", "domain": "std", "role": "doc", "priority": "-1", "uri": "changelog.html", "dispname": "Changelog"}, "97": {"name": "comparison", "domain": "std", "role": "doc", "priority": "-1", "uri": "comparison.html", "dispname": "Comparison"}, "98": {"name": "converters", "domain": "std", "role": "label", "priority": "-1", "uri": "init.html#$", "dispname": "Converters"}, "99": {"name": "custom-comparison", "domain": "std", "role": "label", "priority": "-1", "uri": "comparison.html#$", "dispname": "Customization"}, "100": {"name": "dict classes", "domain": "std", "role": "term", "priority": "-1", "uri": "glossary.html#term-dict-classes", "dispname": "-"}, "101": {"name": "dunder methods", "domain": "std", "role": "term", "priority": "-1", "uri": "glossary.html#term-dunder-methods", "dispname": "-"}, "102": {"name": "examples", "domain": "std", "role": "doc", "priority": "-1", "uri": "examples.html", "dispname": "attrs by Example"}, "103": {"name": "examples_validators", "domain": "std", "role": "label", "priority": "-1", "uri": "examples.html#examples-validators", "dispname": "Validators"}, "104": {"name": "extending", "domain": "std", "role": "doc", "priority": "-1", "uri": "extending.html", "dispname": "Extending"}, "105": {"name": "extending_metadata", "domain": "std", "role": "label", "priority": "-1", "uri": "extending.html#extending-metadata", "dispname": "Metadata"}, "106": {"name": "genindex", "domain": "std", "role": "label", "priority": "-1", "uri": "genindex.html", "dispname": "Index"}, "107": {"name": "glossary", "domain": "std", "role": "doc", "priority": "-1", "uri": "glossary.html", "dispname": "Glossary"}, "108": {"name": "hashing", "domain": "std", "role": "doc", "priority": "-1", "uri": "hashing.html", "dispname": "Hashing"}, "109": {"name": "helpers", "domain": "std", "role": "label", "priority": "-1", "uri": "api.html#$", "dispname": "Helpers"}, "110": {"name": "how", "domain": "std", "role": "label", "priority": "-1", "uri": "how-does-it-work.html#$", "dispname": "How Does It Work?"}, "111": {"name": "how-does-it-work", "domain": "std", "role": "doc", "priority": "-1", "uri": "how-does-it-work.html", "dispname": "How Does It Work?"}, "112": {"name": "how-frozen", "domain": "std", "role": "label", "priority": "-1", "uri": "how-does-it-work.html#$", "dispname": "Immutability"}, "113": {"name": "index", "domain": "std", "role": "doc", "priority": "-1", "uri": "index.html", "dispname": "attrs: Classes Without Boilerplate"}, "114": {"name": "init", "domain": "std", "role": "doc", "priority": "-1", "uri": "init.html", "dispname": "Initialization"}, "115": {"name": "license", "domain": "std", "role": "doc", "priority": "-1", "uri": "license.html", "dispname": "License and Credits"}, "116": {"name": "metadata", "domain": "std", "role": "label", "priority": "-1", "uri": "examples.html#$", "dispname": "Metadata"}, "117": {"name": "modindex", "domain": "std", "role": "label", "priority": "-1", "uri": "py-modindex.html", "dispname": "Module Index"}, "118": {"name": "names", "domain": "std", "role": "doc", "priority": "-1", "uri": "names.html", "dispname": "On The Core API Names"}, "119": {"name": "overview", "domain": "std", "role": "doc", "priority": "-1", "uri": "overview.html", "dispname": "Overview"}, "120": {"name": "philosophy", "domain": "std", "role": "label", "priority": "-1", "uri": "overview.html#$", "dispname": "Philosophy"}, "121": {"name": "py-modindex", "domain": "std", "role": "label", "priority": "-1", "uri": "py-modindex.html", "dispname": "Python Module Index"}, "122": {"name": "search", "domain": "std", "role": "label", "priority": "-1", "uri": "search.html", "dispname": "Search Page"}, "123": {"name": "slotted classes", "domain": "std", "role": "term", "priority": "-1", "uri": "glossary.html#term-slotted-classes", "dispname": "-"}, "124": {"name": "transform-fields", "domain": "std", "role": "label", "priority": "-1", "uri": "extending.html#$", "dispname": "Automatic Field Transformation and Modification"}, "125": {"name": "types", "domain": "std", "role": "doc", "priority": "-1", "uri": "types.html", "dispname": "Type Annotations"}, "126": {"name": "validators", "domain": "std", "role": "label", "priority": "-1", "uri": "init.html#$", "dispname": "Validators"}, "127": {"name": "version-info", "domain": "std", "role": "label", "priority": "-1", "uri": "api.html#$", "dispname": "-"}, "128": {"name": "why", "domain": "std", "role": "doc", "priority": "-1", "uri": "why.html", "dispname": "Why not\u2026"}}
\ No newline at end of file
+{"project": "attrs", "version": "25.4", "count": 180, "0": {"name": "attr", "domain": "py", "role": "module", "priority": "0", "uri": "api-attr.html#module-$", "dispname": "-"}, "1": {"name": "attr.Attribute", "domain": "py", "role": "class", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "2": {"name": "attr.NOTHING", "domain": "py", "role": "data", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "3": {"name": "attr.VersionInfo", "domain": "py", "role": "class", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "4": {"name": "attr._make.Attribute", "domain": "py", "role": "class", "priority": "-1", "uri": "api.html#attrs.Attribute", "dispname": "-"}, "5": {"name": "attr._make.ClassProps", "domain": "py", "role": "class", "priority": "-1", "uri": "api.html#attrs.ClassProps", "dispname": "-"}, "6": {"name": "attr._make.ClassProps.Hashability", "domain": "py", "role": "class", "priority": "-1", "uri": "api.html#attrs.ClassProps.Hashability", "dispname": "-"}, "7": {"name": "attr._make.ClassProps.KeywordOnly", "domain": "py", "role": "class", "priority": "-1", "uri": "api.html#attrs.ClassProps.KeywordOnly", "dispname": "-"}, "8": {"name": "attr._make.Converter", "domain": "py", "role": "class", "priority": "-1", "uri": "api.html#attrs.Converter", "dispname": "-"}, "9": {"name": "attr._make.Factory", "domain": "py", "role": "class", "priority": "-1", "uri": "api.html#attrs.Factory", "dispname": "-"}, "10": {"name": "attr._version_info.VersionInfo", "domain": "py", "role": "class", "priority": "-1", "uri": "api-attr.html#attr.VersionInfo", "dispname": "-"}, "11": {"name": "attr.asdict", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "12": {"name": "attr.assoc", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "13": {"name": "attr.astuple", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "14": {"name": "attr.attrs", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "15": {"name": "attr.cmp_using", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "16": {"name": "attr.converters", "domain": "py", "role": "module", "priority": "0", "uri": "api-attr.html#module-$", "dispname": "-"}, "17": {"name": "attr.define", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "18": {"name": "attr.evolve", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "19": {"name": "attr.exceptions", "domain": "py", "role": "module", "priority": "0", "uri": "api-attr.html#module-$", "dispname": "-"}, "20": {"name": "attr.exceptions.AttrsAttributeNotFoundError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.AttrsAttributeNotFoundError", "dispname": "-"}, "21": {"name": "attr.exceptions.DefaultAlreadySetError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.DefaultAlreadySetError", "dispname": "-"}, "22": {"name": "attr.exceptions.FrozenAttributeError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.FrozenAttributeError", "dispname": "-"}, "23": {"name": "attr.exceptions.FrozenError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.FrozenError", "dispname": "-"}, "24": {"name": "attr.exceptions.FrozenInstanceError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.FrozenInstanceError", "dispname": "-"}, "25": {"name": "attr.exceptions.NotAnAttrsClassError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.NotAnAttrsClassError", "dispname": "-"}, "26": {"name": "attr.exceptions.NotCallableError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.NotCallableError", "dispname": "-"}, "27": {"name": "attr.exceptions.PythonTooOldError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.PythonTooOldError", "dispname": "-"}, "28": {"name": "attr.exceptions.UnannotatedAttributeError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.UnannotatedAttributeError", "dispname": "-"}, "29": {"name": "attr.field", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "30": {"name": "attr.fields", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "31": {"name": "attr.fields_dict", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "32": {"name": "attr.filters", "domain": "py", "role": "module", "priority": "0", "uri": "api-attr.html#module-$", "dispname": "-"}, "33": {"name": "attr.filters.exclude", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "34": {"name": "attr.filters.include", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "35": {"name": "attr.frozen", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "36": {"name": "attr.get_run_validators", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "37": {"name": "attr.has", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "38": {"name": "attr.ib", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "39": {"name": "attr.make_class", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "40": {"name": "attr.mutable", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "41": {"name": "attr.resolve_types", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "42": {"name": "attr.s", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "43": {"name": "attr.set_run_validators", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "44": {"name": "attr.setters", "domain": "py", "role": "module", "priority": "0", "uri": "api-attr.html#module-$", "dispname": "-"}, "45": {"name": "attr.validate", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "46": {"name": "attr.validators", "domain": "py", "role": "module", "priority": "0", "uri": "api-attr.html#module-$", "dispname": "-"}, "47": {"name": "attrs", "domain": "py", "role": "module", "priority": "0", "uri": "api.html#module-$", "dispname": "-"}, "48": {"name": "attrs.Attribute", "domain": "py", "role": "class", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "49": {"name": "attrs.Attribute.evolve", "domain": "py", "role": "method", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "50": {"name": "attrs.ClassProps", "domain": "py", "role": "class", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "51": {"name": "attrs.ClassProps.Hashability", "domain": "py", "role": "class", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "52": {"name": "attrs.ClassProps.Hashability.HASHABLE", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "53": {"name": "attrs.ClassProps.Hashability.HASHABLE_CACHED", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "54": {"name": "attrs.ClassProps.Hashability.LEAVE_ALONE", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "55": {"name": "attrs.ClassProps.Hashability.UNHASHABLE", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "56": {"name": "attrs.ClassProps.KeywordOnly", "domain": "py", "role": "class", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "57": {"name": "attrs.ClassProps.KeywordOnly.FORCE", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "58": {"name": "attrs.ClassProps.KeywordOnly.NO", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "59": {"name": "attrs.ClassProps.KeywordOnly.YES", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "60": {"name": "attrs.ClassProps.added_eq", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "61": {"name": "attrs.ClassProps.added_init", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "62": {"name": "attrs.ClassProps.added_match_args", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "63": {"name": "attrs.ClassProps.added_ordering", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "64": {"name": "attrs.ClassProps.added_pickling", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "65": {"name": "attrs.ClassProps.added_repr", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "66": {"name": "attrs.ClassProps.added_str", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "67": {"name": "attrs.ClassProps.collected_fields_by_mro", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "68": {"name": "attrs.ClassProps.field_transformer", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "69": {"name": "attrs.ClassProps.has_weakref_slot", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "70": {"name": "attrs.ClassProps.hashability", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "71": {"name": "attrs.ClassProps.is_exception", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "72": {"name": "attrs.ClassProps.is_frozen", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "73": {"name": "attrs.ClassProps.is_slotted", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "74": {"name": "attrs.ClassProps.kw_only", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "75": {"name": "attrs.ClassProps.on_setattr_hook", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "76": {"name": "attrs.Converter", "domain": "py", "role": "class", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "77": {"name": "attrs.Factory", "domain": "py", "role": "class", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "78": {"name": "attrs.NOTHING", "domain": "py", "role": "data", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "79": {"name": "attrs.asdict", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "80": {"name": "attrs.astuple", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "81": {"name": "attrs.cmp_using", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "82": {"name": "attrs.converters", "domain": "py", "role": "module", "priority": "0", "uri": "api.html#module-$", "dispname": "-"}, "83": {"name": "attrs.converters.default_if_none", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "84": {"name": "attrs.converters.optional", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "85": {"name": "attrs.converters.pipe", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "86": {"name": "attrs.converters.to_bool", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "87": {"name": "attrs.define", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "88": {"name": "attrs.evolve", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "89": {"name": "attrs.exceptions", "domain": "py", "role": "module", "priority": "0", "uri": "api.html#module-$", "dispname": "-"}, "90": {"name": "attrs.exceptions.AttrsAttributeNotFoundError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "91": {"name": "attrs.exceptions.DefaultAlreadySetError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "92": {"name": "attrs.exceptions.FrozenAttributeError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "93": {"name": "attrs.exceptions.FrozenError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "94": {"name": "attrs.exceptions.FrozenInstanceError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "95": {"name": "attrs.exceptions.NotAnAttrsClassError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "96": {"name": "attrs.exceptions.NotCallableError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "97": {"name": "attrs.exceptions.PythonTooOldError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "98": {"name": "attrs.exceptions.UnannotatedAttributeError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "99": {"name": "attrs.field", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "100": {"name": "attrs.fields", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "101": {"name": "attrs.fields_dict", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "102": {"name": "attrs.filters", "domain": "py", "role": "module", "priority": "0", "uri": "api.html#module-$", "dispname": "-"}, "103": {"name": "attrs.filters.exclude", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "104": {"name": "attrs.filters.include", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "105": {"name": "attrs.frozen", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "106": {"name": "attrs.has", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "107": {"name": "attrs.inspect", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "108": {"name": "attrs.make_class", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "109": {"name": "attrs.mutable", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "110": {"name": "attrs.resolve_types", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "111": {"name": "attrs.setters", "domain": "py", "role": "module", "priority": "0", "uri": "api.html#module-$", "dispname": "-"}, "112": {"name": "attrs.setters.NO_OP", "domain": "py", "role": "data", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "113": {"name": "attrs.setters.convert", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "114": {"name": "attrs.setters.frozen", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "115": {"name": "attrs.setters.pipe", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "116": {"name": "attrs.setters.validate", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "117": {"name": "attrs.validate", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "118": {"name": "attrs.validators", "domain": "py", "role": "module", "priority": "0", "uri": "api.html#module-$", "dispname": "-"}, "119": {"name": "attrs.validators.and_", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "120": {"name": "attrs.validators.deep_iterable", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "121": {"name": "attrs.validators.deep_mapping", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "122": {"name": "attrs.validators.disabled", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "123": {"name": "attrs.validators.ge", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "124": {"name": "attrs.validators.get_disabled", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "125": {"name": "attrs.validators.gt", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "126": {"name": "attrs.validators.in_", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "127": {"name": "attrs.validators.instance_of", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "128": {"name": "attrs.validators.is_callable", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "129": {"name": "attrs.validators.le", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "130": {"name": "attrs.validators.lt", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "131": {"name": "attrs.validators.matches_re", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "132": {"name": "attrs.validators.max_len", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "133": {"name": "attrs.validators.min_len", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "134": {"name": "attrs.validators.not_", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "135": {"name": "attrs.validators.optional", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "136": {"name": "attrs.validators.or_", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "137": {"name": "attrs.validators.set_disabled", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "138": {"name": "api", "domain": "std", "role": "doc", "priority": "-1", "uri": "api.html", "dispname": "API Reference"}, "139": {"name": "api-attr", "domain": "std", "role": "doc", "priority": "-1", "uri": "api-attr.html", "dispname": "API Reference for the attr Namespace"}, "140": {"name": "api-validators", "domain": "std", "role": "label", "priority": "-1", "uri": "api.html#$", "dispname": "Validators"}, "141": {"name": "api_setters", "domain": "std", "role": "label", "priority": "-1", "uri": "api.html#api-setters", "dispname": "Setters"}, "142": {"name": "asdict", "domain": "std", "role": "label", "priority": "-1", "uri": "examples.html#$", "dispname": "Converting to Collections Types"}, "143": {"name": "attribute", "domain": "std", "role": "term", "priority": "-1", "uri": "glossary.html#term-$", "dispname": "-"}, "144": {"name": "changelog", "domain": "std", "role": "doc", "priority": "-1", "uri": "changelog.html", "dispname": "Changelog"}, "145": {"name": "comparison", "domain": "std", "role": "doc", "priority": "-1", "uri": "comparison.html", "dispname": "Comparison"}, "146": {"name": "converters", "domain": "std", "role": "label", "priority": "-1", "uri": "init.html#$", "dispname": "Converters"}, "147": {"name": "custom-comparison", "domain": "std", "role": "label", "priority": "-1", "uri": "comparison.html#$", "dispname": "Customization"}, "148": {"name": "defaults", "domain": "std", "role": "label", "priority": "-1", "uri": "init.html#$", "dispname": "Defaults"}, "149": {"name": "dict classes", "domain": "std", "role": "term", "priority": "-1", "uri": "glossary.html#term-dict-classes", "dispname": "-"}, "150": {"name": "dunder methods", "domain": "std", "role": "term", "priority": "-1", "uri": "glossary.html#term-dunder-methods", "dispname": "-"}, "151": {"name": "examples", "domain": "std", "role": "doc", "priority": "-1", "uri": "examples.html", "dispname": "attrs by Example"}, "152": {"name": "examples-validators", "domain": "std", "role": "label", "priority": "-1", "uri": "examples.html#$", "dispname": "Validators"}, "153": {"name": "extending", "domain": "std", "role": "doc", "priority": "-1", "uri": "extending.html", "dispname": "Extending"}, "154": {"name": "extending-metadata", "domain": "std", "role": "label", "priority": "-1", "uri": "extending.html#$", "dispname": "Metadata"}, "155": {"name": "field", "domain": "std", "role": "term", "priority": "-1", "uri": "glossary.html#term-$", "dispname": "-"}, "156": {"name": "genindex", "domain": "std", "role": "label", "priority": "-1", "uri": "genindex.html", "dispname": "Index"}, "157": {"name": "glossary", "domain": "std", "role": "doc", "priority": "-1", "uri": "glossary.html", "dispname": "Glossary"}, "158": {"name": "hashing", "domain": "std", "role": "doc", "priority": "-1", "uri": "hashing.html", "dispname": "Hashing"}, "159": {"name": "helpers", "domain": "std", "role": "label", "priority": "-1", "uri": "api.html#$", "dispname": "Helpers"}, "160": {"name": "how", "domain": "std", "role": "label", "priority": "-1", "uri": "how-does-it-work.html#$", "dispname": "How Does It Work?"}, "161": {"name": "how-does-it-work", "domain": "std", "role": "doc", "priority": "-1", "uri": "how-does-it-work.html", "dispname": "How Does It Work?"}, "162": {"name": "how-frozen", "domain": "std", "role": "label", "priority": "-1", "uri": "how-does-it-work.html#$", "dispname": "Immutability"}, "163": {"name": "how-slotted-cached_property", "domain": "std", "role": "label", "priority": "-1", "uri": "how-does-it-work.html#how-slotted-cached-property", "dispname": "Cached Properties on Slotted Classes"}, "164": {"name": "index", "domain": "std", "role": "doc", "priority": "-1", "uri": "index.html", "dispname": "attrs: Classes Without Boilerplate"}, "165": {"name": "init", "domain": "std", "role": "doc", "priority": "-1", "uri": "init.html", "dispname": "Initialization"}, "166": {"name": "init-subclass", "domain": "std", "role": "label", "priority": "-1", "uri": "init.html#$", "dispname": "attrs and __init_subclass__"}, "167": {"name": "license", "domain": "std", "role": "doc", "priority": "-1", "uri": "license.html", "dispname": "License and Credits"}, "168": {"name": "metadata", "domain": "std", "role": "label", "priority": "-1", "uri": "examples.html#$", "dispname": "Metadata"}, "169": {"name": "modindex", "domain": "std", "role": "label", "priority": "-1", "uri": "py-modindex.html", "dispname": "Module Index"}, "170": {"name": "names", "domain": "std", "role": "doc", "priority": "-1", "uri": "names.html", "dispname": "On The Core API Names"}, "171": {"name": "overview", "domain": "std", "role": "doc", "priority": "-1", "uri": "overview.html", "dispname": "Overview"}, "172": {"name": "private-attributes", "domain": "std", "role": "label", "priority": "-1", "uri": "init.html#$", "dispname": "Private Attributes and Aliases"}, "173": {"name": "py-modindex", "domain": "std", "role": "label", "priority": "-1", "uri": "py-modindex.html", "dispname": "Python Module Index"}, "174": {"name": "search", "domain": "std", "role": "label", "priority": "-1", "uri": "search.html", "dispname": "Search Page"}, "175": {"name": "slotted classes", "domain": "std", "role": "term", "priority": "-1", "uri": "glossary.html#term-slotted-classes", "dispname": "-"}, "176": {"name": "transform-fields", "domain": "std", "role": "label", "priority": "-1", "uri": "extending.html#$", "dispname": "Automatic Field Transformation and Modification"}, "177": {"name": "types", "domain": "std", "role": "doc", "priority": "-1", "uri": "types.html", "dispname": "Type Annotations"}, "178": {"name": "validators", "domain": "std", "role": "label", "priority": "-1", "uri": "init.html#$", "dispname": "Validators"}, "179": {"name": "why", "domain": "std", "role": "doc", "priority": "-1", "uri": "why.html", "dispname": "Why not\u2026"}}
\ No newline at end of file
diff --git a/tests/resource/objects_attrs.txt b/tests/resource/objects_attrs.txt
index 50cf9464..a81d2965 100644
--- a/tests/resource/objects_attrs.txt
+++ b/tests/resource/objects_attrs.txt
@@ -1,29 +1,27 @@
# Sphinx inventory version 2
# Project: attrs
-# Version: 22.1
+# Version: 25.4
# The remainder of this file is compressed using zlib.
-attr py:module 0 index.html#module-$ -
-attr.VersionInfo py:class 1 api.html#$ -
+attr py:module 0 api-attr.html#module-$ -
+attr.Attribute py:class 1 api-attr.html#$ -
+attr.NOTHING py:data 1 api-attr.html#$ -
+attr.VersionInfo py:class 1 api-attr.html#$ -
attr._make.Attribute py:class -1 api.html#attrs.Attribute -
+attr._make.ClassProps py:class -1 api.html#attrs.ClassProps -
+attr._make.ClassProps.Hashability py:class -1 api.html#attrs.ClassProps.Hashability -
+attr._make.ClassProps.KeywordOnly py:class -1 api.html#attrs.ClassProps.KeywordOnly -
+attr._make.Converter py:class -1 api.html#attrs.Converter -
attr._make.Factory py:class -1 api.html#attrs.Factory -
-attr._version_info.VersionInfo py:class -1 api.html#attr.VersionInfo -
-attr.asdict py:function 1 api.html#$ -
-attr.assoc py:function 1 api.html#$ -
-attr.astuple py:function 1 api.html#$ -
-attr.attr.NOTHING py:data 1 api.html#$ -
-attr.attr.cmp_using py:function 1 api.html#$ -
-attr.attr.evolve py:function 1 api.html#$ -
-attr.attr.fields py:function 1 api.html#$ -
-attr.attr.fields_dict py:function 1 api.html#$ -
-attr.attr.filters.exclude py:function 1 api.html#$ -
-attr.attr.filters.include py:function 1 api.html#$ -
-attr.attr.has py:function 1 api.html#$ -
-attr.attr.resolve_types py:function 1 api.html#$ -
-attr.attr.validate py:function 1 api.html#$ -
-attr.attrs.frozen py:function 1 api.html#$ -
-attr.attrs.mutable py:function 1 api.html#$ -
-attr.attrs.setters.NO_OP py:data 1 api.html#$ -
-attr.define py:function 1 api.html#$ -
+attr._version_info.VersionInfo py:class -1 api-attr.html#attr.VersionInfo -
+attr.asdict py:function 1 api-attr.html#$ -
+attr.assoc py:function 1 api-attr.html#$ -
+attr.astuple py:function 1 api-attr.html#$ -
+attr.attrs py:function 1 api-attr.html#$ -
+attr.cmp_using py:function 1 api-attr.html#$ -
+attr.converters py:module 0 api-attr.html#module-$ -
+attr.define py:function 1 api-attr.html#$ -
+attr.evolve py:function 1 api-attr.html#$ -
+attr.exceptions py:module 0 api-attr.html#module-$ -
attr.exceptions.AttrsAttributeNotFoundError py:exception -1 api.html#attrs.exceptions.AttrsAttributeNotFoundError -
attr.exceptions.DefaultAlreadySetError py:exception -1 api.html#attrs.exceptions.DefaultAlreadySetError -
attr.exceptions.FrozenAttributeError py:exception -1 api.html#attrs.exceptions.FrozenAttributeError -
@@ -33,27 +31,67 @@ attr.exceptions.NotAnAttrsClassError py:exception -1 api.html#attrs.exceptions.N
attr.exceptions.NotCallableError py:exception -1 api.html#attrs.exceptions.NotCallableError -
attr.exceptions.PythonTooOldError py:exception -1 api.html#attrs.exceptions.PythonTooOldError -
attr.exceptions.UnannotatedAttributeError py:exception -1 api.html#attrs.exceptions.UnannotatedAttributeError -
-attr.field py:function 1 api.html#$ -
-attr.frozen py:function 1 api.html#$ -
-attr.get_run_validators py:function 1 api.html#$ -
-attr.ib py:function 1 api.html#$ -
-attr.mutable py:function 1 api.html#$ -
-attr.s py:function 1 api.html#$ -
-attr.set_run_validators py:function 1 api.html#$ -
-attrs py:module 0 index.html#module-$ -
+attr.field py:function 1 api-attr.html#$ -
+attr.fields py:function 1 api-attr.html#$ -
+attr.fields_dict py:function 1 api-attr.html#$ -
+attr.filters py:module 0 api-attr.html#module-$ -
+attr.filters.exclude py:function 1 api-attr.html#$ -
+attr.filters.include py:function 1 api-attr.html#$ -
+attr.frozen py:function 1 api-attr.html#$ -
+attr.get_run_validators py:function 1 api-attr.html#$ -
+attr.has py:function 1 api-attr.html#$ -
+attr.ib py:function 1 api-attr.html#$ -
+attr.make_class py:function 1 api-attr.html#$ -
+attr.mutable py:function 1 api-attr.html#$ -
+attr.resolve_types py:function 1 api-attr.html#$ -
+attr.s py:function 1 api-attr.html#$ -
+attr.set_run_validators py:function 1 api-attr.html#$ -
+attr.setters py:module 0 api-attr.html#module-$ -
+attr.validate py:function 1 api-attr.html#$ -
+attr.validators py:module 0 api-attr.html#module-$ -
+attrs py:module 0 api.html#module-$ -
attrs.Attribute py:class 1 api.html#$ -
attrs.Attribute.evolve py:method 1 api.html#$ -
+attrs.ClassProps py:class 1 api.html#$ -
+attrs.ClassProps.Hashability py:class 1 api.html#$ -
+attrs.ClassProps.Hashability.HASHABLE py:attribute 1 api.html#$ -
+attrs.ClassProps.Hashability.HASHABLE_CACHED py:attribute 1 api.html#$ -
+attrs.ClassProps.Hashability.LEAVE_ALONE py:attribute 1 api.html#$ -
+attrs.ClassProps.Hashability.UNHASHABLE py:attribute 1 api.html#$ -
+attrs.ClassProps.KeywordOnly py:class 1 api.html#$ -
+attrs.ClassProps.KeywordOnly.FORCE py:attribute 1 api.html#$ -
+attrs.ClassProps.KeywordOnly.NO py:attribute 1 api.html#$ -
+attrs.ClassProps.KeywordOnly.YES py:attribute 1 api.html#$ -
+attrs.ClassProps.added_eq py:attribute 1 api.html#$ -
+attrs.ClassProps.added_init py:attribute 1 api.html#$ -
+attrs.ClassProps.added_match_args py:attribute 1 api.html#$ -
+attrs.ClassProps.added_ordering py:attribute 1 api.html#$ -
+attrs.ClassProps.added_pickling py:attribute 1 api.html#$ -
+attrs.ClassProps.added_repr py:attribute 1 api.html#$ -
+attrs.ClassProps.added_str py:attribute 1 api.html#$ -
+attrs.ClassProps.collected_fields_by_mro py:attribute 1 api.html#$ -
+attrs.ClassProps.field_transformer py:attribute 1 api.html#$ -
+attrs.ClassProps.has_weakref_slot py:attribute 1 api.html#$ -
+attrs.ClassProps.hashability py:attribute 1 api.html#$ -
+attrs.ClassProps.is_exception py:attribute 1 api.html#$ -
+attrs.ClassProps.is_frozen py:attribute 1 api.html#$ -
+attrs.ClassProps.is_slotted py:attribute 1 api.html#$ -
+attrs.ClassProps.kw_only py:attribute 1 api.html#$ -
+attrs.ClassProps.on_setattr_hook py:attribute 1 api.html#$ -
+attrs.Converter py:class 1 api.html#$ -
attrs.Factory py:class 1 api.html#$ -
attrs.NOTHING py:data 1 api.html#$ -
attrs.asdict py:function 1 api.html#$ -
attrs.astuple py:function 1 api.html#$ -
attrs.cmp_using py:function 1 api.html#$ -
+attrs.converters py:module 0 api.html#module-$ -
attrs.converters.default_if_none py:function 1 api.html#$ -
attrs.converters.optional py:function 1 api.html#$ -
attrs.converters.pipe py:function 1 api.html#$ -
attrs.converters.to_bool py:function 1 api.html#$ -
attrs.define py:function 1 api.html#$ -
attrs.evolve py:function 1 api.html#$ -
+attrs.exceptions py:module 0 api.html#module-$ -
attrs.exceptions.AttrsAttributeNotFoundError py:exception 1 api.html#$ -
attrs.exceptions.DefaultAlreadySetError py:exception 1 api.html#$ -
attrs.exceptions.FrozenAttributeError py:exception 1 api.html#$ -
@@ -66,16 +104,23 @@ attrs.exceptions.UnannotatedAttributeError py:exception 1 api.html#$ -
attrs.field py:function 1 api.html#$ -
attrs.fields py:function 1 api.html#$ -
attrs.fields_dict py:function 1 api.html#$ -
+attrs.filters py:module 0 api.html#module-$ -
attrs.filters.exclude py:function 1 api.html#$ -
attrs.filters.include py:function 1 api.html#$ -
+attrs.frozen py:function 1 api.html#$ -
attrs.has py:function 1 api.html#$ -
+attrs.inspect py:function 1 api.html#$ -
attrs.make_class py:function 1 api.html#$ -
+attrs.mutable py:function 1 api.html#$ -
attrs.resolve_types py:function 1 api.html#$ -
+attrs.setters py:module 0 api.html#module-$ -
+attrs.setters.NO_OP py:data 1 api.html#$ -
attrs.setters.convert py:function 1 api.html#$ -
attrs.setters.frozen py:function 1 api.html#$ -
attrs.setters.pipe py:function 1 api.html#$ -
attrs.setters.validate py:function 1 api.html#$ -
attrs.validate py:function 1 api.html#$ -
+attrs.validators py:module 0 api.html#module-$ -
attrs.validators.and_ py:function 1 api.html#$ -
attrs.validators.deep_iterable py:function 1 api.html#$ -
attrs.validators.deep_mapping py:function 1 api.html#$ -
@@ -91,23 +136,28 @@ attrs.validators.lt py:function 1 api.html#$ -
attrs.validators.matches_re py:function 1 api.html#$ -
attrs.validators.max_len py:function 1 api.html#$ -
attrs.validators.min_len py:function 1 api.html#$ -
+attrs.validators.not_ py:function 1 api.html#$ -
attrs.validators.optional py:function 1 api.html#$ -
-attrs.validators.provides py:function 1 api.html#$ -
+attrs.validators.or_ py:function 1 api.html#$ -
attrs.validators.set_disabled py:function 1 api.html#$ -
api std:doc -1 api.html API Reference
+api-attr std:doc -1 api-attr.html API Reference for the attr Namespace
+api-validators std:label -1 api.html#$ Validators
api_setters std:label -1 api.html#api-setters Setters
-api_validators std:label -1 api.html#api-validators Validators
asdict std:label -1 examples.html#$ Converting to Collections Types
+attribute std:term -1 glossary.html#term-$ -
changelog std:doc -1 changelog.html Changelog
comparison std:doc -1 comparison.html Comparison
converters std:label -1 init.html#$ Converters
custom-comparison std:label -1 comparison.html#$ Customization
+defaults std:label -1 init.html#$ Defaults
dict classes std:term -1 glossary.html#term-dict-classes -
dunder methods std:term -1 glossary.html#term-dunder-methods -
examples std:doc -1 examples.html attrs by Example
-examples_validators std:label -1 examples.html#examples-validators Validators
+examples-validators std:label -1 examples.html#$ Validators
extending std:doc -1 extending.html Extending
-extending_metadata std:label -1 extending.html#extending-metadata Metadata
+extending-metadata std:label -1 extending.html#$ Metadata
+field std:term -1 glossary.html#term-$ -
genindex std:label -1 genindex.html Index
glossary std:doc -1 glossary.html Glossary
hashing std:doc -1 hashing.html Hashing
@@ -115,19 +165,20 @@ helpers std:label -1 api.html#$ Helpers
how std:label -1 how-does-it-work.html#$ How Does It Work?
how-does-it-work std:doc -1 how-does-it-work.html How Does It Work?
how-frozen std:label -1 how-does-it-work.html#$ Immutability
+how-slotted-cached_property std:label -1 how-does-it-work.html#how-slotted-cached-property Cached Properties on Slotted Classes
index std:doc -1 index.html attrs: Classes Without Boilerplate
init std:doc -1 init.html Initialization
+init-subclass std:label -1 init.html#$ attrs and __init_subclass__
license std:doc -1 license.html License and Credits
metadata std:label -1 examples.html#$ Metadata
modindex std:label -1 py-modindex.html Module Index
names std:doc -1 names.html On The Core API Names
overview std:doc -1 overview.html Overview
-philosophy std:label -1 overview.html#$ Philosophy
+private-attributes std:label -1 init.html#$ Private Attributes and Aliases
py-modindex std:label -1 py-modindex.html Python Module Index
search std:label -1 search.html Search Page
slotted classes std:term -1 glossary.html#term-slotted-classes -
transform-fields std:label -1 extending.html#$ Automatic Field Transformation and Modification
types std:doc -1 types.html Type Annotations
validators std:label -1 init.html#$ Validators
-version-info std:label -1 api.html#$ -
why std:doc -1 why.html Why not…
diff --git a/tests/resource/objects_attrs_22_1.inv b/tests/resource/objects_attrs_22_1.inv
new file mode 100644
index 00000000..85189bdf
Binary files /dev/null and b/tests/resource/objects_attrs_22_1.inv differ
diff --git a/tests/resource/objects_sphinx.inv b/tests/resource/objects_sphinx.inv
index ed2fe5b4..7a4f085b 100644
Binary files a/tests/resource/objects_sphinx.inv and b/tests/resource/objects_sphinx.inv differ
diff --git a/tests/resource/objects_sphinx_6_0b.inv b/tests/resource/objects_sphinx_6_0b.inv
new file mode 100644
index 00000000..ed2fe5b4
Binary files /dev/null and b/tests/resource/objects_sphinx_6_0b.inv differ
diff --git a/tests/resource/objects_textconv.inv b/tests/resource/objects_textconv.inv
new file mode 100644
index 00000000..19a43231
Binary files /dev/null and b/tests/resource/objects_textconv.inv differ
diff --git a/tests/test_api_fail.py b/tests/test_api_fail.py
index 88e31d9f..786ca49b 100644
--- a/tests/test_api_fail.py
+++ b/tests/test_api_fail.py
@@ -10,10 +10,10 @@
20 Mar 2019
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
- http://www.github.com/bskinn/sphobjinv
+ https://github.com/bskinn/sphobjinv
**Documentation**
https://sphobjinv.readthedocs.io/en/stable
@@ -37,7 +37,6 @@
import sphobjinv as soi
-
pytestmark = [pytest.mark.api, pytest.mark.local]
diff --git a/tests/test_api_good.py b/tests/test_api_good.py
index 67dbe35e..d746bff1 100644
--- a/tests/test_api_good.py
+++ b/tests/test_api_good.py
@@ -10,10 +10,10 @@
20 Mar 2019
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
- http://www.github.com/bskinn/sphobjinv
+ https://github.com/bskinn/sphobjinv
**Documentation**
https://sphobjinv.readthedocs.io/en/stable
@@ -39,7 +39,6 @@
import sphobjinv as soi
-
pytestmark = [pytest.mark.api, pytest.mark.local]
@@ -116,19 +115,19 @@ def test_api_decompress(self, path_fxn, scratch_path, misc_info, decomp_cmp_test
soi.DataFields.Domain: b"py",
soi.DataFields.Role: b"module",
soi.DataFields.Priority: b"0",
- soi.DataFields.URI: b"index.html#module-$",
+ soi.DataFields.URI: b"api-attr.html#module-$",
soi.DataFields.DispName: b"-",
},
],
[
-3,
{ # slots std:label -1 examples.html#$ Slots
- soi.DataFields.Name: b"validators",
+ soi.DataFields.Name: b"types",
soi.DataFields.Domain: b"std",
- soi.DataFields.Role: b"label",
+ soi.DataFields.Role: b"doc",
soi.DataFields.Priority: b"-1",
- soi.DataFields.URI: b"init.html#$",
- soi.DataFields.DispName: b"Validators",
+ soi.DataFields.URI: b"types.html",
+ soi.DataFields.DispName: b"Type Annotations",
},
],
),
@@ -137,7 +136,7 @@ def test_api_data_regex(self, element, datadict, bytes_txt, misc_info):
"""Confirm the regex for loading data lines is working properly."""
# Prelim approximate check to be sure we're working with the
# correct file/data.
- assert len(soi.re.pb_data.findall(bytes_txt)) == 129
+ assert len(soi.re.pb_data.findall(bytes_txt)) == 180
mchs = list(soi.re.pb_data.finditer(bytes_txt))
@@ -449,12 +448,12 @@ def test_api_inventory_toosmallflatdict_importbutignore(self, res_dec):
inv2 = soi.Inventory(d, count_error=False)
# 128 (one less than 129) b/c the loop continues past missing elements
- assert inv2.count == 128
+ assert inv2.count == 179
def test_api_inventory_namesuggest(self, res_cmp, check):
"""Confirm object name suggestion is nominally working on a specific object."""
- rst = ":py:function:`attr.attr.evolve`"
- idx = 10
+ rst = ":py:function:`attr.evolve`"
+ idx = 18
inv = soi.Inventory(str(res_cmp))
@@ -585,7 +584,26 @@ def test_api_inventory_matches_sphinx_ifile(
original_ifile_data
), fname
- elif "sphinx.inv" in fname: # pragma: no cover
+ elif re.search(r"sphinx(|_6_0b)[.]inv", fname): # pragma: no cover
+ soi_names = [o.name for o in inv.objects]
+ ifile_names = list(
+ itt.chain.from_iterable(
+ list(original_ifile_data[k].keys()) for k in original_ifile_data
+ )
+ )
+
+ # There is the same set of unique names in the sphobjinv Inventory
+ # as in the Sphinx IFile imported data ...
+ assert set(soi_names) == set(ifile_names), fname
+
+ # ... but there are duplicate names in each ...
+ assert inv.count > len(set(soi_names)), fname
+ assert sphinx_ifile_data_count(original_ifile_data) > len(
+ set(ifile_names)
+ ), fname
+
+ # ... and there are always four more items in the sphobjinv Inventory
+ # than in the IFile data.
assert inv.count == 4 + sphinx_ifile_data_count(original_ifile_data), fname
else:
diff --git a/tests/test_api_good_nonlocal.py b/tests/test_api_good_nonlocal.py
index 928c223a..5d267f93 100644
--- a/tests/test_api_good_nonlocal.py
+++ b/tests/test_api_good_nonlocal.py
@@ -10,10 +10,10 @@
21 Mar 2019
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
- http://www.github.com/bskinn/sphobjinv
+ https://github.com/bskinn/sphobjinv
**Documentation**
https://sphobjinv.readthedocs.io/en/stable
@@ -33,7 +33,6 @@
import sphobjinv as soi
-
pytestmark = [
pytest.mark.api,
pytest.mark.nonloc,
@@ -55,16 +54,23 @@ def skip_if_no_nonloc(pytestconfig):
@pytest.mark.parametrize(
["name", "url"],
[
+ # Intentionally HTTP to test insecure remotes
("flask", "http://flask.palletsprojects.com/en/1.1.x/objects.inv"),
+ # HTTPS as will usually be the case when retrieving inventories
("h5py", "https://docs.h5py.org/en/stable/objects.inv"),
],
ids=(lambda x: "" if "://" in x else x),
)
@pytest.mark.timeout(30)
def test_api_inventory_known_header_required(name, url):
- """Confirm URL load works on docs pages requiring HTTP header config."""
+ """Confirm URL load works on docs pages requiring HTTP header config.
+
+ With the change to local-server HTTP testing, this test is important because
+ it directly exercises `Inventory` instantiation from a true HTTP(S) remote.
+
+ """
inv = soi.Inventory(url=url)
- assert inv.count > 0
+ assert inv.count > 0, f"'{name}' inventory imports without objects"
@pytest.mark.testall
@@ -74,6 +80,7 @@ def test_api_inventory_many_url_imports(
res_path,
scratch_path,
misc_info,
+ http_inv_url_template,
sphinx_load_test,
pytestconfig,
):
@@ -95,7 +102,7 @@ def test_api_inventory_many_url_imports(
mch = misc_info.p_inv.match(fname)
proj_name = mch.group(1)
inv1 = soi.Inventory(str(res_path / fname))
- inv2 = soi.Inventory(url=misc_info.remote_url.format(proj_name))
+ inv2 = soi.Inventory(url=http_inv_url_template.format(proj_name))
# Test the things
assert inv1 == inv2
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 4276e732..06ade7ed 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -10,10 +10,10 @@
20 Mar 2019
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
- http://www.github.com/bskinn/sphobjinv
+ https://github.com/bskinn/sphobjinv
**Documentation**
https://sphobjinv.readthedocs.io/en/stable
@@ -39,10 +39,7 @@
import pytest
from stdio_mgr import stdio_mgr
-from sphobjinv import HeaderFields
-from sphobjinv import Inventory
-from sphobjinv import SourceTypes
-
+from sphobjinv import HeaderFields, Inventory, SourceTypes
CLI_TEST_TIMEOUT = 2
CLI_CMDS = ["sphobjinv", "python -m sphobjinv"]
@@ -76,7 +73,7 @@ def test_cli_noargs_shows_help(self, run_cmdline_test):
with stdio_mgr() as (in_, out_, err_):
run_cmdline_test([])
- assert "usage: sphobjinv" in out_.getvalue()
+ assert re.search("usage:.*sphobjinv", out_.getvalue(), re.I)
@pytest.mark.timeout(CLI_TEST_TIMEOUT)
def test_cli_no_subparser_prs_exit(self, run_cmdline_test):
@@ -358,7 +355,7 @@ def test_cli_suggest_withindex(self, run_cmdline_test, res_cmp):
"""Confirm with_index suggest works."""
with stdio_mgr() as (in_, out_, err_):
run_cmdline_test(["suggest", res_cmp, "instance", "-it", "50"])
- assert re.search("^.*instance_of\\S*\\s+82\\s*$", out_.getvalue(), re.M)
+ assert re.search("^.*instance_of\\S*\\s+127\\s*$", out_.getvalue(), re.M)
@pytest.mark.timeout(CLI_TEST_TIMEOUT)
def test_cli_suggest_withscore(self, run_cmdline_test, res_cmp):
@@ -376,7 +373,7 @@ def test_cli_suggest_withscoreandindex(self, run_cmdline_test, res_cmp):
@pytest.mark.parametrize(
["inp", "flags", "nlines"],
- [("", "-at", 129), ("y\n", "-t", 130), ("n\n", "-t", 1)],
+ [("", "-at", 180), ("y\n", "-t", 181), ("n\n", "-t", 1)],
) # Extra line for input() query in the "y\n" case
@pytest.mark.timeout(CLI_TEST_TIMEOUT)
def test_cli_suggest_long_list(self, inp, flags, nlines, run_cmdline_test, res_cmp):
diff --git a/tests/test_cli_nonlocal.py b/tests/test_cli_nonlocal.py
index b6139e7d..064733ff 100644
--- a/tests/test_cli_nonlocal.py
+++ b/tests/test_cli_nonlocal.py
@@ -10,10 +10,10 @@
20 Mar 2019
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
- http://www.github.com/bskinn/sphobjinv
+ https://github.com/bskinn/sphobjinv
**Documentation**
https://sphobjinv.readthedocs.io/en/stable
@@ -65,7 +65,12 @@ class TestConvert:
@pytest.mark.timeout(CLI_TEST_TIMEOUT * 4)
def test_cli_convert_from_url_with_dest(
- self, scratch_path, misc_info, run_cmdline_test, monkeypatch
+ self,
+ scratch_path,
+ misc_info,
+ http_inv_url_template,
+ run_cmdline_test,
+ monkeypatch,
):
"""Confirm CLI URL D/L, convert works w/outfile supplied."""
monkeypatch.chdir(scratch_path)
@@ -76,7 +81,7 @@ def test_cli_convert_from_url_with_dest(
"convert",
"plain",
"-u",
- misc_info.remote_url.format("attrs"),
+ http_inv_url_template.format("attrs"),
str(dest_path),
]
)
@@ -85,20 +90,30 @@ def test_cli_convert_from_url_with_dest(
@pytest.mark.timeout(CLI_TEST_TIMEOUT * 4)
def test_cli_convert_from_url_no_dest(
- self, scratch_path, misc_info, run_cmdline_test, monkeypatch
+ self,
+ scratch_path,
+ misc_info,
+ http_inv_url_template,
+ run_cmdline_test,
+ monkeypatch,
):
"""Confirm CLI URL D/L, convert works w/o outfile supplied."""
monkeypatch.chdir(scratch_path)
dest_path = scratch_path / (misc_info.FNames.INIT + misc_info.Extensions.DEC)
dest_path.unlink()
run_cmdline_test(
- ["convert", "plain", "-u", misc_info.remote_url.format("attrs")]
+ ["convert", "plain", "-u", http_inv_url_template.format("attrs")]
)
assert dest_path.is_file()
@pytest.mark.timeout(CLI_TEST_TIMEOUT * 4)
def test_cli_url_in_json(
- self, scratch_path, misc_info, run_cmdline_test, monkeypatch
+ self,
+ scratch_path,
+ misc_info,
+ http_inv_url_template,
+ run_cmdline_test,
+ monkeypatch,
):
"""Confirm URL is present when using CLI URL mode."""
monkeypatch.chdir(scratch_path)
@@ -108,7 +123,7 @@ def test_cli_url_in_json(
"convert",
"json",
"-u",
- misc_info.remote_url.format("attrs"),
+ http_inv_url_template.format("attrs"),
str(dest_path.resolve()),
]
)
@@ -118,7 +133,10 @@ def test_cli_url_in_json(
assert "objects" in d.get("metadata", {}).get("url", {})
@pytest.mark.timeout(CLI_TEST_TIMEOUT * 4)
- def test_clifail_bad_url(self, run_cmdline_test, misc_info, scratch_path):
+ @pytest.mark.filterwarnings("ignore:implicitly cleaning up.*404")
+ def test_clifail_bad_url(
+ self, run_cmdline_test, misc_info, http_inv_url_template, scratch_path
+ ):
"""Confirm proper error behavior when a bad URL is passed."""
with stdio_mgr() as (in_, out_, err_):
run_cmdline_test(
@@ -126,16 +144,16 @@ def test_clifail_bad_url(self, run_cmdline_test, misc_info, scratch_path):
"convert",
"plain",
"-u",
- misc_info.remote_url.format("blarghers"),
+ http_inv_url_template.format("blarghers"),
str(scratch_path),
],
expect=1,
)
- assert "HTTP error: 404 Not Found." in err_.getvalue()
+ assert re.search("http error.*404.*not found", err_.getvalue(), re.I)
@pytest.mark.timeout(CLI_TEST_TIMEOUT * 4)
def test_clifail_url_no_leading_http(self, run_cmdline_test, scratch_path):
- """Confirm proper error behavior when a URL w/o leading 'http://' is passed."""
+ """Confirm proper error behavior when a URL w/o leading scheme is passed."""
with stdio_mgr() as (in_, out_, err_):
run_cmdline_test(
[
@@ -150,10 +168,16 @@ def test_clifail_url_no_leading_http(self, run_cmdline_test, scratch_path):
assert "file found but inventory could not be loaded" in err_.getvalue()
def test_cli_json_export_import(
- self, res_cmp, scratch_path, misc_info, run_cmdline_test, sphinx_load_test
+ self,
+ res_cmp,
+ scratch_path,
+ misc_info,
+ http_inv_url_template,
+ run_cmdline_test,
+ sphinx_load_test,
):
"""Confirm JSON sent to stdout from local source imports ok."""
- inv_url = misc_info.remote_url.format("attrs")
+ inv_url = http_inv_url_template.format("attrs")
mod_path = scratch_path / (misc_info.FNames.MOD + misc_info.Extensions.CMP)
with stdio_mgr() as (in_, out_, err_):
@@ -173,14 +197,16 @@ class TestSuggest:
"""Test nonlocal CLI suggest mode functionality."""
@pytest.mark.timeout(CLI_TEST_TIMEOUT * 4)
- def test_cli_suggest_from_url(self, misc_info, run_cmdline_test):
+ def test_cli_suggest_from_url(
+ self, misc_info, http_inv_url_template, run_cmdline_test
+ ):
"""Confirm reST-only suggest output works from URL."""
with stdio_mgr() as (in_, out_, err_):
run_cmdline_test(
[
"suggest",
"-u",
- misc_info.remote_url.format("attrs"),
+ http_inv_url_template.format("attrs"),
"instance",
"-t",
"50",
@@ -191,15 +217,16 @@ def test_cli_suggest_from_url(self, misc_info, run_cmdline_test):
@pytest.mark.parametrize(
"url",
[
- "http://sphobjinv.readthedocs.io/en/v2.0/modules/",
- "http://sphobjinv.readthedocs.io/en/v2.0/modules/cmdline.html",
+ "https://sphobjinv.readthedocs.io/en/v2.0/modules/",
+ "https://sphobjinv.readthedocs.io/en/v2.0/modules/cmdline.html",
(
- "http://sphobjinv.readthedocs.io/en/v2.0/modules/"
+ "https://sphobjinv.readthedocs.io/en/v2.0/modules/"
"cmdline.html#sphobjinv.cmdline.do_convert"
),
],
)
@pytest.mark.timeout(CLI_TEST_TIMEOUT * 4)
+ @pytest.mark.filterwarnings("ignore:implicitly cleaning up.*404")
def test_cli_suggest_from_docset_urls(self, url, run_cmdline_test, check):
"""Confirm reST-only suggest output works from URLs within a docset."""
with stdio_mgr() as (in_, out_, err_):
@@ -208,28 +235,18 @@ def test_cli_suggest_from_docset_urls(self, url, run_cmdline_test, check):
check.is_true(p_inventory.search(out_.getvalue()))
check.is_in("LIKELY", err_.getvalue())
check.is_in(
- "(http://sphobjinv.readthedocs.io/en/v2.0/, None)", err_.getvalue()
+ "(https://sphobjinv.readthedocs.io/en/v2.0/, None)", err_.getvalue()
)
@pytest.mark.timeout(CLI_TEST_TIMEOUT * 4)
def test_cli_suggest_from_typical_objinv_url(self, run_cmdline_test, check):
"""Confirm reST-only suggest works for direct objects.inv URL."""
- url = "http://sphobjinv.readthedocs.io/en/v2.0/objects.inv"
+ url = "https://sphobjinv.readthedocs.io/en/v2.0/objects.inv"
with stdio_mgr() as (in_, out_, err_):
run_cmdline_test(["suggest", "-u", url, "inventory", "-at", "50"])
check.is_true(p_inventory.search(out_.getvalue()))
check.is_in("PROBABLY", err_.getvalue())
check.is_in(
- "(http://sphobjinv.readthedocs.io/en/v2.0/, None)", err_.getvalue()
+ "(https://sphobjinv.readthedocs.io/en/v2.0/, None)", err_.getvalue()
)
-
- @pytest.mark.timeout(CLI_TEST_TIMEOUT * 4)
- def test_cli_suggest_from_django_objinv_url(self, run_cmdline_test, check):
- """Confirm reST-only suggest works for direct objects.inv URL."""
- url = "https://docs.djangoproject.com/en/4.1/_objects/"
- with stdio_mgr() as (in_, out_, err_):
- run_cmdline_test(["suggest", "-u", url, "route", "-a"])
-
- check.is_true(re.search("DATABASE_ROUTERS", out_.getvalue()))
- check.is_in("Cannot infer intersphinx_mapping", err_.getvalue())
diff --git a/tests/test_cli_textconv.py b/tests/test_cli_textconv.py
new file mode 100644
index 00000000..f08356f5
--- /dev/null
+++ b/tests/test_cli_textconv.py
@@ -0,0 +1,160 @@
+r"""*Textconv CLI tests for* ``sphobjinv``.
+
+``sphobjinv`` is a toolkit for manipulation and inspection of
+Sphinx |objects.inv| files.
+
+**Author**
+ Brian Skinn (brian.skinn@gmail.com)
+
+**File Created**
+ 22 Dec 2025
+
+**Copyright**
+ \(c) 2016-2026 Brian Skinn and community contributors
+
+**Source Repository**
+ https://github.com/bskinn/sphobjinv
+
+**Documentation**
+ https://sphobjinv.readthedocs.io/en/stable
+
+**License**
+ Code: `MIT License`_
+
+ Docs & Docstrings: |CC BY 4.0|_
+
+ See |license_txt|_ for full license terms.
+
+**Members**
+
+"""
+
+import re
+import shlex
+import subprocess as sp # noqa: S404
+from pathlib import Path
+
+import pytest
+from stdio_mgr import stdio_mgr
+
+from sphobjinv import Inventory
+from tests.enum import CLICommand
+
+CLI_TEST_TIMEOUT = 2
+CLI_CMDS = ["sphobjinv-textconv"]
+
+pytestmark = [pytest.mark.cli, pytest.mark.textconv, pytest.mark.local]
+
+
+class TestMisc:
+ """Tests for miscellaneous textconv entrypoint behavior."""
+
+ @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ @pytest.mark.parametrize("cmd", CLI_CMDS)
+ def test_cli_invocations(self, cmd):
+ """Confirm that actual shell invocations do not error."""
+ runargs = shlex.split(cmd)
+ runargs.append("--help")
+
+ out = sp.check_output(" ".join(runargs), shell=True).decode() # noqa: S602
+
+ assert "sphobjinv" in out
+ assert "infile" in out
+
+ @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ def test_cli_version_exits_ok(self, run_cmdline_test):
+ """Confirm --version exits cleanly."""
+ run_cmdline_test(["-v"], command=CLICommand.Textconv)
+
+ @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ def test_cli_noargs_shows_help(self, run_cmdline_test):
+ """Confirm help shown when invoked with no arguments."""
+ with stdio_mgr() as (in_, out_, err_):
+ run_cmdline_test([], command=CLICommand.Textconv)
+
+ assert re.search("usage.+sphobjinv", out_.getvalue(), re.I)
+
+
+class TestGood:
+ """Tests for expected-good textconv entrypoint functionality."""
+
+ def test_textconv_matches_main_conv(self, res_cmp, run_cmdline_test):
+ """Ensure that textconv conversion matches main CLI conversion."""
+ with stdio_mgr() as (_, out_, _):
+ run_cmdline_test(
+ ["convert", "plain", res_cmp, "-"], command=CLICommand.Core
+ )
+ core_output = out_.getvalue()
+
+ with stdio_mgr() as (_, out_, _):
+ run_cmdline_test([res_cmp], command=CLICommand.Textconv)
+ textconv_output = out_.getvalue()
+
+ assert core_output == textconv_output
+
+ def test_textconv_matches_original(self, res_cmp, run_cmdline_test):
+ """Confirm textconv produces a consistent Inventory."""
+ with stdio_mgr() as (_, out_, _):
+ run_cmdline_test([str(res_cmp.resolve())], command=CLICommand.Textconv)
+
+ result = out_.getvalue()
+
+ inv1 = Inventory(res_cmp)
+ inv2 = Inventory(result.encode("utf-8"))
+
+ assert inv1 == inv2
+
+
+class TestFail:
+ """Tests for expected-fail textconv entrypoint behaviors."""
+
+ @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ def test_clifail_convert_wrongfiletype(
+ self, scratch_path, run_cmdline_test, monkeypatch
+ ):
+ """Confirm exit code 1 with invalid file format."""
+ monkeypatch.chdir(scratch_path)
+ fname = "testfile"
+ Path(fname).write_bytes(b"this is not objects.inv\n")
+
+ with stdio_mgr() as (in_, out_, err_):
+ run_cmdline_test([fname], command=CLICommand.Textconv, expect=1)
+ assert "Unrecognized" in err_.getvalue()
+
+ @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ def test_clifail_convert_missingfile(self, run_cmdline_test):
+ """Confirm exit code 1 with nonexistent file specified."""
+ run_cmdline_test(
+ ["thisfileshouldbeabsent.txt"], command=CLICommand.Textconv, expect=1
+ )
+
+ @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ def test_clifail_convert_outputdir_provided(
+ self, res_cmp, scratch_path, run_cmdline_test
+ ):
+ """Confirm exit code 2 when too many inputs are provided."""
+ run_cmdline_test(
+ [
+ res_cmp,
+ str(scratch_path / "objects.txt"),
+ ],
+ command=CLICommand.Textconv,
+ expect=2,
+ )
+
+ @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ def test_clifail_convert_pathonlysrc(self, scratch_path, run_cmdline_test):
+ """Confirm cmdline plaintext convert with input directory arg fails."""
+ run_cmdline_test(
+ [str(scratch_path)],
+ command=CLICommand.Textconv,
+ expect=1,
+ )
+
+ def test_clifail_no_url_arg(self, run_cmdline_test):
+ """Confirm textconv parser errors on non-existent -u flag."""
+ with stdio_mgr() as (in_, out_, err_):
+ run_cmdline_test(
+ ["-u", "nofile.inv"], command=CLICommand.Textconv, expect=2
+ )
+ assert "unrecognized argument" in err_.getvalue()
diff --git a/tests/test_fixture.py b/tests/test_fixture.py
index 03531aee..0760ddff 100644
--- a/tests/test_fixture.py
+++ b/tests/test_fixture.py
@@ -10,10 +10,10 @@
20 Mar 2019
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
- http://www.github.com/bskinn/sphobjinv
+ https://github.com/bskinn/sphobjinv
**Documentation**
https://sphobjinv.readthedocs.io/en/stable
diff --git a/tests/test_flake8_ext.py b/tests/test_flake8_ext.py
deleted file mode 100644
index c851fd7a..00000000
--- a/tests/test_flake8_ext.py
+++ /dev/null
@@ -1,73 +0,0 @@
-r"""*Test(s) to ensure full loading of flake8 extensions*.
-
-``sphobjinv`` is a toolkit for manipulation and inspection of
-Sphinx |objects.inv| files.
-
-**Author**
- Brian Skinn (brian.skinn@gmail.com)
-
-**File Created**
- 27 Apr 2019
-
-**Copyright**
- \(c) Brian Skinn 2016-2025
-
-**Source Repository**
- http://www.github.com/bskinn/sphobjinv
-
-**Documentation**
- https://sphobjinv.readthedocs.io/en/stable
-
-**License**
- Code: `MIT License`_
-
- Docs & Docstrings: |CC BY 4.0|_
-
- See |license_txt|_ for full license terms.
-
-**Members**
-
-"""
-
-import re
-import subprocess as sp # noqa: S404
-import sys
-from pathlib import Path
-
-import pytest
-
-pytestmark = [pytest.mark.flake8_ext]
-
-
-@pytest.fixture(scope="module", autouse=True)
-def skip_if_no_flake8_ext(pytestconfig):
- """Skip test if --flake8_ext not provided.
-
- Auto-applied to all functions in module.
-
- """
- if not pytestconfig.getoption("--flake8_ext"):
- pytest.skip("'--flake8_ext' not specified") # pragma: no cover
-
-
-@pytest.mark.skipif(
- sys.version_info < (3, 6),
- reason="Some flake8 extensions require Python 3.6 or later",
-)
-def test_flake8_version_output(check):
- """Confirm that all desired plugins actually report as loaded."""
- p_pkgname = re.compile("^[0-9a-z_-]+", re.I)
- plugins = Path("requirements-flake8.txt").read_text().splitlines()[1:]
- plugins = [p_pkgname.search(p).group(0) for p in plugins]
-
- # This is fragile if anything ends up not having a prefix that needs
- # stripping
- plugins = [p.partition("flake8-")[-1] for p in plugins]
-
- flake8_ver_output = sp.check_output( # noqa: S607,S603
- ["flake8", "--version"], universal_newlines=True
- ) # noqa: S607,S603
-
- for p in plugins:
- with check(msg=p):
- assert p in flake8_ver_output.replace("_", "-").replace("\n", "")
diff --git a/tests/test_intersphinx.py b/tests/test_intersphinx.py
index 64e681c3..7d72739f 100644
--- a/tests/test_intersphinx.py
+++ b/tests/test_intersphinx.py
@@ -10,10 +10,10 @@
21 Jun 2022
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
- http://www.github.com/bskinn/sphobjinv
+ https://github.com/bskinn/sphobjinv
**Documentation**
https://sphobjinv.readthedocs.io/en/stable
@@ -33,7 +33,6 @@
import sphobjinv.cli.suggest as soi_cli_suggest
-
pytestmark = [pytest.mark.intersphinx, pytest.mark.local]
diff --git a/tests/test_valid_objects.py b/tests/test_valid_objects.py
index 130a9e48..b934394b 100644
--- a/tests/test_valid_objects.py
+++ b/tests/test_valid_objects.py
@@ -10,10 +10,10 @@
13 Feb 2021
**Copyright**
- \(c) Brian Skinn 2016-2025
+ \(c) 2016-2026 Brian Skinn and community contributors
**Source Repository**
- http://www.github.com/bskinn/sphobjinv
+ https://github.com/bskinn/sphobjinv
**Documentation**
https://sphobjinv.readthedocs.io/en/stable
diff --git a/tox.ini b/tox.ini
index e7c27a45..47857a09 100644
--- a/tox.ini
+++ b/tox.ini
@@ -3,23 +3,23 @@ minversion=2.0
isolated_build=True
envlist=
# Test all Python versions on latest lib versions
- py3{9,10,11,12,13}-sphx_latest-attrs_latest-jsch_latest
+ py3{10,11,12,13t,14,14t}-sphx_latest-attrs_latest-jsch_latest
# Test leading Python version on current in-repo dev lib versions
py313-sphx_dev-attrs_dev-jsch_dev
- # Scan across Sphinx versions
- py313-sphx_{1_6_x,1_x,2_x,4_x,5_x,6_x,7_x,dev}-attrs_latest-jsch_latest
- # sphx_3_x is incompatible with py310 due to a typing import. Test on py39 instead.
- py39-sphx_3_x-attrs_latest-jsch_latest
+ # Scan across Sphinx versions (skip 3_x due to a typing import error)
+ py313-sphx_{1_6_x,1_x,2_x,4_x,5_x,6_x,7_x,8_x,dev}-attrs_latest-jsch_latest
# Scan attrs versions
py313-sphx_latest-attrs_{19_2,19_3,20_3,21_3,22_2,23_2,24_3,dev}-jsch_latest
# Scan jsonschema versions
- py313-sphx_latest-attrs_latest-jsch_{3_0,3_x,4_0,4_8,4_14,4_20,dev}
+ py313-sphx_latest-attrs_latest-jsch_{3_1_1,3_x,4_0,4_8,4_14,4_20,dev}
# Earliest supported Python and lib versions all together
- py39-sphx_1_6_x-attrs_19_2-jsch_3_0
+ py310-sphx_1_6_x-attrs_19_2-jsch_3_1_1
# Spot matrix of early Python, Sphinx, attrs versions
- py3{9,10}-sphx_{1,2}_x-attrs_{19,20}_2-jsch_latest
+ py3{10,11}-sphx_{1,2}_x-attrs_{19,20}_2-jsch_latest
+ # Test the new-earliest jsonschema version that started failing in 3.13
+ py3{10,11,12,13,14}-sphx_latest-attrs_latest-jsch_3_1_1
# Test the specific Sphinx threshold cases where behavior changed
- py312-sphx_{2_3_1,2_4_0,3_2_1,3_3_0,3_4_0,8_1_3,8_2_0}-attrs_latest-jsch_latest
+ py313-sphx_{2_3_1,2_4_0,3_2_1,3_3_0,3_4_0,8_1_3,8_2_0}-attrs_latest-jsch_latest
# Simple 'does the sdist install' check
sdist_install
# Lints
@@ -30,6 +30,7 @@ envlist=
[testenv]
commands=
python --version
+ python -c 'import sys; print("GIL: ", end=""); print(getattr(sys, "_is_gil_enabled", lambda : True)())'
pip list
# Want the tox *matrix* to ignore warnings since it's primarily
# a compatibility check. The defaults for bare pytest enable -Werror
@@ -43,6 +44,7 @@ deps=
sphx_5_x: sphinx<6
sphx_6_x: sphinx<7
sphx_7_x: sphinx<8
+ sphx_8_x: sphinx<9
sphx_2_3_1: sphinx==2.3.1
sphx_2_4_0: sphinx==2.4.0
sphx_3_2_1: sphinx==3.2.1
@@ -63,7 +65,7 @@ deps=
attrs_latest: attrs
attrs_dev: git+https://github.com/python-attrs/attrs
- jsch_3_0: jsonschema==3.0
+ jsch_3_1_1: jsonschema==3.1.1
jsch_3_x: jsonschema<4
jsch_4_0: jsonschema<4.1
jsch_4_8: jsonschema<4.9
@@ -86,38 +88,56 @@ deps=
[testenv:linux]
platform=linux
basepython=
+ py314: python3.14
+ py314t: python3.14t
py313: python3.13
+ py313t: python3.13t
py312: python3.12
py311: python3.11
py310: python3.10
- py39: python3.9
[testenv:black]
+description=Autoformat code and tests with black
skip_install=True
deps=black
commands=
+ black --version
black {posargs} .
[testenv:flake8]
+description=Lint code and tests with flake8
skip_install=True
deps=-rrequirements-flake8.txt
commands=
- flake8 ./conftest.py src tests
+ flake8 --version
+ flake8 {posargs} src tests
[testenv:flake8_noqa]
+description=Lint noqa directives with flake8-noqa
skip_install=True
deps=-rrequirements-flake8.txt
commands=
pip install flake8-noqa
- flake8 --color=never --exit-zero ./conftest.py tests src
+ flake8 --color=never --exit-zero {posargs} tests src
[testenv:interrogate]
+description=Lint docstrings with interrogate
skip_install=True
deps=interrogate
commands=
- interrogate {posargs} conftest.py tests src
+ interrogate {posargs} tests src
+
+[testenv:isort]
+description=Sort, group, and coalesce imports
+skip_install=True
+deps=isort
+commands=
+ isort --version
+ isort {posargs} src tests
[testenv:linkcheck]
+description=Run Sphinx linkcheck on docs (Linux only)
+platform=linux
skip_install=True
deps=-rrequirements-dev.txt
allowlist_externals=
@@ -127,20 +147,31 @@ commands=
make linkcheck
[testenv:sdist_install]
+description=Confirm that sdist installs and imports
commands=
python -Werror -c "import sphobjinv"
deps=
+[testenv:build]
+skip_install=True
+description=Build sdist and wheel
+deps=
+ build
+ twine
+commands=
+ python -m build
+ twine check dist/*
+
[pytest]
markers =
local: Tests not requiring Internet access
nonloc: Tests requiring Internet access
cli: Command-line interface tests
api: Direct API tests
+ textconv: Textconv CLI tests
intersphinx: Tests on intersphinx-related functionality
fixture: Trivial tests for test suite fixtures
testall: Tests that use *all* objects_xyz.inv files in tests/resource, if --testall is specified
- flake8_ext: Test checking that all desired plugins are active
first: Inherited marker from `pytest-ordering`
timeout: Inherited marker from `pytest-timeout`
@@ -188,7 +219,3 @@ per_file_ignores =
src/sphobjinv/cli/__init__.py: F401, RST305,RST306
# PIE786: CLI uses 'except Exception:' as a catchall... to be changed, eventually
src/sphobjinv/cli/*: PIE786, RST305,RST306
-
-#flake8-import-order
-import-order-style = smarkets
-application-import-names = sphobjinv