Skip to content

Commit

Permalink
Merge pull request #7664 from stealthycoin/missing-flit-core
Browse files Browse the repository at this point in the history
Add error message when flit_core is not installed
  • Loading branch information
stealthycoin authored Feb 14, 2023
2 parents 12d1742 + f9c7813 commit 3ffd858
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 61 deletions.
59 changes: 58 additions & 1 deletion backends/build_system/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
# language governing permissions and limitations under the License.
import os
import re
import sys
import shlex
import json
import shutil
import subprocess
Expand All @@ -21,6 +23,7 @@

from constants import ROOT_DIR
from constants import IS_WINDOWS
from constants import BOOTSTRAP_REQUIREMENTS


PACKAGE_NAME = re.compile(r"(?P<name>[A-Za-z][A-Za-z0-9_\.\-]+)(?P<rest>.+)")
Expand All @@ -34,6 +37,39 @@
}


class UnmetDependenciesException(Exception):
def __init__(self, unmet_deps, in_venv, reason=None):
pip_install_command_args = ["-m", "pip", "install", "--prefer-binary"]
msg = "Environment requires following Python dependencies:\n\n"
for package, actual_version, required in unmet_deps:
msg += (
f"{package} (required: {required.constraints}) "
f"(version installed: {actual_version})\n"
)
pip_install_command_args.append(f'{package}{required.string_constraints()}')

if reason:
msg += f"\n{reason}\n"

msg += (
"\n"
"We recommend using --with-download-deps flag to automatically create a "
"virtualenv and download the dependencies.\n\n"
"If you want to manage the dependencies yourself instead, run the following "
"pip command:\n"
)
msg += f"{sys.executable} {shlex.join(pip_install_command_args)}\n"

if not in_venv:
msg += (
"\nWe noticed you are not in a virtualenv.\nIf not using --with-download-deps "
"we highly recommend using a virtualenv to prevent dependencies "
"from being installed into your global "
"Python environment.\n"
)
super().__init__(msg)


@contextlib.contextmanager
def cd(dirname):
original = os.getcwd()
Expand Down Expand Up @@ -121,7 +157,11 @@ def _parse_req_line(line: str):


def get_install_requires():
import flit_core.buildapi
try:
import flit_core.buildapi
except ImportError:
flit_core_exception = get_flit_core_unmet_exception()
raise flit_core_exception

with cd(ROOT_DIR):
requires = flit_core.buildapi.get_requires_for_build_wheel()
Expand All @@ -140,6 +180,23 @@ def get_install_requires():
return dependencies


def get_flit_core_unmet_exception():
in_venv = sys.prefix != sys.base_prefix
with open(BOOTSTRAP_REQUIREMENTS, 'r') as f:
flit_core_req = [
l for l in f.read().split('\n')
if 'flit_core' in l
]
return UnmetDependenciesException(
[('flit_core', None, list(parse_requirements(flit_core_req))[0])],
in_venv,
reason=(
'flit_core is needed ahead of time in order to parse the '
'rest of the requirements.'
)
)


class Utils:
def isdir(self, path: str) -> bool:
return os.path.isdir(path)
Expand Down
33 changes: 2 additions & 31 deletions backends/build_system/validate_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
# language governing permissions and limitations under the License.
import re
import sys
import shlex
from pathlib import Path
import importlib.metadata

Expand All @@ -21,6 +20,8 @@
PORTABLE_EXE_REQUIREMENTS,
)
from utils import get_install_requires, parse_requirements
from utils import UnmetDependenciesException


ROOT = Path(__file__).parents[2]
PYPROJECT = ROOT / "pyproject.toml"
Expand All @@ -30,36 +31,6 @@
EXTRACT_DEPENDENCIES_RE = re.compile(r'"(.+)"')


class UnmetDependenciesException(Exception):
def __init__(self, unmet_deps, in_venv):
pip_install_command_args = ["-m", "pip", "install", "--prefer-binary"]
msg = "Environment requires following Python dependencies:\n\n"
for package, actual_version, required in unmet_deps:
msg += (
f"{package} (required: {required.constraints}) "
f"(version installed: {actual_version})\n"
)
pip_install_command_args.append(f'{package}{required.string_constraints()}')

