diff --git a/.github/workflows/packages.yaml b/.github/workflows/packages.yaml index e5e1b9876..581b888d2 100644 --- a/.github/workflows/packages.yaml +++ b/.github/workflows/packages.yaml @@ -11,6 +11,8 @@ jobs: runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2.3.4 + with: + fetch-depth: 0 - name: Set up Python 3.8 uses: actions/setup-python@v2.2.2 with: @@ -40,6 +42,8 @@ jobs: runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2.3.4 + with: + fetch-depth: 0 - name: Set up Python 3.8 uses: actions/setup-python@v2.2.2 with: @@ -69,6 +73,8 @@ jobs: runs-on: macos-10.15 steps: - uses: actions/checkout@v2.3.4 + with: + fetch-depth: 0 - name: Set up Python 3.8 uses: actions/setup-python@v2.2.2 with: @@ -101,6 +107,8 @@ jobs: runs-on: windows-2019 steps: - uses: actions/checkout@v2.3.4 + with: + fetch-depth: 0 - name: Set up Python 3.8 uses: actions/setup-python@v2.2.2 with: @@ -129,6 +137,8 @@ jobs: runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2.3.4 + with: + fetch-depth: 0 - name: Set up Python 3.7 uses: actions/setup-python@v2.2.2 with: @@ -136,7 +146,9 @@ jobs: - name: Install run: pip install ".[all,tests]" - name: Build packages - run: ./scripts/build_package.sh + run: | + pip install -U pip + ./scripts/build_package.sh - name: Publish packages to PyPI if: github.event_name == 'release' uses: pypa/gh-action-pypi-publish@master diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 7456ea9e5..71ece2d94 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -21,6 +21,8 @@ jobs: with: access_token: ${{ github.token }} - uses: actions/checkout@v2.3.4 + with: + fetch-depth: 0 - name: Set up Python 3.8 uses: actions/setup-python@v2.2.2 with: @@ -50,6 +52,8 @@ jobs: pyv: "3.10" steps: - uses: actions/checkout@v2.3.4 + with: + fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v2.2.2 with: diff --git a/.gitignore b/.gitignore index 91becd9ad..30fd2dc95 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ scripts/ci/gcp-creds.json .gcp-creds.json *~ +/dvc/_version.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2fc1a86d6..a987e27d4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,6 +7,11 @@ repos: language_version: python3 repo: https://github.com/ambv/black rev: 21.9b0 + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: check-toml + - id: check-yaml - hooks: - id: codespell args: diff --git a/MANIFEST.in b/MANIFEST.in index 03d2f17a9..1aba38f67 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1 @@ -include fastentrypoints.py include LICENSE diff --git a/dvc/version.py b/dvc/version.py index e54b7ad56..3611fbee6 100644 --- a/dvc/version.py +++ b/dvc/version.py @@ -1,74 +1,6 @@ -# Used in setup.py, so don't pull any additional dependencies -# -# Based on: -# - https://github.com/python/mypy/blob/master/mypy/version.py -# - https://github.com/python/mypy/blob/master/mypy/git.py -import os -import subprocess - -_BASE_VERSION = "2.8.1" - - -def _generate_version(base_version): - """Generate a version with information about the Git repository.""" - pkg_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - if not _is_git_repo(pkg_dir) or not _have_git(): - return base_version - - if _is_release(pkg_dir, base_version) and not _is_dirty(pkg_dir): - return base_version - - return "{base_version}+{short_sha}{dirty}".format( - base_version=base_version, - short_sha=_git_revision(pkg_dir).decode("utf-8")[0:6], - dirty=".mod" if _is_dirty(pkg_dir) else "", - ) - - -def _is_git_repo(dir_path): - """Is the given directory version-controlled with Git?""" - return os.path.exists(os.path.join(dir_path, ".git")) - - -def _have_git(): - """Can we run the git executable?""" - try: - subprocess.check_output(["git", "--help"]) - return True - except subprocess.CalledProcessError: - return False - except OSError: - return False - - -def _is_release(dir_path, base_version): - try: - output = subprocess.check_output( - ["git", "describe", "--tags", "--exact-match"], - cwd=dir_path, - stderr=subprocess.STDOUT, - ).decode("utf-8") - tag = output.strip() - return tag == base_version - except subprocess.CalledProcessError: - return False - - -def _git_revision(dir_path): - """Get SHA of the HEAD of a Git repository.""" - return subprocess.check_output( - ["git", "rev-parse", "HEAD"], cwd=dir_path - ).strip() - - -def _is_dirty(dir_path): - """Check whether a git repository has uncommitted changes.""" - try: - subprocess.check_call(["git", "diff", "--quiet"], cwd=dir_path) - return False - except subprocess.CalledProcessError: - return True - - -__version__ = _generate_version(_BASE_VERSION) +# pylint: disable=unused-import +try: + from ._version import version as __version__ + from ._version import version_tuple # noqa: F401 +except ImportError: + __version__ = "UNKNOWN" diff --git a/fastentrypoints.py b/fastentrypoints.py deleted file mode 100644 index 527f68624..000000000 --- a/fastentrypoints.py +++ /dev/null @@ -1,121 +0,0 @@ -# noqa: D300,D400 -# Copyright (c) 2016, Aaron Christianson -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS -# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED -# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -""" -Monkey patch setuptools to write faster console_scripts with this format: - - import sys - from mymodule import entry_function - sys.exit(entry_function()) - -This is better. - -(c) 2016, Aaron Christianson -http://github.com/ninjaaron/fast-entry_points -""" -import re - -from setuptools.command import easy_install - -TEMPLATE = r""" -# -*- coding: utf-8 -*- -# EASY-INSTALL-ENTRY-SCRIPT: '{3}','{4}','{5}' -__requires__ = '{3}' -import re -import sys - -from {0} import {1} - -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) - sys.exit({2}()) -""".lstrip() - - -@classmethod -def get_args(cls, dist, header=None): # noqa: D205,D400 - """ - Yield write_script() argument tuples for a distribution's - console_scripts and gui_scripts entry points. - """ - if header is None: - # pylint: disable=E1101 - header = cls.get_header() - spec = str(dist.as_requirement()) - for type_ in "console", "gui": - group = type_ + "_scripts" - for name, ep in dist.get_entry_map(group).items(): - # ensure_safe_name - if re.search(r"[\\/]", name): - raise ValueError("Path separators not allowed in script names") - script_text = TEMPLATE.format( - ep.module_name, - ep.attrs[0], - ".".join(ep.attrs), - spec, - group, - name, - ) - # pylint: disable=E1101,protected-access - args = cls._get_script_args(type_, name, header, script_text) - yield from args - - -# pylint: disable=E1101 -easy_install.ScriptWriter.get_args = get_args - - -def main(): - import os - import shutil - import sys - - dests = sys.argv[1:] or ["."] - filename = re.sub(r"\.pyc$", ".py", __file__) - - for dst in dests: - shutil.copy(filename, dst) - manifest_path = os.path.join(dst, "MANIFEST.in") - setup_path = os.path.join(dst, "setup.py") - - # Insert the include statement to MANIFEST.in if not present - with open(manifest_path, "a+") as manifest: - manifest.seek(0) - manifest_content = manifest.read() - if "include fastentrypoints.py" not in manifest_content: - manifest.write( - ("\n" if manifest_content else "") - + "include fastentrypoints.py" - ) - - # Insert the import statement to setup.py if not present - with open(setup_path, "a+") as setup: - setup.seek(0) - setup_content = setup.read() - if "import fastentrypoints" not in setup_content: - setup.seek(0) - setup.truncate() - setup.write("import fastentrypoints\n" + setup_content) diff --git a/pyproject.toml b/pyproject.toml index a4fc8cbe3..1d05edf15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,17 @@ +[build-system] +requires = [ + "setuptools>=58.2", + "wheel", + "setuptools_scm[toml]>=6.3.1", + # Prevents pkg_resources import in entry point script, + # see https://github.com/ninjaaron/fast-entry_points. + # This saves about 200 ms on startup time for non-wheel installs. + "fastentrypoints>=0.12" +] + +[tool.setuptools_scm] +write_to = "dvc/_version.py" + [tool.black] line-length = 79 include = '\.pyi?$' diff --git a/scripts/build_package.sh b/scripts/build_package.sh index 8b6e5b5f9..f9d8511e8 100755 --- a/scripts/build_package.sh +++ b/scripts/build_package.sh @@ -10,5 +10,5 @@ fi echo 'PKG = "pip"' >dvc/utils/build.py -python setup.py sdist -python setup.py bdist_wheel +python -m pip install -U build setuptools>=58.2 +python -m build --sdist --wheel --outdir dist/ diff --git a/scripts/pypi_upload.sh b/scripts/pypi_upload.sh index 3afca8ca3..64ee9e36c 100755 --- a/scripts/pypi_upload.sh +++ b/scripts/pypi_upload.sh @@ -9,5 +9,6 @@ fi rm -rf dist/ pip install twine -python setup.py sdist bdist_wheel +python -m pip install -U build setuptools>=58.2 +python -m build --sdist --wheel --outdir dist/ twine upload dist/* diff --git a/setup.cfg b/setup.cfg index e48ba60cd..b4b1fb33e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,10 +26,15 @@ classifiers = Programming Language :: Python :: 3.10 [options] +setup_requires = + setuptools>=58.2 + setuptools_scm[toml]>=6.3.1 + fastentrypoints>=0.12 python_requires = >=3.6 zip_safe = False packages = find: include_package_data = True +use_scm_version = True install_requires = # See https://github.com/pyinstaller/pyinstaller/issues/1945 ply>=3.9 diff --git a/setup.py b/setup.py index ff9bfd763..606849326 100644 --- a/setup.py +++ b/setup.py @@ -1,66 +1,3 @@ -import importlib.util -import os -from pathlib import Path -from textwrap import dedent - from setuptools import setup -from setuptools.command.build_py import build_py as _build_py -# Prevents pkg_resources import in entry point script, -# see https://github.com/ninjaaron/fast-entry_points. -# This saves about 200 ms on startup time for non-wheel installs. -try: - import fastentrypoints # noqa: F401, pylint: disable=unused-import -except ImportError: - pass # not able to import when installing through pre-commit - - -# Read package meta-data from version.py -# see https://packaging.python.org/guides/single-sourcing-package-version/ -try: - pkg_dir = os.path.dirname(os.path.abspath(__file__)) - version_path = os.path.join(pkg_dir, "dvc", "version.py") - spec = importlib.util.spec_from_file_location("dvc.version", version_path) - dvc_version = importlib.util.module_from_spec(spec) - spec.loader.exec_module(dvc_version) - version = dvc_version.__version__ # noqa: F821 -except Exception as exc: # pylint: disable=broad-except - # Dependabot seem to stop working when we don't handle this except block. - # Most likely, it's because of the restrictions in execution of the module. - # We workaround it, and print the error message if it also happens on other - # installations (though this message may likely be suppressed by build - # tools, eg: pip only shows this message on `--verbose` mode). - import sys - - print("Could not load version info: ", exc, file=sys.stderr) - version = "UNKNOWN" - - -# To achieve consistency between the build version and the one provided -# by your package during runtime, you need to **pin** the build version. -# -# This custom class will replace the version.py module with a **static** -# `__version__` that your package can read at runtime, assuring consistency. -# -# References: -# - https://docs.python.org/3.7/distutils/extending.html -# - https://github.com/python/mypy -class build_py(_build_py): - def pin_version(self): - ver_file = Path(self.build_lib) / "dvc" / "version.py" - self.mkpath(os.fspath(ver_file.parent)) - ver_file.write_text( - dedent( - f"""\ - # AUTOGENERATED at build time by setup.py - __version__ = "{version}" - """ - ) - ) - - def run(self): - self.execute(self.pin_version, ()) - _build_py.run(self) - - -setup(version=version, cmdclass={"build_py": build_py}) +setup() diff --git a/tests/func/test_version.py b/tests/func/test_version.py index 08937d649..b7dee992e 100644 --- a/tests/func/test_version.py +++ b/tests/func/test_version.py @@ -1,14 +1,18 @@ import re from dvc.main import main -from tests.unit.test_info import PYTHON_VERSION_REGEX, find_supported_remotes +from tests.unit.test_info import ( + DVC_VERSION_REGEX, + PYTHON_VERSION_REGEX, + find_supported_remotes, +) def test_(tmp_dir, dvc, scm, capsys): assert main(["version"]) == 0 out, _ = capsys.readouterr() - assert re.search(r"DVC version: \d+\.\d+\.\d+.*", out) + assert re.search(fr"DVC version: {DVC_VERSION_REGEX}", out) assert re.search(f"Platform: {PYTHON_VERSION_REGEX} on .*", out) assert find_supported_remotes(out) assert re.search(r"Cache types: .*", out) diff --git a/tests/unit/test_info.py b/tests/unit/test_info.py index 8f01b2c47..22455a54e 100644 --- a/tests/unit/test_info.py +++ b/tests/unit/test_info.py @@ -11,6 +11,7 @@ from dvc.info import get_dvc_info # `patch` is more than enough for the tests. # Refer PEP-0440 for complete regex just in-case. PYTHON_VERSION_REGEX = r"Python \d\.\d+\.\d+\S*" +DVC_VERSION_REGEX = r"\d+\.\d+\.(\d+\.)?.*" def find_supported_remotes(string): @@ -50,7 +51,7 @@ def test_info_in_repo(scm_init, tmp_dir): dvc_info = get_dvc_info() - assert re.search(r"DVC version: \d+\.\d+\.\d+.*", dvc_info) + assert re.search(fr"DVC version: {DVC_VERSION_REGEX}", dvc_info) assert re.search(f"Platform: {PYTHON_VERSION_REGEX} on .*", dvc_info) assert find_supported_remotes(dvc_info) assert re.search(r"Cache types: .*", dvc_info) @@ -125,7 +126,7 @@ def test_fs_info_in_repo(tmp_dir, dvc, caplog): def test_info_outside_of_repo(tmp_dir, caplog): dvc_info = get_dvc_info() - assert re.search(r"DVC version: \d+\.\d+\.\d+.*", dvc_info) + assert re.search(fr"DVC version: {DVC_VERSION_REGEX}", dvc_info) assert re.search(f"Platform: {PYTHON_VERSION_REGEX} on .*", dvc_info) assert find_supported_remotes(dvc_info) assert not re.search(r"Cache types: .*", dvc_info) @@ -134,7 +135,7 @@ def test_info_outside_of_repo(tmp_dir, caplog): def test_fs_info_outside_of_repo(tmp_dir, caplog): dvc_info = get_dvc_info() - assert re.search(r"DVC version: \d+\.\d+\.\d+.*", dvc_info) + assert re.search(fr"DVC version: {DVC_VERSION_REGEX}", dvc_info) assert re.search(f"Platform: {PYTHON_VERSION_REGEX} on .*", dvc_info) assert find_supported_remotes(dvc_info) diff --git a/tests/unit/test_version.py b/tests/unit/test_version.py deleted file mode 100644 index 7362ce54d..000000000 --- a/tests/unit/test_version.py +++ /dev/null @@ -1,20 +0,0 @@ -import subprocess - -import dvc.version - - -def test_is_release(mocker): - m = mocker.patch.object(dvc.version.subprocess, "check_output") - - m.side_effect = subprocess.CalledProcessError(1, "cmd") - ret = dvc.version._is_release(None, dvc.version._BASE_VERSION) - assert ret is False - - m.side_effect = None - m.return_value = dvc.version._BASE_VERSION.encode("ascii") - ret = dvc.version._is_release(None, dvc.version._BASE_VERSION) - assert ret - - m.return_value = b"630d1741c2d5dd89a3176bd15b63121b905d35c9" - ret = dvc.version._is_release(None, dvc.version._BASE_VERSION) - assert ret is False