Saídas do usuário e erros inesperados
A barreira de erros do main (página 7) transforma os
erros conhecidos, de configuração, de gravação e do gerador, em uma linha Error: ... e
código 1. Sobram dois casos: o usuário que quer sair no meio (Ctrl+C, Ctrl+D) e o erro que
ninguém previu, um defeito do código. Os dois também não podem terminar num traceback.
Esta página mostra os dois except que cuidam deles e por que a ordem dos except importa.
except (KeyboardInterrupt, EOFError):
# Keeps the shell prompt off the "Choice? (1-4): " line.
print()
return EXIT_OK
except Exception as error:
print(f"Error: unexpected: {type(error).__name__}: {error}",
file=sys.stderr)
return EXIT_ERROR
Saída do usuário: Ctrl+C e Ctrl+D
Apertar Ctrl+C no terminal pede ao sistema que interrompa o programa. O Python transforma
esse pedido na exceção KeyboardInterrupt, levantada no ponto onde o programa estiver (quase
sempre parado no input do menu). Ctrl+D, num terminal de Linux ou macOS, avisa que a
entrada acabou; o input levanta EOFError. O mesmo acontece quando o texto de um pipe acaba.
Nenhum dos dois é erro: o usuário quis sair. O projeto escolheu terminar com código 0, sem nada
na stderr, como na opção 4. Muitos programas terminam com o código 130 no Ctrl+C: é a
convenção dos terminais do Linux e do macOS para "interrompido", 128 mais o número do sinal que
o Ctrl+C envia ao programa (um sinal é um aviso que o sistema manda a um programa em
execução, identificado por um número; o do Ctrl+C é o SIGINT, sinal 2). Aqui, sair pelo Ctrl+C
é tratado como sair
pela opção 4.
O print() imprime só uma quebra de linha. O usuário estava parado em Choice? (1-4): , sem
quebra no fim; sem o print(), o prompt do terminal apareceria colado na pergunta do menu. É o
que diz o comentário em inglês acima dele: "mantém o prompt do shell fora da linha
Choice? (1-4): ".
O Experimente no fim da página mostra isso com < /dev/null, que liga a entrada do programa a
um arquivo vazio: o primeiro input já encontra o fim da entrada.
A rede de segurança: Exception
Qualquer outra exceção é um defeito: algo que nenhuma camada previu. Ela também vira uma linha
só, com o tipo e a mensagem, e código 1. Um KeyError com a mensagem 'x' (simulado no
"Experimente" abaixo) sai assim:
Error: unexpected: KeyError: 'x'
type(error).__name__ é o nome da classe da exceção (KeyError), a pista mais útil para quem
vai consertar. O unexpected: ("inesperado") avisa que não é um erro de uso: o arquivo de
configuração não tem culpa.
A ordem dos except importa
O Python testa os except de cima para baixo e usa o primeiro que combina. Um except de
uma classe também pega as subclasses dela. As exceções do Python formam uma árvore: no topo está
BaseException; abaixo dela, Exception, a base dos erros comuns; e, ao lado de Exception,
direto sob BaseException, algumas exceções que não são erros, como KeyboardInterrupt.
EOFErroré subclasse deException. Se oexcept Exceptionviesse antes, o Ctrl+D viravaError: unexpected: EOFErrorcom código 1.AppErroreMazeErrortambém são subclasses deException. Pelo mesmo motivo, ficam acima dele, ou todo erro de configuração sairia comounexpected.KeyboardInterruptnão é subclasse deException: herda direto deBaseException, de propósito, para que umexcept Exceptionqualquer não engula um Ctrl+C. Por isso ele precisa estar listado pelo nome.
A ordem fica: erros conhecidos, saídas do usuário e, por último, o genérico.
O que fica fora do try
A conferência dos argumentos fica antes do try porque não levanta nada: é um if que imprime a
linha de uso e devolve 1.
Os import do topo também ficam fora. Se a pasta amazeing/ não estiver ao lado do script, o
Python falha com ModuleNotFoundError antes de o main existir. Isso só acontece com uma
instalação incompleta, e a parte 15 mostra por
que a pasta amazeing/ precisa ir junto com o script.
O arquivo completo
"""Entry point of A-Maze-ing: wire the layers and report every error."""
import sys
from amazeing.domain.entities.view_state import ViewState
from amazeing.errors import AppError
from amazeing.repositories.config_repository import FileConfigRepository
from amazeing.repositories.maze_repository import FileMazeRepository
from amazeing.services.render_service import RenderService
from amazeing.ui.menu import Menu
from amazeing.use_cases.generate_maze import GenerateMaze
from mazegen import MazeError, MazeGenerator
USAGE = "Usage: python3 a_maze_ing.py <config_file>"
EXIT_OK = 0
EXIT_ERROR = 1
def main(argv: list[str]) -> int:
"""Read the configuration, generate the maze and run the menu.
Args:
argv: Command-line arguments without the script name.
Returns:
0 on Quit, end of input or Ctrl+C; 1 on any error.
"""
if len(argv) != 1:
print(USAGE, file=sys.stderr)
return EXIT_ERROR
try:
config = FileConfigRepository().load(argv[0])
generator = MazeGenerator(config.width, config.height, config.entry,
config.exit, config.perfect, config.seed)
generate = GenerateMaze(generator, FileMazeRepository(), config)
return Menu(generate, RenderService(), ViewState()).run()
except (AppError, MazeError) as error:
print(f"Error: {error}", file=sys.stderr)
return EXIT_ERROR
except (KeyboardInterrupt, EOFError):
# Keeps the shell prompt off the "Choice? (1-4): " line.
print()
return EXIT_OK
except Exception as error:
print(f"Error: unexpected: {type(error).__name__}: {error}",
file=sys.stderr)
return EXIT_ERROR
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
Experimente
Com o a_maze_ing.py completo, os exemplos rodam na raiz do projeto. Cada um grava o
maze.txt, o arquivo de saída do config.txt, que o .gitignore deixa fora do repositório.
Fim da entrada logo no primeiro input: o < /dev/null liga a entrada do programa a um arquivo
vazio. O primeiro comando mostra a última linha da saída com cat -e, que marca com $ o fim
de cada linha; o segundo descarta a saída e mostra o código de saída:
.venv/bin/python a_maze_ing.py config.txt < /dev/null | tail -n 1 | cat -e
.venv/bin/python a_maze_ing.py config.txt < /dev/null > /dev/null; echo "exit=$?"
Choice? (1-4): $
exit=0
A pergunta do menu termina com a quebra de linha do print() (o $), e o código é 0.
Um erro inesperado, simulado: o trecho troca o método load do FileConfigRepository por uma
função que levanta KeyError, como se o código tivesse um defeito, e chama o main. O
echo "exit=$?" mostra o código de saída.
.venv/bin/python -c 'import sys, a_maze_ing
from amazeing.repositories.config_repository import FileConfigRepository
def boom(self, path): raise KeyError("x")
FileConfigRepository.load = boom
sys.exit(a_maze_ing.main(["config.txt"]))'; echo "exit=$?"
Error: unexpected: KeyError: 'x'
exit=1
Linha a linha:
import sys, a_maze_ingimporta o programa como um módulo. Importar não roda omain: oif __name__ == "__main__":do fim do arquivo só é verdadeiro quando o arquivo é executado diretamente (página 6).def boom(self, path): raise KeyError("x")cria uma função com os mesmos parâmetros doloadque só levanta o erro.FileConfigRepository.load = boomtroca o método na classe. Todo objetoFileConfigRepository, inclusive o que omaincria lá dentro, passa a usar oboomno lugar doloadverdadeiro. A troca só vale enquanto esse comando roda; o arquivoconfig_repository.pynão muda.sys.exit(a_maze_ing.main(["config.txt"]))chama omaincomo se a linha de comando fosseconfig.txte encerra com o código que ele devolve.
O import a_maze_ing deixa na raiz uma pasta __pycache__/, com a versão já traduzida do
arquivo que o Python guarda para ir mais rápido; o .gitignore também a deixa de fora.