ppqs 2.3.0__tar.gz → 2.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ppqs
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: Python project quick scripts
5
5
  Author-email: Karl Wette <karl.wette@anu.edu.au>
6
6
  License-Expression: MIT
@@ -210,3 +210,18 @@ complete -C "ppqs --bash-completion" ppqs
210
210
  Typing `ppqs ` and hitting `TAB` will then show scripts available for the
211
211
  current project. Typing part of a script name and hitting `TAB` will show
212
212
  matching script name(s), or else complete the full name if unambiguous.
213
+
214
+ By default, Bash completion will only complete names of scripts. Each script may
215
+ additionally define a glob pattern, given as the option
216
+ `bash-complete-file-glob` under `[tool.ppqs.scripts.<script-name>]`. Files
217
+ matching that pattern (relative to the project root directory) will then be
218
+ available as completions for additional arguments to the script. Example:
219
+
220
+ ```
221
+ [tool.ppqs.scripts.test]
222
+ bash-complete-file-glob = "test/**/test_*.py"
223
+ script = "run-tests ..."
224
+ ```
225
+
226
+ This script will complete on any files matching `test/**/test_*.py`, and pass
227
+ the file names to the command `run-tests`.
@@ -194,3 +194,18 @@ complete -C "ppqs --bash-completion" ppqs
194
194
  Typing `ppqs ` and hitting `TAB` will then show scripts available for the
195
195
  current project. Typing part of a script name and hitting `TAB` will show
196
196
  matching script name(s), or else complete the full name if unambiguous.
197
+
198
+ By default, Bash completion will only complete names of scripts. Each script may
199
+ additionally define a glob pattern, given as the option
200
+ `bash-complete-file-glob` under `[tool.ppqs.scripts.<script-name>]`. Files
201
+ matching that pattern (relative to the project root directory) will then be
202
+ available as completions for additional arguments to the script. Example:
203
+
204
+ ```
205
+ [tool.ppqs.scripts.test]
206
+ bash-complete-file-glob = "test/**/test_*.py"
207
+ script = "run-tests ..."
208
+ ```
209
+
210
+ This script will complete on any files matching `test/**/test_*.py`, and pass
211
+ the file names to the command `run-tests`.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ppqs"
7
- version = "2.3.0"
7
+ version = "2.4.0"
8
8
  description = "Python project quick scripts"
