Skip to content

Add system for reporting/discovering Step dependencies #14

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

Merged
merged 7 commits into from
Aug 23, 2022
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
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,14 @@ Anchovy operates on config files written in Python, or even modules directly.
```python
from pathlib import Path

from anchovy.core import InputBuildSettings, Rule
from anchovy.jinja import JinjaMarkdownStep
from anchovy.paths import OutputDirPathCalc, REMatcher
from anchovy.simple import DirectCopyStep
from anchovy import (
DirectCopyStep,
InputBuildSettings,
JinjaMarkdownStep,
OutputDirPathCalc,
REMatcher,
Rule,
)


# Optional, and can be overridden with CLI arguments.
Expand Down
12 changes: 8 additions & 4 deletions examples/basic_site.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from pathlib import Path

from anchovy.core import InputBuildSettings, Rule
from anchovy.jinja import JinjaMarkdownStep
from anchovy.paths import OutputDirPathCalc, REMatcher
from anchovy.simple import DirectCopyStep
from anchovy import (
DirectCopyStep,
InputBuildSettings,
JinjaMarkdownStep,
OutputDirPathCalc,
REMatcher,
Rule,
)


# Optional, and can be overridden with CLI arguments.
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ disable = [
"use-symbolic-message-instead",
"too-few-public-methods",
"inconsistent-return-statements",
"import-outside-toplevel",
]
# Enable the message, report, category or checker with the given id(s).
enable = [
Expand Down
6 changes: 6 additions & 0 deletions src/anchovy/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from .core import Context, InputBuildSettings, Matcher, PathCalc, Rule, Step
from .css import AnchovyCSSStep
from .dependencies import Dependency, import_install_check, which_install_check
from .jinja import JinjaMarkdownStep, JinjaRenderStep
from .paths import DirPathCalc, OutputDirPathCalc, REMatcher, WorkingDirPathCalc
from .simple import DirectCopyStep
88 changes: 82 additions & 6 deletions src/anchovy/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
import argparse
import contextlib
import importlib
import sys
import tempfile
import typing as t
from pathlib import Path

from .core import BuildSettings, Context, InputBuildSettings, Rule
from .core import BuildSettings, Context, InputBuildSettings, Rule, Step, StepUnavailableException
from .pretty_utils import print_with_style


class BuildNamespace:
Expand Down Expand Up @@ -97,12 +99,55 @@ def run_from_rules(settings: InputBuildSettings | None,
context.run()


def pprint_step(step: t.Type[Step]):
"""
Prettily display dependency information for the given Step class.
"""
missing = [
d.name for d in step.get_dependencies()
if d.needed and not d.satisfied

]
if missing:
text = ', '.join(missing)
print_with_style(f'✗ {step.__name__} (missing: {text})', style='red')
else:
print_with_style(f'✓ {step.__name__}', style='green')


def pprint_missing_deps(step: Step):
"""
Prettily display an error for the given Step with missing dependencies.
"""
print_with_style(
f'{step} is unavailable due to missing dependencies!',
file=sys.stderr,
style='red'
)
for dep in step.get_dependencies():
missing = False
if not dep.needed:
style = None
elif dep.satisfied:
style = 'green'
else:
missing = True
style = 'red'

text = f'✗ {dep}: {dep.install_hint}' if missing else f'✓ {dep}'
print_with_style(text, style=style)


def main():
"""
Anchovy main function. Finds or creates a Context using an Anchovy config
file and command line arguments, then executes a build using it.
"""
parser = argparse.ArgumentParser(description='Build an anchovy project.')
parser.add_argument('--audit-steps',
help=('show information about available, unavailable, '
'and used steps, instead of building the project'),
action='store_true')
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-m',
help='import path of a config file to build',
Expand Down Expand Up @@ -135,9 +180,40 @@ def main():
rules: list[Rule] | None = getattr(args.module, 'RULES')
context: Context | None = getattr(args.module, 'CONTEXT')

if context:
context.run()
else:
if not rules:
if args.audit_steps:
audit_rules = context.rules if context else rules
if not audit_rules:
raise RuntimeError('Anchovy config files must have a RULES or CONTEXT attribute!')
run_from_rules(settings, rules, argv=remaining, prog=f'anchovy {label}')

all_steps = set(Step.get_all_steps())
available_steps = set(Step.get_available_steps())
unavailable_steps = all_steps - available_steps
used_steps = {r.step.__class__ for r in audit_rules if r.step}

groups = {
'Available steps': available_steps,
'Unavailable steps': unavailable_steps,
'Used steps': used_steps,
}
for group_label, step_group in groups.items():
print(f'{group_label} ({len(step_group)})')
for step in step_group:
pprint_step(step)

elif context or rules:
try:
if context:
context.run()
elif rules:
run_from_rules(settings, rules, argv=remaining, prog=f'anchovy {label}')
except StepUnavailableException as e:
pprint_missing_deps(e.step)
sys.exit(1)

else:
print_with_style(
'Anchovy config files must have a RULES or CONTEXT attribute!',
file=sys.stderr,
style='red'
)
sys.exit(1)
66 changes: 46 additions & 20 deletions src/anchovy/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,8 @@
import typing as t
from pathlib import Path

try:
import rich.progress as _rich_progress
except ImportError:
_rich_progress = None
try:
import tqdm as _tqdm
except ImportError:
_tqdm = None
from .dependencies import Dependency
from .pretty_utils import track_progress


T = t.TypeVar('T')
Expand Down Expand Up @@ -40,16 +34,6 @@ class BuildSettings(t.TypedDict):
purge_dirs: bool


def _progress(iterable: t.Iterable[T], desc: str) -> t.Iterable[T]:
if _rich_progress:
yield from _rich_progress.track(iterable, desc)
elif _tqdm is not None:
yield from _tqdm.tqdm(iterable, desc)
else:
print(desc)
yield from iterable


def _rm_children(path: Path):
if not path.exists():
return
Expand Down Expand Up @@ -86,6 +70,8 @@ def bind(self, step: Step | None):
and creates a partial for function Steps.
"""
if step:
if not step.is_available():
raise StepUnavailableException(step)
step.bind(self)

def find_inputs(self, path: Path):
Expand All @@ -112,7 +98,7 @@ def process(self, input_paths: list[Path] | None = None):
tasks: dict[Step, list[tuple[Path, list[Path]]]]
tasks = {r.step: [] for r in self.rules if r.step}

for path in _progress(input_paths, 'Planning...'):
for path in track_progress(input_paths, 'Planning...'):
for rule in self.rules:
if match := rule.matcher(self, path):
# None can be used to halt further rule processing.
Expand Down Expand Up @@ -140,7 +126,7 @@ def process(self, input_paths: list[Path] | None = None):
flattened.extend((step, p, ops) for p, ops in paths)

further_processing: list[Path] = []
for step, path, output_paths in _progress(flattened, 'Processing...'):
for step, path, output_paths in track_progress(flattened, 'Processing...'):
print(f'{path} ⇒ {", ".join(str(p) for p in output_paths)}')
step(path, output_paths)
further_processing.extend(
Expand Down Expand Up @@ -231,6 +217,40 @@ class Step(abc.ABC):
full Anchovy ruleset.
"""
context: Context
_step_registry: list[t.Type[Step]] = []

def __init_subclass__(cls, **kw):
super().__init_subclass__(**kw)
cls._step_registry.append(cls)

@classmethod
def get_all_steps(cls):
"""
Return a list of all currently known Steps.
"""
return list(cls._step_registry)

@classmethod
def get_available_steps(cls):
"""
Return a list of all currently known Steps whose requirements are met.
"""
return [s for s in cls._step_registry if s.is_available()]

@classmethod
def is_available(cls) -> bool:
"""
Return whether this Step's requirements are installed, making it
available for use.
"""
return all(d.satisfied for d in cls.get_dependencies())

@classmethod
def get_dependencies(cls) -> set[Dependency]:
"""
Return the requirements for this Step.
"""
return set()

def bind(self, context: Context):
"""
Expand All @@ -241,3 +261,9 @@ def bind(self, context: Context):
@abc.abstractmethod
def __call__(self, path: Path, output_paths: list[Path]):
...


class StepUnavailableException(Exception):
def __init__(self, step: Step, *args: t.Any):
self.step = step
super().__init__(*args)
9 changes: 8 additions & 1 deletion src/anchovy/css/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@
from pathlib import Path

from ..core import Step
from .parser import process
from ..dependencies import Dependency, import_install_check


class AnchovyCSSStep(Step):
"""
Simple Step to preprocess an Anchovy CSS file into compliant CSS.
"""
@classmethod
def get_dependencies(cls) -> set[Dependency]:
return super().get_dependencies() | {
Dependency('tinycss2', 'pip', import_install_check),
}

def __call__(self, path: Path, output_paths: list[Path]):
if not output_paths:
return
from .parser import process
processed = process(path.read_text())
for target_path in output_paths:
target_path.parent.mkdir(parents=True, exist_ok=True)
Expand Down
Loading