codeplain 0.3.11.dev12__py3-none-any.whl → 0.3.11.dev13__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: codeplain
3
- Version: 0.3.11.dev12
3
+ Version: 0.3.11.dev13
4
4
  Summary: Transform plain language specifications into working code
5
5
  License-File: LICENSE
6
6
  Classifier: Environment :: Console
@@ -5,6 +5,7 @@ conformance_fix_journal.py,sha256=7wL3UOc3BWaUo5MCmiFbb3kIwOQ6iEpdQSyEmedo8Vk,94
5
5
  diff_utils.py,sha256=AjiQlqo5pRos_8hVXZo5yBurl5BzSrTMGrQv4dCtRCg,1198
6
6
  event_bus.py,sha256=g5XLwRfjrvTIcB6urjREuCJzZj5TA93hy8lKXLGXuCE,650
7
7
  file_utils.py,sha256=MNZ5li1ofIMOYB6FCrMa_UkX6nkGCgSZuaqzhqVc1Ok,11682
8
+ git_preflight.py,sha256=vB2A8GyYz_iWr8jOE4POFLBHr3qihmedWuidspGdsrw,1509
8
9
  git_utils.py,sha256=gCUA6_9zg19f6k_0aVcRnIVlAJSBD0Vt6takx-0pKME,16547
9
10
  memory_management.py,sha256=CwIOQqF7Xo1sNfvP69zQRK_CpjinQD9dbolhRPl6lTk,2446
10
11
  metadata_utils.py,sha256=YO17o7IAma5TT1ZN0pTwrLVr_KxFQ8APv02KqKPpI5k,1641
@@ -23,7 +24,7 @@ plain2code_state.py,sha256=uePQlC29Pbhm1yXfdCFebONYdy6jo13gTTmXSwC11cQ,3168
23
24
  plain2code_telemetry.py,sha256=e4UCAoFklJMDWGQsL50i7nTLiOBy8gs86A0XO4WLgPo,4597
24
25
  plain2code_utils.py,sha256=unOxuqSQRpwG8YF6uoQhnZ3ZSTJRIUCPLdJ-1lRZR5M,1534
25
26
  plain_file.py,sha256=Kn8m_w6c04_RR-FBjAU05UcLOTDk4zcctpJXghYdZ8I,35757
26
- plain_modules.py,sha256=kUGR2mX908FEKOO8b2CJuk2rn9iRj8qyWGFMWTdfzoY,25977
27
+ plain_modules.py,sha256=nXlmw0J2rKkuU1Y3A68LVtZ1Ffdc-D5iMjxaXSaSHTQ,25963
27
28
  plain_spec.py,sha256=7nb5lLJKHZ3uu9THkSXyJ5brIKkbGW0ooVfRclLdlzA,16062
28
29
  system_config.py,sha256=sA8wiPmBMtRsLwSQ40U5pVdLazAafbV-1Z19Yp0HiSQ,5314
29
30
  usage_summary.py,sha256=MA4WcVaAwz6IWCN4k_rgxVcwO4I157wN7BZlFi3h0R4,1282
@@ -79,6 +80,7 @@ tests/test_dry_run_link_validation.py,sha256=ivvYXQPqUkQ8glbFyUrdCninCxF2mg-rAsD
79
80
  tests/test_file_utils.py,sha256=R_ZIhrTtwlhHxgmbBtA8PFEU5-QB4Fi5skJ7PS2KzdU,3614
80
81
  tests/test_fix_unit_tests_conformance_context.py,sha256=IEsXK0xMLRuHmoQGBwrY0ENWVng2n-db_YzSO2qBfqM,9136
81
82
  tests/test_folder_path_resolution.py,sha256=K9v684xkPE0HOAsovg8uDixpZmaIk7ho-Wd3rTjKLF0,6560
83
+ tests/test_git_preflight.py,sha256=6aQqInDzpk03pS-6UfkGQWjErrIXTEdHs5oeq3ZYzTw,3186
82
84
  tests/test_git_utils.py,sha256=h3rWhhs4XVRYqzVzdmj51siF_AYD-GkbZiefK2iGfWo,15614
83
85
  tests/test_headless_logging.py,sha256=soNESeotKQaU-SQyxtjpq35l-dLVsauZ_lJ-wk9i2mI,6419
84
86
  tests/test_imports.py,sha256=1ncbqQhtQW237kIjyV-Ufl7-R-PuduvoYFj2iS-DXW4,2608
