Skip to content

Commit

Permalink
build: remove versioningit build-req from sdist
Browse files Browse the repository at this point in the history
- Replace `tool.versioningit.onbuild` hook with a custom implementation
  which replaces the entire `streamlink._version` module (similar to
  before) and which additionally removes `versioningit` from the
  `build-system.requires` field in `pyproject.toml` and which sets
  a static version string in `setup.py`
- Rewrite `streamlink._version` module
- Add and update comments
- Update docs
- Add tests
  • Loading branch information
bastimeyer committed Oct 22, 2023
1 parent 80a7645 commit 1376283
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 12 deletions.
81 changes: 81 additions & 0 deletions build_backend/onbuild.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import re
from contextlib import contextmanager
from pathlib import Path
from typing import Any, Dict, Generator, Generic, TypeVar, Union


# noinspection PyUnusedLocal
def onbuild(
build_dir: Union[str, Path],
is_source: bool,
template_fields: Dict[str, Any],
params: Dict[str, Any],
):
"""
Remove the ``versioningit`` build-requirement from Streamlink's source distribution.
Also set the static version string in the :mod:`streamlink._version` module when building the sdist/bdist.
The version string already gets set by ``versioningit`` when building, so the sdist doesn't need to have
``versioningit`` added as a build-requirement. Previously, the generated version string was only applied
to the :mod:`streamlink._version` module while ``versioningit`` was still set as a build-requirement.
This custom onbuild hook gets called via the ``tool.versioningit.onbuild`` config in ``pyproject.toml``,
since ``versioningit`` does only support modifying one file via its default onbuild hook configuration.
"""

base_dir: Path = Path(build_dir).resolve()
pkg_dir: Path = base_dir / "src" if is_source else base_dir
version: str = template_fields["version"]
cmproxy: Proxy[str]

# Remove versioningit from ``build-system.requires`` in ``pyproject.toml``
if is_source:
with update_file(base_dir / "pyproject.toml") as cmproxy:
cmproxy.set(re.sub(
r"^(\s*)(\"versioningit\b.+?\",).*$",
"\\1# \\2",
cmproxy.get(),
flags=re.MULTILINE,
count=1,
))

# Set the static version string that gets passed directly to setuptools via ``setup.py``.
# This is much easier compared to adding the ``project.version`` field and removing "version" from ``project.dynamic``
# in ``pyproject.toml``.
if is_source:
with update_file(base_dir / "setup.py") as cmproxy:
cmproxy.set(re.sub(
r"^(\s*)# (version=\"\",).*$",
f"\\1version=\"{version}\",",
cmproxy.get(),
flags=re.MULTILINE,
count=1,
))

# Overwrite the entire ``streamlink._version`` module
with update_file(pkg_dir / "streamlink" / "_version.py") as cmproxy:
cmproxy.set(f"__version__ = \"{version}\"\n")


TProxyItem = TypeVar("TProxyItem")


class Proxy(Generic[TProxyItem]):
def __init__(self, data: TProxyItem):
self._data = data

def get(self) -> TProxyItem:
return self._data

def set(self, data: TProxyItem) -> None:
self._data = data


@contextmanager
def update_file(file: Path) -> Generator[Proxy[str], None, None]:
with file.open("r+", encoding="utf-8") as fh:
proxy = Proxy(fh.read())
yield proxy
fh.seek(0)
fh.write(proxy.get())
fh.truncate()
66 changes: 66 additions & 0 deletions build_backend/test_onbuild.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import re
import shutil
from pathlib import Path

import pytest

from build_backend.onbuild import onbuild


PROJECT_ROOT = Path(__file__).parents[1]


@pytest.fixture()
def template_fields(request: pytest.FixtureRequest) -> dict:
template_fields = {
"version": "1.2.3+fake",
}
template_fields.update(getattr(request, "param", {}))

return template_fields


@pytest.fixture(autouse=True)
def build(request: pytest.FixtureRequest, tmp_path: Path, template_fields: dict) -> Path:
param = getattr(request, "param", {})
is_source = param.get("is_source", True)
pkg_dir = tmp_path / "src" if is_source else tmp_path

(pkg_dir / "streamlink").mkdir(parents=True)
shutil.copy(PROJECT_ROOT / "pyproject.toml", tmp_path / "pyproject.toml")
shutil.copy(PROJECT_ROOT / "setup.py", tmp_path / "setup.py")
shutil.copy(PROJECT_ROOT / "src" / "streamlink" / "_version.py", pkg_dir / "streamlink" / "_version.py")

onbuild(tmp_path, is_source, template_fields, {})

return tmp_path


@pytest.mark.parametrize("build", [pytest.param({"is_source": True}, id="is_source=True")], indirect=True)
def test_sdist(build: Path):
assert re.search(
r"^(\s*)# (\"versioningit\b.+?\",).*$",
(build / "pyproject.toml").read_text(encoding="utf-8"),
re.MULTILINE,
), "versioningit is not a build-requirement"
assert re.search(
r"^(\s*)(version=\"1\.2\.3\+fake\",).*$",
(build / "setup.py").read_text(encoding="utf-8"),
re.MULTILINE,
), "setup() call defines a static version string"
assert (build / "src" / "streamlink" / "_version.py").read_text(encoding="utf-8") \
== "__version__ = \"1.2.3+fake\"\n", \
"streamlink._version exports a static version string"


