Coverage for .tox/cov/lib/python3.10/site-packages/confattr/types.py: 100%

111 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-04-22 14:03 +0200

1#!./runmodule.sh 

2 

3import os 

4import shutil 

5import shlex 

6import re 

7import typing 

8from collections.abc import Sequence, Callable, Mapping, MutableMapping 

9 

10from .subprocess_pipe import run_and_pipe, CompletedProcess 

11 

12 

13TYPE_CONTEXT: 'typing.TypeAlias' = 'Callable[[SubprocessCommand], typing.ContextManager[SubprocessCommand]] | None' 

14 

15 

16Regex: 'Callable[[str], re.Pattern[str]]' 

17# when https://github.com/python/typing/issues/213 is implemented I could add more methods 

18class Regex: # type: ignore [no-redef] 

19 

20 type_name = 'regular expression' 

21 help = ''' 

22 A regular expression in python syntax. 

23 You can specify flags by starting the regular expression with `(?aiLmsux)`. 

24 https://docs.python.org/3/library/re.html#regular-expression-syntax 

25 ''' 

26 

27 def __init__(self, pattern: str) -> None: 

28 self._compiled_pattern: 're.Pattern[str]' = re.compile(pattern) 

29 

30 def __getattr__(self, attr: str) -> object: 

31 return getattr(self._compiled_pattern, attr) 

32 

33 def __str__(self) -> str: 

34 return self._compiled_pattern.pattern 

35 

36 def __repr__(self) -> str: 

37 return f'{type(self).__name__}({self._compiled_pattern.pattern!r})' 

38 

39 

40class SubprocessCommand: 

41 

42 type_name = 'command' 

43 help = '''\ 

44 A command to be executed as a subprocess. 

45 The command is executed without a shell so redirection or wildcard expansion is not possible. 

46 Setting environment variables and piping like in a POSIX shell, however, are implemented in python and should work platform independently. 

47 If you need a shell write the command to a file, insert an appropriate shebang line, make the file executable and set this value to the file. 

48 ''' 

49 

50 python_callbacks: 'MutableMapping[str, Callable[[SubprocessCommand, TYPE_CONTEXT], None]]' = {} 

51 

52 @classmethod 

53 def register_python_callback(cls, name: str, func: 'Callable[[SubprocessCommand, TYPE_CONTEXT], None]') -> None: 

54 cls.python_callbacks[name] = func 

55 

56 @classmethod 

57 def unregister_python_callback(cls, name: str) -> None: 

58 del cls.python_callbacks[name] 

59 

60 @classmethod 

61 def has_python_callback(cls, name: str) -> bool: 

62 return name in cls.python_callbacks 

63 

64 

65 def __init__(self, arg: 'SubprocessCommand|Sequence[str]|str', *, env: 'Mapping[str, str]|None' = None) -> None: 

66 self.cmd: 'Sequence[str]' 

67 self.env: 'Mapping[str, str]|None' 

68 if isinstance(arg, str): 

69 assert env is None 

70 self.parse_str(arg) 

71 elif isinstance(arg, SubprocessCommand): 

72 self.cmd = list(arg.cmd) 

73 self.env = dict(arg.env) if arg.env else None 

74 if env: 

75 if self.env: 

76 self.env.update(env) 

77 else: 

78 self.env = env 

79 else: 

80 self.cmd = list(arg) 

81 self.env = env 

82 

83 def parse_str(self, arg: str) -> None: 

84 ''' 

85 Parses a string as returned by :meth:`__str__` and initializes this objcet accordingly 

86 

87 :param arg: The string to be parsed 

88 :raises ValueError: if arg is invalid 

89 

90 Example: 

91 If the input is ``arg = 'ENVVAR1=val ENVVAR2= cmd --arg1 --arg2'`` 

92 this function sets 

93 .. code-block:: 

94 

95 self.env = {'ENVVAR1' : 'val', 'ENVVAR2' : ''} 

96 self.cmd = ['cmd', '--arg1', '--arg2'] 

97 ''' 

98 if not arg: 

99 raise ValueError('cmd is empty') 

100 

101 cmd = shlex.split(arg) 

102 

103 self.env = {} 

104 for i in range(len(cmd)): 

105 if '=' in cmd[i]: 

106 var, val = cmd[i].split('=', 1) 

107 self.env[var] = val 

108 else: 

