Skip to content

Commit

Permalink
Add list-actions option
Browse files Browse the repository at this point in the history
  • Loading branch information
abravalheri committed Aug 29, 2017
1 parent e4059a5 commit 87e90ed
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 28 deletions.
42 changes: 30 additions & 12 deletions pyscaffold/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,35 @@ def init_git(struct, opts):

# -------- API --------

DEFAULT_ACTIONS = [
get_default_options,
verify_options_consistency,
define_structure,
apply_update_rules,
create_structure,
init_git
]


def discover_actions(extensions):
"""Retrieve the action list.
This is done by concatenating the default list with the one generated after
activating the extensions.
Args:
extensions (list): list of functions responsible for activating the
extensions.
Returns:
list: scaffold actions.
"""
actions = DEFAULT_ACTIONS

# Activate the extensions
return reduce(lambda acc, f: _activate(f, acc), extensions, actions)


def create_project(opts=None, **kwargs):
"""Create the project's directory structure
Expand Down Expand Up @@ -190,18 +219,7 @@ def create_project(opts=None, **kwargs):
opts = opts if opts else {}
opts.update(kwargs)

actions = [
get_default_options,
verify_options_consistency,
define_structure,
apply_update_rules,
create_structure,
init_git
]

# Activate the extensions
extensions = opts.get('extensions', [])
actions = reduce(lambda acc, f: _activate(f, acc), extensions, actions)
actions = discover_actions(opts.get('extensions', []))

# Call the actions
return reduce(lambda acc, f: _invoke(f, *acc), actions, ({}, opts))
Expand Down
42 changes: 33 additions & 9 deletions pyscaffold/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import pyscaffold

from . import api, shell, templates, utils
from .api.helpers import get_id
from .extensions import (
cookiecutter,
django,
Expand All @@ -20,7 +21,7 @@
tox,
travis
)
from .log import DEFAULT_LOGGER
from .log import DEFAULT_LOGGER, ReportFormatter

__author__ = "Florian Wilhelm"
__copyright__ = "Blue Yonder"
Expand Down Expand Up @@ -86,14 +87,23 @@ def add_default_args(parser):
help="update an existing project by replacing the most important files"
" like setup.py etc. Use additionally --force to "
"replace all scaffold files.")
parser.add_argument(

group = parser.add_mutually_exclusive_group()
group.add_argument(
"--pretend",
"--dry-run",
dest="pretend",
action="store_true",
default=False,
help="do not create project, but show a list of planned actions and"
" operations.")
help="do not create project, but displays the log of all operations"
" as if it had been created.")
group.add_argument(
"--list-actions",
dest="command",
action="store_const",
const=list_actions,
help="do not create project, but show a list of planned actions")

version = pyscaffold.__version__
parser.add_argument('-v',
'--version',
Expand Down Expand Up @@ -148,7 +158,7 @@ def parse_args(args):
parser = argparse.ArgumentParser(
description="PyScaffold is a tool for easily putting up the scaffold "
"of a Python project.")
parser.set_defaults(extensions=[], log_level="INFO")
parser.set_defaults(log_level="INFO", extensions=[], command=run_scaffold)

for augment in cli_creators + cli_extenders:
augment(parser)
Expand All @@ -166,10 +176,8 @@ def parse_args(args):
return {k: v for k, v in opts.items() if v is not None}


def main(args):
"""PyScaffold is a tool for putting up the scaffold of a Python project.
"""
opts = parse_args(args)
def run_scaffold(opts):
"""Actually scaffold the project, calling the python API."""
logging.getLogger(DEFAULT_LOGGER).setLevel(opts['log_level'])
api.create_project(opts)
if opts['update'] and not opts['force']:
Expand All @@ -179,6 +187,22 @@ def main(args):
print(note.format(pyscaffold.__version__))


def list_actions(opts):
"""Do not create a project, just list actions considering extensions."""
actions = api.discover_actions(opts.get('extensions', []))

print('Planned Actions:\n')
for action in actions:
print(ReportFormatter.SPACING + get_id(action))


def main(args):
"""PyScaffold is a tool for putting up the scaffold of a Python project.
"""
opts = parse_args(args)
opts['command'](opts)


@shell.called_process_error2exit_decorator
@utils.exceptions2exit([RuntimeError])
def run():
Expand Down
36 changes: 31 additions & 5 deletions pyscaffold/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ def format_context(self, context):

def format_default(self, record):
"""Format default log messages."""
record.msg = self.SPACING * max(record.nesting, 0) + record.msg

return super(ReportFormatter, self).format(record)

def format_report(self, record):
Expand All @@ -99,22 +101,45 @@ def format_report(self, record):
class ReportLogger(LoggerAdapter):
"""Suitable wrapper for PyScaffold CLI interactive execution reports.
Args:
logger (logging.Logger): custom logger to be used. Optional: the
default logger will be used.
handlers (logging.Handler): custom logging handler to be used.
Optional: a :class:`logging.StreamHandler` is used by default.
formatter (logging.Formatter): custom formatter to be used.
Optional: by default a :class:`~.ReportFormatter` is created and
used.
extra (dict): extra attributes to be merged into the log record.
Options, empty by default.
Attributes:
wrapped (logging.Logger): underlying logger object.
default_handler (logging.StreamHandler): stream handler configured for
default_handler (logging.Handler): stream handler configured for
providing user feedback in PyScaffold CLI.
default_formatter (logging.Formatter): formatter configured in the
default handler.
nesting (int): current nesting level of the report.
"""

def __init__(self, logger=None, extra=None):
def __init__(self, logger=None, handler=None, formatter=None, extra=None):
self.nesting = 0
self.wrapped = logger or getLogger(DEFAULT_LOGGER)
self.extra = extra or {}
self.default_handler = StreamHandler()
self.default_handler.setFormatter(ReportFormatter())
self.default_handler = handler or StreamHandler()
self.default_formatter = formatter or ReportFormatter()
self.default_handler.setFormatter(self.default_formatter)
self.wrapped.addHandler(self.default_handler)
super(ReportLogger, self).__init__(self.wrapped, self.extra)

def process(self, msg, kwargs):
"""Method overridden to augment LogRecord with the `nesting` attribute.
"""
(msg, kwargs) = super(ReportLogger, self).process(msg, kwargs)
extra = kwargs.get('extra', {})
extra['nesting'] = self.nesting
kwargs['extra'] = extra
return (msg, kwargs)

def report(self, activity, subject,
context=None, target=None, nesting=None, level=INFO):
"""Log that an activity has occurred during scaffold.
Expand Down Expand Up @@ -186,7 +211,8 @@ def copy(self):
Sometimes, it is better to make a copy of th report logger to keep
indentation consistent.
"""
clone = self.__class__(self.wrapped, self.extra)
clone = self.__class__(self.wrapped, self.default_handler,
self.default_formatter, self.extra)
clone.nesting = self.nesting

return clone
Expand Down
22 changes: 21 additions & 1 deletion tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
import pytest

from pyscaffold import templates
from pyscaffold.api import create_project, get_default_options
from pyscaffold.api import (
create_project,
discover_actions,
get_default_options
)
from pyscaffold.exceptions import (
DirectoryAlreadyExists,
DirectoryDoesNotExist,
Expand All @@ -30,6 +34,22 @@ def extension(actions, helpers):
return extension


def test_discover_actions():
# Given an extension with actions,
def fake_action(struct, opts):
return (struct, opts)

def extension(actions, _):
return [fake_action] + actions

# When discover_actions is called,
actions = discover_actions([extension])

# Then the extension actions should be listed alongside default actions.
assert get_default_options in actions
assert fake_action in actions


def test_create_project_call_extension_hooks(tmpfolder, git_mock):
# Given an extension with hooks,
called = []
Expand Down
23 changes: 23 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ def test_parse_pretend():
assert not opts["pretend"]


def test_parse_list_actions():
opts = cli.parse_args(["my-project", "--list-actions"])
assert opts["command"] == cli.list_actions
opts = cli.parse_args(["my-project"])
assert opts["command"] == cli.run_scaffold


def test_main(tmpfolder, git_mock, caplog): # noqa
args = ["my-project"]
cli.main(args)
Expand Down Expand Up @@ -95,6 +102,22 @@ def test_main_when_updating(tmpfolder, capsys, git_mock): # noqa
assert "Update accomplished!" in out


def test_main_with_list_actions(capsys):
# When putup is called with --list-actions,
args = ["my-project", "--with-tox", "--list-actions"]
cli.main(args)
# then the action list should be printed,
out, _ = capsys.readouterr()
assert "Planned Actions" in out
assert "pyscaffold.api:get_default_options" in out
assert "pyscaffold.structure:define_structure" in out
assert "pyscaffold.extensions.tox:add_files" in out
assert "pyscaffold.structure:create_structure" in out
assert "pyscaffold.api:init_git" in out
# but no project should be created
assert not os.path.exists(args[0])


def test_run(tmpfolder, git_mock): # noqa
sys.argv = ["pyscaffold", "my-project"]
cli.run()
Expand Down
9 changes: 8 additions & 1 deletion tests/test_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from pyscaffold.log import DEFAULT_LOGGER, ReportFormatter, logger

from .log_helpers import make_record, match_last_report
from .log_helpers import last_log, make_record, match_last_report


def test_default_handler_registered():
Expand Down Expand Up @@ -56,6 +56,13 @@ def test_indent(caplog):
match = match_last_report(caplog)
assert match['spacing'] == ReportFormatter.SPACING * (count + 1)

# When any other method is called with indentation,
count = 3
with logger.indent(count):
logger.info('something')
# Then the spacing should be added in the beginning
assert (ReportFormatter.SPACING * count + 'something') in last_log(caplog)


def test_copy(caplog):
# Given the logger level is set to INFO,
Expand Down

0 comments on commit 87e90ed

Please sign in to comment.