From ca49debee66fba92cbcd5ab6ca0abafbc04ed6b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez=20Mondrag=C3=B3n?= <16805946+edgarrmondragon@users.noreply.github.com> Date: Wed, 24 Jan 2024 01:54:16 -0600 Subject: [PATCH] docs: Publish docs (#23) --- .flake8 | 3 + .github/workflows/documentation-links.yaml | 16 ++++ .github/workflows/test.yaml | 7 +- .readthedocs.yaml | 25 ++++++ README.md | 1 + docs/conf.py | 74 +++++++++++++++++ docs/index.md | 83 +++++++++++++++++++ pyproject.toml | 24 ++++-- src/pep610/__init__.py | 92 ++++++++++++++++------ tests/test_generic.py | 4 +- tests/test_parse.py | 18 ++--- 11 files changed, 304 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/documentation-links.yaml create mode 100644 .readthedocs.yaml create mode 100644 docs/conf.py create mode 100644 docs/index.md diff --git a/.flake8 b/.flake8 index e62457c..98ac601 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,7 @@ [flake8] +ignore = + ; Parameter type mismatch + DAR103 select = DAR docstring_style=google max-line-length = 88 diff --git a/.github/workflows/documentation-links.yaml b/.github/workflows/documentation-links.yaml new file mode 100644 index 0000000..702360f --- /dev/null +++ b/.github/workflows/documentation-links.yaml @@ -0,0 +1,16 @@ +name: readthedocs/actions +on: + pull_request_target: + types: + - opened + +permissions: + pull-requests: write + +jobs: + documentation-links: + runs-on: ubuntu-latest + steps: + - uses: readthedocs/actions/preview@v1 + with: + project-slug: "pep610" diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9126420..8c5d860 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -100,7 +100,6 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 - id: setup-python with: cache: pip python-version: ${{ matrix.python-version }} @@ -109,10 +108,12 @@ jobs: env: PIP_CONSTRAINT: .github/workflows/constraints.txt run: | - pipx install --python=${{ steps.setup-python.outputs.python-path }} hatch + pipx install hatch - name: Run tests + env: + HATCH_ENV: all run: | - hatch run cov + hatch run +py=${{ matrix.python-version }} cov - uses: actions/upload-artifact@v4 with: name: coverage-data-${{ matrix.os }}-${{ matrix.python-version }} diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..1bd401d --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,25 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + jobs: + post_checkout: + - git fetch --unshallow || true + +sphinx: + builder: html + configuration: docs/conf.py + fail_on_warning: true + +formats: + - pdf + - epub + +python: + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/README.md b/README.md index 8a222ca..e2ddd72 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![PyPI - Version](https://img.shields.io/pypi/v/pep610.svg)](https://pypi.org/project/pep610) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pep610.svg)](https://pypi.org/project/pep610) [![codecov](https://codecov.io/gh/edgarrmondragon/pep610/graph/badge.svg?token=6W1M6P9LYI)](https://codecov.io/gh/edgarrmondragon/pep610) +[![Documentation Status](https://readthedocs.org/projects/pep610/badge/?version=latest)](https://pep610.readthedocs.io/en/latest/?badge=latest) [PEP 610][pep610] specifies how the Direct URL Origin of installed distributions should be recorded. diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..8cfd320 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,74 @@ +"""Sphinx configuration.""" + +from __future__ import annotations + +import pep610 + +# Add any Sphinx extension module names here, as strings. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.extlinks", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "myst_parser", + "sphinx_design", +] + +# General information about the project. +project = "pep610" +author = "Edgar Ramírez-Mondragón" +version = pep610.__version__ +release = pep610.__version__ +project_copyright = f"2023, {author}" + +# -- Options for HTML output -------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. + +html_theme = "furo" +html_title = "PEP 610 - Direct URL Parser and Builder for Python" +html_theme_options = { + "navigation_with_keys": True, + "source_repository": "https://github.com/edgarrmondragon/citric/", + "source_branch": "main", + "source_directory": "docs/", +} + +# -- Options for autodoc ---------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#configuration + +autodoc_member_order = "bysource" +autodoc_preserve_defaults = True + +# Automatically extract typehints when specified and place them in +# descriptions of the relevant function/method. +autodoc_typehints = "description" + +# Only document types for parameters or return values that are already documented by the +# docstring. +autodoc_typehints_description_target = "documented" + +# -- Options for extlinks ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html + +extlinks_detect_hardcoded_links = True +extlinks = { + "spec": ( + "https://packaging.python.org/en/latest/specifications/direct-url-data-structure/#%s-urls", + "specification for %s URLs", + ), +} + +# -- Options for intersphinx ---------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#configuration +intersphinx_mapping = { + "metadata": ("https://importlib-metadata.readthedocs.io/en/latest", None), + "python": ("https://docs.python.org/3/", None), +} + +# -- Options for Myst Parser ------------------------------------------------------- +# https://myst-parser.readthedocs.io/en/latest/configuration.html +myst_enable_extensions = ["colon_fence"] diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..d634b0e --- /dev/null +++ b/docs/index.md @@ -0,0 +1,83 @@ +# PEP 610 Parser and Builder + +*A parser and builder for [PEP 610 direct URL metadata](https://packaging.python.org/en/latest/specifications/direct-url-data-structure).* + +Release **v{sub-ref}`version`**. + +::::{tab-set} + +:::{tab-item} Python 3.10+ + +```python +from importlib import metadata + +import pep610 + +dist = metadata.distribution("pep610") +data = pep610.read_from_distribution(dist) + +match data: + case pep610.DirData(url, pep610.DirInfo(editable=True)): + print("Editable install") + case _: + print("Not editable install") +``` + +::: + +:::{tab-item} Python 3.9+ +```python +from importlib import metadata + +import pep610 + +dist = metadata.distribution("pep610") +data = pep610.read_from_distribution(dist) + +if isinstance(data, pep610.DirData) and data.dir_info.is_editable(): + print("Editable install") +else: + print("Not editable install") +``` +::: +:::: + +## Supported formats + +```{eval-rst} +.. autoclass:: pep610.ArchiveData + :members: +``` + +```{eval-rst} +.. autoclass:: pep610.DirData + :members: +``` + +```{eval-rst} +.. autoclass:: pep610.VCSData + :members: +``` + +## Other classes + +```{eval-rst} +.. autoclass:: pep610.ArchiveInfo + :members: +``` + +```{eval-rst} +.. autoclass:: pep610.DirInfo + :members: +``` + +```{eval-rst} +.. autoclass:: pep610.VCSInfo + :members: +``` + +## Functions + +```{eval-rst} +.. autofunction:: pep610.read_from_distribution +``` diff --git a/pyproject.toml b/pyproject.toml index e0353d7..5b42559 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,12 @@ optional-dependencies.dev = [ "hypothesis-jsonschema", "pytest", ] +optional-dependencies.docs = [ + "furo==2023.9.10", + "myst-parser==2", + "sphinx==7.2.6", + "sphinx_design==0.5", +] urls.Documentation = "https://github.com/unknown/pep610#readme" urls.Issues = "https://github.com/unknown/pep610/issues" urls.Source = "https://github.com/unknown/pep610" @@ -92,6 +98,11 @@ style = [ fmt = ["ruff check --fix {args:.}", "ruff format {args:.}", "style"] all = ["style", "typing"] +[tool.hatch.envs.docs] +features = ["docs"] +[tool.hatch.envs.docs.scripts] +build = "sphinx-build -W -b html docs docs/_build" + [tool.ruff] line-length = 100 preview = true @@ -168,11 +179,14 @@ ban-relative-imports = "all" [tool.ruff.lint.per-file-ignores] "tests/**/*" = [ "PLR2004", # magic-value-comparison - "S101", # assert - "TID252", # relative-imports - "D100", # undocumented-public-module - "D104", # undocumented-public-package - "ANN201", # missing-return-type-undocumented-public-function + "S101", # assert + "TID252", # relative-imports + "D100", # undocumented-public-module + "D104", # undocumented-public-package + "ANN201", # missing-return-type-undocumented-public-function +] +"docs/conf.py" = [ + "INP001", # Not an implicit namespace packages ] [tool.ruff.lint.pydocstyle] diff --git a/src/pep610/__init__.py b/src/pep610/__init__.py index 26ef061..9078387 100644 --- a/src/pep610/__init__.py +++ b/src/pep610/__init__.py @@ -51,7 +51,16 @@ @dataclass class VCSInfo: - """VCS information.""" + """VCS information. + + See also :spec:`vcs`. + + Args: + vcs: The VCS type. + commit_id: The exact commit/revision number that was/is to be installed. + requested_revision: A branch/tag/ref/commit/revision/etc (in a format + compatible with the VCS). + """ vcs: str commit_id: str @@ -62,20 +71,34 @@ class VCSInfo: @dataclass class _BaseData: - """Base direct URL data.""" + """Base direct URL data. + + Args: + url: The direct URL. + """ url: str @dataclass class VCSData(_BaseData): - """VCS direct URL data.""" + """VCS direct URL data. + + Args: + url: The VCS URL. + vcs_info: VCS information. + """ vcs_info: VCSInfo class HashData(t.NamedTuple): - """Archive hash data.""" + """(Deprecated) Archive hash data. + + Args: + algorithm: The hash algorithm. + value: The hash value. + """ algorithm: str value: str @@ -83,42 +106,63 @@ class HashData(t.NamedTuple): @dataclass class ArchiveInfo: - """Archive information.""" + """Archive information. - hashes: dict[str, str] | None = None - """Dictionary mapping a hash name to a hex encoded digest of the file.""" + See also :spec:`archive`. + Args: + hashes: Dictionary mapping a hash name to a hex encoded digest of the file. + hash: The archive hash (deprecated). + """ + + hashes: dict[str, str] | None = None hash: HashData | None = None - """The archive hash (deprecated).""" @dataclass class ArchiveData(_BaseData): - """Archive direct URL data.""" + """Archive direct URL data. + + Args: + url: The archive URL. + archive_info: Archive information. + """ archive_info: ArchiveInfo @dataclass class DirInfo: - """Local directory information.""" + """Local directory information. + + See also :spec:`directory`. - _editable: bool | None + Args: + editable: Whether the distribution is installed in editable mode. + """ + + editable: bool | None + + def is_editable(self: Self) -> bool: + """Distribution is editable? - @property - def editable(self: Self) -> bool | None: - """Whether the directory is editable.""" - return self._editable is True + ``True`` if the distribution was/is to be installed in editable mode, + ``False`` otherwise. If absent, default to ``False`` - @editable.setter - def editable(self: Self, value: bool | None) -> None: - """Set whether the directory is editable.""" - self._editable = value + Returns: + Whether the distribution is installed in editable mode. + """ + return self.editable is True @dataclass class DirData(_BaseData): - """Local directory direct URL data.""" + """Local directory direct URL data. + + Args: + url: The local directory URL. + dir_info: Local directory information. + """ dir_info: DirInfo @@ -168,8 +212,8 @@ def _(data: ArchiveData) -> ArchiveDict: @to_dict.register(DirData) def _(data: DirData) -> DirectoryDict: dir_info: DirectoryInfoDict = {} - if data.dir_info._editable is not None: # noqa: SLF001 - dir_info["editable"] = data.dir_info._editable # noqa: SLF001 + if data.dir_info.editable is not None: + dir_info["editable"] = data.dir_info.editable return {"url": data.url, "dir_info": dir_info} @@ -191,7 +235,7 @@ def _parse(content: str) -> VCSData | ArchiveData | DirData | None: return DirData( url=data["url"], dir_info=DirInfo( - _editable=data["dir_info"].get("editable"), + editable=data["dir_info"].get("editable"), ), ) @@ -214,7 +258,7 @@ def read_from_distribution(dist: Distribution) -> VCSData | ArchiveData | DirDat """Read the package data for a given package. Args: - dist: The package distribution. + dist(importlib_metadata.Distribution): The package distribution. Returns: The parsed PEP 610 file. diff --git a/tests/test_generic.py b/tests/test_generic.py index 708a168..36f5969 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -1,5 +1,5 @@ import json -from importlib.metadata import PathDistribution +from importlib.metadata import Distribution import pytest from hypothesis import HealthCheck, given, settings @@ -15,6 +15,6 @@ def test_generic(tmp_path_factory: pytest.TempPathFactory, value: dict): """Test parsing a local directory.""" dist_path = tmp_path_factory.mktemp("pep610") - dist = PathDistribution(dist_path) + dist = Distribution.at(dist_path) write_to_distribution(dist, value) assert read_from_distribution(dist) is not None diff --git a/tests/test_parse.py b/tests/test_parse.py index ed46491..982a52e 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -3,7 +3,7 @@ from __future__ import annotations import typing as t -from importlib.metadata import PathDistribution +from importlib.metadata import Distribution import pytest @@ -31,7 +31,7 @@ {"url": "file:///home/user/project", "dir_info": {"editable": True}}, DirData( url="file:///home/user/project", - dir_info=DirInfo(_editable=True), + dir_info=DirInfo(editable=True), ), id="local_editable", ), @@ -39,7 +39,7 @@ {"url": "file:///home/user/project", "dir_info": {"editable": False}}, DirData( url="file:///home/user/project", - dir_info=DirInfo(_editable=False), + dir_info=DirInfo(editable=False), ), id="local_not_editable", ), @@ -47,7 +47,7 @@ {"url": "file:///home/user/project", "dir_info": {}}, DirData( url="file:///home/user/project", - dir_info=DirInfo(_editable=None), + dir_info=DirInfo(editable=None), ), id="local_no_editable_info", ), @@ -191,7 +191,7 @@ ) def test_parse(data: dict, expected: object, tmp_path: Path): """Test the parse function.""" - dist = PathDistribution(tmp_path) + dist = Distribution.at(tmp_path) write_to_distribution(dist, data) result = read_from_distribution(dist) @@ -213,13 +213,13 @@ def test_local_directory(tmp_path: Path): "url": "file:///home/user/project", "dir_info": {"editable": True}, } - dist = PathDistribution(tmp_path) + dist = Distribution.at(tmp_path) write_to_distribution(dist, data) result = read_from_distribution(dist) assert isinstance(result, DirData) assert result.url == "file:///home/user/project" - assert result.dir_info.editable is True + assert result.dir_info.is_editable() assert to_dict(result) == data result.dir_info.editable = False @@ -241,12 +241,12 @@ def test_unknown_url_type(tmp_path: Path): "url": "unknown:///home/user/project", "unknown_info": {}, } - dist = PathDistribution(tmp_path) + dist = Distribution.at(tmp_path) write_to_distribution(dist, data) assert read_from_distribution(dist) is None def test_no_file(tmp_path: Path): """Test that a missing file is read back as None.""" - dist = PathDistribution(tmp_path) + dist = Distribution.at(tmp_path) assert read_from_distribution(dist) is None