mirror of https://github.com/pypa/hatch.git
1421 lines
52 KiB
Python
1421 lines
52 KiB
Python
import io
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from tempfile import TemporaryDirectory
|
|
|
|
import click
|
|
from twine.utils import DEFAULT_REPOSITORY, TEST_REPOSITORY
|
|
|
|
from hatch.build import build_package
|
|
from hatch.clean import clean_package, remove_compiled_scripts
|
|
from hatch.create import create_package
|
|
from hatch.env import (
|
|
get_editable_packages, get_editable_package_location, get_installed_packages,
|
|
get_python_version, get_python_implementation, install_packages
|
|
)
|
|
from hatch.grow import BUMP, bump_package_version
|
|
from hatch.settings import (
|
|
SETTINGS_FILE, copy_default_settings, load_settings, restore_settings,
|
|
save_settings
|
|
)
|
|
from hatch.shells import run_shell
|
|
from hatch.utils import (
|
|
NEED_SUBPROCESS_SHELL, ON_WINDOWS, basepath, chdir, get_admin_command,
|
|
get_proper_pip, get_proper_python, get_random_venv_name, get_requirements_file,
|
|
remove_path, resolve_path, venv_active
|
|
)
|
|
from hatch.venv import (
|
|
VENV_DIR, clone_venv, create_venv, fix_available_venvs, get_available_venvs, venv
|
|
)
|
|
|
|
|
|
CONTEXT_SETTINGS = {
|
|
'help_option_names': ['-h', '--help'],
|
|
}
|
|
UNKNOWN_OPTIONS = {
|
|
'ignore_unknown_options': True,
|
|
**CONTEXT_SETTINGS
|
|
}
|
|
|
|
|
|
def echo_success(text):
|
|
click.secho(text, fg='cyan', bold=True)
|
|
|
|
|
|
def echo_failure(text):
|
|
click.secho(text, fg='red', bold=True)
|
|
|
|
|
|
def echo_warning(text):
|
|
click.secho(text, fg='yellow', bold=True)
|
|
|
|
|
|
def echo_waiting(text):
|
|
click.secho(text, fg='magenta', bold=True)
|
|
|
|
|
|
@click.group(context_settings=CONTEXT_SETTINGS)
|
|
@click.version_option()
|
|
def hatch():
|
|
pass
|
|
|
|
|
|
@hatch.command(context_settings=CONTEXT_SETTINGS,
|
|
short_help='Locates, updates, or restores the config file')
|
|
@click.option('-u', '--update', 'update_settings', is_flag=True,
|
|
help='Updates the config file with any new fields.')
|
|
@click.option('--restore', is_flag=True,
|
|
help='Restores the config file to default settings.')
|
|
def config(update_settings, restore):
|
|
"""Locates, updates, or restores the config file.
|
|
|
|
\b
|
|
$ hatch config
|
|
Settings location: /home/ofek/.local/share/hatch/settings.json
|
|
"""
|
|
if update_settings:
|
|
try:
|
|
user_settings = load_settings()
|
|
updated_settings = copy_default_settings()
|
|
updated_settings.update(user_settings)
|
|
save_settings(updated_settings)
|
|
echo_success('Settings were successfully updated.')
|
|
except FileNotFoundError:
|
|
restore = True
|
|
|
|
if restore:
|
|
restore_settings()
|
|
echo_success('Settings were successfully restored.')
|
|
|
|
echo_success('Settings location: ' + SETTINGS_FILE)
|
|
|
|
|
|
@hatch.command(context_settings=CONTEXT_SETTINGS,
|
|
short_help='Creates a new Python project')
|
|
@click.argument('name')
|
|
@click.argument('new_env', required=False)
|
|
@click.option('-e', '--env', 'env_name',
|
|
help=(
|
|
'Forward-slash-separated list of named virtual envs to be '
|
|
"installed in. Will create any that don't already exist."
|
|
))
|
|
@click.option('--basic', is_flag=True,
|
|
help='Disables third-party services and readme badges.')
|
|
@click.option('--cli', is_flag=True,
|
|
help=(
|
|
'Creates a `cli.py` in the package directory and an entry '
|
|
'point in `setup.py` pointing to the properly named function '
|
|
'within. Also, a `__main__.py` is created so it can be '
|
|
'invoked via `python -m pkg_name`.'
|
|
))
|
|
@click.option('-l', '--licenses',
|
|
help='Comma-separated list of licenses to use.')
|
|
def new(name, new_env, env_name, basic, cli, licenses):
|
|
"""Creates a new Python project.
|
|
|
|
Values from your config file such as `name` and `pyversions` will be used
|
|
to help populate fields. You can also specify things like the readme format
|
|
and which CI service files to create. All options override the config file.
|
|
|
|
You can also locally install the created project in a virtual env using
|
|
the optional argument or the --env option. If the virtual env for the
|
|
optional argument already exists, an error will be raised.
|
|
|
|
Here is an example using an unmodified config file:
|
|
|
|
\b
|
|
$ hatch new my-app
|
|
Created project `my-app`
|
|
$ tree --dirsfirst my-app
|
|
my-app
|
|
├── my_app
|
|
│ └── __init__.py
|
|
├── tests
|
|
│ └── __init__.py
|
|
├── LICENSE-APACHE
|
|
├── LICENSE-MIT
|
|
├── MANIFEST.in
|
|
├── README.rst
|
|
├── requirements.txt
|
|
├── setup.py
|
|
└── tox.ini
|
|
|
|
2 directories, 8 files
|
|
"""
|
|
try:
|
|
settings = load_settings()
|
|
except FileNotFoundError:
|
|
echo_failure('Unable to locate config file. Try `hatch config --restore`.')
|
|
sys.exit(1)
|
|
|
|
venvs = env_name.split('/') if env_name else []
|
|
if new_env:
|
|
venv_dir = os.path.join(VENV_DIR, new_env)
|
|
if os.path.exists(venv_dir):
|
|
echo_failure('Virtual env `{name}` already exists. To remove '
|
|
'it do `hatch shed -e {name}`.'.format(name=new_env))
|
|
sys.exit(1)
|
|
venvs.insert(0, new_env)
|
|
|
|
if basic:
|
|
settings['basic'] = True
|
|
|
|
if licenses:
|
|
settings['licenses'] = licenses.split(',')
|
|
|
|
settings['cli'] = cli
|
|
|
|
origin = os.getcwd()
|
|
d = os.path.join(origin, name)
|
|
|
|
if os.path.exists(d):
|
|
echo_failure('Directory `{}` already exists.'.format(d))
|
|
sys.exit(1)
|
|
|
|
os.makedirs(d)
|
|
with chdir(d, cwd=origin):
|
|
create_package(d, name, settings)
|
|
echo_success('Created project `{}`'.format(name))
|
|
|
|
for vname in venvs:
|
|
venv_dir = os.path.join(VENV_DIR, vname)
|
|
if not os.path.exists(venv_dir):
|
|
echo_waiting('Creating virtual env `{}`...'.format(vname))
|
|
create_venv(venv_dir)
|
|
|
|
with venv(venv_dir):
|
|
echo_waiting('Installing locally in virtual env `{}`...'.format(vname))
|
|
install_packages(['-q', '-e', '.'])
|
|
|
|
|
|
@hatch.command(context_settings=CONTEXT_SETTINGS,
|
|
short_help='Creates a new Python project in the current directory')
|
|
@click.argument('name')
|
|
@click.argument('new_env', required=False)
|
|
@click.option('-e', '--env', 'env_name',
|
|
help=(
|
|
'Forward-slash-separated list of named virtual envs to be '
|
|
"installed in. Will create any that don't already exist."
|
|
))
|
|
@click.option('--basic', is_flag=True,
|
|
help='Disables third-party services and readme badges.')
|
|
@click.option('--cli', is_flag=True,
|
|
help=(
|
|
'Creates a `cli.py` in the package directory and an entry '
|
|
'point in `setup.py` pointing to the properly named function '
|
|
'within. Also, a `__main__.py` is created so it can be '
|
|
'invoked via `python -m pkg_name`.'
|
|
))
|
|
@click.option('-l', '--licenses',
|
|
help='Comma-separated list of licenses to use.')
|
|
def init(name, new_env, env_name, basic, cli, licenses):
|
|
"""Creates a new Python project in the current directory.
|
|
|
|
Values from your config file such as `name` and `pyversions` will be used
|
|
to help populate fields. You can also specify things like the readme format
|
|
and which CI service files to create. All options override the config file.
|
|
|
|
You can also locally install the created project in a virtual env using
|
|
the optional argument or the --env option. If the virtual env for the
|
|
optional argument already exists, an error will be raised.
|
|
|
|
Here is an example using an unmodified config file:
|
|
|
|
\b
|
|
$ hatch init my-app
|
|
Created project `my-app` here
|
|
$ tree --dirsfirst .
|
|
.
|
|
├── my_app
|
|
│ └── __init__.py
|
|
├── tests
|
|
│ └── __init__.py
|
|
├── LICENSE-APACHE
|
|
├── LICENSE-MIT
|
|
├── MANIFEST.in
|
|
├── README.rst
|
|
├── requirements.txt
|
|
├── setup.py
|
|
└── tox.ini
|
|
|
|
2 directories, 8 files
|
|
"""
|
|
try:
|
|
settings = load_settings()
|
|
except FileNotFoundError:
|
|
echo_failure('Unable to locate config file. Try `hatch config --restore`.')
|
|
sys.exit(1)
|
|
|
|
venvs = env_name.split('/') if env_name else []
|
|
if new_env:
|
|
venv_dir = os.path.join(VENV_DIR, new_env)
|
|
if os.path.exists(venv_dir):
|
|
echo_failure('Virtual env `{name}` already exists. To remove '
|
|
'it do `hatch shed -e {name}`.'.format(name=new_env))
|
|
sys.exit(1)
|
|
venvs.insert(0, new_env)
|
|
|
|
if basic:
|
|
settings['basic'] = True
|
|
|
|
if licenses:
|
|
settings['licenses'] = licenses.split(',')
|
|
|
|
settings['cli'] = cli
|
|
|
|
create_package(os.getcwd(), name, settings)
|
|
echo_success('Created project `{}` here'.format(name))
|
|
|
|
for vname in venvs:
|
|
venv_dir = os.path.join(VENV_DIR, vname)
|
|
if not os.path.exists(venv_dir):
|
|
echo_waiting('Creating virtual env `{}`...'.format(vname))
|
|
create_venv(venv_dir)
|
|
|
|
with venv(venv_dir):
|
|
echo_waiting('Installing locally in virtual env `{}`...'.format(vname))
|
|
install_packages(['-q', '-e', '.'])
|
|
|
|
|
|
@hatch.command(context_settings=CONTEXT_SETTINGS, short_help='Installs packages')
|
|
@click.argument('packages', nargs=-1)
|
|
@click.option('-e', '--env', 'env_name', help='The named virtual env to use.')
|
|
@click.option('-l', '--local', 'editable', is_flag=True,
|
|
help=(
|
|
"Corresponds to pip's --editable option, allowing a local "
|
|
"package to be automatically updated when modifications "
|
|
"are made."
|
|
))
|
|
@click.option('-g', '--global', 'global_install', is_flag=True,
|
|
help=(
|
|
'Installs globally, rather than on a per-user basis. This '
|
|
'has no effect if a virtual env is in use.'
|
|
))
|
|
@click.option('-q', '--quiet', is_flag=True, help='Decreases verbosity.')
|
|
def install(packages, env_name, editable, global_install, quiet):
|
|
"""If the option --env is supplied, the install will be applied using
|
|
that named virtual env. Unless the option --global is selected, the
|
|
install will only affect the current user. Of course, this will have
|
|
no effect if a virtual env is in use. The desired name of the admin
|
|
user can be set with the `_DEFAULT_ADMIN_` environment variable.
|
|
|
|
With no packages selected, this will install using a `setup.py` in the
|
|
current directory.
|
|
"""
|
|
packages = packages or ['.']
|
|
|
|
# Windows' `runas` allows only a single argument for the
|
|
# command so we catch this case and turn our command into
|
|
# a string later.
|
|
windows_admin_command = None
|
|
|
|
if editable:
|
|
packages = ['-e', *packages]
|
|
|
|
if env_name:
|
|
venv_dir = os.path.join(VENV_DIR, env_name)
|
|
if not os.path.exists(venv_dir):
|
|
echo_failure('Virtual env named `{}` does not exist.'.format(env_name))
|
|
sys.exit(1)
|
|
|
|
with venv(venv_dir):
|
|
command = [get_proper_pip(), 'install', *packages] + (['-q'] if quiet else [])
|
|
echo_waiting('Installing in virtual env `{}`...'.format(env_name))
|
|
result = subprocess.run(command, shell=NEED_SUBPROCESS_SHELL)
|
|
else:
|
|
command = [get_proper_pip(), 'install'] + (['-q'] if quiet else [])
|
|
|
|
if not venv_active(): # no cov
|
|
if global_install:
|
|
if ON_WINDOWS:
|
|
windows_admin_command = get_admin_command()
|
|
else:
|
|
command = get_admin_command() + command
|
|
else:
|
|
command.append('--user')
|
|
|
|
command.extend(packages)
|
|
|
|
if windows_admin_command: # no cov
|
|
command = windows_admin_command + [' '.join(command)]
|
|
|
|
echo_waiting('Installing...')
|
|
result = subprocess.run(command, shell=NEED_SUBPROCESS_SHELL)
|
|
|
|
sys.exit(result.returncode)
|
|
|
|
|
|
@hatch.command(context_settings=CONTEXT_SETTINGS, short_help='Uninstalls packages')
|
|
@click.argument('packages', nargs=-1)
|
|
@click.option('-e', '--env', 'env_name', help='The named virtual env to use.')
|
|
@click.option('-g', '--global', 'global_uninstall', is_flag=True,
|
|
help=(
|
|
'Uninstalls globally, rather than on a per-user basis. This '
|
|
'has no effect if a virtual env is in use.'
|
|
))
|
|
@click.option('-d', '--dev', is_flag=True,
|
|
help='When locating a requirements file, only use the dev version.')
|
|
@click.option('-q', '--quiet', is_flag=True, help='Decreases verbosity.')
|
|
@click.option('-y', '--yes', is_flag=True,
|
|
help='Confirms the intent to uninstall without a prompt.')
|
|
def uninstall(packages, env_name, global_uninstall, dev, quiet, yes):
|
|
"""If the option --env is supplied, the uninstall will be applied using
|
|
that named virtual env. Unless the option --global is selected, the
|
|
uninstall will only affect the current user. Of course, this will have
|
|
no effect if a virtual env is in use. The desired name of the admin
|
|
user can be set with the `_DEFAULT_ADMIN_` environment variable.
|
|
|
|
With no packages selected, this will uninstall using a `requirements.txt`
|
|
or a dev version of that in the current directory.
|
|
"""
|
|
if not packages:
|
|
reqs = get_requirements_file(os.getcwd(), dev=dev)
|
|
if not reqs:
|
|
echo_failure('Unable to locate a requirements file.')
|
|
sys.exit(1)
|
|
|
|
packages = ['-r', reqs]
|
|
|
|
# Windows' `runas` allows only a single argument for the
|
|
# command so we catch this case and turn our command into
|
|
# a string later.
|
|
windows_admin_command = None
|
|
|
|
if yes: # no cov
|
|
packages = ['-y', *packages]
|
|
|
|
if env_name:
|
|
venv_dir = os.path.join(VENV_DIR, env_name)
|
|
if not os.path.exists(venv_dir):
|
|
echo_failure('Virtual env named `{}` does not exist.'.format(env_name))
|
|
sys.exit(1)
|
|
|
|
with venv(venv_dir):
|
|
command = [get_proper_pip(), 'uninstall', *packages] + (['-q'] if quiet else [])
|
|
echo_waiting('Uninstalling in virtual env `{}`...'.format(env_name))
|
|
result = subprocess.run(command, shell=NEED_SUBPROCESS_SHELL)
|
|
else:
|
|
command = [get_proper_pip(), 'uninstall'] + (['-q'] if quiet else [])
|
|
|
|
if not venv_active() and global_uninstall: # no cov
|
|
if ON_WINDOWS:
|
|
windows_admin_command = get_admin_command()
|
|
else:
|
|
command = get_admin_command() + command
|
|
|
|
command.extend(packages)
|
|
|
|
if windows_admin_command: # no cov
|
|
command = windows_admin_command + [' '.join(command)]
|
|
|
|
echo_waiting('Uninstalling...')
|
|
result = subprocess.run(command, shell=NEED_SUBPROCESS_SHELL)
|
|
|
|
sys.exit(result.returncode)
|
|
|
|
|
|
@hatch.command(context_settings=CONTEXT_SETTINGS, short_help='Updates packages')
|
|
@click.argument('packages', nargs=-1)
|
|
@click.option('-e', '--env', 'env_name', help='The named virtual env to use.')
|
|
@click.option('--eager', is_flag=True,
|
|
help=(
|
|
'Updates all dependencies regardless of whether they '
|
|
'still satisfy the new parent requirements. See: '
|
|
'https://github.com/pypa/pip/pull/3972'
|
|
))
|
|
@click.option('--all', 'all_packages', is_flag=True,
|
|
help=(
|
|
'Updates all currently installed packages. The packages '
|
|
'`pip`, `setuptools`, and `wheel` are excluded.'
|
|
))
|
|
@click.option('--infra', is_flag=True,
|
|
help='Updates only the packages `pip`, `setuptools`, and `wheel`.')
|
|
@click.option('-g', '--global', 'global_install', is_flag=True,
|
|
help=(
|
|
'Updates globally, rather than on a per-user basis. This '
|
|
'has no effect if a virtual env is in use.'
|
|
))
|
|
@click.option('-f', '--force', is_flag=True,
|
|
help='Forces the use of newer features in global updates.')
|
|
@click.option('-d', '--dev', is_flag=True,
|
|
help='When locating a requirements file, only use the dev version.')
|
|
@click.option('-m', '--module', 'as_module', is_flag=True,
|
|
help=(
|
|
'Invokes `pip` as a module instead of directly, i.e. '
|
|
'`python -m pip`.'
|
|
))
|
|
@click.option('--self', is_flag=True, help='Updates `hatch` itself.')
|
|
@click.option('-q', '--quiet', is_flag=True, help='Decreases verbosity.')
|
|
def update(packages, env_name, eager, all_packages, infra, global_install,
|
|
force, dev, as_module, self, quiet):
|
|
"""If the option --env is supplied, the update will be applied using
|
|
that named virtual env. Unless the option --global is selected, the
|
|
update will only affect the current user. Of course, this will have
|
|
no effect if a virtual env is in use. The desired name of the admin
|
|
user can be set with the `_DEFAULT_ADMIN_` environment variable.
|
|
|
|
When performing a global update, your system may use an older version
|
|
of pip that is incompatible with some features such as --eager. To
|
|
force the use of these features, use --force.
|
|
|
|
With no packages nor options selected, this will update packages by
|
|
looking for a `requirements.txt` or a dev version of that in the current
|
|
directory.
|
|
|
|
To update this tool, use the --self flag. All other methods of updating will
|
|
ignore `hatch`. See: https://github.com/pypa/pip/issues/1299
|
|
"""
|
|
command = ['install', '--upgrade'] + (['-q'] if quiet else [])
|
|
if not global_install or force: # no cov
|
|
command.extend(['--upgrade-strategy', 'eager' if eager else 'only-if-needed'])
|
|
|
|
infra_packages = ['pip', 'setuptools', 'wheel']
|
|
temp_dir = None
|
|
|
|
# Windows' `runas` allows only a single argument for the
|
|
# command so we catch this case and turn our command into
|
|
# a string later.
|
|
windows_admin_command = None
|
|
|
|
if self: # no cov
|
|
as_module = True
|
|
|
|
if env_name and not self:
|
|
venv_dir = os.path.join(VENV_DIR, env_name)
|
|
if not os.path.exists(venv_dir):
|
|
echo_failure('Virtual env named `{}` does not exist.'.format(env_name))
|
|
sys.exit(1)
|
|
|
|
with venv(venv_dir):
|
|
executable = (
|
|
[get_proper_python(), '-m', 'pip']
|
|
if as_module or (infra and ON_WINDOWS)
|
|
else [get_proper_pip()]
|
|
)
|
|
command = executable + command
|
|
if all_packages:
|
|
installed_packages = infra_packages if infra else get_installed_packages()
|
|
else:
|
|
installed_packages = None
|
|
else:
|
|
venv_dir = None
|
|
executable = (
|
|
[sys.executable if self else get_proper_python(), '-m', 'pip']
|
|
if as_module or (infra and ON_WINDOWS)
|
|
else [get_proper_pip()]
|
|
)
|
|
command = executable + command
|
|
if all_packages:
|
|
installed_packages = infra_packages if infra else get_installed_packages()
|
|
else:
|
|
installed_packages = None
|
|
|
|
if not venv_active(): # no cov
|
|
if global_install:
|
|
if ON_WINDOWS:
|
|
windows_admin_command = get_admin_command()
|
|
else:
|
|
command = get_admin_command() + command
|
|
else:
|
|
command.append('--user')
|
|
|
|
if self: # no cov
|
|
command.append('hatch')
|
|
if ON_WINDOWS:
|
|
echo_warning('After the update you may want to press Enter to flush stdout.')
|
|
subprocess.Popen(command, shell=NEED_SUBPROCESS_SHELL)
|
|
sys.exit()
|
|
else:
|
|
result = subprocess.run(command, shell=NEED_SUBPROCESS_SHELL)
|
|
sys.exit(result.returncode)
|
|
elif infra:
|
|
command.extend(infra_packages)
|
|
elif all_packages:
|
|
installed_packages = [
|
|
package for package in installed_packages
|
|
if package not in infra_packages and package != 'hatch'
|
|
]
|
|
if not installed_packages:
|
|
echo_failure('No packages installed.')
|
|
sys.exit(1)
|
|
command.extend(installed_packages)
|
|
elif packages:
|
|
packages = [package for package in packages if package != 'hatch']
|
|
if not packages:
|
|
echo_failure('No packages to install.')
|
|
sys.exit(1)
|
|
command.extend(packages)
|
|
|
|
# When https://github.com/pypa/pipfile is finalized, we'll use it.
|
|
else:
|
|
reqs = get_requirements_file(os.getcwd(), dev=dev)
|
|
if not reqs:
|
|
echo_failure('Unable to locate a requirements file.')
|
|
sys.exit(1)
|
|
|
|
with open(reqs, 'r') as f:
|
|
lines = f.readlines()
|
|
|
|
matches = []
|
|
for line in lines:
|
|
match = re.match(r'^[^=<>]+', line.lstrip())
|
|
if match and match.group(0) == 'hatch':
|
|
matches.append(line)
|
|
|
|
if matches:
|
|
for line in matches:
|
|
lines.remove(line)
|
|
|
|
temp_dir = TemporaryDirectory()
|
|
reqs = os.path.join(temp_dir.name, basepath(reqs))
|
|
|
|
with open(reqs, 'w') as f:
|
|
f.writelines(lines)
|
|
|
|
command.extend(['-r', reqs])
|
|
|
|
if windows_admin_command: # no cov
|
|
command = windows_admin_command + [' '.join(command)]
|
|
|
|
if venv_dir:
|
|
with venv(venv_dir):
|
|
echo_waiting('Updating virtual env `{}`...'.format(env_name))
|
|
result = subprocess.run(command, shell=NEED_SUBPROCESS_SHELL)
|
|
else:
|
|
echo_waiting('Updating...')
|
|
result = subprocess.run(command, shell=NEED_SUBPROCESS_SHELL)
|
|
|
|
if temp_dir is not None:
|
|
temp_dir.cleanup()
|
|
|
|
sys.exit(result.returncode)
|
|
|
|
|
|
@hatch.command(context_settings=CONTEXT_SETTINGS,
|
|
short_help="Increments a project's version")
|
|
@click.argument('part', type=click.Choice(BUMP.keys()))
|
|
@click.argument('package', required=False)
|
|
@click.option('-p', '--path', help='A relative or absolute path to a project or file.')
|
|
@click.option('--pre', 'pre_token',
|
|
help='The token to use for `pre` part, overriding the config file. Default: rc')
|
|
@click.option('--build', 'build_token',
|
|
help='The token to use for `build` part, overriding the config file. Default: build')
|
|
def grow(part, package, path, pre_token, build_token):
|
|
"""Increments a project's version number using semantic versioning.
|
|
Valid choices for the part are `major`, `minor`, `patch` (`fix` alias),
|
|
`pre`, and `build`.
|
|
|
|
The path to the project is derived in the following order:
|
|
|
|
\b
|
|
1. The optional argument, which should be the name of a package
|
|
that was installed via `hatch install -l` or `pip install -e`.
|
|
2. The option --path, which can be a relative or absolute path.
|
|
3. The current directory.
|
|
|
|
If the path is a file, it will be the target. Otherwise, the path, and
|
|
every top level directory within, will be checked for a `__version__.py`,
|
|
`__about__.py`, and `__init__.py`, in that order. The first encounter of
|
|
a `__version__` variable that also appears to equal a version string will
|
|
be updated. Probable package paths will be given precedence.
|
|
|
|
The default tokens for the prerelease and build parts, `rc` and `build`
|
|
respectively, can be altered via the options `--pre` and `--build`, or
|
|
the config entry `semver`.
|
|
|
|
\b
|
|
$ git clone -q https://github.com/requests/requests && cd requests
|
|
$ hatch grow build
|
|
Updated /home/ofek/requests/requests/__version__.py
|
|
2.18.4 -> 2.18.4+build.1
|
|
$ hatch grow fix
|
|
Updated /home/ofek/requests/requests/__version__.py
|
|
2.18.4+build.1 -> 2.18.5
|
|
$ hatch grow pre
|
|
Updated /home/ofek/requests/requests/__version__.py
|
|
2.18.5 -> 2.18.5-rc.1
|
|
$ hatch grow minor
|
|
Updated /home/ofek/requests/requests/__version__.py
|
|
2.18.5-rc.1 -> 2.19.0
|
|
$ hatch grow major
|
|
Updated /home/ofek/requests/requests/__version__.py
|
|
2.19.0 -> 3.0.0
|
|
"""
|
|
if package:
|
|
path = get_editable_package_location(package)
|
|
if not path:
|
|
echo_failure('`{}` is not an editable package.'.format(package))
|
|
sys.exit(1)
|
|
elif path:
|
|
possible_path = resolve_path(path)
|
|
if not possible_path:
|
|
echo_failure('Directory `{}` does not exist.'.format(path))
|
|
sys.exit(1)
|
|
path = possible_path
|
|
else:
|
|
path = os.getcwd()
|
|
|
|
settings = load_settings(lazy=True)
|
|
pre_token = pre_token or settings.get('semver', {}).get('pre')
|
|
build_token = build_token or settings.get('semver', {}).get('build')
|
|
|
|
f, old_version, new_version = bump_package_version(
|
|
path, part, pre_token, build_token
|
|
)
|
|
|
|
if new_version:
|
|
echo_success('Updated {}'.format(f))
|
|
echo_success('{} -> {}'.format(old_version, new_version))
|
|
else:
|
|
if f:
|
|
echo_failure('Found version files:')
|
|
for file in f:
|
|
echo_warning(file)
|
|
echo_failure('\nUnable to find a version specifier.')
|
|
sys.exit(1)
|
|
else:
|
|
echo_failure('No version files found.')
|
|
sys.exit(1)
|
|
|
|
|
|
@hatch.command(context_settings=CONTEXT_SETTINGS, short_help='Runs tests')
|
|
@click.argument('package', required=False)
|
|
@click.option('-p', '--path',
|
|
help='A relative or absolute path to a project or test directory.')
|
|
@click.option('-c', '--cov', is_flag=True,
|
|
help='Computes, then outputs coverage after testing.')
|
|
@click.option('-m', '--merge', is_flag=True,
|
|
help=(
|
|
'If --cov, coverage will run using --parallel-mode '
|
|
'and combine the results.'
|
|
))
|
|
@click.option('-ta', '--test-args', default='',
|
|
help=(
|
|
'Pass through to `pytest`, overriding defaults. Example: '
|
|
'`hatch test -ta "-k test_core.py -vv"`'
|
|
))
|
|
@click.option('-ca', '--cov-args',
|
|
help=(
|
|
'Pass through to `coverage run`, overriding defaults. '
|
|
'Example: `hatch test -ca "--timid --pylib"`'
|
|
))
|
|
@click.option('-g', '--global', 'global_exe', is_flag=True,
|
|
help=(
|
|
'Uses the `pytest` and `coverage` shipped with Hatch instead '
|
|
'of environment-aware modules. This is useful if you just want '
|
|
'want to run a quick test without installing these again in a '
|
|
'virtual env. Keep in mind these will be the Python 3 versions.'
|
|
))
|
|
def test(package, path, cov, merge, test_args, cov_args, global_exe):
|
|
"""Runs tests using `pytest`, optionally checking coverage.
|
|
|
|
The path is derived in the following order:
|
|
|
|
\b
|
|
1. The optional argument, which should be the name of a package
|
|
that was installed via `hatch install -l` or `pip install -e`.
|
|
2. The option --path, which can be a relative or absolute path.
|
|
3. The current directory.
|
|
|
|
If the path points to a package, it should have a `tests` directory.
|
|
|
|
\b
|
|
$ git clone https://github.com/ofek/privy && cd privy
|
|
$ hatch test -c
|
|
========================= test session starts ==========================
|
|
platform linux -- Python 3.5.2, pytest-3.2.1, py-1.4.34, pluggy-0.4.0
|
|
rootdir: /home/ofek/privy, inifile:
|
|
plugins: xdist-1.20.0, mock-1.6.2, httpbin-0.0.7, forked-0.2, cov-2.5.1
|
|
collected 10 items
|
|
|
|
\b
|
|
tests/test_privy.py ..........
|
|
|
|
\b
|
|
====================== 10 passed in 4.34 seconds =======================
|
|
|
|
\b
|
|
Tests completed, checking coverage...
|
|
|
|
\b
|
|
Name Stmts Miss Branch BrPart Cover Missing
|
|
-----------------------------------------------------------------
|
|
privy/__init__.py 1 0 0 0 100%
|
|
privy/core.py 30 0 0 0 100%
|
|
privy/utils.py 13 0 4 0 100%
|
|
tests/__init__.py 0 0 0 0 100%
|
|
tests/test_privy.py 57 0 0 0 100%
|
|
-----------------------------------------------------------------
|
|
TOTAL 101 0 4 0 100%
|
|
"""
|
|
if package:
|
|
path = get_editable_package_location(package)
|
|
if not path:
|
|
echo_failure('`{}` is not an editable package.'.format(package))
|
|
sys.exit(1)
|
|
elif path:
|
|
possible_path = resolve_path(path)
|
|
if not possible_path:
|
|
echo_failure('Directory `{}` does not exist.'.format(path))
|
|
sys.exit(1)
|
|
path = possible_path
|
|
else:
|
|
path = os.getcwd()
|
|
|
|
python_cmd = [sys.executable if global_exe else get_proper_python(), '-m']
|
|
command = python_cmd.copy()
|
|
|
|
if cov:
|
|
command.extend(['coverage', 'run'])
|
|
command.extend(
|
|
cov_args.split() if cov_args is not None
|
|
else (['--parallel-mode'] if merge else [])
|
|
)
|
|
command.append('-m')
|
|
|
|
command.append('pytest')
|
|
command.extend(test_args.split())
|
|
|
|
try: # no cov
|
|
sys.stdout.fileno()
|
|
testing = False
|
|
except io.UnsupportedOperation: # no cov
|
|
testing = True
|
|
|
|
# For testing we need to pipe because Click changes stdio streams.
|
|
stdout = sys.stdout if not testing else subprocess.PIPE
|
|
stderr = sys.stderr if not testing else subprocess.PIPE
|
|
|
|
with chdir(path):
|
|
output = b''
|
|
|
|
test_result = subprocess.run(
|
|
command,
|
|
stdout=stdout, stderr=stderr,
|
|
shell=NEED_SUBPROCESS_SHELL
|
|
)
|
|
output += test_result.stdout or b''
|
|
output += test_result.stderr or b''
|
|
|
|
if cov:
|
|
click.echo('\nTests completed, checking coverage...\n')
|
|
|
|
if merge:
|
|
result = subprocess.run(
|
|
python_cmd + ['coverage', 'combine', '--append'],
|
|
stdout=stdout, stderr=stderr,
|
|
shell=NEED_SUBPROCESS_SHELL
|
|
)
|
|
output += result.stdout or b''
|
|
output += result.stderr or b''
|
|
|
|
result = subprocess.run(
|
|
python_cmd + ['coverage', 'report', '--show-missing'],
|
|
stdout=stdout, stderr=stderr,
|
|
shell=NEED_SUBPROCESS_SHELL
|
|
)
|
|
output += result.stdout or b''
|
|
output += result.stderr or b''
|
|
|
|
if testing: # no cov
|
|
click.echo(output.decode())
|
|
click.echo(output.decode())
|
|
|
|
sys.exit(test_result.returncode)
|
|
|
|
|
|
@hatch.command(context_settings=CONTEXT_SETTINGS,
|
|
short_help="Removes a project's build artifacts")
|
|
@click.argument('package', required=False)
|
|
@click.option('-p', '--path', help='A relative or absolute path to a project.')
|
|
@click.option('-c', '--compiled-only', is_flag=True,
|
|
help='Removes only .pyc files.')
|
|
@click.option('-v', '--verbose', is_flag=True, help='Shows removed paths.')
|
|
def clean(package, path, compiled_only, verbose):
|
|
"""Removes a project's build artifacts.
|
|
|
|
The path to the project is derived in the following order:
|
|
|
|
\b
|
|
1. The optional argument, which should be the name of a package
|
|
that was installed via `hatch install -l` or `pip install -e`.
|
|
2. The option --path, which can be a relative or absolute path.
|
|
3. The current directory.
|
|
|
|
All `*.pyc`/`*.pyd`/`*.pyo` files and `__pycache__` directories will be removed.
|
|
Additionally, the following patterns will be removed from the root of the path:
|
|
`.cache`, `.coverage`, `.eggs`, `.tox`, `build`, `dist`, and `*.egg-info`.
|
|
|
|
If the path was derived from the optional package argument, the pattern
|
|
`*.egg-info` will not be applied so as to not break that installation.
|
|
"""
|
|
if package:
|
|
path = get_editable_package_location(package)
|
|
if not path:
|
|
echo_failure('`{}` is not an editable package.'.format(package))
|
|
sys.exit(1)
|
|
elif path:
|
|
possible_path = resolve_path(path)
|
|
if not possible_path:
|
|
echo_failure('Directory `{}` does not exist.'.format(path))
|
|
sys.exit(1)
|
|
path = possible_path
|
|
else:
|
|
path = os.getcwd()
|
|
|
|
if compiled_only:
|
|
removed_paths = remove_compiled_scripts(path)
|
|
else:
|
|
removed_paths = clean_package(path, editable=package)
|
|
|
|
if verbose:
|
|
if removed_paths:
|
|
click.echo('Removed paths:')
|
|
for p in removed_paths:
|
|
click.echo(p)
|
|
|
|
|
|
@hatch.command(context_settings=CONTEXT_SETTINGS, short_help='Builds a project')
|
|
@click.argument('package', required=False)
|
|
@click.option('-p', '--path', help='A relative or absolute path to a project.')
|
|
@click.option('-py', '--python', 'pyname',
|
|
help='The named Python path to use. This overrides --pypath.')
|
|
@click.option('-pp', '--pypath',
|
|
help='An absolute path to a Python executable.')
|
|
@click.option('-u', '--universal', is_flag=True,
|
|
help='Indicates compatibility with both Python 2 and 3.')
|
|
@click.option('-n', '--name',
|
|
help='Forces a particular platform name, e.g. linux_x86_64.')
|
|
@click.option('-d', '--build-dir',
|
|
help='A relative or absolute path to the desired build directory.')
|
|
@click.option('-c', '--clean', 'clean_first', is_flag=True,
|
|
help='Removes build artifacts before building.')
|
|
@click.option('-v', '--verbose', is_flag=True, help='Increases verbosity.')
|
|
def build(package, path, pyname, pypath, universal, name, build_dir,
|
|
clean_first, verbose):
|
|
"""Builds a project, producing a source distribution and a wheel.
|
|
|
|
The path to the project is derived in the following order:
|
|
|
|
\b
|
|
1. The optional argument, which should be the name of a package
|
|
that was installed via `hatch install -l` or `pip install -e`.
|
|
2. The option --path, which can be a relative or absolute path.
|
|
3. The current directory.
|
|
|
|
The path must contain a `setup.py` file.
|
|
"""
|
|
if package:
|
|
path = get_editable_package_location(package)
|
|
if not path:
|
|
echo_failure('`{}` is not an editable package.'.format(package))
|
|
sys.exit(1)
|
|
elif path:
|
|
possible_path = resolve_path(path)
|
|
if not possible_path:
|
|
echo_failure('Directory `{}` does not exist.'.format(path))
|
|
sys.exit(1)
|
|
path = possible_path
|
|
else:
|
|
path = os.getcwd()
|
|
|
|
if build_dir:
|
|
build_dir = os.path.abspath(build_dir)
|
|
else:
|
|
build_dir = os.path.join(path, 'dist')
|
|
|
|
if pyname:
|
|
try:
|
|
settings = load_settings()
|
|
except FileNotFoundError:
|
|
echo_failure('Unable to locate config file. Try `hatch config --restore`.')
|
|
sys.exit(1)
|
|
|
|
pypath = settings.get('pypaths', {}).get(pyname, None)
|
|
if not pypath:
|
|
echo_failure('Python path named `{}` does not exist or is invalid.'.format(pyname))
|
|
sys.exit(1)
|
|
|
|
if clean_first:
|
|
echo_waiting('Removing build artifacts...')
|
|
clean_package(path, editable=package)
|
|
|
|
return_code = build_package(path, build_dir, universal, name, pypath, verbose)
|
|
|
|
if os.path.isdir(build_dir):
|
|
echo_success('Files found in `{}`:\n'.format(build_dir))
|
|
for file in sorted(os.listdir(build_dir)):
|
|
if os.path.isfile(os.path.join(build_dir, file)):
|
|
click.echo(file)
|
|
|
|
sys.exit(return_code)
|
|
|
|
|
|
@hatch.command(context_settings=CONTEXT_SETTINGS, short_help='Uploads to PyPI')
|
|
@click.argument('package', required=False)
|
|
@click.option('-p', '--path',
|
|
help='A relative or absolute path to a build directory.')
|
|
@click.option('-u', '--username', help='The PyPI username to use.')
|
|
@click.option('-t', '--test', 'test_pypi', is_flag=True,
|
|
help='Uses the test version of PyPI.')
|
|
@click.option('-s', '--strict', is_flag=True,
|
|
help='Aborts if a distribution already exists.')
|
|
def release(package, path, username, test_pypi, strict):
|
|
"""Uploads all files in a directory to PyPI using Twine.
|
|
|
|
The path to the build directory is derived in the following order:
|
|
|
|
\b
|
|
1. The optional argument, which should be the name of a package
|
|
that was installed via `hatch install -l` or `pip install -e`.
|
|
2. The option --path, which can be a relative or absolute path.
|
|
3. The current directory. If the current directory has a `dist`
|
|
directory, that will be used instead.
|
|
|
|
If the path was derived from the optional package argument, the
|
|
files must be in a directory named `dist`.
|
|
|
|
The PyPI username can be saved in the config file entry `pypi_username`.
|
|
If the `TWINE_PASSWORD` environment variable is not set, a hidden prompt
|
|
will be provided for the password.
|
|
"""
|
|
if package:
|
|
path = get_editable_package_location(package)
|
|
if not path:
|
|
echo_failure('`{}` is not an editable package.'.format(package))
|
|
sys.exit(1)
|
|
path = os.path.join(path, 'dist')
|
|
elif path:
|
|
possible_path = resolve_path(path)
|
|
if not possible_path:
|
|
echo_failure('Directory `{}` does not exist.'.format(path))
|
|
sys.exit(1)
|
|
path = possible_path
|
|
else:
|
|
path = os.getcwd()
|
|
default_build_dir = os.path.join(path, 'dist')
|
|
if os.path.isdir(default_build_dir):
|
|
path = default_build_dir
|
|
|
|
if not username:
|
|
try:
|
|
settings = load_settings()
|
|
except FileNotFoundError:
|
|
echo_failure('Unable to locate config file. Try `hatch config --restore`.')
|
|
sys.exit(1)
|
|
|
|
username = settings.get('pypi_username', None)
|
|
if not username:
|
|
echo_failure(
|
|
'A username must be supplied via -u/--username or '
|
|
'in {} as pypi_username.'.format(SETTINGS_FILE)
|
|
)
|
|
sys.exit(1)
|
|
|
|
command = [sys.executable, '-m', 'twine', 'upload', '-u', username,
|
|
'{}{}*'.format(path, os.path.sep)]
|
|
|
|
if test_pypi:
|
|
command.extend(['-r', TEST_REPOSITORY, '--repository-url', TEST_REPOSITORY])
|
|
else: # no cov
|
|
command.extend(['-r', DEFAULT_REPOSITORY, '--repository-url', DEFAULT_REPOSITORY])
|
|
|
|
if not strict:
|
|
command.append('--skip-existing')
|
|
|
|
result = subprocess.run(command, shell=NEED_SUBPROCESS_SHELL)
|
|
sys.exit(result.returncode)
|
|
|
|
|
|
def list_pypaths(ctx, param, value):
|
|
if not value or ctx.resilient_parsing:
|
|
return
|
|
|
|
try:
|
|
settings = load_settings()
|
|
except FileNotFoundError:
|
|
echo_failure('Unable to locate config file. Try `hatch config --restore`.')
|
|
sys.exit(1)
|
|
|
|
pypaths = settings.get('pypaths', {})
|
|
if pypaths:
|
|
for p in pypaths:
|
|
echo_success('{} -> {}'.format(p, pypaths[p]))
|
|
else:
|
|
echo_failure('There are no saved Python paths. Add '
|
|
'one via `hatch pypath NAME PATH`.')
|
|
|
|
ctx.exit()
|
|
|
|
|
|
@hatch.command('pypath', context_settings=CONTEXT_SETTINGS,
|
|
short_help='Names a Python path or shows available ones')
|
|
@click.argument('name')
|
|
@click.argument('path')
|
|
@click.option('-l', '--list', 'show', is_flag=True, is_eager=True, callback=list_pypaths,
|
|
help='Shows available Python paths.')
|
|
def python_path(name, path, show):
|
|
"""Names an absolute path to a Python executable. You can also modify
|
|
these in the config file entry `pypaths`.
|
|
|
|
Hatch can then use these paths by name when creating virtual envs, building
|
|
packages, etc.
|
|
|
|
\b
|
|
$ hatch pypath -l
|
|
There are no saved Python paths. Add one via `hatch pypath NAME PATH`.
|
|
$ hatch pypath py2 /usr/bin/python
|
|
Successfully saved Python `py2` located at `/usr/bin/python`.
|
|
$ hatch pypath py3 /usr/bin/python3
|
|
Successfully saved Python `py3` located at `/usr/bin/python3`.
|
|
$ hatch pypath -l
|
|
py2 -> /usr/bin/python
|
|
py3 -> /usr/bin/python3
|
|
"""
|
|
try:
|
|
settings = load_settings()
|
|
except FileNotFoundError:
|
|
echo_failure('Unable to locate config file. Try `hatch config --restore`.')
|
|
sys.exit(1)
|
|
|
|
if 'pypaths' not in settings:
|
|
updated_settings = copy_default_settings()
|
|
updated_settings.update(settings)
|
|
settings = updated_settings
|
|
echo_success('Settings were successfully updated to include `pypaths` entry.')
|
|
|
|
settings['pypaths'][name] = path
|
|
save_settings(settings)
|
|
echo_success('Successfully saved Python `{}` located at `{}`.'.format(name, path))
|
|
|
|
|
|
def list_envs(ctx, param, value):
|
|
if not value or ctx.resilient_parsing:
|
|
return
|
|
|
|
venvs = get_available_venvs()
|
|
|
|
if venvs:
|
|
echo_success('Virtual environments found in `{}`:\n'.format(VENV_DIR))
|
|
for venv_name, venv_dir in venvs:
|
|
with venv(venv_dir):
|
|
echo_success('{} ->'.format(venv_name))
|
|
if value == 1:
|
|
echo_success(' Version: {}'.format(get_python_version()))
|
|
elif value == 2:
|
|
echo_success(' Version: {}'.format(get_python_version()))
|
|
echo_success(' Implementation: {}'.format(get_python_implementation()))
|
|
else:
|
|
echo_success(' Version: {}'.format(get_python_version()))
|
|
echo_success(' Implementation: {}'.format(get_python_implementation()))
|
|
echo_success(' Local packages: {}'.format(', '.join(sorted(get_editable_packages()))))
|
|
|
|
# I don't want to move users' virtual environments
|
|
# temporarily for tests as one may be in use.
|
|
else: # no cov
|
|
echo_failure('No virtual environments found in `{}`. To create '
|
|
'one do `hatch env NAME`.'.format(VENV_DIR))
|
|
|
|
ctx.exit()
|
|
|
|
|
|
def restore_envs(ctx, param, value):
|
|
if not value or ctx.resilient_parsing:
|
|
return
|
|
|
|
fix_available_venvs()
|
|
echo_success('Successfully restored all available virtual envs.')
|
|
ctx.exit()
|
|
|
|
|
|
@hatch.command(context_settings=CONTEXT_SETTINGS,
|
|
short_help='Manages virtual environments')
|
|
@click.argument('name')
|
|
@click.option('-py', '--python', 'pyname',
|
|
help='The named Python path to use. This overrides --pypath.')
|
|
@click.option('-pp', '--pypath',
|
|
help='An absolute path to a Python executable.')
|
|
@click.option('-c', '--clone',
|
|
help='Specifies an existing virtual env to clone. (Experimental)')
|
|
@click.option('-v', '--verbose', is_flag=True, help='Increases verbosity.')
|
|
@click.option('-r', '--restore', is_flag=True, is_eager=True, callback=restore_envs,
|
|
help=(
|
|
'Attempts to make all virtual envs in `{}` usable by '
|
|
'fixing the executable paths in scripts and removing '
|
|
'all compiled `*.pyc` files. (Experimental)'.format(VENV_DIR)
|
|
))
|
|
@click.option('-l', '--list', 'show', count=True, is_eager=True, callback=list_envs,
|
|
help=(
|
|
'Shows available virtual envs. Can stack up to 3 times to '
|
|
'show more info.'
|
|
))
|
|
def env(name, pyname, pypath, clone, verbose, restore, show):
|
|
"""Creates a new virtual env that can later be utilized with the
|
|
`use` command.
|
|
|
|
\b
|
|
$ hatch pypath -l
|
|
py2 -> /usr/bin/python
|
|
py3 -> /usr/bin/python3
|
|
$ hatch env -l
|
|
No virtual environments found in /home/ofek/.virtualenvs. To create one do `hatch env NAME`.
|
|
$ hatch env my-app
|
|
Already using interpreter /usr/bin/python3
|
|
Successfully saved virtual env `my-app` to `/home/ofek/.virtualenvs/my-app`.
|
|
$ hatch env -py py2 old
|
|
Successfully saved virtual env `old` to `/home/ofek/.virtualenvs/old`.
|
|
$ hatch env -pp ~/pypy3/bin/pypy fast
|
|
Successfully saved virtual env `fast` to `/home/ofek/.virtualenvs/fast`.
|
|
$ hatch env -ll
|
|
Virtual environments found in /home/ofek/.virtualenvs:
|
|
|
|
\b
|
|
fast ->
|
|
Version: 3.5.3
|
|
Implementation: PyPy
|
|
my-app ->
|
|
Version: 3.5.2
|
|
Implementation: CPython
|
|
old ->
|
|
Version: 2.7.12
|
|
Implementation: CPython
|
|
"""
|
|
if pyname:
|
|
try:
|
|
settings = load_settings()
|
|
except FileNotFoundError:
|
|
echo_failure('Unable to locate config file. Try `hatch config --restore`.')
|
|
sys.exit(1)
|
|
|
|
pypath = settings.get('pypaths', {}).get(pyname, None)
|
|
if not pypath:
|
|
echo_failure('Unable to find a Python path named `{}`.'.format(pyname))
|
|
sys.exit(1)
|
|
|
|
venv_dir = os.path.join(VENV_DIR, name)
|
|
if os.path.exists(venv_dir):
|
|
echo_failure('Virtual env `{name}` already exists. To remove '
|
|
'it do `hatch shed -e {name}`.'.format(name=name))
|
|
sys.exit(1)
|
|
|
|
if not clone and not pyname and pypath and not os.path.exists(pypath):
|
|
echo_failure('Python path `{}` does not exist. Be sure to use the absolute path '
|
|
'e.g. `/usr/bin/python` instead of simply `python`.'.format(pypath))
|
|
sys.exit(1)
|
|
|
|
if clone:
|
|
origin = os.path.join(VENV_DIR, clone)
|
|
if not os.path.exists(origin):
|
|
echo_failure('Virtual env `{name}` does not exist.'.format(name=clone))
|
|
sys.exit(1)
|
|
echo_waiting('Cloning virtual env `{}`...'.format(clone))
|
|
clone_venv(origin, venv_dir)
|
|
echo_success('Successfully cloned virtual env `{}` from `{}` to `{}`.'.format(name, clone, venv_dir))
|
|
else:
|
|
echo_waiting('Creating virtual env `{}`...'.format(name))
|
|
create_venv(venv_dir, pypath, verbose=verbose)
|
|
echo_success('Successfully saved virtual env `{}` to `{}`.'.format(name, venv_dir))
|
|
|
|
|
|
@hatch.command(context_settings=CONTEXT_SETTINGS,
|
|
short_help='Removes named Python paths or virtual environments')
|
|
@click.option('-p', '-py', '--pypath', 'pyname',
|
|
help='Forward-slash-separated list of named Python paths.')
|
|
@click.option('-e', '--env', 'env_name',
|
|
help='Forward-slash-separated list of named virtual envs.')
|
|
@click.pass_context
|
|
def shed(ctx, pyname, env_name):
|
|
"""Removes named Python paths or virtual environments.
|
|
|
|
\b
|
|
$ hatch pypath -l
|
|
py2 -> /usr/bin/python
|
|
py3 -> /usr/bin/python3
|
|
invalid -> :\/:
|
|
$ hatch env -ll
|
|
Virtual environments found in /home/ofek/.virtualenvs:
|
|
|
|
\b
|
|
duplicate ->
|
|
Version: 3.5.2
|
|
Implementation: CPython
|
|
fast ->
|
|
Version: 3.5.3
|
|
Implementation: PyPy
|
|
my-app ->
|
|
Version: 3.5.2
|
|
Implementation: CPython
|
|
old ->
|
|
Version: 2.7.12
|
|
Implementation: CPython
|
|
$ hatch shed -p invalid -e duplicate/old
|
|
Successfully removed Python path named `invalid`.
|
|
Successfully removed virtual env named `duplicate`.
|
|
Successfully removed virtual env named `old`.
|
|
"""
|
|
if not (pyname or env_name):
|
|
click.echo(ctx.get_help())
|
|
return
|
|
|
|
if pyname:
|
|
try:
|
|
settings = load_settings()
|
|
except FileNotFoundError:
|
|
echo_failure('Unable to locate config file. Try `hatch config --restore`.')
|
|
sys.exit(1)
|
|
|
|
for pyname in pyname.split('/'):
|
|
pypath = settings.get('pypaths', {}).pop(pyname, None)
|
|
if pypath is not None:
|
|
save_settings(settings)
|
|
echo_success('Successfully removed Python path named `{}`.'.format(pyname))
|
|
else:
|
|
echo_warning('Python path named `{}` already does not exist.'.format(pyname))
|
|
|
|
if env_name:
|
|
for env_name in env_name.split('/'):
|
|
venv_dir = os.path.join(VENV_DIR, env_name)
|
|
if os.path.exists(venv_dir):
|
|
remove_path(venv_dir)
|
|
echo_success('Successfully removed virtual env named `{}`.'.format(env_name))
|
|
else:
|
|
echo_warning('Virtual env named `{}` already does not exist.'.format(env_name))
|
|
|
|
|
|
@hatch.command(context_settings=UNKNOWN_OPTIONS,
|
|
short_help='Activates or sends a command to a virtual environment')
|
|
@click.argument('env_name', required=False)
|
|
@click.argument('command', required=False, nargs=-1, type=click.UNPROCESSED)
|
|
@click.option('-t', '--temp', 'temp_env', is_flag=True,
|
|
help='Use a new temporary virtual env.')
|
|
@click.option('-s', '--shell',
|
|
help=(
|
|
'The name of shell to use e.g. `bash`. If the shell name '
|
|
'is not supported, e.g. `bash -O`, it will be treated as '
|
|
'a command and no custom prompt will be provided. This '
|
|
'overrides the config file entry `shell`.'
|
|
))
|
|
@click.pass_context
|
|
def use(ctx, env_name, command, temp_env, shell): # no cov
|
|
"""Activates or sends a command to a virtual environment. A default shell
|
|
name (or command) can be specified in the config file entry `shell` or the
|
|
environment variable `SHELL`. If there is no entry, env var, nor shell
|
|
option provided, a system default will be used: `cmd` on Windows, `bash`
|
|
otherwise.
|
|
|
|
Any arguments provided after the first will be sent to the virtual env as
|
|
a command without activating it. If there is only the env without args,
|
|
it will be activated similarly to how you are accustomed. The name of
|
|
the virtual env to use must be omitted if using the --temp env option.
|
|
|
|
Activation will not do anything to your current shell, but will rather
|
|
spawn a subprocess to avoid any unwanted strangeness occurring in your
|
|
current environment. If you would like to learn more about the benefits
|
|
of this approach, be sure to read https://gist.github.com/datagrok/2199506.
|
|
To leave a virtual env, type `exit`, or you can do `Ctrl+D` on non-Windows
|
|
machines.
|
|
|
|
\b
|
|
Activation:
|
|
$ hatch env -ll
|
|
Virtual environments found in `/home/ofek/.virtualenvs`:
|
|
|
|
\b
|
|
fast ->
|
|
Version: 3.5.3
|
|
Implementation: PyPy
|
|
my-app ->
|
|
Version: 3.5.2
|
|
Implementation: CPython
|
|
old ->
|
|
Version: 2.7.12
|
|
Implementation: CPython
|
|
$ which python
|
|
/usr/bin/python
|
|
$ hatch use my-app
|
|
(my-app) $ which python
|
|
/home/ofek/.virtualenvs/my-app/bin/python
|
|
|
|
\b
|
|
Commands:
|
|
$ hatch use my-app pip list --format=columns
|
|
Package Version
|
|
---------- -------
|
|
pip 9.0.1
|
|
setuptools 36.3.0
|
|
wheel 0.29.0
|
|
$ hatch use my-app hatch install -q requests six
|
|
$ hatch use my-app pip list --format=columns
|
|
Package Version
|
|
---------- -----------
|
|
certifi 2017.7.27.1
|
|
chardet 3.0.4
|
|
idna 2.6
|
|
pip 9.0.1
|
|
requests 2.18.4
|
|
setuptools 36.3.0
|
|
six 1.10.0
|
|
urllib3 1.22
|
|
wheel 0.29.0
|
|
|
|
\b
|
|
Temporary env:
|
|
$ hatch use -t
|
|
Already using interpreter /usr/bin/python3
|
|
Using base prefix '/usr'
|
|
New python executable in /tmp/tmpzg73untp/Ihqd/bin/python3
|
|
Also creating executable in /tmp/tmpzg73untp/Ihqd/bin/python
|
|
Installing setuptools, pip, wheel...done.
|
|
$ which python
|
|
/tmp/tmpzg73untp/Ihqd/bin/python
|
|
"""
|
|
if not (env_name or temp_env):
|
|
click.echo(ctx.get_help())
|
|
return
|
|
|
|
if env_name and temp_env:
|
|
echo_failure('Cannot use more than one virtual env at a time!')
|
|
sys.exit(1)
|
|
|
|
if not command and '_HATCHING_' in os.environ:
|
|
echo_failure(
|
|
'Virtual environments cannot be nested, sorry! To leave '
|
|
'the current one type `exit` or press `Ctrl+D`.'
|
|
)
|
|
sys.exit(1)
|
|
|
|
if temp_env:
|
|
temp_dir = TemporaryDirectory()
|
|
env_name = get_random_venv_name()
|
|
venv_dir = os.path.join(temp_dir.name, env_name)
|
|
echo_waiting('Creating a temporary virtual env named `{}`...'.format(env_name))
|
|
create_venv(venv_dir, verbose=True)
|
|
else:
|
|
temp_dir = None
|
|
venv_dir = os.path.join(VENV_DIR, env_name)
|
|
if not os.path.exists(venv_dir):
|
|
echo_failure('Virtual env named `{}` does not exist.'.format(env_name))
|
|
sys.exit(1)
|
|
|
|
result = None
|
|
|
|
try:
|
|
if command:
|
|
with venv(venv_dir):
|
|
echo_waiting('Running `{}` in `{}`...'.format(
|
|
' '.join(c if len(c.split()) == 1 else '"{}"'.format(c) for c in command),
|
|
env_name
|
|
))
|
|
result = subprocess.run(command, shell=NEED_SUBPROCESS_SHELL).returncode
|
|
else:
|
|
with venv(venv_dir) as exe_dir:
|
|
result = run_shell(exe_dir, shell)
|
|
finally:
|
|
result = 1 if result is None else result
|
|
if temp_dir is not None:
|
|
temp_dir.cleanup()
|
|
|
|
sys.exit(result)
|