[project] name = "diet-guard" version = "1.0.0" description = "Tkinter/systemd log-to-unlock meal gate with calorie/macro budget tracking" requires-python = ">=3.14" dependencies = [ "gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.12.0#subdirectory=gatelock", "crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.2#subdirectory=crdt-sync", "freedays @ git+https://github.com/kuhyx/utils@freedays-v0.4.0#subdirectory=freedays", "holds @ git+https://github.com/kuhyx/utils@holds-v0.1.0#subdirectory=holds", "sickdays @ git+https://github.com/kuhyx/utils@sickdays-v0.4.0#subdirectory=sickdays", "defusedxml==0.7.1", "requests==2.34.2", ] # Optional extras. The `mcp` group is ONLY needed by the MCP server # (diet_guard._mcp), which Claude Code spawns via a dedicated venv (see # scripts/setup_mcp.sh / .mcp.json). It is deliberately NOT a core dependency # so the systemd/CLI system-python path stays dependency-light and never has # to import the MCP SDK. [project.optional-dependencies] mcp = ["mcp==2.3.0"] # Without this, setuptools' flat-layout auto-discovery treats the Flutter # app/ directory as a second top-level package and refuses to build. [tool.setuptools.packages.find] include = ["diet_guard*"] [tool.ruff] target-version = "py314" include = ["*.py", "**/*.py"] exclude = [".git", ".venv", "__pycache__", "build", "dist", ".eggs"] [tool.ruff.lint] select = ["ALL"] ignore = [ "D203", "D213", "COM812", "ISC001", "S603", # Stabilised in ruff 0.17; this personal repo carries no per-file copyright headers. "CPY001", ] fixable = ["ALL"] unfixable = [] [tool.ruff.lint.per-file-ignores] "**/tests/**/*.py" = ["ARG", "D", "PLC0415", "PLR2004", "S101", "S107", "SLF001"] "**/test_*.py" = ["ARG", "D", "PLC0415", "PLR2004", "S101", "S107", "SLF001"] [tool.ruff.lint.pydocstyle] convention = "google" [tool.ruff.lint.isort] force-single-line = false force-sort-within-sections = true known-first-party = ["diet_guard"] [tool.ruff.lint.flake8-quotes] docstring-quotes = "double" inline-quotes = "double" [tool.ruff.lint.flake8-tidy-imports] ban-relative-imports = "all" [tool.ruff.format] quote-style = "double" indent-style = "space" skip-magic-trailing-comma = false line-ending = "auto" docstring-code-format = true [tool.mypy] python_version = "3.14" strict = true warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true disallow_incomplete_defs = true check_untyped_defs = true disallow_untyped_decorators = true no_implicit_optional = true warn_redundant_casts = true warn_unused_ignores = true warn_no_return = true warn_unreachable = true disallow_any_unimported = true disallow_any_explicit = false disallow_any_generics = true disallow_subclassing_any = true strict_equality = true extra_checks = true ignore_missing_imports = true show_error_codes = true color_output = true exclude = [".venv/"] [tool.pylint.main] # The repo root on the path, so a file outside the package (scripts/) can be # linted on its own. pylint otherwise infers the path from the files it is # given, so a script resolved its package imports only when a package file # happened to be in the same batch -- passing or failing by what else was # staged. init-hook = "import sys; sys.path.insert(0, '.')" analyse-fallback-blocks = true persistent = true jobs = 0 py-version = "3.14" # "tests" is a basename match: test suites intentionally use patterns # (protected-access, magic-value comparisons) that don't apply to source # code, matching testsAndMisc's pylint scope (tests are linted by ruff, not # pylint there either). ignore = [".venv", "__pycache__", "tests"] ignore-patterns = [".*\\.pyi$"] [tool.pylint.messages_control] # Enable all checks by disabling disable enable = "all" # Globally disabled checks. Each is either a stylistic preference that conflicts # with deliberate, clearer code, or a structural false positive that cannot be # rewritten without harming readability. Everything else stays at max strictness. disable = [ # use-implicit-booleaness family (C1803/C1804/C1805): pylint wants # `not x` / `not s` instead of `x == 0` / `s == ""`. Explicit comparisons # against 0 and "" state numeric/string intent more clearly than truthiness # (and are not equivalent when the value may be None), so we keep them. "use-implicit-booleaness-not-comparison", "use-implicit-booleaness-not-comparison-to-string", "use-implicit-booleaness-not-comparison-to-zero", # consider-using-with (R1732): several subprocess.Popen calls are # intentionally fire-and-forget — the process must outlive the calling # scope and is polled/killed later, so a `with` block is wrong here. "consider-using-with", ] [tool.pylint.design] min-public-methods = 0 max-module-lines = 1000 max-attributes = 10 [tool.pylint.similarities] # _foodbank's BankRecord and _mcp's response dict both list the same five # nutrition fields (kcal/protein_g/carbs_g/fat_g/grams) because they mirror the # same domain schema, then diverge (desc/count vs source). Raise the threshold # so pylint doesn't flag this intentional structural overlap as duplicate-code. min-similarity-lines = 8 [tool.pylint.typecheck] generated-members = [ ".*\\.assert_called_once_with", ".*\\.assert_called_once", ".*\\.assert_called", ".*\\.assert_not_called", ".*\\.assert_any_call", ".*\\.call_args", ".*\\.call_args_list", ".*\\.call_count", ] [tool.bandit] exclude_dirs = ["tests", ".venv"] [tool.pytest.ini_options] testpaths = ["diet_guard/tests"] python_files = ["test_*.py", "*_test.py"] python_classes = ["Test*"] python_functions = ["test_*"] addopts = [ "-v", "--strict-markers", "--strict-config", "-ra", "--cov=diet_guard", "--cov-branch", "--cov-report=term-missing", "--cov-report=lcov", ] filterwarnings = [ "error", "ignore::DeprecationWarning", "default::pytest.PytestUnraisableExceptionWarning", ] # pytest-xvfb gives every pytest process -- each xdist worker included -- its # own Xvfb. Keyboard focus is global to an X display, so real-Tk tests from # several workers sharing one xvfb-run display stole each other's focus # (focus-ring assertions failed) and Tk segfaulted in focus_force(): 5 of 2760 # stressed runs failed shared, 0 of 4600 with a display per worker. xvfb_width = "1600" xvfb_height = "1200" xvfb_colordepth = "24" [tool.coverage.run] source = ["diet_guard"] branch = true omit = [ "*/__pycache__/*", "*/tests/*", "*/.venv/*", ] [tool.coverage.report] fail_under = 100 show_missing = true skip_covered = false exclude_lines = [ "pragma: no cover", "raise NotImplementedError", "raise AssertionError", "if TYPE_CHECKING:", 'if __name__ == "__main__":', ] partial_branches = ["pragma: no branch"]