What you pay for, what Discord drops, and pixiv switched off #251
+23
-12
@@ -282,24 +282,32 @@ def _first_party_imports(path: Path) -> set[str]:
|
||||
"""Every `backend.app.*` module this file imports, as a dotted path
|
||||
relative to `backend/app` — absolute and relative forms both."""
|
||||
tree = ast.parse(path.read_text())
|
||||
here = path.relative_to(_APP).with_suffix("").parts
|
||||
if here and here[-1] == "__init__":
|
||||
# A package's `__init__` IS the package, so `from .x import y` inside it
|
||||
# resolves one level shallower than the file path suggests. Getting this
|
||||
# wrong silently under-resolves every relative import in every package
|
||||
# and would make the guard below unable to fail.
|
||||
here = here[:-1]
|
||||
# The dotted parts of the package CONTAINING this module. `parts[:-1]` is
|
||||
# right for both forms without a special case: `services/foo.py` drops
|
||||
# `foo` to leave `services`, and `api/__init__.py` drops `__init__` to
|
||||
# leave `api` — which is exactly what `from .` means inside each.
|
||||
pkg = path.relative_to(_APP).with_suffix("").parts[:-1]
|
||||
out: set[str] = set()
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom):
|
||||
if node.level:
|
||||
# `from ..models import X` -> walk up from this module's package
|
||||
base = list(here[: len(here) - node.level])
|
||||
mod = base + (node.module.split(".") if node.module else [])
|
||||
# `from ..models import X` -> walk up from the containing
|
||||
# package. Clamped at 0: a level that climbs past `backend.app`
|
||||
# leaves this tree, and an unclamped negative index would wrap
|
||||
# and silently resolve to the wrong module.
|
||||
up = max(len(pkg) - (node.level - 1), 0)
|
||||
mod = list(pkg[:up]) + (node.module.split(".") if node.module else [])
|
||||
elif node.module and node.module.startswith("backend.app."):
|
||||
mod = node.module[len("backend.app."):].split(".")
|
||||
else:
|
||||
continue
|
||||
if not mod:
|
||||
# `from . import x` in a module sitting directly under
|
||||
# `backend/app` (celery_app.py does this): the package is the
|
||||
# app root, so the alias alone is the module's dotted name.
|
||||
for alias in node.names:
|
||||
out.add(alias.name)
|
||||
continue
|
||||
out.add(".".join(mod))
|
||||
# `from .membership_roster import x` and
|
||||
# `from . import membership_roster` must both resolve to the module.
|
||||
@@ -320,8 +328,11 @@ def _reachable_from(roots: list[str]) -> set[str]:
|
||||
if mod in seen:
|
||||
continue
|
||||
seen.add(mod)
|
||||
for candidate in (_APP / Path(*mod.split(".")) / "__init__.py",
|
||||
_APP / Path(*mod.split(".")).with_suffix(".py")):
|
||||
parts = [p for p in mod.split(".") if p]
|
||||
if not parts:
|
||||
continue
|
||||
for candidate in (_APP.joinpath(*parts) / "__init__.py",
|
||||
_APP.joinpath(*parts).with_suffix(".py")):
|
||||
if candidate.is_file():
|
||||
queue.extend(_first_party_imports(candidate) - seen)
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user