v0.156.1
 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