_normalization.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. """
  2. Helpers for normalization as expected in wheel/sdist/module file names
  3. and core metadata
  4. """
  5. import re
  6. from pathlib import Path
  7. from typing import Union
  8. from .extern import packaging
  9. from .warnings import SetuptoolsDeprecationWarning
  10. _Path = Union[str, Path]
  11. # https://packaging.python.org/en/latest/specifications/core-metadata/#name
  12. _VALID_NAME = re.compile(r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.I)
  13. _UNSAFE_NAME_CHARS = re.compile(r"[^A-Z0-9.]+", re.I)
  14. _NON_ALPHANUMERIC = re.compile(r"[^A-Z0-9]+", re.I)
  15. def safe_identifier(name: str) -> str:
  16. """Make a string safe to be used as Python identifier.
  17. >>> safe_identifier("12abc")
  18. '_12abc'
  19. >>> safe_identifier("__editable__.myns.pkg-78.9.3_local")
  20. '__editable___myns_pkg_78_9_3_local'
  21. """
  22. safe = re.sub(r'\W|^(?=\d)', '_', name)
  23. assert safe.isidentifier()
  24. return safe
  25. def safe_name(component: str) -> str:
  26. """Escape a component used as a project name according to Core Metadata.
  27. >>> safe_name("hello world")
  28. 'hello-world'
  29. >>> safe_name("hello?world")
  30. 'hello-world'
  31. """
  32. # See pkg_resources.safe_name
  33. return _UNSAFE_NAME_CHARS.sub("-", component)
  34. def safe_version(version: str) -> str:
  35. """Convert an arbitrary string into a valid version string.
  36. >>> safe_version("1988 12 25")
  37. '1988.12.25'
  38. >>> safe_version("v0.2.1")
  39. '0.2.1'
  40. >>> safe_version("v0.2?beta")
  41. '0.2b0'
  42. >>> safe_version("v0.2 beta")
  43. '0.2b0'
  44. >>> safe_version("ubuntu lts")
  45. Traceback (most recent call last):
  46. ...
  47. setuptools.extern.packaging.version.InvalidVersion: Invalid version: 'ubuntu.lts'
  48. """
  49. v = version.replace(' ', '.')
  50. try:
  51. return str(packaging.version.Version(v))
  52. except packaging.version.InvalidVersion:
  53. attempt = _UNSAFE_NAME_CHARS.sub("-", v)
  54. return str(packaging.version.Version(attempt))
  55. def best_effort_version(version: str) -> str:
  56. """Convert an arbitrary string into a version-like string.
  57. >>> best_effort_version("v0.2 beta")
  58. '0.2b0'
  59. >>> import warnings
  60. >>> warnings.simplefilter("ignore", category=SetuptoolsDeprecationWarning)
  61. >>> best_effort_version("ubuntu lts")
  62. 'ubuntu.lts'
  63. """
  64. # See pkg_resources.safe_version
  65. try:
  66. return safe_version(version)
  67. except packaging.version.InvalidVersion:
  68. SetuptoolsDeprecationWarning.emit(
  69. f"Invalid version: {version!r}.",
  70. f"""
  71. Version {version!r} is not valid according to PEP 440.
  72. Please make sure to specify a valid version for your package.
  73. Also note that future releases of setuptools may halt the build process
  74. if an invalid version is given.
  75. """,
  76. see_url="https://peps.python.org/pep-0440/",
  77. due_date=(2023, 9, 26), # See setuptools/dist _validate_version
  78. )
  79. v = version.replace(' ', '.')
  80. return safe_name(v)
  81. def safe_extra(extra: str) -> str:
  82. """Normalize extra name according to PEP 685
  83. >>> safe_extra("_FrIeNdLy-._.-bArD")
  84. 'friendly-bard'
  85. >>> safe_extra("FrIeNdLy-._.-bArD__._-")
  86. 'friendly-bard'
  87. """
  88. return _NON_ALPHANUMERIC.sub("-", extra).strip("-").lower()
  89. def filename_component(value: str) -> str:
  90. """Normalize each component of a filename (e.g. distribution/version part of wheel)
  91. Note: ``value`` needs to be already normalized.
  92. >>> filename_component("my-pkg")
  93. 'my_pkg'
  94. """
  95. return value.replace("-", "_").strip("_")
  96. def safer_name(value: str) -> str:
  97. """Like ``safe_name`` but can be used as filename component for wheel"""
  98. # See bdist_wheel.safer_name
  99. return filename_component(safe_name(value))
  100. def safer_best_effort_version(value: str) -> str:
  101. """Like ``best_effort_version`` but can be used as filename component for wheel"""
  102. # See bdist_wheel.safer_verion
  103. # TODO: Replace with only safe_version in the future (no need for best effort)
  104. return filename_component(best_effort_version(value))