Coverage for src/iso_freeze/cli.py: 77%
53 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-08-01 11:38 +0200
« prev ^ index » next coverage.py v6.4.2, created at 2022-08-01 11:38 +0200
1"""Use pip install --report flag to separate pinned requirements for
2different optional dependencies (e.g. 'dev' and 'doc' requirements)."""
4import argparse
5import sys
6from typing import Optional
7from pathlib import Path
9from iso_freeze.get_requirements import get_pip_report_requirements
10from iso_freeze.lib import run_pip
11from iso_freeze.sync import sync
12from iso_freeze.pin_requirements import pin_requirements
15def get_pip_version(python_exec: Path) -> str:
16 """Return pip --version output.
18 Returns:
19 pip --version output (str)
20 """
21 return run_pip(command=[python_exec, "-m", "pip", "--version"], check_output=True)
24def validate_pip_version(pip_version_output: str) -> bool:
25 """Check if pip version is >= 22.2.
27 Returns:
28 True/False (bool)
29 """
30 # Output of pip --version looks like this:
31 # "pip 22.2 from <path to pip> (<python version>)"
32 # To get version number, split this message on whitespace and pick list item 1.
33 # To check against minimum version, turn the version number into a list of ints
34 # (e.g. '[22, 2]' or '[21, 1, 2]')
35 pip_version: list[int] = [
36 int(number) for number in pip_version_output.split()[1].split(".")
37 ]
38 if pip_version >= [22, 2]:
39 return True
40 return False
43def determine_default_file() -> Optional[Path]:
44 """Determine default input file if none has been specified.
46 Returns:
47 Path to default file (Optional[Path])
48 """
49 if Path("requirements.in").exists():
50 default: Optional[Path] = Path("requirements.in")
51 elif Path("pyproject.toml").exists():
52 default = Path("pyproject.toml")
53 else:
54 default = None
55 return default
58def parse_args() -> argparse.Namespace:
59 """Parse arguments."""
60 argparser = argparse.ArgumentParser(
61 description="Use pip install --report to cleanly separate pinned requirements "
62 "for different optional dependencies (e.g. 'dev' and 'doc' requirements)."
63 )
64 argparser.add_argument(
65 "file",
66 type=Path,
67 nargs="?",
68 default=determine_default_file(),
69 help="Path to input file. Can be pyproject.toml or requirements file. "
70 "Defaults to 'requirements.in' or 'pyproject.toml' in current directory.",
71 )
72 argparser.add_argument(
73 "--dependency",
74 "-d",
75 type=str,
76 help="Name of the optional dependency defined in pyproject.toml to include.",
77 )
78 argparser.add_argument(
79 "--output",
80 "-o",
81 type=Path,
82 default=Path("requirements.txt"),
83 help="Name of the output file. Defaults to 'requirements.txt' if unspecified.",
84 )
85 argparser.add_argument(
86 "--python",
87 "-p",
88 type=Path,
89 default=Path("python3"),
90 help="Specify path to Python interpreter to use. Defaults to 'python3'.",
91 )
92 argparser.add_argument(
93 "--pip-args",
94 type=str,
95 help="List of arguments to be passed to pip install. Call as: "
96 'pip-args "--arg1 value --arg2 value".',
97 )
98 argparser.add_argument(
99 "--sync",
100 "-s",
101 action="store_true",
102 help="Sync current environment with dependencies listed in file (removes "
103 "packages that are not dependencies in file, adds those that are missing)",
104 )
105 argparser.add_argument(
106 "--hashes", action="store_true", help="Add hashes to output file."
107 )
108 args = argparser.parse_args()
109 if not args.file:
110 sys.exit(
111 "No requirements.in or pyproject.toml file found in current directory. "
112 "Please specify input file."
113 )
114 if args.file.suffix != ".toml" and args.dependency:
115 sys.exit(
116 "You can only specify an optional dependency if your input file is "
117 "pyproject.toml."
118 )
119 if not args.file.is_file():
120 sys.exit(f"Not a file: {args.file}")
121 # If pip-args have been provided, split them into list
122 if args.pip_args:
123 args.pip_args = args.pip_args.split(" ")
124 return args
127def main() -> None:
128 """ClI entry point."""
129 arguments: argparse.Namespace = parse_args()
130 if not validate_pip_version(pip_version_output=get_pip_version(arguments.python)):
131 sys.exit("pip >= 22.2 required. Please update pip and try again.")
132 pip_report_requirements = get_pip_report_requirements(
133 file=arguments.file,
134 python_exec=arguments.python,
135 pip_args=arguments.pip_args,
136 optional_dependency=arguments.dependency,
137 )
138 if pip_report_requirements:
139 if arguments.sync:
140 sync(requirements=pip_report_requirements, python_exec=arguments.python)
141 else:
142 pin_requirements(
143 requirements=pip_report_requirements,
144 hashes=arguments.hashes,
145 output_file=arguments.output,
146 )
147 else:
148 sys.exit("There are no requirements to pin.")
151if __name__ == "__main__":
152 main()