linekeeper 1.0.0__tar.gz
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.
- linekeeper-1.0.0/LICENSE +21 -0
- linekeeper-1.0.0/PKG-INFO +28 -0
- linekeeper-1.0.0/README.md +2 -0
- linekeeper-1.0.0/linekeeper.egg-info/PKG-INFO +28 -0
- linekeeper-1.0.0/linekeeper.egg-info/SOURCES.txt +12 -0
- linekeeper-1.0.0/linekeeper.egg-info/dependency_links.txt +1 -0
- linekeeper-1.0.0/linekeeper.egg-info/entry_points.txt +3 -0
- linekeeper-1.0.0/linekeeper.egg-info/requires.txt +1 -0
- linekeeper-1.0.0/linekeeper.egg-info/top_level.txt +1 -0
- linekeeper-1.0.0/linekeeper.py +291 -0
- linekeeper-1.0.0/pyproject.toml +39 -0
- linekeeper-1.0.0/setup.cfg +4 -0
- linekeeper-1.0.0/setup.py +4 -0
- linekeeper-1.0.0/tests/test_linekeeper.py +216 -0
linekeeper-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Renzos666
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: linekeeper
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Preserve line numbers when working with AI (DeepSeek, ChatGPT, Claude)
|
|
5
|
+
Author-email: Renzos666 <renzosilv666@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Renzos666/LineKeeper
|
|
8
|
+
Project-URL: Repository, https://github.com/Renzos666/LineKeeper.git
|
|
9
|
+
Project-URL: Issues, https://github.com/Renzos666/LineKeeper/issues
|
|
10
|
+
Keywords: line-numbers,ai,deepseek,chatgpt,claude,code
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Requires-Python: >=3.7
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: pyperclip>=1.8.2
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# LineKeeper
|
|
28
|
+
"Preserva la numeración de líneas cuando trabajas con IA (DeepSeek, ChatGPT, Claude)"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: linekeeper
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Preserve line numbers when working with AI (DeepSeek, ChatGPT, Claude)
|
|
5
|
+
Author-email: Renzos666 <renzosilv666@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Renzos666/LineKeeper
|
|
8
|
+
Project-URL: Repository, https://github.com/Renzos666/LineKeeper.git
|
|
9
|
+
Project-URL: Issues, https://github.com/Renzos666/LineKeeper/issues
|
|
10
|
+
Keywords: line-numbers,ai,deepseek,chatgpt,claude,code
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Requires-Python: >=3.7
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: pyperclip>=1.8.2
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# LineKeeper
|
|
28
|
+
"Preserva la numeración de líneas cuando trabajas con IA (DeepSeek, ChatGPT, Claude)"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
linekeeper.py
|
|
4
|
+
pyproject.toml
|
|
5
|
+
setup.py
|
|
6
|
+
linekeeper.egg-info/PKG-INFO
|
|
7
|
+
linekeeper.egg-info/SOURCES.txt
|
|
8
|
+
linekeeper.egg-info/dependency_links.txt
|
|
9
|
+
linekeeper.egg-info/entry_points.txt
|
|
10
|
+
linekeeper.egg-info/requires.txt
|
|
11
|
+
linekeeper.egg-info/top_level.txt
|
|
12
|
+
tests/test_linekeeper.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyperclip>=1.8.2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
linekeeper
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
LineKeeper - Preserva la numeración de líneas cuando trabajas con IA
|
|
4
|
+
LineKeeper - Preserve line numbers when working with AI
|
|
5
|
+
|
|
6
|
+
Uso / Usage:
|
|
7
|
+
python linekeeper.py archivo.cs
|
|
8
|
+
python linekeeper.py --clipboard
|
|
9
|
+
python linekeeper.py --idioma es
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
import re
|
|
14
|
+
import argparse
|
|
15
|
+
import locale
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
import pyperclip
|
|
19
|
+
except ImportError:
|
|
20
|
+
pyperclip = None
|
|
21
|
+
|
|
22
|
+
# ============================================================
|
|
23
|
+
# TRADUCCIONES / TRANSLATIONS
|
|
24
|
+
# ============================================================
|
|
25
|
+
|
|
26
|
+
MENSAJES = {
|
|
27
|
+
'es': {
|
|
28
|
+
'nombre': "Español",
|
|
29
|
+
'clipboard_ok': "📋 Texto obtenido del portapapeles",
|
|
30
|
+
'archivo_ok': "📄 Archivo cargado: {}",
|
|
31
|
+
'stdin_ok': "📥 Texto desde stdin",
|
|
32
|
+
'error_sin_texto': "❌ Error: No se proporcionó texto. Usa --help para ayuda.",
|
|
33
|
+
'error_pyperclip': "❌ Error: pyperclip no instalado. Ejecuta: pip install pyperclip",
|
|
34
|
+
'error_archivo': "❌ Error: No se encuentra el archivo '{}'",
|
|
35
|
+
'guardado_exito': "✅ Resultado guardado en {}",
|
|
36
|
+
'estadisticas': "\n📊 Estadísticas:",
|
|
37
|
+
'lineas_originales': " - Líneas originales: {}",
|
|
38
|
+
'lineas_marcadas': " - Líneas con marcador: {}",
|
|
39
|
+
'rango': " - Rango: {} - {}",
|
|
40
|
+
'respuesta_limpia': "✅ Respuesta limpia guardada en {}",
|
|
41
|
+
'modo_limpiar': "🧹 Modo limpiar activado",
|
|
42
|
+
'ayuda_titulo': "LineKeeper - Preserva líneas para IA",
|
|
43
|
+
'ayuda_ejemplos': "Ejemplos:\n python linekeeper.py mi_codigo.py\n python linekeeper.py -c"
|
|
44
|
+
},
|
|
45
|
+
'en': {
|
|
46
|
+
'nombre': "English",
|
|
47
|
+
'clipboard_ok': "📋 Text obtained from clipboard",
|
|
48
|
+
'archivo_ok': "📄 File loaded: {}",
|
|
49
|
+
'stdin_ok': "📥 Text from stdin",
|
|
50
|
+
'error_sin_texto': "❌ Error: No text provided. Use --help for help.",
|
|
51
|
+
'error_pyperclip': "❌ Error: pyperclip not installed. Run: pip install pyperclip",
|
|
52
|
+
'error_archivo': "❌ Error: File '{}' not found",
|
|
53
|
+
'guardado_exito': "✅ Result saved to {}",
|
|
54
|
+
'estadisticas': "\n📊 Statistics:",
|
|
55
|
+
'lineas_originales': " - Original lines: {}",
|
|
56
|
+
'lineas_marcadas': " - Lines with markers: {}",
|
|
57
|
+
'rango': " - Range: {} - {}",
|
|
58
|
+
'respuesta_limpia': "✅ Clean response saved to {}",
|
|
59
|
+
'modo_limpiar': "🧹 Clean mode activated",
|
|
60
|
+
'ayuda_titulo': "LineKeeper - Preserve lines for AI",
|
|
61
|
+
'ayuda_ejemplos': "Examples:\n python linekeeper.py my_code.py\n python linekeeper.py -c"
|
|
62
|
+
},
|
|
63
|
+
'fr': {
|
|
64
|
+
'nombre': "Français",
|
|
65
|
+
'clipboard_ok': "📋 Texte obtenu du presse-papier",
|
|
66
|
+
'archivo_ok': "📄 Fichier chargé : {}",
|
|
67
|
+
'stdin_ok': "📥 Texte depuis stdin",
|
|
68
|
+
'error_sin_texto': "❌ Erreur : Aucun texte fourni. Utilisez --help pour obtenir de l'aide.",
|
|
69
|
+
'error_pyperclip': "❌ Erreur : pyperclip non installé. Exécutez : pip install pyperclip",
|
|
70
|
+
'error_archivo': "❌ Erreur : Fichier '{}' introuvable",
|
|
71
|
+
'guardado_exito': "✅ Résultat enregistré dans {}",
|
|
72
|
+
'estadisticas': "\n📊 Statistiques :",
|
|
73
|
+
'lineas_originales': " - Lignes originales : {}",
|
|
74
|
+
'lineas_marcadas': " - Lignes avec marqueurs : {}",
|
|
75
|
+
'rango': " - Plage : {} - {}",
|
|
76
|
+
'respuesta_limpia': "✅ Réponse nettoyée enregistrée dans {}",
|
|
77
|
+
'modo_limpiar': "🧹 Mode nettoyage activé",
|
|
78
|
+
'ayuda_titulo': "LineKeeper - Préserve les lignes pour l'IA",
|
|
79
|
+
'ayuda_ejemplos': "Exemples :\n python linekeeper.py mon_code.py\n python linekeeper.py -c"
|
|
80
|
+
},
|
|
81
|
+
'zh': {
|
|
82
|
+
'nombre': "中文",
|
|
83
|
+
'clipboard_ok': "📋 从剪贴板获取文本",
|
|
84
|
+
'archivo_ok': "📄 已加载文件: {}",
|
|
85
|
+
'stdin_ok': "📥 从标准输入读取文本",
|
|
86
|
+
'error_sin_texto': "❌ 错误: 未提供文本。使用 --help 查看帮助",
|
|
87
|
+
'error_pyperclip': "❌ 错误: 未安装 pyperclip。运行: pip install pyperclip",
|
|
88
|
+
'error_archivo': "❌ 错误: 找不到文件 '{}'",
|
|
89
|
+
'guardado_exito': "✅ 结果已保存到 {}",
|
|
90
|
+
'estadisticas': "\n📊 统计信息:",
|
|
91
|
+
'lineas_originales': " - 原始行数: {}",
|
|
92
|
+
'lineas_marcadas': " - 带标记的行数: {}",
|
|
93
|
+
'rango': " - 范围: {} - {}",
|
|
94
|
+
'respuesta_limpia': "✅ 清理后的响应已保存到 {}",
|
|
95
|
+
'modo_limpiar': "🧹 清洁模式已激活",
|
|
96
|
+
'ayuda_titulo': "LineKeeper - 为AI保留行号",
|
|
97
|
+
'ayuda_ejemplos': "示例:\n python linekeeper.py my_code.py\n python linekeeper.py -c"
|
|
98
|
+
},
|
|
99
|
+
'de': {
|
|
100
|
+
'nombre': "Deutsch",
|
|
101
|
+
'clipboard_ok': "📋 Text aus der Zwischenablage",
|
|
102
|
+
'archivo_ok': "📄 Datei geladen: {}",
|
|
103
|
+
'stdin_ok': "📥 Text von stdin",
|
|
104
|
+
'error_sin_texto': "❌ Fehler: Kein Text angegeben. Verwenden Sie --help für Hilfe.",
|
|
105
|
+
'error_pyperclip': "❌ Fehler: pyperclip nicht installiert. Führen Sie aus: pip install pyperclip",
|
|
106
|
+
'error_archivo': "❌ Fehler: Datei '{}' nicht gefunden",
|
|
107
|
+
'guardado_exito': "✅ Ergebnis gespeichert in {}",
|
|
108
|
+
'estadisticas': "\n📊 Statistiken:",
|
|
109
|
+
'lineas_originales': " - Ursprüngliche Zeilen: {}",
|
|
110
|
+
'lineas_marcadas': " - Zeilen mit Markierungen: {}",
|
|
111
|
+
'rango': " - Bereich: {} - {}",
|
|
112
|
+
'respuesta_limpia': "✅ Bereinigte Antwort gespeichert in {}",
|
|
113
|
+
'modo_limpiar': "🧹 Bereinigungsmodus aktiviert",
|
|
114
|
+
'ayuda_titulo': "LineKeeper - Zeilen für KI bewahren",
|
|
115
|
+
'ayuda_ejemplos': "Beispiele:\n python linekeeper.py meine_datei.py\n python linekeeper.py -c"
|
|
116
|
+
},
|
|
117
|
+
'ja': {
|
|
118
|
+
'nombre': "日本語",
|
|
119
|
+
'clipboard_ok': "📋 クリップボードからテキストを取得しました",
|
|
120
|
+
'archivo_ok': "📄 ファイルを読み込みました: {}",
|
|
121
|
+
'stdin_ok': "📥 標準入力からテキストを読み込みました",
|
|
122
|
+
'error_sin_texto': "❌ エラー: テキストが提供されていません。--help でヘルプを表示",
|
|
123
|
+
'error_pyperclip': "❌ エラー: pyperclip がインストールされていません。実行: pip install pyperclip",
|
|
124
|
+
'error_archivo': "❌ エラー: ファイル '{}' が見つかりません",
|
|
125
|
+
'guardado_exito': "✅ 結果を {} に保存しました",
|
|
126
|
+
'estadisticas': "\n📊 統計:",
|
|
127
|
+
'lineas_originales': " - 元の行数: {}",
|
|
128
|
+
'lineas_marcadas': " - マーカー付き行数: {}",
|
|
129
|
+
'rango': " - 範囲: {} - {}",
|
|
130
|
+
'respuesta_limpia': "✅ cleaned response saved to {}",
|
|
131
|
+
'modo_limpiar': "🧹 クリーンモードがアクティブになりました",
|
|
132
|
+
'ayuda_titulo': "LineKeeper - AIのための行番号保持",
|
|
133
|
+
'ayuda_ejemplos': "例:\n python linekeeper.py my_code.py\n python linekeeper.py -c"
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Idiomas soportados (códigos ISO 639-1)
|
|
138
|
+
IDIOMAS_SOPORTADOS = ['es', 'en', 'fr', 'zh', 'de', 'ja']
|
|
139
|
+
|
|
140
|
+
def detectar_idioma():
|
|
141
|
+
"""Detecta el idioma del sistema automáticamente"""
|
|
142
|
+
try:
|
|
143
|
+
idioma_sistema = locale.getdefaultlocale()[0]
|
|
144
|
+
if idioma_sistema:
|
|
145
|
+
# Extraer código base (ej: 'es_ES' -> 'es')
|
|
146
|
+
codigo_base = idioma_sistema.split('_')[0].lower()
|
|
147
|
+
if codigo_base in IDIOMAS_SOPORTADOS:
|
|
148
|
+
return codigo_base
|
|
149
|
+
return 'en' # Inglés por defecto
|
|
150
|
+
except:
|
|
151
|
+
return 'en'
|
|
152
|
+
|
|
153
|
+
def obtener_mensaje(idioma, clave):
|
|
154
|
+
"""Obtiene un mensaje en el idioma especificado"""
|
|
155
|
+
if idioma in MENSAJES and clave in MENSAJES[idioma]:
|
|
156
|
+
return MENSAJES[idioma][clave]
|
|
157
|
+
# Fallback a inglés
|
|
158
|
+
return MENSAJES['en'].get(clave, f"[MISSING:{clave}]")
|
|
159
|
+
|
|
160
|
+
# ============================================================
|
|
161
|
+
# FUNCIONES PRINCIPALES
|
|
162
|
+
# ============================================================
|
|
163
|
+
|
|
164
|
+
def numerar_lineas(texto: str, inicio: int = 1, solo_no_vacias: bool = True) -> str:
|
|
165
|
+
"""
|
|
166
|
+
Añade marcadores [Lxxx] al inicio de cada línea.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
texto: Texto a procesar
|
|
170
|
+
inicio: Número de la primera línea
|
|
171
|
+
solo_no_vacias: Si True, las líneas vacías no reciben marcador
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Texto con marcadores de línea
|
|
175
|
+
"""
|
|
176
|
+
# Preservar el tipo de salto de línea original
|
|
177
|
+
if '\r\n' in texto:
|
|
178
|
+
salto = '\r\n'
|
|
179
|
+
lineas = texto.split('\r\n')
|
|
180
|
+
else:
|
|
181
|
+
salto = '\n'
|
|
182
|
+
lineas = texto.split('\n')
|
|
183
|
+
|
|
184
|
+
resultado = []
|
|
185
|
+
for i, linea in enumerate(lineas, start=inicio):
|
|
186
|
+
if solo_no_vacias and linea == "":
|
|
187
|
+
resultado.append("")
|
|
188
|
+
else:
|
|
189
|
+
resultado.append(f"[L{i:03d}] {linea}")
|
|
190
|
+
|
|
191
|
+
# Preservar líneas vacías al final
|
|
192
|
+
return salto.join(resultado)
|
|
193
|
+
|
|
194
|
+
def extraer_referencias(respuesta: str) -> list:
|
|
195
|
+
"""Extrae todas las referencias a [Lxxx] de la respuesta de IA."""
|
|
196
|
+
patron = r'\[L(\d{3,})\]'
|
|
197
|
+
return re.findall(patron, respuesta)
|
|
198
|
+
|
|
199
|
+
def limpiar_respuesta(respuesta: str) -> str:
|
|
200
|
+
"""Elimina los marcadores [Lxxx] de la respuesta para lectura limpia."""
|
|
201
|
+
# Eliminar [Lxxx] pero conservar los espacios que lo rodean
|
|
202
|
+
# El marcador se elimina, los espacios se mantienen
|
|
203
|
+
resultado = re.sub(r'\[L\d{2,}\]', '', respuesta)
|
|
204
|
+
return resultado
|
|
205
|
+
|
|
206
|
+
def main():
|
|
207
|
+
# Detectar idioma automáticamente
|
|
208
|
+
idioma = detectar_idioma()
|
|
209
|
+
|
|
210
|
+
parser = argparse.ArgumentParser(
|
|
211
|
+
description=obtener_mensaje(idioma, 'ayuda_titulo'),
|
|
212
|
+
epilog=obtener_mensaje(idioma, 'ayuda_ejemplos')
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
parser.add_argument("archivo", nargs="?", help=("Archivo a procesar" if idioma == 'es' else "File to process"))
|
|
216
|
+
parser.add_argument("--clipboard", "-c", action="store_true",
|
|
217
|
+
help=("Usar contenido del portapapeles" if idioma == 'es' else "Use clipboard content"))
|
|
218
|
+
parser.add_argument("--inicio", "-i", type=int, default=1,
|
|
219
|
+
help=("Número de línea inicial (default: 1)" if idioma == 'es' else "Starting line number (default: 1)"))
|
|
220
|
+
parser.add_argument("--limpiar", "-l", action="store_true",
|
|
221
|
+
help=("Limpiar respuesta de IA (eliminar [Lxxx])" if idioma == 'es' else "Clean AI response (remove [Lxxx])"))
|
|
222
|
+
parser.add_argument("--salida", "-s", help=("Guardar resultado en archivo" if idioma == 'es' else "Save result to file"))
|
|
223
|
+
parser.add_argument("--incluir-vacias", "-v", action="store_true",
|
|
224
|
+
help=("Incluir marcadores en líneas vacías" if idioma == 'es' else "Include markers on empty lines"))
|
|
225
|
+
parser.add_argument("--idioma", choices=IDIOMAS_SOPORTADOS,
|
|
226
|
+
help=("Forzar idioma (es/en/fr/zh/de/ja)" if idioma == 'es' else "Force language (es/en/fr/zh/de/ja)"))
|
|
227
|
+
|
|
228
|
+
args = parser.parse_args()
|
|
229
|
+
|
|
230
|
+
# Sobrescribir idioma si se especifica --idioma
|
|
231
|
+
if args.idioma:
|
|
232
|
+
idioma = args.idioma
|
|
233
|
+
|
|
234
|
+
# Modo limpiar: recibe respuesta de IA y la limpia
|
|
235
|
+
if args.limpiar:
|
|
236
|
+
print(obtener_mensaje(idioma, 'modo_limpiar'), file=sys.stderr)
|
|
237
|
+
texto = sys.stdin.read() if not sys.stdin.isatty() else input("Pega la respuesta de IA: ")
|
|
238
|
+
texto_limpio = limpiar_respuesta(texto)
|
|
239
|
+
if args.salida:
|
|
240
|
+
with open(args.salida, "w", encoding="utf-8") as f:
|
|
241
|
+
f.write(texto_limpio)
|
|
242
|
+
print(obtener_mensaje(idioma, 'respuesta_limpia').format(args.salida))
|
|
243
|
+
else:
|
|
244
|
+
print(texto_limpio)
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
# Obtener texto de entrada
|
|
248
|
+
if args.clipboard:
|
|
249
|
+
if not pyperclip:
|
|
250
|
+
print(obtener_mensaje(idioma, 'error_pyperclip'), file=sys.stderr)
|
|
251
|
+
sys.exit(1)
|
|
252
|
+
texto = pyperclip.paste()
|
|
253
|
+
print(obtener_mensaje(idioma, 'clipboard_ok'), file=sys.stderr)
|
|
254
|
+
elif args.archivo:
|
|
255
|
+
try:
|
|
256
|
+
with open(args.archivo, "r", encoding="utf-8") as f:
|
|
257
|
+
texto = f.read()
|
|
258
|
+
print(obtener_mensaje(idioma, 'archivo_ok').format(args.archivo), file=sys.stderr)
|
|
259
|
+
except FileNotFoundError:
|
|
260
|
+
print(obtener_mensaje(idioma, 'error_archivo').format(args.archivo), file=sys.stderr)
|
|
261
|
+
sys.exit(1)
|
|
262
|
+
else:
|
|
263
|
+
# Leer desde stdin (pipe)
|
|
264
|
+
texto = sys.stdin.read()
|
|
265
|
+
if not texto.strip():
|
|
266
|
+
print(obtener_mensaje(idioma, 'error_sin_texto'), file=sys.stderr)
|
|
267
|
+
sys.exit(1)
|
|
268
|
+
print(obtener_mensaje(idioma, 'stdin_ok'), file=sys.stderr)
|
|
269
|
+
|
|
270
|
+
# Numerar líneas
|
|
271
|
+
texto_numerado = numerar_lineas(texto, args.inicio, not args.incluir_vacias)
|
|
272
|
+
|
|
273
|
+
# Salida
|
|
274
|
+
if args.salida:
|
|
275
|
+
with open(args.salida, "w", encoding="utf-8") as f:
|
|
276
|
+
f.write(texto_numerado)
|
|
277
|
+
print(obtener_mensaje(idioma, 'guardado_exito').format(args.salida), file=sys.stderr)
|
|
278
|
+
else:
|
|
279
|
+
print(texto_numerado)
|
|
280
|
+
|
|
281
|
+
# Mostrar estadísticas (solo si la salida es a terminal)
|
|
282
|
+
if sys.stdout.isatty():
|
|
283
|
+
lineas_originales = len(texto.splitlines())
|
|
284
|
+
lineas_marcadas = len(texto_numerado.splitlines())
|
|
285
|
+
print(obtener_mensaje(idioma, 'estadisticas'), file=sys.stderr)
|
|
286
|
+
print(obtener_mensaje(idioma, 'lineas_originales').format(lineas_originales), file=sys.stderr)
|
|
287
|
+
print(obtener_mensaje(idioma, 'lineas_marcadas').format(lineas_marcadas), file=sys.stderr)
|
|
288
|
+
print(obtener_mensaje(idioma, 'rango').format(f"L{args.inicio:03d}", f"L{args.inicio + lineas_originales - 1:03d}"), file=sys.stderr)
|
|
289
|
+
|
|
290
|
+
if __name__ == "__main__":
|
|
291
|
+
main()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "linekeeper"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Preserve line numbers when working with AI (DeepSeek, ChatGPT, Claude)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.7"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Renzos666", email = "renzosilv666@gmail.com"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["line-numbers", "ai", "deepseek", "chatgpt", "claude", "code"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Topic :: Software Development :: Code Generators",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.7",
|
|
22
|
+
"Programming Language :: Python :: 3.8",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"pyperclip>=1.8.2"
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/Renzos666/LineKeeper"
|
|
34
|
+
Repository = "https://github.com/Renzos666/LineKeeper.git"
|
|
35
|
+
Issues = "https://github.com/Renzos666/LineKeeper/issues"
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
linekeeper = "linekeeper:main"
|
|
39
|
+
lk = "linekeeper:main"
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tests unitarios para LineKeeper
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import unittest
|
|
7
|
+
import sys
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
# Agregar el directorio padre al path para poder importar linekeeper
|
|
11
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
12
|
+
|
|
13
|
+
from linekeeper import numerar_lineas, limpiar_respuesta, extraer_referencias, detectar_idioma
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestNumerarLineas(unittest.TestCase):
|
|
17
|
+
"""Pruebas para la función numerar_lineas"""
|
|
18
|
+
|
|
19
|
+
def test_lineas_basicas(self):
|
|
20
|
+
"""Caso básico: líneas simples"""
|
|
21
|
+
texto = "Linea 1\nLinea 2\nLinea 3"
|
|
22
|
+
resultado = numerar_lineas(texto)
|
|
23
|
+
esperado = "[L001] Linea 1\n[L002] Linea 2\n[L003] Linea 3"
|
|
24
|
+
self.assertEqual(resultado, esperado)
|
|
25
|
+
|
|
26
|
+
def test_lineas_con_espacios(self):
|
|
27
|
+
"""Líneas con espacios al inicio"""
|
|
28
|
+
texto = " Linea con espacios\n Mas espacios"
|
|
29
|
+
resultado = numerar_lineas(texto)
|
|
30
|
+
esperado = "[L001] Linea con espacios\n[L002] Mas espacios"
|
|
31
|
+
self.assertEqual(resultado, esperado)
|
|
32
|
+
|
|
33
|
+
def test_lineas_vacias_sin_marcador(self):
|
|
34
|
+
"""Líneas vacías no deben tener marcador (por defecto)"""
|
|
35
|
+
texto = "Linea 1\n\nLinea 3\n\n\nLinea 6"
|
|
36
|
+
resultado = numerar_lineas(texto, solo_no_vacias=True)
|
|
37
|
+
esperado = "[L001] Linea 1\n\n[L003] Linea 3\n\n\n[L006] Linea 6"
|
|
38
|
+
self.assertEqual(resultado, esperado)
|
|
39
|
+
|
|
40
|
+
def test_incluir_marcador_en_vacias(self):
|
|
41
|
+
"""Incluir marcador en líneas vacías cuando se solicita"""
|
|
42
|
+
texto = "Linea 1\n\nLinea 3"
|
|
43
|
+
resultado = numerar_lineas(texto, solo_no_vacias=False)
|
|
44
|
+
esperado = "[L001] Linea 1\n[L002] \n[L003] Linea 3"
|
|
45
|
+
self.assertEqual(resultado, esperado)
|
|
46
|
+
|
|
47
|
+
def test_inicio_personalizado(self):
|
|
48
|
+
"""Número de línea inicial personalizado"""
|
|
49
|
+
texto = "Primera\nSegunda"
|
|
50
|
+
resultado = numerar_lineas(texto, inicio=10)
|
|
51
|
+
esperado = "[L010] Primera\n[L011] Segunda"
|
|
52
|
+
self.assertEqual(resultado, esperado)
|
|
53
|
+
|
|
54
|
+
def test_inicio_muy_alto(self):
|
|
55
|
+
"""Número de línea inicial muy alto (4 dígitos)"""
|
|
56
|
+
texto = "Solo una"
|
|
57
|
+
resultado = numerar_lineas(texto, inicio=1000)
|
|
58
|
+
esperado = "[L1000] Solo una"
|
|
59
|
+
self.assertEqual(resultado, esperado)
|
|
60
|
+
|
|
61
|
+
def test_texto_vacio(self):
|
|
62
|
+
"""Texto vacío debe devolver cadena vacía"""
|
|
63
|
+
texto = ""
|
|
64
|
+
resultado = numerar_lineas(texto)
|
|
65
|
+
esperado = ""
|
|
66
|
+
self.assertEqual(resultado, esperado)
|
|
67
|
+
|
|
68
|
+
def test_solo_lineas_vacias(self):
|
|
69
|
+
"""Solo líneas vacías sin marcadores"""
|
|
70
|
+
texto = "\n\n\n"
|
|
71
|
+
resultado = numerar_lineas(texto, solo_no_vacias=True)
|
|
72
|
+
esperado = "\n\n\n"
|
|
73
|
+
self.assertEqual(resultado, esperado)
|
|
74
|
+
|
|
75
|
+
def test_caracteres_unicode(self):
|
|
76
|
+
"""Caracteres Unicode deben preservarse"""
|
|
77
|
+
texto = "ñandú\ncafé\n😊 emoji\nárbol\nniño"
|
|
78
|
+
resultado = numerar_lineas(texto)
|
|
79
|
+
lineas = resultado.split('\n')
|
|
80
|
+
self.assertTrue("[L001] ñandú" in lineas[0])
|
|
81
|
+
self.assertTrue("[L002] café" in lineas[1])
|
|
82
|
+
self.assertTrue("[L003] 😊 emoji" in lineas[2])
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TestLimpiarRespuesta(unittest.TestCase):
|
|
86
|
+
"""Pruebas para la función limpiar_respuesta"""
|
|
87
|
+
|
|
88
|
+
def test_limpiar_marcadores_simples(self):
|
|
89
|
+
"""Eliminar marcadores [Lxxx] básicos"""
|
|
90
|
+
respuesta = "En [L005] hay un error. Revisa [L012] también."
|
|
91
|
+
resultado = limpiar_respuesta(respuesta)
|
|
92
|
+
esperado = "En hay un error. Revisa también."
|
|
93
|
+
self.assertEqual(resultado, esperado)
|
|
94
|
+
|
|
95
|
+
def test_sin_marcadores(self):
|
|
96
|
+
"""Respuesta sin marcadores no debe cambiar"""
|
|
97
|
+
respuesta = "Esto es una respuesta normal sin marcadores"
|
|
98
|
+
resultado = limpiar_respuesta(respuesta)
|
|
99
|
+
self.assertEqual(resultado, respuesta)
|
|
100
|
+
|
|
101
|
+
def test_marcadores_varios_digitos(self):
|
|
102
|
+
"""Marcadores con 2, 3 y 4 dígitos"""
|
|
103
|
+
respuesta = "Líneas: [L01], [L123], [L9999]"
|
|
104
|
+
resultado = limpiar_respuesta(respuesta)
|
|
105
|
+
esperado = "Líneas: , , "
|
|
106
|
+
# El espacio después del marcador también se elimina
|
|
107
|
+
self.assertEqual(resultado, esperado)
|
|
108
|
+
|
|
109
|
+
def test_marcadores_pegados(self):
|
|
110
|
+
"""Marcadores sin espacio después"""
|
|
111
|
+
respuesta = "[L001]inicio sin espacio"
|
|
112
|
+
resultado = limpiar_respuesta(respuesta)
|
|
113
|
+
esperado = "inicio sin espacio"
|
|
114
|
+
self.assertEqual(resultado, esperado)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class TestExtraerReferencias(unittest.TestCase):
|
|
118
|
+
"""Pruebas para la función extraer_referencias"""
|
|
119
|
+
|
|
120
|
+
def test_extraer_una_referencia(self):
|
|
121
|
+
"""Extraer una sola referencia"""
|
|
122
|
+
respuesta = "El error está en [L042]"
|
|
123
|
+
resultado = extraer_referencias(respuesta)
|
|
124
|
+
esperado = ['042']
|
|
125
|
+
self.assertEqual(resultado, esperado)
|
|
126
|
+
|
|
127
|
+
def test_extraer_multiples_referencias(self):
|
|
128
|
+
"""Extraer múltiples referencias"""
|
|
129
|
+
respuesta = "[L001] y [L123] y [L999] y [L005]"
|
|
130
|
+
resultado = extraer_referencias(respuesta)
|
|
131
|
+
esperado = ['001', '123', '999', '005']
|
|
132
|
+
self.assertEqual(resultado, esperado)
|
|
133
|
+
|
|
134
|
+
def test_sin_referencias(self):
|
|
135
|
+
"""Respuesta sin referencias debe devolver lista vacía"""
|
|
136
|
+
respuesta = "No hay números de línea aquí"
|
|
137
|
+
resultado = extraer_referencias(respuesta)
|
|
138
|
+
esperado = []
|
|
139
|
+
self.assertEqual(resultado, esperado)
|
|
140
|
+
|
|
141
|
+
def test_no_confundir_con_numeros_normales(self):
|
|
142
|
+
"""No debe extraer números que no están en formato [Lxxx]"""
|
|
143
|
+
respuesta = "El número 123 no es una referencia, pero [L123] sí"
|
|
144
|
+
resultado = extraer_referencias(respuesta)
|
|
145
|
+
esperado = ['123']
|
|
146
|
+
self.assertEqual(resultado, esperado)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class TestDetectarIdioma(unittest.TestCase):
|
|
150
|
+
"""Pruebas para la función detectar_idioma"""
|
|
151
|
+
|
|
152
|
+
def test_idioma_siempre_devuelve_algo(self):
|
|
153
|
+
"""Siempre debe devolver un string (idioma por defecto)"""
|
|
154
|
+
idioma = detectar_idioma()
|
|
155
|
+
self.assertIsInstance(idioma, str)
|
|
156
|
+
self.assertIn(idioma, ['es', 'en', 'fr', 'zh', 'de', 'ja'])
|
|
157
|
+
|
|
158
|
+
def test_idioma_no_vacio(self):
|
|
159
|
+
"""El idioma no debe ser cadena vacía"""
|
|
160
|
+
idioma = detectar_idioma()
|
|
161
|
+
self.assertTrue(len(idioma) > 0)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class TestIntegracion(unittest.TestCase):
|
|
165
|
+
"""Pruebas de integración (casos de uso reales)"""
|
|
166
|
+
|
|
167
|
+
def test_flujo_completo_numerar_y_limpiar(self):
|
|
168
|
+
"""Simular: numerar código, enviar a IA, limpiar respuesta"""
|
|
169
|
+
# 1. Código original
|
|
170
|
+
codigo = "def suma(a, b):\n return a + b"
|
|
171
|
+
|
|
172
|
+
# 2. Numerar líneas
|
|
173
|
+
numerado = numerar_lineas(codigo)
|
|
174
|
+
esperado = "[L001] def suma(a, b):\n[L002] return a + b"
|
|
175
|
+
self.assertEqual(numerado, esperado)
|
|
176
|
+
|
|
177
|
+
# 3. Simular respuesta de IA que referencia líneas
|
|
178
|
+
respuesta_ia = "En [L002] hay un error. Cambia 'return a + b' por 'return a + b + 1'"
|
|
179
|
+
|
|
180
|
+
# 4. Extraer referencias
|
|
181
|
+
referencias = extraer_referencias(respuesta_ia)
|
|
182
|
+
self.assertEqual(referencias, ['002'])
|
|
183
|
+
|
|
184
|
+
# 5. Limpiar respuesta para el usuario
|
|
185
|
+
respuesta_limpia = limpiar_respuesta(respuesta_ia)
|
|
186
|
+
esperado_limpio = "En hay un error. Cambia 'return a + b' por 'return a + b + 1'"
|
|
187
|
+
self.assertEqual(respuesta_limpia, esperado_limpio)
|
|
188
|
+
|
|
189
|
+
def test_ejemplo_archivo_entrenamiento(self):
|
|
190
|
+
"""Caso real: el archivo Entrenamiento.cs que usamos en la conversación"""
|
|
191
|
+
codigo = """// Este en un Archivo de entrenamiento para DeepSeek,
|
|
192
|
+
para que logre recordar el contexto de codigo y las lineas de forma correcta
|
|
193
|
+
o de lo contrario realizaremos una aplicacion que sirva de intermediario para lograrlo.
|
|
194
|
+
//
|
|
195
|
+
Cafe
|
|
196
|
+
Pimienta
|
|
197
|
+
Libro
|
|
198
|
+
Registro
|
|
199
|
+
Tecnologia
|
|
200
|
+
DeepSeek"""
|
|
201
|
+
|
|
202
|
+
numerado = numerar_lineas(codigo)
|
|
203
|
+
lineas = numerado.split('\n')
|
|
204
|
+
|
|
205
|
+
# Verificar que la línea 7 tiene "Libro"
|
|
206
|
+
# (las líneas vacías no tienen marcador, por eso hay que contar bien)
|
|
207
|
+
self.assertIn(len(lineas), [10, 11])
|
|
208
|
+
self.assertIn("[L001]", lineas[0])
|
|
209
|
+
self.assertIn("Cafe", lineas[4]) # Línea 5: Cafe
|
|
210
|
+
self.assertIn("Pimienta", lineas[5]) # Línea 6: Pimienta
|
|
211
|
+
self.assertIn("Libro", lineas[6]) # Línea 7: Libro
|
|
212
|
+
self.assertIn("Registro", lineas[7]) # Línea 8: Registro
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
if __name__ == "__main__":
|
|
216
|
+
unittest.main()
|