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 a new capsula.run decorator #148

Merged
merged 51 commits into from
Jan 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
dcbaff2
Rename capsule.py to _capsule.py and update imports
shunichironomura Jan 21, 2024
18b0832
Add default method to GitRepositoryContext class
shunichironomura Jan 21, 2024
2915399
Add Reporter to __all__ in capsula/reporter/__init__.py
shunichironomura Jan 21, 2024
94be51d
Update json reporter to handle exceptions and tracebacks
shunichironomura Jan 21, 2024
4e2f052
Refactor normalize_copy_dst_path to its own method
shunichironomura Jan 21, 2024
c2567e8
Update import path for Capsule in encapsulator.py
shunichironomura Jan 21, 2024
80aad6d
Add capsule decorator for encapsulation logic
shunichironomura Jan 21, 2024
3bd0130
Add decorator example for pi calculation
shunichironomura Jan 21, 2024
8e814fa
Fix for Python 3.8 compatibility
shunichironomura Jan 21, 2024
b8ac607
Add context_manager example in capsula module
shunichironomura Jan 27, 2024
0e315a2
Refine type annotations in capsule decorator
shunichironomura Jan 27, 2024
a09d03f
Add record function to capsula module
shunichironomura Jan 27, 2024
589071f
Update calculate_pi method and import Encapsulator
shunichironomura Jan 27, 2024
3a7eedb
Add thread-local context stack to Encapsulator class
shunichironomura Jan 27, 2024
8feb39f
Implement record method in _record.py
shunichironomura Jan 27, 2024
fb66a70
Refactor Encapsulator instantiation in context_manager
shunichironomura Jan 27, 2024
53411c2
Add mkdir option to JsonDumpReporter constructor
shunichironomura Jan 27, 2024
adcc1e6
Refactor context_manager.py for cleaner code
shunichironomura Jan 27, 2024
400d7a6
Rename _get_encapsulator_context_stack to _get_context_stack
shunichironomura Jan 28, 2024
79bbb30
Update context manager in capsula.run usage
shunichironomura Jan 28, 2024
7c3f027
Update context manager in capsula run method
shunichironomura Jan 28, 2024
e0bebad
Rename _record.py to _root.py in capsula module
shunichironomura Jan 28, 2024
bbacbc9
Update import statement in capsula/__init__.py
shunichironomura Jan 28, 2024
1ba2eae
Refactor decorators and add TimeWatcher in decorator.py
shunichironomura Jan 28, 2024
938d3e4
Rename submodule context to _context
shunichironomura Jan 28, 2024
de4aa7a
Rename submodule reporter to _reporter
shunichironomura Jan 28, 2024
f4ec390
Add new context classes and reporter to __init__.py
shunichironomura Jan 28, 2024
a743bc0
Refactor: Move watcher and reporter modules to private scope
shunichironomura Jan 28, 2024
3eb2f02
Add Watcher, TimeWatcher, UncaughtExceptionWatcher to __init__.py
shunichironomura Jan 28, 2024
372a373
Refactor code to use fully qualified names
shunichironomura Jan 28, 2024
82103d5
Refactor Run to Encapsulator in context_manager
shunichironomura Jan 28, 2024
8eb8d7f
Rename context_manager.py to enc_context_manager.py
shunichironomura Jan 28, 2024
a44492e
Add watcher, reporter and context decorators
shunichironomura Jan 28, 2024
43f9704
Add new decorators to capsula __init__.py
shunichironomura Jan 28, 2024
39b2ed5
Refactor GitRepositoryContext to use CapsuleParams
shunichironomura Jan 28, 2024
78e3f18
Rename classes to end with 'Base' for clarity
shunichironomura Jan 28, 2024
6cb1214
Add run.py with Run class and encapsulation methods
shunichironomura Jan 28, 2024
3cdb88d
Refactor decorators to support Run objects
shunichironomura Jan 28, 2024
6e57893
Add 'run' to decorators in capsula init file
shunichironomura Jan 28, 2024
78c0e0f
Update decorator and calculate_pi function call
shunichironomura Jan 28, 2024
ead9424
Add directory creation and phase updates in Run class
shunichironomura Jan 28, 2024
0bdcd72
Add FileContext decorator to calculate_pi function
shunichironomura Jan 28, 2024
ad8e0a5
Update context_manager_stack.get to non-blocking
shunichironomura Jan 28, 2024
6fd87d0
Refactor encapsulator context stack naming and get
shunichironomura Jan 28, 2024
2589f90
Add thread-local run stack to Run class
shunichironomura Jan 28, 2024
78cce75
Remove unnecessary imports in decorator and run modules
shunichironomura Jan 28, 2024
ca1eca0
Refactor imports for type checking in capsula module
shunichironomura Jan 28, 2024
5ee09de
Refactor context import and usage in capsula module
shunichironomura Jan 28, 2024
974bd03
Refactor import statements in capsula modules
shunichironomura Jan 28, 2024
cd56f6f
Merge 974bd03b2871ba9af29faccdabf7afac2c13c35b into a08fec2371acb70a0…
shunichironomura Jan 28, 2024
983beb3
Update tox report
github-actions[bot] Jan 28, 2024
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
35 changes: 35 additions & 0 deletions capsula/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,44 @@
"set_capsule_dir",
"set_capsule_name",
"Encapsulator",
"capsule",
"record",
"Run",
"ContextBase",
"CwdContext",
"EnvVarContext",
"GitRepositoryContext",
"FileContext",
"PlatformContext",
"CpuContext",
"CommandContext",
"JsonDumpReporter",
"ReporterBase",
"WatcherBase",
"TimeWatcher",
"watcher",
"reporter",
"context",
"UncaughtExceptionWatcher",
"run",
]
from ._context import (
CommandContext,
ContextBase,
CpuContext,
CwdContext,
EnvVarContext,
FileContext,
GitRepositoryContext,
PlatformContext,
)
from ._decorator import capsule, context, reporter, run, watcher
from ._monitor import monitor
from ._reporter import JsonDumpReporter, ReporterBase
from ._root import record
from ._run import Run
from ._version import __version__
from ._watcher import TimeWatcher, UncaughtExceptionWatcher, WatcherBase
from .encapsulator import Encapsulator
from .exceptions import CapsulaConfigurationError, CapsulaError
from .globalvars import get_capsule_dir, get_capsule_name, set_capsule_dir, set_capsule_name
10 changes: 3 additions & 7 deletions capsula/_backport.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
from __future__ import annotations

