Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a4ea73c
Add miniconda as a pyenv version for testing
birdcar Nov 17, 2021
0a0c8a7
Disable wordwrap my default for non python files
birdcar Nov 17, 2021
78c3d51
Create a managers subpackage and a base manager class
birdcar Nov 17, 2021
7af93c1
Add tests for the forthcoming venv-based manager
birdcar Nov 17, 2021
4593c21
Create the venv-based manager, based on the base manager API
birdcar Nov 17, 2021
7e991c2
Skip initial version of dependency installation tests
birdcar Nov 21, 2021
88edf4e
Add stubs for new (sub)package organization
birdcar Nov 21, 2021
d9ca084
Add module and package docstrings to silence linter warnings
birdcar Jan 6, 2022
14003bc
Add tox-pyenv as a plugin/dependency for testing
birdcar Jan 6, 2022
9b5ee1e
Configure tox-pyenv with a conda specific test environment
birdcar Jan 6, 2022
19eb04b
Change the kwarg on install_dependencies from 'quiet' to 'verbose'
birdcar Jan 6, 2022
39350ad
Update local variables in VenvManager to match CondaManager style
birdcar Jan 6, 2022
6b1550f
Remove custom pytest fixture in favor of tmp_path
birdcar Jan 6, 2022
fb9d5df
Make docstring for the devwrangler.managers.venv module more consistent
birdcar Jan 6, 2022
48e817d
Write initial test for the devwrangler.managers.conda:CondaManager class
birdcar Jan 6, 2022
0f1e09d
Implement the Anaconda based environment manager
birdcar Jan 6, 2022
f89fcaf
Configure logging for devwrangler and pydev
birdcar Jan 6, 2022
1f6183e
Refactor shell:run_command utility for better logging and flexibility
birdcar Jan 6, 2022
5a938bb
Add current working directory as default work_dir in Base manager
birdcar Jan 14, 2022
eb0e762
Add display utilities using Rich
birdcar Jan 14, 2022
eb2dcd9
Create first pass on new CLI api
birdcar Jan 15, 2022
5e53592
Erase old CLI code and expose the new typer app in the CLI module
birdcar Jan 15, 2022
7f9dd52
Add basic ergonomic usage example
birdcar Jan 15, 2022
9cf9a8a
Replace the application specified by pyproject.toml
birdcar Jan 15, 2022
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
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
3.9.7
3.8.12
3.7.12
miniconda3-4.7.12
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"editor.formatOnSave": true,
"editor.wordWrap": "off",
"python.formatting.provider": "black",
"python.linting.mypyEnabled": true,
"python.linting.flake8Enabled": true,
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ Bringing peace, freedom, justice, and security to your Python empire
* your Python project's virtual environment
* your project's VS Code settings

## Basic Usage

```shell
$ pydev
$ pydev create
$ pydev create --env venv
$ pydev create --env conda
$ pyenv configure
$ pydev configure --editor vscode
$ pydev configure --editor vscode --extras django,jinja2
Comment on lines +24 to +30
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a review comment

```


## Credits

