Repository: bskinn/sphobjinv (language: python)

Issue:
InventoryFile ~appears to be equipped to~ *cannot* handle an empty dispname
~At least, according to the [regex](https://github.com/sphinx-doc/sphinx/blob/v3.4.0/sphinx/util/inventory.py#L124).~

**CORRECTION:** Nope! The [`.rstrip()` applied to the data line contents](https://github.com/sphinx-doc/sphinx/blob/f9968594206e538f13fa1c27c065027f10d4ea27/sphinx/util/inventory.py#L125) makes the regex fail if there's no `dispname`.

- ~Code~
- [x] Tests
- [x] Docs
  - Handled a/o 6632dd4 
  - **Reverted** a/o a434d4f
-  ~CHANGELOG~
Names can have spaces after all
I'm currently writing an objects.inv parser in non-Python and stumbled into something that you might be interested in: contrary to <https://sphobjinv.readthedocs.io/en/stable/syntax.html>, the name field can indeed contain spaces.

I've encountered it in my very own project, that has the following line:

```
dict classes std:term -1 glossary.html#term-dict-classes -
```

I tried loading https://www.attrs.org/en/stable/objects.inv with sphobjinv and it seems to silently ignore these entries.

Maybe you can just steal [their regex](https://github.com/sphinx-doc/sphinx/blob/307a0e580fffa92ece92f08dc1be3a7db4a90dd1/sphinx/util/inventory.py#L122-L123).
Reverse loosening of `priority` constraints in regex & tests
Per discussion at #181, Sphinx will silently ignore object entries with non-integer priority values

- [x] Code
- [x] Tests
  - [x] Remove `objects.inv` files with non-integer `priority` from main tests
  - ~Establish consistent nomenclature to identify `objects.inv` with non-integer `priority` and add fails-to-load tests for them~
    - [x] More complicated than necessary. With the new custom-built data line testing, it was easy to put invalid `priority` cases in.
- [x] Docs
  - Handled a/o 6632dd4
- [x] CHANGELOG


Discussion / hints:

Fix patch:
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..89af26e4
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+tests/resource/objects_mkdoc_zlib0.inv binary
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a2f2540c..9211b6a1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,137 +9,216 @@ and this project strives to adhere to
 
 ### [2.1b2] - *(unreleased)*
 
+#### Added
+
+  * Python 3.10 support was officially added.
+
 #### Changed
 
- * The User-Agent header sent by `Inventory` when making an HTTP(S) request
-   now identifies `sphobjinv` and its version (anticipate no API or
-   behavior change).
+  * The User-Agent header sent by `Inventory` when making an HTTP(S) request
+    now identifies `sphobjinv` and its version (anticipate no API or
+    behavior change).
 
- * An extraneous newline was removed before tables printed in the 
-   'suggest' CLI mode (cosmetic change).
+  * An extraneous newline was removed before tables printed in the
+    'suggest' CLI mode (cosmetic change).
+
+#### Fixed
+
+  * Previously, `sphobjinv.Inventory` would ignore entries in `objects.inv`
+    that contained spaces within `name`
+    (see [#181](https://github.com/bskinn/sphobjinv/issues/181));
+    this is now fixed.
 
 #### Removed
 
- * Python 3.5 is no longer supported.
+  * Python 3.5 is no longer supported.
+
+  * The relaxation of the integer constraint on the `priority` field
+    introduced in v2.1b1 has been *reverted*, as `objects.inv` data lines
+    with such non-integer `priority` values are skipped by Sphinx.
 
 #### Internal
 
- * Where possible, string interpolation has been refactored to use
-   f-strings.
+  * Where possible, string interpolation has been refactored to use
+    f-strings.
+
+  * A 'speedup' `extras_require` entry has been added to allow simple installation
+    of `python-Levenshtein` for Linux and MacOS platforms, as
+    `pip install sphobjinv[speedup]`. This extra does nothing on Windows, since
+    compilation machinery is anticipated not to be available for most users.
+
+  * `objects_mkdoc_zlib0.inv`, which was compressed at `zlib` level 0,
+    has been added to the test resources directory.
+
+  * This file had to be flagged as binary in `.gitattributes` in order to avoid
+      git EOL auto-conversion on Windows.
+
+  * The CLI functionality was refactored from the single `sphobjinv.cmdline` module
+    into a dedicated set of `sphobjinv.cli.*` submodules.
+
+#### Testing
+
+  * Added *significant* body of new tests to confirm inventory compatibility
+    with both `sphobjinv` and Sphinx itself.
+
+    * Consistency checks added both for data within `sphobjinv.Inventory` instances
+      **AND** as emitted from `sphinx.ext.inventory.InventoryFile.load()`.
 
- * A 'speedup' `extras_require` entry has been added to allow simple installation
-   of `python-Levenshtein` for Linux and MacOS platforms, as
-   `pip install sphobjinv[speedup]`. This extra does nothing on Windows, since
-   compilation machinery is anticipated not to be available for most users.
+    * The tests in `tests/test_valid_objects.py` strive to bracket as precisely
+      as possible what content is allowed on an `objects.inv` data line,
+      in addition to providing guidance on what is allowable, but discouraged.
+
+      `docs/source/syntax.rst` was also edited to reflect this guidance.
+
+  * Additional tests have been added to probe corner cases involving Windows EOLs.
+
+  * A test was added to ensure that the schema in `sphobjinv.schema` is in fact
+    a valid JSON schema.
+
+  * Multiple asserts/checks per test method have been converted to use
+    `pytest-check` instead of `pytest-subtests`, due to some inconsistent
+    behavior with the latter.
+
+  * `tox` environments and dependencies were updated, and some flake8 configuration
+    was adjusted.
+
+#### Administrative
+
+  * Standard development Python version bumped to 3.9.
+
+  * Standard development Sphinx version bumped to 3.5.0.
+
+  * Added `[skip ci]` flag in commit text for skipping Github Actions CI.
+
+  * RtD upgraded to use Python 3.8.
+
+  * Added 'radio Sphinx' logo to RtD docs.
+
+  * Drafted `CONTRIBUTING.md` and added PR & issue templates.
+
+  * Tranferred most project metadata from `setup.py` to `setup.cfg`.
 
 
 ### [2.1b1] - 2020-11-13
 
 #### Fixed
 
- * Equality tests on Inventory and DataObjStr/DataObjBytes instances
-   now work correctly.
+  * Equality tests on Inventory and DataObjStr/DataObjBytes instances
+    now work correctly.
 
- * Non-integer and non-numeric values for `priority` are now accepted
-   during `Inventory` instantiation, consistent with what is allowed
-   by `DataObjStr` and `DataObjBytes` instantiation.
+  * Non-integer and non-numeric values for `priority` are now accepted
+    during `Inventory` instantiation, consistent with what is allowed
+    by `DataObjStr` and `DataObjBytes` instantiation.
 
 
 ### [2.1a2] - 2020-10-27
 
 #### Added
 
- * When an inventory is retrieved via CLI from a remote URL with `-u`,
-   the resolved location of the inventory is included in generated JSON
-   at `json_dict.metadata.url`.
+  * When an inventory is retrieved via CLI from a remote URL with `-u`,
+    the resolved location of the inventory is included in generated JSON
+    at `json_dict.metadata.url`.
 
 #### Changed
 
- * CLI logging messages are now emitted to stderr instead of stdout.
+  * CLI logging messages are now emitted to stderr instead of stdout.
 
 
 ### [2.1a1] - 2020-10-26
 
 #### Added
 
- * A hyphen can now be passed as the CLI input and/or output file name
-   to instruct sphobjinv to use stdin and/or stdout, respectively.
+  * A hyphen can now be passed as the CLI input and/or output file name
+    to instruct sphobjinv to use stdin and/or stdout, respectively.
 
- * The `fileops` and `inventory` APIs are now tested to work with
-   both strings and `pathlib.Path` objects, where they interact
-   with the filesystem.
+  * The `fileops` and `inventory` APIs are now tested to work with
+    both strings and `pathlib.Path` objects, where they interact
+    with the filesystem.
 
 #### Refactored
 
- * Patterns in regular expressions are now defined with raw strings
-   to improve readability.
+  * Patterns in regular expressions are now defined with raw strings
+    to improve readability.
 
 
 ### [2.0.1] - 2020-01-26
 
 #### Fixed
 
- * attr.s usage on Inventory changed to use eq=False where possible,
-   per the deprecation of the cmp argument.
- * URL inventory retrieval now sends a User-Agent header, to avoid
-   403 FORBIDDEN errors on some docs servers.
+  * attr.s usage on Inventory changed to use eq=False where possible,
+    per the deprecation of the cmp argument.
+
+  * URL inventory retrieval now sends a User-Agent header, to avoid
+    403 FORBIDDEN errors on some docs servers.
 
 
 ### [2.0.1rc1] - 2019-02-01
 
 #### Fixed
 
- * Sphinx *can* generate inventories with empty-string values for
-   `project` and `version`; `sphobjinv` now can import such
-   inventories without error.
+  * Sphinx *can* generate inventories with empty-string values for
+    `project` and `version`; `sphobjinv` now can import such
+    inventories without error.
 
 
 ### [2.0.0] - 2018-08-16
 
 #### Added
 
- * Loading remote inventories from the CLI now will
-   perform an automatic walk along the directory structure of the
-   provided URL, searching for the objects.inv file of the
-   documentation set.
- * The above URL walking functionality is exposed for API use
-   at sphobjinv.fileops.urlwalk.
+  * Loading remote inventories from the CLI now will
+    perform an automatic walk along the directory structure of the
+    provided URL, searching for the objects.inv file of the
+    documentation set.
+  * The above URL walking functionality is exposed for API use
+    at sphobjinv.fileops.urlwalk.
 
 #### Changed
 
- * Instances of DataObjStr and DataObjBytes are NO LONGER IMMUTABLE.
-   Working with Inventory instances was going to be far too cumbersome
-   with immutable DataObjStr instances in .objects.
+  * Instances of DataObjStr and DataObjBytes are NO LONGER IMMUTABLE.
+    Working with Inventory instances was going to be far too cumbersome
+    with immutable DataObjStr instances in .objects.
 
 
 ### [2.0.0rc1] - 2018-05-18
 
 #### Fixed
 
- * API code no longer contains any `sys.exit` calls;
-   CLI interactions should now be properly segregated from the internal API.
+  * API code no longer contains any `sys.exit` calls;
+    CLI interactions should now be properly segregated from the internal API.
 
 #### Added
 
- * API
-   * Data for individual objects encapsulated in the new `.data.DataObjStr` and `.data.DataObjBytes` classes
-     * Instances of these objects provide granular access to the contained data
-     * Instances are *immutable*, but expose an `evolve()` method for creating new, (optionally) modified instances.
-   * Entire inventory contents, as a `list` of `DataObjStr`, encapsulated in `.inventory.Inventory` instances
-     * `Inventory` instances are anticipated to be the primary point of user interface to inventory data
-     * `.suggest()` method added, exploiting fuzzy string searching by [`fuzzywuzzy`](https://pypi.org/project/fuzzywuzzy) for rapid searching of inventories for objects of interest
+  * API
+    * Data for individual objects encapsulated in the new `.data.DataObjStr` and
+    * `.data.DataObjBytes` classes
+    * Instances of these objects provide granular access to the contained data
+    * Instances are *immutable*, but expose an `evolve()` method for creating new,
+      (optionally) modified instances.
+   * Entire inventory contents, as a `list` of `DataObjStr`, encapsulated in
+     `.inventory.Inventory` instances
+     * `Inventory` instances are anticipated to be the primary point of user interface to
+       inventory data
+     * `.suggest()` method added, exploiting fuzzy string searching by
+       [`fuzzywuzzy`](https://pypi.org/project/fuzzywuzzy) for rapid searching of
+       inventories for objects of interest
      * Import of remote `objects.inv` files via `urllib.request` and `certifi` implemented.
-   * Helper methods are provided for working with the `objects.inv` shorthands for object URI and display-name information, on both `DataObj...` and `Inventory` instances.
+   * Helper methods are provided for working with the `objects.inv` shorthands for object
+     URI and display-name information, on both `DataObj...` and `Inventory` instances.
    * `readjson`/`writejson` helper functions added to `.fileops`
-   * JSON schema added to `.schema` for use with [`jsonschema`](https://pypi.org/project/jsonschema) to validate incoming data during `Inventory` instantiation from  JSON
+   * JSON schema added to `.schema` for use with
+     [`jsonschema`](https://pypi.org/project/jsonschema) to validate incoming data
+     during `Inventory` instantiation from  JSON
  * Command-line interface
-   * Add `suggest` subparser, for recommendation of matching objects within an input inventory
+   * Add `suggest` subparser, for recommendation of matching objects
+     within an input inventory
    * Add ability to read and write JSON inventories
-   * Add ability to use a remote `objects.inv` file (arbitrary filename) as input, specified by URL
+   * Add ability to use a remote `objects.inv` file (arbitrary filename) as input,
+     specified by URL
    * Add arguments to:
      * Expand/contract the `objects.inv` URI and 'display name' abbreviations
      * Force overwrite of an existing output file without prompting
-     * Silence all output (mainly intended for scripting applications); includes automatic overwriting of an existing output file
+     * Silence all output (mainly intended for scripting applications);
+       includes automatic overwriting of an existing output file
      * Various arguments to control the output of a `sphobjinv suggest` invocation
 
 #### Changed
@@ -147,10 +226,14 @@ and this project strives to adhere to
  * `readfile`/`writefile` changed to `readbytes`/`writebytes`
    and moved to new `.fileops` submodule
  * `decode`/`encode` changed to `decompress`/`compress` and moved to new `.zlib` submodule
- * `p_data`/`p_comments` regex patterns renamed with `pb_` prefix to indicate they are `bytes`, not `str` patterns
+ * `p_data`/`p_comments` regex patterns renamed with `pb_` prefix to indicate they are
+   `bytes`, not `str` patterns
  * CLI
-   * Commands for (de)compressing `objects.inv` files changed to a more general `convert` subparser, due to addition of JSON output option
-   * Default filename/extension assumptions removed from input file argument, as multiple input formats make the logic here complicated enough it's not worth messing with
+   * Commands for (de)compressing `objects.inv` files changed to a more general `convert`
+     subparser, due to addition of JSON output option
+   * Default filename/extension assumptions removed from input file argument,
+     as multiple input formats make the logic here complicated enough
+     it's not worth messing with
 
 ### [1.0.0] - 2016-05-24
 
diff --git a/README.rst b/README.rst
index 36b234db..785b9065 100644
--- a/README.rst
+++ b/README.rst
@@ -120,7 +120,7 @@ inventory creation/modification::
     >>> import sphobjinv as soi
     >>> inv = soi.Inventory('doc/build/html/objects.inv')
     >>> print(inv)
-    <Inventory (fname_zlib): sphobjinv v2.1b2, 199 objects>
+    <Inventory (fname_zlib): sphobjinv v2.1b2, 205 objects>
     >>> inv.project
     'sphobjinv'
     >>> inv.version
diff --git a/doc/Makefile b/doc/Makefile
index d0c3cbf1..7b680a8c 100644
--- a/doc/Makefile
+++ b/doc/Makefile
@@ -14,6 +14,10 @@ help:
 
 .PHONY: help Makefile
 
+# Custom for sphinx-autobuild
+livehtml:
+	sphinx-autobuild "$(SOURCEDIR)" "$(BUILDDIR)"/html -nb html $(SPHINXOPTS) $(O)
+
 # Catch-all target: route all unknown targets to Sphinx using the new
 # "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
 %: Makefile
diff --git a/doc/source/_static/mouseover_example.png b/doc/source/_static/mouseover_example.png
index 1502f976..07e60cba 100644
Binary files a/doc/source/_static/mouseover_example.png and b/doc/source/_static/mouseover_example.png differ
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 5cfbb183..dac34c0c 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -71,12 +71,6 @@
 
 rst_epilog = r"""
 
-.. raw:: html
-
-    <small><em>Logo adapted from freely-available image at
-    https://heypik.com/images/hand-drawn-cartoon-color-tourist-attraction-sphinx_8ZU4501.html
-    (retrieved July 2019; now defunct).</em></small>
-
 .. |extlink| image:: /_static/extlink.svg
 
 .. |dag| replace:: :math:`^\dagger`
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 3f363cf1..e9b91cca 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -100,3 +100,9 @@ Indices and Tables
 * :ref:`modindex`
 * :ref:`search`
 
+
+.. raw:: html
+
+   <small><em>Logo adapted from freely-available image at
+   https://heypik.com/images/hand-drawn-cartoon-color-tourist-attraction-sphinx_8ZU4501.html
+   (retrieved July 2019; now defunct).</em></small>
diff --git a/doc/source/syntax.rst b/doc/source/syntax.rst
index b913bbe5..3ae2f495 100644
--- a/doc/source/syntax.rst
+++ b/doc/source/syntax.rst
@@ -5,12 +5,20 @@ 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 completely undocumented. The below
-syntax is believed to be accurate as of Jun 2018 (Sphinx v1.7.4).
+is not included in the Sphinx documentation. The below
+syntax is believed to be accurate as of Feb 2021 (Sphinx v3.4.3).
+
 Based upon a quick ``git diff`` of the `Sphinx repository
-<https://github.com/sphinx-doc/sphinx>`__, it is thought to be accurate for all
+<https://github.com/sphinx-doc/sphinx>`__, it is thought to be valid for all
 Sphinx versions >=1.0b1 that make use of this "version 2" |objects.inv| format.
 
+**NOTE** that the previous version of the syntax presented here has been
+shown to be inaccurate (see :issue:`181`), in that it *is*
+permitted for the |{name}|_ field to contain whitespace.
+The descriptions below have been updated to reflect this and to provide
+more detailed information on the constraints governing each field
+of an |objects.inv| data line.
+
 ----
 
 **The first line** `must be exactly
@@ -44,8 +52,7 @@ the |isphx| cross-references:
 
 **The fourth line** `must contain
 <https://github.com/sphinx-doc/sphinx/blob/f7b3292d87e9a2b7eae0b4ef72e87779beefc699/sphinx/util/inventory.py#L136-L137>`__
-the string 'zlib' somewhere in it, but for the purposes of consistency it should
-be exactly:
+the string ``zlib`` somewhere within it, but for consistency it should be exactly:
 
 .. code-block:: none
 
@@ -61,15 +68,24 @@ be exactly:
 
     {name} {domain}:{role} {priority} {uri} {dispname}
 
+.. _{name}:
+
 ``{name}``
+
     The object name used when cross-referencing the object (falls between the
     backticks).
 
     **Constraints**
 
-    * **MUST** have length greater than zero
+    * **MUST** have nonzero length
 
-    * **MUST NOT** contain whitespace
+    * **MUST NOT** start with ``#``
+
+    * **SHOULD** have no leading or trailing whitespace
+
+    * **MAY** contain internal whitespace
+
+.. _{domain}:
 
 ``{domain}``
     The Sphinx domain used when cross-referencing the object (falls between
@@ -77,15 +93,17 @@ be exactly:
 
     **Constraints**
 
-    * **MUST** have length greater than zero
+    * **MUST** have nonzero length
 
     * **MUST** match a built-in or installable Sphinx domain
 
-    * **MUST** consist of only |wordchars|_
+    * **MUST NOT** contain whitespace or a colon
 
-      * **RECOMMENDED** to consist of only ASCII word characters (``a-z``, ``A-Z``,
+      * **RECOMMENDED** to contain only ASCII word characters (``a-z``, ``A-Z``,
         ``0-9``, and ``_``)
 
+.. _{role}:
+
 ``{role}``
     The Sphinx role used when cross-referencing the object (falls between the
     second and third colons; or, between the first and second colons if
@@ -93,15 +111,16 @@ be exactly:
 
     **Constraints**
 
-    * **MUST** have length greater than zero
+    * **MUST** have nonzero length
 
     * **MUST** match a role defined in the domain referenced by ``{domain}``
 
-    * **MUST** consist of only |wordchars|_
+    * **MUST NOT** contain whitespace or a colon
 
       * **RECOMMENDED** to consist of only ASCII word characters (``a-z``, ``A-Z``,
         ``0-9``, and ``_``)
 
+.. _{priority}:
 
 ``{priority}``
     Flag for `placement in search results
@@ -109,45 +128,65 @@ be exactly:
     __init__.py#L319-L325>`__. Most will be ``1`` (standard priority) or
     ``-1`` (omit from results) for documentation built by Sphinx.
 
-    To note, as of Nov 2020 this value is **not** used by ``intersphinx``;
+    To note, as of Feb 2021 this value is **not** used by ``intersphinx``;
     it is only used internally within the search function of the static webpages
     built *by Sphinx* (|prio_py_search|_ and |prio_js_search|_). Thus, custom
-    inventories likely **MAY** use this field for arbitrary content, if desired.
-    This *would* run the risk of a future change to Sphinx/intersphinx causing
-    such custom |objects.inv| files to become incompatible.
+    inventories likely **MAY** use this field for arbitrary content, if desired,
+    as long as the integer constraint is observed.
+    Such use *would* run the risk of a future change to Sphinx/intersphinx
+    breaking compatibility with |objects.inv| files
+    having non-standard |{priority}|_ values.
 
     **Constraints**
 
-    * **MUST** have length greater than zero
+    * **MUST** have nonzero length
 
-    * **MUST NOT** contain whitespace
+    * **MUST** be a positive or negative integer, or zero,
+      *without* a decimal point
+
+    * **MUST NOT** contain whitespace (implicit in the integer constraint)
+
+.. _{uri}:
 
 ``{uri}``
     Relative URI for the location to which cross-references will point.
     The base URI is taken from the relevant element of the |isphxmap|
-    configuration parameter of ``conf.py``.
+    configuration parameter in ``conf.py``.
 
     **Constraints**
 
-    * **MUST** have length greater than zero
+    * **MAY** have zero length, but typically has nonzero length
+
+      * A zero-length |{uri}|_ can occur for the
+        root/index documentation page in certain instances;
+        see |sphinx_uri_issue|_
 
     * **MUST NOT** contain whitespace
 
+.. _{dispname}:
+
 ``{dispname}``
     Default cross-reference text to be displayed in compiled documentation.
 
     **Constraints**
 
-    * **MUST** have length greater than zero
+    * **MUST** have nonzero length
 
     * **MAY** contain internal whitespace (leading/trailing whitespace
       is ignored)
 
+Unicode characters appear to be valid for all fields except
+|{uri}|_ (where they are `specifically forbidden <https://stackoverflow.com/a/1916747/4376000>`__)
+and |{priority}|_.
+However, it is **RECOMMENDED** that they *only* be used in |{dispname}|_,
+as their use in |{name}|_, |{domain}|_ and |{role}|_ would complicate construction
+of cross-references from other documentation source.
+
 ----
 
 **For illustration**, the following is the entry for the
 :meth:`join() <str.join>` method of the :class:`str` class in the
-Python 3.5 |objects.inv|, broken out field-by-field:
+Python 3.9 |objects.inv|, broken out field-by-field:
 
 .. code-block:: none
 
@@ -166,14 +205,14 @@ Python 3.5 |objects.inv|, broken out field-by-field:
 The above illustrates two shorthand notations that were introduced to shrink the
 size of the inventory file:
 
- #. If ``{uri}`` has an anchor (technically a "`fragment identifier
+ #. If |{uri}|_ has an anchor (technically a "`fragment identifier
     <https://en.wikipedia.org/wiki/Fragment_identifier>`__," the portion
     following the ``#`` symbol) and the tail of the anchor is identical to
-    ``{name}``, that tail is `replaced
+    |{name}|_, that tail is `replaced
     <https://github.com/sphinx-doc/sphinx/blob/f7b3292d87e9a2b7eae0b4ef72e87779beefc699/sphinx/util/inventory.py#L180-L182>`__
     with ``$``. |br| |br|
 
- #. If ``{dispname}`` is identical to ``{name}``, it is `stored
+ #. If |{dispname}|_ is identical to |{name}|_, it is `stored
     <https://github.com/sphinx-doc/sphinx/blob/f7b3292d87e9a2b7eae0b4ef72e87779beefc699/sphinx/util/inventory.py#L186-L187>`__
     as ``-``.
 
@@ -221,14 +260,26 @@ as in :obj:`This is join! <str.join>`:
 
 .. _defdom: https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html
 
-.. |wordchars| replace:: word characters
-
-.. _wordchars: https://docs.python.org/3.8/library/re.html#index-32
-
 .. |prio_js_search| replace:: here
 
-.. _prio_js_search: https://github.com/sphinx-doc/sphinx/blob/d17563987a80007e2310102cfde673c651823a39/sphinx/themes/basic/static/searchtools.js#L26-L43
+.. _prio_js_search: https://github.com/sphinx-doc/sphinx/blob/241577f65eea94a08944bf096bd704b495282373/sphinx/themes/basic/static/searchtools.js#L26-L43
 
 .. |prio_py_search| replace:: here
 
-.. _prio_py_search: https://github.com/sphinx-doc/sphinx/blob/624f6937194e1acfe7311faf6e27e370c3118e55/sphinx/search/__init__.py#L332
+.. _prio_py_search: https://github.com/sphinx-doc/sphinx/blob/241577f65eea94a08944bf096bd704b495282373/sphinx/search/__init__.py#L332
+
+.. |sphinx_uri_issue| replace:: sphinx-doc/sphinx#7096
+
+.. _sphinx_uri_issue: https://github.com/sphinx-doc/sphinx/issues/7096
+
+.. |{name}| replace:: ``{name}``
+
+.. |{domain}| replace:: ``{domain}``
+
+.. |{role}| replace:: ``{role}``
+
+.. |{priority}| replace:: ``{priority}``
+
+.. |{uri}| replace:: ``{uri}``
+
+.. |{dispname}| replace:: ``{dispname}``
diff --git a/requirements-ci.txt b/requirements-ci.txt
index 27b23f9c..4060bd31 100644
--- a/requirements-ci.txt
+++ b/requirements-ci.txt
@@ -2,6 +2,7 @@ attrs>=17.4
 certifi
 codecov
 coverage
+dictdiffer
 fuzzywuzzy>=0.3
 jsonschema
 md-toc
@@ -10,7 +11,7 @@ pytest-check>=0.4
 pytest-cov
 pytest-ordering
 pytest-timeout
-sphinx==3.3.1
+sphinx==3.5.0
 sphinx-issues
 sphinx-rtd-theme
 sphinxcontrib-programoutput
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 56ef758b..fa04aef0 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,6 +1,7 @@
 attrs>=17.4
 certifi
 coverage
+dictdiffer
 fuzzywuzzy>=0.3
 jsonschema
 md-toc
@@ -12,7 +13,7 @@ pytest-ordering
 pytest-timeout
 restview
 rope
-sphinx==3.3.1
+sphinx==3.5.0
 sphinx-autobuild
 sphinx-issues
 sphinxcontrib-programoutput
diff --git a/requirements-rtd.txt b/requirements-rtd.txt
index c3a68f79..a916f574 100644
--- a/requirements-rtd.txt
+++ b/requirements-rtd.txt
@@ -1,5 +1,5 @@
 attrs>=17.4
-sphinx==3.3.1
+sphinx==3.5.0
 sphinx-issues
 sphinx-rtd-theme
 sphinxcontrib-programoutput
diff --git a/src/sphobjinv/re.py b/src/sphobjinv/re.py
index 2aef531f..39b49824 100644
--- a/src/sphobjinv/re.py
+++ b/src/sphobjinv/re.py
@@ -65,19 +65,19 @@
 #: :data:`~sphobjinv.re.p_data` and
 #: :data:`~sphobjinv.re.pb_data`
 ptn_data = rf"""
-    ^                             # Start of line
-    (?P<{DF.Name.value}>[^#]\S+)  # --> Name
-    \s+                           # Dividing space
-    (?P<{DF.Domain.value}>\w+)    # --> Domain
-    :                             # Dividing colon
-    (?P<{DF.Role.value}>\w+)      # --> Role
-    \s+                           # Dividing space
-    (?P<{DF.Priority.value}>\S+)  # --> Priority
-    \s+                           # Dividing space
-    (?P<{DF.URI.value}>\S+)       # --> URI
-    \s+                           # Dividing space
-    (?P<{DF.DispName.value}>.+?)  # --> Display name, lazy b/c possible CR
-    \r?$                          # Ignore possible CR at EOL
+    ^                               # Start of line
+    (?P<{DF.Name.value}>.+?)        # --> Name
+    \s+                             # Dividing space
+    (?P<{DF.Domain.value}>[^\s:]+)  # --> Domain
+    :                               # Dividing colon
+    (?P<{DF.Role.value}>[^\s:]+)    # --> Role
+    \s+                             # Dividing space
+    (?P<{DF.Priority.value}>-?\d+)  # --> Priority
+    \s+?                            # Dividing space
+    (?P<{DF.URI.value}>\S*)         # --> URI
+    \s+                             # Dividing space
+    (?P<{DF.DispName.value}>.+?)    # --> Display name, lazy b/c possible CR
+    \r?$                            # Ignore possible CR at EOL
     """
 
 #: Compiled |re| |bytes| regex pattern for data lines in |bytes| decompressed
diff --git a/tox.ini b/tox.ini
index d2c221f1..ef1627fe 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,12 +4,13 @@ isolated_build=True
 envlist=
     py3_{06,07,08,09,10}-sphx_latest-attrs_latest-jsch_latest-fzwz_latest
     py3_09-sphx_dev-attrs_dev-jsch_dev-fzwz_dev
-    py3_09-sphx_{1_6,1_x,2_x,3_x,dev}-attrs_latest-jsch_latest-fzwz_latest
+    py3_09-sphx_{1_6_x,1_x,2_x,3_x,dev}-attrs_latest-jsch_latest-fzwz_latest
     py3_09-sphx_latest-attrs_{19_2,19_3,20_1,20_2,20_3,dev}-jsch_latest-fzwz_latest
     py3_09-sphx_latest-attrs_latest-jsch_{3_0,3_1,dev}-fzwz_latest
     py3_09-sphx_latest-attrs_latest-jsch_latest-fzwz_0_{08,10,12,14,16,17,dev}
-    py3_06-sphx_1_6-attrs_19_2-jsch_3_0-fzwz_0_08
-    py3_0{7,8}-sphx_{1,2}x-attrs_{19,20}_2-jsch_latest-fzwz_0_{10,16}
+    py3_06-sphx_1_6_x-attrs_19_2-jsch_3_0-fzwz_0_08
+    py3_0{7,8}-sphx_{1,2}_x-attrs_{19,20}_2-jsch_latest-fzwz_0_{10,16}
+    py3_09-sphx_{2_3_1,2_4_0,3_2_1,3_3_0}-attrs_latest-jsch_latest-fzwz_latest
     sdist_install
     flake8
 
@@ -17,12 +18,16 @@ envlist=
 commands=
     python --version
     pip list
-    pytest --nonloc
+    pytest {posargs:--nonloc}
 deps=
-    sphx_1_6:     sphinx<1.7
+    sphx_1_6_x:   sphinx<1.7
     sphx_1_x:     sphinx<2
     sphx_2_x:     sphinx<3
     sphx_3_x:     sphinx<4
+    sphx_2_3_1:   sphinx==2.3.1
+    sphx_2_4_0:   sphinx==2.4.0
+    sphx_3_2_1:   sphinx==3.2.1
+    sphx_3_3_0:   sphinx==3.3.0
     sphx_latest:  sphinx
     sphx_dev:     git+https://github.com/sphinx-doc/sphinx
 
@@ -52,6 +57,7 @@ deps=
     fzwz_latest:  fuzzywuzzy
     fzwz_dev:     git+https://github.com/seatgeek/fuzzywuzzy
 
+    dictdiffer
     pytest>=4.4.0
     pytest-check>=0.4
     pytest-ordering


Test patch:
diff --git a/conftest.py b/conftest.py
index fae3b7fc..028d33c0 100644
--- a/conftest.py
+++ b/conftest.py
@@ -34,10 +34,13 @@
 from enum import Enum
 from filecmp import cmp
 from functools import partial
+from io import BytesIO
 from pathlib import Path
 
 import jsonschema
 import pytest
+from sphinx import __version__ as sphinx_version_str
+from sphinx.util.inventory import InventoryFile as IFile
 
 import sphobjinv as soi
 
@@ -160,23 +163,64 @@ def bytes_txt(misc_info, res_path):
     )
 
 
+def sphinx_ifile_load(path):
+    """Carry out inventory load via Sphinx InventoryFile.
+
+    Defined as a standalone function to allow importing
+    during debugging.
+
+    """
+    return IFile.load(BytesIO(path.read_bytes()), "", osp.join)
+
+
+@pytest.fixture(scope="session", name="sphinx_ifile_load")
+def fixture_sphinx_ifile_load():
+    """Return helper function to load inventory via Sphinx InventoryFile."""
+    return sphinx_ifile_load
+
+
+def sphinx_ifile_data_count(ifile_data):
+    """Report the total number of items in the InventoryFile data.
+
+    Defined standalone to allow import during debugging.
+
+    """
+    return sum(len(ifile_data[k]) for k in ifile_data)
+
+
+@pytest.fixture(scope="session", name="sphinx_ifile_data_count")
+def fixture_sphinx_ifile_data_count():
+    """Return helper function to report total number of objects."""
+    return sphinx_ifile_data_count
+
+
 @pytest.fixture(scope="session")
-def sphinx_load_test():
+def sphinx_load_test(sphinx_ifile_load):
     """Return function to perform 'live' Sphinx inventory load test."""
-    from sphinx.util.inventory import InventoryFile as IFile
 
     def func(path):
         """Perform the 'live' inventory load test."""
-        with path.open("rb") as f:
-            try:
-                IFile.load(f, "", osp.join)
-            except Exception as e:  # noqa: PIE786
-                # An exception here is a failing test, not a test error.
-                pytest.fail(e)
+        try:
+            sphinx_ifile_load(path)
+        except Exception as e:  # noqa: PIE786
+            # An exception here is a failing test, not a test error.
+            pytest.fail(e)
 
     return func
 
 
+@pytest.fixture(scope="session")
+def sphinx_version():
+    """Provide the installed Sphinx version as a tuple.
+
+    Returns (major, minor, patch).
+
+    """
+    p_version = re.compile(r"(\d+)[.]?(\d+)?[.]?(\d+)?")
+    mch = p_version.match(sphinx_version_str)
+    return tuple(map((lambda x: int(x) if x else 0), mch.groups()))
+
+
 @pytest.fixture()  # Must be function scope since uses monkeypatch
 def run_cmdline_test(monkeypatch):
     """Return function to perform command line exit code test."""
@@ -241,14 +285,15 @@ def func(inv, source_type):
     return func
 
 
-testall_inv_paths = (
+testall_inv_paths = [
     p
     for p in (Path(__file__).parent / "tests" / "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]
 
 
-@pytest.fixture(params=testall_inv_paths)
+@pytest.fixture(params=testall_inv_paths, ids=testall_inv_ids)
 def testall_inv_path(request):
     """Provide parametrized --testall inventory paths."""
     return request.param
diff --git a/tests/resource/objects_mdn.inv b/tests/resource/objects_mdn.bad_inv
similarity index 100%
rename from tests/resource/objects_mdn.inv
rename to tests/resource/objects_mdn.bad_inv
diff --git a/tests/resource/objects_prio_test.inv b/tests/resource/objects_prio_test.inv
deleted file mode 100644
index 36007e55..00000000
Binary files a/tests/resource/objects_prio_test.inv and /dev/null differ
diff --git a/tests/test_api_good.py b/tests/test_api_good.py
index e150dd2d..0c194ebd 100644
--- a/tests/test_api_good.py
+++ b/tests/test_api_good.py
@@ -31,6 +31,7 @@
 import warnings
 from numbers import Number
 
+import dictdiffer
 import pytest
 
 import sphobjinv as soi
@@ -502,6 +503,62 @@ def test_api_inventory_datafile_gen_and_reimport(
         # Ensure sphinx likes the regenerated inventory
         sphinx_load_test(scr_fpath)
 
+    @pytest.mark.testall
+    def test_api_inventory_matches_sphinx_ifile(
+        self,
+        testall_inv_path,
+        scratch_path,
+        misc_info,
+        pytestconfig,
+        sphinx_ifile_load,
+        sphinx_ifile_data_count,
+        sphinx_version,
+    ):
+        """Confirm no-op per Sphinx on passing through sphobjinv.Inventory."""
+        fname = testall_inv_path.name
+        scr_fpath = scratch_path / fname
+
+        # Drop most unless testall
+        if not pytestconfig.getoption("--testall") and fname != "objects_attrs.inv":
+            pytest.skip("'--testall' not specified")
+
+        original_ifile_data = sphinx_ifile_load(testall_inv_path)
+
+        inv = soi.Inventory(testall_inv_path)
+        soi.writebytes(scr_fpath, soi.compress(inv.data_file()))
+        soi_ifile_data = sphinx_ifile_load(scr_fpath)
+
+        assert not list(dictdiffer.diff(soi_ifile_data, original_ifile_data)), fname
+
+        if "celery" in fname:  # pragma: no cover
+            # Celery inventory contains some exact domain:role:name duplicates
+            assert inv.count == 54 + sphinx_ifile_data_count(original_ifile_data), fname
+
+        elif "opencv" in fname:  # pragma: no cover
+            # OpenCV inventory contains some lines that
+            # parse incorrectly after sphinx/#8225, which was first
+            # incorporated into Sphinx 3.3.0
+            if sphinx_version < (3, 3, 0):
+                assert inv.count == sphinx_ifile_data_count(original_ifile_data), fname
+            else:
+                assert inv.count == 13 + sphinx_ifile_data_count(
+                    original_ifile_data
+                ), fname
+
+        elif "jsonschema" in fname:  # pragma: no cover
+            # The version of the jsonschema inventory held in tests/resource
+            # has an item with an empty uri. Sphinx<2.4 does not import this line
+            # correctly.
+            if sphinx_version < (2, 4, 0):
+                assert inv.count == 1 + sphinx_ifile_data_count(
+                    original_ifile_data
+                ), fname
+            else:
+                assert inv.count == sphinx_ifile_data_count(original_ifile_data), fname
+
+        else:
+            assert inv.count == sphinx_ifile_data_count(original_ifile_data), fname
+
     def test_api_inventory_one_object_flatdict(self):
         """Confirm a flat dict inventory with one object imports ok.
 
diff --git a/tests/test_valid_objects.py b/tests/test_valid_objects.py
new file mode 100644
index 00000000..c4702dd7
--- /dev/null
+++ b/tests/test_valid_objects.py
@@ -0,0 +1,177 @@
+r"""*Valid/invalid object data tests for* ``sphobjinv``.
+
+``sphobjinv`` is a toolkit for manipulation and inspection of
+Sphinx |objects.inv| files.
+
+**Author**
+    Brian Skinn (bskinn@alum.mit.edu)
+
+**File Created**
+    13 Feb 2021
+
+**Copyright**
+    \(c) Brian Skinn 2016-2021
+
+**Source Repository**
+    http://www.github.com/bskinn/sphobjinv
+
+**Documentation**
+    http://sphobjinv.readthedocs.io
+
+**License**
+    The MIT License; see |license_txt|_ for full license terms
+
+**Members**
+
+"""
+
+import os.path as osp
+import zlib
+from io import BytesIO
+
+import pytest
+from sphinx.util.inventory import InventoryFile as IFile
+
+import sphobjinv as soi
+
+
+@pytest.fixture(autouse=True)
+def skip_on_sphinx_version(sphinx_version):
+    """Trigger test skip if Sphinx version is too low.
+
+    Changes to the Sphinx InventoryFile regex &c. cause older
+    versions of Sphinx to have different behavior.
+
+    This skip *should* only trigger during the tox matrix of
+    environments with various old dependency versions.
+
+    """
+    if sphinx_version < (3, 3, 0):  # pragma: no cover
+        pytest.skip("Sphinx version too low")
+
+
+# Once DataObjStr instance validation is in place, this will probably
+# be a good place to use hypothesis
+@pytest.mark.parametrize(
+    ("name", "domain", "role", "prio", "uri", "dispname"),
+    [
+        ("foo", "py", "data", 1, "quux.html#$", "-"),  # Priorities
+        ("foo", "py", "data", 0, "quux.html#$", "-"),
+        ("foo", "py", "data", -1, "quux.html#$", "-"),
+        ("foo", "py", "data", -1235778, "quux.html#$", "-"),
+        ("foo", "py", "data", 2214888, "quux.html#$", "-"),
+        ("foo bar", "std", "term", 1, "quux.html#$", "-"),  # Space in name
+        ("foo\tbar", "std", "term", 1, "quux.html#$", "-"),  # Valid but discouraged
+        ("Index Page", "std", "doc", 1, "index.html", "-"),
+        ("Index Page", "std", "doc", 1, "index.html", "Index Page Thing"),
+        ("Index Page", "std", "doc", 1, "index.html", "Index\tPage\tThing"),
+        ("Index Page", "std", "doc", 1, "", "-"),  # Zero-length uri
+        ("Index # Page", "std", "doc", 1, "index.html", "-"),  # Symbol in name
+        ("Thing \u33a4", "std", "ref", 1, "index.html#$", "-"),  # Unicode in name
+        ("Thing One", "std", "ref", 1, "index.html#$", "\u33a4"),  # Unicode in dispname
+        ("foo", "py$", "data", 1, "data.html#$", "-"),  # Valid but discouraged
+        ("foo", "py\u33a4", "data", 1, "data.html#$", "-"),  # Valid but discouraged
+        ("foo", "py", "data$", 1, "data.html#$", "-"),  # Valid but discouraged
+        ("foo", "py", "data\u33a4", 1, "data.html#$", "-"),  # Valid but discouraged
+        ("foo", "py", "data", 1, "data/\u33a4.html#$", "-"),  # Valid but discouraged
+        ("  foo", "py", "data", 1, "data.html#$", "-"),  # Valid but discouraged
+    ],
+)
+def test_dataobjstr_valid_objects(
+    misc_info, sphinx_ifile_data_count, name, domain, role, prio, uri, dispname
+):
+    """Run sphobjinv/sphinx comparison on specific object data lines."""
+    dos = soi.DataObjStr(
+        name=name,
+        domain=domain,
+        role=role,
+        priority=str(prio),
+        uri=uri,
+        dispname=dispname,
+    )
+
+    assert dos
+
+    inv = soi.Inventory()
+    inv.project = "Foo"
+    inv.version = "1.0"
+    inv.objects.append(
+        soi.DataObjStr(
+            name="bar", domain="py", role="data", priority="1", uri="$", dispname="-"
+        )
+    )
+    inv.objects.append(dos)
+
+    df = inv.data_file(contract=True)
+
+    ifile_data = IFile.load(BytesIO(soi.compress(df)), "", osp.join)
+
+    ifile_count = sphinx_ifile_data_count(ifile_data)
+
+    assert inv.count == ifile_count
+
+    domrole = "{dos.domain}:{dos.role}".format(dos=dos)
+
+    assert domrole in ifile_data
+    assert dos.name in ifile_data[domrole]
+
+
+@pytest.mark.parametrize(
+    ("name", "domain", "role", "prio", "uri", "dispname"),
+    [
+        ("", "std", "doc", 1, "index.html", "-"),  # Missing name
+        ("foo  ", "py", "data", 1, "data.html#$", "-"),  # Name w/trailing space
+        ("# Index Page", "std", "doc", 1, "index.html", "-"),  # '#' @ name start
+        ("X Y Z 0 foo", "std", "doc", 1, "index.html", "-"),  # Int in name
+        ("foo", "p:y", "data", 1, "data.html#$", "-"),  # Colon in domain
+        ("foo", "py thon", "data", 1, "data.html#$", "-"),  # Space in domain
+        ("foo", "", "data", 1, "data.html#$", "-"),  # Missing domain
+        ("foo", "py", "da:ta", 1, "data.html#$", "-"),  # Colon in role
+        ("foo", "py", "da ta", 1, "data.html#$", "-"),  # Space in role
+        ("foo", "py", "", 1, "data.html#$", "-"),  # Missing role
+        ("foo", "py", "data", 0.5, "data.html#$", "-"),  # Non-integer prio
+        ("foo", "py", "data", "", "data.html#$", "-"),  # Missing prio
+        ("foo", "py", "data", "quux", "data.html#$", "-"),  # Non-numeric prio
+        ("Index Page", "std", "doc", 1, "index.html", ""),  # Missing dispname
+        ("Index Page", "std", "doc", 1, "", ""),  # Missing uri & dispname
+    ],
+)
+def test_dataobjstr_invalid_objects(
+    misc_info, sphinx_ifile_data_count, name, domain, role, prio, uri, dispname
+):
+    """Run sphobjinv/sphinx comparison on specific invalid data lines."""
+    with pytest.raises((AssertionError, zlib.error)):
+        test_dataobjstr_valid_objects(
+            misc_info, sphinx_ifile_data_count, name, domain, role, prio, uri, dispname
+        )
+
+
+def int_to_latin_1(val):
+    """Provide the latin-1 string equivalent of an 8-bit int."""
+    return bytes((val,)).decode("latin-1")
+
+
+def latin_1_id(val):
+    """Provide the value-and-character string for a latin-1 int value."""
+    return str(val) + "_" + int_to_latin_1(val)
+
+
+@pytest.mark.parametrize("leadint", range(255), ids=latin_1_id)
+def test_name_lead_chars(misc_info, sphinx_ifile_data_count, leadint):
+    """Screen for valid/invalid first characters."""
+    name = int_to_latin_1(leadint) + " foo"
+
+    # Expect only two fail cases, newline and '#'
+    if leadint in (10, 35):
+        pytest.xfail("Known invalid name lead char")
+
+    test_dataobjstr_valid_objects(
+        misc_info,
+        sphinx_ifile_data_count,
+        name=name,
+        domain="py",
+        role="data",
+        prio=1,
+        uri="data.html#$",
+        dispname="-",
+    )