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

pip, _cli: Add --path argument to mirror pip list #148

Merged
merged 10 commits into from
Dec 3, 2021
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ python -m pip install pip-audit
usage: pip-audit [-h] [-V] [-l] [-r REQUIREMENTS] [-f FORMAT] [-s SERVICE]
[-d] [-S] [--desc [{on,off,auto}]] [--cache-dir CACHE_DIR]
[--progress-spinner {on,off}] [--timeout TIMEOUT]
[--path PATHS]

audit the Python environment for dependencies with known vulnerabilities

Expand Down Expand Up @@ -62,6 +63,9 @@ optional arguments:
--progress-spinner {on,off}
display a progress spinner (default: on)
--timeout TIMEOUT set the socket timeout (default: 15)
--path PATHS restrict to the specified installation path for
auditing packages; this option can be used multiple
times (default: [])
```
<!-- @end-pip-audit-help@ -->

Expand Down
14 changes: 12 additions & 2 deletions pip_audit/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,15 @@ def audit() -> None:
description="audit the Python environment for dependencies with known vulnerabilities",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
req_source_args = parser.add_mutually_exclusive_group()
parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}")
parser.add_argument(
"-l",
"--local",
action="store_true",
help="show only results for dependencies in the local environment",
)
parser.add_argument(
req_source_args.add_argument(
"-r",
"--requirement",
type=argparse.FileType("r"),
Expand Down Expand Up @@ -216,6 +217,15 @@ def audit() -> None:
parser.add_argument(
"--timeout", type=int, default=15, help="set the socket timeout" # Match the `pip` default
)
req_source_args.add_argument(
"--path",
type=Path,
action="append",
dest="paths",
default=[],
help="restrict to the specified installation path for auditing packages; "
"this option can be used multiple times",
)

args = parser.parse_args()
logger.debug(f"parsed arguments: {args}")
Expand All @@ -232,7 +242,7 @@ def audit() -> None:
req_files: List[Path] = [Path(req.name) for req in args.requirements]
source = RequirementSource(req_files, ResolveLibResolver(args.timeout, state), state)
else:
source = PipSource(local=args.local)
source = PipSource(local=args.local, paths=args.paths)

auditor = Auditor(service, options=AuditOptions(dry_run=args.dry_run))

Expand Down
12 changes: 9 additions & 3 deletions pip_audit/_dependency_source/pip.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"""

import logging
from typing import Iterator, Optional
from pathlib import Path
from typing import Iterator, Optional, Sequence

import pip_api
from packaging.version import InvalidVersion, Version
Expand Down Expand Up @@ -32,7 +33,9 @@ class PipSource(DependencySource):
Wraps `pip` (specifically `pip list`) as a dependency source.
"""

def __init__(self, *, local: bool = False, state: Optional[AuditState] = None) -> None:
def __init__(
self, *, local: bool = False, paths: Sequence[Path] = [], state: Optional[AuditState] = None
) -> None:
"""
Create a new `PipSource`.

Expand All @@ -42,6 +45,7 @@ def __init__(self, *, local: bool = False, state: Optional[AuditState] = None) -
`state` is an optional `AuditState` to use for state callbacks.
"""
self._local = local
self._paths = paths
self.state = state

if _PIP_VERSION < _MINIMUM_RELIABLE_PIP_VERSION:
Expand All @@ -61,7 +65,9 @@ def collect(self) -> Iterator[Dependency]:
# The `pip list` call that underlies `pip_api` could fail for myriad reasons.
# We collect them all into a single well-defined error.
try:
for (_, dist) in pip_api.installed_distributions(local=self._local).items():
for (_, dist) in pip_api.installed_distributions(
local=self._local, paths=list(self._paths)
woodruffw marked this conversation as resolved.
Show resolved Hide resolved
).items():
dep: Dependency
try:
dep = ResolvedDependency(name=dist.name, version=Version(str(dist.version)))
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
platforms="any",
python_requires=">=3.6",
install_requires=[
"pip-api>=0.0.23",
"pip-api>=0.0.25",
"packaging>=21.0.0",
# TODO: Remove this once 3.7 is our minimally supported version.
"dataclasses>=0.6; python_version < '3.7'",
Expand Down
7 changes: 5 additions & 2 deletions test/dependency_source/test_pip.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
from dataclasses import dataclass
from typing import Dict
from typing import Dict, List

import pip_api
import pretend # type: ignore
Expand Down Expand Up @@ -61,7 +62,9 @@ class MockDistribution:

# Return a distribution with a version that doesn't conform to PEP 440.
# We should log a debug message and skip it.
def mock_installed_distributions(local: bool) -> Dict[str, MockDistribution]:
def mock_installed_distributions(
local: bool, paths: List[os.PathLike]
) -> Dict[str, MockDistribution]:
return {
"pytest": MockDistribution("pytest", "0.1"),
"pip-audit": MockDistribution("pip-audit", "1.0-ubuntu0.21.04.1"),
Expand Down