fishertools 0.2.1__py3-none-any.whl → 0.4.0__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.
- fishertools/__init__.py +16 -5
- fishertools/errors/__init__.py +11 -3
- fishertools/errors/exception_types.py +282 -0
- fishertools/errors/explainer.py +87 -1
- fishertools/errors/models.py +73 -1
- fishertools/errors/patterns.py +40 -0
- fishertools/examples/cli_example.py +156 -0
- fishertools/examples/learn_example.py +65 -0
- fishertools/examples/logger_example.py +176 -0
- fishertools/examples/menu_example.py +101 -0
- fishertools/examples/storage_example.py +175 -0
- fishertools/input_utils.py +185 -0
- fishertools/learn/__init__.py +19 -2
- fishertools/learn/examples.py +88 -1
- fishertools/learn/knowledge_engine.py +321 -0
- fishertools/learn/repl/__init__.py +19 -0
- fishertools/learn/repl/cli.py +31 -0
- fishertools/learn/repl/code_sandbox.py +229 -0
- fishertools/learn/repl/command_handler.py +544 -0
- fishertools/learn/repl/command_parser.py +165 -0
- fishertools/learn/repl/engine.py +479 -0
- fishertools/learn/repl/models.py +121 -0
- fishertools/learn/repl/session_manager.py +284 -0
- fishertools/learn/repl/test_code_sandbox.py +261 -0
- fishertools/learn/repl/test_code_sandbox_pbt.py +148 -0
- fishertools/learn/repl/test_command_handler.py +224 -0
- fishertools/learn/repl/test_command_handler_pbt.py +189 -0
- fishertools/learn/repl/test_command_parser.py +160 -0
- fishertools/learn/repl/test_command_parser_pbt.py +100 -0
- fishertools/learn/repl/test_engine.py +190 -0
- fishertools/learn/repl/test_session_manager.py +310 -0
- fishertools/learn/repl/test_session_manager_pbt.py +182 -0
- fishertools/learn/test_knowledge_engine.py +241 -0
- fishertools/learn/test_knowledge_engine_pbt.py +180 -0
- fishertools/patterns/__init__.py +46 -0
- fishertools/patterns/cli.py +175 -0
- fishertools/patterns/logger.py +140 -0
- fishertools/patterns/menu.py +99 -0
- fishertools/patterns/storage.py +127 -0
- fishertools/readme_transformer.py +631 -0
- fishertools/safe/__init__.py +6 -1
- fishertools/safe/files.py +329 -1
- fishertools/transform_readme.py +105 -0
- fishertools-0.4.0.dist-info/METADATA +104 -0
- fishertools-0.4.0.dist-info/RECORD +131 -0
- {fishertools-0.2.1.dist-info → fishertools-0.4.0.dist-info}/WHEEL +1 -1
- tests/test_documentation_properties.py +329 -0
- tests/test_documentation_structure.py +349 -0
- tests/test_errors/test_exception_types.py +446 -0
- tests/test_errors/test_exception_types_pbt.py +333 -0
- tests/test_errors/test_patterns.py +52 -0
- tests/test_input_utils/__init__.py +1 -0
- tests/test_input_utils/test_input_utils.py +65 -0
- tests/test_learn/test_examples.py +179 -1
- tests/test_learn/test_explain_properties.py +307 -0
- tests/test_patterns_cli.py +611 -0
- tests/test_patterns_docstrings.py +473 -0
- tests/test_patterns_logger.py +465 -0
- tests/test_patterns_menu.py +440 -0
- tests/test_patterns_storage.py +447 -0
- tests/test_readme_enhancements_v0_3_1.py +2036 -0
- tests/test_readme_transformer/__init__.py +1 -0
- tests/test_readme_transformer/test_readme_infrastructure.py +1023 -0
- tests/test_readme_transformer/test_transform_readme_integration.py +431 -0
- tests/test_safe/test_files.py +726 -1
- fishertools-0.2.1.dist-info/METADATA +0 -256
- fishertools-0.2.1.dist-info/RECORD +0 -81
- {fishertools-0.2.1.dist-info → fishertools-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {fishertools-0.2.1.dist-info → fishertools-0.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
"""
|
|
2
|
+
README transformation infrastructure for fishertools.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for parsing, transforming, and validating
|
|
5
|
+
the README file structure while preserving existing content.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import shutil
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List, Optional, Tuple
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ReadmeSection:
|
|
17
|
+
"""Represents a section of the README document."""
|
|
18
|
+
|
|
19
|
+
title: Optional[str]
|
|
20
|
+
content: str
|
|
21
|
+
level: int = 0 # Heading level (0 for no heading)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class FeatureEntry:
|
|
26
|
+
"""Represents a single feature entry in the feature table."""
|
|
27
|
+
|
|
28
|
+
task: str # Russian task description
|
|
29
|
+
function: str # Function to call
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ReadmeParser:
|
|
33
|
+
"""Parses and extracts sections from README markdown files."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, readme_path: str) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Initialize the parser with a README file path.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
readme_path: Path to the README.md file
|
|
41
|
+
"""
|
|
42
|
+
self.readme_path = Path(readme_path)
|
|
43
|
+
self.content: str = ""
|
|
44
|
+
self.sections: List[ReadmeSection] = []
|
|
45
|
+
|
|
46
|
+
def read_file(self) -> str:
|
|
47
|
+
"""
|
|
48
|
+
Read the README file content.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
The content of the README file
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
FileNotFoundError: If the README file does not exist
|
|
55
|
+
IOError: If the file cannot be read
|
|
56
|
+
"""
|
|
57
|
+
if not self.readme_path.exists():
|
|
58
|
+
raise FileNotFoundError(f"README file not found: {self.readme_path}")
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
self.content = self.readme_path.read_text(encoding="utf-8")
|
|
62
|
+
return self.content
|
|
63
|
+
except IOError as e:
|
|
64
|
+
raise IOError(f"Failed to read README file: {e}")
|
|
65
|
+
|
|
66
|
+
def extract_first_sentence(self) -> str:
|
|
67
|
+
"""
|
|
68
|
+
Extract the first introductory sentence from the README.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
The first sentence of the README content
|
|
72
|
+
"""
|
|
73
|
+
lines = self.content.strip().split("\n")
|
|
74
|
+
for line in lines:
|
|
75
|
+
line = line.strip()
|
|
76
|
+
if line and not line.startswith("#"):
|
|
77
|
+
# Extract just the first sentence (up to period, exclamation, or question mark)
|
|
78
|
+
sentence = line.split(".")[0].split("!")[0].split("?")[0]
|
|
79
|
+
if sentence.strip():
|
|
80
|
+
return sentence.strip()
|
|
81
|
+
return ""
|
|
82
|
+
|
|
83
|
+
def parse_sections(self) -> List[ReadmeSection]:
|
|
84
|
+
"""
|
|
85
|
+
Parse README content into sections.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
List of ReadmeSection objects representing document structure
|
|
89
|
+
"""
|
|
90
|
+
self.sections = []
|
|
91
|
+
lines = self.content.split("\n")
|
|
92
|
+
current_section = ReadmeSection(title=None, content="", level=0)
|
|
93
|
+
|
|
94
|
+
for line in lines:
|
|
95
|
+
if line.startswith("#"):
|
|
96
|
+
# Save previous section if it has content
|
|
97
|
+
if current_section.content.strip():
|
|
98
|
+
self.sections.append(current_section)
|
|
99
|
+
|
|
100
|
+
# Create new section
|
|
101
|
+
level = len(line) - len(line.lstrip("#"))
|
|
102
|
+
title = line.lstrip("#").strip()
|
|
103
|
+
current_section = ReadmeSection(
|
|
104
|
+
title=title, content="", level=level
|
|
105
|
+
)
|
|
106
|
+
else:
|
|
107
|
+
current_section.content += line + "\n"
|
|
108
|
+
|
|
109
|
+
# Add final section
|
|
110
|
+
if current_section.content.strip():
|
|
111
|
+
self.sections.append(current_section)
|
|
112
|
+
|
|
113
|
+
return self.sections
|
|
114
|
+
|
|
115
|
+
def extract_detailed_content(self) -> str:
|
|
116
|
+
"""
|
|
117
|
+
Extract all content after the first introductory sentence.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
The detailed content sections of the README
|
|
121
|
+
"""
|
|
122
|
+
lines = self.content.strip().split("\n")
|
|
123
|
+
first_sentence_found = False
|
|
124
|
+
detailed_lines = []
|
|
125
|
+
|
|
126
|
+
for line in lines:
|
|
127
|
+
if not first_sentence_found:
|
|
128
|
+
line_stripped = line.strip()
|
|
129
|
+
if line_stripped and not line_stripped.startswith("#"):
|
|
130
|
+
# Skip the first sentence line
|
|
131
|
+
first_sentence_found = True
|
|
132
|
+
continue
|
|
133
|
+
detailed_lines.append(line)
|
|
134
|
+
|
|
135
|
+
return "\n".join(detailed_lines).strip()
|
|
136
|
+
|
|
137
|
+
def identify_feature_descriptions(self) -> List[Tuple[str, str]]:
|
|
138
|
+
"""
|
|
139
|
+
Identify and extract feature descriptions from the README.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
List of tuples containing (feature_name, feature_description)
|
|
143
|
+
"""
|
|
144
|
+
features = []
|
|
145
|
+
lines = self.content.split("\n")
|
|
146
|
+
|
|
147
|
+
for i, line in enumerate(lines):
|
|
148
|
+
# Look for lines that might be feature descriptions
|
|
149
|
+
if "explain_error" in line.lower() or "safe_read_file" in line.lower():
|
|
150
|
+
# Extract the feature and its description
|
|
151
|
+
feature_name = line.strip()
|
|
152
|
+
description = ""
|
|
153
|
+
|
|
154
|
+
# Collect following lines as description
|
|
155
|
+
j = i + 1
|
|
156
|
+
while j < len(lines) and lines[j].strip() and not lines[j].startswith("#"):
|
|
157
|
+
description += lines[j] + " "
|
|
158
|
+
j += 1
|
|
159
|
+
|
|
160
|
+
if feature_name:
|
|
161
|
+
features.append((feature_name, description.strip()))
|
|
162
|
+
|
|
163
|
+
return features
|
|
164
|
+
|
|
165
|
+
def identify_introduction_boundary(self) -> int:
|
|
166
|
+
"""
|
|
167
|
+
Identify the line number where the introduction ends.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Line number where detailed content begins
|
|
171
|
+
"""
|
|
172
|
+
lines = self.content.strip().split("\n")
|
|
173
|
+
first_sentence_found = False
|
|
174
|
+
|
|
175
|
+
for i, line in enumerate(lines):
|
|
176
|
+
line_stripped = line.strip()
|
|
177
|
+
|
|
178
|
+
# Skip empty lines at the beginning
|
|
179
|
+
if not line_stripped:
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
# Skip headings
|
|
183
|
+
if line_stripped.startswith("#"):
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
# First non-heading, non-empty line is the introduction
|
|
187
|
+
if not first_sentence_found:
|
|
188
|
+
first_sentence_found = True
|
|
189
|
+
# Return the index after the introduction line
|
|
190
|
+
return i + 1
|
|
191
|
+
|
|
192
|
+
return 0
|
|
193
|
+
|
|
194
|
+
def identify_feature_list_section(self) -> Optional[Tuple[int, int]]:
|
|
195
|
+
"""
|
|
196
|
+
Identify the boundaries of any existing feature list section.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Tuple of (start_line, end_line) for feature list, or None if not found
|
|
200
|
+
"""
|
|
201
|
+
lines = self.content.split("\n")
|
|
202
|
+
feature_section_start = None
|
|
203
|
+
feature_section_end = None
|
|
204
|
+
|
|
205
|
+
for i, line in enumerate(lines):
|
|
206
|
+
# Look for feature-related keywords
|
|
207
|
+
if any(
|
|
208
|
+
keyword in line.lower()
|
|
209
|
+
for keyword in [
|
|
210
|
+
"возможности",
|
|
211
|
+
"features",
|
|
212
|
+
"функции",
|
|
213
|
+
"capabilities",
|
|
214
|
+
]
|
|
215
|
+
):
|
|
216
|
+
if line.startswith("#"):
|
|
217
|
+
feature_section_start = i
|
|
218
|
+
# Find the end of this section (next heading or end of file)
|
|
219
|
+
for j in range(i + 1, len(lines)):
|
|
220
|
+
if lines[j].startswith("#"):
|
|
221
|
+
feature_section_end = j
|
|
222
|
+
break
|
|
223
|
+
if feature_section_end is None:
|
|
224
|
+
feature_section_end = len(lines)
|
|
225
|
+
return (feature_section_start, feature_section_end)
|
|
226
|
+
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class BackupManager:
|
|
231
|
+
"""Manages backup and recovery of README files."""
|
|
232
|
+
|
|
233
|
+
def __init__(self, readme_path: str, backup_dir: str = ".readme_backups") -> None:
|
|
234
|
+
"""
|
|
235
|
+
Initialize the backup manager.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
readme_path: Path to the README.md file
|
|
239
|
+
backup_dir: Directory to store backups
|
|
240
|
+
"""
|
|
241
|
+
self.readme_path = Path(readme_path)
|
|
242
|
+
self.backup_dir = Path(backup_dir)
|
|
243
|
+
|
|
244
|
+
def create_backup(self) -> Path:
|
|
245
|
+
"""
|
|
246
|
+
Create a timestamped backup of the README file.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Path to the created backup file
|
|
250
|
+
|
|
251
|
+
Raises:
|
|
252
|
+
FileNotFoundError: If the README file does not exist
|
|
253
|
+
IOError: If backup cannot be created
|
|
254
|
+
"""
|
|
255
|
+
if not self.readme_path.exists():
|
|
256
|
+
raise FileNotFoundError(f"README file not found: {self.readme_path}")
|
|
257
|
+
|
|
258
|
+
# Create backup directory if it doesn't exist
|
|
259
|
+
self.backup_dir.mkdir(exist_ok=True)
|
|
260
|
+
|
|
261
|
+
# Generate timestamped backup filename
|
|
262
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
263
|
+
backup_path = self.backup_dir / f"README_backup_{timestamp}.md"
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
shutil.copy2(self.readme_path, backup_path)
|
|
267
|
+
return backup_path
|
|
268
|
+
except IOError as e:
|
|
269
|
+
raise IOError(f"Failed to create backup: {e}")
|
|
270
|
+
|
|
271
|
+
def list_backups(self) -> List[Path]:
|
|
272
|
+
"""
|
|
273
|
+
List all available backup files.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
List of backup file paths, sorted by creation time (newest first)
|
|
277
|
+
"""
|
|
278
|
+
if not self.backup_dir.exists():
|
|
279
|
+
return []
|
|
280
|
+
|
|
281
|
+
backups = sorted(
|
|
282
|
+
self.backup_dir.glob("README_backup_*.md"),
|
|
283
|
+
key=lambda p: p.stat().st_mtime,
|
|
284
|
+
reverse=True,
|
|
285
|
+
)
|
|
286
|
+
return backups
|
|
287
|
+
|
|
288
|
+
def restore_backup(self, backup_path: Path) -> None:
|
|
289
|
+
"""
|
|
290
|
+
Restore README from a backup file.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
backup_path: Path to the backup file to restore
|
|
294
|
+
|
|
295
|
+
Raises:
|
|
296
|
+
FileNotFoundError: If the backup file does not exist
|
|
297
|
+
IOError: If restoration fails
|
|
298
|
+
"""
|
|
299
|
+
if not backup_path.exists():
|
|
300
|
+
raise FileNotFoundError(f"Backup file not found: {backup_path}")
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
shutil.copy2(backup_path, self.readme_path)
|
|
304
|
+
except IOError as e:
|
|
305
|
+
raise IOError(f"Failed to restore backup: {e}")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class ReadmeStructure:
|
|
309
|
+
"""Defines and manages the structure of the transformed README."""
|
|
310
|
+
|
|
311
|
+
def __init__(self) -> None:
|
|
312
|
+
"""Initialize the README structure."""
|
|
313
|
+
self.introduction: str = ""
|
|
314
|
+
self.installation_block: str = ""
|
|
315
|
+
self.feature_table: str = ""
|
|
316
|
+
self.target_audience: str = ""
|
|
317
|
+
self.existing_content: str = ""
|
|
318
|
+
|
|
319
|
+
def set_introduction(self, text: str) -> None:
|
|
320
|
+
"""Set the introduction section."""
|
|
321
|
+
self.introduction = text
|
|
322
|
+
|
|
323
|
+
def set_installation_block(self, command: str = "pip install fishertools") -> None:
|
|
324
|
+
"""
|
|
325
|
+
Set the installation block.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
command: The installation command to display
|
|
329
|
+
"""
|
|
330
|
+
self.installation_block = f"```bash\n{command}\n```"
|
|
331
|
+
|
|
332
|
+
def set_feature_table(self, features: Optional[List[FeatureEntry]] = None) -> None:
|
|
333
|
+
"""
|
|
334
|
+
Set the feature table from a list of feature entries.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
features: List of FeatureEntry objects. If None, uses default features.
|
|
338
|
+
"""
|
|
339
|
+
if features is None:
|
|
340
|
+
features = self._get_default_features()
|
|
341
|
+
|
|
342
|
+
table_lines = ["| Задача | Что вызвать |", "|--------|-------------|"]
|
|
343
|
+
|
|
344
|
+
for feature in features:
|
|
345
|
+
table_lines.append(f"| {feature.task} | {feature.function} |")
|
|
346
|
+
|
|
347
|
+
self.feature_table = "\n".join(table_lines)
|
|
348
|
+
|
|
349
|
+
@staticmethod
|
|
350
|
+
def _get_default_features() -> List[FeatureEntry]:
|
|
351
|
+
"""
|
|
352
|
+
Get the default feature entries for the feature table.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
List of default FeatureEntry objects
|
|
356
|
+
"""
|
|
357
|
+
return [
|
|
358
|
+
FeatureEntry("Объяснить ошибку", "explain_error(e)"),
|
|
359
|
+
FeatureEntry("Красиво показать traceback", "explain_error(e)"),
|
|
360
|
+
FeatureEntry("Безопасно читать файл", "safe_read_file(path)"),
|
|
361
|
+
]
|
|
362
|
+
|
|
363
|
+
def set_target_audience(
|
|
364
|
+
self,
|
|
365
|
+
title: str = "Для кого эта библиотека",
|
|
366
|
+
bullets: Optional[List[str]] = None,
|
|
367
|
+
) -> None:
|
|
368
|
+
"""
|
|
369
|
+
Set the target audience section.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
title: Section title
|
|
373
|
+
bullets: List of bullet points
|
|
374
|
+
"""
|
|
375
|
+
if bullets is None:
|
|
376
|
+
bullets = [
|
|
377
|
+
"Ты только начал изучать Python",
|
|
378
|
+
"Сообщения об ошибках кажутся страшными и непонятными",
|
|
379
|
+
"Хочешь, чтобы ошибки объяснялись на нормальном русском с примерами",
|
|
380
|
+
]
|
|
381
|
+
|
|
382
|
+
lines = [f"## {title}", ""]
|
|
383
|
+
for bullet in bullets:
|
|
384
|
+
lines.append(f"- {bullet}")
|
|
385
|
+
|
|
386
|
+
self.target_audience = "\n".join(lines)
|
|
387
|
+
|
|
388
|
+
def set_existing_content(self, content: str) -> None:
|
|
389
|
+
"""Set the existing detailed content."""
|
|
390
|
+
self.existing_content = content
|
|
391
|
+
|
|
392
|
+
def assemble(self) -> str:
|
|
393
|
+
"""
|
|
394
|
+
Assemble all sections into the final README content.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
The complete transformed README content
|
|
398
|
+
"""
|
|
399
|
+
sections = [
|
|
400
|
+
self.introduction,
|
|
401
|
+
"",
|
|
402
|
+
self.installation_block,
|
|
403
|
+
"",
|
|
404
|
+
self.feature_table,
|
|
405
|
+
"",
|
|
406
|
+
self.target_audience,
|
|
407
|
+
"",
|
|
408
|
+
self.existing_content,
|
|
409
|
+
]
|
|
410
|
+
|
|
411
|
+
# Filter out empty sections and join
|
|
412
|
+
content = "\n".join(s for s in sections if s)
|
|
413
|
+
return content
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class MarkdownValidator:
|
|
417
|
+
"""Validates markdown syntax and structure."""
|
|
418
|
+
|
|
419
|
+
@staticmethod
|
|
420
|
+
def validate_markdown_syntax(content: str) -> Tuple[bool, Optional[str]]:
|
|
421
|
+
"""
|
|
422
|
+
Validate basic markdown syntax.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
content: The markdown content to validate
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Tuple of (is_valid, error_message)
|
|
429
|
+
"""
|
|
430
|
+
lines = content.split("\n")
|
|
431
|
+
code_block_open = False
|
|
432
|
+
code_block_marker = None
|
|
433
|
+
|
|
434
|
+
for i, line in enumerate(lines, 1):
|
|
435
|
+
# Check for code block markers
|
|
436
|
+
if line.strip().startswith("```"):
|
|
437
|
+
if not code_block_open:
|
|
438
|
+
code_block_open = True
|
|
439
|
+
code_block_marker = line.strip()
|
|
440
|
+
else:
|
|
441
|
+
code_block_open = False
|
|
442
|
+
|
|
443
|
+
# Check for unmatched brackets in links
|
|
444
|
+
if "[" in line and "]" not in line:
|
|
445
|
+
# This is a simple check; more complex validation could be added
|
|
446
|
+
pass
|
|
447
|
+
|
|
448
|
+
if code_block_open:
|
|
449
|
+
return False, "Unclosed code block detected"
|
|
450
|
+
|
|
451
|
+
return True, None
|
|
452
|
+
|
|
453
|
+
@staticmethod
|
|
454
|
+
def validate_document_structure(content: str) -> Tuple[bool, Optional[str]]:
|
|
455
|
+
"""
|
|
456
|
+
Validate document structure ordering.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
content: The markdown content to validate
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
Tuple of (is_valid, error_message)
|
|
463
|
+
"""
|
|
464
|
+
lines = content.split("\n")
|
|
465
|
+
|
|
466
|
+
# Find key sections
|
|
467
|
+
intro_idx = None
|
|
468
|
+
install_idx = None
|
|
469
|
+
feature_table_idx = None
|
|
470
|
+
target_audience_idx = None
|
|
471
|
+
|
|
472
|
+
for i, line in enumerate(lines):
|
|
473
|
+
if intro_idx is None and line.strip() and not line.startswith("#"):
|
|
474
|
+
intro_idx = i
|
|
475
|
+
if "```bash" in line and install_idx is None:
|
|
476
|
+
install_idx = i
|
|
477
|
+
if "Задача" in line and "Что вызвать" in line and feature_table_idx is None:
|
|
478
|
+
feature_table_idx = i
|
|
479
|
+
if "Для кого эта библиотека" in line and target_audience_idx is None:
|
|
480
|
+
target_audience_idx = i
|
|
481
|
+
|
|
482
|
+
# Validate ordering
|
|
483
|
+
indices = [
|
|
484
|
+
(intro_idx, "introduction"),
|
|
485
|
+
(install_idx, "installation block"),
|
|
486
|
+
(feature_table_idx, "feature table"),
|
|
487
|
+
(target_audience_idx, "target audience"),
|
|
488
|
+
]
|
|
489
|
+
|
|
490
|
+
valid_indices = [(idx, name) for idx, name in indices if idx is not None]
|
|
491
|
+
|
|
492
|
+
for i in range(len(valid_indices) - 1):
|
|
493
|
+
if valid_indices[i][0] > valid_indices[i + 1][0]:
|
|
494
|
+
return (
|
|
495
|
+
False,
|
|
496
|
+
f"Section ordering error: {valid_indices[i][1]} appears after {valid_indices[i + 1][1]}",
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
return True, None
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
class ReadmeTransformer:
|
|
503
|
+
"""Main transformer class that orchestrates README transformation."""
|
|
504
|
+
|
|
505
|
+
def __init__(self, readme_path: str) -> None:
|
|
506
|
+
"""
|
|
507
|
+
Initialize the transformer.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
readme_path: Path to the README.md file
|
|
511
|
+
"""
|
|
512
|
+
self.readme_path = readme_path
|
|
513
|
+
self.parser = ReadmeParser(readme_path)
|
|
514
|
+
self.backup_manager = BackupManager(readme_path)
|
|
515
|
+
self.structure = ReadmeStructure()
|
|
516
|
+
self.validator = MarkdownValidator()
|
|
517
|
+
|
|
518
|
+
def validate_readme_exists(self) -> bool:
|
|
519
|
+
"""
|
|
520
|
+
Validate that the README file exists.
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
True if README exists, False otherwise
|
|
524
|
+
"""
|
|
525
|
+
return Path(self.readme_path).exists()
|
|
526
|
+
|
|
527
|
+
def parse_readme(self) -> None:
|
|
528
|
+
"""Parse the README file and extract sections."""
|
|
529
|
+
self.parser.read_file()
|
|
530
|
+
self.parser.parse_sections()
|
|
531
|
+
|
|
532
|
+
def extract_content(self) -> Tuple[str, str]:
|
|
533
|
+
"""
|
|
534
|
+
Extract introduction and existing content from README.
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
Tuple of (introduction, existing_content)
|
|
538
|
+
"""
|
|
539
|
+
# Ensure parser has read the file
|
|
540
|
+
if not self.parser.content:
|
|
541
|
+
self.parser.read_file()
|
|
542
|
+
|
|
543
|
+
introduction = self.parser.extract_first_sentence()
|
|
544
|
+
existing_content = self.parser.content
|
|
545
|
+
|
|
546
|
+
return introduction, existing_content
|
|
547
|
+
|
|
548
|
+
def create_backup(self) -> Path:
|
|
549
|
+
"""
|
|
550
|
+
Create a backup of the original README.
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
Path to the created backup file
|
|
554
|
+
"""
|
|
555
|
+
return self.backup_manager.create_backup()
|
|
556
|
+
|
|
557
|
+
def transform(
|
|
558
|
+
self,
|
|
559
|
+
features: Optional[List[FeatureEntry]] = None,
|
|
560
|
+
target_audience_bullets: Optional[List[str]] = None,
|
|
561
|
+
) -> str:
|
|
562
|
+
"""
|
|
563
|
+
Transform the README with new structure.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
features: List of features for the feature table. If None, uses default features.
|
|
567
|
+
target_audience_bullets: Custom target audience bullet points
|
|
568
|
+
|
|
569
|
+
Returns:
|
|
570
|
+
The transformed README content
|
|
571
|
+
"""
|
|
572
|
+
# Parse the original README
|
|
573
|
+
self.parse_readme()
|
|
574
|
+
|
|
575
|
+
# Extract content
|
|
576
|
+
introduction, existing_content = self.extract_content()
|
|
577
|
+
|
|
578
|
+
# Set up structure
|
|
579
|
+
self.structure.set_introduction(introduction)
|
|
580
|
+
self.structure.set_installation_block()
|
|
581
|
+
|
|
582
|
+
# Set feature table (uses default if not provided)
|
|
583
|
+
self.structure.set_feature_table(features)
|
|
584
|
+
|
|
585
|
+
# Set target audience
|
|
586
|
+
self.structure.set_target_audience(bullets=target_audience_bullets)
|
|
587
|
+
|
|
588
|
+
# Set existing content
|
|
589
|
+
self.structure.set_existing_content(existing_content)
|
|
590
|
+
|
|
591
|
+
# Assemble and return
|
|
592
|
+
return self.structure.assemble()
|
|
593
|
+
|
|
594
|
+
def write_transformed_readme(self, content: str) -> None:
|
|
595
|
+
"""
|
|
596
|
+
Write the transformed content to the README file.
|
|
597
|
+
|
|
598
|
+
Args:
|
|
599
|
+
content: The transformed README content
|
|
600
|
+
|
|
601
|
+
Raises:
|
|
602
|
+
IOError: If writing fails
|
|
603
|
+
"""
|
|
604
|
+
try:
|
|
605
|
+
Path(self.readme_path).write_text(content, encoding="utf-8")
|
|
606
|
+
except IOError as e:
|
|
607
|
+
raise IOError(f"Failed to write transformed README: {e}")
|
|
608
|
+
|
|
609
|
+
def validate_transformed_content(self, content: str) -> Tuple[bool, Optional[str]]:
|
|
610
|
+
"""
|
|
611
|
+
Validate the transformed README content.
|
|
612
|
+
|
|
613
|
+
Args:
|
|
614
|
+
content: The transformed README content to validate
|
|
615
|
+
|
|
616
|
+
Returns:
|
|
617
|
+
Tuple of (is_valid, error_message)
|
|
618
|
+
"""
|
|
619
|
+
# Validate markdown syntax
|
|
620
|
+
syntax_valid, syntax_error = self.validator.validate_markdown_syntax(content)
|
|
621
|
+
if not syntax_valid:
|
|
622
|
+
return False, syntax_error
|
|
623
|
+
|
|
624
|
+
# Validate document structure
|
|
625
|
+
structure_valid, structure_error = self.validator.validate_document_structure(
|
|
626
|
+
content
|
|
627
|
+
)
|
|
628
|
+
if not structure_valid:
|
|
629
|
+
return False, structure_error
|
|
630
|
+
|
|
631
|
+
return True, None
|
fishertools/safe/__init__.py
CHANGED
|
@@ -6,11 +6,16 @@ that prevent typical mistakes and provide helpful error messages.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from .collections import safe_get, safe_divide, safe_max, safe_min, safe_sum
|
|
9
|
-
from .files import
|
|
9
|
+
from .files import (
|
|
10
|
+
safe_read_file, safe_write_file, safe_file_exists, safe_get_file_size, safe_list_files,
|
|
11
|
+
safe_open, find_file, project_root, ensure_dir, get_file_hash, read_last_lines
|
|
12
|
+
)
|
|
10
13
|
from .strings import safe_string_operations
|
|
11
14
|
|
|
12
15
|
__all__ = [
|
|
13
16
|
"safe_get", "safe_divide", "safe_max", "safe_min", "safe_sum",
|
|
14
17
|
"safe_read_file", "safe_write_file", "safe_file_exists", "safe_get_file_size", "safe_list_files",
|
|
18
|
+
"safe_open", "find_file", "project_root",
|
|
19
|
+
"ensure_dir", "get_file_hash", "read_last_lines",
|
|
15
20
|
"safe_string_operations"
|
|
16
21
|
]
|