Skip to content

Commit

Permalink
Simplify start (#233)
Browse files Browse the repository at this point in the history
* simplifying start

* fix start generation

* fix tests

* fix tests

* fix build on 3.5

* fix import order with python 3.5 :-(
  • Loading branch information
samuelcolvin authored May 21, 2019
1 parent faf4b4a commit 2e4f034
Show file tree
Hide file tree
Showing 30 changed files with 196 additions and 821 deletions.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ install:

.PHONY: isort
isort:
isort -rc -w 120 -sg */template/* aiohttp_devtools
isort -rc -w 120 aiohttp_devtools
isort -rc -w 120 tests

.PHONY: lint
lint:
python setup.py check -rms
flake8 aiohttp_devtools/ tests/
isort -rc -w 120 --check-only -sg */template/* aiohttp_devtools
isort -rc -w 120 --check-only aiohttp_devtools
isort -rc -w 120 --check-only tests

.PHONY: test
Expand Down
50 changes: 3 additions & 47 deletions aiohttp_devtools/cli.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import sys
import traceback
from pathlib import Path
from textwrap import dedent

import click

Expand All @@ -10,8 +9,7 @@
from .runserver import INFER_HOST, run_app
from .runserver import runserver as _runserver
from .runserver import serve_static
from .start import DatabaseChoice, ExampleChoice, SessionChoices, StartProject, TemplateChoice
from .start.main import check_dir_clean, enum_choices, enum_default
from .start import StartProject, check_dir_clean
from .version import VERSION

_dir_existing = click.Path(exists=True, dir_okay=True, file_okay=False)
Expand Down Expand Up @@ -92,37 +90,11 @@ def runserver(**config):
sys.exit(2)


def _display_enum_choices(enum):
dft = enum_default(enum)
return '[{}*|{}]'.format(click.style(dft, bold=True), '|'.join(c for c in enum_choices(enum) if c != dft))


class EnumChoice(click.Choice):
def __init__(self, choice_enum):
self._enum = choice_enum
super().__init__(enum_choices(choice_enum))

def get_metavar(self, param):
return _display_enum_choices(self._enum)


DECISIONS = [
('template_engine', TemplateChoice),
('session', SessionChoices),
('database', DatabaseChoice),
('example', ExampleChoice),
]


@cli.command()
@click.argument('path', type=_dir_may_exist, required=True)
@click.argument('name', required=False)
@click.option('-v', '--verbose', is_flag=True, help=verbose_help)
@click.option('--template-engine', type=EnumChoice(TemplateChoice), required=False)
@click.option('--session', type=EnumChoice(SessionChoices), required=False)
@click.option('--database', type=EnumChoice(DatabaseChoice), required=False)
@click.option('--example', type=EnumChoice(ExampleChoice), required=False)
def start(*, path, name, verbose, **kwargs):
def start(*, path, name, verbose):
"""
Create a new aiohttp app.
"""
Expand All @@ -132,23 +104,7 @@ def start(*, path, name, verbose, **kwargs):
if name is None:
name = Path(path).name

for kwarg_name, choice_enum in DECISIONS:
docs = dedent(choice_enum.__doc__).split('\n')
title, *help_text = filter(bool, docs)
click.secho('\n' + title, fg='green')
if kwargs[kwarg_name] is None:
click.secho('\n'.join(help_text), dim=True)
choices = _display_enum_choices(choice_enum)
kwargs[kwarg_name] = click.prompt(
'choose which {} to use {}'.format(kwarg_name, choices),
type=EnumChoice(choice_enum),
show_default=False,
default=enum_default(choice_enum),
)
click.echo('using: {}'.format(click.style(kwargs[kwarg_name], bold=True)))
continue

StartProject(path=path, name=name, **kwargs)
StartProject(path=path, name=name)
except AiohttpDevException as e:
main_logger.error('Error: %s', e)
sys.exit(2)
2 changes: 1 addition & 1 deletion aiohttp_devtools/start/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# flake8: noqa
from .main import DatabaseChoice, ExampleChoice, SessionChoices, StartProject, TemplateChoice
from .main import StartProject, check_dir_clean
138 changes: 11 additions & 127 deletions aiohttp_devtools/start/main.py
Original file line number Diff line number Diff line change
@@ -1,91 +1,13 @@
import base64
import os
import re
from enum import Enum
from pathlib import Path

from isort import SortImports
from jinja2 import Template, TemplateError

from ..exceptions import AiohttpDevConfigError
from ..logs import main_logger as logger

THIS_DIR = Path(__file__).parent
TEMPLATE_DIR = THIS_DIR / 'template' # type: Path

FILES_REGEXES = {
'.py': [
(r'^ *# *\n', '', re.M), # blank comments
(r'\n *# *$', '', 0), # blank comment at end of fie
(r'\n{4,}', '\n\n\n', 0), # more than 2 empty lines
(r'^\s+', '', 0), # leading new lines
],
'.sh': [
(r'^ *# *\n', '', re.M), # blank comments
(r'\n *# *$', '', 0), # blank comment at end of fie
],
}

FILES_REGEXES = {k: [(re.compile(p, f), r) for p, r, f in v] for k, v in FILES_REGEXES.items()}


class TemplateChoice(str, Enum):
"""
Template Engine
Please choose which template engine you wish to use.
* with "jinja" views will be rendered using Jinja2 templates using aiohttp-jinja2.
* with "none" views will be rendered directly.
"""
JINJA = 'jinja' # default
NONE = 'none'


class SessionChoices(str, Enum):
"""
Session
Please choose how you want sessions to be managed.
* "secure" will implemented encrypted cookie sessions using aiohttp-session
* "none" will mean no sessions
"""
SECURE = 'secure' # default
# * "redis" will use redis to store session data
# REDIS = 'redis' TODO
NONE = 'none'


class DatabaseChoice(str, Enum):
"""
Database
Please choose which database backend you wish to use.
* "pg-sqlalchemy" will use postgresql via aiopg and the SqlAlchemy ORM
* "none" will use no database, persistence in examples is achieved by simply writing to file
"""
PG_SA = 'pg-sqlalchemy' # default
# * "pg-raw" will use postgresql via aiopg with no ORM
# PG_RAW = 'pg-raw' TODO
NONE = 'none'


class ExampleChoice(str, Enum):
"""
Example
Please choose whether you want a simple example "message board" app to be created demonstrating a little
more of aiohttp's usage than the single simple view created with "none".
"""
MESSAGE_BOARD = 'message-board' # default
NONE = 'none'


def enum_choices(enum):
return [m.value for m in enum.__members__.values()]


def enum_default(enum):
return next(v for v in enum.__members__.values())
TEMPLATE_DIR = THIS_DIR / 'template'


def check_dir_clean(d: Path):
Expand All @@ -99,14 +21,7 @@ def check_dir_clean(d: Path):


class StartProject:
def __init__(self, *,
path: str,
name: str,
template_engine: TemplateChoice = enum_default(TemplateChoice),
session: SessionChoices = enum_default(SessionChoices),
database: DatabaseChoice = enum_default(DatabaseChoice),
example: ExampleChoice = enum_default(ExampleChoice),
template_dir: Path = TEMPLATE_DIR) -> None:
def __init__(self, *, path: str, name: str, template_dir: Path = TEMPLATE_DIR) -> None:
self.project_root = Path(path)
self.template_dir = template_dir
check_dir_clean(self.project_root)
Expand All @@ -117,29 +32,16 @@ def __init__(self, *,
display_path = self.project_root

logger.info('Starting new aiohttp project "%s" at "%s"', name, display_path)
display_config = [
('template_engine', template_engine),
('session', session),
('database', database),
('example', example),
]
logger.info('config:\n%s', '\n'.join(' {}: {}'.format(*c) for c in display_config))
self.ctx = {
'name': name,
'clean_name': re.sub(r'[^\w_]', '', re.sub(r'[.-]', '_', name)),
'cookie_secret_key': base64.urlsafe_b64encode(os.urandom(32)).decode(),
'template_engine': self._choice_context(template_engine, TemplateChoice),
'session': self._choice_context(session, SessionChoices),
'database': self._choice_context(database, DatabaseChoice),
'example': self._choice_context(example, ExampleChoice),
'cookie_name': re.sub(r'[^\w_]', '', re.sub(r'[.-]', '_', name)),
'auth_key': base64.urlsafe_b64encode(os.urandom(32)).decode(),
}
self.ctx_regex = re.compile(r'\{\{ ?(%s) ?\}\}' % '|'.join(self.ctx.keys()))
self.files_created = 0
self.generate_directory(TEMPLATE_DIR)
logger.info('project created, %d files generated', self.files_created)

def _choice_context(self, value, enum):
return {'is_' + o.replace('-', '_'): value == o for o in enum_choices(enum)}

def generate_directory(self, p: Path):
for pp in p.iterdir():
if pp.is_dir():
Expand All @@ -150,34 +52,16 @@ def generate_directory(self, p: Path):
self.generate_file(pp)

def generate_file(self, p: Path):
try:
template = Template(p.read_text())
text = template.render(**self.ctx)
except TemplateError as e:
raise TemplateError('error in {}'.format(p)) from e
text = text.strip('\n\t ')
text = p.read_text()
new_path = self.project_root / p.relative_to(self.template_dir)
if len(text) < 3:
# empty files don't get created, in case a few characters get left behind
logger.debug('not creating "%s", as it would be empty', new_path)
return
logger.debug('creating "%s"', new_path)

if p.name == 'requirements.txt':
packages = {p.strip() for p in text.split('\n') if p.strip() and p.strip() != '#'}
text = '\n'.join(sorted(packages))
else:
# helpful when debugging: print(text.replace(' ', '·').replace('\n', '⏎\n'))
if p.suffix == '.py':
text = SortImports(file_contents=text).output
if p.name == 'settings.py':
text = self.ctx_regex.sub(self.ctx_replace, text)

regexes = FILES_REGEXES.get(p.suffix, [])
for regex, repl in regexes:
text = regex.sub(repl, text)

# re-add a trailing newline accounting for newlines added by PY_REGEXES
# changed as per https://stackoverflow.com/questions/53642571
text = re.sub(r'\n+$', r'\n', text + '\n')
new_path.parent.mkdir(parents=True, exist_ok=True)
new_path.write_text(text)
self.files_created += 1

def ctx_replace(self, m):
return self.ctx[m.group(1)]
13 changes: 0 additions & 13 deletions aiohttp_devtools/start/template/Makefile

This file was deleted.

10 changes: 5 additions & 5 deletions aiohttp_devtools/start/template/README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{{ name }}
{{ '=' * name|length }}
aiohttp demo app
================

Your new web app is ready to go!

To run your app you'll need to:

1. Activate a python 3.5 or 3.6 environment
1. Activate a python 3.6 or 3.7 environment
2. Install the required packages with `pip install -r requirements.txt`
3. Make sure the app's settings are configured correctly (see `activate_settings.sh` and `app/settings.py`). You can also
3. Make sure the app's settings are configured correctly (see `app/settings.py`). You can also
use environment variables to define sensitive settings, eg. DB connection variables
4. You can then run your app during development with `adev runserver .`
4. You can then run your app during development with `adev runserver -s static app`

18 changes: 0 additions & 18 deletions aiohttp_devtools/start/template/activate.settings.sh

This file was deleted.

3 changes: 0 additions & 3 deletions aiohttp_devtools/start/template/app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +0,0 @@
"""
{{ name }}
"""
Loading

0 comments on commit 2e4f034

Please sign in to comment.