9
9
  authors = [
10
10
  { name = "Karl Wette", email = "karl.wette@anu.edu.au" },
@@ -28,15 +28,25 @@ Issues = "https://github.com/kwwette/ppqs/issues"
28
28
  [project.scripts]
29
29
  ppqs = "py_proj_quick_scripts:cli"
30
30
 
31
+ [tool.ruff]
32
+ fix = true
33
+ show-fixes = true
34
+
31
35
  [tool.ruff.lint]
32
36
  select = [
33
- "E4",
34
- "E7",
35
- "E9",
37
+ "D1", "D2", "D4",
38
+ "E4", "E7", "E9",
36
39
  "EM",
37
40
  "F",
41
+ "I",
42
+ ]
43
+ ignore = [
44
+ "D105", "D202",
38
45
  ]
39
46
 
47
+ [tool.ruff.lint.pydocstyle]
48
+ convention = "google"
49
+
40
50
  [tool.pytest.ini_options]
41
51
  minversion = "6.0"
42
52
  addopts = "--cov=py_proj_quick_scripts --cov-report=term-missing --cov-fail-under=100"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ppqs
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: Python project quick scripts
5
5
  Author-email: Karl Wette <karl.wette@anu.edu.au>
6
6
  License-Expression: MIT
@@ -210,3 +210,18 @@ complete -C "ppqs --bash-completion" ppqs
210
210
  Typing `ppqs ` and hitting `TAB` will then show scripts available for the
211
211
  current project. Typing part of a script name and hitting `TAB` will show
212
212
  matching script name(s), or else complete the full name if unambiguous.
213
+
214
+ By default, Bash completion will only complete names of scripts. Each script may
215
+ additionally define a glob pattern, given as the option
216
+ `bash-complete-file-glob` under `[tool.ppqs.scripts.<script-name>]`. Files
217
+ matching that pattern (relative to the project root directory) will then be
218
+ available as completions for additional arguments to the script. Example:
219
+
220
+ ```
221
+ [tool.ppqs.scripts.test]
222
+ bash-complete-file-glob = "test/**/test_*.py"
223
+ script = "run-tests ..."
224
+ ```
225
+
226
+ This script will complete on any files matching `test/**/test_*.py`, and pass
227
+ the file names to the command `run-tests`.
@@ -2,6 +2,8 @@
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
4
 
5
+ """Python project quick scripts."""
6
+
5
7
  import glob
6
8
  import os
7
9
  import re
@@ -20,19 +22,16 @@ __author__ = "Karl Wette"
20
22
 
21
23
 
22
24
  class MissingPyProjectError(Exception):
23
- """
24
- Raise if 'pyproject.toml' could not be found.
25
- """
25
+ """Raise if 'pyproject.toml' could not be found."""
26
26
 
27
27
  pass
28
28
 
29
29
 
30
30
  class InvalidScriptError(Exception):
31
- """
32
- Raise for invalid 'pyproject.toml' script errors.
33
- """
31
+ """Raise for invalid 'pyproject.toml' script errors."""
34
32
 
35
33
  def __init__(self, pyproject_path, msg):
34
+ """Initialise exception."""
36
35
  self.pyproject_path = pyproject_path
37
36
  self.msg = msg
38
37
  super().__init__(self.msg)
@@ -42,9 +41,7 @@ class InvalidScriptError(Exception):
42
41
 
43
42
 
44
43
  def find_pyproject():
45
- """
46
- Look for pyproject.toml in current/parent directories.
47
- """
44
+ """Look for pyproject.toml in current/parent directories."""
48
45
 
49
46
  # Start in the current directory
50
47
  current_dir = starting_dir = Path.cwd()
@@ -70,9 +67,7 @@ def find_pyproject():
70
67
 
71
68
 
72
69
  def parse_scripts(pyproject_path):
73
- """
74
- Parse scripts from pyproject.toml.
75
- """
70
+ """Parse scripts from pyproject.toml."""
76
71
 
77
72
  # Read pyproject.toml
78
73
  pyproject_toml = tomllib.load(pyproject_path.open("rb"))
@@ -92,6 +87,12 @@ def parse_scripts(pyproject_path):
92
87
  )
93
88
  except KeyError:
94
89
  default_script_print_header = False
90
+ try:
91
+ default_bash_complete_file_glob = bool(
92
+ pyproject_toml["tool"]["ppqs"]["defaults"]["bash-complete-file-glob"]
93
+ )
94
+ except KeyError:
95
+ default_bash_complete_file_glob = None
95
96
 
96
97
  # Parse scripts
97
98
  scripts = {}
@@ -109,6 +110,7 @@ def parse_scripts(pyproject_path):
109
110
  # Create default description
110
111
  script_description = f"Run {script_name} script"
111
112
  script_print_header = default_script_print_header
113
+ script_bash_complete_file_glob = default_bash_complete_file_glob
112
114
  script_commands_toml = script_toml
113
115
 
114
116
  if isinstance(script_toml, dict):
@@ -117,7 +119,13 @@ def parse_scripts(pyproject_path):
117
119
  invalid_keys = [
118
120
  k
119
121
  for k in script_toml.keys()
120
- if k not in ("description", "print-header", "script")
122
+ if k
123
+ not in (
124
+ "description",
125
+ "print-header",
126
+ "bash-complete-file-glob",
127
+ "script",
128
+ )
121
129
  ]
122
130
  if len(invalid_keys) > 0:
123
131
  invalid_keys_str = "', '".join(invalid_keys)
@@ -130,6 +138,11 @@ def parse_scripts(pyproject_path):
130
138
  script_print_header = bool(
131
139
  script_toml.get("print-header", script_print_header)
132
140
  )
141
+ script_bash_complete_file_glob = script_toml.get(
142
+ "bash-complete-file-glob", script_bash_complete_file_glob
143
+ )
144
+ if script_bash_complete_file_glob is not None:
145
+ script_bash_complete_file_glob = str(script_bash_complete_file_glob)
133
146
  script_commands_toml = script_toml["script"]
