Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement linksmith inventory and linksmith output-formats #4

Merged
merged 1 commit into from
Apr 1, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.inv binary
3 changes: 2 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
@@ -3,4 +3,5 @@
## Unreleased

## v0.0.0 - 2024-xx-xx

- Implement `linksmith inventory` and `linksmith output-formats`
subcommands, based on `sphobjinv` and others. Thanks, @bskinn.
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -38,17 +38,19 @@ pip install 'linksmith @ git+https://github.com/tech-writing/linksmith.git'


## Usage
Nothing works yet. All just sketched out.

sphobjinv call delegation ftw.
```shell
linksmith inventory https://linksmith.readthedocs.io/en/latest/objects.inv
```
# Shorthand command ...
anansi suggest matplotlib draw

# ... for:
sphobjinv suggest -u https://matplotlib.org/stable/ draw
```
Read more at the [Linksmith Usage] documentation.

The `linksmith inventory` subsystem is heavily based on
`sphinx.ext.intersphinx` and `sphobjinv`.

> [!WARNING]
> Here be dragons. Please note the program is pre-alpha, and a work in
> progress, so everything may change while we go.

## Development
@@ -89,7 +91,7 @@ please let us know._

## Acknowledgements

Kudos to [Sviatoslav Sydorenko], [Brian Skinn], [Chris Sewell], and all other
Kudos to [Brian Skinn], [Sviatoslav Sydorenko], [Chris Sewell], and all other
lovely people around Sphinx and Read the Docs.


@@ -103,6 +105,7 @@ lovely people around Sphinx and Read the Docs.
[Hyperlinks]: https://en.wikipedia.org/wiki/Hyperlink
[linksmith]: https://linksmith.readthedocs.io/
[`linksmith`]: https://pypi.org/project/linksmith/
[Linksmith Usage]: https://linksmith.readthedocs.io/en/latest/usage.html
[rfc]: https://linksmith.readthedocs.io/en/latest/rfc.html
[Sphinx]: https://www.sphinx-doc.org/
[sphobjinv]: https://sphobjinv.readthedocs.io/
19 changes: 19 additions & 0 deletions docs/backlog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Backlog

## Iteration +1
- Docs: Based on sphobjinv.
- Response caching to buffer subsequent invocations
- Add output flavor, like `--details=compact,full`.
**Full details**, well, should display **full URLs**, ready for
navigational consumption (clicking).
- Improve HTML output. (sticky breadcrumb/navbar, etc.)

## Iteration +2
sphobjinv call delegation ftw.
```
# Shorthand command ...
anansi suggest matplotlib draw
# ... for:
sphobjinv suggest -u https://matplotlib.org/stable/ draw
```
32 changes: 29 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@

A program for processing Hyperlinks, Sphinx references, and inventories.

:::::{grid} 1 3 3 3
::::::{grid} 1 3 3 3
:margin: 4 4 0 0
:padding: 0
:gutter: 2
@@ -26,15 +26,41 @@ Just the proposal, nothing more.
- [](#rfc-community-operations)
::::

:::::
::::{grid-item}
:::{card} Setup
:margin: 0 2 0 0
:link: setup
:link-type: ref
`pip install ...`
:::
:::{card} Usage
:margin: 0 2 0 0
:link: usage
:link-type: ref
`linksmith inventory ...`
:::
::::

::::::


:::{toctree}
:caption: Handbook
:hidden:

rfc
sandbox
setup
usage
:::


:::{toctree}
:caption: Workbench
:hidden:

project
sandbox
backlog
:::


12 changes: 12 additions & 0 deletions docs/setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
(setup)=
# Setup

Up until published on PyPI, please install the package that way. Thank you.

```bash
pip install 'linksmith @ git+https://github.com/tech-writing/linksmith.git'
```

:::{note}
This command will need an installation of Git on your system.
:::
48 changes: 48 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
(usage)=
# Usage

Linksmith provides the `linksmith` command line program. It harbours
different subsystems, accessible by using corresponding subcommands,
like `linksmith inventory`.

:::{warning}
Here be dragons. Please note the program is pre-alpha, and a work in
progress, so everything may change while we go.
:::


## Output Formats
Display all the available output formats at a glance.
```shell
linksmith output-formats
```


## Sphinx Inventories
The `linksmith inventory` subsystem supports working with Sphinx inventories,
it is heavily based on `sphinx.ext.intersphinx` and `sphobjinv`.

:::{rubric} Single Inventory
:::
Refer to `objects.inv` on the local filesystem or on a remote location.
```shell
linksmith inventory /path/to/objects.inv
```
```shell
linksmith inventory https://linksmith.readthedocs.io/en/latest/objects.inv
```

```shell
linksmith inventory \
https://linksmith.readthedocs.io/en/latest/objects.inv \
--format=markdown+table
```

:::{rubric} Multiple Inventories
:::
Refer to multiple `objects.inv` resources.
```shell
linksmith inventory \
https://github.com/crate/crate-docs/raw/main/registry/sphinx-inventories.txt \
--format=html+table
```
33 changes: 33 additions & 0 deletions linksmith/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import json

import rich_click as click
from pueblo.util.cli import boot_click

from linksmith.settings import help_config

from .model import OutputFormatRegistry
from .sphinx.cli import cli as inventory_cli


@click.group()
@click.rich_config(help_config=help_config)
@click.option("--verbose", is_flag=True, required=False, help="Turn on logging")
@click.option("--debug", is_flag=True, required=False, help="Turn on logging with debug level")
@click.version_option()
@click.pass_context
def cli(ctx: click.Context, verbose: bool, debug: bool):
return boot_click(ctx, verbose, debug)


@click.command()
@click.rich_config(help_config=help_config)
@click.pass_context
def output_formats(ctx: click.Context): # noqa: ARG001
"""
Display available output format aliases.
"""
print(json.dumps(sorted(OutputFormatRegistry.aliases()), indent=2))


cli.add_command(output_formats, name="output-formats")
cli.add_command(inventory_cli, name="inventory")
70 changes: 70 additions & 0 deletions linksmith/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import dataclasses
import io
import typing as t
from enum import auto
from pathlib import Path

from linksmith.util.python import AutoStrEnum


class OutputFormat(AutoStrEnum):
TEXT_INSPECT = auto()
TEXT_PLAIN = auto()
MARKDOWN = auto()
MARKDOWN_TABLE = auto()
RESTRUCTUREDTEXT = auto()
HTML = auto()
HTML_TABLE = auto()
JSON = auto()
YAML = auto()


@dataclasses.dataclass
class OutputFormatRule:
format: OutputFormat
aliases: t.List[str]


class OutputFormatRegistry:
rules = [
OutputFormatRule(format=OutputFormat.TEXT_INSPECT, aliases=["text"]),
OutputFormatRule(format=OutputFormat.TEXT_PLAIN, aliases=["text+plain"]),
OutputFormatRule(format=OutputFormat.MARKDOWN, aliases=["markdown", "md"]),
OutputFormatRule(format=OutputFormat.MARKDOWN_TABLE, aliases=["markdown+table", "md+table"]),
OutputFormatRule(format=OutputFormat.RESTRUCTUREDTEXT, aliases=["restructuredtext", "rst"]),
OutputFormatRule(format=OutputFormat.HTML, aliases=["html", "html+table"]),
OutputFormatRule(format=OutputFormat.JSON, aliases=["json"]),
OutputFormatRule(format=OutputFormat.YAML, aliases=["yaml"]),
]

@classmethod
def resolve(cls, format_: str) -> OutputFormat:
for rule in cls.rules:
if format_ in rule.aliases:
return rule.format
raise NotImplementedError(f"Output format not implemented: {format_}")

@classmethod
def aliases(cls) -> t.List[str]:
data = []
for rule in cls.rules:
data += rule.aliases
return data


class ResourceType(AutoStrEnum):
BUFFER = auto()
PATH = auto()
URL = auto()

@classmethod
def detect(cls, location):
if isinstance(location, io.IOBase):
return cls.BUFFER
path = Path(location)
if path.exists():
return cls.PATH
elif location.startswith("http://") or location.startswith("https://"):
return cls.URL
else:
raise NotImplementedError(f"Resource type not implemented: {location}")
10 changes: 10 additions & 0 deletions linksmith/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import rich_click as click

help_config = click.RichHelpConfiguration(
use_markdown=True,
width=100,
style_option="bold white",
style_argument="dim cyan",
style_command="bold yellow",
style_errors_suggestion_command="bold magenta",
)
Empty file added linksmith/sphinx/__init__.py
Empty file.
47 changes: 47 additions & 0 deletions linksmith/sphinx/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import typing as t

import rich_click as click
from click import ClickException

from linksmith.settings import help_config
from linksmith.sphinx.core import inventories_to_text, inventory_to_text


@click.command()
@click.rich_config(help_config=help_config)
@click.argument("infiles", nargs=-1)
@click.option("--format", "format_", type=str, default="text", help="Output format")
@click.pass_context
def cli(ctx: click.Context, infiles: t.List[str], format_: str):
"""
Decode one or multiple intersphinx inventories and output in different formats.
Use `linksmith output-formats` to learn about available output formats.
Examples:
Refer to `objects.inv` on the local filesystem or on a remote location:
```bash
linksmith inventory /path/to/objects.inv --format=html
linksmith inventory https://linksmith.readthedocs.io/en/latest/objects.inv --format=markdown
```
Refer to **multiple** `objects.inv` resources:
```bash
linksmith inventory https://github.com/crate/crate-docs/raw/main/registry/sphinx-inventories.txt
```
"""
if not infiles:
raise click.ClickException("No input")
for infile in infiles:
try:
if infile.endswith(".inv"):
inventory_to_text(infile, format_=format_)
elif infile.endswith(".txt"):
inventories_to_text(infile, format_=format_)
else:
raise NotImplementedError(f"Unknown input file type: {infile}")
except Exception as ex:
if ctx.parent and ctx.parent.params.get("debug"):
raise
raise ClickException(f"{ex.__class__.__name__}: {ex}")
75 changes: 75 additions & 0 deletions linksmith/sphinx/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# ruff: noqa: T201 `print` found
import io
import logging
import typing as t
from pathlib import Path

import requests

from linksmith.model import OutputFormat, OutputFormatRegistry, ResourceType
from linksmith.sphinx.inventory import InventoryFormatter

logger = logging.getLogger(__name__)


def inventory_to_text(url: str, format_: str = "text"):
"""
Display intersphinx inventory for individual project, using selected output format.
"""
of = OutputFormatRegistry.resolve(format_)
inventory = InventoryFormatter(url=url)

if of is OutputFormat.TEXT_INSPECT:
inventory.to_text_inspect()
elif of is OutputFormat.TEXT_PLAIN:
inventory.to_text_plain()
elif of is OutputFormat.RESTRUCTUREDTEXT:
inventory.to_restructuredtext()
elif of in [OutputFormat.MARKDOWN, OutputFormat.MARKDOWN_TABLE]:
inventory.to_markdown(format_)
elif of is OutputFormat.HTML:
inventory.to_html(format_)
elif of is OutputFormat.JSON:
inventory.to_json()
elif of is OutputFormat.YAML:
inventory.to_yaml()


def inventories_to_text(urls: t.Union[str, Path, io.IOBase], format_: str = "text"):
"""
Display intersphinx inventories of multiple projects, using selected output format.
"""
if format_.startswith("html"):
print("<!DOCTYPE html>")
print("<html>")
print(
"""
<style>
html, body, table {
font-size: small;
}
</style>
""",
)
print("<body>")
resource_type = ResourceType.detect(urls)
if resource_type is ResourceType.BUFFER:
url_list = t.cast(io.IOBase, urls).read().splitlines()
elif resource_type is ResourceType.PATH:
url_list = Path(t.cast(str, urls)).read_text().splitlines()
# TODO: Test coverage needs to be unlocked by `test_multiple_inventories_url`
elif resource_type is ResourceType.URL: # pragma: nocover
url_list = requests.get(t.cast(str, urls), timeout=10).text.splitlines()

# Generate header.
if format_.startswith("html"):
print("<h1>Inventory Overview</h1>")
print(f"<p>Source: {urls}</p>")
for url in url_list:
inventory = InventoryFormatter(url=url)
name = inventory.name
print(f"""- <a href="#{name}">{name}</a><br/>""")

# Generate content.
for url in url_list:
inventory_to_text(url, format_)
161 changes: 161 additions & 0 deletions linksmith/sphinx/inventory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
"""
Format content of Sphinx inventories.
Source:
- https://github.com/crate/crate-docs/blob/5a7b02f/tasks.py
- https://github.com/pyveci/pueblo/blob/878a31f94/pueblo/sphinx/inventory.py
"""

import dataclasses
import io
import logging
import typing as t
from contextlib import redirect_stdout

import sphobjinv as soi
import tabulate
import yaml
from marko.ext.gfm import gfm as markdown_to_html
from sphinx.application import Sphinx
from sphinx.ext.intersphinx import fetch_inventory, inspect_main
from sphinx.util.typing import InventoryItem

from linksmith.model import ResourceType

logger = logging.getLogger(__name__)


@dataclasses.dataclass
class InventoryRecord:
"""
Manage details of a single record of a Sphinx inventory.
"""

type: str
name: str
project: str
version: str
url_path: str
display_name: str


InventoryEntries = t.List[t.Tuple[str, InventoryItem]]


class InventoryManager:
def __init__(self, location: str):
self.location = location

def soi_factory(self) -> soi.Inventory:
resource_type = ResourceType.detect(self.location)
if resource_type is ResourceType.PATH:
return soi.Inventory(source=self.location)
elif resource_type is ResourceType.URL:
return soi.Inventory(url=self.location)
else: # pragma: nocover
raise TypeError(f"Unknown inventory type: {self.location}")


class InventoryFormatter:
"""
Decode and process intersphinx inventories created by Sphinx.
https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html
"""

def __init__(self, url: str, labels_only: bool = False, omit_documents: bool = False):
self.url = url
self.labels_only = labels_only
self.omit_documents = omit_documents

self.invman = InventoryManager(location=self.url)
self.soi = self.invman.soi_factory()
self.name = self.soi.project

def to_text_inspect(self):
inspect_main([self.url])

def to_text_plain(self):
print(self.soi.data_file().decode("utf-8"))

def to_restructuredtext(self):
line = len(self.name) * "#"
print(line)
print(self.name)
print(line)
print("\n".join(sorted(self.soi.objects_rst)))

# ruff: noqa: T201 `print` found
def to_markdown(self, format_: str = ""):
class MockConfig:
intersphinx_timeout: t.Union[int, None] = None
tls_verify = False
tls_cacerts: t.Union[str, t.Dict[str, str], None] = None
user_agent: str = ""

class MockApp:
srcdir = ""
config = MockConfig()

app = t.cast(Sphinx, MockApp())
inv_data = fetch_inventory(app, "", self.url)
print(f"# {self.name}")
print()
for key in sorted(inv_data or {}):
if self.labels_only and key != "std:label":
continue
if self.omit_documents and key == "std:doc":
continue
print(f"## {key}")
inv_entries = sorted(inv_data[key].items())
if format_.endswith("+table"):
print(tabulate.tabulate(inv_entries, headers=("Reference", "Inventory Record (raw)"), tablefmt="pipe"))
else:
print("```text")
records = self.decode_entries(key, inv_entries)
for line in self.format_records(records):
print(line)
print("```")
print()