__all__ = [
"TypeAlias",
"file_digest",
"Self",
]
__all__ = ["TypeAlias", "file_digest", "Self", "ParamSpec"]

import hashlib
import sys
Expand All @@ -16,9 +12,9 @@
from typing_extensions import Self

if sys.version_info >= (3, 10):
from typing import TypeAlias
from typing import ParamSpec, TypeAlias
else:
from typing_extensions import TypeAlias
from typing_extensions import ParamSpec, TypeAlias


if sys.version_info >= (3, 11):
Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions capsula/context/__init__.py → capsula/_context/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
__all__ = [
"Context",
"ContextBase",
"CwdContext",
"EnvVarContext",
"GitRepositoryContext",
Expand All @@ -8,7 +8,7 @@
"CpuContext",
"CommandContext",
]
from ._base import Context
from ._base import ContextBase
from ._command import CommandContext
from ._cpu import CpuContext
from ._cwd import CwdContext
Expand Down
5 changes: 5 additions & 0 deletions capsula/_context/_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from capsula._capsule import CapsuleItem


class ContextBase(CapsuleItem):
pass
4 changes: 2 additions & 2 deletions capsula/context/_command.py → capsula/_context/_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
if TYPE_CHECKING:
from pathlib import Path

from ._base import Context
from ._base import ContextBase

logger = logging.getLogger(__name__)


class CommandContext(Context):
class CommandContext(ContextBase):
def __init__(self, command: str, cwd: Path | None = None) -> None:
self.command = command
self.cwd = cwd
Expand Down
4 changes: 2 additions & 2 deletions capsula/context/_cpu.py → capsula/_context/_cpu.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from cpuinfo import get_cpu_info

from ._base import Context
from ._base import ContextBase


class CpuContext(Context):
class CpuContext(ContextBase):
def encapsulate(self) -> dict:
return get_cpu_info()

Expand Down
4 changes: 2 additions & 2 deletions capsula/context/_cwd.py → capsula/_context/_cwd.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from pathlib import Path

from ._base import Context
from ._base import ContextBase


class CwdContext(Context):
class CwdContext(ContextBase):
def encapsulate(self) -> Path:
return Path.cwd()

Expand Down
4 changes: 2 additions & 2 deletions capsula/context/_envvar.py → capsula/_context/_envvar.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

