verifica 1.0.0.dev2__tar.gz → 1.0.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: verifica
3
- Version: 1.0.0.dev2
3
+ Version: 1.0.2
4
4
  Summary: Uma ferramenta simples para correção de atividades em python via CLI.
5
5
  Author-email: Théo Modeneis Ruela <theo.ruela@gmail.com>
6
6
  License-Expression: MIT
@@ -8,7 +8,7 @@ Project-URL: Homepage, https://github.com/espinafr/Verifica
8
8
  Project-URL: Issues, https://github.com/espinafr/Verifica/issues
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Operating System :: OS Independent
11
- Requires-Python: >=3.9.13
11
+ Requires-Python: >=3.9
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: colorama>=0.4.6
@@ -24,9 +24,6 @@ Dynamic: license-file
24
24
  <td><a href="https://pypi.org/project/verifica/">Página no Pypi</a></td>
25
25
  <td align="right"><a href="https://github.com/espinafr/verifica/issues/new?template=denúncia-de-bug.yml">Reportar bug</a></td>
26
26
  </tr>
27
- <tr>
28
- <td colspan="2" align="center"><a href="https://espinafr.github.io/verifica">Gerador <code>correcao.json</code></a></td>
29
- </tr>
30
27
  <tr>
31
28
  <td colspan="2" align="center"><a href="https://github.com/espinafr/verifica/tree/master/docs">Como configurar</a></td>
32
29
  </tr>
@@ -74,7 +71,7 @@ pip install dist/{NOME DO ARQUIVO GERADO}
74
71
 
75
72
  ## Reconhecimentos
76
73
 
