Example #1
0
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
Example #2
0
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
Example #3
0
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
Example #4
0
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))
Example #5
0
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)
Example #6
0
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
Example #7
0
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!')
Example #8
0
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)
Example #9
0
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)
Example #10
0
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)
Example #11
0
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)