This package was created with [Cookiecutter](https://github.com/audreyr/cookiecutter) and the [waynerv/cookiecutter-pypackage](https://github.com/waynerv/cookiecutter-pypackage) project template.
38 changes: 2 additions & 36 deletions devwrangler/cli.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,5 @@
"""Typer-based CLI for devwrangler."""
from pathlib import Path

import typer

from .devwrangler import (
create_virtual_environment,
parse_config_file,
set_workspace_settings,
)

APP_NAME = 'devwrangler'

PROJECT_ROOT = Path.cwd()
VENV_PATH = PROJECT_ROOT / ".venv"


def main(
req_file: str = typer.Argument('requirements-dev.txt'),
dev: bool = typer.Argument(True),
):
"""CLI main function."""
app_dir = typer.get_app_dir(APP_NAME, force_posix=True)
config_file_path: Path = Path(app_dir) / 'config.json'

# check if config data exists, if not, proceed with default config
if not config_file_path.exists():
typer.echo(f"Config file not found at {config_file_path}")
typer.echo("Using our default configuration")
config_data = parse_config_file()
else:
config_data = parse_config_file(config_file_path)

create_virtual_environment(VENV_PATH, logger=typer.echo)
set_workspace_settings(config_data, workspace_path=(PROJECT_ROOT / '.vscode'))

from .commands import app

if __name__ == '__main__':
main()
app()
25 changes: 25 additions & 0 deletions devwrangler/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Devwrangler's CLI package: pydev."""
import typer

from devwrangler.commands.configure import config_app
from devwrangler.commands.create import create_app
from devwrangler.utilities.display import STD_ERR, STD_OUT

app = typer.Typer(name="devwrangler")
app.add_typer(create_app, name='create', invoke_without_command=True)
app.add_typer(config_app, name="configure", invoke_without_command=True)


@app.callback(invoke_without_command=True)
def main(ctx: typer.Context):
"""Bringing Peace, Justice, and Security to your Python empire."""
if ctx.invoked_subcommand is None:
with STD_OUT.status("Generating environment..."):
STD_OUT.print("Gathering configuration from environment...")
STD_ERR.print("NO CONFIGURATION FOUND")
ctx.invoke(app.registered_groups)
ctx.invoke(config_app.registered_callback)


if __name__ == '__main__':
app()
42 changes: 42 additions & 0 deletions devwrangler/commands/configure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Typer command module for the `configure` command."""
from enum import Enum

import typer

from ..utilities.display import STD_OUT

config_app = typer.Typer()


class Editor(Enum):
"""Enum of valid editor options."""

vscode = "vscode"


def parse_extras(extras: str) -> tuple:
"""Parse a comma separated options list provided by the user."""
return tuple(s.strip() for s in extras.split(','))


@config_app.callback('configure')
def configure(
editor: Editor = typer.Option(
'vscode',
metavar="e",
help="The editor you'd like configured for development",
),
extras: str = typer.Option(
"black,bandit,flake8,jinja2,mypy,isort",
metavar="x",
help="Extras to enable and configure in your editor (see docs for supported options)",
callback=parse_extras,
),
):
"""Configure an editor to use an already configured dev environment."""
STD_OUT.print("Configuring VS Code...")
STD_OUT.print(f"Configuring extras {extras} in {editor.value}")


if __name__ == '__main__':
config_app()
43 changes: 43 additions & 0 deletions devwrangler/commands/create.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Typer command module for the Create command."""
from enum import Enum
from pathlib import Path

import typer

from ..managers import CondaManager, VenvManager
from ..utilities.display import STD_OUT

PROJECT_ROOT = Path.cwd()
VENV_PATH = PROJECT_ROOT / ".venv"

create_app = typer.Typer()


class Manager(Enum):
"""An Enum containing the valid managers a user may pick from."""

conda = "conda"
venv = "venv"


@create_app.callback()
def main(
env: Manager = typer.Option(
'venv',
metavar="e",
)
):
"""Create a developer environment."""
STD_OUT.print(f"[bold green]Creating {env.value} environment...[/]")
if env == Manager.venv:
venv = VenvManager()
venv.create()
venv.install_dependencies()
elif env == Manager.conda:
conda = CondaManager()
conda.create()
conda.install_dependencies()


if __name__ == '__main__':
create_app()
3 changes: 3 additions & 0 deletions devwrangler/managers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Managers exposed for use by devwrangler's CLI: pydev."""
from .conda import CondaManager
from .venv import VenvManager
35 changes: 35 additions & 0 deletions devwrangler/managers/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Defines an Abstract Base Class (ABC) to Standardize the API for various environment managers."""
import abc
import platform
from pathlib import Path

from ..utilities.shell import run_command


class BaseManager(abc.ABC):
"""A standard API for various environment managers."""

def __init__(self, work_dir: Path = Path.cwd()):
"""Initialize a virtual environment model with venv."""
self.prefixes = {
'windows': 'Scripts/python.exe',
}
self.venv_path = work_dir / ".venv"
self.cmd = run_command

@property
def prefix(self) -> Path:
"""Return the path to the python executable for the virtual environment."""
return self.venv_path.joinpath(
self.prefixes.get(platform.system().lower(), "bin/python")
)

@abc.abstractmethod
def create(self):
"""Create a sandboxed virtual environment with the specific manager."""
pass

@abc.abstractmethod
def install_dependencies(self, verbose: bool = False):
"""Install dependencies into a created environment."""
pass
52 changes: 52 additions & 0 deletions devwrangler/managers/conda.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""A wrapper for creating an Anaconda-based dev environment."""
import shutil as sh
from pathlib import Path

from .base import BaseManager


class CondaManager(BaseManager):
"""A manager for conda and it's environment."""

def create(self):
"""Create a conda environment in the location specified by venv_path."""
if not sh.which("conda"):
raise FileNotFoundError("No conda executable found in your $PATH")

self.cmd(
[
"conda",
"create",
"-y",
"--quiet",
"--prefix",
str(self.venv_path),
"python",
]
)

def install_dependencies(self, verbose: bool = False):
"""Install dependencies from conda's environment.yml file in the root of your project."""
if not sh.which("conda"):
raise FileNotFoundError("No conda executable found in your $PATH")

if not (Path.cwd() / "environment.yml").exists():
raise FileNotFoundError(
"No environment.yml file round in the project directory"
)

CONDA_CMD = [
"conda",
"env",
"update",
"--prefix",
str(self.venv_path),
"--file",
"environment.yml",
"--prune",
]

if not verbose:
CONDA_CMD.append("--quiet")

self.cmd(CONDA_CMD, critical=True)
33 changes: 33 additions & 0 deletions devwrangler/managers/venv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""A wrapper for creating a venv-based dev environment."""
import venv

from .base import BaseManager


class VenvManager(BaseManager):
"""Manage virtual environments with Python's 'venv' module."""

def create(self):
"""Create a virtual environment using venv."""
venv.create(
self.venv_path,
with_pip=True,
prompt=self.venv_path.parent.name,
)

def install_dependencies(self, verbose: bool = False):
"""Install environment dependencies."""
PIP_CMD = [
str(self.prefix),
"-m",
"pip",
"install",
"-U",
"pip",
"setuptools",
]

if not verbose:
PIP_CMD.append("-qqq")

self.cmd(PIP_CMD, critical=True)
Empty file.
2 changes: 2 additions & 0 deletions devwrangler/templates/requirements/dev.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-r base.txt
-r test.txt
2 changes: 2 additions & 0 deletions devwrangler/templates/requirements/doc.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-r base.txt
-r test.txt
1 change: 1 addition & 0 deletions devwrangler/templates/requirements/test.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-r base.txt
12 changes: 12 additions & 0 deletions devwrangler/templates/vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"recommendations": [
"ms-python.python",
"ms-python.vscode-pylance",
"hbenl.vscode-test-explorer",
"littlefoxteam.vscode-python-test-adapter",
"ms-vscode.test-adapter-converter",
"samuelcolvin.jinjahtml",
"batisteo.vscode-django",
"bungcip.better-toml"
]
}
59 changes: 59 additions & 0 deletions devwrangler/templates/vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"editor.formatOnSave": true,
"files.associations": {
"**/*.html": "html",
{% if config["use_django"] %}
"**/templates/**/*": "django-txt",
"**/templates/**/*.html": "django-html",
{% else %}
"**/templates/**/*": "jinja",
{% for ext, kind in config["jinja"]["types"].items() %}
"**/templates/**/*.html": "jinja-{{ kind }}",
{% endfor %}
{% endif %}
"**/requirements{/**,*}.{txt,in}": "pip-requirements"
},
"python.analysis.diagnosticPublishDelay": 1000,
"python.analysis.disabled": [],
"python.analysis.errors": [
"inherit-non-class",
"no-cls-argument",
"no-self-argument",
"parameter-already-specified",
"parameter-missing",
"positional-argument-after-keyword",
"positional-only-named",
"return-in-init",
"typing-generic-arguments",
"typing-typevar-arguments",
"typing-newtype-arguments",
"unresolved-import",
"undefined-variable"
],
"python.analysis.warnings": [
"unknown-parameter-name",
"variable-not-defined-globally",
"variable-not-defined-nonlocal"
],
"python.analysis.information": [
"too-many-function-arguments",
"too-many-positional-arguments-before-star",
"no-method-argument"
],
"python.autoComplete.addBrackets": true,
"python.formatting.provider": "black",
"python.languageServer": "Pylance",
"python.linting.banditEnabled": true,
"python.linting.flake8Enabled": true,
"python.linting.flake8Args": ["--max-line-length", "88"],
"python.linting.mypyEnabled": false,
"python.sortImports.path": "isort",
"python.sortImports.args": ["--profile", "black"],
"[python]": {
"editor.insertSpaces": true,
"editor.tabSize": 4,
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
}
}
1 change: 1 addition & 0 deletions devwrangler/utilities/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Functional utilities for devwrangler."""
1 change: 1 addition & 0 deletions devwrangler/utilities/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Configuration utility functions for devwrangler."""
Loading