Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/invoke/loader.py: 28%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import os
2import sys
3from importlib.machinery import ModuleSpec
4from importlib.util import module_from_spec, spec_from_file_location
5from pathlib import Path
6from types import ModuleType
7from typing import Any, Optional, Tuple
9from . import Config
10from .exceptions import CollectionNotFound
11from .util import debug
14class Loader:
15 """
16 Abstract class defining how to find/import a session's base `.Collection`.
18 .. versionadded:: 1.0
19 """
21 def __init__(self, config: Optional["Config"] = None) -> None:
22 """
23 Set up a new loader with some `.Config`.
25 :param config:
26 An explicit `.Config` to use; it is referenced for loading-related
27 config options. Defaults to an anonymous ``Config()`` if none is
28 given.
29 """
30 if config is None:
31 config = Config()
32 self.config = config
34 def find(self, name: str) -> Optional[ModuleSpec]:
35 """
36 Implementation-specific finder method seeking collection ``name``.
38 Must return a ModuleSpec valid for use by `importlib`, which is
39 typically a name string followed by the contents of the 3-tuple
40 returned by `importlib.module_from_spec` (``name``, ``loader``,
41 ``origin``.)
43 For a sample implementation, see `.FilesystemLoader`.
45 .. versionadded:: 1.0
46 """
47 raise NotImplementedError
49 def load(self, name: Optional[str] = None) -> Tuple[ModuleType, str]:
50 """
51 Load and return collection module identified by ``name``.
53 This method requires a working implementation of `.find` in order to
54 function.
56 In addition to importing the named module, it will add the module's
57 parent directory to the front of `sys.path` to provide normal Python
58 import behavior (i.e. so the loaded module may load local-to-it modules
59 or packages.)
61 :returns:
62 Two-tuple of ``(module, directory)`` where ``module`` is the
63 collection-containing Python module object, and ``directory`` is
64 the string path to the directory the module was found in.
66 .. versionadded:: 1.0
67 """
68 if name is None:
69 name = self.config.tasks.collection_name
70 spec = self.find(name)
71 if spec and spec.loader and spec.origin:
72 # Typically either tasks.py or tasks/__init__.py
73 source_file = Path(spec.origin)
74 # Will be 'the dir tasks.py is in', or 'tasks/', in both cases this
75 # is what wants to be in sys.path for "from . import sibling"
76 enclosing_dir = source_file.parent
77 # Will be "the directory above the spot that 'import tasks' found",
78 # namely the parent of "your task tree", i.e. "where project level
79 # config files are looked for". So, same as enclosing_dir for
80 # tasks.py, but one more level up for tasks/__init__.py...
81 module_parent = enclosing_dir
82 if spec.parent: # it's a package, so we have to go up again
83 module_parent = module_parent.parent
84 # Get the enclosing dir on the path
85 enclosing_str = str(enclosing_dir)
86 if enclosing_str not in sys.path:
87 sys.path.insert(0, enclosing_str)
88 # Actual import
89 module = module_from_spec(spec)
90 sys.modules[spec.name] = module # so 'from . import xxx' works
91 spec.loader.exec_module(module)
92 # Return the module and the folder it was found in
93 return module, str(module_parent)
94 msg = "ImportError loading {!r}, raising ImportError"
95 debug(msg.format(name))
96 raise ImportError
99class FilesystemLoader(Loader):
100 """
101 Loads Python files from the filesystem (e.g. ``tasks.py``.)
103 Searches recursively towards filesystem root from a given start point.
105 .. versionadded:: 1.0
106 """
108 # TODO: could introduce config obj here for transmission to Collection
109 # TODO: otherwise Loader has to know about specific bits to transmit, such
110 # as auto-dashes, and has to grow one of those for every bit Collection
111 # ever needs to know
112 def __init__(self, start: Optional[str] = None, **kwargs: Any) -> None:
113 super().__init__(**kwargs)
114 if start is None:
115 start = self.config.tasks.search_root
116 self._start = start
118 @property
119 def start(self) -> str:
120 # Lazily determine default CWD if configured value is falsey
121 return self._start or os.getcwd()
123 def find(self, name: str) -> Optional[ModuleSpec]:
124 debug("FilesystemLoader find starting at {!r}".format(self.start))
125 spec = None
126 module = "{}.py".format(name)
127 paths = self.start.split(os.sep)
128 try:
129 # walk the path upwards to check for dynamic import
130 for x in reversed(range(len(paths) + 1)):
131 path = os.sep.join(paths[0:x])
132 if module in os.listdir(path):
133 spec = spec_from_file_location(
134 name, os.path.join(path, module)
135 )
136 break
137 elif name in os.listdir(path) and os.path.exists(
138 os.path.join(path, name, "__init__.py")
139 ):
140 basepath = os.path.join(path, name)
141 spec = spec_from_file_location(
142 name,
143 os.path.join(basepath, "__init__.py"),
144 submodule_search_locations=[basepath],
145 )
146 break
147 if spec:
148 debug("Found module: {!r}".format(spec))
149 return spec
150 except (FileNotFoundError, ModuleNotFoundError):
151 msg = "ImportError loading {!r}, raising CollectionNotFound"
152 debug(msg.format(name))
153 raise CollectionNotFound(name=name, start=self.start)
154 return None