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: click.echo('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 click.echo('Settings were successfully updated to include `pypaths` entry.') settings['pypaths'][name] = path save_settings(settings) click.echo('Successfully saved Python `{}` located at `{}`.'.format(name, path))
def test_pyname_and_env(): with temp_chdir() as d: runner = CliRunner() env_name = os.urandom(10).hex() while os.path.exists(os.path.join(VENV_DIR, env_name)): # no cov env_name = os.urandom(10).hex() venv_dir = os.path.join(VENV_DIR, env_name) try: runner.invoke(hatch, ['env', env_name]) assert os.path.exists(venv_dir) with temp_move_path(SETTINGS_FILE, d): settings = copy_default_settings() settings['pypaths']['pyname'] = 'pypath' save_settings(settings) result = runner.invoke( hatch, ['shed', '-p', 'pyname', '-e', env_name]) assert load_settings()['pypaths'] == {} assert not os.path.exists(venv_dir) finally: remove_path(venv_dir) assert result.exit_code == 0 assert 'Successfully removed Python path named `pyname`.' in result.output assert 'Successfully removed virtual env named `{}`.'.format( env_name) in result.output
def wrapper(cached=True, reset=False): nonlocal cached_venv_folder if not cached or not cached_venv_folder or reset: venv_folder = os.environ.get('_VENV_FOLDER_') or load_settings(lazy=True).get('venv_folder') cached_venv_folder = venv_folder or 'venv' return cached_venv_folder
def test_update(): with temp_chdir() as d: runner = CliRunner() with temp_move_path(SETTINGS_FILE, d): new_settings = copy_default_settings() new_settings.pop('email') new_settings['new setting'] = '' save_settings(new_settings) assert load_settings() == new_settings result = runner.invoke(hatch, ['config', '-u']) updated_settings = load_settings() assert result.exit_code == 0 assert 'Settings were successfully updated.' in result.output assert 'email' in updated_settings assert 'new setting' in updated_settings
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 -l Virtual environments found in /home/ofek/.local/share/hatch/venvs: \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: click.echo('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) click.echo('Successfully removed Python path named `{}`.'.format(pyname)) else: click.echo('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) click.echo('Successfully removed virtual env named `{}`.'.format(env_name)) else: click.echo('Virtual env named `{}` already does not exist.'.format(env_name))
def test_update_config_not_exist(): with temp_chdir() as d: runner = CliRunner() with temp_move_path(SETTINGS_FILE, d): result = runner.invoke(hatch, ['config', '-u']) assert result.exit_code == 0 assert 'Settings were successfully restored.' in result.output assert load_settings() == copy_default_settings()
def egg(name, basic, cli, licenses): """Creates a new Python project. Think of an "egg" as a new idea. 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. Here is an example using an unmodified config file: \b $ hatch egg 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: click.echo('Unable to locate config file. Try `hatch config --restore`.') sys.exit(1) 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): click.echo('Directory `{}` already exists.'.format(d)) sys.exit(1) os.makedirs(d) with chdir(d, cwd=origin): create_package(d, name, settings) click.echo('Created project `{}`'.format(name))
def test_success(): with temp_chdir() as d: runner = CliRunner() with temp_move_path(SETTINGS_FILE, d): restore_settings() result = runner.invoke(hatch, ['pypath', 'name', 'path']) settings = load_settings() assert settings['pypaths']['name'] == 'path' assert result.exit_code == 0 assert 'Successfully saved Python `name` located at `path`.' in result.output
def test_pyname(): with temp_chdir() as d: runner = CliRunner() with temp_move_path(SETTINGS_FILE, d): settings = copy_default_settings() settings['pypaths']['pyname'] = 'pypath' save_settings(settings) result = runner.invoke(hatch, ['shed', '-p', 'pyname']) assert load_settings()['pypaths'] == {} assert result.exit_code == 0 assert 'Successfully removed Python path named `pyname`.' in result.output
def set_venv_dir(): # no cov global VENV_DIR venv_dir = os.environ.get('_VENV_DIR_') or load_settings( lazy=True).get('venv_dir') if venv_dir: if venv_dir == 'isolated': VENV_DIR = VENV_DIR_ISOLATED elif venv_dir == 'shared': VENV_DIR = VENV_DIR_SHARED else: VENV_DIR = venv_dir else: VENV_DIR = VENV_DIR_SHARED
def wrapper(cached=True, reset=False): nonlocal cached_venv_dir if not cached or not cached_venv_dir or reset: venv_dir = os.environ.get('_VENV_DIR_') or load_settings(lazy=True).get('venv_dir') if venv_dir: # no cov if venv_dir == 'isolated': venv_dir = VENV_DIR_ISOLATED elif venv_dir == 'shared': venv_dir = VENV_DIR_SHARED else: # no cov venv_dir = VENV_DIR_SHARED cached_venv_dir = venv_dir return cached_venv_dir
def wrapper(cached=True, reset=False): nonlocal cached_python_dir if not cached or not cached_python_dir or reset: python_dir = os.environ.get('_PYTHON_DIR_') or load_settings(lazy=True).get('python_dir') if python_dir: # no cov if python_dir == 'isolated': python_dir = PYTHON_DIR_ISOLATED elif python_dir == 'shared': python_dir = PYTHON_DIR_SHARED else: # no cov python_dir = PYTHON_DIR_SHARED cached_python_dir = python_dir return cached_python_dir
def test_success_missing_key(): with temp_chdir() as d: runner = CliRunner() with temp_move_path(SETTINGS_FILE, d): settings = copy_default_settings() settings.pop('pypaths') save_settings(settings) result = runner.invoke(hatch, ['pypath', 'name', 'path']) settings = load_settings() assert settings['pypaths']['name'] == 'path' assert list(settings.keys())[-1] != 'pypaths' assert result.exit_code == 0 assert 'Settings were successfully updated to include `pypaths` entry.' in result.output assert 'Successfully saved Python `name` located at `path`.' in result.output
def get_default_shell_info(shell_name=None, settings=None): if not shell_name: settings = settings or load_settings(lazy=True) shell_name = settings.get('shell') if shell_name: return shell_name, None shell_path = os.environ.get('SHELL') if shell_path: shell_name = basepath(shell_path) else: shell_name = DEFAULT_SHELL return shell_name, shell_path return shell_name, None
def build(package, path, pyname, pypath, universal, name, build_dir, clean_first): """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: click.echo('`{}` is not an editable package.'.format(package)) sys.exit(1) elif path: relative_path = os.path.join(os.getcwd(), basepath(path)) if os.path.exists(relative_path): path = relative_path elif not os.path.exists(path): click.echo('Directory `{}` does not exist.'.format(path)) sys.exit(1) else: path = os.getcwd() if pyname: try: settings = load_settings() except FileNotFoundError: click.echo('Unable to locate config file. Try `hatch config --restore`.') sys.exit(1) pypath = settings.get('pypaths', {}).get(pyname, None) if not pypath: click.echo('Python path named `{}` does not exist or is invalid.'.format(pyname)) sys.exit(1) if clean_first: clean_package(path, editable=package) sys.exit(build_package(path, universal, name, build_dir, pypath))
def init(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. 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: click.echo('Unable to locate config file. Try `hatch config --restore`.') sys.exit(1) if basic: settings['basic'] = True if licenses: settings['licenses'] = licenses.split(',') settings['cli'] = cli create_package(os.getcwd(), name, settings) click.echo('Created project `{}` here'.format(name))
def list_pypaths(ctx, param, value): if not value or ctx.resilient_parsing: return try: settings = load_settings() except FileNotFoundError: click.echo('Unable to locate config file. Try `hatch config --restore`.') sys.exit(1) pypaths = settings.get('pypaths', {}) if pypaths: for p in pypaths: click.echo('{} -> {}'.format(p, pypaths[p])) else: click.echo('There are no saved Python paths. Add ' 'one via `hatch pypath NAME PATH`.') ctx.exit()
def get_default_shell_info(shell_name=None, settings=None): if not shell_name: try: settings = settings or load_settings() except FileNotFoundError: settings = {} shell_name = settings.get('shell') if shell_name: return shell_name, None shell_path = os.environ.get('SHELL') if shell_path: shell_name = basepath(shell_path) else: shell_name = DEFAULT_SHELL return shell_name, shell_path return shell_name, None
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)
def shell(env_name, command, shell_name, temp_env, pyname, pypath, global_packages): # 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. If no env is chosen, this will attempt to detect a project and activate its virtual env. To run a command in a project's virtual env, use `.` as the env name. 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. `use` is an alias for this command. \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 shell my-app (my-app) $ which python /home/ofek/.virtualenvs/my-app/bin/python \b Commands: $ hatch shell my-app pip list --format=columns Package Version ---------- ------- pip 9.0.1 setuptools 36.3.0 wheel 0.29.0 $ hatch shell my-app hatch install -q requests six $ hatch shell 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 shell -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 """ venv_dir = None if resolve_path(env_name) == os.getcwd(): env_name = '' if not (env_name or temp_env): if is_project(): venv_dir = os.path.join(os.getcwd(), 'venv') if not is_venv(venv_dir): echo_info('A project has been detected!') echo_waiting('Creating a dedicated virtual env... ', nl=False) create_venv(venv_dir, use_global=global_packages) echo_success('complete!') with venv(venv_dir): echo_waiting('Installing this project in the virtual env... ', nl=False) install_packages(['-q', '-e', '.']) echo_success('complete!') else: echo_failure('No project found.') sys.exit(1) 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: 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) 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, pypath=pypath, use_global=global_packages, verbose=True) else: temp_dir = None venv_dir = venv_dir or os.path.join(get_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), '`{}`'.format(env_name) if env_name else "this project's env" )) result = subprocess.run(command, shell=NEED_SUBPROCESS_SHELL).returncode else: with venv(venv_dir) as exe_dir: result = run_shell(exe_dir, shell_name) finally: result = 1 if result is None else result if temp_dir is not None: temp_dir.cleanup() sys.exit(result)
def build(package, local, 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 --local flag. 3. The option --path, which can be a relative or absolute path. 4. The current directory. The path must contain a `setup.py` file. """ if package: echo_waiting('Locating package...') path = get_editable_package_location(package) if not path: echo_failure('`{}` is not an editable package.'.format(package)) sys.exit(1) elif local: echo_waiting('Locating package...') name, path = get_editable_package_location() if not name: if path is None: echo_failure('There are no local packages available.') sys.exit(1) else: echo_failure( 'There are multiple local packages available. Select ' 'one with the optional argument.') sys.exit(1) echo_info('Package `{}` has been selected.'.format(name)) 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 or local, detect_project=True) 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)): echo_info(file) sys.exit(return_code)
def env(name, pyname, pypath, clone, quiet, 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/.local/share/hatch/venvs. To create one do `hatch env NAME`. $ hatch env -q my-app Already using interpreter /usr/bin/python3 Successfully saved virtual env `my-app` to `/home/ofek/.local/share/hatch/venvs/my-app`. $ hatch env -q -py py2 old Successfully saved virtual env `old` to `/home/ofek/.local/share/hatch/venvs/old`. $ hatch env -q -pp ~/pypy3/bin/pypy fast Successfully saved virtual env `fast` to `/home/ofek/.local/share/hatch/venvs/fast`. $ hatch env -l Virtual environments found in /home/ofek/.local/share/hatch/venvs: \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: click.echo('Unable to locate config file. Try `hatch config --restore`.') sys.exit(1) pypath = settings.get('pypaths', {}).get(pyname, None) if not pypath: click.echo('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): click.echo('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): click.echo('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): click.echo('Virtual env `{name}` does not exist.'.format(name=clone)) sys.exit(1) clone_venv(origin, venv_dir) click.echo('Successfully cloned virtual env `{}` from `{}` to {}.'.format(name, clone, venv_dir)) else: create_venv(venv_dir, pypath, quiet=quiet) click.echo('Successfully saved virtual env `{}` to `{}`.'.format(name, venv_dir))
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 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: click.echo('`{}` is not an editable package.'.format(package)) sys.exit(1) path = os.path.join(path, 'dist') elif path: relative_path = os.path.join(os.getcwd(), basepath(path)) if os.path.exists(relative_path): path = relative_path elif not os.path.exists(path): click.echo('Directory `{}` does not exist.'.format(path)) sys.exit(1) else: path = os.getcwd() if not username: try: settings = load_settings() except FileNotFoundError: click.echo('Unable to locate config file. Try `hatch config --restore`.') sys.exit(1) username = settings.get('pypi_username', None) if not username: click.echo( 'A username must be supplied via -u/--username or ' 'in {} as pypi_username.'.format(SETTINGS_FILE) ) sys.exit(1) command = ['twine', 'upload', '{}{}*'.format(path, os.path.sep), '-u', username] 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 use(env_name, command, shell, nest): # 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`. If there is no entry 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. 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 Non-nesting: $ hatch env -l Virtual environments found in `/home/ofek/.local/share/hatch/venvs`: \b fast -> Version: 3.5.3 Implementation: PyPy my-app -> Version: 3.5.2 Implementation: CPython old -> Version: 2.7.12 Implementation: CPython $ python -c "import sys;print(sys.executable)" /usr/bin/python $ hatch use my-app (my-app) $ python -c "import sys;print(sys.executable)" /home/ofek/.local/share/hatch/venvs/my-app/bin/python (my-app) $ hatch use fast (my-app) $ exit (fast) $ python -c "import sys;print(sys.executable)" /home/ofek/.local/share/hatch/venvs/fast/bin/python (fast) $ exit $ \b Nesting: $ hatch use my-app (my-app) $ hatch use -n fast 2 (fast) $ hatch use -n old 3 (old) $ exit 2 (fast) $ exit (my-app) $ exit $ \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 """ # Run commands regardless of virtual env activation. if command: venv_dir = os.path.join(VENV_DIR, env_name) if not os.path.exists(venv_dir): click.echo('Virtual env named `{}` does not exist.'.format(env_name)) sys.exit(1) result = None try: with venv(venv_dir): result = subprocess.run(command, shell=NEED_SUBPROCESS_SHELL) finally: sys.exit(1 if result is None else result.returncode) try: settings = load_settings() except FileNotFoundError: settings = {} shell_name, shell_path = get_default_shell_info(shell, settings) if shell_name in IMMORTAL_SHELLS or nest or (nest is None and settings.get('nest_shells')): venv_dir = os.path.join(VENV_DIR, env_name) if not os.path.exists(venv_dir): click.echo('Virtual env named `{}` does not exist.'.format(env_name)) sys.exit(1) with venv(venv_dir): with get_shell_command(env_name, shell_name, shell_path, nest=True) as shell_command: subprocess.run(shell_command, shell=NEED_SUBPROCESS_SHELL) return # If in activated venv shell, notify main loop and exit. if '_HATCH_FILE_' in os.environ: with atomic_write(os.environ['_HATCH_FILE_'], overwrite=True) as f: data = json.dumps({ 'env_name': env_name, 'shell': shell }) f.write(data) return with TemporaryDirectory() as d: communication_file = os.path.join(d, 'temp.json') evars = {'_HATCH_FILE_': communication_file} while True: venv_dir = os.path.join(VENV_DIR, env_name) if not os.path.exists(venv_dir): click.echo('Virtual env named `{}` does not exist.'.format(env_name)) sys.exit(1) shell_name, shell_path = get_default_shell_info(shell) with venv(venv_dir, evars=evars): try: with get_shell_command(env_name, shell_name, shell_path) as shell_command: process = subprocess.Popen(shell_command) while True: if process.poll() is not None: return if os.path.exists(communication_file): # This is necessary on non-Windows machines. The # only hint of this situation I was able to find # on Google was https://unix.stackexchange.com/a/289003 # # Killing a spawned shell suspends execution of # this script due to competition for terminal use. # Termination works, however only if the spawned # shell has no active processes. Therefore, we sleep # shortly to ensure the second `hatch use ...` has # time to write the communication file and exit. On # some shells, even this doesn't work! # # Please, if you know of a workaround, do submit # an issue/PR or tweet me https://twitter.com/Ofekmeister # # Having all shells support the non-nesting workflow # would be ideal {^.^} if not ON_WINDOWS: time.sleep(0.2) with open(communication_file, 'r') as f: args = json.loads(f.read()) env_name = args['env_name'] shell = args['shell'] remove_path(communication_file) process.terminate() break except KeyboardInterrupt: break
def grow(part, package, local, 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 --local flag. 3. The option --path, which can be a relative or absolute path. 4. 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: echo_waiting('Locating package...') path = get_editable_package_location(package) if not path: echo_failure('`{}` is not an editable package.'.format(package)) sys.exit(1) elif local: echo_waiting('Locating package...') name, path = get_editable_package_location() if not name: if path is None: echo_failure('There are no local packages available.') sys.exit(1) else: echo_failure( 'There are multiple local packages available. Select ' 'one with the optional argument.') sys.exit(1) echo_info('Package `{}` has been selected.'.format(name)) 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)
def release(package, local, 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 --local flag. 3. The option --path, which can be a relative or absolute path. 4. 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: echo_waiting('Locating 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 local: echo_waiting('Locating package...') name, path = get_editable_package_location() if not name: if path is None: echo_failure('There are no local packages available.') sys.exit(1) else: echo_failure( 'There are multiple local packages available. Select ' 'one with the optional argument.') sys.exit(1) echo_info('Package `{}` has been selected.'.format(name)) 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 init(name, no_env, pyname, pypath, global_packages, 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. By default a virtual env will be created in the project directory and will install the project locally so any edits will auto-update the installation. You can also locally install the created project in other virtual envs using the --env option. 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: settings = {} echo_warning( 'Unable to locate config file; try `hatch config --restore`. ' 'The default project structure will be used.') if basic: settings['basic'] = True if licenses: settings['licenses'] = licenses.split(',') settings['cli'] = cli d = os.getcwd() create_package(d, name, settings) echo_success('Created project `{}` here'.format(name)) venvs = env_name.split('/') if env_name else [] if (venvs or not no_env) and pyname: try: settings = load_settings() except FileNotFoundError: # no cov 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) if not no_env: venv_dir = os.path.join(d, 'venv') echo_waiting('Creating its own virtual env... ', nl=False) create_venv(venv_dir, pypath=pypath, use_global=global_packages) echo_success('complete!') with venv(venv_dir): echo_waiting('Installing locally in the virtual env... ', nl=False) install_packages(['-q', '-e', '.']) echo_success('complete!') 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), nl=False) create_venv(venv_dir, pypath=pypath, use_global=global_packages) echo_success('complete!') with venv(venv_dir): echo_waiting( 'Installing locally in virtual env `{}`... '.format(vname), nl=False) install_packages(['-q', '-e', '.']) echo_success('complete!')
def python(version, name, head): # no cov if not conda_available(): echo_failure( 'Conda is unavailable. You can install it by doing `hatch conda`.') sys.exit(1) exe_name = 'py{}'.format(name or version) + ('.exe' if ON_WINDOWS else '') name = name or version path = os.path.join(get_python_dir(), name) command = [ 'conda', 'create', '--yes', '-p', path, 'python={}'.format(version) ] if os.path.exists(path): echo_failure('The path `{}` already exists.'.format(path)) sys.exit(1) settings = load_settings(lazy=True) 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.') old_path = settings['pypaths'].get(name) if old_path: echo_failure('The Python path `{}` already points to `{}`.'.format( name, old_path)) sys.exit(1) echo_waiting('Installing Python {}...'.format(version)) try: subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) except subprocess.CalledProcessError as e: echo_failure('The installation was seemingly unsuccessful.') click.echo(e.stdout) click.echo(e.stderr) sys.exit(e.returncode) conda_path = get_conda_new_exe_path(path) python_path = resolve_path(shutil.which('python', path=conda_path)) settings['pypaths'][name] = python_path save_settings(settings) echo_success('Successfully saved Python `{}` located at `{}`.'.format( name, python_path)) if head is not None: add_to_path = userpath.prepend if head else userpath.append success = add_to_path(conda_path, app_name='Hatch') shutil.copy(python_path, os.path.join(os.path.dirname(python_path), exe_name)) if success: echo_info( 'Please restart your shell for PATH changes to take effect.') else: echo_warning( 'It appears that we were unable to modify PATH. Please ' 'do so using the following: ', nl=False) echo_info(conda_path)
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: click.echo('`{}` is not an editable package.'.format(package)) sys.exit(1) elif path: relative_path = os.path.join(os.getcwd(), basepath(path)) if os.path.exists(relative_path): path = relative_path elif not os.path.exists(path): click.echo('Directory `{}` does not exist.'.format(path)) sys.exit(1) else: path = os.getcwd() try: settings = load_settings() except FileNotFoundError: settings = {} 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: click.echo('Updated {}'.format(f)) click.echo('{} -> {}'.format(old_version, new_version)) else: if f: click.echo('Found version files:') for file in f: click.echo(file) click.echo('\nUnable to find a version specifier.') sys.exit(1) else: click.echo('No version files found.') sys.exit(1)
def new(name, no_env, pyname, pypath, global_packages, env_name, venv_prompt, basic, cli, licenses, interactive): """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. By default a virtual env will be created in the project directory and will install the project locally so any edits will auto-update the installation. You can also locally install the created project in other virtual envs using the --env option. 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: settings = copy_default_settings() echo_warning( 'Unable to locate config file; try `hatch config --restore`. ' 'The default project structure will be used.') origin = os.getcwd() package_name = name or click.prompt('Project name') d = os.path.join(origin, package_name) if os.path.exists(d): echo_failure('Directory `{}` already exists.'.format(d)) sys.exit(1) if interactive or not name: settings['version'] = click.prompt('Version', default='0.0.1') settings['description'] = click.prompt('Description', default='') settings['name'] = click.prompt('Author', default=settings.get('name', '')) settings['email'] = click.prompt("Author's email", default=settings.get('email', '')) licenses = click.prompt('License(s)', default=licenses or 'mit,apache2') if licenses: settings['licenses'] = [str.strip(li) for li in licenses.split(',')] if basic: settings['basic'] = True settings['cli'] = cli venvs = env_name.split('/') if env_name else [] if (venvs or not no_env) and pyname: try: settings = load_settings() except FileNotFoundError: # no cov 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) os.makedirs(d) with chdir(d, cwd=origin): create_package(d, package_name, settings) echo_success('Created project `{}`'.format(package_name)) if not no_env: venv_dir = os.path.join(d, get_venv_folder()) echo_waiting('Creating its own virtual env... ', nl=False) create_venv(venv_dir, pypath=pypath, use_global=global_packages, venv_prompt=venv_prompt or package_name) echo_success('complete!') with venv(venv_dir): echo_waiting('Installing locally in the virtual env... ', nl=False) install_packages(['-q', '-e', '.']) echo_success('complete!') for vname in venvs: venv_dir = os.path.join(get_venv_dir(), vname) if not os.path.exists(venv_dir): echo_waiting('Creating virtual env `{}`... '.format(vname), nl=False) create_venv(venv_dir, pypath=pypath, use_global=global_packages, venv_prompt=vname) echo_success('complete!') with venv(venv_dir): echo_waiting( 'Installing locally in virtual env `{}`... '.format(vname), nl=False) install_packages(['-q', '-e', '.']) echo_success('complete!')