Skip to content

Commit

Permalink
Merge pull request #262 from shunichironomura/improve-docs-index
Browse files Browse the repository at this point in the history
Improve documentation
  • Loading branch information
shunichironomura authored Jul 7, 2024
2 parents 1ff6b1c + 4c8c7f5 commit bd8211f
Show file tree
Hide file tree
Showing 27 changed files with 835 additions and 418 deletions.
57 changes: 43 additions & 14 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ on:
workflow_dispatch:

env:
PYTHON_VERSION: "3.11"
PYTHON_VERSION: "3.12"
POETRY_VERSION: "1.8.3"

jobs:
create-tag:
Expand All @@ -21,10 +22,10 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install poetry
- name: Set up Poetry
uses: abatilo/actions-poetry@v3
with:
poetry-version: ${{ env.POETRY_VERSION }}

- name: Get the version
id: get-version
Expand Down Expand Up @@ -52,10 +53,10 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install poetry
- name: Set up Poetry
uses: abatilo/actions-poetry@v3
with:
poetry-version: ${{ env.POETRY_VERSION }}

- name: Publish to PyPI
run: |
Expand All @@ -78,13 +79,41 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install poetry
- name: Create a GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create v${{ needs.create-tag.outputs.version }} --verify-tag --generate-notes --title "v${{ needs.create-tag.outputs.version }}" --repo ${{ github.repository }} --target ${{ github.sha }}
deploy-docs:
name: Deploy docs
runs-on: ubuntu-latest
needs: create-tag
permissions:
contents: write

steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Set up Poetry
uses: abatilo/actions-poetry@v3
with:
poetry-version: ${{ env.POETRY_VERSION }}

- name: Install dependencies
run: poetry install --no-interaction

