ppqs 2.1.0__tar.gz → 2.2.1__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,18 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Karl Wette
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6
+ associated documentation files (the "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
9
+ following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all copies or substantial
12
+ portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
15
+ LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
16
+ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
18
+ USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ppqs
3
- Version: 2.1.0
3
+ Version: 2.2.1
4
4
  Summary: Python project quick scripts
5
5
  Author-email: Karl Wette <karl.wette@anu.edu.au>
6
6
  License-Expression: MIT
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Operating System :: OS Independent
11
11
  Requires-Python: >=3.9
12
12
  Description-Content-Type: text/markdown
13
- License-File: LICENSE
13
+ License-File: LICENSES/MIT.txt
14
14
  Requires-Dist: tomli>=1.1.0; python_version < "3.11"
15
15
  Dynamic: license-file
16
16
 
@@ -100,6 +100,36 @@ $ ppqs print-something Hi
100
100
  Hi
101
101
  ```
102
102
 
103
+ `ppqs` can recursively run its own scripts, however it will only run any script
104
+ once. For example, given the following script:
105
+
106
+ ```toml
107
+ [tool.ppqs.scripts]
108
+ task-init = """
109
+ echo task init
110
+ """
111
+ task-a = """
112
+ ppqs task-init
113
+ echo task 1
114
+ """
115
+ task-b = """
116
+ ppqs task-init
117
+ echo task 2
118
+ """
119
+ all-tasks = """
120
+ ppqs task-a
121
+ ppqs task-b
122
+ """
123
+ ```
124
+
125
+ running `ppqs all-tasks` will only run `ppqs task-init` once:
126
+ ```
127
+ $ ppqs all-tasks
128
+ task init
129
+ task 1
130
+ task 2
131
+ ```
132
+
103
133
  The following only applies to scripts defined as lists of lists:
104
134
 
105
135
  * If a command argument is itself a list, it is concatenated into a path
@@ -84,6 +84,36 @@ $ ppqs print-something Hi
84
84
  Hi
85
85
  ```
86
86
 
87
+ `ppqs` can recursively run its own scripts, however it will only run any script
88
+ once. For example, given the following script:
89
+
90
+ ```toml
91
+ [tool.ppqs.scripts]
92
+ task-init = """
93
+ echo task init
94
+ """
95
+ task-a = """
96
+ ppqs task-init
97
+ echo task 1
98
+ """
99
+ task-b = """
100
+ ppqs task-init
101
+ echo task 2
102
+ """
103
+ all-tasks = """
104
+ ppqs task-a
105
+ ppqs task-b
106
+ """
107
+ ```
108
+
109
+ running `ppqs all-tasks` will only run `ppqs task-init` once:
110
+ ```
111
+ $ ppqs all-tasks
112
+ task init
113
+ task 1
114
+ task 2
115
+ ```
116
+
87
117
  The following only applies to scripts defined as lists of lists:
88
118
 
89
119
  * If a command argument is itself a list, it is concatenated into a path
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ppqs"
7
- version = "2.1.0"
7
+ version = "2.2.1"
8
8
  description = "Python project quick scripts"
9
9
  authors = [
10
10
  { name = "Karl Wette", email = "karl.wette@anu.edu.au" },
@@ -15,7 +15,7 @@ dependencies = [
15
15
  ]
16
16
  readme = "README.md"
17
17
  license = "MIT"
18
- license-files = ["LICENSE"]
18
+ license-files = ["LICENSES/MIT.txt"]
19
19
  classifiers = [
20
20
  "Programming Language :: Python :: 3",
21
21
  "Operating System :: OS Independent",
@@ -28,6 +28,15 @@ Issues = "https://github.com/kwwette/ppqs/issues"
28
28
  [project.scripts]
29
29
  ppqs = "py_proj_quick_scripts:cli"
30
30
 
31
+ [tool.ruff.lint]
32
+ select = [
33
+ "E4",
34
+ "E7",
35
+ "E9",
36
+ "EM",
37
+ "F",
38
+ ]
39
+
31
40
  [tool.pytest.ini_options]
32
41
  minversion = "6.0"
33
42
  addopts = "--cov=py_proj_quick_scripts --cov-report=term-missing --cov-fail-under=100"
@@ -53,16 +62,26 @@ pre-commit run -a
53
62
  [tool.ppqs.scripts.build]
54
63
  description = "Build project"
55
64
  script = """
56
- git clean -Xdf
65
+ ppqs clean
57
66
  python -m build
58
67
  """
59
68
 
60
69
  [tool.ppqs.scripts.test]
61
70
  description = "Run tests"
62
71
  script = """
63
- pytest ...
72
+ python -m pytest ...
64
73
  """
65
74
 
75
+ [tool.ppqs.scripts.upload]
76
+ description = "Upload package"
77
+ script = [
78
+ ["ppqs", "test"],
79
+ ["ppqs", "build"],
80
+ ["twine", "upload", ["dist", "*"]],
81
+ ["git", "push", "origin"],
82
+ ["git", "push", "origin", "--tags"],
83
+ ]
84
+
66
85
  [tool.ppqs.scripts.clean]
67
86
  description = "Clean up build files"
68
87
  script = "git clean -Xdf"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ppqs
3
- Version: 2.1.0
3
+ Version: 2.2.1
4
4
  Summary: Python project quick scripts
5
5
  Author-email: Karl Wette <karl.wette@anu.edu.au>
6
6
  License-Expression: MIT
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Operating System :: OS Independent
11
11
  Requires-Python: >=3.9
12
12
  Description-Content-Type: text/markdown
13
- License-File: LICENSE
13
+ License-File: LICENSES/MIT.txt
14
14
  Requires-Dist: tomli>=1.1.0; python_version < "3.11"
15
15
  Dynamic: license-file
16
16
 
@@ -100,6 +100,36 @@ $ ppqs print-something Hi
100
100
  Hi
101
101
  ```
102
102
 
103
+ `ppqs` can recursively run its own scripts, however it will only run any script
104
+ once. For example, given the following script:
105
+
106
+ ```toml
107
+ [tool.ppqs.scripts]
108
+ task-init = """
109
+ echo task init
110
+ """
111
+ task-a = """
112
+ ppqs task-init
113
+ echo task 1
114
+ """
115
+ task-b = """
116
+ ppqs task-init
117
+ echo task 2
118
+ """
119
+ all-tasks = """
120
+ ppqs task-a
121
+ ppqs task-b
122
+ """
123
+ ```
124
+
125
+ running `ppqs all-tasks` will only run `ppqs task-init` once:
126
+ ```
127
+ $ ppqs all-tasks
128
+ task init
129
+ task 1
130
+ task 2
131
+ ```
132
+
103
133
  The following only applies to scripts defined as lists of lists:
104
134
 
105
135
  * If a command argument is itself a list, it is concatenated into a path
@@ -1,6 +1,6 @@
1
- LICENSE
2
1
  README.md
3
2
  pyproject.toml
3
+ LICENSES/MIT.txt
4
4
  src/ppqs.egg-info/PKG-INFO
5
5
  src/ppqs.egg-info/SOURCES.txt
6
6
  src/ppqs.egg-info/dependency_links.txt
@@ -1,3 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2025 Karl Wette
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
1
5
  import glob
2
6
  import os
3
7
  import re
@@ -13,7 +17,6 @@ else: # pragma: no cover
13
17
  import tomli as tomllib
14
18
 
15
19
  __author__ = "Karl Wette"
16
- __version__ = "2.1.0"
17
20
 
18
21
 
19
22
  class MissingPyProjectError(Exception):
@@ -175,6 +178,16 @@ def parse_scripts(pyproject_path):
175
178
  "commands": script_commands,
176
179
  }
177
180
 
181
+ # Check that recursively run scripts exist
182
+ for script in scripts.values():
183
+ for command in script["commands"]:
184
+ cmd_exec = command[0]
185
+ if cmd_exec == "ppqs":
186
+ script_name_r = command[1]
187
+ if script_name_r not in scripts:
188
+ msg = f"script {script_name_r}' does not exist"
189
+ raise InvalidScriptError(pyproject_path, msg)
190
+
178
191
  return project_name, scripts
179
192
 
180
193
 
@@ -206,14 +219,38 @@ def print_help(project_name, scripts):
206
219
  )
207
220
 
208
221
 
209
- def run_script(script_name, script, argv, cwd):
222
+ def run_script(scripts, script_name, argv, cwd, has_been_run):
210
223
  """
211
224
  Run a script.
212
225
  """
226
+
213
227
  col_width = shutil.get_terminal_size().columns
228
+
229
+ # Mark script has having been run
230
+ has_been_run.add(script_name)
231
+
232
+ script = scripts[script_name]
214
233
  for command in script["commands"]:
215
234
  cmd = []
216
235
 
236
+ cmd_exec = command.pop(0)
237
+ if cmd_exec == "ppqs":
238
+
239
+ # Recursively run script
240
+ # - silently ignore scripts which have already been run
241
+ script_name_r = command.pop(0)
242
+ if script_name_r not in has_been_run:
243
+ run_script(scripts, script_name_r, argv, cwd, has_been_run)
244
+ continue
245
+
246
+ elif cmd_exec == "python":
247
+
248
+ # Use same Python as ppqs to run Python scripts
249
+ cmd.append(sys.executable)
250
+
251
+ else:
252
+ cmd.append(cmd_exec)
253
+
217
254
  for arg in command:
218
255
  if isinstance(arg, Path):
219
256
  if "*" in arg.parts[-1]:
@@ -244,25 +281,35 @@ def run_script(script_name, script, argv, cwd):
244
281
  raise SystemExit(retn.returncode)
245
282
 
246
283
 
247
- def regular_cli(argv):
284
+ def bash_completion(): # pragma: no cover
248
285
  """
249
- Parse regular command line.
286
+ Perform Bash command-line completion.
250
287
  """
251
288
 
252
289
  # Parse scripts
253
290
  pyproject_path = find_pyproject()
254
- project_name, scripts = parse_scripts(pyproject_path)
255
-
256
- # Parse command line
257
- if argv[0] in ("-h", "-help", "--help"):
258
- print_help(project_name, scripts)
259
- raise SystemExit(1)
260
- elif argv[0] not in scripts:
261
- msg = f"unknown script '{argv[0]}'"
262
- raise InvalidScriptError(pyproject_path, msg)
263
- else:
264
- cwd = pyproject_path.parent
265
- run_script(argv[0], scripts[argv[0]], argv[1:], cwd)
291
+ _, scripts = parse_scripts(pyproject_path)
292
+
293
+ # Get current command line
294
+ line = os.environ["COMP_LINE"]
295
+ words = line.split()
296
+
297
+ if line.endswith(" ") and len(words) == 1:
298
+
299
+ # Complete full script name, e.g.:
300
+ # $ ppqs <TAB><TAB>
301
+ for script_name in scripts:
302
+ print(script_name)
303
+ return
304
+
305
+ if not line.endswith(" ") and len(words) == 2:
306
+
307
+ # Complete partial script name, e.g.:
308
+ # $ ppqs nam<TAB>
309
+ for script_name in scripts:
310
+ if script_name.startswith(words[-1]):
311
+ print(script_name)
312
+ return
266
313
 
267
314
 
268
315
  def cli(*argv):
@@ -274,32 +321,26 @@ def cli(*argv):
274
321
 
275
322
  if argv[0] == "--bash-completion": # pragma: no cover
276
323
 
277
- # Perform Bash completion
278
- # - suppress any error output
324
+ # Perform Bash command-line completion, suppress any errors
279
325
  try:
280
- pyproject_path = find_pyproject()
281
- _, scripts = parse_scripts(pyproject_path)
282
- line = os.environ["COMP_LINE"]
283
- words = line.split()
284
- if line.endswith(" ") and len(words) == 1:
285
-
286
- # Complete full script name, e.g.:
287
- # $ ppqs <TAB><TAB>
288
- for script_name in scripts:
289
- print(script_name)
290
-
291
- if not line.endswith(" ") and len(words) == 2:
292
-
293
- # Complete partial script name, e.g.:
294
- # $ ppqs nam<TAB>
295
- for script_name in scripts:
296
- if script_name.startswith(words[-1]):
297
- print(script_name)
298
-
326
+ bash_completion()
299
327
  except Exception:
300
328
  pass
301
329
 
302
330
  else:
303
331
 
304
- # Parse regular command line
305
- regular_cli(argv)
332
+ # Parse scripts
333
+ pyproject_path = find_pyproject()
334
+ project_name, scripts = parse_scripts(pyproject_path)
335
+
336
+ # Parse command line
337
+ if argv[0] in ("-h", "-help", "--help"):
338
+ print_help(project_name, scripts)
339
+ raise SystemExit(1)
340
+ elif argv[0] not in scripts:
341
+ msg = f"unknown script '{argv[0]}'"
342
+ raise InvalidScriptError(pyproject_path, msg)
343
+ else:
344
+ cwd = pyproject_path.parent
345
+ has_been_run = set()
346
+ run_script(scripts, argv[0], argv[1:], cwd, has_been_run)
@@ -1,3 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2025 Karl Wette
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
1
5
  import os
2
6
 
3
7
  import pytest
@@ -34,6 +38,14 @@ def pyproject_path(tmp_path):
34
38
  script = [
35
39
  ["python", "-c", "import sys; print(*sys.argv[1:])", ["subdir", "*.txt"]],
36
40
  ]
41
+ [tool.ppqs.scripts.greeting]
42
+ script = "echo Hello"
43
+ [tool.ppqs.scripts.farewell]
44
+ script = '''
45
+ ppqs greeting
46
+ ppqs greeting
47
+ echo Goodbye
48
+ '''
37
49
  """
38
50
 
39
51
  write_file(pyproject_path, pyproject_toml)
@@ -109,3 +121,18 @@ def test_echo_wildcard(pyproject_path, capfd):
109
121
  captured = capfd.readouterr()
110
122
  assert captured.out == "subdir/a.txt subdir/b.txt\n".replace("/", os.sep)
111
123
  assert captured.err == ""
124
+
125
+
126
+ def test_recursion(pyproject_path, capfd):
127
+ """
128
+ Test recursive script running.
129
+ """
130
+
131
+ with in_dir(pyproject_path.parent):
132
+ cli("farewell")
133
+ captured = capfd.readouterr()
134
+ assert captured.out == "Hello\nGoodbye\n"
135
+ assert captured.err == ""
136
+
137
+ with pytest.raises(InvalidScriptError):
138
+ cli("no-script")
@@ -1,3 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2025 Karl Wette
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
1
5
  import pytest
2
6
 
3
7
  from py_proj_quick_scripts import (
@@ -1,3 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2025 Karl Wette
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
1
5
  from contextlib import nullcontext
2
6
  from pathlib import Path
3
7
 
@@ -240,21 +244,14 @@ from . import in_dir, write_file
240
244
  ),
241
245
  pytest.param(
242
246
  """
243
- [tool.ppqs.scripts]
244
- paths = [
245
- ["echo", ["a", "*.txt"]],
246
- ]
247
+ [tool.ppqs.scripts.no-script]
248
+ script = "ppqs script-does-not-exist"
247
249
  """,
248
- nullcontext(
249
- {
250
- "paths": {
251
- "description": "Run paths script",
252
- "print-header": False,
253
- "commands": [["echo", Path("a", "*.txt")]],
254
- }
255
- }
250
+ pytest.raises(
251
+ InvalidScriptError,
252
+ match="does not exist",
256
253
  ),
257
- id="list script with path and valid wildcard",
254
+ id="recursive script does not exist",
258
255
  ),
259
256
  ],
260
257
  )
ppqs-2.1.0/LICENSE DELETED
@@ -1,18 +0,0 @@
1
- Copyright 2025 Karl Wette
2
-
3
- Permission is hereby granted, free of charge, to any person obtaining a copy of
4
- this software and associated documentation files (the “Software”), to deal in
5
- the Software without restriction, including without limitation the rights to
6
- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
- the Software, and to permit persons to whom the Software is furnished to do so,
8
- subject to the following conditions:
9
-
10
- The above copyright notice and this permission notice shall be included in all
11
- copies or substantial portions of the Software.
12
-
13
- THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
File without changes