def to_html(self, format_: str = ""):
"""
Format intersphinx repository using HTML.
TODO: Reference implementation by @webknjaz.
https://webknjaz.github.io/intersphinx-untangled/setuptools.rtfd.io/
"""
print(f"""<a id="{self.name}"></a>""")
buffer = io.StringIO()
with redirect_stdout(buffer):
self.to_markdown(format_)
buffer.seek(0)
markdown = buffer.read()
html = markdown_to_html(markdown)
print(html)

def to_json(self):
print(self.soi.json_dict())

def to_yaml(self):
logger.warning("There is certainly a better way to present an inventory in YAML format")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check out bskinn/sphobjinv#283 -- @machow proposed a better JSON output format that might(?) make for more attractive YAML.

I need to actually implement that...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much. I knew you would have excellent suggestions about it. 💯

I merged this PR early, to get something working into main before going afk, but that does not mean I will not consider any other suggestions here, also in retrospective. Thanks!

print(yaml.dump(self.soi.json_dict()))

def decode_entries(
self,
reference_type: str,
inv_entries: InventoryEntries,
) -> t.Generator[InventoryRecord, None, None]:
"""
Decode inv_entries, as per `fetch_inventory`.
item: (_proj, _ver, url_path, display_name)
"""
for name, entry in inv_entries:
yield InventoryRecord(reference_type, name, *entry)

