Testes Unitários


Testando o código

Vimos no artigo Testando o Código algumas abordagens iniciais para realizar testes no código, tais como usar a declaração assert e as docstrings. Recursos mais avançados epoderosos estão disponíveis, como é o caso do módulo unittest, que veremos agora.

Testes unitários com unittest

Testes unitários permitem que unidades de código possam ser testadas em diversas de suas características. Uma unidade pode ser uma função individual, um método ou procedimento de uma classe ou objeto. Ele é feito durante o desenvolvimento pelo programador.

O módulo unittest, incluído na biblioteca padrão, fornece ferramentas para testes unitários. Com ele podemos projetar um conjunto de testes que verificam se uma função (por exemplo) se comporta como o esperado sob situações variadas. Um bom conjunto de testes considera os possíveis tipos de entrada que uma função pode receber, incluindo testes de cada dessas situações. Uma cobertura completa de testes, em um projeto grande, pode ser muito difícil e, nesses casos pode ser considerado suficiente cobrir os casos críticos de uso do bloco testado. Diversos editores e IDEs, incluindo Jupyter Notebook, PyCharm e VSCode, podem usar unittest integrado.

Para usar unittest vamos escrever uma função a ser testada. Em seguida importamos o módulo unittest e criamos uma classe que herda de unittest.TestCase. Objetos dessa classe chamam e verificam o comportamento dessa função testada ao serem inicializados. Métodos diversos podem ser inseridos para verificar o funcionamento da função sob a inserção de parâmetros diferentes.

Para observar o funcionamento dos testes unitários vamos gravar dois arquivos do python, formata_nomes.py e nomes.py. O primeiro contém a função que queremos testar, o segundo chama essa função.

#formata_nomes.py

def ler_nome_formatado(nomes):
    msg = ""
    if nomes.strip() == "":
        msg = ""
    partes = nomes.split()
    nome = partes[0].title()
    msg = f"Primeiro nome: {nome}"
    if len(partes) > 1:
        sobre = " ".join(partes[1:]).title()
        msg = f"{msg}, Sobrenome: {sobre}"
    return msg


Essa função recebe nomes e sobrenomes separados por espaços e retorna esse nome formatado como Primeiro nome: nome, Sobrenome: sobrenomes . Ela considera todas as palavras após a primeira como sobrenome. Para usar essa função gravamos e executamos o arquivo nomes.py.

from formata_nome import ler_nome_formatado as nf
print("Digite nome e sobrenomes.")
print("Deixe em branco para terminar.")
while True:
    nomes = input("\nDigite o nome completo: ")
    formatado = nf(nomes)
    if formatado=="": break
    print(f"\tNome formatado: {formatado}.")

Podemos iniciar uma sessão no console (terminal) e executar python nomes.py. O output aparece no código abaixo.

$ python nomes.py
  Digite nome e sobrenomes.
  Deixe em branco para terminar.

  Digite o nome completo: PEDRO
	  Nome formatado: Primeiro nome: Pedro.

  Digite o nome completo: pedro de alcantara
	  Nome formatado: Primeiro nome: Pedro, Sobrenome: De Alcantara.

  Digite o nome completo: pedro II
	  Nome formatado: Primeiro nome: Pedro, Sobrenome: Ii.

  Digite o nome completo:

Na última linha foi inserida uma string vazia, o que termina o loop. Aparentemente a função retorna o que se espera. Mesmo assim vamos testar nossa função: em um novo módulo importamos unittest e a função que pretendemos testar. Depois criamos uma classe que herda de unittest.TestCase e acrescentamos diversos métodos para verificar aspectos diferentes da função. Cada um dos métodos test_1, test_2, test_3 verifica um comportamento da função para diferentes tipos de inputs.

# teste_formata_nomes.py
import unittest
from formata_nome import ler_nome_formatado as nf

class TestaFormataNomes(unittest.TestCase):
    """Testes para 'formata_nome.py'."""

    def test_1(self):
        """testando o nome 'palito'."""

        formatado = nf('palito')
        self.assertEqual(formatado, 'Primeiro nome: Palito')

    def test_2(self):
        """testando nomes com maísculas."""

        formatado = nf('MARCO POLO')
        self.assertEqual(formatado, 'Primeiro nome: Marco, Sobrenome: Polo')

    def test_3(self):
        """testando strings vazias."""

        formatado = nf('')
        self.assertEqual(formatado, '')

if __name__ == '__main__':
    unittest.main()

Ao rodar esse teste observamos o output:

$ python teste_formata_nome.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK

Nenhum erro foi encontrado em nenhum dos três testes, como mostrado no console. Essa classe pode ter qualquer nome embora seja boa ideia dar um nome representativo de seu objetivo. Ela contém três métodos para testar a função formata_nome.ler_nome_formatado. Qualquer classe que herda de unittest.TestCase executa automaticamente todos os seus métodos que começam com test_ quando é invocada. O retorno da função testada é comparado com o resultado na linha self.assertEqual(formatado, 'string esperada') (um dos método de unittest.TestCase) e gera a informação sobre se o teste foi bem sucedido ou não, com as devidas mensagens.

O bloco if no final, como já vimos, verifica o valor da variável especial __name__. Se o arquivo estiver sendo rodado como programa principal, como ocorreu no nosso caso, ela assume o valor __name__ = "__main__". Nesse caso unittest.main() é chamado e os testes executados.

Suponha que queremos expandir nossa função ler_nome_formatado para que ela retorne uma mensagem de erro caso algum dígito esteja entre os caracteres dos nomes. Se um nome for digitado como “lu1s quinze” a função deve retornar: “Erro: dígito encontrado!”

Vamos então acrescentar um teste em teste_formata_nomes.py. O código abaixo mostra só o acréscimo ao arquivo.

# teste_formata_nomes.py
...
    def test_4(self):
        """testando dígitos no nome."""

        formatado = nf('lu1z paulo')
        self.assertEqual(formatado, 'Erro: dígito encontrado!')
...

Rodamos o teste novamente: desta vez um nome inserido com um dígito não retorna o resultado correto e uma mensagem de erro informa qual o teste falhou, onde e porque.

$ python teste_formata_nome.py
...F
======================================================================
FAIL: test_4 (__main__.TestaFormataNomes)
testando dígitos no nome.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "teste_formata_nome.py", line 29, in test_4
    self.assertEqual(formatado, 'Erro: dígito encontrado!')
AssertionError: 'Lu1Z Paulo' != 'Erro: dígito encontrado!'
- Lu1Z Paulo
+ Erro: dígito encontrado!
----------------------------------------------------------------------
Ran 4 tests in 0.001s
FAILED (failures=1)

Claro que esse defeito deve ser corrigido em formata_nomes.py. Alteramos o código da seguinte forma:

# formata_nomes.py
def ler_nome_formatado(nomes):
    msg = ""
    if nomes.strip() == "":
        msg = ""
    elif True in [i.isdigit() for i in nomes]:
        msg = "Erro: dígito encontrado!"
    else:
        partes = nomes.split()
        nome = partes[0].title()
        msg = f"Primeiro nome: {nome}"
        if len(partes) > 1:
            sobre = " ".join(partes[1:]).title()
            msg = f"{msg}, Sobrenome: {sobre}"
    return msg

No código temos a condição True in [i.isdigit() for i in nomes] que testa cada caracter da variável de string nomes, retornando uma lista de valores booleanos. O teste resulta verdadeiro se uma ou mais das entradas dessa lista for True, ou seja, se existirem dígitos no nome. Com essa alteração rodarmos o teste mais uma vez e veremos que todas as condições testadas foram satisfeitas.

$ python teste_formata_nome.py
...
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK

Para ignorar um dos testes podemos decorar a função com @unittest.skip.

...
    @unittest.skip('Esse teste já foi executado!')
    def test_4(self):
        formatado = nf('lu1z paulo')
        self.assertEqual(formatado, 'Erro: dígito encontrado!')
...

Uma classe inteira pode ser ignorada.

@unittest.skip("Uma classe a ser ignorada")
class Classe_de_Teste(unittest.TestCase):
    def um_metodo_qualquer(self):
        pass

Os seguintes decoradores estão disponíveis na classe:

@unittest.skip(msg) ignore o teste em qualquer caso,
@unittest.skipIf(bool, msg) ignore o teste se bool==True,
@unittest.skipUnless(bool, msg) ignore o teste, exceto se bool==True,
@unittest.expectedFailure marca o teste como falha esperada. Se o teste falhar mensagem de sucesso é emitida e, se passar um erro é lançado,
exception unittest.SkipTest(msg) Uma exceção é levantada ao ignorar em teste,

msg é a mensagem retornada com a exceção, que deve ser descritiva do problema ocorrido. bool é qualquer expressão que retorne um booleano.

Métodos setUp() e tearDown()


Os métodos setUp() e tearDown() são usados para definir instruções executadas antes e depois dos testes definidos. setUp() é executado antes de cada teste no módulo e tearDown() depois de cada um deles. Eles podem ser usados, por exemplo, definir variáveis, abrir e fechar uma conexão com banco de dados ou ler dados em um arquivo.

Erros levantados dentro setUp() ou tearDown() serão considerados erros comuns e não uma falha do teste. A implementação default não realiza nenhuma ação (como em pass). Por exemplo, suponha que pretendemos testar nossa classe Calculadora onde

# classe Calculadora
class Calculadora:
    def __init__(self):
        pass

    def soma(self, a, b):
        return a + b

    def subtrai(self, a, b):
        return a - b

    def muliplica(self, a, b):
        return a * b

    def divide(self, a, b):
        if b != 0:
            return a / b

Na classe de teste teríamos que inicializar uma calculadora para cada teste. Alternativamente podemos inicializar uma calculadora no método setUp().

class TestCalculadora(unittest.TestCase):

    def setUp(self):
        self.calc = Calculadora()

    def tearDown(self):
        self.calc = None

    def test_soma(self):
        self.assertEqual(self.calc.add(4, 7), 11)

    def test_subtrai(self):
        self.assertEqual(self.calc.sub(10, 5), 5)

    def test_multiplica(self):
        self.assertEqual(self.calc.mul(3, 7), 21)

    def test_divide(self):
        self.assertEqual(self.calc.div(10, 2), 5)

Também podemos usar métodos semelhantes para classes e módulos: isso é feito com setUpClass() e tearDownClass() em classes, e setUpModule() e tearDownModule() em módulos.

Testes ignorados não acionam setUp() nem tearDown(), caso estejam definidos. Da mesma forma classes ignoradas não acionam setUpClass() nem tearDownClass(). Módulos ignorados não acionam setUpModule() nem tearDownModule().

Para ver uma lista de opções de uso do unittest podemos digitar:

python -m unittest -h

Métodos assert em unittest.TestCase

Nos testes usando unittest.TestCase podemos usar um assert puro ou um dos seguintes métodos definidos no módulo unittest.TestCase:

Método levanta erro se a condição não se verifica
assertEqual(m, n) m == n
assertNotEqual(m, n) m != n
assertTrue(a) a é True
assertFalse(a) a é False
assertIn(item, lista) item está na lista
assertNotIn(item, lista) item não está na lista
assertIs(a, b) a is b
assertIsNot(a, b) a is not b
assertIsNone(x) x == None
assertIsNotNone(x) x != None
assertIsInstance(a, b) a é uma instância de b
assertNotIsInstance(a, b) a não é uma instância de b
assertAlmostEqual(a, b[, n]) se a == b, precisão de n decimais (default: n = 7)
assertNotAlmostEqual(a, b[, n]) negação de assertAlmostEqual(a, b[, n])
assertGreater(a, b) a > b
assertGreaterEqual(a, b) a >= b
assertLess(a, b) a > b
assertLessEqual(a, b) a <= b
assertRegex(s, r) regex r.search(s)
assertNotRegex(s, r) regex not r.search(s)
assertCountEqual(a, b) a e b tem os mesmos elementos e em igual número, independente da ordem.
Método
fail() sempre gera erro

Os erros são levantados quando o teste for falso. Em todos os casos um parâmetro opcional pode ser usado para determinar a mensagem de erro mostrado, como em TestCase.assertEqual(m, n [, mensagem]). Devemos nos lembrar, como dito acima, que um teste com assert pode ser desligado com o ajuste da variável __debug__ = False.

Simulações (Mocks)

Geralmente o estado de uma função, classe ou um de seus métodos depende de objetos externos para a coleta de dados ou outra interação qualquer, tais como arquivos em disco a serem lidos ou acesso a bancos de dados, ou uma peça de hardware a ser acionada. Como não é boa prática acessar em fase de desenvolvimento os objetos na produção desenvolveu-se a abordagem de criar “objetos simulados” ou mocks. Um objeto mock substitui e imita o comportamento de um objeto real, no ambiente de teste. Usando mocks fica mais fácil gerar situações que podem ser raras no ambiente real, por exemplo para o teste de blocos except ou testes condicionais if. Ainda ocorrem casos em que os objetos (que podem ser blocos de código) ainda não foram desenvolvidos ou oferecem respostas muito lentas para efeito de teste. Com esses objetos é possível verificar se e como um método foi chamado, e com qual frequência.

O módulo unittest inclui um subpacote chamado unittest.mock com ferramentas úteis para essa simulação. Ele também oferece uma função patch() que substitui os objetos reais no código por instâncias mocks. patch() pode ser usado como um decorador ou gerenciador de contexto, facilitando a escolha de qual escopo será simulado. Ao final do teste patch() retornará no código as referências aos objetos originais.

O objeto mock

Um objeto mock pode ser instanciado e a ele podemos atribuir métodos e propriedades.

from unittest.mock import Mock
mock = Mock()
print(mock)
<Mock id='140292494179968'>

# ao objeto podemos atribuir métodos e propriedades
mock.propriedade
mock.metodo()

Além da classe unittest.mock (que é a base das classes simuladas) o módulo também contém uma subclasse unittest.mock.MagicMock que fornece implementações de vários métodos mágicos como .__len__(), __str__() e .__iter__().
Por exemplo, gravamos o arquivo dia_semana.py, que imprime fim de semana se o dia for sábado ou domingo, e dia da semana para os demais dias.

from datetime import datetime
def is_fds():
    dia_semana = datetime.today().weekday()
    return dia_semana > 4

print('fim de semana' if is_fds() else 'dia da semana')

O módulo datatime retorna weekday() = 0 para segunda feira, weekday() = 5, 6 para sábado e domingo. O resultado desse código depende do dia em que está sendo executado. Para uma execução feita na terça feira temos:

$ python dia_semana.py
# é impresso no console
dia da semana

É claro que seria interessante testar o código para outros dias, sem ter que esperar a data correta, nem alterar o relógio do computador. Para fazer isso fazemos um mock de datetime.

import datetime
from unittest.mock import Mock

# fixamos 2 dias para serem usados no teste
ter = datetime.datetime(year=2022, month=3, day=1)  # terça feira (1)
sab = datetime.datetime(year=2022, month=3, day=5)  # sábado (5)

# Mock datetime para controlar a data
datetime = Mock()

def is_fds():
    dia_semana = datetime.datetime.today().weekday()
    return dia_semana > 4

# força datetime para retornar a data em ter (terça feira)
datetime.datetime.today.return_value = ter
# teste para dia = terça
print('fim de semana' if is_fds() else 'dia da semana')

# força datetime para retornar a data em sab (sábado)
datetime.datetime.today.return_value = sab
# teste para dia = sábado
print('fim de semana' if is_fds() else 'dia da semana')

Agora, ao executar o script temos duas respostas:

$ python dia_semana.py
dia da semana
fim de semana

Nesse exemplo, quando fazemos datetime = Mock() tornamos datetime.datetime.today um método simulado, que pode receber a propriedade datetime.datetime.today.return_value a critério do programador. Com isso o método interno .today() retorna a data especificada.

Bibliografia

Livros:

  • Ceder, Vernon; Mcdonald, Kenneth: The Quick Python Book, 2nd. Ed., Manning, Greenwich, 2010.
  • Hunt, John: Advanced Guide to Python 3 Programming, Springer, Suíça, 2019. Disponível em Academia.edu.

Sites:

todos eles visitados em março de 2020.

Testando o Código


Erros no código

