Skip to content

Commit

Permalink
Merge pull request #29 from michaeljoseph/cli
Browse files Browse the repository at this point in the history
Initial CLI to capture and apply mould replacements
  • Loading branch information
michaeljoseph committed Sep 30, 2017
2 parents 7f1c19b + 7bd815e commit e81f35f
Show file tree
Hide file tree
Showing 10 changed files with 308 additions and 47 deletions.
4 changes: 2 additions & 2 deletions mould/__main__.py
@@ -1,3 +1,3 @@
from .cli import main

import mould.cli
mould.cli.main()
main(prog_name='mould')
91 changes: 80 additions & 11 deletions mould/cli.py
@@ -1,19 +1,88 @@
import logging
import json

import click

log = logging.getLogger(__name__)
from . import mould
from . import read_directory
from .transform import preview


@click.command()
@click.option('--count', default=1, help='Number of greetings.')
@click.option('--name', prompt='Your name', help='The person to greet.')
@click.option('--debug', default=False, help='Debug mode.')
def main(count, name, debug):
"""Simple program that greets NAME for a total of COUNT times."""
logging.basicConfig(level=logging.DEBUG if debug else logging.INFO)
@click.argument('source', type=click.Path(exists=True))
@click.argument('destination', type=click.Path())
@click.option('--debug', is_flag=True, default=False, help='Debug mode.')
@click.option('--dry-run', is_flag=True, default=False, help='Dry run.')
def main(source, destination, debug, dry_run):

for x in range(count):
click.echo('Hello %s!' % name)
click.secho(
'Reading from {} and writing to {}'.format(source, destination),
fg='green',
)

log.debug('Goodbye %s!' % name)
source_files = read_directory(source)

replacements = {}

done = False
while not done:
try:
pattern = click.prompt(
click.style(
'Enter a pattern to search for '
'(enter when done)',
fg='green'
),
default='',
show_default=False
)
if pattern == '':
done = True
continue

click.echo(
preview(
source_files,
{
pattern: '<temporary-name>'
},
)
)

name = click.prompt(
click.style(
'Give this pattern a name (or enter to discard)',
fg='green'
),
default='',
show_default=False
)

if name:
replacements[pattern] = name

if replacements:
click.secho(
'Current replacements {}'.format(json.dumps(replacements)),
fg='yellow'
)
except click.Abort:
done = True

if replacements:
if not click.confirm(click.style(
'Confirm moulding {} to {}, with replacements:\n{}\n'.format(
source,
destination,
# FIXME: print replacements better
json.dumps(replacements)),
fg='yellow'
)):
import sys
sys.exit(0)

if not dry_run:
mould(
source,
replacements,
destination,
)
26 changes: 26 additions & 0 deletions mould/gitignore.py
@@ -0,0 +1,26 @@
def read_ignore(ignore_content):
return [
ignore_line
for ignore_line in ignore_content.split()
if not ignore_line.startswith('#')
]


def remove_ignores(file_paths, ignore_list):
"""
Remove files that match gitignore patterns
:param file_paths:
:param ignore_list:
:return:
"""
# https://stackoverflow.com/a/25230908/5549
from fnmatch import fnmatch
matches = []
for ignore in ignore_list:
file_paths = [
n for n
in file_paths
if n.startswith('#') or not fnmatch(n, ignore)
]
matches.extend(file_paths)
return matches
33 changes: 23 additions & 10 deletions mould/read.py
Expand Up @@ -2,30 +2,45 @@
import os
from binaryornot import check

from .gitignore import read_ignore, remove_ignores


def read_directory(project_directory):
directory_entries = []
exclude_directories = ['.git']

project_parent_directory = os.path.normpath(
os.path.join(project_directory, os.pardir)
)

ignore_path = os.path.join(project_directory, '.gitignore')
ignore_list = []
if os.path.exists(ignore_path):
ignore_list = read_ignore(read_file(ignore_path))

for root, dirs, files in os.walk(project_directory):

directory_path = os.path.relpath(root, project_parent_directory)

if '.git' in directory_path:
dirs[:] = []
continue

directory = {
'path': os.path.relpath(root, project_parent_directory),
'path': directory_path,
'files': [],
}

for exclude_dir in exclude_directories:
if exclude_dir in dirs:
dirs.remove(exclude_dir)
if ignore_list:
files = remove_ignores(files, ignore_list)

for file_path in files:
if file_path.startswith('.'):
continue

file_path = os.path.join(root, file_path)

