Skip to content

Commit

Permalink
Allow to use virtual environment site-packages on Windows
Browse files Browse the repository at this point in the history
On Windows, Python virtual environment keeps the 3rd partiies libraries under `{venv_dir}\Lib\site-packages`. Python version is not mentioned anywhere in the directories names.
  • Loading branch information
mknorps committed Dec 21, 2023
1 parent 36e957a commit 8c4ecb9
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 7 deletions.
10 changes: 10 additions & 0 deletions fawltydeps/packages.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Encapsulate the lookup of packages and their provided import names."""

import logging
import os
import platform
import subprocess
import sys
Expand Down Expand Up @@ -346,6 +347,15 @@ def find_package_dirs(cls, path: Path) -> Iterator[Path]:
if found:
return

# Check for packages on Windows
if platform.system() == "Windows":
for site_packages in path.glob(os.path.join("Lib", "site-packages")):
if site_packages.is_dir():
yield site_packages
found = True
if found:
return

# Given path is not a python environment, but it might be _inside_ one.
# Try again with parent directory
if path.parent != path:
Expand Down
8 changes: 8 additions & 0 deletions fawltydeps/types.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Common types used across FawltyDeps."""

import os
import platform
import sys
from abc import ABC, abstractmethod
from dataclasses import asdict, dataclass, field, replace
Expand Down Expand Up @@ -155,6 +157,12 @@ def __post_init__(self) -> None:
elif self.path.match("__pypackages__/?.*/lib"):
return # also ok

# Support Windows projects
if platform.system() == "Windows" and self.path.match(
os.path.join("Lib", "site-packages")
):
return # also ok

raise ValueError(f"{self.path} is not a valid dir for Python packages!")

def render(self, detailed: bool) -> str:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ profile = "black"
main.jobs = 4
main.py-version = "3.7"
reports.output-format = "colorized"
"messages control".disable = "fixme,logging-fstring-interpolation,unspecified-encoding,too-few-public-methods,consider-using-in,duplicate-code"
"messages control".disable = "fixme,logging-fstring-interpolation,unspecified-encoding,too-few-public-methods,consider-using-in,duplicate-code,too-many-locals,too-many-branches"

[tool.mypy]
files = ['*.py', 'fawltydeps/*.py', 'tests/*.py']
Expand Down
9 changes: 8 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Fixtures for tests"""
import platform
import sys
import venv
from pathlib import Path
Expand Down Expand Up @@ -54,7 +55,13 @@ def create_one_fake_venv(

# Create fake packages
major, minor = py_version
site_dir = venv_dir / f"lib/python{major}.{minor}/site-packages"

def _env_site_packages():
if platform.system() == "Windows":
return venv_dir / "Lib" / "site-packages"
return venv_dir / "lib" / f"python{major}.{minor}" / "site-packages"

site_dir = _env_site_packages()
assert site_dir.is_dir()
for package_name, import_names in fake_packages.items():
# Create just enough files under site_dir to fool importlib_metadata
Expand Down
25 changes: 21 additions & 4 deletions tests/test_cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,9 @@ def test_list_sources__in_varied_project__lists_all_files(fake_project):
"pyproject.toml",
"setup.py",
"setup.cfg",
f"my_venv/lib/python{major}.{minor}/site-packages",
os.path.join("my_venv", "Lib", "site-packages")
if platform.system() == "Windows"
else f"my_venv/lib/python{major}.{minor}/site-packages",
]
]
assert_unordered_equivalence(output.splitlines()[:-2], expect)
Expand Down Expand Up @@ -497,8 +499,19 @@ def test_list_sources_detailed__in_varied_project__lists_all_files(fake_project)
]
major, minor = sys.version_info[:2]
expect_pyenv_lines = [
f" {tmp_path}/my_venv/lib/python{major}.{minor}/site-packages "
+ "(as a source of Python packages)",
(
" " + str(tmp_path / "my_venv" / "Lib" / "site-packages")
if platform.system() == "Windows"
else " "
+ str(
tmp_path
/ "my_venv"
/ "lib"
/ f"python{major}.{minor}"
/ "site-packages"
)
)
+ " (as a source of Python packages)",
]
expect = [
"Sources of Python code:",
Expand Down Expand Up @@ -688,7 +701,11 @@ def test_check_json__simple_project__can_report_both_undeclared_and_unused(
},
{
"source_type": "PyEnvSource",
"path": f"{tmp_path}/my_venv/lib/python{major}.{minor}/site-packages",
"path": (
f"{tmp_path / 'my_venv' / 'Lib' / 'site-packages'}"
if platform.system() == "Windows"
else f"{tmp_path}/my_venv/lib/python{major}.{minor}/site-packages"
),
},
],
"imports": [
Expand Down
14 changes: 13 additions & 1 deletion tests/test_local_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@
f"__pypackages__/{major}.{minor}/lib",
]

# When the user gives us a --pyenv arg that points to a Python virtualenv
# on Windows, what are the the possible paths inside that Python environment
# that they might point at (and that we should accept)?
windows_subdirs = [
"",
"Lib",
os.path.join("Lib", "site-packages"),
]

@pytest.mark.parametrize(
"subdir", [pytest.param(d, id=f"venv:{d}") for d in env_subdirs]
Expand Down Expand Up @@ -128,7 +136,11 @@ def test_local_env__default_venv__contains_pip(tmp_path):
# "pip" package is installed, and that it provides a "pip" import name.
venv.create(tmp_path, with_pip=True)
lpl = LocalPackageResolver(pyenv_sources(tmp_path))
expect_location = tmp_path / f"lib/python{major}.{minor}/site-packages"
expect_location = (
tmp_path / "Lib" / "site-packages"
if platform.system() == "Windows"
else tmp_path / f"lib/python{major}.{minor}/site-packages"
)
assert "pip" in lpl.packages
pip = lpl.packages["pip"]
assert pip.package_name == "pip"
Expand Down

0 comments on commit 8c4ecb9

Please sign in to comment.