Skip to content

Commit

Permalink
Install proper Django during pinax-cli installation
Browse files Browse the repository at this point in the history
Install required Django at installation, based on enviroment markers
in setup.py.
Update help with usage examples, colored output, and ordered sub-command list.
  • Loading branch information
grahamu committed Mar 15, 2018
1 parent fb61d18 commit d782627
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 65 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ Django apps, themes, and starter project templates. This collection can be found
`pinax-cli` is a command-line interface for installing Pinax starter projects
and learning more about the latest Pinax app releases.

#### Supported Django and Python versions

Django \ Python | 2.7 | 3.4 | 3.5 | 3.6
--------------- | --- | --- | --- | ---
1.11 | * | * | * | *
2.0 | | * | * | *


## Documentation

Expand Down Expand Up @@ -102,11 +109,27 @@ Show a list of Pinax tools with their release version in the latest Pinax distri
$ pinax tools
```

#### `pinax start <starter_project> <my_project> [--dev] [--location <path>]`

Create a new project based on a specific Pinax starter project.

`<starter_project>` must be one of the project names shown by `pinax projects`.

The `--dev` flag tells pinax-cli to install the latest starter project code from the repository rather than the most recent release.
Use this option if you want the latest version of a starter project.

The `--location <path>` flag tells pinax-cli where to create the new project. By default
the project is created in a sub-directory named `my_project`.

## Change Log

### 1.1.1

* Add `pip` requirement to setup.py
* Use Django v1.11 for project creation if installed Python == 2.7

### 1.1.1

* Fix post-start cleanup path references

### 1.1.0
Expand Down
Empty file added pinaxcli/__init__.py
Empty file.
152 changes: 99 additions & 53 deletions pcli.py → pinaxcli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import sys

import click
import django
from django.core.management import call_command, CommandError
import requests
from pip.commands import install

from .utils import format_help, order_manually


class Config(object):
Expand All @@ -13,64 +16,59 @@ def __init__(self):
self.apps_url = "https://raw.githubusercontent.com/pinax/pinax/master/distributions.json"


CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
pass_config = click.make_pass_decorator(Config, ensure=True)


def pip_install(package):
command = install.InstallCommand()
opts, args = command.parser.parse_args([])
click.echo("Installing {}...".format(package))
command.run(opts, [package])


def start_project(project, name, dev, location):
from django.core.management import call_command, CommandError
click.echo("Starting project from Pinax")
template = project["url"] if dev else max(project["releases"])
kwargs = dict(
template=template,
files=project["process-files"]
)
args = [name]
if location:
args.append(location)
try:
call_command("startproject", *args, **kwargs)
except CommandError as e:
click.echo(click.style("Error: ", fg="red") + str(e))
sys.exit(1)


def output_instructions(project):
if "instructions" in project:
click.echo(project["instructions"])


def cleanup(name, location):
if not location:
# if location was not specified, start_project used `name` for new subdir
location = name
os.remove(os.path.join(location, "LICENSE"))
os.remove(os.path.join(location, "CONTRIBUTING.md"))
os.remove(os.path.join(location, "update.sh"))
managepy = os.path.join(location, "manage.py")
st = os.stat(managepy)
os.chmod(managepy, st.st_mode | stat.S_IEXEC)


@click.group()
class PinaxGroup(click.Group):
"""Custom Group class with specially formatted help"""

def list_commands(self, ctx):
"""Override for showing commands in particular order"""
commands = super(PinaxGroup, self).list_commands(ctx)
return [cmd for cmd in order_manually(commands)]

def get_help_option(self, ctx):
"""Override for showing formatted main help via --help and -h options"""
help_options = self.get_help_option_names(ctx)
if not help_options or not self.add_help_option:
return

def show_help(ctx, param, value):
if value and not ctx.resilient_parsing:
if not ctx.invoked_subcommand:
# pinax main help
click.echo(format_help(ctx.get_help()))
else:
# pinax sub-command help
click.echo(ctx.get_help(), color=ctx.color)
ctx.exit()
return click.Option(
help_options,
is_flag=True,
is_eager=True,
expose_value=False,
callback=show_help,
help='Show this message and exit.')


@click.group(cls=PinaxGroup, invoke_without_command=True, context_settings=CONTEXT_SETTINGS)
@click.option("--url", type=str, required=False, help="url to project data source")
@click.option("--apps_url", type=str, required=False, help="url to application data source")
@click.version_option()
@pass_config
def main(config, url, apps_url):
@click.pass_context
def main(ctx, config, url, apps_url):
if url:
config.url = url
if apps_url:
config.apps_url = apps_url
if ctx.invoked_subcommand is None:
# Display help to user if no commands were passed.
click.echo(format_help(ctx.get_help()))


@main.command()
@main.command(short_help="Display Pinax starter projects")
@pass_config
def projects(config):
payload = requests.get(config.url).json()
Expand Down Expand Up @@ -107,31 +105,31 @@ def show_distribution_section(config, title, section_name):
click.echo("{} {}".format(section[name].rjust(7), name))


@main.command()
@main.command(short_help="Display Pinax apps")
@pass_config
def apps(config):
show_distribution_section(config, "Application", "apps")


@main.command()
@main.command(short_help="Display Pinax demo projects")
@pass_config
def demos(config):
show_distribution_section(config, "Demo", "demos")


@main.command()
@main.command(short_help="Display Pinax themes")
@pass_config
def themes(config):
show_distribution_section(config, "Theme", "themes")


@main.command()
@main.command(short_help="Display Pinax tools")
@pass_config
def tools(config):
show_distribution_section(config, "Tool", "tools")


@main.command()
@main.command(short_help="Create a new project based on a Pinax starter project")
@click.option("--dev", is_flag=True, help="use latest development branch instead of release")
@click.option("--location", type=str, default="", help="specify where project is created")
@click.argument("project", type=str, required=True)
Expand All @@ -143,14 +141,62 @@ def start(config, dev, location, project, name):
projects = payload.get("projects")
try:
if dev or len(projects[project]["releases"]) > 0:
pip_install("Django")
validate_django_compatible_with_python()
start_project(projects[project], name, dev, location)
click.echo("Finished")
output_instructions(projects[project])
cleanup(name, location)
else:
click.echo("There are no releases for {}. You need to specify the --dev flag to use.".format(project))
except KeyError:
click.echo("There are no releases for {}.".format(project))
click.echo("Project {} is not found.".format(project))
else:
click.echo("The projects manifest you are trying to consume will not work: \n{}".format(config.url))


def validate_django_compatible_with_python():
"""
Verify Django 1.11 is present if Python 2.7 is active
Installation of pinax-cli requires the correct version of Django for
the active Python version. If the developer subsequently changes
the Python version the installed Django may no longer be compatible.
"""
python_version = sys.version[:5]
django_version = django.get_version()
if sys.version_info == (2, 7) and django_version >= "1.12":
click.BadArgumentUsage("Please install Django v1.11 for Python {}, or switch to Python >= v3.4".format(python_version))


def start_project(project, name, dev, location):
click.echo("Starting project from Pinax")
template = project["url"] if dev else max(project["releases"])
kwargs = dict(
template=template,
files=project["process-files"]
)
args = [name]
if location:
args.append(location)
try:
call_command("startproject", *args, **kwargs)
except CommandError as e:
click.echo(click.style("Error: ", fg="red") + str(e))
sys.exit(1)


def output_instructions(project):
if "instructions" in project:
click.echo(project["instructions"])


def cleanup(name, location):
if not location:
# if location was not specified, start_project used `name` for new subdir
location = name
os.remove(os.path.join(location, "LICENSE"))
os.remove(os.path.join(location, "CONTRIBUTING.md"))
os.remove(os.path.join(location, "update.sh"))
managepy = os.path.join(location, "manage.py")
st = os.stat(managepy)
os.chmod(managepy, st.st_mode | stat.S_IEXEC)
71 changes: 71 additions & 0 deletions pinaxcli/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import crayons


def order_manually(sub_commands):
"""Order sub-commands for display"""
order = [
"start",
]
ordered = []
commands = dict(zip([cmd for cmd in sub_commands], sub_commands))
for k in order:
ordered.append(commands.get(k, ""))
if k in commands:
del commands[k]

# Add commands not present in `order` above
for k in commands:
ordered.append(commands[k])

return ordered


def format_help(help):
"""Format the help string."""
help = help.replace('Options:', str(crayons.black('Options:', bold=True)))

help = help.replace('Usage: pinax', str('Usage: {0}'.format(crayons.black('pinax', bold=True))))

help = help.replace(' start', str(crayons.green(' start', bold=True)))
help = help.replace(' apps', str(crayons.yellow(' apps', bold=True)))
help = help.replace(' demos', str(crayons.yellow(' demos', bold=True)))
help = help.replace(' projects', str(crayons.yellow(' projects', bold=True)))
help = help.replace(' themes', str(crayons.yellow(' themes', bold=True)))
help = help.replace(' tools', str(crayons.yellow(' tools', bold=True)))

additional_help = \
"""Usage Examples:
Create new project based on Pinax 'account' starter project:
$ {0}
Create new project based on development version of 'blog' starter project
$ {6}
View all Pinax starter projects:
$ {1}
View all Pinax demo projects:
$ {2}
View all Pinax apps:
$ {3}
View all Pinax tools:
$ {4}
View all Pinax themes:
$ {5}
Commands:""".format(
crayons.red('pinax start account my_project'),
crayons.red('pinax projects'),
crayons.red('pinax demos'),
crayons.red('pinax apps'),
crayons.red('pinax tools'),
crayons.red('pinax themes'),
crayons.red('pinax start --dev blog my_project')
)

help = help.replace('Commands:', additional_help)

return help
32 changes: 20 additions & 12 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from setuptools import find_packages, setup
from setuptools import setup

VERSION = "1.1.1"
VERSION = "1.1.2"
LONG_DESCRIPTION = """
.. image:: http://pinaxproject.com/pinax-design/patches/pinax-blank.svg
:target: https://pypi.python.org/pypi/pinax-cli/
Expand Down Expand Up @@ -38,11 +38,16 @@
and learning more about available Pinax apps.
Supported Python Versions
-------------------------
``pinax-cli`` supports Python 2.7, 3.4, 3.5, and 3.6
Supported Django and Python Versions
------------------------------------
+-----------------+-----+-----+-----+-----+
| Django / Python | 2.7 | 3.4 | 3.5 | 3.6 |
+=================+=====+=====+=====+=====+
| 1.11 | * | * | * | * |
+-----------------+-----+-----+-----+-----+
| 2.0 | | * | * | * |
+-----------------+-----+-----+-----+-----+
"""

setup(
Expand All @@ -53,17 +58,20 @@
long_description=LONG_DESCRIPTION,
version=VERSION,
url="http://github.com/pinax/pinax-cli/",
packages=["pinaxcli"],
license="MIT",
py_modules=["pcli"],
install_requires=[
"click>=6.7",
"colorama>=0.3.9",
"crayons>=0.1.2",
'django==1.11; python_version == "2.7"',
'django>=2.0; python_version >= "3"',
"requests>=2.18.4",
],
entry_points="""
[console_scripts]
pinax=pcli:main
""",
entry_points={
"console_scripts": [
"pinax = pinaxcli.cli:main",
],
},
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
Expand Down

0 comments on commit d782627

Please sign in to comment.