c1f5bea9f7
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4m46s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m32s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 7s
Testy / Kontrola składni wszystkich warstw (push) Successful in 5s
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>
137 lines
4.5 KiB
Python
137 lines
4.5 KiB
Python
"""Most do modułów opcjonalnych — CELOWO nie zna ani jednej ich nazwy.
|
|
|
|
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.
|
|
|
|
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
|
|
|
|
|
|
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], ...]:
|
|
"""Pozycje do katalogu funkcji — jako surowe krotki, nie obiekty.
|
|
|
|
Surowe, bo katalog buduje `features`, a gdyby moduł importował stamtąd klasę,
|
|
powstałby cykl: features → most → moduł → features."""
|
|
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]:
|
|
out: dict[tuple[str, str], str] = {}
|
|
for wartosc in _zbierz("TRASY", []):
|
|
out.update(wartosc)
|
|
return out
|
|
|
|
|
|
def zasoby() -> dict[str, Any]:
|
|
out: dict[str, Any] = {}
|
|
for wartosc in _zbierz("ZASOBY", []):
|
|
out.update(wartosc)
|
|
return out
|
|
|
|
|
|
def akcje() -> dict[str, str]:
|
|
"""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ć 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ły dokładają 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):
|
|
"""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łó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łów — nakładane PRZED wspólnymi."""
|
|
return _katalogi("templates")
|
|
|
|
|
|
def katalog_zasobow() -> list[str]:
|
|
return _katalogi("static")
|