Crear un fichero .py y ejecutar un script de Python es muy sencillo. Sin embargo, configurar un mínimo entorno de desarrollo que te ayude a programar con ciertas garantías, puede ser algo “intimidante” si estás empezando.
Este post incluirá un par de recetas para tener listo en un periquete un nuevo proyecto de Python y algunos consejos iniciales para ponerlo en marcha.
Serán un par porque todavía ando divagando sobre si usar pipenv o poetry. Aunque, para ser honesto, a día de hoy me gusta más poetry.
Dependencias
Dependencia | Uso |
---|---|
isort | Ordenación de import |
black | Formatear código |
flake8 | Linter, también podrías usar pylint |
pflake8 | Configuración de flake8 en pyproject.toml en vez del fichero .flake8 |
pytest | Framework de testing |
assertpy | Librería de aserciones |
pytest-asyncio | Tests asíncronos (async , await ) |
pytest-cov | Cobertura de código |
mypy | Tipado opcional |
pre-commit | Git hook |
.gitignore | Auto-generado |
Esta lista es la que es, pero podría ser otra perfectamente. Es decir, está totalmente sesgada por lo que yo considero “más o menos” indispensable en cualquier proyecto de Python. Siéntete libre de añadir y/o eliminar todo aquello que consideres oportuno. De hecho, sólo añadiendo plugins de
flake8
podría crecer considerablemente.
El script es PowerShell, pero entiendo debería ser sencillo adaptarlo a tu shell preferida.
Ambos scripts definen un par de variables $app
(nombre del paquete) y $directory
(directorio donde crear el proyecto). $directory
puede ser lo que tu quieras, no es relevante. $app
sin embargo será el nombre del paquete de Python, luego tendrías que usar (si es preciso) la notación snake_case. Por ejemplo:
$app = "my_project"
$directory = "MyProject"
poetry
$app = "app1"
$directory = "App1"
poetry new --name $app --src $directory
cd $directory
poetry add -G dev isort black flake8 flake8-pyproject pytest assertpy pytest-asyncio pytest-cov mypy pre-commit
echo "[tool.black]
line-length = 120
target-version = [""py311""]
[tool.isort]
profile = ""black""
[tool.flake8]
max-line-length = 120
max-doc-length = 140
[tool.mypy]
disallow_untyped_defs = true
no_implicit_reexport = true
warn_redundant_casts = true
warn_unused_ignores = true
show_error_codes = true
pretty = true
show_error_context = true
show_column_numbers = true
[[tool.mypy.overrides]]
module = [
""assertpy.*""
]
ignore_missing_imports = true
[tool.pytest.ini_options]
pythonpath = ""src/""
asyncio_mode = ""auto""
[tool.coverage.run]
omit = [""tests/*""]
[tool.coverage.report]
exclude_lines = [""pass""]" >> pyproject.toml
echo "fail_fast: true
repos:
- repo: https://github.com/python-poetry/poetry
rev: 1.2.2
hooks:
- id: poetry-check
verbose: true
# - id: poetry-lock
# verbose: true
- repo: https://github.com/PyCQA/isort
rev: 5.10.1
hooks:
- id: isort
verbose: true
- repo: https://github.com/psf/black
rev: 22.10.0
hooks:
- id: black
verbose: true
- repo: local
hooks:
- id: Flake8-pyproject
name: Flake8-pyproject
entry: flake8p
language: system
types: [python]
verbose: true
- repo: local
hooks:
- id: mypy
name: mypy
entry: mypy
language: system
pass_filenames: false
args: ["".""]
verbose: true
always_run: true" > .pre-commit-config.yaml
Invoke-RestMethod -Method Get -Uri "https://www.toptal.com/developers/gitignore/api/python,pycharm,visualstudiocode" | Out-File .gitignore
git init
poetry run pre-commit install
poetry run pre-commit autoupdate
echo "def greet(name: str) -> str:
return f""Hello {name}""
if __name__ == ""__main__"":
print(greet(""World""))" > src\$app\main.py
echo "from assertpy import assert_that
from $app.main import greet
def test_greet() -> None:
assert_that(greet(""World"")).is_equal_to(""Hello World"")" > tests\test_main.py
El hook de poetry lock es muy opinable. Además de llevarse un tiempo precioso, dejo a tu elección el actualizar librerías de forma tan alegre.
│ .gitignore
│ .pre-commit-config.yaml
│ poetry.lock
│ pyproject.toml
│ README.md
│
├───src
│ └───app1
│ main.py
│ __init__.py
│
└───tests
test_main.py
__init__.py
pipenv
A pipenv
no le quiero dejar de lado porque con él empezó mi andadura en Python y le tengo cariño. Sin embargo, si empezara hoy a programar en Python, te recomendaría optar por poetry.
$app = "app1"
$directory = "App1"
mkdir $directory
cd $directory
pipenv install --dev isort black flake8 flake8-pyproject pytest assertpy pytest-asyncio pytest-cov mypy pre-commit
echo "[tool.black]`
line-length = 120
target-version = [""py311""]
[tool.isort]
profile = ""black""
[tool.flake8]
max-line-length = 120
max-doc-length = 140
[tool.mypy]
disallow_untyped_defs = true
no_implicit_reexport = true
warn_redundant_casts = true
warn_unused_ignores = true
show_error_codes = true
pretty = true
show_error_context = true
show_column_numbers = true
[[tool.mypy.overrides]]
module = [
""assertpy.*""
]
ignore_missing_imports = true
[tool.pytest.ini_options]
pythonpath = ""src/""
asyncio_mode = ""auto""
[tool.coverage.run]
omit = [""tests/*""]
[tool.coverage.report]
exclude_lines = [""pass""]" > pyproject.toml
echo "fail_fast: true`
repos:
- repo: https://github.com/PyCQA/isort
rev: 5.10.1
hooks:
- id: isort
verbose: true
- repo: https://github.com/psf/black
rev: 22.10.0
hooks:
- id: black
verbose: true
- repo: local
hooks:
- id: Flake8-pyproject
name: Flake8-pyproject
entry: flake8p
language: system
types: [python]
verbose: true
- repo: local
hooks:
- id: mypy
name: mypy
entry: mypy
language: system
pass_filenames: false
args: ["".""]
verbose: true
always_run: true" > .pre-commit-config.yaml
Invoke-RestMethod -Method Get -Uri "https://www.toptal.com/developers/gitignore/api/python,pycharm,visualstudiocode" | Out-File .gitignore
git init
pipenv run pre-commit install
pipenv run pre-commit autoupdate
mkdir src
mkdir src\$app
mkdir tests
echo "def greet(name: str) -> str:
return f""Hello {name}""
if __name__ == ""__main__"":
print(greet(""World""))" > src\$app\main.py
echo "from assertpy import assert_that
from $app.main import greet
def test_greet() -> None:
assert_that(greet(""World"")).is_equal_to(""Hello World"")" > tests\test_main.py
Ahora aparecen los ficheros Pipfile
y Pipfile.lock
y desaparece poetry.lock
.
│ .gitignore
│ .pre-commit-config.yaml
│ Pipfile
│ Pipfile.lock
│ pyproject.toml
│
├───src
│ └───app1
│ main.py
│
└───tests
test_main.py
Entorno virtual
En este punto, nuestra siguiente acción debería ser activar el entorno virtual.
Ten en cuenta que, cuando borras la carpeta del proyecto, no se borrará el entorno virtual asociado (a no ser que esté dentro de la misma carpeta, que no es lo habitual aunque podría ser). Tanto
pipenv
comopoetry
tienen comandos para borrar el entorno virtual.pipenv -rm
ypoetry env remove
. Sin embargo, si eres como yo y no te acuerdas nunca de hacerlo antes de borrar el proyecto, te tocará de vez de en cuando hacer limpia manualmente. Los depipenv
se guardan por defecto en%USERPROFILE%\.virtualenvs
y los depoetry
donde digapoetry config virtualenvs.path
. Adicionalmente,pre-commit
creará un entorno virtual para cada hook que no sea del tiporepo: local
en%USERPROFILE%\.cache\pre-commit
.
Para activar el entorno virtual desde línea de comandos, bien usa poetry shell
o pipenv shell
.
Una vez activado, lo siguiente debería funcionar:
python .\src\<your_app>\main.py
pytest
pytest --cov --cov-report=html
VSCode
En VSCode recuerda seleccionar el entorno virtual.
Para ejecutar los tests te recomiendo la extensión Test Explorer UI.
PyCharm
Para PyCharm, igualmente tendrás que seleccionar el interprete.
Además, para que los import
de tests no te muestren ningún error en el IDE, tienes que marcar la carpeta src como Sources Root (que hará que se agregue a la variable de entorno PYTHONPATH
).
Para ejecutar los tests quizás tengas que decirle que tu framework es pytest
(File > Settings > Tools > Python Integrared Tools > Testing > Default test runner).
python.exe
Si tuvieras que localizar manualmente el interprete (esto es el fichero python.exe de tu entorno virtual porque el IDE de turno no te lo detecta automáticamente) con pipenv --py
o poetry env info
puedes obtener la ruta.
FastAPI
Como bola extra, una pequeña variación de poetry
usando FastAPI:
Si ya habías activado el entorno virtual, vuelve a declarar la variable
$app
.
poetry add fastapi[all]
echo "import uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.get(""/"")
async def root():
return ""Hello World!""
if __name__ == ""__main__"":
uvicorn.run(""main:app"", reload=True)" > src\$app\main.py
echo "from http import HTTPStatus
from starlette.testclient import TestClient
from $app.main import app
client = TestClient(app)
def test_root():
r = client.get(""/"")
assert r.status_code == HTTPStatus.OK
assert r.json() == ""Hello World!""" > tests\test_main.py
También puedes instalar algunos plugins para ayudarte un poco más.
- Plugin para PyCharm/Pydantic
- Plugin para Mypy/Pydantic
- Plugin para Mypy/SQLAlchemy
El plugin de pydantic
ya estará instalado porque el propio pydantic
es una dependencia de fastapi
.
Para instalar el plugin de SQLAlchemy
, poetry add sqlalchemy[mypy]
que instala el paquete sqlalchemy2-stubs
como dependencia extra de sqlalchemy
.
A continuación, añade lo siguiente en la sección [tool.mypy]
de pyproject.toml
:
plugins = [
"sqlalchemy.ext.mypy.plugin",
"pydantic.mypy",
]
pre-commit
Esta sección es una explicación de porqué usar repo: local en pre-commit
.
Es decir, por qué esto:
- repo: local
hooks:
- id: Flake8-pyproject
name: Flake8-pyproject
entry: flake8p
language: system
types: [python]
verbose: true
- repo: local
hooks:
- id: mypy
name: mypy
entry: mypy
language: system
pass_filenames: false
args: ["."]
verbose: true
always_run: true"
En vez de esto:
- repo: https://github.com/john-hen/Flake8-pyproject
rev: 1.2.0
hooks:
- id: Flake8-pyproject
verbose: true
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.991
hooks:
- id: mypy
args: []
verbose: true
mypy
Si usáramos la segunda opción, mypy .
funcionaría pero pre-commit
no:
pyproject.toml:1:1: error: Error importing plugin "sqlalchemy.ext.mypy.plugin":
No module named 'sqlalchemy' [misc]
[tool.poetry]
^
Found 1 error in 1 file (errors prevented further checking)
Eso es porque el directorio virtual que crea pre-commit
para ejecutar el hook de mypy
sólo instala mypy
, no instala ninguna otra dependencia de nuestro proyecto, y como los plugins están en los paquetes sqlalchemy2-stubs
y pydantic
, no están disponibles en el entorno virtual de pre-commit
. Para instalar los paquetes tendríamos que ser nosotros mismos quien se lo dijera a pre-commit
en el hook de mypy
:
additional_dependencies: ['sqlalchemy[mypy]==<your_current_version>',pydantic==<your_current_version>]
<your_current_version> se puede saber con poetry show <package>
.
Ahora el error de pre-commit
sería otro:
src\web_app1\main.py:1:1: error: Cannot find implementation or library stub
for module named "uvicorn" [import]
import uvicorn
^
src\web_app1\main.py:1:1: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
src\web_app1\main.py:2:1: error: Cannot find implementation or library stub
for module named "fastapi" [import]
from fastapi import FastAPI
^
tests\test_main.py:4:1: error: Cannot find implementation or library stub for
module named "starlette.testclient" [import]
from starlette.testclient import TestClient
^
Found 3 errors in 2 files (checked 4 source files)
Lo que estaría pasando es que mypy
no sería capaz de seguir los import
. Por defecto, pre-commit
ejecuta mypy
con --ignore-missing-imports
(una mala decisión en mi opinión), pero al haber incluido args: []
en la definición del hook habríamos invalidado ese comportamiento predeterminado. Luego ahora tocaría también instalar en el entorno virtual de pre-commit
las dependencias para las que quisiéramos seguir los import
.
additional_dependencies: [sqlalchemy-stubs==0.4,pydantic==1.10.2,'fastapi[all]==0.87.0']
Los
import
que se hubieran ignorado deliberadamente enpyproject.toml
no habría que incluirlos enadditional_dependencies
. En nuestro caso por ejemplo, ignoramosassertpy
porque la librería no tiene tipos.
flake8
Al igual que con mypy, si usarámos plugins adicionales para flake8
habría que especificarlos en additional_dependencies
Para ver cuál es el problema se podría instalar un plugin cualquiera poetry add -G dev flake8-builtins
.
Y con un código como el siguiente.
def function_using_a_parameter_that_shadows_built_in_name_list(list):
print(list)
Al ejecutar flake8p
recibirías un error pero no al usar pre-commit run Flake8-pyproject
.
.\src\web_app1\main.py:12:64: A002 argument "list" is shadowing a python builtin
Para solucionarlo, bastaría con añadir la dependencia al hook de Flake8-pyproject
:
additional_dependencies: [flake8-builtins==2.0.1]
repo: local
La verdad es que conseguir que mypy .
/pre-commit run mypy
y flake8p
/pre-commit run Flake8-pyproject
funcionen exactamente igual implica tener sincronizadas algunas dependencias vía additional_dependencies
y eso es un poco molesto, la verdad. No lo veo mantenible a largo plazo.
Ya no es sólo la perdida de confianza y que termines ejecutando en local mypy
/flake8p
además de pre-commit
, es que puede ser que rompas la build en el servidor de integración por algo que “supuestamente” pre-commit
te había dicho estaba ok.
Por eso y para no tener la mosca detrás de la oreja y olvidarnos de additional_dependencies
, mejor usar repo: local
y a correr.
Un saludo!