Parse setup.py without setuptools

Question:

I’m using python on my ipad and need a way to grab the name, version, packages etc from a packages setup.py. I do not have access to setuptools or distutils. At first I thought that I’d parse setup.py but that does not seem to be the answer as there are many ways to pass args to setup(). I’d like to create a mock setup() that returns the args passed to it, but I am unsure how to get past the import errors. Any help would be greatly appreciated.

Asked By: briarfox

||

Answers:

You can dynamically create a setuptools module and capture the values passed to setup indeed:

>>> import imp
>>> module = """
... def setup(*args, **kwargs):
...     print(args, kwargs)
... """
>>>
>>> setuptools = imp.new_module("setuptools")
>>> exec module in setuptools.__dict__
>>> setuptools
<module 'setuptools' (built-in)>
>>> setuptools.setup(3)
((3,), {})

After the above you have a setuptools module with a setup function in it. You may need to create a few more functions to make all the imports work. After that you can import setup.py and gather the contents. That being said, in general this is a tricky approach as setup.py can contain any Python code with conditional imports and dynamic computations to pass values to setup().

Answered By: Simeon Visser

Parsing setup.py can be dangerous, in case of malicious files like this:

from setuptools import setup
import shutil

setup(
    install_requires=[
        shutil.rmtree('/'),  # very dangerous!
        'django',
    ],
)

I have prepared a simple script (based on idea of @simeon-visser) and docker image, which parse setup.py file in isolated and secure container:

Usage

$ git clone https://github.com/noisy/parse_setup.py
$ cd parse_setup.py/
$ docker build -t parse .
$ ./parse.sh ./example_files/setup.py
#[OK]
lxml==3.4.4
termcolor==1.1.0

$ ./parse.sh ./example_files/dangerous_setup.py
[Errno 39] Directory not empty: '/usr/local/lib'
#nothing bad happend :)
Answered By: noisy

No kidding. This worked on python 3.4.3 and 2.7.6 😉

export VERSION=$(python my_package/setup.py --version)

contents of setup.py:

from distutils.core import setup

setup(
    name='bonsai',
    version='0.0.1',
    packages=['my_package'],
    url='',
    license='MIT',
    author='',
    author_email='',
    description='',
    test_suite='nose.collector',
    tests_require=['nose'],
)
Answered By: guneysus

You could replace the setup method of the setuptools package like this

>>> import setuptools
>>> def setup(**kwargs):
        print(kwargs)
>>> setuptools.setup = setup
>>> content = open('setup.py').read()
>>> exec(content)
Answered By: Priyam Singh

Accordingly to setup.py CLI help there’s a nice command line argument for that, but it’s not working in my case:

python setup.py --requires

So I chose a more violent approach, using the ast module to parse the file directly, it works pretty well if your setup.py contains a list of strings as requirements otherwise it can be very complex:

from pathlib import Path
import ast
import pkg_resources


class SetupPyAnalyzer(ast.NodeVisitor):
    def __init__(self):
        self.requirements = list()

    def visit_Call(self, node):
        is_setup_func = False

        if node.func and type(node.func) == ast.Name:
            func: ast.Name = node.func
            is_setup_func = func.id == "setup"

        if is_setup_func:
            for kwarg in node.keywords:
                if kwarg.arg == 'install_requires':
                    install_requires: ast.List = kwarg.value
                    for require in install_requires.elts:
                        require: ast.Constant = require

                        self.requirements.append(require.value)

        self.generic_visit(node)


def parse_requirements(content):
    return pkg_resources.parse_requirements(content)

def parse_setup_py():
    with Path('setup.py').open() as file:
        tree = ast.parse(file.read())

    analyzer = SetupPyAnalyzer()
    analyzer.visit(tree)

    return [
        lib.project_name
        for lib in parse_requirements("n".join(analyzer.requirements))
    ]
Answered By: Luca Faggianelli
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.