extra_validations.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. """The purpose of this module is implement PEP 621 validations that are
  2. difficult to express as a JSON Schema (or that are not supported by the current
  3. JSON Schema library).
  4. """
  5. import collections
  6. import itertools
  7. from inspect import cleandoc
  8. from typing import Generator, Iterable, Mapping, TypeVar
  9. from .error_reporting import ValidationError
  10. T = TypeVar("T", bound=Mapping)
  11. class RedefiningStaticFieldAsDynamic(ValidationError):
  12. _DESC = """According to PEP 621:
  13. Build back-ends MUST raise an error if the metadata specifies a field
  14. statically as well as being listed in dynamic.
  15. """
  16. __doc__ = _DESC
  17. _URL = (
  18. "https://packaging.python.org/en/latest/specifications/pyproject-toml/#dynamic"
  19. )
  20. class IncludedDependencyGroupMustExist(ValidationError):
  21. _DESC = """An included dependency group must exist and must not be cyclic.
  22. """
  23. __doc__ = _DESC
  24. _URL = "https://peps.python.org/pep-0735/"
  25. class ImportNameCollision(ValidationError):
  26. _DESC = """According to PEP 794:
  27. All import-names and import-namespaces items must be unique.
  28. """
  29. __doc__ = _DESC
  30. _URL = "https://peps.python.org/pep-0794/"
  31. class ImportNameMissing(ValidationError):
  32. _DESC = """According to PEP 794:
  33. An import name must have all parents listed.
  34. """
  35. __doc__ = _DESC
  36. _URL = "https://peps.python.org/pep-0794/"
  37. def validate_project_dynamic(pyproject: T) -> T:
  38. project_table = pyproject.get("project", {})
  39. dynamic = project_table.get("dynamic", [])
  40. for field in dynamic:
  41. if field in project_table:
  42. raise RedefiningStaticFieldAsDynamic(
  43. message=f"You cannot provide a value for `project.{field}` and "
  44. "list it under `project.dynamic` at the same time",
  45. value={
  46. field: project_table[field],
  47. "...": " # ...",
  48. "dynamic": dynamic,
  49. },
  50. name=f"data.project.{field}",
  51. definition={
  52. "description": cleandoc(RedefiningStaticFieldAsDynamic._DESC),
  53. "see": RedefiningStaticFieldAsDynamic._URL,
  54. },
  55. rule="PEP 621",
  56. )
  57. return pyproject
  58. def validate_include_depenency(pyproject: T) -> T:
  59. dependency_groups = pyproject.get("dependency-groups", {})
  60. for key, value in dependency_groups.items():
  61. for each in value:
  62. if (
  63. isinstance(each, dict)
  64. and (include_group := each.get("include-group"))
  65. and include_group not in dependency_groups
  66. ):
  67. raise IncludedDependencyGroupMustExist(
  68. message=f"The included dependency group {include_group} doesn't exist",
  69. value=each,
  70. name=f"data.dependency_groups.{key}",
  71. definition={
  72. "description": cleandoc(IncludedDependencyGroupMustExist._DESC),
  73. "see": IncludedDependencyGroupMustExist._URL,
  74. },
  75. rule="PEP 735",
  76. )
  77. # TODO: check for `include-group` cycles (can be conditional to graphlib)
  78. return pyproject
  79. def _remove_private(items: Iterable[str]) -> Generator[str, None, None]:
  80. for item in items:
  81. yield item.partition(";")[0].rstrip()
  82. def validate_import_name_issues(pyproject: T) -> T:
  83. project = pyproject.get("project", {})
  84. import_names = collections.Counter(_remove_private(project.get("import-names", [])))
  85. import_namespaces = collections.Counter(
  86. _remove_private(project.get("import-namespaces", []))
  87. )
  88. duplicated = [k for k, v in (import_names + import_namespaces).items() if v > 1]
  89. if duplicated:
  90. raise ImportNameCollision(
  91. message="Duplicated names are not allowed in import-names/import-namespaces",
  92. value=duplicated,
  93. name="data.project.importnames(paces)",
  94. definition={
  95. "description": cleandoc(ImportNameCollision._DESC),
  96. "see": ImportNameCollision._URL,
  97. },
  98. rule="PEP 794",
  99. )
  100. names = frozenset(import_names + import_namespaces)
  101. for name in names:
  102. for parent in itertools.accumulate(
  103. name.split(".")[:-1], lambda a, b: f"{a}.{b}"
  104. ):
  105. if parent not in names:
  106. raise ImportNameMissing(
  107. message="All parents of an import name must also be listed in import-namespace/import-names",
  108. value=name,
  109. name="data.project.importnames(paces)",
  110. definition={
  111. "description": cleandoc(ImportNameMissing._DESC),
  112. "see": ImportNameMissing._URL,
  113. },
  114. rule="PEP 794",
  115. )
  116. return pyproject
  117. EXTRA_VALIDATIONS = (
  118. validate_project_dynamic,
  119. validate_include_depenency,
  120. validate_import_name_issues,
  121. )