|
9 | 9 | #
|
10 | 10 | """Generate requirements from `setup.py` and `requirements-devel.txt`."""
|
11 | 11 |
|
| 12 | +import ast |
12 | 13 | import os
|
13 |
| -import re |
14 | 14 | import sys
|
15 | 15 |
|
16 | 16 | try:
|
17 | 17 | import configparser
|
18 | 18 | except ImportError: # pragma: no cover
|
19 | 19 | import ConfigParser as configparser
|
20 | 20 |
|
| 21 | +import distutils.core |
| 22 | + |
21 | 23 | import mock
|
22 | 24 | import setuptools
|
23 | 25 | from commoncode.command import pushd
|
@@ -54,11 +56,65 @@ def iter_requirements(level, extras, setup_file):
|
54 | 56 | setup_requires = {}
|
55 | 57 | # change directory to setup.py path
|
56 | 58 | with pushd(os.path.dirname(setup_file)):
|
57 |
| - with mock.patch.object(setuptools, "setup") as mock_setup: |
58 |
| - sys.path.append(os.path.dirname(setup_file)) |
59 |
| - g = {"__file__": setup_file, "__name__": "__main__"} |
60 |
| - with open(setup_file) as sf: |
61 |
| - exec(sf.read(), g) |
| 59 | + with open(setup_file) as sf: |
| 60 | + file_contents = sf.read() |
| 61 | + node = ast.parse(file_contents) |
| 62 | + asnames = {} |
| 63 | + imports = [] |
| 64 | + for elem in ast.walk(node): |
| 65 | + # save the asnames to parse aliases later |
| 66 | + if isinstance(elem, ast.Import): |
| 67 | + for n in elem.names: |
| 68 | + asnames[(n.asname if n.asname is not None else n.name)] = n.name |
| 69 | + for elem in ast.walk(node): |
| 70 | + # for function imports, e.g. from setuptools import setup; setup() |
| 71 | + if isinstance(elem, ast.ImportFrom) and "setup" in [e.name for e in elem.names]: |
| 72 | + imports.append(elem.module) |
| 73 | + # for module imports, e.g. import setuptools; setuptools.setup(...) |
| 74 | + elif ( |
| 75 | + isinstance(elem, ast.Expr) |
| 76 | + and isinstance(elem.value, ast.Call) |
| 77 | + and isinstance(elem.value.func, ast.Attribute) |
| 78 | + and isinstance(elem.value.func.value, ast.Name) |
| 79 | + and elem.value.func.attr == "setup" |
| 80 | + ): |
| 81 | + name = elem.value.func.value.id |
| 82 | + if name in asnames.keys(): |
| 83 | + name = asnames[name] |
| 84 | + imports.append(name) |
| 85 | + # for module imports, e.g. import disttools.core; disttools.core.setup(...) |
| 86 | + elif ( |
| 87 | + isinstance(elem, ast.Expr) |
| 88 | + and isinstance(elem.value, ast.Call) |
| 89 | + and isinstance(elem.value.func, ast.Attribute) |
| 90 | + and isinstance(elem.value.func.value, ast.Attribute) |
| 91 | + and elem.value.func.attr == "setup" |
| 92 | + ): |
| 93 | + name = ( |
| 94 | + str(elem.value.func.value.value.id) + "." + str(elem.value.func.value.attr) |
| 95 | + ) |
| 96 | + if name in asnames.keys(): |
| 97 | + name = asnames[name] |
| 98 | + imports.append(name) |
| 99 | + setup_providers = [i for i in imports if i in ["distutils.core", "setuptools"]] |
| 100 | + if len(setup_providers) == 0: |
| 101 | + print( |
| 102 | + f"Warning: unable to recognize setup provider in {setup_file}: " |
| 103 | + "defaulting to 'distutils.core'." |
| 104 | + ) |
| 105 | + setup_provider = "distutils.core" |
| 106 | + elif len(setup_providers) == 1: |
| 107 | + setup_provider = setup_providers[0] |
| 108 | + else: |
| 109 | + print( |
| 110 | + f"Warning: ambiguous setup provider in {setup_file}: candidates are {setup_providers}" |
| 111 | + "defaulting to 'distutils.core'." |
| 112 | + ) |
| 113 | + setup_provider = "distutils.core" |
| 114 | + with mock.patch.object(eval(setup_provider), "setup") as mock_setup: |
| 115 | + sys.path.append(os.path.dirname(setup_file)) |
| 116 | + g = {"__file__": setup_file, "__name__": "__main__"} |
| 117 | + exec(file_contents, g) |
62 | 118 | sys.path.pop()
|
63 | 119 | # removing the assertion `assert g["setup"]`` since this is not true for all cases
|
64 | 120 | # for example when setuptools.setup() is called instead of setup()
|
|
0 commit comments