Skip to content

Commit

Permalink
Merge pull request #28 from hacettepeoyt/fix-config
Browse files Browse the repository at this point in the history
lint: add automatic linter to check config
  • Loading branch information
furkansimsekli authored Aug 19, 2024
2 parents 2da9249 + 901bfde commit 481b187
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 3 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
name: Lint

on:
pull_request:
push:
branches:
- 'master'

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v25
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: run lint scripts
run: |
for f in lint/lint_*.py; do
if ! python $f; then
echo "errors from $f ^^^^"
exit 1
fi
done
6 changes: 3 additions & 3 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,12 @@
};
PORT = lib.mkOption {
type = lib.types.port;
default = 48879;
description = "Port to listen the webhook on. Defaults to 48879.";
default = 51413;
description = "Port to listen the webhook on. Defaults to 51413.";
};
WEBHOOK_URL = lib.mkOption {
type = lib.types.str;
default = "https://${cfg.hostname}/${cfg.settings.TELEGRAM_API_KEY}";
default = "https://${cfg.hostname}";
description = "The URL for the webhook. Defaults to hostname + api key.";
};
BACKGROUND_COLORS = lib.mkOption {
Expand Down
126 changes: 126 additions & 0 deletions lint/lint_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/usr/bin/env python3

"""Ensures config defined config parameters in config.py are identical to the ones in the NixOS module."""

import ast
import json
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any


class NoDefault:
def __eq__(self, o: Any) -> bool:
return isinstance(o, NoDefault)

def __repr__(self) -> str:
return "@NO_DEFAULT@"


class ComplicatedDefault:
def __eq__(self, o: Any) -> bool:
return isinstance(o, ComplicatedDefault)

def __repr__(self) -> str:
return "@COMPLICATED_DEFAULT@"


@dataclass(eq=True, order=True)
class ConfigKey:
key: str
default: Any


def parse_config_keys(module: ast.Module) -> list[ConfigKey]:
keys: list[ConfigKey] = []

for child in ast.walk(module):
if isinstance(child, ast.Subscript):
# config[key]
if not isinstance(child.value, ast.Name) or child.value.id != "config":
continue

assert isinstance(child.slice, ast.Constant) and isinstance(child.slice.value, str)
keys.append(ConfigKey(child.slice.value, NoDefault()))
elif isinstance(child, ast.Call):
# config.get(key, ...)
if not isinstance(child.func, ast.Attribute) or child.func.attr != "get" or not isinstance(child.func.value, ast.Name) or child.func.value.id != "config":
continue

assert isinstance(child.args[0], ast.Constant) and isinstance(child.args[0].value, str)
if len(child.args) == 2:
# We have a default if we're here.
keys.append(ConfigKey(child.args[0].value, ast.literal_eval(child.args[1])))
else:
keys.append(ConfigKey(child.args[0].value, NoDefault()))

return keys


def parse_nix_keys(flake_path: Path) -> list[ConfigKey]:
obj: dict[str, Any] = json.loads(subprocess.check_output([
"nix", "eval", "--impure", "--json", "--expr", f'builtins.getFlake "{flake_path}"', "--apply",
"""flake: (
builtins.mapAttrs (name: value:
let eval = builtins.tryEval (value.default or { nodefault = true; });
in if eval.success then eval.value else { complicateddefault = true; }
) (
builtins.elemAt (
flake.outputs.nixosModules { lib = flake.inputs.nixpkgs.lib; config = throw "not full eval"; pkgs = {}; }).options.services.hu-cafeteria-bot.settings.type.getSubModules
0
).options
)"""
], encoding="utf-8"))

keys: list[ConfigKey] = []
for key, default in obj.items():
if isinstance(default, dict):
if default.get("nodefault"):
keys.append(ConfigKey(key, NoDefault()))
continue
elif default.get("complicateddefault"):
keys.append(ConfigKey(key, ComplicatedDefault()))
continue
keys.append(ConfigKey(key, default))

return keys


def main() -> bool:
config_path: Path = Path(__file__).parents[1] / "src" / "config.py"

config_keys: list[ConfigKey] = parse_config_keys(ast.parse(config_path.read_text()))
config_keys.sort(reverse=True)
config_key_set: set[str] = set(map(lambda c: c.key, config_keys))

nix_keys: list[ConfigKey] = parse_nix_keys(Path(__file__).parents[1])
nix_keys.sort(reverse=True)
nix_key_set: set[str] = set(map(lambda c: c.key, nix_keys))

if config_key_set != nix_key_set:
print("ERROR: Either config and nix keys are not same.")

if nix_key_set - config_key_set:
print(" Following keys are present in flake.nix but not in config.py:")
print(" - ", end="")
print(*(nix_key_set - config_key_set), sep=', ')

if config_key_set - nix_key_set:
print(" Following keys are present in config.py but not in flake.nix:")
print(" - ", end="")
print(*(config_key_set - nix_key_set), sep=', ')

return False

for (config_key, nix_key) in zip(config_keys, nix_keys):
if config_key.default != nix_key.default and nix_key.default != ComplicatedDefault():
print("ERROR: Default mismatch:", "key:", config_key.key, "config.py:", config_key.default, "flake.nix:", nix_key.default)
return False

return True


if __name__ == "__main__":
sys.exit(int(not main()))

0 comments on commit 481b187

Please sign in to comment.