astrololo: ekrany i eksport jako moduły, katalog z rejestracji (3/5)
Testy / Testy warstwy logicznej (silnik) (pull_request) Failing after 4s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Failing after 3s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Failing after 3s
Testy / Testy astrodemo (pull_request) Failing after 3s
Testy / Build obrazu silnika B (swisseph) (pull_request) Failing after 2s
Testy / Kontrola składni wszystkich warstw (pull_request) Failing after 3s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Failing after 3s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Failing after 3s
Testy / Testy astrodemo (push) Failing after 3s
Testy / Build obrazu silnika B (swisseph) (push) Failing after 2s
Testy / Kontrola składni wszystkich warstw (push) Failing after 3s
build / build (push) Successful in 9s

Trzeci z pięciu kroków. Zmieniłem jego zakres wobec planu i warto wiedzieć
dlaczego: pierwotnie miała to być deduplikacja pięciu kopii link_crypto.py, ale
po zrobieniu PR 2 widać, że astroklienta blokuje co innego — ekrany, których nie
ma mieć, siedzą wewnątrz jednego main.py. Deduplikacja kryptografii jest realnym
długiem, ale niczego nie blokuje.

PODZIAŁ. main.py (1023 linie) rozpadł się na podstawa.py (wspólne obiekty
i pomocnicy), jedenaście modułów w app/ekrany/ i main.py, który jest już samym
ZŁOŻENIEM: lista importów JEST definicją produktu. Podział zrobiony mechanicznie,
z osobnym sprawdzeniem, że żadna sekcja nie wołała pomocnika z innej (nie wołała).

KATALOG Z REJESTRACJI. Dotąd wszystkie funkcje były wypisane w features.py, więc
obraz produktu, który części z nich nie ma, i tak niósł ich nazwy — spis funkcji,
których nie ma jak włączyć. Teraz ekran zgłasza siebie, swoje trasy i swoje
zasoby przy imporcie własnego modułu, a features.py nie wymienia ani jednego
ekranu. Kolejność w nawigacji jest jawna (`kolejnosc`), żeby nie rządziła nią
kolejność importów.

To samo dotyczy nawigacji administratora: odsyłacz do ekranu kont był wpisany na
sztywno w base.html, więc w węższym produkcie zostawał martwy link i nazwa
ekranu, którego nie ma.

EKSPORT JAKO MODUŁ. Zgodnie z ustaleniem eksport jest funkcją administracyjną,
więc musi dać się usunąć. app/moduly/eksport/ zabiera arkusz, trasę PDF-a i akcję
formularza. „Można, ale nie temu kontu" i „nie ma takiej możliwości" to dwie
różne gwarancje, a eksport wynosi najwięcej treści baz naraz.

MOST ODKRYWA MODUŁY. Skoro modułów jest więcej niż jeden, most nie może ich znać
z nazwy — nazwa nieobecnego modułu jechałaby do obrazu, w którym go nie ma.
Przechodzi więc po podkatalogach app/moduly/ i pyta każdy, co wnosi. Katalog
generowania przeniesiony z app/dodatki na app/moduly/dodatki.

ZNALEZIONE PRZY OKAZJI. Po wydzieleniu eksportu okazało się, że jego ścieżki
SZCZĘŚLIWEJ nie sprawdzał żaden test — badano wyłącznie odmowę dla konta bez
uprawnienia. Moduł dostaje zależności z wywołania montującego, więc brak jednej
z nich wyszedłby dopiero przy pierwszym kliknięciu. Dopisany test funkcjonalny
(realny arkusz, sprawdzany aż do nagłówka ZIP-a) i brakujące zależności.

Cztery komentarze w plikach współdzielonych wymieniały zakładkę „Skompiluj",
w tym wheelzoom.js ze wzmianką o „przyszłej zakładce" — to samo zgłoszenie, które
audyt podnosił wcześniej.

TEST ZŁOŻENIA. Buduje węższy produkt NAPRAWDĘ: kopiuje drzewo, usuwa cztery
ekrany i oba moduły, uruchamia aplikację w OSOBNYM PROCESIE (importy są
zapamiętywane, więc sprawdzanie tego w procesie, który moduł już zaimportował,
dawałoby wynik fałszywie pozytywny) i sprawdza, że wstaje, że zachowane ekrany
oddają 200, że usunięte oddają 404 (nie 403 i nie 500), że katalog opisuje ten
obraz, i że w nawigacji nie ma martwych odsyłaczy.

