Coverage for sources/appcore/distribution.py: 100%
57 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-11 00:41 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-11 00:41 +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 ):
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 _discover_invoker_location( ) -> tuple[ __.Absential[ str ], __.Path ]:
109 ''' Discovers file path of caller for project root detection. '''
110 import inspect
111 package_location = __.Path( __file__ ).parent.resolve( )
112 python_location = __.Path( __.sys.executable ).parent.parent.resolve( )
113 frame = inspect.currentframe( )
114 if frame is None: return __.absent, __.Path.cwd( )
115 # Walk up the call stack to find frame outside of this package.
116 while True:
117 frame = frame.f_back
118 if frame is None: break # pragma: no cover
119 location = __.Path( frame.f_code.co_filename).resolve( )
120 # Skip frames within this package and Python installation.
121 if location.is_relative_to( package_location ): # pragma: no cover
122 continue
123 if location.is_relative_to( python_location ): # pragma: no cover
124 continue
125 mname = frame.f_globals.get( '__module__' )
126 if not mname: continue
127 pname = mname.split( '.', maxsplit = 1 )[ 0 ]
128 return pname, location.parent
129 # Fallback location is current working directory.
130 return __.absent, __.Path.cwd( ) # pragma: no cover
133def _locate_pyproject( project_anchor: __.Path ) -> __.Path:
134 ''' Finds project manifest, if it exists. Errors otherwise. '''
135 initial = project_anchor.resolve( )
136 current = initial if initial.is_dir( ) else initial.parent
137 limits: set[ __.Path ] = set( )
138 for limits_variable in ( 'GIT_CEILING_DIRECTORIES', ):
139 limits_value = __.os.environ.get( limits_variable )
140 if not limits_value: continue # pragma: no cover
141 limits.update( # pragma: no cover
142 __.Path( limit ).resolve( )
143 for limit in limits_value.split( ':' ) if limit.strip( ) )
144 while current != current.parent: # Not at filesystem root
145 if ( current / 'pyproject.toml' ).exists( ):
146 return current
147 if current in limits:
148 raise _exceptions.FileLocateFailure( # noqa: TRY003 # pragma: no cover
149 'project root discovery', 'pyproject.toml' )
150 current = current.parent
151 raise _exceptions.FileLocateFailure( # noqa: TRY003 # pragma: no cover
152 'project root discovery', 'pyproject.toml' )