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

1#!./runmodule.sh

2

3 import os

4 import shutil

5 import shlex

6 import re

7 import typing

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

9

10 from .subprocess_pipe import run_and_pipe, CompletedProcess

11

12

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

14

15

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

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

18 class 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

40 class 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

173 class 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)