134
147
 
135
148
  msg = f"script '{script_name}' may be either a string or a list of lists"
@@ -175,6 +188,7 @@ def parse_scripts(pyproject_path):
175
188
  scripts[script_name] = {
176
189
  "description": script_description,
177
190
  "print-header": script_print_header,
191
+ "bash-complete-file-glob": script_bash_complete_file_glob,
178
192
  "commands": script_commands,
179
193
  }
180
194
 
@@ -192,9 +206,7 @@ def parse_scripts(pyproject_path):
192
206
 
193
207
 
194
208
  def print_help(project_name, scripts):
195
- """
196
- Print help.
197
- """
209
+ """Print help."""
198
210
  script_names = ",".join(scripts.keys())
199
211
  max_script_name_len = max(len(script_name) for script_name in scripts)
200
212
  list_of_scripts = "\n".join(
@@ -220,9 +232,7 @@ def print_help(project_name, scripts):
220
232
 
221
233
 
222
234
  def run_script(scripts, script_name, argv, cwd, has_been_run):
223
- """
224
- Run a script.
225
- """
235
+ """Run a script."""
226
236
 
227
237
  col_width = shutil.get_terminal_size().columns
228
238
 
@@ -233,24 +243,6 @@ def run_script(scripts, script_name, argv, cwd, has_been_run):
233
243
  for command in script["commands"]:
234
244
  cmd = []
235
245
 
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
-
254
246
  for arg in command:
255
247
  if isinstance(arg, Path):
256
248
  if "*" in arg.parts[-1]:
@@ -269,6 +261,20 @@ def run_script(scripts, script_name, argv, cwd, has_been_run):
269
261
  else:
270
262
  cmd.append(arg)
271
263
 
264
+ if cmd[0] == "ppqs":
265
+
266
+ # Recursively run script
267
+ # - silently ignore scripts which have already been run
268
+ script_name_r = cmd[1]
269
+ if script_name_r not in has_been_run:
270
+ run_script(scripts, script_name_r, cmd[2:], cwd, has_been_run)
271
+ continue
272
+
273
+ elif cmd[0] == "python":
274
+
275
+ # Use same Python as ppqs to run Python scripts
276
+ cmd[0] = sys.executable
277
+
272
278
  # Print header
273
279
  if script["print-header"]:
274
280
  cmd_str = " ".join(cmd)
@@ -282,9 +288,7 @@ def run_script(scripts, script_name, argv, cwd, has_been_run):
282
288
 
283
289
 
284
290
  def bash_completion(): # pragma: no cover
285
- """
286
- Perform Bash command-line completion.
287
- """
291
+ """Perform Bash command-line completion."""
288
292
 
289
293
  # Parse scripts
290
294
  pyproject_path = find_pyproject()
@@ -311,11 +315,37 @@ def bash_completion(): # pragma: no cover
311
315
  print(script_name)
312
316
  return
313
317
 
318
+ script_name = words[1]
319
+ script_bash_complete_file_glob = scripts[script_name]["bash-complete-file-glob"]
320
+ if script_bash_complete_file_glob is not None:
321
+
322
+ # Generate list of files to complete on
323
+ project_dir = pyproject_path.parent
324
+ files = [
325
+ str(f.relative_to(project_dir))
326
+ for f in project_dir.glob(script_bash_complete_file_glob)
327
+ ]
328
+
329
+ if line.endswith(" "):
330
+
331
+ # Complete full script name, e.g.:
332
+ # $ ppqs script <TAB><TAB>
333
+ for file_name in files:
334
+ print(file_name)
335
+ return
336
+
337
+ else:
338
+
339
+ # Complete partial script name, e.g.:
340
+ # $ ppqs script fil<TAB>
341
+ for file_name in files:
342
+ if file_name.startswith(words[-1]):
343
+ print(file_name)
344
+ return
345
+
314
346
 
315
347
  def cli(*argv):
316
- """
317
- Parse command line.
318
- """
348
+ """Parse command line."""
319
349
 
320
350
  argv = [str(a) for a in (argv or sys.argv[1:] or ["--help"])]
321
351
 
@@ -2,6 +2,8 @@
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
4
 
5
+ """Test command-line interface."""
6
+
5
7
  import os
6
8
 
7
9
  import pytest
@@ -13,9 +15,7 @@ from . import in_dir, write_file
13
15
 
14
16
  @pytest.fixture
15
17
  def pyproject_path(tmp_path):
16
- """
17
- Write pyproject.toml for command-line tests.
18
- """
18
+ """Write pyproject.toml for command-line tests."""
19
19
 
20
20
  pyproject_path = tmp_path / "pyproject.toml"
21
21
  pyproject_toml = """
@@ -39,11 +39,11 @@ def pyproject_path(tmp_path):
39
39
  ["python", "-c", "import sys; print(*sys.argv[1:])", ["subdir", "*.txt"]],
40
40
  ]
41
41
  [tool.ppqs.scripts.greeting]
42
- script = "echo Hello"
42
+ script = "echo Hello ..."
43
43
  [tool.ppqs.scripts.farewell]
44
44
  script = '''
45
- ppqs greeting
46
- ppqs greeting
45
+ ppqs greeting friend!
46
+ ppqs greeting comrade!
47
47
  echo Goodbye
48
48
  '''
49
49
  """
@@ -54,9 +54,7 @@ def pyproject_path(tmp_path):
54
54
 
55
55
 
56
56
  def test_help(pyproject_path):
57
- """
58
- Test command line help.
59
- """
57
+ """Test command line help."""
60
58
 
61
59
  with in_dir(pyproject_path.parent):
62
60
  with pytest.raises(SystemExit):
@@ -64,9 +62,7 @@ def test_help(pyproject_path):
64
62
 
65
63
 
66
64
  def test_notest(pyproject_path):
67
- """
68
- Test a non-existent test.
69
- """
65
+ """Test a non-existent test."""
70
66
 
71
67
  with in_dir(pyproject_path.parent):
72
68
  with pytest.raises(InvalidScriptError):
@@ -74,9 +70,7 @@ def test_notest(pyproject_path):
74
70
 
75
71
 
76
72
  def test_exit(pyproject_path):
77
- """
78
- Test `exit` script.
79
- """
73
+ """Test `exit` script."""
80
74
 
81
75
  with in_dir(pyproject_path.parent):
82
76
  with pytest.raises(SystemExit):
@@ -84,9 +78,7 @@ def test_exit(pyproject_path):
84
78
 
85
79
 
86
80
  def test_echo(pyproject_path, capfd):
87
- """
88
- Test `echo` script.
89
- """
81
+ """Test `echo` script."""
90
82
 
91
83
  with in_dir(pyproject_path.parent):
92
84
  cli("echo", "Hello")
@@ -96,9 +88,7 @@ def test_echo(pyproject_path, capfd):
96
88
 
97
89
 
98
90
  def test_echo_path(pyproject_path, capfd):
99
- """
100
- Test `echo-path` script.
101
- """
91
+ """Test `echo-path` script."""
102
92
 
103
93
  with in_dir(pyproject_path.parent):
104
94
  cli("echo-path")
@@ -108,9 +98,7 @@ def test_echo_path(pyproject_path, capfd):
108
98
 
109
99
 
110
100
  def test_echo_wildcard(pyproject_path, capfd):
111
- """
112
- Test `echo-wildcard` script.
113
- """
101
+ """Test `echo-wildcard` script."""
114
102
 
115
103
  with in_dir(pyproject_path.parent) as wd:
116
104
  subdir = wd / "subdir"
@@ -124,14 +112,12 @@ def test_echo_wildcard(pyproject_path, capfd):
124
112
 
125
113
 
126
114
  def test_recursion(pyproject_path, capfd):
127
- """
128
- Test recursive script running.
129
- """
115
+ """Test recursive script running."""
130
116
 
131
117
  with in_dir(pyproject_path.parent):
132
118
  cli("farewell")
133
119
  captured = capfd.readouterr()
134
- assert captured.out == "Hello\nGoodbye\n"
120
+ assert captured.out == "Hello friend!\nGoodbye\n"
135
121
  assert captured.err == ""
136
122
 
137
123
  with pytest.raises(InvalidScriptError):
@@ -2,6 +2,8 @@
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
4
 
5
+ """Test find_pyproject()."""
6
+
5
7
  import pytest
6
8
 
7
9
  from py_proj_quick_scripts import (
@@ -13,9 +15,7 @@ from . import in_dir, write_file
13
15
 
14
16
 
15
17
  def test_find_pyproject(tmp_path):
16
- """
17
- Test find_pyproject().
18
- """
18
+ """Test find_pyproject()."""
19
19
 
20
20
  with in_dir(tmp_path):
21
21
  with pytest.raises(MissingPyProjectError):
@@ -2,6 +2,8 @@
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
4
 
5
+ """Test parse_scripts()."""
6
+
5
7
  from contextlib import nullcontext
6
8
  from pathlib import Path
7
9
 
@@ -56,6 +58,7 @@ from . import in_dir, write_file
56
58
  "init": {
57
59
  "description": "Run init script",
58
60
  "print-header": False,
61
+ "bash-complete-file-glob": None,
59
62
  "commands": [["pip", "install", "-r", "requirements.txt"]],
60
63
  }
61
64
  }
@@ -75,6 +78,7 @@ from . import in_dir, write_file
75
78
  "init": {
76
79
  "description": "Run init script",
77
80
  "print-header": False,
81
+ "bash-complete-file-glob": None,
78
82
  "commands": [
79
83
  ["pip", "install", "-r", "requirements.txt"],
80
84
  ["pre-commit", "install"],
@@ -121,6 +125,7 @@ from . import in_dir, write_file
121
125
  "init": {
122
126
  "description": "Initialise project",
123
127
  "print-header": False,
128
+ "bash-complete-file-glob": None,
124
129
  "commands": [["pip", "install", "-r", "requirements.txt"]],
125
130
  }
126
131
  }
@@ -152,6 +157,7 @@ from . import in_dir, write_file
152
157
  "init": {
153
158
  "description": "Run init script",
154
159
  "print-header": False,
160
+ "bash-complete-file-glob": None,
155
161
  "commands": [["pip", "install", "-r", "requirements.txt"]],
156
162
  }
157
163
  }
@@ -172,6 +178,7 @@ from . import in_dir, write_file
172
178
  "init": {
173
179
  "description": "Initialise project",
174
180
  "print-header": True,
181
+ "bash-complete-file-glob": None,
175
182
  "commands": [["pip", "install", "-r", "requirements.txt"]],
176
183
  }
177
184
  }
