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

1"""Use pip install --report flag to separate pinned requirements for 

2different optional dependencies (e.g. 'dev' and 'doc' requirements).""" 

3 

4import argparse 

5import sys 

6from typing import Optional 

7from pathlib import Path 

8 

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 

13 

14 

15def get_pip_version(python_exec: Path) -> str: 

16 """Return pip --version output. 

17 

18 Returns: 

19 pip --version output (str) 

20 """ 

21 return run_pip(command=[python_exec, "-m", "pip", "--version"], check_output=True) 

22 

23 

24def validate_pip_version(pip_version_output: str) -> bool: 

25 """Check if pip version is >= 22.2. 

26 

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 

41 

42 

43def determine_default_file() -> Optional[Path]: 

44 """Determine default input file if none has been specified. 

45 

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 

56 

57 

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 

125 

126 

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.") 

149 

150 

151if __name__ == "__main__": 

152 main()