def get_editable_package_location(package_name=None): location = '' try: output = subprocess.check_output( [get_proper_pip(), 'list', '-e', '--format', 'columns'], shell=NEED_SUBPROCESS_SHELL).decode().strip().splitlines()[2:] except subprocess.CalledProcessError: # no cov return location if package_name: for line in output: name, _, path = line.split() if name == package_name: return resolve_path(path) else: if len(output) == 1: name, _, path = output[0].split() return name, resolve_path(path) elif len(output) > 1: return None, False else: return None, None return location
def create_venv(d, pypath=None, verbose=False): command = [ sys.executable, '-m', 'virtualenv', d, '-p', pypath or resolve_path(shutil.which(get_proper_python())) ] if not verbose: # no cov command.append('--quiet') subprocess.run(command, shell=NEED_SUBPROCESS_SHELL)
def create_venv(d, pypath=None, use_global=False, verbose=False): command = [ sys.executable, '-m', 'virtualenv', d, '-p', pypath or resolve_path(shutil.which(get_proper_python())) ] if use_global: # no cov command.append('--system-site-packages') if not verbose: # no cov command.append('-qqq') result = subprocess.run(command, shell=NEED_SUBPROCESS_SHELL) return result.returncode
def get_editable_package_location(package_name): location = '' try: output = subprocess.check_output( [get_proper_pip(), 'list', '-e', '--format', 'columns'], shell=NEED_SUBPROCESS_SHELL).decode().strip() except subprocess.CalledProcessError: # no cov return location for line in output.splitlines()[2:]: name, _, path = line.split() if name == package_name: return resolve_path(path) return location
def test(package, local, path, cov, merge, test_args, cov_args, global_exe, no_detect): """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 --local flag. 3. The option --path, which can be a relative or absolute path. 4. The current directory. If the path points to a package, it should have a `tests` directory. If a project is detected but there is no dedicated virtual env, it will be created and any dev requirements will be installed in it. \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: 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() 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 venv_dir = None if not (package or local) and not venv_active() and not no_detect and is_project(): venv_dir = os.path.join(path, get_venv_folder()) 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) echo_success('complete!') with venv(venv_dir): echo_waiting('Installing this project in the virtual env...') install_packages(['-e', '.']) click.echo() echo_waiting('Ensuring pytest and coverage are available...') install_packages(['pytest', 'coverage']) click.echo() dev_requirements = get_requirements_file(path, dev=True) if dev_requirements: echo_waiting( 'Installing test dependencies in the virtual env...') install_packages(['-r', dev_requirements]) click.echo() with chdir(path): echo_waiting('Testing...') output = b'' if venv_dir: with venv(venv_dir): test_result = subprocess.run(command, stdout=stdout, stderr=stderr, shell=NEED_SUBPROCESS_SHELL) else: 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: echo_waiting('\nTests completed, checking coverage...\n') if merge: if venv_dir: with venv(venv_dir): result = subprocess.run( python_cmd + ['coverage', 'combine', '--append'], stdout=stdout, stderr=stderr, shell=NEED_SUBPROCESS_SHELL) else: 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'' if venv_dir: with venv(venv_dir): result = subprocess.run( python_cmd + ['coverage', 'report', '--show-missing'], stdout=stdout, stderr=stderr, shell=NEED_SUBPROCESS_SHELL) else: 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()) sys.exit(test_result.returncode)
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 clean(package, local, path, compiled_only, no_detect, 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 --local flag. 3. The option --path, which can be a relative or absolute path. 4. 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: 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 compiled_only: removed_paths = remove_compiled_scripts(path, detect_project=not no_detect) else: removed_paths = clean_package(path, editable=package or local, detect_project=not no_detect) if verbose: if removed_paths: echo_success('Removed paths:') for p in removed_paths: echo_info(p) if removed_paths: echo_success('Cleaned!') else: echo_success('Already clean!')
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 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 release(package, local, path, username, repo, repo_url, 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` or the `TWINE_USERNAME` environment variable. 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) # Fall back when 'pypi_username' value is '' or None username = settings.get('pypi_username') or os.environ.get( 'TWINE_USERNAME') if not username: echo_failure('A username must be supplied via -u/--username, ' 'in {} as pypi_username, or in the TWINE_USERNAME ' 'environment variable.'.format(SETTINGS_FILE)) sys.exit(1) command = [ sys.executable, '-m', 'twine', 'upload', '-u', username, '{}{}*'.format(path, os.path.sep) ] if test_pypi: # Print all error messages before exiting any_failed = False # Disallow these combinations, since it is ambiguous whether they are intended # to be used as the test repository (if a custom test repository is desired, # then users can omit the '--test'.) if repo: echo_failure('Cannot specify both --test and --repo.') any_failed = True if repo_url: echo_failure('Cannot specify both --test and --repo-url.') any_failed = True if any_failed: sys.exit(1) command.extend( ['-r', TEST_REPOSITORY, '--repository-url', TEST_REPOSITORY]) else: # no cov # Only pass these to twine if they are given to us. Otherwise, # fall back onto the default twine behavior if repo: command.extend(['-r', repo]) if repo_url: command.extend(['--repository-url', repo_url]) if not strict: command.append('--skip-existing') result = subprocess.run(command, shell=NEED_SUBPROCESS_SHELL) sys.exit(result.returncode)