Napisałem najpierw ostrzejszy test — „nazwa ekranu nie pada poza jego modułem" —
i go wyrzuciłem: zgłaszał wzmianki o Horoskopie w plikach współdzielonych, choć
astroklient Horoskop MA. Ślad ma znaczenie wyłącznie wobec konkretnego złożenia,
więc sprawdzenie należy do produktu, nie do mechanizmu.

Testy: presentation 368, logic 342, data 37, render 41.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit was merged in pull request #81.
This commit is contained in:
2026-08-26 13:50:34 +02:00
parent 62d6f9d4d5
commit fd79513ce2
64 changed files with 1799 additions and 1205 deletions
+93 -43
View File
@@ -1,23 +1,44 @@
"""Most do modułów opcjonalnych — CELOWO nic o nich nie wie.
"""Most do modułów opcjonalnych — CELOWO nie zna ani jednej ich nazwy.
Ten plik jest w każdym obrazie, także tam, gdzie żadnego modułu nie ma. Dlatego
nie wolno w nim wymienić z nazwy ani jednej funkcji, którą moduł wnosi: nazwa
w moście byłaby dokładnie tym śladem, którego wydzielanie ma się pozbyć.
Ten plik jest w KAŻDYM obrazie, także tam, gdzie nie ma żadnego modułu. Gdyby
wymieniał moduły z nazwy, nazwa nieobecnej funkcji i tak jechałaby do obrazu,
w którym tej funkcji nie ma — czyli dokładnie ten ślad, którego całe wydzielanie
ma się pozbyć. Dlatego moduły są ODKRYWANE: most przechodzi po podkatalogach
`app/moduly/` i pyta każdy, co wnosi.
Most odpowiada wyłącznie na pytanie „czy coś jest podpięte i co wnosi". Gdy nie
ma nic, wszystkie odpowiedzi są puste, a aplikacja zachowuje się tak, jakby
takiego pojęcia nigdy nie było — nie jak aplikacja z wyłączoną funkcją.
Gdy nie ma żadnego, wszystkie odpowiedzi są puste, a aplikacja zachowuje się
tak, jakby takiego pojęcia nigdy nie było — nie jak aplikacja z wyłączoną
funkcją.
"""
from __future__ import annotations
import importlib
import pkgutil
from typing import Any
try: # pragma: no cover - zależy od obrazu
from app import dodatki as _modul
except ImportError: # obraz zbudowany bez tego katalogu
_modul = None # type: ignore[assignment]
DOSTEPNY = _modul is not None
def _znalezione() -> list[Any]:
"""Moduły obecne w tym obrazie, w kolejności alfabetycznej katalogów.
Kolejność jest stała, żeby dwa obrazy z tym samym zestawem zachowywały się
identycznie — inaczej wynik zależałby od kolejności odczytu katalogu."""
try:
paczka = importlib.import_module("app.moduly")
except ImportError:
return []
znalezione = []
for info in sorted(pkgutil.iter_modules(paczka.__path__), key=lambda i: i.name):
znalezione.append(importlib.import_module(f"app.moduly.{info.name}"))
return znalezione
_MODULY = _znalezione()
DOSTEPNE = bool(_MODULY)
def _zbierz(nazwa: str, domyslne):
"""Wartość atrybutu z każdego modułu, który go ma."""
return [getattr(m, nazwa) for m in _MODULY if hasattr(m, nazwa)] or domyslne
def funkcje() -> tuple[tuple[str, str, str, str], ...]:
@@ -25,62 +46,91 @@ def funkcje() -> tuple[tuple[str, str, str, str], ...]:
Surowe, bo katalog buduje `features`, a gdyby moduł importował stamtąd klasę,
powstałby cykl: features → most → moduł → features."""
return _modul.FUNKCJA if _modul else ()
out: list[tuple[str, str, str, str]] = []
for wartosc in _zbierz("FUNKCJA", []):
out.extend(wartosc)
return tuple(out)
def trasy() -> dict[tuple[str, str], str]:
return _modul.TRASY if _modul else {}
out: dict[tuple[str, str], str] = {}
for wartosc in _zbierz("TRASY", []):
out.update(wartosc)
return out
def zasoby() -> dict[str, Any]:
return _modul.ZASOBY if _modul else {}
out: dict[str, Any] = {}
for wartosc in _zbierz("ZASOBY", []):
out.update(wartosc)
return out
def akcje() -> dict[str, str]:
"""Akcje formularza wnoszone przez moduł: nazwa → wymagane uprawnienie."""
return _modul.AKCJE if _modul else {}
"""Akcje formularza wnoszone przez moduły: nazwa → wymagane uprawnienie."""
out: dict[str, str] = {}
for wartosc in _zbierz("AKCJE", []):
out.update(wartosc)
return out
def pola_formularza():
"""Zależność FastAPI deklarująca dodatkowe pola formularza.
Pola muszą być zadeklarowane w SYGNATURZE, żeby FastAPI je odczytało, a
sygnatura wspólnego handlera nie może ich wymieniać. Zależność rozwiązuje
jedno i drugie: moduł deklaruje własne pola u siebie, a handler wie tylko,
że dostaje słownik."""
if _modul:
return _modul.pola
return lambda: {}
Pola muszą być w SYGNATURZE, żeby FastAPI je odczytało, a sygnatura wspólnego
handlera nie może ich wymieniać. Zależność przenosi deklarację do modułu.
Gdy pola deklaruje więcej niż jeden moduł, trzeba je złożyć — a składanie
sygnatur w locie to kod, który psuje się cicho. Póki nikt tego nie potrzebuje,
mówimy o tym wprost przy starcie, zamiast budować machinerię na zapas.
"""
deklarujace = _zbierz("pola", [])
if not deklarujace:
return lambda: {}
if len(deklarujace) > 1:
raise RuntimeError(
"Więcej niż jeden moduł deklaruje pola formularza. Trzeba je złożyć "
"w jedną zależność — patrz komentarz w app/rozszerzenia.py.")
return deklarujace[0]
def kontekst_szablonu(request) -> dict:
"""Co moduł dokłada do kontekstu szablonu. Bez modułu — nic."""
return _modul.katalog_dla(request) if _modul else {}
"""Co moduły dokłada do kontekstu szablonu. Bez modułów — nic."""
out: dict = {}
for funkcja in _zbierz("katalog_dla", []):
out.update(funkcja(request))
return out
def obsluz(akcja: str, **dane) -> dict | None:
"""Wykonanie akcji modułu. None = to nie jest jego akcja."""
return _modul.obsluz(akcja, **dane) if _modul else None
def obsluz(akcja: str, **dane):
"""Wykonanie akcji modułu. None = żaden się do niej nie przyznaje.
Moduł może oddać słownik (trafi do kontekstu szablonu) albo gotową
odpowiedź HTTP — na przykład plik do pobrania, który nie jest stroną."""
for funkcja in _zbierz("obsluz", []):
wynik = funkcja(akcja, **dane)
if wynik is not None:
return wynik
return None
def zamontuj(app, **zaleznosci) -> None:
"""Podpięcie tras modułu i przekazanie mu tego, czego potrzebuje."""
if _modul:
_modul.zamontuj(app, **zaleznosci)
"""Podpięcie tras modułów i przekazanie im tego, czego potrzebują."""
for funkcja in _zbierz("zamontuj", []):
funkcja(app, **zaleznosci)
def _katalogi(nazwa: str) -> list[str]:
import pathlib
return [str(pathlib.Path(m.__file__).parent / nazwa) for m in _MODULY
if (pathlib.Path(m.__file__).parent / nazwa).is_dir()]
def katalog_szablonow() -> list[str]:
"""Katalogi szablonów modułu — nakładane PRZED wspólnymi."""
if not _modul:
return []
import pathlib
return [str(pathlib.Path(_modul.__file__).parent / "templates")]
"""Katalogi szablonów modułów — nakładane PRZED wspólnymi."""
return _katalogi("templates")
def katalog_zasobow() -> list[str]:
if not _modul:
return []
import pathlib
return [str(pathlib.Path(_modul.__file__).parent / "static")]
return _katalogi("static")