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.
@@ -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,2 @@
1
+ # LineKeeper
2
+ "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,3 @@
1
+ [console_scripts]
2
+ linekeeper = linekeeper:main
3
+ lk = linekeeper:main
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ # setup.py mínimo para compatibilidad con herramientas antiguas
2
+ # La configuración principal está en pyproject.toml
3
+ from setuptools import setup
4
+ setup()
@@ -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()