diff --git a/fawltydeps/packages.py b/fawltydeps/packages.py index 4a17c6e1..438d1363 100644 --- a/fawltydeps/packages.py +++ b/fawltydeps/packages.py @@ -1,6 +1,7 @@ """Encapsulate the lookup of packages and their provided import names.""" import logging +import os import platform import subprocess import sys @@ -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: diff --git a/fawltydeps/types.py b/fawltydeps/types.py index 45f6aeb8..788ce5d8 100644 --- a/fawltydeps/types.py +++ b/fawltydeps/types.py @@ -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 @@ -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: diff --git a/pyproject.toml b/pyproject.toml index c6b18ded..a9850dd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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'] diff --git a/tests/conftest.py b/tests/conftest.py index dd041ec7..515b749b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ """Fixtures for tests""" +import platform import sys import venv from pathlib import Path @@ -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 diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 3e5c0072..f60a7d85 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -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) @@ -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:", @@ -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": [ diff --git a/tests/test_local_env.py b/tests/test_local_env.py index d372cd15..92f5a9ed 100644 --- a/tests/test_local_env.py +++ b/tests/test_local_env.py @@ -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] @@ -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"