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

lint: add automatic linter to check config #28

Merged
merged 3 commits into from
Aug 19, 2024
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
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()))