@@ -114,8 +116,8 @@ tui/spinner.py,sha256=Ro6Gd9Przf-whuHqPRY6HwI0T57yJjyNPbhDbigZKZE,2471
114
116
  tui/state_handlers.py,sha256=zbUS_D9eU8qIwVpaxPEp9zL9bpg8gXsWNoUaqg9TLrQ,16664
115
117
  tui/styles.css,sha256=kiC7Og_2G1E7owoiZgV95-3_dSeQnsNj3qzaY04xkDg,6727
116
118
  tui/widget_helpers.py,sha256=jitm2WHiKj-NVFL-hULxWzckPreRbUTl9OIZLN31-ek,6997
117
- codeplain-0.3.11.dev12.dist-info/METADATA,sha256=PFNQMSVAbaU9mXzrGebpjoy7xkwPpXz_7mC9kQdtSVs,7257
118
- codeplain-0.3.11.dev12.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
119
- codeplain-0.3.11.dev12.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
120
- codeplain-0.3.11.dev12.dist-info/licenses/LICENSE,sha256=pCeKgQ1mXE5OmNUuKOfOZh1T1vqkeCUxghE7N8qgnzc,11345
121
- codeplain-0.3.11.dev12.dist-info/RECORD,,
119
+ codeplain-0.3.11.dev13.dist-info/METADATA,sha256=KVfLUeUZa4B9EB2D8iLyUz7RqGDLrDGSNrGCDwp1MIE,7257
120
+ codeplain-0.3.11.dev13.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
121
+ codeplain-0.3.11.dev13.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
122
+ codeplain-0.3.11.dev13.dist-info/licenses/LICENSE,sha256=pCeKgQ1mXE5OmNUuKOfOZh1T1vqkeCUxghE7N8qgnzc,11345
123
+ codeplain-0.3.11.dev13.dist-info/RECORD,,
git_preflight.py ADDED
@@ -0,0 +1,41 @@
1
+ """Verify a working git is available before GitPython is imported.
2
+
3
+ GitPython probes for the git executable on import and raises from module scope
4
+ if it is missing or broken, so the CLI would crash with a traceback before
5
+ main() could explain it.
6
+ """
7
+
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+
12
+ from plain2code_console import console
13
+
14
+ GIT_MISSING_MESSAGE = "git is not installed. Please install git and try again."
15
+ GIT_BROKEN_MESSAGE = "git is installed but not working. Please repair your git installation and try again."
16
+ MACOS_BROKEN_GIT_HINT = (
17
+ "This usually means the Command Line Tools are missing; install them with 'xcode-select --install'."
18
+ )
19
+
20
+
21
+ def git_runs(git_path):
22
+ """Whether `git version` succeeds. A git on PATH can still be unusable: on
23
+ macOS without the Command Line Tools /usr/bin/git is a stub that exits with
24
+ an xcrun error, and GIT_PYTHON_REFRESH=quiet does not suppress that."""
25
+ try:
26
+ return subprocess.run([git_path, "version"], capture_output=True, timeout=10).returncode == 0
27
+ except (OSError, subprocess.SubprocessError):
28
+ return False
29
+
30
+
31
+ def require_git():
32
+ """Exit with a clean message unless a working git is available."""
33
+ git_path = shutil.which("git")
34
+ if git_path is None:
35
+ console.error(f"{GIT_MISSING_MESSAGE}\n")
36
+ sys.exit(1)
37
+
38
+ if not git_runs(git_path):
39
+ hint = f"\n{MACOS_BROKEN_GIT_HINT}" if sys.platform == "darwin" else ""
40
+ console.error(f"{GIT_BROKEN_MESSAGE}{hint}\n")
41
+ sys.exit(1)
plain_modules.py CHANGED
@@ -6,18 +6,18 @@ import tempfile
6
6
  import zipfile
7
7
  from functools import cached_property
8
8
 
9
+ from git_preflight import require_git
9
10
  from plain2code_exceptions import (
10
- GitNotInstalledError,
11
11
  InvalidModuleArchiveError,
12
12
  MissingPreviousFunctionalitiesError,
13
13
  ModuleDoesNotExistError,
14
14
  )
15
15
 
16
- try:
17
- from git import Repo
18
- from git.exc import NoSuchPathError
19
- except ImportError:
20
- raise GitNotInstalledError("git is not installed. Please install git and try again.")
16
+ # Must run before GitPython is imported, which raises on a missing or broken git.
17
+ require_git()
18
+
19
+ from git import Repo
20
+ from git.exc import NoSuchPathError
21
21
 