def format_records(self, records: t.Iterable[InventoryRecord]) -> t.Generator[str, None, None]:
yield (f"{'Reference': <40} {'Display Name': <40} {'Path'}")
yield (f"{'---------': <40} {'------------': <40} {'----'}")
for record in records:
display_name_effective = record.display_name * (record.display_name != "-")
yield (f"{record.name: <40} {display_name_effective: <40} {record.url_path}")
Empty file added linksmith/util/__init__.py
Empty file.
24 changes: 24 additions & 0 deletions linksmith/util/python.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from enum import Enum


class AutoStrEnum(str, Enum):
"""
StrEnum where enum.auto() returns the field name.
See https://docs.python.org/3.9/library/enum.html#using-automatic-values
From https://stackoverflow.com/a/74539097.
"""

@staticmethod
def _generate_next_value_(name: str, start: int, count: int, last_values: list) -> str: # noqa: ARG004
return name


class AutoStrEnumLCase(str, Enum): # pragma: nocover
"""
From https://stackoverflow.com/a/74539097.
"""

@staticmethod
def _generate_next_value_(name, start, count, last_values): # noqa: ARG004
return name.lower()
16 changes: 13 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -78,9 +78,15 @@ dynamic = [
"version",
]
dependencies = [
"marko<3",
"myst-parser[linkify]<3,>=0.18",
"pueblo[cli]==0.0.9",
"pyyaml<7",
"requests<3",
"rich-click<2",
"sphinx<7.3",
"sphobjinv<2.4",
"tabulate<0.10",
]
[project.optional-dependencies]
develop = [
@@ -113,6 +119,8 @@ changelog = "https://github.com/tech-writing/linksmith/blob/main/CHANGES.md"
documentation = "https://linksmith.readthedocs.io/"
homepage = "https://linksmith.readthedocs.io/"
repository = "https://github.com/tech-writing/linksmith"
[project.scripts]
linksmith = "linksmith.cli:cli"

[tool.black]
line-length = 120
@@ -132,7 +140,8 @@ show_missing = true
packages = ["linksmith"]
exclude = [
]
check_untyped_defs = true
ignore_missing_imports = true
check_untyped_defs = false
implicit_optional = true
install_types = true
no_implicit_optional = true
@@ -202,8 +211,9 @@ lint.extend-ignore = [


[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"] # Allow use of `assert`, and `print`.
"docs/conf.py" = ["ERA001"] # Allow commented-out code (ERA001).
"tests/*" = ["S101"] # Allow use of `assert`.
"docs/conf.py" = ["ERA001"] # Allow commented-out code.
"linksmith/cli.py" = ["T201"] # Allow `print`.

[tool.setuptools.packages.find]
namespaces = false
Empty file added tests/__init__.py
Empty file.
2 changes: 2 additions & 0 deletions tests/assets/index.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
tests/assets/linksmith.inv
tests/assets/sde.inv
7 changes: 7 additions & 0 deletions tests/assets/linksmith.inv
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Sphinx inventory version 2
# Project: Linksmith
# Version:
# The remainder of this file is compressed using zlib.
xڝ��N� ��}
�b����Mܴ�O@��Ba�o�hk�7������p��D�@;�;�$Y��G�3��|蜒w��9��X*�M� �s� ���IN����؎PF�
}�p]���䙁N�h8 �R����˳��*����V1'PY�/���u��w��6�v�.�A�^v35gO�u�?HڢR^ 7P4`�+�M�n�I9�!E1{���)zg�� 8$����^/���4W���fZ{c�F?6�$.���rF�|���}[ݶ�ni̶���ث������r�.
Binary file added tests/assets/sde.inv
Binary file not shown.
2 changes: 2 additions & 0 deletions tests/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
OBJECTS_INV_URL = "https://linksmith.readthedocs.io/en/latest/objects.inv"
OBJECTS_INV_PATH = "tests/assets/linksmith.inv"
7 changes: 7 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import pytest
from click.testing import CliRunner


