1from __future__ import annotations
2
3import os
4from functools import cached_property
5from importlib import import_module
6from types import ModuleType
7from typing import TYPE_CHECKING
8
9from plain.exceptions import ImproperlyConfigured
10
11if TYPE_CHECKING:
12 from plain.packages.registry import PackagesRegistry
13
14_CONFIG_MODULE_NAME = "config"
15
16
17class PackageConfig:
18 """Class representing a Plain application and its configuration."""
19
20 package_label: str
21
22 def __init__(self, name: str):
23 # Full Python path to the application e.g. 'plain.admin.admin'.
24 self.name = name
25
26 # Reference to the Packages registry that holds this PackageConfig. Set by the
27 # registry when it registers the PackageConfig instance.
28 self.packages: PackagesRegistry | None = None
29
30 if not hasattr(self, "package_label"):
31 # Last component of the Python path to the application e.g. 'admin'.
32 # This value must be unique across a Plain project.
33 self.package_label = self.name.rpartition(".")[2]
34
35 if not self.package_label.isidentifier():
36 raise ImproperlyConfigured(
37 f"The app label '{self.package_label}' is not a valid Python identifier."
38 )
39
40 def __repr__(self) -> str:
41 return f"<{self.__class__.__name__}: {self.package_label}>"
42
43 @cached_property
44 def path(self) -> str:
45 # Filesystem path to the application directory e.g.
46 # '/path/to/admin'.
47 def _path_from_module(module: ModuleType) -> str:
48 """Attempt to determine app's filesystem path from its module."""
49 # See #21874 for extended discussion of the behavior of this method in
50 # various cases.
51 # Convert to list because __path__ may not support indexing.
52 paths = list(getattr(module, "__path__", []))
53 if len(paths) != 1:
54 filename = getattr(module, "__file__", None)
55 if filename is not None:
56 paths = [os.path.dirname(filename)]
57 else:
58 # For unknown reasons, sometimes the list returned by __path__
59 # contains duplicates that must be removed (#25246).
60 paths = list(set(paths))
61 if len(paths) > 1:
62 raise ImproperlyConfigured(
63 f"The app module {module!r} has multiple filesystem locations ({paths!r}); "
64 "you must configure this app with an PackageConfig subclass "
65 "with a 'path' class attribute."
66 )
67 elif not paths:
68 raise ImproperlyConfigured(
69 f"The app module {module!r} has no filesystem location, "
70 "you must configure this app with an PackageConfig subclass "
71 "with a 'path' class attribute."
72 )
73 return paths[0]
74
75 module = import_module(self.name)
76 return _path_from_module(module)
77
78 def ready(self) -> None:
79 """
80 Override this method in subclasses to run code when Plain starts.
81 """
82 return None