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.
@@ -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/
@@ -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,8 @@
1
+ # cx_wealth
2
+
3
+ ## 介绍
4
+ cx_wealth 是一个用于构建结构化命令行帮助信息和丰富终端显示的工具库。
5
+ 它基于 [Rich](https://rich.readthedocs.io) 库实现,提供优雅的样式化输出、模块化参数管理以及可扩展的详细信息面板,
6
+ 适用于需要复杂命令行界面的工具和应用程序。
7
+
8
+ ## 常用组件
@@ -0,0 +1,6 @@
1
+ from .common import *
2
+ from .dynamic_columns import *
3
+ from .indexed_list_panel import *
4
+ from .wealth_detail import *
5
+ from .wealth_help import *
6
+ from .wealth_label import *
@@ -0,0 +1,6 @@
1
+ from typing import Protocol, runtime_checkable
2
+
3
+
4
+ @runtime_checkable
5
+ class RichPrettyMixin(Protocol):
6
+ def __rich_repr__(self): ...
@@ -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"