Coverage for sources/appcore/distribution.py: 100%

78 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-16 02:09 +0000

1# vim: set filetype=python fileencoding=utf-8: 

2# -*- coding: utf-8 -*- 

3 

4#============================================================================# 

5# # 

6# Licensed under the Apache License, Version 2.0 (the "License"); # 

7# you may not use this file except in compliance with the License. # 

8# You may obtain a copy of the License at # 

9# # 

10# http://www.apache.org/licenses/LICENSE-2.0 # 

11# # 

12# Unless required by applicable law or agreed to in writing, software # 

13# distributed under the License is distributed on an "AS IS" BASIS, # 

14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

15# See the License for the specific language governing permissions and # 

16# limitations under the License. # 

17# # 

18#============================================================================# 

19 

20 

21''' Information about package distribution. ''' 

22 

23 

24from . import __ 

25from . import exceptions as _exceptions 

26from . import io as _io 

27 

28 

29class Information( __.immut.DataclassObject ): 

30 ''' Information about a package distribution. ''' 

31 

32 name: str 

33 location: __.Path 

34 editable: bool 

35 

36 @classmethod 

37 async def prepare( 

38 selfclass, 

39 exits: __.ctxl.AsyncExitStack, 

40 anchor: __.Absential[ __.Path ] = __.absent, 

41 package: __.Absential[ str ] = __.absent, 

42 ) -> __.typx.Self: 

43 ''' Acquires information about package distribution. ''' 

44 if ( __.is_absent( anchor ) 

45 and getattr( __.sys, 'frozen', False ) 

46 and hasattr( __.sys, '_MEIPASS' ) 

47 ): # pragma: no cover 

48 location, name = await _acquire_pyinstaller_information( ) 

49 return selfclass( 

50 editable = False, location = location, name = name ) 

51 if __.is_absent( package ): 

52 package, _ = _discover_invoker_location( ) 

53 if not __.is_absent( package ): # pragma: no branch 

54 # TODO: Python 3.12: importlib.metadata 

55 from importlib_metadata import packages_distributions 

56 name = packages_distributions( ).get( package ) 

57 if name: 

58 location = ( 

59 await _acquire_production_location( package, exits ) ) 

60 return selfclass( 

61 editable = False, location = location, name = name[ 0 ] ) 

62 # https://github.com/pypa/packaging-problems/issues/609 

63 # Development sources rather than distribution. 

64 # Implies no use of importlib.resources. 

65 if __.is_absent( anchor ): 

66 _, anchor = _discover_invoker_location( ) 

67 location, name = ( 

68 await _acquire_development_information( anchor = anchor ) ) 

69 return selfclass( 

70 editable = True, location = location, name = name ) 

71 

72 def provide_data_location( self, *appendages: str ) -> __.Path: 

73 ''' Provides location of distribution data. ''' 

74 base = self.location / 'data' 

75 if appendages: return base.joinpath( *appendages ) 

76 return base 

77 

78 

79async def _acquire_development_information( 

80 anchor: __.Path 

81) -> tuple[ __.Path, str ]: 

82 location = _locate_pyproject( anchor ) 

83 pyproject = await _io.acquire_text_file_async( 

84 location / 'pyproject.toml', deserializer = __.tomli.loads ) 

85 name = pyproject[ 'project' ][ 'name' ] 

86 return location, name 

87 

88 

89async def _acquire_production_location( 

90 package: str, exits: __.ctxl.AsyncExitStack 

91) -> __.Path: 

92 # TODO: Python 3.12: importlib.resources 

93 from importlib_resources import files, as_file # pyright: ignore 

94 # Extract package contents to temporary directory, if necessary. 

95 return exits.enter_context( 

96 as_file( files( package ) ) ) # pyright: ignore 

97 

98 

99async def _acquire_pyinstaller_information( # pragma: no cover 

100) -> tuple[ __.Path, str ]: 

101 anchor_ = __.Path( 

102 getattr( __.sys, '_MEIPASS' ) ) 

103 # TODO: More rigorously determine package name. 

104 # Currently assumes 'pyproject.toml' is present in distribution. 

105 return await _acquire_development_information( anchor = anchor_ ) 

106 

107 

108def _detect_package_boundary( mname: str ) -> __.Absential[ str ]: 

