Coverage for roter/cli.py: 25.26%

69 statements  

« prev     ^ index     » next       coverage.py v7.5.4, created at 2024-06-24 19:21:34 +00:00

1"""CLI operations for rotate and combine tables (Danish: Roter og kombiner borde).""" 

2 

3import argparse 

4import pathlib 

5import sys 

6from typing import Union 

7 

8import roter.api as api 

9from roter import ( 

10 APP_ALIAS, 

11 APP_NAME, 

12 APP_VERSION, 

13 CHILD_ATTRIBUTES, 

14 COMMA, 

15 MARKERS, 

16 PARENT_ATTRIBUTES, 

17 parse_csl_preserve_case 

18) 

19 

20 

21def parse_request(argv: list[str]) -> Union[int, argparse.Namespace]: 

22 """DRY.""" 

23 parser = argparse.ArgumentParser( 

24 prog=APP_ALIAS, description=APP_NAME, formatter_class=argparse.RawTextHelpFormatter 

25 ) 

26 parser.add_argument( 

27 '--table-files', 

28 '-t', 

29 dest='table_files', 

30 default='', 

31 help='Markdown files with tables to parse. Optional\n(default: positional table files value)', 

32 required=False, 

33 ) 

34 parser.add_argument( 

35 'table_files_pos', nargs='*', default='', help='markdown files with tables to parse. Optional' 

36 ) 

37 parser.add_argument( 

38 '--excludes', 

39 '-x', 

40 dest='excludes', 

41 default='', 

42 help='comma separated list of values to exclude paths\ncontaining the substring (default: empty string)', 

43 ) 

44 parser.add_argument( 

45 '--out-path', 

46 '-o', 

47 dest='out_path', 

48 default=sys.stdout, 

49 help='output file path (stem) to inject combined and inverted markdown tables in between markers', 

50 ) 

51 parser.add_argument( 

52 '--markers', 

53 '-m', 

54 dest='markers', 

55 type=str, 

56 default=MARKERS, 

57 help=f'comma separated begin/end markers in output file path (default: MARKERS)', 

58 ) 

59 parser.add_argument( 

60 '--child-attributes', 

61 '-c', 

62 dest='child_attributes', 

63 type=str, 

64 default=CHILD_ATTRIBUTES, 

65 help=f'heading position and labels (singular and plural) for children related columns (default:{CHILD_ATTRIBUTES})', 

66 ) 

67 parser.add_argument( 

68 '--parent-attributes', 

69 '-p', 

70 dest='parent_attributes', 

71 type=str, 

72 default=PARENT_ATTRIBUTES, 

73 help=f'heading position and labels (singular and plural) for parents related columns (default: {PARENT_ATTRIBUTES})', 

74 ) 

75 parser.add_argument( 

76 '--invert-only', 

77 dest='invert_only', 

78 default=False, 

79 action='store_true', 

80 help='Only inject the inverted table', 

81 required=False, 

82 ) 

83 parser.add_argument( 

84 '--concat-only', 

85 dest='concat_only', 

86 default=False, 

87 action='store_true', 

88 help='Only inject the combined table', 

89 required=False, 

90 ) 

91 parser.add_argument( 

92 '--version', 

93 '-V', 

94 dest='version_request', 

95 default=False, 

96 action='store_true', 

97 help='show version of the app and exit', 

98 required=False, 

99 ) 

100 

101 if not argv: 101 ↛ 106line 101 didn't jump to line 106 because the condition on line 101 was always true

102 print(f'{APP_NAME} version {APP_VERSION}') 

103 parser.print_help() 

104 return 0 

105 

106 options = parser.parse_args(argv) 

107 

108 if options.version_request: 

109 print(f'{APP_NAME} version {APP_VERSION}') 

110 return 0 

111 

112 if not options.table_files: 

113 if options.table_files_pos: 

114 options.table_files = options.table_files_pos 

115 else: 

116 parser.error('missing any paths to parse tables from') 

117 else: 

118 options.table_files = [p.strip() for p in options.table_files.split() if p.strip()] 

119 if options.table_files_pos: 

120 options.table_files.extend(options.table_files_pos) 

121 

122 options.excludes_parsed = parse_csl(options.excludes) if options and options.excludes else [] 

123 options.paths = (pathlib.Path(p) for p in options.table_files if not any(x in p for x in options.excludes_parsed )) 

124 if not options.paths: 

125 parser.error('missing non-excluded paths to parse tables from') 

126 

127 if options.out_path is sys.stdout: 

128 parser.error('missing output template to inject combined and inverted tables into') 

129 

130 if options.markers.count(COMMA) != 4 - 1: 

131 parser.error('4 markers separated by comma are required to inject two tables') 

132 

133 markers_seq = parse_csl_preserve_case(options.markers) 

134 if len(markers_seq) != 4: 

135 parser.error('4 non-empty markers are required to inject two tables') 

136 

137 options.markers_combined = (markers_seq[0], markers_seq[1]) 

138 options.markers_inverted = (markers_seq[2], markers_seq[3]) 

139 

140 child_seq = parse_csl_preserve_case(options.child_attributes) 

141 if len(child_seq) != 3: 

142 parser.error('3 non-empty child attributes are required to process the tables') 

143 try: 

144 options.child_column_pos = int(child_seq[0]) 

145 except: # noqa 

146 parser.error('child column position as integer in [1, N] is required to process the tables') 

147 options.child, options.children = child_seq[1], child_seq[2] 

148 

149 parent_seq = parse_csl_preserve_case(options.parent_attributes) 

150 if len(parent_seq) != 3: 

151 parser.error('3 non-empty parent attributes are required to process the tables') 

152 try: 

153 options.parents_column_pos = int(parent_seq[0]) 

154 except: # noqa 

155 parser.error('parent column position as integer in [1, N] is required to process the tables') 

156 options.parent, options.parents = parent_seq[1], parent_seq[2] 

157 

158 return options 

159 

160 

161def main(argv: Union[list[str], None] = None) -> int: 

162 """Delegate processing to functional module.""" 

163 argv = sys.argv[1:] if argv is None else argv 

164 options = parse_request(argv) 

165 if isinstance(options, int): 

166 return 0 

167 return api.main(options)