@@ -192,6 +199,7 @@ from . import in_dir, write_file
192
199
  "init": {
193
200
  "description": "Initialise project",
194
201
  "print-header": False,
202
+ "bash-complete-file-glob": None,
195
203
  "commands": [
196
204
  ["pip", "install", "-r", "requirements.txt"],
197
205
  ["pre-commit", "install"],
@@ -223,6 +231,7 @@ from . import in_dir, write_file
223
231
  "paths": {
224
232
  "description": "Run paths script",
225
233
  "print-header": False,
234
+ "bash-complete-file-glob": None,
226
235
  "commands": [["echo", Path("a", "b")]],
227
236
  }
228
237
  }
@@ -253,12 +262,28 @@ from . import in_dir, write_file
253
262
  ),
254
263
  id="recursive script does not exist",
255
264
  ),
265
+ pytest.param(
266
+ """
267
+ [tool.ppqs.scripts.test]
268
+ bash-complete-file-glob = "test/**/test_*.py"
269
+ script = "run-tests ..."
270
+ """,
271
+ nullcontext(
272
+ {
273
+ "test": {
274
+ "description": "Run test script",
275
+ "print-header": False,
276
+ "bash-complete-file-glob": "test/**/test_*.py",
277
+ "commands": [["run-tests", "..."]],
278
+ }
279
+ }
280
+ ),
281
+ id="string script with complete file glob",
282
+ ),
256
283
  ],
257
284
  )
258
285
  def test_parse_scripts(tmp_path, script_toml, expectation):
259
- """
260
- Test parse_scripts().
261
- """
286
+ """Test parse_scripts()."""
262
287
 
263
288
  pyproject_path = tmp_path / "pyproject.toml"
264
289
  pyproject_toml = f"""
File without changes
File without changes
File without changes