109 ''' Finds package boundary, including for namespace packages. ''' 

110 if not mname or mname == '__main__': return __.absent 

111 components = mname.split( '.' ) 

112 # Work backwards through dotted name to find deepest package. 

113 for i in range( len( components ), 0, -1 ): 

114 candidate = '.'.join( components[ : i ] ) 

115 if candidate not in __.sys.modules: continue 

116 module = __.sys.modules[ candidate ] 

117 if hasattr( module, '__path__' ): # Is it a package? 

118 return candidate 

119 # Fallback to first component for edge cases. 

120 return components[ 0 ] 

121 

122 

123def _discover_invoker_location( ) -> tuple[ __.Absential[ str ], __.Path ]: 

124 ''' Discovers file path of caller for project root detection. ''' 

125 package_location = __.Path( __file__ ).parent.resolve( ) 

126 stdlib_locations, sp_locations = _provide_standard_locations( ) 

127 frame = __.inspect.currentframe( ) 

128 if frame is None: return __.absent, __.Path.cwd( ) 

129 # Walk up the call stack to find frame outside of this package. 

130 while True: 

131 frame = frame.f_back 

132 if frame is None: break # pragma: no cover 

133 location = __.Path( frame.f_code.co_filename).resolve( ) 

134 # Skip frames within this package 

135 if location.is_relative_to( package_location ): # pragma: no cover 

136 continue 

137 in_site_packages = any( 

138 location.is_relative_to( sp_location ) 

139 for sp_location in sp_locations ) 

140 in_stdlib_locations = any( 

141 location.is_relative_to( stdlib_location ) 

142 for stdlib_location in stdlib_locations ) 

143 # Skip standard library paths, unless in site-packages. 

144 if not in_site_packages and in_stdlib_locations: continue 

145 mname = frame.f_globals.get( '__name__' ) 

146 if not mname or mname == '__main__': continue 

147 pname = _detect_package_boundary( mname ) 

148 if not __.is_absent( pname ): 

149 return pname, location.parent 

150 continue # pragma: no cover 

151 # Fallback location is current working directory. 

152 return __.absent, __.Path.cwd( ) # pragma: no cover 

153 

154 

155def _locate_pyproject( project_anchor: __.Path ) -> __.Path: 

156 ''' Finds project manifest, if it exists. Errors otherwise. ''' 

157 initial = project_anchor.resolve( ) 

158 current = initial if initial.is_dir( ) else initial.parent 

159 limits: set[ __.Path ] = set( ) 

160 for limits_variable in ( 'GIT_CEILING_DIRECTORIES', ): 

161 limits_value = __.os.environ.get( limits_variable ) 

162 if not limits_value: continue # pragma: no cover 

163 limits.update( # pragma: no cover 

164 __.Path( limit ).resolve( ) 

165 for limit in limits_value.split( ':' ) if limit.strip( ) ) 

166 while current != current.parent: # Not at filesystem root 

167 if ( current / 'pyproject.toml' ).exists( ): 

168 return current 

169 if current in limits: 

170 raise _exceptions.FileLocateFailure( # noqa: TRY003 # pragma: no cover 

171 'project root discovery', 'pyproject.toml' ) 

172 current = current.parent 

173 raise _exceptions.FileLocateFailure( # noqa: TRY003 # pragma: no cover 

174 'project root discovery', 'pyproject.toml' ) 

175 

176 

177def _provide_standard_locations( ) -> tuple[ 

178 frozenset[ __.Path ], frozenset[ __.Path ] 

179]: 

180 stdlib_locations = frozenset( ( 

181 __.Path( __.syscfg.get_path( 'stdlib' ) ).resolve( ), 

182 __.Path( __.syscfg.get_path( 'platstdlib' ) ).resolve( ) ) ) 

183 sp_locations: set[ __.Path ] = set( ) 

184 for path in __.site.getsitepackages( ): 

185 sp_locations.add( __.Path( path ).resolve( ) ) 

186 with __.ctxl.suppress( AttributeError ): 

187 sp_locations.add( 

188 __.Path( __.site.getusersitepackages( ) ).resolve( ) ) 

189 return frozenset( stdlib_locations ), frozenset( sp_locations )