import os

from ._base import Context
from ._base import ContextBase


class EnvVarContext(Context):
class EnvVarContext(ContextBase):
def __init__(self, name: str) -> None:
self.name = name

Expand Down
18 changes: 9 additions & 9 deletions capsula/context/_file.py → capsula/_context/_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@

from capsula._backport import file_digest

from ._base import Context
from ._base import ContextBase

logger = logging.getLogger(__name__)


class FileContext(Context):
class FileContext(ContextBase):
def __init__(
self,
path: Path | str,
Expand All @@ -32,13 +32,11 @@ def __init__(
else:
self.copy_to = tuple(Path(p) for p in copy_to)

def normalize_copy_dst_path(p: Path) -> Path:
if p.is_dir():
return p / self.path.name
else:
return p

self.copy_to = tuple(normalize_copy_dst_path(p) for p in self.copy_to)
def _normalize_copy_dst_path(self, p: Path) -> Path:
if p.is_dir():
return p / self.path.name
else:
return p

def encapsulate(self) -> dict:
if self.hash_algorithm is None:
Expand All @@ -47,6 +45,8 @@ def encapsulate(self) -> dict:
with self.path.open("rb") as f:
digest = file_digest(f, self.hash_algorithm).hexdigest()

self.copy_to = tuple(self._normalize_copy_dst_path(p) for p in self.copy_to)

info: dict = {
"hash": {
"algorithm": self.hash_algorithm,
Expand Down
25 changes: 23 additions & 2 deletions capsula/context/_git.py → capsula/_context/_git.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
from __future__ import annotations

import inspect
import logging
from pathlib import Path
from typing import TYPE_CHECKING, Callable

from git.repo import Repo

from capsula.exceptions import CapsulaError

from ._base import Context
from ._base import ContextBase

if TYPE_CHECKING:
from capsula._decorator import CapsuleParams

logger = logging.getLogger(__name__)

Expand All @@ -18,7 +23,7 @@ def __init__(self, repo: Repo) -> None:
super().__init__(f"Repository {repo.working_dir} is dirty")


class GitRepositoryContext(Context):
class GitRepositoryContext(ContextBase):
def __init__(
self,
name: str,
Expand Down Expand Up @@ -57,3 +62,19 @@ def encapsulate(self) -> dict:

def default_key(self) -> tuple[str, str]:
return ("git", self.name)

@classmethod
def default(cls) -> Callable[[CapsuleParams], GitRepositoryContext]:
def callback(params: CapsuleParams) -> GitRepositoryContext:
func_file_path = Path(inspect.getfile(params.func))
repo = Repo(func_file_path.parent, search_parent_directories=True)
repo_name = Path(repo.working_dir).name
return cls(
name=Path(repo.working_dir).name,
path=Path(repo.working_dir),
diff_file=params.run_dir / f"{repo_name}.diff",
search_parent_directories=False,
allow_dirty=True,
)

return callback
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import platform as pf

from ._base import Context
from ._base import ContextBase


class PlatformContext(Context):
class PlatformContext(ContextBase):
def encapsulate(self) -> dict:
return {
"machine": pf.machine(),
Expand Down
132 changes: 132 additions & 0 deletions capsula/_decorator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
from __future__ import annotations

from functools import wraps
from pathlib import Path
from typing import TYPE_CHECKING, Callable, Literal, Tuple, TypeVar, Union

from capsula._reporter import ReporterBase
from capsula.encapsulator import Encapsulator

from ._backport import ParamSpec
from ._context import ContextBase
from ._run import CapsuleParams, FuncInfo, Run
from ._watcher import WatcherBase

if TYPE_CHECKING:
from collections.abc import Sequence

from ._backport import TypeAlias

_P = ParamSpec("_P")
_T = TypeVar("_T")


_ContextInput: TypeAlias = Union[
ContextBase,
Tuple[ContextBase, Tuple[str, ...]],
Callable[[Path, Callable], Union[ContextBase, Tuple[ContextBase, Tuple[str, ...]]]],
]
_WatcherInput: TypeAlias = Union[
WatcherBase,
Tuple[WatcherBase, Tuple[str, ...]],
Callable[[Path, Callable], Union[WatcherBase, Tuple[WatcherBase, Tuple[str, ...]]]],
]
_ReporterInput: TypeAlias = Union[ReporterBase, Callable[[Path, Callable], ReporterBase]]


def capsule( # noqa: C901
capsule_directory: Path | str | None = None,
pre_run_contexts: Sequence[_ContextInput] | None = None,
pre_run_reporters: Sequence[_ReporterInput] | None = None,
in_run_watchers: Sequence[_WatcherInput] | None = None,
post_run_contexts: Sequence[_ContextInput] | None = None,
) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]:
if capsule_directory is None:
raise NotImplementedError
capsule_directory = Path(capsule_directory)

assert pre_run_contexts is not None
assert pre_run_reporters is not None
assert in_run_watchers is not None
assert post_run_contexts is not None

def decorator(func: Callable[_P, _T]) -> Callable[_P, _T]:
pre_run_enc = Encapsulator()
for cxt in pre_run_contexts:
if isinstance(cxt, ContextBase):
pre_run_enc.add_context(cxt)
elif isinstance(cxt, tuple):
pre_run_enc.add_context(cxt[0], key=cxt[1])
else:
cxt_hydrated = cxt(capsule_directory, func)
if isinstance(cxt_hydrated, ContextBase):
pre_run_enc.add_context(cxt_hydrated)
elif isinstance(cxt_hydrated, tuple):
pre_run_enc.add_context(cxt_hydrated[0], key=cxt_hydrated[1])

@wraps(func)
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _T:
capsule_directory.mkdir(parents=True, exist_ok=True)
pre_run_capsule = pre_run_enc.encapsulate()
for reporter in pre_run_reporters:
if isinstance(reporter, ReporterBase):
reporter.report(pre_run_capsule)
else:
reporter(capsule_directory, func).report(pre_run_capsule)

return func(*args, **kwargs)

return wrapper

return decorator


def watcher(
watcher: WatcherBase | Callable[[CapsuleParams], WatcherBase],
) -> Callable[[Callable[_P, _T] | Run[_P, _T]], Run[_P, _T]]:
def decorator(func_or_run: Callable[_P, _T] | Run[_P, _T]) -> Run[_P, _T]:
func = func_or_run.func if isinstance(func_or_run, Run) else func_or_run
run = func_or_run if isinstance(func_or_run, Run) else Run(func)
run.add_watcher(watcher)
return run

return decorator


def reporter(
reporter: ReporterBase | Callable[[CapsuleParams], ReporterBase],
mode: Literal["pre", "in", "post", "all"],
) -> Callable[[Callable[_P, _T] | Run[_P, _T]], Run[_P, _T]]:
def decorator(func_or_run: Callable[_P, _T] | Run[_P, _T]) -> Run[_P, _T]:
func = func_or_run.func if isinstance(func_or_run, Run) else func_or_run
run = func_or_run if isinstance(func_or_run, Run) else Run(func)
run.add_reporter(reporter, mode=mode)
return run

return decorator


def context(
context: ContextBase | Callable[[CapsuleParams], ContextBase],
mode: Literal["pre", "post", "all"],
) -> Callable[[Callable[_P, _T] | Run[_P, _T]], Run[_P, _T]]:
def decorator(func_or_run: Callable[_P, _T] | Run[_P, _T]) -> Run[_P, _T]:
func = func_or_run.func if isinstance(func_or_run, Run) else func_or_run
run = func_or_run if isinstance(func_or_run, Run) else Run(func)
run.add_context(context, mode=mode)
return run

return decorator


def run(
run_dir: Path | Callable[[FuncInfo], Path],
) -> Callable[[Callable[_P, _T] | Run[_P, _T]], Run[_P, _T]]:
def decorator(func_or_run: Callable[_P, _T] | Run[_P, _T]) -> Run[_P, _T]:
func = func_or_run.func if isinstance(func_or_run, Run) else func_or_run
run = func_or_run if isinstance(func_or_run, Run) else Run(func)
run.set_run_dir(run_dir)

return run

return decorator
3 changes: 3 additions & 0 deletions capsula/_reporter/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__all__ = ["JsonDumpReporter", "ReporterBase"]
from ._base import ReporterBase
from ._json import JsonDumpReporter
2 changes: 1 addition & 1 deletion capsula/reporter/_base.py → capsula/_reporter/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from capsula.encapsulator import Capsule


class Reporter(ABC):
class ReporterBase(ABC):
@abstractmethod
def report(self, capsule: Capsule) -> None:
raise NotImplementedError
Loading