@pytest.fixture
def cli_runner() -> CliRunner:
return CliRunner()
103 changes: 103 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import pytest

from linksmith.cli import cli
from tests.config import OBJECTS_INV_PATH, OBJECTS_INV_URL


def test_cli_version(cli_runner):
"""
CLI test: Invoke `linksmith --version`.
"""
result = cli_runner.invoke(
cli,
args="--version",
catch_exceptions=False,
)
assert result.exit_code == 0


def test_cli_output_formats(cli_runner):
"""
CLI test: Invoke `linksmith output-formats`.
"""
result = cli_runner.invoke(
cli,
args="output-formats",
catch_exceptions=False,
)
assert result.exit_code == 0


def test_cli_inventory_no_input(cli_runner):
"""
CLI test: Invoke `linksmith inventory`.
"""
result = cli_runner.invoke(
cli,
args="inventory",
catch_exceptions=False,
)
assert result.exit_code == 1
assert "No input" in result.output


def test_cli_inventory_unknown_input(cli_runner):
"""
CLI test: Invoke `linksmith inventory example.foo`.
"""
result = cli_runner.invoke(
cli,
args="inventory example.foo",
catch_exceptions=False,
)
assert result.exit_code == 1
assert "Unknown input file type: example.foo" in result.output


def test_cli_inventory_unknown_input_with_debug(cli_runner):
"""
CLI test: Invoke `linksmith inventory example.foo`.
"""
with pytest.raises(NotImplementedError) as ex:
cli_runner.invoke(
cli,
args="--debug inventory example.foo",
catch_exceptions=False,
)
assert ex.match("Unknown input file type: example.foo")


