detect.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import re
  2. from pathlib import Path
  3. from typing import List, Union
  4. import toml
  5. from .exceptions import CouldNotParseRequirements, RequirementsNotFound
  6. from .handle_setup import from_setup_py
  7. from .poetry_semver import parse_constraint
  8. from .requirement import DetectedRequirement
  9. __all__ = [
  10. "find_requirements",
  11. "from_requirements_txt",
  12. "from_requirements_dir",
  13. "from_requirements_blob",
  14. "from_pyproject_toml",
  15. "from_setup_py",
  16. "RequirementsNotFound",
  17. "CouldNotParseRequirements",
  18. ]
  19. _PIP_OPTIONS = (
  20. "-i",
  21. "--index-url",
  22. "--extra-index-url",
  23. "--no-index",
  24. "-f",
  25. "--find-links",
  26. "-r",
  27. )
  28. P = Union[str, Path]
  29. def find_requirements(path: P) -> List[DetectedRequirement]:
  30. """
  31. This method tries to determine the requirements of a particular project
  32. by inspecting the possible places that they could be defined.
  33. It will attempt, in order:
  34. 1) to parse setup.py in the root for an install_requires value
  35. 2) to read a requirements.txt file or a requirements.pip in the root
  36. 3) to read all .txt files in a folder called 'requirements' in the root
  37. 4) to read files matching "*requirements*.txt" and "*reqs*.txt" in the root,
  38. excluding any starting or ending with 'test'
  39. If one of these succeeds, then a list of pkg_resources.Requirement's
  40. will be returned. If none can be found, then a RequirementsNotFound
  41. will be raised
  42. """
  43. requirements = []
  44. if isinstance(path, str):
  45. path = Path(path)
  46. setup_py = path / "setup.py"
  47. if setup_py.exists() and setup_py.is_file():
  48. try:
  49. requirements = from_setup_py(setup_py)
  50. requirements.sort()
  51. return requirements
  52. except CouldNotParseRequirements:
  53. pass
  54. poetry_toml = path / "pyproject.toml"
  55. if poetry_toml.exists() and poetry_toml.is_file():
  56. try:
  57. requirements = from_pyproject_toml(poetry_toml)
  58. if len(requirements) > 0:
  59. requirements.sort()
  60. return requirements
  61. except CouldNotParseRequirements:
  62. pass
  63. for reqfile_name in ("requirements.txt", "requirements.pip"):
  64. reqfile = path / reqfile_name
  65. if reqfile.exists and reqfile.is_file():
  66. try:
  67. requirements += from_requirements_txt(reqfile)
  68. except CouldNotParseRequirements as e:
  69. pass
  70. requirements_dir = path / "requirements"
  71. if requirements_dir.exists() and requirements_dir.is_dir():
  72. from_dir = from_requirements_dir(requirements_dir)
  73. if from_dir is not None:
  74. requirements += from_dir
  75. from_blob = from_requirements_blob(path)
  76. if from_blob is not None:
  77. requirements += from_blob
  78. requirements = list(set(requirements))
  79. if len(requirements) > 0:
  80. requirements.sort()
  81. return requirements
  82. raise RequirementsNotFound
  83. def from_pyproject_toml(toml_file: P) -> List[DetectedRequirement]:
  84. requirements = []
  85. if isinstance(toml_file, str):
  86. toml_file = Path(toml_file)
  87. parsed = toml.load(toml_file)
  88. poetry_section = parsed.get("tool", {}).get("poetry", {})
  89. dependencies = poetry_section.get("dependencies", {})
  90. dependencies.update(poetry_section.get("dev-dependencies", {}))
  91. for name, spec in dependencies.items():
  92. if name.lower() == "python":
  93. continue
  94. if isinstance(spec, dict):
  95. if "version" in spec:
  96. spec = spec["version"]
  97. else:
  98. req = DetectedRequirement.parse(f"{name}", toml_file)
  99. if req is not None:
  100. requirements.append(req)
  101. continue
  102. parsed_spec = str(parse_constraint(spec))
  103. if "," not in parsed_spec and "<" not in parsed_spec and ">" not in parsed_spec and "=" not in parsed_spec:
  104. parsed_spec = f"=={parsed_spec}"
  105. req = DetectedRequirement.parse(f"{name}{parsed_spec}", toml_file)
  106. if req is not None:
  107. requirements.append(req)
  108. return requirements
  109. def from_requirements_txt(requirements_file: P) -> List[DetectedRequirement]:
  110. # see http://www.pip-installer.org/en/latest/logic.html
  111. requirements = []
  112. if isinstance(requirements_file, str):
  113. requirements_file = Path(requirements_file)
  114. with requirements_file.open() as f:
  115. for req in f.readlines():
  116. if req.strip() == "":
  117. # empty line
  118. continue
  119. if req.strip().startswith("#"):
  120. # this is a comment
  121. continue
  122. if req.strip().split()[0] in _PIP_OPTIONS:
  123. # this is a pip option
  124. continue
  125. detected = DetectedRequirement.parse(req, requirements_file)
  126. if detected is None:
  127. continue
  128. requirements.append(detected)
  129. return requirements
  130. def from_requirements_dir(path: P) -> List[DetectedRequirement]:
  131. requirements = []
  132. if isinstance(path, str):
  133. path = Path(path)
  134. for entry in path.iterdir():
  135. if not entry.is_file():
  136. continue
  137. if entry.name.endswith(".txt") or entry.name.endswith(".pip"):
  138. requirements += from_requirements_txt(entry)
  139. return list(set(requirements))
  140. def from_requirements_blob(path: P) -> List[DetectedRequirement]:
  141. requirements = []
  142. if isinstance(path, str):
  143. path = Path(path)
  144. for entry in path.iterdir():
  145. if not entry.is_file():
  146. continue
  147. m = re.match(r"^(\w*)req(uirement)?s(\w*)\.txt$", entry.name)
  148. if m is None:
  149. continue
  150. if m.group(1).startswith("test") or m.group(3).endswith("test"):
  151. continue
  152. requirements += from_requirements_txt(entry)
  153. return requirements