Skip to content
New issue

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

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

Already on GitHub? # 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)
).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