Skip to content

Commit cdfa2f3

Browse files
committed
fix: make legacy utilities safe by default
1 parent 477403f commit cdfa2f3

6 files changed

Lines changed: 454 additions & 98 deletions

File tree

‎.github/workflows/python-app.yml‎

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,49 @@
1-
# Legacy PythonProjects workflow. SimpleDjIA has its own isolated compatibility matrix.
1+
# Curated checks for the maintained root exercises. Historical experiments and
2+
# standalone subprojects have their own validation paths.
23
name: Python application
34

45
on:
56
push:
6-
branches: [ "master" ]
7-
paths-ignore:
8-
- 'SimpleDjIA/**'
9-
- '.github/workflows/simple-djia.yml'
10-
- '.github/workflows/python-app.yml'
7+
branches: ["master"]
8+
paths:
9+
- "music_composer/**"
10+
- "tests/**"
11+
- "calculador.py"
12+
- "formatapendrivelinux.py"
13+
- "requirements.txt"
14+
- ".github/workflows/python-app.yml"
1115
pull_request:
12-
branches: [ "master" ]
13-
paths-ignore:
14-
- 'SimpleDjIA/**'
15-
- '.github/workflows/simple-djia.yml'
16-
- '.github/workflows/python-app.yml'
16+
branches: ["master"]
17+
paths:
18+
- "music_composer/**"
19+
- "tests/**"
20+
- "calculador.py"
21+
- "formatapendrivelinux.py"
22+
- "requirements.txt"
23+
- ".github/workflows/python-app.yml"
1724

1825
permissions:
1926
contents: read
2027

2128
jobs:
2229
build:
2330
runs-on: ubuntu-latest
31+
timeout-minutes: 10
2432
steps:
25-
- uses: actions/checkout@v4
26-
- name: Set up Python 3.10
27-
uses: actions/setup-python@v3
28-
with:
29-
python-version: "3.10"
30-
- name: Install dependencies
31-
run: |
32-
python -m pip install --upgrade pip
33-
pip install flake8 pytest
34-
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
35-
- name: Lint with flake8
36-
run: |
37-
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
38-
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
39-
- name: Test with pytest
40-
run: pytest
33+
- uses: actions/checkout@v4
34+
- name: Set up Python 3.10
35+
uses: actions/setup-python@v5
36+
with:
37+
python-version: "3.10"
38+
cache: pip
39+
- name: Install dependencies
40+
run: |
41+
python -m pip install --upgrade pip
42+
python -m pip install -r requirements.txt
43+
python -m pip install flake8
44+
- name: Check maintained source
45+
run: >-
46+
flake8 music_composer tests calculador.py formatapendrivelinux.py
47+
--count --select=E9,F63,F7,F82 --show-source --statistics
48+
- name: Run maintained tests
49+
run: python -m pytest -q tests

‎README.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ This initiative was dedicated to harnessing the power of Python programming and
77

88
---
99

10+
## Segurança dos experimentos
11+
12+
Este repositório preserva exercícios produzidos em diferentes momentos de aprendizagem. Dois utilitários que exigem cuidado receberam proteções explícitas:
13+
14+
- `calculador.py` aceita somente números, parênteses e as quatro operações exibidas; não executa texto como código;
15+
- `formatapendrivelinux.py` mostra apenas partições removíveis, graváveis e desmontadas, revalida o dispositivo e exige duas confirmações antes de apagar dados.
16+
17+
Formatar uma partição continua sendo uma ação irreversível. Leia o caminho e o tamanho do dispositivo, mantenha cópias dos arquivos importantes e nunca confirme uma unidade que você não identificou fisicamente.
18+
1019
## Project Highlights
1120

1221
1. **Automation of Musical Structures**:

‎calculador.py‎

Lines changed: 100 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,120 @@
1+
"""Calculadora gráfica com avaliação aritmética segura."""
2+
3+
from __future__ import annotations
4+
5+
import ast
6+
import math
7+
import operator
18
import tkinter as tk
29