109 self.cmd = cmd[i:] 

110 if not self.env: 

111 self.env = None 

112 return 

113 

114 raise ValueError('cmd consists of environment variables only, there is no command to be executed') 

115 

116 # ------- compare ------- 

117 

118 def __eq__(self, other: typing.Any) -> bool: 

119 if isinstance(other, SubprocessCommand): 

120 return self.cmd == other.cmd and self.env == other.env 

121 return NotImplemented 

122 

123 # ------- custom methods ------- 

124 

125 def replace(self, wildcard: str, replacement: str) -> 'SubprocessCommand': 

126 return SubprocessCommand([replacement if word == wildcard else word for word in self.cmd], env=self.env) 

127 

128 def run(self, *, context: 'TYPE_CONTEXT|None') -> 'CompletedProcess[bytes]|None': 

129 ''' 

130 Runs this command and returns when the command is finished. 

131 

132 :param context: returns a context manager which can be used to stop and start an urwid screen. 

133 It takes the command to be executed so that it can log the command 

134 and it returns the command to be executed so that it can modify the command, 

135 e.g. processing and intercepting some environment variables. 

136 

137 :return: The completed process 

138 :raises OSError: e.g. if the program was not found 

139 :raises CalledProcessError: if the called program failed 

140 ''' 

141 if self.cmd[0] in self.python_callbacks: 

142 self.python_callbacks[self.cmd[0]](self, context) 

143 return None 

144 

145 if context is None: 

146 return run_and_pipe(self.cmd, env=self._add_os_environ(self.env)) 

147 

148 with context(self) as command: 

149 return run_and_pipe(command.cmd, env=self._add_os_environ(command.env)) 

150 

151 @staticmethod 

152 def _add_os_environ(env: 'Mapping[str, str]|None') -> 'Mapping[str, str]|None': 

153 if env is None: 

154 return env 

155 return dict(os.environ, **env) 

156 

157 def is_installed(self) -> bool: 

158 return self.cmd[0] in self.python_callbacks or bool(shutil.which(self.cmd[0])) 

159 

160 # ------- to str ------- 

161 

162 def __str__(self) -> str: 

163 if self.env: 

164 env = ' '.join('%s=%s' % (var, shlex.quote(val)) for var, val in self.env.items()) 

165 env += ' ' 

166 else: 

167 env = '' 

168 return env + ' '.join(shlex.quote(word) for word in self.cmd) 

169 

170 def __repr__(self) -> str: 

171 return '%s(%r, env=%r)' % (type(self).__name__, self.cmd, self.env) 

172 

173class SubprocessCommandWithAlternatives: 

174 

175 type_name = 'command with alternatives' 

176 help = ''' 

177 One or more commands separated by ||. 

178 The first command where the program is installed is executed. The other commands are ignored. 

179 

180 The command is executed without a shell so redirection or wildcard expansion is not possible. 

181 Setting environment variables and piping like in a POSIX shell, however, are implemented in python and should work platform independently. 

182 If you need a shell write the command to a file, insert an appropriate shebang line, make the file executable and set this value to the file. 

183 ''' 

184 

185 SEP = '||' 

186 

187 def get_preferred_command(self) -> SubprocessCommand: 

188 for cmd in self.commands: 

189 if cmd.is_installed(): 

190 return cmd 

191 

192 raise FileNotFoundError('none of the commands is installed: %s' % self) 

193 

194 

195 def __init__(self, commands: 'Sequence[SubprocessCommand|Sequence[str]|str]|str') -> None: 

196 if isinstance(commands, str): 

197 self.commands = [SubprocessCommand(cmd) for cmd in commands.split(self.SEP)] 

198 else: 

199 self.commands = [SubprocessCommand(cmd) for cmd in commands] 

200 

201 

202 def __str__(self) -> str: 

203 return self.SEP.join(str(cmd) for cmd in self.commands) 

204 

205 def __repr__(self) -> str: 

206 return '%s(%s)' % (type(self).__name__, self.commands) 

207 

208 

209 def replace(self, wildcard: str, replacement: str) -> SubprocessCommand: 

210 return self.get_preferred_command().replace(wildcard, replacement) 

211 

212 def run(self, context: 'TYPE_CONTEXT|None' = None) -> 'CompletedProcess[bytes]|None': 

213 return self.get_preferred_command().run(context=context)