feat: ж/д слой + cant_swim в скоринг + профили вне модели
Определение специфических рекомендаций (матрица профилей §8):
1. Ж/д слой (закрыт мёртвый railway ×2.5 у РАС):
- /api/v1/water/{case_id} отдаёт railway=rail как LineString
(без service/industrial/military веток), кэш общий v2;
- railway_warning «перекрыть/проверить немедленно» по профилям;
- SearchMap: Polyline слой ж/д (тёмно-красный), счётчики 💧/🚂.
2. cant_swim → профиль не_умеет_плавать (water ×3.0, без изменения
радиуса, critical_warning «обследовать водоёмы НЕМЕДЛЕННО»):
- раньше чекбокс влиял только на текст, в скоринге был пробел;
- derive в analyze._derive_profiles — работает и для closed_cases.
3. unmodeled_profiles: ДЦП/слабое зрение/слух — честная пометка
«вне поведенческой модели» с пояснением (vector_tasks B12:
профили без аналога не выдавать за учтённые); блок на фронте
в карточке здоровья.
Площадь воды: сферический эксцесс, проверен на квадрате 53° (744017 м²
vs 743272 точного). Тесты: 202 passed (новый test_cant_swim_profile).
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = (
|
||||
"fix_package_names",
|
||||
"install_lazy_importer",
|
||||
"set_deprecated_aliases",
|
||||
)
|
||||
|
||||
import ast
|
||||
import inspect
|
||||
import sys
|
||||
import warnings
|
||||
from importlib import import_module
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
|
||||
|
||||
def install_lazy_importer() -> bool:
|
||||
module_globals = sys._getframe(1).f_globals
|
||||
module_name = module_globals["__name__"]
|
||||
module_prefix = module_name + "."
|
||||
module = sys.modules[module_name]
|
||||
lazy_map, deprecated_aliases, submodule_names = _build_lazy_map(module)
|
||||
names = sorted(lazy_map)
|
||||
|
||||
# Delete symbols that are not part of the API
|
||||
del module_globals["TYPE_CHECKING"]
|
||||
del module_globals["install_lazy_importer"]
|
||||
|
||||
if not lazy_map and not deprecated_aliases:
|
||||
return False
|
||||
|
||||
def __getattr__(name: str) -> Any:
|
||||
if new_name := deprecated_aliases.get(name):
|
||||
emit_deprecation_warning(module_name, name, new_name)
|
||||
target_mod, target_attr = new_name.rsplit(".", 1)
|
||||
elif name in submodule_names:
|
||||
target_mod, target_attr = "." + name, ""
|
||||
else:
|
||||
try:
|
||||
target_mod, target_attr = lazy_map[name]
|
||||
except KeyError:
|
||||
raise AttributeError(
|
||||
f"module {module_name!r} has no attribute {name!r}"
|
||||
) from None
|
||||
|
||||
imported = import_module(target_mod, module_name)
|
||||
value = getattr(imported, target_attr) if target_attr else imported
|
||||
|
||||
# patch the module name to match
|
||||
if (
|
||||
getattr(value, "__module__", "").startswith(module_prefix)
|
||||
and name not in deprecated_aliases
|
||||
):
|
||||
value.__module__ = module_name
|
||||
|
||||
module_globals[name] = value
|
||||
return value
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
return names
|
||||
|
||||
module_globals["__dir__"] = __dir__
|
||||
module_globals["__getattr__"] = __getattr__
|
||||
module_globals.pop("fix_package_names", None)
|
||||
module_globals.pop("set_deprecated_aliases", None)
|
||||
return True
|
||||
|
||||
|
||||
def fix_package_names() -> None:
|
||||
module_globals = sys._getframe(1).f_globals
|
||||
module_prefix = module_globals["__name__"] + "."
|
||||
del module_globals[fix_package_names.__name__]
|
||||
for value in module_globals.values():
|
||||
if modname := getattr(value, "__module__", ""):
|
||||
if modname.startswith(module_prefix):
|
||||
parts = modname.split(".")
|
||||
value.__module__ = ".".join(
|
||||
part for part in parts if not part.startswith("_")
|
||||
)
|
||||
|
||||
|
||||
def emit_deprecation_warning(module_name: str, name: str, target: str) -> None:
|
||||
warnings.warn(
|
||||
f"The {module_name}.{name} alias is deprecated, use {target} instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
|
||||
|
||||
def set_deprecated_aliases(aliases: dict[str, str]) -> None:
|
||||
module_globals = sys._getframe(1).f_globals
|
||||
module_name = module_globals["__name__"]
|
||||
del module_globals[set_deprecated_aliases.__name__]
|
||||
|
||||
def __getattr__(name: str) -> Any:
|
||||
try:
|
||||
target = aliases[name]
|
||||
except KeyError:
|
||||
raise AttributeError(
|
||||
f"module {module_name!r} has no attribute {name!r}"
|
||||
) from None
|
||||
|
||||
emit_deprecation_warning(module_name, name, target)
|
||||
target_modname, attrname = target.rsplit(".", 1)
|
||||
module = import_module(target_modname)
|
||||
return getattr(module, attrname)
|
||||
|
||||
sys.modules[module_name].__dict__["__getattr__"] = __getattr__
|
||||
|
||||
|
||||
def _build_lazy_map(
|
||||
module: ModuleType,
|
||||
) -> tuple[dict[str, tuple[str, str]], dict[str, str], list[str]]:
|
||||
try:
|
||||
source = inspect.getsource(module)
|
||||
except OSError:
|
||||
return {}, {}, []
|
||||
|
||||
tree = compile(source, module.__file__ or "", "exec", ast.PyCF_ONLY_AST)
|
||||
assert isinstance(tree, ast.Module)
|
||||
out: dict[str, tuple[str, str]] = {}
|
||||
deprecated_aliases: dict[str, str] = {}
|
||||
submodule_names: list[str] = []
|
||||
|
||||
for node in tree.body:
|
||||
if not isinstance(node, ast.If) or not _is_type_checking_block(node.test):
|
||||
continue
|
||||
|
||||
for stmt in node.body:
|
||||
match stmt:
|
||||
case ast.ImportFrom():
|
||||
if stmt.module is None:
|
||||
submodule_names.extend(alias.name for alias in stmt.names)
|
||||
else:
|
||||
base = "." * stmt.level + (stmt.module or "")
|
||||
for alias in stmt.names:
|
||||
if alias.name == "*":
|
||||
raise RuntimeError("star imports not supported")
|
||||
|
||||
exported = alias.asname or alias.name
|
||||
out[exported] = (base, alias.name)
|
||||
case ast.Expr() if isinstance(stmt.value, ast.Call):
|
||||
call = stmt.value
|
||||
if (
|
||||
isinstance(call.func, ast.Name)
|
||||
and call.func.id == "set_deprecated_aliases"
|
||||
):
|
||||
arg0 = call.args[0]
|
||||
assert isinstance(arg0, ast.Dict)
|
||||
for key, value in zip(arg0.keys, arg0.values, strict=True):
|
||||
assert isinstance(key, ast.Constant)
|
||||
assert isinstance(key.value, str)
|
||||
assert isinstance(value, ast.Constant)
|
||||
assert isinstance(value.value, str)
|
||||
deprecated_aliases[key.value] = value.value
|
||||
|
||||
return out, deprecated_aliases, submodule_names
|
||||
|
||||
|
||||
def _is_type_checking_block(test: ast.AST) -> bool:
|
||||
if not isinstance(test, ast.BoolOp):
|
||||
return False
|
||||
|
||||
subtest = test.values[0]
|
||||
match subtest:
|
||||
case ast.Name():
|
||||
return subtest.id == "TYPE_CHECKING"
|
||||
case ast.Attribute():
|
||||
return (
|
||||
isinstance(subtest.value, ast.Name)
|
||||
and subtest.value.id == "typing"
|
||||
and subtest.attr == "TYPE_CHECKING"
|
||||
)
|
||||
case _:
|
||||
return False
|
||||
Reference in New Issue
Block a user