pdd-cli 0.0.43__py3-none-any.whl → 0.0.45__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.
- pdd/__init__.py +1 -1
- pdd/cli.py +2 -2
- pdd/cmd_test_main.py +9 -0
- pdd/data/language_format.csv +1 -0
- pdd/data/llm_model.csv +2 -2
- pdd/fix_code_loop.py +2 -2
- pdd/fix_error_loop.py +5 -2
- pdd/fix_verification_errors_loop.py +14 -1
- pdd/fix_verification_main.py +29 -8
- pdd/get_jwt_token.py +39 -7
- pdd/increase_tests.py +7 -0
- pdd/llm_invoke.py +9 -7
- pdd/prompts/auto_include_LLM.prompt +811 -9
- pdd/prompts/change_LLM.prompt +1 -3
- pdd/prompts/detect_change_LLM.prompt +460 -3
- pdd/prompts/extract_program_code_fix_LLM.prompt +7 -1
- pdd/prompts/fix_code_module_errors_LLM.prompt +13 -3
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +0 -43
- pdd/prompts/increase_tests_LLM.prompt +4 -1
- pdd/prompts/insert_includes_LLM.prompt +1061 -6
- pdd/prompts/split_LLM.prompt +1 -62
- pdd/prompts/xml_convertor_LLM.prompt +3246 -7
- pdd/pytest_output.py +72 -20
- pdd/python_env_detector.py +151 -0
- pdd/summarize_directory.py +7 -1
- pdd/sync_determine_operation.py +396 -109
- pdd/sync_main.py +1 -1
- pdd/sync_orchestration.py +448 -28
- {pdd_cli-0.0.43.dist-info → pdd_cli-0.0.45.dist-info}/METADATA +4 -4
- {pdd_cli-0.0.43.dist-info → pdd_cli-0.0.45.dist-info}/RECORD +26 -25
- {pdd_cli-0.0.43.dist-info → pdd_cli-0.0.45.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.43.dist-info → pdd_cli-0.0.45.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.43.dist-info → pdd_cli-0.0.45.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.43.dist-info → pdd_cli-0.0.45.dist-info}/top_level.txt +0 -0
pdd/__init__.py
CHANGED
pdd/cli.py
CHANGED
|
@@ -362,7 +362,7 @@ def example(
|
|
|
362
362
|
"--target-coverage",
|
|
363
363
|
type=click.FloatRange(0.0, 100.0),
|
|
364
364
|
default=None, # Use None, default handled in cmd_test_main or env var
|
|
365
|
-
help="Desired code coverage percentage (default:
|
|
365
|
+
help="Desired code coverage percentage (default: 10.0 or PDD_TEST_COVERAGE_TARGET).",
|
|
366
366
|
)
|
|
367
367
|
@click.option(
|
|
368
368
|
"--merge",
|
|
@@ -1130,7 +1130,7 @@ def verify(
|
|
|
1130
1130
|
@click.option(
|
|
1131
1131
|
"--target-coverage",
|
|
1132
1132
|
type=click.FloatRange(0.0, 100.0),
|
|
1133
|
-
default=
|
|
1133
|
+
default=10.0,
|
|
1134
1134
|
show_default=True,
|
|
1135
1135
|
help="Target code coverage percentage for generated tests.",
|
|
1136
1136
|
)
|
pdd/cmd_test_main.py
CHANGED
|
@@ -155,6 +155,15 @@ def cmd_test_main(
|
|
|
155
155
|
print("[bold red]Error: Output file path could not be determined.[/bold red]")
|
|
156
156
|
ctx.exit(1)
|
|
157
157
|
return "", 0.0, ""
|
|
158
|
+
|
|
159
|
+
# Check if unit_test content is empty
|
|
160
|
+
if not unit_test or not unit_test.strip():
|
|
161
|
+
print(f"[bold red]Error: Generated unit test content is empty or whitespace-only.[/bold red]")
|
|
162
|
+
print(f"[bold yellow]Debug: unit_test length: {len(unit_test) if unit_test else 0}[/bold yellow]")
|
|
163
|
+
print(f"[bold yellow]Debug: unit_test content preview: {repr(unit_test[:100]) if unit_test else 'None'}[/bold yellow]")
|
|
164
|
+
ctx.exit(1)
|
|
165
|
+
return "", 0.0, ""
|
|
166
|
+
|
|
158
167
|
try:
|
|
159
168
|
with open(output_file, "w", encoding="utf-8") as file_handle:
|
|
160
169
|
file_handle.write(unit_test)
|
pdd/data/language_format.csv
CHANGED
pdd/data/llm_model.csv
CHANGED
|
@@ -3,8 +3,8 @@ OpenAI,gpt-4.1-nano,0.1,0.4,1249,,OPENAI_API_KEY,0,True,none
|
|
|
3
3
|
xai,xai/grok-3-beta,3.0,15.0,1332,https://api.x.ai/v1,XAI_API_KEY,0,False,none
|
|
4
4
|
Anthropic,claude-3-5-haiku-20241022,.8,4,1261,,ANTHROPIC_API_KEY,0,True,none
|
|
5
5
|
OpenAI,deepseek/deepseek-chat,.27,1.1,1353,https://api.deepseek.com/beta,DEEPSEEK_API_KEY,0,False,none
|
|
6
|
-
Google,vertex_ai/gemini-2.5-flash
|
|
7
|
-
Google,gemini-2.5-pro
|
|
6
|
+
Google,vertex_ai/gemini-2.5-flash,0.15,0.6,1330,,VERTEX_CREDENTIALS,0,True,effort
|
|
7
|
+
Google,gemini-2.5-pro,1.25,10.0,1360,,GOOGLE_API_KEY,0,True,none
|
|
8
8
|
Anthropic,claude-sonnet-4-20250514,3.0,15.0,1340,,ANTHROPIC_API_KEY,64000,True,budget
|
|
9
9
|
Google,vertex_ai/gemini-2.5-pro,1.25,10.0,1361,,VERTEX_CREDENTIALS,0,True,none
|
|
10
10
|
OpenAI,o4-mini,1.1,4.4,1333,,OPENAI_API_KEY,0,True,effort
|
pdd/fix_code_loop.py
CHANGED
|
@@ -3,7 +3,7 @@ import shutil
|
|
|
3
3
|
import subprocess
|
|
4
4
|
import sys
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Tuple
|
|
6
|
+
from typing import Tuple, Optional, Union
|
|
7
7
|
from . import DEFAULT_TIME # Added DEFAULT_TIME
|
|
8
8
|
|
|
9
9
|
# Use Rich for pretty printing to the console
|
|
@@ -38,7 +38,7 @@ def fix_code_loop(
|
|
|
38
38
|
error_log_file: str,
|
|
39
39
|
verbose: bool = False,
|
|
40
40
|
time: float = DEFAULT_TIME,
|
|
41
|
-
) -> Tuple[bool, str, str, int, float, str
|
|
41
|
+
) -> Tuple[bool, str, str, int, float, Optional[str]]:
|
|
42
42
|
"""
|
|
43
43
|
Attempts to fix errors in a code module through multiple iterations.
|
|
44
44
|
|
pdd/fix_error_loop.py
CHANGED
|
@@ -12,6 +12,7 @@ from rich.console import Console
|
|
|
12
12
|
# Relative import from an internal module.
|
|
13
13
|
from .fix_errors_from_unit_tests import fix_errors_from_unit_tests
|
|
14
14
|
from . import DEFAULT_TIME # Import DEFAULT_TIME
|
|
15
|
+
from .python_env_detector import detect_host_python_executable
|
|
15
16
|
|
|
16
17
|
console = Console()
|
|
17
18
|
|
|
@@ -26,7 +27,9 @@ def run_pytest_on_file(test_file: str) -> tuple[int, int, int, str]:
|
|
|
26
27
|
"""
|
|
27
28
|
try:
|
|
28
29
|
# Include "--json-only" to ensure only valid JSON is printed.
|
|
29
|
-
|
|
30
|
+
# Use environment-aware Python executable for pytest execution
|
|
31
|
+
python_executable = detect_host_python_executable()
|
|
32
|
+
cmd = [python_executable, "-m", "pdd.pytest_output", "--json-only", test_file]
|
|
30
33
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
31
34
|
|
|
32
35
|
# Parse the JSON output from stdout
|
|
@@ -380,7 +383,7 @@ def fix_error_loop(unit_test_file: str,
|
|
|
380
383
|
|
|
381
384
|
# Run the verification:
|
|
382
385
|
try:
|
|
383
|
-
verify_cmd = [
|
|
386
|
+
verify_cmd = [detect_host_python_executable(), verification_program]
|
|
384
387
|
verify_result = subprocess.run(verify_cmd, capture_output=True, text=True)
|
|
385
388
|
# Safely handle None for stdout or stderr:
|
|
386
389
|
verify_stdout = verify_result.stdout or ""
|
|
@@ -2,6 +2,7 @@ import os
|
|
|
2
2
|
import shutil
|
|
3
3
|
import subprocess
|
|
4
4
|
import datetime
|
|
5
|
+
import sys
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Dict, Tuple, Any, Optional
|
|
7
8
|
from xml.sax.saxutils import escape
|
|
@@ -25,6 +26,7 @@ except ImportError:
|
|
|
25
26
|
)
|
|
26
27
|
|
|
27
28
|
from . import DEFAULT_TIME # Import DEFAULT_TIME
|
|
29
|
+
from .python_env_detector import detect_host_python_executable
|
|
28
30
|
|
|
29
31
|
# Initialize Rich Console for pretty printing
|
|
30
32
|
console = Console()
|
|
@@ -49,19 +51,30 @@ def _run_program(
|
|
|
49
51
|
if not program_path.is_file():
|
|
50
52
|
return -1, f"Error: Program file not found at {program_path}"
|
|
51
53
|
|
|
52
|
-
command = [
|
|
54
|
+
command = [detect_host_python_executable(), str(program_path)]
|
|
53
55
|
if args:
|
|
54
56
|
command.extend(args)
|
|
55
57
|
|
|
56
58
|
try:
|
|
59
|
+
# Run from staging root directory instead of examples/ directory
|
|
60
|
+
# This allows imports from both pdd/ and examples/ subdirectories
|
|
61
|
+
staging_root = program_path.parent.parent # Go up from examples/ to staging root
|
|
62
|
+
|
|
57
63
|
result = subprocess.run(
|
|
58
64
|
command,
|
|
59
65
|
capture_output=True,
|
|
60
66
|
text=True,
|
|
61
67
|
timeout=timeout,
|
|
62
68
|
check=False, # Don't raise exception for non-zero exit codes
|
|
69
|
+
env=os.environ.copy(), # Pass current environment variables
|
|
70
|
+
cwd=staging_root # Set working directory to staging root
|
|
63
71
|
)
|
|
64
72
|
combined_output = result.stdout + result.stderr
|
|
73
|
+
|
|
74
|
+
# Check for syntax errors
|
|
75
|
+
if result.returncode != 0 and "SyntaxError" in result.stderr:
|
|
76
|
+
return result.returncode, f"SYNTAX_ERROR: {combined_output}"
|
|
77
|
+
|
|
65
78
|
return result.returncode, combined_output
|
|
66
79
|
except FileNotFoundError:
|
|
67
80
|
return -1, f"Error: Python interpreter not found or '{program_path}' not found."
|
pdd/fix_verification_main.py
CHANGED
|
@@ -17,6 +17,7 @@ from .fix_verification_errors import fix_verification_errors
|
|
|
17
17
|
from .fix_verification_errors_loop import fix_verification_errors_loop
|
|
18
18
|
# Import DEFAULT_STRENGTH from the main package
|
|
19
19
|
from . import DEFAULT_STRENGTH, DEFAULT_TIME
|
|
20
|
+
from .python_env_detector import detect_host_python_executable
|
|
20
21
|
|
|
21
22
|
# Default values from the README
|
|
22
23
|
DEFAULT_TEMPERATURE = 0.0
|
|
@@ -48,7 +49,7 @@ def run_program(program_path: str, args: List[str] = []) -> Tuple[bool, str, str
|
|
|
48
49
|
# A more robust solution might use the 'language' from construct_paths
|
|
49
50
|
interpreter = []
|
|
50
51
|
if program_path.endswith(".py"):
|
|
51
|
-
interpreter = [
|
|
52
|
+
interpreter = [detect_host_python_executable()] # Use environment-aware Python executable
|
|
52
53
|
elif program_path.endswith(".js"):
|
|
53
54
|
interpreter = ["node"]
|
|
54
55
|
elif program_path.endswith(".sh"):
|
|
@@ -57,13 +58,21 @@ def run_program(program_path: str, args: List[str] = []) -> Tuple[bool, str, str
|
|
|
57
58
|
|
|
58
59
|
command = interpreter + [program_path] + args
|
|
59
60
|
rich_print(f"[dim]Running command:[/dim] {' '.join(command)}")
|
|
61
|
+
rich_print(f"[dim]Working directory:[/dim] {os.path.dirname(program_path) if program_path else 'None'}")
|
|
62
|
+
rich_print(f"[dim]Environment PYTHONPATH:[/dim] {os.environ.get('PYTHONPATH', 'Not set')}")
|
|
60
63
|
|
|
64
|
+
# Create a copy of environment with PYTHONUNBUFFERED set
|
|
65
|
+
env = os.environ.copy()
|
|
66
|
+
env['PYTHONUNBUFFERED'] = '1' # Force unbuffered output
|
|
67
|
+
|
|
61
68
|
process = subprocess.run(
|
|
62
69
|
command,
|
|
63
70
|
capture_output=True,
|
|
64
71
|
text=True,
|
|
65
72
|
check=False, # Don't raise exception on non-zero exit code
|
|
66
|
-
timeout=60 # Add a timeout to prevent hangs
|
|
73
|
+
timeout=60, # Add a timeout to prevent hangs
|
|
74
|
+
env=env, # Pass modified environment variables
|
|
75
|
+
cwd=os.path.dirname(program_path) if program_path else None # Set working directory
|
|
67
76
|
)
|
|
68
77
|
|
|
69
78
|
success = process.returncode == 0
|
|
@@ -71,11 +80,17 @@ def run_program(program_path: str, args: List[str] = []) -> Tuple[bool, str, str
|
|
|
71
80
|
stderr = process.stderr
|
|
72
81
|
|
|
73
82
|
if not success:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
rich_print(f"[yellow]Warning:[/yellow] Program '{os.path.basename(program_path)}' exited with code {process.returncode}.")
|
|
84
|
+
|
|
85
|
+
# Check for syntax errors specifically
|
|
86
|
+
if "SyntaxError" in stderr:
|
|
87
|
+
rich_print("[bold red]Syntax Error Detected:[/bold red]")
|
|
88
|
+
rich_print(Panel(stderr, border_style="red", title="Python Syntax Error"))
|
|
89
|
+
# Return with special indicator for syntax errors
|
|
90
|
+
return False, stdout, f"SYNTAX_ERROR: {stderr}"
|
|
91
|
+
elif stderr:
|
|
92
|
+
rich_print("[yellow]Stderr:[/yellow]")
|
|
93
|
+
rich_print(Panel(stderr, border_style="yellow"))
|
|
79
94
|
|
|
80
95
|
return success, stdout, stderr
|
|
81
96
|
|
|
@@ -354,7 +369,13 @@ def fix_verification_main(
|
|
|
354
369
|
results_log_content += f"Model Used: {model_name}\n"
|
|
355
370
|
results_log_content += f"Total Cost: ${total_cost:.6f}\n"
|
|
356
371
|
results_log_content += "\n--- LLM Explanation ---\n"
|
|
357
|
-
|
|
372
|
+
# The original code here was:
|
|
373
|
+
# results_log_content += "\n".join(fix_results.get('explanation', ['N/A']))
|
|
374
|
+
# This was incorrect because fix_results['explanation'] is a single string.
|
|
375
|
+
# The list constructor would then iterate through it character-by-character,
|
|
376
|
+
# causing the single-character-per-line output.
|
|
377
|
+
# The fix is to just append the string directly, using a default value if it is None.
|
|
378
|
+
results_log_content += fix_results.get('explanation') or 'N/A'
|
|
358
379
|
results_log_content += "\n\n--- Program Output Used for Verification ---\n"
|
|
359
380
|
results_log_content += program_output
|
|
360
381
|
|
pdd/get_jwt_token.py
CHANGED
|
@@ -2,7 +2,20 @@ import asyncio
|
|
|
2
2
|
import time
|
|
3
3
|
from typing import Dict, Optional, Tuple
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
# Cross-platform keyring import with fallback for WSL compatibility
|
|
6
|
+
try:
|
|
7
|
+
import keyring
|
|
8
|
+
KEYRING_AVAILABLE = True
|
|
9
|
+
except ImportError:
|
|
10
|
+
try:
|
|
11
|
+
import keyrings.alt.file
|
|
12
|
+
keyring = keyrings.alt.file.PlaintextKeyring()
|
|
13
|
+
KEYRING_AVAILABLE = True
|
|
14
|
+
print("Warning: Using alternative keyring (PlaintextKeyring) - tokens stored in plaintext")
|
|
15
|
+
except ImportError:
|
|
16
|
+
keyring = None
|
|
17
|
+
KEYRING_AVAILABLE = False
|
|
18
|
+
print("Warning: No keyring available - token storage disabled")
|
|
6
19
|
import requests
|
|
7
20
|
|
|
8
21
|
# Custom exception classes for better error handling
|
|
@@ -128,20 +141,39 @@ class FirebaseAuthenticator:
|
|
|
128
141
|
|
|
129
142
|
def _store_refresh_token(self, refresh_token: str):
|
|
130
143
|
"""Stores the Firebase refresh token in the system keyring."""
|
|
131
|
-
keyring
|
|
144
|
+
if not KEYRING_AVAILABLE or keyring is None:
|
|
145
|
+
print("Warning: No keyring available, refresh token not stored")
|
|
146
|
+
return
|
|
147
|
+
try:
|
|
148
|
+
keyring.set_password(self.keyring_service_name, self.keyring_user_name, refresh_token)
|
|
149
|
+
except Exception as e:
|
|
150
|
+
print(f"Warning: Failed to store refresh token in keyring: {e}")
|
|
132
151
|
|
|
133
152
|
def _get_stored_refresh_token(self) -> Optional[str]:
|
|
134
153
|
"""Retrieves the Firebase refresh token from the system keyring."""
|
|
135
|
-
|
|
154
|
+
if not KEYRING_AVAILABLE or keyring is None:
|
|
155
|
+
return None
|
|
156
|
+
try:
|
|
157
|
+
return keyring.get_password(self.keyring_service_name, self.keyring_user_name)
|
|
158
|
+
except Exception as e:
|
|
159
|
+
print(f"Warning: Failed to retrieve refresh token from keyring: {e}")
|
|
160
|
+
return None
|
|
136
161
|
|
|
137
162
|
def _delete_stored_refresh_token(self):
|
|
138
163
|
"""Deletes the stored Firebase refresh token from the keyring."""
|
|
164
|
+
if not KEYRING_AVAILABLE or keyring is None:
|
|
165
|
+
print("No keyring available. Token deletion skipped.")
|
|
166
|
+
return
|
|
139
167
|
try:
|
|
140
168
|
keyring.delete_password(self.keyring_service_name, self.keyring_user_name)
|
|
141
|
-
except
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
169
|
+
except Exception as e:
|
|
170
|
+
# Handle both keyring.errors and generic exceptions for cross-platform compatibility
|
|
171
|
+
if "NoKeyringError" in str(type(e)) or "no keyring" in str(e).lower():
|
|
172
|
+
print("No keyring found. Token deletion skipped.")
|
|
173
|
+
elif "PasswordDeleteError" in str(type(e)) or "delete" in str(e).lower():
|
|
174
|
+
print("Failed to delete token from keyring.")
|
|
175
|
+
else:
|
|
176
|
+
print(f"Warning: Error deleting token from keyring: {e}")
|
|
145
177
|
|
|
146
178
|
async def _refresh_firebase_token(self, refresh_token: str) -> str:
|
|
147
179
|
"""
|
pdd/increase_tests.py
CHANGED
|
@@ -78,6 +78,13 @@ def increase_tests(
|
|
|
78
78
|
time=time,
|
|
79
79
|
verbose=verbose
|
|
80
80
|
)
|
|
81
|
+
|
|
82
|
+
# Debug: Check LLM response
|
|
83
|
+
console.print(f"[blue]DEBUG increase_tests: LLM response type: {type(llm_response)}[/blue]")
|
|
84
|
+
console.print(f"[blue]DEBUG increase_tests: LLM response keys: {llm_response.keys() if isinstance(llm_response, dict) else 'Not a dict'}[/blue]")
|
|
85
|
+
console.print(f"[blue]DEBUG increase_tests: LLM result type: {type(llm_response.get('result', 'No result key'))}[/blue]")
|
|
86
|
+
console.print(f"[blue]DEBUG increase_tests: LLM result length: {len(llm_response['result']) if 'result' in llm_response and llm_response['result'] else 0}[/blue]")
|
|
87
|
+
console.print(f"[blue]DEBUG increase_tests: LLM result preview: {repr(llm_response['result'][:300]) if 'result' in llm_response and llm_response['result'] else 'Empty or no result'}[/blue]")
|
|
81
88
|
|
|
82
89
|
increase_test_function, total_cost, model_name = postprocess(
|
|
83
90
|
llm_response['result'],
|
pdd/llm_invoke.py
CHANGED
|
@@ -79,7 +79,11 @@ import time as time_module # Alias to avoid conflict with 'time' parameter
|
|
|
79
79
|
from pdd import DEFAULT_LLM_MODEL
|
|
80
80
|
|
|
81
81
|
# Opt-in to future pandas behavior regarding downcasting
|
|
82
|
-
|
|
82
|
+
try:
|
|
83
|
+
pd.set_option('future.no_silent_downcasting', True)
|
|
84
|
+
except pd._config.config.OptionError:
|
|
85
|
+
# Skip if option doesn't exist in older pandas versions
|
|
86
|
+
pass
|
|
83
87
|
|
|
84
88
|
|
|
85
89
|
def _is_wsl_environment() -> bool:
|
|
@@ -152,8 +156,8 @@ if PDD_PATH_ENV:
|
|
|
152
156
|
|
|
153
157
|
if PROJECT_ROOT is None: # If PDD_PATH wasn't set or was invalid
|
|
154
158
|
try:
|
|
155
|
-
# Start from the directory
|
|
156
|
-
current_dir = Path(
|
|
159
|
+
# Start from the current working directory (where user is running PDD)
|
|
160
|
+
current_dir = Path.cwd().resolve()
|
|
157
161
|
# Look for project markers (e.g., .git, pyproject.toml, data/, .env)
|
|
158
162
|
# Go up a maximum of 5 levels to prevent infinite loops
|
|
159
163
|
for _ in range(5):
|
|
@@ -164,7 +168,7 @@ if PROJECT_ROOT is None: # If PDD_PATH wasn't set or was invalid
|
|
|
164
168
|
|
|
165
169
|
if has_git or has_pyproject or has_data or has_dotenv:
|
|
166
170
|
PROJECT_ROOT = current_dir
|
|
167
|
-
logger.debug(f"Determined PROJECT_ROOT by marker search: {PROJECT_ROOT}")
|
|
171
|
+
logger.debug(f"Determined PROJECT_ROOT by marker search from CWD: {PROJECT_ROOT}")
|
|
168
172
|
break
|
|
169
173
|
|
|
170
174
|
parent_dir = current_dir.parent
|
|
@@ -172,10 +176,8 @@ if PROJECT_ROOT is None: # If PDD_PATH wasn't set or was invalid
|
|
|
172
176
|
break
|
|
173
177
|
current_dir = parent_dir
|
|
174
178
|
|
|
175
|
-
except NameError: # __file__ might not be defined (e.g., interactive session)
|
|
176
|
-
warnings.warn("__file__ not defined. Cannot automatically detect project root from script location.")
|
|
177
179
|
except Exception as e: # Catch potential permission errors etc.
|
|
178
|
-
warnings.warn(f"Error during project root auto-detection: {e}")
|
|
180
|
+
warnings.warn(f"Error during project root auto-detection from current working directory: {e}")
|
|
179
181
|
|
|
180
182
|
if PROJECT_ROOT is None: # Fallback to CWD if no method succeeded
|
|
181
183
|
PROJECT_ROOT = Path.cwd().resolve()
|