pysealer 0.4.1__cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.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.
- pysealer/__init__.py +24 -0
- pysealer/_pysealer.cpython-313t-powerpc64le-linux-gnu.so +0 -0
- pysealer/add_decorators.py +237 -0
- pysealer/check_decorators.py +205 -0
- pysealer/cli.py +391 -0
- pysealer/dummy_decorators.py +83 -0
- pysealer/git_diff.py +210 -0
- pysealer/github_secrets.py +175 -0
- pysealer/remove_decorators.py +88 -0
- pysealer/setup.py +137 -0
- pysealer-0.4.1.dist-info/METADATA +171 -0
- pysealer-0.4.1.dist-info/RECORD +15 -0
- pysealer-0.4.1.dist-info/WHEEL +5 -0
- pysealer-0.4.1.dist-info/entry_points.txt +2 -0
- pysealer-0.4.1.dist-info/licenses/LICENSE +21 -0
pysealer/cli.py
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for pysealer.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
- init: Initialize pysealer with a new keypair and .env file.
|
|
6
|
+
- lock: Add pysealer decorators to all functions and classes in a Python file.
|
|
7
|
+
- check: Check the integrity and validity of pysealer decorators in a Python file.
|
|
8
|
+
- remove: Remove all pysealer decorators from a Python file.
|
|
9
|
+
|
|
10
|
+
Use `pysealer --help` to see available options and command details.
|
|
11
|
+
Use `pysealer --version` to see the current version of pysealer installed.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import typer
|
|
17
|
+
from typing_extensions import Annotated
|
|
18
|
+
|
|
19
|
+
from . import __version__
|
|
20
|
+
from .setup import setup_keypair
|
|
21
|
+
from .add_decorators import add_decorators, add_decorators_to_folder
|
|
22
|
+
from .check_decorators import check_decorators, check_decorators_in_folder
|
|
23
|
+
from .remove_decorators import remove_decorators, remove_decorators_from_folder
|
|
24
|
+
|
|
25
|
+
app = typer.Typer(
|
|
26
|
+
name="pysealer",
|
|
27
|
+
help="Version control your Python functions and classes with cryptographic decorators",
|
|
28
|
+
no_args_is_help=True,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _format_diff_output(func_name: str, diff_lines):
|
|
33
|
+
"""Format and display git diff with color coding."""
|
|
34
|
+
if not diff_lines:
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
typer.echo(f" Function '{func_name}' was modified:")
|
|
38
|
+
|
|
39
|
+
for diff_type, content, line_num in diff_lines:
|
|
40
|
+
# Format line with appropriate styling
|
|
41
|
+
if diff_type == '-':
|
|
42
|
+
# Deletions in red
|
|
43
|
+
line_str = f" {line_num:<4}{typer.style('-', fg=typer.colors.RED)}{typer.style(content, fg=typer.colors.RED)}"
|
|
44
|
+
elif diff_type == '+':
|
|
45
|
+
# Additions in green
|
|
46
|
+
line_str = f" {line_num:<4}{typer.style('+', fg=typer.colors.GREEN)}{typer.style(content, fg=typer.colors.GREEN)}"
|
|
47
|
+
else:
|
|
48
|
+
# Context lines in dim/default color
|
|
49
|
+
line_str = f" {line_num:<4} {content}"
|
|
50
|
+
|
|
51
|
+
typer.echo(line_str)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def version_callback(value: bool):
|
|
55
|
+
"""Helper function to display version information."""
|
|
56
|
+
if value:
|
|
57
|
+
typer.echo(f"pysealer {__version__}")
|
|
58
|
+
raise typer.Exit()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@app.callback()
|
|
62
|
+
def version(
|
|
63
|
+
version: Annotated[
|
|
64
|
+
bool,
|
|
65
|
+
typer.Option("--version", help="Report the current version of pysealer installed.", callback=version_callback, is_eager=True)
|
|
66
|
+
] = False
|
|
67
|
+
):
|
|
68
|
+
"""Report the current version of pysealer installed."""
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.command()
|
|
73
|
+
def init(
|
|
74
|
+
env_file: Annotated[
|
|
75
|
+
str,
|
|
76
|
+
typer.Argument(help="Path to the .env file")
|
|
77
|
+
] = ".env",
|
|
78
|
+
github_token: Annotated[
|
|
79
|
+
str,
|
|
80
|
+
typer.Option("--github-token", help="GitHub personal access token for uploading public key to repository secrets.")
|
|
81
|
+
] = None
|
|
82
|
+
):
|
|
83
|
+
"""Initialize pysealer with an .env file and optionally upload public key to GitHub."""
|
|
84
|
+
try:
|
|
85
|
+
env_path = Path(env_file)
|
|
86
|
+
|
|
87
|
+
# Generate and store keypair (will raise error if keys already exist)
|
|
88
|
+
public_key, private_key = setup_keypair(env_path)
|
|
89
|
+
typer.echo(typer.style("Successfully initialized pysealer!", fg=typer.colors.BLUE, bold=True))
|
|
90
|
+
typer.echo(f"🔑 Keypair generated and stored in {env_path}")
|
|
91
|
+
typer.echo("🔍 Keep your .env file secure and add it to .gitignore")
|
|
92
|
+
|
|
93
|
+
# GitHub secrets integration (optional, only if token provided)
|
|
94
|
+
if github_token:
|
|
95
|
+
typer.echo(typer.style("Attempting to upload public key to GitHub repository secrets...", fg=typer.colors.BLUE, bold=True))
|
|
96
|
+
try:
|
|
97
|
+
from .github_secrets import setup_github_secrets
|
|
98
|
+
|
|
99
|
+
success, message = setup_github_secrets(public_key, github_token)
|
|
100
|
+
|
|
101
|
+
if success:
|
|
102
|
+
typer.echo(typer.style(f"✓ {message}", fg=typer.colors.GREEN))
|
|
103
|
+
else:
|
|
104
|
+
typer.echo(typer.style(f"⚠️ Warning: {message}", fg=typer.colors.YELLOW))
|
|
105
|
+
typer.echo(" You can manually add the PYSEALER_PUBLIC_KEY to GitHub secrets later.")
|
|
106
|
+
|
|
107
|
+
except ImportError as e:
|
|
108
|
+
typer.echo(typer.style(f"⚠️ Warning: GitHub integration dependencies not installed: {e}", fg=typer.colors.YELLOW))
|
|
109
|
+
except Exception as e:
|
|
110
|
+
typer.echo(typer.style(f"⚠️ Warning: Failed to upload to GitHub: {e}", fg=typer.colors.YELLOW))
|
|
111
|
+
typer.echo(" You can manually add the PYSEALER_PUBLIC_KEY to GitHub secrets later.")
|
|
112
|
+
|
|
113
|
+
except Exception as e:
|
|
114
|
+
typer.echo(typer.style(f"Error during initialization: {e}", fg=typer.colors.RED, bold=True), err=True)
|
|
115
|
+
raise typer.Exit(code=1)
|
|
116
|
+
|
|
117
|
+
@app.command()
|
|
118
|
+
def lock(
|
|
119
|
+
file_path: Annotated[
|
|
120
|
+
str,
|
|
121
|
+
typer.Argument(help="Path to the Python file or folder to decorate")
|
|
122
|
+
]
|
|
123
|
+
):
|
|
124
|
+
"""Add decorators to all functions and classes in a Python file or all Python files in a folder."""
|
|
125
|
+
path = Path(file_path)
|
|
126
|
+
|
|
127
|
+
# Validate path exists
|
|
128
|
+
if not path.exists():
|
|
129
|
+
typer.echo(typer.style(f"Error: Path '{path}' does not exist.", fg=typer.colors.RED, bold=True), err=True)
|
|
130
|
+
raise typer.Exit(code=1)
|
|
131
|
+
|
|
132
|
+
# Validate it's a Python file or directory
|
|
133
|
+
if path.is_file() and path.suffix != '.py':
|
|
134
|
+
typer.echo(typer.style(f"Error: File '{path}' is not a Python file.", fg=typer.colors.RED, bold=True), err=True)
|
|
135
|
+
raise typer.Exit(code=1)
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
# Handle folder path
|
|
139
|
+
if path.is_dir():
|
|
140
|
+
resolved_path = str(path.resolve())
|
|
141
|
+
decorated_files = add_decorators_to_folder(resolved_path)
|
|
142
|
+
|
|
143
|
+
file_word = "file" if len(decorated_files) == 1 else "files"
|
|
144
|
+
typer.echo(typer.style(f"Successfully added decorators to {len(decorated_files)} {file_word}:", fg=typer.colors.BLUE, bold=True))
|
|
145
|
+
for file in decorated_files:
|
|
146
|
+
typer.echo(f" {typer.style('✓', fg=typer.colors.GREEN)} {file}")
|
|
147
|
+
|
|
148
|
+
# Handle file path
|
|
149
|
+
else:
|
|
150
|
+
|
|
151
|
+
# Add decorators to all functions and classes in the file
|
|
152
|
+
resolved_path = str(path.resolve())
|
|
153
|
+
modified_code, has_changes = add_decorators(resolved_path)
|
|
154
|
+
|
|
155
|
+
if has_changes:
|
|
156
|
+
# Write the modified code back to the file
|
|
157
|
+
with open(resolved_path, 'w') as f:
|
|
158
|
+
f.write(modified_code)
|
|
159
|
+
|
|
160
|
+
typer.echo(typer.style(f"Successfully added decorators to 1 file:", fg=typer.colors.BLUE, bold=True))
|
|
161
|
+
typer.echo(f" {typer.style('✓', fg=typer.colors.GREEN)} {resolved_path}")
|
|
162
|
+
else:
|
|
163
|
+
typer.echo(typer.style(f"No functions or classes found in file:", fg=typer.colors.YELLOW, bold=True))
|
|
164
|
+
typer.echo(f" {typer.style('⊘', fg=typer.colors.YELLOW)} {resolved_path}")
|
|
165
|
+
|
|
166
|
+
except (RuntimeError, FileNotFoundError, NotADirectoryError, ValueError) as e:
|
|
167
|
+
typer.echo(typer.style(f"Error: {e}", fg=typer.colors.RED, bold=True), err=True)
|
|
168
|
+
raise typer.Exit(code=1)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
typer.echo(typer.style(f"Unexpected error while locking file: {e}", fg=typer.colors.RED, bold=True), err=True)
|
|
171
|
+
raise typer.Exit(code=1)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@app.command()
|
|
175
|
+
def check(
|
|
176
|
+
file_path: Annotated[
|
|
177
|
+
str,
|
|
178
|
+
typer.Argument(help="Path to the Python file or folder to check")
|
|
179
|
+
]
|
|
180
|
+
):
|
|
181
|
+
"""Check the integrity of decorators in a Python file or all Python files in a folder."""
|
|
182
|
+
path = Path(file_path)
|
|
183
|
+
|
|
184
|
+
# Validate path exists
|
|
185
|
+
if not path.exists():
|
|
186
|
+
typer.echo(typer.style(f"Error: Path '{path}' does not exist.", fg=typer.colors.RED, bold=True), err=True)
|
|
187
|
+
raise typer.Exit(code=1)
|
|
188
|
+
|
|
189
|
+
# Validate it's a Python file or directory
|
|
190
|
+
if path.is_file() and path.suffix != '.py':
|
|
191
|
+
typer.echo(typer.style(f"Error: File '{path}' is not a Python file.", fg=typer.colors.RED, bold=True), err=True)
|
|
192
|
+
raise typer.Exit(code=1)
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
# Handle folder path
|
|
196
|
+
if path.is_dir():
|
|
197
|
+
resolved_path = str(path.resolve())
|
|
198
|
+
all_results = check_decorators_in_folder(resolved_path)
|
|
199
|
+
|
|
200
|
+
total_decorated = 0
|
|
201
|
+
total_valid = 0
|
|
202
|
+
total_files = 0
|
|
203
|
+
files_with_issues = []
|
|
204
|
+
|
|
205
|
+
for file_path, results in all_results.items():
|
|
206
|
+
# Skip files with errors
|
|
207
|
+
if "error" in results:
|
|
208
|
+
typer.echo(typer.style(f"✗ {file_path}: {results['error']}", fg=typer.colors.RED))
|
|
209
|
+
files_with_issues.append(file_path)
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
total_files += 1
|
|
213
|
+
decorated_count = sum(1 for r in results.values() if r["has_decorator"])
|
|
214
|
+
valid_count = sum(1 for r in results.values() if r["valid"])
|
|
215
|
+
|
|
216
|
+
total_decorated += decorated_count
|
|
217
|
+
total_valid += valid_count
|
|
218
|
+
|
|
219
|
+
# Track files with validation failures
|
|
220
|
+
if decorated_count > 0 and valid_count < decorated_count:
|
|
221
|
+
files_with_issues.append(file_path)
|
|
222
|
+
|
|
223
|
+
# Summary header
|
|
224
|
+
if total_decorated == 0:
|
|
225
|
+
python_files = list(path.rglob("*.py"))
|
|
226
|
+
if python_files:
|
|
227
|
+
typer.echo(typer.style(f"No pysealer decorators found in {len(python_files)} files:", fg=typer.colors.YELLOW, bold=True))
|
|
228
|
+
for file in python_files:
|
|
229
|
+
typer.echo(f" {typer.style('⊘', fg=typer.colors.YELLOW)} {file.resolve()}")
|
|
230
|
+
else:
|
|
231
|
+
typer.echo(typer.style(f"No Python files found in folder.", fg=typer.colors.YELLOW, bold=True))
|
|
232
|
+
elif total_valid == total_decorated:
|
|
233
|
+
file_word = "file" if total_files == 1 else "files"
|
|
234
|
+
typer.echo(typer.style(f"All decorators are valid in {total_files} {file_word}:", fg=typer.colors.BLUE, bold=True))
|
|
235
|
+
else:
|
|
236
|
+
failed_count = total_decorated - total_valid
|
|
237
|
+
failed_files = len(files_with_issues)
|
|
238
|
+
decorator_word = "decorator" if failed_count == 1 else "decorators"
|
|
239
|
+
file_word = "file" if failed_files == 1 else "files"
|
|
240
|
+
typer.echo(typer.style(f"{failed_count} {decorator_word} failed in {failed_files} {file_word}:", fg=typer.colors.BLUE, bold=True), err=True)
|
|
241
|
+
|
|
242
|
+
# File-by-file details
|
|
243
|
+
if total_decorated > 0:
|
|
244
|
+
for file_path, results in all_results.items():
|
|
245
|
+
if "error" in results:
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
decorated_count = sum(1 for r in results.values() if r["has_decorator"])
|
|
249
|
+
valid_count = sum(1 for r in results.values() if r["valid"])
|
|
250
|
+
|
|
251
|
+
if decorated_count > 0:
|
|
252
|
+
if valid_count == decorated_count:
|
|
253
|
+
typer.echo(f" {typer.style('✓', fg=typer.colors.GREEN)} {file_path}")
|
|
254
|
+
else:
|
|
255
|
+
typer.echo(f" {typer.style('✗', fg=typer.colors.RED)} {file_path}")
|
|
256
|
+
|
|
257
|
+
# Show diff for each failed function
|
|
258
|
+
for func_name, result in results.items():
|
|
259
|
+
if result["has_decorator"] and not result["valid"]:
|
|
260
|
+
if result.get("diff"):
|
|
261
|
+
_format_diff_output(func_name, result["diff"])
|
|
262
|
+
|
|
263
|
+
# Exit with error if there were failures
|
|
264
|
+
if total_decorated > 0 and total_valid < total_decorated:
|
|
265
|
+
raise typer.Exit(code=1)
|
|
266
|
+
|
|
267
|
+
# Handle file path
|
|
268
|
+
else:
|
|
269
|
+
|
|
270
|
+
# Check all decorators in the file
|
|
271
|
+
resolved_path = str(path.resolve())
|
|
272
|
+
results = check_decorators(resolved_path)
|
|
273
|
+
|
|
274
|
+
# Return success if all decorated functions are valid
|
|
275
|
+
decorated_count = sum(1 for r in results.values() if r["has_decorator"])
|
|
276
|
+
valid_count = sum(1 for r in results.values() if r["valid"])
|
|
277
|
+
|
|
278
|
+
if decorated_count == 0:
|
|
279
|
+
typer.echo(typer.style(f"No pysealer decorators found in 1 file:", fg=typer.colors.YELLOW, bold=True))
|
|
280
|
+
typer.echo(f" {typer.style('⊘', fg=typer.colors.YELLOW)} {resolved_path}")
|
|
281
|
+
elif valid_count == decorated_count:
|
|
282
|
+
decorator_word = "decorator" if decorated_count == 1 else "decorators"
|
|
283
|
+
typer.echo(typer.style(f"All {decorator_word} are valid in 1 file:", fg=typer.colors.BLUE, bold=True))
|
|
284
|
+
typer.echo(f" {typer.style('✓', fg=typer.colors.GREEN)} {resolved_path}")
|
|
285
|
+
else:
|
|
286
|
+
failed = decorated_count - valid_count
|
|
287
|
+
decorator_word = "decorator" if decorated_count == 1 else "decorators"
|
|
288
|
+
typer.echo(typer.style(f"{failed}/{decorated_count} {decorator_word} failed in 1 file:", fg=typer.colors.BLUE, bold=True), err=True)
|
|
289
|
+
typer.echo(f" {typer.style('✗', fg=typer.colors.RED)} {resolved_path}")
|
|
290
|
+
|
|
291
|
+
# Show diff for each failed function
|
|
292
|
+
for func_name, result in results.items():
|
|
293
|
+
if result["has_decorator"] and not result["valid"]:
|
|
294
|
+
if result.get("diff"):
|
|
295
|
+
_format_diff_output(func_name, result["diff"])
|
|
296
|
+
|
|
297
|
+
raise typer.Exit(code=1)
|
|
298
|
+
|
|
299
|
+
except (FileNotFoundError, NotADirectoryError, ValueError) as e:
|
|
300
|
+
typer.echo(typer.style(f"Error: {e}", fg=typer.colors.RED, bold=True), err=True)
|
|
301
|
+
raise typer.Exit(code=1)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@app.command()
|
|
305
|
+
def remove(
|
|
306
|
+
file_path: Annotated[
|
|
307
|
+
str,
|
|
308
|
+
typer.Argument(help="Path to the Python file or folder to remove pysealer decorators from")
|
|
309
|
+
]
|
|
310
|
+
):
|
|
311
|
+
"""Remove pysealer decorators from all functions and classes in a Python file or all Python files in a folder."""
|
|
312
|
+
path = Path(file_path)
|
|
313
|
+
|
|
314
|
+
# Validate path exists
|
|
315
|
+
if not path.exists():
|
|
316
|
+
typer.echo(typer.style(f"Error: Path '{path}' does not exist.", fg=typer.colors.RED, bold=True), err=True)
|
|
317
|
+
raise typer.Exit(code=1)
|
|
318
|
+
|
|
319
|
+
# Validate it's a Python file or directory
|
|
320
|
+
if path.is_file() and path.suffix != '.py':
|
|
321
|
+
typer.echo(typer.style(f"Error: File '{path}' is not a Python file.", fg=typer.colors.RED, bold=True), err=True)
|
|
322
|
+
raise typer.Exit(code=1)
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
# Handle folder path
|
|
326
|
+
if path.is_dir():
|
|
327
|
+
resolved_path = str(path.resolve())
|
|
328
|
+
modified_files = remove_decorators_from_folder(resolved_path)
|
|
329
|
+
|
|
330
|
+
if modified_files:
|
|
331
|
+
file_word = "file" if len(modified_files) == 1 else "files"
|
|
332
|
+
typer.echo(typer.style(f"Successfully removed decorators from {len(modified_files)} {file_word}:", fg=typer.colors.BLUE, bold=True))
|
|
333
|
+
for file in modified_files:
|
|
334
|
+
typer.echo(f" {typer.style('✓', fg=typer.colors.GREEN)} {file}")
|
|
335
|
+
else:
|
|
336
|
+
# Find all Python files to show in the output
|
|
337
|
+
python_files = list(path.rglob("*.py"))
|
|
338
|
+
if python_files:
|
|
339
|
+
file_word = "file" if len(python_files) == 1 else "files"
|
|
340
|
+
typer.echo(typer.style(f"No pysealer decorators found in {len(python_files)} {file_word}:", fg=typer.colors.YELLOW, bold=True))
|
|
341
|
+
for file in python_files:
|
|
342
|
+
typer.echo(f" {typer.style('⊘', fg=typer.colors.YELLOW)} {file.resolve()}")
|
|
343
|
+
else:
|
|
344
|
+
typer.echo(typer.style(f"No Python files found in folder.", fg=typer.colors.YELLOW, bold=True))
|
|
345
|
+
|
|
346
|
+
# Handle file path
|
|
347
|
+
else:
|
|
348
|
+
|
|
349
|
+
resolved_path = str(path.resolve())
|
|
350
|
+
modified_code, found = remove_decorators(resolved_path)
|
|
351
|
+
|
|
352
|
+
with open(resolved_path, 'w') as f:
|
|
353
|
+
f.write(modified_code)
|
|
354
|
+
|
|
355
|
+
if found:
|
|
356
|
+
typer.echo(typer.style(f"Successfully removed decorators from 1 file:", fg=typer.colors.BLUE, bold=True))
|
|
357
|
+
typer.echo(f" {typer.style('✓', fg=typer.colors.GREEN)} {resolved_path}")
|
|
358
|
+
else:
|
|
359
|
+
typer.echo(typer.style(f"No pysealer decorators found in 1 file:", fg=typer.colors.YELLOW, bold=True))
|
|
360
|
+
typer.echo(f" {typer.style('⊘', fg=typer.colors.YELLOW)} {resolved_path}")
|
|
361
|
+
|
|
362
|
+
except (FileNotFoundError, NotADirectoryError, ValueError) as e:
|
|
363
|
+
typer.echo(typer.style(f"Error: {e}", fg=typer.colors.RED, bold=True), err=True)
|
|
364
|
+
raise typer.Exit(code=1)
|
|
365
|
+
except Exception as e:
|
|
366
|
+
typer.echo(typer.style(f"Unexpected error while removing decorators: {e}", fg=typer.colors.RED, bold=True), err=True)
|
|
367
|
+
raise typer.Exit(code=1)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def main():
|
|
371
|
+
"""Main CLI entry point."""
|
|
372
|
+
app()
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
if __name__ == '__main__':
|
|
376
|
+
main()
|
|
377
|
+
|
|
378
|
+
'''
|
|
379
|
+
modify the check command to print out the specific reason why the decorator is invalid for each that fails. basically i need to get the diff of the previos to the current using git somehow and print the dif in a super readable and user friendly way.
|
|
380
|
+
|
|
381
|
+
1. Color-code the diff lines
|
|
382
|
+
2. Add context lines (show unchanged code around changes)
|
|
383
|
+
get rid of what comes after this /Users/aidandyga/Downloads/SeniorThesis/pysealer/examples/math_operations.py:
|
|
384
|
+
specifically1/5 decorators failed or 7 decorators valid. i dont need this showing up
|
|
385
|
+
|
|
386
|
+
ex.
|
|
387
|
+
2 decorators failed in 2 files:
|
|
388
|
+
✗ /Users/aidandyga/Downloads/SeniorThesis/pysealer/examples/math_operations.py: 1/5 decorators failed
|
|
389
|
+
✓ /Users/aidandyga/Downloads/SeniorThesis/pysealer/examples/text_processing.py: 7 decorators valid
|
|
390
|
+
✗ /Users/aidandyga/Downloads/SeniorThesis/pysealer/examples/fibonacci.py: 1/1 decorator failed
|
|
391
|
+
'''
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Defines dummy decorators for all decorators found in the target file."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import ast
|
|
5
|
+
import inspect
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _dummy_decorator(func=None, *args, **kwargs):
|
|
9
|
+
"""
|
|
10
|
+
A no-op (dummy) decorator that can be used in place of any decorator.
|
|
11
|
+
|
|
12
|
+
Handles both @deco and @deco(...) usages. If used as @deco, it returns the function unchanged.
|
|
13
|
+
If used as @deco(...), it returns a wrapper that returns the function unchanged.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
func (callable, optional): The function to decorate, or None if called with arguments.
|
|
17
|
+
*args: Positional arguments (ignored).
|
|
18
|
+
**kwargs: Keyword arguments (ignored).
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
callable: The original function, unchanged.
|
|
22
|
+
"""
|
|
23
|
+
if callable(func) and not args and not kwargs:
|
|
24
|
+
return func
|
|
25
|
+
def wrapper(f):
|
|
26
|
+
return f
|
|
27
|
+
return wrapper
|
|
28
|
+
|
|
29
|
+
def _discover_decorators(file_path):
|
|
30
|
+
"""
|
|
31
|
+
Yield all decorator names used in the given Python file.
|
|
32
|
+
|
|
33
|
+
This function parses the specified Python file and walks its AST to find all decorator names
|
|
34
|
+
used on functions, async functions, and classes. It handles decorators used as @deco, @deco(...),
|
|
35
|
+
and @obj.deco or @obj.deco(...).
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
file_path (str): Path to the Python file to scan.
|
|
39
|
+
|
|
40
|
+
Yields:
|
|
41
|
+
str: The name of each decorator found.
|
|
42
|
+
"""
|
|
43
|
+
if not os.path.exists(file_path):
|
|
44
|
+
return
|
|
45
|
+
with open(file_path, "r") as f:
|
|
46
|
+
src = f.read()
|
|
47
|
+
try:
|
|
48
|
+
tree = ast.parse(src)
|
|
49
|
+
except Exception:
|
|
50
|
+
return
|
|
51
|
+
for node in ast.walk(tree):
|
|
52
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
53
|
+
for deco in node.decorator_list:
|
|
54
|
+
# Handles @deco or @deco(...)
|
|
55
|
+
if isinstance(deco, ast.Name):
|
|
56
|
+
yield deco.id
|
|
57
|
+
elif isinstance(deco, ast.Attribute):
|
|
58
|
+
yield deco.attr
|
|
59
|
+
elif isinstance(deco.func, ast.Name):
|
|
60
|
+
yield deco.func.id
|
|
61
|
+
elif isinstance(deco.func, ast.Attribute):
|
|
62
|
+
yield deco.func.attr
|
|
63
|
+
|
|
64
|
+
def _get_caller_file():
|
|
65
|
+
"""
|
|
66
|
+
Find the filename of the module that imported this module at the top level.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
str or None: The filename of the caller module, or None if not found.
|
|
70
|
+
"""
|
|
71
|
+
stack = inspect.stack()
|
|
72
|
+
for frame in stack:
|
|
73
|
+
if frame.function == "<module>" and frame.filename != __file__:
|
|
74
|
+
return frame.filename
|
|
75
|
+
|
|
76
|
+
# Main logic: define dummy decorators for all found in the caller file
|
|
77
|
+
_CALLER_FILE = _get_caller_file()
|
|
78
|
+
if _CALLER_FILE:
|
|
79
|
+
_seen = set()
|
|
80
|
+
for deco_name in _discover_decorators(_CALLER_FILE):
|
|
81
|
+
if deco_name and deco_name not in globals() and deco_name not in _seen:
|
|
82
|
+
globals()[deco_name] = _dummy_decorator
|
|
83
|
+
_seen.add(deco_name)
|
pysealer/git_diff.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Git-based diff functionality for comparing function/class changes."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, Tuple, List
|
|
7
|
+
import difflib
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_file_from_git(file_path: str, ref: str = "HEAD") -> Optional[str]:
|
|
11
|
+
"""
|
|
12
|
+
Retrieve file content from a specific git reference.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
file_path: Absolute path to the file
|
|
16
|
+
ref: Git reference (default: HEAD)
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
File content as string, or None if not in git or error occurs
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
# Get relative path from git root
|
|
23
|
+
result = subprocess.run(
|
|
24
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
25
|
+
cwd=Path(file_path).parent,
|
|
26
|
+
capture_output=True,
|
|
27
|
+
text=True,
|
|
28
|
+
timeout=5
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if result.returncode != 0:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
git_root = result.stdout.strip()
|
|
35
|
+
relative_path = Path(file_path).relative_to(git_root)
|
|
36
|
+
|
|
37
|
+
# Get file content from git
|
|
38
|
+
result = subprocess.run(
|
|
39
|
+
["git", "show", f"{ref}:{relative_path}"],
|
|
40
|
+
cwd=git_root,
|
|
41
|
+
capture_output=True,
|
|
42
|
+
text=True,
|
|
43
|
+
timeout=5
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if result.returncode == 0:
|
|
47
|
+
return result.stdout
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
except (subprocess.TimeoutExpired, subprocess.SubprocessError, ValueError, OSError):
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def extract_function_from_source(source_code: str, function_name: str) -> Optional[Tuple[str, int]]:
|
|
55
|
+
"""
|
|
56
|
+
Extract a specific function or class from source code.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
source_code: Python source code
|
|
60
|
+
function_name: Name of function/class to extract
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Tuple of (function_source, start_line) or None if not found
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
tree = ast.parse(source_code)
|
|
67
|
+
lines = source_code.splitlines(keepends=True)
|
|
68
|
+
|
|
69
|
+
for node in ast.walk(tree):
|
|
70
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
71
|
+
if node.name == function_name:
|
|
72
|
+
# Get the source lines for this node
|
|
73
|
+
start_line = node.lineno
|
|
74
|
+
end_line = node.end_lineno if node.end_lineno else start_line
|
|
75
|
+
|
|
76
|
+
function_lines = lines[start_line - 1:end_line]
|
|
77
|
+
function_source = ''.join(function_lines)
|
|
78
|
+
|
|
79
|
+
return function_source, start_line
|
|
80
|
+
|
|
81
|
+
return None
|
|
82
|
+
except (SyntaxError, AttributeError):
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def generate_function_diff(
|
|
87
|
+
old_source: str,
|
|
88
|
+
new_source: str,
|
|
89
|
+
function_name: str,
|
|
90
|
+
old_start_line: int,
|
|
91
|
+
new_start_line: int,
|
|
92
|
+
context_lines: int = 2
|
|
93
|
+
) -> List[Tuple[str, str, int]]:
|
|
94
|
+
"""
|
|
95
|
+
Generate a unified diff for a specific function.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
old_source: Old function source code
|
|
99
|
+
new_source: New function source code
|
|
100
|
+
function_name: Name of the function/class
|
|
101
|
+
old_start_line: Starting line number in old file
|
|
102
|
+
new_start_line: Starting line number in new file
|
|
103
|
+
context_lines: Number of context lines to show
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
List of tuples: (diff_type, line_content, line_number)
|
|
107
|
+
where diff_type is ' ', '-', or '+'
|
|
108
|
+
"""
|
|
109
|
+
old_lines = old_source.splitlines(keepends=False)
|
|
110
|
+
new_lines = new_source.splitlines(keepends=False)
|
|
111
|
+
|
|
112
|
+
# Generate unified diff
|
|
113
|
+
diff = list(difflib.unified_diff(
|
|
114
|
+
old_lines,
|
|
115
|
+
new_lines,
|
|
116
|
+
lineterm='',
|
|
117
|
+
n=context_lines
|
|
118
|
+
))
|
|
119
|
+
|
|
120
|
+
# Skip the header lines (first 3 lines of unified diff)
|
|
121
|
+
if len(diff) > 3:
|
|
122
|
+
diff = diff[3:]
|
|
123
|
+
|
|
124
|
+
result = []
|
|
125
|
+
current_old_line = old_start_line
|
|
126
|
+
current_new_line = new_start_line
|
|
127
|
+
|
|
128
|
+
for line in diff:
|
|
129
|
+
if not line:
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
prefix = line[0]
|
|
133
|
+
content = line[1:] if len(line) > 1 else ''
|
|
134
|
+
|
|
135
|
+
if prefix == '-':
|
|
136
|
+
result.append(('-', content, current_old_line))
|
|
137
|
+
current_old_line += 1
|
|
138
|
+
elif prefix == '+':
|
|
139
|
+
result.append(('+', content, current_new_line))
|
|
140
|
+
current_new_line += 1
|
|
141
|
+
elif prefix == ' ':
|
|
142
|
+
result.append((' ', content, current_new_line))
|
|
143
|
+
current_old_line += 1
|
|
144
|
+
current_new_line += 1
|
|
145
|
+
elif prefix == '@':
|
|
146
|
+
# Parse line numbers from @@ -old_start,old_count +new_start,new_count @@
|
|
147
|
+
try:
|
|
148
|
+
parts = line.split()
|
|
149
|
+
if len(parts) >= 3:
|
|
150
|
+
old_part = parts[1].lstrip('-')
|
|
151
|
+
new_part = parts[2].lstrip('+')
|
|
152
|
+
|
|
153
|
+
if ',' in old_part:
|
|
154
|
+
current_old_line = int(old_part.split(',')[0])
|
|
155
|
+
else:
|
|
156
|
+
current_old_line = int(old_part)
|
|
157
|
+
|
|
158
|
+
if ',' in new_part:
|
|
159
|
+
current_new_line = int(new_part.split(',')[0])
|
|
160
|
+
else:
|
|
161
|
+
current_new_line = int(new_part)
|
|
162
|
+
except (ValueError, IndexError):
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
return result
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def get_function_diff(
|
|
169
|
+
file_path: str,
|
|
170
|
+
function_name: str,
|
|
171
|
+
new_source: str,
|
|
172
|
+
new_start_line: int
|
|
173
|
+
) -> Optional[List[Tuple[str, str, int]]]:
|
|
174
|
+
"""
|
|
175
|
+
Get the diff for a specific function comparing current version to git HEAD.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
file_path: Absolute path to the Python file
|
|
179
|
+
function_name: Name of the function/class
|
|
180
|
+
new_source: Current source code of the function
|
|
181
|
+
new_start_line: Starting line number of function in current file
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
List of diff tuples or None if git history unavailable
|
|
185
|
+
"""
|
|
186
|
+
# Get the file from git
|
|
187
|
+
old_file_content = get_file_from_git(file_path)
|
|
188
|
+
|
|
189
|
+
if not old_file_content:
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
# Extract the old version of the function
|
|
193
|
+
old_function = extract_function_from_source(old_file_content, function_name)
|
|
194
|
+
|
|
195
|
+
if not old_function:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
old_source, old_start_line = old_function
|
|
199
|
+
|
|
200
|
+
# Generate the diff
|
|
201
|
+
diff = generate_function_diff(
|
|
202
|
+
old_source,
|
|
203
|
+
new_source,
|
|
204
|
+
function_name,
|
|
205
|
+
old_start_line,
|
|
206
|
+
new_start_line,
|
|
207
|
+
context_lines=2
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return diff if diff else None
|