├── .gitattributes ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── build.yml │ └── test.yml ├── .gitignore ├── HISTORY.md ├── LICENSE.txt ├── README.md ├── hatch.toml ├── pyproject.toml ├── src └── hatch_autorun │ ├── __init__.py │ ├── hooks.py │ └── plugin.py └── tests ├── __init__.py ├── conftest.py ├── test_build.py ├── test_config.py └── utils.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: 2 | - ofek 3 | custom: 4 | - https://ofek.dev/donate/ 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | concurrency: 9 | group: build-${{ github.head_ref }} 10 | 11 | jobs: 12 | build: 13 | name: Build wheels and source distribution 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Install build dependencies 20 | run: python -m pip install --upgrade build 21 | 22 | - name: Build 23 | run: python -m build 24 | 25 | - uses: actions/upload-artifact@v3 26 | with: 27 | name: artifacts 28 | path: dist/* 29 | if-no-files-found: error 30 | 31 | publish: 32 | name: Publish release 33 | needs: 34 | - build 35 | runs-on: ubuntu-latest 36 | 37 | steps: 38 | - uses: actions/download-artifact@v3 39 | with: 40 | name: artifacts 41 | path: dist 42 | 43 | - name: Push build artifacts to PyPI 44 | uses: pypa/gh-action-pypi-publish@v1.6.4 45 | with: 46 | skip_existing: true 47 | user: __token__ 48 | password: ${{ secrets.PYPI_API_TOKEN }} 49 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | concurrency: 12 | group: test-${{ github.head_ref }} 13 | cancel-in-progress: true 14 | 15 | env: 16 | PYTHONUNBUFFERED: "1" 17 | FORCE_COLOR: "1" 18 | 19 | jobs: 20 | run: 21 | name: Python ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} 22 | runs-on: ${{ matrix.os }} 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | os: [ubuntu-latest, windows-latest, macos-latest] 27 | python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] 28 | 29 | steps: 30 | - uses: actions/checkout@v3 31 | 32 | - name: Set up Python ${{ matrix.python-version }} 33 | uses: actions/setup-python@v4 34 | with: 35 | python-version: ${{ matrix.python-version }} 36 | 37 | - name: Install Hatch 38 | run: pip install --upgrade hatch 39 | 40 | - if: matrix.python-version == '3.9' && runner.os == 'Linux' 41 | name: Lint 42 | run: hatch run lint:all 43 | 44 | - name: Run tests 45 | run: hatch run cov 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Global directories 2 | __pycache__/ 3 | 4 | # Global files 5 | *.py[cod] 6 | *.dll 7 | *.so 8 | *.log 9 | *.swp 10 | 11 | # Root directories 12 | /.benchmarks/ 13 | /.env/ 14 | /.idea/ 15 | /.mypy_cache/ 16 | /.pytest_cache/ 17 | /.vscode/ 18 | /dist/ 19 | /site/ 20 | 21 | # Root files 22 | /.coverage* 23 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | ----- 4 | 5 | All notable changes to this project will be documented in this file. 6 | 7 | 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). 8 | 9 | ## Unreleased 10 | 11 | ## 1.1.0 - 2022-08-26 12 | 13 | This is the initial public release. 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-present Ofek Lev 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hatch-autorun 2 | 3 | | | | 4 | | --- | --- | 5 | | CI/CD | [![CI - Test](https://github.com/ofek/hatch-autorun/actions/workflows/test.yml/badge.svg)](https://github.com/ofek/hatch-autorun/actions/workflows/test.yml) [![CD - Build](https://github.com/ofek/hatch-autorun/actions/workflows/build.yml/badge.svg)](https://github.com/ofek/hatch-autorun/actions/workflows/build.yml) | 6 | | Package | [![PyPI - Version](https://img.shields.io/pypi/v/hatch-autorun.svg?logo=pypi&label=PyPI&logoColor=gold)](https://pypi.org/project/hatch-autorun/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/hatch-autorun.svg?logo=python&label=Python&logoColor=gold)](https://pypi.org/project/hatch-autorun/) | 7 | | 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) | 8 | 9 | ----- 10 | 11 | This provides a [build hook](https://hatch.pypa.io/latest/config/build/#build-hooks) plugin for [Hatch](https://github.com/pypa/hatch) that injects code into an installation that will automatically run before the first import. 12 | 13 | **Table of Contents** 14 | 15 | - [Configuration](#configuration) 16 | - [File](#file) 17 | - [Code](#code) 18 | - [Template](#template) 19 | - [Conditional execution](#conditional-execution) 20 | - [License](#license) 21 | 22 | ## Configuration 23 | 24 | The [build hook plugin](https://hatch.pypa.io/latest/plugins/build-hook/) name is `autorun`. 25 | 26 | - ***pyproject.toml*** 27 | 28 | ```toml 29 | [tool.hatch.build.targets.wheel.hooks.autorun] 30 | dependencies = ["hatch-autorun"] 31 | ``` 32 | 33 | - ***hatch.toml*** 34 | 35 | ```toml 36 | [build.targets.wheel.hooks.autorun] 37 | dependencies = ["hatch-autorun"] 38 | ``` 39 | 40 | ### File 41 | 42 | You can select a relative path to a file containing the code with the `file` option: 43 | 44 | ```toml 45 | [tool.hatch.build.targets.wheel.hooks.autorun] 46 | file = "resources/code.emded" 47 | ``` 48 | 49 | ### Code 50 | 51 | You can define the code itself with the `code` option: 52 | 53 | ```toml 54 | [tool.hatch.build.targets.wheel.hooks.autorun] 55 | code = """ 56 | import coverage 57 | coverage.process_startup() 58 | """ 59 | ``` 60 | 61 | ### Template 62 | 63 | The current implementation uses a `.pth` file to execute the code. You can set the `.pth` file template with the `template` option, which will be formatted with a `code` variable representing the `code` option or the contents of the file defined by the `file` option. The following shows the default template: 64 | 65 | ```toml 66 | [tool.hatch.build.targets.wheel.hooks.autorun] 67 | template = "import os, sys;exec({code!r})" 68 | ``` 69 | 70 | ## Conditional execution 71 | 72 | Sometimes you'll only want builds to induce auto-run behavior when installed under certain circumstances, like for tests. In such cases, set the `enable-by-default` [option](https://hatch.pypa.io/latest/config/build/#conditional-execution) to `false`: 73 | 74 | ```toml 75 | [tool.hatch.build.targets.wheel.hooks.autorun] 76 | enable-by-default = false 77 | ``` 78 | 79 | Then when the desired build conditions are met, set the `HATCH_BUILD_HOOK_ENABLE_AUTORUN` [environment variable](https://hatch.pypa.io/latest/config/build/#environment-variables) to `true` or `1`. 80 | 81 | ## License 82 | 83 | `hatch-autorun` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. 84 | -------------------------------------------------------------------------------- /hatch.toml: -------------------------------------------------------------------------------- 1 | [envs.default] 2 | dependencies = [ 3 | "coverage[toml]>=6.5", 4 | "pytest", 5 | "pytest-mock", 6 | "build[virtualenv]", 7 | ] 8 | [envs.default.scripts] 9 | test = "pytest {args:tests}" 10 | test-cov = "coverage run -m pytest {args:tests}" 11 | cov-report = [ 12 | "- coverage combine", 13 | "coverage report --show-missing", 14 | ] 15 | cov = [ 16 | "test-cov", 17 | "cov-report", 18 | ] 19 | 20 | [[envs.all.matrix]] 21 | python = ["3.7", "3.8", "3.9", "3.10", "3.11"] 22 | 23 | [envs.lint] 24 | detached = true 25 | dependencies = [ 26 | "black>=22.10.0", 27 | "mypy>=0.991", 28 | "ruff>=0.0.166", 29 | ] 30 | [envs.lint.scripts] 31 | typing = "mypy --install-types --non-interactive {args:src/hatch_autorun tests}" 32 | style = [ 33 | "ruff {args:.}", 34 | "black --check --diff {args:.}", 35 | ] 36 | fmt = [ 37 | "black {args:.}", 38 | "ruff --fix {args:.}", 39 | "style", 40 | ] 41 | all = [ 42 | "style", 43 | "typing", 44 | ] 45 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling", "hatch-vcs"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "hatch-autorun" 7 | dynamic = ["version"] 8 | description = "Hatch build hook plugin to inject code that will automatically run" 9 | readme = "README.md" 10 | requires-python = ">=3.7" 11 | license = "MIT" 12 | keywords = [ 13 | "autorun", 14 | "build", 15 | "hatch", 16 | "plugin", 17 | "pth", 18 | ] 19 | authors = [ 20 | { name = "Ofek Lev", email = "oss@ofek.dev" }, 21 | ] 22 | classifiers = [ 23 | "Development Status :: 5 - Production/Stable", 24 | "Framework :: Hatch", 25 | "License :: OSI Approved :: MIT License", 26 | "Programming Language :: Python", 27 | "Programming Language :: Python :: 3.7", 28 | "Programming Language :: Python :: 3.8", 29 | "Programming Language :: Python :: 3.9", 30 | "Programming Language :: Python :: 3.10", 31 | "Programming Language :: Python :: 3.11", 32 | "Programming Language :: Python :: Implementation :: CPython", 33 | "Programming Language :: Python :: Implementation :: PyPy", 34 | "Topic :: Software Development :: Build Tools", 35 | ] 36 | dependencies = [ 37 | "hatchling>=1.6.0", 38 | ] 39 | 40 | [project.urls] 41 | Funding = "https://github.com/sponsors/ofek" 42 | History = "https://github.com/ofek/hatch-autorun/blob/master/HISTORY.md" 43 | Issues = "https://github.com/ofek/hatch-autorun/issues" 44 | Source = "https://github.com/ofek/hatch-autorun" 45 | 46 | [project.entry-points.hatch] 47 | autorun = "hatch_autorun.hooks" 48 | 49 | [tool.hatch.version] 50 | source = "vcs" 51 | 52 | [tool.mypy] 53 | disallow_untyped_defs = false 54 | follow_imports = "normal" 55 | ignore_missing_imports = true 56 | pretty = true 57 | show_column_numbers = true 58 | warn_no_return = false 59 | warn_unused_ignores = true 60 | 61 | [tool.black] 62 | target-version = ["py37"] 63 | line-length = 120 64 | skip-string-normalization = true 65 | 66 | [tool.ruff] 67 | target-version = "py37" 68 | line-length = 120 69 | select = [ 70 | "A", 71 | "B", 72 | "C", 73 | "E", 74 | "F", 75 | "FBT", 76 | "I", 77 | "N", 78 | "Q", 79 | "RUF", 80 | "S", 81 | "T", 82 | "UP", 83 | "W", 84 | "YTT", 85 | ] 86 | ignore = [ 87 | # Allow non-abstract empty methods in abstract base classes 88 | "B027", 89 | # Ignore McCabe complexity 90 | "C901", 91 | # Allow boolean positional values in function calls, like `dict.get(... True)` 92 | "FBT003", 93 | # Ignore checks for possible passwords 94 | "S105", "S106", "S107", 95 | ] 96 | unfixable = [ 97 | # Don't touch unused imports 98 | "F401", 99 | ] 100 | 101 | [tool.ruff.isort] 102 | known-first-party = ["hatch_autorun"] 103 | 104 | [tool.ruff.flake8-quotes] 105 | inline-quotes = "single" 106 | 107 | [tool.ruff.flake8-tidy-imports] 108 | ban-relative-imports = "all" 109 | 110 | [tool.ruff.per-file-ignores] 111 | # Tests can use relative imports and assertions 112 | "tests/**/*" = ["I252", "S101"] 113 | 114 | [tool.coverage.run] 115 | source_pkgs = ["hatch_autorun", "tests"] 116 | branch = true 117 | parallel = true 118 | omit = [ 119 | "src/hatch_autorun/__main__.py", 120 | ] 121 | 122 | [tool.coverage.paths] 123 | hatch_autorun = ["src/hatch_autorun", "*/hatch-autorun/src/hatch_autorun"] 124 | tests = ["tests", "*/hatch-autorun/tests"] 125 | 126 | [tool.coverage.report] 127 | exclude_lines = [ 128 | "no cov", 129 | "if __name__ == .__main__.:", 130 | "if TYPE_CHECKING:", 131 | ] 132 | -------------------------------------------------------------------------------- /src/hatch_autorun/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022-present Ofek Lev 2 | # 3 | # SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /src/hatch_autorun/hooks.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022-present Ofek Lev 2 | # 3 | # SPDX-License-Identifier: MIT 4 | from hatchling.plugin import hookimpl 5 | 6 | from hatch_autorun.plugin import AutoRunBuildHook 7 | 8 | 9 | @hookimpl 10 | def hatch_register_build_hook(): 11 | return AutoRunBuildHook 12 | -------------------------------------------------------------------------------- /src/hatch_autorun/plugin.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022-present Ofek Lev 2 | # 3 | # SPDX-License-Identifier: MIT 4 | from __future__ import annotations 5 | 6 | import os 7 | import tempfile 8 | 9 | from hatchling.builders.hooks.plugin.interface import BuildHookInterface 10 | 11 | 12 | class AutoRunBuildHook(BuildHookInterface): 13 | PLUGIN_NAME = 'autorun' 14 | 15 | def __init__(self, *args, **kwargs): 16 | super().__init__(*args, **kwargs) 17 | 18 | self.__config_file = None 19 | self.__config_code = None 20 | self.__config_template = None 21 | self.__temp_dir = None 22 | 23 | @property 24 | def config_file(self): 25 | if self.__config_file is None: 26 | file = self.config.get('file', '') 27 | if not isinstance(file, str): 28 | raise TypeError(f'Option `file` for build hook `{self.PLUGIN_NAME}` must be a string') 29 | 30 | self.__config_file = file 31 | 32 | return self.__config_file 33 | 34 | @property 35 | def config_code(self): 36 | if self.__config_code is None: 37 | code = self.config.get('code', '') 38 | if not isinstance(code, str): 39 | raise TypeError(f'Option `code` for build hook `{self.PLUGIN_NAME}` must be a string') 40 | 41 | self.__config_code = code 42 | 43 | return self.__config_code 44 | 45 | @property 46 | def config_template(self): 47 | if self.__config_template is None: 48 | template = self.config.get('template', 'import os, sys;exec({code!r})') 49 | if not isinstance(template, str): 50 | raise TypeError(f'Option `template` for build hook `{self.PLUGIN_NAME}` must be a string') 51 | 52 | self.__config_template = template 53 | 54 | return self.__config_template 55 | 56 | @property 57 | def temp_dir(self): 58 | if self.__temp_dir is None: 59 | self.__temp_dir = os.path.realpath(tempfile.mkdtemp()) 60 | 61 | return self.__temp_dir 62 | 63 | def initialize(self, version, build_data): 64 | if self.target_name != 'wheel': 65 | return 66 | elif not (self.config_file or self.config_code): 67 | raise ValueError(f'The build hook `{self.PLUGIN_NAME}` option `file` or `code` must be specified') 68 | elif self.config_file and self.config_code: 69 | raise ValueError(f'The build hook `{self.PLUGIN_NAME}` options `file` and `code` are mutually exclusive') 70 | elif self.config_file: 71 | with open(os.path.normpath(os.path.join(self.root, self.config_file)), encoding='utf-8') as f: 72 | code = f.read() 73 | else: 74 | code = self.config_code 75 | 76 | project_name = self.build_config.builder.metadata.core.name.replace('-', '_') 77 | file_name = f'hatch_{self.PLUGIN_NAME}_{project_name}.pth' 78 | pth_file = os.path.join(self.temp_dir, file_name) 79 | with open(pth_file, 'w', encoding='utf-8') as f: 80 | f.write(self.config_template.format(code=code)) 81 | 82 | if version == 'editable': # no cov 83 | build_data['force_include_editable'][pth_file] = file_name 84 | else: 85 | build_data['force_include'][pth_file] = file_name 86 | 87 | def finalize(self, version, build_data, artifact_path): 88 | import shutil 89 | 90 | try: 91 | shutil.rmtree(self.temp_dir) 92 | except Exception: 93 | pass 94 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022-present Ofek Lev 2 | # 3 | # SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022-present Ofek Lev 2 | # 3 | # SPDX-License-Identifier: MIT 4 | import errno 5 | import os 6 | import shutil 7 | import stat 8 | import sys 9 | from pathlib import Path 10 | from tempfile import TemporaryDirectory 11 | from typing import Generator 12 | 13 | import pytest 14 | 15 | 16 | def handle_remove_readonly(func, path, exc): # no cov 17 | # TODO: remove when we drop Python 3.7 18 | # PermissionError: [WinError 5] Access is denied: '...\\.git\\...' 19 | if func in (os.rmdir, os.remove, os.unlink) and exc[1].errno == errno.EACCES: 20 | os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) 21 | func(path) 22 | else: 23 | raise 24 | 25 | 26 | @pytest.fixture(scope='session') 27 | def plugin_dir() -> Generator[Path, None, None]: 28 | with TemporaryDirectory() as d: 29 | directory = Path(d, 'plugin') 30 | shutil.copytree(Path.cwd(), directory) 31 | 32 | yield directory.resolve() 33 | 34 | shutil.rmtree(directory, ignore_errors=False, onerror=handle_remove_readonly) 35 | 36 | 37 | @pytest.fixture 38 | def new_project(plugin_dir, tmp_path) -> Generator[Path, None, None]: 39 | project_dir = tmp_path / 'my-app' 40 | project_dir.mkdir() 41 | 42 | project_file = project_dir / 'pyproject.toml' 43 | project_file.write_text( 44 | f"""\ 45 | [build-system] 46 | requires = ["hatchling", "hatch-autorun @ {plugin_dir.as_uri()}"] 47 | build-backend = "hatchling.build" 48 | 49 | [project] 50 | name = "my-app" 51 | dynamic = ["version"] 52 | requires-python = ">={sys.version_info[0]}" 53 | 54 | [tool.hatch.version] 55 | path = "my_app/__init__.py" 56 | 57 | [tool.hatch.build.targets.wheel.hooks.autorun] 58 | """, 59 | encoding='utf-8', 60 | ) 61 | 62 | package_dir = project_dir / 'my_app' 63 | package_dir.mkdir() 64 | 65 | package_root = package_dir / '__init__.py' 66 | package_root.write_text('__version__ = "1.2.3"', encoding='utf-8') 67 | 68 | origin = os.getcwd() 69 | os.chdir(project_dir) 70 | try: 71 | yield project_dir 72 | finally: 73 | os.chdir(origin) 74 | -------------------------------------------------------------------------------- /tests/test_build.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022-present Ofek Lev 2 | # 3 | # SPDX-License-Identifier: MIT 4 | import sys 5 | import zipfile 6 | 7 | import pytest 8 | 9 | from .utils import build_project 10 | 11 | CODE = """\ 12 | try: 13 | import coverage 14 | # coverage throws OSError when $PWD does not exist 15 | except (ImportError, OSError): 16 | pass 17 | else: 18 | coverage.process_startup() 19 | """ 20 | 21 | 22 | def test_no_options(new_project): 23 | with pytest.raises(Exception, match='The build hook `autorun` option `file` or `code` must be specified'): 24 | build_project() 25 | 26 | 27 | def test_multiple_options(new_project): 28 | project_file = new_project / 'pyproject.toml' 29 | contents = project_file.read_text(encoding='utf-8') 30 | contents += '\nfile = "code.emded"' 31 | contents += f'\ncode = """\n{CODE}"""' 32 | project_file.write_text(contents, encoding='utf-8') 33 | 34 | with pytest.raises(Exception, match='The build hook `autorun` options `file` and `code` are mutually exclusive'): 35 | build_project() 36 | 37 | 38 | def test_target_not_wheel(new_project): 39 | project_file = new_project / 'pyproject.toml' 40 | contents = project_file.read_text(encoding='utf-8') 41 | contents = contents.replace('[tool.hatch.build.targets.wheel.hooks.autorun]', '[tool.hatch.build.hooks.autorun]') 42 | project_file.write_text(contents, encoding='utf-8') 43 | 44 | build_project('-s') 45 | 46 | build_dir = new_project / 'dist' 47 | assert build_dir.is_dir() 48 | 49 | artifacts = list(build_dir.iterdir()) 50 | assert len(artifacts) == 1 51 | 52 | assert artifacts[0].name.endswith('.tar.gz') 53 | 54 | 55 | def test_file(new_project): 56 | project_file = new_project / 'pyproject.toml' 57 | contents = project_file.read_text(encoding='utf-8') 58 | contents += '\nfile = "code.emded"' 59 | project_file.write_text(contents, encoding='utf-8') 60 | 61 | package_main = new_project / 'code.emded' 62 | package_main.write_text(CODE, encoding='utf-8') 63 | 64 | build_project() 65 | 66 | build_dir = new_project / 'dist' 67 | assert build_dir.is_dir() 68 | 69 | artifacts = list(build_dir.iterdir()) 70 | assert len(artifacts) == 1 71 | wheel_file = artifacts[0] 72 | 73 | assert wheel_file.name == f'my_app-1.2.3-py{sys.version_info[0]}-none-any.whl' 74 | 75 | extraction_directory = new_project.parent / '_archive' 76 | extraction_directory.mkdir() 77 | 78 | with zipfile.ZipFile(str(wheel_file), 'r') as zip_archive: 79 | zip_archive.extractall(str(extraction_directory)) 80 | 81 | root_paths = list(extraction_directory.iterdir()) 82 | assert len(root_paths) == 3 83 | 84 | pth_file = extraction_directory / 'hatch_autorun_my_app.pth' 85 | assert pth_file.is_file() 86 | assert pth_file.read_text() == f'import os, sys;exec({CODE!r})' 87 | 88 | 89 | def test_code(new_project): 90 | project_file = new_project / 'pyproject.toml' 91 | contents = project_file.read_text(encoding='utf-8') 92 | contents += f'\ncode = """\n{CODE}"""' 93 | project_file.write_text(contents, encoding='utf-8') 94 | 95 | package_main = new_project / 'code.emded' 96 | package_main.write_text(CODE, encoding='utf-8') 97 | 98 | build_project() 99 | 100 | build_dir = new_project / 'dist' 101 | assert build_dir.is_dir() 102 | 103 | artifacts = list(build_dir.iterdir()) 104 | assert len(artifacts) == 1 105 | wheel_file = artifacts[0] 106 | 107 | assert wheel_file.name == f'my_app-1.2.3-py{sys.version_info[0]}-none-any.whl' 108 | 109 | extraction_directory = new_project.parent / '_archive' 110 | extraction_directory.mkdir() 111 | 112 | with zipfile.ZipFile(str(wheel_file), 'r') as zip_archive: 113 | zip_archive.extractall(str(extraction_directory)) 114 | 115 | root_paths = list(extraction_directory.iterdir()) 116 | assert len(root_paths) == 3 117 | 118 | pth_file = extraction_directory / 'hatch_autorun_my_app.pth' 119 | assert pth_file.is_file() 120 | assert pth_file.read_text() == f'import os, sys;exec({CODE!r})' 121 | 122 | 123 | def test_file_and_template(new_project): 124 | project_file = new_project / 'pyproject.toml' 125 | contents = project_file.read_text(encoding='utf-8') 126 | contents += '\nfile = "code.emded"' 127 | contents += '\ntemplate = "import foo;exec({code!r})"' 128 | project_file.write_text(contents, encoding='utf-8') 129 | 130 | package_main = new_project / 'code.emded' 131 | package_main.write_text(CODE, encoding='utf-8') 132 | 133 | build_project() 134 | 135 | build_dir = new_project / 'dist' 136 | assert build_dir.is_dir() 137 | 138 | artifacts = list(build_dir.iterdir()) 139 | assert len(artifacts) == 1 140 | wheel_file = artifacts[0] 141 | 142 | assert wheel_file.name == f'my_app-1.2.3-py{sys.version_info[0]}-none-any.whl' 143 | 144 | extraction_directory = new_project.parent / '_archive' 145 | extraction_directory.mkdir() 146 | 147 | with zipfile.ZipFile(str(wheel_file), 'r') as zip_archive: 148 | zip_archive.extractall(str(extraction_directory)) 149 | 150 | root_paths = list(extraction_directory.iterdir()) 151 | assert len(root_paths) == 3 152 | 153 | pth_file = extraction_directory / 'hatch_autorun_my_app.pth' 154 | assert pth_file.is_file() 155 | assert pth_file.read_text() == f'import foo;exec({CODE!r})' 156 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022-present Ofek Lev 2 | # 3 | # SPDX-License-Identifier: MIT 4 | import pytest 5 | 6 | from hatch_autorun.plugin import AutoRunBuildHook 7 | 8 | 9 | class TestFile: 10 | def test_default(self, new_project): 11 | config = {} 12 | build_dir = new_project / 'dist' 13 | build_hook = AutoRunBuildHook(str(new_project), config, None, None, str(build_dir), 'wheel') 14 | 15 | assert build_hook.config_file == build_hook.config_file == '' 16 | 17 | def test_correct(self, new_project): 18 | config = {'file': 'foo'} 19 | build_dir = new_project / 'dist' 20 | build_hook = AutoRunBuildHook(str(new_project), config, None, None, str(build_dir), 'wheel') 21 | 22 | assert build_hook.config_file == build_hook.config_file == 'foo' 23 | 24 | def test_not_string(self, new_project): 25 | config = {'file': 9000} 26 | build_dir = new_project / 'dist' 27 | build_hook = AutoRunBuildHook(str(new_project), config, None, None, str(build_dir), 'wheel') 28 | 29 | with pytest.raises(TypeError, match='Option `file` for build hook `autorun` must be a string'): 30 | _ = build_hook.config_file 31 | 32 | 33 | class TestCode: 34 | def test_default(self, new_project): 35 | config = {} 36 | build_dir = new_project / 'dist' 37 | build_hook = AutoRunBuildHook(str(new_project), config, None, None, str(build_dir), 'wheel') 38 | 39 | assert build_hook.config_code == build_hook.config_code == '' 40 | 41 | def test_correct(self, new_project): 42 | config = {'code': 'foo'} 43 | build_dir = new_project / 'dist' 44 | build_hook = AutoRunBuildHook(str(new_project), config, None, None, str(build_dir), 'wheel') 45 | 46 | assert build_hook.config_code == build_hook.config_code == 'foo' 47 | 48 | def test_not_string(self, new_project): 49 | config = {'code': 9000} 50 | build_dir = new_project / 'dist' 51 | build_hook = AutoRunBuildHook(str(new_project), config, None, None, str(build_dir), 'wheel') 52 | 53 | with pytest.raises(TypeError, match='Option `code` for build hook `autorun` must be a string'): 54 | _ = build_hook.config_code 55 | 56 | 57 | class TestTemplate: 58 | def test_default(self, new_project): 59 | config = {} 60 | build_dir = new_project / 'dist' 61 | build_hook = AutoRunBuildHook(str(new_project), config, None, None, str(build_dir), 'wheel') 62 | 63 | assert build_hook.config_template == build_hook.config_template == 'import os, sys;exec({code!r})' 64 | 65 | def test_correct(self, new_project): 66 | config = {'template': 'foo'} 67 | build_dir = new_project / 'dist' 68 | build_hook = AutoRunBuildHook(str(new_project), config, None, None, str(build_dir), 'wheel') 69 | 70 | assert build_hook.config_template == build_hook.config_template == 'foo' 71 | 72 | def test_not_string(self, new_project): 73 | config = {'template': 9000} 74 | build_dir = new_project / 'dist' 75 | build_hook = AutoRunBuildHook(str(new_project), config, None, None, str(build_dir), 'wheel') 76 | 77 | with pytest.raises(TypeError, match='Option `template` for build hook `autorun` must be a string'): 78 | _ = build_hook.config_template 79 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022-present Ofek Lev 2 | # 3 | # SPDX-License-Identifier: MIT 4 | import subprocess 5 | import sys 6 | 7 | 8 | def build_project(*args): 9 | if not args: 10 | args = ['-w'] 11 | 12 | process = subprocess.run([sys.executable, '-m', 'build', *args], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 13 | if process.returncode: # no cov 14 | raise Exception(process.stdout.decode('utf-8')) 15 | --------------------------------------------------------------------------------