from dataclasses import dataclass
from typing import Optional, Tuple
import pandas as pd
import semver
[docs]@dataclass
class Version:
major: Optional[int] = None
minor: Optional[int] = None
patch: Optional[int] = None
prerelease: Optional[str] = None
build: Optional[str] = None
full_str: Optional[str] = None
is_semver: bool = False
def __post_init__(self):
self._validate()
def _validate(self):
attrs = [
"major",
"minor",
"patch",
"prerelease",
"build",
"full_str",
]
if all([getattr(self, attr) is None for attr in attrs]):
raise ValueError(
"must provide some version information, all attributes are None"
)
@property
def semantic_version(self) -> semver.VersionInfo:
if not self.is_semver:
raise ValueError("is_semver=False, cannot create semver object")
return semver.VersionInfo(
major=self.major,
minor=self.minor,
patch=self.patch,
prerelease=self.prerelease,
build=self.build,
)
def __eq__(self, other):
if not self.is_semver or isinstance(other, Version) and not other.is_semver:
return super().__eq__(other)
if isinstance(other, Version):
compare = other.semantic_version
else:
compare = other
return self.semantic_version == compare
def __lt__(self, other):
if not self.is_semver:
raise ValueError("cannot compare non-semantic versions")
if isinstance(other, Version):
compare = other.semantic_version
else:
compare = other
return self.semantic_version < compare
def __le__(self, other):
if not self.is_semver:
raise ValueError("cannot compare non-semantic versions")
if isinstance(other, Version):
compare = other.semantic_version
else:
compare = other
return self.semantic_version <= compare
def __gt__(self, other):
if not self.is_semver:
raise ValueError("cannot compare non-semantic versions")
if isinstance(other, Version):
compare = other.semantic_version
else:
compare = other
return self.semantic_version > compare
def __ge__(self, other):
if not self.is_semver:
raise ValueError("cannot compare non-semantic versions")
if isinstance(other, Version):
compare = other.semantic_version
else:
compare = other
return self.semantic_version >= compare
[docs] @classmethod
def from_str(cls, version: str) -> "Version":
full_str = version
if version.startswith("v") and version[1].isdigit():
# Looks like could be semantic versioning but starting with v, strip the v
# but keep it in full_str
version = version[1:]
sem_version: Optional[semver.VersionInfo] = None
try:
sem_version = semver.VersionInfo.parse(version)
except ValueError as e:
if "not valid SemVer string" not in str(e):
raise e
if sem_version is not None:
return cls.from_semver_version(sem_version, full_str=full_str)
# Not semantic versioning, don't parse it
return cls(full_str=full_str)
[docs] @classmethod
def from_semver_version(
cls, version: semver.VersionInfo, full_str: Optional[str] = None
) -> "Version":
return cls(
major=version.major,
minor=version.minor,
patch=version.patch,
prerelease=version.prerelease,
build=version.build,
full_str=full_str,
is_semver=True,
)
def __str__(self) -> str:
if self.full_str is not None:
return self.full_str
return str(self.semantic_version)
[docs]def major_minor_patch(
version: Version,
) -> Tuple[Optional[int], Optional[int], Optional[int]]:
if not version.is_semver:
return None, None, None
return version.major, version.minor, version.patch
[docs]def add_major_minor_patch_to_df(df: pd.DataFrame, version_col: str = "tag_name"):
df["Version"] = df[version_col].apply(lambda tag: Version.from_str(tag))
df["Major"], df["Minor"], df["Patch"] = zip(*df["Version"].map(major_minor_patch))
[docs]def add_major_minor_patch_changed_to_df(df: pd.DataFrame):
df["Max Version"] = df["Version"].cummax()
df["Max Major"], df["Max Minor"], df["Max Patch"] = zip(
*df["Max Version"].map(major_minor_patch)
)
for col in ["Major", "Minor", "Patch"]:
df[f"Max {col}"] = df[f"Max {col}"].shift()
df[f"{col} Diff"] = df[col] - df[f"Max {col}"]
df["Major Changed"] = False
df.loc[
(~pd.isnull(df["Major Diff"])) & (df["Major Diff"] != 0), "Major Changed"
] = True
df["Minor Changed"] = False
df.loc[
(~pd.isnull(df["Minor Diff"]))
& (df["Minor Diff"] != 0)
& (~df["Major Changed"]),
"Minor Changed",
] = True
df["Patch Changed"] = False
df.loc[
(~pd.isnull(df["Patch Diff"]))
& (df["Patch Diff"] != 0)
& (~df["Minor Changed"])
& (~df["Major Changed"]),
"Patch Changed",
] = True