is_binary, content = _read_file(file_path)
content = read_file(file_path)
is_binary = check.is_binary(file_path)

directory['files'].append({
'path': os.path.relpath(file_path, project_parent_directory),
Expand All @@ -38,9 +53,7 @@ def read_directory(project_directory):
return directory_entries


def _read_file(file_path):
def read_file(file_path):
is_binary = check.is_binary(file_path)

mode = 'r{}'.format('b' if is_binary else 't')

return is_binary, io.open(file_path, mode=mode).read()
return io.open(file_path, mode=mode).read()
93 changes: 73 additions & 20 deletions mould/transform.py
@@ -1,42 +1,95 @@
import difflib

import click

def replace_directory_entries(directory_entries, replacements):
"""
Perform `replacements` substitutions on `DirectoryEntry` instances.
def replace_directory_entries(directory_entries, replacements, preview=False):
Also mention the cookiecutter thing?
https://docs.python.org/3.3/library/stdtypes.html#str.replace
Specifically the path elements
:param directory_entries:
:param replacements: A dict where the key is the [value of the needle]
and the value
:return:
"""
replaced_entries = []

for directory_entry in directory_entries:
entry = dict(directory_entry)
# make a copy of the current entry
directory = dict(directory_entry)

for search, replace in replacements.items():
# transform the value into a cookiecutter variable
replace = '{{cookiecutter.' + replace + '}}'
entry['path'] = entry['path'].replace(search, replace)

for file_record in entry['files']:
old_path = file_record['path']
# replace directory path names
directory['path'] = directory['path'].replace(search, replace)

for file_record in directory['files']:
# transform the file path
file_record['path'] = file_record['path'].replace(
search,
replace
)

if not file_record['binary']:
before = file_record['content']
# don't try to run replace on binary files
is_text_file = not file_record['binary']
if is_text_file:
# transform the file content
file_record['content'] = file_record['content'].replace(
search,
replace
)
if preview:
click.echo_via_pager('\n'.join(
difflib.unified_diff(
before.splitlines(),
file_record['content'].splitlines(),
fromfile=old_path,
tofile=file_record['path'],
lineterm='',
)
))

replaced_entries.append(entry)

replaced_entries.append(directory)

return replaced_entries


def preview(directory_entries, replacements):
preview_content = []

for directory_entry in directory_entries:
# make a copy of the current entry
directory = dict(directory_entry)

for search, replace in replacements.items():
# transform the value into a cookiecutter variable
replace = '{{cookiecutter.' + replace + '}}'

# replace directory path names
# directory['path'] = directory['path'].replace(search, replace)

for file_record in directory['files']:
# save current path for diff
old_path = file_record['path']

# transform the file path
new_path = old_path.replace(
search,
replace
)

# don't try to run replace on binary files
is_text_file = not file_record['binary']
if is_text_file:
# save current content for diff
old_content = file_record['content']

# transform the file content
new_content = old_content.replace(
search,
replace
)

preview_content.append('\n'.join(difflib.unified_diff(
old_content.splitlines(),
new_content.splitlines(),
fromfile=old_path,
tofile=new_path,
lineterm='',
)))

return '\n'.join(preview_content)
31 changes: 29 additions & 2 deletions mould/write.py
@@ -1,18 +1,45 @@
import io
import os

import click


def write_directory(directory_entries, target_directory):
"""
:param directory_entries: DirectoryEntry instances
:param target_directory: The directory to write the mould to
:return:
"""

if not os.path.exists(target_directory):
os.mkdir(target_directory)
click.secho(
'Creating {} directory'.format(target_directory),
fg='green'
)
else:
# TODO: handle existing git target directory
click.secho(
'{} already exists, overwriting contents'.format(target_directory),
fg='red'
)

for directory_entry in directory_entries:
dir_to_create = os.path.join(target_directory, directory_entry['path'])
print('creating {}'.format(dir_to_create))
click.secho(
'creating {}'.format(dir_to_create),
fg='blue'
)

if not os.path.exists(dir_to_create):
os.mkdir(dir_to_create)

for file_entry in directory_entry['files']:
file_to_create = os.path.join(target_directory, file_entry['path'])
print('writing {}'.format(file_to_create))
click.secho(
'writing {}'.format(file_to_create),
fg='blue'
)

mode = 'w{}'.format('b' if file_entry['binary'] else 't')
with io.open(file_to_create, mode=mode) as fh:
Expand Down

0 comments on commit e81f35f

Please sign in to comment.