Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: type checking #4

Merged
merged 3 commits into from
May 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,23 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3

- uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Setup Poetry
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}

- name: Install Dependencies
run: poetry install --extras all
- name: Linting
run: poetry run pylint typer_config/
run: poetry install --all-extras

- name: Linting and Type checking
run: |
poetry run pylint typer_config/
poetry run mypy typer_config/

- name: Testing
run: poetry run pytest
73 changes: 72 additions & 1 deletion poetry.lock

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

3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ pylint = "^2.17.3"
black = "^23.3.0"
isort = "^5.12.0"
pytest = "^7.3.1"
mypy = "^1.2.0"
types-toml = "^0.10.8.6"
types-pyyaml = "^6.0.12.9"

[build-system]
requires = ["poetry-core"]
Expand Down
6 changes: 3 additions & 3 deletions tests/test_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,21 @@ def test_simple_example(simple_app):

result = RUNNER.invoke(_app, ["--config", conf])

assert result.exit_code == 0, f"Loading failed for {conf}"
assert result.exit_code == 0, f"Loading failed for {conf}\n\n{result.stdout}"
assert (
result.stdout.strip() == "things nothing stuff"
), f"Unexpected output for {conf}"

result = RUNNER.invoke(_app, ["--config", conf, "others"])

assert result.exit_code == 0, f"Loading failed for {conf}"
assert result.exit_code == 0, f"Loading failed for {conf}\n\n{result.stdout}"
assert (
result.stdout.strip() == "things nothing others"
), f"Unexpected output for {conf}"

result = RUNNER.invoke(_app, ["--config", conf, "--opt1", "people"])

assert result.exit_code == 0, f"Loading failed for {conf}"
assert result.exit_code == 0, f"Loading failed for {conf}\n\n{result.stdout}"
assert (
result.stdout.strip() == "people nothing stuff"
), f"Unexpected output for {conf}"
23 changes: 11 additions & 12 deletions typer_config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,41 @@
Typer Configuration Utilities
"""

from typing import Any, Callable, Dict

import typer

from .loaders import json_loader, toml_loader, yaml_loader
from .types import ConfigParameterCallback, Loader, ParameterValue


def conf_callback_factory(
loader: Callable[[Any], Dict[str, Any]]
) -> Callable[[typer.Context, typer.CallbackParam, Any], Any]:
def conf_callback_factory(loader: Loader) -> ConfigParameterCallback:
"""Configuration callback factory

Parameters
----------
loader : Callable[[Any], Dict[str, Any]]
loader : Loader
Loader function that takes the value passed to the typer CLI and
returns a dictionary that is applied to the click context's default map.

Returns
-------
Callable[[typer.Context, typer.CallbackParam, Any], Any]
ConfigParameterCallback
Configuration callback function.
"""

def _callback(ctx: typer.Context, param: typer.CallbackParam, value: Any) -> Any:
def _callback(
ctx: typer.Context, param: typer.CallbackParam, value: ParameterValue
) -> ParameterValue:
try:
conf = loader(value) # Load config file
ctx.default_map = ctx.default_map or {} # Initialize the default map
ctx.default_map.update(conf) # Merge the config Dict into default_map
except Exception as ex:
raise typer.BadParameter(value, ctx=ctx, param=param) from ex
raise typer.BadParameter(str(ex), ctx=ctx, param=param) from ex
return value

return _callback


yaml_conf_callback = conf_callback_factory(yaml_loader)
json_conf_callback = conf_callback_factory(json_loader)
toml_conf_callback = conf_callback_factory(toml_loader)
yaml_conf_callback: ConfigParameterCallback = conf_callback_factory(yaml_loader)
json_conf_callback: ConfigParameterCallback = conf_callback_factory(json_loader)
toml_conf_callback: ConfigParameterCallback = conf_callback_factory(toml_loader)
61 changes: 40 additions & 21 deletions typer_config/loaders.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,45 @@
"""
Configuration File Loaders.

These loaders must follow the signature: Callable[[Any], Dict[str, Any]]
These loaders must implement the interface:
typer_config.types.Loader = Callable[[Any], Dict[str, Any]]
"""

import json
from typing import Any, Dict
import sys

from .types import ConfDict

USING_TOMLLIB = False
TOML_MISSING = True
YAML_MISSING = True

try:
# Only available for python>=3.11
import tomllib as toml

if sys.version_info >= (3, 11):
import tomllib # type: ignore

TOML_MISSING = False
USING_TOMLLIB = True
except ImportError:
else:
try:
# Third-party toml parsing library
import toml

TOML_MISSING = False

except ImportError:
toml = None
pass


try:
import yaml

YAML_MISSING = False
except ImportError:
yaml = None
pass


# pylint: disable-next=unused-argument
def dummy_loader(path: str) -> Dict[str, Any]:
def dummy_loader(path: str) -> ConfDict:
"""Dummy loader to show the required interface.

Parameters
Expand All @@ -38,13 +49,13 @@ def dummy_loader(path: str) -> Dict[str, Any]:

Returns
-------
Dict
ConfDict
dictionary loaded from file
"""
return {}


def yaml_loader(path: str) -> Dict[str, Any]:
def yaml_loader(path: str) -> ConfDict:
"""YAML file loader

Parameters
Expand All @@ -54,17 +65,20 @@ def yaml_loader(path: str) -> Dict[str, Any]:

Returns
-------
Dict
ConfDict
dictionary loaded from file
"""

if YAML_MISSING:
raise ModuleNotFoundError("Please install the pyyaml library.")

with open(path, "r", encoding="utf-8") as _file:
conf = yaml.safe_load(_file)
conf: ConfDict = yaml.safe_load(_file)

return conf


def json_loader(path: str) -> Dict[str, Any]:
def json_loader(path: str) -> ConfDict:
"""JSON file loader

Parameters
Expand All @@ -74,17 +88,17 @@ def json_loader(path: str) -> Dict[str, Any]:

Returns
-------
Dict
ConfDict
dictionary loaded from file
"""

with open(path, "r", encoding="utf-8") as _file:
conf = json.load(_file)
conf: ConfDict = json.load(_file)

return conf


def toml_loader(path: str) -> Dict[str, Any]:
def toml_loader(path: str) -> ConfDict:
"""TOML file loader

Parameters
Expand All @@ -94,15 +108,20 @@ def toml_loader(path: str) -> Dict[str, Any]:

Returns
-------
Dict
ConfDict
dictionary loaded from file
"""

if TOML_MISSING:
raise ModuleNotFoundError("Please install the toml library.")

conf: ConfDict = {}

if USING_TOMLLIB:
with open(path, "rb") as _file:
conf = toml.load(_file)
conf = tomllib.load(_file) # type: ignore
else:
with open(path, "r", encoding="utf-8") as _file:
conf = toml.load(_file)
conf = toml.load(_file) # type: ignore

return conf
25 changes: 25 additions & 0 deletions typer_config/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""
Data and Function types.
"""

import sys
from typing import Any, Callable, Dict

from typer import CallbackParam as typer_CallbackParam
from typer import Context as typer_Context

# Handle TypeAlias based on python version
if sys.version_info < (3, 10):
from typing_extensions import TypeAlias
else:
from typing import TypeAlias

# Data types
ConfDict: TypeAlias = Dict[str, Any]
ParameterValue: TypeAlias = Any

# Function types
Loader: TypeAlias = Callable[[Any], ConfDict]
ConfigParameterCallback: TypeAlias = Callable[
[typer_Context, typer_CallbackParam, ParameterValue], ParameterValue
]