def test_cli_single_inventory_path(cli_runner):
"""
CLI test: Invoke `linksmith inventory tests/assets/linksmith.inv --format=text`.
"""
result = cli_runner.invoke(
cli,
args=f"inventory {OBJECTS_INV_PATH} --format=text",
catch_exceptions=False,
)
assert result.exit_code == 0


def test_cli_single_inventory_url(cli_runner):
"""
CLI test: Invoke `linksmith inventory https://linksmith.readthedocs.io/en/latest/objects.inv --format=text`.
"""
result = cli_runner.invoke(
cli,
args=f"inventory {OBJECTS_INV_URL} --format=text",
catch_exceptions=False,
)
assert result.exit_code == 0


def test_cli_multiple_inventories_path(cli_runner):
"""
CLI test: Invoke `linksmith inventory tests/assets/index.txt --format=text`.
"""
result = cli_runner.invoke(
cli,
args="inventory tests/assets/index.txt --format=text",
catch_exceptions=False,
)
assert result.exit_code == 0
57 changes: 57 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import io

import pytest

from linksmith.model import OutputFormatRegistry
from linksmith.sphinx.core import inventories_to_text, inventory_to_text
from tests.config import OBJECTS_INV_PATH, OBJECTS_INV_URL


@pytest.mark.parametrize("format_", OutputFormatRegistry.aliases())
def test_single_inventory_path(format_: str):
inventory_to_text(OBJECTS_INV_PATH, format_)


