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

Generate API documentation from docstrings #343

Merged
merged 3 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
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
17 changes: 17 additions & 0 deletions .github/workflows/trigger-packit-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Rebuild docs on merge

on:
push:
branches:
- main

jobs:
trigger:
runs-on: ubuntu-latest
steps:
- name: Trigger packit.dev
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.PACKIT_DEV_TOKEN }}
repository: packit/packit.dev
event-type: specfile-docs-updated
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ check-in-container:
--env COLOR \
--env COV_REPORT \
$(TEST_IMAGE) make check

generate-api-docs:
PYTHONPATH=$(CURDIR)/docs/api pydoc-markdown --verbose docs/api/specfile.yml
28 changes: 28 additions & 0 deletions docs/api/processors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import dataclasses
import re
from typing import List, Optional

import docspec
from pydoc_markdown.interfaces import Processor, Resolver


@dataclasses.dataclass
class EscapeBracketsProcessor(Processor):
"""
Processor that escapes curly brackets in Python template placeholders
and RPM macros as they have special meaning in MDX files.
"""

def process(
self, modules: List[docspec.Module], resolver: Optional[Resolver]
) -> None:
docspec.visit(modules, self._process)

def _process(self, obj: docspec.ApiObject) -> None:
if not obj.docstring:
return
obj.docstring.content = re.sub(
r"(%|\$)\{(.+?)\}",
r"\g<1>\{\g<2>\}",
obj.docstring.content,
)
32 changes: 32 additions & 0 deletions docs/api/specfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
loaders:
- type: python
modules:
- specfile.specfile
- specfile.changelog
- specfile.conditions
- specfile.context_management
- specfile.exceptions
- specfile.formatter
- specfile.macro_definitions
- specfile.macros
- specfile.options
- specfile.prep
- specfile.sections
- specfile.sourcelist
- specfile.sources
- specfile.spec_parser
- specfile.tags
- specfile.utils
- specfile.value_parser
processors:
- type: filter
- type: google
- type: crossref
- type: processors.EscapeBracketsProcessor
renderer:
type: docusaurus
docs_base_path: docs
relative_output_path: api
markdown:
descriptive_class_title: false
render_typehint_in_data_header: true
18 changes: 18 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# specfile

specfile is a pure-Python library for parsing and manipulating RPM spec files.
Main focus is on modifying existing spec files, any change should result in a minimal diff.

## Installation

The library is packaged for Fedora, EPEL 9 and EPEL 8 and you can simply instal it with dnf:

```bash
dnf install python3-specfile
```

On other systems, you can use pip (just note that it requires RPM Python bindings to be installed):

```bash
pip install specfile
```
50 changes: 25 additions & 25 deletions specfile/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@

class ChangelogEntry:
"""
Class that represents a changelog entry.
Class that represents a changelog entry. Changelog entry consists of
a header line starting with _*_, followed by timestamp, author and optional
extra text (usually EVR), and one or more content lines.

Attributes:
header: Header of the entry.
Expand All @@ -53,15 +55,12 @@ def __init__(
following_lines: Optional[List[str]] = None,
) -> None:
"""
Constructs a `ChangelogEntry` object.
Initializes a changelog entry object.

Args:
header: Header of the entry.
content: List of lines forming the content of the entry.
following_lines: Extra lines that follow the entry.

