pysfi 0.1.13__py3-none-any.whl → 0.1.14__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.
- {pysfi-0.1.13.dist-info → pysfi-0.1.14.dist-info}/METADATA +1 -1
- {pysfi-0.1.13.dist-info → pysfi-0.1.14.dist-info}/RECORD +29 -31
- {pysfi-0.1.13.dist-info → pysfi-0.1.14.dist-info}/entry_points.txt +1 -0
- sfi/__init__.py +20 -5
- sfi/alarmclock/__init__.py +3 -3
- sfi/bumpversion/__init__.py +5 -5
- sfi/bumpversion/bumpversion.py +64 -15
- sfi/cleanbuild/__init__.py +3 -3
- sfi/cleanbuild/cleanbuild.py +5 -1
- sfi/cli.py +13 -2
- sfi/condasetup/__init__.py +1 -1
- sfi/condasetup/condasetup.py +91 -76
- sfi/docdiff/__init__.py +1 -1
- sfi/docdiff/docdiff.py +3 -2
- sfi/docscan/__init__.py +3 -3
- sfi/docscan/docscan.py +78 -23
- sfi/docscan/docscan_gui.py +5 -5
- sfi/filedate/filedate.py +12 -5
- sfi/img2pdf/img2pdf.py +5 -5
- sfi/llmquantize/llmquantize.py +44 -33
- sfi/llmserver/__init__.py +1 -1
- sfi/makepython/makepython.py +880 -319
- sfi/pdfsplit/pdfsplit.py +45 -12
- sfi/pyarchive/__init__.py +1 -1
- sfi/pylibpack/pylibpack.py +5 -13
- sfi/pypack/pypack.py +127 -105
- sfi/pyprojectparse/pyprojectparse.py +11 -14
- sfi/pysourcepack/__init__.py +1 -1
- sfi/workflowengine/__init__.py +0 -0
- sfi/workflowengine/workflowengine.py +0 -547
- {pysfi-0.1.13.dist-info → pysfi-0.1.14.dist-info}/WHEEL +0 -0
sfi/makepython/makepython.py
CHANGED
|
@@ -2,22 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
This module provides a command-line interface for common Python development tasks
|
|
4
4
|
including building, testing, publishing, and version management.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Automatic build tool detection (uv, poetry, hatch)
|
|
8
|
+
- Project configuration management
|
|
9
|
+
- Command execution with proper error handling
|
|
10
|
+
- PyPI token management
|
|
11
|
+
- Clean build artifacts
|
|
12
|
+
- Test execution with various configurations
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
>>> # Build project
|
|
16
|
+
>>> makepython build
|
|
17
|
+
>>>
|
|
18
|
+
>>> # Run tests
|
|
19
|
+
>>> makepython test
|
|
20
|
+
>>>
|
|
21
|
+
>>> # Publish to PyPI
|
|
22
|
+
>>> makepython publish
|
|
5
23
|
"""
|
|
6
24
|
|
|
7
25
|
from __future__ import annotations
|
|
8
26
|
|
|
9
|
-
__version__ = "0.2.0"
|
|
10
|
-
__author__ = "pysfi Developers"
|
|
11
|
-
__email__ = "developers@pysfi.org"
|
|
27
|
+
__version__: Final[str] = "0.2.0"
|
|
28
|
+
__author__: Final[str] = "pysfi Developers"
|
|
29
|
+
__email__: Final[str] = "developers@pysfi.org"
|
|
12
30
|
import argparse
|
|
13
31
|
import atexit
|
|
14
|
-
import contextlib
|
|
15
32
|
import json
|
|
16
33
|
import logging
|
|
17
34
|
import os
|
|
18
35
|
import shutil
|
|
19
36
|
import subprocess
|
|
20
37
|
import sys
|
|
38
|
+
import time
|
|
21
39
|
from dataclasses import dataclass
|
|
22
40
|
from enum import Enum
|
|
23
41
|
from functools import cached_property
|
|
@@ -32,23 +50,80 @@ else:
|
|
|
32
50
|
|
|
33
51
|
# Platform detection
|
|
34
52
|
IS_WINDOWS: Final[bool] = sys.platform == "win32"
|
|
35
|
-
|
|
36
|
-
|
|
53
|
+
|
|
54
|
+
# File and directory constants
|
|
55
|
+
CONFIG_DIR_NAME: Final[str] = ".pysfi"
|
|
56
|
+
CONFIG_FILE_NAME: Final[str] = "makepython.json"
|
|
57
|
+
PYPROJECT_FILE_NAME: Final[str] = "pyproject.toml"
|
|
58
|
+
|
|
59
|
+
# Default constants
|
|
60
|
+
DEFAULT_TIMEOUT_SECONDS: Final[int] = 300
|
|
61
|
+
DEFAULT_MAX_RETRIES: Final[int] = 3
|
|
62
|
+
DEFAULT_ENCODING: Final[str] = "utf-8"
|
|
63
|
+
DEFAULT_SHELL_CONFIG: Final[str] = ".bashrc"
|
|
64
|
+
DEFAULT_CACHE_SIZE: Final[int] = 100
|
|
65
|
+
DEFAULT_CACHE_EXPIRY: Final[int] = 300 # 5 minutes
|
|
66
|
+
|
|
67
|
+
# Clean command template
|
|
68
|
+
CLEAN_COMMANDS: Final[list[str]] = ["rm", "-rf"]
|
|
37
69
|
|
|
38
70
|
# Logging setup
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
71
|
+
LOG_FORMAT: Final[str] = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
72
|
+
LOG_DATE_FORMAT: Final[str] = "%Y-%m-%d %H:%M:%S"
|
|
73
|
+
|
|
74
|
+
logging.basicConfig(level=logging.INFO, format=LOG_FORMAT, datefmt=LOG_DATE_FORMAT)
|
|
42
75
|
logger = logging.getLogger(__name__)
|
|
43
76
|
CURRENT_WORKING_DIR: Final[Path] = Path.cwd()
|
|
44
77
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
78
|
+
CONFIG_FILE: Final[Path] = Path.home() / CONFIG_DIR_NAME / CONFIG_FILE_NAME
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class MakePythonError(Exception):
|
|
82
|
+
"""Base exception class for MakePython tool errors."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, message: str, error_code: int = 1):
|
|
85
|
+
super().__init__(message)
|
|
86
|
+
self.message = message
|
|
87
|
+
self.error_code = error_code
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ConfigurationError(MakePythonError):
|
|
91
|
+
"""Raised when configuration loading or saving fails."""
|
|
92
|
+
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class BuildToolNotFoundError(MakePythonError):
|
|
97
|
+
"""Raised when no suitable build tool is found."""
|
|
98
|
+
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class TokenConfigurationError(MakePythonError):
|
|
103
|
+
"""Raised when PyPI token configuration fails."""
|
|
104
|
+
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class CommandExecutionError(MakePythonError):
|
|
109
|
+
"""Raised when command execution fails."""
|
|
110
|
+
|
|
111
|
+
def __init__(
|
|
112
|
+
self, message: str, return_code: int = 1, stdout: str = "", stderr: str = ""
|
|
113
|
+
):
|
|
114
|
+
super().__init__(message, return_code)
|
|
115
|
+
self.stdout = stdout
|
|
116
|
+
self.stderr = stderr
|
|
48
117
|
|
|
49
118
|
|
|
50
119
|
class BuildTool(Enum):
|
|
51
|
-
"""Enumeration of supported build tools.
|
|
120
|
+
"""Enumeration of supported build tools.
|
|
121
|
+
|
|
122
|
+
Attributes:
|
|
123
|
+
UV: The uv build tool
|
|
124
|
+
POETRY: The poetry build tool
|
|
125
|
+
HATCH: The hatch build tool
|
|
126
|
+
"""
|
|
52
127
|
|
|
53
128
|
UV = "uv"
|
|
54
129
|
POETRY = "poetry"
|
|
@@ -56,7 +131,16 @@ class BuildTool(Enum):
|
|
|
56
131
|
|
|
57
132
|
|
|
58
133
|
class CommandType(Enum):
|
|
59
|
-
"""Enumeration of command types.
|
|
134
|
+
"""Enumeration of command types.
|
|
135
|
+
|
|
136
|
+
Attributes:
|
|
137
|
+
BUILD: Build project command
|
|
138
|
+
CLEAN: Clean build artifacts command
|
|
139
|
+
TEST: Run tests command
|
|
140
|
+
PUBLISH: Publish package command
|
|
141
|
+
BUMP_VERSION: Bump version command
|
|
142
|
+
TOKEN: Manage PyPI token command
|
|
143
|
+
"""
|
|
60
144
|
|
|
61
145
|
BUILD = "build"
|
|
62
146
|
CLEAN = "clean"
|
|
@@ -68,7 +152,18 @@ class CommandType(Enum):
|
|
|
68
152
|
|
|
69
153
|
@dataclass
|
|
70
154
|
class ProjectInfo:
|
|
71
|
-
"""Information about the current Python project.
|
|
155
|
+
"""Information about the current Python project.
|
|
156
|
+
|
|
157
|
+
This dataclass holds metadata about a Python project including its name,
|
|
158
|
+
version, build tool, and testing configuration.
|
|
159
|
+
|
|
160
|
+
Attributes:
|
|
161
|
+
name: Project name
|
|
162
|
+
version: Project version string
|
|
163
|
+
build_tool: Detected build tool (optional)
|
|
164
|
+
has_tests: Whether tests directory exists
|
|
165
|
+
has_benchmarks: Whether benchmarks directory exists
|
|
166
|
+
"""
|
|
72
167
|
|
|
73
168
|
name: str
|
|
74
169
|
version: str
|
|
@@ -78,8 +173,25 @@ class ProjectInfo:
|
|
|
78
173
|
|
|
79
174
|
@classmethod
|
|
80
175
|
def from_pyproject(cls, directory: Path) -> ProjectInfo | None:
|
|
81
|
-
"""Create ProjectInfo from pyproject.toml file.
|
|
82
|
-
|
|
176
|
+
"""Create ProjectInfo from pyproject.toml file.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
directory: Directory containing pyproject.toml file
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
ProjectInfo instance if pyproject.toml exists and is valid, None otherwise
|
|
183
|
+
|
|
184
|
+
Note:
|
|
185
|
+
This method will log warnings for parsing errors but won't raise exceptions.
|
|
186
|
+
Uses caching to improve performance for repeated calls.
|
|
187
|
+
"""
|
|
188
|
+
# Check cache first
|
|
189
|
+
cached_info = _project_info_cache.get(directory)
|
|
190
|
+
if cached_info is not None:
|
|
191
|
+
logger.debug(f"Using cached ProjectInfo for {directory}")
|
|
192
|
+
return cached_info
|
|
193
|
+
|
|
194
|
+
pyproject_path = directory / PYPROJECT_FILE_NAME
|
|
83
195
|
if not pyproject_path.exists():
|
|
84
196
|
return None
|
|
85
197
|
|
|
@@ -109,13 +221,18 @@ class ProjectInfo:
|
|
|
109
221
|
benchmarks_dir = directory / "tests" / "benchmarks"
|
|
110
222
|
has_benchmarks = benchmarks_dir.exists() and benchmarks_dir.is_dir()
|
|
111
223
|
|
|
112
|
-
|
|
224
|
+
project_info = cls(
|
|
113
225
|
name=name,
|
|
114
226
|
version=version,
|
|
115
227
|
build_tool=build_tool,
|
|
116
228
|
has_tests=has_tests,
|
|
117
229
|
has_benchmarks=has_benchmarks,
|
|
118
230
|
)
|
|
231
|
+
|
|
232
|
+
# Cache the result
|
|
233
|
+
_project_info_cache.put(directory, project_info)
|
|
234
|
+
|
|
235
|
+
return project_info
|
|
119
236
|
except Exception as e:
|
|
120
237
|
logger.warning(f"Failed to parse pyproject.toml: {e}")
|
|
121
238
|
return None
|
|
@@ -123,13 +240,24 @@ class ProjectInfo:
|
|
|
123
240
|
|
|
124
241
|
@dataclass
|
|
125
242
|
class MakePythonConfig:
|
|
126
|
-
"""Configuration for MakePython tool.
|
|
243
|
+
"""Configuration for MakePython tool.
|
|
244
|
+
|
|
245
|
+
Manages the configuration settings for the MakePython tool including
|
|
246
|
+
default build tools, verbosity settings, and retry policies.
|
|
247
|
+
|
|
248
|
+
Attributes:
|
|
249
|
+
default_build_tool: Default build tool to use
|
|
250
|
+
auto_detect_tool: Whether to automatically detect build tools
|
|
251
|
+
verbose_output: Enable verbose output
|
|
252
|
+
max_retries: Maximum number of retries for failed operations
|
|
253
|
+
timeout_seconds: Timeout for command execution in seconds
|
|
254
|
+
"""
|
|
127
255
|
|
|
128
256
|
default_build_tool: BuildTool = BuildTool.UV
|
|
129
257
|
auto_detect_tool: bool = True
|
|
130
258
|
verbose_output: bool = False
|
|
131
|
-
max_retries: int =
|
|
132
|
-
timeout_seconds: int =
|
|
259
|
+
max_retries: int = DEFAULT_MAX_RETRIES
|
|
260
|
+
timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS
|
|
133
261
|
|
|
134
262
|
def __post_init__(self) -> None:
|
|
135
263
|
"""Initialize configuration after creation."""
|
|
@@ -137,23 +265,41 @@ class MakePythonConfig:
|
|
|
137
265
|
atexit.register(self.save)
|
|
138
266
|
|
|
139
267
|
def _load_from_file(self) -> None:
|
|
140
|
-
"""Load configuration from file if it exists.
|
|
268
|
+
"""Load configuration from file if it exists.
|
|
269
|
+
|
|
270
|
+
Silently continues if configuration file doesn't exist or is invalid.
|
|
271
|
+
Invalid configurations will use default values.
|
|
272
|
+
"""
|
|
141
273
|
if not CONFIG_FILE.exists():
|
|
142
274
|
return
|
|
143
275
|
|
|
144
276
|
try:
|
|
145
|
-
config_data = json.loads(CONFIG_FILE.read_text(encoding=
|
|
277
|
+
config_data = json.loads(CONFIG_FILE.read_text(encoding=DEFAULT_ENCODING))
|
|
146
278
|
for key, value in config_data.items():
|
|
147
279
|
if hasattr(self, key):
|
|
148
280
|
if key == "default_build_tool":
|
|
149
|
-
|
|
281
|
+
try:
|
|
282
|
+
setattr(self, key, BuildTool(value))
|
|
283
|
+
except ValueError:
|
|
284
|
+
logger.warning(
|
|
285
|
+
f"Invalid build tool value '{value}', using default"
|
|
286
|
+
)
|
|
287
|
+
setattr(self, key, self.default_build_tool)
|
|
150
288
|
else:
|
|
151
289
|
setattr(self, key, value)
|
|
152
290
|
except (json.JSONDecodeError, TypeError, AttributeError) as e:
|
|
153
291
|
logger.warning(f"Could not load config from {CONFIG_FILE}: {e}")
|
|
292
|
+
except Exception as e:
|
|
293
|
+
logger.error(f"Unexpected error loading configuration: {e}")
|
|
154
294
|
|
|
155
295
|
def save(self) -> None:
|
|
156
|
-
"""Save current configuration to file.
|
|
296
|
+
"""Save current configuration to file.
|
|
297
|
+
|
|
298
|
+
Creates parent directories if they don't exist.
|
|
299
|
+
|
|
300
|
+
Raises:
|
|
301
|
+
ConfigurationError: If saving configuration fails
|
|
302
|
+
"""
|
|
157
303
|
try:
|
|
158
304
|
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
159
305
|
config_dict = {
|
|
@@ -163,79 +309,177 @@ class MakePythonConfig:
|
|
|
163
309
|
"max_retries": self.max_retries,
|
|
164
310
|
"timeout_seconds": self.timeout_seconds,
|
|
165
311
|
}
|
|
166
|
-
CONFIG_FILE.write_text(
|
|
312
|
+
CONFIG_FILE.write_text(
|
|
313
|
+
json.dumps(config_dict, indent=4), encoding=DEFAULT_ENCODING
|
|
314
|
+
)
|
|
167
315
|
logger.debug(f"Configuration saved to {CONFIG_FILE}")
|
|
168
316
|
except OSError as e:
|
|
169
317
|
logger.error(f"Failed to save configuration: {e}")
|
|
318
|
+
raise ConfigurationError(f"Failed to save configuration: {e}") from e
|
|
319
|
+
except Exception as e:
|
|
320
|
+
logger.error(f"Unexpected error saving configuration: {e}")
|
|
321
|
+
raise ConfigurationError(
|
|
322
|
+
f"Unexpected error saving configuration: {e}"
|
|
323
|
+
) from e
|
|
170
324
|
|
|
171
325
|
@cached_property
|
|
172
326
|
def supported_tools(self) -> list[BuildTool]:
|
|
173
|
-
"""Get list of available build tools.
|
|
174
|
-
|
|
175
|
-
for tool
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
327
|
+
"""Get list of available build tools.
|
|
328
|
+
|
|
329
|
+
Uses caching to avoid repeated system calls for tool detection.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
List of available BuildTool enums
|
|
333
|
+
"""
|
|
334
|
+
return [
|
|
335
|
+
tool
|
|
336
|
+
for tool in BuildTool
|
|
337
|
+
if shutil.which(tool.value)
|
|
338
|
+
]
|
|
179
339
|
|
|
180
340
|
|
|
181
341
|
class CommandExecutor(Protocol):
|
|
182
|
-
"""Protocol for command execution.
|
|
342
|
+
"""Protocol for command execution.
|
|
343
|
+
|
|
344
|
+
Defines the interface for executing commands in a consistent manner.
|
|
345
|
+
Implementations should handle subprocess execution with proper error handling.
|
|
346
|
+
"""
|
|
183
347
|
|
|
184
348
|
def execute(
|
|
185
349
|
self, command: list[str], cwd: Path
|
|
186
|
-
) -> subprocess.CompletedProcess[str]:
|
|
350
|
+
) -> subprocess.CompletedProcess[str]:
|
|
351
|
+
"""Execute a command in the specified directory.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
command: Command to execute as list of arguments
|
|
355
|
+
cwd: Working directory for command execution
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
CompletedProcess object with command results
|
|
359
|
+
|
|
360
|
+
Raises:
|
|
361
|
+
subprocess.CalledProcessError: If command execution fails
|
|
362
|
+
subprocess.TimeoutExpired: If command times out
|
|
363
|
+
"""
|
|
364
|
+
...
|
|
187
365
|
|
|
188
366
|
|
|
189
367
|
class SubprocessExecutor:
|
|
190
|
-
"""Execute commands using subprocess.
|
|
368
|
+
"""Execute commands using subprocess.
|
|
369
|
+
|
|
370
|
+
Provides robust subprocess execution with configurable timeouts,
|
|
371
|
+
proper error handling, and encoding support.
|
|
372
|
+
|
|
373
|
+
Attributes:
|
|
374
|
+
config: MakePython configuration object
|
|
375
|
+
"""
|
|
191
376
|
|
|
192
377
|
def __init__(self, config: MakePythonConfig):
|
|
378
|
+
"""Initialize the executor with configuration.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
config: MakePython configuration object
|
|
382
|
+
"""
|
|
193
383
|
self.config = config
|
|
194
384
|
|
|
195
385
|
def execute(
|
|
196
386
|
self, command: list[str], cwd: Path
|
|
197
387
|
) -> subprocess.CompletedProcess[str]:
|
|
198
|
-
"""Execute command with proper error handling.
|
|
388
|
+
"""Execute command with proper error handling.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
command: Command to execute as list of arguments
|
|
392
|
+
cwd: Working directory for command execution
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
CompletedProcess object with command results
|
|
396
|
+
|
|
397
|
+
Raises:
|
|
398
|
+
CommandExecutionError: For command execution failures
|
|
399
|
+
subprocess.TimeoutExpired: If command times out
|
|
400
|
+
"""
|
|
199
401
|
logger.debug(f"Executing command: {' '.join(command)} in {cwd}")
|
|
200
402
|
|
|
201
403
|
try:
|
|
202
|
-
|
|
404
|
+
return _execute_command(
|
|
203
405
|
command,
|
|
204
|
-
cwd
|
|
205
|
-
capture_output=True,
|
|
206
|
-
text=True,
|
|
207
|
-
encoding="utf-8",
|
|
208
|
-
errors="replace",
|
|
209
|
-
timeout=self.config.timeout_seconds,
|
|
406
|
+
cwd,
|
|
210
407
|
check=True,
|
|
408
|
+
capture=True,
|
|
409
|
+
timeout=self.config.timeout_seconds,
|
|
410
|
+
fallback_encoding=True,
|
|
211
411
|
)
|
|
212
|
-
return result
|
|
213
|
-
except subprocess.TimeoutExpired:
|
|
214
|
-
logger.error(
|
|
215
|
-
f"Command timed out after {self.config.timeout_seconds} seconds"
|
|
216
|
-
)
|
|
217
|
-
raise
|
|
218
412
|
except subprocess.CalledProcessError as e:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
413
|
+
raise CommandExecutionError(
|
|
414
|
+
f"Command failed: {' '.join(command)}",
|
|
415
|
+
return_code=e.returncode,
|
|
416
|
+
stdout=e.stdout or "",
|
|
417
|
+
stderr=e.stderr or "",
|
|
418
|
+
) from e
|
|
225
419
|
except Exception as e:
|
|
226
|
-
|
|
227
|
-
|
|
420
|
+
raise CommandExecutionError(
|
|
421
|
+
f"Unexpected error executing command: {e}"
|
|
422
|
+
) from e
|
|
228
423
|
|
|
229
424
|
|
|
230
|
-
|
|
231
|
-
"""
|
|
232
|
-
logger.debug(f"Parsing pyproject.toml in {directory}")
|
|
425
|
+
class ProjectInfoCache:
|
|
426
|
+
"""Simple cache for ProjectInfo objects to improve performance.
|
|
233
427
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
428
|
+
Caches parsed pyproject.toml data to avoid repeated file I/O operations.
|
|
429
|
+
"""
|
|
430
|
+
|
|
431
|
+
def __init__(self, max_size: int = DEFAULT_CACHE_SIZE):
|
|
432
|
+
self._cache: dict[Path, tuple[ProjectInfo, float]] = {}
|
|
433
|
+
self._max_size = max_size
|
|
434
|
+
self._access_order: list[Path] = []
|
|
435
|
+
|
|
436
|
+
def get(self, directory: Path) -> ProjectInfo | None:
|
|
437
|
+
"""Get cached ProjectInfo for directory.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
directory: Directory to get cached info for
|
|
237
441
|
|
|
238
|
-
|
|
442
|
+
Returns:
|
|
443
|
+
Cached ProjectInfo or None if not found/expired
|
|
444
|
+
"""
|
|
445
|
+
if directory not in self._cache:
|
|
446
|
+
return None
|
|
447
|
+
|
|
448
|
+
info, timestamp = self._cache[directory]
|
|
449
|
+
# Cache expires after 5 minutes
|
|
450
|
+
if time.time() - timestamp > DEFAULT_CACHE_EXPIRY:
|
|
451
|
+
del self._cache[directory]
|
|
452
|
+
if directory in self._access_order:
|
|
453
|
+
self._access_order.remove(directory)
|
|
454
|
+
return None
|
|
455
|
+
|
|
456
|
+
# Move to end for LRU
|
|
457
|
+
if directory in self._access_order:
|
|
458
|
+
self._access_order.remove(directory)
|
|
459
|
+
self._access_order.append(directory)
|
|
460
|
+
return info
|
|
461
|
+
|
|
462
|
+
def put(self, directory: Path, info: ProjectInfo) -> None:
|
|
463
|
+
"""Store ProjectInfo in cache.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
directory: Directory key
|
|
467
|
+
info: ProjectInfo to cache
|
|
468
|
+
"""
|
|
469
|
+
# Remove oldest if cache is full
|
|
470
|
+
if len(self._cache) >= self._max_size:
|
|
471
|
+
oldest = self._access_order.pop(0)
|
|
472
|
+
del self._cache[oldest]
|
|
473
|
+
|
|
474
|
+
self._cache[directory] = (info, time.time())
|
|
475
|
+
self._access_order.append(directory)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
# Global cache instance
|
|
479
|
+
_project_info_cache = ProjectInfoCache()
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
# Removed _get_build_command_from_toml as it duplicates _get_build_command functionality
|
|
239
483
|
|
|
240
484
|
|
|
241
485
|
def _get_build_command(directory: Path, config: MakePythonConfig) -> str | None:
|
|
@@ -247,6 +491,9 @@ def _get_build_command(directory: Path, config: MakePythonConfig) -> str | None:
|
|
|
247
491
|
|
|
248
492
|
Returns:
|
|
249
493
|
Build command string or None if not found
|
|
494
|
+
|
|
495
|
+
Raises:
|
|
496
|
+
BuildToolNotFoundError: If no build tools are available
|
|
250
497
|
"""
|
|
251
498
|
project_info = ProjectInfo.from_pyproject(directory)
|
|
252
499
|
if project_info and project_info.build_tool:
|
|
@@ -261,8 +508,9 @@ def _get_build_command(directory: Path, config: MakePythonConfig) -> str | None:
|
|
|
261
508
|
|
|
262
509
|
logger.error(f"No build command found in {directory}")
|
|
263
510
|
logger.error("Please install uv, poetry, or hatch")
|
|
264
|
-
|
|
265
|
-
|
|
511
|
+
raise BuildToolNotFoundError(
|
|
512
|
+
f"No build tools found. Please install uv, poetry, or hatch in {directory}"
|
|
513
|
+
)
|
|
266
514
|
|
|
267
515
|
|
|
268
516
|
@dataclass
|
|
@@ -276,140 +524,331 @@ class Command:
|
|
|
276
524
|
description: str = ""
|
|
277
525
|
|
|
278
526
|
|
|
527
|
+
def _validate_token_format(token: str) -> tuple[bool, str]:
|
|
528
|
+
"""Validate PyPI token format.
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
token: Token string to validate
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
Tuple of (is_valid, message)
|
|
535
|
+
"""
|
|
536
|
+
if not token:
|
|
537
|
+
return False, "Token cannot be empty"
|
|
538
|
+
|
|
539
|
+
if len(token) < 10:
|
|
540
|
+
return False, "Token appears too short to be valid"
|
|
541
|
+
|
|
542
|
+
# Basic format validation
|
|
543
|
+
if not (token.startswith("pypi-") or token.startswith("PYPI-")):
|
|
544
|
+
return True, "Warning: Token doesn't start with 'pypi-' (may still be valid)"
|
|
545
|
+
|
|
546
|
+
return True, "Token format appears valid"
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _show_detailed_help(commands: list[Command]) -> None:
|
|
550
|
+
"""Show detailed command descriptions and usage information.
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
commands: List of available commands
|
|
554
|
+
"""
|
|
555
|
+
print("\nMakePython - Detailed Command Help")
|
|
556
|
+
print("=" * 40)
|
|
557
|
+
print(f"Version: {__version__}")
|
|
558
|
+
print(f"Author: {__author__}")
|
|
559
|
+
print("\nAvailable Commands:")
|
|
560
|
+
print("-" * 20)
|
|
561
|
+
|
|
562
|
+
# Group commands by type for better organization
|
|
563
|
+
command_groups = {}
|
|
564
|
+
for command in commands:
|
|
565
|
+
group = command.command_type.value
|
|
566
|
+
if group not in command_groups:
|
|
567
|
+
command_groups[group] = []
|
|
568
|
+
command_groups[group].append(command)
|
|
569
|
+
|
|
570
|
+
for group_name, group_commands in command_groups.items():
|
|
571
|
+
print(f"\n{group_name.upper()} COMMANDS:")
|
|
572
|
+
for command in group_commands:
|
|
573
|
+
aliases = f" ({command.alias})" if command.alias != command.name else ""
|
|
574
|
+
print(f" {command.name}{aliases:<8} - {command.description}")
|
|
575
|
+
|
|
576
|
+
print("\nUSAGE EXAMPLES:")
|
|
577
|
+
print(" makepython build # Build the project")
|
|
578
|
+
print(" makepython test # Run tests")
|
|
579
|
+
print(" makepython publish # Publish to PyPI")
|
|
580
|
+
print(" makepython token # Configure PyPI token")
|
|
581
|
+
print(" makepython clean # Clean build artifacts")
|
|
582
|
+
|
|
583
|
+
print("\nOPTIONS:")
|
|
584
|
+
print(" --debug, -d Enable debug output")
|
|
585
|
+
print(" --verbose, -v Enable verbose output")
|
|
586
|
+
print(" --quiet, -q Suppress most messages")
|
|
587
|
+
print(" --version Show version information")
|
|
588
|
+
print(" --help-commands Show this detailed help")
|
|
589
|
+
|
|
590
|
+
print("\nFor more information, visit: https://github.com/pysfi/makepython")
|
|
591
|
+
|
|
592
|
+
|
|
279
593
|
def _clean(root_dir: Path = CURRENT_WORKING_DIR) -> None:
|
|
280
|
-
"""Clean build artifacts and temporary files.
|
|
281
|
-
clean_commands = [
|
|
282
|
-
["rm", "-rf", "dist"],
|
|
283
|
-
["rm", "-rf", "build"],
|
|
284
|
-
["rm", "-rf", "*.egg-info"],
|
|
285
|
-
]
|
|
594
|
+
"""Clean build artifacts and temporary files.
|
|
286
595
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
596
|
+
Args:
|
|
597
|
+
root_dir: Root directory to clean
|
|
598
|
+
"""
|
|
599
|
+
logger.info(f"Cleaning build artifacts in {root_dir}")
|
|
290
600
|
|
|
601
|
+
# Clean common build directories
|
|
602
|
+
targets = ["dist", "build", "*.egg-info"]
|
|
603
|
+
cleaned_count = 0
|
|
604
|
+
failed_count = 0
|
|
291
605
|
|
|
606
|
+
for target in targets:
|
|
607
|
+
try:
|
|
608
|
+
_execute_command([*CLEAN_COMMANDS, target], root_dir, check=False)
|
|
609
|
+
cleaned_count += 1
|
|
610
|
+
except Exception as e:
|
|
611
|
+
logger.warning(f"Failed to clean {target}: {e}")
|
|
612
|
+
failed_count += 1
|
|
613
|
+
continue
|
|
614
|
+
|
|
615
|
+
logger.info(f"Clean operation completed: {cleaned_count} succeeded, {failed_count} failed")
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _dispatch_command(command: Command, *, realtime: bool = False) -> None:
|
|
619
|
+
"""Execute a command based on its type.
|
|
620
|
+
|
|
621
|
+
Args:
|
|
622
|
+
command: Command object to execute
|
|
623
|
+
realtime: Whether to stream output in real-time
|
|
624
|
+
"""
|
|
625
|
+
if callable(command.cmds):
|
|
626
|
+
command.cmds()
|
|
627
|
+
elif isinstance(command.cmds, list):
|
|
628
|
+
# Use realtime output for test commands or if explicitly requested
|
|
629
|
+
use_realtime = realtime or command.command_type == CommandType.TEST
|
|
630
|
+
_run_command(command.cmds, CURRENT_WORKING_DIR, realtime=use_realtime)
|
|
631
|
+
else:
|
|
632
|
+
logger.debug("No preset commands found for this command type")
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def _handle_publish_command(build_command: str) -> None:
|
|
636
|
+
"""Handle publish command with token validation.
|
|
637
|
+
|
|
638
|
+
Args:
|
|
639
|
+
build_command: Build command string
|
|
640
|
+
"""
|
|
641
|
+
if not _check_pypi_token(build_command):
|
|
642
|
+
logger.warning(
|
|
643
|
+
"PyPI token not configured. Starting configuration process..."
|
|
644
|
+
)
|
|
645
|
+
_set_token(build_command)
|
|
646
|
+
_run_command([build_command, "publish"], CURRENT_WORKING_DIR)
|
|
647
|
+
|
|
648
|
+
|
|
292
649
|
def main() -> None:
|
|
293
|
-
"""Main entry point for MakePython tool.
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
650
|
+
"""Main entry point for MakePython tool.
|
|
651
|
+
|
|
652
|
+
Parses command line arguments, loads configuration, detects build tools,
|
|
653
|
+
and executes the requested command.
|
|
654
|
+
|
|
655
|
+
Raises:
|
|
656
|
+
SystemExit: If command execution fails or unknown command is provided
|
|
657
|
+
"""
|
|
658
|
+
try:
|
|
659
|
+
# Initialize configuration
|
|
660
|
+
config = MakePythonConfig()
|
|
661
|
+
|
|
662
|
+
# Get build command
|
|
663
|
+
try:
|
|
664
|
+
build_command = _get_build_command(CURRENT_WORKING_DIR, config) or ""
|
|
665
|
+
except BuildToolNotFoundError as e:
|
|
666
|
+
logger.error(str(e))
|
|
667
|
+
sys.exit(1)
|
|
668
|
+
|
|
669
|
+
# Create commands with proper structure
|
|
670
|
+
commands = [
|
|
671
|
+
Command(
|
|
672
|
+
name="build",
|
|
673
|
+
alias="b",
|
|
674
|
+
command_type=CommandType.BUILD,
|
|
675
|
+
cmds=[build_command, "build"],
|
|
676
|
+
description="Build the project package",
|
|
677
|
+
),
|
|
678
|
+
Command(
|
|
679
|
+
name="bumpversion",
|
|
680
|
+
alias="bump",
|
|
681
|
+
command_type=CommandType.BUMP_VERSION,
|
|
682
|
+
cmds=["bumpversion", "patch", "--tag"],
|
|
683
|
+
description="Bump project version (patch/minor/major)",
|
|
684
|
+
),
|
|
685
|
+
Command(
|
|
686
|
+
name="clean",
|
|
687
|
+
alias="c",
|
|
688
|
+
command_type=CommandType.CLEAN,
|
|
689
|
+
cmds=_clean,
|
|
690
|
+
description="Clean build artifacts and temporary files",
|
|
691
|
+
),
|
|
692
|
+
Command(
|
|
693
|
+
name="publish",
|
|
694
|
+
alias="p",
|
|
695
|
+
command_type=CommandType.PUBLISH,
|
|
696
|
+
description="Publish package to PyPI (requires token)",
|
|
697
|
+
),
|
|
698
|
+
Command(
|
|
699
|
+
name="test",
|
|
700
|
+
alias="t",
|
|
701
|
+
command_type=CommandType.TEST,
|
|
702
|
+
cmds=["pytest", "-n", "8"],
|
|
703
|
+
description="Run tests with parallel execution",
|
|
704
|
+
),
|
|
705
|
+
Command(
|
|
706
|
+
name="test-benchmark",
|
|
707
|
+
alias="tb",
|
|
708
|
+
command_type=CommandType.TEST,
|
|
709
|
+
cmds=["pytest", "-m", "benchmark", "-n", "8"],
|
|
710
|
+
description="Run benchmark tests specifically",
|
|
711
|
+
),
|
|
712
|
+
Command(
|
|
713
|
+
name="test-coverage",
|
|
714
|
+
alias="tc",
|
|
715
|
+
command_type=CommandType.TEST,
|
|
716
|
+
cmds=["pytest", "--cov=sfi", "-n", "8"],
|
|
717
|
+
description="Run tests with coverage reporting",
|
|
718
|
+
),
|
|
719
|
+
Command(
|
|
720
|
+
name="token",
|
|
721
|
+
alias="tk",
|
|
722
|
+
command_type=CommandType.TOKEN,
|
|
723
|
+
cmds=lambda: _set_token(build_command),
|
|
724
|
+
description="Configure PyPI token for publishing",
|
|
725
|
+
),
|
|
726
|
+
]
|
|
727
|
+
command_dict = {command.name: command for command in commands}
|
|
728
|
+
command_dict.update({command.alias: command for command in commands})
|
|
729
|
+
choices = [command.alias for command in commands]
|
|
730
|
+
choices.extend([command.name for command in commands])
|
|
731
|
+
|
|
732
|
+
# Parse arguments
|
|
733
|
+
parser = argparse.ArgumentParser(
|
|
734
|
+
prog="makepython",
|
|
735
|
+
description="Automated Python project building and management tool",
|
|
736
|
+
epilog="""Examples:
|
|
737
|
+
makepython build Build the project
|
|
738
|
+
makepython test Run tests with real-time output (default)
|
|
739
|
+
makepython test -r Run tests with real-time output (explicit)
|
|
740
|
+
makepython publish Publish to PyPI
|
|
741
|
+
makepython clean Clean build artifacts
|
|
742
|
+
|
|
743
|
+
Note: Test commands (test, test-benchmark, test-coverage) automatically use real-time output.
|
|
744
|
+
Use --realtime/-r for other commands if you want to see output as it happens.""",
|
|
745
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
# Add argument groups for better organization
|
|
749
|
+
info_group = parser.add_argument_group("Information Options")
|
|
750
|
+
info_group.add_argument(
|
|
751
|
+
"--version",
|
|
752
|
+
action="version",
|
|
753
|
+
version=f"MakePython v{__version__}",
|
|
754
|
+
help="Show version information and exit",
|
|
755
|
+
)
|
|
756
|
+
info_group.add_argument(
|
|
757
|
+
"--help-commands",
|
|
758
|
+
action="store_true",
|
|
759
|
+
help="Show detailed command descriptions and exit",
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
config_group = parser.add_argument_group("Configuration Options")
|
|
763
|
+
config_group.add_argument(
|
|
764
|
+
"--config-path", type=Path, help="Specify custom configuration file path"
|
|
765
|
+
)
|
|
766
|
+
config_group.add_argument(
|
|
767
|
+
"--build-tool",
|
|
768
|
+
choices=[tool.value for tool in BuildTool],
|
|
769
|
+
help="Force specific build tool (uv, poetry, hatch)",
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
logging_group = parser.add_argument_group("Logging Options")
|
|
773
|
+
logging_group.add_argument(
|
|
774
|
+
"--debug",
|
|
775
|
+
"-d",
|
|
776
|
+
action="store_true",
|
|
777
|
+
help="Enable debug mode with verbose output",
|
|
778
|
+
)
|
|
779
|
+
logging_group.add_argument(
|
|
780
|
+
"--verbose", "-v", action="store_true", help="Enable verbose output"
|
|
781
|
+
)
|
|
782
|
+
logging_group.add_argument(
|
|
783
|
+
"--quiet", "-q", action="store_true", help="Suppress most output messages"
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
output_group = parser.add_argument_group("Output Options")
|
|
787
|
+
output_group.add_argument(
|
|
788
|
+
"--realtime",
|
|
789
|
+
"-r",
|
|
790
|
+
action="store_true",
|
|
791
|
+
help="Stream command output in real-time (useful for tests)",
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
parser.add_argument(
|
|
795
|
+
"command",
|
|
796
|
+
nargs="?",
|
|
797
|
+
type=str,
|
|
798
|
+
choices=choices,
|
|
799
|
+
help=f"Command to run. Available: {', '.join(choices)}",
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
args = parser.parse_args()
|
|
803
|
+
|
|
804
|
+
# Handle help commands option
|
|
805
|
+
if hasattr(args, "help_commands") and args.help_commands:
|
|
806
|
+
_show_detailed_help(commands)
|
|
807
|
+
return
|
|
808
|
+
|
|
809
|
+
# Validate that a command was provided
|
|
810
|
+
if not args.command:
|
|
811
|
+
parser.print_help()
|
|
812
|
+
logger.error("No command specified. Use --help for usage information.")
|
|
813
|
+
sys.exit(1)
|
|
814
|
+
|
|
815
|
+
# Set logging level
|
|
816
|
+
if args.quiet:
|
|
817
|
+
logger.setLevel(logging.ERROR)
|
|
818
|
+
config.verbose_output = False
|
|
819
|
+
elif args.debug:
|
|
820
|
+
logger.setLevel(logging.DEBUG)
|
|
821
|
+
config.verbose_output = True
|
|
822
|
+
elif args.verbose:
|
|
823
|
+
logger.setLevel(logging.INFO)
|
|
824
|
+
config.verbose_output = True
|
|
825
|
+
|
|
826
|
+
logger.debug(f"Using build command: {build_command}")
|
|
827
|
+
logger.debug(f"Working directory: {CURRENT_WORKING_DIR}")
|
|
828
|
+
|
|
829
|
+
# Execute command
|
|
830
|
+
command = command_dict.get(args.command)
|
|
831
|
+
if command:
|
|
832
|
+
logger.info(f"Executing command: {command.name} ({command.description})")
|
|
833
|
+
_dispatch_command(command, realtime=args.realtime)
|
|
401
834
|
else:
|
|
402
|
-
logger.
|
|
403
|
-
|
|
404
|
-
|
|
835
|
+
logger.error(f"Unknown command: {args.command}")
|
|
836
|
+
sys.exit(1)
|
|
837
|
+
|
|
838
|
+
# Special handling for publish command
|
|
839
|
+
if args.command in {"publish", "p"}:
|
|
840
|
+
_handle_publish_command(build_command)
|
|
841
|
+
|
|
842
|
+
except KeyboardInterrupt:
|
|
843
|
+
logger.info("Operation cancelled by user")
|
|
844
|
+
sys.exit(130) # Standard exit code for Ctrl+C
|
|
845
|
+
except MakePythonError as e:
|
|
846
|
+
logger.error(f"MakePython error: {e.message}")
|
|
847
|
+
sys.exit(e.error_code)
|
|
848
|
+
except Exception as e:
|
|
849
|
+
logger.error(f"Unexpected error: {e}")
|
|
405
850
|
sys.exit(1)
|
|
406
851
|
|
|
407
|
-
# Special handling for publish command
|
|
408
|
-
if args.command in {"publish", "p"}:
|
|
409
|
-
if not _check_pypi_token(build_command):
|
|
410
|
-
_set_token(build_command)
|
|
411
|
-
_run_command([build_command, "publish"], CURRENT_WORKING_DIR)
|
|
412
|
-
|
|
413
852
|
|
|
414
853
|
def _set_token(build_command: str, show_header: bool = True) -> None:
|
|
415
854
|
"""Set PyPI token for the specified build command.
|
|
@@ -417,223 +856,335 @@ def _set_token(build_command: str, show_header: bool = True) -> None:
|
|
|
417
856
|
Args:
|
|
418
857
|
build_command: The build tool to configure (uv, poetry, hatch)
|
|
419
858
|
show_header: Whether to show the header message
|
|
859
|
+
|
|
860
|
+
Raises:
|
|
861
|
+
TokenConfigurationError: If unknown build command is provided or token setting fails
|
|
420
862
|
"""
|
|
421
863
|
if show_header:
|
|
422
864
|
logger.info(f"Setting PyPI token for {build_command}...")
|
|
865
|
+
logger.info(
|
|
866
|
+
"Note: Your token will be stored securely in the appropriate configuration files."
|
|
867
|
+
)
|
|
423
868
|
|
|
424
869
|
if build_command.lower() not in [tool.value for tool in BuildTool]:
|
|
425
|
-
|
|
426
|
-
logger.error(
|
|
427
|
-
|
|
870
|
+
error_msg = f"Unknown build command: {build_command}. Please use one of: {[tool.value for tool in BuildTool]}"
|
|
871
|
+
logger.error(error_msg)
|
|
872
|
+
raise TokenConfigurationError(error_msg)
|
|
428
873
|
|
|
429
874
|
token = input("Enter your PyPI token (leave empty to cancel): ").strip()
|
|
430
875
|
if not token:
|
|
431
876
|
logger.info("Operation cancelled - no token provided.")
|
|
432
877
|
return
|
|
433
878
|
|
|
879
|
+
# Validate token format
|
|
880
|
+
is_valid, message = _validate_token_format(token)
|
|
881
|
+
if not is_valid:
|
|
882
|
+
logger.error(f"Invalid token: {message}")
|
|
883
|
+
raise TokenConfigurationError(message)
|
|
884
|
+
elif "Warning" in message:
|
|
885
|
+
logger.warning(message)
|
|
886
|
+
|
|
434
887
|
try:
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
elif build_command == BuildTool.POETRY.value:
|
|
438
|
-
_set_poetry_token(token)
|
|
439
|
-
elif build_command == BuildTool.HATCH.value:
|
|
440
|
-
_set_hatch_token(token)
|
|
888
|
+
build_tool = BuildTool(build_command)
|
|
889
|
+
_set_build_tool_token(token, build_tool)
|
|
441
890
|
logger.info("PyPI token configured successfully!")
|
|
891
|
+
logger.info("You can now use 'makepython publish' to publish your package.")
|
|
442
892
|
except Exception as e:
|
|
443
|
-
|
|
444
|
-
|
|
893
|
+
error_msg = f"Failed to set token: {e}"
|
|
894
|
+
logger.error(error_msg)
|
|
895
|
+
raise TokenConfigurationError(error_msg) from e
|
|
445
896
|
|
|
446
897
|
|
|
447
|
-
def
|
|
448
|
-
"""
|
|
449
|
-
_write_to_env_file("UV_PUBLISH_TOKEN", token)
|
|
898
|
+
def _save_token_to_file(filepath: Path, content: str, description: str) -> None:
|
|
899
|
+
"""Save token content to file with proper error handling.
|
|
450
900
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
token = "{token}"
|
|
456
|
-
"""
|
|
457
|
-
config_path.write_text(content)
|
|
458
|
-
logger.info(f"Token saved to {config_path}")
|
|
901
|
+
Args:
|
|
902
|
+
filepath: Path to save the file
|
|
903
|
+
content: Content to write
|
|
904
|
+
description: Description for logging
|
|
459
905
|
|
|
906
|
+
Raises:
|
|
907
|
+
OSError: If file operation fails
|
|
908
|
+
"""
|
|
909
|
+
try:
|
|
910
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
911
|
+
filepath.write_text(content, encoding=DEFAULT_ENCODING)
|
|
912
|
+
logger.info(f"Token saved to {filepath} ({description})")
|
|
913
|
+
except OSError as e:
|
|
914
|
+
logger.error(f"Failed to save token to {filepath}: {e}")
|
|
915
|
+
raise
|
|
460
916
|
|
|
461
|
-
def _set_poetry_token(token: str) -> None:
|
|
462
|
-
"""Set PyPI token for poetry."""
|
|
463
|
-
_write_to_env_file("POETRY_PYPI_TOKEN_PYPI", token)
|
|
464
|
-
_run_command(["poetry", "config", "pypi-token.pypi", token], CURRENT_WORKING_DIR)
|
|
465
|
-
logger.info("Token saved to Poetry configuration.")
|
|
466
917
|
|
|
918
|
+
def _set_build_tool_token(token: str, build_tool: BuildTool) -> None:
|
|
919
|
+
"""Set PyPI token for the specified build tool.
|
|
467
920
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
921
|
+
Args:
|
|
922
|
+
token: PyPI token to set
|
|
923
|
+
build_tool: Build tool to configure
|
|
924
|
+
|
|
925
|
+
Raises:
|
|
926
|
+
TokenConfigurationError: If token setting fails
|
|
927
|
+
"""
|
|
928
|
+
try:
|
|
929
|
+
if build_tool == BuildTool.UV:
|
|
930
|
+
# Set UV environment variable and config file
|
|
931
|
+
_write_to_env_file("UV_PUBLISH_TOKEN", token)
|
|
932
|
+
config_content = f"""[publish]
|
|
933
|
+
token = "{token}"
|
|
934
|
+
"""
|
|
935
|
+
config_path = Path.home() / ".config" / "uv" / "uv.toml"
|
|
936
|
+
_save_token_to_file(config_path, config_content, "UV config")
|
|
937
|
+
|
|
938
|
+
elif build_tool == BuildTool.POETRY:
|
|
939
|
+
# Set Poetry environment variable and config
|
|
940
|
+
_write_to_env_file("POETRY_PYPI_TOKEN_PYPI", token)
|
|
941
|
+
_run_command(
|
|
942
|
+
["poetry", "config", "pypi-token.pypi", token], CURRENT_WORKING_DIR
|
|
943
|
+
)
|
|
944
|
+
logger.debug("Token saved to Poetry configuration.")
|
|
945
|
+
|
|
946
|
+
elif build_tool == BuildTool.HATCH:
|
|
947
|
+
# Write to .pypirc
|
|
948
|
+
pypirc_content = f"""[pypi]
|
|
472
949
|
repository = https://upload.pypi.org/legacy/
|
|
473
950
|
username = __token__
|
|
474
951
|
password = {token}
|
|
475
952
|
"""
|
|
476
|
-
|
|
477
|
-
|
|
953
|
+
pypirc_path = Path.home() / ".pypirc"
|
|
954
|
+
_save_token_to_file(pypirc_path, pypirc_content, "PyPI RC")
|
|
955
|
+
except Exception as e:
|
|
956
|
+
raise TokenConfigurationError(f"Failed to set token for {build_tool.value}: {e}") from e
|
|
478
957
|
|
|
479
958
|
|
|
480
959
|
def _check_uv_token() -> bool:
|
|
481
|
-
"""Check if PyPI token is configured
|
|
960
|
+
"""Check if UV PyPI token is configured."""
|
|
482
961
|
# Check environment variables
|
|
483
962
|
token_env_vars = ["UV_PUBLISH_TOKEN", "PYPI_API_TOKEN"]
|
|
484
963
|
for var in token_env_vars:
|
|
485
964
|
if os.getenv(var):
|
|
486
|
-
logger.
|
|
965
|
+
logger.debug(f"Found PyPI token in environment variable: {var}")
|
|
487
966
|
return True
|
|
488
967
|
|
|
489
968
|
# Check config file
|
|
490
969
|
config_path = Path.home() / ".config" / "uv" / "uv.toml"
|
|
491
970
|
if config_path.exists():
|
|
492
|
-
logger.
|
|
971
|
+
logger.debug(f"Found uv config file: {config_path}")
|
|
493
972
|
return True
|
|
494
973
|
|
|
495
974
|
return False
|
|
496
975
|
|
|
497
976
|
|
|
498
977
|
def _check_poetry_token() -> bool:
|
|
499
|
-
"""Check if PyPI token is configured
|
|
978
|
+
"""Check if Poetry PyPI token is configured."""
|
|
500
979
|
# Check environment variable
|
|
501
980
|
if os.getenv("POETRY_PYPI_TOKEN_PYPI"):
|
|
502
|
-
logger.
|
|
981
|
+
logger.debug("Found PyPI token in POETRY_PYPI_TOKEN_PYPI environment variable")
|
|
503
982
|
return True
|
|
504
983
|
|
|
505
|
-
# Check
|
|
984
|
+
# Check Poetry config
|
|
506
985
|
try:
|
|
507
|
-
result =
|
|
986
|
+
result = _execute_command(
|
|
508
987
|
["poetry", "config", "pypi-token.pypi"],
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
errors="replace",
|
|
513
|
-
check=True,
|
|
988
|
+
CURRENT_WORKING_DIR,
|
|
989
|
+
check=False,
|
|
990
|
+
capture=True,
|
|
514
991
|
)
|
|
515
992
|
if result.stdout.strip() and result.stdout.strip() != "None":
|
|
516
|
-
logger.
|
|
993
|
+
logger.debug("Found PyPI token in Poetry configuration")
|
|
517
994
|
return True
|
|
518
|
-
except
|
|
519
|
-
logger.
|
|
520
|
-
|
|
995
|
+
except Exception as e:
|
|
996
|
+
logger.debug(f"Error checking Poetry token: {e}")
|
|
521
997
|
return False
|
|
522
998
|
|
|
523
999
|
|
|
524
1000
|
def _check_hatch_token() -> bool:
|
|
525
|
-
"""Check if PyPI token is configured
|
|
1001
|
+
"""Check if Hatch PyPI token is configured."""
|
|
526
1002
|
pypirc_path = Path.home() / ".pypirc"
|
|
527
1003
|
if pypirc_path.exists():
|
|
528
|
-
logger.
|
|
1004
|
+
logger.debug(f"Found .pypirc file: {pypirc_path}")
|
|
529
1005
|
return True
|
|
530
1006
|
return False
|
|
531
1007
|
|
|
532
1008
|
|
|
533
|
-
def
|
|
534
|
-
"""Check if PyPI token is configured
|
|
535
|
-
|
|
1009
|
+
def _check_build_tool_token(build_tool: BuildTool) -> bool:
|
|
1010
|
+
"""Check if PyPI token is configured for the specified build tool.
|
|
1011
|
+
|
|
1012
|
+
Args:
|
|
1013
|
+
build_tool: Build tool to check
|
|
536
1014
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
1015
|
+
Returns:
|
|
1016
|
+
True if token is found, False otherwise
|
|
1017
|
+
"""
|
|
1018
|
+
check_functions = {
|
|
1019
|
+
BuildTool.UV: _check_uv_token,
|
|
1020
|
+
BuildTool.POETRY: _check_poetry_token,
|
|
1021
|
+
BuildTool.HATCH: _check_hatch_token,
|
|
542
1022
|
}
|
|
543
1023
|
|
|
544
|
-
checker =
|
|
545
|
-
if
|
|
1024
|
+
checker = check_functions.get(build_tool)
|
|
1025
|
+
if checker:
|
|
1026
|
+
return checker()
|
|
1027
|
+
|
|
1028
|
+
logger.error(f"Unknown build tool for token checking: {build_tool}")
|
|
1029
|
+
return False
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
def _check_pypi_token(build_command: str) -> bool:
|
|
1033
|
+
"""Check if PyPI token is configured before publishing.
|
|
1034
|
+
|
|
1035
|
+
Args:
|
|
1036
|
+
build_command: Build tool name (uv, poetry, hatch)
|
|
1037
|
+
|
|
1038
|
+
Returns:
|
|
1039
|
+
True if token is configured, False otherwise
|
|
1040
|
+
"""
|
|
1041
|
+
logger.info("Checking PyPI token configuration...")
|
|
1042
|
+
|
|
1043
|
+
try:
|
|
1044
|
+
build_tool = BuildTool(build_command.lower())
|
|
1045
|
+
return _check_build_tool_token(build_tool)
|
|
1046
|
+
except ValueError:
|
|
546
1047
|
logger.error(f"Unknown build command for token checking: {build_command}")
|
|
547
1048
|
return False
|
|
548
1049
|
|
|
549
|
-
return checker()
|
|
550
1050
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
1051
|
+
def _execute_command(
|
|
1052
|
+
cmd: list[str],
|
|
1053
|
+
directory: Path,
|
|
1054
|
+
*,
|
|
1055
|
+
check: bool = True,
|
|
1056
|
+
capture: bool = True,
|
|
1057
|
+
timeout: int | None = None,
|
|
1058
|
+
fallback_encoding: bool = True,
|
|
1059
|
+
realtime: bool = False,
|
|
554
1060
|
) -> subprocess.CompletedProcess[str]:
|
|
555
|
-
"""Execute
|
|
556
|
-
return subprocess.run(
|
|
557
|
-
cmd,
|
|
558
|
-
cwd=directory,
|
|
559
|
-
capture_output=True,
|
|
560
|
-
text=True,
|
|
561
|
-
encoding=encoding,
|
|
562
|
-
errors="replace",
|
|
563
|
-
check=True,
|
|
564
|
-
)
|
|
565
|
-
|
|
1061
|
+
"""Execute command with comprehensive error handling.
|
|
566
1062
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
1063
|
+
Args:
|
|
1064
|
+
cmd: Command to execute as list of arguments
|
|
1065
|
+
directory: Working directory
|
|
1066
|
+
check: Whether to raise CalledProcessError on non-zero exit
|
|
1067
|
+
capture: Whether to capture stdout/stderr (must be False for realtime)
|
|
1068
|
+
timeout: Timeout in seconds (None for no timeout)
|
|
1069
|
+
fallback_encoding: Whether to try fallback encoding on Unicode errors
|
|
1070
|
+
realtime: Whether to stream output in real-time (overrides capture)
|
|
573
1071
|
|
|
1072
|
+
Returns:
|
|
1073
|
+
CompletedProcess object with command results
|
|
574
1074
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
logger.error(f"STDERR: {e.stderr}")
|
|
582
|
-
sys.exit(e.returncode)
|
|
1075
|
+
Raises:
|
|
1076
|
+
SystemExit: If command fails to execute and check=True
|
|
1077
|
+
subprocess.CalledProcessError: If command fails and check=True
|
|
1078
|
+
subprocess.TimeoutExpired: If command times out
|
|
1079
|
+
"""
|
|
1080
|
+
logger.debug(f"Running command: {' '.join(cmd)} in {directory}")
|
|
583
1081
|
|
|
1082
|
+
# If realtime output is requested, disable capture
|
|
1083
|
+
if realtime:
|
|
1084
|
+
capture = False
|
|
1085
|
+
|
|
1086
|
+
def _run_with_encoding(encoding: str) -> subprocess.CompletedProcess[str]:
|
|
1087
|
+
"""Run command with specified encoding."""
|
|
1088
|
+
return subprocess.run(
|
|
1089
|
+
cmd,
|
|
1090
|
+
cwd=directory,
|
|
1091
|
+
capture_output=capture,
|
|
1092
|
+
text=capture,
|
|
1093
|
+
encoding=encoding if capture else None,
|
|
1094
|
+
errors="replace" if capture else None,
|
|
1095
|
+
check=check,
|
|
1096
|
+
timeout=timeout,
|
|
1097
|
+
)
|
|
584
1098
|
|
|
585
|
-
def _handle_unicode_decode_error(
|
|
586
|
-
cmd: list[str], directory: Path, error: UnicodeDecodeError
|
|
587
|
-
) -> None:
|
|
588
|
-
"""Handle UnicodeDecodeError with fallback encoding."""
|
|
589
|
-
logger.error(f"Encoding error while processing command output: {error}")
|
|
590
1099
|
try:
|
|
591
|
-
result =
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
1100
|
+
result = _run_with_encoding(DEFAULT_ENCODING)
|
|
1101
|
+
if result.stdout and capture:
|
|
1102
|
+
print(result.stdout)
|
|
1103
|
+
if result.stderr and capture:
|
|
1104
|
+
print(result.stderr, file=sys.stderr)
|
|
1105
|
+
return result
|
|
1106
|
+
|
|
1107
|
+
except subprocess.CalledProcessError as e:
|
|
1108
|
+
logger.error(f"Command failed with exit code {e.returncode}: {' '.join(cmd)}")
|
|
1109
|
+
if e.stdout:
|
|
1110
|
+
logger.error(f"STDOUT: {e.stdout}")
|
|
1111
|
+
if e.stderr:
|
|
1112
|
+
logger.error(f"STDERR: {e.stderr}")
|
|
1113
|
+
raise
|
|
1114
|
+
|
|
1115
|
+
except subprocess.TimeoutExpired:
|
|
1116
|
+
logger.error(f"Command timed out: {' '.join(cmd)}")
|
|
1117
|
+
raise
|
|
1118
|
+
|
|
1119
|
+
except UnicodeDecodeError as e:
|
|
1120
|
+
if not fallback_encoding or realtime:
|
|
1121
|
+
logger.error(f"Encoding error while processing command output: {e}")
|
|
1122
|
+
raise
|
|
596
1123
|
|
|
1124
|
+
logger.warning(f"Encoding error, trying fallback encoding: {e}")
|
|
1125
|
+
try:
|
|
1126
|
+
result = _run_with_encoding("gbk" if IS_WINDOWS else "utf-8")
|
|
1127
|
+
if result.stdout and capture:
|
|
1128
|
+
print(result.stdout)
|
|
1129
|
+
if result.stderr and capture:
|
|
1130
|
+
print(result.stderr, file=sys.stderr)
|
|
1131
|
+
return result
|
|
1132
|
+
except Exception as fallback_error:
|
|
1133
|
+
logger.error(f"Fallback encoding also failed: {fallback_error}")
|
|
1134
|
+
raise
|
|
597
1135
|
|
|
598
|
-
|
|
599
|
-
|
|
1136
|
+
except Exception as e:
|
|
1137
|
+
logger.error(f"Unexpected error executing command {' '.join(cmd)}: {e}")
|
|
1138
|
+
raise
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
def _run_command(cmd: list[str], directory: Path, *, realtime: bool = False) -> None:
|
|
1142
|
+
"""Run a command in specified directory with proper error handling.
|
|
600
1143
|
|
|
601
1144
|
Args:
|
|
602
1145
|
cmd: Command to execute as list of arguments
|
|
603
1146
|
directory: Directory to run the command in
|
|
1147
|
+
realtime: Whether to stream output in real-time
|
|
604
1148
|
|
|
605
1149
|
Raises:
|
|
606
1150
|
SystemExit: If command fails to execute
|
|
1151
|
+
ValueError: If inputs are invalid
|
|
607
1152
|
"""
|
|
608
|
-
|
|
1153
|
+
# Validate inputs
|
|
1154
|
+
if not directory.exists() or not directory.is_dir():
|
|
1155
|
+
raise ValueError(f"Invalid directory: {directory}")
|
|
1156
|
+
|
|
1157
|
+
if not cmd or not all(isinstance(c, str) for c in cmd):
|
|
1158
|
+
raise ValueError("Invalid command list")
|
|
609
1159
|
|
|
610
1160
|
try:
|
|
611
|
-
|
|
612
|
-
_handle_command_result(result)
|
|
1161
|
+
_execute_command(cmd, directory, check=True, realtime=realtime)
|
|
613
1162
|
except subprocess.CalledProcessError as e:
|
|
614
|
-
|
|
615
|
-
except
|
|
616
|
-
|
|
1163
|
+
sys.exit(e.returncode)
|
|
1164
|
+
except subprocess.TimeoutExpired:
|
|
1165
|
+
sys.exit(124) # Standard exit code for timeout
|
|
617
1166
|
except Exception as e:
|
|
618
|
-
logger.error(f"
|
|
1167
|
+
logger.error(f"Command execution failed: {e}")
|
|
619
1168
|
sys.exit(1)
|
|
620
1169
|
|
|
621
1170
|
|
|
622
1171
|
def _write_to_env_file(key: str, value: str) -> None:
|
|
623
1172
|
"""Write key-value pair to environment file.
|
|
624
1173
|
|
|
1174
|
+
Sets environment variables either through system utilities (Windows)
|
|
1175
|
+
or shell configuration files (Unix-like systems).
|
|
1176
|
+
|
|
625
1177
|
Args:
|
|
626
1178
|
key: Environment variable name
|
|
627
1179
|
value: Environment variable value
|
|
628
1180
|
"""
|
|
629
1181
|
if IS_WINDOWS:
|
|
630
1182
|
try:
|
|
631
|
-
|
|
1183
|
+
_execute_command(
|
|
632
1184
|
["setx", key, value],
|
|
633
|
-
|
|
634
|
-
encoding="utf-8",
|
|
635
|
-
errors="replace",
|
|
1185
|
+
CURRENT_WORKING_DIR,
|
|
636
1186
|
check=True,
|
|
1187
|
+
capture=False,
|
|
637
1188
|
)
|
|
638
1189
|
logger.info(f"Environment variable {key} set successfully")
|
|
639
1190
|
except Exception as e:
|
|
@@ -644,20 +1195,35 @@ def _write_to_env_file(key: str, value: str) -> None:
|
|
|
644
1195
|
|
|
645
1196
|
|
|
646
1197
|
def _get_shell_config_path() -> Path:
|
|
647
|
-
"""Get the appropriate shell config file based on the current shell.
|
|
1198
|
+
"""Get the appropriate shell config file based on the current shell.
|
|
1199
|
+
|
|
1200
|
+
Detects the user's shell and returns the corresponding configuration file path.
|
|
1201
|
+
Defaults to .bashrc for bash/zsh shells.
|
|
1202
|
+
|
|
1203
|
+
Returns:
|
|
1204
|
+
Path to the appropriate shell configuration file
|
|
1205
|
+
"""
|
|
648
1206
|
# Try to detect the shell
|
|
649
1207
|
shell = os.getenv("SHELL", "")
|
|
650
1208
|
if "zsh" in shell:
|
|
651
1209
|
return Path.home() / ".zshrc"
|
|
652
1210
|
else:
|
|
653
1211
|
# Default to .bashrc
|
|
654
|
-
return Path.home() /
|
|
1212
|
+
return Path.home() / DEFAULT_SHELL_CONFIG
|
|
655
1213
|
|
|
656
1214
|
|
|
657
1215
|
def _write_to_shell_config(content: str) -> None:
|
|
658
|
-
"""Write content to
|
|
1216
|
+
"""Write content to shell configuration file, replacing existing entries.
|
|
659
1217
|
|
|
660
|
-
|
|
1218
|
+
Updates shell configuration files by removing existing export statements
|
|
1219
|
+
for the same variable and adding the new content.
|
|
1220
|
+
|
|
1221
|
+
Args:
|
|
1222
|
+
content: Content to write (should be export statements)
|
|
1223
|
+
|
|
1224
|
+
Raises:
|
|
1225
|
+
ValueError: If export statement format is invalid
|
|
1226
|
+
OSError: If file operation fails
|
|
661
1227
|
"""
|
|
662
1228
|
config_path = _get_shell_config_path()
|
|
663
1229
|
if not config_path.exists():
|
|
@@ -669,17 +1235,16 @@ def _write_to_shell_config(content: str) -> None:
|
|
|
669
1235
|
var_name = None
|
|
670
1236
|
for line in content.strip().split("\n"):
|
|
671
1237
|
if line.startswith("export ") and "=" in line:
|
|
672
|
-
var_name = line.split("=")[0].
|
|
1238
|
+
var_name = line.split("=")[0].removeprefix("export ").strip()
|
|
673
1239
|
break
|
|
674
1240
|
|
|
675
1241
|
if not var_name:
|
|
676
|
-
|
|
1242
|
+
raise ValueError(
|
|
677
1243
|
"Invalid export statement format. Expected: export VARIABLE_NAME=value"
|
|
678
1244
|
)
|
|
679
|
-
return
|
|
680
1245
|
|
|
681
1246
|
# Read existing content
|
|
682
|
-
existing_lines = config_path.read_text(encoding=
|
|
1247
|
+
existing_lines = config_path.read_text(encoding=DEFAULT_ENCODING).split("\n")
|
|
683
1248
|
|
|
684
1249
|
# Find and remove existing export statements for this variable
|
|
685
1250
|
new_lines = []
|
|
@@ -694,17 +1259,13 @@ def _write_to_shell_config(content: str) -> None:
|
|
|
694
1259
|
new_lines.append(line)
|
|
695
1260
|
|
|
696
1261
|
if found_existing:
|
|
697
|
-
logger.
|
|
1262
|
+
logger.debug(f"Found existing export statement for {var_name}, replacing it...")
|
|
698
1263
|
|
|
699
1264
|
# Add new content
|
|
700
1265
|
new_lines.append(content.strip())
|
|
701
1266
|
|
|
702
1267
|
# Write back to file
|
|
703
|
-
config_path.write_text("\n".join(new_lines), encoding=
|
|
1268
|
+
config_path.write_text("\n".join(new_lines), encoding=DEFAULT_ENCODING)
|
|
704
1269
|
|
|
705
1270
|
logger.info(f"Content written to {config_path}")
|
|
706
1271
|
logger.info(f"Run `source {config_path}` to apply the changes")
|
|
707
|
-
logger.info(f"Run `cat {config_path}` to view the content")
|
|
708
|
-
logger.info(
|
|
709
|
-
f"Run `cat {config_path} | grep 'export'` to view the exported variables"
|
|
710
|
-
)
|