10+
11+
_OPERADORES_BINARIOS = {
12+
ast.Add: operator.add,
13+
ast.Sub: operator.sub,
14+
ast.Mult: operator.mul,
15+
ast.Div: operator.truediv,
16+
}
17+
_OPERADORES_UNARIOS = {
18+
ast.UAdd: operator.pos,
19+
ast.USub: operator.neg,
20+
}
21+
22+
23+
def avaliar_expressao(expressao: str) -> int | float:
24+
"""Avalia somente números e os operadores exibidos pela calculadora."""
25+
if not expressao or len(expressao) > 100:
26+
raise ValueError("Expressão vazia ou longa demais")
27+
28+
try:
29+
arvore = ast.parse(expressao, mode="eval")
30+
except SyntaxError as erro:
31+
raise ValueError("Expressão inválida") from erro
32+
33+
def avaliar(no: ast.AST, profundidade: int = 0) -> int | float:
34+
if profundidade > 20:
35+
raise ValueError("Expressão complexa demais")
36+
37+
if isinstance(no, ast.Expression):
38+
return avaliar(no.body, profundidade + 1)
39+
40+
if isinstance(no, ast.Constant):
41+
if isinstance(no.value, bool) or not isinstance(no.value, (int, float)):
42+
raise ValueError("Somente números são permitidos")
43+
return no.value
44+
45+
if isinstance(no, ast.BinOp) and type(no.op) in _OPERADORES_BINARIOS:
46+
esquerda = avaliar(no.left, profundidade + 1)
47+
direita = avaliar(no.right, profundidade + 1)
48+
resultado = _OPERADORES_BINARIOS[type(no.op)](esquerda, direita)
49+
elif isinstance(no, ast.UnaryOp) and type(no.op) in _OPERADORES_UNARIOS:
50+
resultado = _OPERADORES_UNARIOS[type(no.op)](
51+
avaliar(no.operand, profundidade + 1)
52+
)
53+
else:
54+
raise ValueError("Operação não permitida")
55+
56+
if isinstance(resultado, float) and not math.isfinite(resultado):
57+
raise ValueError("Resultado fora do intervalo permitido")
58+
return resultado
59+
60+
return avaliar(arvore)
61+
62+
363
class Calculadora(tk.Tk):
4-
def __init__(self):
64+
def __init__(self) -> None:
565
super().__init__()
666
self.title("Calculadora")
767
self.geometry("300x400")
8-
968
self.resultado_var = tk.StringVar()
10-
self.limpar_var = tk.StringVar()
69+
self.criar_widgets()
1170

12-
self.create_widgets()
71+
def criar_widgets(self) -> None:
72+
entrada = tk.Entry(
73+
self,
74+
textvariable=self.resultado_var,
75+
font=("Arial", 24),
76+
bd=10,
77+
relief=tk.GROOVE,
78+
justify=tk.RIGHT,
79+
)
80+
entrada.grid(row=0, column=0, columnspan=4)
1381

14-
def create_widgets(self):
15-
# Entry para exibir os resultados
16-
entry = tk.Entry(self, textvariable=self.resultado_var, font=('Arial', 24), bd=10, relief=tk.GROOVE, justify=tk.RIGHT)
17-
entry.grid(row=0, column=0, columnspan=4)
18-
19-
# Botões
2082
botoes = [
21-
'7', '8', '9', '/',
22-
'4', '5', '6', '*',
23-
'1', '2', '3', '-',
24-
'0', '.', '=', '+',
25-
'C' # Botão para limpar
83+
"7", "8", "9", "/",
84+
"4", "5", "6", "*",
85+
"1", "2", "3", "-",
86+
"0", ".", "=", "+",
87+
"C",
2688
]
2789

28-
row_val = 1
29-
col_val = 0
30-
31-
for botao in botoes:
32-
tk.Button(self, text=botao, padx=20, pady=20, font=('Arial', 18), bd=8, relief=tk.RIDGE, command=lambda b=botao: self.botao_click(b)).grid(row=row_val, column=col_val)
33-
col_val += 1
34-
if col_val > 3:
35-
col_val = 0
36-
row_val += 1
90+
linha = 1
91+
coluna = 0
92+
for rotulo in botoes:
93+
tk.Button(
94+
self,
95+
text=rotulo,
96+
padx=20,
97+
pady=20,
98+
font=("Arial", 18),
99+
bd=8,
100+
relief=tk.RIDGE,
101+
command=lambda valor=rotulo: self.botao_click(valor),
102+
).grid(row=linha, column=coluna)
103+
coluna += 1
104+
if coluna > 3:
105+
coluna = 0
106+
linha += 1
37107

38-
def botao_click(self, valor):
39-
if valor == '=':
108+
def botao_click(self, valor: str) -> None:
109+
if valor == "=":
40110
try:
41-
resultado = eval(self.resultado_var.get())
42-
self.resultado_var.set(resultado)
43-
except Exception as e:
111+
self.resultado_var.set(avaliar_expressao(self.resultado_var.get()))
112+
except (ArithmeticError, TypeError, ValueError):
44113
self.resultado_var.set("Erro")
45-
elif valor == 'C':
114+
elif valor == "C":
46115
self.resultado_var.set("")
47116
else:
48-
current_text = self.resultado_var.get()
49-
self.resultado_var.set(current_text + valor)
117+
self.resultado_var.set(self.resultado_var.get() + valor)
50118

51119

52120
if __name__ == "__main__":

0 commit comments

Comments
 (0)