Returns:
Constructed instance of `ChangelogEntry` class.
"""
self.header = header
self.content = content.copy()
Expand All @@ -87,7 +86,7 @@ def __repr__(self) -> str:

@property
def evr(self) -> Optional[str]:
"""EVR (Epoch, Version, Release) of the entry."""
"""EVR (epoch, version, release) of the entry."""
m = re.match(
r"""
^.*
Expand Down Expand Up @@ -170,15 +169,15 @@ def assemble(

Args:
timestamp: Timestamp of the entry.
Supply `datetime` rather than `date` for extended format.
Supply `datetime` rather than `date` for extended format.
author: Author of the entry.
content: List of lines forming the content of the entry.
evr: EVR (epoch, version, release) of the entry.
day_of_month_padding: Padding to apply to day of month in the timestamp.
append_newline: Whether the entry should be followed by an empty line.

Returns:
Constructed instance of `ChangelogEntry` class.
New instance of `ChangelogEntry` class.
"""
weekday = WEEKDAYS[timestamp.weekday()]
month = MONTHS[timestamp.month - 1]
Expand All @@ -200,7 +199,9 @@ def assemble(

class Changelog(UserList[ChangelogEntry]):
"""
Class that represents a changelog.
Class that represents a changelog. It behaves like a list of changelog entries,
ordered from bottom to top - the top (newest) entry has index _-1_, the bottom
(oldest) one has index _0_.

Attributes:
data: List of individual entries.
Expand All @@ -212,15 +213,12 @@ def __init__(
predecessor: Optional[List[str]] = None,
) -> None:
"""
Constructs a `Changelog` object.
Initializes a changelog object.

Args:
data: List of individual changelog entries.
predecessor: Lines at the beginning of a section that can't be parsed
into changelog entries.

Returns:
Constructed instance of `Changelog` class.
predecessor: List of lines at the beginning of a section
that can't be parsed into changelog entries.
"""
super().__init__()
if data is not None:
Expand Down Expand Up @@ -274,9 +272,9 @@ def filter(

Args:
since: Optional lower bound. If specified, entries with EVR higher
than or equal to this will be included.
than or equal to this will be included.
until: Optional upper bound. If specified, entries with EVR lower
than or equal to this will be included.
than or equal to this will be included.

Returns:
Filtered changelog.
Expand Down Expand Up @@ -316,13 +314,13 @@ def parse_evr(s):
@classmethod
def parse(cls, section: Section) -> "Changelog":
"""
Parses a %changelog section.
Parses a `%changelog` section.

Args:
section: Section to parse.

Returns:
Constructed instance of `Changelog` class.
New instance of `Changelog` class.
"""

def extract_following_lines(content: List[str]) -> List[str]:
Expand Down Expand Up @@ -378,13 +376,15 @@ def _getent_name() -> str:

def guess_packager() -> str:
"""
Guess the name and email of a packager to use for changelog entries.
This uses similar logic to rpmdev-packager.
Guesses the name and e-mail of a packager to use for changelog entries.
This function uses logic similar to `rpmdev-packager` utility.

The following places are searched for this value (in this order):
- $RPM_PACKAGER envvar
- %packager macro
- git config
- Unix username

- `$RPM_PACKAGER` environment variable
- `%packager` macro
- git config
- Unix username

Returns:
A string to use for the changelog entry author.
Expand Down
6 changes: 3 additions & 3 deletions specfile/conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ def resolve_expression(
Resolves a RPM expression.

Args:
keyword: Condition keyword, e.g. `%if` or `%ifarch`.
keyword: Condition keyword, e.g. _%if_ or _%ifarch_.
expression: Expression string or a whitespace-delimited list
of arches/OSes in case keyword is a variant of `%ifarch`/`%ifos`.
of arches/OSes in case keyword is a variant of _%ifarch_/_%ifos_.
context: `Specfile` instance that defines the context for macro expansions.

Returns:
Expand Down Expand Up @@ -72,7 +72,7 @@ def process_conditions(
Args:
lines: List of lines in a spec file.
macro_definitions: Parsed macro definitions to be used to prevent parsing conditions
inside their bodies (and most likely failing).
inside their bodies (and most likely failing).
context: `Specfile` instance that defines the context for macro expansions.

Returns:
Expand Down
13 changes: 7 additions & 6 deletions specfile/context_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
@contextlib.contextmanager
def capture_stderr() -> Generator[List[bytes], None, None]:
"""
Context manager for capturing output to stderr. A stderr output of anything run
in its context will be captured in the target variable of the with statement.
Context manager for capturing output to _stderr_. A _stderr_ output
of anything run in its context will be captured in the target variable
of the __with__ statement.

Yields:
List of captured lines.
Expand All @@ -39,7 +40,7 @@ def capture_stderr() -> Generator[List[bytes], None, None]:

class GeneratorContextManager(contextlib._GeneratorContextManager):
"""
Extended contextlib._GeneratorContextManager that provides content property.
Extended `contextlib._GeneratorContextManager` that provides `content` property.
"""

def __init__(self, function: Callable) -> None:
Expand All @@ -56,10 +57,10 @@ def content(self) -> Any:
Fully consumes the underlying generator and returns the yielded value.

Returns:
Value that would normally be the target variable of an associated with statement.
Value that would normally be the target variable of an associated __with__ statement.

Raises:
StopIteration if the underlying generator is already exhausted.
StopIteration: If the underlying generator is already exhausted.
"""
result = next(self.gen)
next(self.gen, None)
Expand All @@ -70,7 +71,7 @@ class ContextManager:
"""
Class for decorating generator functions that should act as a context manager.

Just like with contextlib.contextmanager, the generator returned from the decorated function
Just like with `contextlib.contextmanager`, the generator returned from the decorated function
must yield exactly one value that will be used as the target variable of the with statement.
If the same function with the same arguments is called again from within previously generated
context, the generator will be ignored and the target variable will be reused.
Expand Down
14 changes: 7 additions & 7 deletions specfile/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@


class SpecfileException(Exception):
"""Something went wrong during our execution."""
"""Base class for all library exceptions."""


class RPMException(SpecfileException):
"""Exception related to RPM."""
"""RPM exception."""

def __init__(self, stderr: List[bytes]) -> None:
super().__init__()
Expand All @@ -24,20 +24,20 @@ def __str__(self) -> str:


class MacroRemovalException(SpecfileException):
"""Exception related to failed removal of RPM macros."""
"""Impossible to remove a RPM macro."""


class OptionsException(SpecfileException):
"""Exception related to processing options."""
"""Unparseable option string."""


class UnterminatedMacroException(SpecfileException):
"""Exception related to parsing unterminated macro."""
"""Macro starts but doesn't end."""


class DuplicateSourceException(SpecfileException):
"""Exception related to adding a duplicate source."""
"""Source with the same location already exists."""


class SourceNumberException(SpecfileException):
"""Exception related to source numbers."""
"""Incorrect numbering of sources."""
10 changes: 5 additions & 5 deletions specfile/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@ def format_expression(expression: str, line_length_threshold: int = 80) -> str:
"""
Formats the specified Python expression.

Only supports a small subset of Python AST that should be sufficient for use in __repr__().
Only supports a small subset of Python AST that should be sufficient for use in `__repr__()`.

Args:
expression: Python expression to reformat.
line_length_threshold: Threshold for line lengths. It's not a hard limit,
it can be exceeded in some cases.
it can be exceeded in some cases.

Returns:
Formatted expression.

Raises:
SyntaxError if the expression is not parseable.
SpecfileException if there is an unsupported AST node in the expression.
SyntaxError: If the expression is not parseable.
SpecfileException: If there is an unsupported AST node in the expression.
"""

def fmt(node, indent=0, prefix="", multiline=False):
Expand Down Expand Up @@ -127,7 +127,7 @@ def find_matching_bracket(value, index):


def formatted(function: Callable[..., str]) -> Callable[..., str]:
"""Decorator for formatting the output of __repr__()."""
"""Decorator for formatting the output of `__repr__()`."""

@functools.wraps(function)
def wrapper(*args, **kwargs):
Expand Down
Loading