How do I get the current 'package' name? (setup.py)

后端 未结 2 1811
故里飘歌
故里飘歌 2020-12-19 08:35

How do I get the current topmost package, i.e., the name defined in setup.py?

Here is my tree:

.
|-- README.md
|-- the_project_name_for_         


        
相关标签:
2条回答
  • 2020-12-19 09:21

    Not entirely sure what the larger goal is, but maybe you could be interested in reading about importlib.resources as well as importlib.metadata.

    Something like the following:

    import importlib.metadata
    import importlib.resources
    
    version = importlib.metadata.version('SomeProject')
    data = importlib.resources.files('top_level_package.sub_package').joinpath('file.txt').read_text()
    

    And more generally, it is near impossible (or not worth the amount of work) to 100% reliably detect the name of the project (SomeProject) from within the code. It is easier to just hard-code it.

    Nevertheless here are some techniques, and ideas to retrieve the name of the project from one of its modules:

    • https://bitbucket.org/pypa/distlib/issues/102/getting-the-distribution-that-a-module
    • https://stackoverflow.com/a/22845276/11138259
    • https://stackoverflow.com/a/56032725/11138259

    Update:

    I believe some function like the following should return the name of the installed distribution containing the current file:

    import pathlib
    import importlib_metadata
    
    def get_project_name():
        for dist in importlib_metadata.distributions():
            try:
                relative = pathlib.Path(__file__).relative_to(dist.locate_file(''))
            except ValueError:
                pass
            else:
                if relative in dist.files:
                    return dist.metadata['Name']
        return None
    
    0 讨论(0)
  • 2020-12-19 09:40

    A solution I've been working on:

    from os import listdir, path
    from contextlib import suppress
    import ast
    
    
    
    def get_first_setup_py(cur_dir):
        if 'setup.py' in listdir(cur_dir):
            return path.join(cur_dir, 'setup.py')
        prev_dir = cur_dir
        cur_dir = path.realpath(path.dirname(cur_dir))
        if prev_dir == cur_dir:
            raise StopIteration()
        return get_first_setup_py(cur_dir)
    
    
    setup_py_file_name = get_first_setup_py(path.dirname(__file__))
    

    First pass:

    def get_from_setup_py(setup_file): # mostly https://stackoverflow.com/a/47463422
        import importlib.util
    
    
        spec = importlib.util.spec_from_file_location('setup', setup_file)
        setup = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(setup)
        # And now access it
        print(setup)
    

    That option did work. So I returned to the ast solution I referenced in the question, and got this second pass to work:

    def parse_package_name_from_setup_py(setup_py_file_name):
        with open(setup_py_file_name, 'rt') as f:
            parsed_setup_py = ast.parse(f.read(), 'setup.py')
    
        # Assumes you have an `if __name__ == '__main__'` block:
        main_body = next(sym for sym in parsed_setup_py.body[::-1]
                         if isinstance(sym, ast.If)).body
    
        setup_call = next(sym.value
                          for sym in main_body[::-1]
                          if isinstance(sym, ast.Expr)
                          and isinstance(sym.value, ast.Call)
                          and sym.value.func.id in frozenset(('setup',
                                                              'distutils.core.setup',
                                                              'setuptools.setup')))
    
        package_name = next(keyword
                            for keyword in setup_call.keywords
                            if keyword.arg == 'name'
                            and isinstance(keyword.value, ast.Name))
    
        # Return the raw string if it is one
        if isinstance(package_name.value, ast.Str):
            return package_name.value.s
    
        # Otherwise it's a variable defined in the `if __name__ == '__main__'` block:
        elif isinstance(package_name.value, ast.Name):
            return next(sym.value.s
                        for sym in main_body
                        if isinstance(sym, ast.Assign)
                        and isinstance(sym.value, ast.Str)
                        and any(target.id == package_name.value.id
                                for target in sym.targets)
                        )
    
        else:
            raise NotImplemented('Package name extraction only built for raw strings & '
                                 'assigment in the same scope that setup() is called')
    

    Third pass (works for both installed and development versions):

    # Originally from https://stackoverflow.com/a/56032725;
    # but made more concise and added support whence source
    class App(object):
        def get_app_name(self) -> str:
            # Iterate through all installed packages and try to find one
            # that has the app's file in it
            app_def_path = inspect.getfile(self.__class__)
            with suppress(FileNotFoundError):
                return next(
                    (dist.project_name
                     for dist in pkg_resources.working_set
                     if any(app_def_path == path.normpath(path.join(dist.location, r[0]))
                            for r in csv.reader(dist.get_metadata_lines('RECORD')))),
                    None) or parse_package_name_from_setup_py(
                    get_first_setup_py(path.dirname(__file__)))
    
    0 讨论(0)
提交回复
热议问题