Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Try to make virtualenv metadata findable when invoked globally #207

Closed
wants to merge 11 commits into from
16 changes: 16 additions & 0 deletions deptry/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
from deptry.compat import metadata
from deptry.config import read_configuration_from_pyproject_toml
from deptry.core import Core
from deptry.metadata_finder import (
install_metadata_finder,
warn_if_not_running_in_virtualenv,
)
from deptry.utils import PYPROJECT_TOML_PATH, run_within_dir


Expand Down Expand Up @@ -187,6 +191,12 @@ def display_deptry_version(ctx: click.Context, _param: click.Parameter, value: b
expose_value=False,
hidden=True,
)
@click.option(
"--python-site-packages",
"site_packages",
type=click.Path(exists=True, path_type=Path),
help="Path to the site-packages directory where dependencies are installed. Required if `deptry` is run while being installed globally.",
)
def deptry(
root: Path,
ignore_obsolete: Tuple[str, ...],
Expand All @@ -203,6 +213,7 @@ def deptry(
requirements_txt: Tuple[str, ...],
requirements_txt_dev: Tuple[str, ...],
json_output: str,
site_packages: Path,
) -> None:
"""Find dependency issues in your Python project.

Expand All @@ -211,6 +222,11 @@ def deptry(

"""

if site_packages:
install_metadata_finder(site_packages)
else:
warn_if_not_running_in_virtualenv(root)

with run_within_dir(root):
Core(
ignore_obsolete=ignore_obsolete,
Expand Down
79 changes: 79 additions & 0 deletions deptry/metadata_finder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import logging
import os
import sys
from dataclasses import dataclass
from pathlib import Path

from deptry.compat import metadata


@dataclass
class ExecutionContext:
project_root: Path
base_prefix: str
prefix: str
active_virtual_env: str = None

@classmethod
def from_runtime(cls, project_root: Path):
return cls(
project_root=project_root,
base_prefix=sys.base_prefix,
prefix=sys.prefix,
active_virtual_env=os.environ.get("VIRTUAL_ENV"),
)

@property
def project_name(self):
return self.project_root.absolute().name

def running_in_project_virtualenv(self) -> bool:
"""Determine if executed by the interpreter in the project's virtual environment

If we are executed from virtual environment, the context `prefix`
will be set to the virtual environment's directory, whereas
`base_prefix` will point to the global Python installation
used to create the virtual environment.

The `active_virtual_env` context field holds the value of the
VIRTUAL_ENV environment variable, that tells with good reliability
that a virtual environment has been activated in the current shell.
"""

# Gobal installation
if self.prefix == self.base_prefix:
return False

# No virtualenv has been activated. Unless the project name is in
# the interpreter path, assume we are not in the project's virtualenv
if not self.active_virtual_env:
return self.project_name in self.prefix

# A virtualenv has been activated. But if `deptry` was installed gloabally using
# `pipx`, we could be running in another installation.
return self.active_virtual_env == self.prefix


def install_metadata_finder(site_packages: Path) -> None:
"""Add poject virtualenv site packages to metadata search path"""
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo in project

path = [str(site_packages.absolute()), *sys.path]

class VirtualenvDistributionFinder(metadata.MetadataPathFinder):
@classmethod
def find_distributions(cls, context):
context = metadata.DistributionFinder.Context(name=context.name, path=path)
return super().find_distributions(context)

sys.meta_path.insert(0, VirtualenvDistributionFinder())


def warn_if_not_running_in_virtualenv(root: Path) -> None:
ctx = ExecutionContext.from_runtime(root)
if not ctx.running_in_project_virtualenv():
log_msg = (
f"If deptry is not installed within the `{ctx.project_name}` project's virtual environment, it does not "
"have access to the metadata of dependencies installed within the virtual environment. This can be "
"solved by installing deptry in the virtual environment, or by passing the path to your virtual "
"environment's site-packages directory as the `--python-site-packages` argument."
)
logging.warn(log_msg)
25 changes: 25 additions & 0 deletions tests/test_virtualenv_finder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from pathlib import Path

import pytest

from deptry.metadata_finder import ExecutionContext


@pytest.mark.parametrize(
"params, expected",
[
# Global installation
((Path("theproject"), "/usr", "/usr", None), False),
# Direct invocation with project interpreter
((Path("theproject"), "/usr", "/home/user/.virtualenvs/theproject", None), True),
# Pipx global install. Project virtualenv active
((Path("theproject"), "/usr", "/home/user/.local/pipx/venvs/deptry", "/home/user/theproject/.venv"), False),
# Project virtualenv active and running
((Path("theproject"), "/usr", "/home/user/theproject/.venv", "/home/user/theproject/.venv"), True),
],
)
def test_running_in_project_virtualenv(params, expected):
arg_names = ("project_root", "base_prefix", "prefix", "active_virtual_env")
kwargs = dict(zip(arg_names, params))
ctx = ExecutionContext(**kwargs)
assert ctx.running_in_project_virtualenv() == expected