verifica 1.0.0.dev1__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 Théo Modeneis Ruela
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,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: verifica
3
+ Version: 1.0.0.dev1
4
+ Summary: Uma ferramenta simples para correção de atividades em python via CLI.
5
+ Author-email: Théo Modeneis Ruela <theo.ruela@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/espinafr/Verifica
8
+ Project-URL: Issues, https://github.com/espinafr/Verifica/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9.13
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: colorama>=0.4.6
15
+ Requires-Dist: platformdirs>=2.5.2
16
+ Dynamic: license-file
17
+
18
+ <div align="center">
19
+ <h1 align="center">Verifica</h1>
20
+ <p align="center">Uma ferramenta simples para correção de atividades em python via CLI.</p>
21
+ </div>
22
+
23
+ ## Sobre o projeto
24
+
25
+ **Verifica** é uma ferramenta simples que permite que professores preparem testes automatizados para corrigir suas ativiaddes em python, fornecendo aos alunos uma correção instantânea para seus programas antes deles serem enviados.
26
+
27
+ ## Como compilar
28
+
29
+ Para gerar os arquivos de distribuição do projeto, certifique-se de ter o módulo `build` instalado:
30
+
31
+ ```bash
32
+ pip install build
33
+ ```
34
+
35
+ Em seguida, execute o comando de construção na raiz do projeto:
36
+
37
+ ```bash
38
+ python -m build
39
+ ```
40
+
41
+ Os arquivos gerados (arquivos `.tar.gz` e `.whl`) estarão disponíveis no diretório `dist/`. Use o arquivo `.whl` para distribuição e instalação do pacote via pip.
42
+
43
+ ```bash
44
+ pip install dist/{NOME DO ARQUIVO GERADO}
45
+ ```
46
+
47
+ ## Licença
48
+
49
+ Veja o arquivo LICENSE.
@@ -0,0 +1,32 @@
1
+ <div align="center">
2
+ <h1 align="center">Verifica</h1>
3
+ <p align="center">Uma ferramenta simples para correção de atividades em python via CLI.</p>
4
+ </div>
5
+
6
+ ## Sobre o projeto
7
+
8
+ **Verifica** é uma ferramenta simples que permite que professores preparem testes automatizados para corrigir suas ativiaddes em python, fornecendo aos alunos uma correção instantânea para seus programas antes deles serem enviados.
9
+
10
+ ## Como compilar
11
+
12
+ Para gerar os arquivos de distribuição do projeto, certifique-se de ter o módulo `build` instalado:
13
+
14
+ ```bash
15
+ pip install build
16
+ ```
17
+
18
+ Em seguida, execute o comando de construção na raiz do projeto:
19
+
20
+ ```bash
21
+ python -m build
22
+ ```
23
+
24
+ Os arquivos gerados (arquivos `.tar.gz` e `.whl`) estarão disponíveis no diretório `dist/`. Use o arquivo `.whl` para distribuição e instalação do pacote via pip.
25
+
26
+ ```bash
27
+ pip install dist/{NOME DO ARQUIVO GERADO}
28
+ ```
29
+
30
+ ## Licença
31
+
32
+ Veja o arquivo LICENSE.
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools >= 77.0.3"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "verifica"
7
+ version = "1.0.0.dev1"
8
+ dependencies = [
9
+ "colorama>=0.4.6",
10
+ "platformdirs>=2.5.2"
11
+ ]
12
+ authors = [
13
+ { name="Théo Modeneis Ruela", email="theo.ruela@gmail.com" },
14
+ ]
15
+ description = "Uma ferramenta simples para correção de atividades em python via CLI."
16
+ readme = "README.md"
17
+ requires-python = ">=3.9.13"
18
+ classifiers = [
19
+ "Programming Language :: Python :: 3",
20
+ "Operating System :: OS Independent",
21
+ ]
22
+ license = "MIT"
23
+ license-files = ["LICEN[CS]E*"]
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/espinafr/Verifica"
27
+ Issues = "https://github.com/espinafr/Verifica/issues"
28
+
29
+ [project.scripts]
30
+ verifica = "verifica.__main__:main"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,71 @@
1
+ import argparse
2
+ from pathlib import Path
3
+ import logging
4
+ import sys
5
+
6
+ from .config import settings
7
+ from .check import Checker
8
+ from .fetcher import Fetcher
9
+
10
+ logging.basicConfig(
11
+ level=logging.ERROR,
12
+ format='%(levelname)s - %(message)s',
13
+ handlers=[logging.FileHandler(f'{settings.config_dir}/app.log', mode='w', encoding='utf-8'), logging.StreamHandler(stream=sys.stdout)]
14
+ )
15
+
16
+ def main():
17
+ parser = argparse.ArgumentParser(description="Uma ferramenta simples para correção de atividades em python via CLI.", add_help=False)
18
+ positional = parser.add_argument_group("argumentos posicionais")
19
+ options = parser.add_argument_group("opções")
20
+
21
+ positional.add_argument("atividade", nargs="?", help="A URL do github usada para a correção da atividade.")
22
+ options.add_argument("-f", "--files", default=Path.cwd(), help="Diretório com a(s) atividade(s) a ser(em) corrigida(s).")
23
+ options.add_argument("-h", "--help", "--ajuda", action="help", help="Mostra essa mensagem de ajuda.")
24
+ options.add_argument("-c", "--config", action="store_true", help="Mostra o caminho do arquivo de configuração.")
25
+ options.add_argument("-d", "--debug", action="store_true", help="Ativa o modo debug, mostrando mais informações durante a execução.")
26
+ 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
+
28
+ args = parser.parse_args()
29
+
30
+ if args.debug:
31
+ logging.getLogger().setLevel(logging.DEBUG)
32
+ logging.info("Modo debug ativado. Mostrando informações detalhadas durante a execução.")
33
+
34
+ if args.config:
35
+ print(settings.config_path)
36
+ return
37
+
38
+ if not args.atividade:
39
+ parser.error("o seguinte argumento é obrigatório: atividade")
40
+
41
+ if not args.local:
42
+ answers = Fetcher(args.atividade)
43
+ try:
44
+ print("Baixando arquivo de correção...")
45
+ logging.debug(f"CAMINHO DO ARQUIVO BAIXADO: {answers.fetch()}")
46
+ except Exception as e:
47
+ logging.error(f"Não foi possível localizar o arquivo de correção em '{args.atividade}'")
48
+ sys.exit(1)
49
+ else:
50
+ answers = Fetcher(args.atividade, local=True)
51
+ try:
52
+ print("Buscando arquivo de correção local...")
53
+ logging.debug(f"CAMINHO DO ARQUIVO LOCAL: {answers.get_file()}")
54
+ except Exception as e:
55
+ logging.error(f"Não foi possível localizar o arquivo de correção em '{args.atividade}'")
56
+ sys.exit(1)
57
+
58
+ decoded_answers = answers.get_decoded_json()
59
+ answers.cleanup()
60
+
61
+ checker = Checker(args.files, decoded_answers)
62
+
63
+ checker.setup_roadmap()
64
+
65
+ results = checker.run_roadmap()
66
+ checker.show_results(results)
67
+
68
+ sys.exit(0)
69
+
70
+ if __name__ == "__main__":
71
+ main()
@@ -0,0 +1,219 @@
1
+ from colorama import Fore, Style
2
+ from pathlib import Path
3
+ import subprocess
4
+ import traceback
5
+ import logging
6
+ import sys
7
+ from types import SimpleNamespace
8
+
9
+ from . import imports_tester
10
+ from .config import settings
11
+
12
+ class Checker:
13
+
14
+ def __init__(self, exercises_path: str, answers: dict):
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
19
+ """
20
+ self.exercises_path = exercises_path
21
+ self.answers = answers
22
+ self.roadmap = []
23
+ self.logger = logging.getLogger(__name__)
24
+
25
+ def __file_exists(self, file_path: str) -> bool:
26
+ """Checa se um arquivo existe em determinado caminho
27
+
28
+ :param file_path: Caminho do arquivo
29
+ :returns: Verdadeiro caso exista
30
+ :rtype: bool
31
+ """
32
+ return Path(self.exercises_path / file_path).is_file()
33
+
34
+ def setup_roadmap(self) -> bool:
35
+ """Popula a lista roadmap com uma sequência de testes a serem realizados"""
36
+ try:
37
+ self.logger.info(f"Configurando roadmap")
38
+ for file in self.answers["files"]:
39
+ current_file_path = Path(self.exercises_path) / file
40
+ self.logger.info(f"Inicindo detecções para o arquivo '{file}'")
41
+
42
+ if len(self.answers[file]) == 0:
43
+ raise ValueError(f"O arquivo de correção para '{file}' não possui características a serem testadas")
44
+
45
+ self.logger.info(file)
46
+ self.roadmap.append({
47
+ "info": f"'{file}' existe",
48
+ "args": [self, file],
49
+ "action": Checker.__file_exists
50
+ })
51
+
52
+ for check_step, subsequent_steps in self.answers[file].items():
53
+ self.logger.info(f"CARACTERÍSTICA DETECTADA: {check_step}")
54
+ if check_step == "CLI":
55
+ for command in subsequent_steps:
56
+ self.logger.info(f"ADICIONANDO COMANDO CLI: {file} {command['input']}")
57
+ self.roadmap.append({
58
+ "info": command.get("info", f"comando '{command['input']}' retorna '{command['expected']}'"),
59
+ "args": [self, current_file_path, command["input"].split(" "), command["expected"]],
60
+ "action": Checker.test_CLI
61
+ })
62
+ elif check_step == "STRUCTURE":
63
+ try:
64
+ importedFile = imports_tester.Imported(current_file_path)
65
+ except Exception as e:
66
+ self.logger.warning(f"Falha ao importar o arquivo '{file}': {e}")
67
+ importedFile = SimpleNamespace(module=None, logger=self.logger)
68
+ if subsequent_steps.get("CLASSES"):
69
+ self.logger.info(f"CLASSES DETECTADAS")
70
+ for class_info in subsequent_steps["CLASSES"]:
71
+ self.logger.info(f"ADICIONANDO CLASSE {class_info['name']}")
72
+ is_initialized = class_info.get("initialized", False)
73
+ currentClass = imports_tester.ClassTester(importedFile, class_info["name"], class_info["methods"], is_initialized)
74
+ self.roadmap.append({
75
+ "info": class_info.get("info", f"classe {class_info['name']} existe e possui os métodos esperados"),
76
+ "args": [currentClass],
77
+ "action": imports_tester.ClassTester.get_existance
78
+ })
79
+ if is_initialized:
80
+ self.roadmap.append({
81
+ "info": class_info.get("info", f"classe {class_info['name']} pode ser instanciada"),
82
+ "args": [currentClass, *class_info["initializer"]["args"]],
83
+ "action": imports_tester.ClassTester.initialize_instance
84
+ })
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
+ })
91
+ if subsequent_steps.get("FUNCTIONS"):
92
+ self.logger.info(f"FUNÇÕES DETECTADAS")
93
+ for function_info in subsequent_steps["FUNCTIONS"]:
94
+ self.logger.info(f"ADICIONANDO FUNÇÃO {function_info['name']}")
95
+ currentFunction = imports_tester.FunctionTester(importedFile, function_info["name"])
96
+ self.roadmap.append({
97
+ "info": function_info.get("info", f"função {function_info['name']} existe"),
98
+ "args": [currentFunction],
99
+ "action": imports_tester.FunctionTester.get_existance
100
+ })
101
+ for run in function_info["runs"]:
102
+ self.logger.info(f"ADICIONANDO RUN {function_info['name']}({', '.join(map(str, run['input']))})")
103
+ self.roadmap.append({
104
+ "info": run.get("info", f"função {function_info['name']}({', '.join(map(str, run['input']))}) retorna {run['expected']}"),
105
+ "args": [currentFunction, run["input"], run["expected"]],
106
+ "action": imports_tester.FunctionTester.test
107
+ })
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":
117
+ for sequence_input_info in subsequent_steps:
118
+ self.logger.info(f"ADICIONANDO SEQUENCE_INPUTS {sequence_input_info['input']}")
119
+ 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
123
+ })
124
+ else:
125
+ raise ValueError(f"Característica desconhecida '{check_step}' no arquivo de correção")
126
+ except (ValueError, KeyError) as e:
127
+ self.logger.error(f"Estrutura do arquivo de correção inválida para o arquivo '{file}'.\nDetalhes: {e}")
128
+ return False
129
+ except Exception as e:
130
+ self.logger.error(f"Erro ao configurar roadmap para o arquivo '{file}'.")
131
+ self.logger.error(f"Detalhes do erro: {e}\n{traceback.format_exc()}")
132
+ return False
133
+
134
+ return True
135
+
136
+
137
+ 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
146
+
147
+ def test_INPUT(self, file: str, input_data: str, expected_output: str) -> bool:
148
+ try:
149
+ result = subprocess.run(
150
+ [sys.executable, file],
151
+ input=input_data,
152
+ capture_output=True,
153
+ text=True
154
+ )
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}")
159
+ return False
160
+
161
+ def test_SEQUENCE_INPUT(self, file: str, input_data: list[str], expected_output: list[str]) -> bool:
162
+ try:
163
+ result = subprocess.run(
164
+ [sys.executable, file],
165
+ input=("\n").join(input_data),
166
+ capture_output=True,
167
+ text=True
168
+ )
169
+ 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}")
174
+ return False
175
+
176
+ def make_result_message(self, result: bool, info: str) -> str:
177
+ """Gera uma mensagem de resultado formatada com cores
178
+
179
+ :param result: Resultado do teste
180
+ :param info: Informação sobre o teste
181
+ :returns: Mensagem formatada
182
+ :rtype: str
183
+ """
184
+ 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 ""
189
+ }
190
+ return f"{colors[result]}{colors['bold']}{':)' if result else ':('}{colors['reset']} {colors[result]}{info}{colors['reset']}"
191
+
192
+ def run_roadmap(self) -> list:
193
+ """Executa os testes do roadmap e retorna uma lista de resultados
194
+
195
+ :returns: Lista de resultados dos testes
196
+ :rtype: list
197
+ """
198
+ results = []
199
+ for step in self.roadmap:
200
+ try:
201
+ self.logger.debug(step["args"])
202
+ result = step["action"](*step["args"])
203
+ except Exception as e:
204
+ result = False
205
+ self.logger.warning(f"Erro ao executar o teste '{step['info']}': {e}\n{traceback.format_exc()}")
206
+
207
+ results.append(self.make_result_message(result, step["info"]))
208
+
209
+ return results
210
+
211
+ def show_results(self, results: list) -> None:
212
+ """Exibe os resultados dos testes no console
213
+
214
+ :param results: Lista de resultados dos testes
215
+ """
216
+ if self.answers.get("description"):
217
+ print(f"{self.answers['description']}")
218
+ for result in results:
219
+ print(result)
@@ -0,0 +1,88 @@
1
+ from platformdirs import user_config_dir
2
+ from importlib.metadata import version, PackageNotFoundError
3
+ from colorama import init
4
+ from time import time
5
+ import json
6
+ import sys
7
+ import os
8
+
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
+
17
+ class Config:
18
+ _instance = None
19
+
20
+ def __new__(cls, *args, **kwargs):
21
+ if cls._instance is None:
22
+ cls._instance = super().__new__(cls, *args, **kwargs)
23
+ return cls._instance
24
+
25
+ def __init__(self):
26
+ if hasattr(self, "_initialized"):
27
+ return
28
+ self._initialized = True
29
+ self.enviroment_supports_colors = setup_colors()
30
+
31
+ config_dir = user_config_dir("verifica")
32
+ os.makedirs(config_dir, exist_ok=True)
33
+
34
+ self.config_dir = config_dir
35
+ self.config_path = os.path.join(config_dir, "config.toml")
36
+ self.__check_config_state()
37
+
38
+ def __save_keys(self, data: dict):
39
+ with open(self.config_path, "w", encoding="utf-8") as f:
40
+ json.dump(data, f, indent=4)
41
+
42
+ def __read_keys(self) -> dict:
43
+ if not self.config_path or not os.path.exists(self.config_path):
44
+ return {}
45
+ with open(self.config_path, "r", encoding="utf-8") as f:
46
+ try:
47
+ return json.load(f)
48
+ except json.decoder.JSONDecodeError:
49
+ return {}
50
+
51
+ def __check_config_state(self):
52
+ try:
53
+ current_version = version("verifica")
54
+ except PackageNotFoundError:
55
+ current_version = f"indev-{time()}"
56
+
57
+ defaultConfig = {
58
+ "version": current_version,
59
+ "url": "https://raw.githubusercontent.com",
60
+ "answers_file_name": "correcao.json"
61
+ }
62
+
63
+ current_config = self.__read_keys()
64
+ if len(current_config) == 0:
65
+ self.__save_keys(defaultConfig)
66
+ else:
67
+ if current_config.get("version") != defaultConfig.get("version"):
68
+ self.update_config(defaultConfig, defaultConfig.get("version"))
69
+
70
+ def update_config(self, new_data: dict, version: str = None):
71
+ current_config = self.__read_keys()
72
+ if version:
73
+ current_config["version"] = version
74
+ new_data.update(current_config)
75
+ self.__save_keys(new_data)
76
+
77
+ def get_config(self, key: str = None) -> dict:
78
+ config = self.__read_keys()
79
+ if key:
80
+ return config.get(key, "")
81
+ return config
82
+
83
+ def set_config(self, key: str, value):
84
+ config = self.__read_keys()
85
+ config[key] = value
86
+ self.update_config(config)
87
+
88
+ settings = Config()
@@ -0,0 +1,75 @@
1
+ from pathlib import Path
2
+ from urllib.error import HTTPError, URLError
3
+ from urllib.request import Request, urlopen
4
+ import tempfile
5
+ import logging
6
+ import json
7
+
8
+ from .config import settings
9
+
10
+ class Fetcher:
11
+ def __init__(self, path, local: bool = False):
12
+ self.exercise = path
13
+ self.local = local
14
+ if not local:
15
+ self.base_url = settings.get_config("url")
16
+ self.remote_path = f"{self.exercise.strip()}/{settings.get_config('answers_file_name')}"
17
+ self.logger = logging.getLogger(__name__)
18
+
19
+ def __str__(self):
20
+ return f"Fetcher(url=\"{self._build_url()}\")"
21
+
22
+ def _build_url(self):
23
+ return f"{self.base_url}/{self.remote_path}"
24
+
25
+ def fetch(self) -> str:
26
+ """Baixa o arquivo de respostas e salva em uma pasta temporária
27
+
28
+ :returns: O caminho do arquivo salvo
29
+ :rtype: str
30
+ :raises RuntimeError: Se não for possível buscar o exercício
31
+ """
32
+ try:
33
+ self.logger.info("Baixando arquivo de correção...")
34
+ request = Request(self._build_url())
35
+
36
+ request.add_header("Cache-Control", "no-cache, no-store, must-revalidate")
37
+ request.add_header("Pragma", "no-cache")
38
+ request.add_header("Expires", "0")
39
+
40
+ with urlopen(request) as response:
41
+ content = response.read().decode("utf-8")
42
+ except (HTTPError, URLError) as error:
43
+ self.logger.error(f"Falha ao buscar o arquivo de correção '{self.exercise}': {error}")
44
+ raise RuntimeError(f"Falha ao buscar o arquivo de correção '{self.exercise}'") from error
45
+
46
+ self.file = tempfile.NamedTemporaryFile(mode='w+t', prefix='verifica-', suffix='.json', encoding='utf-8')
47
+ self.file.write(content)
48
+
49
+ return self.file.name
50
+
51
+ def get_file(self):
52
+ if self.local:
53
+ local_path = Path(self.exercise) / settings.get_config("answers_file_name")
54
+ if not local_path.is_file():
55
+ raise FileNotFoundError(f"O arquivo de respostas não foi encontrado em '{local_path}'")
56
+ self.file = open(local_path, 'r', encoding='utf-8')
57
+ return str(local_path)
58
+
59
+
60
+ def get_content(self):
61
+ if not self.file:
62
+ raise ValueError("o arquivo de correção não existe")
63
+
64
+ self.file.seek(0)
65
+ return self.file.read()
66
+
67
+ def get_decoded_json(self):
68
+ if not self.file:
69
+ raise ValueError("o arquivo de correção não existe")
70
+
71
+ return json.loads(self.get_content())
72
+
73
+ def cleanup(self):
74
+ if self.file != None:
75
+ self.file.close()
@@ -0,0 +1,146 @@
1
+ import importlib.util
2
+ import logging
3
+ import traceback
4
+ from contextlib import redirect_stderr, redirect_stdout
5
+ from io import StringIO
6
+
7
+ def _call_with_captured_output(callable_obj, *args, **kwargs):
8
+ stdout_buffer = StringIO()
9
+ stderr_buffer = StringIO()
10
+ with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer):
11
+ result = callable_obj(*args, **kwargs)
12
+
13
+ return result, stdout_buffer.getvalue().strip(), stderr_buffer.getvalue().strip()
14
+
15
+ class Imported:
16
+ def __init__(self, module_path: str):
17
+ self.logger = logging.getLogger(__name__)
18
+ self.module = self.__import_module(module_path)
19
+
20
+ def __import_module(self, module_path: str):
21
+ """Importa um módulo Python a partir de um caminho de arquivo
22
+
23
+ :param module_path: Caminho do arquivo do módulo
24
+ :returns: O módulo importado
25
+ :rtype: module
26
+ :raises ImportError: Se não for possível importar o módulo
27
+ """
28
+ try:
29
+ spec = importlib.util.spec_from_file_location("module_custom", module_path)
30
+ if spec is None or spec.loader is None:
31
+ raise ImportError(f"Não foi possível criar o carregador do módulo '{module_path}'")
32
+
33
+ module = importlib.util.module_from_spec(spec)
34
+ with redirect_stdout(StringIO()), redirect_stderr(StringIO()):
35
+ spec.loader.exec_module(module)
36
+ return module
37
+ except Exception as e:
38
+ self.logger.warning(f"Falha ao importar o módulo '{module_path}': {e}")
39
+ raise ImportError(f"Falha ao importar o módulo '{module_path}'") from e
40
+
41
+ class ClassTester:
42
+ def __init__(self, import_info: Imported, class_name: str, methods: list, initialized: bool):
43
+ self.import_info = import_info
44
+ self.class_name = class_name
45
+ self.methods = methods
46
+ self.initialized = initialized
47
+ self.exists = self.check_existance()
48
+ self.instance = None
49
+
50
+ def check_existance(self):
51
+ """Testa se a classe existe e se possui os métodos esperados"""
52
+ cls = getattr(self.import_info.module, self.class_name, None)
53
+ if cls is None:
54
+ self.import_info.logger.warning(f"A classe '{self.class_name}' não foi encontrada no módulo.")
55
+ return False
56
+
57
+ for method in self.methods:
58
+ if not hasattr(cls, method["name"]):
59
+ self.import_info.logger.warning(f"O método '{method['name']}' não foi encontrado na classe '{self.class_name}'.")
60
+ return False
61
+
62
+ return True
63
+
64
+ def initialize_instance(self, *args) -> bool:
65
+ """Inicializa uma instância da classe com os argumentos fornecidos"""
66
+ if not self.exists:
67
+ self.import_info.logger.warning(f"A classe '{self.class_name}' não existe ou não possui os métodos esperados.")
68
+ return False
69
+
70
+ cls = getattr(self.import_info.module, self.class_name, None)
71
+ try:
72
+ self.instance = cls(*args)
73
+ except Exception as e:
74
+ self.import_info.logger.warning(f"Falha ao inicializar a instância da classe '{self.class_name}': {e}")
75
+ return False
76
+
77
+ return True
78
+
79
+ def get_existance(self):
80
+ """Retorna se a classe existe e possui os métodos esperados"""
81
+ return self.exists
82
+
83
+ def test_method(self, method_name: str, static: bool, input_args: list, expected_output):
84
+ """Testa se um método da classe retorna o valor esperado"""
85
+ if not self.exists:
86
+ self.import_info.logger.warning(f"A classe '{self.class_name}' não existe ou não possui os métodos esperados.")
87
+ return False
88
+
89
+ try:
90
+ cls = getattr(self.import_info.module, self.class_name, None)
91
+ method = getattr(cls, method_name, None)
92
+
93
+ if not static and self.instance is not None:
94
+ result, captured_stdout, _ = _call_with_captured_output(method, self.instance, *input_args)
95
+ else:
96
+ result, captured_stdout, _ = _call_with_captured_output(method, *input_args)
97
+
98
+ observed_output = result if result is not None else captured_stdout
99
+ if observed_output != expected_output and str(observed_output) != str(expected_output):
100
+ self.import_info.logger.warning(f"O método '{method_name}' retornou '{observed_output}', mas era esperado '{expected_output}'.")
101
+ return False
102
+ except Exception as e:
103
+ self.import_info.logger.warning(f"Falha ao testar o método '{method_name}': {e}")
104
+ self.import_info.logger.warning(f"Traceback: {traceback.format_exc()}")
105
+ return False
106
+
107
+ return True
108
+
109
+ class FunctionTester:
110
+ def __init__(self, import_info: Imported, function_name: str):
111
+ self.import_info = import_info
112
+ self.function_name = function_name
113
+ self.exists = self.check_existance()
114
+
115
+ def check_existance(self):
116
+ """Testa se a função existe"""
117
+ func = getattr(self.import_info.module, self.function_name, None)
118
+ if func is None:
119
+ self.import_info.logger.warning(f"A função '{self.function_name}' não foi encontrada no módulo.")
120
+ return False
121
+
122
+ return True
123
+
124
+ def get_existance(self):
125
+ """Retorna se a função existe"""
126
+ return self.exists
127
+
128
+ def test(self, input_args: list, expected_output):
129
+ """Testa se a função retorna o valor esperado"""
130
+ if not self.exists:
131
+ self.import_info.logger.info(f"A função '{self.function_name}' não existe.")
132
+ return False
133
+
134
+ try:
135
+ func = getattr(self.import_info.module, self.function_name, None)
136
+ result, captured_stdout, _ = _call_with_captured_output(func, *input_args)
137
+ observed_output = result if result is not None else captured_stdout
138
+ if observed_output != expected_output and str(observed_output) != str(expected_output):
139
+ self.import_info.logger.warning(f"A função '{self.function_name}' retornou '{observed_output}', mas era esperado '{expected_output}'.")
140
+ return False
141
+ except Exception as e:
142
+ self.import_info.logger.warning(f"Falha ao testar a função '{self.function_name}': {e}")
143
+ self.import_info.logger.warning(f"Traceback: {traceback.format_exc()}")
144
+ return False
145
+
146
+ return True
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: verifica
3
+ Version: 1.0.0.dev1
4
+ Summary: Uma ferramenta simples para correção de atividades em python via CLI.
5
+ Author-email: Théo Modeneis Ruela <theo.ruela@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/espinafr/Verifica
8
+ Project-URL: Issues, https://github.com/espinafr/Verifica/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9.13
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: colorama>=0.4.6
15
+ Requires-Dist: platformdirs>=2.5.2
16
+ Dynamic: license-file
17
+
18
+ <div align="center">
19
+ <h1 align="center">Verifica</h1>
20
+ <p align="center">Uma ferramenta simples para correção de atividades em python via CLI.</p>
21
+ </div>
22
+
23
+ ## Sobre o projeto
24
+
25
+ **Verifica** é uma ferramenta simples que permite que professores preparem testes automatizados para corrigir suas ativiaddes em python, fornecendo aos alunos uma correção instantânea para seus programas antes deles serem enviados.
26
+
27
+ ## Como compilar
28
+
29
+ Para gerar os arquivos de distribuição do projeto, certifique-se de ter o módulo `build` instalado:
30
+
31
+ ```bash
32
+ pip install build
33
+ ```
34
+
35
+ Em seguida, execute o comando de construção na raiz do projeto:
36
+
37
+ ```bash
38
+ python -m build
39
+ ```
40
+
41
+ Os arquivos gerados (arquivos `.tar.gz` e `.whl`) estarão disponíveis no diretório `dist/`. Use o arquivo `.whl` para distribuição e instalação do pacote via pip.
42
+
43
+ ```bash
44
+ pip install dist/{NOME DO ARQUIVO GERADO}
45
+ ```
46
+
47
+ ## Licença
48
+
49
+ Veja o arquivo LICENSE.
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/verifica/__init__.py
5
+ src/verifica/__main__.py
6
+ src/verifica/check.py
7
+ src/verifica/config.py
8
+ src/verifica/fetcher.py
9
+ src/verifica/imports_tester.py
10
+ src/verifica.egg-info/PKG-INFO
11
+ src/verifica.egg-info/SOURCES.txt
12
+ src/verifica.egg-info/dependency_links.txt
13
+ src/verifica.egg-info/entry_points.txt
14
+ src/verifica.egg-info/requires.txt
15
+ src/verifica.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ verifica = verifica.__main__:main
@@ -0,0 +1,2 @@
1
+ colorama>=0.4.6
2
+ platformdirs>=2.5.2
@@ -0,0 +1 @@
1
+ verifica