@pytest.mark.parametrize("format_", OutputFormatRegistry.aliases())
def test_single_inventory_url(format_: str):
inventory_to_text(OBJECTS_INV_URL, format_)


@pytest.mark.parametrize("format_", OutputFormatRegistry.aliases())
def test_multiple_inventories_path(format_: str):
inventories_to_text("tests/assets/index.txt", format_)


@pytest.mark.parametrize("format_", OutputFormatRegistry.aliases())
def test_multiple_inventories_buffer(format_: str):
urls = io.StringIO(
"""
tests/assets/linksmith.inv
tests/assets/sde.inv
""".strip(),
)
inventories_to_text(urls, format_)


@pytest.mark.skip("Does not work yet")
def test_multiple_inventories_url():
url = "https://github.com/tech-writing/linksmith/raw/main/tests/assets/index.txt"
inventories_to_text(url, "html")


def test_unknown_output_format():
with pytest.raises(NotImplementedError) as ex:
inventory_to_text(OBJECTS_INV_PATH, "foo-format")
ex.match("Output format not implemented: foo-format")


def test_unknown_input_format_single():
with pytest.raises(NotImplementedError) as ex:
inventory_to_text("foo.bar", "text")
ex.match("Resource type not implemented: foo.bar")


def test_unknown_input_format_multiple():
with pytest.raises(NotImplementedError) as ex:
inventories_to_text("foo.bar", "text")
ex.match("Resource type not implemented: foo.bar")
27 changes: 27 additions & 0 deletions tests/test_inventory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import pytest

