__meta__.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. """Meta related things."""
  2. from __future__ import annotations
  3. from collections import namedtuple
  4. import re
  5. RE_VER = re.compile(
  6. r'''(?x)
  7. (?P<major>\d+)(?:\.(?P<minor>\d+))?(?:\.(?P<micro>\d+))?
  8. (?:(?P<type>a|b|rc)(?P<pre>\d+))?
  9. (?:\.post(?P<post>\d+))?
  10. (?:\.dev(?P<dev>\d+))?
  11. '''
  12. )
  13. REL_MAP = {
  14. ".dev": "",
  15. ".dev-alpha": "a",
  16. ".dev-beta": "b",
  17. ".dev-candidate": "rc",
  18. "alpha": "a",
  19. "beta": "b",
  20. "candidate": "rc",
  21. "final": ""
  22. }
  23. DEV_STATUS = {
  24. ".dev": "2 - Pre-Alpha",
  25. ".dev-alpha": "2 - Pre-Alpha",
  26. ".dev-beta": "2 - Pre-Alpha",
  27. ".dev-candidate": "2 - Pre-Alpha",
  28. "alpha": "3 - Alpha",
  29. "beta": "4 - Beta",
  30. "candidate": "4 - Beta",
  31. "final": "5 - Production/Stable"
  32. }
  33. PRE_REL_MAP = {"a": 'alpha', "b": 'beta', "rc": 'candidate'}
  34. class Version(namedtuple("Version", ["major", "minor", "micro", "release", "pre", "post", "dev"])):
  35. """
  36. Get the version (PEP 440).
  37. A biased approach to the PEP 440 semantic version.
  38. Provides a tuple structure which is sorted for comparisons `v1 > v2` etc.
  39. (major, minor, micro, release type, pre-release build, post-release build, development release build)
  40. Release types are named in is such a way they are comparable with ease.
  41. Accessors to check if a development, pre-release, or post-release build. Also provides accessor to get
  42. development status for setup files.
  43. How it works (currently):
  44. - You must specify a release type as either `final`, `alpha`, `beta`, or `candidate`.
  45. - To define a development release, you can use either `.dev`, `.dev-alpha`, `.dev-beta`, or `.dev-candidate`.
  46. The dot is used to ensure all development specifiers are sorted before `alpha`.
  47. You can specify a `dev` number for development builds, but do not have to as implicit development releases
  48. are allowed.
  49. - You must specify a `pre` value greater than zero if using a prerelease as this project (not PEP 440) does not
  50. allow implicit prereleases.
  51. - You can optionally set `post` to a value greater than zero to make the build a post release. While post releases
  52. are technically allowed in prereleases, it is strongly discouraged, so we are rejecting them. It should be
  53. noted that we do not allow `post0` even though PEP 440 does not restrict this. This project specifically
  54. does not allow implicit post releases.
  55. - It should be noted that we do not support epochs `1!` or local versions `+some-custom.version-1`.
  56. Acceptable version releases:
  57. ```
  58. Version(1, 0, 0, "final") 1.0
  59. Version(1, 2, 0, "final") 1.2
  60. Version(1, 2, 3, "final") 1.2.3
  61. Version(1, 2, 0, ".dev-alpha", pre=4) 1.2a4
  62. Version(1, 2, 0, ".dev-beta", pre=4) 1.2b4
  63. Version(1, 2, 0, ".dev-candidate", pre=4) 1.2rc4
  64. Version(1, 2, 0, "final", post=1) 1.2.post1
  65. Version(1, 2, 3, ".dev") 1.2.3.dev0
  66. Version(1, 2, 3, ".dev", dev=1) 1.2.3.dev1
  67. ```
  68. """
  69. def __new__(
  70. cls,
  71. major: int, minor: int, micro: int, release: str = "final",
  72. pre: int = 0, post: int = 0, dev: int = 0
  73. ) -> Version:
  74. """Validate version info."""
  75. # Ensure all parts are positive integers.
  76. for value in (major, minor, micro, pre, post):
  77. if not (isinstance(value, int) and value >= 0):
  78. raise ValueError("All version parts except 'release' should be integers.")
  79. if release not in REL_MAP:
  80. raise ValueError(f"'{release}' is not a valid release type.")
  81. # Ensure valid pre-release (we do not allow implicit pre-releases).
  82. if ".dev-candidate" < release < "final":
  83. if pre == 0:
  84. raise ValueError("Implicit pre-releases not allowed.")
  85. elif dev:
  86. raise ValueError("Version is not a development release.")
  87. elif post:
  88. raise ValueError("Post-releases are not allowed with pre-releases.")
  89. # Ensure valid development or development/pre release
  90. elif release < "alpha":
  91. if release > ".dev" and pre == 0:
  92. raise ValueError("Implicit pre-release not allowed.")
  93. elif post:
  94. raise ValueError("Post-releases are not allowed with pre-releases.")
  95. # Ensure a valid normal release
  96. else:
  97. if pre:
  98. raise ValueError("Version is not a pre-release.")
  99. elif dev:
  100. raise ValueError("Version is not a development release.")
  101. return super().__new__(cls, major, minor, micro, release, pre, post, dev)
  102. def _is_pre(self) -> bool:
  103. """Is prerelease."""
  104. return bool(self.pre > 0)
  105. def _is_dev(self) -> bool:
  106. """Is development."""
  107. return bool(self.release < "alpha")
  108. def _is_post(self) -> bool:
  109. """Is post."""
  110. return bool(self.post > 0)
  111. def _get_dev_status(self) -> str: # pragma: no cover
  112. """Get development status string."""
  113. return DEV_STATUS[self.release]
  114. def _get_canonical(self) -> str:
  115. """Get the canonical output string."""
  116. # Assemble major, minor, micro version and append `pre`, `post`, or `dev` if needed..
  117. if self.micro == 0:
  118. ver = f"{self.major}.{self.minor}"
  119. else:
  120. ver = f"{self.major}.{self.minor}.{self.micro}"
  121. if self._is_pre():
  122. ver += f'{REL_MAP[self.release]}{self.pre}'
  123. if self._is_post():
  124. ver += f".post{self.post}"
  125. if self._is_dev():
  126. ver += f".dev{self.dev}"
  127. return ver
  128. def parse_version(ver: str) -> Version:
  129. """Parse version into a comparable Version tuple."""
  130. m = RE_VER.match(ver)
  131. if m is None:
  132. raise ValueError(f"'{ver}' is not a valid version")
  133. # Handle major, minor, micro
  134. major = int(m.group('major'))
  135. minor = int(m.group('minor')) if m.group('minor') else 0
  136. micro = int(m.group('micro')) if m.group('micro') else 0
  137. # Handle pre releases
  138. if m.group('type'):
  139. release = PRE_REL_MAP[m.group('type')]
  140. pre = int(m.group('pre'))
  141. else:
  142. release = "final"
  143. pre = 0
  144. # Handle development releases
  145. dev = m.group('dev') if m.group('dev') else 0
  146. if m.group('dev'):
  147. dev = int(m.group('dev'))
  148. release = '.dev-' + release if pre else '.dev'
  149. else:
  150. dev = 0
  151. # Handle post
  152. post = int(m.group('post')) if m.group('post') else 0
  153. return Version(major, minor, micro, release, pre, post, dev)
  154. __version_info__ = Version(2, 8, 3, "final")
  155. __version__ = __version_info__._get_canonical()