- name: Set up Git
run: |
git config --global user.name "github-actions"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git fetch --prune --unshallow
- name: Deploy docs
run: |
git fetch --tags
git checkout v${{ needs.create-tag.outputs.version }}
poetry run --no-interaction -- mike deploy --push --update-aliases v${{ needs.create-tag.outputs.version }} latest
25 changes: 14 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,25 @@
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/capsula)
![Test Status](https://github.com/shunichironomura/capsula/workflows/Test/badge.svg?event=push&branch=main)
[![codecov](https://codecov.io/gh/shunichironomura/capsula/graph/badge.svg?token=BZXF2PPDM0)](https://codecov.io/gh/shunichironomura/capsula)
[![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
![PyPI - Downloads](https://img.shields.io/pypi/dm/capsula)

*Capsula*, a Latin word meaning *box*, is a Python package designed to help researchers and developers easily capture their command/function execution context for reproducibility.
See the [documentation](https://shunichironomura.github.io/capsula/) for more information.

With Capsula, you can capture:

- CPU information with [`CpuContext`](contexts/cpu.md)
- Python version with [`PlatformContext`](contexts/platform.md)
- Current working directory with [`CwdContext`](contexts/cwd.md)
- Git repository information (commit hash, branch, etc.) with [`GitRepositoryContext`](contexts/git.md)
- Output of shell commands (e.g., `poetry check --lock`) with [`CommandContext`](contexts/command.md)
- Files (e.g., output files, `pyproject.toml`, `requirements.txt`) with [`FileContext`](contexts/file.md)
- Arguments of Python functions with [`FunctionContext`](contexts/function.md)
- Environment variables with [`EnvVarContext`](contexts/envvar.md)
- Uncaught exceptions with [`UncaughtExceptionWatcher`](watchers/uncaught_exception.md)
- Execution time with [`TimeWatcher`](watchers/time.md)
- CPU information with [`CpuContext`](docs/contexts/cpu.md)
- Python version with [`PlatformContext`](docs/contexts/platform.md)
- Current working directory with [`CwdContext`](docs/contexts/cwd.md)
- Git repository information (commit hash, branch, etc.) with [`GitRepositoryContext`](docs/contexts/git.md)
- Output of shell commands (e.g., `poetry check --lock`) with [`CommandContext`](docs/contexts/command.md)
- Files (e.g., output files, `pyproject.toml`, `requirements.txt`) with [`FileContext`](docs/contexts/file.md)
- Arguments of Python functions with [`FunctionContext`](docs/contexts/function.md)
- Environment variables with [`EnvVarContext`](docs/contexts/envvar.md)
- Uncaught exceptions with [`UncaughtExceptionWatcher`](docs/watchers/uncaught_exception.md)
- Execution time with [`TimeWatcher`](docs/watchers/time.md)

The captured contexts are dumped into JSON files for future reference and reproduction.

Expand Down Expand Up @@ -83,7 +86,7 @@ if __name__ == "__main__":
calculate_pi(n_samples=1_000)
```

After running the script, a directory (`calculate_pi_20240630_015823_S3vb` in this example) will be created under the `vault` directory, and you will find the following files there:
After running the script, a directory (`calculate_pi_20240630_015823_S3vb` in this example) will be created under the `<project-root>/vault` directory, and you will find the following files there:

<details>
<summary>Example of output <code>pre-run-report.json</code>:</summary>
Expand Down
2 changes: 2 additions & 0 deletions capsula.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
contexts = [
{ type = "CwdContext" },
{ type = "CpuContext" },
{ type = "PlatformContext" },
{ type = "GitRepositoryContext", name = "capsula", path = ".", path_relative_to_project_root = true },
{ type = "CommandContext", command = "poetry check --lock", cwd = ".", cwd_relative_to_project_root = true },
{ type = "FileContext", path = "pyproject.toml", copy = true, path_relative_to_project_root = true },
{ type = "FileContext", path = "poetry.lock", copy = true, path_relative_to_project_root = true },
{ type = "CommandContext", command = "pip freeze --exclude-editable > requirements.txt", cwd = ".", cwd_relative_to_project_root = true },
{ type = "FileContext", path = "requirements.txt", move = true, path_relative_to_project_root = true },
{ type = "EnvVarContext", name = "HOME" },
]
reporters = [{ type = "JsonDumpReporter" }]

Expand Down
3 changes: 2 additions & 1 deletion capsula/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"CapsulaConfigurationError",
"CapsulaError",
"Capsule",
"CapsuleParams",
"CommandContext",
"ContextBase",
"CpuContext",
Expand Down Expand Up @@ -43,7 +44,7 @@
from ._exceptions import CapsulaConfigurationError, CapsulaError
from ._reporter import JsonDumpReporter, ReporterBase
from ._root import current_run_name, record
from ._run import Run
from ._run import CapsuleParams, Run
from ._utils import search_for_project_root
from ._version import __version__
from ._watcher import TimeWatcher, UncaughtExceptionWatcher, WatcherBase
4 changes: 2 additions & 2 deletions capsula/_capsule.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ def default_key(self) -> str | tuple[str, ...]:

@classmethod
def builder(cls, *args: Any, **kwargs: Any) -> Callable[[CapsuleParams], Self]:
def callback(params: CapsuleParams) -> Self: # type: ignore[type-var,misc] # noqa: ARG001
def build(params: CapsuleParams) -> Self: # type: ignore[type-var,misc] # noqa: ARG001
return cls(*args, **kwargs)

return callback
return build

@classmethod
@deprecated("Use builder instead")
Expand Down
4 changes: 2 additions & 2 deletions capsula/_context/_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def builder(
),
] = False,
) -> Callable[[CapsuleParams], CommandContext]:
def callback(params: CapsuleParams) -> CommandContext:
def build(params: CapsuleParams) -> CommandContext:
if cwd_relative_to_project_root and cwd is not None and not Path(cwd).is_absolute():
cwd_path: Path | None = params.project_root / cwd
elif cwd_relative_to_project_root and cwd is None:
Expand All @@ -72,7 +72,7 @@ def callback(params: CapsuleParams) -> CommandContext:
abort_on_error=abort_on_error,
)

return callback
return build

def __init__(
self,
Expand Down
11 changes: 7 additions & 4 deletions capsula/_context/_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,18 @@ def builder(
compute_hash: Annotated[bool, Doc("Whether to compute the hash of the file")] = True,
hash_algorithm: Annotated[
str | None,
Doc("Hash algorithm to use. This will be fed to `hashlib.file_digest` as the `digest` argument."),
Doc(
"Hash algorithm to use. This will be fed to `hashlib.file_digest` as the `digest` argument. "
"If not provided, `sha256` will be used.",
),
] = None,
copy: Annotated[bool, Doc("Whether to copy the file to the run directory")] = False,
move: Annotated[bool, Doc("Whether to move the file to the run directory")] = False,
ignore_missing: Annotated[bool, Doc("Whether to ignore if the file does not exist")] = False,
path_relative_to_project_root: Annotated[
bool,
Doc(
"Whether `path` is relative to the project root. Will be ignored if `path` is absolute."
"Whether `path` is relative to the project root. Will be ignored if `path` is absolute. "
"If True, it will be interpreted as relative to the project root. "
"If False, `path` will be interpreted as relative to the current working directory. "
"It is recommended to set this to True in the configuration file.",
Expand All @@ -57,7 +60,7 @@ def builder(
move = True
copy = False

def callback(params: CapsuleParams) -> FileContext:
def build(params: CapsuleParams) -> FileContext:
if path_relative_to_project_root and path is not None and not Path(path).is_absolute():
file_path = params.project_root / path
else:
Expand All @@ -72,7 +75,7 @@ def callback(params: CapsuleParams) -> FileContext:
ignore_missing=ignore_missing,
)

return callback
return build

def __init__(
self,
Expand Down
17 changes: 13 additions & 4 deletions capsula/_context/_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,18 @@ class GitRepositoryContext(ContextBase):
@classmethod
def builder(
cls,
name: Annotated[str | None, Doc("Name of the Git repository")] = None,
name: Annotated[
str | None,
Doc("Name of the Git repository. If not provided, the name of the working directory will be used."),
] = None,
*,
path: Annotated[Path | str | None, Doc("Path to the Git repository")] = None,
path: Annotated[
Path | str | None,
Doc(
"Path to the Git repository. If not provided, the parent directories of the file where the function is "
"defined will be searched for a Git repository.",
),
] = None,
path_relative_to_project_root: Annotated[
bool,
Doc(
Expand All @@ -56,7 +65,7 @@ def builder(
] = False,
allow_dirty: Annotated[bool, Doc("Whether to allow the repository to be dirty")] = True,
) -> Callable[[CapsuleParams], GitRepositoryContext]:
def callback(params: CapsuleParams) -> GitRepositoryContext:
def build(params: CapsuleParams) -> GitRepositoryContext:
if path_relative_to_project_root and path is not None and not Path(path).is_absolute():
repository_path: Path | None = params.project_root / path
else:
Expand Down Expand Up @@ -84,7 +93,7 @@ def callback(params: CapsuleParams) -> GitRepositoryContext:
allow_dirty=allow_dirty,
)

return callback
return build

def __init__(
self,
Expand Down
4 changes: 2 additions & 2 deletions capsula/_reporter/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ def report(self, capsule: Capsule) -> None:

@classmethod
def builder(cls, *args: Any, **kwargs: Any) -> Callable[[CapsuleParams], Self]:
def callback(params: CapsuleParams) -> Self: # type: ignore[type-var,misc] # noqa: ARG001
def build(params: CapsuleParams) -> Self: # type: ignore[type-var,misc] # noqa: ARG001
return cls(*args, **kwargs)

return callback
return build

@classmethod
@deprecated("Use builder instead")
Expand Down
4 changes: 2 additions & 2 deletions capsula/_reporter/_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ def builder(
Doc("Option to pass to `orjson.dumps`. If not provided, `orjson.OPT_INDENT_2` will be used."),
] = None,
) -> Callable[[CapsuleParams], JsonDumpReporter]:
def callback(params: CapsuleParams) -> JsonDumpReporter:
def build(params: CapsuleParams) -> JsonDumpReporter:
return cls(
params.run_dir / f"{params.phase}-run-report.json",
option=orjson.OPT_INDENT_2 if option is None else option,
)

return callback
return build

def __init__(
self,
Expand Down
6 changes: 6 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,9 @@ For each encapsulators, the order of encapsulation is as follows:

1. Contexts, watchers, and reporters specified in the `capsula.toml` file, in the order of appearance (from top to bottom).
2. Contexts, watchers, and reporters specified using the `@capsula.context()` and `@capsula.watcher()` decorators, in the order of appearance (from top to bottom).

## `builder` method or `__init__` method?

The reason for using the `builder` method instead of the `__init__` method to create an instance of a context, watcher, or reporter is to use the runtime information, such as the run directory, to create the instance. This is why the configuration specified in the `capsula.toml` file by default uses the `builder` method to create instances of contexts, watchers, and reporters.

The `builder` method returns, instead of an instance of the class, a function that takes the runtime information ([`capsula.CapsuleParams`](reference/capsula/index.md#capsula.CapsuleParams)) as an argument and returns an instance of the class.
42 changes: 41 additions & 1 deletion docs/contexts/command.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,43 @@
# `CommandContext`

::: capsula.CommandContext
The [`CommandContext`](../reference/capsula/index.md#capsula.CommandContext) captures the output of shell commands.
It can be created using the `capsula.CommandContext.builder` method or the `capsula.CommandContext.__init__` method.

::: capsula.CommandContext.builder
::: capsula.CommandContext.__init__

## Configuration example

### Via `capsula.toml`

```toml
[pre-run]
contexts = [
{ type = "CommandContext", command = "poetry check --lock", cwd = ".", cwd_relative_to_project_root = true },
]
```

### Via `@capsula.context` decorator

```python
import capsula
PROJECT_ROOT = capsula.search_for_project_root(__file__)

@capsula.run()
@capsula.context(capsula.CommandContext("poetry check --lock", cwd=PROJECT_ROOT), mode="pre")
def func(): ...
```

## Output example

The following is an example of the output of the `CommandContext`, reported by the [`JsonDumpReporter`](../reporters/json_dump.md):

```json
"poetry check --lock": {
"command": "poetry check --lock",
"cwd": "/home/nomura/ghq/github.com/shunichironomura/capsula",
"returncode": 0,
"stdout": "All set!\n",
"stderr": ""
}
```
Loading

0 comments on commit bd8211f

Please sign in to comment.