handle_setup.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. from pathlib import Path
  2. from typing import Union
  3. from astroid import MANAGER, AstroidBuildingException, AstroidSyntaxError
  4. from astroid.builder import AstroidBuilder
  5. from astroid.nodes import Assign, AssignName, Call, Const, Keyword, List, Name, Tuple
  6. from .exceptions import CouldNotParseRequirements
  7. from .requirement import DetectedRequirement
  8. class SetupWalker:
  9. def __init__(self, ast):
  10. self._ast = ast
  11. self._setup_call = None
  12. self._top_level_assigns = {}
  13. self.walk()
  14. def walk(self, node=None):
  15. top = node is None
  16. node = node or self._ast
  17. # test to see if this is a call to setup()
  18. if isinstance(node, Call):
  19. for child_node in node.get_children():
  20. if isinstance(child_node, Name) and child_node.name == "setup":
  21. # TODO: what if this isn't actually the distutils setup?
  22. self._setup_call = node
  23. for child_node in node.get_children():
  24. if top and isinstance(child_node, Assign):
  25. for target in child_node.targets:
  26. if isinstance(target, AssignName):
  27. self._top_level_assigns[target.name] = child_node.value
  28. self.walk(child_node)
  29. def _get_list_value(self, list_node):
  30. values = []
  31. for child_node in list_node.get_children():
  32. if not isinstance(child_node, Const):
  33. # we can't handle anything fancy, only constant values
  34. raise CouldNotParseRequirements
  35. values.append(child_node.value)
  36. return values
  37. def get_requires(self):
  38. # first, if we have a call to setup, then we can see what its "install_requires" argument is
  39. if not self._setup_call:
  40. raise CouldNotParseRequirements
  41. found_requirements = []
  42. for child_node in self._setup_call.get_children():
  43. if not isinstance(child_node, Keyword):
  44. # do we want to try to handle positional arguments?
  45. continue
  46. if child_node.arg not in ("install_requires", "requires"):
  47. continue
  48. if isinstance(child_node.value, (List, Tuple)):
  49. # joy! this is a simple list or tuple of requirements
  50. # this is a Keyword -> List or Keyword -> Tuple
  51. found_requirements += self._get_list_value(child_node.value)
  52. continue
  53. if isinstance(child_node.value, Name):
  54. # otherwise, it's referencing a value defined elsewhere
  55. # this will be a Keyword -> Name
  56. try:
  57. reqs = self._top_level_assigns[child_node.value.name]
  58. except KeyError:
  59. raise CouldNotParseRequirements
  60. else:
  61. if isinstance(reqs, (List, Tuple)):
  62. found_requirements += self._get_list_value(reqs)
  63. continue
  64. # otherwise it's something funky and we can't handle it
  65. raise CouldNotParseRequirements
  66. # if we've fallen off the bottom with nothing in our list of requirements,
  67. # we simply didn't find anything useful
  68. if len(found_requirements) > 0:
  69. return found_requirements
  70. raise CouldNotParseRequirements
  71. def from_setup_py(setup_file: Union[str, Path]):
  72. if isinstance(setup_file, str):
  73. setup_file = Path(setup_file)
  74. try:
  75. ast = AstroidBuilder(MANAGER).string_build(setup_file.open().read())
  76. except (SyntaxError, AstroidBuildingException, AstroidSyntaxError):
  77. # if the setup file is broken, we can't do much about that...
  78. raise CouldNotParseRequirements
  79. walker = SetupWalker(ast)
  80. requirements = []
  81. for req in walker.get_requires():
  82. requirements.append(DetectedRequirement.parse(req, setup_file))
  83. return [requirement for requirement in requirements if requirement is not None]