Skip to content

Commit

Permalink
Add support for type checks and type annotations
Browse files Browse the repository at this point in the history
fixes #164 

* Add mypy to dev dependencies
* Add basic mypy configuration to pyproject.toml
* Add mypy target(s) to noxfile
* Add type annotations to git script
* Add type annotations to version_check script/module
* Fix type annotations in links script/module
* Add various type annotations and add pyodbc typeshed
* Bump version to 3.1.7
  • Loading branch information
Nicoretti authored Aug 4, 2022
1 parent 57beb85 commit c1684b8
Show file tree
Hide file tree
Showing 9 changed files with 614 additions and 71 deletions.
79 changes: 54 additions & 25 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,25 @@
SCRIPTS = PROJECT_ROOT / "scripts"
sys.path.append(f"{SCRIPTS}")

from typing import (
Dict,
Iterable,
Iterator,
MutableMapping,
Tuple,
)

import nox
from git import tags
from links import check as _check
from links import documentation as _documentation
from links import urls as _urls
from pyodbc import connect
from nox import Session
from nox.sessions import SessionRunner
from pyodbc import (
Connection,
connect,
)
from version_check import (
version_from_poetry,
version_from_python_module,
Expand Down Expand Up @@ -54,15 +67,15 @@ class Settings:
)


def find_session_runner(session: nox.Session, name: str):
def find_session_runner(session: Session, name: str) -> SessionRunner:
"""Helper function to find parameterized action by name"""
for s, _ in session._runner.manifest.list_all_sessions():
if name in s.signatures:
return s
session.error(f"Could not find a nox session by the name {name!r}")


def transaction(connection, sql_statements):
def transaction(connection: Connection, sql_statements: Iterable[str]) -> None:
cur = connection.cursor()
for statement in sql_statements:
cur.execute(statement)
Expand All @@ -71,7 +84,7 @@ def transaction(connection, sql_statements):


@contextmanager
def environment(env_vars):
def environment(env_vars: Dict[str, str]) -> Iterator[MutableMapping[str, str]]:
_env = os.environ.copy()
os.environ.update(env_vars)
yield os.environ
Expand All @@ -80,10 +93,9 @@ def environment(env_vars):


@contextmanager
def temporary_odbc_config(config):
def temporary_odbc_config(config: str) -> Iterator[Path]:
with TemporaryDirectory() as tmp_dir:
tmp_dir = Path(tmp_dir)
config_dir = tmp_dir / "odbcconfig"
config_dir = Path(tmp_dir) / "odbcconfig"
config_dir.mkdir(exist_ok=True)
config_file = config_dir / "odbcinst.ini"
with open(config_file, "w") as f:
Expand All @@ -92,7 +104,7 @@ def temporary_odbc_config(config):


@contextmanager
def odbcconfig():
def odbcconfig() -> Iterator[Tuple[Path, MutableMapping[str, str]]]:
with temporary_odbc_config(
ODBCINST_INI_TEMPLATE.format(driver=Settings.ODBC_DRIVER)
) as cfg:
Expand All @@ -102,7 +114,7 @@ def odbcconfig():