@pytest.mark.parametrize("build", [pytest.param({"is_source": False}, id="is_source=False")], indirect=True)
def test_bdist(build: Path):
assert (build / "pyproject.toml").read_text(encoding="utf-8") \
== (PROJECT_ROOT / "pyproject.toml").read_text(encoding="utf-8"), \
"Doesn't touch pyproject.toml (irrelevant for non-sdist)"
assert (build / "setup.py").read_text(encoding="utf-8") \
== (PROJECT_ROOT / "setup.py").read_text(encoding="utf-8"), \
"Doesn't touch setup.py (irrelevant for non-sdist)"
assert (build / "streamlink" / "_version.py").read_text(encoding="utf-8") \
== "__version__ = \"1.2.3+fake\"\n", \
"streamlink._version exports a static version string"
11 changes: 10 additions & 1 deletion docs/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,14 @@ To install Streamlink from source you will need these dependencies.
Since :ref:`4.0.0 <changelog:streamlink 4.0.0 (2022-05-01)>`,
Streamlink defines a `build system <pyproject.toml_>`__ according to `PEP-517`_ / `PEP-518`_.

.. warning::

Do not build Streamlink from tarballs generated by GitHub from (tagged) git commits,
as they are lacking the release version string.

Instead, install from Streamlink's signed source-distribution tarballs which are uploaded to PyPI and GitHub releases,
or from the cloned git repository.

.. list-table::
:header-rows: 1
:class: table-custom-layout table-custom-layout-dependencies
Expand All @@ -421,7 +429,8 @@ Streamlink defines a `build system <pyproject.toml_>`__ according to `PEP-517`_
* - build
- `versioningit`_
- At least version **2.0.0** |br|
Used for generating the version string from git when building, or when running in an editable install
Used for generating the version string from git when building, or when running in an editable install.
Not needed when building wheels and installing from the source distribution.
* - runtime
- `certifi`_
- Used for loading the CA bundle extracted from the Mozilla Included CA Certificate List
Expand Down
12 changes: 9 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
requires = [
"setuptools >=64",
"wheel",
"versioningit >=2.0.0, <3",
# The versioningit build-requirement gets removed from the source distribution,
# as the version string is already built into it (see the onbuild versioningit hook):
"versioningit >=2.0.0, <3", # disabled in sdist
]
# setuptools build-backend override
# https://setuptools.pypa.io/en/stable/build_meta.html
Expand Down Expand Up @@ -93,6 +95,10 @@ streamlink = [

# https://versioningit.readthedocs.io/en/stable/index.html
[tool.versioningit]
# Packagers: don't patch the `default-version` string while using the tarball built by GitHub from the tagged git commit!
# Instead, use Streamlink's signed source distribution as package source, which has the correct version string built in.
# This fallback `default-version` string is only used when not building from the sdist or a git repo with at least one tag.
# See the versioningit comment at the very top of this file!
default-version = "0.0.0+unknown"

[tool.versioningit.vcs]
Expand All @@ -107,8 +113,8 @@ distance-dirty = "{base_version}+{distance}.{vcs}{rev}.dirty"
method = "null"

[tool.versioningit.onbuild]
source-file = "src/streamlink/_version.py"
build-file = "streamlink/_version.py"
# When building the sdist or wheel, remove versioningit build-requirement and set the static version string
method = { module = "build_backend.onbuild", value = "onbuild" }


# https://docs.pytest.org/en/latest/reference/customize.html#configuration
Expand Down
9 changes: 8 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,17 @@ def is_wheel_for_windows():

if __name__ == "__main__":
from setuptools import setup # type: ignore[import]
from versioningit import get_cmdclasses

try:
# versioningit is only required when building from git (see pyproject.toml)
from versioningit import get_cmdclasses
except ImportError: # pragma: no cover
def get_cmdclasses(): # type: ignore
return {}

setup(
cmdclass=get_cmdclasses(),
entry_points=entry_points,
data_files=data_files,
# version="", # static version string template, uncommented and substituted by versioningit's onbuild hook
)
14 changes: 7 additions & 7 deletions src/streamlink/_version.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# Always get the current version in "editable" installs
# `pip install -e .` / `python setup.py develop`
# This module will get replaced by versioningit when building a source distribution
# and instead of trying to get the version string from git, a static version string will be set

def _get_version() -> str:
"""
Get the current version from git in "editable" installs
"""
from pathlib import Path
from versioningit import get_version
import streamlink

return get_version(
project_dir=Path(streamlink.__file__).parents[2],
)
return get_version(project_dir=Path(streamlink.__file__).parents[2])


# The following _get_version() call will get replaced by versioningit with a static version string when building streamlink
# `pip install .` / `pip wheel .` / `python setup.py build` / `python setup.py bdist_wheel` / etc.
__version__ = _get_version()

0 comments on commit 1376283

Please sign in to comment.