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

69 statements  

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 

8 

9from . import Config 

10from .exceptions import CollectionNotFound 

11from .util import debug 

12 

13 

14class Loader: 

15 """ 

16 Abstract class defining how to find/import a session's base `.Collection`. 

17 

18 .. versionadded:: 1.0 

19 """ 

20 

21 def __init__(self, config: Optional["Config"] = None) -> None: 

22 """ 

23 Set up a new loader with some `.Config`. 

24 

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 

33 

34 def find(self, name: str) -> Optional[ModuleSpec]: 

35 """ 

36 Implementation-specific finder method seeking collection ``name``. 

37 

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``.) 

42 

43 For a sample implementation, see `.FilesystemLoader`. 

44 

45 .. versionadded:: 1.0 

46 """ 

47 raise NotImplementedError 

48 

49 def load(self, name: Optional[str] = None) -> Tuple[ModuleType, str]: 

50 """ 

51 Load and return collection module identified by ``name``. 

52 

53 This method requires a working implementation of `.find` in order to 

54 function. 

55 

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.) 

60 

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. 

65 

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 

97 

98 

99class FilesystemLoader(Loader): 

100 """ 

101 Loads Python files from the filesystem (e.g. ``tasks.py``.) 

102 

103 Searches recursively towards filesystem root from a given start point. 

104 

105 .. versionadded:: 1.0 

106 """ 

107 

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 

117 

118 @property 

119 def start(self) -> str: 

120 # Lazily determine default CWD if configured value is falsey 

121 return self._start or os.getcwd() 

122 

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