msg += (
"\n"
"We recommend using --with-download-deps flag to automatically create a "
"virtualenv and download the dependencies.\n\n"
"If you want to manage the dependencies yourself instead, run the following "
"pip command:\n"
)
msg += f"{sys.executable} {shlex.join(pip_install_command_args)}\n"

if not in_venv:
msg += (
"\nWe noticed you are not in a virtualenv.\nIf not using --with-download-deps "
"we highly recommend using a virtualenv to prevent dependencies "
"from being installed into your global "
"Python environment.\n"
)
super().__init__(msg)


def validate_env(target_artifact):
requirements = _get_requires_list(target_artifact)
unmet_deps = _get_unmet_dependencies(requirements)
Expand Down
41 changes: 40 additions & 1 deletion tests/backends/build_system/functional/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import os
import sys
import json
import platform
from typing import List
Expand All @@ -21,6 +22,7 @@
from build_system.utils import parse_requirements
from build_system.utils import ParseError
from build_system.utils import Requirement
from build_system.utils import UnmetDependenciesException

from tests.backends.build_system.markers import skip_if_windows, if_windows

Expand Down Expand Up @@ -241,4 +243,41 @@ def test_create_venv_windows(self, utils: Utils, tmp_path):
)

def assert_dir_has_content(self, path: str, expected_files: List[str]):
assert set(expected_files).issubset(set(os.listdir(path)))
assert set(l.lower() for l in expected_files).issubset(
set(l.lower() for l in os.listdir(path))
)


@pytest.fixture
def unmet_error(request):
error = UnmetDependenciesException([
('colorama', '1.0', Requirement('colorama', '>=2.0', '<3.0')),
], **request.param)
return str(error)


class TestUnmetDependencies:
@pytest.mark.parametrize('unmet_error', [{'in_venv': False}], indirect=True)
def test_in_error_message(self, unmet_error):
assert (
"colorama (required: ('>=2.0', '<3.0')) (version installed: 1.0)"
) in unmet_error
assert (
f"{sys.executable} -m pip install --prefer-binary 'colorama>=2.0,<3.0'"
) in unmet_error

@pytest.mark.parametrize('unmet_error', [{'in_venv': False}], indirect=True)
def test_not_in_venv(self, unmet_error):
assert 'We noticed you are not in a virtualenv.' in unmet_error

@pytest.mark.parametrize('unmet_error', [{'in_venv': True}], indirect=True)
def test_in_venv(self, unmet_error):
assert 'We noticed you are not in a virtualenv.' not in unmet_error

@pytest.mark.parametrize(
'unmet_error',
[{'in_venv': False, 'reason': "custom reason message"}],
indirect=True,
)
def test_custom_reason(self, unmet_error):
assert 'custom reason message' in unmet_error
28 changes: 0 additions & 28 deletions tests/backends/build_system/unit/test_validate_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,4 @@

import pytest

from backends.build_system.validate_env import UnmetDependenciesException
from backends.build_system.utils import Requirement


@pytest.fixture
def unmet_error(request):
error = UnmetDependenciesException([
('colorama', '1.0', Requirement('colorama', '>=2.0', '<3.0')),
], in_venv=request.param)
return str(error)


class TestUnmetDependencies:
@pytest.mark.parametrize('unmet_error', [False], indirect=True)
def test_in_error_message(self, unmet_error):
assert (
"colorama (required: ('>=2.0', '<3.0')) (version installed: 1.0)"
) in unmet_error
assert (
f"{sys.executable} -m pip install --prefer-binary 'colorama>=2.0,<3.0'"
) in unmet_error

@pytest.mark.parametrize('unmet_error', [False], indirect=True)
def test_not_in_venv(self, unmet_error):
assert 'We noticed you are not in a virtualenv.' in unmet_error

@pytest.mark.parametrize('unmet_error', [True], indirect=True)
def test_in_venv(self, unmet_error):
assert 'We noticed you are not in a virtualenv.' not in unmet_error

0 comments on commit 3ffd858

Please sign in to comment.