from linksmith.sphinx.inventory import InventoryFormatter, InventoryManager
from tests.config import OBJECTS_INV_PATH


def test_inventory_labels_only(capsys):
inventory = InventoryFormatter(url=OBJECTS_INV_PATH, labels_only=True)
inventory.to_markdown()
out, err = capsys.readouterr()
assert "std:label" in out
assert "std:doc" not in out


def test_inventory_omit_documents(capsys):
inventory = InventoryFormatter(url=OBJECTS_INV_PATH, omit_documents=True)
inventory.to_markdown()
out, err = capsys.readouterr()
assert "std:label" in out
assert "std:doc" not in out


def test_inventory_manager_unknown():
invman = InventoryManager("foo")
with pytest.raises(NotImplementedError) as ex:
invman.soi_factory()
assert ex.match("Resource type not implemented: foo")
34 changes: 34 additions & 0 deletions tests/test_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import io

import pytest

from linksmith.model import OutputFormat, OutputFormatRegistry, ResourceType


def test_output_format_success():
assert OutputFormatRegistry.resolve("text") is OutputFormat.TEXT_INSPECT


def test_output_format_unknown():
with pytest.raises(NotImplementedError) as ex:
OutputFormatRegistry.resolve("foo-format")
assert ex.match("Output format not implemented: foo-format")


def test_resource_type_path():
assert ResourceType.detect("README.md") is ResourceType.PATH


def test_resource_type_url():
assert ResourceType.detect("http://example.org") is ResourceType.URL


def test_resource_type_buffer():
buffer = io.StringIO("http://example.org")
assert ResourceType.detect(buffer) is ResourceType.BUFFER


def test_resource_type_unknown():
with pytest.raises(NotImplementedError) as ex:
ResourceType.detect("foobar")
assert ex.match("Resource type not implemented: foobar")