因此,我使用i3,一个Linux管理器来管理我的窗口。此外,这是运行在笔记本电脑,经常挂载到几个输出显示器。这是由我的i3配置文件中的下列行处理的
set $firstMonitor DP-2-1
set $secondMonitor DP-1-2
set $laptop eDP-1
workspace 1 output $firstMonitor $laptop
workspace 2 output $firstMonitor $laptop
workspace 3 output $firstMonitor $laptop
workspace 4 output $firstMonitor $laptop
workspace 5 output $secondMonitor $laptop
workspace 6 output $secondMonitor $laptop
workspace 7 output $secondMonitor $laptop
workspace 8 output $secondMonitor $laptop
workspace 9 output $secondMonitor $laptop
workspace 10 output $laptop $laptop为了方便起见,我们可以使用set在配置文件中定义变量,并以$作为前缀。上面的行告诉我的窗口管理器,如果workspace 1存在的话,我希望它能在4 on $firstMonitor上运行。否则会回到$laptop。
我将此用于各种脚本,因此需要从我的配置文件中提取这些信息,并将其存储在某个地方。例如,它可能看起来像这样
[
{
'DP-2-1': ['1', '2', '3', '4'],
'DP-1-2': ['5', '6', '7', '8', '9'],
'eDP-1': ['10']
},
{
'eDP-1': ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'
}
]被解析了。为此,我编写了以下Python代码,以提取分配给每个工作区的输出。注意,i3文件对空格很挑剔,这意味着行set$firstMonitor DP-2-1不会编译。
from pathlib import Path
import collections
CONFIG = Path.home() / ".config" / "i3" / "config"
def read_config(config=CONFIG):
with open(config, "r") as f:
return f.readlines()
def read_workspace_outputs(lines=read_config()):
"""Reads an i3 config, returns which output each workspaces is assigned to
Example:
set $firstMonitor DP-2-1
set $secondMonitor DP-1-2
set $laptop eDP-1
set $browser 1
workspace $browser output $firstMonitor $laptop
workspace 2 output $firstMonitor $laptop
workspace 3 output $firstMonitor $laptop
workspace 4 output $firstMonitor $laptop
workspace 5 output $secondMonitor $laptop
workspace 6 output $secondMonitor $laptop
workspace 7 output $secondMonitor $laptop
workspace 8 output $secondMonitor $laptop
workspace 9 output $secondMonitor $laptop
workspace 10 output $laptop $laptop
Will return
[
{
'DP-2-1': ['1', '2', '3', '4'],
'DP-1-2': ['5', '6', '7', '8', '9'],
'eDP-1': ['10']
},
{
'eDP-1': ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'
}
]
>>> read_workspace_outputs(lines=['workspace 1 output eDP-1'])
[defaultdict(, {'eDP-1': ['1']})]
>>> read_workspace_outputs(lines=['set $laptop eDP-1','workspace 1 output eDP-1'])
[defaultdict(, {'eDP-1': ['1']})]
>>> read_workspace_outputs(lines=['set $browser 1','workspace $browser output eDP-1'])
[defaultdict(, {'eDP-1': ['1']})]
>>> read_workspace_outputs(lines=[
... "set $firstMonitor DP-2-1",
... "set $secondMonitor DP-1-2",
... "set $laptop eDP-1",
... "",
... "workspace 1 output $firstMonitor $laptop",
... "workspace 3 output $secondMonitor $laptop",
... "workspace 5 output $laptop $laptop",
... ])
[defaultdict(, {'DP-2-1': ['1'], 'DP-1-2': ['3'], 'eDP-1': ['5']}), defaultdict(, {'eDP-1': ['1', '3', '5']})]
"""
# Extract workspaces and variables [set $name command] from file
def get_workspaces_and_variables(lines):
workspaces = []
variables_2_commands = dict()
for line in lines:
if line.startswith("workspace"):
workspaces.append(line.strip())
elif line.startswith("set"):
_, variable, *command = line.split()
variables_2_commands[variable] = " ".join(command)
return workspaces, variables_2_commands
# Convert back $name to command for outputs and workspaces
def workspaces_without_variables(workspaces, variables):
workspace_outputs = []
for workspace in workspaces:
workspace_str, output_str = workspace.split("output")
workspace, variable = workspace_str.split()
workspace_number = (
variables[variable] if variable.startswith("$") else variable
)
outputs = [
variables[output] if output.startswith("$") else output
for output in output_str.split()
]
workspace_outputs.append([workspace_number, outputs])
return workspace_outputs
# Currently things are stored as workspaces = [[output1, output 2], ...]
# This flips the order and stores it as a dict with outputs as keys and values workspaces
def workspaces_2_outputs(workspaces):
output_workspaces = [
collections.defaultdict(list)
for _ in range(len(max((x[1] for x in workspaces), key=len)))
]
for (workspace_number, outputs) in workspaces:
for j, output in enumerate(outputs):
output_workspaces[j][output].append(workspace_number)
return output_workspaces
workspaces_w_variables, variables = get_workspaces_and_variables(lines)
variable_free_workspaces = workspaces_without_variables(
workspaces_w_variables, variables
)
return workspaces_2_outputs(variable_free_workspaces)
if __name__ == "__main__":
import doctest
import yaml
from yaml.representer import Representer
doctest.testmod()
OUTPUT_WORKSPACE_CONFIG = (
Path.home() / ".config" / "i3" / "oisov-scripts" / "i3-output-workspace-2.yaml"
)
yaml.add_representer(collections.defaultdict, Representer.represent_dict)
with open(OUTPUT_WORKSPACE_CONFIG, "w") as file:
yaml.dump(read_workspace_outputs(), file)output.yaml文件如下所示
- DP-1-2:
- '5'
- '6'
- '7'
- '8'
- '9'
DP-2-1:
- '1'
- '2'
- '3'
- '4'
eDP-1:
- '10'
- eDP-1:
- '1'
- '2'
- '3'
- '4'
- '5'
- '6'
- '7'
- '8'
- '9'
- '10'将来我会添加一些输入提示,但现在我想知道这是否是提取这些信息的最佳方法。这段代码感觉有点笨重,即使是我想要的。
为了进行测试,可以使用本例中的第一个代码块。例如,较长的i3示例文件可以是https://github.com/sainathadapa/i3-wm-config/blob/master/i3-default-config-backup,请注意,这并不会将工作区设置为特定的输出。
发布于 2021-10-29 18:14:48
至于用Python编写代码,我认为这做得很好,包括良好的风格、注释和测试。我同意你的看法,这有点笨重,我也有一些其他的小评论。
我有一些想法,为什么它会觉得有点笨重。
get_workspaces_and_variables可以返回更多有用的对象。workspaces是未解析的原始行。variables已经很好了,它是一本可以用实际值替换符号的字典。当我试图在我的脑海中建立一个程序的心理模型时,我在这里感觉到了沉重的负担,我不得不记住两种对象,其中一种需要更多的工作。我更喜欢以直观的格式返回工作区的parse_workspaces函数。
workspaces_without_variables采用原始工作区行和变量映射,并以直观的格式返回工作区,解析所有变量。这很好,只是看起来有很多代码。最好将复制的variables[name] if name.startswith("$") else name提取到函数中。另外,我认为解析可以做得更简单一些。在代码注释中,示例原始行也会有所帮助。
最后,workspaces_2_outputs创建了最终想要的映射,我只是觉得这一行有点神秘:
for _ in范围(len(max(在工作区中为x),key=len))
也就是说,它根据长度找到最大条目,然后以长度作为范围的限制。我会发现更自然的是,采取的长度,然后找到最大的。而不是数字索引,我会使用描述性名称。如下所示:
for _ in range(max(len(outputs) for _, outputs in workspaces))]综合上述意见,考虑这一备选实施办法:
def parse_variable(line):
# example line: "set $firstMonitor DP-2-1"
_, name, value = line.split()
return name, value
def parse_workspace(line, variables):
def resolve(name):
return variables[name] if name.startswith('这通过了最初的测试用例,所以我认为行为是保留的,即使解析使用了一些简化的逻辑。注意,要使parse_workspaces像这样工作,解析工作区行所需的所有变量都已经定义好了,这一点很重要。我不知道i3格式,如果这是一个安全的假设或不。如果没有,那么确实需要第二次传球,就像你做的那样。请注意,variables的范围比发布的代码限制得多。我认为这有助于减轻精神负担。来自外部作用域的隐藏名称在发布的代码中,一些名称隐藏了外部作用域的名称,例如:def get_workspaces_and_variables(行):^ def workspaces_without_variables(工作区、变量):^由于这个原因,我曾经在过去经历过一些讨厌的错误,我建议避免这样的阴影。可用性我本来想玩一下这个程序,但是用硬编码的输入和输出文件路径,这还不够容易。最好接受这些参数作为命令行参数,并可能使用当前值作为默认值。) else name
# example line: "workspace 1 output $firstMonitor $laptop"
_, workspace, _, *devices = line.split()
return resolve(workspace), [resolve(device) for device in devices]
def outputs_to_workspaces(workspaces):
mappings = [collections.defaultdict(list)
for _ in range(max(len(outputs) for _, outputs in workspaces))]
for workspace, outputs in workspaces:
for index, output in enumerate(outputs):
mappings[index][output].append(workspace)
return mappings
def parse_workspaces():
variables = {}
workspaces = []
for line in lines:
if line.startswith('set'):
name, value = parse_variable(line)
variables[name] = value
elif line.startswith('workspace'):
workspaces.append(parse_workspace(line, variables))
return workspaces
return outputs_to_workspaces(parse_workspaces())这通过了最初的测试用例,所以我认为行为是保留的,即使解析使用了一些简化的逻辑。
注意,要使D13像这样工作,解析工作区行所需的所有变量都已经定义好了,这一点很重要。我不知道i3格式,如果这是一个安全的假设或不。如果没有,那么确实需要第二次传球,就像你做的那样。
请注意,D14的范围比发布的代码限制得多。我认为这有助于减轻精神负担。
来自外部作用域的K115隐藏名称K216
在发布的代码中,一些名称隐藏了外部作用域的名称,例如:
J117
def get_workspaces_and_variables(行):^ def workspaces_without_variables(工作区、变量):^
J218
由于这个原因,我曾经在过去经历过一些讨厌的错误,我建议避免这样的阴影。
K119可用性K220
我本来想玩一下这个程序,但是用硬编码的输入和输出文件路径,这还不够容易。最好接受这些参数作为命令行参数,并可能使用当前值作为默认值。
发布于 2021-10-29 19:06:28
你有医生测试-很好,继续。
最主要的原因是,您有未键入的数据汤。内部数据表示形式不应该类似于JSON。Python (特别是dataclass)使轻量级模型变得简单,这样您就可以获得一些正确性的保证,特别是当您使用mypy时。
指定参数默认值为lines=read_config()是个坏主意。此默认设置将应用于模块加载。如果默认文件是巨大的模块加载将是缓慢的,如果默认文件更改后,从模块负载经过一段时间后,默认将是过时的。拥有默认文件名是可以的,但我不会有这样的整个默认配置。
可以通过使用正则表达式简化startswith解析。
除了doctest之外,在内部有一个带有嵌套函数集合的函数read_workspace_outputs,使得单元测试无法进行。
我对yaml模块不太了解,而且yaml格式非常琐碎,所以直接生成标记并不困难。我对这一点没有强烈的看法,但下面建议的代码显示了直接的标记生成。
您的输出格式有点奇怪;您对它有多少控制?看起来,您是在暗示最外层的树级别是工作区监视器列表中的索引,但是如果您还显示了数字索引本身,它就不会那么令人困惑了。也就是说,下面的示例代码遵循您现有的输出结构。
import re
from dataclasses import dataclass, field
from pathlib import Path
from string import Template
from typing import Iterator, Iterable, Tuple, Dict, List, Collection
CONFIG = Path.home() / ".config" / "i3" / "config"
@dataclass(frozen=True)
class Monitor:
name: str
workspaces: Dict[int, 'Workspace'] = field(default_factory=dict)
@dataclass(frozen=True)
class Workspace:
id: int
monitors: List[Monitor] = field(default_factory=list)
WorkspaceDefinition = Tuple[
int, # Workspace ID
Tuple[str], # monitors
]
def read_config(config: Path = CONFIG) -> Iterator[WorkspaceDefinition]:
variables: Dict[str, str] = {}
var_pat = re.compile(
r'^set'
r' \$(?P\S+)'
r' (?P.*)'
r'\n
)
work_pat = re.compile(
r'^workspace'
r' (?P\S+)'
r' output'
r' (?P.+)'
r'\n
)
with config.open() as f:
for line in f:
var = var_pat.match(line)
if var:
variables[var['key']] = var['value']
continue
filled = Template(line).substitute(variables)
workspace = work_pat.match(filled)
if workspace:
yield int(workspace['id']), workspace['monitors'].split(' ')
def load_workspaces(work_defs: Iterable[WorkspaceDefinition]) -> Tuple[
Tuple[Workspace],
Tuple[Monitor],
]:
monitors = {}
workspaces = {}
for work_id, monitor_names in work_defs:
workspace = Workspace(id=work_id)
workspaces[work_id] = workspace
for monitor_name in monitor_names:
monitor = monitors.get(monitor_name)
if monitor is None:
monitor = Monitor(name=monitor_name)
monitors[monitor_name] = monitor
monitor.workspaces[work_id] = workspace
workspace.monitors.append(monitor)
return tuple(workspaces.values()), tuple(monitors.values())
@dataclass(frozen=True)
class MonitorPosition:
index: int
workspaces_by_monitor: Dict[str, List[Workspace]]
@classmethod
def all(
cls,
workspaces: Collection[Workspace],
monitors: Collection[Monitor],
) -> Iterator['MonitorPosition']:
monitor_positions = max(
len(workspace.monitors)
for workspace in workspaces
)
for index in range(monitor_positions):
yield cls(
index,
dict(cls.for_index(workspaces, monitors, index)),
)
@classmethod
def for_index(
cls,
workspaces: Collection[Workspace],
monitors: Collection[Monitor],
index: int,
) -> Iterator[Tuple[str, List[Workspace]]]:
for monitor in monitors:
workspaces_used = [
workspace for workspace in workspaces
if len(workspace.monitors) > index
and workspace.monitors[index] is monitor
]
if workspaces_used:
yield monitor.name, workspaces_used
def to_yaml(
positions: Iterable[MonitorPosition],
filename: Path,
) -> None:
with filename.open('w') as yaml:
for position in positions:
group_prefix = '-'
for monitor_name, workspaces in position.workspaces_by_monitor.items():
yaml.write(f'{group_prefix:<2}{monitor_name}:\n')
yaml.write('\n'.join(
f" - '{workspace.id}'"
for workspace in workspaces
))
yaml.write('\n')
group_prefix = ' '
def test() -> None:
workspaces, monitors = load_workspaces(read_config(Path('config')))
positions = MonitorPosition.all(workspaces, monitors)
to_yaml(positions, Path('i3-output-workspace-2.yaml'))
if __name__ == '__main__':
test()https://codereview.stackexchange.com/questions/269510
复制相似问题