77
- Veja o arquivo [ACKNOWLEDGEMENTS](https://github.com/espinafr/verifica/blob/master/ACKNOWLEDGEMENTS.md)
74
+ Veja o arquivo [ACKNOWLEDGEMENTS](https://github.com/espinafr/verifica/blob/master/ACKNOWLEDGEMENTS.md).
78
75
 
79
76
  ## Licença
80
77
 
@@ -7,9 +7,6 @@
7
7
  <td><a href="https://pypi.org/project/verifica/">Página no Pypi</a></td>
8
8
  <td align="right"><a href="https://github.com/espinafr/verifica/issues/new?template=denúncia-de-bug.yml">Reportar bug</a></td>
9
9
  </tr>
10
- <tr>
11
- <td colspan="2" align="center"><a href="https://espinafr.github.io/verifica">Gerador <code>correcao.json</code></a></td>
12
- </tr>
13
10
  <tr>
14
11
  <td colspan="2" align="center"><a href="https://github.com/espinafr/verifica/tree/master/docs">Como configurar</a></td>
15
12
  </tr>
@@ -57,7 +54,7 @@ pip install dist/{NOME DO ARQUIVO GERADO}
57
54
 
58
55
  ## Reconhecimentos
59
56
 
60
- Veja o arquivo [ACKNOWLEDGEMENTS](https://github.com/espinafr/verifica/blob/master/ACKNOWLEDGEMENTS.md)
57
+ Veja o arquivo [ACKNOWLEDGEMENTS](https://github.com/espinafr/verifica/blob/master/ACKNOWLEDGEMENTS.md).
61
58
 
62
59
  ## Licença
63
60
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "verifica"
7
- version = "1.0.0.dev2"
7
+ version = "1.0.2"
8
8
  dependencies = [
9
9
  "colorama>=0.4.6",
10
10
  "platformdirs>=2.5.2"
@@ -14,7 +14,7 @@ authors = [
14
14
  ]
15
15
  description = "Uma ferramenta simples para correção de atividades em python via CLI."
16
16
  readme = "README.md"
17
- requires-python = ">=3.9.13"
17
+ requires-python = ">=3.9"
18
18
  classifiers = [
19
19
  "Programming Language :: Python :: 3",
20
20
  "Operating System :: OS Independent",
@@ -1,17 +1,20 @@
1
1
  import argparse
2
2
  from pathlib import Path
3
+ from colorama import init
3
4
  import logging
4
5
  import sys
5
6
 
6
7
  from .config import settings
7
8
  from .check import Checker
8
9
  from .fetcher import Fetcher
10
+ from .builder import Builder
9
11
 
10
12
  logging.basicConfig(
11
13
  level=logging.ERROR,
12
14
  format='%(levelname)s - %(message)s',
13
15
  handlers=[logging.FileHandler(f'{settings.config_dir}/app.log', mode='w', encoding='utf-8'), logging.StreamHandler(stream=sys.stdout)]
14
16
  )
17
+ init(autoreset=True)
15
18
 
16
19
  def main():
17
20
  parser = argparse.ArgumentParser(description="Uma ferramenta simples para correção de atividades em python via CLI.", add_help=False)
@@ -24,7 +27,9 @@ def main():
24
27
  options.add_argument("-c", "--config", action="store_true", help="Mostra o caminho do arquivo de configuração.")
25
28
  options.add_argument("-d", "--debug", action="store_true", help="Ativa o modo debug, mostrando mais informações durante a execução.")
26
29
  options.add_argument("-l", "--local", action="store_true", help="Caminho local para a pasta com o arquivo de respostas, caso não queira baixar do github.")
27
-
30
+ options.add_argument("--version", action="store_true", help="Mostra a versão do programa.")
31
+ options.add_argument("-b", "--builder", action="store_true", help="TUI para criar arquivos de correção de atividades.")
32
+
28
33
  args = parser.parse_args()
29
34
 
30
35
  if args.debug:
@@ -35,6 +40,15 @@ def main():
35
40
  print(settings.config_path)
36
41
  return
37
42
 
43
+ if args.version:
44
+ print(settings.get_version())
45
+ return
46
+
47
+ if args.builder:
48
+ builder = Builder()
49
+ builder.start()
50
+ return
51
+
38
52
  if not args.atividade:
39
53
  parser.error("o seguinte argumento é obrigatório: atividade")
40
54
 
@@ -55,7 +69,11 @@ def main():
55
69
  logging.error(f"Não foi possível localizar o arquivo de correção em '{args.atividade}'")
56
70
  sys.exit(1)
57
71
 
58
- decoded_answers = answers.get_decoded_json()
72
+ try:
73
+ decoded_answers = answers.get_decoded_json()
74
+ except Exception as _:
75
+ sys.exit(1)
76
+
59
77
  answers.cleanup()
60
78
 
61
79
  checker = Checker(args.files, decoded_answers)
@@ -0,0 +1,447 @@
1
+ from colorama import Style, Fore
2
+ from functools import wraps
3
+ from pathlib import Path
4
+ import subprocess
5
+ import copy
6
+ import json
7
+ import sys
8
+ import os
9
+
10
+ from .tui import TUI, ANSI_PATTERN
11
+ from .config import settings
12
+
13
+ # Código ANSI para limpar a linha atual no terminal
14
+ CLEAR_LINE = "\r\033[K"
15
+
16
+ class Controller:
17
+ @staticmethod
18
+ def clear_command():
19
+ if os.name == 'nt':
20
+ subprocess.run(['cmd', '/c', 'cls'])
21
+ else:
22
+ subprocess.run(['clear'])
23
+
24
+
25
+ def clear_terminal(func):
26
+ @wraps(func)
27
+ def wrapper(*args, **kwargs):
28
+ Controller.clear_command()
29
+ return func(*args, **kwargs)
30
+ return wrapper
31
+
32
+
33
+ def loop_breaker(func):
34
+ @wraps(func)
35
+ def wrapper(*args, **kwargs):
36
+ try:
37
+ return func(*args, **kwargs)
38
+ except KeyboardInterrupt:
39
+ return None
40
+ return wrapper
41
+
42
+
43
+ def sequential_question(input_text):
44
+ answers = []
45
+ try:
46
+ while True:
47
+ result = input(input_text)
48
+ answers.append(result)
49
+
50
+ # Altera a linha anterior para explicitar que foi salvo
51
+ sys.stdout.write("\033[F\033[K")
52
+ sys.stdout.flush()
53
+ print(f"{Fore.GREEN}{ANSI_PATTERN.sub('', input_text)}{result}{Style.RESET_ALL}")
54
+ except KeyboardInterrupt:
55
+ return answers
56
+
57
+
58
+ def required_question(func):
59
+ @wraps(func)
60
+ def wrapper(*args, **kwargs):
61
+ while True:
62
+ result = func(*args, **kwargs)
63
+ if not isinstance(result, str) or result.strip() == "":
64
+ print(f"{Style.BRIGHT}{Fore.RED}Este campo é obrigatório. Por favor, insira um valor válido.{Style.RESET_ALL}")
65
+ else:
66
+ return result
67
+ return wrapper
68
+
69
+
70
+ def required_text(text) -> str:
71
+ return f"{Fore.RED}{text}{Style.RESET_ALL}"
72
+
73
+
74
+ def optional_text(text) -> str:
75
+ return f"{Fore.YELLOW}{text}{Style.RESET_ALL}"
76
+
77
+
78
+ def sequence_text(text) -> str:
79
+ return f"{Fore.GREEN}{text}{Style.RESET_ALL}"
80
+
81
+
82
+ class Selector:
83
+ def __init__(self, name: str, action, selected: bool = False):
84
+ self.name = name
85
+ self.action = action
86
+ self.selected = selected
87
+
88
+ class FileBuilder(Controller):
89
+ def __init__(self, file: str):
90
+ self.file = file
91
+ self.selectors = [
92
+ Selector("Classe", self.create_classes),
93
+ Selector("Função", self.create_functions),
94
+ Selector("CLI", self.create_cli),
95
+ Selector("Inputs", self.create_inputs),
96
+ ]
97
+ self.current_file = {}
98
+
99
+
100
+ @Controller.clear_terminal
101
+ def show_selector(self):
102
+ if not hasattr(self, 'tui'):
103
+ self.tui = TUI([self.file, {"":f"Selecione o tipo de funcionalidade que você quer testar no arquivo {Style.BRIGHT}{Fore.CYAN}{self.file}{Style.RESET_ALL}. Digite o número correspondente e aperte ENTER."}, {}], max_line=50)
104
+
105
+ for index, selector in enumerate(self.selectors):
106
+ self.tui.contents[2][f"{Style.BRIGHT}{Fore.CYAN}[{index + 1}]"] = f"{Fore.GREEN if selector.selected else Fore.WHITE}{selector.name}"
107
+
108
+ self.tui.show()
109
+
110
+
111
+ def index_is_selectable(self, index: int) -> bool:
112
+ try:
113
+ index = int(index)
114
+ available_selectors = len(self.selectors)
115
+ if index <= available_selectors and index >= 1:
116
+ return True
117
+ else:
118
+ return False
119
+ except ValueError:
120
+ return False
121
+
122
+
123
+ def get_selectors(self):
124
+ self.show_selector()
125
+ selecteds = 0
126
+ while True:
127
+ index = Controller.required_question(input)(Controller.required_text("> "))
128
+ if self.index_is_selectable(index):
129
+ self.selectors[int(index) - 1].selected = True
130
+ selecteds += 1
131
+ break
132
+
133
+ self.show_selector()
134
+ while True:
135
+ index = input(Controller.optional_text("> "))
136
+ if index.strip() != "":
137
+ if self.index_is_selectable(index):
138
+ index = int(index)
139
+ self.selectors[index - 1].selected = not self.selectors[index - 1].selected
140
+ if self.selectors[index - 1].selected:
141
+ selecteds += 1
142
+ else:
143
+ selecteds -= 1
144
+ self.show_selector()
145
+ else:
146
+ print(Controller.required_text("Indice inválido."))
147
+ else:
148
+ if selecteds == 0:
149
+ print(Controller.required_text("Pelo menos um item deve ser selecionado."))
150
+ else:
151
+ return
152
+
153
+
154
+ def create_structure(self):
155
+ if not "STRUCTURE" in self.current_file:
156
+ self.current_file["STRUCTURE"] = {}
157
+ return True
158
+
159
+
160
+ @Controller.clear_terminal
161
+ def create_classes(self):
162
+ self.create_structure()
163
+ if not "CLASSES" in self.current_file["STRUCTURE"]:
164
+ self.current_file["STRUCTURE"]["CLASSES"] = []
165
+
166
+ print(f"Iniciando registro de classes para o arquivo {Style.BRIGHT}{Fore.CYAN}{self.file}{Style.RESET_ALL}.")
167
+ while True:
168
+ current_class = {}
169
+ class_name = Controller.required_question(input)(Controller.required_text("Nome da classe: "))
170
+ current_class["name"] = class_name
171
+
172
+ is_initialized = Controller.required_question(input)(Controller.required_text("A classe possui um método __init__? (s/n): "))
173
+ if is_initialized.strip()[0].lower() == "s":
174
+ current_class["initializer"] = {}
175
+ current_class["initialized"] = True
176
+ while True:
177
+ print(f"Digite os parâmetros do método __init__ (um por vez). {Style.DIM}Aperte CTRL-C para parar.")
178
+ inputs = Controller.sequential_question(Controller.optional_text("> "))
179
+ current_class["initializer"]["input"] = inputs if inputs else []
180
+
181
+ class_info = input(Controller.optional_text(f"{CLEAR_LINE}Descrição da validação (instancialização da classe): "))
182
+ if class_info.strip() != "":
183
+ current_class["initializer"]["info"] = class_info
184
+ break
185
+
186
+ while True:
187
+ add_method = Controller.required_question(input)(Controller.required_text("Deseja adicionar um método à classe? (s/n): "))
188
+ if add_method[0].lower() == "s":
189
+ if not "methods" in current_class:
190
+ current_class["methods"] = []
191
+ method_data = {}
192
+
193
+ method_name = Controller.required_question(input)(Controller.required_text("Nome do método: "))
194
+ method_data["name"] = method_name
195
+
196
+ print(f"Digite os parâmetros do método {Style.DIM}Aperte CTRL-C para parar.")
197
+ method_inputs = Controller.sequential_question(Controller.optional_text("> "))
198
+ method_data["input"] = method_inputs if method_inputs else []
199
+
200
+ is_static = Controller.required_question(input)(Controller.required_text(f"{CLEAR_LINE}O método é estático? (s/n): "))
201
+ method_data["static"] = True if is_static.strip()[0].lower() == "s" else False
202
+
203
+ expected_output = input(Controller.optional_text("Output esperado: "))
204
+ method_data["expected"] = expected_output if expected_output else ""
205
+
206
+ method_info = input(Controller.optional_text("Descrição da validação: "))
207
+ if method_info.strip() != "":
208
+ method_data["info"] = method_info
209
+ current_class["methods"].append(method_data)
210
+ else:
211
+ break
212
+
213
+ self.current_file["STRUCTURE"]["CLASSES"].append(current_class)
214
+ print(f"Classe {Style.BRIGHT}{Fore.CYAN}{class_name}{Style.RESET_ALL} adicionada com sucesso!")
215
+ new_class = Controller.required_question(input)(Controller.required_text("Deseja adicionar outra classe? (s/n): "))
216
+ if new_class.strip()[0].lower() != "s":
217
+ break
218
+
219
+
220
+
221
+ @Controller.clear_terminal
222
+ def create_functions(self):
223
+ self.create_structure()
224
+ if not "FUNCTIONS" in self.current_file["STRUCTURE"]:
225
+ self.current_file["STRUCTURE"]["FUNCTIONS"] = []
226
+
227
+ print(f"Iniciando registro de funções para o arquivo {Style.BRIGHT}{Fore.CYAN}{self.file}{Style.RESET_ALL}.")
228
+ while True:
229
+ current_function = {}
230
+ function_name = Controller.required_question(input)(Controller.required_text("Nome da função: "))
231
+ current_function["name"] = function_name
232
+
233
+ print("Bateria de testes da função")
234
+ current_function["runs"] = []
235
+ while True:
236
+ current_run = {}
237
+
238
+ print(f"Digite os inputs necessários para testar a função. {Style.DIM}Aperte CTRL-C para parar.")
239
+ inputs = Controller.sequential_question(Controller.optional_text("> "))
240
+ current_run["input"] = inputs if inputs else []
241
+
242
+ expected = input(Controller.optional_text(f"{CLEAR_LINE}Output esperado: "))
243
+ current_run["expected"] = expected if expected else ""
244
+
245
+ info = input(Controller.optional_text("Descrição da validação: "))
246
+ if info.strip() != "":
247
+ current_run["info"] = info
248
+
249
+ current_function["runs"].append(current_run)
250
+ new_run = Controller.required_question(input)(Controller.required_text("Deseja adicionar outra bateria de testes? (s/n): "))
251
+ if new_run.strip()[0].lower() != "s":
252
+ break
253
+
254
+ self.current_file["STRUCTURE"]["FUNCTIONS"].append(current_function)
255
+ print(f"Função {Style.BRIGHT}{Fore.CYAN}{function_name}{Style.RESET_ALL} adicionada com sucesso!")
256
+
257
+ new_class = Controller.required_question(input)(Controller.required_text("Deseja adicionar outra função? (s/n): "))
258
+ if new_class.strip()[0].lower() != "s":
259
+ break
260
+
261
+
262
+
263
+ @Controller.clear_terminal
264
+ def create_cli(self):
265
+ self.current_file["CLI"] = []
266
+ print(f"Iniciando registro de CLI para o arquivo {Style.BRIGHT}{Fore.CYAN}{self.file}{Style.RESET_ALL}.")
267
+ while True:
268
+ current_cli = {}
269
+ inputs = input(Controller.optional_text("Argumentos do comando: "))
270
+ current_cli["input"] = inputs if inputs else ""
271
+
272
+ expected = input(Controller.optional_text("Output esperado: "))
273
+ current_cli["expected"] = expected if expected else ""
274
+
275
+ info = input(Controller.optional_text("Descrição da validação: "))
276
+ if info.strip() != "":
277
+ current_cli["info"] = info
278
+
279
+ self.current_file["CLI"].append(current_cli)
280
+ print(f"{Style.BRIGHT}{Fore.CYAN}Comando registrado com sucesso!")
281
+ new_run = Controller.required_question(input)(Controller.required_text("Deseja adicionar outro comando CLI? (s/n): "))
282
+ if new_run.strip()[0].lower() != "s":
283
+ break
284
+
285
+
286
+ @Controller.clear_terminal
287
+ def create_inputs(self):
288
+ print(f"Iniciando registro de inputs para o arquivo {Style.BRIGHT}{Fore.CYAN}{self.file}{Style.RESET_ALL}.")
289
+ self.current_file["INPUTS"] = []
290
+ while True:
291
+ current_seqinput = {}
292
+ print(f"Digite os inputs. {Style.DIM}Aperte CTRL-C para parar.")
293
+ inputs = Controller.sequential_question(Controller.optional_text("> "))
294
+ current_seqinput["input"] = inputs if inputs else []
295
+
296
+ print(f"{CLEAR_LINE}Digite os outputs esperados. {Style.DIM}Aperte CTRL-C para parar.")
297
+ expected = Controller.sequential_question(Controller.optional_text("> "))
298
+ current_seqinput["expected"] = expected if expected else []
299
+
300
+ info = input(Controller.optional_text(f"{CLEAR_LINE}Descrição da validação: "))
301
+ if info.strip() != "":
302
+ current_seqinput["info"] = info
303
+
304
+ self.current_file["INPUTS"].append(current_seqinput)
305
+ print(f"{Style.BRIGHT}{Fore.CYAN}Input registrado com sucesso!")
306
+ new_run = Controller.required_question(input)(Controller.required_text("Deseja adicionar outro input? (s/n): "))
307
+ if new_run.strip()[0].lower() != "s":
308
+ break
309
+
310
+
311
+ def purge_unselected(self):
312
+ selected = []
313
+ for selector in self.selectors:
314
+ if selector.selected:
315
+ selected.append(selector)
316
+ self.selectors = selected
317
+
318
+
319
+ def run_selected(self):
320
+ for selector in self.selectors:
321
+ selector.action()
322
+
323
+
324
+ def show_progress(self):
325
+ if not hasattr(self, 'progress_visualizer'):
326
+ self.progress_visualizer = copy.deepcopy(self.current_file)
327
+ visualizer = self.progress_visualizer
328
+ if "STRUCTURE" in visualizer:
329
+ for key in visualizer["STRUCTURE"]:
330
+ visualizer[key] = visualizer["STRUCTURE"][key]
331
+ del visualizer["STRUCTURE"]
332
+
333
+ TUI([f"{Fore.CYAN}{self.file}", *({f"{Fore.CYAN}[{index+1}]": f"{Fore.GREEN}{key}{Style.RESET_ALL} {self.current_file[key]}"} for index, key in enumerate(self.current_file.keys()))], max_line=50).show()
334
+
335
+
336
+ def start(self):
337
+ self.get_selectors()
338
+ self.purge_unselected()
339
+ self.run_selected()
340
+ self.show_progress()
341
+
342
+
343
+ class Builder:
344
+ default_info =[
345
+ f"{Fore.YELLOW}{Style.BRIGHT}Gerador de {settings.get_config('answers_file_name')}",
346
+ f"{Style.DIM}Seu progresso vai ficar aqui.",
347
+ {
348
+ f"{Style.BRIGHT}{Fore.RED}Informações importantes:": "",
349
+ f"{Style.BRIGHT}{Fore.CYAN}1.": "Leia atentamente a todos os avisos e instruções antes de prosseguir.",
350
+ f"{Style.BRIGHT}{Fore.CYAN}2.": f"Inputs {Fore.RED}VERMELHOS{Style.RESET_ALL} são obrigatórios, enquanto inputs {Fore.YELLOW}AMARELOS{Style.RESET_ALL} são opcionais.",
351
+ f"{Style.BRIGHT}{Fore.CYAN}3.": f"Alguns inputs exigem uma sequência de informções. Esses serão indicados com {Style.BRIGHT}setas (>){Style.RESET_ALL}.\nPara esse tipo de input, digite cada informação e aperte ENTER para confirmar. Inputs registrados com sucesso ficarão {Fore.GREEN}verdes{Style.RESET_ALL}.\nPara parar de adicionar informações, aperte {Style.BRIGHT}CTRL-C{Style.RESET_ALL} (salvo em situações que explicitamente indiquem o contrário).",
352
+ }
353
+ ]
354
+
355
+ @staticmethod
356
+ def confirm_yield():
357
+ input("Pressione ENTER para continuar...")
358
+
359
+ def __init__(self):
360
+ self.build = {}
361
+ self.info = TUI(self.default_info, max_line=80)
362
+
363
+ @Controller.clear_terminal
364
+ def show_info(self):
365
+ self.info.show()
366
+
367
+ def add_block(self, name: str, content: str):
368
+ self.build[name] = content
369
+
370
+ def show_progress(self, name: str, text: str):
371
+ progress = self.info.contents[1]
372
+ if not isinstance(progress, dict):
373
+ progress = {}
374
+
375
+ progress[f"{Style.BRIGHT}{name}"] = text
376
+ self.info.contents[1] = progress
377
+
378
+ self.show_info()
379
+
380
+ def get_files(self):
381
+ files = []
382
+
383
+ def format_filename(filename: str) -> str:
384
+ if not filename.endswith(".py"):
385
+ filename += ".py"
386
+ return filename
387
+
388
+ def get_extra_files() -> list:
389
+ extra_files = []
390
+ while True:
391
+ extra_file = input(f"{Controller.optional_text('> ')}")
392
+ if extra_file.strip() != "":
393
+ extra_files.append(format_filename(extra_file))
394
+ else:
395
+ break
396
+ return extra_files
397
+
398
+ print(f"\nNomeie os arquivos que você quer analisar. O primeiro arquivo é obrigatório.\n{Fore.YELLOW}OBSERVAÇÃO: A extensão .py será adicionada automaticamente.")
399
+ files.append(format_filename(Controller.required_question(input)(f"{Controller.required_text('Nome do primeiro arquivo: ')}")))
400
+ print(f"\nSe quiser adicionar mais arquivos, digite o nome deles um por vez.\n{Style.DIM}Para parar aperte ENTER sem digitar nada.")
401
+ files.extend(get_extra_files())
402
+
403
+ return list(dict.fromkeys(files))
404
+
405
+
406
+ @Controller.clear_terminal
407
+ def save_file(self):
408
+ assignment_name = Controller.required_question(input)(f"{Controller.required_text('Nome da atividade: ')}")
409
+ save_path = Path.cwd() / assignment_name
410
+
411
+ print(f"O arquivo \"correcao.json\" será salvo em {save_path}.")
412
+ change_path = input(f"{Controller.optional_text('Deseja alterar o caminho? (s/n): ')}")
413
+
414
+ if change_path and change_path.strip()[0].lower() == "s":
415
+ user_input = Controller.required_question(input)(f"{Controller.required_text('Novo caminho: ')}")
416
+ save_path = Path(user_input)
417
+
418
+ file_path = save_path / "correcao.json"
419
+ file_path.parent.mkdir(parents=True, exist_ok=True)
420
+
421
+ with open(file_path, "w", encoding="utf-8") as file:
422
+ json.dump(self.build, file, indent=4, ensure_ascii=False)
423
+
424
+ print(f"Arquivo salvo em {file_path}.")
425
+
426
+ def start(self):
427
+ try:
428
+ self.show_info()
429
+
430
+ self.confirm_yield()
431
+ files = self.get_files()
432
+ self.add_block("files", files)
433
+ self.show_progress("Arquivos selecionados: ", f"{'; '.join(files)}.")
434
+ self.confirm_yield()
435
+
436
+ Controller.clear_command()
437
+ for file in files:
438
+ fb = FileBuilder(file)
439
+ fb.start()
440
+ self.add_block(file, fb.current_file)
441
+
442
+ self.confirm_yield()
443
+
444
+ self.save_file()
445
+ except KeyboardInterrupt:
446
+ Controller.clear_command()
447
+ print(f"{Fore.RED}Operação cancelada pelo usuário. Nenhum arquivo foi salvo.")
@@ -7,32 +7,45 @@ import sys
7
7
  from types import SimpleNamespace
8
8
 
9
9
  from . import imports_tester
10
- from .config import settings
11
10
 
12
11
  class Checker:
12
+ """Classe responsável por validar exercícios baseado em um arquivo de correção"""
13
13
 
14
- def __init__(self, exercises_path: str, answers: dict):
14
+ def __init__(self, exercises_path: str, answers: dict) -> None:
15
15
  """Atribui os parâmetros passados para o objeto
16
-
17
- :param exercises_path: Caminho da pasta com arquivos do exercício
18
- :param answers: Dicionário de respostas no formato apropriado
16
+
17
+ Args:
18
+ exercises_path (str): Caminho da pasta com arquivos do exercício
19
+ answers (dict): Dicionário de respostas no formato apropriado
19
20
  """
20
21
  self.exercises_path = exercises_path
21
22
  self.answers = answers
22
23
  self.roadmap = []
23
24
  self.logger = logging.getLogger(__name__)
25
+
24
26
 
25
27
  def __file_exists(self, file_path: str) -> bool:
26
28
  """Checa se um arquivo existe em determinado caminho
27
29
 
28
- :param file_path: Caminho do arquivo
29
- :returns: Verdadeiro caso exista
30
- :rtype: bool
30
+ Args:
31
+ file_path (str): Caminho do arquivo
32
+
33
+ Returns:
34
+ bool: Booleano indicando se o arquivo existe
31
35
  """
32
36
  return Path(self.exercises_path / file_path).is_file()
33
37
 
38
+
34
39
  def setup_roadmap(self) -> bool:
35
- """Popula a lista roadmap com uma sequência de testes a serem realizados"""
40
+ """Popula a lista roadmap com uma sequência de testes a serem realizados
41
+
42
+ Raises:
43
+ ValueError: Arquivo de correção não possui características a serem testadas
44
+ ValueError: Característica desconhecida no arquivo de correção
45
+
46
+ Returns:
47
+ bool: Booleano indicando se o roadmap foi configurado com sucesso
48
+ """
36
49
  try:
37
50
  self.logger.info(f"Configurando roadmap")
38
51
  for file in self.answers["files"]:
@@ -70,7 +83,8 @@ class Checker:
70
83
  for class_info in subsequent_steps["CLASSES"]:
71
84
  self.logger.info(f"ADICIONANDO CLASSE {class_info['name']}")
72
85
  is_initialized = class_info.get("initialized", False)
73
- currentClass = imports_tester.ClassTester(importedFile, class_info["name"], class_info["methods"], is_initialized)
86
+ has_methods = class_info.get("methods", []) != []
87
+ currentClass = imports_tester.ClassTester(importedFile, class_info["name"], class_info["methods"] if has_methods else [], is_initialized)
74
88
  self.roadmap.append({
75
89
  "info": class_info.get("info", f"classe {class_info['name']} existe e possui os métodos esperados"),
76
90
  "args": [currentClass],
@@ -79,15 +93,16 @@ class Checker:
79
93
  if is_initialized:
80
94
  self.roadmap.append({
81
95
  "info": class_info.get("info", f"classe {class_info['name']} pode ser instanciada"),
82
- "args": [currentClass, *class_info["initializer"]["args"]],
96
+ "args": [currentClass, *class_info["initializer"]["input"]],
83
97
  "action": imports_tester.ClassTester.initialize_instance
84
98
  })
85
- for method_info in class_info["methods"]:
86
- self.roadmap.append({
87
- "info": method_info.get("info", f"{method_info['name']}({', '.join(map(str, method_info['input']))}) retorna {method_info['expected']}"),
88
- "args": [method_info["name"], method_info.get("static", False), method_info["input"], method_info["expected"]],
89
- "action": currentClass.test_method
90
- })
99
+ if has_methods:
100
+ for method_info in class_info["methods"]:
101
+ self.roadmap.append({
102
+ "info": method_info.get("info", f"{method_info['name']}({', '.join(map(str, method_info['input']))}) retorna {method_info['expected']}"),
103
+ "args": [method_info["name"], method_info.get("static", False), method_info["input"], method_info["expected"]],
104
+ "action": currentClass.test_method
105
+ })
91
106
  if subsequent_steps.get("FUNCTIONS"):
92
107
  self.logger.info(f"FUNÇÕES DETECTADAS")
93
108
  for function_info in subsequent_steps["FUNCTIONS"]:
@@ -105,21 +120,22 @@ class Checker:
105
120
  "args": [currentFunction, run["input"], run["expected"]],
106
121
  "action": imports_tester.FunctionTester.test
107
122
  })
108
- elif check_step == "INPUTS":
109
- for input_info in subsequent_steps:
110
- self.logger.info(f"ADICIONANDO INPUT {input_info['input']}")
111
- self.roadmap.append({
112
- "info": input_info.get("info", f"input '{input_info['input']}' retorna '{input_info['expected']}'"),
113
- "args": [current_file_path, input_info["input"], input_info["expected"]],
114
- "action": self.test_INPUT
115
- })
116
- elif check_step == "SEQUENCE_INPUTS":
123
+ elif check_step == "INPUTS" or check_step == "SEQUENCE_INPUTS":
117
124
  for sequence_input_info in subsequent_steps:
118
- self.logger.info(f"ADICIONANDO SEQUENCE_INPUTS {sequence_input_info['input']}")
125
+ self.logger.info(f"ADICIONANDO INPUTS {sequence_input_info['input']}")
126
+
127
+ expected = sequence_input_info['expected']
128
+ input_data = sequence_input_info['input']
129
+
130
+ if isinstance(expected, str):
131
+ expected = [expected]
132
+ if isinstance(input_data, str):
133
+ input_data = [input_data]
134
+
119
135
  self.roadmap.append({
120
- "info": sequence_input_info.get("info", f"input '{', '.join(sequence_input_info['input'])}' retorna '{', '.join(sequence_input_info['expected'])}'"),
121
- "args": [current_file_path, sequence_input_info["input"], sequence_input_info["expected"]],
122
- "action": self.test_SEQUENCE_INPUT
136
+ "info": sequence_input_info.get("info", f"input '{', '.join(input_data)}' retorna '{', '.join(expected)}'"),
137
+ "args": [current_file_path, input_data, expected],
138
+ "action": self.test_INPUTS
123
139
  })
124
140
  else:
125
141
  raise ValueError(f"Característica desconhecida '{check_step}' no arquivo de correção")
@@ -135,65 +151,104 @@ class Checker:
135
151
 
136
152
 
137
153
  def test_CLI(self, file_path: str, input_args: list[str], expected_output: str) -> bool:
138
- try:
139
- result = subprocess.run([sys.executable, file_path] + input_args, capture_output=True, text=True)
140
- self.logger.warning(f"Saída do comando '{' '.join(input_args)}': {result.stdout.strip()}")
141
- return expected_output in result.stdout.strip()
142
- except subprocess.CalledProcessError as e:
143
- self.logger.warning(f"O script '{input_args[0]}' falhou com o código de saída {e.returncode}")
144
- self.logger.warning(f"Detalhes do erro: {e.stderr}")
145
- return False
154
+ """Testa um comando CLI de um programa Python
155
+
156
+ Args:
157
+ file_path (str): Caminho do arquivo a ser testado
158
+ input_data (list[str]): Lista de inputs a serem fornecidos ao programa
159
+ expected_output (str): Saída esperada
146
160
 
147
- def test_INPUT(self, file: str, input_data: str, expected_output: str) -> bool:
161
+ Returns:
162
+ bool: Booleano indicando se a saída do programa contém a saída esperada
163
+ """
148
164
  try:
149
165
  result = subprocess.run(
150
- [sys.executable, file],
151
- input=input_data,
166
+ [sys.executable, file_path] + input_args,
152
167
  capture_output=True,
153
168
  text=True
154
169
  )
155
- return expected_output in result.stdout.strip()
156
- except subprocess.CalledProcessError as e:
157
- self.logger.warning(f"A ação do input '{input_data}' falhou com o código de saída {e.returncode}")
158
- self.logger.warning(f"Detalhes do erro: {e.stderr}")
170
+
171
+ if result.returncode != 0:
172
+ self.logger.warning(
173
+ f"[CLI] O programa terminou com código {result.returncode}"
174
+ )
175
+ self.logger.warning(f"[CLI] stderr: {result.stderr}")
176
+ return False
177
+
178
+ output = result.stdout.strip()
179
+ self.logger.debug(f"[CLI] Saída do comando '{' '.join(input_args)}': {output}")
180
+
181
+ return expected_output in output
182
+
183
+ except Exception as e:
184
+ self.logger.warning(
185
+ f"[CLI] Falha ao executar o comando '{file_path} {' '.join(input_args)}': {e}"
186
+ )
159
187
  return False
160
188
 
161
- def test_SEQUENCE_INPUT(self, file: str, input_data: list[str], expected_output: list[str]) -> bool:
189
+ def test_INPUTS(self, file_path: str, input_data: list[str], expected_output: list[str]) -> bool:
190
+ """Testa uma sequência de inputs
191
+
192
+ Args:
193
+ file_path (str): Caminho do arquivo a ser testado
194
+ input_data (list[str]): Lista de inputs a serem fornecidos ao programa
195
+ expected_output (list[str]): Lista de saídas esperadas
196
+
197
+ Returns:
198
+ bool: Booleano indicando se a saída do programa contém a saída esperada
199
+ """
162
200
  try:
163
201
  result = subprocess.run(
164
- [sys.executable, file],
165
- input=("\n").join(input_data),
166
- capture_output=True,
202
+ [sys.executable, file_path],
203
+ input="\n".join(input_data) + "\n",
204
+ capture_output=True,
167
205
  text=True
168
206
  )
207
+
208
+ if result.returncode != 0:
209
+ self.logger.warning(
210
+ f"[INPUT] O programa terminou com código {result.returncode}"
211
+ )
212
+ self.logger.warning(f"[INPUT] stderr: {result.stderr}")
213
+ return False
214
+
169
215
  output = result.stdout.strip()
170
- return all(palavra in output for palavra in expected_output)
171
- except subprocess.CalledProcessError as e:
172
- self.logger.warning(f"A ação do input múltiplo '{input_data}' falhou com o código de saída {e.returncode}")
173
- self.logger.warning(f"Detalhes do erro: {e.stderr}")
216
+ self.logger.debug(f"[INPUT] Saída do input: {output}")
217
+
218
+ return all(
219
+ expected in output
220
+ for expected in expected_output
221
+ )
222
+
223
+ except Exception as e:
224
+ self.logger.warning(
225
+ f"[INPUT] Falha ao executar o input '{input_data}': {e}"
226
+ )
174
227
  return False
175
228
 
229
+
176
230
  def make_result_message(self, result: bool, info: str) -> str:
177
- """Gera uma mensagem de resultado formatada com cores
231
+ """Forma uma mensagem de resultado com cores e estilo apropriados
232
+
233
+ Args:
234
+ result (bool): Resultado do teste
235
+ info (str): Informação sobre o teste
178
236
 
179
- :param result: Resultado do teste
180
- :param info: Informação sobre o teste
181
- :returns: Mensagem formatada
182
- :rtype: str
237
+ Returns:
238
+ str: Mensagem formatada
183
239
  """
184
240
  colors = {
185
- True: Fore.GREEN if settings.enviroment_supports_colors else "",
186
- False: Fore.RED if settings.enviroment_supports_colors else "",
187
- "bold": Style.BRIGHT if settings.enviroment_supports_colors else "",
188
- "reset": Style.RESET_ALL if settings.enviroment_supports_colors else ""
241
+ True: Fore.GREEN,
242
+ False: Fore.RED
189
243
  }
190
- return f"{colors[result]}{colors['bold']}{':)' if result else ':('}{colors['reset']} {colors[result]}{info}{colors['reset']}"
244
+ return f"{colors[result]}{Style.BRIGHT}{':)' if result else ':('}{Style.RESET_ALL} {colors[result]}{info}{Style.RESET_ALL}"
245
+
191
246
 
192
247
  def run_roadmap(self) -> list:
193
248
  """Executa os testes do roadmap e retorna uma lista de resultados
194
249
 
195
- :returns: Lista de resultados dos testes
196
- :rtype: list
250
+ Returns:
251
+ list: Lista de resultados dos testes
197
252
  """
198
253
  results = []
199
254
  for step in self.roadmap:
@@ -208,10 +263,12 @@ class Checker:
208
263
 
209
264
  return results
210
265
 
266
+
211
267
  def show_results(self, results: list) -> None:
212
268
  """Exibe os resultados dos testes no console
213
269
 
214
- :param results: Lista de resultados dos testes
270
+ Args:
271
+ results (list): Lista de resultados dos testes
215
272
  """
216
273
  if self.answers.get("description"):
217
274
  print(f"{self.answers['description']}")
@@ -1,18 +1,9 @@
1
1
  from platformdirs import user_config_dir
2
2
  from importlib.metadata import version, PackageNotFoundError
3
- from colorama import init
4
3
  from time import time
5
4
  import json
6
- import sys
7
5
  import os
8
6
 
9
- def setup_colors():
10
- if not sys.stdout.isatty():
11
- init(strip=True, convert=False)
12
- return False
13
-
14
- init(autoreset=True)
15
- return True
16
7
 
17
8
  class Config:
18
9
  _instance = None
@@ -26,7 +17,6 @@ class Config:
26
17
  if hasattr(self, "_initialized"):
27
18
  return
28
19
  self._initialized = True
29
- self.enviroment_supports_colors = setup_colors()
30
20
 
31
21
  config_dir = user_config_dir("verifica")
32
22
  os.makedirs(config_dir, exist_ok=True)
@@ -49,13 +39,8 @@ class Config:
49
39
  return {}
50
40
 
51
41
  def __check_config_state(self):
52
- try:
53
- current_version = version("verifica")
54
- except PackageNotFoundError:
55
- current_version = f"indev-{time()}"
56
-
57
42
  defaultConfig = {
58
- "version": current_version,
43
+ "version": self.get_version(),
59
44
  "url": "https://raw.githubusercontent.com",
60
45
  "answers_file_name": "correcao.json"
61
46
  }
@@ -67,6 +52,13 @@ class Config:
67
52
  if current_config.get("version") != defaultConfig.get("version"):
68
53
  self.update_config(defaultConfig, defaultConfig.get("version"))
69
54
 
55
+ def get_version(self):
56
+ try:
57
+ __version__ = version("verifica")
58
+ except PackageNotFoundError:
59
+ __version__ = f"indev-{time()}"
60
+ return __version__
61
+
70
62
  def update_config(self, new_data: dict, version: str = None):
71
63
  current_config = self.__read_keys()
72
64
  if version:
@@ -68,7 +68,14 @@ class Fetcher:
68
68
  if not self.file:
69
69
  raise ValueError("o arquivo de correção não existe")
70
70
 
71
- return json.loads(self.get_content())
71
+ try:
72
+ decoded = json.loads(self.get_content())
73
+ except json.JSONDecodeError as error:
74
+ self.logger.error(f"Falha ao decodificar o arquivo de correção '{self.exercise}': {error}")
75
+ raise RuntimeError(f"Falha ao decodificar o arquivo de correção '{self.exercise}'") from error
76
+
77
+ return decoded
78
+
72
79
 
73
80
  def cleanup(self):
74
81
  if self.file != None:
@@ -39,7 +39,7 @@ class Imported:
39
39
  raise ImportError(f"Falha ao importar o módulo '{module_path}'") from e
40
40
 
41
41
  class ClassTester:
42
- def __init__(self, import_info: Imported, class_name: str, methods: list, initialized: bool):
42
+ def __init__(self, import_info: Imported, class_name: str, methods: list = [], initialized: bool = False):
43
43
  self.import_info = import_info
44
44
  self.class_name = class_name
45
45
  self.methods = methods
@@ -0,0 +1,104 @@
1
+ from colorama import Style
2
+ import re
3
+
4
+ # Regex para códigos ANSI de cores e formatação
5
+ ANSI_PATTERN = re.compile(r"(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]")
6
+
7
+ class TUI:
8
+ @staticmethod
9
+ def visible_len(text: str) -> int:
10
+ """Retorna o tamanho de uma string sem caracteres de formatação ANSI"""
11
+ return len(ANSI_PATTERN.sub("", text))
12
+
13
+ def __init__(self, contents: list, show: bool = False, max_line: int = -1, padding: int = 1):
14
+ self.contents = contents
15
+ self.padding = padding
16
+ self.max_line = max_line
17
+ self.min_size = self.__get_longest_message_length() + (padding * 2)
18
+ self.row_size = self.min_size + self.padding
19
+ if show:
20
+ self.show()
21
+
22
+ def __get_longest_message_length(self):
23
+ longest = 0
24
+ for block in self.contents:
25
+ if isinstance(block, dict):
26
+ for k, v in block.items():
27
+ for line in self.__get_paragraphs(k, self.__create_paragraphs(k, v)):
28
+ length = self.visible_len(line)
29
+ if length > longest:
30
+ longest = length
31
+ else:
32
+ length = self.visible_len(block)
33
+ if length > longest:
34
+ longest = length
35
+ return longest
36
+
37
+ def __get_paragraphs(self, key: str, text: str) -> list:
38
+ lines = []
39
+ paragraphs = text.split('\n')
40
+
41
+ lines.append(f"{key}{Style.RESET_ALL} {paragraphs[0]}")
42
+ for paragraph in paragraphs[1:]:
43
+ lines.append(f"{' ' * (self.visible_len(key) + 1)}{paragraph}{Style.RESET_ALL}")
44
+
45
+ return lines
46
+
47
+ def __create_paragraphs(self, key: str, text: str) -> str:
48
+ if self.max_line >= 1:
49
+ paragraphs = []
50
+ unformatted_paragraphs = text.split('\n')
51
+
52
+ for paragraph in unformatted_paragraphs:
53
+ words = paragraph.split()
54
+ current_line = ""
55
+ for word in words:
56
+ if self.visible_len(f"{key} {current_line}") + self.visible_len(word) + 1 <= self.max_line:
57
+ current_line += f"{word} "
58
+ else:
59
+ paragraphs.append(current_line.strip())
60
+ current_line = f"{word} "
61
+
62
+ if current_line:
63
+ paragraphs.append(current_line.strip())
64
+
65
+ return '\n'.join(paragraphs)
66
+ return [text]
67
+
68
+ def __create_top_border(self):
69
+ return f"╔{'═' * self.row_size}╗"
70
+
71
+ def __create_internal_border(self):
72
+ return f"╠{'═' * self.row_size}╣"
73
+
74
+ def __create_bottom_border(self):
75
+ return f"╚{'═' * self.row_size}╝"
76
+
77
+ def __create_left_border(self):
78
+ return f"║"
79
+
80
+ def __create_right_border(self):
81
+ return f"║"
82
+
83
+ def __create_center_row(self, text: str):
84
+ return f"{self.__create_left_border()}{" "*self.padding}{text.center(self.min_size - self.padding + (len(text) - self.visible_len(text)))}{Style.RESET_ALL}{" "*self.padding}{self.__create_right_border()}"
85
+
86
+ def __create_row(self, text: str):
87
+ return f"{self.__create_left_border()}{" "*self.padding}{text.ljust(self.min_size - self.padding + (len(text) - self.visible_len(text)))}{" "*self.padding}{Style.RESET_ALL}{self.__create_right_border()}"
88
+
89
+ def show(self):
90
+ last_element = len(self.contents) - 1
91
+
92
+ print(f"{self.__create_top_border()}")
93
+ for index, block in enumerate(self.contents):
94
+ if isinstance(block, dict):
95
+ for key, message in self.contents[index].items():
96
+ for line in self.__get_paragraphs(key, self.__create_paragraphs(key, message)):
97
+ print(f"{self.__create_row(line)}")
98
+ else:
99
+ print(f"{self.__create_center_row(block)}")
100
+
101
+ if index != last_element:
102
+ print(f"{self.__create_internal_border()}")
103
+ else:
104
+ print(f"{self.__create_bottom_border()}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: verifica
3
- Version: 1.0.0.dev2
3
+ Version: 1.0.2
4
4
  Summary: Uma ferramenta simples para correção de atividades em python via CLI.
5
5
  Author-email: Théo Modeneis Ruela <theo.ruela@gmail.com>
6
6
  License-Expression: MIT
@@ -8,7 +8,7 @@ Project-URL: Homepage, https://github.com/espinafr/Verifica
8
8
  Project-URL: Issues, https://github.com/espinafr/Verifica/issues
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Operating System :: OS Independent
11
- Requires-Python: >=3.9.13
11
+ Requires-Python: >=3.9
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: colorama>=0.4.6
@@ -24,9 +24,6 @@ Dynamic: license-file
24
24
  <td><a href="https://pypi.org/project/verifica/">Página no Pypi</a></td>
25
25
  <td align="right"><a href="https://github.com/espinafr/verifica/issues/new?template=denúncia-de-bug.yml">Reportar bug</a></td>
26
26
  </tr>
27
- <tr>
28
- <td colspan="2" align="center"><a href="https://espinafr.github.io/verifica">Gerador <code>correcao.json</code></a></td>
29
- </tr>
30
27
  <tr>
31
28
  <td colspan="2" align="center"><a href="https://github.com/espinafr/verifica/tree/master/docs">Como configurar</a></td>
32
29
  </tr>
@@ -74,7 +71,7 @@ pip install dist/{NOME DO ARQUIVO GERADO}
74
71
 
75
72
  ## Reconhecimentos
76
73
 
77
- Veja o arquivo [ACKNOWLEDGEMENTS](https://github.com/espinafr/verifica/blob/master/ACKNOWLEDGEMENTS.md)
74
+ Veja o arquivo [ACKNOWLEDGEMENTS](https://github.com/espinafr/verifica/blob/master/ACKNOWLEDGEMENTS.md).
78
75
 
79
76
  ## Licença
80
77
 
@@ -3,10 +3,12 @@ README.md
3
3
  pyproject.toml
4
4
  src/verifica/__init__.py
5
5
  src/verifica/__main__.py
6
+ src/verifica/builder.py
6
7
  src/verifica/check.py
7
8
  src/verifica/config.py
8
9
  src/verifica/fetcher.py
9
10
  src/verifica/imports_tester.py
11
+ src/verifica/tui.py
10
12
  src/verifica.egg-info/PKG-INFO
11
13
  src/verifica.egg-info/SOURCES.txt
12
14
  src/verifica.egg-info/dependency_links.txt
File without changes
File without changes