É natural que ao escrever código, principalmente em projetos grandes, sempre sejam introduzidos erros. Existem três tipos básicos de erros:

  1. erros de sintaxe são o tipo mais básico e os mais fáceis de serem encontrados. Eles ocorrem quando uma ou mais linhas de código estão escritas incorretamente de forma que o interpretador do Python não consegue processá-las. Eles são quase sempre fatais, impedindo que o código seja executado. Eles são, em geral, erros de digitação, indentação incorreta ou argumentos incorretos passados para funções e classes. Por ex.: print "esqueci o parênteses" não é uma linha válida no python 3.x (embora esteja correta no python 2.x).
  2. erros de tempo de execução ocorrem quando a sintaxe está correta mas o interpretador não pode executar a ação. Isso pode ser causado pelo uso de uma variável não definida, um loop infinito, uma divisão por zero, etc.
  3. erros lógicos são os mais difíceis de serem detectados. O código roda sem interrupções mas não executa a tarefa proposta. Ele pode envolver uma operação matemática incorreta, uso incorreto de índices (como começar no indíce 1, e não 0), um loop interrompido prematuramente, etc.


IDES com preenchimento de código e realce de sintaxe ajudam bastante a evitar os dois primeiros tipos de erros. Frameworks de testes podem ser úteis na depuração de erros lógicos.

O processo de se buscar erros (bugs) no código pode (e deve) ser usado várias vezes durante sua construção. Mas, uma vez finalizado, pelo menos em etapa, o código deve ser testado para verificação de que está realizando corretamente a tarefa proposta. Esses últimos testes devem ser os mais amplos possíveis pois sempre podem aparecer erros não cobertos pelas tentativas prévias de checagem. Mesmo projetos completos e bem testados podem necessitar de novas versões com a inserção de novas funcionalidades ou aprimoramento das que existem. Inserir código em um projeto que já existe é uma prática sensível à introdução de erros.

Embora possam excluir muitos possíveis erros, os teste não são capazes de detectar todos os erros de um código porque é praticamente impossível avaliar seu comportamento sob todos os caminhos de execução, em projetos não triviais. Por isso é importante projetar testes que verificam aspectos do código tão amplos quanto possível.

Além dos testes de correção lógica do código, diversos outros aspectos devem ser testados:

  • performance de execução,
  • robustez do aplicativo sob alta demanda ou uso prolongado, principalmente em aplicativos web,
  • capacidade de implantação e instalação nas plataformas alvo,
  • adaptabilidade às atualizações de versões (quando existirem),
  • habilidade realização de backups de dados e do próprio aplicativo e seu estado,
  • segurança do sistema sob ação do aplicativo e capacidade de recuperação em caso de falha.

Existem muitos recursos disponíveis aos desenvolvedores para testes de código em python. A mais simples delas consiste em inserir verificações assert em pontos críticos do código.

Declaração ASSERT


Vimos na seção sobre tratamentos de erros como levantar uma exceção com assert. Podemos usar assert para inserir no código um teste que gera uma exceção caso uma expressão não seja verdadeira.

A sintaxe é: assert teste_booleano [, mensagem], que lança um AssertionError com uma mensagem opcional.

Para recordar o comportamento de assert definimos uma função que lança um erro quando seu argumento é maior que 3, emitindo uma mensagem de erro.

def menor_que_4(i):
    assert i < 4, 'O número deve ser menor que 4'
    dic={1:'um', 2:'dois', 3:'três'}
    return dic[i]

for i in range(1,5):
    try:
        print (menor_que_4(i))
    except AssertionError as msg:
        print(msg)
        
# esse código gera o output
  um
  dois
  três
  O número deve ser menor que 4

Essa é uma forma de se assegurar que um valor está em conformidade com o esperado e, caso contrário, descobrir que valor ofendeu a condição imposta. Se temos vários asserts no código saberemos também em que módulo e linha o erro ocorreu.

Variável __debug__: Caso existam muitas declarações assert no código, além de torná-lo mais extenso e menos legível, pode haver impacto no desempenho do aplicativo. Você pode remover ou comentar todas as declarações (o que não é difícil com um bom editor ou IDE) mas, nesse caso, terá que retornar com todas elas se precisar modificar o projeto.

Declarações assert funcionam junto com a variável interna (built-in) do Python __debug__, que é True por default. Internamente a declaração assert i < 4 é equivalente a:

if __debug__:
    if not i < 4:
        raise AssertionError

Se marcarmos __debug__ == False o teste de assert não será executado. Ocorre que não podemos atribuir valores a essa variável no código, como __debug__ == False. Para isso é necessário definir a variável de ambiente PYTHONOPTIMIZE ou executar o Python com a opção – O. Assim podemos ter todos os testes de assert ativos na fase de desenvolvimento e desligados na produção. Também é possível remover as instruções assert e as docstrings ao compilar o código com compileall. (Leia sobre compilação em Módulos e Pacotes.)

Docstrings

Um mecanismo mais poderoso que lançar erros em pontos específicos consiste em usar docstrings contendo testes e seus resultados, e usar o módulo doctest. (Leia mais sobre doscstrings.)

O módulo doctest procura por trechos na docstring com o formato de sessões interativas do Python e executa essas linhas de comando para conferir o output proposto. O doctest pode:

  • verificar se as docstrings estão atualizadas, conferindo se os exemplos interativos funcionam como documentado.
  • realizar testes de regressão para verificar se os exemplos interativos de um arquivo (ou objeto) em teste funcionam conforme o esperado.
  • facilitar a composição de documentos tutoriais sobre um pacote com com exemplos de entrada-saída. Se corretamente estruturados esses documentos podem ser considerados uma “documentação executável”.

Por exemplo, gravamos o arquivo testando.py, incluindo as linhas import doctest e doctest.testmod() no bloco de inicialização.

# testando.py
def fatorial(n):
    """Retorna fatorial de n inteiro, onde n >= 0.

    >>> [fatorial(n) for n in range(6)]
    [1, 1, 2, 6, 24, 120]
    >>> fatorial(30)
    265252859812191058636308480000000
    """

    if n ≤ 1:
        return 1
    else:
        return n * fatorial(n-1)

if __name__ == "__main__":
    import doctest
    doctest.testmod()

Quando esse script é executado diretamente o módulo doctest é importado e a documentação é “executada” em suas linhas de código e conferida com os outputs fornecidos. Se todos os testes forem bem sucedidos nenhuma mensagem será exibida.

# 1º teste
$ python testando.py

Introduzindo erros: Para efeito de teste vamos introduzir um erro proposital, alterando a linha >>> fatorial(30) para >>> fatorial(20) nas linhas do docstring. O output proposto agora está incorreto, e isso será mostrado no output de doctest.

# 2º teste
$ python testando.py
  **********************************************************************
  File "testando.py", line 6, in __main__.fatorial
  Failed example:
      fatorial(20)
  Expected:
      265252859812191058636308480000000
  Got:
      2432902008176640000
  **********************************************************************
  1 items had failures:
     1 of   2 in __main__.fatorial
  ***Test Failed*** 1 failures.

O resultado aponta como errônea a linha 6 do “testando.py”, como seria esperado.
Módulos executados com

if __name__ == "__main__":
    import doctest
    doctest.testmod()

executarão todas as suas docstrings. Alternativamente, podemos executar o arquivo com o sinalizador python testando.py -v, o que resultará em um output mais extenso e pormenorizado.

Outra forma interessante de se usar o doctest é passando um arquivo de texto como parâmetro para análise de suas linhas de código e output. Um arquivo de texto, digamos que sobre_python.txt (que pode ser parte de um livro, digamos) é gravado com testes idênticos aos de um docstring. Em seguida fazemos:

import doctest
doctest.testfile("sobre_python.txt")

O arquivo sobre_python.txt não precisa ser completo nem conter todas as definições de funções ou módulos usados. Nesse caso as funções e módulos testadas (ou necessárias para o teste) devem ser importadas.

    Sobre o módulo "testando"
    =========================
    Uso da função ``fatorial``
    -------------------
    Importe o módulo e função
    >>> from testando import fatorial
    Agora você pode usar:
    >>> fatorial(6)
    120

Existem algumas formas de alterar a forma como doctest lê as docstrings. As mais comuns são +ELLIPSIS (significando que um sinal de reticência representa qualquer substring) e +NORMALIZE_WHITESPACE (que força o tratamento de qualquer sequência de espaços em branco da mesma forma). Isso fazer isso basta inserir um comentário com a forma de # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE, onde o sinal + ativa a instrução. Os testes abaixos são bem sucedidos:

>>> lista = [2, 4, 6, 8, 10]
>>> lista # doctest: +ELLIPSIS
    [2, ..., 10]
>>> lista # doctest: +NORMALIZE_WHITESPACE
    [2,4,   6,8,     10]

Para desativar as instruções usamos , como em # doctest: -ELLIPSIS, -NORMALIZE_WHITESPACE. As instruções podem ser usadas em conjunto.

Ao realizar testes com doctest alguns cuidados devem ser tomados quando se compara objetos que podem ser retornados em diferentes ordens. Por exemplo, dicionários não são objetos ordenados, e um teste supondo uma ordem específica pode falhar.

# Testando valores em um dicionário
>>> capitais = {"Belo Horizonte":"MG", "São Paulo": "SP", "Rio de Janeiro":"RJ"}
>>> capitais
{"Belo Horizonte":"MG", "São Paulo": "SP", "Rio de Janeiro":"RJ"}

O teste acima falhará se o dicionário for retornado em ordem diferente. Uma solução consiste em testar por cada chave ou ordenar o dicionário a ser
testado. Como dicionários não possuem elementos com chaves repetidas, o mais apropriado é ordenar por chaves (key).

# Testando valores em um dicionário com ordenação
>>> capitais = {"Belo Horizonte":"MG", "São Paulo": "SP", "Rio de Janeiro":"RJ"}

>>> ordenado = dict(sorted(capitais.items(), key=lambda i: i[0]))
>>> ordenado
{"Belo Horizonte":"MG", "Rio de Janeiro":"RJ", "São Paulo": "SP"}

Além disso, como linhas vazias são consideradas marcas para terminar o processamento doctest, se linhas em branco fazem parte do output esperado é necessário inserir uma linha com <BLANKLINE>. Para inserir caracteres \ como escape de outro caracter ou para marcar continuação de linha, o string do docteste deve ser raw, ou seja, precedido por r.

Doctestes são úteis e devem ser usados, mas podem ficar grandes e pesados para projetos maiores. Outras formas de testagem estão disponíveis no python.

Testes unitários com unittest

Testes unitários permitem que unidades de código possam ser testadas em diversas de suas características. Uma unidade pode ser uma função individual, um método ou procedimento de uma classe ou objeto. Ele é feito durante o desenvolvimento pelo programador.

O módulo unittest, incluído na biblioteca padrão, fornece ferramentas para testes unitários. Com ele podemos projetar um conjunto de testes que verificam se uma função (por exemplo) se comporta como o esperado sob situações variadas. Um bom conjunto de testes considera os possíveis tipos de entrada que uma função pode receber, incluindo testes de cada dessas situações. Uma cobertura completa de testes, em um projeto grande, pode ser muito difícil e, nesses casos pode ser considerado suficiente cobrir os casos críticos de uso do bloco testado. Diversos editores e IDEs, incluindo Jupyter Notebook, PyCharm e VSCode, podem usar unittest integrado.

Para usar unittest vamos escrever uma função a ser testada. Em seguida importamos o módulo unittest e criamos uma classe que herda de unittest.TestCase. Objetos dessa classe chamam e verificam o comportamento dessa função testada ao serem inicializados. Métodos diversos podem ser inseridos para verificar o funcionamento da função sob a inserção de parâmetros diferentes.

Para observar o funcionamento dos testes unitários vamos gravar dois arquivos do python, formata_nomes.py e nomes.py. O primeiro contém a função que queremos testar, o segundo chama essa função.

#formata_nomes.py

def ler_nome_formatado(nomes):
    msg = ""
    if nomes.strip() == "":
        msg = ""
    partes = nomes.split()
    nome = partes[0].title()
    msg = f"Primeiro nome: {nome}"
    if len(partes) > 1:
        sobre = " ".join(partes[1:]).title()
        msg = f"{msg}, Sobrenome: {sobre}"
    return msg

Essa função recebe nomes e sobrenomes separados por espaços e retorna esse nome formatado como Primeiro nome: nome, Sobrenome: sobrenomes . Ela considera todas as palavras após a primeira como sobrenome. Para usar essa função gravamos e executamos o arquivo nomes.py.

from formata_nome import ler_nome_formatado as nf
print("Digite nome e sobrenomes.")
print("Deixe em branco para terminar.")
while True:
    nomes = input("\nDigite o nome completo: ")
    formatado = nf(nomes)
    if formatado=="": break
    print(f"\tNome formatado: {formatado}.")

Podemos iniciar uma sessão no console (terminal) e executar python nomes.py. O output aparece no código abaixo.

$ python nomes.py
  Digite nome e sobrenomes.
  Deixe em branco para terminar.

  Digite o nome completo: PEDRO
	  Nome formatado: Primeiro nome: Pedro.

  Digite o nome completo: pedro de alcantara
	  Nome formatado: Primeiro nome: Pedro, Sobrenome: De Alcantara.

  Digite o nome completo: pedro II
	  Nome formatado: Primeiro nome: Pedro, Sobrenome: Ii.

  Digite o nome completo:

Na última linha foi inserida uma string vazia, o que termina o loop. Aparentemente a função retorna o que se espera. Mesmo assim vamos testar nossa função: em um novo módulo importamos unittest e a função que pretendemos testar. Depois criamos uma classe que herda de unittest.TestCase e acrescentamos diversos métodos para verificar aspectos diferentes da função. Cada um dos métodos test_1, test_2, test_3 verifica um comportamento da função para diferentes tipos de inputs.

# teste_formata_nomes.py
import unittest
from formata_nome import ler_nome_formatado as nf

class TestaFormataNomes(unittest.TestCase):
    """Testes para 'formata_nome.py'."""

    def test_1(self):
        """testando o nome 'palito'."""

        formatado = nf('palito')
        self.assertEqual(formatado, 'Primeiro nome: Palito')

    def test_2(self):
        """testando nomes com maísculas."""

        formatado = nf('MARCO POLO')
        self.assertEqual(formatado, 'Primeiro nome: Marco, Sobrenome: Polo')

    def test_3(self):
        """testando strings vazias."""

        formatado = nf('')
        self.assertEqual(formatado, '')

if __name__ == '__main__':
    unittest.main()

Podemos rodar esse teste e observar seu output:

$ python teste_formata_nome.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK

Nenhum erro foi encontrado em nenhum dos três testes, como mostrado no console. Essa classe pode ter qualquer nome embora seja boa ideia dar um nome representativo de seu objetivo. Ela contém três métodos para testar a função formata_nome.ler_nome_formatado. Qualquer classe que herda de unittest.TestCase executa automaticamente todos os seus métodos que começam com test_ quando é invocada. O retorno da função testada é comparado com o resultado na linha self.assertEqual(formatado, 'string esperada') (um dos método de unittest.TestCase) e gera a informação sobre se o teste foi bem sucedido ou não, com as devidas mensagens.

O bloco if no final, como já vimos, verifica o valor da variável especial __name__. Se o arquivo estiver sendo rodado como programa principal, como ocorreu no nosso caso, ela assume o valor __name__ = "__main__". Nesse caso unittest.main() é chamado e os testes executados.

Suponha que queremos expandir nossa função ler_nome_formatado para que ela retorne uma mensagem de erro caso algum dígito esteja entre os caracteres dos nomes. Se um nome for digitado como “lu1s quinze” a função deve retornar: “Erro: dígito encontrado!”

Vamos então acrescentar um teste em teste_formata_nomes.py. O código abaixo mostra só o acréscimo ao arquivo.

# teste_formata_nomes.py
...
    def test_4(self):
        """testando dígitos no nome."""

        formatado = nf('lu1z paulo')
        self.assertEqual(formatado, 'Erro: dígito encontrado!')
...

Rodamos o teste novamente: desta vez um nome inserido com um dígito não retorna o resultado correto e uma mensagem de erro informa qual o teste falhou, onde e porque.

$ python teste_formata_nome.py
...F
======================================================================
FAIL: test_4 (__main__.TestaFormataNomes)
testando dígitos no nome.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "teste_formata_nome.py", line 29, in test_4
    self.assertEqual(formatado, 'Erro: dígito encontrado!')
AssertionError: 'Lu1Z Paulo' != 'Erro: dígito encontrado!'
- Lu1Z Paulo
+ Erro: dígito encontrado!
----------------------------------------------------------------------
Ran 4 tests in 0.001s
FAILED (failures=1)

Claro que esse defeito deve ser corrigido em formata_nomes.py. Alteramos o código da seguinte forma:

# formata_nomes.py
def ler_nome_formatado(nomes):
    msg = ""
    if nomes.strip() == "":
        msg = ""
    elif True in [i.isdigit() for i in nomes]:
        msg = "Erro: dígito encontrado!"
    else:
        partes = nomes.split()
        nome = partes[0].title()
        msg = f"Primeiro nome: {nome}"
        if len(partes) > 1:
            sobre = " ".join(partes[1:]).title()
            msg = f"{msg}, Sobrenome: {sobre}"
    return msg

