|
1 | 1 | from invoke.tasks import task |
| 2 | +from invoke import Context, task |
| 3 | +from typing import Any |
2 | 4 | import shlex |
3 | 5 |
|
| 6 | +from invoke import Context, task |
| 7 | +from typing import Any |
| 8 | + |
| 9 | + |
4 | 10 | @task |
5 | | -def test(c): |
6 | | - c.run("poetry run pytest --cov=gen_surv --cov-report=term --cov-report=xml") |
| 11 | +def test(c: Context) -> None: |
| 12 | + """ |
| 13 | + Run pytest via Poetry with coverage reporting for the 'gen_surv' package. |
| 14 | +
|
| 15 | + This task will: |
| 16 | + 1. Execute 'pytest' through Poetry. |
| 17 | + 2. Generate a terminal coverage report. |
| 18 | + 3. Write an XML coverage report to 'coverage.xml'. |
| 19 | +
|
| 20 | + :param c: Invoke context used to run shell commands. |
| 21 | + :raises TypeError: If 'c' is not an Invoke Context. |
| 22 | + """ |
| 23 | + # Ensure we were passed a valid Context object. |
| 24 | + if not isinstance(c, Context): |
| 25 | + raise TypeError(f"Expected Invoke Context, got {type(c).__name__!r} instead") |
| 26 | + |
| 27 | + # Build the command string. You can adjust '--cov=gen_surv' if you |
| 28 | + # need to cover a different package or add extra pytest flags. |
| 29 | + command = ( |
| 30 | + "poetry run pytest " |
| 31 | + "--cov=gen_surv " |
| 32 | + "--cov-report=term " |
| 33 | + "--cov-report=xml" |
| 34 | + ) |
| 35 | + |
| 36 | + # Run pytest. |
| 37 | + # - warn=True: capture non-zero exit codes without aborting Invoke. |
| 38 | + # - pty=False: pytest doesn’t require an interactive TTY here. |
| 39 | + result = c.run(command, warn=True, pty=False) |
| 40 | + |
| 41 | + # Check the exit code and report accordingly. |
| 42 | + if result.ok: |
| 43 | + print("✔️ All tests passed.") |
| 44 | + else: |
| 45 | + print("❌ Some tests failed.") |
| 46 | + print(f"Exit code: {result.exited}") |
| 47 | + if result.stderr: |
| 48 | + print("Error output:") |
| 49 | + print(result.stderr) |
| 50 | + |
7 | 51 |
|
8 | 52 | @task |
9 | | -def docs(c): |
10 | | - c.run("poetry run sphinx-build docs/source docs/build") |
| 53 | +def docs(c: Context) -> None: |
| 54 | + """ |
| 55 | + Build Sphinx documentation for the project using Poetry. |
| 56 | +
|
| 57 | + This task will: |
| 58 | + 1. Run 'sphinx-build' via Poetry. |
| 59 | + 2. Read source files from 'docs/source'. |
| 60 | + 3. Output HTML (or other format) into 'docs/build'. |
| 61 | +
|
| 62 | + :param c: Invoke context, used to run shell commands. |
| 63 | + :type c: Context |
| 64 | + :raises TypeError: If 'c' is not an Invoke Context. |
| 65 | + """ |
| 66 | + # Verify we have a proper Invoke Context. |
| 67 | + if not isinstance(c, Context): |
| 68 | + raise TypeError(f"Expected Invoke Context, got {type(c).__name__!r}") |
| 69 | + |
| 70 | + # Construct the Sphinx build command. Adjust paths if needed. |
| 71 | + command = "poetry run sphinx-build docs/source docs/build" |
| 72 | + |
| 73 | + # Execute sphinx-build. |
| 74 | + # - warn=True: capture non-zero exits without immediately aborting Invoke. |
| 75 | + # - pty=False: sphinx-build does not require interactive input. |
| 76 | + result = c.run(command, warn=True, pty=False) |
| 77 | + |
| 78 | + # Report on the result of the documentation build. |
| 79 | + if result.ok: |
| 80 | + print("✔️ Documentation built successfully.") |
| 81 | + else: |
| 82 | + print("❌ Documentation build failed.") |
| 83 | + print(f"Exit code: {result.exited}") |
| 84 | + if result.stderr: |
| 85 | + print("Error output:") |
| 86 | + print(result.stderr) |
| 87 | + |
| 88 | +from invoke import Context, task |
| 89 | +from typing import Any |
| 90 | + |
11 | 91 |
|
12 | 92 | @task |
13 | | -def stubs(c): |
14 | | - c.run("poetry run stubgen -p gen_surv -o stubs") |
| 93 | +def stubs(c: Context) -> None: |
| 94 | + """ |
| 95 | + Generate type stubs for the 'gen_surv' package using stubgen and Poetry. |
| 96 | +
|
| 97 | + This task will: |
| 98 | + 1. Run 'stubgen' via Poetry to analyze 'gen_surv'. |
| 99 | + 2. Output the generated stubs into the 'stubs' directory. |
| 100 | +
|
| 101 | + :param c: Invoke context used to run shell commands. |
| 102 | + :raises TypeError: If 'c' is not an Invoke Context. |
| 103 | + """ |
| 104 | + # Verify that 'c' is the correct Invoke Context. |
| 105 | + if not isinstance(c, Context): |
| 106 | + raise TypeError(f"Expected Invoke Context, got {type(c).__name__!r}") |
| 107 | + |
| 108 | + # Build the stubgen command. Adjust '-p gen_surv' or output path if needed. |
| 109 | + command = "poetry run stubgen -p gen_surv -o stubs" |
| 110 | + |
| 111 | + # Execute stubgen. |
| 112 | + # - warn=True: capture non-zero exit codes without aborting Invoke. |
| 113 | + # - pty=False: stubgen does not require interactive input. |
| 114 | + result = c.run(command, warn=True, pty=False) |
| 115 | + |
| 116 | + # Report on the outcome of stub generation. |
| 117 | + if result.ok: |
| 118 | + print("✔️ Type stubs generated successfully in 'stubs/'.") |
| 119 | + else: |
| 120 | + print("❌ Stub generation failed.") |
| 121 | + print(f"Exit code: {result.exited}") |
| 122 | + if result.stderr: |
| 123 | + print("Error output:") |
| 124 | + print(result.stderr) |
| 125 | + |
15 | 126 |
|
16 | 127 | @task |
17 | | -def build(c): |
18 | | - c.run("poetry build") |
| 128 | +def build(c: Context) -> None: |
| 129 | + """ |
| 130 | + Build the project distributions using Poetry. |
| 131 | +
|
| 132 | + This task will: |
| 133 | + 1. Run 'poetry build' to create source and wheel packages. |
| 134 | + 2. Place the built artifacts in the 'dist/' directory. |
| 135 | +
|
| 136 | + :param c: Invoke context used to run shell commands. |
| 137 | + :raises TypeError: If 'c' is not an Invoke Context. |
| 138 | + """ |
| 139 | + # Verify that we received a valid Invoke Context. |
| 140 | + if not isinstance(c, Context): |
| 141 | + raise TypeError(f"Expected Invoke Context, got {type(c).__name__!r}") |
| 142 | + |
| 143 | + # Construct the build command. Adjust if you need custom build options. |
| 144 | + command = "poetry build" |
| 145 | + |
| 146 | + # Execute the build. |
| 147 | + # - warn=True: capture non-zero exit codes without aborting Invoke. |
| 148 | + # - pty=False: no interactive input is required for building. |
| 149 | + result = c.run(command, warn=True, pty=False) |
| 150 | + |
| 151 | + # Report the result of the build process. |
| 152 | + if result.ok: |
| 153 | + print("✔️ Build completed successfully. Artifacts are in the 'dist/' directory.") |
| 154 | + else: |
| 155 | + print("❌ Build failed.") |
| 156 | + print(f"Exit code: {result.exited}") |
| 157 | + if result.stderr: |
| 158 | + print("Error output:") |
| 159 | + print(result.stderr) |
19 | 160 |
|
20 | 161 | @task |
21 | | -def publish(c): |
22 | | - c.run("poetry publish --build") |
| 162 | +def publish(c: Context) -> None: |
| 163 | + """ |
| 164 | + Build and publish the package to PyPI using Poetry. |
| 165 | +
|
| 166 | + This task will: |
| 167 | + 1. Build the distribution via 'poetry publish --build'. |
| 168 | + 2. Attach to a pseudo-TTY so you can enter credentials or confirm prompts. |
| 169 | + 3. Not abort immediately if an error occurs; instead, it will print diagnostics. |
| 170 | +
|
| 171 | + :param c: Invoke context, used to run shell commands. |
| 172 | + :type c: Context |
| 173 | + """ |
| 174 | + # Run the poetry publish command. |
| 175 | + # - warn=True: do not abort on non-zero exit, so we can inspect and report. |
| 176 | + # - pty=True: allocate a pseudo-TTY for interactive prompts (username/password, etc.). |
| 177 | + result = c.run( |
| 178 | + "poetry publish --build", |
| 179 | + warn=True, |
| 180 | + pty=True, |
| 181 | + ) |
| 182 | + |
| 183 | + # If the exit code is zero, the publish succeeded. |
| 184 | + if result.ok: |
| 185 | + print("✔️ Package published successfully.") |
| 186 | + return |
| 187 | + |
| 188 | + # Otherwise, print out details to help debug. |
| 189 | + print("❌ Poetry publish failed.") |
| 190 | + print(f"Exit code: {result.exited}") |
| 191 | + if result.stderr: |
| 192 | + print("Error output:") |
| 193 | + print(result.stderr) |
| 194 | + else: |
| 195 | + print("No stderr output captured.") |
23 | 196 |
|
24 | 197 | @task |
25 | | -def clean(c): |
26 | | - c.run("rm -rf dist build docs/build .pytest_cache .mypy_cache coverage.xml .coverage stubs") |
| 198 | +def clean(c: Context) -> None: |
| 199 | + """ |
| 200 | + Remove build artifacts, caches, and generated files. |
| 201 | +
|
| 202 | + This task will: |
| 203 | + 1. Delete the 'dist' and 'build' directories. |
| 204 | + 2. Remove generated documentation in 'docs/build'. |
| 205 | + 3. Clear pytest and mypy caches. |
| 206 | + 4. Delete coverage reports and stub files. |
| 207 | +
|
| 208 | + :param c: Invoke context used to run shell commands. |
| 209 | + :raises TypeError: If 'c' is not an Invoke Context. |
| 210 | + """ |
| 211 | + # Verify the argument is an Invoke Context. |
| 212 | + if not isinstance(c, Context): |
| 213 | + raise TypeError(f"Expected Invoke Context, got {type(c).__name__!r}") |
| 214 | + |
| 215 | + # List of paths and files to remove. Adjust if you add new artifacts. |
| 216 | + targets = [ |
| 217 | + "dist", |
| 218 | + "build", |
| 219 | + "docs/build", |
| 220 | + ".pytest_cache", |
| 221 | + ".mypy_cache", |
| 222 | + "coverage.xml", |
| 223 | + ".coverage", |
| 224 | + "stubs", |
| 225 | + ] |
| 226 | + |
| 227 | + # Join targets into a single rm command. |
| 228 | + # Using '-rf' to force removal without prompts. |
| 229 | + command = f"rm -rf {' '.join(targets)}" |
| 230 | + |
| 231 | + # Execute the cleanup command. |
| 232 | + # - warn=True: capture non-zero exits without aborting Invoke. |
| 233 | + # - pty=False: no interactive input is required. |
| 234 | + result = c.run(command, warn=True, pty=False) |
| 235 | + |
| 236 | + # Report the outcome of the cleanup. |
| 237 | + if result.ok: |
| 238 | + print("✔️ Cleaned all build artifacts and caches.") |
| 239 | + else: |
| 240 | + print("❌ Cleanup failed for some targets.") |
| 241 | + print(f"Exit code: {result.exited}") |
| 242 | + if result.stderr: |
| 243 | + print("Error output:") |
| 244 | + print(result.stderr) |
27 | 245 |
|
28 | 246 | @task |
29 | | -def git_push(c): |
| 247 | +def gitpush(c: Context) -> None: |
30 | 248 | """ |
31 | | - Stage all changes, prompt for a commit message, create a signed commit, and push. |
| 249 | + Stage all changes, prompt for a commit message, create a signed commit, and push to the remote repository. |
| 250 | +
|
| 251 | + This task will: |
| 252 | + 1. Verify that 'c' is an Invoke Context. |
| 253 | + 2. Run 'git add .' to stage all unstaged changes. |
| 254 | + 3. Prompt the user for a commit message; abort if empty. |
| 255 | + 4. Sanitize the message, then run 'git commit -S -m <message>'. |
| 256 | + 5. Run 'git push' to publish commits. |
| 257 | +
|
| 258 | + :param c: Invoke Context used to run shell commands. |
| 259 | + :raises TypeError: If 'c' is not an Invoke Context. |
32 | 260 | """ |
33 | | - import getpass |
| 261 | + # Verify the argument is a valid Invoke Context. |
| 262 | + if not isinstance(c, Context): |
| 263 | + raise TypeError(f"Expected Invoke Context, got {type(c).__name__!r}") |
34 | 264 |
|
35 | | - c.run("git add .") |
| 265 | + # Stage all changes. |
| 266 | + result_add = c.run("git add .", warn=True, pty=False) |
| 267 | + if not result_add.ok: |
| 268 | + print("❌ Failed to stage changes (git add).") |
| 269 | + print(f"Exit code: {result_add.exited}") |
| 270 | + if result_add.stderr: |
| 271 | + print("Error output:") |
| 272 | + print(result_add.stderr) |
| 273 | + return |
36 | 274 |
|
37 | 275 | try: |
38 | | - # Prompt for a commit message |
| 276 | + # Prompt for a commit message. |
39 | 277 | message = input("Enter commit message: ").strip() |
40 | 278 | if not message: |
41 | 279 | print("Aborting: empty commit message.") |
42 | 280 | return |
43 | 281 |
|
| 282 | + # Sanitize the message to prevent shell injection. |
44 | 283 | sanitized_message = shlex.quote(message) |
45 | | - c.run(f"git commit -S -m {sanitized_message}") |
46 | | - c.run("git push") |
| 284 | + |
| 285 | + # Create a signed commit. Use a pseudo-TTY so GPG passphrase can be entered if needed. |
| 286 | + result_commit = c.run( |
| 287 | + f"git commit -S -m {sanitized_message}", |
| 288 | + warn=True, |
| 289 | + pty=True, |
| 290 | + ) |
| 291 | + if not result_commit.ok: |
| 292 | + print("❌ Commit failed.") |
| 293 | + print(f"Exit code: {result_commit.exited}") |
| 294 | + if result_commit.stderr: |
| 295 | + print("Error output:") |
| 296 | + print(result_commit.stderr) |
| 297 | + return |
| 298 | + |
| 299 | + # Push to the remote repository. |
| 300 | + result_push = c.run("git push", warn=True, pty=False) |
| 301 | + if result_push.ok: |
| 302 | + print("✔️ Changes pushed successfully.") |
| 303 | + else: |
| 304 | + print("❌ Push failed.") |
| 305 | + print(f"Exit code: {result_push.exited}") |
| 306 | + if result_push.stderr: |
| 307 | + print("Error output:") |
| 308 | + print(result_push.stderr) |
47 | 309 | except KeyboardInterrupt: |
48 | 310 | print("\nAborted by user.") |
49 | | - |
|
0 commit comments