1import importlib.metadata
2import sys
3from importlib.metadata import entry_points
4from pathlib import Path
5from typing import Self
6
7from plain.logs.configure import configure_logging
8from plain.packages import packages_registry
9
10from .secret import Secret
11from .user_settings import Settings
12
13try:
14 __version__ = importlib.metadata.version("plain")
15except importlib.metadata.PackageNotFoundError:
16 __version__ = "dev"
17
18
19# Made available without setup or settings
20APP_PATH = Path.cwd() / "app"
21PLAIN_TEMP_PATH = Path.cwd() / ".plain"
22
23# from plain.runtime import settings
24settings = Settings()
25
26_is_setup_complete = False
27
28
29class AppPathNotFound(RuntimeError):
30 pass
31
32
33class SetupError(RuntimeError):
34 pass
35
36
37def setup() -> None:
38 """
39 Configure the settings (this happens as a side effect of accessing the
40 first setting), configure logging and populate the app registry.
41 """
42 global _is_setup_complete
43
44 if _is_setup_complete:
45 raise SetupError(
46 "Plain runtime is already set up. You can only call `setup()` once."
47 )
48 else:
49 # Make sure we don't call setup() again
50 _is_setup_complete = True
51
52 # Packages can hook into the setup process through an entrypoint.
53 for entry_point in entry_points().select(group="plain.setup"):
54 entry_point.load()()
55
56 if not APP_PATH.exists():
57 raise AppPathNotFound(
58 "No app directory found. Are you sure you're in a Plain project?"
59 )
60
61 # Automatically put the project dir on the Python path
62 # which doesn't otherwise happen when you run `plain` commands.
63 # This makes "app.<module>" imports and relative imports work.
64 if APP_PATH.parent.as_posix() not in sys.path:
65 sys.path.insert(0, APP_PATH.parent.as_posix())
66
67 configure_logging(
68 plain_log_level=settings.FRAMEWORK_LOG_LEVEL,
69 app_log_level=settings.LOG_LEVEL,
70 app_log_format=settings.LOG_FORMAT,
71 log_stream=settings.LOG_STREAM,
72 )
73
74 packages_registry.populate(settings.INSTALLED_PACKAGES)
75
76
77class SettingsReference(str):
78 """
79 String subclass which references a current settings value. It's treated as
80 the value in memory but serializes to a settings.NAME attribute reference.
81 """
82
83 def __new__(self, setting_name: str) -> Self:
84 value = getattr(settings, setting_name)
85 return str.__new__(self, value)
86
87 def __init__(self, setting_name: str):
88 self.setting_name = setting_name
89
90
91__all__ = [
92 "APP_PATH",
93 "AppPathNotFound",
94 "PLAIN_TEMP_PATH",
95 "Secret",
96 "SetupError",
97 "SettingsReference",
98 "__version__",
99 "settings",
100 "setup",
101]