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
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-22 14:03 +0200
1#!./runmodule.sh
3import os
4import shutil
5import shlex
6import re
7import typing
8from collections.abc import Sequence, Callable, Mapping, MutableMapping
10from .subprocess_pipe import run_and_pipe, CompletedProcess
13TYPE_CONTEXT: 'typing.TypeAlias' = 'Callable[[SubprocessCommand], typing.ContextManager[SubprocessCommand]] | None'
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]
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 '''
27 def __init__(self, pattern: str) -> None:
28 self._compiled_pattern: 're.Pattern[str]' = re.compile(pattern)
30 def __getattr__(self, attr: str) -> object:
31 return getattr(self._compiled_pattern, attr)
33 def __str__(self) -> str:
34 return self._compiled_pattern.pattern
36 def __repr__(self) -> str:
37 return f'{type(self).__name__}({self._compiled_pattern.pattern!r})'
40class SubprocessCommand:
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 '''
50 python_callbacks: 'MutableMapping[str, Callable[[SubprocessCommand, TYPE_CONTEXT], None]]' = {}
52 @classmethod
53 def register_python_callback(cls, name: str, func: 'Callable[[SubprocessCommand, TYPE_CONTEXT], None]') -> None:
54 cls.python_callbacks[name] = func
56 @classmethod
57 def unregister_python_callback(cls, name: str) -> None:
58 del cls.python_callbacks[name]
60 @classmethod
61 def has_python_callback(cls, name: str) -> bool:
62 return name in cls.python_callbacks
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
83 def parse_str(self, arg: str) -> None:
84 '''
85 Parses a string as returned by :meth:`__str__` and initializes this objcet accordingly
87 :param arg: The string to be parsed
88 :raises ValueError: if arg is invalid
90 Example:
91 If the input is ``arg = 'ENVVAR1=val ENVVAR2= cmd --arg1 --arg2'``
92 this function sets
93 .. code-block::
95 self.env = {'ENVVAR1' : 'val', 'ENVVAR2' : ''}
96 self.cmd = ['cmd', '--arg1', '--arg2']
97 '''
98 if not arg:
99 raise ValueError('cmd is empty')
101 cmd = shlex.split(arg)
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
114 raise ValueError('cmd consists of environment variables only, there is no command to be executed')
116 # ------- compare -------
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
123 # ------- custom methods -------
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)
128 def run(self, *, context: 'TYPE_CONTEXT|None') -> 'CompletedProcess[bytes]|None':
129 '''
130 Runs this command and returns when the command is finished.
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.
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
145 if context is None:
146 return run_and_pipe(self.cmd, env=self._add_os_environ(self.env))
148 with context(self) as command:
149 return run_and_pipe(command.cmd, env=self._add_os_environ(command.env))
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)
157 def is_installed(self) -> bool:
158 return self.cmd[0] in self.python_callbacks or bool(shutil.which(self.cmd[0]))
160 # ------- to str -------
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)
170 def __repr__(self) -> str:
171 return '%s(%r, env=%r)' % (type(self).__name__, self.cmd, self.env)
173class SubprocessCommandWithAlternatives:
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.
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 '''
185 SEP = '||'
187 def get_preferred_command(self) -> SubprocessCommand:
188 for cmd in self.commands:
189 if cmd.is_installed():
190 return cmd
192 raise FileNotFoundError('none of the commands is installed: %s' % self)
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]
202 def __str__(self) -> str:
203 return self.SEP.join(str(cmd) for cmd in self.commands)
205 def __repr__(self) -> str:
206 return '%s(%s)' % (type(self).__name__, self.commands)
209 def replace(self, wildcard: str, replacement: str) -> SubprocessCommand:
210 return self.get_preferred_command().replace(wildcard, replacement)
212 def run(self, context: 'TYPE_CONTEXT|None' = None) -> 'CompletedProcess[bytes]|None':
213 return self.get_preferred_command().run(context=context)