pypa-hatch/tests/cli/python/test_install.py

291 lines
10 KiB
Python

import json
import secrets
import pytest
from hatch.errors import PythonDistributionResolutionError
from hatch.python.core import InstalledDistribution
from hatch.python.distributions import ORDERED_DISTRIBUTIONS
from hatch.python.resolve import get_distribution
def test_unknown(hatch, helpers, path_append, mocker):
install = mocker.patch('hatch.python.core.PythonManager.install')
result = hatch('python', 'install', 'foo', 'bar')
assert result.exit_code == 1, result.output
assert result.output == helpers.dedent(
"""
Unknown distributions: foo, bar
"""
)
install.assert_not_called()
path_append.assert_not_called()
def test_incompatible_single(hatch, helpers, path_append, dist_name, mocker):
mocker.patch('hatch.python.resolve.get_distribution', side_effect=PythonDistributionResolutionError)
install = mocker.patch('hatch.python.core.PythonManager.install')
result = hatch('python', 'install', dist_name)
assert result.exit_code == 1, result.output
assert result.output == helpers.dedent(
f"""
Incompatible distributions: {dist_name}
"""
)
install.assert_not_called()
path_append.assert_not_called()
def test_incompatible_all(hatch, helpers, path_append, mocker):
mocker.patch('hatch.python.resolve.get_distribution', side_effect=PythonDistributionResolutionError)
install = mocker.patch('hatch.python.core.PythonManager.install')
result = hatch('python', 'install', 'all')
assert result.exit_code == 1, result.output
assert result.output == helpers.dedent(
f"""
Incompatible distributions: {', '.join(ORDERED_DISTRIBUTIONS)}
"""
)
install.assert_not_called()
path_append.assert_not_called()
@pytest.mark.requires_internet
def test_installation(
hatch, helpers, temp_dir_data, platform, path_append, default_shells, compatible_python_distributions
):
selection = [name for name in compatible_python_distributions if not name.startswith('pypy')]
dist_name = secrets.choice(selection)
result = hatch('python', 'install', dist_name)
install_dir = temp_dir_data / 'data' / 'pythons' / dist_name
metadata_file = install_dir / InstalledDistribution.metadata_filename()
python_path = install_dir / json.loads(metadata_file.read_text())['python_path']
assert result.exit_code == 0, result.output
assert result.output == helpers.dedent(
f"""
Installing {dist_name}
Installed {dist_name} @ {install_dir}
The following directory has been added to your PATH (pending a shell restart):
{python_path.parent}
"""
)
assert python_path.is_file()
output = platform.check_command_output([python_path, '-c', 'import sys;print(sys.executable)']).strip()
assert output == str(python_path)
output = platform.check_command_output([python_path, '--version']).strip()
assert output.startswith(f'Python {dist_name}.')
path_append.assert_called_once_with(str(python_path.parent), shells=default_shells)
def test_already_installed_latest(hatch, helpers, temp_dir_data, path_append, dist_name, mocker):
install = mocker.patch('hatch.python.core.PythonManager.install')
install_dir = temp_dir_data / 'data' / 'pythons'
installed_dist = helpers.write_distribution(install_dir, dist_name)
result = hatch('python', 'install', dist_name)
assert result.exit_code == 0, result.output
assert result.output == helpers.dedent(
f"""
The latest version is already installed: {installed_dist.version}
"""
)
install.assert_not_called()
path_append.assert_not_called()
def test_already_installed_update_disabled(hatch, helpers, temp_dir_data, path_append, dist_name, mocker):
install = mocker.patch('hatch.python.core.PythonManager.install')
install_dir = temp_dir_data / 'data' / 'pythons'
helpers.write_distribution(install_dir, dist_name)
helpers.downgrade_distribution_metadata(install_dir / dist_name)
result = hatch('python', 'install', dist_name, input='n\n')
assert result.exit_code == 1, result.output
assert result.output == helpers.dedent(
f"""
Update {dist_name}? [y/N]: n
Distribution is already installed: {dist_name}
"""
)
install.assert_not_called()
path_append.assert_not_called()
def test_already_installed_update_prompt(hatch, helpers, temp_dir_data, path_append, default_shells, dist_name, mocker):
install_dir = temp_dir_data / 'data' / 'pythons'
helpers.write_distribution(install_dir, dist_name)
dist_dir = install_dir / dist_name
metadata = helpers.downgrade_distribution_metadata(dist_dir)
python_path = dist_dir / metadata['python_path']
install = mocker.patch(
'hatch.python.core.PythonManager.install', return_value=mocker.MagicMock(path=dist_dir, python_path=python_path)
)
result = hatch('python', 'install', dist_name, input='y\n')
assert result.exit_code == 0, result.output
assert result.output == helpers.dedent(
f"""
Update {dist_name}? [y/N]: y
Updating {dist_name}
Updated {dist_name} @ {dist_dir}
The following directory has been added to your PATH (pending a shell restart):
{python_path.parent}
"""
)
install.assert_called_once_with(dist_name)
path_append.assert_called_once_with(str(python_path.parent), shells=default_shells)
def test_already_installed_update_flag(hatch, helpers, temp_dir_data, path_append, default_shells, dist_name, mocker):
install_dir = temp_dir_data / 'data' / 'pythons'
helpers.write_distribution(install_dir, dist_name)
dist_dir = install_dir / dist_name
metadata = helpers.downgrade_distribution_metadata(dist_dir)
python_path = dist_dir / metadata['python_path']
install = mocker.patch(
'hatch.python.core.PythonManager.install', return_value=mocker.MagicMock(path=dist_dir, python_path=python_path)
)
result = hatch('python', 'install', '--update', dist_name)
assert result.exit_code == 0, result.output
assert result.output == helpers.dedent(
f"""
Updating {dist_name}
Updated {dist_name} @ {dist_dir}
The following directory has been added to your PATH (pending a shell restart):
{python_path.parent}
"""
)
install.assert_called_once_with(dist_name)
path_append.assert_called_once_with(str(python_path.parent), shells=default_shells)
@pytest.mark.parametrize('detector', ['in_current_path', 'in_new_path'])
def test_already_in_path(hatch, helpers, temp_dir_data, path_append, mocker, detector, dist_name):
mocker.patch(f'userpath.{detector}', return_value=True)
dist_dir = temp_dir_data / 'data' / 'pythons' / dist_name
python_path = dist_dir / get_distribution(dist_name).python_path
install = mocker.patch(
'hatch.python.core.PythonManager.install', return_value=mocker.MagicMock(path=dist_dir, python_path=python_path)
)
result = hatch('python', 'install', dist_name)
assert result.exit_code == 0, result.output
assert result.output == helpers.dedent(
f"""
Installing {dist_name}
Installed {dist_name} @ {dist_dir}
"""
)
install.assert_called_once_with(dist_name)
path_append.assert_not_called()
def test_private(hatch, helpers, temp_dir_data, path_append, dist_name, mocker):
dist_dir = temp_dir_data / 'data' / 'pythons' / dist_name
python_path = dist_dir / get_distribution(dist_name).python_path
install = mocker.patch(
'hatch.python.core.PythonManager.install', return_value=mocker.MagicMock(path=dist_dir, python_path=python_path)
)
result = hatch('python', 'install', '--private', dist_name)
assert result.exit_code == 0, result.output
assert result.output == helpers.dedent(
f"""
Installing {dist_name}
Installed {dist_name} @ {dist_dir}
"""
)
install.assert_called_once_with(dist_name)
path_append.assert_not_called()
def test_specific_location(hatch, helpers, temp_dir_data, path_append, dist_name, mocker):
install_dir = temp_dir_data / 'foo' / 'bar' / 'baz'
dist_dir = install_dir / dist_name
python_path = dist_dir / get_distribution(dist_name).python_path
install = mocker.patch(
'hatch.python.core.PythonManager.install', return_value=mocker.MagicMock(path=dist_dir, python_path=python_path)
)
result = hatch('python', 'install', '--private', '-d', str(install_dir), dist_name)
assert result.exit_code == 0, result.output
assert result.output == helpers.dedent(
f"""
Installing {dist_name}
Installed {dist_name} @ {dist_dir}
"""
)
install.assert_called_once_with(dist_name)
path_append.assert_not_called()
def test_all(hatch, temp_dir_data, path_append, default_shells, mocker, compatible_python_distributions):
mocked_dists = []
for name in compatible_python_distributions:
dist_dir = temp_dir_data / 'data' / 'pythons' / name
python_path = dist_dir / get_distribution(name).python_path
mocked_dists.append(mocker.MagicMock(path=dist_dir, python_path=python_path))
install = mocker.patch('hatch.python.core.PythonManager.install', side_effect=mocked_dists)
result = hatch('python', 'install', 'all')
assert result.exit_code == 0, result.output
expected_lines = []
for dist in mocked_dists:
expected_lines.extend((f'Installing {dist.path.name}', f'Installed {dist.path.name} @ {dist.path}'))
expected_lines.extend((
'',
'The following directories have been added to your PATH (pending a shell restart):',
'',
))
expected_lines.extend(str(dist.python_path.parent) for dist in mocked_dists)
expected_lines.append('')
assert result.output == '\n'.join(expected_lines)
assert install.call_args_list == [mocker.call(name) for name in compatible_python_distributions]
assert path_append.call_args_list == [
mocker.call(str(dist.python_path.parent), shells=default_shells) for dist in mocked_dists
]