pax_global_header00006660000000000000000000000064145221022160014505gustar00rootroot0000000000000052 comment=e186a8b5d97dd9c9f93ae6015139b55411d9011b hatch-vcs-0.4.0/000077500000000000000000000000001452210221600133665ustar00rootroot00000000000000hatch-vcs-0.4.0/.gitattributes000066400000000000000000000001021452210221600162520ustar00rootroot00000000000000# Auto detect text files and perform LF normalization * text=auto hatch-vcs-0.4.0/.github/000077500000000000000000000000001452210221600147265ustar00rootroot00000000000000hatch-vcs-0.4.0/.github/dependabot.yml000066400000000000000000000001511452210221600175530ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: github-actions directory: / schedule: interval: monthly hatch-vcs-0.4.0/.github/workflows/000077500000000000000000000000001452210221600167635ustar00rootroot00000000000000hatch-vcs-0.4.0/.github/workflows/build.yml000066400000000000000000000015741452210221600206140ustar00rootroot00000000000000name: build on: push: tags: - v* concurrency: group: build-${{ github.head_ref }} jobs: build: name: Build wheels and source distribution runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install build dependencies run: python -m pip install --upgrade build - name: Build run: python -m build - uses: actions/upload-artifact@v3 with: name: artifacts path: dist/* if-no-files-found: error publish: name: Publish release needs: - build runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v3 with: name: artifacts path: dist - name: Push build artifacts to PyPI uses: pypa/gh-action-pypi-publish@v1.6.4 with: skip_existing: true user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} hatch-vcs-0.4.0/.github/workflows/test.yml000066400000000000000000000020011452210221600204560ustar00rootroot00000000000000name: test on: push: branches: - master pull_request: branches: - master concurrency: group: test-${{ github.head_ref }} cancel-in-progress: true env: PYTHONUNBUFFERED: "1" FORCE_COLOR: "1" jobs: run: name: Python ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install Hatch run: pip install --upgrade hatch - if: matrix.python-version == '3.9' && runner.os == 'Linux' name: Lint run: hatch run lint:all - name: Run tests run: hatch run cov hatch-vcs-0.4.0/.gitignore000066400000000000000000000003241452210221600153550ustar00rootroot00000000000000# Global directories __pycache__/ # Global files *.py[cod] *.dll *.so *.log *.swp # Root directories /.benchmarks/ /.env/ /.idea/ /.mypy_cache/ /.pytest_cache/ /.vscode/ /dist/ /site/ # Root files /.coverage* hatch-vcs-0.4.0/HISTORY.md000066400000000000000000000016511452210221600150540ustar00rootroot00000000000000# History ----- All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased ## 0.4.0 - 2023-11-06 ***Changed:*** - Drop support for Python 3.7 ***Added:*** - Officially support Python 3.12 ***Fixed:*** - Prevent `UserWarning` when a template is not defined explicitly ## 0.3.0 - 2022-12-10 ***Changed:*** - Drop support for Python 2 ***Added:*** - Add a metadata hook for injecting VCS metadata - Bump the minimum supported version of Hatchling ## 0.2.1 - 2022-12-06 ***Fixed:*** - Allow `root` in `raw-options` ## 0.2.0 - 2022-03-18 ***Added:*** - Bump the minimum supported version of Hatchling ***Fixed:*** - Fix handling of `fallback_version` default value ## 0.1.0 - 2022-01-18 This is the initial public release. hatch-vcs-0.4.0/LICENSE.txt000066400000000000000000000021001452210221600152020ustar00rootroot00000000000000MIT License Copyright (c) 2022-present Ofek Lev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. hatch-vcs-0.4.0/README.md000066400000000000000000000165121452210221600146520ustar00rootroot00000000000000# hatch-vcs | | | | --- | --- | | CI/CD | [![CI - Test](https://github.com/ofek/hatch-vcs/actions/workflows/test.yml/badge.svg)](https://github.com/ofek/hatch-vcs/actions/workflows/test.yml) [![CD - Build](https://github.com/ofek/hatch-vcs/actions/workflows/build.yml/badge.svg)](https://github.com/ofek/hatch-vcs/actions/workflows/build.yml) | | Package | [![PyPI - Version](https://img.shields.io/pypi/v/hatch-vcs.svg?logo=pypi&label=PyPI&logoColor=gold)](https://pypi.org/project/hatch-vcs/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/hatch-vcs.svg?logo=python&label=Python&logoColor=gold)](https://pypi.org/project/hatch-vcs/) | | Meta | [![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/pypa/hatch) [![code style - black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![types - Mypy](https://img.shields.io/badge/types-Mypy-blue.svg)](https://github.com/ambv/black) [![License - MIT](https://img.shields.io/badge/license-MIT-9400d3.svg)](https://spdx.org/licenses/) [![GitHub Sponsors](https://img.shields.io/github/sponsors/ofek?logo=GitHub%20Sponsors&style=social)](https://github.com/sponsors/ofek) | ----- This provides a plugin for [Hatch](https://github.com/pypa/hatch) that uses your preferred version control system (like Git) to determine project versions. **Table of Contents** - [Global dependency](#global-dependency) - [Version source](#version-source) - [Version source options](#version-source-options) - [Version source environment variables](#version-source-environment-variables) - [Build hook](#build-hook) - [Build hook options](#build-hook-options) - [Editable installs](#editable-installs) - [Metadata hook](#metadata-hook) - [Metadata hook options](#metadata-hook-options) - [URLs](#urls) - [Example](#example) - [License](#license) ## Global dependency Ensure `hatch-vcs` is defined within the `build-system.requires` field in your `pyproject.toml` file. ```toml [build-system] requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" ``` ## Version source The [version source plugin](https://hatch.pypa.io/latest/plugins/version-source/reference/) name is `vcs`. - ***pyproject.toml*** ```toml [tool.hatch.version] source = "vcs" ``` - ***hatch.toml*** ```toml [version] source = "vcs" ``` ### Version source options | Option | Type | Default | Description | | --- | --- | --- | --- | | `tag-pattern` | `str` | see [code](https://github.com/pypa/setuptools_scm/blob/v6.4.0/src/setuptools_scm/config.py#L13) | A regular expression used to extract the version part from VCS tags. The pattern needs to contain either a single match group, or a group named `version`, that captures the actual version information. | | `fallback-version` | `str` | | The version that will be used if no other method for detecting the version is successful. If not specified, unsuccessful version detection will raise an error. | | `raw-options` | `dict` | | A table of [`setuptools-scm` parameters](https://github.com/pypa/setuptools_scm#configuration-parameters) that will override any of the options listed above. The `write_to` and `write_to_template` parameters are ignored. | ### Version source environment variables - `SETUPTOOLS_SCM_PRETEND_VERSION`: When defined and not empty, it's used as the primary source for the version, in which case it will be an unparsed string. ## Build hook The [build hook plugin](https://hatch.pypa.io/latest/plugins/build-hook/reference/) name is `vcs`. - ***pyproject.toml*** ```toml [tool.hatch.build.hooks.vcs] version-file = "_version.py" ``` - ***hatch.toml*** ```toml [build.hooks.vcs] version-file = "_version.py" ``` Building or installing when the latest tag is ``v1.2.3`` will generate the file - ***_version.py*** ```python # coding: utf-8 # file generated by setuptools_scm # don't change, don't track in version control __version__ = version = '1.2.3' __version_tuple__ = version_tuple = (1, 2, 3) ``` ### Build hook options | Option | Type | Default | Description | | --- | --- | --- | --- | | `version-file` | `str` | ***REQUIRED*** | The relative path to the file that gets updated with the current version. | | `template` | `str` | | The template used to overwrite the `version-file`. See the [code](https://github.com/pypa/setuptools_scm/blob/v6.4.0/src/setuptools_scm/__init__.py#L30-L39) for the default template for each file extension. | ### Editable installs The version file is only updated upon install or build. Thus the version number in an [editable install](https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs) (Hatch's [dev mode](https://hatch.pypa.io/latest/config/build/#dev-mode)) will be incorrect if the version changes and the project is not rebuilt. An unsupported workaround for keeping the version number up-to-date can be found at [hatch-vcs-footgun-example](https://github.com/maresb/hatch-vcs-footgun-example). ## Metadata hook **Note:** only Git is supported The [metadata hook plugin](https://hatch.pypa.io/latest/plugins/metadata-hook/reference/) is for inserting VCS data (currently the commit hash) into metadata fields other than `version`. Its name is `vcs`. - ***pyproject.toml*** ```toml [tool.hatch.metadata.hooks.vcs] ``` - ***hatch.toml*** ```toml [metadata.hooks.vcs] ``` ### Metadata hook options #### URLs The `urls` option is equivalent to [`project.urls`](https://hatch.pypa.io/latest/config/metadata/#urls) except that each URL supports [context formatting](https://hatch.pypa.io/latest/config/context/) with the following fields: - `commit_hash` - the latest commit hash Be sure to add `urls` to [`project.dynamic`](https://hatch.pypa.io/latest/config/metadata/#dynamic): - ***pyproject.toml*** ```toml [project] dynamic = [ "urls", ] ``` ### Example - ***pyproject.toml*** ```toml [tool.hatch.metadata.hooks.vcs.urls] Homepage = "https://www.example.com" source_archive = "https://github.com/org/repo/archive/{commit_hash}.zip" ``` - ***hatch.toml*** ```toml [metadata.hooks.vcs.urls] Homepage = "https://www.example.com" source_archive = "https://github.com/org/repo/archive/{commit_hash}.zip" ``` ## Migration tips If you are migrating from [setuptools](https://setuptools.pypa.io), you may want access to the version without performing a full build. By default, `python -m setuptools_scm` will display the version and perform any side-effects like writing to a file. `hatch` separates these functions. ### Display version `hatch version` will print the version to the terminal without modifying the source directory. ```console $ hatch version 23.0.0.dev17+g462372ba ``` ### Write version to file If `version-file` is defined, you can write it to the source directory with the `build` command, using the `--hooks-only` flag to modify the source tree but skip creation of sdists or wheels. ```console $ hatch build --hooks-only $ cat package/_version.py # file generated by setuptools_scm # don't change, don't track in version control __version__ = version = '23.0.0.dev17+g462372ba' __version_tuple__ = version_tuple = (23, 0, 0, 'dev17', 'g462372ba') ``` ## License `hatch-vcs` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. hatch-vcs-0.4.0/hatch.toml000066400000000000000000000013121452210221600153470ustar00rootroot00000000000000[envs.default] dependencies = [ "coverage[toml]", "pytest", ] [envs.default.scripts] test = "pytest {args:tests}" test-cov = "coverage run -m pytest {args:tests}" cov-report = [ "- coverage combine", "coverage report --show-missing", ] cov = [ "test-cov", "cov-report", ] [[envs.all.matrix]] python = ["3.7", "3.8", "3.9", "3.10", "3.11"] [envs.lint] detached = true dependencies = [ "black>=22.10.0", "mypy>=0.991", "ruff>=0.0.166", ] [envs.lint.scripts] typing = "mypy --install-types --non-interactive {args:hatch_vcs tests}" style = [ "ruff {args:.}", "black --check --diff {args:.}", ] fmt = [ "black {args:.}", "ruff --fix {args:.}", "style", ] all = [ "style", "typing", ] hatch-vcs-0.4.0/hatch_vcs/000077500000000000000000000000001452210221600153305ustar00rootroot00000000000000hatch-vcs-0.4.0/hatch_vcs/__about__.py000066400000000000000000000001661452210221600176130ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2022-present Ofek Lev # # SPDX-License-Identifier: MIT __version__ = '0.4.0' hatch-vcs-0.4.0/hatch_vcs/__init__.py000066400000000000000000000001401452210221600174340ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2022-present Ofek Lev # # SPDX-License-Identifier: MIT hatch-vcs-0.4.0/hatch_vcs/build_hook.py000066400000000000000000000025231452210221600200230ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2022-present Ofek Lev # # SPDX-License-Identifier: MIT from functools import cached_property from hatchling.builders.hooks.plugin.interface import BuildHookInterface class VCSBuildHook(BuildHookInterface): PLUGIN_NAME = 'vcs' @cached_property def config_version_file(self): version_file = self.config.get('version-file') if not version_file: raise ValueError(f'Option `version-file` for build hook `{self.PLUGIN_NAME}` is required') elif not isinstance(version_file, str): raise TypeError(f'Option `version-file` for build hook `{self.PLUGIN_NAME}` must be a string') return version_file @cached_property def config_template(self): template = self.config.get('template') if template is not None and not isinstance(template, str): raise TypeError(f'Option `template` for build hook `{self.PLUGIN_NAME}` must be a string') return template def initialize(self, version, build_data): from setuptools_scm import dump_version kwargs = {} if self.config_template: kwargs['template'] = self.config_template dump_version(self.root, self.metadata.version, self.config_version_file, **kwargs) build_data['artifacts'].append(f'/{self.config_version_file}') hatch-vcs-0.4.0/hatch_vcs/hooks.py000066400000000000000000000007741452210221600170350ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2022-present Ofek Lev # # SPDX-License-Identifier: MIT from hatchling.plugin import hookimpl from hatch_vcs.build_hook import VCSBuildHook from hatch_vcs.metadata_hook import VCSMetadataHook from hatch_vcs.version_source import VCSVersionSource @hookimpl def hatch_register_version_source(): return VCSVersionSource @hookimpl def hatch_register_build_hook(): return VCSBuildHook @hookimpl def hatch_register_metadata_hook(): return VCSMetadataHook hatch-vcs-0.4.0/hatch_vcs/metadata_hook.py000066400000000000000000000025321452210221600205040ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2022-present Ofek Lev # # SPDX-License-Identifier: MIT from collections import ChainMap from hatchling.metadata.plugin.interface import MetadataHookInterface from hatchling.utils.context import ContextStringFormatter from hatch_vcs import vcs_utils class VCSMetadataHook(MetadataHookInterface): PLUGIN_NAME = 'vcs' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__config_urls = None @property def config_urls(self): if self.__config_urls is None: urls = self.config.get('urls', {}) if not isinstance(urls, dict): raise TypeError('option `urls` must be a table') for key, url in urls.items(): if not isinstance(url, str): raise TypeError(f'URL `{key}` in option `urls` must be a string') self.__config_urls = urls return self.__config_urls def update(self, metadata): formatter = ContextStringFormatter( ChainMap( { 'commit_hash': lambda *args: vcs_utils.get_commit_hash(self.root), }, ) ) urls = self.config_urls.copy() for key, url in urls.items(): urls[key] = formatter.format(url) metadata['urls'] = urls hatch-vcs-0.4.0/hatch_vcs/vcs_utils.py000066400000000000000000000004601452210221600177150ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2022-present Ofek Lev # # SPDX-License-Identifier: MIT import subprocess from functools import lru_cache @lru_cache(maxsize=None) def get_commit_hash(root: str): return subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=root).decode('utf-8').strip() hatch-vcs-0.4.0/hatch_vcs/version_source.py000066400000000000000000000045111452210221600207500ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2022-present Ofek Lev # # SPDX-License-Identifier: MIT from hatchling.version.source.plugin.interface import VersionSourceInterface class VCSVersionSource(VersionSourceInterface): PLUGIN_NAME = 'vcs' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__config_tag_pattern = None self.__config_fallback_version = None self.__config_raw_options = None @property def config_tag_pattern(self): if self.__config_tag_pattern is None: tag_pattern = self.config.get('tag-pattern', '') if not isinstance(tag_pattern, str): raise TypeError('option `tag-pattern` must be a string') self.__config_tag_pattern = tag_pattern return self.__config_tag_pattern @property def config_fallback_version(self): if self.__config_fallback_version is None: fallback_version = self.config.get('fallback-version', '') if not isinstance(fallback_version, str): raise TypeError('option `fallback-version` must be a string') self.__config_fallback_version = fallback_version return self.__config_fallback_version @property def config_raw_options(self): if self.__config_raw_options is None: raw_options = self.config.get('raw-options', {}) if not isinstance(raw_options, dict): raise TypeError('option `raw-options` must be a table') self.__config_raw_options = raw_options return self.__config_raw_options def construct_setuptools_scm_config(self): from copy import deepcopy config = deepcopy(self.config_raw_options) config.setdefault('root', self.root) config.setdefault('tag_regex', self.config_tag_pattern) # Only set for non-empty strings if self.config_fallback_version: config['fallback_version'] = self.config_fallback_version # Writing only occurs when the build hook is enabled config.pop('write_to', None) config.pop('write_to_template', None) return config def get_version_data(self): from setuptools_scm import get_version version = get_version(**self.construct_setuptools_scm_config()) return {'version': version} hatch-vcs-0.4.0/pyproject.toml000066400000000000000000000053341452210221600163070ustar00rootroot00000000000000[build-system] requires = ["hatchling>=1.1.0"] build-backend = "hatchling.build" [project] name = "hatch-vcs" dynamic = ["version"] description = 'Hatch plugin for versioning with your preferred VCS' readme = "README.md" license = { text = "MIT" } requires-python = ">= 3.8" keywords = [ "git", "hatch", "mercurial", "plugin", "scm", "vcs", "version", ] authors = [ { name = "Ofek Lev", email = "oss@ofek.dev" }, ] classifiers = [ "Development Status :: 4 - Beta", "Programming Language :: Python", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] dependencies = [ "hatchling>=1.1.0", "setuptools-scm>=6.4.0", ] [project.urls] Funding = "https://github.com/sponsors/ofek" History = "https://github.com/ofek/hatch-vcs/blob/master/HISTORY.md" Issues = "https://github.com/ofek/hatch-vcs/issues" Source = "https://github.com/ofek/hatch-vcs" [project.entry-points.hatch] vcs = "hatch_vcs.hooks" [tool.hatch.version] path = "hatch_vcs/__about__.py" [tool.black] target-version = ["py38"] line-length = 120 skip-string-normalization = true [tool.ruff] target-version = "py38" line-length = 120 select = [ "A", "B", "C", "E", "F", "FBT", "I", "N", "Q", "RUF", "S", "T", "UP", "W", "YTT", ] ignore = [ # Allow non-abstract empty methods in abstract base classes "B027", # Ignore McCabe complexity "C901", # Allow boolean positional values in function calls, like `dict.get(... True)` "FBT003", # Ignore checks for possible passwords "S105", "S106", "S107", # Ignore noisy checks for insecure subprocess calls "S603", "S607", # Boolean default values "FBT002", ] [tool.ruff.isort] known-first-party = ["hatch_vcs"] [tool.ruff.flake8-quotes] inline-quotes = "single" [tool.ruff.flake8-tidy-imports] ban-relative-imports = "all" [tool.ruff.per-file-ignores] # Tests can use relative imports and assertions "tests/**/*" = ["TID252", "S101"] [tool.mypy] disallow_untyped_defs = false follow_imports = "normal" ignore_missing_imports = true pretty = true show_column_numbers = true show_error_codes = true warn_no_return = false warn_unused_ignores = true [tool.coverage.run] source_pkgs = ["hatch_vcs", "tests"] branch = true parallel = true omit = [ "hatch_vcs/__about__.py", ] [tool.coverage.paths] hatch_vcs = ["hatch_vcs", "*/hatch-vcs/hatch_vcs"] tests = ["tests", "*/hatch-vcs/tests"] [tool.coverage.report] exclude_lines = [ "no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:", ] hatch-vcs-0.4.0/tests/000077500000000000000000000000001452210221600145305ustar00rootroot00000000000000hatch-vcs-0.4.0/tests/__init__.py000066400000000000000000000001401452210221600166340ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2022-present Ofek Lev # # SPDX-License-Identifier: MIT hatch-vcs-0.4.0/tests/conftest.py000066400000000000000000000110651452210221600167320ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2022-present Ofek Lev # # SPDX-License-Identifier: MIT import errno import os import stat import tempfile from contextlib import contextmanager from functools import wraps from sys import version_info if version_info[:2] >= (3, 12): from shutil import rmtree else: from shutil import rmtree as _rmtree # Backport the onexc keyword argument from Python 3.12 @wraps(_rmtree) def rmtree(path, ignore_errors=False, onerror=None, *args, **kwds): if 'onexc' in kwds: kwds = dict(kwds) onexc = kwds.pop('onexc') def onerror(func, path, exc): return onexc(func, path, exc[1]) return _rmtree(path, ignore_errors, onerror, *args, **kwds) import pytest from .utils import create_file, git, write_file def handle_remove_readonly(func, path, exc): # no cov # PermissionError: [WinError 5] Access is denied: '...\\.git\\...' if func in (os.rmdir, os.remove, os.unlink) and exc.errno == errno.EACCES: os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # noqa: S103 func(path) else: raise @pytest.fixture def temp_dir(): directory = tempfile.mkdtemp() try: directory = os.path.realpath(directory) yield directory finally: rmtree(directory, ignore_errors=False, onexc=handle_remove_readonly) @contextmanager def create_project(directory, metadata, *, setup_vcs=True, nested=False): root_dir = project_dir = os.path.join(directory, 'my-app') os.mkdir(root_dir) gitignore_file = os.path.join(root_dir, '.gitignore') write_file(gitignore_file, '/my_app/version.py') if nested: project_dir = os.path.join(root_dir, 'project') os.mkdir(project_dir) project_file = os.path.join(project_dir, 'pyproject.toml') write_file(project_file, metadata) package_dir = os.path.join(project_dir, 'my_app') os.mkdir(package_dir) create_file(os.path.join(package_dir, '__init__.py')) create_file(os.path.join(package_dir, 'foo.py')) create_file(os.path.join(package_dir, 'bar.py')) create_file(os.path.join(package_dir, 'baz.py')) origin = os.getcwd() os.chdir(project_dir) try: if setup_vcs: if nested: os.chdir(root_dir) git('init') git('config', '--local', 'user.name', 'foo') git('config', '--local', 'user.email', 'foo@bar.baz') git('add', '.') git('commit', '-m', 'test') git('tag', '1.2.3') if nested: os.chdir(project_dir) yield project_dir finally: os.chdir(origin) @pytest.fixture def new_project_basic(temp_dir): with create_project( temp_dir, """\ [build-system] requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" [project] name = "my-app" dynamic = ["version"] [tool.hatch.version] source = "vcs" """, ) as project: yield project @pytest.fixture def new_project_write(temp_dir): with create_project( temp_dir, """\ [build-system] requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" [project] name = "my-app" dynamic = ["version"] [tool.hatch.version] source = "vcs" [tool.hatch.build.targets.wheel.hooks.vcs] version-file = "my_app/_version.py" """, ) as project: yield project @pytest.fixture def new_project_fallback(temp_dir): with create_project( temp_dir, """\ [build-system] requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" [project] name = "my-app" dynamic = ["version"] [tool.hatch.version] source = "vcs" fallback-version = "7.8.9" """, setup_vcs=False, ) as project: yield project @pytest.fixture def new_project_root_elsewhere(temp_dir): with create_project( temp_dir, """\ [build-system] requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" [project] name = "my-app" dynamic = ["version"] [tool.hatch.version] source = "vcs" raw-options = { root = ".." } """, nested=True, ) as project: yield project @pytest.fixture def new_project_metadata(temp_dir): with create_project( temp_dir, """\ [build-system] requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" [project] name = "my-app" dynamic = ["version", "urls"] [tool.hatch.version] source = "vcs" [tool.hatch.metadata.hooks.vcs.urls] Homepage = "https://www.google.com" foo = "https://github.com/bar/baz#{commit_hash}" """, ) as project: yield project hatch-vcs-0.4.0/tests/test_build.py000066400000000000000000000152511452210221600172440ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2022-present Ofek Lev # # SPDX-License-Identifier: MIT import os import sys import zipfile import pytest from .utils import build_project, git, read_file def test_basic(new_project_basic): build_project('-t', 'wheel') build_dir = os.path.join(new_project_basic, 'dist') assert os.path.isdir(build_dir) artifacts = os.listdir(build_dir) assert len(artifacts) == 1 wheel_file = artifacts[0] assert wheel_file == 'my_app-1.2.3-py2.py3-none-any.whl' extraction_directory = os.path.join(os.path.dirname(new_project_basic), '_archive') os.mkdir(extraction_directory) with zipfile.ZipFile(os.path.join(build_dir, wheel_file), 'r') as zip_archive: zip_archive.extractall(extraction_directory) metadata_directory = os.path.join(extraction_directory, 'my_app-1.2.3.dist-info') assert os.path.isdir(metadata_directory) package_directory = os.path.join(extraction_directory, 'my_app') assert os.path.isdir(package_directory) assert len(os.listdir(package_directory)) == 4 assert os.path.isfile(os.path.join(package_directory, '__init__.py')) assert os.path.isfile(os.path.join(package_directory, 'foo.py')) assert os.path.isfile(os.path.join(package_directory, 'bar.py')) assert os.path.isfile(os.path.join(package_directory, 'baz.py')) def test_write(new_project_write): build_project('-t', 'wheel') build_dir = os.path.join(new_project_write, 'dist') assert os.path.isdir(build_dir) artifacts = os.listdir(build_dir) assert len(artifacts) == 1 wheel_file = artifacts[0] assert wheel_file == 'my_app-1.2.3-py2.py3-none-any.whl' extraction_directory = os.path.join(os.path.dirname(new_project_write), '_archive') os.mkdir(extraction_directory) with zipfile.ZipFile(os.path.join(build_dir, wheel_file), 'r') as zip_archive: zip_archive.extractall(extraction_directory) metadata_directory = os.path.join(extraction_directory, 'my_app-1.2.3.dist-info') assert os.path.isdir(metadata_directory) package_directory = os.path.join(extraction_directory, 'my_app') assert os.path.isdir(package_directory) assert len(os.listdir(package_directory)) == 5 assert os.path.isfile(os.path.join(package_directory, '__init__.py')) assert os.path.isfile(os.path.join(package_directory, 'foo.py')) assert os.path.isfile(os.path.join(package_directory, 'bar.py')) assert os.path.isfile(os.path.join(package_directory, 'baz.py')) version_file = os.path.join(package_directory, '_version.py') assert os.path.isfile(version_file) lines = read_file(version_file).splitlines() version_starts = ('version = ', '__version__ = ') assert any(line.startswith(version_starts) for line in lines) version_line = next(line for line in lines if line.startswith(version_starts)) assert " = '1.2.3'" in version_line @pytest.mark.skipif(sys.version_info[0] == 2, reason='Depends on fix in 6.4.0 which is Python 3-only') def test_fallback(new_project_fallback): build_project('-t', 'wheel') build_dir = os.path.join(new_project_fallback, 'dist') assert os.path.isdir(build_dir) artifacts = os.listdir(build_dir) assert len(artifacts) == 1 wheel_file = artifacts[0] assert wheel_file == 'my_app-7.8.9-py2.py3-none-any.whl' extraction_directory = os.path.join(os.path.dirname(new_project_fallback), '_archive') os.mkdir(extraction_directory) with zipfile.ZipFile(os.path.join(build_dir, wheel_file), 'r') as zip_archive: zip_archive.extractall(extraction_directory) metadata_directory = os.path.join(extraction_directory, 'my_app-7.8.9.dist-info') assert os.path.isdir(metadata_directory) package_directory = os.path.join(extraction_directory, 'my_app') assert os.path.isdir(package_directory) assert len(os.listdir(package_directory)) == 4 assert os.path.isfile(os.path.join(package_directory, '__init__.py')) assert os.path.isfile(os.path.join(package_directory, 'foo.py')) assert os.path.isfile(os.path.join(package_directory, 'bar.py')) assert os.path.isfile(os.path.join(package_directory, 'baz.py')) def test_root(new_project_root_elsewhere): build_project('-t', 'wheel') build_dir = os.path.join(new_project_root_elsewhere, 'dist') assert os.path.isdir(build_dir) artifacts = os.listdir(build_dir) assert len(artifacts) == 1 wheel_file = artifacts[0] assert wheel_file == 'my_app-1.2.3-py2.py3-none-any.whl' extraction_directory = os.path.join(os.path.dirname(new_project_root_elsewhere), '_archive') os.mkdir(extraction_directory) with zipfile.ZipFile(os.path.join(build_dir, wheel_file), 'r') as zip_archive: zip_archive.extractall(extraction_directory) metadata_directory = os.path.join(extraction_directory, 'my_app-1.2.3.dist-info') assert os.path.isdir(metadata_directory) package_directory = os.path.join(extraction_directory, 'my_app') assert os.path.isdir(package_directory) assert len(os.listdir(package_directory)) == 4 assert os.path.isfile(os.path.join(package_directory, '__init__.py')) assert os.path.isfile(os.path.join(package_directory, 'foo.py')) assert os.path.isfile(os.path.join(package_directory, 'bar.py')) assert os.path.isfile(os.path.join(package_directory, 'baz.py')) def test_metadata(new_project_metadata): build_project('-t', 'wheel') build_dir = os.path.join(new_project_metadata, 'dist') assert os.path.isdir(build_dir) artifacts = os.listdir(build_dir) assert len(artifacts) == 1 wheel_file = artifacts[0] assert wheel_file == 'my_app-1.2.3-py2.py3-none-any.whl' extraction_directory = os.path.join(os.path.dirname(new_project_metadata), '_archive') os.mkdir(extraction_directory) with zipfile.ZipFile(os.path.join(build_dir, wheel_file), 'r') as zip_archive: zip_archive.extractall(extraction_directory) metadata_directory = os.path.join(extraction_directory, 'my_app-1.2.3.dist-info') assert os.path.isdir(metadata_directory) package_directory = os.path.join(extraction_directory, 'my_app') assert os.path.isdir(package_directory) assert len(os.listdir(package_directory)) == 4 assert os.path.isfile(os.path.join(package_directory, '__init__.py')) assert os.path.isfile(os.path.join(package_directory, 'foo.py')) assert os.path.isfile(os.path.join(package_directory, 'bar.py')) assert os.path.isfile(os.path.join(package_directory, 'baz.py')) metadata_file = os.path.join(metadata_directory, 'METADATA') with open(metadata_file, encoding='utf-8') as f: contents = f.read() assert f'Project-URL: foo, https://github.com/bar/baz#{git("rev-parse", "HEAD")}' in contents hatch-vcs-0.4.0/tests/test_build_config.py000066400000000000000000000045511452210221600205720ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2022-present Ofek Lev # # SPDX-License-Identifier: MIT import os import pytest from hatch_vcs.build_hook import VCSBuildHook class TestVersionFile: def test_correct(self, new_project_basic): config = {'version-file': 'foo/_version.py'} build_dir = os.path.join(new_project_basic, 'dist') build_hook = VCSBuildHook(new_project_basic, config, None, None, build_dir, 'wheel') assert build_hook.config_version_file == 'foo/_version.py' def test_not_string(self, new_project_basic): config = {'version-file': 9000} build_dir = os.path.join(new_project_basic, 'dist') build_hook = VCSBuildHook(new_project_basic, config, None, None, build_dir, 'wheel') with pytest.raises(TypeError, match='Option `version-file` for build hook `vcs` must be a string'): _ = build_hook.config_version_file def test_missing(self, new_project_basic): config = {} build_dir = os.path.join(new_project_basic, 'dist') build_hook = VCSBuildHook(new_project_basic, config, None, None, build_dir, 'wheel') with pytest.raises(ValueError, match='Option `version-file` for build hook `vcs` is required'): _ = build_hook.config_version_file class TestTemplate: def test_correct(self, new_project_basic): config = {'template': '__version__ = {version!r}'} build_dir = os.path.join(new_project_basic, 'dist') build_hook = VCSBuildHook(new_project_basic, config, None, None, build_dir, 'wheel') assert build_hook.config_template == '__version__ = {version!r}' def test_not_string(self, new_project_basic): config = {'template': 9000} build_dir = os.path.join(new_project_basic, 'dist') build_hook = VCSBuildHook(new_project_basic, config, None, None, build_dir, 'wheel') with pytest.raises(TypeError, match='Option `template` for build hook `vcs` must be a string'): _ = build_hook.config_template def test_coverage(new_project_basic): config = {'version-file': 'foo/_version.py'} build_dir = os.path.join(new_project_basic, 'dist') build_hook = VCSBuildHook(new_project_basic, config, None, None, build_dir, 'wheel') assert build_hook.config_version_file is build_hook.config_version_file assert build_hook.config_template is build_hook.config_template hatch-vcs-0.4.0/tests/test_metadata_config.py000066400000000000000000000021531452210221600212470ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2022-present Ofek Lev # # SPDX-License-Identifier: MIT import pytest from hatch_vcs.metadata_hook import VCSMetadataHook class TestURLs: def test_correct(self, new_project_basic): config = {'urls': {'foo': 'url'}} metadata_hook = VCSMetadataHook(new_project_basic, config) assert metadata_hook.config_urls == {'foo': 'url'} def test_not_table(self, new_project_basic): config = {'urls': 9000} metadata_hook = VCSMetadataHook(new_project_basic, config) with pytest.raises(TypeError, match='option `urls` must be a table'): _ = metadata_hook.config_urls def test_url_not_string(self, new_project_basic): config = {'urls': {'foo': 9000}} metadata_hook = VCSMetadataHook(new_project_basic, config) with pytest.raises(TypeError, match='URL `foo` in option `urls` must be a string'): _ = metadata_hook.config_urls def test_coverage(new_project_basic): metadata_hook = VCSMetadataHook(new_project_basic, {}) assert metadata_hook.config_urls is metadata_hook.config_urls hatch-vcs-0.4.0/tests/test_version_config.py000066400000000000000000000042141452210221600211540ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2022-present Ofek Lev # # SPDX-License-Identifier: MIT import pytest from hatch_vcs.version_source import VCSVersionSource class TestTagPattern: def test_correct(self, new_project_basic): config = {'tag-pattern': '.+'} version_source = VCSVersionSource(new_project_basic, config) assert version_source.config_tag_pattern == '.+' def test_not_string(self, new_project_basic): config = {'tag-pattern': 9000} version_source = VCSVersionSource(new_project_basic, config) with pytest.raises(TypeError, match='option `tag-pattern` must be a string'): _ = version_source.config_tag_pattern class TestFallbackVersion: def test_correct(self, new_project_basic): config = {'fallback-version': '0.0.1'} version_source = VCSVersionSource(new_project_basic, config) assert version_source.config_fallback_version == '0.0.1' def test_not_string(self, new_project_basic): config = {'fallback-version': 9000} version_source = VCSVersionSource(new_project_basic, config) with pytest.raises(TypeError, match='option `fallback-version` must be a string'): _ = version_source.config_fallback_version class TestRawOptions: def test_correct(self, new_project_basic): config = {'raw-options': {'normalize': False}} version_source = VCSVersionSource(new_project_basic, config) assert version_source.config_raw_options == {'normalize': False} def test_not_table(self, new_project_basic): config = {'raw-options': 9000} version_source = VCSVersionSource(new_project_basic, config) with pytest.raises(TypeError, match='option `raw-options` must be a table'): _ = version_source.config_raw_options def test_coverage(new_project_basic): version_source = VCSVersionSource(new_project_basic, {}) assert version_source.config_tag_pattern is version_source.config_tag_pattern assert version_source.config_fallback_version is version_source.config_fallback_version assert version_source.config_raw_options is version_source.config_raw_options hatch-vcs-0.4.0/tests/utils.py000066400000000000000000000017571452210221600162540ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2022-present Ofek Lev # # SPDX-License-Identifier: MIT import os import subprocess import sys def create_file(path): with open(path, 'a'): os.utime(path, None) def read_file(path): with open(path) as f: return f.read() def write_file(path, contents): with open(path, 'w') as f: f.write(contents) def build_project(*args, **kwargs): if 'env' not in kwargs: env = os.environ.copy() env.pop('SETUPTOOLS_SCM_PRETEND_VERSION', None) else: env = kwargs['env'] _run_command(sys.executable, '-m', 'hatchling', 'build', *args, env=env) def git(*args): return _run_command('git', *args) def _run_command(*command, **kwargs): process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kwargs) stdout, _ = process.communicate() stdout = stdout.decode('utf-8') if process.returncode: # no cov raise Exception(stdout) return stdout