|
10 | 10 | """Generate requirements from `setup.py` and `requirements-devel.txt`.""" |
11 | 11 |
|
12 | 12 | import os |
13 | | -import re |
14 | 13 | import sys |
| 14 | +import ast |
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 | 21 | import mock |
| 22 | +import setuptools |
| 23 | +import distutils.core |
22 | 24 | from commoncode.command import pushd |
23 | 25 | from packvers.requirements import Requirement |
24 | 26 |
|
25 | 27 |
|
26 | 28 | def minver_error(pkg_name): |
27 | 29 | """Report error about missing minimum version constraint and exit.""" |
28 | 30 | print( |
29 | | - 'ERROR: specify minimal version of "{0}" using ' '">=" or "=="'.format(pkg_name), |
| 31 | + 'ERROR: specify minimal version of "{0}" using ' |
| 32 | + '">=" or "=="'.format(pkg_name), |
30 | 33 | file=sys.stderr, |
31 | 34 | ) |
32 | 35 | sys.exit(1) |
@@ -55,18 +58,65 @@ def iter_requirements(level, extras, setup_file): |
55 | 58 | with pushd(os.path.dirname(setup_file)): |
56 | 59 | with open(setup_file) as sf: |
57 | 60 | file_contents = sf.read() |
58 | | - setup_provider = re.findall(r"from ([a-z._]+) import setup", file_contents) |
59 | | - if len(setup_provider) == 1: |
60 | | - setup_provider = setup_provider[0] |
| 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 [ |
| 72 | + e.name for e in elem.names |
| 73 | + ]: |
| 74 | + imports.append(elem.module) |
| 75 | + # for module imports, e.g. import setuptools; setuptools.setup(...) |
| 76 | + elif ( |
| 77 | + isinstance(elem, ast.Expr) |
| 78 | + and isinstance(elem.value, ast.Call) |
| 79 | + and isinstance(elem.value.func, ast.Attribute) |
| 80 | + and isinstance(elem.value.func.value, ast.Name) |
| 81 | + and elem.value.func.attr == "setup" |
| 82 | + ): |
| 83 | + name = elem.value.func.value.id |
| 84 | + if name in asnames.keys(): |
| 85 | + name = asnames[name] |
| 86 | + imports.append(name) |
| 87 | + # for module imports, e.g. import disttools.core; disttools.core.setup(...) |
| 88 | + elif ( |
| 89 | + isinstance(elem, ast.Expr) |
| 90 | + and isinstance(elem.value, ast.Call) |
| 91 | + and isinstance(elem.value.func, ast.Attribute) |
| 92 | + and isinstance(elem.value.func.value, ast.Attribute) |
| 93 | + and elem.value.func.attr == "setup" |
| 94 | + ): |
| 95 | + name = ( |
| 96 | + str(elem.value.func.value.value.id) |
| 97 | + + "." |
| 98 | + + str(elem.value.func.value.attr) |
| 99 | + ) |
| 100 | + if name in asnames.keys(): |
| 101 | + name = asnames[name] |
| 102 | + imports.append(name) |
| 103 | + setup_providers = [ |
| 104 | + i for i in imports if i in ["distutils.core", "setuptools"] |
| 105 | + ] |
| 106 | + if len(setup_providers) == 0: |
| 107 | + print( |
| 108 | + f"Warning: unable to recognize setup provider in {setup_file}: " |
| 109 | + "defaulting to 'distutils.core'." |
| 110 | + ) |
| 111 | + setup_provider = "distutils.core" |
| 112 | + elif len(setup_providers) == 1: |
| 113 | + setup_provider = setup_providers[0] |
61 | 114 | else: |
62 | | - setup_provider = "" |
63 | | - if not ((setup_provider == "distutils.core") or (setup_provider == "setuptools")): |
64 | 115 | print( |
65 | | - f"Warning: unable to recognize 'import {setup_provider}' in {setup_file}: " |
| 116 | + f"Warning: ambiguous setup provider in {setup_file}: candidates are {setup_providers}" |
66 | 117 | "defaulting to 'distutils.core'." |
67 | 118 | ) |
68 | 119 | setup_provider = "distutils.core" |
69 | | - exec(f"import {setup_provider}") |
70 | 120 | with mock.patch.object(eval(setup_provider), "setup") as mock_setup: |
71 | 121 | sys.path.append(os.path.dirname(setup_file)) |
72 | 122 | g = {"__file__": setup_file, "__name__": "__main__"} |
@@ -145,7 +195,9 @@ def iter_requirements(level, extras, setup_file): |
145 | 195 | result[pkg.name] = "{0}=={1}".format(build_pkg_name(pkg), specs["~="]) |
146 | 196 | else: |
147 | 197 | ver, _ = os.path.splitext(specs["~="]) |
148 | | - result[pkg.name] = "{0}>={1},=={2}.*".format(build_pkg_name(pkg), specs["~="], ver) |
| 198 | + result[pkg.name] = "{0}>={1},=={2}.*".format( |
| 199 | + build_pkg_name(pkg), specs["~="], ver |
| 200 | + ) |
149 | 201 |
|
150 | 202 | else: |
151 | 203 | if level == "min": |
|
0 commit comments