22
33from __future__ import annotations
44
5- import os
6- import os .path
75import tokenize
86from importlib import import_module
97from typing import TYPE_CHECKING , Any , Literal
1311from sphinx .util ._pathlib import _StrPath
1412
1513if TYPE_CHECKING :
14+ import os
1615 from inspect import Signature
1716
1817
@@ -28,7 +27,7 @@ class ModuleAnalyzer:
2827 cache : dict [tuple [Literal ['file' , 'module' ], str | _StrPath ], Any ] = {}
2928
3029 @staticmethod
31- def get_module_source (modname : str ) -> tuple [str | None , str | None ]:
30+ def get_module_source (modname : str ) -> tuple [_StrPath | None , str | None ]:
3231 """Try to find the source code for a module.
3332
3433 Returns ('filename', 'source'). One of it can be None if
@@ -39,14 +38,15 @@ def get_module_source(modname: str) -> tuple[str | None, str | None]:
3938 except Exception as err :
4039 raise PycodeError ('error importing %r' % modname , err ) from err
4140 loader = getattr (mod , '__loader__' , None )
42- filename = getattr (mod , '__file__' , None )
41+ filename : str | None = getattr (mod , '__file__' , None )
4342 if loader and getattr (loader , 'get_source' , None ):
4443 # prefer Native loader, as it respects #coding directive
4544 try :
4645 source = loader .get_source (modname )
4746 if source :
47+ mod_path = None if filename is None else _StrPath (filename )
4848 # no exception and not None - it must be module source
49- return filename , source
49+ return mod_path , source
5050 except ImportError :
5151 pass # Try other "source-mining" methods
5252 if filename is None and loader and getattr (loader , 'get_filename' , None ):
@@ -60,24 +60,28 @@ def get_module_source(modname: str) -> tuple[str | None, str | None]:
6060 if filename is None :
6161 # all methods for getting filename failed, so raise...
6262 raise PycodeError ('no source found for module %r' % modname )
63- filename = os .path .normpath (os .path .abspath (filename ))
64- if filename .lower ().endswith (('.pyo' , '.pyc' )):
65- filename = filename [:- 1 ]
66- if not os .path .isfile (filename ) and os .path .isfile (filename + 'w' ):
67- filename += 'w'
68- elif not filename .lower ().endswith (('.py' , '.pyw' )):
69- raise PycodeError ('source is not a .py file: %r' % filename )
70-
71- if not os .path .isfile (filename ):
72- raise PycodeError ('source file is not present: %r' % filename )
73- return filename , None
63+ mod_path = _StrPath (filename ).resolve ()
64+ if mod_path .suffix in {'.pyo' , '.pyc' }:
65+ mod_path_pyw = mod_path .with_suffix ('.pyw' )
66+ if not mod_path .is_file () and mod_path_pyw .is_file ():
67+ mod_path = mod_path_pyw
68+ else :
69+ mod_path = mod_path .with_suffix ('.py' )
70+ elif mod_path .suffix not in {'.py' , '.pyw' }:
71+ msg = f'source is not a .py file: { mod_path !r} '
72+ raise PycodeError (msg )
73+
74+ if not mod_path .is_file ():
75+ msg = f'source file is not present: { mod_path !r} '
76+ raise PycodeError (msg )
77+ return mod_path , None
7478
7579 @classmethod
7680 def for_string (
7781 cls : type [ModuleAnalyzer ],
7882 string : str ,
7983 modname : str ,
80- srcname : str = '<string>' ,
84+ srcname : str | os . PathLike [ str ] = '<string>' ,
8185 ) -> ModuleAnalyzer :
8286 return cls (string , modname , srcname )
8387
0 commit comments