22
22
  import file_utils
23
23
  import git_utils
@@ -0,0 +1,98 @@
1
+ """Regression tests for issue #133.
2
+
3
+ When git is unusable the CLI must report a clean message and exit 1, never dump
4
+ a traceback. The failure is at import time (GitPython raises from module scope
5
+ before main() runs), so the CLI is run in a subprocess and asserted on what a
6
+ user would actually see. sys.executable is launched by absolute path, so
7
+ replacing PATH only controls which git, if any, can be found.
8
+ """
9
+
10
+ import os
11
+ import re
12
+ import subprocess
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ import pytest
17
+
18
+ from git_preflight import GIT_BROKEN_MESSAGE, GIT_MISSING_MESSAGE
19
+
20
+ REPO_ROOT = Path(__file__).resolve().parents[1]
21
+ TRACEBACK_MARKER = "Traceback (most recent call last)"
22
+
23
+ # The crash was in the import chain, so it hit every entry point equally.
24
+ CLI_INVOCATIONS = [["--version"], ["--status"], ["does-not-exist.plain"]]
25
+
26
+
27
+ def run_cli(args, path_dir=None):
28
+ """Run the CLI and return its exit code and output, stripped of styling and
29
+ with whitespace collapsed, so assertions survive console line wrapping."""
30
+ env = os.environ.copy()
31
+ # Drop GitPython overrides so the PATH search decides whether git is found.
32
+ env.pop("GIT_PYTHON_GIT_EXECUTABLE", None)
33
+ env.pop("GIT_PYTHON_REFRESH", None)
34
+ env.setdefault("CODEPLAIN_API_KEY", "dummy")
35
+ if path_dir is not None:
36
+ env["PATH"] = str(path_dir)
37
+
38
+ result = subprocess.run(
39
+ [sys.executable, "plain2code.py", *args],
40
+ cwd=REPO_ROOT,
41
+ env=env,
42
+ capture_output=True,
43
+ text=True,
44
+ timeout=120,
45
+ )
46
+ output = re.sub(r"\x1b\[[0-9;]*m", "", result.stdout + result.stderr)
47
+ return result.returncode, " ".join(output.split())
48
+
49
+
50
+ @pytest.fixture
51
+ def no_git_dir(tmp_path):
52
+ """A directory containing no git at all, used as the whole PATH."""
53
+ d = tmp_path / "no-git"
54
+ d.mkdir()
55
+ return d
56
+
57
+
58
+ @pytest.fixture
59
+ def broken_git_dir(tmp_path):
60
+ """A directory whose git resolves but always fails, used as the whole PATH.
61
+ This is the shape of macOS without the Command Line Tools."""
62
+ d = tmp_path / "broken-git"
63
+ d.mkdir()
64
+ if sys.platform == "win32":
65
+ (d / "git.bat").write_text("@echo off\nexit /b 127\n")
66
+ else:
67
+ git = d / "git"
68
+ git.write_text("#!/bin/sh\nexit 127\n")
69
+ git.chmod(0o755)
70
+ return d
71
+
72
+
73
+ @pytest.mark.parametrize("args", CLI_INVOCATIONS)
74
+ def test_missing_git_reports_cleanly(args, no_git_dir):
75
+ rc, output = run_cli(args, no_git_dir)
76
+
77
+ assert GIT_MISSING_MESSAGE in output, output
78
+ assert TRACEBACK_MARKER not in output, output
79
+ # GitPython's own advice about GIT_PYTHON_REFRESH must not reach the user.
80
+ assert "GIT_PYTHON_REFRESH" not in output, output
81
+ assert rc == 1, output
82
+
83
+
84
+ @pytest.mark.parametrize("args", CLI_INVOCATIONS)
85
+ def test_broken_git_reports_cleanly(args, broken_git_dir):
86
+ rc, output = run_cli(args, broken_git_dir)
87
+
88
+ assert GIT_BROKEN_MESSAGE in output, output
89
+ assert GIT_MISSING_MESSAGE not in output, output
90
+ assert TRACEBACK_MARKER not in output, output
91
+ assert rc == 1, output
92
+
93
+
94
+ def test_cli_works_when_git_is_present():
95
+ rc, output = run_cli(["--version"])
96
+
97
+ assert rc == 0, output
98
+ assert "codeplain version" in output, output