No código temos a condição True in [i.isdigit() for i in nomes] que testa cada caracter da variável de string nomes, retornando uma lista de valores booleanos. O teste resulta verdadeiro se uma ou mais das entradas dessa lista for True, ou seja, se existirem dígitos no nome. Com essa alteração rodarmos o teste mais uma vez e veremos que todas as condições testadas foram satisfeitas.

$ python teste_formata_nome.py
...
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK

Para ignorar um dos testes podemos decorar a função com @unittest.skip.

...
    @unittest.skip('Esse teste já foi executado!')
    def test_4(self):
        formatado = nf('lu1z paulo')
        self.assertEqual(formatado, 'Erro: dígito encontrado!')
...

Uma classe inteira pode ser ignorada.

@unittest.skip("Uma classe a ser ignorada")
class Classe_de_Teste(unittest.TestCase):
    def um_metodo_qualquer(self):
        pass

Os seguintes decoradores estão disponíveis na classe:

@unittest.skip(msg) ignore o teste em qualquer caso,
@unittest.skipIf(bool, msg) ignore o teste se bool==True,
@unittest.skipUnless(bool, msg) ignore o teste, exceto se bool==True,
@unittest.expectedFailure marca o teste como falha esperada. Se o teste falhar mensagem de sucesso é emitida e, se passar um erro é lançado,
exception unittest.SkipTest(msg) Uma exceção é levantada ao ignorar em teste,

msg é a mensagem retornada com a exceção, que deve ser descritiva do problema ocorrido. bool é qualquer expressão que retorne um booleano.

Bibliografia

Livros:

  • Ceder, Vernon; Mcdonald, Kenneth: The Quick Python Book, 2nd. Ed., Manning, Greenwich, 2010.
  • Hunt, John: Advanced Guide to Python 3 Programming, Springer, Suíça, 2019. Disponível em Academia.edu.
  • Romano, F., Phillips, D., Hattem, R.: Python: Journey from Novice to Expert, Packt Publishing, 2016.

Sites:

todos eles visitados em março de 2020.




Testes: unittest

Módulos e Pacotes


Executando o código

Podemos digitar comandos em sessões interativas do python diretamente em uma sessão do terminal, e ler nele os outputs das linhas executadas. Vários editores e IDEs, como Pycharm e VSCode, oferecem acesso a um terminal onde o código pode ser executado diretamente sem abandonar o IDE. O Jupyter Notebook também roda de modo interativo, exibindo em suas células o resultado de blocos de código.

# sessão interativa do python (no terminal)
$ python
  Python 3.8.8 (default, Apr 13 2021, 19:58:26) 
  [GCC 7.3.0] :: Anaconda, Inc. on linux
  Type "help", "copyright", "credits" or "license" for more information.
  >>> lista = ["sogra","da","Casa"]
  >>> for i in lista[::-1]:
  ...     print(i, end = " ")
  Casa da sogra

Para programas mais extensos, e que devem ser executados várias vezes, torna-se mais prático gravar em um arquivo as linhas de código e executá-lo depois, em bloco. Esses arquivos são chamados de scripts e podem ser divididos em múltiplos arquivos, se isso for necessário para facilitar seu desenvolvimento e manutenção.

Módulos

Uma prática comum consiste em gravar arquivos separados com um conjunto de funções que podem ser usadas em outras partes do mesmo projeto ou em projetos diferentes, o que facilita a reutilização do código. Esses arquivos são os módulos do Python. Ele devem ser gravados com extensão .py e seu nome (sem a extensão) é o nome do módulo, usado na sua importação. Dentro de cada módulo seu nome está disponível na variável global __name__.

Por exemplo, gravamos um arquivo com o nome texto.py com duas funções. Uma delas remove as letras duplicadas de uma string (banana ↦ ban), e outra retorna strings com suas letras invertidas entre maiúscula e minúsculas (AzUlEjO ↦ aZuLeJo).

# arquivo texto.py
def remove_duplicadas(palavra):
    nova, foi = "", ""
    for i in palavra:
        if not i in foi:
            nova += i
        foi += i    
    return nova

def inverte_caso(palavra):
    t = [x.upper() if x.islower() else x.lower() for x in palavra]
    return "".join(t)

Para usar essas funções criamos agora um arquivo que importa texto.py, dando a ele o nome de usa_texto.py.

# usa_texto.py
# (1º bloco)    
import texto as tx
print(tx.remove_duplicadas("inconstitucionalissimamente"))
print(tx.inverte_caso("rUA DA bAHIA"))

Em seguida o executamos no prompt do terminal, estando todos na mesma pasta:

$ python usa_texto.py
  incostualme
  Rua da Bahia

Outras formas de importação para usa_texto.py estão listadas abaixo, todas com o mesmo resultado no output:

# (2º bloco)
  from texto import *
  print(remove_duplicadas("inconstitucionalissimamente"))
  print(inverte_caso("rUA DA bAHIA"))

# (3º bloco)
  from texto import remove_duplicadas, inverte_caso
  print(remove_duplicadas("inconstitucionalissimamente"))
  print(inverte_caso("rUA DA bAHIA"))

# (4º bloco)
  import texto
  remove = texto.remove_duplicadas
  inverte = texto.inverte_caso
  print(remove("inconstitucionalissimamente"))
  print(inverte("rUA DA bAHIA"))

No 2º bloco todos os objetos são importados. Essa prática é desaconselhada porque, em módulos grandes, pode aumentar a chance de conflitos de nomes (de existirem objetos com nomes iguais em mais de um módulo). A instrução de import apenas indica o caminho onde estão os objetos que serão usados. Ela não implica carregamento de objetos não usados para a memória. import * promove a importação de todos os nomes de objetos exceto aqueles que começam com _ (underscore ou sublinhado). import module as m cria um aliás (um apelido) para module e from module import objeto as obj faz o mesmo para um objeto dentro do módulo. No 4º bloco as funções receberam nomes, o mesmo que from texto import remove_duplicadas as remove, etc.

Além de funções, um módulo pode conter outras instruções destinadas a inicializar o módulo. Elas são executados na primeira vez que o nome do módulo é encontrado em uma importação ou quando o arquivo é executado como um script. Cada módulo tem seu próprio namespace (leia sobre o assunto) ou tabela de símbolos, que é usado por todas suas as funções. Essas variáveis ​​globais no módulo não conflituam com outras eventuais que apareçam com o mesmo nome em outras partes do código. Mesmo assim é uma boa prática se referir às variáveis e funções de um módulo com a notação nome_modulo.nome_item, onde item é um objeto qualquer dentro do módulo.

Também podemos importar módulos dentro de outros módulos. Nomes (que são referências aos objetos) dos módulos importados são juntados no mesmo namespace do módulo importador. É uma convenção, mas não obrigatório, listar todas as instruções de importação no início do módulo, o que facilita a leitura do código.

Executando um módulo

Módulos podem ser preparados para executar diretamente no terminal. Para ver isso gravamos o seguinte conteúdo em arquivo com o nome fatorial.py.

# fatorial.py
import sys
def fatorial(n):
    if n <= 1:
        return 1
    else:
        return n * fatorial(n-1)
      
if __name__ == "__main__":
    print(fatorial(int(sys.argv[1])))    

Se esse arquivo for executado diretamente no terminal, a variável __name__ assume o valor “__main__” e a condição final é satisfeita. sys.argv[1] traz para o código um parâmetro digitado no terminal, após o nome do módulo.

$ python fatorial.py 8
40320
$ python fatorial.py 10
3628800

!# shebang


Podemos executar diretamente um arquivo de script se ele contiver uma referência ao interpretador. Para isso acrescentamos a 1ª linha com #!/usr/bin/env python3. O sinal #! é denominado shebang e é destinado a indicar o caminho para o interpretador, no caso python 3 na pasta #!/usr/bin/env. Gravamos o arquivo letras.py, com a função mai_min_uscula() com o efeito de transformar uma string em outra com letras alternadamente maiúsculas e minúsculas (azulejo ↦ aZuLeJo).

#!/usr/bin/env python3

def mai_min_uscula(palavra):
    coleta = ""
    for i in range(len(palavra)):
        j = palavra[i]
        coleta += j.upper() if i%2==0 else j.lower()
    return coleta   

if __name__ == "__main__":
    import sys
    print(mai_min_uscula(sys.argv[1]))

Antes de tentar rodá-lo diretamente no terminal temos que tornar o arquivo executável. No linux:

# para tornar o arquivo executável    
$ chmod +x letras.py

# para executá-lo
$ ./letras.py "mais vale um pássaro na mão"
MaIs vAlE Um pÁsSaRo nA MãO

Além de permitir executar diretamente o arquivo, o shebang também é útil quando queremos que outras versões do python instaladas no computador sejam usadas.

# o comando
$ ./letras.py "texto"
# será executado como
usr/bin/env python3 letras.py "texto"


Naturalmente se o script for executado em outra máquina pode ser necessário ajustar o endereço em #!endereço para corresponder a uma instalação presente do python. Apenas para recordar, se o módulo for importado, e não executado diretamente, a variável __name__ tem outro valor e o bloco final é ignorado.

Localização de módulos

Quando importamos o módulo caminho/modulo_nome.py o interpretador busca por esse nome

  • primeiro no módulo interno, (built-in);
  • se não encontrar ele busca nos diretórios listados na variável sys.path que contém caminho (ou pasta local, ativa no momento, se caminho não for especificado);
  • nos diretórios estabelecidos em PYTHONPATH, uma variável de ambiente;
  • ou na pasta default estabelecida pela instalação de seu python.

É possível aterar dinamicamente, dentro do código, o conteúdo de sys.path de forma a procurar por módulos nos diretórios estabelecidos.

Python Bytecode

Python é uma linguagem interpretada e o interpretador é instalado junto com o próprio python. A sintaxe do código é a mesma para qualquer plataforma mas o interpretador é diferente para cada uma delas. Dessa forma pode-se escrever um único código que roda em qualquer plataforma. Também existem compiladores para transformar o arquivo (ou projeto) em um executável.

Na execução o interpretador gera um arquivo de bytecode (instruções de máquina de baixo nível) que, em seguida pode ser executada pela máquina virtual do python (python virtual machine). Esse bytecode fica em __pycache__, um subdiretório do diretório ativo na compilação. Apenas módulos carregados por imports são armazenados em __pycache__. Para obter todo o código como arquivo.pyc ele deve ser compilado com instruções explícitas para isso (como mostrado abaixo).

Na complição os seguintes arquivos são armazenados, com as extensões:

  • arquivo.py: O código fonte sendo executado.
  • arquivo.pyc: arquivo bytecode compilado. Todos os módulos importados são convertidos em *.pyc pelo interpretador. Esse arquivo pode ser carregado mais rapidamente que o módulo original.
  • arquivo.pyo: Um arquivo *.pyc é criado quando o parâmetro de otimização (-O) é usado.
  • arquivo.pyd: Arquivo dll do Windows.

Quando um arquivo é executado em um terminal as extensões .pyc tem prioridade para serem carregadas, a menos que o código fonte tenha sido alterado mais recentemente. Na compilação as chaves (opções) -O ou -OO podem ser usadas para diminuir o tamanho do módulo complilado:

  • A opção -O remove as instruções assert,
  • a opção -OO remove as instruções assert e as docstrings.

Ao fazer isso deve-se ter certeza de que tais instruções não serão mais necessárias. Módulos compilados são carregados em menor tempo mas não executam mais rápido. O módulo compileall pode ser usado para criar arquivos .pyc para todos os módulos de um diretório.

Módulos na biblioteca padrão e módulos instalados


Na instalação padrão do Python diversos módulos são instalados por default e podem ser importados diretamente em qualquer outro módulo. Alguns deles estão descritos no artigo Biblioteca Padrão, neste site. Alguns módulos são específicos da plataforma, como o winreg para o Windows, que permite diversas operações com o registro desse sistema.

Um exemplo é o módulo sys com diversas propriedades e métodos úteis.

import sys
sys.version
'3.8.8 (default, Apr 13 2021, 19:58:26) \n[GCC 7.3.0]'
sys.path
['/home/guilherme/.anaconda3/lib/python38.zip',
 '/home/guilherme/.anaconda3/lib/python3.8',
 '/home/guilherme/.anaconda3/lib/python3.8/lib-dynload',
 '',
 '/home/guilherme/.anaconda3/lib/python3.8/site-packages']
# acrescenta um diretório no path
sys.path.append('/home/guilherme/Projetos/Python/unittest')

A variável sys.path contém uma lista de strings, inicializada com a variável de ambiente PYTHONPATH. Ela pode ser modificada com
sys.path.append('novo/caminho')

Pacotes (Packages)

Um pacote (ou package) é uma coleção de módulos, dispostos de forma organizada para tornar mais simples a construção do código, sua utilização e reutilização. Essa estrutura facilita a localização e execução cada um de seus módulos. Pacotes são compreendidos como:

  • aqueles instalados de fontes externas usando ferramentas como pip ou pipenv. Frequentemente buscamos pacotes no Python Package Index, (Pypi);
  • aqueles construídos pelo desenvolvedor com seu próprio código. Pacotes são usados como formas de estruturar o projeto.

Os pacotes permitem que os namespaces (as tabelas de referência entre nomes e objetos) de módulos do python sejam estruturados usando a notação de ponto. Por exemplo modulo1.modulo2 é uma referência para o submódulo modulo2 contido em modulo1. Isso ajuda a impedir conflito de nomes em projetos grandes, principalmente quando módulos diferentes foram escritos por desenvolvedores diferentes.

Um diretório com um pacote deve necessariamente conter um arquivo __init__.py. O nome do pacote é mesmo do diretório base. É boa prática dar nomes que nos ajudem a lembrar de sua funcionalidade.

Arquivo __init__.py


O arquivo especial __init__.py é sempre executado quando o pacote é importado. Ele pode ser vazio ou pode conter código de inicialização do aplicativo ou dos módulos em seu diretório. Uma prática comum é a de importar os módulos necessários nessa inicialização. Quando um subpacote é importado, por exemplo com import meu_app.gerar_excel (em referência à figura), os arquivos de inicialização são executados na seguinte ordem:

  1. meu_app.__init__.py
  2. meu_app.gerar_excel.__init__.py

Por exemplo, vamos exibir a estrutura de um pacote de nome meu_app, cuja finalidade é ler e escrever dados numéricos em arquivos .txt ou .csv, e construir planilhas Excel com esses dados. Na figura mostramos a estrutura de diretórios, com nomes após sinais de #. Usuários do pacote podem importar os módulos:

# para ler os arquivos csv    
import meu_app.ler.ler_csv

# para gerar planilhas com dados importados
import meu_app.gerar_excel.gerar_xl
# ou
from  meu_app.gerar_excel import gerar_xl

# para importar todos os módulos de meu_app.escrever from meu_app.escrever import *

Se o objeto buscado não for encontrado um ImportError é lançado. No último exemplo, podemos limitar os módulos importados com * inserindo a variável especial __all__ em __inti__.py. Se meu_app.escrever.__init__.py contiver a linha: __all__=["escrever_csv"] apenas esse módulo será importado. Essa variável recebe uma lista dos módulos a serem importados: __all__=["modulo1", "modulo2", ...].

Quando os pacotes estão estruturados como o meu_app na figura, contendo subpacotes, podemos importar módulos de pacotes diferentes usando caminhos completos. Por exemplo, se para gerar uma planilha precisamos de ler_csv usamos from meu_app.ler import ler_csv dentro de gerar_xl.

Podemos ainda usar um atributo especial, __path__ contendo uma lista com todos os caminhos onde existem pacotes. Desta forma subpacotes que são partes de um pacote central podem estar distribuídos em diretórios quaisquer.

Função dir()

A função built-in dir() retorna uma lista de nomes definidos em um namespace. Sem argumentos ela retorna a lista, em ordem alfabética, de todos os nomes definidos na tabela de símbolos local. Abaixo dir() é usado logo após a inicialização do python, após a importação do módulo math e depois da criação de uma nova classe.

$ python
Python 3.8.8 (default, Apr 13 2021, 19:58:26) 
[GCC 7.3.0] :: Anaconda, Inc. on linux
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__']

# importando math
>>> import math
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'math']

