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.

Files changed (148) hide show
  1. omdev/.omlish-manifests.json +18 -30
  2. omdev/README.md +51 -0
  3. omdev/__about__.py +11 -7
  4. omdev/amalg/gen/gen.py +49 -6
  5. omdev/amalg/gen/imports.py +1 -1
  6. omdev/amalg/gen/manifests.py +1 -1
  7. omdev/amalg/gen/resources.py +1 -1
  8. omdev/amalg/gen/srcfiles.py +13 -3
  9. omdev/amalg/gen/strip.py +1 -1
  10. omdev/amalg/gen/types.py +1 -1
  11. omdev/amalg/gen/typing.py +1 -1
  12. omdev/amalg/info.py +32 -0
  13. omdev/cache/data/actions.py +1 -1
  14. omdev/cache/data/specs.py +1 -1
  15. omdev/cexts/_boilerplate.cc +2 -3
  16. omdev/cexts/cmake.py +4 -1
  17. omdev/ci/cli.py +2 -3
  18. omdev/cli/clicli.py +37 -7
  19. omdev/cmdlog/cli.py +1 -2
  20. omdev/dataclasses/_dumping.py +1960 -0
  21. omdev/dataclasses/_template.py +22 -0
  22. omdev/dataclasses/cli.py +7 -2
  23. omdev/dataclasses/codegen.py +340 -60
  24. omdev/dataclasses/dumping.py +200 -0
  25. omdev/interp/cli.py +1 -1
  26. omdev/interp/types.py +3 -2
  27. omdev/interp/uv/provider.py +37 -0
  28. omdev/interp/venvs.py +1 -0
  29. omdev/irc/messages/base.py +50 -0
  30. omdev/irc/messages/formats.py +92 -0
  31. omdev/irc/messages/messages.py +775 -0
  32. omdev/irc/messages/parsing.py +99 -0
  33. omdev/irc/numerics/__init__.py +0 -0
  34. omdev/irc/numerics/formats.py +97 -0
  35. omdev/irc/numerics/numerics.py +865 -0
  36. omdev/irc/numerics/types.py +59 -0
  37. omdev/irc/protocol/LICENSE +11 -0
  38. omdev/irc/protocol/__init__.py +61 -0
  39. omdev/irc/protocol/consts.py +6 -0
  40. omdev/irc/protocol/errors.py +30 -0
  41. omdev/irc/protocol/message.py +21 -0
  42. omdev/irc/protocol/nuh.py +55 -0
  43. omdev/irc/protocol/parsing.py +158 -0
  44. omdev/irc/protocol/rendering.py +153 -0
  45. omdev/irc/protocol/tags.py +102 -0
  46. omdev/irc/protocol/utils.py +30 -0
  47. omdev/manifests/_dumping.py +125 -25
  48. omdev/manifests/main.py +1 -1
  49. omdev/markdown/__init__.py +0 -0
  50. omdev/markdown/incparse.py +116 -0
  51. omdev/markdown/tokens.py +51 -0
  52. omdev/packaging/marshal.py +8 -8
  53. omdev/packaging/requires.py +6 -6
  54. omdev/packaging/revisions.py +1 -1
  55. omdev/packaging/specifiers.py +2 -1
  56. omdev/packaging/versions.py +4 -4
  57. omdev/packaging/wheelfile.py +2 -0
  58. omdev/precheck/blanklines.py +66 -0
  59. omdev/precheck/caches.py +1 -1
  60. omdev/precheck/imports.py +14 -1
  61. omdev/precheck/main.py +4 -3
  62. omdev/precheck/unicode.py +39 -15
  63. omdev/py/asts/__init__.py +0 -0
  64. omdev/py/asts/parents.py +28 -0
  65. omdev/py/asts/toplevel.py +123 -0
  66. omdev/py/asts/visitors.py +18 -0
  67. omdev/py/attrdocs.py +1 -1
  68. omdev/py/bracepy.py +12 -4
  69. omdev/py/reprs.py +32 -0
  70. omdev/py/srcheaders.py +1 -1
  71. omdev/py/tokens/__init__.py +0 -0
  72. omdev/py/tools/mkrelimp.py +1 -1
  73. omdev/py/tools/pipdepup.py +686 -0
  74. omdev/pyproject/cli.py +1 -1
  75. omdev/pyproject/pkg.py +190 -45
  76. omdev/pyproject/reqs.py +31 -9
  77. omdev/pyproject/tools/__init__.py +0 -0
  78. omdev/pyproject/tools/aboutdeps.py +60 -0
  79. omdev/pyproject/venvs.py +8 -1
  80. omdev/rs/__init__.py +0 -0
  81. omdev/scripts/ci.py +752 -98
  82. omdev/scripts/interp.py +232 -39
  83. omdev/scripts/lib/inject.py +74 -27
  84. omdev/scripts/lib/logs.py +187 -43
  85. omdev/scripts/lib/marshal.py +67 -25
  86. omdev/scripts/pyproject.py +1369 -143
  87. omdev/tools/git/cli.py +10 -0
  88. omdev/tools/json/formats.py +2 -0
  89. omdev/tools/json/processing.py +5 -2
  90. omdev/tools/jsonview/cli.py +49 -65
  91. omdev/tools/jsonview/resources/jsonview.html.j2 +43 -0
  92. omdev/tools/pawk/README.md +195 -0
  93. omdev/tools/pawk/pawk.py +2 -2
  94. omdev/tools/pip.py +8 -0
  95. omdev/tui/__init__.py +0 -0
  96. omdev/tui/apps/__init__.py +0 -0
  97. omdev/tui/apps/edit/__init__.py +0 -0
  98. omdev/tui/apps/edit/main.py +167 -0
  99. omdev/tui/apps/irc/__init__.py +0 -0
  100. omdev/tui/apps/irc/__main__.py +4 -0
  101. omdev/tui/apps/irc/app.py +286 -0
  102. omdev/tui/apps/irc/client.py +187 -0
  103. omdev/tui/apps/irc/commands.py +175 -0
  104. omdev/tui/apps/irc/main.py +26 -0
  105. omdev/tui/apps/markdown/__init__.py +0 -0
  106. omdev/tui/apps/markdown/__main__.py +11 -0
  107. omdev/{ptk → tui/apps}/markdown/cli.py +5 -7
  108. omdev/tui/rich/__init__.py +46 -0
  109. omdev/tui/rich/console2.py +20 -0
  110. omdev/tui/rich/markdown2.py +186 -0
  111. omdev/tui/textual/__init__.py +265 -0
  112. omdev/tui/textual/app2.py +16 -0
  113. omdev/tui/textual/autocomplete/LICENSE +21 -0
  114. omdev/tui/textual/autocomplete/__init__.py +33 -0
  115. omdev/tui/textual/autocomplete/matching.py +226 -0
  116. omdev/tui/textual/autocomplete/paths.py +202 -0
  117. omdev/tui/textual/autocomplete/widget.py +612 -0
  118. omdev/tui/textual/debug/__init__.py +10 -0
  119. omdev/tui/textual/debug/dominfo.py +151 -0
  120. omdev/tui/textual/debug/screen.py +24 -0
  121. omdev/tui/textual/devtools.py +187 -0
  122. omdev/tui/textual/drivers2.py +55 -0
  123. omdev/tui/textual/logging2.py +20 -0
  124. omdev/tui/textual/types.py +45 -0
  125. {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev495.dist-info}/METADATA +15 -9
  126. {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev495.dist-info}/RECORD +135 -80
  127. omdev/ptk/__init__.py +0 -103
  128. omdev/ptk/apps/ncdu.py +0 -167
  129. omdev/ptk/confirm.py +0 -60
  130. omdev/ptk/markdown/LICENSE +0 -22
  131. omdev/ptk/markdown/__init__.py +0 -10
  132. omdev/ptk/markdown/__main__.py +0 -11
  133. omdev/ptk/markdown/border.py +0 -94
  134. omdev/ptk/markdown/markdown.py +0 -390
  135. omdev/ptk/markdown/parser.py +0 -42
  136. omdev/ptk/markdown/styles.py +0 -29
  137. omdev/ptk/markdown/tags.py +0 -299
  138. omdev/ptk/markdown/utils.py +0 -366
  139. omdev/pyproject/cexts.py +0 -110
  140. /omdev/{ptk/apps → irc}/__init__.py +0 -0
  141. /omdev/{tokens → irc/messages}/__init__.py +0 -0
  142. /omdev/{tokens → py/tokens}/all.py +0 -0
  143. /omdev/{tokens → py/tokens}/tokenizert.py +0 -0
  144. /omdev/{tokens → py/tokens}/utils.py +0 -0
  145. {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev495.dist-info}/WHEEL +0 -0
  146. {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev495.dist-info}/entry_points.txt +0 -0
  147. {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev495.dist-info}/licenses/LICENSE +0 -0
  148. {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
@@ -2,6 +2,8 @@
2
2
  TODO:
3
3
  - options lol - csv header, newline, etc
4
4
  - edn
5
+ - jsonl
6
+ - jsonc (just comments)
5
7
  """
6
8
  import dataclasses as dc
7
9
  import enum
@@ -53,8 +53,11 @@ class Processor:
53
53
 
54
54
  def _marshal(self, v: ta.Any) -> ta.Any:
55
55
  return msh.MarshalContext(
56
- config_registry=msh.global_config_registry(),
57
- factory=self._marshaler_factory(),
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]:
@@ -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 and templates
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
- HTML_TEMPLATE = """
26
- <!DOCTYPE html>
27
- <html lang="en">
28
-
29
- <head>
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 filepath.endswith('.jsonl'):
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 filepath.endswith('.json5'):
63
+ elif mode == 'json5':
87
64
  from omlish.formats import json5
88
65
  json_content = json5.loads(raw_content)
89
- else:
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 = HTML_TEMPLATE.format(
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
- parser.add_argument(
136
- 'filepath',
137
- help='The path to the JSON file you want to view.',
138
- )
139
- parser.add_argument(
140
- '-p', '--port',
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
- view_json(args.filepath, args.port)
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
- sys.exit(1)
387
+ raise SystemExit(1) from None
388
388
  except KeyboardInterrupt:
389
- sys.exit(1)
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
@@ -0,0 +1,4 @@
1
+ if __name__ == '__main__':
2
+ from .main import _main
3
+
4
+ _main()