ppqs 2.3.1__tar.gz → 2.5.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.1
3
+ Version: 2.5.0
4
4
  Summary: Python project quick scripts
5
5
  Author-email: Karl Wette <karl.wette@anu.edu.au>
6
6
  License-Expression: MIT
@@ -8,7 +8,7 @@ Project-URL: Homepage, https://github.com/kwwette/ppqs
8
8
  Project-URL: Issues, https://github.com/kwwette/ppqs/issues
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Operating System :: OS Independent
11
- Requires-Python: >=3.9
11
+ Requires-Python: >=3.10
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSES/MIT.txt
14
14
  Requires-Dist: tomli>=1.1.0; python_version < "3.11"
@@ -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,12 +4,12 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ppqs"
7
- version = "2.3.1"
7
+ version = "2.5.0"
8
8
  description = "Python project quick scripts"
9
9
  authors = [
10
10
  { name = "Karl Wette", email = "karl.wette@anu.edu.au" },
11
11
  ]
12
- requires-python = ">=3.9"
12
+ requires-python = ">=3.10"
13
13
  dependencies = [
14
14
  "tomli>=1.1.0; python_version<'3.11'"
15
15
  ]
@@ -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.1
3
+ Version: 2.5.0
4
4
  Summary: Python project quick scripts
5
5
  Author-email: Karl Wette <karl.wette@anu.edu.au>
6
6
  License-Expression: MIT
@@ -8,7 +8,7 @@ Project-URL: Homepage, https://github.com/kwwette/ppqs
8
8
  Project-URL: Issues, https://github.com/kwwette/ppqs/issues
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Operating System :: OS Independent
11
- Requires-Python: >=3.9
11
+ Requires-Python: >=3.10
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSES/MIT.txt
14
14
  Requires-Dist: tomli>=1.1.0; python_version < "3.11"
@@ -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
 
@@ -262,9 +272,15 @@ def run_script(scripts, script_name, argv, cwd, has_been_run):
262
272
 
263
273
  elif cmd[0] == "python":
264
274
 
265
- # Use same Python as ppqs to run Python scripts
275
+ # By default, use same Python as ppqs to run Python scripts
266
276
  cmd[0] = sys.executable
267
277
 
278
+ # If virtual environment is present, try to use its Python
279
+ if "VIRTUAL_ENV" in os.environ:
280
+ venv_python = Path(os.environ["VIRTUAL_ENV"]) / "bin" / "python"
281
+ if venv_python.exists() and os.access(venv_python, os.X_OK):
282
+ cmd[0] = str(venv_python)
283
+
268
284
  # Print header
269
285
  if script["print-header"]:
270
286
  cmd_str = " ".join(cmd)
@@ -278,9 +294,7 @@ def run_script(scripts, script_name, argv, cwd, has_been_run):
278
294
 
279
295
 
280
296
  def bash_completion(): # pragma: no cover
281
- """
282
- Perform Bash command-line completion.
283
- """
297
+ """Perform Bash command-line completion."""
284
298
 
285
299
  # Parse scripts
286
300
  pyproject_path = find_pyproject()
@@ -307,11 +321,37 @@ def bash_completion(): # pragma: no cover
307
321
  print(script_name)
308
322
  return
309
323
 
324
+ script_name = words[1]
325
+ script_bash_complete_file_glob = scripts[script_name]["bash-complete-file-glob"]
326
+ if script_bash_complete_file_glob is not None:
327
+
328
+ # Generate list of files to complete on
329
+ project_dir = pyproject_path.parent
330
+ files = [
331
+ str(f.relative_to(project_dir))
332
+ for f in project_dir.glob(script_bash_complete_file_glob)
333
+ ]
334
+
335
+ if line.endswith(" "):
336
+
337
+ # Complete full script name, e.g.:
338
+ # $ ppqs script <TAB><TAB>
339
+ for file_name in files:
340
+ print(file_name)
341
+ return
342
+
343
+ else:
344
+
345
+ # Complete partial script name, e.g.:
346
+ # $ ppqs script fil<TAB>
347
+ for file_name in files:
348
+ if file_name.startswith(words[-1]):
349
+ print(file_name)
350
+ return
351
+
310
352
 
311
353
  def cli(*argv):
312
- """
313
- Parse command line.
314
- """
354
+ """Parse command line."""
315
355
 
316
356
  argv = [str(a) for a in (argv or sys.argv[1:] or ["--help"])]
317
357
 
@@ -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 = """
@@ -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,9 +112,7 @@ 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")
@@ -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