def test_src_package_path(): with temp_chdir() as d: runner = CliRunner() runner.invoke(hatch, ['egg', 'zzz', '--basic']) origin = os.path.join(d, 'zzz', 'zzz') project_name = basepath(d) src_package_dir = os.path.join(d, 'src', project_name) package_dir = os.path.join(d, project_name) priority_dir = os.path.join(d, 'aaa') src_package_file = os.path.join(src_package_dir, '__init__.py') package_file = os.path.join(package_dir, '__init__.py') priority_file = os.path.join(priority_dir, '__init__.py') shutil.copytree(origin, src_package_dir) shutil.copytree(origin, package_dir) shutil.copytree(origin, priority_dir) result = runner.invoke(hatch, ['grow', 'minor']) assert result.exit_code == 0 assert read_file(priority_file) == "__version__ = '0.0.1'\n" assert read_file(package_file) == "__version__ = '0.0.1'\n" assert read_file(src_package_file) == "__version__ = '0.1.0'\n" assert 'Updated {}'.format(src_package_file) in result.output assert '0.0.1 -> 0.1.0' in result.output
def bump_package_version(d, part='patch', pre_token=None, build_token=None): if os.path.isfile(d): version_files = [d] else: version_files = [] for filename in FILE_NAMES: path = os.path.join(d, filename) if os.path.exists(path): version_files.append(path) package_name = normalize_package_name(basepath(d)) locations = sorted(p.path for p in os.scandir(d)) package_path = os.path.join(d, package_name) if package_path in locations: locations.remove(package_path) locations.insert(0, package_path) # https://hynek.me/articles/testing-packaging src_package_path = os.path.join(d, 'src', package_name) if os.path.exists(src_package_path): locations.insert(0, src_package_path) for path in locations: for filename in FILE_NAMES: file = os.path.join(path, filename) if os.path.exists(file): version_files.append(file) for version_file in version_files: with open(version_file, 'r') as f: lines = f.readlines() for i, line in enumerate(lines): if line.startswith('__version__'): match = VERSION.search(line) if match: old_version = line.strip().split('=')[1].strip(' \'"') if part == 'pre': new_version = BUMP[part](old_version, pre_token) elif part == 'build': new_version = BUMP[part](old_version, build_token) else: new_version = BUMP[part](old_version) lines[i] = lines[i].replace(old_version, new_version) with atomic_write(version_file, overwrite=True) as f: f.write(''.join(lines)) return version_file, old_version, new_version return version_files, None, None
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 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` 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: 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 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)
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 init(name, no_env, pyname, pypath, global_packages, env_name, basic, cli, licenses, interactive): """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 = copy_default_settings() echo_warning( 'Unable to locate config file; try `hatch config --restore`. ' 'The default project structure will be used.') cwd = os.getcwd() package_name = name or click.prompt('Project name', default=basepath(cwd)) 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 create_package(cwd, package_name, settings) echo_success('Created project `{}` here'.format(package_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(cwd, get_venv_folder()) 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(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) 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 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 test(package, path, cov, merge, test_args, cov_args, env_aware): """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: 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() python_cmd = [get_proper_python(), '-m'] if env_aware else [] 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)
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 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. After the update, you may want to press Enter. 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: 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): 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 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 venv_dir: with venv(venv_dir): subprocess.Popen(command, shell=NEED_SUBPROCESS_SHELL) else: subprocess.Popen(command, shell=NEED_SUBPROCESS_SHELL) sys.exit() 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: click.echo('No packages installed.') sys.exit(1) command.extend(installed_packages) elif packages: packages = [package for package in packages if package != 'hatch'] if not packages: click.echo('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: click.echo('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): result = subprocess.run(command, shell=NEED_SUBPROCESS_SHELL) else: result = subprocess.run(command, shell=NEED_SUBPROCESS_SHELL) if temp_dir is not None: temp_dir.cleanup() sys.exit(result.returncode)