Coverage for .tox/cov/lib/python3.10/site-packages/confattr/types.py: 79%
112 statements
« prev ^ index » next coverage.py v7.2.1, created at 2023-03-06 19:52 +0100
« prev ^ index » next coverage.py v7.2.1, created at 2023-03-06 19:52 +0100
1#!./runmodule.sh
3import os
4import shutil
5import shlex
6import typing
7from collections.abc import Sequence, Callable, Mapping, MutableMapping
9from .subprocess_pipe import run_and_pipe, CompletedProcess
12TYPE_CONTEXT: 'typing.TypeAlias' = 'Callable[[SubprocessCommand], typing.ContextManager[SubprocessCommand]] | None'
15class Path:
17 '''
18 This is the path as it is stored in the config file.
19 It needs to be processed before usage.
20 In the easiest case that is as easy as calling os.path.expanduser
21 but you may want to do more like checking that the path exists
22 or mounting an external drive.
23 '''
25 type_name = 'path'
26 help = 'The path to a file or directory'
28 def __init__(self, value: str) -> None:
29 self.raw = value
31 def __str__(self) -> str:
32 return self.raw
34 def __repr__(self) -> str:
35 return '%s(%r)' % (type(self).__name__, self.raw)
37 def __bool__(self) -> bool:
38 return bool(self.raw)
41class Regex(str):
43 type_name = 'regular expression'
44 help = '''
45 A regular expression in python syntax.
46 You can specify flags by starting the regular expression with `(?aiLmsux)`.
47 https://docs.python.org/3/library/re.html#regular-expression-syntax
48 '''
51class SubprocessCommand:
53 type_name = 'command'
54 help = '''\
55 A command to be executed as a subprocess.
56 The command is executed without a shell so redirection or wildcard expansion is not possible.
57 Setting environment variables and piping like in a POSIX shell, however, are implemented in python and should work platform independently.
58 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.
59 '''
61 python_callbacks: 'MutableMapping[str, Callable[[SubprocessCommand, TYPE_CONTEXT], None]]' = {}
63 @classmethod
64 def register_python_callback(cls, name: str, func: 'Callable[[SubprocessCommand, TYPE_CONTEXT], None]') -> None:
65 cls.python_callbacks[name] = func
67 @classmethod
68 def unregister_python_callback(cls, name: str) -> None:
69 del cls.python_callbacks[name]
71 @classmethod
72 def has_python_callback(cls, name: str) -> bool:
73 return name in cls.python_callbacks
76 def __init__(self, arg: 'SubprocessCommand|Sequence[str]|str', *, env: 'Mapping[str, str]|None' = None) -> None:
77 self.cmd: 'Sequence[str]'
78 self.env: 'Mapping[str, str]|None'
79 if isinstance(arg, str):
80 assert env is None
81 self.parse_str(arg)
82 elif isinstance(arg, SubprocessCommand):
83 self.cmd = list(arg.cmd)
84 self.env = dict(arg.env) if arg.env else None
85 if env:
86 if self.env:
87 self.env.update(env)
88 else:
89 self.env = env
90 else:
91 self.cmd = list(arg)
92 self.env = env
94 def parse_str(self, arg: str) -> None:
95 '''
96 Parses a string as returned by :meth:`__str__` and initializes this objcet accordingly
98 :param arg: The string to be parsed
99 :raises ValueError: if arg is invalid
101 Example:
102 If the input is ``arg = 'ENVVAR1=val ENVVAR2= cmd --arg1 --arg2'``
103 this function sets
104 .. code-block::
106 self.env = {'ENVVAR1' : 'val', 'ENVVAR2' : ''}
107 self.cmd = ['cmd', '--arg1', '--arg2']
108 '''
109 if not arg:
110 raise ValueError('cmd is empty')
112 cmd = shlex.split(arg)
114 self.env = {}
115 for i in range(len(cmd)):
116 if '=' in cmd[i]:
117 var, val = cmd[i].split('=', 1)
118 self.env[var] = val
119 else:
120 self.cmd = cmd[i:]
121 if not self.env:
122 self.env = None
123 return
125 raise ValueError('cmd consists of environment variables only, there is no command to be executed')
127 # ------- compare -------
129 def __eq__(self, other: typing.Any) -> bool:
130 if isinstance(other, SubprocessCommand):
131 return self.cmd == other.cmd and self.env == other.env
132 return NotImplemented
134 # ------- custom methods -------
136 def replace(self, wildcard: str, replacement: str) -> 'SubprocessCommand':
137 return SubprocessCommand([replacement if word == wildcard else word for word in self.cmd], env=self.env)
139 def run(self, *, context: 'TYPE_CONTEXT|None') -> 'CompletedProcess[bytes]|None':
140 '''
141 Runs this command and returns when the command is finished.
143 :param context: returns a context manager which can be used to stop and start an urwid screen.
144 It takes the command to be executed so that it can log the command
145 and it returns the command to be executed so that it can modify the command,
146 e.g. processing and intercepting some environment variables.
148 :return: The completed process
149 :raises OSError: e.g. if the program was not found
150 :raises CalledProcessError: if the called program failed
151 '''
152 if self.cmd[0] in self.python_callbacks:
153 self.python_callbacks[self.cmd[0]](self, context)
154 return None
156 if context is None:
157 return run_and_pipe(self.cmd, env=self._add_os_environ(self.env))
159 with context(self) as command:
160 return run_and_pipe(command.cmd, env=self._add_os_environ(command.env))
162 @staticmethod
163 def _add_os_environ(env: 'Mapping[str, str]|None') -> 'Mapping[str, str]|None':
164 if env is None:
165 return env
166 return dict(os.environ, **env)
168 def is_installed(self) -> bool:
169 return self.cmd[0] in self.python_callbacks or bool(shutil.which(self.cmd[0]))
171 # ------- to str -------
173 def __str__(self) -> str:
174 if self.env:
175 env = ' '.join('%s=%s' % (var, shlex.quote(val)) for var, val in self.env.items())
176 env += ' '
177 else:
178 env = ''
179 return env + ' '.join(shlex.quote(word) for word in self.cmd)
181 def __repr__(self) -> str:
182 return '%s(%r, env=%r)' % (type(self).__name__, self.cmd, self.env)
184class SubprocessCommandWithAlternatives:
186 type_name = 'command with alternatives'
187 help = '''
188 One or more commands separated by ||.
189 The first command where the program is installed is executed. The other commands are ignored.
191 The command is executed without a shell so redirection or wildcard expansion is not possible.
192 Setting environment variables and piping like in a POSIX shell, however, are implemented in python and should work platform independently.
193 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.
194 '''
196 SEP = '||'
198 def get_preferred_command(self) -> SubprocessCommand:
199 for cmd in self.commands:
200 if cmd.is_installed():
201 return cmd
203 raise FileNotFoundError('none of the commands is installed: %s' % self)
206 def __init__(self, commands: 'Sequence[SubprocessCommand|Sequence[str]|str]|str') -> None:
207 if isinstance(commands, str):
208 self.commands = [SubprocessCommand(cmd) for cmd in commands.split(self.SEP)]
209 else:
210 self.commands = [SubprocessCommand(cmd) for cmd in commands]
213 def __str__(self) -> str:
214 return self.SEP.join(str(cmd) for cmd in self.commands)
216 def __repr__(self) -> str:
217 return '%s(%s)' % (type(self).__name__, self.commands)
220 def replace(self, wildcard: str, replacement: str) -> SubprocessCommand:
221 return self.get_preferred_command().replace(wildcard, replacement)
223 def run(self, context: 'TYPE_CONTEXT|None' = None) -> 'CompletedProcess[bytes]|None':
224 return self.get_preferred_command().run(context=context)