# se definirmos uma classe ou definirmos uma variável
>>> class Nova():
...     pass
>>> x =1010
>>> dir()
['Nova', '__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'math', 'x']

Dessa forma a função é útil para mostrar o que foi inserido na tabela após uma importação ou qualquer outra ação do usuário. Se um um módulo for passado como argumento dir(modulo) lista os nomes dentro desse módulo.

>>> dir(math)
['__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'comb', 'copysign', 'cos', 'cosh', 'degrees', 'dist', 'e', 'erf', 'erfc', 'exp', 'expm1', 'fabs', 'factorial', 'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'gcd', 'hypot', 'inf', 'isclose', 'isfinite', 'isinf', 'isnan', 'isqrt', 'ldexp', 'lgamma', 'log', 'log10', 'log1p', 'log2', 'modf', 'nan', 'perm', 'pi', 'pow', 'prod', 'radians', 'remainder', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'tau', 'trunc']

Classes, Módulos e Pacotes

No python um módulo pode conter diversas classes (diferente do Java ou C#), funções e definições de constantes. Módulos muito grandes, com muitas classes, podem ser divididos, principalmente se existirem objetivos diferentes em suas classes. Módulos não precisam ser parte de um pacote. Um pacote pode conter diversos módulos, com funcionalidade e objetivo semelhante.

Além de tornar o código mais estruturado e fácil de ser lido (protanto mais fácil de manter) a estrutura de classes, módulos e pacotes favorece a reutilização de código. Essa prática está em acordo com o princípio DRY (“Don’t Repeat Yourself” ou “Não se repita”). A abordagem DRY para programação se refere à agrupar partes repetidas do código em funções, classes, módulos ou pacotes (dependendo da complexidade) que podem ser chamadas em partes diversas do projeto.

Compilando o código

Usamos aqui o termo “compilar” significando “gerar um bytecode”.

Quando executamos um aplicativo ou módulo do python apenas os módulos importados são armazenados como bytecodes. Para forçar a compilação e armazenamento podemos usar py_compile e compileall.

py_compile(arquivo.py) Compila arquivo.py para bytecode
compileall.compile_dir(dir) Compila todos os arquivos no diretório para bytecode
compileall.compile_file(arquivo.py) Compila arquivo.py para bytecode
compileall.compile_path() Compila arquivos em sys.path para bytecode

Esses módulos são rodados no terminal do sistema operacional:

# para compilar um arquivo
$ python -m py_compile arquivo_compilar.py

# para compilar arquivos em um diretório
$ python -m compileall.compile_dir caminho_da_pasta

Com esses processos (ou durante a execução usual do python) os arquivos *.pyc são colocados na pasta “__pycache__” com o nome arquivo_compilar.cpython-39.pyc, onde cpython-xx indica que foram compliados sob cpython-xx, sendo xx a versão do cpython. Esses arquivos podem ser executados com:

$ cd __pycache__    
$ python arquivo_gerado_bytecode.cpyton-39.pyc

Você pode entregar apenas os arquivos *.pyc para um usuário final que deve ter o interpretador do python instalado em seu computador para executá-los.

Embora python seja uma linguagem interpretada existem compiladores desenvolvidos para transformar um aplicativo em um executável. Entre eles:

  • py2exe transforma programas do Python em pacotes que podem rodar no Windows sem a necessidade de ter uma instalação do Python.
  • Nuitka the Python Compiler, um compilador compatível com python2 e python3.
  • Pyinstaller, empacota todo um aplicativo python, com suas dependências, em um pacote único. Funciona com python3.6 ou superior.

PyInstaller é um empacotador que permite ao usuário final executar o aplicativo sem instalar um interpretador python ou qualquer módulo. Ele funciona corretamente com os principais pacotes, como numpy, PyQt, Django e wxPython. Ele deve ser executado na versão específica de plataforma a que é destinado o pacote final. O PyInstaller tem sido usado com sucesso no Windows, Mac OS X, GNU/Linux, Solaris, FreeBSD e OpenBSD.

Python Eggs e Wheels


Python Eggs: Dois formatos de distribuição de aplicativos do python são os arquivos *.egg e *.whl. Eggs são uma tecnologia mais antiga, recentemente substituída pelo formato wheels. Um arquivo egg é basicamente um arquivo zipado com instruções para a instalação de um pacote. Apesar de ter sido substituído ainda existem arquivos eggs para serem baixados e instalados. Ele faz uso do pacote SetupTools, que é a forma padrão original de lidar com pacotes do PyPI (e outras fontes) usando comando de linha. Você pode criar seu próprio egg para distribuí-lo, ou instalar pacotes baixados de terceiros. (Documentação sobre eggs).

Python wheels: O formato Wheel foi criado para substituir os python eggs. Ele trabalha junto com o pip e foi projetado para conter todos os arquivos para uma instalação de maneira próxima do formato em disco após a instalação. Semelhante a um *.egg, arquivo *.whl também é um arquivo zip com extensão renomeada. Se essa extensão for renomeada (*.whl ↦ *.zip), o arquivo pode ser aberto e analisado com qualquer aplicativo zip.

O nome de uma instalação wheel contém informações sobre quais as versões do python e da plataforma são suportadas, o que é usado pelos instaladores para fazer uma escolha correta da versão do pacote a ser instalado. Existe suporte para wheels em pip >= 1.4 e setuptools >= 0.8.

Um único arquivo .whl contém todos os módulos de seu projeto, inclusive aqueles importados por ele. Uma vez criado ele pode ser carregado para um ambiente na nuvem, como o Pypi, e dali instalado por qualquer usuário que queira usá-lo. Se o arquivo se chama arquivo.whl ele pode ser instalado com o comando pip install arquivo.whl.

Para usar wheels precisamos instalá-lo. Certifique-se de que você tem uma versão recente do pip fazendo o upgrade, e instale o wheel e setuptolls, lembrando que o ideal é fazer isso em um ambiente virtual (Leia sobe Ambientes Virtuais):

# upgrade do pip    
$ pip install --upgrade pip
# instalação do wheel e setuptools
$ pip install wheel
$ pip install setuptools

Para a criação de um arquivo .whl devemos proceder da seguinte forma:

  • Copie todos os módulos e pacotes de seu projeto em um diretório raiz. Vamos chamá-lo aqui de raiz. Lembramos, como visto acima, que todos os diretórios e subdiretórios devem possuir um arquivo __init__.py. (Não obrigatório no diretório raiz.)
  • Crie um arquivo com o nome setup.py no diretório raiz. Esse arquivo deve conter um mínimo de informações como: nome da distribuição, número da versão e lista de nomes de pacotes.

Um exemplo mínimo desse arquivo seria o seguinte:

from setuptools import setup, find_packages

setup(
    # nome da distribuição
    name = 'nome_da_distribuição', 
    # versão
    version='1.0', 
  
    # definição de nomes dos pacotes e módulos (uma das 3 abordagens)
 
    # 1. se existem poucos módulos no diretório raiz
    packages = ['']
  
    # 2. liste todos os nomes dos pacotes
    packages = ['pacote1', 'pacote2', ...]
  
    # 3. encontre automaticamente todos os pacotes
    packages = find_packages()
)
  • O nome da distribuição será exibido com python -m pip list ou conda list.
  • A versão pode ser incrementada, conforme o desenvolvedor.
  • A 1ª abordagem pode ser usada quando existem poucos módulos na raiz e nenhum subdiretório com pacotes. Os módulos podem ser importados diretamente sem o uso do nome da distribuição (como import modulo_1 para importar o arquivo import modulo_1.py.
  • Na 2ª abordagem todos os pacotes estão na lista e a importação é feita como from pacote1 import modulo1 ou import pacate1.modulo1 as m1.
  • Na 3ª abordagem a lista packages é preenchida automaticamente pelo método find_packages().

Depois, no terminal, navegue até o diretório raiz e execute setup

$ cd raiz
$ python setup.py bdist_wheel

Esse procedimento cria .whl (o arquivo wheel) em um subdiretório chamado dist de raiz. (Ou seja, cria raiz/dist/nome_completo_do_wheel.wheel). Para testar seu funcionamento você pode instalá-lo com

$ pip install nome_completo_do_wheel.whl

Wheels são uma boa maneira de criar um repositório local, com as dependências para seu projeto, que pode ser instalado rapidamente. A criação de vários repositórios wheel facilitam a alternância entre versões para fins de teste. Quando usado em conjunto com os ambientes virtuais essa tecnologia permite verificar como as versões atualizadas das dependências de seu projeto podem afetar seu desepenho sem precisar baixá-las várias vezes.

Bibliografia

todos acessados em março de 2020.

Git e GitHub


Introdução: Git e GitHub

No desenvolvimento de software duas situações ocorrem com frequência:

  • na medida em que se escreve código é comum que uma alteração crie problemas que tornam importante voltar para um estágio anterior, onde o problema não existe;
  • vários programadores podem trabalhar em um mesmo projeto e nem sempre é fácil juntar as alterações feitas.

Além disso é comum se fazer uma bifurcação do projeto em algum ponto para fazer experimentações ou gerar novo projeto. O controle de versões facilita o fluxo de trabalho de quem necessita acrescentar características, corrigir erros ou voltar para etapas anteriores de um projeto.


O Git é uma ferramenta de controle de versão (um sistema de versionamento) que mantém um histórico do projeto, permitindo o retorno à qualquer ponto e facilitando as junções de códigos desenvolvidos separadamente. Ela é uma ferramenta usada na linha de comando (embora existam aplicativos GUI que podem ser encontrados na página Git – GUI clients). Ele não é o único aplicativo para o controle de versões mas tem se tornado o mais usado deles. Git é considerado a segunda criação mais famosa e usada de Linus Torvalds, o criador do Linux. Git é um sistema de controle de versão distribuído (distributed version control system, DVCS).

Além dos clientes GUI disponíveis para o controle do Git e integração com o GitHub existem diversas IDEs que facilitam esse controle de versão, como o PyCharm e o VSCode. As sessões do Jupyter Notebook também podem ser controladas com o Git.

GitHub é uma plataforma na internet que usa o Git para hospedar código e projetos, e controle de versão. Existem outras alternativas, como GitLab e BitBucket.

Git e GitHub são coisas diferentes! Git é uma ferramenta de controle de versão de código aberto criada em 2005. GitHub é uma empresa fundada em 2008 que criou um site que usa o Git. Você pode usar o Git apenas em sua máquina, mas isso torna mais difícil o compartilhamento de código.

Conceitos e definições:

Alguns conceitos e definições são importantes para o uso do Git e do GitHub. Todos eles serão desenvolvidos no texto do artigo.

Projeto: é qualquer conjunto de código destinado à realização de uma tarefa. Ele pode ser um texto em processo de construção, um aplicativo com seu código ou um conjunto de páginas da web.
Diretório de trabalho é aquele em que seu projeto é desenvolvido. Seus arquivos não são automaticamente rastreados pelo Git, que precisa ser informado de quais arquivos deve acompanhar.
Área de preparação
(staging area)
é o conjunto de arquivos marcados para acompanhamento pelo Git. Nenhum arquivo no diretório de trabalho é automaticamente marcado para acompanhamento pelo Git. Arquivos são colocados na staging area com o commando git add <arquivo> e no repositório com git commit.
Repositório local é uma área na máquina local dedicado ao armazenamento do Git para o estado do projeto nos momentos decididos pelo desenvolvedor. Ele armazena as diversas versões e ramificações feitas por um ou mais desenvolvedores. Repositórios são, às vezes, chamados de “repos”.
Repositório do GitHub é um repositório remoto, hospedado no site do GitHub. Podemos sincronizar nosso repositório local com o do GitHub ou vice-versa, baixando para a máquina local o repositório remoto.
HEAD é o ponteiro que marca o estado do código ativado no Git. Esse estado consiste nos branch e commit ativos.

Nem todas as alterações no projeto precisam ser armazenadas. Por isso apenas são guardadas as situações quando se faz um commit, por decisão do desenvolvedor. Estritamente dizendo, commit não necessita guardar sempre os arquivos inteiros no repositório. Ele tenta ser o mais leve possível, armazenando apenas as modificações feitas desde o último commit. Por isso alternar entre um e outro estado é uma operação bastante rápida.

A estrutura do Git dispensa o uso de um servidor centralizado para armazenar todas as modificações. Ele permite que vários desenvolvedores alterem seus repositóritos locamente em suas máquinas e depois o façam o upload de seus trabalhos. Ele também facilita o trabalho de juntar as diversas modificações feitas.

Instalando o Git

Muitos sistemas operacionais instalam e mantém atualizado o Git. Você pode verificar se tem o Git instalado em seu sistema abrindo um terminal e digitando:

$ git --version
git version 2.35.1

A versão será exibida, ou uma mensagem de erro caso ele não esteja instalado. É possível atualizar a versão do Git usando o próprio Git:

$ git clone https://github.com/git/git

Se necessária a instalação visite o site do Git – downloads. Alguns exemplos de instalação:

 Debian/Ubuntu
# PPA para Ubuntu da última versão estável
$ add-apt-repository ppa:git-core/ppa
$ apt update
$ apt install git

# Debian/Ubuntu
$ apt-get install git

Fedora (até versão 21, usando yum)
$ yum install git
Fedora (versão 22 ou posterior, usando dnf)
$ dnf install git

Para Windows faça download e instale os executáveis apropriados.

Configurando o Git

Vamos também configurar o nome e email associado ao repositório local, usando git config:

$ git config --global user.email "seu_email@example.com"
$ git config --global user.name "Seu nome"
# nenhuma resposta é exibida

Essas informações são usadas pelo Git para marcar quem foi o autor de cada alteração registrada. Além do nome podemos usar outros comandos para alterar o comportamento e aparência do Git no console.

# os seguintes comandos instruem git usar cores para realce de sintaxe no console
$ git config --global color.ui true
$ git config --global color.status auto
$ git config --global color.branch auto

# para definir um editor
$ git config --global core.editor nome_do_editor

# para definir a ferramenta default de merge (junção de versões)
$ git config --global merge.tool vimdiff

# para listar todas as configurações alteradas do git
$ git config --list

As configurações de config --global color tornam mais legíveis as linhas do console. Por default Git usa o editor padrão definido nas configurações do sistema. Esse padrão pode ser alterado aqui, por exemplo fazendo git config --global core.editor vim

Quando se trabalha simultaneamente com o GitHub podemos fazer alterações em qualquer um dos ambientes e depois se sincronizar com o outro. Alterações locais não alteram o repositório do GitHub, até que sejam a ele enviadas.

Usando o Git localmente

Na máquina local usamos um diretório de trabalho para desenvolver nosso projeto. Digamos que queremos iniciar um projeto que contenha código Python para manipulação de texto. Denominaremos esse projeto de PyTexto e criaremos um diretório com esse mesmo nome. Depois, usando o terminal, navegamos até este diretório e inicializamos o Git.

# cria pasta, inicializa git
$ mkdir ~/Projetos/PyTexto
$ cd ~/Projetos/PyTexto
$ git init
  hint: Using 'master' as the name for the initial branch. This default branch name
  hint: is subject to change.  (...)

# para renomear o branch ou ramo podemos usar git branch -m novo_nome

# vamos renomear para "main" 
$ git branch -m main
# para visualizar a situação do git
$ git status
  On branch main
  No commits yet
  nothing to commit (create/copy files and use "git add" to track)
  
# outras informações sobre o repositório local podem ser vistas com
$ git log  

) O Git cria um diretório oculto .git dentro do diretório de trabalho, e inicializa um branch (ramificação) original, chamada master, que renomearemos para main. Por default, até o Git 2.35, a inicialização cria um branch (ou ramo) principal chamado branch master. Tem sido uma prática adotada por desenvolvedores renomear esse ramo para branch main ou branch trunk (ou o nome que você preferir). É possível configurar o Git para que o branch inicial de seus novos repositórios se chame branch main:

# altera o nome default da branch inicial
$ git config --global init.defaultBranch main


Para inserir ou editar arquivos usarei o editor de código Geany, disponível para Linux, macOS e Windows. Qualquer editor ou IDE podem ser usados.

É uma boa prática criar um arquivo (usando o seu IDE preferido ou qualquer editor de texto) README.md (ou README.txt) onde o desenvolvedor coloca instruções de uso e instalação, comentários sobre o projeto, versão, etc. A extenção .md indica que o arquivo contém texto formatado com a marcação markdown. Você pode ler sobre markdown aqui!

Depois criamos outro arquivo no nosso diretório de trabalho. Nesse caso inserimos um arquivo do python com o nome palindromo.py. Os conteúdos podem ser, por exemplo:

# arquivo README.md
# Projeto de teste e aprendizado do Git
Consiste em testes de funcionamento do Git

e o arquivo do python:

# arquivo palindromo.py
# palindromo (testa se a palavra é um palíndromo)
def palindromo(palavra):
    palavra = palavra.upper()
    p = ("" if palavra == palavra[::-1] else "não")
    return (f"{palavra} {p} é um palíndromo")

Em princípio nenhum dos arquivos nesse diretório são rastreados pelo Git até que o informemos que deles devem fazer parte do repositório. Podemos ver isso com git status:

$ git status
  On branch main
  No commits yet
  Untracked files:
  (use "git add ..." to include in what will be committed)
    README.md
    palindromo.py
  nothing added to commit but untracked files present (use "git add" to track)

Vemos que o Git reconhece que dois arquivos foram inseridos na pasta de trabalho, mas nenhum deles foi marcado para acompanhamento (colocado em stage). Isso é feito com o comando:

$ git add nome_do_arquivo
# no nosso caso
$ git add README.md
$ git add palindromo.py

# caso existam vários arquivos, e todos devem ser marcados, usamos
$ git add -A

Com isso os arquivos são marcados como estando na área de preparação (staging area), e são atualizados pelo Git até que um commit seja executado. Vamos verificar novamente o status do repositório:

$ git status
  On branch main
  No commits yet
  Changes to be committed:
     (use "git rm --cached ..." to unstage)
     new file:   README.md
     new file:   palindromo.py

Esse comando informa que o branch main está ativo, que README.md e palindromo.py estão em stage, prontos para o commit. Para enviar arquivos em stage para o repositório fazemos o commit. A lista de todos os commits feitos naquele branch pode ser vista com git log.

$ git commit - m "Commit 1 com arquivos README.md e palindromo.py"

# para ver a lista de todos os commits naquele branch
$ git log
  commit 48c3f1f8fae05f87bd79f58b2d4f0cca42d8f8d5 (HEAD -> main)
  Author: Guilherme Santos Silva <gssilva57.gmail.com>
  Date:   Tue Mar 15 14:39:42 2022 -0300

    Commit 1 com arquivos README.md e palindromo.py
:

Se o output de git log for maior que a área disponível do console ele termina com o sinal :. Isso é uma indicação de que você pode rolar a tela do console para ler todo o texto. Para sair desse modo digite q ou CTRL-z. Uma forma compacta de adicionar arquivos e fazer commit ao mesmo tempo consiste em usar a marca commit -am:

$ git commit -am "descrição do commit"

O comando commit faz com todos os arquivos adicionados à área de preparação sejam enviados para o repositório. A chave -m "mensagem" adiciona uma mensagem junto a esse commit, que pode ser visualizado mais tarde. Ela deve ser um texto curto e explicativo de quais alterações estão sendo gravadas.
git
Esse comando falha se não tiver sido feita a configuração de git config --global user.email e git config --global user.name. Caso contrário a alteração pode ser verificada com git status.

$ git status
  On branch main
  nothing to commit, working tree clean

Dessa forma criamos, alteramos e inserimos todas as alterações no repositório.

A lista de todos os commits feitos naquele branch pode ser vista com git log.

# para ver a lista de todos os commits naquele branch
$ git log
  commit 48c3f1f8fae05f87bd79f58b2d4f0cca42d8f8d5 (HEAD -> main)
  Author: Guilherme Santos Silva <gssilva57.gmail.com>
  Date:   Tue Mar 15 14:39:42 2022 -0300
    Commit 1 com arquivos README.md e palindromo.py

O output desse comando mostra um hash identificador e mostra que o branch ativo é HEAD -> main, o autor e data e o texto atribuído ao commit.

Após ter feito um commit podemos voltar para a sitação anterior usando git reset. Existem três modos de ser fazer um reset.

# desfaz o commit sem modificar nenhum dos arquivos comitados
$ git reset --soft <numero de hash>

# desfaz o commit sem modificar arquivos comitados, mas desfazendo os adds
$ git reset --mixed <numero de hash>

# desfaz o commit e apaga tudo o que foi modificado após o commit prévio
$ git reset --hard <numero de hash>

Fazendo o reset --soft retornamos o projeto para um estado logo anterior ao commit. Desta forma podemos fazer os consertos necessários e voltar a dar commit. reset --hard deve ser usado com cuidado pois significa a perda de todas as alterações feitas após o último commit (principalmente quando se trabalha em grupo). O <numero de hash> é o código exibido em git log (48c3f1f8fae05f87bd79f58b2d4f0cca42d8f8d5 no último caso). Apenas os primeiros 7 dígitos podem ser usados (48c3f1f). Após o reset o estado ativo (indicado por HEAD -> main) será aquele indicado por esse número.

Retomando o projeto: Mais tarde, para retomar o trabalho no projeto local, basta voltar no mesmo diretório de trabalho (que contém a pasta oculta .git. Nesse diretório podemos continuar a edição dos arquivos existentes, inserindo novos arquivos, modificando ou apagando os existentes.

Agora editamos o arquivo README.md e inserimos o arquivo indesejado.py (com qualquer conteúdo) e o adicionamos à área de stage.

# volte para o diretório de trabalho    
$ cd ~/Projetos/PyTexto
# conteúdo do arquivos listado abaixo
$ geany README.md
# insere novo arquivo indesejado.py
$ geany indesejado.py

# para adicionar todos os arquivos de uma vez
$ git add -A

Use o seguinte conteúdo para README.md:

# arquivo README.md
# Projeto de teste e aprendizado do Git
Consiste em testes de funcionamento do Git
Inserimos um arquivo a ser removido

Se, mais tarde, verificamos que um arquivo qualquer <arquivo.ext> não deveria fazer parte do projeto podemos removê-lo de duas formas:

# rm --cached é usado para remover o arquivo de stage sem apagá-lo do diretório de trabalho
git rm --cached <arquivo.ext>

# rm -f para remover o arquivo de stage e forçar seu apagamento do diretório de trabalho
git rm -f <arquivo.ext>

# se o arquivo não está na pasta default
$ git rm --cached diretorio/<arquivo.ext>

# para remover todos os arquivos na pasta
$ git rm --cached diretorio/*

Para efeito de teste vamos remover o arquivo indesejado.py. Depois vamos restaurá-lo, criando outro arquivo com o mesmo nome, adicioná-lo e fazer um commit.

# removemos o arquivo
$ git rm -f indesejado.py

# gravamos novo arquivo, add e commit
$ echo "qualquer coisa" > indesejado.py
$ git add indesejado.py
$ git commit -m "3 commit, inserindo indesejado.py"

# para ver o estado do repositório commitado
$ git ls-tree -r main
  100644 blob 26bb6383445abf68be689e9724de464d1908bbcc   README.md
  100644 blob 6f16acbbc80d444145a09d897a93591cd806d3f8   indesejado.py
  100644 blob 1904a1ec5c3900cccbc80ae3b94e3debd34afb42   palindromo.py

# para remover do repertório (e do sistema de arquivos)   
$ git rm indesejado.py
  rm 'indesejado.py'

echo "qualquer coisa" > indesejado.py é uma linha de comando do bash que permite gravar um arquivo com o conteúdo da string.

Podemos navegar para qualquer commit, que terá o estado do projeto feito naquele momento. Vamos verificar isso. O comando cat do linux lista o conteúdo do arquivo. Use o comando equivalente para seu SO ou abra o arquivo com o editor. Para ver os commits feitos, juntamente com um código que os identificam, usamos git log ou git log --oneline (uma versão com output simplificado).

$ cat README.md
  # Projeto de teste e aprendizado do Git
  Consiste em testes de funcionamento do Git

# para listar todos os commits feitos
$ git log --oneline
48c3f1f (HEAD -> main) 3. insere indesejado.py
0073fc0 3. insere indesejado.py
6ce467f Commit 1 com arquivos README.md e palindromo.py

# podemos alternar entre diferentes commits. Vamos voltar para o 1º
$ git checkout 6ce467f
  Note: switching to '6ce467f'.
  HEAD is now at 6ce467f Commit 1 com arquivos README.md e palindromo.py

# nesse estado o arquivo README.md tem o seguinte conteúdo
$ cat README.md
  # Projeto de teste e aprendizado do Git
  Consiste em testes de funcionamento do Git  

Essa operação mostra como é importante inserir texto explicativos de cada situação quando fazemos o commit. O procedimento acima mostra que, voltando para um commit anterior o estado do projeto, incluindo todos os arquivos nele existentes, volta para o que foi gravado naquele commit.

Visualizando alterações

Uma funcionalidade importante do Git consiste na possibilidade de verificar quais alterações foram feitas em arquivos. Para ver isso vamos alterar o arquivo README.md de seguinte forma:

arquivo original arquivo modificado
# Projeto de teste e aprendizado do Git
Consiste em testes de funcionamento do Git
# Projeto de teste e aprendizado do Git

Linha inserida para testar o diff

Para ver as alterações desde o último estado gravado usamos git diff.

$ git diff
  diff --git a/README.md b/README.md
  index 26bb638..85818ef 100644
  --- a/README.md
  +++ b/README.md
  @@ -1,2 +1,3 @@
  # Projeto de teste e aprendizado do Git
  -Consiste em testes de funcionamento do Git
  +
  +Linha inserida para testar o diff

Os sinais significam: + linha inserida; linha removida.
Esse output indica que o arquivo README.md foi alterado: a 1ª linha foi mantida; a 2ª foi apagada (sinal ) e uma linha em branco inserida; uma 3ª linha foi inserida, (sinal +).
Se todas as linhas fossem removidas e uma nova linha inserida teríamos o output de diff:

$ git diff
-# Projeto de teste e aprendizado do Git
-Consiste em testes de funcionamento do Git
+# Novo linha inserida

Se mais de um arquivo foi modificado, todas as alterações aparecem em git diff. Para ver apenas o nome dos arquivos alterados, o que é útil quando houve muitas alterações, usamos a chave –name-only.

$ git diff --name-only
  README.md

Podemos também ver alterações de apenas um arquivo com git diff nome_arquivo.ext. Se, após examinar as modificações, desistimos de alterar um dos arquivos, voltamos atrás apenas na edição desse arquivo.

$ git checkout HEAD README.md

O README.md voltará para o estado anterior, usado na comparação por diff. HEAD é um atalho para o branch atual. Agora verificamos que git diff não exibirá nenhuma mensagem.

$ git checkout HEAD README.md
  Updated 1 path from 384f1f8
$ git diff

Conta do GitHub


GitHub é um serviço de hospedagem online para repositórios Git. Ele permite a sincronização entre repositórios na máquina local e no site remoto. Com esse repositório remoto você tem uma cópia atualizada de todas as suas versões do projeto.

Abra uma conta no GitHub. Dentro da página existem manuais e ajudas para o uso do site. Depois crie um novo repositório (veja figura), dando o nome desse repositório. Escolha o nível de acesso em Descrição -> acessibilidade (Público ou Privado). Quaisquer alterações feitas em um repositório ficam em suspenso até que uma ordem de commit seja emitida, o que faz com que o estado atual do projeto seja armazenado no repositório.

É sempre recomendado inserir um arquivo README junto com seu projeto. Ele serve para descrever o projeto ou adicionar instruções de instalação e seu conteúdo é exibido na primeira página do repositório. Assim como no Git local, toda alteração de arquivo de projeto só se torna parte do repositório após um commit (o que no GitHub é feito com um botão!)

Como veremos, sempre podemos sincronizar as alterações de um repositório local com o repositório do GitHub.

Clonagens

Quando nossas alterações locais estão terminadas podemos clonar nosso projeto para o GitHub. Isso significa que replicamos todo o repositório local para o GitHub. Para isso usamos git push:

O nome do repositório local é o nome de nosso projeto, PyTexto. Entre no GitHub e crie remotamente um repositório com o mesmo nome. (Isso é feito clicando no botão New na sua página inicial.) Quando isso é feito o GitHub fornece um endereço para https, no meu caso https://github.com/gssilva57/PyTexto.git. Agora os repos podem ser conectados:

# conecta repos
$ git remote add origin https://github.com/gssilva57/PyTexto.git

# abra o branch principal de seu repo
$ git branch -M main

# push repo local para o remoto
$ git push -u origin main


Dessa forma o repositório local para o projeto PyTexto estará espelhado no GitHub.

Se estamos na pasta de projeto onde um repositório Git foi definido, todas as alterações serão sincronizadas com o GitHub. Para ver essas alterações atualizamos o navegador aberto no GitHub e podemos ver nele, dentro desse projeto, os arquivos palindromo.py e README.md.

Se você fizer alterações remotas no GitHub de qualquer parte do projeto dê um commit no site. Em sua máquina local vá até o diretório do projeto e digite, na linha de comando:

# para fazer o download das alterações do github para o projeto local
$ git pull

Agora seu projeto local estará novament sincronizado com o remoto e exibirá as alterações feitas lá.

Sincronização com o GitHub

Clonar um repositório do GitHub significa fazer uma cópia do repositório para seu computador local com um download ( git pull). A clonagem faz uma cópia completa de todos os dados do repositório, incluindo todos os commits e branches que o GitHub possui naquele momento. Você pode clonar um projeto para o mesmo repositório junto com outros desenvolvedores para realizar a mesclagem (merge) e fazer correção de conflitos.

Repo Local com o GitHub

Se você tem conteúdo próprio no GitHub você pode reproduzir em sua máquina local o repositório remoto. Esse é um processo chamado clone. Para isso precisamos obter a URL da página do repositório do GitHub. Depois navegamos até o diretório onde queremos esse repositório duplicado e usamos:

$ git clone url_do_projeto_no_github
# o seguintes informações são pedidas
Username for 'https://github.com': seu_user_namer
Password for 'https://gssilva57@github.com':  token de acesso

Atualmente o GitHub não permite que essa operação seja feita apenas com a senha de acesso ao site. A melhor forma de autenticar o comando de linha é habilitando a autenticação em dois passos no GitHub e criando um token de acesso. Todas as informações para isso estão disponíveis no GitHub.

Clonando repositórios de terceiros


Você também pode clonar o repositório de outra pessoa que o tenha tornado público. Dessa forma você para contribuir com um projeto ou simplesmente usar o código de outro desenvolvedor (desde que ele assim o autorize). No GitHub existem livros, vídeos, áudios para treinamento de desenvolvedores, bancos de dados sobre temas variados, blocos de código para diversas finalidades, projetos de jogos, de aplicativos open-source abertos para a participação de devenvolvedores que desejam colaborar, e muito mais.

Você pode fazer uma busca no próprio GitHub ou procurar por sugestões com um mecanismo de busca. Uma lista de projetos interessantes pode ser vista no site Hackernoon.com, página: githubs top 100 most valuable repositories.

Clonando o Microsoft VSCode

Como um exemplo, vamos clonar o Microsoft VSCode. Na página citada acima, do Hackernoon.com encontramos a URL: https://github.com/Microsoft/vscode. Na página do projeto encontramos uma tabela com arquivos e pastas do projeto. Acima da tabela temos o botão code, como na figura, com as alternativas para se baixar todo o repositório. Uma delas consite em usar a própria url acima, como o comando git clone URL:

# criamos uma pasta para abrigar o repo
$ mkdir ~/Projetos/vscode
# navegamos até essa pasta
$ cd ~/Projetos/vscode

# executamos o comando git de clonagem
$ git clone https://github.com/Microsoft/vscode

Agora, se verificarmos o conteúdo da pasta, veremos que o projeto do vscode está lá. Nesse caso está incluído um arquivo README.md com instruções de uso do repositório e instalação do aplicativo.

O VSCode (ou Visual Studio Code) é um bom editor de código feito pela Microsoft para Windows, Linux e macOS. Ele pode ser usado com várias linguagens, inclusive o Python. VSCode é leve e inclui recursos para depuração, realce de sintaxe, conclusão de código inteligente, snippets, refatoração de código e Git incorporado. Usuários podem alterar tema, atalhos de teclado e instalar extensões que adicionam funcionalidades adicionais.

Gerenciando Conflitos

Quando mais de um desenvolvedor altera o projeto em locais diferentes as alterações são mescladas sem nenhuma mensagem de conflito. Um conflito de versões ocorre quando dois desenvolvedores alteram as mesmas linhas de código no mesmo arquivo. Nesse caso o Git lança uma mensagem de conflito de mesclagem e passa para o desenvolvedor a responsabilidade de resolver a situação.

Uma forma de visualizar a situação é a seguinte: em sua máquina local edite o arquivo palindromo.py alterando apenas a linha com o return:

# arquivo palindromo.py
...
    return ("Texto alterado no palíndromo")

Salve o arquivo e faça o commit. Agora no site do GitHub abra o mesmo arquivo e faça uma alteração diferente, na mesma linha:

...
return (f"A palavra {palavra} {p} é um palíndromo")

Faça o commit para que a alteração entre no repositório. Na prática essa seria uma alteração feita por um colega desenvolvedor. Temos agora alterações feitas localmente e no GitHub.

Agora, na máquina local, vamos fazer o push do arquivo modificado para o GitHub. Um mensagem de erro é exibida e o arquivo onde existe o conflito é listado.

# Um atalho (shortcut) para git add e git commit pode ser usado:
$ git pull

# em seu computador local, no terminal, uma mensagem de erro é emitida
  CONFLICT (content): Merge conflict in palindromo.py
  Automatic merge failed; fix conflicts and then commit the result

O arquivo palindromo.py em seu computador foi alterado para mostrar onde foi o conflito. Ele agora tem o seguinte conteúdo:

# arquivo palindromo.py
# palindromo (testa se a palavra é um palíndromo)
def palindromo(palavra):
    palavra= palavra.upper()
    p = ("" if palavra == palavra[::-1] else "não")
<<<<<<< HEAD
    return (f"{palavra} {p} é um palíndromo")
=======
    return ("Texto alterado no palíndromo")
>>>>>>> a986 ... (um código longo)

Como vemos as duas alterações foram exibidas, a primeira feita na máquina local, a segunda no GitHub. Para resolver o conflito apague as mensagens de erro, mantendo apenas a linha (ou linhas) considerada correta:

# exibindo só a linha alterada de palindromo.py
    return (f"{palavra} {p} é um palíndromo")

Em seguida refazemos as etapas add e commit, enviando arquivo palindromo.py atualizado para o GitHub:

# podemos usar atalho para git add e  git commit
$ git commit –am “commit resolvendo conflito em merge”

O arquivo correto, editado à mão pelo desenvolvedor, estará em ambos os repositórios, local e remoto.

Ramificações (ou branching )

É possível ramificar o código de um projeto com o Git de forma que o desenvolvedor possa trabalhar com um “ramo” sem alterar o código-fonte do projeto original. Isso é útil no caso de uma tentativa de variação do projeto que pode ser, mais tarde, definitivamente incorporada ou não. Se o novo ramo for bem sucedido ele pode ser mesclado com o original ou mesmo substituí-lo completamente. Um novo branch permite a alteração daquele estado sem alterar o estado original de onde foi ramificado. Quando um novo branch é criado ele é composto exatamente dos mesmos arquivos e commits de onde foi copiado.

Suponha, por exemplo, que o projeto esteja em uma fase que agrada ao seu cliente e você entrega a ele uma versão funcional. Pode ocorrer mas tarde que ele deseje acrescentar funcionalidades ao projeto. Nesse ponto será interessante criar novo branch ou ramificação. Uma ramificação pode ser criada à partir de qualquer estágio de commit.

Ramificando um projeto

À partir do branch main podemos criar uma ramificação com git checkout (ou git switch). Um novo branch é criado e ativado, com conteúdo idêntico ao do daquele usado como base. O comando git branch pode ser usado para verificar todas as ramificações criadas até agora.

# para ramificar o branch ativo
$ git checkout -b novo_branch
  Switched to a new branch 'novo_branch'

$ git branch
  main
* novo_branch

checkout
O parâmetro -b faz com que, além de criar um novo branch, ele também será selecionado. O prefixo * mostra que estamos agora com novo_branch ativado. O comando alternativo git switch para a troca de branchs é novo e está em fase experimental. Se estivermos com o branch main ativado podemos usar switch:

$ git branch
* main
  novo_branch
$ git switch novo_branch
  Switched to branch 'novo_branch'

Para alterar o nome de um branch ative aquele cujo nome você quer mudar. Depois use git branch -m nome_novo caso queira trocar o nome do branch.

$ git checkout nome_antigo
$ git branch -m nome_novo
$ git status
  On branch nome_novo

Também é possível renomear qualquer branch se você estiver com o branch raiz ativado (no nosso caso branch main) sem ativá-lo antes.

# vá para o branch raiz
$ git checkout main

# use parâmetro -m para renomear qualquer branch
git branch -m nome_antigo nome_novo

Verificando o significado das ramificações

Na mesma pasta de trabalho, edite o arquivo README.md.

# Projeto de teste e aprendizado do Git
Consiste em testes de funcionamento do Git
## Esse linha foi inserida no novo_branch

Use git add e git commit para inserir a nova versão no repositório.

$ git add palindromo.py
$ git commit -m "versão destruída do código"

O código deve ser atualizado, para esse branch. Depois podemos voltar para o branch main:

$ git checkout main
Switched to branch 'main'

# verifique a situação dos branches
$ git branch
* main
  novo_branch

# verifique o conteúdo de README.md
$ cat README.md  
  # Projeto de teste e aprendizado do Git
  Consiste em testes de funcionamento do Git

O branch main está novamente selecionado. Abrindo novamente o arquivo README.md vemos que ele está agora em sua versão anterior, antes que a última linha tenha sido inserida.

Caso as alterações feitas no branch ‘novo_branch’ sejam aprovadas e você queira incorporá-las no projeto principal, use git merge:

# estando branch main ativo faça
$ git merge novo_branch

Resumo de comandos

Podemos ver uma lista de comandos do Git usando:
$ git --help
Esses são os comandos do Git mais usados:

Para iniciar nova área de trabalho
clone clona um repositório para novo directório
init cria repositório vazio ou reinicializa um existente
Para atuar sobre modificações atuais
add adiciona um arquivo na área de preparação (staging)
mv move ou renomeia um arquivo, diretória ou symlink
restore restaura árvore de arquivos de trabalho
rm remove arquivos da árvore de trabalho
Para examinar histórico e estado
bisect use busca binária para encontrar o commit que introduziu um erro
diff exibe alterações entre commits e na árvore de trabalho
grep imprime linhas que satisfazem um padrão
log exibe logs de commits
show exibe diversos tipos de objetos
status exibe o status da árvore de trabalho
Para acrescentar, marcar e alterar seu histórico
branch lista, cria ou apaga ramificações (branches)
commit grava alterações no repositório
merge faz a junção de dois ou mais históricos de desenvolvimento
rebase reaplica commits sobre outra base
reset reset HEAD atual para um estado especificado
switch alterna entre ramos (branches)
tag cria, lista, apaga ou verifica um objeto tag assinado com GPG
Atividades de colaboração
fetch download objetos e referências de outro repositório
pull download de outro repositório e integra com branch local
push atualiza referências remotas com objetos associados

Bibliografia

Livros

  • Ahmad, Jawwad & Belanger, Chris: Advanced Git, The Raywenderlich Tutorail Team, 2021.
  • Chacon, Scott e Straub, Ben: Pro Git, Second Edition, Apress, disponível em git-scm.
  • Santacroce, Ferdinando: Git Essentials, Packt, Mumbai, 2017.
  • Umali, Rick: Learn Git in a Month of Lunches, Manning, Nova Iorque, 2015.

Sites

todos visualizados em março de 2022.

Flask, parte 2


Templates do Jinja2


Um aplicativo web (ou outro qualquer) deve ser escrito de forma clara, para facilitar sua expansão e manutenção. Uma das formas usadas pelo Flask para implementar esse estratégia é a de colocar código python e html separados. Os templates, como vimos, são modelos ou estruturas básicas que podem ser preenchidas dinamicamente, de acordo com as requisições. Esse é o chamado de modelo de separação entre lógica de negócio e lógica de exibição (business and presentation logic). Templates são tratados por um dos módulos que compõem o Flask: o módulo Jinja2.

Um exemplo básico de template para a exibição de um artigo poderia ser o seguinte:

# template.html
<h1>{{ titulo }} </h1>
<p>{{ autor }}, {{ data }} </p>
<p>{{ texto_do_artigo }} </p>
<p>{{ pe_de_pagina }} </p>

Os campos {{ variavel }} são chamados de localizadores (placeholders) para os valores que serão passados pelas funções view. Em muitos casos as informações usadas para popular essas variáveis são lidas em um banco de dados.

Já vimos o exemplo:

@app.route(“/frutas/<nome_da_fruta>”)
def frutas(nome_da_fruta):
return render_template(“frutas.html”, nome_da_fruta = nome_da_fruta)

onde /frutas/<nome_da_fruta>, fornece o valor da varíavel passada para o parâmetro da função (em vermelho). Dentro do corpo da função a variável de mesmo nome (em verde) recebe esse valor. Esses nomes não precisam ser os mesmo, embora esse seja uma prática comum entre programadores do python.

O método render_template() é parte do Jinja2 para integrar templates e lógica do aplicativo.

Filtros


Variáveis e objetos do python podem ser integrados nos templates de algumas formas. Por meio do módulo Jinja temos diversos filtros para manipular campos em templates. Já vimos como inserir uma variável em um template. Um exemplo de filtro é title(), que torna a string no formado de título, com a primeira letra de cada palavra em maísculo.

# suponha que temos a variável titulo = "a casa da mãe joana"
# essa string pode ser exibida dentro de uma tag <h1>
<h1> {{ titulo }} </h1>
↳ a casa da mãe joana

# para maísculas na primeira letra de cada palavra
<h1> {{ titulo | title() }} </h1>
↳ A Casa Da Mãe Joana

Uma descrição dos filtros para texto está na tabela abaixo.

Filtro Descrição
capitalize Converte 1º caracter em maiúsculo, os demais em minúsculo
lower Converte todos os caracteres minúsculo
upper Converte todos os caracteres maiúsculo
title Converte 1º caracter de cada palavra em maiúsculo
trim Remove espaços em branco no início e no fim
safe Renderiza o valor sem aplicar escape (inclui tags)
striptags Remove todas as tags HTML do valor

safe: O filtro safe informa ao Flask que a tag html pode ser renderizada com segurança. Mais exemplos abaixo.
striptags: remove as tags <tag> e </tag> e retorna o texto puro.

Exemplos de Filtros em Strings

Suponha que temos uma variável de nome titulo. Nos templates ela pode ser exibida diretamente, como uma string, ou passando por algum dos vários filtros. Nos quadros seguintes os comentários são iniciados por # enquanto outputs são identificados pelo sinal .

# suponha que a variável titulo2 não está definida
# default fornece um valor default (se titulo2 não está definido).
<h1> {{titulo2 | default ("Título Não Encontrado")}} </h1>
↳ Título Não Encontrado

# torna maiúscula a primeira letra
<h1> {{"mercado" | capitalize()}} </h1>
↳ Mercado

# em linha anterior ao uso podemos definir um valor
# capitalize torna maiúscula a 1ª letra de cada palavra
{% set titulo2 = "um título para a página" %}
<h1> {{ titulo2 | capitalize()}} </h1>
↳ Um título para a página

# title() torna maiúscula a 1ª letra de cada palavra
<h1> {{titulo2 | title()}} </h1>
↳ Um Título Para A Página

# substituir um trecho em uma string
{{ "Bom dia galera!" | replace("Bom dia", "Boa noite") }}
↳ Boa noite galera!

# inverter a ordem dos elementos
{{ "Olá galera!" | reverse() }}
↳ !arelag álO

Conversores

Por default os valores passados em uma url e capturados como valores do python são strings. Alguns conversores podem transformar essas strings em caminhos (que usam barras / ), inteiros ou decimais.

@app.route("/usuario/<int:id>")
def exibir_id(id):
    # esta função recebe id como um inteiro e o exibe
    return f"O id digitado é {id}"

@app.route("/path/")
def exibir_caminho(caminho):
    # recebe e retorna o caminho passado
    return f"Caminho {caminho}"

Os seguintes conversores estão disponívies:

string (default) qualquer string sem barras / ou \
int converte em inteiros positivos
float converte em números decimais
path strings contendo barras de caminho
uuid strings UUID†

† Uma string UUID (Universally Unique IDentifier), também chamada de GUID (Globally Unique IDentifier) é um número de 128-bits usado na troca de informações em computação.

Valores numéricos podem ser convertidos entre inteiros e decimais, e um valor default ser fornecido.

# números inteiros podem ser convertidos em decimais, ou decimais em inteiros
{{ 10 | float() }}
↳ 10.0 ou 0.0      # 0.0 se a conversão não for possível
{{ 10.0 | int() }}
↳ 10

# um valor default, em caso de erro
{{ "qualquer" | float (default = "Erro: texto não pode ser convertido em decimal") }}
↳ Erro: texto não pode ser convertido em decimal

Manipulação de Listas

Diversas operações são disponíveis em listas.

# join: junta elementos de uma lista
{{ [1, 2, 3] | join() }}
↳ 123
{{ ["Um", "Dois", "Tres"] | join() }}
↳ UmDoisTres

# inserindo um separador
{{ [1, 2, 3] | join ("|") }}
↳ 1|2|3
{{ ["Um", "Dois", "Tres"] | join("-") }}
↳ Um-Dois-Tres

# o filtro list() retorna uma lista
{{ "Guilherme" | list()}}
↳ ["G","u","i","l","h","e","r","m","e"]

# random() seleciona um item aleatorio da lista
{{ ["Mercúrio", "Venus", "Terra"] | random() }}
↳ Venus

{% set pe_pagina = ["citacao 1", "citacao 2", "citacao 3", "citacao 4", "citacao 5"] %}
{{ pe_pagina | random() }}
↳ citacao 4

# replace (visto acima para strings) também pode ser usado em listas
{% set lista = ["Nada", "a", "dizer"] %}
{{ lista | replace ("Nada", "Tudo") }}
↳ ["Tudo", "a", "dizer"]

# o filtro reverse() também pode inverter uma lista
# mas seu resultado é um objeto iterador
{% set lista = ["unidade", "dezena", "centena"] %}
{{ list | reverse() }}
↳ <list_reverseiterator object at 0x7fc0b6262518>

# para usar o objeto lista sem usar iterações temos que usar o método list()
{{ list | reverse() | list() }}
↳ ["centena", "dezena", "unidade"]

O filtro random() pode ser útil para exibir um artigo aleatório do site na homepage, para escolher uma imagem ou um pé de página, etc.

Outros exemplos de manipulação de listas incluem o uso de first(), last(), uso de índices e de laços para percorrer toda a lista.

# first() é 1º elemento da lista, last() é o último elemento
{% set nomes = ["João", "Pedro", "da", "Silva"]  %}
<p> Nome: {{ nomes | first() }} </p>
<p> Segundo Nome: {{ nomes [1] }} </p>
<p> Sobrenome: {{ nomes | last() }} </p>
↳ Nome: João
↳ Segundo Nome: Pedro
↳ Sobrenome: Silva

# o tamanho de uma lista é retornado com {{ lista | length }}
# laços for são usados para percorrer os elementos
{% set comentarios = ["Comenta 1", "Comenta 2", "Comenta 3", "Comenta 4"]%}
<p>Temos ({{comentarios | length}}): comentários</p>
{% for comentario in comentarios %}
    <p> {{ comentario }} </p>
{% endfor%}
# resulta em
↳ Temos 4: comentários
↳ Comenta 1
↳ Comenta 2
↳ Comenta 3
↳ Comenta 4

O filtro safe

O filtro safe serve para passar para o interpretador do Flask a informação de que as tags html devem ser renderizadas. Sem ele a string "<texto>" é exibida literalmente, inclusive com os delimitadores "<>".

Os códigos &lt; (<) e &gt; (>) são entidades html, descritas nesse site.

Por motivo de segurança o Jinja2 remove as tags html. Por exemplo: uma variável com valor "<li> TEXTO </li>" será renderizada como "&lt;li&gt; TEXTO &lt;/li&gt;" por extenso e sem provocar a renderização do navegador. Com o filtro safe o TEXTO é exibido como um ítem de lista.

# exibição literal de uma string
{{ "<b>Texto a exibir!</b>" }}
↳ <b>Texto a exibir!</b>

# para forçar a renderização da tag <b> (negrito)
{{ "<b>Texto a exibir!</b>" | safe }}
↳ Texto a exibir!

# define uma lista
{% set lista = ["<li>Um elefante</li>", "<li>Dois elefantes</li>", "<li>Três elefantes</li>"] %}
<ul>
{% for item in list %}
    {{ item | safe }}
{% endfor %}
</ul>
# será renderizado como
↳
⏺ Um elefante
⏺ Dois elefantes
⏺ Três elefantes

# alternativamente
{% set lista = ["Um elefante", "Dois elefantes", "Três elefantes"] %}
<ul>
{% for item in list %}
    <li> {{ item }} </li>
{% endfor %}
</ul>
# será renderizado da mesma forma.
# Nesse caso não existem tags na lista e safe é desnecessário.

Observação importante: Qualquer input digitado por usuários deve passar pelo filtro safe para evitar que alguma instrução danosa seja processada pelo navegador.

Laços e bifurcações


Vimos que um template recebe variáveis do python e pode processá-las com código. Por exemplo, modificamos o template frutas.html da seguinte forma:

# frutas.html
<body>
    {% if nome_da_fruta == None %}
        <p>Você não escolheu uma fruta!</p>
    {% elif nome_da_fruta == "laranja" %}
        <p>Você escolheu laranja, a melhor fruta!</p>
    {% else %}
        <p>Você escolheu a fruta: {{ nome_da_fruta }}</p>
    {% endif %}
</body>

No código de meu_site.py modificamos a função frutas para que por default ela receba None (caso nada seja escrito após o nome do diretório /frutas/:

# meu_site.py (apenas trecho)
@app.route("/frutas/")
@app.route("/frutas/<nome_da_fruta>")
def frutas(nome_da_fruta=None):
    return render_template("frutas.html", nome_da_fruta=nome_da_fruta)

Agora temos as respostas:

url resposta no navegador
http://127.0.0.1:5000/frutas/ Você não escolheu uma fruta!
http://127.0.0.1:5000/frutas/laranja Você escolheu laranja, a melhor fruta!
http://127.0.0.1:5000/frutas/goiaba Você escolheu a fruta: goiaba

No template, os trechos entre chaves não são parte do html e sim do Python, gerenciado pelo Flask. Dessa forma podemos integrar as páginas da web com as inúmeras bibliotecas do Python.

Quatro tipos de marcações estão disponíveis para para inserção do código nos templates.

sintaxe usadas para
{% ... %} linhas de instruções
{{ ... }} expressões
{# ... #} comentários
# ... ## instruções inline

Variáveis

Variáveis a serem usadas nos templates podem ser de qualquer tipo. Por exemplo:

{% set nomes = ["João", "Pedro", "da", "Silva"]  %}
<p>O segundo nome da lista: {{ nomes[1] }}.</p>
↳ O segundo nome da lista: Pedro.

{% set id = 3 %}
<p>Quem? {{ nomes[id] }}!</p>
↳  Quem: Silva!

# uso de um dicionário
{% set dicionario = {"Nome":"Paul"; "Sobrenome":"Dirac"; "Profissão":"Físico"}  %}
<p>Estes são os dados do: {{ dicionario["Nome"] }}.</p>
{% for chave, valor in dicionario.items() %}
    <p>{{ chave }} : {{ valor }}</p>
{% end for %}
↳ Estes são os dados do: Paul.
↳ Nome: Paul
↳ Sobrenome: Dirac
↳ Profissão :Físico

# no caso geral, se um objeto é passado para o template e tem um método, podemos usar:
<p>Obtendo um valor com um método de objeto disponível: {{ objeto.metodo() }}.</p>

Incluindo trechos com include

Se uma parte do template é repetida várias vezes ela pode ser colocada à parte, em arquivo separado, e incluída na template principal. Por ex., se temos um pé de página que aparece em diversas de nossas páginas ele pode ser gravado à parte.

# arquivo pe_de_pagina.html
<div class="pe_pagina">
<p>Esté é o meu pé de página</p>
</div>

Esse código assume que existe a definição de uma classe css chamada pe_pagina.
Em todos os arquivos que devem exibir o pé de página inserimos:

# todo o texto da página
# pé de página
{% include 'pe_de_pagina.html' %}

Macros

Macros são formas de implementar o princípio DRY (Don’t Repeat Yourself ), uma prática de desenvolvimento de software que visa reduzir a repetição do linhas semelhantes ou com mesma função no código. Ele torna o código mais legível e de fácil manutenção, uma vez que alterações devem ser feitas em um único bloco. Templates, macros, inclusão de conteúdo externo (como em include()) e heranças de modelos são todos instrumentos utilizados para isso.

Outra forma de gerar código reutilizável é através da criação de macros, um recurso similar às funções usuais do Python. Macros podem ser gravadas em arquivos separados e importadas dentro de todos os templates que fazem uso delas, facilitando a modularização do código.

Uma macro pode executar tarefas simples como simplesmente montar as linhas da lista. Suponha que temos uma lista de linhas e queremos montar uma lista não ordenada em html:

 {% macro montar_lista(linha) %}
     <li>{{ linha }}</li>
 {% endmacro %}
 <ul>
 {% for linha in linhas %}
     {{ montar_lista(linha) }}
 {% endfor %}
 </ul>

Outra possibilidade importante para a modularização consiste em gravar um arquivo macros.html que contém a macro vermelho(texto, marcas). Ele retorna linhas de uma lista coloridas de vermelho se texto == marcas, de azul caso contrário.

# arquivo macros.html
{% macro vermelho(texto, marcar) %}
{% if texto == marcar %}
    <li style="color:red;">{{ texto }}</li>
{% else %}
    <li style="color:blue;">{{ texto }}</li>
{% endif %}
{% endmacro %}

O arquivo mostrar_frutas.html importa a arquivo anterior, com a sua macro, e faz uso dela para exibir a lista ordenada.

# arquivo mostrar_frutas.html
{% from "macros.html" import vermelho %}
{% set frutas = ["Abacate", "Abacaxi", "Laranja", "Uva"]  %}
{% set selecionado = "Abacaxi"  %}
<ol>
 {% for fruta in frutas %}
     {{ vermelho(fruta, selecionado) }}
 {% endfor %}
</ol>

O resultado no navegador é o seguinte:

Assim como é válido no Python, podemos fazer a importação de forma alternativa (mostrando só linhas diferentes):

# arquivo mostrar_frutas.html
{% import "macros.html" as macros %}
{% for fruta in frutas %}
    {{ macros.vermelho(fruta, selecionado) }}
{% endfor %}

Herança de Templates

Similar à herança de classes no modelo POO do python, podemos criar um template base e derivar dele outros templates que herdam a sua estrutura. Os templates base definem blocos que podem ser sobrescritos nos templates filhos. Um template base pode ter a seguinte estrutura:

# arquivo base.html    
<html>
<head>
 {% block head %}
 <title> Artigo {% block title %}{% endblock %} </title>
 {% endblock %}
</head>
<body>
 {% block body %}
 {% endblock %}
 {% block final %}
 <p>Site construído com Python-Flask!</p>
 {% endblock %} 
</body>
</html>

A instrução {% block nome_do_bloco %}{% endblock %} pode ser substituída por conteúdo nos templates filhos (ou derivados). Para herdar desse template usamos extends e redefinimos os blocos da base:

# arquivo derivado.html    
{% extends "base.html" %}
{% block title %}Nome do Artigo{% endblock %}
{% block head %}
<style>
# estilos css ficam aqui
</style>
{% endblock %}
{% block body %}
<h1>Nome do Artigo</h1>
<p>Texto do artigo</p>
{% endblock %}
{% block final %}
 {{ super() }}
{% endblock %}

O bloco final, {% block final %}{{ super() }}{% endblock %}, usa super() para simplesmente importar o conteúdo do arquivo base. Se a base e o derivado contém texto o conteúdo da base é sobreposto.

Solicitações e Respostas do Servidor (Request, Response)

† O que é uma thread?

Ao receber uma solicitação de um cliente o Flask responde passando para as funções de visualização (view functions) os objetos que serão usados para a construção da página web. Um exemplo é o objeto request, que contém a solicitação HTTP enviada pelo cliente. Temos que nos lembrar que o aplicativo pode receber um grande volume de solicitações em múltiplas threads. Para evitar que todas as funções de visualização recebam essas informações como parâmetros, o que pode tornar o código complexo e gerar conflitos o Flask usa contextos para que esses objetos fiquem temporariamente acessíveis dentro desses contextos.

Para os exemplos que se seguem vamos trabalhar usando o python no terminal. Para isso ativamos o ambiente virtual e o próprio python:

$ cd ~/caminho_para_o_projeto
$ source ./bin/activate
# o prompt muda para indicar ativação de venv
$ (venv) $ python
Python 3.9.10 (main, Jan 17 2022, 00:00:00)

Graças aos contextos, funções de visualização como a seguinte podem ser escritas:

from flask import request
@app.route('/')
def index():
    navegador = request.headers.get('User-Agent')
    return f'<p>Verificamos que seu navegador é o {navegador} </p>'

Que retorna no navegador (no meu caso):

Contextos: Note que request não foi passado explicitamente para index(), agindo como se fosse uma variável global. Isso é obtido pelo Flask com os contextos ou ambientes reservados. Dois ambientes são usados: o contexto de aplicativo e o contexto de requisição.

As seguintes variáveis existem nesses contextos:

nome da variável Contexto Descrição
current_app Aplicativo A instância do aplicativo ativo.
g Aplicativo Objeto usado pelo aplicativo para armazenamento de dados durante uma requisição. Resetada a cada requisição.
request Requisição Objeto que encapsula o conteúdo da requisição HTTP enviada pelo cliente.
session Requisição Representa a sessão do usuário, um dicionário que o aplicativo usa para armazenar valores entre requisições.

Esses contextos só estão disponíveis quando o aplicativo recebe uma requisição (por meio de uma url digitada no navegador). O Flask ativa o contexto de aplicativo disponibilizando current_app e g, e o contexto de requisição disponibilizando request e session, para a thread, e em seguida os remove. Um exemplo desse comportamento pode ser visto no código, executado dentro do python:

from meu_site import app
from flask import current_app
current_app.name                     # um mensagem de erro é exibida
                                     # pois o contexto não está ativo
RuntimeError: working outside of application context

app_contexto = app.app_context()
app_contexto.push()                 # o flask ativa o contexto
current_app.name                    # o nome do app é impresso
'meu_site'
app_contexto.pop()

Flask usa os métodos objeto.push() e objeto.pop() para iniciar e terminar um contexto. A variável current_app.name só existe enquanto o contexto está ativado.

Preparando uma resposta

Para responder a uma solicitação o Flask armazena um mapa que associa URLs (e suas partes) à função resposta que deve ser executada. Esse mapa está armazendo em app.url_map.

from meu_site import app
app.url_map
Map([<Rule '/contatos/' (HEAD, OPTIONS, GET) -> contatos>,
 <Rule '/frutas/' (HEAD, OPTIONS, GET) -> frutas>,
 <Rule '/' (HEAD, OPTIONS, GET) -> index>,
 <Rule '/frutas/<nome_da_fruta>' (HEAD, OPTIONS, GET) -> frutas>,
 <Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>])

A lista mostra o mapeamento das funções view que criamos e mais um, denominado /static, acrescentado automaticamente para acessar arquivos estáticos como arquivos de estilo (cascading style sheets, *.css) e imagens. Os elementos (HEAD, OPTIONS, GET) são passados dentro da URL. Os dois primeiros são gerenciados internamento pelo Flask.

Objeto request

O objeto request, armazenado na variável request, contém toda a informação passada na requisição pela URL. Os atributos e métodos mais comuns desse objeto são listados a seguir.

Atributo/Método Descrição
form Dicionário com todos os campos de form submetidos na requisição.
args Dicionário com todos os argumentos passados na string de pesquisa da URL.
values Dicionário com valores combinados de form e args.
cookies Dicionário com todos os cookies incluídos na requisição.
headers Dicionário com todos os cabeçalhos HTTP incluídos na requisição.
files Dicionário com todos os arquivos de upload incluídos na requisição.
get_data() Retorna dados em buffer na requisição.
get_json() Retorna dicionário com dados JSON incluído na requisição.
blueprint Nome do blueprint que está processando a requisição.
endpoint Nome do endpoint processando a requisição. Flask usa a função view como nome do endpoint para um caminho.
method Método da requisição HTTP, (GET ou POST).
scheme Esquema da URL (http or https).
is_secure() Retorna True se a requisição veio de conexão segura (HTTPS).
host O host definido na requisição, incluindo o número da porta se fornecido pelo cliente.
path Parte da URL que define o caminho.
query_string Parte da URL que define a string de pesquisa (query), como um valor binary (raw).
full_path Parte da URL que define caminho e pesquisa (query).
url Requisição completa da URL fornecida pelo cliente.
base_url O mesmo que url, sem a parte de pesquisa (query).
remote_addr Endereço de IP do cliente.
environ Dicionário com o ambiente de WSGI da requisição.
(†) Blueprints serão vistos mais tarde.

Hooks de Solicitação (request hooks)

Com o Flask podemos registrar funções que devem ser chamadas antes ou depois de uma solicitação. Essas funções podem ser usadas para executar tarefas úteis, tais como autenticar um usuário, abrir e fechar a conexão com um banco de dados, etc. Quatro ganchos (hooks) são disponibilizados:

before_first_request Registra função para execução antes da primeira requisição. Útil para tarefas de inicialização do servidor.
before_request Registra função a ser executada antes de cada requisição.
after_request Registra função a ser executada após cada requisição, caso não ocorram exceções não tratadas.
teardown_request Registra função a ser executada após cada requisição, mesmo que ocorram exceções não tratadas.

Um exemplo de uso desses hooks seria o de usar before_request para coletar dados que serão usados ao longo do ciclo de vida do aplicativo para um usuário e os armazenar na variável g para uso posterior.

Como vimos, uma requisição resulta em uma resposta por meio de uma das funções view enviada ao cliente. Ela pode ser uma página simples de html construída com auxílio dos templates ou algo mais complexo. Junto com a resposta, de acordo com o protocolo HTTP, é enviado um código de status (status code) indicando o sucesso da solicitação. Por default o Flask retorna o status_code = 200 para solicitação bem sucedida. Podemos usar uma função view para retornar outro código.

@app.route('/')
def index():
    return 'Ocorreu um erro!', 400
42 é a resposta do Guia do Mochileiro das Galaxias!

A função acima retorna uma tupla com uma string e um inteiro. É possível e útil fazer com que essas funções retornem um objeto response no lugar da tupla.

from flask import make_response
@app.route('/')
def index():
   response = make_response('Resposta Final sobre o Universo!')
   response.set_cookie('resposta', '42') return response

Dessa forma response passa a conter um cookie (que é gerenciado pelo navegador que o recebe). A tabela seguinte mostra métodos e atributos mais usados no objeto response.

Métodos e atributos do objeto response

Atributo/Método Descrição
status_code Código numérico de status do HTTP
headers Objeto tipo dicionário com todos os cabeçalhos a serem eviados em response
set_cookie() Acrescenta um cookie no objeto response
delete_cookie() Remove um cookie
content_length Comprimento do corpo da response
content_type Tipo de midia do corpo da response
set_data() Define o corpo da response como string ou bytes
get_data() Retorna o corpo da response


Dois tipos de resposta especiais para casos que ocorrem com frequência são fornecidos como funções auxiliares. Uma delas é uma forma de lidar com erros, eviando um código por meio do método abort(). No exemplo abaixo usamos essa função para enviar uma mensagem 404 (página não encontrada) caso a id de um usuário não seja encontrada com load_user(id).

from flask import abort
@app.route('/usuario/<id>')
def usuario(id):
    usuario = load_user(id)
    if not usuario:
        abort(404)
    return f'<div class="usuario">Bem vindo {usuario.name}</div>'

Esse exemplo supõe que exista um template para usuario e que ele carrega instruções css para a classe usuario.

Se load_user(id) retornar None é executada abort(404) que retorna a mensagem de erro. Uma exceção é levantada e a função usuario() é abandonada antes de atingir a instrução return.

O outro tipo de resposta é o redirect que não retorna uma página mas sim uma nova URL redirecionando o navegador. redirect retorna status_code = 302 e a nova URL (dada no código).

from flask import redirect
@app.route('/')
def index():
    return redirect('https://phylos.com/programacao')

Bibliografia

Veja a bibliografia na Parte 1.


Flask, Parte 3 está em preparação!

Flask, parte 1


Web Frameworks

Para a construção de páginas e aplicativos web é essencial algum conhecimento de html e css, não cobertos nesse artigo.

Um aplicativo web web application ou simplesmente web app) é um aplicativo executado através de um servidor web, diferente de um aplicativo executado na máquina local, e geralmente rodados e visualizados por meio de um browser ou navegador. Eles podem ser um conjunto de páginas de texto dinâmicas contendo, por exemplo, pesquisas em uma biblioteca, um gerenciador de arquivos na nuvem, um gerenciador de contas bancárias ou emails, um servidor de músicas ou filmes, etc. Com frequência esses aplicativos estão conectados a um banco de dados, podendo fazer neles consultas e modificações.

Um framework para a web é um conjunto de softwares destinados a oferecer suporte ao desenvolvimento de aplicativos na Web. Eles buscam automatizar a construção dos web apps e seu gerenciamento durante o funcionamento, após sua publicação na web. Alguns frameworks web incluem bibliotecas para acesso a banco de dados, templates para a construção dinâmica das páginas e auxílio à reutilização de código. Eles podem facilitar o desenvolvimento de sites dinâmicos ou, em alguns casos, de sites estáticos.

Framework Flask

O Flask é um micro-framework em Python usado para desenvolvimento e gerenciamento de web apps. Ele é considerado micro porque possui poucas dependências para seu funcionamento e pode ser usado com uma estrutura inicial bem básica, voltada para aplicações simples. Apenas duas bibliotecas são instaladas junto com o Flask. Ele não contém, por ex., um gerenciador de banco de dados ou um servidor de email. No entanto esses serviços podem ser acrescentados para ampliar as funcionalidades do aplicativo.

Em particular, o Flask não inclui uma camada de abstração com banco de dados. Em vez disso é possível instalar extensões, escolhendo o banco de dados específico que se quer usar. Essas extensões também podem auxiliar a validação de formulário, manipulação de upload, tecnologias de autenticação aberta, entre outras.

Flask foi desenvolvido por Armin Ronacher e lançado em 2010. Algumas vantagens citadas em seu uso são: (a) projetos escrito com a Flask são simples (comparados àqueles gerados por frameworks maiores, como o Django) e tendem a ser mais rápidos. (b) Ele é ágil e modular: o desenvolvedor se concentra apenas nos aspectos utilizados de seu aplicativo, podendo ampliar sob demanda. (c) Os projetos são pequenos mas robustos. (d) Existe uma vasta comunidade de desenvolvedores contribuindo com seu desenvolvimento, apresentando bibliotecas que ampliam sua funcionalidade.

Criando um aplicativo básico

Nessa primeira parte vamos criar um aplicativo bem básico mas funcional. Depois entraremos em outros detalhes do Flask.

Para criar um projeto usando o Flask (ou, na verdade, outro projeto qualquer) é aconselhável criar antes um ambiente virtual para que os pacotes instalados não conflituem com outros em projetos diferentes. A criação e manutenção de ambientes virtuais está descrita na página Ambientes Virtuais, PIP e Conda. Alguns IDEs, como o Pycharm realizam automaticamente esse processo. No meu caso ele ficará abrigado em ~/Projetos/flask/venv. Para simplificar denotarei esse ambiente simplesmente pelo nome flask, que é também o nome da pasta que abriga esse projeto.

Nesse ambiente instalamos o Flask com pip install flask. Uma estrutura básica de ambiente já estará montada após esse passo. Em seguida criamos um arquivo do python, de nome meu_site.py.

# meu_site.py
from flask import Flask
app = Flask(__name__)

@app.route("/")
def homepage():
    return "Esta é a minha homepage"

if __name__ == "__main__":
    app.run()

# é exibido no console do python
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

Esse arquivo não deve se chamar flask.py para evitar conflito de nomes.

A variável __name__, passada para o construtor do Flask contém o nome do módulo principal e é usada para determinar a localização do aplicativo no app. Outros diretórios e arquivos, como pastas de templates, arquivo de estilo e imagens, serão localizadas à partir dessa raiz.

Aplicativos do Flask incluem um servidor de desenvolvimento que pode ser iniciado com o comando run. Esse comando busca pelo nome do aplicativo na variável de ambiente FLASK_APP. Se queremos rodar o aplicativo meu_site.py executamos na linha de comando:

# Linux ou macOS
(venv) $ export FLASK_APP=meu_site.py
(venv) $ flask run
 * Serving Flask app "hello"
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

# Microsoft Windows
(venv) $ set FLASK_APP=meu_site.py
(venv) $ flask run
 * Serving Flask app "hello"
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 
 # alternativamente se pode iniciar o servidor com
 app.run()

No Pycharm, ou outros IDES, você pode executar diretamente esse código.

Da biblioteca flask importamos apenas (por enquanto) a classe Flask. Uma instância da classe é criada com app = Flask(__name__) onde a variável __name__ contém o nome do projeto. A linha @app.route("/") é um decorador que informa que a função seguinte será rodada na raiz / do site. Quando esse arquivo .py é executado dentro de uma IDE ou usando python meu_site.py, na linha de comando, é exibido no console várias mensagens, entre elas a url http://127.0.0.1:5000/, que pode ser clicada ou copiada para a linha de endereço do navegador. Isso resulta na exibição, dentro do navegador, da página:


Clientes e Servidores: O navegador age como o cliente que envia ao servidor uma solicitação, através de uma URL digitada na barra de endereços. O servidor da web transforma essa solicitação em ações a serem realizadas do lado do servidor e retorna uma página com conteúdo de texto e multimídia, renderizados pelo navegador. O Flask fica do lado do servidor, construindo a resposta. Entre outras coisas ele possui um mapeamento entre as URLs e as funções route() que serão executadas no código *.py.

O endereço e a porta 127.0.0.1:5000 são padrões para o Flask. app.run() cria um servidor que atende à requisição HTTP do navegador, exibindo a página html. Qualquer texto retornado pela função homepage() é renderizado no formato html e exibido no navegador. Por exemplo, se fizermos as alterações, colocando o texto entre tags h1:

@app.route("/")
def homepage():
    return "<h1>Esta é a minha homepage</h1>"

if __name__ == "__main__":
    app.run(debug=True)

o texto agora é renderizado como um título de nível 1:

o mesmo texto será exibido mas agora com formatação de título, a tag h1. Todas as demais tags podem ser utilizadas. O parâmetro debug=True faz com que alterações no código sejam imediatamente repassadas para as requisições ao servidor, sem a necessidade de rodar todo o projeto novamente. Com isso basta recarregar a página do navegador para que alterações sejam exibidas, clicando no ícone de atualização ou pressionando F5. No mode debug os módulos dois módulos chamados reloader e debugger estão ativados por default. Com o debugger ativado as mensagens de erro são direcionadas para a página exibida. O mode debug nunca deve ser ativado em um servidor em produção pois isso fragiliza a segurança do site.

Também podemos ativar o módulo no código que executa o aplicativo:

(venv) $ export FLASK_APP=meu_site.py
(venv) $ export FLASK_DEBUG=1
(venv) $ flask run

O decorador @app.route("/") registra a função homepage() junto com a página raiz do site. Outras páginas vão executar outras funções. Por exemplo, uma página de contatos pode ser inserida por meio da inserção de nova função no código. Nesse caso criaremos a função contatos().

# meu_site.py

from flask import Flask
app = Flask(__name__)

@app.route("/")
def homepage():
    return "<h1>Esta é a minha homepage</h1>"

@app.route("/contatos")
def contatos():
    txt = (
    "<h1>Página de Contatos</h1>"
    "<ul>"
    "<li>Contato 1</li>"
    "<li>Contato 2</li>"
    "</ul>"
    )
    return txt

if __name__ == "__main__":
    app.run(debug=True)

Usamos acima a concatenação de string com parênteses: (str1 str2 ... strn).
Agora, além da homepage temos a página de contatos em 127.0.0.1:5000/contatos, com a seguinte aparência.

A funções contatos() e homegage() são chamadas funções de visualização (view functions).

Html e templates: Notamos agora que o código em meu_site.py contém sintaxe misturada de Python e html e pode ficar bem complexo em uma página real exibida na web. Para evitar isso o Flask permite a criação de templates. Fazemos isso da seguinte forma: no diretório raiz onde está o projeto (o mesmo onde foi gravado meu_site.py) criamos o diretório template (o nome default do Flask). Dentro dele colocamos nossos templates. Por exemplo, criamos os arquivos homepage.html,

# homepage.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Homepage: teste Flask</title>
</head>
<body>
    <h1>Este é o título da Homepage</h1>
    <p>Com os devidos parágrafos...</p>
</body>
</html>

e contatos.html:

# contatos.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Contatos</title>
</head>
<body>
    <h1>Página de Contatos</h1>
    <ul>
    <li>Contato 1</li>
    <li>Contato 2</li>
    </ul>
    </body>
</html>

Vários IDEs podem auxiliar na criação desses arquivos html, fornecendo um esqueleto básico a ser preenchido pelo programador.

Além disso modificamos nosso código python para usar a renderização dos templates, importando render_template.

# meu_site.py
from flask import Flask, render_template
app = Flask(__name__)

@app.route("/")
def homepage():
    return render_template("homepage.html")

@app.route("/contatos")
def contatos():
    return render_template("contatos.html")

if __name__ == "__main__":
    app.run(debug=True)

Quando esse código é executado temos a referência ao link que, se aberto, mostra as páginas criadas: digitando 127.0.0.1:5000 abrimos nossa homepage:

Mas, se digitarmos 127.0.0.1:5000/contatos a outra página é exibida:

Uma página pode receber parâmetros do código em python. Por exemplo, digamos que queremos exibir uma página para cada produto existente em uma loja virtual que vende frutas. Nesse caso acrescentamos no código de meu_site.py:

@app.route("/frutas/<nome_da_fruta>")
def frutas(nome_da_fruta):
    return render_template("frutas.html")

Para receber esse parâmetro temos que gravar a página frutas.html na pasta templates, com um conteúdo que receba essa variável.

# frutas.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Frutas disponíveis</title>
</head>
<body>
    <h1>Frutas</h1>
    <p>Você escolheu a fruta: {{nome_da_fruta}}</p>
</body>
</html>

Se for digitado no campo de endereços do navegador, ou passado por meio de um link na tag <a href="http://127.0.0.1:5000/frutas/laranja">Laranja</a> a parte do endereço <nome_da_fruta> = laranja é passado como valor de parâmetro na função frutas("laranja") que é disponibilizado dentro do código html como {{nome_da_fruta}}.

Resumindo: @app.route("/frutas/<nome_da_fruta>") envia uma string na variável nome_da_fruta para a função frutas que, por sua vez repassa ao código html. Dentro do html a variável fica disponível como {{nome_da_fruta}} (dentro de uma dupla chave).

Por exemplo, se digitamos na barra de endereços do navegador http://127.0.0.1:5000/frutas/laranja teremos a exibição de

Essa técnica pode ser usada, por ex., para criar páginas para diversos usuários usando um único template usuario.html.

@app.route('/usuario/<nome>')
def usuario(nome):
    return render_template("usuario.html")
# ou
@app.route('/usuario/<int:id>')
    return render_template("usuario.html")

A parte do código <int:id> é um filtro que transforma a entrada digitada em inteiro, quando possível e será melhor explicada adiante.

Formatando com CSS

O texto dentro de uma página html (HyperText Markup Language) pode ser formatado de algumas formas diferentes, usando css (Cascading Style Sheets). Quando se trata do uso de um framework a forma preferida consiste em apontar no cabeçalho para um arquivo externo css. No Flask isso é feito da seguinte forma: um arquivo css é gravado na pasta static da pasta do projeto. Digamos que gravamos o arquivo /static/styles.css com um conteúdo mínimo, apenas para demonstração, tornando vermelhas as letras do título e azuis as letras dos parágrafos:

# arquivo /static/styles.css
h1 { color:red; }
p { color:blue; }

No cabeçalho das páginas html, dentro da tag <head> colocamos um link para o arquivo de estilos:

# homepage.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" type="text/css" href="static/styles.css">
    <title>Homepage: teste Flask</title>
</head>

Agora, ao acessar a homepage veremos:


Com todas essas alterações o projeto tem agora a estrutura de pastas mostrada. Na figura à esquerda todas as pastas, inclusive aquelas criadas pelo gerenciador de ambientes virtuais são mostradas. No meu caso elas foram criadas automaticamente pelo IDE Pycharm, mas podem ser criadas pelo programador sem dificuldade. Na figura à direita são mostradas apenas as pastas criadas pela programador diretamente. Um projeto com esse formato roda perfeitamente, apesar de não contar com as vantagens do ambiente virtual (veja artigo).

Outras estruturas de código podem ser inseridas nos templates, como veremos.

Opções de comando de linha

Quando se roda o flask diretamente no terminal podemos ver uma mensagem de ajuda com (venv) $ flask --help, verificar os caminhos definidos no app ou entrar em uma shell interativa.

# Exibir ajuda do flask    
(venv) $ flask --help
  Usage: flask [OPTIONS] COMMAND [ARGS]...
  
    A general utility script for Flask applications.
  
    Provides commands from Flask, extensions, and the application. Loads the
    application defined in the FLASK_APP environment variable, or from a wsgi.py
    file. Setting the FLASK_ENV environment variable to 'development' will
    enable debug mode.
  
      $ export FLASK_APP=hello.py
      $ export FLASK_ENV=development
      $ flask run
  
  Options:
    --version  Show the flask version
    --help     Show this message and exit.
  
  Commands:
    routes  Show the routes for the app.
    run     Run a development server.
    shell   Run a shell in the app context.
  
# exibe os caminhos ativos no aplicativo
  (venv) $ flask routes
  Endpoint  Methods  Rule
  --------  -------  -----------------------
  contatos  GET      /contatos/
  frutas    GET      /frutas/
  frutas    GET      /frutas/
  homepage  GET      /
  static    GET      /static/
  
# entra em uma shell interativa    
  (venv) $ flask shell

A shell do flask inicia uma sessão python no contexto do atual aplicativo onde podemos executar testes ou tarefas de manutenção. O comando flask run admite vários parâmetros:

(venv) $ flask run --help
  Usage: flask run [OPTIONS]
  
    Run a local development server.
  
    This server is for development purposes only. It does not provide the
    stability, security, or performance of production WSGI servers.
  
    The reloader and debugger are enabled by default if FLASK_ENV=development or
    FLASK_DEBUG=1.
  
  Options:
    -h, --host TEXT                 The interface to bind to.
    -p, --port INTEGER              The port to bind to.
    --cert PATH                     Specify a certificate file to use HTTPS.
    --key FILE                      The key file to use when specifying a
                                    certificate.
    --reload / --no-reload          Enable or disable the reloader. By default
                                    the reloader is active if debug is enabled.
    --debugger / --no-debugger      Enable or disable the debugger. By default
                                    the debugger is active if debug is enabled.
    --eager-loading / --lazy-loading
                                    Enable or disable eager loading. By default
                                    eager loading is enabled if the reloader is
                                    disabled.
    --with-threads / --without-threads
                                    Enable or disable multithreading.
    --extra-files PATH              Extra files that trigger a reload on change.
                                    Multiple paths are separated by ':'.
    --help                          Show this message and exit.  

O argumento --host informa ao servido qual é o ambiente web que pode acessar nosso servidor de desenvolvimento. Por default o servidor de desenvovimento do Flask só aceita chamadas do computador local, em localhost. Mas é possível configurá-lo para receber chamadas da rede local ou de ambientes mais amplos. Por exemplo, como o código

(venv) $ flask run --host 0.0.0.0
 * Serving Flask app "hello"
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

todos os computadores conectados pelo mesmo ip terão acesso ao aplicativo.

Implantação de um aplicativo Flask

O processo de implantação (ou deploy) de um aplicativo consiste nas etapas necessários para colocá-lo acessível para seus usuários. No caso de um aplicativo web a implantação significa estabelecer um servidor ou usar servidores já disponíveis, que os usuários possam acessar, e colocar seu aplicativo como um de seus serviços.

O desenvolvimento do aplicativo se dá em um ambiente de desenvolvimento onde podem existir condições próprias para o debug e nem todas as medidas de segurança estão implementadas. Depois ele passa para a etapa de uso, no ambiente de produção. Uma conta no Heroku pode ser criada, e um site com poucos acessos pode ser mantido sem custos. Se o site for escalonado e crescer a conta deve ser atualizada e paga. A lista abaixo contém links para o Heroku e outros provedores.

Bibliografia

Livros sobre Flask

  • Aggarwal, Shalabh: Flask Framework Cookbook, 2.Ed., Packt, Birmingham-Mumbai, 2019.
  • Ashley, David: Foundation Dynamic Web Pages with Python Create Dynamic Web Pages with Django and Flask, Apress, 2020.
  • Gaspar, D.;StoufferHaider, J.: Mastering Flask Web Development, 2.Ed., Packt, Birmingham-Mumbai, 2018.
  • Grinberg, Miguel: The Flask Mega-Tutorial, Edição do autor, 2020.
  • Grinberg, Miguel: Flask Web Development, Developing Web Applications with Python, O’Reilly, Sebastopol, 2018.
  • Haider, Rehan: Web API Development With Python, CloudBytes, 2020.
  • Maia, Italo: Building Web Applications with Flask, 2.Ed., Packt, Birmingham-Mumbai, 2015.
  • Relan, Kunal: Building REST APIs with Flask, 2.Ed., Apress, 2019.

Referências na Web

Sobre HTML e CSS

todos acessados em fevereiro de 2022.