├── tests ├── __init__.py ├── packages │ ├── readme-md │ │ ├── README.md │ │ ├── readme_md.py │ │ └── pyproject.toml │ ├── readme-rst │ │ ├── README.rst │ │ ├── readme_rst.py │ │ └── pyproject.toml │ ├── file-module │ │ ├── file_module.py │ │ └── pyproject.toml │ ├── full-tasks │ │ ├── full_tasks.py │ │ ├── pyproject.toml │ │ └── .trampolim.py │ ├── license-file │ │ ├── license_file.py │ │ ├── some-license-file │ │ └── pyproject.toml │ ├── license-text │ │ ├── license_text.py │ │ └── pyproject.toml │ ├── no-version │ │ ├── no_version.py │ │ └── pyproject.toml │ ├── vcs-version1 │ │ ├── vcs_version1.py │ │ ├── .git-archive.txt │ │ └── pyproject.toml │ ├── vcs-version2 │ │ ├── vcs_version2.py │ │ ├── .git-archive.txt │ │ └── pyproject.toml │ ├── vcs-version3 │ │ ├── vcs_version3.py │ │ ├── .git-archive.txt │ │ └── pyproject.toml │ ├── full-metadata │ │ ├── full_metadata.py │ │ ├── README.md │ │ └── pyproject.toml │ ├── readme-unknown │ │ ├── README.unknown │ │ ├── readme_unknown.py │ │ └── pyproject.toml │ ├── source-include │ │ ├── source_include.py │ │ ├── helper-data │ │ │ ├── a │ │ │ ├── b │ │ │ └── c │ │ ├── some-config.txt │ │ └── pyproject.toml │ ├── sample-source │ │ ├── sample_source │ │ │ ├── d │ │ │ │ ├── da.py │ │ │ │ └── db.py │ │ │ ├── e │ │ │ │ ├── ea.py │ │ │ │ ├── eb.py │ │ │ │ └── ec │ │ │ │ │ ├── eca.py │ │ │ │ │ ├── ecb.py │ │ │ │ │ └── ecc.py │ │ │ ├── f │ │ │ │ ├── fa.py │ │ │ │ ├── fb.py │ │ │ │ └── fc │ │ │ │ │ ├── fca.py │ │ │ │ │ ├── fcb.py │ │ │ │ │ ├── fcc.py │ │ │ │ │ ├── fcc │ │ │ │ │ └── fcca.py │ │ │ │ │ └── fcd │ │ │ │ │ ├── fcda.py │ │ │ │ │ └── fcdb.py │ │ │ ├── a.py │ │ │ ├── c.py │ │ │ └── b.py │ │ └── pyproject.toml │ ├── src-layout │ │ ├── src │ │ │ └── src_layout │ │ │ │ ├── __init__.py │ │ │ │ ├── d │ │ │ │ └── __init__.py │ │ │ │ ├── a.py │ │ │ │ ├── c.py │ │ │ │ └── b.py │ │ └── pyproject.toml │ ├── task-missing-source │ │ ├── example_source.py │ │ ├── task_missing_source.py │ │ ├── pyproject.toml │ │ └── .trampolim.py │ ├── readme-unknown-with-type │ │ ├── README.unknown │ │ ├── readme_unknown_with_type.py │ │ └── pyproject.toml │ ├── invalid-parameter-task │ │ ├── invalid_parameter_task.py │ │ ├── pyproject.toml │ │ └── .trampolim.py │ ├── absolute-module-location │ │ ├── absolute_module_location.py │ │ └── pyproject.toml │ ├── vcs-version-unpopulated │ │ ├── vcs_version_unpopulated.py │ │ ├── .git-archive.txt │ │ └── pyproject.toml │ ├── no-module │ │ └── pyproject.toml │ ├── test-top-level-module │ │ └── pyproject.toml │ └── custom-top-modules │ │ └── pyproject.toml ├── test_pep517.py ├── test_tasks.py ├── test_metadata.py ├── conftest.py ├── test_main.py ├── test_wheel.py ├── test_sdist.py └── test_project.py ├── .gitignore ├── codecov.yml ├── .github ├── FUNDING.yml └── workflows │ ├── check.yml │ └── test.yml ├── docs ├── changelog.rst ├── index.rst ├── api.rst └── conf.py ├── .gitattributes ├── .git-archive.txt ├── .readthedocs.yml ├── trampolim ├── types.py ├── _metadata.py ├── __init__.py ├── _tasks.py ├── _wheel.py ├── _sdist.py ├── __main__.py └── _build.py ├── setup.cfg ├── .pre-commit-config.yaml ├── LICENSE ├── CHANGELOG.rst ├── pyproject.toml ├── README.md └── noxfile.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .trampolim/ 2 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: FFY00 2 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.rst -------------------------------------------------------------------------------- /tests/packages/readme-md/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/readme-md/readme_md.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/readme-rst/README.rst: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/file-module/file_module.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/full-tasks/full_tasks.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/license-file/license_file.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/license-text/license_text.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/no-version/no_version.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/readme-rst/readme_rst.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/vcs-version1/vcs_version1.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/vcs-version2/vcs_version2.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/vcs-version3/vcs_version3.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/full-metadata/full_metadata.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/readme-unknown/README.unknown: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/readme-unknown/readme_unknown.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/source-include/source_include.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | .git-archive.txt export-subst 2 | -------------------------------------------------------------------------------- /tests/packages/sample-source/sample_source/d/da.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/sample-source/sample_source/d/db.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/sample-source/sample_source/e/ea.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/sample-source/sample_source/e/eb.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/sample-source/sample_source/f/fa.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/sample-source/sample_source/f/fb.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/source-include/helper-data/a: -------------------------------------------------------------------------------- 1 | aaa 2 | -------------------------------------------------------------------------------- /tests/packages/source-include/helper-data/b: -------------------------------------------------------------------------------- 1 | bbb 2 | -------------------------------------------------------------------------------- /tests/packages/source-include/helper-data/c: -------------------------------------------------------------------------------- 1 | ccc 2 | -------------------------------------------------------------------------------- /tests/packages/src-layout/src/src_layout/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/task-missing-source/example_source.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/full-metadata/README.md: -------------------------------------------------------------------------------- 1 | some readme 2 | -------------------------------------------------------------------------------- /tests/packages/license-file/some-license-file: -------------------------------------------------------------------------------- 1 | blah 2 | -------------------------------------------------------------------------------- /tests/packages/readme-unknown-with-type/README.unknown: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/sample-source/sample_source/e/ec/eca.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/sample-source/sample_source/e/ec/ecb.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/sample-source/sample_source/e/ec/ecc.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/sample-source/sample_source/f/fc/fca.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/sample-source/sample_source/f/fc/fcb.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/sample-source/sample_source/f/fc/fcc.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/src-layout/src/src_layout/d/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/task-missing-source/task_missing_source.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/invalid-parameter-task/invalid_parameter_task.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/sample-source/sample_source/a.py: -------------------------------------------------------------------------------- 1 | # test a! 2 | -------------------------------------------------------------------------------- /tests/packages/sample-source/sample_source/f/fc/fcc/fcca.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/sample-source/sample_source/f/fc/fcd/fcda.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/src-layout/src/src_layout/a.py: -------------------------------------------------------------------------------- 1 | # test a! 2 | -------------------------------------------------------------------------------- /tests/packages/absolute-module-location/absolute_module_location.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/readme-unknown-with-type/readme_unknown_with_type.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/source-include/some-config.txt: -------------------------------------------------------------------------------- 1 | idk, some config 2 | -------------------------------------------------------------------------------- /tests/packages/src-layout/src/src_layout/c.py: -------------------------------------------------------------------------------- 1 | hello = [0, 1, 2] 2 | -------------------------------------------------------------------------------- /tests/packages/vcs-version-unpopulated/vcs_version_unpopulated.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/packages/sample-source/sample_source/c.py: -------------------------------------------------------------------------------- 1 | hello = [0, 1, 2] 2 | -------------------------------------------------------------------------------- /tests/packages/src-layout/src/src_layout/b.py: -------------------------------------------------------------------------------- 1 | print('some python code...') 2 | -------------------------------------------------------------------------------- /tests/packages/sample-source/sample_source/b.py: -------------------------------------------------------------------------------- 1 | print('some python code...') 2 | -------------------------------------------------------------------------------- /tests/packages/vcs-version1/.git-archive.txt: -------------------------------------------------------------------------------- 1 | ref-names: tag: 0.0.1 2 | commit: dummy 3 | -------------------------------------------------------------------------------- /.git-archive.txt: -------------------------------------------------------------------------------- 1 | ref-names: HEAD -> main 2 | commit: aed1982f019fbfe1811dc0a03739a2f4dd19662b 3 | -------------------------------------------------------------------------------- /tests/packages/sample-source/sample_source/f/fc/fcd/fcdb.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | print(sys.version_info) 5 | -------------------------------------------------------------------------------- /tests/packages/vcs-version-unpopulated/.git-archive.txt: -------------------------------------------------------------------------------- 1 | ref-names: HEAD -> main 2 | commit: aed1982f019fbfe1811dc0a03739a2f4dd19662b 3 | -------------------------------------------------------------------------------- /tests/packages/vcs-version2/.git-archive.txt: -------------------------------------------------------------------------------- 1 | ref-names: HEAD -> my_branch, some_upstream/some_branch, some_branch, tag: 0.0.2 2 | commit: dummy 3 | -------------------------------------------------------------------------------- /tests/packages/vcs-version3/.git-archive.txt: -------------------------------------------------------------------------------- 1 | ref-names: HEAD -> my_branch, some_upstream/some_branch, some_branch 2 | commit: this-is-a-commit 3 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | python: 4 | version: 3.8 5 | install: 6 | - method: pip 7 | path: . 8 | extra_requirements: [docs] 9 | -------------------------------------------------------------------------------- /tests/packages/full-tasks/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'trampolim' 3 | requires = ['trampolim'] 4 | 5 | [project] 6 | name = 'full-tasks' 7 | version = '0.0.1' 8 | -------------------------------------------------------------------------------- /tests/packages/task-missing-source/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'trampolim' 3 | requires = ['trampolim'] 4 | 5 | [project] 6 | name = 'task-missing-source' 7 | version = '0.0.1' 8 | -------------------------------------------------------------------------------- /tests/packages/readme-md/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'trampolim' 3 | requires = ['trampolim'] 4 | 5 | [project] 6 | name = 'readme-md' 7 | version = '1.0.0' 8 | readme = 'README.md' 9 | -------------------------------------------------------------------------------- /tests/packages/invalid-parameter-task/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'trampolim' 3 | requires = ['trampolim'] 4 | 5 | [project] 6 | name = 'invalid-parameter-task' 7 | version = '0.0.0' 8 | -------------------------------------------------------------------------------- /tests/packages/readme-rst/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'trampolim' 3 | requires = ['trampolim'] 4 | 5 | [project] 6 | name = 'readme-rst' 7 | version = '1.0.0' 8 | readme = 'README.rst' 9 | -------------------------------------------------------------------------------- /tests/packages/file-module/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'trampolim' 3 | requires = ['trampolim'] 4 | 5 | [project] 6 | name = 'file-module' 7 | version = '0.0.0' 8 | license = { text = '...' } 9 | -------------------------------------------------------------------------------- /tests/packages/no-module/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'trampolim' 3 | requires = ['trampolim'] 4 | 5 | [project] 6 | name = 'no-module' 7 | version = '0.0.0' 8 | license = { text = '...' } 9 | -------------------------------------------------------------------------------- /tests/packages/readme-unknown/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'trampolim' 3 | requires = ['trampolim'] 4 | 5 | [project] 6 | name = 'readme-unknown' 7 | version = '1.0.0' 8 | readme = 'README.unknown' 9 | -------------------------------------------------------------------------------- /tests/packages/sample-source/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'trampolim' 3 | requires = ['trampolim'] 4 | 5 | [project] 6 | name = 'sample-source' 7 | version = '0.0.0' 8 | license = { text = '...' } 9 | -------------------------------------------------------------------------------- /tests/packages/license-text/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'trampolim' 3 | requires = ['trampolim'] 4 | 5 | [project] 6 | name = 'license-text' 7 | version = '0.0.0' 8 | license = { text = 'inline license!' } 9 | -------------------------------------------------------------------------------- /tests/packages/invalid-parameter-task/.trampolim.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | import trampolim 4 | 5 | 6 | @trampolim.task 7 | def invalid_parameters(this_is_not_a_valid_parameter): 8 | pass # pragma: no cover 9 | -------------------------------------------------------------------------------- /tests/packages/license-file/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'trampolim' 3 | requires = ['trampolim'] 4 | 5 | [project] 6 | name = 'license-file' 7 | version = '0.0.0' 8 | license = { file = 'some-license-file' } 9 | -------------------------------------------------------------------------------- /tests/packages/no-version/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'trampolim' 3 | requires = ['trampolim'] 4 | 5 | [project] 6 | name = 'no-version' 7 | license = { text = '...' } 8 | dynamic = [ 9 | 'version', 10 | ] 11 | -------------------------------------------------------------------------------- /tests/packages/src-layout/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'trampolim' 3 | requires = ['trampolim'] 4 | 5 | [project] 6 | name = 'src-layout' 7 | version = '0.0.0' 8 | 9 | [tool.trampolim] 10 | module-location = 'src' 11 | -------------------------------------------------------------------------------- /tests/packages/vcs-version1/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'trampolim' 3 | requires = ['trampolim'] 4 | 5 | [project] 6 | name = 'vcs-version1' 7 | license = { text = '...' } 8 | dynamic = [ 9 | 'version', 10 | ] 11 | -------------------------------------------------------------------------------- /tests/packages/vcs-version2/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'trampolim' 3 | requires = ['trampolim'] 4 | 5 | [project] 6 | name = 'vcs-version2' 7 | license = { text = '...' } 8 | dynamic = [ 9 | 'version', 10 | ] 11 | -------------------------------------------------------------------------------- /tests/packages/vcs-version3/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'trampolim' 3 | requires = ['trampolim'] 4 | 5 | [project] 6 | name = 'vcs-version3' 7 | license = { text = '...' } 8 | dynamic = [ 9 | 'version', 10 | ] 11 | -------------------------------------------------------------------------------- /tests/packages/vcs-version-unpopulated/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'trampolim' 3 | requires = ['trampolim'] 4 | 5 | [project] 6 | name = 'vcs-version-unpopulated' 7 | license = { text = '...' } 8 | dynamic = [ 9 | 'version', 10 | ] 11 | -------------------------------------------------------------------------------- /tests/packages/absolute-module-location/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'trampolim' 3 | requires = ['trampolim'] 4 | 5 | [project] 6 | name = 'absolute-module-location' 7 | version = '0.0.0' 8 | 9 | [tool.trampolim] 10 | module-location = '/hello' 11 | -------------------------------------------------------------------------------- /tests/packages/readme-unknown-with-type/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'trampolim' 3 | requires = ['trampolim'] 4 | 5 | [project] 6 | name = 'readme-unknown-with-type' 7 | version = '1.0.0' 8 | readme = { file = 'README.unknown', content-type = 'text/some-unknown-type' } 9 | -------------------------------------------------------------------------------- /tests/packages/test-top-level-module/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'trampolim' 3 | requires = ['trampolim'] 4 | 5 | [project] 6 | name = 'test-top-level-module' 7 | version = '0.0.0' 8 | 9 | [tool.trampolim] 10 | top-level-modules = [ 11 | 'test', 12 | ] 13 | -------------------------------------------------------------------------------- /tests/packages/custom-top-modules/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'trampolim' 3 | requires = ['trampolim'] 4 | 5 | [project] 6 | name = 'custom-top-modules' 7 | version = '0.0.0' 8 | 9 | [tool.trampolim] 10 | top-level-modules = [ 11 | 'module1', 12 | 'module2', 13 | 'module3', 14 | ] 15 | -------------------------------------------------------------------------------- /tests/packages/source-include/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'trampolim' 3 | requires = ['trampolim'] 4 | 5 | [project] 6 | name = 'source-include' 7 | version = '0.0.0' 8 | 9 | [tool.trampolim] 10 | source-include = [ 11 | 'some-config.txt', 12 | 'helper-data/a', 13 | 'helper-data/b', 14 | 'helper-data/c', 15 | ] 16 | -------------------------------------------------------------------------------- /tests/test_pep517.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | import trampolim 4 | 5 | 6 | def test_get_requires_for_build_sdist(package_sample_source): 7 | assert trampolim.get_requires_for_build_sdist() == [] 8 | 9 | 10 | def test_get_requires_for_build_wheel(package_sample_source): 11 | assert trampolim.get_requires_for_build_wheel() == ['wheel'] 12 | -------------------------------------------------------------------------------- /tests/packages/task-missing-source/.trampolim.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | import shutil 4 | 5 | import trampolim 6 | 7 | 8 | @trampolim.task 9 | def inject_some_file(session): 10 | shutil.copy2( 11 | session.source_path / 'example_source.py', 12 | 'example_source.py', 13 | ) 14 | 15 | # session.extra_source += [ 16 | # 'example_source.py', 17 | # ] 18 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | :hide-toc: 2 | 3 | ********* 4 | trampolim 5 | ********* 6 | 7 | A modern Python build backend. 8 | 9 | 10 | .. toctree:: 11 | :caption: Usage 12 | :hidden: 13 | 14 | changelog 15 | api 16 | 17 | .. toctree:: 18 | :caption: Project Links 19 | :hidden: 20 | 21 | Source Code 22 | Issue Tracker 23 | -------------------------------------------------------------------------------- /tests/packages/full-tasks/.trampolim.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | import trampolim 4 | 5 | 6 | @trampolim.task 7 | def no_parameters(): 8 | pass 9 | 10 | 11 | @trampolim.task 12 | def receives_session(session): 13 | pass 14 | 15 | 16 | @trampolim.task 17 | def inject_some_file(session): 18 | with open('example_source.py', 'w') as f: 19 | f.write('# Example!') 20 | 21 | session.extra_source += [ 22 | 'example_source.py', 23 | ] 24 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: check 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | schedule: 10 | - cron: "0 8 * * *" 11 | 12 | jobs: 13 | pre-commit: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v1 17 | 18 | - name: Setup python for pre-commit 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: 3.9 22 | 23 | - uses: pre-commit/action@v2.0.0 24 | -------------------------------------------------------------------------------- /trampolim/types.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | import collections 4 | import dataclasses 5 | 6 | import pep621 7 | 8 | import trampolim._metadata 9 | 10 | from trampolim._tasks import Session 11 | 12 | 13 | FrozenMetadata = collections.namedtuple('FrozenMetadata', [ # type: ignore[misc] 14 | field.name 15 | for cls in (pep621.StandardMetadata, trampolim._metadata.TrampolimMetadata) 16 | for field in dataclasses.fields(cls) 17 | ]) 18 | 19 | 20 | __all__ = [ 21 | 'FrozenMetadata', 22 | 'Session', 23 | ] 24 | -------------------------------------------------------------------------------- /tests/test_tasks.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | import re 4 | 5 | import pytest 6 | 7 | import trampolim._build 8 | 9 | 10 | def test_invalid_parameter(package_invalid_parameter_task): 11 | with pytest.raises(trampolim._build.TrampolimError, match=re.escape( 12 | 'Task `invalid_parameters` has unknown parameter `this_is_not_a_valid_parameter`' 13 | )): 14 | trampolim._build.Project() 15 | 16 | 17 | def test_missing_source(package_task_missing_source): 18 | project = trampolim._build.Project() 19 | 20 | with pytest.raises(FileNotFoundError): 21 | project.run_tasks() 22 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 127 3 | max-complexity = 10 4 | extend-ignore = E203 5 | 6 | [mypy] 7 | warn_unused_configs = True 8 | strict = True 9 | python_version = 3.7 10 | 11 | [mypy-wheel.*] 12 | ignore_missing_imports = True 13 | 14 | [mypy-pep621.*] 15 | ignore_missing_imports = True 16 | 17 | [isort] 18 | line_length = 127 19 | lines_between_types = 1 20 | lines_after_imports = 2 21 | known_first_party = trampolim 22 | 23 | [coverage:paths] 24 | source = 25 | src 26 | */site-packages 27 | *\site-packages 28 | 29 | [coverage:report] 30 | exclude_lines = 31 | \#\s*pragma: no cover 32 | ^\s*raise NotImplementedError\b 33 | 34 | [coverage:html] 35 | show_contexts = true 36 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autofix_prs: false 3 | autoupdate_commit_msg: 'pre-commit: bump repositories' 4 | 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v4.0.1 8 | hooks: 9 | - id: check-ast 10 | - id: check-builtin-literals 11 | - id: check-docstring-first 12 | exclude: tests 13 | - id: check-merge-conflict 14 | - id: check-yaml 15 | - id: check-toml 16 | - id: debug-statements 17 | - id: end-of-file-fixer 18 | - id: trailing-whitespace 19 | - id: double-quote-string-fixer 20 | - repo: https://github.com/PyCQA/isort 21 | rev: 5.10.1 22 | hooks: 23 | - id: isort 24 | - repo: https://github.com/PyCQA/flake8 25 | rev: "4.0.1" 26 | hooks: 27 | - id: flake8 28 | language_version: python3.9 29 | -------------------------------------------------------------------------------- /trampolim/_metadata.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | from __future__ import annotations 4 | 5 | import dataclasses 6 | 7 | from typing import Any, List, Mapping, Optional 8 | 9 | import pep621 10 | 11 | 12 | @dataclasses.dataclass 13 | class TrampolimMetadata(): 14 | top_level_modules: List[str] 15 | module_location: Optional[str] 16 | source_include: List[str] 17 | 18 | @classmethod 19 | def from_pyproject(cls, data: Mapping[str, Any]) -> TrampolimMetadata: 20 | fetcher = pep621.DataFetcher(data) 21 | return cls( 22 | fetcher.get_list('tool.trampolim.top-level-modules'), 23 | fetcher.get_str('tool.trampolim.module-location'), 24 | fetcher.get_list('tool.trampolim.source-include'), 25 | ) 26 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ================= 3 | 4 | 5 | PEP 517 hooks 6 | ------------- 7 | 8 | .. autoclass:: trampolim.get_requires_for_build_sdist 9 | :members: 10 | :undoc-members: 11 | :noindex: 12 | 13 | .. autoclass:: trampolim.get_requires_for_build_wheel 14 | :members: 15 | :undoc-members: 16 | :noindex: 17 | 18 | .. autoclass:: trampolim.build_sdist 19 | :members: 20 | :undoc-members: 21 | :noindex: 22 | 23 | .. autoclass:: trampolim.build_wheel 24 | :members: 25 | :undoc-members: 26 | :noindex: 27 | 28 | 29 | Errors and Warnings 30 | ------------------- 31 | 32 | .. autoclass:: trampolim.TrampolimError 33 | :members: 34 | :undoc-members: 35 | :noindex: 36 | 37 | .. autoclass:: trampolim.ConfigurationError 38 | :members: 39 | :undoc-members: 40 | :noindex: 41 | 42 | .. autoclass:: trampolim.TrampolimWarning 43 | :members: 44 | :undoc-members: 45 | :noindex: 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2019 Filipe Laíns 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice (including the next 11 | paragraph) shall be included in all copies or substantial portions of the 12 | Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /tests/test_metadata.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | import re 4 | import textwrap 5 | 6 | import pep621 7 | import pytest 8 | import tomli 9 | 10 | import trampolim 11 | import trampolim._metadata 12 | 13 | 14 | @pytest.mark.parametrize( 15 | ('data', 'error'), 16 | [ 17 | ( 18 | textwrap.dedent(''' 19 | [tool.trampolim] 20 | top-level-modules = true 21 | '''), 22 | ('Field `tool.trampolim.top-level-modules` has an invalid type, expecting a list of strings (got `True`)'), 23 | ), 24 | ( 25 | textwrap.dedent(''' 26 | [tool.trampolim] 27 | module-location = 0 28 | '''), 29 | ('Field `tool.trampolim.module-location` has an invalid type, expecting a string (got `0`)'), 30 | ), 31 | ( 32 | textwrap.dedent(''' 33 | [tool.trampolim] 34 | source-include = 0 35 | '''), 36 | ('Field `tool.trampolim.source-include` has an invalid type, expecting a list of strings (got `0`)'), 37 | ), 38 | ], 39 | ) 40 | def test_trampolim_metadata(package_full_metadata, data, error): 41 | with pytest.raises(pep621.ConfigurationError, match=re.escape(error)): 42 | trampolim._metadata.TrampolimMetadata.from_pyproject(tomli.loads(data)) 43 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | +++++++++ 2 | Changelog 3 | +++++++++ 4 | 5 | 6 | 0.1.0 (13-10-2021) 7 | ================== 8 | 9 | - Fix dynamic version when building from sdist (`PR #20`_, Fixes ` #4`_) 10 | - Add ``module-location`` option, allowing ``src``-layout (`PR #15`_, Fixes ` #3`_) 11 | 12 | .. _PR #15: https://github.com/FFY00/trampolim/pull/15 13 | .. _PR #15: https://github.com/FFY00/trampolim/pull/20 14 | .. _#3: https://github.com/FFY00/trampolim/issues/3 15 | .. _#3: https://github.com/FFY00/trampolim/issues/4 16 | 17 | 18 | 0.0.4 (30-09-2021) 19 | ================== 20 | 21 | - Use TOML 1.0 compliant parser (`PR #11`_) 22 | - Fix incorrect files being included in wheels 23 | 24 | .. _PR #11: https://github.com/FFY00/trampolim/pull/11 25 | 26 | 27 | 28 | 0.0.3 (06-06-2021) 29 | ================== 30 | 31 | - Add task system (`PR #1`_, `PR #2`_) 32 | - Add ``source-include`` setting 33 | 34 | .. _PR #1: https://github.com/FFY00/trampolim/pull/1 35 | .. _PR #2: https://github.com/FFY00/trampolim/pull/2 36 | 37 | 38 | 39 | 0.0.2 (18-05-2021) 40 | ================== 41 | 42 | - Add cli with a build command 43 | - Add ``top-level-modules`` setting 44 | 45 | 46 | 0.0.1 (06-05-2021) 47 | ================== 48 | 49 | Initial release 50 | 51 | - Implemented PEP 517 hooks 52 | - Implemented PEP 621 pyproject.toml metadata parsing 53 | - Implemented automated version from git repos and archives 54 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'trampolim' 3 | backend-path = ['.'] 4 | requires = [ 5 | 'tomli>=1.0.0', 6 | 'packaging', 7 | 'pep621>=0.4.0', 8 | 'backports.cached-property ; python_version < "3.8"', 9 | ] 10 | 11 | [project] 12 | name = 'trampolim' 13 | version = '0.1.0' 14 | description = 'A modern Python build backend' 15 | readme = 'README.md' 16 | requires-python = '>=3.7' 17 | license = { file = 'LICENSE' } 18 | keywords = ['build', 'pep517', 'package', 'packaging'] 19 | authors = [ 20 | { name = 'Filipe Laíns', email = 'lains@riseup.net' }, 21 | ] 22 | classifiers = [ 23 | 'Development Status :: 4 - Beta', 24 | 'Programming Language :: Python' 25 | ] 26 | 27 | dependencies = [ 28 | 'tomli>=1.0.0', 29 | 'packaging', 30 | 'pep621>=0.4.0', 31 | 'backports.cached-property ; python_version < "3.8"', 32 | ] 33 | 34 | [project.optional-dependencies] 35 | test = [ 36 | 'wheel', 37 | 'pytest>=3.9.1', 38 | 'pytest-cov', 39 | 'pytest-mock', 40 | ] 41 | docs = [ 42 | 'furo>=2021.04.11b34', 43 | 'sphinx~=3.0', 44 | 'sphinx-autodoc-typehints>=1.10', 45 | ] 46 | 47 | [project.scripts] 48 | trampolim = 'trampolim.__main__:entrypoint' 49 | 50 | [project.urls] 51 | homepage = 'https://github.com/FFY00/trampolim' 52 | repository = 'https://github.com/FFY00/trampolim' 53 | documentation = 'https://trampolim.readthedocs.io' 54 | changelog = 'https://trampolim.readthedocs.io/en/latest/changelog.html' 55 | -------------------------------------------------------------------------------- /tests/packages/full-metadata/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = 'full-metadata' 3 | version = '3.2.1' 4 | description = 'A package with all the metadata :)' 5 | readme = 'README.md' 6 | license = { text = 'some license text' } 7 | keywords = ['trampolim', 'is', 'interesting'] 8 | authors = [ 9 | { email = 'example@example.com' }, 10 | { name = 'Example!' }, 11 | ] 12 | maintainers = [ 13 | { name = 'Other Example', email = 'other@example.com' }, 14 | ] 15 | classifiers = [ 16 | 'Development Status :: 4 - Beta', 17 | 'Programming Language :: Python', 18 | ] 19 | 20 | requires-python = '>=3.8' 21 | dependencies = [ 22 | 'dependency1', 23 | 'dependency2>1.0.0', 24 | 'dependency3[extra]', 25 | 'dependency4; os_name != "nt"', 26 | 'dependency5[other-extra]>1.0; os_name == "nt"', 27 | ] 28 | 29 | [project.optional-dependencies] 30 | test = [ 31 | 'test_dependency', 32 | 'test_dependency[test_extra]', 33 | 'test_dependency[test_extra2] > 3.0; os_name == "nt"', 34 | ] 35 | 36 | [project.urls] 37 | homepage = 'example.com' 38 | documentation = 'readthedocs.org' 39 | repository = 'github.com/some/repo' 40 | changelog = 'github.com/some/repo/blob/master/CHANGELOG.rst' 41 | 42 | [project.scripts] 43 | full-metadata = 'full_metadata:main_cli' 44 | 45 | [project.gui-scripts] 46 | full-metadata-gui = 'full_metadata:main_gui' 47 | 48 | [project.entry-points.custom] 49 | full-metadata = 'full_metadata:main_custom' 50 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths-ignore: 7 | - 'docs/**' 8 | - '*.md' 9 | - '*.rst' 10 | pull_request: 11 | branches: 12 | - main 13 | paths-ignore: 14 | - 'docs/**' 15 | - '*.md' 16 | - '*.rst' 17 | 18 | jobs: 19 | pytest: 20 | runs-on: ${{ matrix.os }}-latest 21 | env: 22 | PYTEST_ADDOPTS: '--showlocals -vv --durations=10' 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | os: 27 | - ubuntu 28 | - macos 29 | - windows 30 | py: 31 | - "3.10" 32 | - "3.9" 33 | - "3.8" 34 | - "3.7" 35 | # - pypy3 36 | 37 | steps: 38 | - uses: actions/checkout@v2 39 | 40 | - uses: actions/setup-python@v2 41 | with: 42 | python-version: ${{ matrix.py }} 43 | 44 | - name: Run tests 45 | run: pipx run nox --force-color -s test-${{ matrix.py }} 46 | 47 | - uses: codecov/codecov-action@v1 48 | if: ${{ always() }} 49 | env: 50 | PYTHON: ${{ matrix.py }} 51 | with: 52 | flags: tests 53 | env_vars: PYTHON 54 | name: ${{ matrix.py }} - ${{ matrix.os }} 55 | 56 | type: 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: actions/checkout@v2 60 | 61 | - name: Run check for type 62 | run: pipx run nox -s type 63 | -------------------------------------------------------------------------------- /trampolim/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | from typing import Any, Dict, List, Optional 4 | 5 | import trampolim._build 6 | import trampolim._sdist 7 | 8 | from trampolim._build import ConfigurationError, TrampolimError, TrampolimWarning # noqa: F401 9 | from trampolim._tasks import Session, task # noqa: F401 10 | 11 | 12 | __version__ = '0.1.0' 13 | 14 | # get_requires 15 | 16 | 17 | def get_requires_for_build_sdist( 18 | config_settings: Optional[Dict[str, Any]] = None, 19 | ) -> List[str]: 20 | return [] 21 | 22 | 23 | def get_requires_for_build_wheel( 24 | config_settings: Optional[Dict[str, Any]] = None, 25 | ) -> List[str]: 26 | return ['wheel'] 27 | 28 | 29 | # prepare_metadata 30 | 31 | # def prepare_metadata_for_build_wheel( 32 | # metadata_directory: trampolim._build.Path, 33 | # config_settings: Optional[Dict[str, Any]] = None, 34 | # ) -> str: 35 | # raise NotImplementedError 36 | 37 | 38 | # build 39 | 40 | 41 | def build_sdist( 42 | sdist_directory: trampolim._build.Path, 43 | config_settings: Optional[Dict[str, Any]] = None, 44 | ) -> str: 45 | project = trampolim._build.Project() 46 | builder = trampolim._sdist.SdistBuilder(project) 47 | 48 | builder.build(sdist_directory) 49 | return builder.file 50 | 51 | 52 | def build_wheel( 53 | wheel_directory: trampolim._build.Path, 54 | config_settings: Optional[Dict[str, Any]] = None, 55 | metadata_directory: Optional[trampolim._build.Path] = None, 56 | ) -> str: 57 | import trampolim._wheel 58 | 59 | project = trampolim._build.Project() 60 | builder = trampolim._wheel.WheelBuilder(project) 61 | 62 | builder.build(wheel_directory) 63 | return builder.file 64 | -------------------------------------------------------------------------------- /trampolim/_tasks.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | from __future__ import annotations 4 | 5 | import inspect 6 | import pathlib 7 | 8 | from typing import Callable, List 9 | 10 | import trampolim._build 11 | 12 | 13 | class Session(): 14 | '''Task running session. 15 | 16 | Provices access to information about the project and allows changing some 17 | aspects of it. 18 | ''' 19 | def __init__(self, project: trampolim._build.Project) -> None: 20 | self._project = project 21 | 22 | self.extra_source: List[str] = [] 23 | 24 | @property 25 | def source_path(self) -> pathlib.Path: 26 | return pathlib.Path(self._project._dist_srcpath) 27 | 28 | 29 | class Task(): 30 | _SUPPORTED_PARAMS = ( 31 | 'session', 32 | ) 33 | 34 | def __init__(self, name: str, call: Callable[..., None]) -> None: 35 | self._name = name 36 | self._set_callable(call) 37 | 38 | def _set_callable(self, call: Callable[..., None]) -> None: 39 | '''Set and validate the task callable.''' 40 | self._callable = call 41 | self._params = inspect.signature(self._callable).parameters 42 | 43 | for param in self._params: 44 | if param not in self._SUPPORTED_PARAMS: 45 | raise trampolim._build.TrampolimError( 46 | f'Task `{self.name}` has unknown parameter `{param}`' 47 | ) 48 | 49 | def run(self, session: Session) -> None: 50 | '''Run the task in a given session.''' 51 | kwargs = { 52 | 'session': session, 53 | } 54 | self._callable(**{ 55 | arg: val for arg, val in kwargs.items() 56 | if arg in self._params 57 | }) 58 | 59 | @property 60 | def name(self) -> str: 61 | '''Task name.''' 62 | return self._name 63 | 64 | 65 | def task(func: Callable[..., None]) -> Task: 66 | '''Decorator that marks a function as a task.''' 67 | return Task(func.__name__, func) 68 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | import contextlib 4 | import os 5 | import pathlib 6 | 7 | import pytest 8 | 9 | import trampolim 10 | 11 | 12 | package_dir = pathlib.Path(__file__).parent / 'packages' 13 | 14 | 15 | @contextlib.contextmanager 16 | def cd_package(package): 17 | cur_dir = os.getcwd() 18 | package_path = package_dir / package 19 | os.chdir(package_path) 20 | try: 21 | yield package_path 22 | finally: 23 | os.chdir(cur_dir) 24 | 25 | 26 | @pytest.fixture 27 | def source_date_epoch(): 28 | old_val = os.environ.get('SOURCE_DATE_EPOCH') 29 | os.environ['SOURCE_DATE_EPOCH'] = '0' 30 | try: 31 | yield 32 | finally: # pragma: no cover 33 | if old_val is not None: 34 | os.environ['SOURCE_DATE_EPOCH'] = old_val 35 | else: 36 | del os.environ['SOURCE_DATE_EPOCH'] 37 | 38 | 39 | def generate_package_fixture(package): 40 | @pytest.fixture 41 | def fixture(): 42 | with cd_package(package) as new_path: 43 | yield new_path 44 | return fixture 45 | 46 | 47 | def generate_sdist_fixture(package): 48 | @pytest.fixture(scope='session') 49 | def fixture(tmp_path_factory): 50 | tmp_path_sdist = tmp_path_factory.mktemp('sdist_package') 51 | with cd_package(package): 52 | return tmp_path_sdist / trampolim.build_sdist(tmp_path_sdist) 53 | return fixture 54 | 55 | 56 | def generate_wheel_fixture(package): 57 | @pytest.fixture(scope='session') 58 | def fixture(tmp_path_factory): 59 | tmp_path_wheel = tmp_path_factory.mktemp('wheel_package') 60 | with cd_package(package): 61 | return tmp_path_wheel / trampolim.build_wheel(tmp_path_wheel) 62 | return fixture 63 | 64 | 65 | # inject {package,sdist,wheel}_* fixtures (https://github.com/pytest-dev/pytest/issues/2424) 66 | for package in os.listdir(package_dir): 67 | normalized = package.replace('-', '_') 68 | globals()[f'package_{normalized}'] = generate_package_fixture(package) 69 | globals()[f'sdist_{normalized}'] = generate_sdist_fixture(package) 70 | globals()[f'wheel_{normalized}'] = generate_wheel_fixture(package) 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # trampolim 2 | 3 | [![test](https://github.com/FFY00/trampolim/actions/workflows/test.yml/badge.svg)](https://github.com/FFY00/trampolim/actions/workflows/test.yml) 4 | [![codecov](https://codecov.io/gh/FFY00/trampolim/branch/main/graph/badge.svg?token=QAfQGa1bld)](https://codecov.io/gh/FFY00/trampolim) 5 | [![check](https://github.com/FFY00/trampolim/actions/workflows/check.yml/badge.svg)](https://github.com/FFY00/trampolim/actions/workflows/check.yml) 6 | [![Documentation Status](https://readthedocs.org/projects/trampolim/badge/?version=latest)](https://trampolim.readthedocs.io/en/latest/?badge=latest) 7 | [![PyPI version](https://badge.fury.io/py/trampolim.svg)](https://pypi.org/project/trampolim/) 8 | 9 | A modern Python build backend. 10 | 11 | ### Features 12 | 13 | - Task system, allowing to run arbitrary Python code during the build process 14 | - Automatic version detection from git repos and git archives 15 | - Easy to use CLI -- build, publish, check for errors and recommended practices (**Planned**) 16 | 17 | ### Usage 18 | 19 | `trampolim` implements [PEP 621](https://www.python.org/dev/peps/pep-0621). 20 | Your `pyproject.toml` should look something like this: 21 | 22 | ```toml 23 | [build-system] 24 | build-backend = 'trampolim' 25 | requires = ['trampolim~=0.1.0'] 26 | 27 | [project] 28 | name = 'sample_project' 29 | version = '1.0.0' 30 | description = 'A sample project' 31 | readme = 'README.md' 32 | requires-python = '>=3.7' 33 | license = { file = 'LICENSE' } 34 | authors = [ 35 | { name = 'Filipe Laíns', email = 'lains@riseup.net' }, 36 | ] 37 | classifiers = [ 38 | 'Development Status :: 4 - Beta', 39 | 'Programming Language :: Python', 40 | ] 41 | 42 | dependencies = [ 43 | 'dependency', 44 | 'some-backport ; python_version < "3.8"', 45 | ] 46 | 47 | [project.optional-dependencies] 48 | test = [ 49 | 'pytest', 50 | 'pytest-cov', 51 | ] 52 | 53 | [project.scripts] 54 | sample_entrypoint = 'sample_project:entrypoint_function' 55 | 56 | [project.urls] 57 | homepage = 'https://my-sample-project-website.example.com' 58 | documentation = 'https://github.com/some-user/sample-project' 59 | repository = 'https://github.com/some-user/sample-project' 60 | changelog = 'https://github.com/some-user/sample-project/blob/master/CHANGELOG.rst' 61 | ``` 62 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | 13 | import trampolim 14 | 15 | 16 | # -- Project information ----------------------------------------------------- 17 | 18 | project = 'trampolim' 19 | copyright = '2021, Filipe Laíns' 20 | author = 'Filipe Laíns' 21 | 22 | # The short X.Y version 23 | version = trampolim.__version__ 24 | # The full version, including alpha/beta/rc tags 25 | release = trampolim.__version__ 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.intersphinx', 36 | 'sphinx_autodoc_typehints', 37 | ] 38 | 39 | intersphinx_mapping = { 40 | 'python': ('https://docs.python.org/3/', None), 41 | } 42 | 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ['_templates'] 45 | 46 | # List of patterns, relative to source directory, that match files and 47 | # directories to ignore when looking for source files. 48 | # This pattern also affects html_static_path and html_extra_path. 49 | exclude_patterns = [] 50 | 51 | default_role = 'any' 52 | 53 | # -- Options for HTML output ------------------------------------------------- 54 | 55 | # The theme to use for HTML and HTML Help pages. See the documentation for 56 | # a list of builtin themes. 57 | # 58 | html_theme = 'furo' 59 | html_title = f'trampolim {version}' 60 | 61 | # Add any paths that contain custom static files (such as style sheets) here, 62 | # relative to this directory. They are copied after the builtin static files, 63 | # so a file named 'default.css' will overwrite the builtin 'default.css'. 64 | # html_static_path = ['_static'] 65 | 66 | autoclass_content = 'both' 67 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | 5 | import nox 6 | 7 | 8 | nox.options.sessions = ['lint', 'type', 'test'] 9 | 10 | 11 | @nox.session(reuse_venv=True) 12 | def lint(session): 13 | """ 14 | Run the linter. 15 | """ 16 | session.install('pre-commit') 17 | session.run('pre-commit', 'run', '--all-files', *session.posargs) 18 | 19 | 20 | @nox.session(python=['3.7', '3.8', '3.9', '3.10'], reuse_venv=True) 21 | def test(session): 22 | """ 23 | Run the unit and regular tests. 24 | """ 25 | 26 | htmlcov_output = os.path.join(session.virtualenv.location, 'htmlcov') 27 | xmlcov_output = os.path.join(session.virtualenv.location, f'coverage-{session.python}.xml') 28 | 29 | session.install( 30 | 'tomli', 31 | 'packaging', 32 | 'rich', 33 | 'wheel', 34 | 'pytest', 35 | 'pytest-cov', 36 | 'pytest-mock', 37 | 'backports.cached-property', 38 | 'pep621', 39 | ) 40 | 41 | session.run( 42 | 'pytest', '--cov', '--cov-config', 'setup.cfg', 43 | f'--cov-report=html:{htmlcov_output}', 44 | f'--cov-report=xml:{xmlcov_output}', 45 | '--showlocals', 46 | '-vv', 47 | '--durations=1', 48 | 'tests/', *session.posargs 49 | ) 50 | 51 | 52 | @nox.session(reuse_venv=True) 53 | def type(session): 54 | session.install('mypy', 'packaging', 'tomli', 'rich', 'backports.cached_property') 55 | session.run('mypy', '--python-version=3.7', '--package=trampolim', *session.posargs) 56 | session.run('mypy', '--python-version=3.9', '--package=trampolim', *session.posargs) 57 | 58 | 59 | @nox.session(reuse_venv=True) 60 | def docs(session): 61 | """ 62 | Build the docs. Pass "serve" to serve. 63 | """ 64 | 65 | session.install('.[docs]') 66 | session.chdir('docs') 67 | session.run('sphinx-build', '-M', 'html', '.', '_build') 68 | 69 | if session.posargs: 70 | if 'serve' in session.posargs: 71 | print('Launching docs at http://localhost:8000/ - use Ctrl-C to quit') 72 | session.run('python', '-m', 'http.server', '8000', '-d', '_build/html') 73 | else: 74 | print('Unsupported argument to docs') 75 | 76 | 77 | @nox.session 78 | def build(session): 79 | """ 80 | Build an SDist and wheel. 81 | """ 82 | 83 | session.install('build') 84 | session.run('python', '-m', 'build', *session.posargs) 85 | -------------------------------------------------------------------------------- /trampolim/_wheel.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | import os.path 4 | import textwrap 5 | 6 | import wheel.wheelfile 7 | 8 | import trampolim._build 9 | 10 | 11 | class WheelBuilder(): 12 | def __init__(self, project: trampolim._build.Project) -> None: 13 | self._project = project 14 | 15 | @property 16 | def name(self) -> str: 17 | return '{distribution}-{version}-{python_tag}-{abi_tag}-{platform_tag}'.format( 18 | distribution=self._project.name.replace('-', '_'), 19 | version=self._project.version, 20 | python_tag=self._project.python_tag, 21 | abi_tag=self._project.abi_tag, 22 | platform_tag=self._project.platform_tag, 23 | ) 24 | 25 | @property 26 | def file(self) -> str: 27 | return f'{self.name}.whl' 28 | 29 | def build(self, path: trampolim._build.Path) -> None: 30 | self._project.run_tasks() 31 | with wheel.wheelfile.WheelFile(os.path.join(path, self.file), 'w') as whl: 32 | with self._project.cd_binary_source(): 33 | # add source 34 | for destination, source_path in self._project.binary_source.items(): 35 | whl.write(source_path, arcname=destination.as_posix()) 36 | 37 | # add metadata 38 | whl.writestr(f'{whl.dist_info_path}/METADATA', bytes(self._project._meta.as_rfc822())) 39 | whl.writestr(f'{whl.dist_info_path}/WHEEL', self.wheel) 40 | if self.entrypoints_txt: 41 | whl.writestr(f'{whl.dist_info_path}/entrypoints.txt', self.entrypoints_txt) 42 | 43 | @property 44 | def wheel(self) -> bytes: 45 | '''dist-info WHEEL.''' 46 | return textwrap.dedent(''' 47 | Wheel-Version: 1.0 48 | Generator: trampolim {version} 49 | Root-Is-Purelib: {is_purelib} 50 | Tag: {tags} 51 | ''').strip().format( 52 | version=trampolim.__version__, 53 | is_purelib='true' if self._project.abi_tag == 'none' else 'false', 54 | tags=f'{self._project.python_tag}-{self._project.abi_tag}-{self._project.platform_tag}', 55 | ).encode() 56 | 57 | @property 58 | def entrypoints_txt(self) -> bytes: 59 | '''dist-info entry-points.txt.''' 60 | data = self._project._meta.entrypoints.copy() 61 | data.update({ 62 | 'console_scripts': self._project._meta.scripts, 63 | 'gui_scripts': self._project._meta.gui_scripts, 64 | }) 65 | 66 | text = '' 67 | for entrypoint in data: 68 | if data[entrypoint]: 69 | text += f'[{entrypoint}]\n' 70 | for name, target in data[entrypoint].items(): 71 | text += f'{name} = {target}\n' 72 | text += '\n' 73 | 74 | return text.encode() 75 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | import os 4 | import pathlib 5 | import shutil 6 | import sys 7 | import tarfile 8 | 9 | import pytest 10 | import wheel.wheelfile 11 | 12 | import trampolim.__main__ 13 | 14 | from .conftest import cd_package, package_dir 15 | 16 | 17 | @pytest.fixture() 18 | def package(tmpdir): 19 | if sys.version_info >= (3, 8): 20 | shutil.copytree(package_dir / 'file-module', tmpdir, dirs_exist_ok=True) 21 | else: 22 | import distutils.dir_util 23 | 24 | distutils.dir_util.copy_tree(str(package_dir / 'file-module'), str(tmpdir)) 25 | with cd_package(tmpdir): 26 | yield 27 | 28 | 29 | def test_entrypoint(package): 30 | prev = sys.argv 31 | sys.argv = ['something', 'build'] 32 | try: 33 | trampolim.__main__.entrypoint() 34 | finally: 35 | sys.argv = prev 36 | 37 | dist = pathlib.Path('dist') 38 | t = tarfile.open(dist / 'file-module-0.0.0.tar.gz', 'r') 39 | 40 | with open('pyproject.toml', 'rb') as f: 41 | assert f.read() == t.extractfile('file-module-0.0.0/pyproject.toml').read() 42 | 43 | with wheel.wheelfile.WheelFile(dist / 'file_module-0.0.0-py3-none-any.whl', 'r') as w: 44 | assert 'file_module-0.0.0.dist-info/WHEEL' in w.namelist() 45 | 46 | 47 | def test_build_no_args(package): 48 | trampolim.__main__.main(['build'], 'something') 49 | 50 | dist = pathlib.Path('dist') 51 | t = tarfile.open(dist / 'file-module-0.0.0.tar.gz', 'r') 52 | 53 | with open('pyproject.toml', 'rb') as f: 54 | assert f.read() == t.extractfile('file-module-0.0.0/pyproject.toml').read() 55 | 56 | with wheel.wheelfile.WheelFile(dist / 'file_module-0.0.0-py3-none-any.whl', 'r') as w: 57 | assert 'file_module-0.0.0.dist-info/WHEEL' in w.namelist() 58 | 59 | 60 | def test_build_custom_outdir(package): 61 | dist = pathlib.Path('some') / 'place' 62 | 63 | trampolim.__main__.main(['build', str(dist)], 'something') 64 | 65 | t = tarfile.open(dist / 'file-module-0.0.0.tar.gz', 'r') 66 | 67 | with open('pyproject.toml', 'rb') as f: 68 | assert f.read() == t.extractfile('file-module-0.0.0/pyproject.toml').read() 69 | 70 | with wheel.wheelfile.WheelFile(dist / 'file_module-0.0.0-py3-none-any.whl', 'r') as w: 71 | assert 'file_module-0.0.0.dist-info/WHEEL' in w.namelist() 72 | 73 | 74 | def test_build_outdir_notdir(package, capsys): 75 | pathlib.Path('dist').touch() 76 | 77 | with pytest.raises(SystemExit): 78 | trampolim.__main__.main(['build'], 'something') 79 | 80 | assert capsys.readouterr().err == 'ERROR Output path `dist` exists and is not a directory!\n' 81 | 82 | 83 | def test_build_only_sdist(package): 84 | trampolim.__main__.main(['build', '-s'], 'something') 85 | 86 | assert os.listdir('dist') == ['file-module-0.0.0.tar.gz'] 87 | 88 | 89 | def test_build_only_wheel(package): 90 | trampolim.__main__.main(['build', '-w'], 'something') 91 | 92 | assert os.listdir('dist') == ['file_module-0.0.0-py3-none-any.whl'] 93 | -------------------------------------------------------------------------------- /trampolim/_sdist.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | import contextlib 4 | import gzip 5 | import io 6 | import os.path 7 | import pathlib 8 | import tarfile 9 | import typing 10 | 11 | from typing import IO 12 | 13 | import trampolim._build 14 | 15 | 16 | class SdistBuilder(): 17 | '''Simple sdist builder. 18 | 19 | Will only include by default the files relevant for source code distribution 20 | and the required files to be able to build binary distributions. 21 | ''' 22 | def __init__(self, project: trampolim._build.Project) -> None: 23 | self._project = project 24 | 25 | @property 26 | def name(self) -> str: 27 | return f'{self._project.name}-{self._project.version}' 28 | 29 | @property 30 | def file(self) -> str: 31 | return f'{self.name}.tar.gz' 32 | 33 | def build(self, path: trampolim._build.Path) -> None: 34 | # reproducibility 35 | source_date_epoch = os.environ.get('SOURCE_DATE_EPOCH') 36 | mtime = int(source_date_epoch) if source_date_epoch else None 37 | 38 | # open files 39 | file = typing.cast( 40 | IO[bytes], 41 | gzip.GzipFile( 42 | os.path.join(path, self.file), 43 | mode='wb', 44 | mtime=mtime, 45 | ), 46 | ) 47 | tar = tarfile.TarFile( 48 | str(path), 49 | mode='w', 50 | fileobj=file, 51 | format=tarfile.PAX_FORMAT, # changed in 3.8 to GNU 52 | ) 53 | 54 | with contextlib.closing(file), tar: 55 | with self._project.cd_dist_source(): 56 | # add source 57 | for path in self._project.distribution_source: 58 | arcname = pathlib.Path(self.name) / path 59 | tar.add(os.fspath(path), arcname.as_posix()) 60 | 61 | # add version file 62 | info = tarfile.TarInfo(f'{self.name}/.trampolim/version') 63 | version_raw = str(self._project.version).encode() 64 | info.size = len(version_raw) 65 | with io.BytesIO(version_raw) as data: 66 | tar.addfile(info, data) 67 | 68 | # add license 69 | license_ = self._project._meta.license 70 | if license_: 71 | if license_.file: 72 | tar.add(license_.file, f'{self.name}/{license_.file}') 73 | elif license_.text: 74 | license_raw = license_.text.encode() 75 | info = tarfile.TarInfo(f'{self.name}/LICENSE') 76 | info.size = len(license_raw) 77 | with io.BytesIO(license_raw) as data: 78 | tar.addfile(info, data) 79 | 80 | # add readme 81 | readme = self._project._meta.readme 82 | if readme: 83 | tar.add(readme.file, f'{self.name}/{readme.file}') 84 | 85 | # PKG-INFO 86 | pkginfo = bytes(self._project._meta.as_rfc822()) 87 | info = tarfile.TarInfo(f'{self.name}/PKG-INFO') 88 | info.size = len(pkginfo) 89 | with io.BytesIO(pkginfo) as data: 90 | tar.addfile(info, data) 91 | -------------------------------------------------------------------------------- /tests/test_wheel.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | import os.path 4 | import tarfile 5 | import textwrap 6 | 7 | import wheel.wheelfile 8 | 9 | import trampolim 10 | import trampolim._build 11 | import trampolim._wheel 12 | 13 | 14 | def test_name(package_full_metadata): 15 | assert ( 16 | trampolim._wheel.WheelBuilder(trampolim._build.Project()).name 17 | == 'full_metadata-3.2.1-py3-none-any' 18 | ) 19 | 20 | 21 | def test_wheel_info(wheel_full_metadata): 22 | with wheel.wheelfile.WheelFile(wheel_full_metadata, 'r') as w: 23 | data = w.read('full_metadata-3.2.1.dist-info/WHEEL') 24 | assert data == textwrap.dedent(f''' 25 | Wheel-Version: 1.0 26 | Generator: trampolim {trampolim.__version__} 27 | Root-Is-Purelib: true 28 | Tag: py3-none-any 29 | ''').strip().encode() 30 | 31 | 32 | def test_entrypoints(wheel_full_metadata): 33 | with wheel.wheelfile.WheelFile(wheel_full_metadata, 'r') as w: 34 | data = w.read('full_metadata-3.2.1.dist-info/entrypoints.txt') 35 | assert data == textwrap.dedent(''' 36 | [custom] 37 | full-metadata = full_metadata:main_custom 38 | 39 | [console_scripts] 40 | full-metadata = full_metadata:main_cli 41 | 42 | [gui_scripts] 43 | full-metadata-gui = full_metadata:main_gui 44 | 45 | ''').lstrip().encode() 46 | 47 | 48 | def test_source(package_sample_source, wheel_sample_source): 49 | expected_source = [ 50 | 'sample_source/e/eb.py', 51 | 'sample_source/e/ea.py', 52 | 'sample_source/e/ec/ecc.py', 53 | 'sample_source/e/ec/eca.py', 54 | 'sample_source/e/ec/ecb.py', 55 | 'sample_source/a.py', 56 | 'sample_source/c.py', 57 | 'sample_source/d/db.py', 58 | 'sample_source/d/da.py', 59 | 'sample_source/b.py', 60 | 'sample_source/f/fc/fcb.py', 61 | 'sample_source/f/fc/fca.py', 62 | 'sample_source/f/fc/fcc.py', 63 | 'sample_source/f/fc/fcc/fcca.py', 64 | 'sample_source/f/fc/fcd/fcda.py', 65 | 'sample_source/f/fc/fcd/fcdb.py', 66 | 'sample_source/f/fb.py', 67 | 'sample_source/f/fa.py', 68 | ] 69 | with wheel.wheelfile.WheelFile(wheel_sample_source, 'r') as w: 70 | for file in expected_source: 71 | with open(os.path.join(*file.split('/')), 'rb') as f: 72 | assert f.read() == w.read(file) 73 | 74 | 75 | def test_src_layout(package_src_layout, wheel_src_layout): 76 | with wheel.wheelfile.WheelFile(wheel_src_layout) as w: 77 | assert set(w.namelist()) == { 78 | 'src_layout-0.0.0.dist-info/METADATA', 79 | 'src_layout-0.0.0.dist-info/RECORD', 80 | 'src_layout-0.0.0.dist-info/WHEEL', 81 | 'src_layout/__init__.py', 82 | 'src_layout/a.py', 83 | 'src_layout/b.py', 84 | 'src_layout/c.py', 85 | 'src_layout/d/__init__.py', 86 | } 87 | 88 | 89 | def test_overwrite_version(monkeypatch, package_no_version, tmp_path): 90 | monkeypatch.setenv('TRAMPOLIM_VCS_VERSION', '1.0.0+custom') 91 | 92 | with wheel.wheelfile.WheelFile(tmp_path / trampolim.build_wheel(tmp_path), 'r') as w: 93 | metadata = w.read('no_version-1.0.0+custom.dist-info/METADATA').decode() 94 | assert metadata == textwrap.dedent(''' 95 | Metadata-Version: 2.1 96 | Name: no-version 97 | Version: 1.0.0+custom 98 | ''').lstrip() 99 | 100 | 101 | def test_build_via_sdist(monkeypatch, package_no_version, tmp_path): 102 | monkeypatch.setenv('TRAMPOLIM_VCS_VERSION', '1.0.0+custom') 103 | 104 | sdist_file = tmp_path / trampolim.build_sdist(tmp_path) 105 | with tarfile.open(sdist_file) as t: 106 | t.extractall(tmp_path) 107 | 108 | monkeypatch.delenv('TRAMPOLIM_VCS_VERSION') 109 | 110 | monkeypatch.chdir(tmp_path / sdist_file.name[:-len('.tar.gz')]) 111 | wheel_file = tmp_path / trampolim.build_wheel(tmp_path) 112 | with wheel.wheelfile.WheelFile(wheel_file) as w: 113 | metadata = w.read('no_version-1.0.0+custom.dist-info/METADATA').decode() 114 | 115 | assert metadata == textwrap.dedent(''' 116 | Metadata-Version: 2.1 117 | Name: no-version 118 | Version: 1.0.0+custom 119 | ''').lstrip() 120 | -------------------------------------------------------------------------------- /trampolim/__main__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | import argparse 4 | import os 5 | import os.path 6 | import sys 7 | import warnings 8 | 9 | from typing import Iterable, List, Optional, TextIO, Type, Union 10 | 11 | import rich 12 | import rich.traceback 13 | 14 | import trampolim 15 | 16 | 17 | def _showwarning( 18 | message: Union[Warning, str], 19 | category: Type[Warning], 20 | filename: str, 21 | lineno: int, 22 | file: Optional[TextIO] = None, 23 | line: Optional[str] = None, 24 | ) -> None: # pragma: no cover 25 | rich.print(f'[bold orange]WARNING[/bold orange] {str(message)}') 26 | 27 | 28 | warnings.showwarning = _showwarning 29 | 30 | 31 | def _error(msg: str, code: int = 1) -> None: # pragma: no cover 32 | '''Print an error message and exit.''' 33 | rich.print(f'[bold red]ERROR[/bold red] {msg}', file=sys.stderr) 34 | exit(code) 35 | 36 | 37 | def main_parser(prog: str) -> argparse.ArgumentParser: 38 | '''Construct the main parser.''' 39 | # mypy does not recognize module.__path__ 40 | # https://github.com/python/mypy/issues/1422 41 | paths: Iterable[Optional[str]] = trampolim.__path__ # type: ignore 42 | parser = argparse.ArgumentParser() 43 | parser.prog = prog 44 | parser.add_argument( 45 | '--version', 46 | '-V', 47 | action='version', 48 | version='trampolim {} ({})'.format( 49 | trampolim.__version__, 50 | ', '.join(path for path in paths if path) 51 | ), 52 | ) 53 | subparsers = parser.add_subparsers( 54 | dest='command', 55 | title='subcommands', 56 | required=True, 57 | ) 58 | 59 | # build subcommand 60 | build_parser = subparsers.add_parser( 61 | 'build', 62 | description='build package distributions', 63 | ) 64 | build_parser.prog = prog 65 | build_parser.add_argument( 66 | 'outdir', 67 | type=str, 68 | nargs='?', 69 | default='dist', 70 | help='output directory (defaults to `dist`)', 71 | ) 72 | build_parser.add_argument( 73 | '--sdist', 74 | '-s', 75 | action='store_true', 76 | help='build a source distribution (enabled by default if no target is specified)', 77 | ) 78 | build_parser.add_argument( 79 | '--wheel', 80 | '-w', 81 | action='store_true', 82 | help='build a wheel (enabled by default if no target is specified)', 83 | ) 84 | 85 | ''' 86 | # publish subcommand 87 | publish_parser = subparsers.add_parser( 88 | 'publish', 89 | description='publish package distributions', 90 | ) 91 | publish_parser.prog = prog 92 | 93 | # check subcommand 94 | check_parser = subparsers.add_parser( 95 | 'check', 96 | description='check the project for issues', 97 | ) 98 | check_parser.prog = prog 99 | ''' 100 | 101 | return parser 102 | 103 | 104 | def main_task(cli_args: List[str], prog: str) -> None: 105 | '''Parse the CLI arguments and invoke the build process.''' 106 | parser = main_parser(prog) 107 | args = parser.parse_args(cli_args) 108 | 109 | if args.command == 'build': 110 | if not args.sdist and not args.wheel: 111 | args.sdist = True 112 | args.wheel = True 113 | 114 | if os.path.exists(args.outdir): 115 | if not os.path.isdir(args.outdir): 116 | _error(f'Output path `{args.outdir}` exists and is not a directory!') 117 | else: 118 | os.makedirs(args.outdir) 119 | 120 | if args.sdist: 121 | trampolim.build_sdist(args.outdir) 122 | if args.wheel: 123 | trampolim.build_wheel(args.outdir) 124 | 125 | 126 | def main(cli_args: List[str], prog: str) -> None: 127 | try: 128 | main_task(cli_args, prog) 129 | except Exception: # pragma: no cover 130 | exc_type, exc_value, tb = sys.exc_info() 131 | assert exc_type and exc_value 132 | rich.print(rich.traceback.Traceback.from_exception( 133 | exc_type, 134 | exc_value, 135 | tb.tb_next if tb else tb, 136 | )) 137 | 138 | 139 | def entrypoint() -> None: 140 | main(sys.argv[1:], sys.argv[0]) 141 | 142 | 143 | if __name__ == '__main__': # pragma: no cover 144 | try: 145 | main(sys.argv[1:], 'python -m trampolim') 146 | except KeyboardInterrupt: 147 | rich.print('Exiting...') 148 | -------------------------------------------------------------------------------- /tests/test_sdist.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | import pathlib 4 | import tarfile 5 | import textwrap 6 | 7 | import trampolim._build 8 | 9 | 10 | def assert_contents(archive, name, expected_source): 11 | for file in expected_source: 12 | f = pathlib.Path(*file[len(name):].split('/')) 13 | assert f.read_bytes() == archive.extractfile(file).read() 14 | 15 | 16 | def test_pyproject(package_license_file, sdist_license_file): 17 | t = tarfile.open(sdist_license_file, 'r') 18 | 19 | data = t.extractfile('license-file-0.0.0/pyproject.toml') 20 | 21 | with open(package_license_file / 'pyproject.toml', 'rb') as f: 22 | assert f.read() == data.read() 23 | 24 | 25 | def test_license_file(package_license_file, sdist_license_file): 26 | t = tarfile.open(sdist_license_file, 'r') 27 | 28 | assert 'license-file-0.0.0/some-license-file' in t.getnames() 29 | 30 | data = t.extractfile('license-file-0.0.0/some-license-file') 31 | 32 | with open(package_license_file / 'some-license-file', 'rb') as f: 33 | assert f.read() == data.read() 34 | 35 | 36 | def test_license_text(package_license_text, sdist_license_text): 37 | t = tarfile.open(sdist_license_text, 'r') 38 | 39 | assert t.extractfile('license-text-0.0.0/LICENSE').read() == 'inline license!'.encode() 40 | 41 | 42 | def test_readme_file(package_readme_md, sdist_readme_md): 43 | t = tarfile.open(sdist_readme_md, 'r') 44 | 45 | data = t.extractfile('readme-md-1.0.0/README.md') 46 | 47 | with open(package_readme_md / 'README.md', 'rb') as f: 48 | assert f.read() == data.read() 49 | 50 | 51 | def test_source(package_sample_source, sdist_sample_source): 52 | t = tarfile.open(sdist_sample_source, 'r') 53 | 54 | assert_contents( 55 | t, 56 | 'sample-source-0.0.0', 57 | [ 58 | 'sample-source-0.0.0/pyproject.toml', 59 | 'sample-source-0.0.0/sample_source/e/eb.py', 60 | 'sample-source-0.0.0/sample_source/e/ea.py', 61 | 'sample-source-0.0.0/sample_source/e/ec/ecc.py', 62 | 'sample-source-0.0.0/sample_source/e/ec/eca.py', 63 | 'sample-source-0.0.0/sample_source/e/ec/ecb.py', 64 | 'sample-source-0.0.0/sample_source/a.py', 65 | 'sample-source-0.0.0/sample_source/c.py', 66 | 'sample-source-0.0.0/sample_source/d/db.py', 67 | 'sample-source-0.0.0/sample_source/d/da.py', 68 | 'sample-source-0.0.0/sample_source/b.py', 69 | 'sample-source-0.0.0/sample_source/f/fc/fcb.py', 70 | 'sample-source-0.0.0/sample_source/f/fc/fca.py', 71 | 'sample-source-0.0.0/sample_source/f/fc/fcc.py', 72 | 'sample-source-0.0.0/sample_source/f/fc/fcc/fcca.py', 73 | 'sample-source-0.0.0/sample_source/f/fc/fcd/fcda.py', 74 | 'sample-source-0.0.0/sample_source/f/fc/fcd/fcdb.py', 75 | 'sample-source-0.0.0/sample_source/f/fb.py', 76 | 'sample-source-0.0.0/sample_source/f/fa.py', 77 | ], 78 | ) 79 | 80 | 81 | def test_pkginfo(package_license_text, sdist_license_text): 82 | p = trampolim._build.Project() 83 | t = tarfile.open(sdist_license_text, 'r') 84 | 85 | assert t.extractfile('license-text-0.0.0/PKG-INFO').read() == bytes(p._meta.as_rfc822()) 86 | 87 | 88 | def tests_source_include(package_source_include, sdist_source_include): 89 | t = tarfile.open(sdist_source_include, 'r') 90 | 91 | assert_contents( 92 | t, 93 | 'source-include-0.0.0', 94 | [ 95 | 'source-include-0.0.0/pyproject.toml', 96 | 'source-include-0.0.0/helper-data/a', 97 | 'source-include-0.0.0/helper-data/b', 98 | 'source-include-0.0.0/helper-data/c', 99 | 'source-include-0.0.0/some-config.txt', 100 | 'source-include-0.0.0/source_include.py', 101 | ], 102 | ) 103 | 104 | 105 | def tests_src_layout(package_src_layout, sdist_src_layout): 106 | t = tarfile.open(sdist_src_layout, 'r') 107 | 108 | assert_contents( 109 | t, 110 | 'src-layout-0.0.0', 111 | [ 112 | 'src-layout-0.0.0/src/src_layout/d/__init__.py', 113 | 'src-layout-0.0.0/src/src_layout/b.py', 114 | 'src-layout-0.0.0/src/src_layout/__init__.py', 115 | 'src-layout-0.0.0/pyproject.toml', 116 | 'src-layout-0.0.0/src/src_layout/c.py', 117 | 'src-layout-0.0.0/src/src_layout/a.py', 118 | ], 119 | ) 120 | 121 | 122 | def test_overwrite_version(monkeypatch, package_no_version, tmp_path): 123 | monkeypatch.setenv('TRAMPOLIM_VCS_VERSION', '1.0.0+custom') 124 | 125 | t = tarfile.open(tmp_path / trampolim.build_sdist(tmp_path), 'r') 126 | pkginfo = t.extractfile('no-version-1.0.0+custom/PKG-INFO').read().decode() 127 | assert pkginfo == textwrap.dedent(''' 128 | Metadata-Version: 2.1 129 | Name: no-version 130 | Version: 1.0.0+custom 131 | ''').lstrip() 132 | -------------------------------------------------------------------------------- /tests/test_project.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | import os 4 | import os.path 5 | import re 6 | import textwrap 7 | 8 | import packaging.version 9 | import pytest 10 | 11 | import trampolim 12 | import trampolim._build 13 | 14 | 15 | def test_unsupported_dynamic(): 16 | with pytest.raises( 17 | trampolim.ConfigurationError, 18 | match=re.escape('Unsupported field `readme` in `project.dynamic`'), 19 | ): 20 | trampolim._build.Project(_toml=''' 21 | [project] 22 | name = 'test' 23 | dynamic = [ 24 | 'readme', 25 | ] 26 | ''') 27 | 28 | 29 | def test_no_version(): 30 | with pytest.raises( 31 | trampolim.ConfigurationError, 32 | match=re.escape( 33 | 'Missing required field `project.version` (if you want to infer the ' 34 | 'project version automatically, `version` needs to be added to the ' 35 | '`project.dynamic` list field' 36 | ), 37 | ): 38 | trampolim._build.Project(_toml=''' 39 | [project] 40 | name = 'test' 41 | ''') 42 | 43 | 44 | def test_full_metadata(package_full_metadata): 45 | p = trampolim._build.Project() 46 | m = p.meta 47 | assert m.name == 'full-metadata' 48 | assert str(m.version) == '3.2.1' 49 | assert m.description == 'A package with all the metadata :)' 50 | assert m.license.text == 'some license text' 51 | assert m.keywords == ['trampolim', 'is', 'interesting'] 52 | assert m.authors == [ 53 | ('Unknown', 'example@example.com'), 54 | ('Example!', None), 55 | ] 56 | assert m.maintainers == [ 57 | ('Other Example', 'other@example.com'), 58 | ] 59 | assert m.classifiers == [ 60 | 'Development Status :: 4 - Beta', 61 | 'Programming Language :: Python' 62 | ] 63 | assert m.urls['homepage'] == 'example.com' 64 | assert m.urls['documentation'] == 'readthedocs.org' 65 | assert m.urls['repository'] == 'github.com/some/repo' 66 | assert m.urls['changelog'] == 'github.com/some/repo/blob/master/CHANGELOG.rst' 67 | assert m.scripts == { 68 | 'full-metadata': 'full_metadata:main_cli', 69 | } 70 | assert m.gui_scripts == { 71 | 'full-metadata-gui': 'full_metadata:main_gui', 72 | } 73 | assert m.entrypoints == { 74 | 'custom': { 75 | 'full-metadata': 'full_metadata:main_custom', 76 | } 77 | } 78 | assert p.python_tags == ['py3'] 79 | assert p.python_tag == 'py3' 80 | assert p.abi_tag == 'none' 81 | assert p.platform_tag == 'any' 82 | # TODO: requires-python, dependencies, optional-dependencies 83 | 84 | 85 | def test_rfc822_metadata(package_full_metadata): 86 | assert str(trampolim._build.Project()._meta.as_rfc822()) == textwrap.dedent(''' 87 | Metadata-Version: 2.1 88 | Name: full-metadata 89 | Version: 3.2.1 90 | Summary: A package with all the metadata :) 91 | Keywords: trampolim is interesting 92 | Home-page: example.com 93 | Author: Example! 94 | Author-Email: Unknown 95 | Maintainer-Email: Other Example 96 | Classifier: Development Status :: 4 - Beta 97 | Classifier: Programming Language :: Python 98 | Project-URL: Homepage, example.com 99 | Project-URL: Documentation, readthedocs.org 100 | Project-URL: Repository, github.com/some/repo 101 | Project-URL: Changelog, github.com/some/repo/blob/master/CHANGELOG.rst 102 | Requires-Python: >=3.8 103 | Requires-Dist: dependency1 104 | Requires-Dist: dependency2>1.0.0 105 | Requires-Dist: dependency3[extra] 106 | Requires-Dist: dependency4; os_name != "nt" 107 | Requires-Dist: dependency5[other-extra]>1.0; os_name == "nt" 108 | Requires-Dist: test_dependency; extra == "test" 109 | Requires-Dist: test_dependency[test_extra]; extra == "test" 110 | Requires-Dist: test_dependency[test_extra2]>3.0; os_name == "nt" and extra == "test" 111 | Provides-Extra: test 112 | Description-Content-Type: text/markdown 113 | 114 | some readme 115 | ''').lstrip() 116 | 117 | 118 | def test_rfc822_metadata_bytes(package_sample_source): 119 | assert bytes(trampolim._build.Project()._meta.as_rfc822()) == textwrap.dedent(''' 120 | Metadata-Version: 2.1 121 | Name: sample-source 122 | Version: 0.0.0 123 | ''').lstrip().encode() 124 | 125 | 126 | def test_no_module(package_no_module): 127 | with pytest.raises( 128 | trampolim.TrampolimError, 129 | match=re.escape('Could not find the top-level module(s) (looking for `no_module`)'), 130 | ): 131 | trampolim._build.Project() 132 | 133 | 134 | def test_root_modules_dir(package_sample_source): 135 | project = trampolim._build.Project() 136 | 137 | assert project.root_modules == ['sample_source'] 138 | 139 | 140 | def test_root_modules_file(package_file_module): 141 | project = trampolim._build.Project() 142 | 143 | assert project.root_modules == ['file_module.py'] 144 | 145 | 146 | @pytest.mark.parametrize( 147 | ('original', 'normalized'), 148 | [ 149 | ('simple', 'simple'), 150 | ('a-b', 'a-b'), 151 | ('a_b', 'a-b'), 152 | ('a.b', 'a-b'), 153 | ('a---b', 'a-b'), 154 | ('a___b', 'a-b'), 155 | ('a...b', 'a-b'), 156 | ('a-_.b', 'a-b'), 157 | ('a-b_c', 'a-b-c'), 158 | ('a_b_c', 'a-b-c'), 159 | ('a.b_c', 'a-b-c'), 160 | ] 161 | ) 162 | def test_name_normalization(package_sample_source, original, normalized): 163 | class DummyModules(trampolim._build.Project): 164 | @property 165 | def root_modules(self): 166 | return ['dummy'] # remove module dicovery 167 | 168 | assert DummyModules(_toml=''' 169 | [project] 170 | name = '%NAME%' 171 | version = '1.0.0' 172 | license = { text = '...' } 173 | '''.replace('%NAME%', original)).name == normalized 174 | 175 | 176 | def test_version(package_sample_source): 177 | assert trampolim._build.Project().version == packaging.version.Version('0.0.0') 178 | 179 | 180 | def test_vcs_version_envvar(package_vcs_version1): 181 | os.environ['TRAMPOLIM_VCS_VERSION'] = '1.2.3' 182 | assert str(trampolim._build.Project().version) == '1.2.3' 183 | os.environ.pop('TRAMPOLIM_VCS_VERSION') 184 | 185 | 186 | def test_vcs_version_git_archive_tag_alone(package_vcs_version1): 187 | assert str(trampolim._build.Project().version) == '0.0.1' 188 | 189 | 190 | def test_vcs_version_git_archive_many_refs_tag(package_vcs_version2): 191 | assert str(trampolim._build.Project().version) == '0.0.2' 192 | 193 | 194 | def test_vcs_version_git_archive_commit(package_vcs_version3): 195 | assert str(trampolim._build.Project().version) == '0.dev0+this.is.a.commit' 196 | 197 | 198 | def test_vcs_version_git_archive_unpopulated(mocker, package_vcs_version_unpopulated): 199 | mocker.patch('subprocess.check_output', side_effect=FileNotFoundError) 200 | with pytest.raises(trampolim.TrampolimError, match=re.escape( 201 | 'Could not find the project version from VCS (you can set the ' 202 | 'TRAMPOLIM_VCS_VERSION environment variable to manually override the version)' 203 | )): 204 | trampolim._build.Project() 205 | 206 | 207 | def test_vcs_git_repo(mocker, package_no_version): 208 | mocker.patch( 209 | 'subprocess.check_output', 210 | side_effect=[b'1.0.0-23-gea1f213'], 211 | ) 212 | assert str(trampolim._build.Project().version) == '1.0.0.23+gea1f213' 213 | 214 | 215 | def test_vcs_git_repo_exact(mocker, package_no_version): 216 | mocker.patch( 217 | 'subprocess.check_output', 218 | side_effect=[b'v2.1.1-0-g5ce8958'], 219 | ) 220 | assert str(trampolim._build.Project().version) == '2.1.1' 221 | 222 | 223 | def test_vcs_no_version(mocker, package_no_version): 224 | mocker.patch('subprocess.check_output', side_effect=FileNotFoundError) 225 | with pytest.raises(trampolim.TrampolimError, match=re.escape( 226 | 'Could not find the project version from VCS (you can set the ' 227 | 'TRAMPOLIM_VCS_VERSION environment variable to manually override the version)' 228 | )): 229 | trampolim._build.Project() 230 | 231 | 232 | def test_vcs_custom_top_modules(mocker, package_custom_top_modules): 233 | assert trampolim._build.Project().root_modules == [ 234 | 'module1', 235 | 'module2', 236 | 'module3', 237 | ] 238 | 239 | 240 | def test_vcs_test_top_module(mocker, package_test_top_level_module): 241 | with pytest.warns(trampolim.TrampolimWarning, match=re.escape( 242 | 'Top-level module `test` selected, are you sure you want to install it??' 243 | )): 244 | trampolim._build.Project() 245 | 246 | 247 | def tests_source_include(package_source_include): 248 | project = trampolim._build.Project() 249 | assert set( 250 | path.as_posix() 251 | for path in project.distribution_source 252 | ) == { 253 | 'pyproject.toml', 254 | 'helper-data/a', 255 | 'helper-data/b', 256 | 'helper-data/c', 257 | 'some-config.txt', 258 | 'source_include.py', 259 | } 260 | 261 | 262 | def test_src_layout(package_src_layout): 263 | project = trampolim._build.Project() 264 | assert { 265 | destination.as_posix(): path.as_posix() 266 | for destination, path in project.binary_source.items() 267 | } == { 268 | 'src_layout/__init__.py': 'src/src_layout/__init__.py', 269 | 'src_layout/a.py': 'src/src_layout/a.py', 270 | 'src_layout/b.py': 'src/src_layout/b.py', 271 | 'src_layout/c.py': 'src/src_layout/c.py', 272 | 'src_layout/d/__init__.py': 'src/src_layout/d/__init__.py', 273 | } 274 | 275 | 276 | def tests_task_extra_source(package_full_tasks): 277 | project = trampolim._build.Project() 278 | project.run_tasks() 279 | 280 | assert { 281 | destination.as_posix(): path.as_posix() 282 | for destination, path in project.binary_source.items() 283 | } == { 284 | 'example_source.py': 'example_source.py', 285 | 'full_tasks.py': 'full_tasks.py', 286 | } 287 | 288 | 289 | def tests_task_extra_source_epoch(source_date_epoch, package_full_tasks): 290 | project = trampolim._build.Project() 291 | project.run_tasks() 292 | 293 | with project.cd_binary_source(): 294 | st = os.stat('example_source.py') 295 | 296 | assert st.st_atime == st.st_mtime == 0 297 | 298 | 299 | def test_absolute_module_location(package_absolute_module_location): 300 | with pytest.raises(trampolim._build.ConfigurationError, match=re.escape( 301 | 'Location in `tool.trampolim.module-location` is not relative to the project source: /hello' 302 | )): 303 | trampolim._build.Project() 304 | -------------------------------------------------------------------------------- /trampolim/_build.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | import contextlib 4 | import dataclasses 5 | import email 6 | import functools 7 | import importlib.util 8 | import itertools 9 | import os 10 | import os.path 11 | import pathlib 12 | import shutil 13 | import subprocess 14 | import sys 15 | import warnings 16 | 17 | from typing import ContextManager, Iterable, Iterator, List, Mapping, NamedTuple, Optional, Sequence, Set, Union 18 | 19 | import packaging.markers 20 | import packaging.requirements 21 | import packaging.version 22 | import pep621 23 | import tomli 24 | 25 | import trampolim._metadata 26 | import trampolim._tasks 27 | import trampolim.types 28 | 29 | 30 | if sys.version_info < (3, 8): 31 | from backports.cached_property import cached_property 32 | else: 33 | from functools import cached_property 34 | 35 | 36 | Path = Union[str, os.PathLike] 37 | 38 | 39 | class PythonSource(NamedTuple): 40 | origin: pathlib.Path 41 | source: Set[pathlib.Path] 42 | 43 | 44 | class TrampolimError(Exception): 45 | '''Backend error.''' 46 | 47 | 48 | class ConfigurationError(TrampolimError): 49 | '''Error in the backend configuration.''' 50 | def __init__(self, msg: str, *, key: Optional[str] = None): 51 | super().__init__(msg) 52 | self._key = key 53 | 54 | @property 55 | def key(self) -> Optional[str]: # pragma: no cover 56 | return self._key 57 | 58 | 59 | class TrampolimWarning(Warning): 60 | '''Backend warning.''' 61 | 62 | 63 | def load_file_module(name: str, path: str) -> object: 64 | spec = importlib.util.spec_from_file_location(name, path) 65 | assert spec 66 | if not spec.loader: # pragma: no cover 67 | raise ImportError(f'Unable to import `{path}`: no loader') 68 | module = importlib.util.module_from_spec(spec) 69 | spec.loader.exec_module(module) # type: ignore 70 | return module 71 | 72 | 73 | @contextlib.contextmanager 74 | def cd(path: Path) -> Iterator[None]: 75 | cwd = os.getcwd() 76 | os.chdir(os.fspath(path)) 77 | 78 | try: 79 | yield 80 | finally: 81 | os.chdir(cwd) 82 | 83 | 84 | def ensure_empty_dir(path: pathlib.Path) -> None: 85 | shutil.rmtree(path, ignore_errors=True) 86 | path.mkdir(exist_ok=True, parents=True) 87 | 88 | 89 | def copy_to_dir(files: Iterable[pathlib.Path], out: pathlib.Path) -> None: 90 | for file in files: 91 | if os.path.dirname(file): 92 | destdirpath = out / file.parent 93 | destdirpath.mkdir(exist_ok=True, parents=True) 94 | shutil.copystat(file.parent, destdirpath) 95 | shutil.copy2( 96 | file, 97 | out / file, 98 | follow_symlinks=False, 99 | ) 100 | 101 | 102 | class Project(): 103 | _VALID_DYNAMIC = [ 104 | 'version', 105 | ] 106 | 107 | def __init__(self, _toml: Optional[str] = None) -> None: 108 | if _toml is not None: 109 | self._pyproject = tomli.loads(_toml) 110 | else: 111 | with open('pyproject.toml', 'rb') as f: 112 | self._pyproject = tomli.load(f) 113 | 114 | self._meta = pep621.StandardMetadata.from_pyproject(self._pyproject) 115 | self._trampolim_meta = trampolim._metadata.TrampolimMetadata.from_pyproject(self._pyproject) 116 | 117 | if self._trampolim_meta.module_location: 118 | try: 119 | pathlib.Path(self._trampolim_meta.module_location).relative_to(os.curdir) 120 | except ValueError: 121 | raise ConfigurationError( 122 | 'Location in `tool.trampolim.module-location` is not relative ' 123 | f'to the project source: {self._trampolim_meta.module_location}' 124 | ) from None 125 | 126 | for field in self._meta.dynamic: 127 | if field not in self._VALID_DYNAMIC: 128 | raise ConfigurationError(f'Unsupported field `{field}` in `project.dynamic`') 129 | 130 | self.version # calculate version 131 | 132 | self._path = os.getcwd() 133 | self._extra_binary_source: Set[pathlib.Path] = set() 134 | 135 | # warn users about test/tests modules -- they probably don't want them installed! 136 | for module in ('test', 'tests'): 137 | if module in self.root_modules: 138 | warnings.warn( 139 | f'Top-level module `{module}` selected, are you sure you want to install it??', 140 | TrampolimWarning, 141 | ) 142 | 143 | working_dir = pathlib.Path('.trampolim').absolute() 144 | # copy distribution source to working directory 145 | self._dist_srcpath: pathlib.Path = working_dir / 'dist-source' 146 | ensure_empty_dir(self._dist_srcpath) 147 | copy_to_dir( 148 | itertools.chain(self.distribution_source, self.build_system_source), 149 | self._dist_srcpath, 150 | ) 151 | # copy binary source to working directory 152 | self._bin_srcpath: pathlib.Path = working_dir / 'bin-source' 153 | ensure_empty_dir(self._bin_srcpath) 154 | copy_to_dir( 155 | itertools.chain(self.binary_source.values(), self.build_system_source), 156 | self._bin_srcpath, 157 | ) 158 | 159 | # collect tasks 160 | if os.path.isfile('.trampolim.py'): 161 | config = load_file_module('trampolim_config', '.trampolim.py') 162 | self._tasks = [ 163 | getattr(config, attr) 164 | for attr in dir(config) 165 | if not attr.startswith('_') and isinstance( 166 | getattr(config, attr), trampolim._tasks.Task 167 | ) 168 | ] 169 | else: 170 | self._tasks = [] 171 | 172 | @functools.lru_cache(maxsize=None) 173 | def run_tasks(self) -> None: 174 | '''Runs the project build tasks. 175 | 176 | If the ``SOURCE_DATE_EPOCH`` environment variable is present, extra 177 | sources files added by tasks will have their atime and mtime set to it. 178 | ''' 179 | with self.cd_binary_source(): 180 | for task in self._tasks: 181 | print(f'> Running `{task.name}`') 182 | session = trampolim._tasks.Session(self) 183 | task.run(session) 184 | self._extra_binary_source |= { 185 | pathlib.Path(*path.split('/')) 186 | for path in session.extra_source 187 | } 188 | # TODO: print summary 189 | 190 | # set the extra source atime and mtime 191 | source_date_epoch = os.environ.get('SOURCE_DATE_EPOCH') 192 | if source_date_epoch: 193 | timestamp = int(source_date_epoch) 194 | with self.cd_binary_source(): 195 | for file in self._extra_binary_source: 196 | os.utime(file, times=(timestamp, timestamp)) 197 | 198 | def cd_dist_source(self) -> ContextManager[None]: 199 | return cd(self._dist_srcpath) 200 | 201 | def cd_binary_source(self) -> ContextManager[None]: 202 | return cd(self._bin_srcpath) 203 | 204 | @property 205 | def name(self) -> str: 206 | assert isinstance(self._meta.name, str) 207 | return self._meta.name 208 | 209 | @property 210 | def normalized_name(self) -> str: 211 | return self.name.replace('-', '_') 212 | 213 | @property 214 | def build_system_source(self) -> Iterable[pathlib.Path]: 215 | return map(pathlib.Path, filter(None, [ 216 | 'pyproject.toml', 217 | '.trampolim.py' if os.path.isfile('.trampolim.py') else None, 218 | self._meta.license.file if self._meta.license else None, 219 | self._meta.readme.file if self._meta.readme else None, 220 | ])) 221 | 222 | @property 223 | def distribution_source(self) -> Iterable[pathlib.Path]: 224 | '''Project source, excluding modules -- for source distributions.''' 225 | return ( 226 | set(self.build_system_source) 227 | | self.config_source_include 228 | | self.python_source.source 229 | ) 230 | 231 | @property 232 | def binary_source(self) -> Mapping[pathlib.Path, pathlib.Path]: 233 | '''Python package source, excluding modules -- for binary distributions.''' 234 | source = { 235 | path: path 236 | for path in self._extra_binary_source 237 | } 238 | source.update({ 239 | path.relative_to(self.python_source.origin): path 240 | for path in self.python_source.source 241 | }) 242 | return source 243 | 244 | @property 245 | def modules_location(self) -> pathlib.Path: 246 | return pathlib.Path( 247 | self._trampolim_meta.module_location or os.path.curdir 248 | ).relative_to(os.path.curdir) 249 | 250 | @property 251 | def root_modules(self) -> Sequence[str]: 252 | '''Project top-level modules. 253 | 254 | By default will look for the normalized name of the project name 255 | replacing `-` with `_`. 256 | ''' 257 | if self._trampolim_meta.top_level_modules: 258 | return self._trampolim_meta.top_level_modules 259 | 260 | name = self.name.replace('-', '_') 261 | files = os.listdir(self._trampolim_meta.module_location or os.path.curdir) 262 | if f'{name}.py' in files: # file module 263 | return [f'{name}.py'] 264 | if name in files: # dir module 265 | return [name] 266 | raise TrampolimError(f'Could not find the top-level module(s) (looking for `{name}`)') 267 | 268 | @property 269 | def python_source(self) -> PythonSource: 270 | '''Full source for the modules in root_modules.''' 271 | source = set() 272 | # TODO: ignore files not escaped as specified in PEP 427 273 | for module in self.root_modules: 274 | if module.endswith('.py'): # file 275 | source.add(self.modules_location / module) 276 | else: # dir 277 | source |= { 278 | path 279 | for directory in self.modules_location.joinpath(module).rglob('**') 280 | for path in directory.iterdir() 281 | if path.is_file() and path.suffix != '.pyc' 282 | } 283 | return PythonSource(self.modules_location, source) 284 | 285 | @property 286 | def config_source_include(self) -> Set[pathlib.Path]: 287 | '''Extra source include paths specified in the config.''' 288 | return { 289 | pathlib.Path(*path.split('/')) 290 | for path in self._trampolim_meta.source_include 291 | } 292 | 293 | @property 294 | def meta(self) -> trampolim.types.FrozenMetadata: 295 | '''Project metadata.''' 296 | return trampolim.types.FrozenMetadata( 297 | **dataclasses.asdict(self._meta), 298 | **dataclasses.asdict(self._trampolim_meta), 299 | ) # type: ignore[call-arg] 300 | 301 | @cached_property 302 | def version(self) -> packaging.version.Version: # noqa: C901 303 | '''Project version.''' 304 | 305 | if self._meta.version: 306 | # static metadata 307 | assert isinstance(self._meta.version, packaging.version.Version) 308 | return self._meta.version 309 | 310 | version_file = pathlib.Path('.trampolim', 'version') 311 | if version_file.is_file(): 312 | if 'version' in self._meta.dynamic: 313 | # XXX: This should probably be done automatically by the pep621 module. 314 | self._meta.dynamic.remove('version') 315 | self._meta.version = packaging.version.Version(version_file.read_text()) 316 | assert isinstance(self._meta.version, packaging.version.Version) 317 | return self._meta.version 318 | 319 | if 'version' not in self._meta.dynamic: 320 | raise ConfigurationError( 321 | 'Missing required field `project.version` (if you want to infer the project version ' 322 | 'automatically, `version` needs to be added to the `project.dynamic` list field)' 323 | ) 324 | 325 | if 'TRAMPOLIM_VCS_VERSION' in os.environ: 326 | # manual overwrite 327 | self._meta.version = packaging.version.Version(os.environ['TRAMPOLIM_VCS_VERSION']) 328 | elif os.path.isfile('.git-archive.txt'): 329 | # from git archive 330 | # http://git-scm.com/book/en/v2/Customizing-Git-Git-Attributes 331 | # https://git-scm.com/docs/pretty-formats 332 | with open('.git-archive.txt') as f: 333 | data = email.message_from_file(f) 334 | if 'ref-names' in data: 335 | for ref in data['ref-names'].split(', '): 336 | try: 337 | name, value = ref.split(': ', maxsplit=1) 338 | except ValueError: 339 | continue 340 | if name == 'tag' and '$' not in value: 341 | assert isinstance(value, str) 342 | return packaging.version.Version(value.strip(' v')) 343 | if 'commit' in data and '$' not in data['commit']: 344 | assert isinstance(data['commit'], str) 345 | self._meta.version = packaging.version.Version(f'0.dev0+{data["commit"]}') 346 | else: 347 | # from git repo 348 | try: 349 | tag, r, commit = subprocess.check_output([ 350 | 'git', 'describe', '--tags', '--long' 351 | ]).decode().strip(' v').split('-') 352 | self._meta.version = packaging.version.Version( 353 | f'{tag}' if r == '0' else f'{tag}.{r}+{commit}' 354 | ) 355 | except (FileNotFoundError, subprocess.CalledProcessError, TypeError): 356 | pass 357 | 358 | if self._meta.version: 359 | if 'version' in self._meta.dynamic: 360 | # XXX: This should probably be done automatically by the pep621 module. 361 | self._meta.dynamic.remove('version') # we set the version, so it is no longer dynamic 362 | assert isinstance(self._meta.version, packaging.version.Version) 363 | return self._meta.version 364 | 365 | raise TrampolimError( 366 | 'Could not find the project version from VCS (you can set the ' 367 | 'TRAMPOLIM_VCS_VERSION environment variable to manually override the version)' 368 | ) 369 | 370 | @property 371 | def python_tags(self) -> List[str]: 372 | return ['py3'] 373 | 374 | @property 375 | def python_tag(self) -> str: 376 | return '.'.join(self.python_tags) 377 | 378 | @property 379 | def abi_tag(self) -> str: 380 | return 'none' 381 | 382 | @property 383 | def platform_tag(self) -> str: 384 | return 'any' 385 | --------------------------------------------------------------------------------