@nox.session(python=False)
def fix(session):
def fix(session: Session) -> None:
session.run(
"poetry",
"run",
Expand All @@ -116,7 +128,7 @@ def fix(session):


@nox.session(name="code-format", python=False)
def code_format(session):
def code_format(session: Session) -> None:
session.run(
"poetry",
"run",
Expand All @@ -131,14 +143,14 @@ def code_format(session):


@nox.session(python=False)
def isort(session):
def isort(session: Session) -> None:
session.run(
"poetry", "run", "python", "-m", "isort", "-v", "--check", f"{PROJECT_ROOT}"
)


@nox.session(python=False)
def lint(session):
def lint(session: Session) -> None:
session.run(
"poetry",
"run",
Expand All @@ -150,13 +162,28 @@ def lint(session):
)


@nox.session(name="type-check", python=False)
def type_check(session: Session) -> None:
session.run(
"poetry",
"run",
"mypy",
"--strict",
"--show-error-codes",
"--pretty",
"--show-column-numbers",
"--show-error-context",
"--scripts-are-modules",
)


@nox.session(python=False)
@nox.parametrize("db_version", Settings.DB_VERSIONS)
@nox.parametrize("connector", Settings.CONNECTORS)
def verify(session, connector, db_version):
def verify(session: Session, connector: str, db_version: str) -> None:
"""Prepare and run all available tests"""

def is_version_in_sync():
def is_version_in_sync() -> bool:
return (
version_from_python_module(Settings.VERSION_FILE) == version_from_poetry()
)
Expand All @@ -169,7 +196,9 @@ def is_version_in_sync():
)
session.notify("isort")
session.notify("code-format")
session.notify("type-check")
session.notify("lint")
session.notify("type-check")
session.notify(find_session_runner(session, f"db-start(db_version='{db_version}')"))
session.notify(
find_session_runner(session, f"integration(connector='{connector}')")
Expand All @@ -179,10 +208,10 @@ def is_version_in_sync():

@nox.session(name="db-start", python=False)
@nox.parametrize("db_version", Settings.DB_VERSIONS)
def start_db(session, db_version=Settings.DB_VERSIONS[0]):
def start_db(session: Session, db_version: str = Settings.DB_VERSIONS[0]) -> None:
"""Start the test database"""

def start():
def start() -> None:
# Consider adding ITDE as dev dependency once ITDE is packaged properly
with session.chdir(Settings.ITDE):
session.run(
Expand All @@ -202,7 +231,7 @@ def start():
external=True,
)

def populate():
def populate() -> None:
with odbcconfig():
settings = {
"driver": "EXAODBC",
Expand Down Expand Up @@ -236,15 +265,15 @@ def populate():


@nox.session(name="db-stop", python=False)
def stop_db(session):
def stop_db(session: Session) -> None:
"""Stop the test database"""
session.run("docker", "kill", "db_container_test", external=True)
session.run("docker", "kill", "test_container_test", external=True)


@nox.session(python=False)
@nox.parametrize("connector", Settings.CONNECTORS)
def integration(session, connector):
def integration(session: Session, connector: str) -> None:
"""Run(s) the integration tests for a specific connector. Expects a test database to be available."""

with odbcconfig() as (config, env):
Expand All @@ -259,7 +288,7 @@ def integration(session, connector):


@nox.session(name="report-skipped", python=False)
def report_skipped(session):
def report_skipped(session: Session) -> None:
"""
Runs all tests for all supported connectors and creates a csv report of skipped tests for each connector.
Expand Down Expand Up @@ -301,7 +330,7 @@ def report_skipped(session):


@nox.session(name="check-links", python=False)
def check_links(session):
def check_links(session: Session) -> None:
"""Checks weather or not all links in the documentation can be accessed"""
errors = []
for path, url in _urls(_documentation(PROJECT_ROOT)):
Expand All @@ -317,15 +346,15 @@ def check_links(session):


@nox.session(name="list-links", python=False)
def list_links(session):
def list_links(session: Session) -> None:
"""List all links within the documentation"""
for path, url in _urls(_documentation(PROJECT_ROOT)):
session.log(f"Url: {url}, File: {path}")


@nox.session(python=False)
def release(session: nox.Session):
def create_parser():
def release(session: Session) -> None:
def create_parser() -> ArgumentParser:
p = ArgumentParser(
"Release a pypi package",
usage="nox -s release -- [-h] [-d]",
Expand All @@ -341,7 +370,7 @@ def create_parser():

version_file = version_from_python_module(Settings.VERSION_FILE)
module_version = version_from_poetry()
git_version = version_from_string(tags()[-1])
git_version = version_from_string(list(tags())[-1])

if not (module_version == git_version == version_file):
session.error(
Expand Down
21 changes: 20 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 13 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "sqlalchemy_exasol"
version = "3.1.6"
version = "3.1.7"
description = "EXASOL dialect for SQLAlchemy"
readme = "README.rst"
authors = [
Expand Down Expand Up @@ -74,6 +74,7 @@ wheel = "^0.37.1"
black = "^22.6.0"
isort = "^5.10.1"
pylint = "^2.14.5"
mypy = "^0.971"

[tool.poetry.extras]
turbodbc = ["turbodbc"]
Expand Down Expand Up @@ -104,4 +105,14 @@ fail-under = 5.6

[tool.pylint.format]
max-line-length = 88
max-module-lines = 800
max-module-lines = 800

[tool.mypy]
files = [
'noxfile.py',
'scripts/**/*.py',
# Incrementaly add files and paths to fix until the entire project is checked
]
mypy_path = [
'typeshed'
]
11 changes: 4 additions & 7 deletions scripts/git.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
from subprocess import (
PIPE,
run,
)
from subprocess import run
from typing import Iterable


def tags():
def tags() -> Iterable[str]:
"""
Returns a list of all tags, sorted from [0] oldest to [-1] newest.
PreConditions:
Expand All @@ -14,5 +12,4 @@ def tags():
"""
command = ["git", "tag", "--sort=committerdate"]
result = run(command, capture_output=True, check=True)
tags = (tag.strip() for tag in result.stdout.decode("utf-8").splitlines())
return list(tags)
return [tag.strip() for tag in result.stdout.decode("utf-8").splitlines()]
12 changes: 7 additions & 5 deletions scripts/links.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
from itertools import repeat
from pathlib import Path
from typing import (
Container,
Iterable,
Optional,
Tuple,
)
from urllib import request
Expand All @@ -13,7 +15,7 @@ def documentation(root: Path) -> Iterable[Path]:
"""Returns an iterator over all documentation files of the project"""
docs = Path(root).glob("**/*.rst")

def _deny_filter(path):
def _deny_filter(path: Path) -> bool:
return not ("venv" in path.parts)

return filter(lambda path: _deny_filter(path), docs)
Expand All @@ -22,8 +24,8 @@ def _deny_filter(path):
def urls(files: Iterable[Path]) -> Iterable[Tuple[Path, str]]:
"""Returns an iterable over all urls contained in the provided files"""

def should_filter(url):
_filtered = []
def should_filter(url: str) -> bool:
_filtered: Container[str] = []
return url.startswith("mailto") or url in _filtered

for file in files:
Expand All @@ -38,12 +40,12 @@ def should_filter(url):
yield from zip(repeat(file), filter(lambda url: not should_filter(url), _urls))


def check(url: str) -> Tuple[int, str]:
def check(url: str) -> Tuple[Optional[int], str]:
"""Checks if an url is still working (can be accessed)"""
try:
# User-Agent needs to be faked otherwise some webpages will deny access with a 403
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
result = request.urlopen(req)
return result.code, f"{result.msg}"
except urllib.error.HTTPError as ex:
return ex.status, f"{ex}"
return ex.code, f"{ex}"
Loading

0 comments on commit c1684b8

Please sign in to comment.