Coverage for sources/appcore/distribution.py: 100%
78 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-13 17:16 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-13 17:16 +0000
1# vim: set filetype=python fileencoding=utf-8:
2# -*- coding: utf-8 -*-
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#============================================================================#
21''' Information about package distribution. '''
24from . import __
25from . import exceptions as _exceptions
26from . import io as _io
29class Information( __.immut.DataclassObject ):
30 ''' Information about a package distribution. '''
32 name: str
33 location: __.Path
34 editable: bool
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 )
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
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
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
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_ )
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 ]
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
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' )
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 )