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.
- verifica-1.0.0.dev1/LICENSE +21 -0
- verifica-1.0.0.dev1/PKG-INFO +49 -0
- verifica-1.0.0.dev1/README.md +32 -0
- verifica-1.0.0.dev1/pyproject.toml +30 -0
- verifica-1.0.0.dev1/setup.cfg +4 -0
- verifica-1.0.0.dev1/src/verifica/__init__.py +0 -0
- verifica-1.0.0.dev1/src/verifica/__main__.py +71 -0
- verifica-1.0.0.dev1/src/verifica/check.py +219 -0
- verifica-1.0.0.dev1/src/verifica/config.py +88 -0
- verifica-1.0.0.dev1/src/verifica/fetcher.py +75 -0
- verifica-1.0.0.dev1/src/verifica/imports_tester.py +146 -0
- verifica-1.0.0.dev1/src/verifica.egg-info/PKG-INFO +49 -0
- verifica-1.0.0.dev1/src/verifica.egg-info/SOURCES.txt +15 -0
- verifica-1.0.0.dev1/src/verifica.egg-info/dependency_links.txt +1 -0
- verifica-1.0.0.dev1/src/verifica.egg-info/entry_points.txt +2 -0
- verifica-1.0.0.dev1/src/verifica.egg-info/requires.txt +2 -0
- verifica-1.0.0.dev1/src/verifica.egg-info/top_level.txt +1 -0
|
@@ -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"
|
|
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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
verifica
|