omdev 0.0.0.dev440__py3-none-any.whl → 0.0.0.dev495__py3-none-any.whl
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.
Potentially problematic release.
This version of omdev might be problematic. Click here for more details.
- omdev/.omlish-manifests.json +18 -30
- omdev/README.md +51 -0
- omdev/__about__.py +11 -7
- omdev/amalg/gen/gen.py +49 -6
- omdev/amalg/gen/imports.py +1 -1
- omdev/amalg/gen/manifests.py +1 -1
- omdev/amalg/gen/resources.py +1 -1
- omdev/amalg/gen/srcfiles.py +13 -3
- omdev/amalg/gen/strip.py +1 -1
- omdev/amalg/gen/types.py +1 -1
- omdev/amalg/gen/typing.py +1 -1
- omdev/amalg/info.py +32 -0
- omdev/cache/data/actions.py +1 -1
- omdev/cache/data/specs.py +1 -1
- omdev/cexts/_boilerplate.cc +2 -3
- omdev/cexts/cmake.py +4 -1
- omdev/ci/cli.py +2 -3
- omdev/cli/clicli.py +37 -7
- omdev/cmdlog/cli.py +1 -2
- omdev/dataclasses/_dumping.py +1960 -0
- omdev/dataclasses/_template.py +22 -0
- omdev/dataclasses/cli.py +7 -2
- omdev/dataclasses/codegen.py +340 -60
- omdev/dataclasses/dumping.py +200 -0
- omdev/interp/cli.py +1 -1
- omdev/interp/types.py +3 -2
- omdev/interp/uv/provider.py +37 -0
- omdev/interp/venvs.py +1 -0
- omdev/irc/messages/base.py +50 -0
- omdev/irc/messages/formats.py +92 -0
- omdev/irc/messages/messages.py +775 -0
- omdev/irc/messages/parsing.py +99 -0
- omdev/irc/numerics/__init__.py +0 -0
- omdev/irc/numerics/formats.py +97 -0
- omdev/irc/numerics/numerics.py +865 -0
- omdev/irc/numerics/types.py +59 -0
- omdev/irc/protocol/LICENSE +11 -0
- omdev/irc/protocol/__init__.py +61 -0
- omdev/irc/protocol/consts.py +6 -0
- omdev/irc/protocol/errors.py +30 -0
- omdev/irc/protocol/message.py +21 -0
- omdev/irc/protocol/nuh.py +55 -0
- omdev/irc/protocol/parsing.py +158 -0
- omdev/irc/protocol/rendering.py +153 -0
- omdev/irc/protocol/tags.py +102 -0
- omdev/irc/protocol/utils.py +30 -0
- omdev/manifests/_dumping.py +125 -25
- omdev/manifests/main.py +1 -1
- omdev/markdown/__init__.py +0 -0
- omdev/markdown/incparse.py +116 -0
- omdev/markdown/tokens.py +51 -0
- omdev/packaging/marshal.py +8 -8
- omdev/packaging/requires.py +6 -6
- omdev/packaging/revisions.py +1 -1
- omdev/packaging/specifiers.py +2 -1
- omdev/packaging/versions.py +4 -4
- omdev/packaging/wheelfile.py +2 -0
- omdev/precheck/blanklines.py +66 -0
- omdev/precheck/caches.py +1 -1
- omdev/precheck/imports.py +14 -1
- omdev/precheck/main.py +4 -3
- omdev/precheck/unicode.py +39 -15
- omdev/py/asts/__init__.py +0 -0
- omdev/py/asts/parents.py +28 -0
- omdev/py/asts/toplevel.py +123 -0
- omdev/py/asts/visitors.py +18 -0
- omdev/py/attrdocs.py +1 -1
- omdev/py/bracepy.py +12 -4
- omdev/py/reprs.py +32 -0
- omdev/py/srcheaders.py +1 -1
- omdev/py/tokens/__init__.py +0 -0
- omdev/py/tools/mkrelimp.py +1 -1
- omdev/py/tools/pipdepup.py +686 -0
- omdev/pyproject/cli.py +1 -1
- omdev/pyproject/pkg.py +190 -45
- omdev/pyproject/reqs.py +31 -9
- omdev/pyproject/tools/__init__.py +0 -0
- omdev/pyproject/tools/aboutdeps.py +60 -0
- omdev/pyproject/venvs.py +8 -1
- omdev/rs/__init__.py +0 -0
- omdev/scripts/ci.py +752 -98
- omdev/scripts/interp.py +232 -39
- omdev/scripts/lib/inject.py +74 -27
- omdev/scripts/lib/logs.py +187 -43
- omdev/scripts/lib/marshal.py +67 -25
- omdev/scripts/pyproject.py +1369 -143
- omdev/tools/git/cli.py +10 -0
- omdev/tools/json/formats.py +2 -0
- omdev/tools/json/processing.py +5 -2
- omdev/tools/jsonview/cli.py +49 -65
- omdev/tools/jsonview/resources/jsonview.html.j2 +43 -0
- omdev/tools/pawk/README.md +195 -0
- omdev/tools/pawk/pawk.py +2 -2
- omdev/tools/pip.py +8 -0
- omdev/tui/__init__.py +0 -0
- omdev/tui/apps/__init__.py +0 -0
- omdev/tui/apps/edit/__init__.py +0 -0
- omdev/tui/apps/edit/main.py +167 -0
- omdev/tui/apps/irc/__init__.py +0 -0
- omdev/tui/apps/irc/__main__.py +4 -0
- omdev/tui/apps/irc/app.py +286 -0
- omdev/tui/apps/irc/client.py +187 -0
- omdev/tui/apps/irc/commands.py +175 -0
- omdev/tui/apps/irc/main.py +26 -0
- omdev/tui/apps/markdown/__init__.py +0 -0
- omdev/tui/apps/markdown/__main__.py +11 -0
- omdev/{ptk → tui/apps}/markdown/cli.py +5 -7
- omdev/tui/rich/__init__.py +46 -0
- omdev/tui/rich/console2.py +20 -0
- omdev/tui/rich/markdown2.py +186 -0
- omdev/tui/textual/__init__.py +265 -0
- omdev/tui/textual/app2.py +16 -0
- omdev/tui/textual/autocomplete/LICENSE +21 -0
- omdev/tui/textual/autocomplete/__init__.py +33 -0
- omdev/tui/textual/autocomplete/matching.py +226 -0
- omdev/tui/textual/autocomplete/paths.py +202 -0
- omdev/tui/textual/autocomplete/widget.py +612 -0
- omdev/tui/textual/debug/__init__.py +10 -0
- omdev/tui/textual/debug/dominfo.py +151 -0
- omdev/tui/textual/debug/screen.py +24 -0
- omdev/tui/textual/devtools.py +187 -0
- omdev/tui/textual/drivers2.py +55 -0
- omdev/tui/textual/logging2.py +20 -0
- omdev/tui/textual/types.py +45 -0
- {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev495.dist-info}/METADATA +15 -9
- {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev495.dist-info}/RECORD +135 -80
- omdev/ptk/__init__.py +0 -103
- omdev/ptk/apps/ncdu.py +0 -167
- omdev/ptk/confirm.py +0 -60
- omdev/ptk/markdown/LICENSE +0 -22
- omdev/ptk/markdown/__init__.py +0 -10
- omdev/ptk/markdown/__main__.py +0 -11
- omdev/ptk/markdown/border.py +0 -94
- omdev/ptk/markdown/markdown.py +0 -390
- omdev/ptk/markdown/parser.py +0 -42
- omdev/ptk/markdown/styles.py +0 -29
- omdev/ptk/markdown/tags.py +0 -299
- omdev/ptk/markdown/utils.py +0 -366
- omdev/pyproject/cexts.py +0 -110
- /omdev/{ptk/apps → irc}/__init__.py +0 -0
- /omdev/{tokens → irc/messages}/__init__.py +0 -0
- /omdev/{tokens → py/tokens}/all.py +0 -0
- /omdev/{tokens → py/tokens}/tokenizert.py +0 -0
- /omdev/{tokens → py/tokens}/utils.py +0 -0
- {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev495.dist-info}/WHEEL +0 -0
- {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev495.dist-info}/entry_points.txt +0 -0
- {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev495.dist-info}/licenses/LICENSE +0 -0
- {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev495.dist-info}/top_level.txt +0 -0
omdev/tools/git/cli.py
CHANGED
|
@@ -21,6 +21,7 @@ TODO:
|
|
|
21
21
|
fatal: Need to specify how to reconcile divergent branches.
|
|
22
22
|
"""
|
|
23
23
|
import dataclasses as dc
|
|
24
|
+
import glob
|
|
24
25
|
import os
|
|
25
26
|
import shutil
|
|
26
27
|
import tempfile
|
|
@@ -452,6 +453,7 @@ class Cli(ap.Cli):
|
|
|
452
453
|
BUILTIN_COMMIT_MESSAGES: ta.Mapping[str, str] = {
|
|
453
454
|
'tableflip': '(╯°□°)╯︵ ┻━┻',
|
|
454
455
|
'tableunflip': '┬─┬ノ(º _ ºノ)',
|
|
456
|
+
'shrug': r'¯\_(ツ)_/¯',
|
|
455
457
|
}
|
|
456
458
|
|
|
457
459
|
@ap.cmd(
|
|
@@ -501,10 +503,18 @@ class Cli(ap.Cli):
|
|
|
501
503
|
repo_dir = os.path.join(tmp_dir, repo_dir_name)
|
|
502
504
|
check.state(os.path.isdir(repo_dir))
|
|
503
505
|
|
|
506
|
+
#
|
|
507
|
+
|
|
504
508
|
git_dir = os.path.join(repo_dir, '.git')
|
|
505
509
|
check.state(os.path.isdir(git_dir))
|
|
506
510
|
shutil.rmtree(git_dir)
|
|
507
511
|
|
|
512
|
+
for f in glob.glob(os.path.join(repo_dir, '**/.gitattributes'), recursive=True):
|
|
513
|
+
if os.path.isfile(f):
|
|
514
|
+
os.unlink(f)
|
|
515
|
+
|
|
516
|
+
#
|
|
517
|
+
|
|
508
518
|
shutil.move(repo_dir, cwd)
|
|
509
519
|
|
|
510
520
|
out_dir = repo_dir_name
|
omdev/tools/json/formats.py
CHANGED
omdev/tools/json/processing.py
CHANGED
|
@@ -53,8 +53,11 @@ class Processor:
|
|
|
53
53
|
|
|
54
54
|
def _marshal(self, v: ta.Any) -> ta.Any:
|
|
55
55
|
return msh.MarshalContext(
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
configs=msh.global_config_registry(),
|
|
57
|
+
marshal_factory_context=msh.MarshalFactoryContext(
|
|
58
|
+
configs=msh.global_config_registry(),
|
|
59
|
+
marshaler_factory=self._marshaler_factory(),
|
|
60
|
+
),
|
|
58
61
|
).marshal(v)
|
|
59
62
|
|
|
60
63
|
def process(self, v: ta.Any) -> ta.Iterator[ta.Any]:
|
omdev/tools/jsonview/cli.py
CHANGED
|
@@ -3,7 +3,7 @@ TODO:
|
|
|
3
3
|
- read from stdin
|
|
4
4
|
- evaluate jmespath on server using extended engine
|
|
5
5
|
- integrate with json tool
|
|
6
|
-
- use omlish server
|
|
6
|
+
- use omlish server
|
|
7
7
|
- vendor deps, serve local
|
|
8
8
|
- update to https://github.com/josdejong/svelte-jsoneditor
|
|
9
9
|
"""
|
|
@@ -14,62 +14,33 @@ import os
|
|
|
14
14
|
import socketserver
|
|
15
15
|
import sys
|
|
16
16
|
import threading
|
|
17
|
+
import typing as ta
|
|
17
18
|
import webbrowser
|
|
18
19
|
|
|
20
|
+
from omlish import check
|
|
19
21
|
from omlish import lang
|
|
22
|
+
from omlish.sockets.ports import get_available_port
|
|
23
|
+
from omlish.text import minja
|
|
20
24
|
|
|
21
25
|
|
|
22
26
|
##
|
|
23
27
|
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<meta charset="UTF-8">
|
|
31
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
32
|
-
<title>JSON Viewer</title>
|
|
33
|
-
<link
|
|
34
|
-
href="https://cdn.jsdelivr.net/npm/jsoneditor/dist/jsoneditor.min.css"
|
|
35
|
-
rel="stylesheet"
|
|
36
|
-
type="text/css"
|
|
37
|
-
>
|
|
38
|
-
<style>
|
|
39
|
-
{css_src}
|
|
40
|
-
</style>
|
|
41
|
-
</head>
|
|
42
|
-
|
|
43
|
-
<body>
|
|
44
|
-
<div class="input-area">
|
|
45
|
-
<label for="jmespath-input">JMESPath:</label>
|
|
46
|
-
<input
|
|
47
|
-
type="text"
|
|
48
|
-
id="jmespath-input"
|
|
49
|
-
list="jmespath-history"
|
|
50
|
-
placeholder="Enter JMESPath expression..."
|
|
51
|
-
autocomplete="off"
|
|
52
|
-
>
|
|
53
|
-
<datalist id="jmespath-history"></datalist>
|
|
54
|
-
<span id="error-message"></span>
|
|
55
|
-
</div>
|
|
56
|
-
<div id="jsoneditor"></div>
|
|
57
|
-
|
|
58
|
-
<script src="https://cdn.jsdelivr.net/npm/jsoneditor@10.2.0/dist/jsoneditor.min.js"></script>
|
|
59
|
-
<script src="https://cdn.jsdelivr.net/npm/jmespath@0.16.0/jmespath.min.js"></script>
|
|
60
|
-
<script>
|
|
61
|
-
const originalJsonData = {json_data};
|
|
62
|
-
</script>
|
|
63
|
-
<script>
|
|
64
|
-
{js_src}
|
|
65
|
-
</script>
|
|
66
|
-
</body>
|
|
67
|
-
|
|
68
|
-
</html>
|
|
69
|
-
"""
|
|
29
|
+
@lang.cached_function
|
|
30
|
+
def html_template() -> minja.MinjaTemplate:
|
|
31
|
+
src = lang.get_relative_resources('resources', globals=globals())['jsonview.html.j2'].read_text()
|
|
32
|
+
return minja.compile_minja_template(src, ['ctx'])
|
|
33
|
+
|
|
70
34
|
|
|
35
|
+
def view_json(
|
|
36
|
+
filepath: str,
|
|
37
|
+
port: int | None,
|
|
38
|
+
*,
|
|
39
|
+
mode: ta.Literal['jsonl', 'json5', 'json', None] = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
if filepath == '-':
|
|
42
|
+
filepath = '/dev/stdin'
|
|
71
43
|
|
|
72
|
-
def view_json(filepath: str, port: int) -> None:
|
|
73
44
|
if not os.path.exists(filepath):
|
|
74
45
|
print(f"Error: File not found at '{filepath}'", file=sys.stderr)
|
|
75
46
|
return
|
|
@@ -81,13 +52,21 @@ def view_json(filepath: str, port: int) -> None:
|
|
|
81
52
|
print(f'Error: Invalid JSON file. {e}', file=sys.stderr)
|
|
82
53
|
return
|
|
83
54
|
|
|
84
|
-
if
|
|
55
|
+
if mode is None:
|
|
56
|
+
if filepath.endswith('.jsonl'):
|
|
57
|
+
mode = 'json'
|
|
58
|
+
elif filepath.endswith('.json5'):
|
|
59
|
+
mode = 'json5'
|
|
60
|
+
|
|
61
|
+
if mode == 'jsonl':
|
|
85
62
|
json_content = [json.loads(sl) for l in raw_content.splitlines() if (sl := l.strip())]
|
|
86
|
-
elif
|
|
63
|
+
elif mode == 'json5':
|
|
87
64
|
from omlish.formats import json5
|
|
88
65
|
json_content = json5.loads(raw_content)
|
|
89
|
-
|
|
66
|
+
elif mode in ('json', None):
|
|
90
67
|
json_content = json.loads(raw_content)
|
|
68
|
+
else:
|
|
69
|
+
raise ValueError(mode)
|
|
91
70
|
|
|
92
71
|
# Use compact dumps for embedding in JS, it's more efficient
|
|
93
72
|
json_string = json.dumps(json_content)
|
|
@@ -102,15 +81,18 @@ def view_json(filepath: str, port: int) -> None:
|
|
|
102
81
|
self.send_response(200)
|
|
103
82
|
self.send_header('Content-type', 'text/html')
|
|
104
83
|
self.end_headers()
|
|
105
|
-
html_content =
|
|
106
|
-
css_src=css_src,
|
|
107
|
-
js_src=js_src,
|
|
84
|
+
html_content = html_template()(ctx=dict(
|
|
85
|
+
css_src=css_src.strip(),
|
|
86
|
+
js_src=js_src.strip(),
|
|
108
87
|
json_data=json_string,
|
|
109
|
-
)
|
|
88
|
+
))
|
|
110
89
|
self.wfile.write(html_content.encode('utf-8'))
|
|
111
90
|
else:
|
|
112
91
|
super().do_GET()
|
|
113
92
|
|
|
93
|
+
if port is None:
|
|
94
|
+
port = get_available_port()
|
|
95
|
+
|
|
114
96
|
handler_cls = JsonViewerHttpRequestHandler
|
|
115
97
|
with socketserver.TCPServer(('127.0.0.1', port), handler_cls) as httpd:
|
|
116
98
|
url = f'http://127.0.0.1:{port}'
|
|
@@ -132,19 +114,21 @@ def _main() -> None:
|
|
|
132
114
|
description='Launch a web-based JSON viewer with JMESPath transformations.',
|
|
133
115
|
formatter_class=argparse.RawTextHelpFormatter,
|
|
134
116
|
)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
)
|
|
139
|
-
parser.add_argument(
|
|
140
|
-
|
|
141
|
-
type=int,
|
|
142
|
-
default=(default_port := 8999),
|
|
143
|
-
help=f'The port to run the web server on. Defaults to {default_port}.',
|
|
144
|
-
)
|
|
117
|
+
|
|
118
|
+
parser.add_argument('filepath')
|
|
119
|
+
parser.add_argument('-p', '--port', type=int)
|
|
120
|
+
parser.add_argument('-l', '--lines', action='store_true')
|
|
121
|
+
parser.add_argument('-5', '--five', action='store_true')
|
|
122
|
+
|
|
145
123
|
args = parser.parse_args()
|
|
146
124
|
|
|
147
|
-
|
|
125
|
+
check.state(not (args.lines and args.five))
|
|
126
|
+
|
|
127
|
+
view_json(
|
|
128
|
+
args.filepath,
|
|
129
|
+
args.port,
|
|
130
|
+
mode='jsonl' if args.lines else 'json5' if args.five else None,
|
|
131
|
+
)
|
|
148
132
|
|
|
149
133
|
|
|
150
134
|
if __name__ == '__main__':
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>JSON Viewer</title>
|
|
8
|
+
<link
|
|
9
|
+
href="https://cdn.jsdelivr.net/npm/jsoneditor/dist/jsoneditor.min.css"
|
|
10
|
+
rel="stylesheet"
|
|
11
|
+
type="text/css"
|
|
12
|
+
>
|
|
13
|
+
<style>
|
|
14
|
+
{{ ctx['css_src'] }}
|
|
15
|
+
</style>
|
|
16
|
+
</head>
|
|
17
|
+
|
|
18
|
+
<body>
|
|
19
|
+
<div class="input-area">
|
|
20
|
+
<label for="jmespath-input">JMESPath:</label>
|
|
21
|
+
<input
|
|
22
|
+
type="text"
|
|
23
|
+
id="jmespath-input"
|
|
24
|
+
list="jmespath-history"
|
|
25
|
+
placeholder="Enter JMESPath expression..."
|
|
26
|
+
autocomplete="off"
|
|
27
|
+
>
|
|
28
|
+
<datalist id="jmespath-history"></datalist>
|
|
29
|
+
<span id="error-message"></span>
|
|
30
|
+
</div>
|
|
31
|
+
<div id="jsoneditor"></div>
|
|
32
|
+
|
|
33
|
+
<script src="https://cdn.jsdelivr.net/npm/jsoneditor@10.2.0/dist/jsoneditor.min.js"></script>
|
|
34
|
+
<script src="https://cdn.jsdelivr.net/npm/jmespath@0.16.0/jmespath.min.js"></script>
|
|
35
|
+
<script>
|
|
36
|
+
const originalJsonData = {{ ctx['json_data'] }};
|
|
37
|
+
</script>
|
|
38
|
+
<script>
|
|
39
|
+
{{ ctx['js_src'] }}
|
|
40
|
+
</script>
|
|
41
|
+
</body>
|
|
42
|
+
|
|
43
|
+
</html>
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
[From alecthomas/pawk](https://github.com/alecthomas/pawk/blob/d60f78399e8a01857ebd73415a00e7eb424043ab/pawk.py)
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
PAWK - A Python line processor (like AWK)
|
|
6
|
+
|
|
7
|
+
PAWK aims to bring the full power of Python to AWK-like line-processing.
|
|
8
|
+
|
|
9
|
+
Here are some quick examples to show some of the advantages of pawk over AWK.
|
|
10
|
+
|
|
11
|
+
The first example transforms `/etc/hosts` into a JSON map of host to IP:
|
|
12
|
+
|
|
13
|
+
cat /etc/hosts | pawk -B 'd={}' -E 'json.dumps(d)' '!/^#/ d[f[1]] = f[0]'
|
|
14
|
+
|
|
15
|
+
Breaking this down:
|
|
16
|
+
|
|
17
|
+
1. `-B 'd={}'` is a begin statement initializing a dictionary, executed once before processing begins.
|
|
18
|
+
2. `-E 'json.dumps(d)'` is an end statement expression, producing the JSON representation of the dictionary `d`.
|
|
19
|
+
3. `!/^#/` tells pawk to match any line *not* beginning with `#`.
|
|
20
|
+
4. `d[f[1]] = f[0]` adds a dictionary entry where the key is the second field in the line (the first hostname) and the
|
|
21
|
+
value is the first field (the IP address).
|
|
22
|
+
|
|
23
|
+
And another example showing how to bzip2-compress + base64-encode a file:
|
|
24
|
+
|
|
25
|
+
cat pawk.py | pawk -E 'base64.encodestring(bz2.compress(t))'
|
|
26
|
+
|
|
27
|
+
### AWK example translations
|
|
28
|
+
|
|
29
|
+
Most basic AWK constructs are available. You can find more idiomatic examples below in the example section, but here are
|
|
30
|
+
a bunch of awk commands and their equivalent pawk commands to get started with:
|
|
31
|
+
|
|
32
|
+
Print lines matching a pattern:
|
|
33
|
+
|
|
34
|
+
ls -l / | awk '/etc/'
|
|
35
|
+
ls -l / | pawk '/etc/'
|
|
36
|
+
|
|
37
|
+
Print lines *not* matching a pattern:
|
|
38
|
+
|
|
39
|
+
ls -l / | awk '!/etc/'
|
|
40
|
+
ls -l / | pawk '!/etc/'
|
|
41
|
+
|
|
42
|
+
Field slicing and dicing (here pawk wins because of Python's array slicing):
|
|
43
|
+
|
|
44
|
+
ls -l / | awk '/etc/ {print $5, $6, $7, $8, $9}'
|
|
45
|
+
ls -l / | pawk '/etc/ f[4:]'
|
|
46
|
+
|
|
47
|
+
Begin and end actions (in this case, summing the sizes of all files):
|
|
48
|
+
|
|
49
|
+
ls -l | awk 'BEGIN {c = 0} {c += $5} END {print c}'
|
|
50
|
+
ls -l | pawk -B 'c = 0' -E 'c' 'c += int(f[4])'
|
|
51
|
+
|
|
52
|
+
Print files where a field matches a numeric expression (in this case where files are > 1024 bytes):
|
|
53
|
+
|
|
54
|
+
ls -l | awk '$5 > 1024'
|
|
55
|
+
ls -l | pawk 'int(f[4]) > 1024'
|
|
56
|
+
|
|
57
|
+
Matching a single field (any filename with "t" in it):
|
|
58
|
+
|
|
59
|
+
ls -l | awk '$NF ~/t/'
|
|
60
|
+
ls -l | pawk '"t" in f[-1]'
|
|
61
|
+
|
|
62
|
+
## Expression evaluation
|
|
63
|
+
|
|
64
|
+
PAWK evaluates a Python expression or statement against each line in stdin. The following variables are available in
|
|
65
|
+
local context:
|
|
66
|
+
|
|
67
|
+
- `line` - Current line text, including newline.
|
|
68
|
+
- `l` - Current line text, excluding newline.
|
|
69
|
+
- `n` - The current 1-based line number.
|
|
70
|
+
- `f` - Fields of the line (split by the field separator `-F`).
|
|
71
|
+
- `nf` - Number of fields in this line.
|
|
72
|
+
- `m` - Tuple of match regular expression capture groups, if any.
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
In the context of the `-E` block:
|
|
76
|
+
|
|
77
|
+
- `t` - The entire input text up to the current cursor position.
|
|
78
|
+
|
|
79
|
+
If the flag `-H, --header` is provided, each field in the first row of the input will be treated as field variable names
|
|
80
|
+
in subsequent rows. The header is not output. For example, given the input:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
count name
|
|
84
|
+
12 bob
|
|
85
|
+
34 fred
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
We could do:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
$ pawk -H '"%s is %s" % (name, count)' < input.txt
|
|
92
|
+
bob is 12
|
|
93
|
+
fred is 34
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
To output a header as well, use `-B`:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
$ pawk -H -B '"name is count"' '"%s is %s" % (name, count)' < input.txt
|
|
100
|
+
name is count
|
|
101
|
+
bob is 12
|
|
102
|
+
fred is 34
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Module references will be automatically imported if possible. Additionally, the `--import <module>[,<module>,...]` flag
|
|
106
|
+
can be used to import symbols from a set of modules into the evaluation context.
|
|
107
|
+
|
|
108
|
+
eg. `--import os.path` will import all symbols from `os.path`, such as `os.path.isfile()`, into the context.
|
|
109
|
+
|
|
110
|
+
## Output
|
|
111
|
+
|
|
112
|
+
### Line actions
|
|
113
|
+
|
|
114
|
+
The type of the evaluated expression determines how output is displayed:
|
|
115
|
+
|
|
116
|
+
- `tuple` or `list`: the elements are converted to strings and joined with the output delimiter (`-O`).
|
|
117
|
+
- `None` or `False`: nothing is output for that line.
|
|
118
|
+
- `True`: the original line is output.
|
|
119
|
+
- Any other value is converted to a string.
|
|
120
|
+
|
|
121
|
+
### Start/end blocks
|
|
122
|
+
|
|
123
|
+
The rules are the same as for line actions with one difference. Because there is no "line" that corresponds to them, an
|
|
124
|
+
expression returning True is ignored.
|
|
125
|
+
|
|
126
|
+
$ echo -ne 'foo\nbar' | pawk -E t
|
|
127
|
+
foo
|
|
128
|
+
bar
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
## Command-line usage
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
Usage: cat input | pawk [<options>] <expr>
|
|
135
|
+
|
|
136
|
+
A Python line-processor (like awk).
|
|
137
|
+
|
|
138
|
+
See https://github.com/alecthomas/pawk for details. Based on
|
|
139
|
+
http://code.activestate.com/recipes/437932/.
|
|
140
|
+
|
|
141
|
+
Options:
|
|
142
|
+
-h, --help show this help message and exit
|
|
143
|
+
-I <filename>, --in_place=<filename>
|
|
144
|
+
modify given input file in-place
|
|
145
|
+
-i <modules>, --import=<modules>
|
|
146
|
+
comma-separated list of modules to "from x import *"
|
|
147
|
+
from
|
|
148
|
+
-F <delim> input delimiter
|
|
149
|
+
-O <delim> output delimiter
|
|
150
|
+
-L <delim> output line separator
|
|
151
|
+
-B <statement>, --begin=<statement>
|
|
152
|
+
begin statement
|
|
153
|
+
-E <statement>, --end=<statement>
|
|
154
|
+
end statement
|
|
155
|
+
-s, --statement DEPRECATED. retained for backward compatibility
|
|
156
|
+
-H, --header use first row as field variable names in subsequent
|
|
157
|
+
rows
|
|
158
|
+
--strict abort on exceptions
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Examples
|
|
162
|
+
|
|
163
|
+
### Line processing
|
|
164
|
+
|
|
165
|
+
Print the name and size of every file from stdin:
|
|
166
|
+
|
|
167
|
+
find . -type f | pawk 'f[0], os.stat(f[0]).st_size'
|
|
168
|
+
|
|
169
|
+
> **Note:** this example also shows how pawk automatically imports referenced modules, in this case `os`.
|
|
170
|
+
|
|
171
|
+
Print the sum size of all files from stdin:
|
|
172
|
+
|
|
173
|
+
find . -type f | \
|
|
174
|
+
pawk \
|
|
175
|
+
--begin 'c=0' \
|
|
176
|
+
--end c \
|
|
177
|
+
'c += os.stat(f[0]).st_size'
|
|
178
|
+
|
|
179
|
+
Short-flag version:
|
|
180
|
+
|
|
181
|
+
find . -type f | pawk -B c=0 -E c 'c += os.stat(f[0]).st_size'
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
### Whole-file processing
|
|
185
|
+
|
|
186
|
+
If you do not provide a line expression, but do provide an end statement, pawk will accumulate each line, and the entire
|
|
187
|
+
file's text will be available in the end statement as `t`. This is useful for operations on entire files, like the
|
|
188
|
+
following example of converting a file from markdown to HTML:
|
|
189
|
+
|
|
190
|
+
cat README.md | \
|
|
191
|
+
pawk --end 'markdown.markdown(t)'
|
|
192
|
+
|
|
193
|
+
Short-flag version:
|
|
194
|
+
|
|
195
|
+
cat README.md | pawk -E 'markdown.markdown(t)'
|
omdev/tools/pawk/pawk.py
CHANGED
|
@@ -384,9 +384,9 @@ def main() -> None:
|
|
|
384
384
|
# Workaround for close failed in file object destructor: sys.excepthook is missing lost sys.stderr
|
|
385
385
|
# http://stackoverflow.com/questions/7955138/addressing-sys-excepthook-error-in-bash-script
|
|
386
386
|
sys.stderr.write(str(e) + '\n')
|
|
387
|
-
|
|
387
|
+
raise SystemExit(1) from None
|
|
388
388
|
except KeyboardInterrupt:
|
|
389
|
-
|
|
389
|
+
raise SystemExit(1) from None
|
|
390
390
|
|
|
391
391
|
|
|
392
392
|
# @omlish-manifest
|
omdev/tools/pip.py
CHANGED
|
@@ -22,6 +22,11 @@ from ..pip import lookup_latest_package_version
|
|
|
22
22
|
##
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
DEV_DEP_NAMES: ta.AbstractSet[str] = frozenset([
|
|
26
|
+
'pydevd-pycharm',
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
|
|
25
30
|
class Cli(ap.Cli):
|
|
26
31
|
@ap.cmd(
|
|
27
32
|
ap.arg('package'),
|
|
@@ -49,6 +54,9 @@ class Cli(ap.Cli):
|
|
|
49
54
|
for l in src.splitlines(keepends=True):
|
|
50
55
|
if l.startswith('-e'):
|
|
51
56
|
continue
|
|
57
|
+
pr = parse_requirement(l)
|
|
58
|
+
if pr.name in DEV_DEP_NAMES:
|
|
59
|
+
continue
|
|
52
60
|
out.append(l)
|
|
53
61
|
|
|
54
62
|
new_src = ''.join(out)
|
omdev/tui/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import pathlib
|
|
3
|
+
import typing as ta
|
|
4
|
+
|
|
5
|
+
from ... import textual as tx
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
##
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class QuitConfirmScreen(tx.ModalScreen[bool]):
|
|
12
|
+
"""Screen with a dialog to confirm quit without saving."""
|
|
13
|
+
|
|
14
|
+
CSS = """
|
|
15
|
+
QuitConfirmScreen {
|
|
16
|
+
align: center middle;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#dialog {
|
|
20
|
+
width: 60;
|
|
21
|
+
height: 11;
|
|
22
|
+
|
|
23
|
+
border: thick $background 80%;
|
|
24
|
+
|
|
25
|
+
padding: 1 2;
|
|
26
|
+
|
|
27
|
+
background: $surface;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#question {
|
|
31
|
+
height: 3;
|
|
32
|
+
|
|
33
|
+
content-align: center middle;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
Button {
|
|
37
|
+
width: 1fr;
|
|
38
|
+
}
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def compose(self) -> tx.ComposeResult:
|
|
42
|
+
yield tx.Vertical(
|
|
43
|
+
tx.Label('You have unsaved changes. Do you want to save before quitting?', id='question'),
|
|
44
|
+
tx.Button('Save and Quit', variant='success', id='save'),
|
|
45
|
+
tx.Button('Quit Without Saving', variant='warning', id='quit'),
|
|
46
|
+
tx.Button('Cancel', variant='primary', id='cancel'),
|
|
47
|
+
id='dialog',
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def on_button_pressed(self, event: tx.Button.Pressed) -> None:
|
|
51
|
+
if event.button.id == 'save':
|
|
52
|
+
self.dismiss(True)
|
|
53
|
+
elif event.button.id == 'quit':
|
|
54
|
+
self.dismiss(False)
|
|
55
|
+
else:
|
|
56
|
+
self.dismiss(None)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TextEditor(tx.App):
|
|
60
|
+
"""A simple text editor using Textual."""
|
|
61
|
+
|
|
62
|
+
CSS = """
|
|
63
|
+
TextArea {
|
|
64
|
+
height: 1fr;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
Container {
|
|
68
|
+
height: 100%;
|
|
69
|
+
}
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
BINDINGS: ta.ClassVar[ta.Sequence[tx.Binding]] = [
|
|
73
|
+
tx.Binding('ctrl+s', 'save', 'Save', show=True),
|
|
74
|
+
tx.Binding('ctrl+q', 'quit', 'Quit', show=True),
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
def __init__(self, filepath: pathlib.Path):
|
|
78
|
+
super().__init__()
|
|
79
|
+
|
|
80
|
+
self.filepath = filepath
|
|
81
|
+
self.text_area: tx.TextArea | None = None
|
|
82
|
+
self.modified = False
|
|
83
|
+
self.original_content = ''
|
|
84
|
+
|
|
85
|
+
def compose(self) -> tx.ComposeResult:
|
|
86
|
+
"""Create child widgets for the app."""
|
|
87
|
+
|
|
88
|
+
yield tx.Header()
|
|
89
|
+
yield tx.Container(tx.TextArea(id='editor'))
|
|
90
|
+
yield tx.Footer()
|
|
91
|
+
|
|
92
|
+
def on_mount(self) -> None:
|
|
93
|
+
"""Load the file content when the app starts."""
|
|
94
|
+
|
|
95
|
+
self.text_area = self.query_one('#editor', tx.TextArea)
|
|
96
|
+
|
|
97
|
+
# Load existing file or create new
|
|
98
|
+
if self.filepath.exists():
|
|
99
|
+
content = self.filepath.read_text()
|
|
100
|
+
self.text_area.load_text(content)
|
|
101
|
+
self.original_content = content
|
|
102
|
+
else:
|
|
103
|
+
self.original_content = ''
|
|
104
|
+
|
|
105
|
+
self.title = f'Text Editor - {self.filepath.name}'
|
|
106
|
+
self.sub_title = str(self.filepath)
|
|
107
|
+
|
|
108
|
+
def on_text_area_changed(self, event: tx.TextArea.Changed) -> None:
|
|
109
|
+
"""Track if the document has been modified."""
|
|
110
|
+
|
|
111
|
+
if self.text_area:
|
|
112
|
+
current_content = self.text_area.text
|
|
113
|
+
self.modified = current_content != self.original_content
|
|
114
|
+
|
|
115
|
+
# Update title to show modified status
|
|
116
|
+
status = ' *' if self.modified else ''
|
|
117
|
+
self.title = f'Text Editor - {self.filepath.name}{status}'
|
|
118
|
+
|
|
119
|
+
def action_save(self) -> None:
|
|
120
|
+
"""Save the current content to file."""
|
|
121
|
+
|
|
122
|
+
if self.text_area:
|
|
123
|
+
content = self.text_area.text
|
|
124
|
+
self.filepath.write_text(content)
|
|
125
|
+
self.original_content = content
|
|
126
|
+
self.modified = False
|
|
127
|
+
self.title = f'Text Editor - {self.filepath.name}'
|
|
128
|
+
self.notify(f'Saved to {self.filepath.name}')
|
|
129
|
+
|
|
130
|
+
async def action_quit(self) -> None:
|
|
131
|
+
"""Quit the editor, prompting to save if modified."""
|
|
132
|
+
|
|
133
|
+
if self.modified:
|
|
134
|
+
def check_quit(should_save: bool | None) -> None:
|
|
135
|
+
if should_save is None:
|
|
136
|
+
# User cancelled
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
if should_save:
|
|
140
|
+
self.action_save()
|
|
141
|
+
|
|
142
|
+
self.exit()
|
|
143
|
+
|
|
144
|
+
await self.push_screen(QuitConfirmScreen(), callback=check_quit)
|
|
145
|
+
|
|
146
|
+
else:
|
|
147
|
+
self.exit()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def main() -> None:
|
|
151
|
+
"""Parse arguments and run the editor."""
|
|
152
|
+
|
|
153
|
+
parser = argparse.ArgumentParser(description='Simple text editor')
|
|
154
|
+
parser.add_argument('filename', help='File to edit')
|
|
155
|
+
args = parser.parse_args()
|
|
156
|
+
|
|
157
|
+
filepath = pathlib.Path(args.filename).resolve()
|
|
158
|
+
|
|
159
|
+
# Create parent directories if needed
|
|
160
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
|
|
162
|
+
app = TextEditor(filepath)
|
|
163
|
+
app.run()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
if __name__ == '__main__':
|
|
167
|
+
main()
|
|
File without changes
|