cx-wealth 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cx_wealth-0.1.0/.gitignore +180 -0
- cx_wealth-0.1.0/PKG-INFO +17 -0
- cx_wealth-0.1.0/README.md +8 -0
- cx_wealth-0.1.0/cx_wealth/__init__.py +6 -0
- cx_wealth-0.1.0/cx_wealth/common.py +6 -0
- cx_wealth-0.1.0/cx_wealth/dynamic_columns.py +44 -0
- cx_wealth-0.1.0/cx_wealth/indexed_list_panel.py +118 -0
- cx_wealth-0.1.0/cx_wealth/rich_types.py +78 -0
- cx_wealth-0.1.0/cx_wealth/wealth_detail.py +138 -0
- cx_wealth-0.1.0/cx_wealth/wealth_help/__init__.py +1 -0
- cx_wealth-0.1.0/cx_wealth/wealth_help/_action.py +149 -0
- cx_wealth-0.1.0/cx_wealth/wealth_help/_group.py +78 -0
- cx_wealth-0.1.0/cx_wealth/wealth_help/_node.py +43 -0
- cx_wealth-0.1.0/cx_wealth/wealth_help/w_help.py +150 -0
- cx_wealth-0.1.0/cx_wealth/wealth_label.py +63 -0
- cx_wealth-0.1.0/pyproject.toml +19 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
.python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
poetry.lock
|
|
109
|
+
|
|
110
|
+
# pdm
|
|
111
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
112
|
+
#pdm.lock
|
|
113
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
114
|
+
# in version control.
|
|
115
|
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
116
|
+
.pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
121
|
+
__pypackages__/
|
|
122
|
+
|
|
123
|
+
# Celery stuff
|
|
124
|
+
celerybeat-schedule
|
|
125
|
+
celerybeat.pid
|
|
126
|
+
|
|
127
|
+
# SageMath parsed files
|
|
128
|
+
*.sage.py
|
|
129
|
+
|
|
130
|
+
# Environments
|
|
131
|
+
.env
|
|
132
|
+
.venv
|
|
133
|
+
env/
|
|
134
|
+
venv/
|
|
135
|
+
ENV/
|
|
136
|
+
env.bak/
|
|
137
|
+
venv.bak/
|
|
138
|
+
|
|
139
|
+
# Spyder project settings
|
|
140
|
+
.spyderproject
|
|
141
|
+
.spyproject
|
|
142
|
+
|
|
143
|
+
# Rope project settings
|
|
144
|
+
.ropeproject
|
|
145
|
+
|
|
146
|
+
# mkdocs documentation
|
|
147
|
+
/site
|
|
148
|
+
|
|
149
|
+
# mypy
|
|
150
|
+
.mypy_cache/
|
|
151
|
+
.dmypy.json
|
|
152
|
+
dmypy.json
|
|
153
|
+
|
|
154
|
+
# Pyre type checker
|
|
155
|
+
.pyre/
|
|
156
|
+
|
|
157
|
+
# pytype static type analyzer
|
|
158
|
+
.pytype/
|
|
159
|
+
|
|
160
|
+
# Cython debug symbols
|
|
161
|
+
cython_debug/
|
|
162
|
+
|
|
163
|
+
# PyCharm
|
|
164
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
165
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
166
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
167
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
168
|
+
#.idea/
|
|
169
|
+
|
|
170
|
+
# Ruff stuff:
|
|
171
|
+
.ruff_cache/
|
|
172
|
+
|
|
173
|
+
# PyPI configuration file
|
|
174
|
+
.pypirc
|
|
175
|
+
|
|
176
|
+
.vscode/
|
|
177
|
+
.idea/
|
|
178
|
+
.vs/
|
|
179
|
+
|
|
180
|
+
/temp/
|
cx_wealth-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cx-wealth
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lib of CLI gadgets extended from rich.
|
|
5
|
+
Author-email: xiii_1991 <xiii_1991@163.com>
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Requires-Dist: rich>=14.0.0
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# cx_wealth
|
|
11
|
+
|
|
12
|
+
## 介绍
|
|
13
|
+
cx_wealth 是一个用于构建结构化命令行帮助信息和丰富终端显示的工具库。
|
|
14
|
+
它基于 [Rich](https://rich.readthedocs.io) 库实现,提供优雅的样式化输出、模块化参数管理以及可扩展的详细信息面板,
|
|
15
|
+
适用于需要复杂命令行界面的工具和应用程序。
|
|
16
|
+
|
|
17
|
+
## 常用组件
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from collections.abc import Sequence, Iterable, Generator
|
|
2
|
+
|
|
3
|
+
from . import rich_types as r
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DynamicColumns:
|
|
7
|
+
"""动态列渲染器,根据终端宽度自动调整列数和列宽。
|
|
8
|
+
|
|
9
|
+
将一组渲染对象按列布局显示,最多显示指定的最大列数。当对象数量超过最大列数时,
|
|
10
|
+
每列宽度会根据终端宽度平均分配;若对象数量不足,则按单列自适应显示。支持扩展填充终端可用空间。
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
renderables (Sequence | Iterable): 要渲染的对象集合。
|
|
14
|
+
max_columns (int, optional): 允许的最大列数,默认为2。
|
|
15
|
+
expand (bool, optional): 是否扩展填充终端宽度,默认为True。
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self, renderables: Sequence | Iterable, max_columns: int = 2, expand=True
|
|
20
|
+
):
|
|
21
|
+
self._renderables = list(renderables)
|
|
22
|
+
self._max_columns = max_columns
|
|
23
|
+
self._expand = expand
|
|
24
|
+
|
|
25
|
+
def __rich_console__(
|
|
26
|
+
self, console: r.Console, _options: r.ConsoleOptions
|
|
27
|
+
) -> "Generator[r.RenderableType, None, None]":
|
|
28
|
+
"""Rich库渲染钩子,生成列式布局的渲染对象。
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
console (rich.Console): 当前控制台实例。
|
|
32
|
+
_options (rich.ConsoleOptions): 渲染选项(未使用)。
|
|
33
|
+
|
|
34
|
+
Yields:
|
|
35
|
+
rich.Columns: 根据终端宽度计算后的列式布局对象。
|
|
36
|
+
"""
|
|
37
|
+
w = (
|
|
38
|
+
None
|
|
39
|
+
if len(self._renderables) < self._max_columns
|
|
40
|
+
else int(console.width / self._max_columns) - 1
|
|
41
|
+
)
|
|
42
|
+
yield r.Columns(
|
|
43
|
+
self._renderables, expand=self._expand, equal=True, width=w
|
|
44
|
+
)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from collections.abc import Sequence, Iterable
|
|
2
|
+
|
|
3
|
+
from . import rich_types as r
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class IndexedListPanel:
|
|
7
|
+
"""索引列表面板,用于在终端中以带索引的表格形式显示列表内容。
|
|
8
|
+
|
|
9
|
+
功能特点:
|
|
10
|
+
- 索引编号右对齐并绿色高亮显示
|
|
11
|
+
- 支持设置起始索引(默认1)
|
|
12
|
+
- 提供最大显示行数控制,超过时显示省略提示
|
|
13
|
+
- 自动计算索引宽度适配最大值位数
|
|
14
|
+
- 自带面板封装,显示项数统计信息
|
|
15
|
+
- 支持边框样式定制(使用Rich样式语法)
|
|
16
|
+
|
|
17
|
+
该组件基于Rich库的Table和Panel组件实现,适用于命令行工具的列表展示场景。
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
items: Sequence | Iterable,
|
|
23
|
+
title: str | None = None,
|
|
24
|
+
start_index: int = 1,
|
|
25
|
+
max_lines: int = 20,
|
|
26
|
+
border_style: r.StyleType | None = None,
|
|
27
|
+
):
|
|
28
|
+
"""初始化索引列表面板
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
items (Sequence | Iterable): 待展示的列表数据源
|
|
32
|
+
title (str, optional): 面板标题. Defaults to None.
|
|
33
|
+
start_index (int, optional): 索引起始值,默认从1开始. Defaults to 1.
|
|
34
|
+
max_lines (int, optional): 最大显示行数,超过时截断并提示剩余项数. Defaults to 20.
|
|
35
|
+
border_style (rich.StyleType, optional): 边框样式,支持Rich样式名称或对象. Defaults to "none".
|
|
36
|
+
"""
|
|
37
|
+
self._items = list(items)
|
|
38
|
+
self._title = title
|
|
39
|
+
self._start_index = start_index
|
|
40
|
+
self._max_lines = max_lines
|
|
41
|
+
self._border_style = border_style or "none"
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def default_width_calculator(console: r.Console) -> int:
|
|
45
|
+
"""计算默认表格宽度(占终端宽度的80%)
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
console (rich.Console): 当前控制台实例
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
int: 计算后的宽度值
|
|
52
|
+
"""
|
|
53
|
+
return int(console.width * 0.8)
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def __check_item(item) -> r.RenderableType:
|
|
57
|
+
"""验证并适配项的渲染格式
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
item (Any): 待检查的项内容
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
rich.RenderableType: 可渲染对象(不可渲染时转为字符串)
|
|
64
|
+
"""
|
|
65
|
+
if r.protocol.is_renderable(item):
|
|
66
|
+
return item
|
|
67
|
+
return str(item)
|
|
68
|
+
|
|
69
|
+
def get_table(self) -> r.Table:
|
|
70
|
+
"""生成带索引的表格对象
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
rich.Table: 包含索引列和内容列的表格对象
|
|
74
|
+
"""
|
|
75
|
+
table = r.Table(show_header=False, box=None)
|
|
76
|
+
table.add_column("index", justify="right", style="green", ratio=1)
|
|
77
|
+
table.add_column(
|
|
78
|
+
"content",
|
|
79
|
+
justify="left",
|
|
80
|
+
highlight=True,
|
|
81
|
+
overflow="fold",
|
|
82
|
+
ratio=200,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
total = len(self._items)
|
|
86
|
+
total_digits = len(str(total))
|
|
87
|
+
|
|
88
|
+
for i, item in enumerate(self._items, start=self._start_index):
|
|
89
|
+
if self._max_lines and i > self._max_lines:
|
|
90
|
+
table.add_row(
|
|
91
|
+
f"[red][{'.'*total_digits}][/]",
|
|
92
|
+
f"[italic red]skipped {total - i -1} items...[/]",
|
|
93
|
+
)
|
|
94
|
+
table.add_row(f"[{total}]", self.__check_item(self._items[-1]))
|
|
95
|
+
break
|
|
96
|
+
table.add_row(
|
|
97
|
+
f"[{i:>{total_digits}}]",
|
|
98
|
+
self.__check_item(item),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return table
|
|
102
|
+
|
|
103
|
+
def __rich__(self) -> r.Panel:
|
|
104
|
+
"""Rich库渲染钩子,返回封装好的面板对象
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
rich.Panel: 包含表格或空提示的面板
|
|
108
|
+
"""
|
|
109
|
+
content = self.get_table() if self._items else r.Text("(empty)", style="dim")
|
|
110
|
+
total = len(self._items)
|
|
111
|
+
return r.Panel(
|
|
112
|
+
content,
|
|
113
|
+
title=self._title,
|
|
114
|
+
subtitle=f"{total} items",
|
|
115
|
+
title_align="left",
|
|
116
|
+
subtitle_align="right",
|
|
117
|
+
border_style=self._border_style,
|
|
118
|
+
)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import rich.text
|
|
2
|
+
|
|
3
|
+
Text = rich.text.Text
|
|
4
|
+
|
|
5
|
+
import rich.console
|
|
6
|
+
|
|
7
|
+
Console = rich.console.Console
|
|
8
|
+
ConsoleOptions = rich.console.ConsoleOptions
|
|
9
|
+
Group = rich.console.Group
|
|
10
|
+
group = rich.console.group
|
|
11
|
+
RenderableType = rich.console.RenderableType
|
|
12
|
+
NewLine = rich.console.NewLine
|
|
13
|
+
|
|
14
|
+
import rich.segment
|
|
15
|
+
|
|
16
|
+
Segment = rich.segment.Segment
|
|
17
|
+
|
|
18
|
+
import rich.panel
|
|
19
|
+
|
|
20
|
+
Panel = rich.panel.Panel
|
|
21
|
+
|
|
22
|
+
import rich.theme
|
|
23
|
+
|
|
24
|
+
Theme = rich.theme.Theme
|
|
25
|
+
|
|
26
|
+
import rich.columns
|
|
27
|
+
|
|
28
|
+
Columns = rich.columns.Columns
|
|
29
|
+
|
|
30
|
+
import rich.markup
|
|
31
|
+
|
|
32
|
+
markup = rich.markup
|
|
33
|
+
|
|
34
|
+
import rich.markdown
|
|
35
|
+
|
|
36
|
+
Markdown = rich.markdown.Markdown
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
import rich.pretty
|
|
40
|
+
|
|
41
|
+
Pretty = rich.pretty.Pretty
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
import rich.table
|
|
45
|
+
|
|
46
|
+
Table = rich.table.Table
|
|
47
|
+
Column = rich.table.Column
|
|
48
|
+
|
|
49
|
+
import rich.style
|
|
50
|
+
|
|
51
|
+
Style = rich.style.Style
|
|
52
|
+
StyleType = rich.style.StyleType
|
|
53
|
+
|
|
54
|
+
import rich.padding
|
|
55
|
+
|
|
56
|
+
Padding = rich.padding.Padding
|
|
57
|
+
|
|
58
|
+
import rich.protocol
|
|
59
|
+
|
|
60
|
+
protocol = rich.protocol
|
|
61
|
+
|
|
62
|
+
import rich.align
|
|
63
|
+
|
|
64
|
+
Align = rich.align.Align
|
|
65
|
+
|
|
66
|
+
import rich.measure
|
|
67
|
+
|
|
68
|
+
Measurement = rich.measure.Measurement
|
|
69
|
+
|
|
70
|
+
import rich.progress
|
|
71
|
+
|
|
72
|
+
Progress = rich.progress.Progress
|
|
73
|
+
TaskID = rich.progress.TaskID
|
|
74
|
+
SpinnerColumn = rich.progress.SpinnerColumn
|
|
75
|
+
TextColumn = rich.progress.TextColumn
|
|
76
|
+
BarColumn = rich.progress.BarColumn
|
|
77
|
+
TaskProgressColumn = rich.progress.TaskProgressColumn
|
|
78
|
+
TimeRemainingColumn = rich.progress.TimeRemainingColumn
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from collections.abc import Generator
|
|
2
|
+
from typing import Iterable, Mapping, Protocol, Sequence, runtime_checkable
|
|
3
|
+
|
|
4
|
+
from rich.console import (
|
|
5
|
+
RenderableType,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
from . import rich_types as r
|
|
9
|
+
from .common import RichPrettyMixin
|
|
10
|
+
from .indexed_list_panel import IndexedListPanel
|
|
11
|
+
from .wealth_label import WealthLabel, WealthLabelMixin
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@runtime_checkable
|
|
15
|
+
class WealthDetailMixin(Protocol):
|
|
16
|
+
def __rich_detail__(self) -> Generator: ...
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WealthDetail:
|
|
20
|
+
def __init__(self, item: WealthDetailMixin):
|
|
21
|
+
self._item = item
|
|
22
|
+
|
|
23
|
+
def __rich_repr__(self):
|
|
24
|
+
yield from self._item.__rich_detail__()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class WealthDetailTable:
|
|
28
|
+
_SUB_BOX_BORDER_STYLE = "grey70"
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
item: WealthDetailMixin | RichPrettyMixin | Mapping | dict,
|
|
33
|
+
sub_box: bool = True,
|
|
34
|
+
) -> None:
|
|
35
|
+
self._item = item
|
|
36
|
+
self._sub_box = sub_box
|
|
37
|
+
|
|
38
|
+
def make_table(self, item):
|
|
39
|
+
table = r.Table(
|
|
40
|
+
show_header=False,
|
|
41
|
+
box=None,
|
|
42
|
+
)
|
|
43
|
+
table.add_column("key", justify="left", style="italic yellow", no_wrap=True)
|
|
44
|
+
table.add_column("value", justify="left", overflow="fold", highlight=True)
|
|
45
|
+
|
|
46
|
+
iterator = None
|
|
47
|
+
if isinstance(item, WealthDetailMixin):
|
|
48
|
+
iterator = item.__rich_detail__()
|
|
49
|
+
elif isinstance(item, RichPrettyMixin):
|
|
50
|
+
iterator = item.__rich_repr__()
|
|
51
|
+
elif isinstance(item, Mapping | dict):
|
|
52
|
+
iterator = item.items()
|
|
53
|
+
elif isinstance(item, Iterable):
|
|
54
|
+
iterator = [None, list(item)]
|
|
55
|
+
|
|
56
|
+
if iterator is None:
|
|
57
|
+
return table
|
|
58
|
+
|
|
59
|
+
for tup in iterator:
|
|
60
|
+
if not isinstance(tup, tuple):
|
|
61
|
+
continue
|
|
62
|
+
key, value = None, None
|
|
63
|
+
match len(tup):
|
|
64
|
+
case 0:
|
|
65
|
+
continue
|
|
66
|
+
case 1:
|
|
67
|
+
value = tup[0]
|
|
68
|
+
case 2:
|
|
69
|
+
key, value = tup
|
|
70
|
+
case _:
|
|
71
|
+
key, *values = tup
|
|
72
|
+
value = list(values)
|
|
73
|
+
table.add_row(key, self.__check_value(value))
|
|
74
|
+
|
|
75
|
+
if table.row_count == 0:
|
|
76
|
+
return r.Text("(empty)", style="dim yellow")
|
|
77
|
+
return table
|
|
78
|
+
|
|
79
|
+
def __check_value(
|
|
80
|
+
self, value, disable_sub_box: bool = False
|
|
81
|
+
) -> RenderableType | None:
|
|
82
|
+
if value is None:
|
|
83
|
+
return None
|
|
84
|
+
if isinstance(value, Mapping | dict | WealthDetailMixin | RichPrettyMixin):
|
|
85
|
+
if self._sub_box and not disable_sub_box:
|
|
86
|
+
return WealthDetailPanel(
|
|
87
|
+
value,
|
|
88
|
+
border_style=self._SUB_BOX_BORDER_STYLE,
|
|
89
|
+
sub_box=False,
|
|
90
|
+
title=value.__class__.__name__,
|
|
91
|
+
)
|
|
92
|
+
return self.make_table(value)
|
|
93
|
+
if isinstance(value, WealthLabelMixin):
|
|
94
|
+
return WealthLabel(value)
|
|
95
|
+
if isinstance(value, RenderableType):
|
|
96
|
+
return value
|
|
97
|
+
if isinstance(value, Sequence | Iterable):
|
|
98
|
+
list_panel = IndexedListPanel(
|
|
99
|
+
list(self.__check_value(v, disable_sub_box=True) for v in value),
|
|
100
|
+
title=value.__class__.__name__,
|
|
101
|
+
border_style=self._SUB_BOX_BORDER_STYLE,
|
|
102
|
+
start_index=0,
|
|
103
|
+
)
|
|
104
|
+
if self._sub_box and not disable_sub_box:
|
|
105
|
+
return list_panel
|
|
106
|
+
return list_panel.get_table()
|
|
107
|
+
return r.Pretty(value)
|
|
108
|
+
|
|
109
|
+
def __rich__(self):
|
|
110
|
+
return self.make_table(self._item)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class WealthDetailPanel:
|
|
114
|
+
def __init__(
|
|
115
|
+
self,
|
|
116
|
+
item: WealthDetailMixin | RichPrettyMixin | Mapping | dict,
|
|
117
|
+
title: str | None = None,
|
|
118
|
+
border_style: r.StyleType | None = None,
|
|
119
|
+
sub_box: bool = True,
|
|
120
|
+
):
|
|
121
|
+
self._item = item
|
|
122
|
+
self._title = title
|
|
123
|
+
self._border_style = border_style or "none"
|
|
124
|
+
self._sub_box = sub_box
|
|
125
|
+
|
|
126
|
+
def __rich__(self):
|
|
127
|
+
content = WealthDetailTable(self._item, sub_box=self._sub_box)
|
|
128
|
+
|
|
129
|
+
panel = r.Panel(
|
|
130
|
+
content,
|
|
131
|
+
title=self._title,
|
|
132
|
+
subtitle=self._item.__class__.__name__,
|
|
133
|
+
title_align="left",
|
|
134
|
+
subtitle_align="right",
|
|
135
|
+
expand=True,
|
|
136
|
+
border_style=self._border_style,
|
|
137
|
+
)
|
|
138
|
+
return panel
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .w_help import WealthHelp
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Literal, override
|
|
3
|
+
|
|
4
|
+
from ._node import _Node
|
|
5
|
+
from .. import rich_types as r
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_ActionNargs = Literal["?", "+", "*", "**"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _Action(_Node):
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
*flags,
|
|
16
|
+
name: str | None = None,
|
|
17
|
+
description: str | None = None,
|
|
18
|
+
metavar: str | None = None,
|
|
19
|
+
nargs: int | _ActionNargs | None = None,
|
|
20
|
+
optional: bool | None = None,
|
|
21
|
+
parent: _Node | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
super().__init__(name=name, description=description, parent=parent)
|
|
24
|
+
self.flags = [str(x) for x in flags]
|
|
25
|
+
self.metavar = metavar
|
|
26
|
+
self.nargs = nargs
|
|
27
|
+
self.optional = optional
|
|
28
|
+
|
|
29
|
+
def _argument(self):
|
|
30
|
+
return (
|
|
31
|
+
self.metavar
|
|
32
|
+
or (self.flags[0] if self.flags and self.is_positional() else None)
|
|
33
|
+
or self.name
|
|
34
|
+
or ""
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def _format_argument(self, pattern: str | None = None) -> r.Text:
|
|
38
|
+
a = self._argument() if pattern is None else pattern.format(self._argument())
|
|
39
|
+
return r.Text(a, style="cx.help.usage.argument")
|
|
40
|
+
|
|
41
|
+
def is_positional(self) -> bool:
|
|
42
|
+
return not self.flags or all(not re.match(r"^[-+]+\w+", x) for x in self.flags)
|
|
43
|
+
|
|
44
|
+
def is_optional(self) -> bool:
|
|
45
|
+
if self.optional is not None:
|
|
46
|
+
return self.optional
|
|
47
|
+
|
|
48
|
+
return not self.is_positional() or self.nargs == "?"
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def _format_option(option: str) -> r.Text:
|
|
52
|
+
return r.Text(option, style="cx.help.usage.option")
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def _make_optional(*text: r.Text | str | None) -> r.Text:
|
|
56
|
+
left = ("[", "cx.help.usage.bracket")
|
|
57
|
+
right = ("]", "cx.help.usage.bracket")
|
|
58
|
+
ts = [
|
|
59
|
+
x if isinstance(x, r.Text) else r.Text.from_markup(x)
|
|
60
|
+
for x in text
|
|
61
|
+
if x is not None
|
|
62
|
+
]
|
|
63
|
+
return r.Text.assemble(left, *ts, right)
|
|
64
|
+
|
|
65
|
+
def render_options(self, sep: str = "|") -> r.Text | None:
|
|
66
|
+
if not self.flags:
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
elements = [self._format_option(x) for x in self.flags]
|
|
70
|
+
separator = r.Text(sep, style="cx.help.usage.bracket")
|
|
71
|
+
return separator.join(elements)
|
|
72
|
+
|
|
73
|
+
def render_argument(self) -> r.Text | None:
|
|
74
|
+
if not self._argument():
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
if isinstance(self.nargs, int):
|
|
78
|
+
args = [
|
|
79
|
+
self._format_argument(pattern="{}" + str(i + 1))
|
|
80
|
+
for i in range(self.nargs)
|
|
81
|
+
]
|
|
82
|
+
sep = r.Text(", ", style="cx.help.usage.bracket")
|
|
83
|
+
return sep.join(args)
|
|
84
|
+
|
|
85
|
+
sep = r.Text(", ", style="cx.help.usage.bracket")
|
|
86
|
+
|
|
87
|
+
if self.nargs == "+":
|
|
88
|
+
args = [
|
|
89
|
+
self._format_argument(pattern="{}1"),
|
|
90
|
+
self._make_optional(sep, self._format_argument(pattern="{}2")),
|
|
91
|
+
self._make_optional(sep, self._format_argument(pattern="{}3")),
|
|
92
|
+
self._make_optional(sep, self._format_argument(pattern="{}...")),
|
|
93
|
+
]
|
|
94
|
+
return r.Text.assemble(*args)
|
|
95
|
+
|
|
96
|
+
if self.nargs == "*":
|
|
97
|
+
args = [
|
|
98
|
+
self._make_optional(self._format_argument(pattern="{}1")),
|
|
99
|
+
self._make_optional(sep, self._format_argument(pattern="{}2")),
|
|
100
|
+
self._make_optional(sep, self._format_argument(pattern="{}3")),
|
|
101
|
+
self._make_optional(sep, self._format_argument(pattern="{}...")),
|
|
102
|
+
]
|
|
103
|
+
return r.Text.assemble(*args)
|
|
104
|
+
|
|
105
|
+
return self._format_argument()
|
|
106
|
+
|
|
107
|
+
@override
|
|
108
|
+
def render_usage(self) -> r.Text:
|
|
109
|
+
res = r.Text()
|
|
110
|
+
if self.is_positional():
|
|
111
|
+
res = self.render_argument() or res
|
|
112
|
+
else:
|
|
113
|
+
ps = [self.render_options(), self.render_argument()]
|
|
114
|
+
res = r.Text(" ").join([x for x in ps if x is not None])
|
|
115
|
+
|
|
116
|
+
if self.nargs == "**":
|
|
117
|
+
res = r.Text.assemble(
|
|
118
|
+
self._make_optional(res),
|
|
119
|
+
self._make_optional(
|
|
120
|
+
self.render_options(),
|
|
121
|
+
r.Text(" ...", style="cx.help.usage.argument"),
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if self.is_optional():
|
|
126
|
+
res = self._make_optional(res)
|
|
127
|
+
|
|
128
|
+
return res
|
|
129
|
+
|
|
130
|
+
def render_detail_title(self):
|
|
131
|
+
res = r.Text()
|
|
132
|
+
if self.is_positional():
|
|
133
|
+
res = self.render_argument() or res
|
|
134
|
+
else:
|
|
135
|
+
ps = [self.render_options(","), self.render_argument()]
|
|
136
|
+
res = r.Text(" ").join([x for x in ps if x is not None])
|
|
137
|
+
return res
|
|
138
|
+
|
|
139
|
+
@override
|
|
140
|
+
@r.group(True)
|
|
141
|
+
def render_details(self):
|
|
142
|
+
yield self.render_detail_title()
|
|
143
|
+
if self.description:
|
|
144
|
+
yield r.Padding(
|
|
145
|
+
r.Text.from_markup(
|
|
146
|
+
self.description, style="cx.help.details.description"
|
|
147
|
+
),
|
|
148
|
+
pad=(0, 0, 0, 4),
|
|
149
|
+
)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Generator
|
|
4
|
+
from typing import Literal
|
|
5
|
+
from typing import override
|
|
6
|
+
|
|
7
|
+
from cx_wealth.rich_types import Text
|
|
8
|
+
from ._action import _Action, _ActionNargs
|
|
9
|
+
from ._node import _Node
|
|
10
|
+
from .. import rich_types as r
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _Group(_Node):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
name: str | None = None,
|
|
17
|
+
description: str | None = None,
|
|
18
|
+
parent: _Node | None = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
super().__init__(name, description, parent)
|
|
21
|
+
|
|
22
|
+
def add_action(
|
|
23
|
+
self,
|
|
24
|
+
*flags,
|
|
25
|
+
name: str | None = None,
|
|
26
|
+
description: str | None = None,
|
|
27
|
+
metavar: str | None = None,
|
|
28
|
+
nargs: int | _ActionNargs | None = None,
|
|
29
|
+
optional: bool | None = None,
|
|
30
|
+
) -> _Action:
|
|
31
|
+
action = _Action(
|
|
32
|
+
*flags,
|
|
33
|
+
name=name,
|
|
34
|
+
description=description,
|
|
35
|
+
metavar=metavar,
|
|
36
|
+
nargs=nargs,
|
|
37
|
+
optional=optional,
|
|
38
|
+
parent=self,
|
|
39
|
+
)
|
|
40
|
+
return action
|
|
41
|
+
|
|
42
|
+
def add_group(
|
|
43
|
+
self,
|
|
44
|
+
name: str | None = None,
|
|
45
|
+
description: str | None = None,
|
|
46
|
+
) -> _Group:
|
|
47
|
+
group = _Group(name, description, self)
|
|
48
|
+
return group
|
|
49
|
+
|
|
50
|
+
def iter_actions(self) -> Generator[_Action, None, None]:
|
|
51
|
+
for action in self.children:
|
|
52
|
+
if isinstance(action, _Action):
|
|
53
|
+
yield action
|
|
54
|
+
elif isinstance(action, _Group):
|
|
55
|
+
yield from action.iter_actions()
|
|
56
|
+
|
|
57
|
+
@override
|
|
58
|
+
def render_usage(self) -> Text:
|
|
59
|
+
usages = [x.render_usage() for x in self.iter_actions()]
|
|
60
|
+
return r.Text(" ").join(usages)
|
|
61
|
+
|
|
62
|
+
@override
|
|
63
|
+
@r.group(True)
|
|
64
|
+
def render_details(self):
|
|
65
|
+
if self.name:
|
|
66
|
+
yield r.Padding(
|
|
67
|
+
r.Text(self.name, style="cx.help.group.title"), pad=(1, 0, 0, 0)
|
|
68
|
+
)
|
|
69
|
+
if self.description:
|
|
70
|
+
yield r.Padding(
|
|
71
|
+
r.Text(
|
|
72
|
+
self.description, style="cx.help.group.description", overflow="fold"
|
|
73
|
+
),
|
|
74
|
+
pad=(0, 0, 0, 2),
|
|
75
|
+
)
|
|
76
|
+
for child in self.children:
|
|
77
|
+
p = r.Padding(child.render_details(), (0, 0, 1, child.level))
|
|
78
|
+
yield p
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Self
|
|
4
|
+
|
|
5
|
+
from .. import rich_types as r
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _Node:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
name: str | None = None,
|
|
12
|
+
description: str | None = None,
|
|
13
|
+
parent: _Node | None = None,
|
|
14
|
+
) -> None:
|
|
15
|
+
self.name = name
|
|
16
|
+
self.description = description
|
|
17
|
+
self.children: list[_Node] = []
|
|
18
|
+
self.parent: _Node | None = None
|
|
19
|
+
self._set_parent(parent)
|
|
20
|
+
|
|
21
|
+
def _set_parent(self, parent: _Node | None):
|
|
22
|
+
if self.parent is not None:
|
|
23
|
+
self.parent.children.remove(self)
|
|
24
|
+
self.parent = parent
|
|
25
|
+
if parent is not None and self not in parent.children:
|
|
26
|
+
parent.children.append(self)
|
|
27
|
+
|
|
28
|
+
def add_node(self, node: _Node) -> Self:
|
|
29
|
+
node._set_parent(self)
|
|
30
|
+
return self
|
|
31
|
+
|
|
32
|
+
def __iter__(self):
|
|
33
|
+
yield from self.children
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def level(self) -> int:
|
|
37
|
+
return self.parent.level + 1 if self.parent is not None else 0
|
|
38
|
+
|
|
39
|
+
def render_usage(self) -> r.Text:
|
|
40
|
+
return r.Text(" ").join(x.render_usage() for x in self.children)
|
|
41
|
+
|
|
42
|
+
def render_details(self) -> r.RenderableType:
|
|
43
|
+
return r.Group(*(x.render_details() for x in self.children))
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Literal
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from ._action import _Action, _ActionNargs
|
|
6
|
+
from ._group import _Group
|
|
7
|
+
from .. import rich_types as r
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WealthHelp:
|
|
11
|
+
DEFAULT_STYLES = {
|
|
12
|
+
"cx.help.usage.title": "green",
|
|
13
|
+
"cx.help.usage.prog": "orange1",
|
|
14
|
+
"cx.help.usage.bracket": "bright_black",
|
|
15
|
+
"cx.help.usage.option": "cyan",
|
|
16
|
+
"cx.help.usage.argument": "italic yellow",
|
|
17
|
+
"cx.help.group.title": "orange1",
|
|
18
|
+
"cx.help.group.description": "italic dim default",
|
|
19
|
+
"cx.help.details.box": "blue",
|
|
20
|
+
"cx.help.details.description": "italic default",
|
|
21
|
+
"cx.help.epilog": "dim italic default",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
prog: str | None = None,
|
|
27
|
+
description: str | r.RenderableType | None = None,
|
|
28
|
+
epilog: str | r.RenderableType | None = None,
|
|
29
|
+
styles: dict | None = None,
|
|
30
|
+
):
|
|
31
|
+
self.prog = prog or sys.argv[0]
|
|
32
|
+
self.description = description
|
|
33
|
+
self._root = _Group()
|
|
34
|
+
self.styles = self.DEFAULT_STYLES
|
|
35
|
+
self.epilog = epilog
|
|
36
|
+
if styles is not None:
|
|
37
|
+
self.styles.update(styles)
|
|
38
|
+
self.theme = r.Theme(self.styles)
|
|
39
|
+
|
|
40
|
+
def add_action(
|
|
41
|
+
self,
|
|
42
|
+
*flags,
|
|
43
|
+
name: str | None = None,
|
|
44
|
+
description: str | None = None,
|
|
45
|
+
metavar: str | None = None,
|
|
46
|
+
nargs: int | _ActionNargs | None = None,
|
|
47
|
+
optional: bool | None = None,
|
|
48
|
+
) -> _Action:
|
|
49
|
+
return self._root.add_action(
|
|
50
|
+
*flags,
|
|
51
|
+
name=name,
|
|
52
|
+
description=description,
|
|
53
|
+
metavar=metavar,
|
|
54
|
+
nargs=nargs,
|
|
55
|
+
optional=optional,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def add_group(
|
|
59
|
+
self,
|
|
60
|
+
name: str | None = None,
|
|
61
|
+
description: str | None = None,
|
|
62
|
+
) -> _Group:
|
|
63
|
+
return self._root.add_group(name=name, description=description)
|
|
64
|
+
|
|
65
|
+
def render_description(self) -> r.RenderableType | None:
|
|
66
|
+
if isinstance(self.description, str):
|
|
67
|
+
desc = r.Text.from_markup(
|
|
68
|
+
self.description, style="cx.help.group.description"
|
|
69
|
+
)
|
|
70
|
+
# desc.no_wrap = True
|
|
71
|
+
desc.overflow = "fold"
|
|
72
|
+
elif isinstance(self.description, r.RenderableType):
|
|
73
|
+
desc = self.description
|
|
74
|
+
else:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
r.Padding(
|
|
79
|
+
desc,
|
|
80
|
+
(1, 1, 0, 1),
|
|
81
|
+
)
|
|
82
|
+
if self.description
|
|
83
|
+
else None
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def render_epilog(self) -> r.RenderableType | None:
|
|
87
|
+
if isinstance(self.epilog, str):
|
|
88
|
+
return r.Text.from_markup(
|
|
89
|
+
self.epilog, style="cx.help.epilog", justify="right"
|
|
90
|
+
)
|
|
91
|
+
if isinstance(self.epilog, r.RenderableType):
|
|
92
|
+
return self.epilog
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
def render_usage(self) -> r.RenderableType:
|
|
96
|
+
def separate(x: _Action):
|
|
97
|
+
a = "o" if x.is_optional() else ""
|
|
98
|
+
b = "+p" if x.is_positional() else "-p"
|
|
99
|
+
return a + b
|
|
100
|
+
|
|
101
|
+
grouped_actions = {
|
|
102
|
+
k: list(v)
|
|
103
|
+
for k, v in itertools.groupby(self._root.iter_actions(), key=separate)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
usages = [
|
|
107
|
+
x.render_usage()
|
|
108
|
+
for x in itertools.chain(
|
|
109
|
+
*(grouped_actions.get(x, []) for x in ["o-p", "-p", "o+p", "+p"])
|
|
110
|
+
)
|
|
111
|
+
]
|
|
112
|
+
usage = r.Text(" ").join(usages)
|
|
113
|
+
|
|
114
|
+
program = r.Text(self.prog, style="cx.help.usage.prog")
|
|
115
|
+
table = r.Table(box=None, show_header=False, expand=True)
|
|
116
|
+
table.add_column("prog", no_wrap=True, overflow="ignore")
|
|
117
|
+
table.add_column("usage", overflow="fold")
|
|
118
|
+
table.add_row(program, usage)
|
|
119
|
+
|
|
120
|
+
desc = self.render_description()
|
|
121
|
+
|
|
122
|
+
return r.Panel(
|
|
123
|
+
r.Group(table, desc) if desc else table,
|
|
124
|
+
title="用法",
|
|
125
|
+
expand=True,
|
|
126
|
+
title_align="left",
|
|
127
|
+
style="cx.help.usage.title",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def render_details(self) -> r.RenderableType:
|
|
131
|
+
details = [x.render_details() for x in self._root.children]
|
|
132
|
+
return r.Panel(
|
|
133
|
+
r.Group(*details),
|
|
134
|
+
title="参数详情",
|
|
135
|
+
expand=True,
|
|
136
|
+
title_align="left",
|
|
137
|
+
style="cx.help.details.box",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def render(self) -> Iterable[r.RenderableType]:
|
|
141
|
+
yield self.render_usage()
|
|
142
|
+
yield self.render_details()
|
|
143
|
+
r_epilog = self.render_epilog()
|
|
144
|
+
if r_epilog:
|
|
145
|
+
yield r_epilog
|
|
146
|
+
|
|
147
|
+
def __rich_console__(self, console: r.Console, options: r.ConsoleOptions):
|
|
148
|
+
with console.use_theme(self.theme):
|
|
149
|
+
o = options.update(highlight=False)
|
|
150
|
+
yield from console.render(r.Group(*self.render(), fit=True), o)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from collections.abc import Generator
|
|
2
|
+
from typing import Literal
|
|
3
|
+
from typing import Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
from cx_studio.utils import FunctionalUtils
|
|
6
|
+
from . import rich_types as r
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@runtime_checkable
|
|
10
|
+
class WealthLabelMixin(Protocol):
|
|
11
|
+
def __rich_label__(self) -> Generator: ...
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WealthLabel:
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
obj: WealthLabelMixin,
|
|
18
|
+
markup=True,
|
|
19
|
+
sep: str = " ",
|
|
20
|
+
tab_size: int = 1,
|
|
21
|
+
overflow: Literal["ignore", "crop", "ellipsis", "fold"] = "crop",
|
|
22
|
+
justify: Literal["left", "center", "right"] = "left",
|
|
23
|
+
):
|
|
24
|
+
self._obj = obj
|
|
25
|
+
self._markup = markup
|
|
26
|
+
self._tab_size = tab_size
|
|
27
|
+
self._sep = sep
|
|
28
|
+
self._overflow: Literal["ignore", "crop", "ellipsis", "fold"] = overflow
|
|
29
|
+
self._justify: Literal["left", "center", "right"] = justify
|
|
30
|
+
|
|
31
|
+
def __unpack_item(self, item):
|
|
32
|
+
if isinstance(item, WealthLabelMixin):
|
|
33
|
+
for x in item.__rich_label__():
|
|
34
|
+
yield from self.__unpack_item(x)
|
|
35
|
+
elif isinstance(item, WealthLabel):
|
|
36
|
+
yield from self.__unpack_item(item._obj)
|
|
37
|
+
elif isinstance(item, str):
|
|
38
|
+
yield r.Text.from_markup(item) if self._markup else r.markup.escape(item)
|
|
39
|
+
elif isinstance(item, r.Text):
|
|
40
|
+
yield item
|
|
41
|
+
elif isinstance(item, r.Segment):
|
|
42
|
+
yield item.text
|
|
43
|
+
if item.style:
|
|
44
|
+
yield item.style
|
|
45
|
+
else:
|
|
46
|
+
yield str(item)
|
|
47
|
+
|
|
48
|
+
def __rich__(self):
|
|
49
|
+
if not isinstance(self._obj, WealthLabelMixin):
|
|
50
|
+
cls_name = self._obj.__class__.__name__
|
|
51
|
+
return r.Pretty(f"[{cls_name}] (instance)")
|
|
52
|
+
|
|
53
|
+
elements = self.__unpack_item(self._obj)
|
|
54
|
+
elements_with_sep = list(
|
|
55
|
+
FunctionalUtils.iter_with_separator(elements, self._sep)
|
|
56
|
+
)
|
|
57
|
+
text = r.Text.assemble(
|
|
58
|
+
*elements_with_sep, # type:ignore
|
|
59
|
+
tab_size=self._tab_size,
|
|
60
|
+
overflow=self._overflow,
|
|
61
|
+
justify=self._justify,
|
|
62
|
+
)
|
|
63
|
+
return text
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cx-wealth"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A lib of CLI gadgets extended from rich."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "xiii_1991", email = "xiii_1991@163.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"rich>=14.0.0",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
cx-wealth = "cx_wealth:main"
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["hatchling"]
|
|
19
|
+
build-backend = "hatchling.build"
|