├── .dockerignore
├── .github
├── CODEOWNERS
├── CONTRIBUTING.md
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug-report.md
│ ├── config.yml
│ └── feature-request.md
├── PULL_REQUEST_TEMPLATE.md
├── config.yml
├── dependabot.yml
├── release.yml
└── workflows
│ ├── check.yaml
│ └── release.yaml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yaml
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── SECURITY.md
├── docs
├── _static
│ ├── custom.css
│ └── img
│ │ ├── tox.png
│ │ ├── tox.svg
│ │ └── toxfavi.ico
├── changelog.rst
├── changelog
│ ├── 3534.feature.rst
│ └── template.jinja2
├── cli_interface.rst
├── conf.py
├── config.rst
├── development.rst
├── faq.rst
├── img
│ ├── overview.mermaidjs
│ ├── overview_dark.svg
│ └── overview_light.svg
├── index.rst
├── installation.rst
├── plugins.rst
├── plugins_api.rst
├── tox_conf.py
├── upgrading.rst
└── user_guide.rst
├── ignore-words.txt
├── pyproject.toml
├── src
└── tox
│ ├── __init__.py
│ ├── __main__.py
│ ├── config
│ ├── __init__.py
│ ├── cli
│ │ ├── __init__.py
│ │ ├── env_var.py
│ │ ├── ini.py
│ │ ├── parse.py
│ │ └── parser.py
│ ├── loader
│ │ ├── __init__.py
│ │ ├── api.py
│ │ ├── convert.py
│ │ ├── ini
│ │ │ ├── __init__.py
│ │ │ ├── factor.py
│ │ │ └── replace.py
│ │ ├── memory.py
│ │ ├── replacer.py
│ │ ├── section.py
│ │ ├── str_convert.py
│ │ ├── stringify.py
│ │ └── toml
│ │ │ ├── __init__.py
│ │ │ ├── _api.py
│ │ │ ├── _replace.py
│ │ │ └── _validate.py
│ ├── main.py
│ ├── of_type.py
│ ├── set_env.py
│ ├── sets.py
│ ├── source
│ │ ├── __init__.py
│ │ ├── api.py
│ │ ├── discover.py
│ │ ├── ini.py
│ │ ├── ini_section.py
│ │ ├── legacy_toml.py
│ │ ├── setup_cfg.py
│ │ ├── toml_pyproject.py
│ │ ├── toml_tox.py
│ │ └── tox_ini.py
│ └── types.py
│ ├── execute
│ ├── __init__.py
│ ├── api.py
│ ├── local_sub_process
│ │ ├── __init__.py
│ │ ├── read_via_thread.py
│ │ ├── read_via_thread_unix.py
│ │ └── read_via_thread_windows.py
│ ├── pep517_backend.py
│ ├── request.py
│ ├── stream.py
│ └── util.py
│ ├── journal
│ ├── __init__.py
│ ├── env.py
│ └── main.py
│ ├── plugin
│ ├── __init__.py
│ ├── inline.py
│ ├── manager.py
│ └── spec.py
│ ├── provision.py
│ ├── py.typed
│ ├── pytest.py
│ ├── report.py
│ ├── run.py
│ ├── session
│ ├── __init__.py
│ ├── cmd
│ │ ├── __init__.py
│ │ ├── depends.py
│ │ ├── devenv.py
│ │ ├── exec_.py
│ │ ├── legacy.py
│ │ ├── list_env.py
│ │ ├── quickstart.py
│ │ ├── run
│ │ │ ├── __init__.py
│ │ │ ├── common.py
│ │ │ ├── parallel.py
│ │ │ ├── sequential.py
│ │ │ └── single.py
│ │ ├── schema.py
│ │ ├── show_config.py
│ │ └── version_flag.py
│ ├── env_select.py
│ └── state.py
│ ├── tox.schema.json
│ ├── tox_env
│ ├── __init__.py
│ ├── api.py
│ ├── errors.py
│ ├── info.py
│ ├── installer.py
│ ├── package.py
│ ├── python
│ │ ├── __init__.py
│ │ ├── api.py
│ │ ├── dependency_groups.py
│ │ ├── package.py
│ │ ├── pip
│ │ │ ├── __init__.py
│ │ │ ├── pip_install.py
│ │ │ ├── req
│ │ │ │ ├── __init__.py
│ │ │ │ ├── args.py
│ │ │ │ ├── file.py
│ │ │ │ └── util.py
│ │ │ └── req_file.py
│ │ ├── runner.py
│ │ └── virtual_env
│ │ │ ├── __init__.py
│ │ │ ├── api.py
│ │ │ ├── package
│ │ │ ├── __init__.py
│ │ │ ├── cmd_builder.py
│ │ │ ├── pyproject.py
│ │ │ └── util.py
│ │ │ └── runner.py
│ ├── register.py
│ ├── runner.py
│ └── util.py
│ └── util
│ ├── __init__.py
│ ├── ci.py
│ ├── cpu.py
│ ├── file_view.py
│ ├── graph.py
│ ├── path.py
│ └── spinner.py
├── tasks
└── release.py
├── tests
├── __init__.py
├── config
│ ├── __init__.py
│ ├── cli
│ │ ├── __init__.py
│ │ ├── conftest.py
│ │ ├── test_cli_env_var.py
│ │ ├── test_cli_ini.py
│ │ ├── test_parse.py
│ │ └── test_parser.py
│ ├── conftest.py
│ ├── loader
│ │ ├── __init__.py
│ │ ├── conftest.py
│ │ ├── ini
│ │ │ ├── __init__.py
│ │ │ ├── conftest.py
│ │ │ ├── replace
│ │ │ │ ├── __init__.py
│ │ │ │ ├── test_replace_env_var.py
│ │ │ │ ├── test_replace_os_pathsep.py
│ │ │ │ ├── test_replace_os_sep.py
│ │ │ │ ├── test_replace_posargs.py
│ │ │ │ ├── test_replace_tox_env.py
│ │ │ │ └── test_replace_tty.py
│ │ │ ├── test_factor.py
│ │ │ └── test_ini_loader.py
│ │ ├── test_loader.py
│ │ ├── test_memory_loader.py
│ │ ├── test_replace.py
│ │ ├── test_section.py
│ │ ├── test_str_convert.py
│ │ └── test_toml_loader.py
│ ├── source
│ │ ├── __init__.py
│ │ ├── test_discover.py
│ │ ├── test_legacy_toml.py
│ │ ├── test_setup_cfg.py
│ │ ├── test_source_ini.py
│ │ ├── test_toml_pyproject.py
│ │ └── test_toml_tox.py
│ ├── test_main.py
│ ├── test_of_types.py
│ ├── test_set_env.py
│ ├── test_sets.py
│ └── test_types.py
├── conftest.py
├── demo_pkg_inline
│ ├── build.py
│ └── pyproject.toml
├── demo_pkg_setuptools
│ ├── demo_pkg_setuptools
│ │ └── __init__.py
│ ├── pyproject.toml
│ └── setup.cfg
├── execute
│ ├── __init__.py
│ ├── conftest.py
│ ├── local_subprocess
│ │ ├── __init__.py
│ │ ├── bad_process.py
│ │ ├── local_subprocess_sigint.py
│ │ ├── test_execute_util.py
│ │ ├── test_local_subprocess.py
│ │ └── tty_check.py
│ ├── test_request.py
│ └── test_stream.py
├── journal
│ ├── __init__.py
│ └── test_main_journal.py
├── plugin
│ ├── conftest.py
│ ├── test_inline.py
│ ├── test_plugin.py
│ └── test_plugin_custom_config_set.py
├── pytest_
│ ├── __init__.py
│ └── test_init.py
├── session
│ ├── __init__.py
│ ├── cmd
│ │ ├── __init__.py
│ │ ├── run
│ │ │ ├── __init__.py
│ │ │ └── test_common.py
│ │ ├── test_depends.py
│ │ ├── test_devenv.py
│ │ ├── test_exec_.py
│ │ ├── test_legacy.py
│ │ ├── test_list_envs.py
│ │ ├── test_parallel.py
│ │ ├── test_quickstart.py
│ │ ├── test_schema.py
│ │ ├── test_sequential.py
│ │ ├── test_show_config.py
│ │ └── test_state.py
│ ├── test_env_select.py
│ └── test_session_common.py
├── test_call_modes.py
├── test_provision.py
├── test_report.py
├── test_run.py
├── test_version.py
├── tox_env
│ ├── __init__.py
│ ├── python
│ │ ├── __init__.py
│ │ ├── pip
│ │ │ ├── req
│ │ │ │ └── test_file.py
│ │ │ ├── test_pip_install.py
│ │ │ └── test_req_file.py
│ │ ├── test-pkg
│ │ │ └── pyproject.toml
│ │ ├── test_python_api.py
│ │ ├── test_python_runner.py
│ │ └── virtual_env
│ │ │ ├── __init__.py
│ │ │ ├── package
│ │ │ ├── __init__.py
│ │ │ ├── conftest.py
│ │ │ ├── test_package_cmd_builder.py
│ │ │ ├── test_package_pyproject.py
│ │ │ └── test_python_package_util.py
│ │ │ ├── test_setuptools.py
│ │ │ └── test_virtualenv_api.py
│ ├── test_api.py
│ ├── test_info.py
│ ├── test_register.py
│ ├── test_tox_env_api.py
│ └── test_tox_env_runner.py
├── type_check
│ └── add_config_container_factory.py
└── util
│ ├── __init__.py
│ ├── test_ci.py
│ ├── test_cpu.py
│ ├── test_graph.py
│ ├── test_path.py
│ └── test_spinner.py
└── tox.toml
/.dockerignore:
--------------------------------------------------------------------------------
1 | .tox*
2 | .*_cache
3 | *.egg-info
4 | Dockerfile
5 | build
6 | dist
7 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @gaborbernat
2 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to `tox`
2 |
3 | Thank you for your interest in contributing to `tox`! There are many ways to contribute, and we appreciate all of them.
4 | As a reminder, all contributors are expected to follow our [Code of Conduct][coc].
5 |
6 | [coc]: https://www.pypa.io/en/latest/code-of-conduct/
7 |
8 | ## Development Documentation
9 |
10 | Our [development documentation](http://tox.readthedocs.org/en/latest/development.html#development) contains details on
11 | how to get started with contributing to `tox`, and details of our development processes.
12 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | tidelift: pypi/tox
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ""
5 | labels: bug
6 | assignees: ""
7 | ---
8 |
9 | ## Issue
10 |
11 |
12 |
13 | ## Environment
14 |
15 | Provide at least:
16 |
17 | - OS:
18 |
19 |
20 | Output of pip list
of the host Python, where tox
is installed
21 |
22 | ```console
23 |
24 | ```
25 |
26 |
27 |
28 | ## Output of running tox
29 |
30 |
31 | Output of tox -rvv
32 |
33 | ```console
34 |
35 | ```
36 |
37 |
38 |
39 | ## Minimal example
40 |
41 |
42 |
43 | ```console
44 |
45 | ```
46 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | # Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
2 | blank_issues_enabled: true # default
3 | contact_links:
4 | - name: 🤷💻🤦 Discussions
5 | url: https://github.com/tox-dev/tox/discussions
6 | about: |
7 | Ask typical Q&A here. Please note that we cannot give support about Python packaging in general, questions about structuring projects and so on.
8 | - name: 📝 PyPA Code of Conduct
9 | url: https://www.pypa.io/en/latest/code-of-conduct/
10 | about: ❤ Be nice to other members of the community. ☮ Behave.
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an enhancement for this project
4 | title: ""
5 | labels: enhancement
6 | assignees: ""
7 | ---
8 |
9 | ## What's the problem this feature will solve?
10 |
11 |
12 |
13 | ## Describe the solution you'd like
14 |
15 |
16 |
17 |
18 |
19 | ## Alternative Solutions
20 |
21 |
23 |
24 | ## Additional context
25 |
26 |
27 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
5 |
6 | - [ ] ran the linter to address style issues (`tox -e fix`)
7 | - [ ] wrote descriptive pull request text
8 | - [ ] ensured there are test(s) validating the fix
9 | - [ ] added news fragment in `docs/changelog` folder
10 | - [ ] updated/extended the documentation
11 |
--------------------------------------------------------------------------------
/.github/config.yml:
--------------------------------------------------------------------------------
1 | chronographer:
2 | enforce_name:
3 | suffix: .rst
4 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 |
--------------------------------------------------------------------------------
/.github/release.yml:
--------------------------------------------------------------------------------
1 | changelog:
2 | exclude:
3 | authors:
4 | - dependabot
5 | - pre-commit-ci
6 |
--------------------------------------------------------------------------------
/.github/workflows/check.yaml:
--------------------------------------------------------------------------------
1 | name: check
2 | on:
3 | workflow_dispatch:
4 | push:
5 | branches: ["main"]
6 | tags-ignore: ["**"]
7 | pull_request:
8 | schedule:
9 | - cron: "0 8 * * *"
10 |
11 | env:
12 | FORCE_COLOR: 1
13 |
14 | concurrency:
15 | group: check-${{ github.ref }}
16 | cancel-in-progress: true
17 |
18 | jobs:
19 | test:
20 | name: test ${{ matrix.py }} on ${{ matrix.os }}
21 | runs-on: ${{ matrix.os }}
22 | strategy:
23 | fail-fast: false
24 | matrix:
25 | py:
26 | - "3.14"
27 | - "3.13"
28 | - "3.12"
29 | - "3.11"
30 | - "3.10"
31 | - "3.9"
32 | os:
33 | - ubuntu-latest
34 | - windows-latest
35 | - macos-latest
36 | steps:
37 | - uses: actions/checkout@v4
38 | with:
39 | fetch-depth: 0
40 | - name: Install the latest version of uv
41 | uses: astral-sh/setup-uv@v6
42 | with:
43 | enable-cache: true
44 | cache-dependency-glob: "pyproject.toml"
45 | github-token: ${{ secrets.GITHUB_TOKEN }}
46 | - name: Add .local/bin to Windows PATH
47 | if: runner.os == 'Windows'
48 | shell: bash
49 | run: echo "$USERPROFILE/.local/bin" >> $GITHUB_PATH
50 | - name: Install tox@self
51 | run: uv tool install --python-preference only-managed --python ${{ matrix.py }} tox@.
52 | - name: Setup test suite
53 | run: tox run -vv --notest --skip-missing-interpreters false -e ${{ matrix.py }}
54 | - name: Run test suite
55 | run: tox run --skip-pkg-install -e ${{ matrix.py }}
56 | env:
57 | PYTEST_ADDOPTS: "-vv --durations=20"
58 | DIFF_AGAINST: HEAD
59 | PYTEST_XDIST_AUTO_NUM_WORKERS: 0
60 |
61 | check:
62 | name: tox env ${{ matrix.tox_env }} on ${{ matrix.os }}
63 | runs-on: ${{ matrix.os }}
64 | strategy:
65 | fail-fast: false
66 | matrix:
67 | tox_env:
68 | - type
69 | - dev
70 | - docs
71 | - pkg_meta
72 | os:
73 | - ubuntu-latest
74 | - windows-latest
75 | exclude:
76 | - { os: windows-latest, tox_env: docs }
77 | steps:
78 | - uses: actions/checkout@v4
79 | with:
80 | fetch-depth: 0
81 | - name: Install the latest version of uv
82 | uses: astral-sh/setup-uv@v6
83 | with:
84 | enable-cache: true
85 | cache-dependency-glob: "pyproject.toml"
86 | github-token: ${{ secrets.GITHUB_TOKEN }}
87 | - name: Add .local/bin to Windows PATH
88 | if: runner.os == 'Windows'
89 | shell: bash
90 | run: echo "$USERPROFILE/.local/bin" >> $GITHUB_PATH
91 | - name: Install tox@self
92 | run: uv tool install --python-preference only-managed --python 3.13 tox@.
93 | - name: Setup check suite
94 | run: tox r -vv --notest --skip-missing-interpreters false -e ${{ matrix.tox_env }}
95 | - name: Run check for ${{ matrix.tox_env }}
96 | run: tox r --skip-pkg-install -e ${{ matrix.tox_env }}
97 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Release to PyPI
2 | on:
3 | push:
4 | tags: ["*"]
5 |
6 | env:
7 | dists-artifact-name: python-package-distributions
8 | FORCE_COLOR: 1
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 | with:
16 | fetch-depth: 0
17 | - name: Install the latest version of uv
18 | uses: astral-sh/setup-uv@v6
19 | with:
20 | enable-cache: true
21 | cache-dependency-glob: "pyproject.toml"
22 | github-token: ${{ secrets.GITHUB_TOKEN }}
23 | - name: Build package
24 | run: uv build --python 3.13 --python-preference only-managed --sdist --wheel . --out-dir dist
25 | - name: Store the distribution packages
26 | uses: actions/upload-artifact@v4
27 | with:
28 | name: ${{ env.dists-artifact-name }}
29 | path: dist/*
30 |
31 | release:
32 | needs:
33 | - build
34 | runs-on: ubuntu-latest
35 | environment:
36 | name: release
37 | url: https://pypi.org/project/tox/${{ github.ref_name }}
38 | permissions:
39 | id-token: write
40 | steps:
41 | - name: Download all the dists
42 | uses: actions/download-artifact@v4
43 | with:
44 | name: ${{ env.dists-artifact-name }}
45 | path: dist/
46 | - name: Publish to PyPI
47 | uses: pypa/gh-action-pypi-publish@v1.12.4
48 | with:
49 | attestations: true
50 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.*_cache
2 | /build
3 | /dist
4 | /docs/_draft.rst
5 | /src/tox/version.py
6 | /toxfile.py
7 | /Dockerfile
8 | /.tox
9 | *.py[co]
10 | __pycache__
11 | *.swp
12 | *.egg-info
13 | /tests/demo_pkg_setuptools/build/lib/demo_pkg_setuptools/__init__.py
14 | /tests/demo_pkg_inline.lock
15 | /tests/demo_pkg_inline/.tox/
16 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v5.0.0
4 | hooks:
5 | - id: end-of-file-fixer
6 | - id: trailing-whitespace
7 | - repo: https://github.com/python-jsonschema/check-jsonschema
8 | rev: 0.33.0
9 | hooks:
10 | - id: check-github-workflows
11 | args: ["--verbose"]
12 | - repo: https://github.com/codespell-project/codespell
13 | rev: v2.4.1
14 | hooks:
15 | - id: codespell
16 | additional_dependencies: ["tomli>=2.2.1"]
17 | - repo: https://github.com/tox-dev/pyproject-fmt
18 | rev: "v2.6.0"
19 | hooks:
20 | - id: pyproject-fmt
21 | - repo: https://github.com/abravalheri/validate-pyproject
22 | rev: "v0.24.1"
23 | hooks:
24 | - id: validate-pyproject
25 | - repo: https://github.com/astral-sh/ruff-pre-commit
26 | rev: "v0.11.11"
27 | hooks:
28 | - id: ruff-format
29 | - id: ruff
30 | args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"]
31 | - repo: https://github.com/asottile/blacken-docs
32 | rev: 1.19.1
33 | hooks:
34 | - id: blacken-docs
35 | additional_dependencies: [black==25.1]
36 | - repo: https://github.com/pre-commit/pygrep-hooks
37 | rev: v1.10.0
38 | hooks:
39 | - id: rst-backticks
40 | - repo: https://github.com/rbubley/mirrors-prettier
41 | rev: "v3.5.3"
42 | hooks:
43 | - id: prettier
44 | - repo: local
45 | hooks:
46 | - id: changelogs-rst
47 | name: changelog filenames
48 | language: fail
49 | entry: "changelog files must be named ####.(feature|bugfix|doc|removal|misc).rst"
50 | exclude: ^docs/changelog/(\d+\.(feature|bugfix|doc|removal|misc).rst|template.jinja2)
51 | files: ^docs/changelog/
52 | - repo: meta
53 | hooks:
54 | - id: check-hooks-apply
55 | - id: check-useless-excludes
56 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | build:
3 | os: ubuntu-lts-latest
4 | tools:
5 | python: "3"
6 | commands:
7 | - pip install uv
8 | - uv venv
9 | - uv pip install tox-uv tox@.
10 | - .venv/bin/tox run -e docs --
11 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making
6 | participation in our project and our community a harassment-free experience for everyone, regardless of age, body size,
7 | disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race,
8 | religion, or sexual identity and orientation.
9 |
10 | ## Our Standards
11 |
12 | Examples of behavior that contributes to creating a positive environment include:
13 |
14 | - Using welcoming and inclusive language
15 | - Being respectful of differing viewpoints and experiences
16 | - Gracefully accepting constructive criticism
17 | - Focusing on what is best for the community
18 | - Showing empathy towards other community members
19 |
20 | Examples of unacceptable behavior by participants include:
21 |
22 | - The use of sexualized language or imagery and unwelcome sexual attention or advances
23 | - Trolling, insulting/derogatory comments, and personal or political attacks
24 | - Public or private harassment
25 | - Publishing others' private information, such as a physical or electronic address, without explicit permission
26 | - Other conduct which could reasonably be considered inappropriate in a professional setting
27 |
28 | ## Our Responsibilities
29 |
30 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take
31 | appropriate and fair corrective action in response to any instances of unacceptable behavior.
32 |
33 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits,
34 | issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any
35 | contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
36 |
37 | ## Scope
38 |
39 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the
40 | project or its community. Examples of representing a project or community include using an official project e-mail
41 | address, posting via an official social media account, or acting as an appointed representative at an online or offline
42 | event. Representation of a project may be further defined and clarified by project maintainers.
43 |
44 | ## Enforcement
45 |
46 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. The
47 | project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the
48 | circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident.
49 | Further details of specific enforcement policies may be posted separately.
50 |
51 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent
52 | repercussions as determined by other members of the project's leadership.
53 |
54 | ## Attribution
55 |
56 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at
57 | [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html][version]
58 |
59 | [homepage]: https://www.contributor-covenant.org/
60 | [version]: https://www.contributor-covenant.org/version/1/4/
61 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Permission is hereby granted, free of charge, to any person obtaining a
2 | copy of this software and associated documentation files (the
3 | "Software"), to deal in the Software without restriction, including
4 | without limitation the rights to use, copy, modify, merge, publish,
5 | distribute, sublicense, and/or sell copies of the Software, and to
6 | permit persons to whom the Software is furnished to do so, subject to
7 | the following conditions:
8 |
9 | The above copyright notice and this permission notice shall be included
10 | in all copies or substantial portions of the Software.
11 |
12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
13 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
14 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
15 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
16 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
17 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
18 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # tox
2 |
3 | [](https://pypi.org/project/tox/)
4 | [](https://pypi.org/project/tox/)
6 | [](https://pepy.tech/project/tox)
7 | [](https://tox.readthedocs.io/en/latest/?badge=latest)
9 | [](https://github.com/tox-dev/tox/actions/workflows/check.yaml)
10 |
11 | `tox` aims to automate and standardize testing in Python. It is part of a larger vision of easing the packaging, testing
12 | and release process of Python software (alongside [pytest](https://docs.pytest.org/en/latest/) and
13 | [devpi](https://www.devpi.net)).
14 |
15 | tox is a generic virtual environment management and test command line tool you can use for:
16 |
17 | - checking your package builds and installs correctly under different environments (such as different Python
18 | implementations, versions or installation dependencies),
19 | - running your tests in each of the environments with the test tool of choice,
20 | - acting as a frontend to continuous integration servers, greatly reducing boilerplate and merging CI and shell-based
21 | testing.
22 |
23 | Please read our [user guide](https://tox.wiki/en/latest/user_guide.html#basic-example) for an example and more detailed
24 | introduction, or watch [this YouTube video](https://www.youtube.com/watch?v=SFqna5ilqig) that presents the problem space
25 | and how tox solves it.
26 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Reporting Vulnerabilities
2 |
3 | **⚠️ Please do not file public GitHub issues for security vulnerabilities as they are open for everyone to see! ⚠️**
4 |
5 | We encourage responsible disclosure practices for security vulnerabilities.
6 |
7 | ## Reporting a Vulnerability
8 |
9 | If you believe you've found a security-related bug, fill out a new
10 | vulnerability report via GitHub directly. To do so, follow these instructions:
11 |
12 | 1. Click on the `Security` tab in the project repository.
13 | 2. Click the green `Report a vulnerability` button at the top right corner.
14 | 3. Fill in the form as accurately as you can, including as many details as possible.
15 | 4. Click the green `Submit report` button at the bottom.
16 |
17 | ## Don't have a GitHub account?
18 |
19 | Alternatively, to report a security vulnerability, please use the
20 | [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure.
21 |
22 | It is currently set up to forward every incoming report to Bernát Gábor. We will try to assess the problem in timely
23 | manner and disclose it in a responsible way.
24 |
--------------------------------------------------------------------------------
/docs/_static/custom.css:
--------------------------------------------------------------------------------
1 | blockquote {
2 | border-left: none;
3 | font-style: normal;
4 | margin-left: 1.5rem;
5 | margin-right: 0;
6 | padding: 0;
7 | }
8 |
--------------------------------------------------------------------------------
/docs/_static/img/tox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/docs/_static/img/tox.png
--------------------------------------------------------------------------------
/docs/_static/img/toxfavi.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/docs/_static/img/toxfavi.ico
--------------------------------------------------------------------------------
/docs/changelog/3534.feature.rst:
--------------------------------------------------------------------------------
1 | Add ``free_threaded`` flag to to ``"python"`` entries in json output of ``--result-json``.
2 |
--------------------------------------------------------------------------------
/docs/changelog/template.jinja2:
--------------------------------------------------------------------------------
1 | {% set top_underline = underlines[0] %}
2 | {% if versiondata.name %}
3 | v{{ versiondata.version }} ({{ versiondata.date }})
4 | {{ top_underline * ((versiondata.version + versiondata.date)|length + 4)}}
5 | {% else %}
6 | {{ versiondata.version }} ({{ versiondata.date }})
7 | {{ top_underline * ((versiondata.version + versiondata.date)|length + 3)}}
8 | {% endif %}
9 |
10 | {% for section, _ in sections.items() %}
11 | {% set underline = underlines[1] %}
12 | {% if sections[section] %}
13 | {% for category, val in definitions.items() if category in sections[section]%}
14 | {{ definitions[category]['name'] }} - {{ versiondata.version }}
15 | {{ underline * ((definitions[category]['name'] + versiondata.version)|length + 3)}}
16 | {% if definitions[category]['showcontent'] %}
17 | {% for text, values in sections[section][category].items() %}
18 | - {{ text }} ({{ values|join(', ') }})
19 | {% endfor %}
20 |
21 | {% else %}
22 | - {{ sections[section][category]['']|join(', ') }}
23 |
24 | {% endif %}
25 | {% if sections[section][category]|length == 0 %}
26 | No significant changes.
27 |
28 | {% else %}
29 | {% endif %}
30 | {% endfor %}
31 | {% else %}
32 | No significant changes.
33 |
34 |
35 | {% endif %}
36 | {% endfor %}
37 |
--------------------------------------------------------------------------------
/docs/cli_interface.rst:
--------------------------------------------------------------------------------
1 | .. _cli:
2 |
3 | .. sphinx_argparse_cli::
4 | :module: tox.config.cli.parse
5 | :func: _get_parser_doc
6 |
--------------------------------------------------------------------------------
/docs/img/overview.mermaidjs:
--------------------------------------------------------------------------------
1 | stateDiagram-v2
2 | %%{init:{'state':{'nodeSpacing': 250, 'rankSpacing': 30}}}%%
3 |
4 | [*] --> conf
5 | conf --> tox_env
6 |
7 | state tox_env {
8 | state hdi <>
9 | state hpi <>
10 | state fpi <>
11 |
12 | [*] --> create
13 | create --> hdi : has (new) project dependencies (deps)
14 | hdi --> deps: yes
15 | hdi --> hpi: no, has package
16 | deps --> hpi: has package
17 | hpi --> fpi: yes, built package in this run
18 | hpi --> commands : no
19 | fpi --> install_deps: yes
20 | fpi --> package: no
21 | package --> install_deps
22 | install_deps --> install
23 | install --> commands
24 | commands --> commands: for each entry
in commands*
25 | commands --> [*] : pass outcome to report
26 | }
27 | tox_env --> tox_env :for each tox environment
28 |
29 | tox_env --> report
30 | report --> report :for each tox environment
31 | report --> [*]
32 |
33 | conf: build configuration (CLI + files)
identify environments to run
34 | create: create an isolated tox environment
the other steps are executed within this
35 | deps: install project dependencies (if has deps)
36 | package: build package
37 | install: install package without dependencies
38 | install_deps: install (new) package dependencies
39 | commands: run command
40 | report: report the outcome of the run
41 |
--------------------------------------------------------------------------------
/docs/installation.rst:
--------------------------------------------------------------------------------
1 | Installation
2 | ============
3 |
4 | As tool
5 | --------
6 |
7 | :pypi:`tox` is a CLI tool that needs a Python interpreter (version 3.9 or higher) to run. We recommend either
8 | :pypi:`pipx` or :pypi:`uv` to install tox into an isolated environment. This has the added benefit that later you'll
9 | be able to upgrade tox without affecting other parts of the system. We provide method for ``pip`` too here but we
10 | discourage that path if you can:
11 |
12 | .. tab:: uv
13 |
14 | .. code-block:: bash
15 |
16 | # install uv per https://docs.astral.sh/uv/#getting-started
17 | uv tool install tox
18 | tox --help
19 |
20 |
21 | .. tab:: pipx
22 |
23 | .. code-block:: bash
24 |
25 | python -m pip install pipx-in-pipx --user
26 | pipx install tox
27 | tox --help
28 |
29 | .. tab:: pip
30 |
31 | .. code-block:: bash
32 |
33 | python -m pip install --user tox
34 | python -m tox --help
35 |
36 | You can install it within the global Python interpreter itself (perhaps as a user package via the
37 | ``--user`` flag). Be cautious if you are using a Python installation that is managed by your operating system or
38 | another package manager. ``pip`` might not coordinate with those tools, and may leave your system in an inconsistent
39 | state. Note, if you go down this path you need to ensure pip is new enough per the subsections below
40 |
41 | wheel
42 | ~~~~~
43 | Installing tox via a wheel (default with pip) requires an installer that can understand the ``python-requires`` tag (see
44 | :pep:`503`), with pip this is version ``9.0.0`` (released in November 2016). Furthermore, in case you're not installing
45 | it via PyPI you need to use a mirror that correctly forwards the ``python-requires`` tag (notably the OpenStack mirrors
46 | don't do this, or older :gh_repo:`devpi/devpi` versions - added with version ``4.7.0``).
47 |
48 | .. _sdist:
49 |
50 | sdist
51 | ~~~~~
52 | When installing via a source distribution you need an installer that handles the :pep:`517` specification. In case of
53 | ``pip`` this is version ``18.0.0`` or later (released in July 2018). If you cannot upgrade your pip to support this you
54 | need to ensure that the build requirements from :gh:`pyproject.toml ` are
55 | satisfied before triggering the installation.
56 |
57 | via ``setup.py``
58 | ----------------
59 | We don't recommend and officially support this method. You should prefer using an installer that supports :pep:`517`
60 | interface, such as pip ``19.0.0`` or later. That being said you might be able to still install a package via this method
61 | if you satisfy build dependencies before calling the installation command (as described under :ref:`sdist`).
62 |
63 | latest unreleased
64 | -----------------
65 | Installing an unreleased version is discouraged and should be only done for testing purposes. If you do so you'll need
66 | a pip version of at least ``18.0.0`` and use the following command:
67 |
68 |
69 | .. code-block:: bash
70 |
71 | pip install git+https://github.com/tox-dev/tox.git@main
72 |
73 | .. _compatibility-requirements:
74 |
75 | Python and OS Compatibility
76 | ---------------------------
77 |
78 | tox works with the following Python interpreter implementations:
79 |
80 | - `CPython `_ versions 3.9, 3.10, 3.11, 3.12, 3.13
81 |
82 | This means tox works on the latest patch version of each of these minor versions. Previous patch versions are supported
83 | on a best effort approach.
84 |
--------------------------------------------------------------------------------
/docs/plugins.rst:
--------------------------------------------------------------------------------
1 | Extending tox
2 | =============
3 |
4 | Extensions points
5 | ~~~~~~~~~~~~~~~~~
6 |
7 | .. automodule:: tox.plugin
8 | :members:
9 | :exclude-members: impl
10 |
11 | .. autodata:: tox.plugin.impl
12 | :no-value:
13 |
14 | .. automodule:: tox.plugin.spec
15 | :members:
16 |
17 | A plugin can define its plugin module a:
18 |
19 | .. code-block:: python
20 |
21 | def tox_append_version_info() -> str:
22 | return "magic"
23 |
24 | and this message will be appended to the output of the ``--version`` flag.
25 |
26 | Adoption of a plugin under tox-dev Github organization
27 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
28 |
29 | You're free to host your plugin on your favorite platform, however the core tox development is happening on Github,
30 | under the ``tox-dev`` org organization. We are happy to adopt tox plugins under the ``tox-dev`` organization if:
31 |
32 | - we determine it's trying to solve a valid use case and it's not malicious (e.g. no plugin that deletes the users home
33 | directory),
34 | - it's released on PyPI with at least 100 downloads per month (to ensure it's a plugin used by people).
35 |
36 | What's in for you in this:
37 |
38 | - you get owner rights on the repository under the tox-dev organization,
39 | - exposure of your plugin under the core umbrella,
40 | - backup maintainers from other tox plugin development.
41 |
42 | How to apply:
43 |
44 | - create an issue under the ``tox-dev/tox`` Github repository with the title
45 | :gh:`Adopt plugin \ `,
46 | - wait for the green light by one of our maintainers (see :ref:`current-maintainers`),
47 | - follow the `guidance by Github
48 | `_,
49 | - (optionally) add at least one other people as co-maintainer on PyPI.
50 |
51 | Migration from tox 3
52 | ~~~~~~~~~~~~~~~~~~~~
53 | This section explains how the plugin interface changed between tox 3 and 4, and provides guidance for plugin developers
54 | on how to migrate.
55 |
56 | ``tox_get_python_executable``
57 | -----------------------------
58 | With tox 4 the Python discovery is performed ``tox.tox_env.python.virtual_env.api._get_python`` that delegates the job
59 | to ``virtualenv``. Therefore first `define a new virtualenv discovery mechanism
60 | `_ and then set that by setting the
61 | ``VIRTUALENV_DISCOVERY`` environment variable.
62 |
63 | ``tox_package``
64 | ---------------
65 | Register new packager types via :func:`tox_register_tox_env `.
66 |
67 | ``tox_addoption``
68 | -----------------
69 | Renamed to :func:`tox_add_option `.
70 |
--------------------------------------------------------------------------------
/ignore-words.txt:
--------------------------------------------------------------------------------
1 | releas
2 | master
3 |
--------------------------------------------------------------------------------
/src/tox/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from .run import main
4 | from .version import version as __version__
5 |
6 | __all__ = (
7 | "__version__",
8 | "main",
9 | )
10 |
--------------------------------------------------------------------------------
/src/tox/__main__.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from tox.run import run
4 |
5 | if __name__ == "__main__":
6 | run()
7 |
--------------------------------------------------------------------------------
/src/tox/config/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/src/tox/config/__init__.py
--------------------------------------------------------------------------------
/src/tox/config/cli/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/src/tox/config/cli/__init__.py
--------------------------------------------------------------------------------
/src/tox/config/cli/env_var.py:
--------------------------------------------------------------------------------
1 | """Provides configuration values from the environment variables."""
2 |
3 | from __future__ import annotations
4 |
5 | import logging
6 | import os
7 | from typing import Any, List
8 |
9 | from tox.config.loader.str_convert import StrConvert
10 |
11 | CONVERT = StrConvert()
12 |
13 |
14 | def get_env_var(key: str, of_type: type[Any]) -> tuple[Any, str] | None:
15 | """
16 | Get the environment variable option.
17 |
18 | :param key: the config key requested
19 | :param of_type: the type we would like to convert it to
20 | :return:
21 | """
22 | key_upper = key.upper()
23 | for environ_key in (f"TOX_{key_upper}", f"TOX{key_upper}"):
24 | if environ_key in os.environ:
25 | value = os.environ[environ_key]
26 | origin = getattr(of_type, "__origin__", of_type.__class__)
27 | try:
28 | if origin in {list, List}:
29 | entry_type = of_type.__args__[0]
30 | result = [CONVERT.to(raw=v, of_type=entry_type, factory=None) for v in value.split(";")]
31 | else:
32 | result = CONVERT.to(raw=value, of_type=of_type, factory=None)
33 | except Exception as exception: # noqa: BLE001
34 | logging.warning(
35 | "env var %s=%r cannot be transformed to %r because %r",
36 | environ_key,
37 | value,
38 | of_type,
39 | exception,
40 | )
41 | else:
42 | return result, f"env var {environ_key}"
43 | return None
44 |
45 |
46 | __all__ = ("get_env_var",)
47 |
--------------------------------------------------------------------------------
/src/tox/config/cli/ini.py:
--------------------------------------------------------------------------------
1 | """Provides configuration values from tox.ini files."""
2 |
3 | from __future__ import annotations
4 |
5 | import logging
6 | import os
7 | from configparser import ConfigParser
8 | from pathlib import Path
9 | from typing import Any, ClassVar
10 |
11 | from platformdirs import user_config_dir
12 |
13 | from tox.config.loader.api import ConfigLoadArgs
14 | from tox.config.loader.ini import IniLoader
15 | from tox.config.source.ini_section import CORE
16 |
17 | DEFAULT_CONFIG_FILE = Path(user_config_dir("tox")) / "config.ini"
18 |
19 |
20 | class IniConfig:
21 | TOX_CONFIG_FILE_ENV_VAR = "TOX_USER_CONFIG_FILE"
22 | STATE: ClassVar[dict[bool | None, str]] = {None: "failed to parse", True: "active", False: "missing"}
23 |
24 | def __init__(self) -> None:
25 | config_file = os.environ.get(self.TOX_CONFIG_FILE_ENV_VAR, None)
26 | self.is_env_var = config_file is not None
27 | self.config_file = Path(config_file if config_file is not None else DEFAULT_CONFIG_FILE)
28 | self._cache: dict[tuple[str, type[Any]], Any] = {}
29 | self.has_config_file: bool | None = self.config_file.exists()
30 | self.ini: IniLoader | None = None
31 |
32 | if self.has_config_file:
33 | self.config_file = self.config_file.absolute()
34 | try:
35 | parser = ConfigParser(interpolation=None)
36 | with self.config_file.open() as file_handler:
37 | parser.read_file(file_handler)
38 | self.has_tox_section = parser.has_section(CORE.key)
39 | if self.has_tox_section:
40 | self.ini = IniLoader(CORE, parser, overrides=[], core_section=CORE)
41 | except Exception as exception: # noqa: BLE001
42 | logging.error("failed to read config file %s because %r", config_file, exception) # noqa: TRY400
43 | self.has_config_file = None
44 |
45 | def get(self, key: str, of_type: type[Any]) -> Any:
46 | cache_key = key, of_type
47 | if cache_key in self._cache:
48 | result = self._cache[cache_key]
49 | else:
50 | try:
51 | if self.ini is None: # pragma: no cover # this can only happen if we don't call __bool__ firsts
52 | result = None
53 | else:
54 | source = "file"
55 | args = ConfigLoadArgs(chain=[key], name=CORE.prefix, env_name=None)
56 | value = self.ini.load(key, of_type=of_type, conf=None, factory=None, args=args)
57 | result = value, source
58 | except KeyError: # just not found
59 | result = None
60 | except Exception as exception: # noqa: BLE001
61 | logging.warning("%s key %s as type %r failed with %r", self.config_file, key, of_type, exception)
62 | result = None
63 | self._cache[cache_key] = result
64 | return result
65 |
66 | def __bool__(self) -> bool:
67 | return bool(self.has_config_file) and bool(self.has_tox_section)
68 |
69 | @property
70 | def epilog(self) -> str:
71 | # text to show within the parsers epilog
72 | return (
73 | f"{os.linesep}config file {str(self.config_file)!r} {self.STATE[self.has_config_file]} "
74 | f"(change{'d' if self.is_env_var else ''} via env var {self.TOX_CONFIG_FILE_ENV_VAR})"
75 | )
76 |
--------------------------------------------------------------------------------
/src/tox/config/cli/parse.py:
--------------------------------------------------------------------------------
1 | """This module pulls together this package: create and parse CLI arguments for tox."""
2 |
3 | from __future__ import annotations
4 |
5 | import locale
6 | import os
7 | from contextlib import redirect_stderr
8 | from pathlib import Path
9 | from typing import TYPE_CHECKING, Callable, NamedTuple, Sequence, cast
10 |
11 | from tox.config.source import Source, discover_source
12 | from tox.report import ToxHandler, setup_report
13 |
14 | from .parser import Parsed, ToxParser
15 |
16 | if TYPE_CHECKING:
17 | from tox.session.state import State
18 |
19 |
20 | class Options(NamedTuple):
21 | parsed: Parsed
22 | pos_args: Sequence[str] | None
23 | source: Source
24 | cmd_handlers: dict[str, Callable[[State], int]]
25 | log_handler: ToxHandler
26 |
27 |
28 | def get_options(*args: str) -> Options:
29 | pos_args: tuple[str, ...] | None = None
30 | try: # remove positional arguments passed to parser if specified, they are pulled directly from sys.argv
31 | pos_arg_at = args.index("--")
32 | except ValueError:
33 | pass
34 | else:
35 | pos_args = tuple(args[pos_arg_at + 1 :])
36 | args = args[:pos_arg_at]
37 |
38 | guess_verbosity, log_handler, source = _get_base(args)
39 | parsed, cmd_handlers = _get_all(args)
40 | if guess_verbosity != parsed.verbosity:
41 | log_handler.update_verbosity(parsed.verbosity)
42 | return Options(parsed, pos_args, source, cmd_handlers, log_handler)
43 |
44 |
45 | def _get_base(args: Sequence[str]) -> tuple[int, ToxHandler, Source]:
46 | """First just load the base options (verbosity+color) to setup the logging framework."""
47 | tox_parser = ToxParser.base()
48 | parsed = Parsed()
49 | try:
50 | with Path(os.devnull).open(
51 | "w", encoding=locale.getpreferredencoding(do_setlocale=False)
52 | ) as file_handler, redirect_stderr(file_handler):
53 | tox_parser.parse_known_args(args, namespace=parsed)
54 | except SystemExit:
55 | ... # ignore parse errors, such as -va raises ignored explicit argument 'a'
56 | guess_verbosity = parsed.verbosity
57 | handler = setup_report(guess_verbosity, parsed.is_colored)
58 | from tox.plugin.manager import MANAGER # load the plugin system right after we set up report # noqa: PLC0415
59 |
60 | source = discover_source(parsed.config_file, parsed.root_dir)
61 |
62 | MANAGER.load_plugins(source.path)
63 |
64 | return guess_verbosity, handler, source
65 |
66 |
67 | def _get_all(args: Sequence[str]) -> tuple[Parsed, dict[str, Callable[[State], int]]]:
68 | """Parse all the options."""
69 | tox_parser = _get_parser()
70 | parsed = cast("Parsed", tox_parser.parse_args(args))
71 | handlers = {k: p for k, (_, p) in tox_parser.handlers.items()}
72 | return parsed, handlers
73 |
74 |
75 | def _get_parser() -> ToxParser:
76 | tox_parser = ToxParser.core() # load the core options
77 | # plus options setup by plugins
78 | from tox.plugin.manager import MANAGER # noqa: PLC0415
79 |
80 | MANAGER.tox_add_option(tox_parser)
81 | tox_parser.fix_defaults()
82 | return tox_parser
83 |
84 |
85 | def _get_parser_doc() -> ToxParser:
86 | # trigger register of tox env types (during normal run we call this later to handle plugins)
87 | from tox.plugin.manager import MANAGER # pragma: no cover # noqa: PLC0415
88 |
89 | MANAGER.load_plugins(Path.cwd())
90 |
91 | return _get_parser() # pragma: no cover
92 |
93 |
94 | __all__ = (
95 | "Options",
96 | "get_options",
97 | )
98 |
--------------------------------------------------------------------------------
/src/tox/config/loader/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/src/tox/config/loader/__init__.py
--------------------------------------------------------------------------------
/src/tox/config/loader/memory.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from pathlib import Path
4 | from typing import TYPE_CHECKING, Any, Iterator
5 |
6 | from tox.config.types import Command, EnvList
7 |
8 | from .api import Loader
9 | from .section import Section
10 | from .str_convert import StrConvert
11 |
12 | if TYPE_CHECKING:
13 | from tox.config.main import Config
14 |
15 |
16 | class MemoryLoader(Loader[Any]):
17 | """Loads configuration directly from data in memory."""
18 |
19 | def __init__(self, **kwargs: Any) -> None:
20 | super().__init__(Section(prefix="", name=str(id(self))), [])
21 | self.raw: dict[str, Any] = {**kwargs}
22 |
23 | def load_raw(self, key: Any, conf: Config | None, env_name: str | None) -> Any: # noqa: ARG002
24 | return self.raw[key]
25 |
26 | def found_keys(self) -> set[str]:
27 | return set(self.raw.keys())
28 |
29 | @staticmethod
30 | def to_bool(value: Any) -> bool:
31 | return bool(value)
32 |
33 | @staticmethod
34 | def to_str(value: Any) -> str:
35 | return str(value)
36 |
37 | @staticmethod
38 | def to_list(value: Any, of_type: type[Any]) -> Iterator[Any]: # noqa: ARG004
39 | return iter(value)
40 |
41 | @staticmethod
42 | def to_set(value: Any, of_type: type[Any]) -> Iterator[Any]: # noqa: ARG004
43 | return iter(value)
44 |
45 | @staticmethod
46 | def to_dict(value: Any, of_type: tuple[type[Any], type[Any]]) -> Iterator[tuple[Any, Any]]: # noqa: ARG004
47 | return value.items() # type: ignore[no-any-return]
48 |
49 | @staticmethod
50 | def to_path(value: Any) -> Path:
51 | return Path(value)
52 |
53 | @staticmethod
54 | def to_command(value: Any) -> Command | None:
55 | if isinstance(value, Command):
56 | return value
57 | if isinstance(value, str):
58 | return StrConvert.to_command(value)
59 | raise TypeError(value)
60 |
61 | @staticmethod
62 | def to_env_list(value: Any) -> EnvList:
63 | if isinstance(value, EnvList):
64 | return value
65 | if isinstance(value, str):
66 | return StrConvert.to_env_list(value)
67 | raise TypeError(value)
68 |
--------------------------------------------------------------------------------
/src/tox/config/loader/section.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | if TYPE_CHECKING:
6 | import sys
7 |
8 | if sys.version_info >= (3, 11): # pragma: no cover (py311+)
9 | from typing import Self
10 | else: # pragma: no cover ( None:
20 | self._prefix = prefix
21 | self._name = name
22 |
23 | @classmethod
24 | def from_key(cls: type[Self], key: str) -> Self:
25 | """
26 | Create a section from a section key.
27 |
28 | :param key: the section key
29 | :return: the constructed section
30 | """
31 | sep_at = key.find(cls.SEP)
32 | if sep_at == -1:
33 | prefix, name = None, key
34 | else:
35 | prefix, name = key[:sep_at], key[sep_at + 1 :]
36 | return cls(prefix, name)
37 |
38 | @property
39 | def prefix(self) -> str | None:
40 | """:return: the prefix of the section"""
41 | return self._prefix
42 |
43 | @property
44 | def name(self) -> str:
45 | """:return: the name of the section"""
46 | return self._name
47 |
48 | @property
49 | def key(self) -> str:
50 | """:return: the section key"""
51 | return self.SEP.join(i for i in (self._prefix, self._name) if i is not None)
52 |
53 | def __str__(self) -> str:
54 | return self.key
55 |
56 | def __repr__(self) -> str:
57 | return f"{self.__class__.__name__}(prefix={self._prefix!r}, name={self._name!r})"
58 |
59 | def __eq__(self, other: object) -> bool:
60 | return isinstance(other, self.__class__) and (self._prefix, self._name) == (
61 | other._prefix,
62 | other.name,
63 | )
64 |
65 |
66 | __all__ = [
67 | "Section",
68 | ]
69 |
--------------------------------------------------------------------------------
/src/tox/config/loader/stringify.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from pathlib import Path
4 | from typing import Any, Mapping, Sequence, Set
5 |
6 | from tox.config.set_env import SetEnv
7 | from tox.config.types import Command, EnvList
8 | from tox.tox_env.python.pip.req_file import PythonDeps
9 |
10 |
11 | def stringify(value: Any) -> tuple[str, bool]: # noqa: PLR0911
12 | """
13 | Transform a value into a string representation.
14 |
15 | :param value: the value in question
16 | :return: a tuple, first the value as str, second a flag if the value if a multi-line one
17 | """
18 | if isinstance(value, str):
19 | return value, False
20 | if isinstance(value, (Path, float, int, bool)):
21 | return str(value), False
22 | if isinstance(value, Mapping):
23 | return "\n".join(f"{stringify(k)[0]}={stringify(v)[0]}" for k, v in value.items()), True
24 | if isinstance(value, Sequence):
25 | return "\n".join(stringify(i)[0] for i in value), True
26 | if isinstance(value, Set): # sort it to make it stable
27 | return "\n".join(sorted(stringify(i)[0] for i in value)), True
28 | if isinstance(value, EnvList):
29 | return "\n".join(e for e in value.envs), True
30 | if isinstance(value, Command):
31 | return value.shell, True
32 | if isinstance(value, SetEnv):
33 | env_var_keys = sorted(value)
34 | return stringify({k: value.load(k) for k in env_var_keys})
35 | if isinstance(value, PythonDeps):
36 | return stringify(value.lines())
37 | return str(value), False
38 |
39 |
40 | __all__ = ("stringify",)
41 |
--------------------------------------------------------------------------------
/src/tox/config/loader/toml/_api.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING, Dict, List, Union
4 |
5 | if TYPE_CHECKING:
6 | import sys
7 |
8 | if sys.version_info >= (3, 10): # pragma: no cover (py310+)
9 | from typing import TypeAlias
10 | else: # pragma: no cover (py310+)
11 | from typing_extensions import TypeAlias
12 |
13 | TomlTypes: TypeAlias = Union[Dict[str, "TomlTypes"], List["TomlTypes"], str, int, float, bool, None]
14 |
15 | __all__ = [
16 | "TomlTypes",
17 | ]
18 |
--------------------------------------------------------------------------------
/src/tox/config/loader/toml/_validate.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from inspect import isclass
4 | from typing import (
5 | TYPE_CHECKING,
6 | Any,
7 | Dict,
8 | List,
9 | Literal,
10 | TypeVar,
11 | Union,
12 | cast,
13 | )
14 |
15 | from tox.config.types import Command
16 |
17 | if TYPE_CHECKING:
18 | import sys
19 |
20 | from ._api import TomlTypes
21 |
22 | if sys.version_info >= (3, 11): # pragma: no cover (py311+)
23 | from typing import TypeGuard
24 | else: # pragma: no cover (py311+)
25 | from typing_extensions import TypeGuard
26 |
27 | T = TypeVar("T")
28 |
29 |
30 | def validate(val: TomlTypes, of_type: type[T]) -> TypeGuard[T]: # noqa: C901, PLR0912
31 | casting_to = getattr(of_type, "__origin__", of_type.__class__)
32 | msg = ""
33 | if casting_to in {list, List}:
34 | entry_type = of_type.__args__[0] # type: ignore[attr-defined]
35 | if isinstance(val, list):
36 | for va in val:
37 | validate(va, entry_type)
38 | else:
39 | msg = f"{val!r} is not list"
40 | elif isclass(of_type) and issubclass(of_type, Command):
41 | # first we cast it to list then create commands, so for now validate it as a nested list
42 | validate(val, List[str])
43 | elif casting_to in {dict, Dict}:
44 | key_type, value_type = of_type.__args__[0], of_type.__args__[1] # type: ignore[attr-defined]
45 | if isinstance(val, dict):
46 | for va in val:
47 | validate(va, key_type)
48 | for va in val.values():
49 | validate(va, value_type)
50 | else:
51 | msg = f"{val!r} is not dictionary"
52 | elif casting_to == Union: # handle Optional values
53 | args: list[type[Any]] = of_type.__args__ # type: ignore[attr-defined]
54 | for arg in args:
55 | try:
56 | validate(val, arg)
57 | break
58 | except TypeError:
59 | pass
60 | else:
61 | msg = f"{val!r} is not union of {', '.join(a.__name__ for a in args)}"
62 | elif casting_to in {Literal, type(Literal)}:
63 | choice = of_type.__args__ # type: ignore[attr-defined]
64 | if val not in choice:
65 | msg = f"{val!r} is not one of literal {','.join(repr(i) for i in choice)}"
66 | elif not isinstance(val, of_type):
67 | if issubclass(of_type, (bool, str, int)):
68 | fail = not isinstance(val, of_type)
69 | else:
70 | try: # check if it can be converted
71 | of_type(val) # type: ignore[call-arg]
72 | fail = False
73 | except Exception: # noqa: BLE001
74 | fail = True
75 | if fail:
76 | msg = f"{val!r} is not of type {of_type.__name__!r}"
77 | if msg:
78 | raise TypeError(msg)
79 | return cast("T", val) # type: ignore[return-value] # logic too complicated for mypy
80 |
81 |
82 | __all__ = [
83 | "validate",
84 | ]
85 |
--------------------------------------------------------------------------------
/src/tox/config/source/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from .api import Source
4 | from .discover import discover_source
5 |
6 | __all__ = (
7 | "Source",
8 | "discover_source",
9 | )
10 |
--------------------------------------------------------------------------------
/src/tox/config/source/discover.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import logging
4 | from itertools import chain
5 | from pathlib import Path
6 | from typing import TYPE_CHECKING
7 |
8 | from tox.report import HandledError
9 |
10 | from .legacy_toml import LegacyToml
11 | from .setup_cfg import SetupCfg
12 | from .toml_pyproject import TomlPyProject
13 | from .toml_tox import TomlTox
14 | from .tox_ini import ToxIni
15 |
16 | if TYPE_CHECKING:
17 | from .api import Source
18 |
19 | SOURCE_TYPES: tuple[type[Source], ...] = (
20 | ToxIni,
21 | SetupCfg,
22 | LegacyToml,
23 | TomlPyProject,
24 | TomlTox,
25 | )
26 |
27 |
28 | def discover_source(config_file: Path | None, root_dir: Path | None) -> Source:
29 | """
30 | Discover a source for configuration.
31 |
32 | :param config_file: the file storing the source
33 | :param root_dir: the root directory as set by the user (None means not set)
34 | :return: the source of the config
35 | """
36 | if config_file is None:
37 | src = _locate_source()
38 | if src is None:
39 | src = _create_default_source(root_dir)
40 | elif config_file.is_dir():
41 | src = None
42 | for src_type in SOURCE_TYPES:
43 | candidate: Path = config_file / src_type.FILENAME
44 | try:
45 | src = src_type(candidate)
46 | break
47 | except ValueError:
48 | continue
49 | if src is None:
50 | msg = f"could not find any config file in {config_file}"
51 | raise HandledError(msg)
52 | else:
53 | src = _load_exact_source(config_file)
54 | return src
55 |
56 |
57 | def _locate_source() -> Source | None:
58 | folder = Path.cwd()
59 | for base in chain([folder], folder.parents):
60 | for src_type in SOURCE_TYPES:
61 | candidate: Path = base / src_type.FILENAME
62 | try:
63 | return src_type(candidate)
64 | except ValueError:
65 | pass
66 | return None
67 |
68 |
69 | def _load_exact_source(config_file: Path) -> Source:
70 | # if the filename matches to the letter some config file name do not fallback to other source types
71 | exact_match = [s for s in SOURCE_TYPES if config_file.name == s.FILENAME] # pragma: no cover
72 | for src_type in exact_match or SOURCE_TYPES: # pragma: no branch
73 | try:
74 | return src_type(config_file)
75 | except ValueError: # noqa: PERF203
76 | pass
77 | msg = f"could not recognize config file {config_file}"
78 | raise HandledError(msg)
79 |
80 |
81 | def _create_default_source(root_dir: Path | None) -> Source:
82 | if root_dir is None: # if set use that
83 | empty = Path.cwd()
84 | for base in chain([empty], empty.parents):
85 | if (base / "pyproject.toml").exists():
86 | empty = base
87 | break
88 | else: # if not set use where we find pyproject.toml in the tree or cwd
89 | empty = root_dir
90 | names = " or ".join({i.FILENAME: None for i in SOURCE_TYPES})
91 | logging.warning("No %s found, assuming empty tox.ini at %s", names, empty)
92 | return ToxIni(empty / "tox.ini", content="")
93 |
94 |
95 | __all__ = ("discover_source",)
96 |
--------------------------------------------------------------------------------
/src/tox/config/source/ini_section.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from tox.config.loader.ini.factor import expand_ranges, extend_factors
4 | from tox.config.loader.section import Section
5 |
6 |
7 | class IniSection(Section):
8 | @classmethod
9 | def test_env(cls, name: str) -> IniSection:
10 | return cls(TEST_ENV_PREFIX, name)
11 |
12 | @property
13 | def is_test_env(self) -> bool:
14 | return self.prefix == TEST_ENV_PREFIX
15 |
16 | @property
17 | def names(self) -> list[str]:
18 | return list(extend_factors(expand_ranges(self.name)))
19 |
20 |
21 | TEST_ENV_PREFIX = "testenv"
22 | PKG_ENV_PREFIX = "pkgenv"
23 | CORE = IniSection(None, "tox")
24 |
25 | __all__ = [
26 | "CORE",
27 | "PKG_ENV_PREFIX",
28 | "TEST_ENV_PREFIX",
29 | "IniSection",
30 | ]
31 |
--------------------------------------------------------------------------------
/src/tox/config/source/legacy_toml.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 |
5 | if sys.version_info >= (3, 11): # pragma: no cover (py311+)
6 | import tomllib
7 | else: # pragma: no cover (py311+)
8 | import tomli as tomllib
9 |
10 |
11 | from typing import TYPE_CHECKING
12 |
13 | from .ini import IniSource
14 |
15 | if TYPE_CHECKING:
16 | from pathlib import Path
17 |
18 |
19 | class LegacyToml(IniSource):
20 | FILENAME = "pyproject.toml"
21 |
22 | def __init__(self, path: Path) -> None:
23 | if path.name != self.FILENAME or not path.exists():
24 | raise ValueError
25 | with path.open("rb") as file_handler:
26 | toml_content = tomllib.load(file_handler)
27 | try:
28 | content = toml_content["tool"]["tox"]["legacy_tox_ini"]
29 | except KeyError as exc:
30 | raise ValueError(path) from exc
31 | super().__init__(path, content=content)
32 |
33 |
34 | __all__ = ("LegacyToml",)
35 |
--------------------------------------------------------------------------------
/src/tox/config/source/setup_cfg.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | from .ini import IniSource
6 | from .ini_section import IniSection
7 |
8 | if TYPE_CHECKING:
9 | from pathlib import Path
10 |
11 |
12 | class SetupCfg(IniSource):
13 | """Configuration sourced from a tox.ini file."""
14 |
15 | CORE_SECTION = IniSection("tox", "tox")
16 | FILENAME = "setup.cfg"
17 |
18 | def __init__(self, path: Path) -> None:
19 | super().__init__(path)
20 | if not self._parser.has_section(self.CORE_SECTION.key):
21 | raise ValueError
22 |
23 |
24 | __all__ = ("SetupCfg",)
25 |
--------------------------------------------------------------------------------
/src/tox/config/source/toml_tox.py:
--------------------------------------------------------------------------------
1 | """Load from a tox.toml file."""
2 |
3 | from __future__ import annotations
4 |
5 | from .toml_pyproject import TomlPyProject, TomlSection
6 |
7 |
8 | class TomlToxSection(TomlSection):
9 | PREFIX = ()
10 |
11 |
12 | class TomlTox(TomlPyProject):
13 | """Configuration sourced from a pyproject.toml files."""
14 |
15 | FILENAME = "tox.toml"
16 | _Section = TomlToxSection
17 |
18 |
19 | __all__ = [
20 | "TomlTox",
21 | ]
22 |
--------------------------------------------------------------------------------
/src/tox/config/source/tox_ini.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from .ini import IniSource
4 |
5 |
6 | class ToxIni(IniSource):
7 | """Configuration sourced from a tox.ini file."""
8 |
9 | FILENAME = "tox.ini"
10 |
11 |
12 | __all__ = ("ToxIni",)
13 |
--------------------------------------------------------------------------------
/src/tox/config/types.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from collections import OrderedDict
4 | from typing import Iterator, Sequence
5 |
6 | from tox.execute.request import shell_cmd
7 |
8 |
9 | class CircularChainError(ValueError):
10 | """circular chain in config"""
11 |
12 |
13 | class Command: # noqa: PLW1641
14 | """A command to execute."""
15 |
16 | def __init__(self, args: list[str]) -> None:
17 | """
18 | Create a new command to execute.
19 |
20 | :param args: the command line arguments (first value can be ``-`` to indicate ignore the exit code)
21 | """
22 | self.ignore_exit_code: bool = args[0] == "-" #: a flag indicating if the exit code should be ignored
23 | self.invert_exit_code: bool = args[0] == "!" #: a flag for flipped exit code (non-zero = success, 0 = error)
24 | self.args: list[str] = (
25 | args[1:] if self.ignore_exit_code or self.invert_exit_code else args
26 | ) #: the command line arguments
27 |
28 | def __repr__(self) -> str:
29 | args = (["-"] if self.ignore_exit_code else ["!"] if self.invert_exit_code else []) + self.args
30 | return f"{type(self).__name__}(args={args!r})"
31 |
32 | def __eq__(self, other: object) -> bool:
33 | return type(self) == type(other) and (self.args, self.ignore_exit_code, self.invert_exit_code) == ( # noqa: E721
34 | other.args, # type: ignore[attr-defined]
35 | other.ignore_exit_code, # type: ignore[attr-defined]
36 | other.invert_exit_code, # type: ignore[attr-defined]
37 | )
38 |
39 | def __ne__(self, other: object) -> bool:
40 | return not (self == other)
41 |
42 | @property
43 | def shell(self) -> str:
44 | """:return: a shell representation of the command (platform dependent)"""
45 | return shell_cmd(self.args)
46 |
47 |
48 | class EnvList: # noqa: PLW1641
49 | """A tox environment list."""
50 |
51 | def __init__(self, envs: Sequence[str]) -> None:
52 | """
53 | Crate a new tox environment list.
54 |
55 | :param envs: the list of tox environments
56 | """
57 | self.envs = list(OrderedDict((e, None) for e in envs).keys())
58 |
59 | def __repr__(self) -> str:
60 | return f"{type(self).__name__}({self.envs!r})"
61 |
62 | def __eq__(self, other: object) -> bool:
63 | return type(self) == type(other) and self.envs == other.envs # type: ignore[attr-defined] # noqa: E721
64 |
65 | def __ne__(self, other: object) -> bool:
66 | return not (self == other)
67 |
68 | def __iter__(self) -> Iterator[str]:
69 | """:return: iterator that goes through the defined env-list"""
70 | return iter(self.envs)
71 |
72 |
73 | __all__ = (
74 | "Command",
75 | "EnvList",
76 | )
77 |
--------------------------------------------------------------------------------
/src/tox/execute/__init__.py:
--------------------------------------------------------------------------------
1 | """Package that handles execution of commands within tox environments."""
2 |
3 | from __future__ import annotations
4 |
5 | from .api import Outcome
6 | from .request import ExecuteRequest
7 |
8 | __all__ = (
9 | "ExecuteRequest",
10 | "Outcome",
11 | )
12 |
--------------------------------------------------------------------------------
/src/tox/execute/local_sub_process/read_via_thread.py:
--------------------------------------------------------------------------------
1 | """A reader that drain a stream via its file no on a background thread."""
2 |
3 | from __future__ import annotations
4 |
5 | from abc import ABC, abstractmethod
6 | from threading import Event, Thread
7 | from typing import TYPE_CHECKING, Callable
8 |
9 | if TYPE_CHECKING:
10 | import sys
11 | from types import TracebackType
12 |
13 | if sys.version_info >= (3, 11): # pragma: no cover (py311+)
14 | from typing import Self
15 | else: # pragma: no cover ( None: # noqa: FBT001
24 | self.file_no = file_no
25 | self.stop = Event()
26 | self.thread = Thread(target=self._read_stream, name=f"tox-r-{name}-{file_no}")
27 | self.handler = handler
28 | self._on_exit_drain = drain
29 |
30 | def __enter__(self) -> Self:
31 | self.thread.start()
32 | return self
33 |
34 | def __exit__(
35 | self,
36 | exc_type: type[BaseException] | None,
37 | exc_val: BaseException | None,
38 | exc_tb: TracebackType | None,
39 | ) -> None:
40 | self.stop.set() # signal thread to stop
41 | while self.thread.is_alive(): # wait until it stops
42 | self.thread.join(WAIT_GENERAL)
43 | self._drain_stream() # read anything left
44 |
45 | @abstractmethod
46 | def _read_stream(self) -> None:
47 | raise NotImplementedError
48 |
49 | @abstractmethod
50 | def _drain_stream(self) -> None:
51 | raise NotImplementedError
52 |
--------------------------------------------------------------------------------
/src/tox/execute/local_sub_process/read_via_thread_unix.py:
--------------------------------------------------------------------------------
1 | """On UNIX we use select.select to ensure we drain in a non-blocking fashion."""
2 |
3 | from __future__ import annotations
4 |
5 | import errno # pragma: win32 no cover
6 | import os # pragma: win32 no cover
7 | import select # pragma: win32 no cover
8 | from typing import Callable
9 |
10 | from .read_via_thread import ReadViaThread # pragma: win32 no cover
11 |
12 | STOP_EVENT_CHECK_PERIODICITY_IN_MS = 0.01 # pragma: win32 no cover
13 |
14 |
15 | class ReadViaThreadUnix(ReadViaThread): # pragma: win32 no cover
16 | def __init__(self, file_no: int, handler: Callable[[bytes], None], name: str, drain: bool) -> None: # noqa: FBT001
17 | super().__init__(file_no, handler, name, drain)
18 |
19 | def _read_stream(self) -> None:
20 | while not self.stop.is_set():
21 | # we need to drain the stream, but periodically give chance for the thread to break if the stop event has
22 | # been set (this is so that an interrupt can be handled)
23 | if self._read_available() is None: # pragma: no branch
24 | break # pragma: no cover
25 |
26 | def _drain_stream(self) -> None:
27 | # no block just poll
28 | while True:
29 | if self._read_available(timeout=0) is not True: # pragma: no branch
30 | break # pragma: no cover
31 |
32 | def _read_available(self, timeout: float = STOP_EVENT_CHECK_PERIODICITY_IN_MS) -> bool | None:
33 | try:
34 | ready, __, ___ = select.select([self.file_no], [], [], timeout)
35 | if ready:
36 | data = os.read(self.file_no, 1024) # read up to 1024 characters
37 | # If the end of the file referred to by fd has been reached, an empty bytes object is returned.
38 | if data:
39 | self.handler(data)
40 | return True
41 | except OSError as exception: # pragma: no cover
42 | # Bad file descriptor or Input/output error
43 | if exception.errno in {errno.EBADF, errno.EIO}:
44 | return None
45 | raise
46 | else:
47 | return False
48 |
--------------------------------------------------------------------------------
/src/tox/execute/local_sub_process/read_via_thread_windows.py:
--------------------------------------------------------------------------------
1 | """On Windows we use overlapped mechanism, borrowing it from asyncio (but without the event loop)."""
2 |
3 | from __future__ import annotations # pragma: win32 cover
4 |
5 | import _overlapped # type: ignore[import] # pragma: win32 cover # noqa: PLC2701
6 | import logging # pragma: win32 cover
7 | from asyncio.windows_utils import BUFSIZE # type: ignore[attr-defined] # pragma: win32 cover
8 | from time import sleep # pragma: win32 cover
9 | from typing import Callable # pragma: win32 cover
10 |
11 | from .read_via_thread import ReadViaThread # pragma: win32 cover
12 |
13 | # mypy: warn-unused-ignores=false
14 |
15 |
16 | class ReadViaThreadWindows(ReadViaThread): # pragma: win32 cover
17 | def __init__(self, file_no: int, handler: Callable[[bytes], None], name: str, drain: bool) -> None: # noqa: FBT001
18 | super().__init__(file_no, handler, name, drain)
19 | self.closed = False
20 | self._ov: _overlapped.Overlapped | None = None
21 | self._waiting_for_read = False
22 |
23 | def _read_stream(self) -> None:
24 | keep_reading = True
25 | while keep_reading: # try to read at least once
26 | wait = self._read_batch()
27 | if wait is None:
28 | break
29 | if wait is True:
30 | sleep(0.01) # sleep for 10ms if there was no data to read and try again
31 | keep_reading = not self.stop.is_set()
32 |
33 | def _drain_stream(self) -> None:
34 | wait: bool | None = self.closed
35 | while wait is False:
36 | wait = self._read_batch()
37 |
38 | def _read_batch(self) -> bool | None:
39 | """:returns: None means error can no longer read, True wait for result, False try again"""
40 | if self._waiting_for_read is False:
41 | self._ov = _overlapped.Overlapped(0) # can use it only once to read a batch
42 | try: # read up to BUFSIZE at a time
43 | self._ov.ReadFile(self.file_no, BUFSIZE) # type: ignore[attr-defined]
44 | self._waiting_for_read = True
45 | except OSError:
46 | self.closed = True
47 | return None
48 | try: # wait=False to not block and give chance for the stop check
49 | data = self._ov.getresult(False) # type: ignore[union-attr] # noqa: FBT003
50 | except OSError as exception:
51 | # 996 (0x3E4) Overlapped I/O event is not in a signaled state.
52 | # 995 (0x3E3) The I/O operation has been aborted because of either a thread exit or an application request.
53 | win_error = getattr(exception, "winerror", None)
54 | if win_error == 996: # noqa: PLR2004
55 | return True
56 | if win_error != 995: # noqa: PLR2004
57 | logging.error("failed to read %r", exception) # noqa: TRY400
58 | return None
59 | else:
60 | self._ov = None
61 | self._waiting_for_read = False
62 | if data:
63 | self.handler(data)
64 | else:
65 | return None
66 | return False
67 |
--------------------------------------------------------------------------------
/src/tox/execute/request.py:
--------------------------------------------------------------------------------
1 | """Module declaring a command execution request."""
2 |
3 | from __future__ import annotations
4 |
5 | import sys
6 | from enum import Enum
7 | from pathlib import Path
8 | from typing import Sequence
9 |
10 |
11 | class StdinSource(Enum):
12 | OFF = 0 #: input disabled
13 | USER = 1 #: input via the standard input
14 | API = 2 #: input via programmatic access
15 |
16 | @staticmethod
17 | def user_only() -> StdinSource:
18 | """:return: ``USER`` if the standard input is tty type else ``OFF``"""
19 | return StdinSource.USER if sys.stdin.isatty() else StdinSource.OFF
20 |
21 |
22 | class ExecuteRequest:
23 | """Defines a commands execution request."""
24 |
25 | def __init__( # noqa: PLR0913
26 | self,
27 | cmd: Sequence[str | Path],
28 | cwd: Path,
29 | env: dict[str, str],
30 | stdin: StdinSource,
31 | run_id: str,
32 | allow: list[str] | None = None,
33 | ) -> None:
34 | """
35 | Create a new execution request.
36 |
37 | :param cmd: the command to run
38 | :param cwd: the current working directory
39 | :param env: the environment variables
40 | :param stdin: the type of standard input allowed
41 | :param run_id: an id to identify this run
42 | """
43 | if len(cmd) == 0:
44 | msg = "cannot execute an empty command"
45 | raise ValueError(msg)
46 | self.cmd: list[str] = [str(i) for i in cmd] #: the command to run
47 | self.cwd = cwd #: the working directory to use
48 | self.env = env #: the environment variables to use
49 | self.stdin = stdin #: the type of standard input interaction allowed
50 | self.run_id = run_id #: an id to identify this run
51 | if allow is not None and "*" in allow:
52 | allow = None # if we allow everything we can just disable the check
53 | self.allow = allow
54 |
55 | @property
56 | def shell_cmd(self) -> str:
57 | """:return: the command to run as a shell command"""
58 | try:
59 | exe = str(Path(self.cmd[0]).relative_to(self.cwd))
60 | except ValueError:
61 | exe = self.cmd[0]
62 | cmd = [exe]
63 | cmd.extend(self.cmd[1:])
64 | return shell_cmd(cmd)
65 |
66 | def __repr__(self) -> str:
67 | return f"{self.__class__.__name__}(cmd={self.cmd!r}, cwd={self.cwd!r}, env=..., stdin={self.stdin!r})"
68 |
69 |
70 | def shell_cmd(cmd: Sequence[str]) -> str:
71 | if sys.platform == "win32": # pragma: win32 cover
72 | from subprocess import list2cmdline # noqa: PLC0415
73 |
74 | return list2cmdline(tuple(str(x) for x in cmd))
75 | # pragma: win32 no cover
76 | from shlex import quote as shlex_quote # noqa: PLC0415
77 |
78 | return " ".join(shlex_quote(str(x)) for x in cmd)
79 |
80 |
81 | __all__ = (
82 | "ExecuteRequest",
83 | "StdinSource",
84 | "shell_cmd",
85 | )
86 |
--------------------------------------------------------------------------------
/src/tox/execute/util.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from pathlib import Path
4 |
5 |
6 | def shebang(exe: str) -> list[str] | None:
7 | """
8 | :param exe: the executable
9 | :return: the shebang interpreter arguments
10 | """
11 | # When invoking a command using a shebang line that exceeds the OS shebang limit (e.g. Linux has a limit of 128;
12 | # BINPRM_BUF_SIZE) the invocation will fail. In this case you'd want to replace the shebang invocation with an
13 | # explicit invocation.
14 | # see https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/fs/binfmt_script.c#n34
15 | try:
16 | with Path(exe).open("rb") as file_handler:
17 | marker = file_handler.read(2)
18 | if marker != b"#!":
19 | return None
20 | shebang_line = file_handler.readline()
21 | except OSError:
22 | return None
23 | try:
24 | decoded = shebang_line.decode("UTF-8")
25 | except UnicodeDecodeError:
26 | return None
27 | return [i.strip() for i in decoded.strip().split() if i.strip()]
28 |
29 |
30 | __all__ = [
31 | "shebang",
32 | ]
33 |
--------------------------------------------------------------------------------
/src/tox/journal/__init__.py:
--------------------------------------------------------------------------------
1 | """This module handles collecting and persisting in json format a tox session."""
2 |
3 | from __future__ import annotations
4 |
5 | import json
6 | import locale
7 | from pathlib import Path
8 |
9 | from .env import EnvJournal
10 | from .main import Journal
11 |
12 |
13 | def write_journal(path: Path | None, journal: Journal) -> None:
14 | if path is None:
15 | return
16 | with Path(path).open("w", encoding=locale.getpreferredencoding(do_setlocale=False)) as file_handler:
17 | json.dump(journal.content, file_handler, indent=2, ensure_ascii=False)
18 |
19 |
20 | __all__ = (
21 | "EnvJournal",
22 | "Journal",
23 | "write_journal",
24 | )
25 |
--------------------------------------------------------------------------------
/src/tox/journal/env.py:
--------------------------------------------------------------------------------
1 | """Record information about tox environments."""
2 |
3 | from __future__ import annotations
4 |
5 | from typing import TYPE_CHECKING, Any
6 |
7 | if TYPE_CHECKING:
8 | from tox.execute import Outcome
9 |
10 |
11 | class EnvJournal:
12 | """Report the status of a tox environment."""
13 |
14 | def __init__(self, enabled: bool, name: str) -> None: # noqa: FBT001
15 | self._enabled = enabled
16 | self.name = name
17 | self._content: dict[str, Any] = {}
18 | self._executes: list[tuple[str, Outcome]] = []
19 |
20 | def __setitem__(self, key: str, value: Any) -> None:
21 | """
22 | Add a new entry under key into the event journal.
23 |
24 | :param key: the key under what to add the data
25 | :param value: the data to add
26 | """
27 | self._content[key] = value
28 |
29 | def __bool__(self) -> bool:
30 | """:return: a flag indicating if the event journal is on or not"""
31 | return self._enabled
32 |
33 | def add_execute(self, outcome: Outcome, run_id: str) -> None:
34 | """
35 | Add a command execution to the journal.
36 |
37 | :param outcome: the execution outcome
38 | :param run_id: the execution id
39 | """
40 | self._executes.append((run_id, outcome))
41 |
42 | @property
43 | def content(self) -> dict[str, Any]:
44 | """:return: the env journal content (merges explicit keys and execution commands)"""
45 | tests: list[dict[str, Any]] = []
46 | setup: list[dict[str, Any]] = []
47 | for run_id, outcome in self._executes:
48 | one = {
49 | "command": outcome.cmd,
50 | "output": outcome.out,
51 | "err": outcome.err,
52 | "retcode": outcome.exit_code,
53 | "elapsed": outcome.elapsed,
54 | "show_on_standard": outcome.show_on_standard,
55 | "run_id": run_id,
56 | "start": outcome.start,
57 | "end": outcome.end,
58 | }
59 | if run_id.startswith(("commands", "build")):
60 | tests.append(one)
61 | else:
62 | setup.append(one)
63 | if tests:
64 | self["test"] = tests
65 | if setup:
66 | self["setup"] = setup
67 | return self._content
68 |
69 |
70 | __all__ = ("EnvJournal",)
71 |
--------------------------------------------------------------------------------
/src/tox/journal/main.py:
--------------------------------------------------------------------------------
1 | """Generate json report of a tox run."""
2 |
3 | from __future__ import annotations
4 |
5 | import socket
6 | import sys
7 | from typing import Any
8 |
9 | from tox.version import version
10 |
11 | from .env import EnvJournal
12 |
13 |
14 | class Journal:
15 | """The result of a tox session."""
16 |
17 | def __init__(self, enabled: bool) -> None: # noqa: FBT001
18 | self._enabled = enabled
19 | self._content: dict[str, Any] = {}
20 | self._env: dict[str, EnvJournal] = {}
21 |
22 | if self._enabled:
23 | self._content.update(
24 | {
25 | "reportversion": "1",
26 | "toxversion": version,
27 | "platform": sys.platform,
28 | "host": socket.getfqdn(),
29 | },
30 | )
31 |
32 | def get_env_journal(self, name: str) -> EnvJournal:
33 | """Return the env log of an environment (create on first call)."""
34 | if name not in self._env:
35 | env = EnvJournal(self._enabled, name)
36 | self._env[name] = env
37 | return self._env[name]
38 |
39 | @property
40 | def content(self) -> dict[str, Any]:
41 | test_env_journals: dict[str, Any] = {}
42 | for name, value in self._env.items():
43 | test_env_journals[name] = value.content
44 | if test_env_journals:
45 | self._content["testenvs"] = test_env_journals
46 | return self._content
47 |
48 | def __bool__(self) -> bool:
49 | return self._enabled
50 |
51 |
52 | __all__ = ("Journal",)
53 |
--------------------------------------------------------------------------------
/src/tox/plugin/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | tox uses `pluggy `_ to customize the default behavior. It provides an
3 | extension mechanism for plugin management by calling hooks.
4 |
5 | Pluggy discovers a plugin by looking up for entry-points named ``tox``, for example in a pyproject.toml:
6 |
7 | .. code-block:: toml
8 |
9 | [project.entry-points.tox]
10 | your_plugin = "your_plugin.hooks"
11 |
12 | Therefore, to start using a plugin, you solely need to install it in the same environment tox is running in and it will
13 | be discovered via the defined entry-point (in the example above, tox will load ``your_plugin.hooks``).
14 |
15 | A plugin is created by implementing extension points in the form of hooks. For example the following code snippet would
16 | define a new ``--magic`` command line interface flag the user can specify:
17 |
18 | .. code-block:: python
19 |
20 | from tox.config.cli.parser import ToxParser
21 | from tox.plugin import impl
22 |
23 |
24 | @impl
25 | def tox_add_option(parser: ToxParser) -> None:
26 | parser.add_argument("--magic", action="store_true", help="magical flag")
27 |
28 | You can define such hooks either in a package installed alongside tox or within a ``toxfile.py`` found alongside your
29 | tox configuration file (root of your project).
30 | """
31 |
32 | from __future__ import annotations
33 |
34 | from typing import Any, Callable, TypeVar
35 |
36 | import pluggy
37 |
38 | NAME = "tox" #: the name of the tox hook
39 |
40 | _F = TypeVar("_F", bound=Callable[..., Any])
41 | impl: Callable[[_F], _F] = pluggy.HookimplMarker(NAME) #: decorator to mark tox plugin hooks
42 |
43 |
44 | __all__ = (
45 | "NAME",
46 | "impl",
47 | )
48 |
--------------------------------------------------------------------------------
/src/tox/plugin/inline.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import importlib
4 | import sys
5 | from typing import TYPE_CHECKING
6 |
7 | if TYPE_CHECKING:
8 | from pathlib import Path
9 | from types import ModuleType
10 |
11 |
12 | def load_inline(path: Path) -> ModuleType | None:
13 | # nox uses here the importlib.machinery.SourceFileLoader but I consider this similarly good, and we can keep any
14 | # name for the tox file, its content will always be loaded in this module from a system point of view
15 | for name in ("toxfile", "☣"):
16 | candidate = path.parent / f"{name}.py"
17 | if candidate.exists():
18 | return _load_plugin(candidate)
19 | return None
20 |
21 |
22 | def _load_plugin(path: Path) -> ModuleType:
23 | in_folder = path.parent
24 | module_name = path.stem
25 |
26 | sys.path.insert(0, str(in_folder))
27 | try:
28 | if module_name in sys.modules:
29 | del sys.modules[module_name] # pragma: no cover
30 | return importlib.import_module(module_name)
31 | finally:
32 | del sys.path[0]
33 |
--------------------------------------------------------------------------------
/src/tox/plugin/spec.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING, Any
4 |
5 | import pluggy
6 |
7 | from . import NAME
8 |
9 | if TYPE_CHECKING:
10 | from tox.config.cli.parser import ToxParser
11 | from tox.config.sets import ConfigSet, EnvConfigSet
12 | from tox.execute import Outcome
13 | from tox.session.state import State
14 | from tox.tox_env.api import ToxEnv
15 | from tox.tox_env.register import ToxEnvRegister
16 |
17 | _spec = pluggy.HookspecMarker(NAME)
18 |
19 |
20 | @_spec
21 | def tox_register_tox_env(register: ToxEnvRegister) -> None:
22 | """
23 | Register new tox environment type. You can register:
24 |
25 | - **run environment**: by default this is a local subprocess backed virtualenv Python
26 | - **packaging environment**: by default this is a PEP-517 compliant local subprocess backed virtualenv Python
27 |
28 | :param register: a object that can be used to register new tox environment types
29 | """
30 |
31 |
32 | @_spec
33 | def tox_add_option(parser: ToxParser) -> None:
34 | """
35 | Add a command line argument. This is the first hook to be called, right after the logging setup and config source
36 | discovery.
37 |
38 | :param parser: the command line parser
39 | """
40 |
41 |
42 | @_spec
43 | def tox_add_core_config(core_conf: ConfigSet, state: State) -> None:
44 | """
45 | Called when the core configuration is built for a tox environment.
46 |
47 | :param core_conf: the core configuration object
48 | :param state: the global tox state object
49 | """
50 |
51 |
52 | @_spec
53 | def tox_add_env_config(env_conf: EnvConfigSet, state: State) -> None:
54 | """
55 | Called when configuration is built for a tox environment.
56 |
57 | :param env_conf: the core configuration object
58 | :param state: the global tox state object
59 | """
60 |
61 |
62 | @_spec
63 | def tox_before_run_commands(tox_env: ToxEnv) -> None:
64 | """
65 | Called before the commands set is executed.
66 |
67 | :param tox_env: the tox environment being executed
68 | """
69 |
70 |
71 | @_spec
72 | def tox_after_run_commands(tox_env: ToxEnv, exit_code: int, outcomes: list[Outcome]) -> None:
73 | """
74 | Called after the commands set is executed.
75 |
76 | :param tox_env: the tox environment being executed
77 | :param exit_code: exit code of the command
78 | :param outcomes: outcome of each command execution
79 | """
80 |
81 |
82 | @_spec
83 | def tox_on_install(tox_env: ToxEnv, arguments: Any, section: str, of_type: str) -> None:
84 | """
85 | Called before executing an installation command.
86 |
87 | :param tox_env: the tox environment where the command runs in
88 | :param arguments: installation arguments
89 | :param section: section of the installation
90 | :param of_type: type of the installation
91 | """
92 |
93 |
94 | @_spec
95 | def tox_env_teardown(tox_env: ToxEnv) -> None:
96 | """
97 | Called after a tox environment has been teared down.
98 |
99 | :param tox_env: the tox environment
100 | """
101 |
102 |
103 | __all__ = [
104 | "NAME",
105 | "tox_add_core_config",
106 | "tox_add_env_config",
107 | "tox_add_option",
108 | "tox_after_run_commands",
109 | "tox_before_run_commands",
110 | "tox_env_teardown",
111 | "tox_on_install",
112 | "tox_register_tox_env",
113 | ]
114 |
--------------------------------------------------------------------------------
/src/tox/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/src/tox/py.typed
--------------------------------------------------------------------------------
/src/tox/run.py:
--------------------------------------------------------------------------------
1 | """Main entry point for tox."""
2 |
3 | from __future__ import annotations
4 |
5 | import faulthandler
6 | import logging
7 | import os
8 | import sys
9 | import time
10 | from typing import Sequence
11 |
12 | from tox.config.cli.parse import get_options
13 | from tox.report import HandledError, ToxHandler
14 | from tox.session.state import State
15 |
16 |
17 | def run(args: Sequence[str] | None = None) -> None:
18 | try:
19 | with ToxHandler.patch_thread():
20 | result = main(sys.argv[1:] if args is None else args)
21 | except Exception as exception:
22 | if isinstance(exception, HandledError):
23 | logging.error("%s| %s", type(exception).__name__, str(exception)) # noqa: TRY400
24 | result = -2
25 | else:
26 | raise
27 | except KeyboardInterrupt:
28 | result = -2
29 | finally:
30 | if "_TOX_SHOW_THREAD" in os.environ: # pragma: no cover
31 | import threading # pragma: no cover # noqa: PLC0415
32 |
33 | for thread in threading.enumerate(): # pragma: no cover
34 | print(thread) # pragma: no cover # noqa: T201
35 | raise SystemExit(result)
36 |
37 |
38 | def main(args: Sequence[str]) -> int:
39 | state = setup_state(args)
40 | from tox.provision import provision # noqa: PLC0415
41 |
42 | result = provision(state)
43 | if result is not False:
44 | return result
45 | handler = state._options.cmd_handlers[state.conf.options.command] # noqa: SLF001
46 | return handler(state)
47 |
48 |
49 | def setup_state(args: Sequence[str]) -> State:
50 | """Setup the state object of this run."""
51 | start = time.monotonic()
52 | # parse CLI arguments
53 | options = get_options(*args)
54 | options.parsed.start = start
55 | if options.parsed.exit_and_dump_after:
56 | faulthandler.dump_traceback_later(timeout=options.parsed.exit_and_dump_after, exit=True) # pragma: no cover
57 | # build tox environment config objects
58 | return State(options, args)
59 |
--------------------------------------------------------------------------------
/src/tox/session/__init__.py:
--------------------------------------------------------------------------------
1 | """Package that handles execution of various commands within tox."""
2 |
3 | from __future__ import annotations
4 |
--------------------------------------------------------------------------------
/src/tox/session/cmd/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/src/tox/session/cmd/__init__.py
--------------------------------------------------------------------------------
/src/tox/session/cmd/depends.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING, cast
4 |
5 | from tox.plugin import impl
6 | from tox.session.cmd.run.common import env_run_create_flags, run_order
7 |
8 | if TYPE_CHECKING:
9 | from tox.config.cli.parser import ToxParser
10 | from tox.session.state import State
11 | from tox.tox_env.runner import RunToxEnv
12 |
13 |
14 | @impl
15 | def tox_add_option(parser: ToxParser) -> None:
16 | our = parser.add_command(
17 | "depends",
18 | ["de"],
19 | "visualize tox environment dependencies",
20 | depends,
21 | )
22 | env_run_create_flags(our, mode="depends")
23 |
24 |
25 | def depends(state: State) -> int:
26 | to_run_list = list(state.envs.iter(only_active=False))
27 | order, todo = run_order(state, to_run_list)
28 | print(f"Execution order: {', '.join(order)}") # noqa: T201
29 |
30 | deps: dict[str, list[str]] = {k: [o for o in order if o in v] for k, v in todo.items()}
31 | deps["ALL"] = to_run_list
32 |
33 | def _handle(at: int, env: str) -> None:
34 | print(" " * at, end="") # noqa: T201
35 | print(env, end="") # noqa: T201
36 | if env != "ALL":
37 | run_env = cast("RunToxEnv", state.envs[env])
38 | packager_list: list[str] = []
39 | try:
40 | for pkg_env in run_env.package_envs:
41 | packager_list.append(pkg_env.name) # noqa: PERF401
42 | except Exception as exception: # noqa: BLE001
43 | packager_list.append(f"... ({exception})")
44 | names = " | ".join(packager_list)
45 | if names:
46 | print(f" ~ {names}", end="") # noqa: T201
47 | print() # noqa: T201
48 | at += 1
49 | for dep in deps[env]:
50 | _handle(at, dep)
51 |
52 | _handle(0, "ALL")
53 | return 0
54 |
--------------------------------------------------------------------------------
/src/tox/session/cmd/devenv.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import logging
4 | from pathlib import Path
5 | from typing import TYPE_CHECKING
6 |
7 | from tox.config.loader.memory import MemoryLoader
8 | from tox.plugin import impl
9 | from tox.report import HandledError
10 | from tox.session.cmd.run.common import env_run_create_flags
11 | from tox.session.cmd.run.sequential import run_sequential
12 | from tox.session.env_select import CliEnv, register_env_select_flags
13 |
14 | if TYPE_CHECKING:
15 | from tox.config.cli.parser import ToxParser
16 | from tox.session.state import State
17 |
18 |
19 | @impl
20 | def tox_add_option(parser: ToxParser) -> None:
21 | help_msg = "sets up a development environment at ENVDIR based on the tox configuration specified "
22 | our = parser.add_command("devenv", ["d"], help_msg, devenv)
23 | our.add_argument("devenv_path", metavar="path", default=Path("venv"), nargs="?", type=Path)
24 | register_env_select_flags(our, default=CliEnv("py"), multiple=False)
25 | env_run_create_flags(our, mode="devenv")
26 |
27 |
28 | def devenv(state: State) -> int:
29 | opt = state.conf.options
30 | opt.devenv_path = opt.devenv_path.absolute()
31 | opt.skip_missing_interpreters = False # the target python must exist
32 | opt.no_test = False # do not run the test suite
33 | opt.package_only = False
34 | opt.install_pkg = None # no explicit packages to install
35 | opt.skip_pkg_install = False # always install a package in this case
36 | opt.no_test = True # do not run the test phase
37 | loader = MemoryLoader( # these configuration values are loaded from in-memory always (no file conf)
38 | usedevelop=True, # dev environments must be of type dev
39 | env_dir=opt.devenv_path, # move it in source
40 | )
41 | state.conf.memory_seed_loaders[next(iter(opt.env))].append(loader)
42 |
43 | state.envs.ensure_only_run_env_is_active()
44 | envs = list(state.envs.iter())
45 | if len(envs) != 1:
46 | msg = f"exactly one target environment allowed in devenv mode but found {', '.join(envs)}"
47 | raise HandledError(msg)
48 | result = run_sequential(state)
49 | if result == 0:
50 | logging.warning("created development environment under %s", opt.devenv_path)
51 | return result
52 |
--------------------------------------------------------------------------------
/src/tox/session/cmd/exec_.py:
--------------------------------------------------------------------------------
1 | """Execute a command in a tox environment."""
2 |
3 | from __future__ import annotations
4 |
5 | from typing import TYPE_CHECKING
6 |
7 | from tox.config.loader.memory import MemoryLoader
8 | from tox.config.types import Command
9 | from tox.plugin import impl
10 | from tox.report import HandledError
11 | from tox.session.cmd.run.common import env_run_create_flags
12 | from tox.session.cmd.run.sequential import run_sequential
13 | from tox.session.env_select import CliEnv, register_env_select_flags
14 |
15 | if TYPE_CHECKING:
16 | from pathlib import Path
17 |
18 | from tox.config.cli.parser import ToxParser
19 | from tox.session.state import State
20 |
21 |
22 | @impl
23 | def tox_add_option(parser: ToxParser) -> None:
24 | our = parser.add_command("exec", ["e"], "execute an arbitrary command within a tox environment", exec_)
25 | our.epilog = "For example: tox exec -e py39 -- python --version"
26 | register_env_select_flags(our, default=CliEnv("py"), multiple=False)
27 | env_run_create_flags(our, mode="exec")
28 |
29 |
30 | def exec_(state: State) -> int:
31 | state.conf.options.skip_pkg_install = True # avoid package install
32 | envs = list(state.envs.iter())
33 | if len(envs) != 1:
34 | msg = f"exactly one target environment allowed in exec mode but found {', '.join(envs)}"
35 | raise HandledError(msg)
36 | loader = MemoryLoader( # these configuration values are loaded from in-memory always (no file conf)
37 | commands_pre=[],
38 | commands=[],
39 | commands_post=[],
40 | )
41 | conf = state.envs[envs[0]].conf
42 | conf.loaders.insert(0, loader)
43 | to_path: Path | None = conf["change_dir"] if conf["args_are_paths"] else None
44 | pos_args = state.conf.pos_args(to_path)
45 | if not pos_args:
46 | msg = "You must specify a command as positional arguments, use -- "
47 | raise HandledError(msg)
48 | loader.raw["commands"] = [Command(list(pos_args))]
49 | return run_sequential(state)
50 |
--------------------------------------------------------------------------------
/src/tox/session/cmd/list_env.py:
--------------------------------------------------------------------------------
1 | """Print available tox environments."""
2 |
3 | from __future__ import annotations
4 |
5 | from itertools import chain
6 | from typing import TYPE_CHECKING
7 |
8 | from tox.plugin import impl
9 | from tox.session.env_select import register_env_select_flags
10 |
11 | if TYPE_CHECKING:
12 | from tox.config.cli.parser import ToxParser
13 | from tox.session.state import State
14 |
15 |
16 | @impl
17 | def tox_add_option(parser: ToxParser) -> None:
18 | our = parser.add_command("list", ["l"], "list environments", list_env)
19 | our.add_argument("--no-desc", action="store_true", help="do not show description", dest="list_no_description")
20 | d = register_env_select_flags(our, default=None, group_only=True)
21 | d.add_argument("-d", action="store_true", help="list just default envs", dest="list_default_only")
22 |
23 |
24 | def list_env(state: State) -> int:
25 | option = state.conf.options
26 | has_group_select = bool(option.factors or option.labels)
27 | active_only = has_group_select or option.list_default_only
28 |
29 | active = dict.fromkeys(state.envs.iter())
30 | inactive = {} if active_only else {env: None for env in state.envs.iter(only_active=False) if env not in active}
31 |
32 | if not has_group_select and not option.list_no_description and active:
33 | print("default environments:") # noqa: T201
34 | max_length = max((len(env) for env in chain(active, inactive)), default=0)
35 |
36 | def report_env(name: str) -> None:
37 | if not option.list_no_description:
38 | tox_env = state.envs[name]
39 | text = tox_env.conf["description"]
40 | if not text.strip():
41 | text = "[no description]"
42 | text = text.replace("\n", " ")
43 | msg = f"{env.ljust(max_length)} -> {text}".strip()
44 | else:
45 | msg = env
46 | print(msg) # noqa: T201
47 |
48 | for env in active:
49 | report_env(env)
50 |
51 | if not has_group_select and not option.list_default_only and inactive:
52 | if not option.list_no_description:
53 | if active: # pragma: no branch
54 | print() # noqa: T201
55 | print("additional environments:") # noqa: T201
56 | for env in inactive:
57 | report_env(env)
58 | return 0
59 |
--------------------------------------------------------------------------------
/src/tox/session/cmd/quickstart.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 | from pathlib import Path
5 | from textwrap import dedent
6 | from typing import TYPE_CHECKING
7 |
8 | from packaging.version import Version
9 |
10 | from tox.plugin import impl
11 | from tox.version import version as __version__
12 |
13 | if TYPE_CHECKING:
14 | from tox.config.cli.parser import ToxParser
15 | from tox.session.state import State
16 |
17 |
18 | @impl
19 | def tox_add_option(parser: ToxParser) -> None:
20 | our = parser.add_command(
21 | "quickstart",
22 | ["q"],
23 | "Command line script to quickly create a tox config file for a Python project",
24 | quickstart,
25 | )
26 | our.add_argument(
27 | "quickstart_root",
28 | metavar="root",
29 | default=Path().absolute(),
30 | nargs="?",
31 | help="folder to create the tox.ini file",
32 | type=Path,
33 | )
34 |
35 |
36 | def quickstart(state: State) -> int:
37 | root = state.conf.options.quickstart_root.absolute()
38 | tox_ini = root / "tox.ini"
39 | if tox_ini.exists():
40 | print(f"{tox_ini} already exist, refusing to overwrite") # noqa: T201
41 | return 1
42 | version = str(Version(__version__.split("+")[0]))
43 | text = f"""
44 | [tox]
45 | env_list =
46 | py{"".join(str(i) for i in sys.version_info[0:2])}
47 | minversion = {version}
48 |
49 | [testenv]
50 | description = run the tests with pytest
51 | package = wheel
52 | wheel_build_env = .pkg
53 | deps =
54 | pytest>=6
55 | commands =
56 | pytest {{tty:--color=yes}} {{posargs}}
57 | """
58 | content = dedent(text).lstrip()
59 |
60 | print(f"tox {__version__} quickstart utility, will create {tox_ini}:") # noqa: T201
61 | print(content, end="") # noqa: T201
62 |
63 | root.mkdir(parents=True, exist_ok=True)
64 | tox_ini.write_text(content)
65 | return 0
66 |
--------------------------------------------------------------------------------
/src/tox/session/cmd/run/__init__.py:
--------------------------------------------------------------------------------
1 | """Defines how we execute a tox environment."""
2 |
3 | from __future__ import annotations
4 |
--------------------------------------------------------------------------------
/src/tox/session/cmd/run/parallel.py:
--------------------------------------------------------------------------------
1 | """Run tox environments in parallel."""
2 |
3 | from __future__ import annotations
4 |
5 | import logging
6 | from argparse import ArgumentParser, ArgumentTypeError
7 | from typing import TYPE_CHECKING
8 |
9 | from tox.plugin import impl
10 | from tox.session.env_select import CliEnv, register_env_select_flags
11 | from tox.util.ci import is_ci
12 | from tox.util.cpu import auto_detect_cpus
13 |
14 | from .common import env_run_create_flags, execute
15 |
16 | if TYPE_CHECKING:
17 | from tox.config.cli.parser import ToxParser
18 | from tox.session.state import State
19 |
20 | logger = logging.getLogger(__name__)
21 |
22 | ENV_VAR_KEY = "TOX_PARALLEL_ENV"
23 | OFF_VALUE = 0
24 | DEFAULT_PARALLEL = "auto"
25 |
26 |
27 | @impl
28 | def tox_add_option(parser: ToxParser) -> None:
29 | our = parser.add_command("run-parallel", ["p"], "run environments in parallel", run_parallel)
30 | register_env_select_flags(our, default=CliEnv())
31 | env_run_create_flags(our, mode="run-parallel")
32 | parallel_flags(our, default_parallel=DEFAULT_PARALLEL, default_spinner=is_ci())
33 |
34 |
35 | def parse_num_processes(str_value: str) -> int | None:
36 | if str_value == "all":
37 | return None
38 | if str_value == "auto":
39 | return auto_detect_cpus()
40 | try:
41 | value = int(str_value)
42 | except ValueError as exc:
43 | msg = f"value must be a positive number, is {str_value!r}"
44 | raise ArgumentTypeError(msg) from exc
45 | if value < 0:
46 | msg = f"value must be positive, is {value!r}"
47 | raise ArgumentTypeError(msg)
48 | return value
49 |
50 |
51 | def parallel_flags(
52 | our: ArgumentParser,
53 | default_parallel: int | str,
54 | no_args: bool = False, # noqa: FBT001, FBT002
55 | *,
56 | default_spinner: bool = False,
57 | ) -> None:
58 | our.add_argument(
59 | "-p",
60 | "--parallel",
61 | dest="parallel",
62 | help="run tox environments in parallel, the argument controls limit: all,"
63 | " auto - cpu count, some positive number, zero is turn off",
64 | action="store",
65 | type=parse_num_processes,
66 | default=default_parallel,
67 | metavar="VAL",
68 | **({"nargs": "?"} if no_args else {}), # type: ignore[arg-type] # type checker can't unroll it
69 | )
70 | our.add_argument(
71 | "-o",
72 | "--parallel-live",
73 | action="store_true",
74 | dest="parallel_live",
75 | help="connect to stdout while running environments",
76 | )
77 | our.add_argument(
78 | "--parallel-no-spinner",
79 | action="store_true",
80 | dest="parallel_no_spinner",
81 | default=default_spinner,
82 | help=(
83 | "run tox environments in parallel, but don't show the spinner, implies --parallel. "
84 | "Disabled by default if CI is detected (not in legacy API)."
85 | ),
86 | )
87 |
88 |
89 | def run_parallel(state: State) -> int:
90 | """Here we'll just start parallel sub-processes."""
91 | option = state.conf.options
92 | return execute(
93 | state,
94 | max_workers=auto_detect_cpus() if option.parallel == 0 else option.parallel,
95 | has_spinner=option.parallel_no_spinner is False and option.parallel_live is False,
96 | live=option.parallel_live,
97 | )
98 |
--------------------------------------------------------------------------------
/src/tox/session/cmd/run/sequential.py:
--------------------------------------------------------------------------------
1 | """Run tox environments in sequential order."""
2 |
3 | from __future__ import annotations
4 |
5 | from typing import TYPE_CHECKING
6 |
7 | from tox.plugin import impl
8 | from tox.session.env_select import CliEnv, register_env_select_flags
9 |
10 | from .common import env_run_create_flags, execute
11 |
12 | if TYPE_CHECKING:
13 | from tox.config.cli.parser import ToxParser
14 | from tox.session.state import State
15 |
16 |
17 | @impl
18 | def tox_add_option(parser: ToxParser) -> None:
19 | our = parser.add_command("run", ["r"], "run environments", run_sequential)
20 | register_env_select_flags(our, default=CliEnv())
21 | env_run_create_flags(our, mode="run")
22 |
23 |
24 | def run_sequential(state: State) -> int:
25 | return execute(state, max_workers=1, has_spinner=False, live=True)
26 |
--------------------------------------------------------------------------------
/src/tox/session/cmd/version_flag.py:
--------------------------------------------------------------------------------
1 | """Display the version information about tox."""
2 |
3 | from __future__ import annotations
4 |
5 | import sys
6 | from argparse import SUPPRESS, Action, ArgumentParser, Namespace
7 | from pathlib import Path
8 | from typing import TYPE_CHECKING, Any, Sequence, cast
9 |
10 | import tox
11 | from tox.plugin import impl
12 | from tox.plugin.manager import MANAGER
13 | from tox.version import version
14 |
15 | if TYPE_CHECKING:
16 | from tox.config.cli.parser import HelpFormatter, ToxParser
17 |
18 |
19 | @impl
20 | def tox_add_option(parser: ToxParser) -> None:
21 | class _V(Action):
22 | def __init__(self, option_strings: Sequence[str], dest: str = SUPPRESS) -> None:
23 | help_msg = "show program's and plugins version number and exit"
24 | super().__init__(option_strings=option_strings, dest=dest, nargs=0, help=help_msg, default=SUPPRESS)
25 |
26 | def __call__(
27 | self,
28 | parser: ArgumentParser,
29 | namespace: Namespace, # noqa: ARG002
30 | values: str | Sequence[Any] | None, # noqa: ARG002
31 | option_string: str | None = None, # noqa: ARG002
32 | ) -> None:
33 | formatter = cast("HelpFormatter", parser._get_formatter()) # noqa: SLF001
34 | formatter.add_raw_text(get_version_info())
35 | parser._print_message(formatter.format_help(), sys.stdout) # noqa: SLF001
36 | parser.exit()
37 |
38 | parser.add_argument("--version", action=_V)
39 |
40 |
41 | def get_version_info() -> str:
42 | out = [f"{version} from {Path(tox.__file__).absolute()}"]
43 | plugin_info = MANAGER.manager.list_plugin_distinfo()
44 | if plugin_info:
45 | out.append("registered plugins:")
46 | for module, egg_info in plugin_info:
47 | source = getattr(module, "__file__", repr(module))
48 | info = module.tox_append_version_info() if hasattr(module, "tox_append_version_info") else ""
49 | with_info = f" {info}" if info else ""
50 | out.append(f" {egg_info.project_name}-{egg_info.version} at {source}{with_info}")
51 | return "\n".join(out)
52 |
--------------------------------------------------------------------------------
/src/tox/session/state.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 | from typing import TYPE_CHECKING, Sequence
5 |
6 | from tox.config.main import Config
7 | from tox.journal import Journal
8 | from tox.plugin import impl
9 |
10 | from .env_select import EnvSelector
11 |
12 | if TYPE_CHECKING:
13 | from tox.config.cli.parse import Options
14 | from tox.config.cli.parser import ToxParser
15 |
16 |
17 | class State:
18 | """Runtime state holder."""
19 |
20 | def __init__(self, options: Options, args: Sequence[str]) -> None:
21 | self.conf = Config.make(options.parsed, options.pos_args, options.source)
22 | self.conf.core.add_constant(
23 | keys=["on_platform"],
24 | desc="platform we are running on",
25 | value=sys.platform,
26 | )
27 | self._options = options
28 | self.args = args
29 | self._journal: Journal = Journal(getattr(options.parsed, "result_json", None) is not None)
30 | self._selector: EnvSelector | None = None
31 |
32 | @property
33 | def envs(self) -> EnvSelector:
34 | """:return: provides access to the tox environments"""
35 | if self._selector is None:
36 | self._selector = EnvSelector(self)
37 | return self._selector
38 |
39 |
40 | @impl
41 | def tox_add_option(parser: ToxParser) -> None:
42 | from tox.tox_env.register import REGISTER # noqa: PLC0415
43 |
44 | parser.add_argument(
45 | "--runner",
46 | dest="default_runner",
47 | help="the tox run engine to use when not explicitly stated in tox env configuration",
48 | default=REGISTER.default_env_runner,
49 | choices=list(REGISTER.env_runners),
50 | )
51 |
--------------------------------------------------------------------------------
/src/tox/tox_env/__init__.py:
--------------------------------------------------------------------------------
1 | """Package handling the creation and management of tox environments."""
2 |
3 | from __future__ import annotations
4 |
--------------------------------------------------------------------------------
/src/tox/tox_env/errors.py:
--------------------------------------------------------------------------------
1 | """Defines tox error types."""
2 |
3 | from __future__ import annotations
4 |
5 |
6 | class Recreate(Exception): # noqa: N818
7 | """Recreate the tox environment."""
8 |
9 |
10 | class Skip(Exception): # noqa: N818
11 | """Skip this tox environment."""
12 |
13 |
14 | class Fail(Exception): # noqa: N818
15 | """Failed creating env."""
16 |
--------------------------------------------------------------------------------
/src/tox/tox_env/info.py:
--------------------------------------------------------------------------------
1 | """
2 | Declare and handle the tox env info file (a file at the root of every tox environment that contains information about
3 | the status of the tox environment - python version of the environment, installed packages, etc.).
4 | """
5 |
6 | from __future__ import annotations
7 |
8 | import json
9 | from contextlib import contextmanager
10 | from typing import TYPE_CHECKING, Any, Iterator
11 |
12 | if TYPE_CHECKING:
13 | from pathlib import Path
14 |
15 |
16 | class Info:
17 | """Stores metadata about the tox environment."""
18 |
19 | def __init__(self, path: Path) -> None:
20 | self._path = path / ".tox-info.json"
21 | try:
22 | value = json.loads(self._path.read_text())
23 | except (ValueError, OSError):
24 | value = {}
25 | self._content = value
26 |
27 | def __repr__(self) -> str:
28 | return f"{self.__class__.__name__}(path={self._path})"
29 |
30 | @contextmanager
31 | def compare(
32 | self,
33 | value: Any,
34 | section: str,
35 | sub_section: str | None = None,
36 | ) -> Iterator[tuple[bool, Any | None]]:
37 | """
38 | Compare new information with the existing one and update if differs.
39 |
40 | :param value: the value stored
41 | :param section: the primary key of the information
42 | :param sub_section: the secondary key of the information
43 | :return: a tuple where the first value is if it differs and the second is the old value
44 | """
45 | old = self._content.get(section)
46 | if sub_section is not None and old is not None:
47 | old = old.get(sub_section)
48 |
49 | if old == value:
50 | yield True, old
51 | else:
52 | yield False, old
53 | # if no exception thrown update
54 | if sub_section is None:
55 | self._content[section] = value
56 | elif self._content.get(section) is None:
57 | self._content[section] = {sub_section: value}
58 | else:
59 | self._content[section][sub_section] = value
60 | self._write()
61 |
62 | def reset(self) -> None:
63 | self._content = {}
64 |
65 | def _write(self) -> None:
66 | self._path.parent.mkdir(parents=True, exist_ok=True)
67 | self._path.write_text(json.dumps(self._content, indent=2))
68 |
69 |
70 | __all__ = ("Info",)
71 |
--------------------------------------------------------------------------------
/src/tox/tox_env/installer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from abc import ABC, abstractmethod
4 | from typing import TYPE_CHECKING, Any, Generic, TypeVar
5 |
6 | if TYPE_CHECKING:
7 | from tox.tox_env.api import ToxEnv
8 |
9 | T = TypeVar("T", bound="ToxEnv")
10 |
11 |
12 | class Installer(ABC, Generic[T]):
13 | def __init__(self, tox_env: T) -> None:
14 | self._env = tox_env
15 | self._register_config()
16 |
17 | @abstractmethod
18 | def _register_config(self) -> None:
19 | """Register configurations for the installer."""
20 | raise NotImplementedError
21 |
22 | @abstractmethod
23 | def installed(self) -> Any:
24 | """:returns: a list of packages installed (JSON dump-able)"""
25 | raise NotImplementedError
26 |
27 | @abstractmethod
28 | def install(self, arguments: Any, section: str, of_type: str) -> None:
29 | raise NotImplementedError
30 |
--------------------------------------------------------------------------------
/src/tox/tox_env/python/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/src/tox/tox_env/python/__init__.py
--------------------------------------------------------------------------------
/src/tox/tox_env/python/dependency_groups.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 | from typing import TYPE_CHECKING, TypedDict
5 |
6 | from packaging.requirements import InvalidRequirement, Requirement
7 | from packaging.utils import canonicalize_name
8 |
9 | from tox.tox_env.errors import Fail
10 |
11 | if TYPE_CHECKING:
12 | from pathlib import Path
13 |
14 |
15 | if sys.version_info >= (3, 11): # pragma: no cover (py311+)
16 | import tomllib
17 | else: # pragma: no cover (py311+)
18 | import tomli as tomllib
19 |
20 | _IncludeGroup = TypedDict("_IncludeGroup", {"include-group": str})
21 |
22 |
23 | def resolve(root: Path, groups: set[str]) -> set[Requirement]:
24 | pyproject_file = root / "pyproject.toml"
25 | if not pyproject_file.exists(): # check if it's static PEP-621 metadata
26 | return set()
27 | with pyproject_file.open("rb") as file_handler:
28 | pyproject = tomllib.load(file_handler)
29 | dependency_groups = pyproject["dependency-groups"]
30 | if not isinstance(dependency_groups, dict):
31 | msg = f"dependency-groups is {type(dependency_groups).__name__} instead of table"
32 | raise Fail(msg)
33 | result: set[Requirement] = set()
34 | for group in groups:
35 | result = result.union(_resolve_dependency_group(dependency_groups, group))
36 | return result
37 |
38 |
39 | def _resolve_dependency_group(
40 | dependency_groups: dict[str, list[str] | _IncludeGroup], group: str, past_groups: tuple[str, ...] = ()
41 | ) -> set[Requirement]:
42 | if group in past_groups:
43 | msg = f"Cyclic dependency group include: {group!r} -> {past_groups!r}"
44 | raise Fail(msg)
45 | if group not in dependency_groups:
46 | msg = f"dependency group {group!r} not found"
47 | raise Fail(msg)
48 | raw_group = dependency_groups[group]
49 | if not isinstance(raw_group, list):
50 | msg = f"dependency group {group!r} is not a list"
51 | raise Fail(msg)
52 |
53 | result = set()
54 | for item in raw_group:
55 | if isinstance(item, str):
56 | # packaging.requirements.Requirement parsing ensures that this is a valid
57 | # PEP 508 Dependency Specifier
58 | # raises InvalidRequirement on failure
59 | try:
60 | result.add(Requirement(item))
61 | except InvalidRequirement as exc:
62 | msg = f"{item!r} is not valid requirement due to {exc}"
63 | raise Fail(msg) from exc
64 | elif isinstance(item, dict) and tuple(item.keys()) == ("include-group",):
65 | include_group = canonicalize_name(next(iter(item.values())))
66 | result = result.union(_resolve_dependency_group(dependency_groups, include_group, (*past_groups, group)))
67 | else:
68 | msg = f"invalid dependency group item: {item!r}"
69 | raise Fail(msg)
70 | return result
71 |
72 |
73 | __all__ = [
74 | "resolve",
75 | ]
76 |
--------------------------------------------------------------------------------
/src/tox/tox_env/python/pip/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/src/tox/tox_env/python/pip/__init__.py
--------------------------------------------------------------------------------
/src/tox/tox_env/python/pip/req/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Specification is defined within pip itself and documented under:
3 | - https://pip.pypa.io/en/stable/reference/pip_install/#requirements-file-format
4 | - https://github.com/pypa/pip/blob/master/src/pip/_internal/req/constructors.py#L291.
5 | """
6 |
7 | from __future__ import annotations
8 |
--------------------------------------------------------------------------------
/src/tox/tox_env/python/pip/req/util.py:
--------------------------------------------------------------------------------
1 | """Borrowed from the pip code base."""
2 |
3 | from __future__ import annotations
4 |
5 | from urllib.parse import urlsplit
6 | from urllib.request import url2pathname
7 |
8 | from packaging.utils import canonicalize_name
9 |
10 | VCS = ["ftp", "ssh", "git", "hg", "bzr", "sftp", "svn"]
11 | VALID_SCHEMAS = ["http", "https", "file", *VCS]
12 |
13 |
14 | def is_url(name: str) -> bool:
15 | return get_url_scheme(name) in VALID_SCHEMAS
16 |
17 |
18 | def get_url_scheme(url: str) -> str | None:
19 | if ":" not in url:
20 | return None
21 | return url.split(":", 1)[0].lower()
22 |
23 |
24 | def url_to_path(url: str) -> str:
25 | _, netloc, path, _, _ = urlsplit(url)
26 | if not netloc or netloc == "localhost": # According to RFC 8089, same as empty authority.
27 | netloc = ""
28 | else:
29 | msg = f"non-local file URIs are not supported on this platform: {url!r}"
30 | raise ValueError(msg)
31 | return url2pathname(netloc + path)
32 |
33 |
34 | def handle_binary_option(value: str, target: set[str], other: set[str]) -> None:
35 | new = value.split(",")
36 | while ":all:" in new:
37 | other.clear()
38 | target.clear()
39 | target.add(":all:")
40 | del new[: new.index(":all:") + 1]
41 | if ":none:" not in new:
42 | return
43 | for name in new:
44 | if name == ":none:":
45 | target.clear()
46 | continue
47 | normalized_name = canonicalize_name(name)
48 | other.discard(normalized_name)
49 | target.add(normalized_name)
50 |
--------------------------------------------------------------------------------
/src/tox/tox_env/python/virtual_env/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/src/tox/tox_env/python/virtual_env/__init__.py
--------------------------------------------------------------------------------
/src/tox/tox_env/python/virtual_env/package/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/src/tox/tox_env/python/virtual_env/package/__init__.py
--------------------------------------------------------------------------------
/src/tox/tox_env/python/virtual_env/package/util.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from copy import deepcopy
4 | from typing import TYPE_CHECKING, Optional, Set, cast
5 |
6 | if TYPE_CHECKING:
7 | from packaging._parser import Op, Variable
8 | from packaging.markers import Marker
9 | from packaging.requirements import Requirement
10 |
11 |
12 | def dependencies_with_extras(deps: list[Requirement], extras: set[str], package_name: str) -> list[Requirement]:
13 | return dependencies_with_extras_from_markers(extract_extra_markers(deps), extras, package_name)
14 |
15 |
16 | def dependencies_with_extras_from_markers(
17 | deps_with_markers: list[tuple[Requirement, set[str | None]]],
18 | extras: set[str],
19 | package_name: str,
20 | ) -> list[Requirement]:
21 | result: list[Requirement] = []
22 | found: set[str] = set()
23 | todo: set[str | None] = extras | {None}
24 | visited: set[str | None] = set()
25 | while todo:
26 | new_extras: set[str | None] = set()
27 | for req, extra_markers in deps_with_markers:
28 | if todo & extra_markers:
29 | if req.name == package_name: # support for recursive extras
30 | new_extras.update(req.extras or set())
31 | else:
32 | req_str = str(req)
33 | if req_str not in found:
34 | found.add(req_str)
35 | result.append(req)
36 | visited.update(todo)
37 | todo = new_extras - visited
38 | return result
39 |
40 |
41 | def extract_extra_markers(deps: list[Requirement]) -> list[tuple[Requirement, set[str | None]]]:
42 | """
43 | Extract extra markers from dependencies.
44 |
45 | :param deps: the dependencies
46 | :return: a list of requirement, extras set
47 | """
48 | return [_extract_extra_markers(d) for d in deps]
49 |
50 |
51 | def _extract_extra_markers(req: Requirement) -> tuple[Requirement, set[str | None]]:
52 | req = deepcopy(req)
53 | markers: list[str | tuple[Variable, Op, Variable]] = getattr(req.marker, "_markers", []) or []
54 | new_markers: list[str | tuple[Variable, Op, Variable]] = []
55 | extra_markers: set[str] = set() # markers that have a key of extra
56 | marker = markers.pop(0) if markers else None
57 | while marker:
58 | extra = _get_extra(marker)
59 | if extra is not None:
60 | extra_markers.add(extra)
61 | if new_markers and new_markers[-1] in {"and", "or"}:
62 | del new_markers[-1]
63 | marker = markers.pop(0) if markers else None
64 | if marker in {"and", "or"}:
65 | marker = markers.pop(0) if markers else None
66 | else:
67 | new_markers.append(marker)
68 | marker = markers.pop(0) if markers else None
69 | if new_markers:
70 | cast("Marker", req.marker)._markers = new_markers # noqa: SLF001
71 | else:
72 | req.marker = None
73 | return req, cast("Set[Optional[str]]", extra_markers) or {None}
74 |
75 |
76 | def _get_extra(_marker: str | tuple[Variable, Op, Variable]) -> str | None:
77 | if (
78 | isinstance(_marker, tuple)
79 | and len(_marker) == 3 # noqa: PLR2004
80 | and _marker[0].value == "extra"
81 | and _marker[1].value == "=="
82 | ):
83 | return _marker[2].value
84 | return None
85 |
--------------------------------------------------------------------------------
/src/tox/tox_env/python/virtual_env/runner.py:
--------------------------------------------------------------------------------
1 | """A tox python environment runner that uses the virtualenv project."""
2 |
3 | from __future__ import annotations
4 |
5 | from typing import TYPE_CHECKING
6 |
7 | from tox.plugin import impl
8 | from tox.tox_env.python.runner import PythonRun
9 |
10 | from .api import VirtualEnv
11 |
12 | if TYPE_CHECKING:
13 | from pathlib import Path
14 |
15 | from tox.tox_env.register import ToxEnvRegister
16 |
17 |
18 | class VirtualEnvRunner(VirtualEnv, PythonRun):
19 | """local file system python virtual environment via the virtualenv package."""
20 |
21 | @staticmethod
22 | def id() -> str:
23 | return "virtualenv"
24 |
25 | @property
26 | def _package_tox_env_type(self) -> str:
27 | return "virtualenv-pep-517"
28 |
29 | @property
30 | def _external_pkg_tox_env_type(self) -> str:
31 | return "virtualenv-cmd-builder"
32 |
33 | @property
34 | def default_pkg_type(self) -> str:
35 | tox_root: Path = self.core["tox_root"]
36 | if not (any((tox_root / i).exists() for i in ("pyproject.toml", "setup.py", "setup.cfg"))):
37 | return "skip"
38 | return super().default_pkg_type
39 |
40 |
41 | @impl
42 | def tox_register_tox_env(register: ToxEnvRegister) -> None:
43 | register.add_run_env(VirtualEnvRunner)
44 |
--------------------------------------------------------------------------------
/src/tox/tox_env/register.py:
--------------------------------------------------------------------------------
1 | """Manages the tox environment registry."""
2 |
3 | from __future__ import annotations
4 |
5 | from typing import TYPE_CHECKING, Iterable
6 |
7 | if TYPE_CHECKING:
8 | from tox.plugin.manager import Plugin
9 |
10 | from .package import PackageToxEnv
11 | from .runner import RunToxEnv
12 |
13 |
14 | class ToxEnvRegister:
15 | """tox environment registry."""
16 |
17 | def __init__(self) -> None:
18 | self._run_envs: dict[str, type[RunToxEnv]] = {}
19 | self._package_envs: dict[str, type[PackageToxEnv]] = {}
20 | self._default_run_env: str = ""
21 |
22 | def _register_tox_env_types(self, manager: Plugin) -> None:
23 | manager.tox_register_tox_env(register=self)
24 |
25 | def add_run_env(self, of_type: type[RunToxEnv]) -> None:
26 | """
27 | Define a new run tox environment type.
28 |
29 | :param of_type: the new run environment type
30 | """
31 | self._run_envs[of_type.id()] = of_type
32 |
33 | def add_package_env(self, of_type: type[PackageToxEnv]) -> None:
34 | """
35 | Define a new packaging tox environment type.
36 |
37 | :param of_type: the new packaging environment type
38 | """
39 | self._package_envs[of_type.id()] = of_type
40 |
41 | @property
42 | def env_runners(self) -> Iterable[str]:
43 | """:returns: run environment types currently defined"""
44 | return self._run_envs.keys()
45 |
46 | @property
47 | def default_env_runner(self) -> str:
48 | """:returns: the default run environment type"""
49 | if not self._default_run_env and self._run_envs:
50 | self._default_run_env = next(iter(self._run_envs.keys()))
51 | return self._default_run_env
52 |
53 | @default_env_runner.setter
54 | def default_env_runner(self, value: str) -> None:
55 | """
56 | Change the default run environment type.
57 |
58 | :param value: the new run environment type by name
59 | """
60 | if value not in self._run_envs:
61 | msg = "run env must be registered before setting it as default"
62 | raise ValueError(msg)
63 | self._default_run_env = value
64 |
65 | def runner(self, name: str) -> type[RunToxEnv]:
66 | """
67 | Lookup a run tox environment type by name.
68 |
69 | :param name: the name of the runner type
70 | :return: the type of the runner type
71 | """
72 | return self._run_envs[name]
73 |
74 | def package(self, name: str) -> type[PackageToxEnv]:
75 | """
76 | Lookup a packaging tox environment type by name.
77 |
78 | :param name: the name of the packaging type
79 | :return: the type of the packaging type
80 | """
81 | return self._package_envs[name]
82 |
83 |
84 | REGISTER = ToxEnvRegister() #: the tox register
85 |
86 | __all__ = (
87 | "REGISTER",
88 | "ToxEnvRegister",
89 | )
90 |
--------------------------------------------------------------------------------
/src/tox/tox_env/util.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from pathlib import Path
4 | from typing import TYPE_CHECKING, cast
5 |
6 | if TYPE_CHECKING:
7 | from tox.config.sets import CoreConfigSet, EnvConfigSet
8 |
9 |
10 | def add_change_dir_conf(config: EnvConfigSet, core: CoreConfigSet) -> None:
11 | def _post_process_change_dir(value: Path) -> Path:
12 | if not value.is_absolute():
13 | value = (core["tox_root"] / value).resolve()
14 | return value
15 |
16 | config.add_config(
17 | keys=["change_dir", "changedir"],
18 | of_type=Path,
19 | default=lambda conf, name: cast("Path", conf.core["tox_root"]), # noqa: ARG005
20 | desc="change to this working directory when executing the test command",
21 | post_process=_post_process_change_dir,
22 | )
23 |
24 |
25 | __all__ = [
26 | "add_change_dir_conf",
27 | ]
28 |
--------------------------------------------------------------------------------
/src/tox/util/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/src/tox/util/__init__.py
--------------------------------------------------------------------------------
/src/tox/util/ci.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import os
4 |
5 | _ENV_VARS = { # per https://adamj.eu/tech/2020/03/09/detect-if-your-tests-are-running-on-ci
6 | "CI": None, # generic flag
7 | "TF_BUILD": "true", # Azure Pipelines
8 | "bamboo.buildKey": None, # Bamboo
9 | "BUILDKITE": "true", # Buildkite
10 | "CIRCLECI": "true", # Circle CI
11 | "CIRRUS_CI": "true", # Cirrus CI
12 | "CODEBUILD_BUILD_ID": None, # CodeBuild
13 | "GITHUB_ACTIONS": "true", # GitHub Actions
14 | "GITLAB_CI": None, # GitLab CI
15 | "HEROKU_TEST_RUN_ID": None, # Heroku CI
16 | "BUILD_ID": None, # Hudson
17 | "TEAMCITY_VERSION": None, # TeamCity
18 | "TRAVIS": "true", # Travis CI
19 | }
20 |
21 |
22 | def is_ci() -> bool:
23 | """:return: a flag indicating if running inside a CI env or not"""
24 | for env_key, value in _ENV_VARS.items():
25 | if env_key in os.environ if value is None else os.environ.get(env_key) == value:
26 | if env_key == "TEAMCITY_VERSION" and os.environ.get(env_key) == "LOCAL":
27 | continue
28 | return True
29 | return False
30 |
31 |
32 | __all__ = [
33 | "is_ci",
34 | ]
35 |
--------------------------------------------------------------------------------
/src/tox/util/cpu.py:
--------------------------------------------------------------------------------
1 | """Helper methods related to the CPU."""
2 |
3 | from __future__ import annotations
4 |
5 | import multiprocessing
6 |
7 |
8 | def auto_detect_cpus() -> int:
9 | try:
10 | n: int | None = multiprocessing.cpu_count()
11 | except NotImplementedError:
12 | n = None
13 | return n or 1
14 |
15 |
16 | __all__ = ("auto_detect_cpus",)
17 |
--------------------------------------------------------------------------------
/src/tox/util/file_view.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import logging
4 | import os
5 | import shutil
6 | from itertools import chain
7 | from os.path import commonpath
8 | from typing import TYPE_CHECKING
9 |
10 | if TYPE_CHECKING:
11 | from pathlib import Path
12 |
13 |
14 | def create_session_view(package: Path, temp_path: Path) -> Path:
15 | """Allows using the file after you no longer holding a lock to it by moving it into a temp folder."""
16 | # we'll number the active instances, and use the max value as session folder for a new build
17 | # note we cannot change package names as PEP-491 (wheel binary format)
18 | # is strict about file name structure
19 |
20 | temp_path.mkdir(parents=True, exist_ok=True)
21 | exists = [i.name for i in temp_path.iterdir()]
22 | file_id = max(chain((0,), (int(i) for i in exists if str(i).isnumeric())))
23 | session_dir = temp_path / str(file_id + 1)
24 | session_dir.mkdir()
25 | session_package = session_dir / package.name
26 |
27 | links = False # if we can do hard links do that, otherwise just copy
28 | if hasattr(os, "link"):
29 | try:
30 | os.link(package, session_package)
31 | links = True
32 | except (OSError, NotImplementedError):
33 | pass
34 | if not links:
35 | shutil.copyfile(package, session_package)
36 | operation = "links" if links else "copied"
37 | common = commonpath((session_package, package))
38 | rel_session, rel_package = session_package.relative_to(common), package.relative_to(common)
39 | logging.debug("package %s %s to %s (%s)", rel_session, operation, rel_package, common)
40 | return session_package
41 |
--------------------------------------------------------------------------------
/src/tox/util/graph.py:
--------------------------------------------------------------------------------
1 | """Helper methods related to graph theory."""
2 |
3 | from __future__ import annotations
4 |
5 | from collections import OrderedDict, defaultdict
6 |
7 |
8 | def stable_topological_sort(graph: dict[str, set[str]]) -> list[str]: # noqa: C901
9 | to_order = set(graph.keys()) # keep a log of what we need to order
10 |
11 | # normalize graph - fill missing nodes (assume no dependency)
12 | for values in list(graph.values()):
13 | for value in values:
14 | if value not in graph:
15 | graph[value] = set()
16 |
17 | inverse_graph = defaultdict(set)
18 | for key, depends in graph.items():
19 | for depend in depends:
20 | inverse_graph[depend].add(key)
21 |
22 | topology = []
23 | degree = {k: len(v) for k, v in graph.items()}
24 | ready_to_visit = {n for n, d in degree.items() if not d}
25 | need_to_visit = OrderedDict((i, None) for i in graph)
26 | while need_to_visit:
27 | # to keep stable, pick the first node ready to visit in the original order
28 | for node in need_to_visit:
29 | if node in ready_to_visit:
30 | break
31 | else:
32 | break
33 | del need_to_visit[node]
34 |
35 | topology.append(node)
36 |
37 | # decrease degree for nodes we're going too
38 | for to_node in inverse_graph[node]:
39 | degree[to_node] -= 1
40 | if not degree[to_node]: # if a node has no more incoming node it's ready to visit
41 | ready_to_visit.add(to_node)
42 |
43 | result = [n for n in topology if n in to_order] # filter out missing nodes we extended
44 |
45 | if len(result) < len(to_order):
46 | identify_cycle(graph)
47 | msg = "could not order tox environments and failed to detect circle" # pragma: no cover
48 | raise ValueError(msg) # pragma: no cover
49 | return result
50 |
51 |
52 | def identify_cycle(graph: dict[str, set[str]]) -> None:
53 | path: dict[str, None] = OrderedDict()
54 | visited = set()
55 |
56 | def visit(vertex: str) -> dict[str, None] | None:
57 | if vertex in visited:
58 | return None
59 | visited.add(vertex)
60 | path[vertex] = None
61 | for neighbor in graph.get(vertex, ()):
62 | if neighbor in path or visit(neighbor):
63 | return path
64 | del path[vertex]
65 | return None
66 |
67 | for node in graph: # pragma: no branch # we never get here if the graph is empty
68 | result = visit(node)
69 | if result is not None:
70 | msg = f"{' | '.join(result.keys())}"
71 | raise ValueError(msg)
72 |
--------------------------------------------------------------------------------
/src/tox/util/path.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from shutil import rmtree
4 | from typing import TYPE_CHECKING
5 |
6 | if TYPE_CHECKING:
7 | from pathlib import Path
8 |
9 |
10 | def ensure_empty_dir(path: Path, except_filename: str | None = None) -> None:
11 | if path.exists():
12 | if path.is_dir():
13 | for sub_path in path.iterdir():
14 | if sub_path.name == except_filename:
15 | continue
16 | if sub_path.is_dir():
17 | rmtree(sub_path, ignore_errors=True)
18 | else:
19 | sub_path.unlink()
20 | else:
21 | path.unlink()
22 | path.mkdir()
23 | else:
24 | path.mkdir(parents=True)
25 |
26 |
27 | __all__ = [
28 | "ensure_empty_dir",
29 | ]
30 |
--------------------------------------------------------------------------------
/tasks/release.py:
--------------------------------------------------------------------------------
1 | """Handles creating a release PR."""
2 |
3 | from __future__ import annotations
4 |
5 | from pathlib import Path
6 | from subprocess import check_call
7 |
8 | from git import Commit, Head, Remote, Repo, TagReference
9 | from packaging.version import Version
10 |
11 | ROOT_SRC_DIR = Path(__file__).parents[1]
12 |
13 |
14 | def main(version_str: str) -> None:
15 | version = Version(version_str)
16 | repo = Repo(str(ROOT_SRC_DIR))
17 |
18 | if repo.is_dirty():
19 | msg = "Current repository is dirty. Please commit any changes and try again."
20 | raise RuntimeError(msg)
21 | upstream, release_branch = create_release_branch(repo, version)
22 | try:
23 | release_commit = release_changelog(repo, version)
24 | tag = tag_release_commit(release_commit, repo, version)
25 | print("push release commit") # noqa: T201
26 | repo.git.push(upstream.name, f"{release_branch}:main", "-f")
27 | print("push release tag") # noqa: T201
28 | repo.git.push(upstream.name, tag, "-f")
29 | finally:
30 | print("checkout main to new release and delete release branch") # noqa: T201
31 | repo.heads.main.checkout()
32 | repo.delete_head(release_branch, force=True)
33 | upstream.fetch()
34 | repo.git.reset("--hard", "upstream/main")
35 | print("All done! ✨ 🍰 ✨") # noqa: T201
36 |
37 |
38 | def create_release_branch(repo: Repo, version: Version) -> tuple[Remote, Head]:
39 | print("create release branch from upstream main") # noqa: T201
40 | upstream = get_upstream(repo)
41 | upstream.fetch()
42 | branch_name = f"release-{version}"
43 | release_branch = repo.create_head(branch_name, upstream.refs.main, force=True)
44 | upstream.push(refspec=f"{branch_name}:{branch_name}", force=True)
45 | release_branch.set_tracking_branch(repo.refs[f"{upstream.name}/{branch_name}"])
46 | release_branch.checkout()
47 | return upstream, release_branch
48 |
49 |
50 | def get_upstream(repo: Repo) -> Remote:
51 | for remote in repo.remotes:
52 | if any(url.endswith("tox-dev/tox.git") for url in remote.urls):
53 | return remote
54 | msg = "could not find tox-dev/tox.git remote"
55 | raise RuntimeError(msg)
56 |
57 |
58 | def release_changelog(repo: Repo, version: Version) -> Commit:
59 | print("generate release commit") # noqa: T201
60 | check_call(["towncrier", "build", "--yes", "--version", version.public], cwd=str(ROOT_SRC_DIR)) # noqa: S607
61 | return repo.index.commit(f"release {version}")
62 |
63 |
64 | def tag_release_commit(release_commit: Commit, repo: Repo, version: Version) -> TagReference:
65 | print("tag release commit") # noqa: T201
66 | existing_tags = [x.name for x in repo.tags]
67 | if version in existing_tags:
68 | print(f"delete existing tag {version}") # noqa: T201
69 | repo.delete_tag(version)
70 | print(f"create tag {version}") # noqa: T201
71 | return repo.create_tag(version, ref=release_commit, force=True)
72 |
73 |
74 | if __name__ == "__main__":
75 | import argparse
76 |
77 | parser = argparse.ArgumentParser(prog="release")
78 | parser.add_argument("--version", required=True)
79 | options = parser.parse_args()
80 | main(options.version)
81 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/tests/__init__.py
--------------------------------------------------------------------------------
/tests/config/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/tests/config/__init__.py
--------------------------------------------------------------------------------
/tests/config/cli/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/tests/config/cli/__init__.py
--------------------------------------------------------------------------------
/tests/config/cli/conftest.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING, Callable
4 |
5 | import pytest
6 |
7 | from tox.session.cmd.depends import depends
8 | from tox.session.cmd.devenv import devenv
9 | from tox.session.cmd.exec_ import exec_
10 | from tox.session.cmd.legacy import legacy
11 | from tox.session.cmd.list_env import list_env
12 | from tox.session.cmd.quickstart import quickstart
13 | from tox.session.cmd.run.parallel import run_parallel
14 | from tox.session.cmd.run.sequential import run_sequential
15 | from tox.session.cmd.schema import gen_schema
16 | from tox.session.cmd.show_config import show_config
17 |
18 | if TYPE_CHECKING:
19 | from tox.session.state import State
20 |
21 |
22 | @pytest.fixture
23 | def core_handlers() -> dict[str, Callable[[State], int]]:
24 | return {
25 | "config": show_config,
26 | "c": show_config,
27 | "schema": gen_schema,
28 | "list": list_env,
29 | "l": list_env,
30 | "run": run_sequential,
31 | "r": run_sequential,
32 | "run-parallel": run_parallel,
33 | "p": run_parallel,
34 | "d": devenv,
35 | "devenv": devenv,
36 | "q": quickstart,
37 | "quickstart": quickstart,
38 | "de": depends,
39 | "depends": depends,
40 | "le": legacy,
41 | "legacy": legacy,
42 | "e": exec_,
43 | "exec": exec_,
44 | }
45 |
--------------------------------------------------------------------------------
/tests/config/cli/test_parse.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import logging
4 | from typing import TYPE_CHECKING
5 |
6 | import pytest
7 |
8 | from tox.config.cli.parse import get_options
9 |
10 | if TYPE_CHECKING:
11 | from tox.pytest import CaptureFixture
12 |
13 |
14 | def test_help_does_not_default_cmd(capsys: CaptureFixture) -> None:
15 | with pytest.raises(SystemExit):
16 | get_options("-h")
17 | out, err = capsys.readouterr()
18 | assert not err
19 | assert "--verbose" in out
20 | assert "subcommands:" in out
21 |
22 |
23 | def test_verbosity_guess_miss_match(capsys: CaptureFixture) -> None:
24 | result = get_options("-rv")
25 | assert result.parsed.verbosity == 3
26 |
27 | assert logging.getLogger().level == logging.INFO
28 |
29 | for name in ("distlib.util", "filelock"):
30 | logger = logging.getLogger(name)
31 | assert logger.disabled
32 | logging.error("E")
33 | logging.warning("W")
34 | logging.info("I")
35 | logging.debug("D")
36 |
37 | out, _err = capsys.readouterr()
38 | assert out == "ROOT: E\nROOT: W\nROOT: I\n"
39 |
40 |
41 | @pytest.mark.parametrize("arg", ["-av", "-va"])
42 | def test_verbosity(arg: str) -> None:
43 | result = get_options(arg)
44 | assert result.parsed.verbosity == 3
45 |
--------------------------------------------------------------------------------
/tests/config/conftest.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | import pytest
6 |
7 | if TYPE_CHECKING:
8 | from tests.conftest import ToxIniCreator
9 | from tox.config.main import Config
10 |
11 |
12 | @pytest.fixture
13 | def empty_config(tox_ini_conf: ToxIniCreator) -> Config:
14 | return tox_ini_conf("")
15 |
--------------------------------------------------------------------------------
/tests/config/loader/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/tests/config/loader/__init__.py
--------------------------------------------------------------------------------
/tests/config/loader/conftest.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | import pytest
6 |
7 | from tox.config.cli.parser import Parsed
8 | from tox.config.loader.api import ConfigLoadArgs
9 | from tox.config.main import Config
10 | from tox.config.source.tox_ini import ToxIni
11 |
12 | if TYPE_CHECKING:
13 | from pathlib import Path
14 |
15 | from typing import Protocol
16 |
17 |
18 | class ReplaceOne(Protocol):
19 | def __call__(self, conf: str, pos_args: list[str] | None = None) -> str: ...
20 |
21 |
22 | @pytest.fixture
23 | def replace_one(tmp_path: Path) -> ReplaceOne:
24 | def example(conf: str, pos_args: list[str] | None = None) -> str:
25 | tox_ini_file = tmp_path / "tox.ini"
26 | tox_ini_file.write_text(f"[testenv:py]\nenv={conf}\n")
27 | tox_ini = ToxIni(tox_ini_file)
28 |
29 | config = Config(
30 | tox_ini,
31 | options=Parsed(override=[]),
32 | root=tmp_path,
33 | pos_args=pos_args,
34 | work_dir=tmp_path,
35 | )
36 | loader = config.get_env("py").loaders[0]
37 | args = ConfigLoadArgs(chain=[], name="a", env_name="a")
38 | return loader.load(key="env", of_type=str, conf=config, factory=None, args=args)
39 |
40 | return example
41 |
--------------------------------------------------------------------------------
/tests/config/loader/ini/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/tests/config/loader/ini/__init__.py
--------------------------------------------------------------------------------
/tests/config/loader/ini/conftest.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from configparser import ConfigParser
4 | from typing import TYPE_CHECKING, Callable
5 |
6 | import pytest
7 |
8 | if TYPE_CHECKING:
9 | from pathlib import Path
10 |
11 |
12 | @pytest.fixture
13 | def mk_ini_conf(tmp_path: Path) -> Callable[[str], ConfigParser]:
14 | def _func(raw: str) -> ConfigParser:
15 | filename = tmp_path / "demo.ini"
16 | filename.write_bytes(raw.encode("utf-8")) # win32: avoid CR normalization - what you pass is what you get
17 | parser = ConfigParser(interpolation=None)
18 | with filename.open() as file_handler:
19 | parser.read_file(file_handler)
20 | return parser
21 |
22 | return _func
23 |
--------------------------------------------------------------------------------
/tests/config/loader/ini/replace/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/tests/config/loader/ini/replace/__init__.py
--------------------------------------------------------------------------------
/tests/config/loader/ini/replace/test_replace_os_pathsep.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import os
4 | from typing import TYPE_CHECKING
5 |
6 | if TYPE_CHECKING:
7 | from tests.config.loader.conftest import ReplaceOne
8 |
9 |
10 | def test_replace_os_pathsep(replace_one: ReplaceOne) -> None:
11 | result = replace_one("{:}")
12 | assert result == os.pathsep
13 |
--------------------------------------------------------------------------------
/tests/config/loader/ini/replace/test_replace_os_sep.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import os
4 | from typing import TYPE_CHECKING
5 |
6 | import pytest
7 |
8 | if TYPE_CHECKING:
9 | from tests.config.loader.conftest import ReplaceOne
10 | from tox.pytest import MonkeyPatch
11 |
12 |
13 | def test_replace_os_sep(replace_one: ReplaceOne) -> None:
14 | result = replace_one("{/}")
15 | assert result == os.sep
16 |
17 |
18 | @pytest.mark.parametrize("sep", ["/", "\\"])
19 | def test_replace_os_sep_before_curly(monkeypatch: MonkeyPatch, replace_one: ReplaceOne, sep: str) -> None:
20 | """Explicit test case for issue #2732 (windows only)."""
21 | monkeypatch.setattr(os, "sep", sep)
22 | monkeypatch.delenv("_", raising=False)
23 | result = replace_one("{/}{env:_:foo}")
24 | assert result == os.sep + "foo"
25 |
26 |
27 | @pytest.mark.parametrize("sep", ["/", "\\"])
28 | def test_replace_os_sep_sub_exp_regression(monkeypatch: MonkeyPatch, replace_one: ReplaceOne, sep: str) -> None:
29 | monkeypatch.setattr(os, "sep", sep)
30 | monkeypatch.delenv("_", raising=False)
31 | result = replace_one("{env:_:{posargs}{/}.{posargs}}", ["foo"])
32 | assert result == f"foo{os.sep}.foo"
33 |
--------------------------------------------------------------------------------
/tests/config/loader/ini/replace/test_replace_posargs.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 | from typing import TYPE_CHECKING
5 |
6 | import pytest
7 |
8 | if TYPE_CHECKING:
9 | from tests.config.loader.conftest import ReplaceOne
10 |
11 |
12 | @pytest.mark.parametrize("syntax", ["{posargs}", "[]"])
13 | def test_replace_pos_args_none_sys_argv(syntax: str, replace_one: ReplaceOne) -> None:
14 | result = replace_one(syntax, None)
15 | assert not result
16 |
17 |
18 | @pytest.mark.parametrize("syntax", ["{posargs}", "[]"])
19 | def test_replace_pos_args_empty_sys_argv(syntax: str, replace_one: ReplaceOne) -> None:
20 | result = replace_one(syntax, [])
21 | assert not result
22 |
23 |
24 | @pytest.mark.parametrize("syntax", ["{posargs}", "[]"])
25 | def test_replace_pos_args_extra_sys_argv(syntax: str, replace_one: ReplaceOne) -> None:
26 | result = replace_one(syntax, [sys.executable, "magic"])
27 | assert result == f"{sys.executable} magic"
28 |
29 |
30 | @pytest.mark.parametrize("syntax", ["{posargs}", "[]"])
31 | def test_replace_pos_args(syntax: str, replace_one: ReplaceOne) -> None:
32 | result = replace_one(syntax, ["ok", "what", " yes "])
33 | quote = '"' if sys.platform == "win32" else "'"
34 | assert result == f"ok what {quote} yes {quote}"
35 |
36 |
37 | @pytest.mark.parametrize(
38 | ("value", "result"),
39 | [
40 | ("magic", "magic"),
41 | ("magic:colon", "magic:colon"),
42 | ("magic\n b c", "magic\nb c"), # an unescaped newline keeps the newline
43 | ("magi\\\n c d", "magic d"), # an escaped newline merges the lines
44 | ("\\{a\\}", "{a}"), # escaped curly braces
45 | ],
46 | )
47 | def test_replace_pos_args_default(replace_one: ReplaceOne, value: str, result: str) -> None:
48 | outcome = replace_one(f"{{posargs:{value}}}", None)
49 | assert result == outcome
50 |
51 |
52 | @pytest.mark.parametrize(
53 | "value",
54 | [
55 | "\\{posargs}",
56 | "{posargs\\}",
57 | "\\{posargs\\}",
58 | "{\\{posargs}",
59 | "{\\{posargs}{}",
60 | "\\[]",
61 | "[\\]",
62 | "\\[\\]",
63 | ],
64 | )
65 | def test_replace_pos_args_escaped(replace_one: ReplaceOne, value: str) -> None:
66 | result = replace_one(value, None)
67 | outcome = value.replace("\\", "")
68 | assert result == outcome
69 |
70 |
71 | @pytest.mark.parametrize(
72 | ("value", "result"),
73 | [
74 | ("[]-{posargs}", "foo-foo"),
75 | ("{posargs}-[]", "foo-foo"),
76 | ],
77 | )
78 | def test_replace_mixed_brackets_and_braces(replace_one: ReplaceOne, value: str, result: str) -> None:
79 | outcome = replace_one(value, ["foo"])
80 | assert result == outcome
81 |
82 |
83 | def test_half_escaped_braces(replace_one: ReplaceOne) -> None:
84 | """See https://github.com/tox-dev/tox/issues/1956"""
85 | outcome = replace_one(r"\{posargs} {posargs}", ["foo"])
86 | assert outcome == "{posargs} foo"
87 |
--------------------------------------------------------------------------------
/tests/config/loader/ini/replace/test_replace_tty.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 | from typing import TYPE_CHECKING
5 |
6 | import pytest
7 |
8 | if TYPE_CHECKING:
9 | from pytest_mock import MockFixture
10 |
11 | from tests.config.loader.conftest import ReplaceOne
12 |
13 |
14 | @pytest.mark.parametrize("is_atty", [True, False])
15 | def test_replace_env_set(replace_one: ReplaceOne, mocker: MockFixture, is_atty: bool) -> None:
16 | mocker.patch.object(sys.stdout, "isatty", return_value=is_atty)
17 |
18 | result = replace_one("1 {tty} 2")
19 | assert result == "1 2"
20 |
21 | result = replace_one("1 {tty:a} 2")
22 | assert result == f"1 {'a' if is_atty else ''} 2"
23 |
24 | result = replace_one("1 {tty:a:b} 2")
25 | assert result == f"1 {'a' if is_atty else 'b'} 2"
26 |
--------------------------------------------------------------------------------
/tests/config/loader/ini/test_ini_loader.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING, Callable
4 |
5 | import pytest
6 |
7 | from tox.config.loader.api import ConfigLoadArgs, Override
8 | from tox.config.loader.ini import IniLoader
9 | from tox.config.source.ini_section import IniSection
10 |
11 | if TYPE_CHECKING:
12 | from configparser import ConfigParser
13 |
14 |
15 | def test_ini_loader_keys(mk_ini_conf: Callable[[str], ConfigParser]) -> None:
16 | core = IniSection(None, "tox")
17 | loader = IniLoader(core, mk_ini_conf("\n[tox]\n\na=b\nc=d\n\n"), [], core_section=core)
18 | assert loader.found_keys() == {"a", "c"}
19 |
20 |
21 | def test_ini_loader_repr(mk_ini_conf: Callable[[str], ConfigParser]) -> None:
22 | core = IniSection(None, "tox")
23 | loader = IniLoader(core, mk_ini_conf("\n[tox]\n\na=b\nc=d\n\n"), [Override("tox.a=1")], core_section=core)
24 | assert repr(loader) == "IniLoader(section=tox, overrides={'a': [Override('tox.a=1')]})"
25 |
26 |
27 | def test_ini_loader_has_section(mk_ini_conf: Callable[[str], ConfigParser]) -> None:
28 | core = IniSection(None, "tox")
29 | loader = IniLoader(core, mk_ini_conf("[magic]\n[tox]\n\na=b\nc=d\n\n"), [], core_section=core)
30 | assert loader.get_section("magic") is not None
31 |
32 |
33 | def test_ini_loader_has_no_section(mk_ini_conf: Callable[[str], ConfigParser]) -> None:
34 | core = IniSection(None, "tox")
35 | loader = IniLoader(core, mk_ini_conf("[tox]\n\na=b\nc=d\n\n"), [], core_section=core)
36 | assert loader.get_section("magic") is None
37 |
38 |
39 | def test_ini_loader_raw(mk_ini_conf: Callable[[str], ConfigParser]) -> None:
40 | core = IniSection(None, "tox")
41 | args = ConfigLoadArgs([], "name", None)
42 | loader = IniLoader(core, mk_ini_conf("[tox]\na=b"), [], core_section=core)
43 | result = loader.load(key="a", of_type=str, conf=None, factory=None, args=args)
44 | assert result == "b"
45 |
46 |
47 | @pytest.mark.parametrize("sep", ["\n", "\r\n"])
48 | def test_ini_loader_raw_strip_escaped_newline(mk_ini_conf: Callable[[str], ConfigParser], sep: str) -> None:
49 | core = IniSection(None, "tox")
50 | args = ConfigLoadArgs([], "name", None)
51 | loader = IniLoader(core, mk_ini_conf(f"[tox]{sep}a=b\\{sep} c"), [], core_section=core)
52 | result = loader.load(key="a", of_type=str, conf=None, factory=None, args=args)
53 | assert result == "bc"
54 |
55 |
56 | @pytest.mark.parametrize(
57 | ("case", "result"),
58 | [
59 | ("# a", ""),
60 | ("#", ""),
61 | ("a # w", "a"),
62 | ("a\t# w", "a"),
63 | ("a# w", "a"),
64 | ("a\\# w", "a# w"),
65 | ("#a\n b # w\n w", "b\nw"),
66 | ],
67 | )
68 | def test_ini_loader_strip_comments(mk_ini_conf: Callable[[str], ConfigParser], case: str, result: str) -> None:
69 | core = IniSection(None, "tox")
70 | args = ConfigLoadArgs([], "name", None)
71 | loader = IniLoader(core, mk_ini_conf(f"[tox]\na={case}"), [], core_section=core)
72 | outcome = loader.load(key="a", of_type=str, conf=None, factory=None, args=args)
73 | assert outcome == result
74 |
--------------------------------------------------------------------------------
/tests/config/loader/test_loader.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | import pytest
6 |
7 | from tox.config.cli.parse import get_options
8 | from tox.config.loader.api import Override
9 |
10 | if TYPE_CHECKING:
11 | from tox.pytest import CaptureFixture
12 |
13 |
14 | @pytest.mark.parametrize("flag", ["-x", "--override"])
15 | def test_override_incorrect(flag: str, capsys: CaptureFixture) -> None:
16 | with pytest.raises(SystemExit):
17 | get_options(flag, "magic")
18 | out, err = capsys.readouterr()
19 | assert not out
20 | assert "override magic has no = sign in it" in err
21 |
22 |
23 | @pytest.mark.parametrize("flag", ["-x", "--override"])
24 | def test_override_add(flag: str) -> None:
25 | parsed, _, __, ___, ____ = get_options(flag, "magic=true")
26 | assert len(parsed.override) == 1
27 | value = parsed.override[0]
28 | assert value.key == "magic"
29 | assert value.value == "true"
30 | assert not value.namespace
31 | assert value.append is False
32 |
33 |
34 | @pytest.mark.parametrize("flag", ["-x", "--override"])
35 | def test_override_append(flag: str) -> None:
36 | parsed, _, __, ___, ____ = get_options(flag, "magic+=true")
37 | assert len(parsed.override) == 1
38 | value = parsed.override[0]
39 | assert value.key == "magic"
40 | assert value.value == "true"
41 | assert not value.namespace
42 | assert value.append is True
43 |
44 |
45 | @pytest.mark.parametrize("flag", ["-x", "--override"])
46 | def test_override_multiple(flag: str) -> None:
47 | parsed, _, __, ___, ____ = get_options(flag, "magic+=1", flag, "magic+=2")
48 | assert len(parsed.override) == 2
49 |
50 |
51 | def test_override_equals() -> None:
52 | assert Override("a=b") == Override("a=b")
53 |
54 |
55 | def test_override_not_equals() -> None:
56 | assert Override("a=b") != Override("c=d")
57 |
58 |
59 | def test_override_not_equals_different_type() -> None:
60 | assert Override("a=b") != 1
61 |
62 |
63 | def test_override_repr() -> None:
64 | assert repr(Override("b.a=c")) == "Override('b.a=c')"
65 |
--------------------------------------------------------------------------------
/tests/config/loader/test_memory_loader.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import os
4 | from pathlib import Path
5 | from typing import Any, Dict, List, Optional, Set
6 |
7 | import pytest
8 |
9 | from tox.config.loader.api import ConfigLoadArgs, Override
10 | from tox.config.loader.memory import MemoryLoader
11 | from tox.config.types import Command, EnvList
12 |
13 |
14 | def test_memory_loader_repr() -> None:
15 | loader = MemoryLoader(a=1)
16 | assert repr(loader) == "MemoryLoader"
17 |
18 |
19 | def test_memory_loader_override() -> None:
20 | loader = MemoryLoader(a=1)
21 | loader.overrides["a"] = [Override("a=2")]
22 | args = ConfigLoadArgs([], "name", None)
23 | loaded = loader.load("a", of_type=int, conf=None, factory=None, args=args)
24 | assert loaded == 2
25 |
26 |
27 | @pytest.mark.parametrize(
28 | ("value", "of_type", "outcome"),
29 | [
30 | (True, bool, True),
31 | (1, int, 1),
32 | ("magic", str, "magic"),
33 | ({"1"}, Set[str], {"1"}),
34 | ([1], List[int], [1]),
35 | ({1: 2}, Dict[int, int], {1: 2}),
36 | (Path.cwd(), Path, Path.cwd()),
37 | (Command(["a"]), Command, Command(["a"])),
38 | (EnvList("a,b"), EnvList, EnvList("a,b")),
39 | (1, Optional[int], 1),
40 | ("1", Optional[str], "1"),
41 | (0, bool, False),
42 | (1, bool, True),
43 | ("1", int, 1),
44 | (1, str, "1"),
45 | ({1}, Set[str], {"1"}),
46 | ({"1"}, List[int], [1]),
47 | ({"1": "2"}, Dict[int, int], {1: 2}),
48 | (os.getcwd(), Path, Path.cwd()), # noqa: PTH109
49 | ("pip list", Command, Command(["pip", "list"])),
50 | ("a\nb", EnvList, EnvList(["a", "b"])),
51 | ("1", Optional[int], 1),
52 | ],
53 | )
54 | def test_memory_loader(value: Any, of_type: type[Any], outcome: Any) -> None:
55 | loader = MemoryLoader(a=value, kwargs={})
56 | args = ConfigLoadArgs([], "name", None)
57 | loaded = loader.load("a", of_type=of_type, conf=None, factory=None, args=args)
58 | assert loaded == outcome
59 |
60 |
61 | @pytest.mark.parametrize(
62 | ("value", "of_type", "exception", "msg"),
63 | [
64 | ("m", int, ValueError, "invalid literal for int"),
65 | ({"m"}, Set[int], ValueError, "invalid literal for int"),
66 | (["m"], List[int], ValueError, "invalid literal for int"),
67 | ({"m": 1}, Dict[int, int], ValueError, "invalid literal for int"),
68 | ({1: "m"}, Dict[int, int], ValueError, "invalid literal for int"),
69 | (object, Path, TypeError, r"str(, bytes)? or (an )?os\.PathLike object"),
70 | (1, Command, TypeError, "1"),
71 | (1, EnvList, TypeError, "1"),
72 | ],
73 | )
74 | def test_memory_loader_fails_invalid(value: Any, of_type: type[Any], exception: Exception, msg: str) -> None:
75 | loader = MemoryLoader(a=value, kwargs={})
76 | args = ConfigLoadArgs([], "name", None)
77 | with pytest.raises(exception, match=msg): # type: ignore[call-overload]
78 | loader.load("a", of_type=of_type, conf=None, factory=None, args=args)
79 |
80 |
81 | def test_memory_found_keys() -> None:
82 | loader = MemoryLoader(a=1, c=2)
83 | assert loader.found_keys() == {"a", "c"}
84 |
85 |
86 | def test_memory_loader_contains() -> None:
87 | loader = MemoryLoader(a=1)
88 | assert "a" in loader
89 | assert "b" not in loader
90 |
--------------------------------------------------------------------------------
/tests/config/loader/test_section.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import Any
4 |
5 | import pytest
6 |
7 | from tox.config.loader.section import Section
8 |
9 |
10 | @pytest.mark.parametrize(
11 | ("section", "outcome"),
12 | [
13 | (Section("a", "b"), "a:b"),
14 | (Section(None, "a"), "a"),
15 | ],
16 | )
17 | def test_section_str(section: Section, outcome: str) -> None:
18 | assert str(section) == outcome
19 |
20 |
21 | @pytest.mark.parametrize(
22 | ("section", "outcome"),
23 | [
24 | (Section("a", "b"), "Section(prefix='a', name='b')"),
25 | (Section(None, "a"), "Section(prefix=None, name='a')"),
26 | ],
27 | )
28 | def test_section_repr(section: Section, outcome: str) -> None:
29 | assert repr(section) == outcome
30 |
31 |
32 | def test_section_eq() -> None:
33 | assert Section(None, "a") == Section(None, "a")
34 |
35 |
36 | @pytest.mark.parametrize(
37 | ("section", "other"),
38 | [
39 | (Section("a", "b"), "a-b"),
40 | (Section(None, "a"), Section("b", "a")),
41 | (Section("a", "b"), Section("a", "c")),
42 | ],
43 | )
44 | def test_section_not_eq(section: Section, other: Any) -> None:
45 | assert section != other
46 |
--------------------------------------------------------------------------------
/tests/config/source/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/tests/config/source/__init__.py
--------------------------------------------------------------------------------
/tests/config/source/test_discover.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | if TYPE_CHECKING:
6 | from pathlib import Path
7 |
8 | from tox.pytest import ToxProjectCreator
9 |
10 |
11 | def out_no_src(path: Path) -> str:
12 | return (
13 | f"ROOT: No tox.ini or setup.cfg or pyproject.toml or tox.toml found, assuming empty tox.ini at {path}\n"
14 | f"default environments:\npy -> [no description]\n"
15 | )
16 |
17 |
18 | def test_no_src_cwd(tox_project: ToxProjectCreator) -> None:
19 | project = tox_project({})
20 | outcome = project.run("l")
21 | outcome.assert_success()
22 | assert outcome.out == out_no_src(project.path)
23 | assert outcome.state.conf.src_path == (project.path / "tox.ini")
24 |
25 |
26 | def test_no_src_has_py_project_toml_above(tox_project: ToxProjectCreator, tmp_path: Path) -> None:
27 | (tmp_path / "pyproject.toml").write_text("")
28 | project = tox_project({})
29 | outcome = project.run("l")
30 | outcome.assert_success()
31 | assert outcome.out == out_no_src(tmp_path)
32 | assert outcome.state.conf.src_path == (tmp_path / "tox.ini")
33 |
34 |
35 | def test_no_src_root_dir(tox_project: ToxProjectCreator, tmp_path: Path) -> None:
36 | root = tmp_path / "root"
37 | root.mkdir()
38 | project = tox_project({})
39 | outcome = project.run("l", "--root", str(root))
40 | outcome.assert_success()
41 | assert outcome.out == out_no_src(root)
42 | assert outcome.state.conf.src_path == (root / "tox.ini")
43 |
44 |
45 | def test_bad_src_content(tox_project: ToxProjectCreator, tmp_path: Path) -> None:
46 | project = tox_project({})
47 |
48 | outcome = project.run("l", "-c", str(tmp_path / "setup.cfg"))
49 | outcome.assert_failed()
50 | assert outcome.out == f"ROOT: HandledError| could not recognize config file {tmp_path / 'setup.cfg'}\n"
51 |
--------------------------------------------------------------------------------
/tests/config/source/test_legacy_toml.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | if TYPE_CHECKING:
6 | from tox.pytest import ToxProjectCreator
7 |
8 |
9 | def test_conf_in_legacy_toml(tox_project: ToxProjectCreator) -> None:
10 | project = tox_project({"pyproject.toml": '[tool.tox]\nlegacy_tox_ini="""[tox]\nenv_list=\n a\n b\n"""'})
11 |
12 | outcome = project.run("l")
13 | outcome.assert_success()
14 | assert outcome.out == "default environments:\na -> [no description]\nb -> [no description]\n"
15 |
--------------------------------------------------------------------------------
/tests/config/source/test_setup_cfg.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | if TYPE_CHECKING:
6 | from tox.pytest import ToxProjectCreator
7 |
8 |
9 | def test_conf_in_setup_cfg(tox_project: ToxProjectCreator) -> None:
10 | project = tox_project({"setup.cfg": "[tox:tox]\nenv_list=\n a\n b"})
11 |
12 | outcome = project.run("l")
13 | outcome.assert_success()
14 | assert outcome.out == "default environments:\na -> [no description]\nb -> [no description]\n"
15 |
16 |
17 | def test_bad_conf_setup_cfg(tox_project: ToxProjectCreator) -> None:
18 | project = tox_project({"setup.cfg": "[tox]\nenv_list=\n a\n b"})
19 | filename = str(project.path / "setup.cfg")
20 | outcome = project.run("l", "-c", filename)
21 | outcome.assert_failed()
22 | assert outcome.out == f"ROOT: HandledError| could not recognize config file {filename}\n"
23 |
--------------------------------------------------------------------------------
/tests/config/source/test_source_ini.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | from tox.config.loader.section import Section
6 | from tox.config.sets import ConfigSet
7 | from tox.config.source.ini import IniSource
8 |
9 | if TYPE_CHECKING:
10 | from pathlib import Path
11 |
12 | from tests.conftest import ToxIniCreator
13 |
14 |
15 | def test_source_ini_with_interpolated(tmp_path: Path) -> None:
16 | loader = IniSource(tmp_path, content="[tox]\na = %(c)s").get_loader(Section(None, "tox"), {})
17 | assert loader is not None
18 | loader.load_raw("a", None, None)
19 |
20 |
21 | def test_source_ini_ignore_non_testenv_sections(tmp_path: Path) -> None:
22 | loader = IniSource(tmp_path, content="[mypy-rest_framework.compat.*]")
23 | res = list(loader.envs({"env_list": []})) # type: ignore[arg-type]
24 | assert not res
25 |
26 |
27 | def test_source_ini_ignore_invalid_factor_filters(tmp_path: Path) -> None:
28 | loader = IniSource(tmp_path, content="[a]\nb= if c: d")
29 | res = list(loader.envs({"env_list": []})) # type: ignore[arg-type]
30 | assert not res
31 |
32 |
33 | def test_source_ini_custom_non_testenv_sections(tox_ini_conf: ToxIniCreator) -> None:
34 | """Validate that a plugin can load section with custom prefix overlapping testenv name."""
35 |
36 | class CustomConfigSet(ConfigSet):
37 | def register_config(self) -> None:
38 | self.add_config(
39 | keys=["a"],
40 | of_type=str,
41 | default="",
42 | desc="d",
43 | )
44 |
45 | config = tox_ini_conf("[testenv:foo]\n[custom:foo]\na = b")
46 | known_envs = list(config._src.envs(config.core)) # noqa: SLF001
47 | assert known_envs
48 | custom_section = config.get_section_config(
49 | section=Section("custom", "foo"),
50 | base=[],
51 | of_type=CustomConfigSet,
52 | for_env=None,
53 | )
54 | assert custom_section["a"] == "b"
55 |
--------------------------------------------------------------------------------
/tests/config/test_of_types.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from tox.config.of_type import ConfigConstantDefinition, ConfigDynamicDefinition
4 |
5 |
6 | def test_config_constant_eq() -> None:
7 | val_1 = ConfigConstantDefinition(("key",), "description", "value")
8 | val_2 = ConfigConstantDefinition(("key",), "description", "value")
9 | assert val_1 == val_2
10 |
11 |
12 | def test_config_dynamic_eq() -> None:
13 | def func(name: str) -> str:
14 | return name # pragma: no cover
15 |
16 | val_1 = ConfigDynamicDefinition(("key",), "description", str, "default", post_process=func)
17 | val_2 = ConfigDynamicDefinition(("key",), "description", str, "default", post_process=func)
18 | assert val_1 == val_2
19 |
--------------------------------------------------------------------------------
/tests/config/test_types.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from tox.config.types import Command, EnvList
4 |
5 |
6 | def tests_command_repr() -> None:
7 | cmd = Command(["python", "-m", "pip", "list"])
8 | assert repr(cmd) == "Command(args=['python', '-m', 'pip', 'list'])"
9 | assert cmd.invert_exit_code is False
10 | assert cmd.ignore_exit_code is False
11 |
12 |
13 | def tests_command_repr_ignore() -> None:
14 | cmd = Command(["-", "python", "-m", "pip", "list"])
15 | assert repr(cmd) == "Command(args=['-', 'python', '-m', 'pip', 'list'])"
16 | assert cmd.invert_exit_code is False
17 | assert cmd.ignore_exit_code is True
18 |
19 |
20 | def tests_command_repr_invert() -> None:
21 | cmd = Command(["!", "python", "-m", "pip", "list"])
22 | assert repr(cmd) == "Command(args=['!', 'python', '-m', 'pip', 'list'])"
23 | assert cmd.invert_exit_code is True
24 | assert cmd.ignore_exit_code is False
25 |
26 |
27 | def tests_command_eq() -> None:
28 | cmd_1 = Command(["python", "-m", "pip", "list"])
29 | cmd_2 = Command(["python", "-m", "pip", "list"])
30 | assert cmd_1 == cmd_2
31 |
32 |
33 | def tests_command_ne() -> None:
34 | cmd_1 = Command(["python", "-m", "pip", "list"])
35 | cmd_2 = Command(["-", "python", "-m", "pip", "list"])
36 | cmd_3 = Command(["!", "python", "-m", "pip", "list"])
37 | assert cmd_1 != cmd_2 != cmd_3
38 |
39 |
40 | def tests_env_list_repr() -> None:
41 | env = EnvList(["py39", "py38"])
42 | assert repr(env) == "EnvList(['py39', 'py38'])"
43 |
44 |
45 | def tests_env_list_eq() -> None:
46 | env_1 = EnvList(["py39", "py38"])
47 | env_2 = EnvList(["py39", "py38"])
48 | assert env_1 == env_2
49 |
50 |
51 | def tests_env_list_ne() -> None:
52 | env_1 = EnvList(["py39", "py38"])
53 | env_2 = EnvList(["py38", "py39"])
54 | assert env_1 != env_2
55 |
--------------------------------------------------------------------------------
/tests/demo_pkg_inline/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | build-backend = "build"
3 | requires = [
4 | ]
5 | backend-path = [
6 | ".",
7 | ]
8 |
9 | [tool.black]
10 | line-length = 120
11 |
--------------------------------------------------------------------------------
/tests/demo_pkg_setuptools/demo_pkg_setuptools/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 |
4 | def do() -> None:
5 | print("greetings from demo_pkg_setuptools") # noqa: T201
6 |
--------------------------------------------------------------------------------
/tests/demo_pkg_setuptools/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | build-backend = 'setuptools.build_meta'
3 | requires = [
4 | "setuptools>=63",
5 | ]
6 |
--------------------------------------------------------------------------------
/tests/demo_pkg_setuptools/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | name = demo_pkg_setuptools
3 | version = 1.2.3
4 |
5 | [options]
6 | packages = find:
7 |
--------------------------------------------------------------------------------
/tests/execute/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/tests/execute/__init__.py
--------------------------------------------------------------------------------
/tests/execute/conftest.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import os
4 |
5 | import pytest
6 |
7 |
8 | @pytest.fixture(scope="session")
9 | def os_env() -> dict[str, str]:
10 | return os.environ.copy()
11 |
--------------------------------------------------------------------------------
/tests/execute/local_subprocess/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/tests/execute/local_subprocess/__init__.py
--------------------------------------------------------------------------------
/tests/execute/local_subprocess/bad_process.py:
--------------------------------------------------------------------------------
1 | """This is a non compliant process that does not listens to signals"""
2 |
3 | # pragma: no cover
4 | from __future__ import annotations
5 |
6 | import os
7 | import signal
8 | import sys
9 | import time
10 | from pathlib import Path
11 | from typing import TYPE_CHECKING
12 |
13 | if TYPE_CHECKING:
14 | from types import FrameType
15 |
16 | out = sys.stdout
17 |
18 |
19 | def handler(signum: int, _: FrameType | None) -> None:
20 | _p(f"how about no signal {signum!r}")
21 |
22 |
23 | def _p(m: str) -> None:
24 | out.write(f"{m}{os.linesep}")
25 | out.flush() # force output flush in case we get killed
26 |
27 |
28 | _p(f"start {__name__} with {sys.argv!r}")
29 | signal.signal(signal.SIGINT, handler)
30 | signal.signal(signal.SIGTERM, handler)
31 |
32 | try:
33 | start_file = Path(sys.argv[1])
34 | _p(f"create {start_file}")
35 | start_file.write_text("")
36 | _p(f"created {start_file}")
37 | while True:
38 | time.sleep(0.01)
39 | finally:
40 | _p(f"done {__name__}")
41 |
--------------------------------------------------------------------------------
/tests/execute/local_subprocess/local_subprocess_sigint.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import locale
4 | import logging
5 | import os
6 | import signal
7 | import sys
8 | from io import TextIOWrapper
9 | from pathlib import Path
10 | from typing import TYPE_CHECKING
11 | from unittest.mock import MagicMock
12 |
13 | from tox.execute.local_sub_process import LocalSubProcessExecutor
14 | from tox.execute.request import ExecuteRequest, StdinSource
15 | from tox.report import NamedBytesIO
16 |
17 | if TYPE_CHECKING:
18 | from types import FrameType
19 |
20 | from tox.execute import Outcome
21 |
22 | logging.basicConfig(level=logging.DEBUG, format="%(relativeCreated)d\t%(levelname).1s\t%(message)s")
23 | bad_process = Path(__file__).parent / "bad_process.py"
24 |
25 | executor = LocalSubProcessExecutor(colored=False)
26 | request = ExecuteRequest(
27 | cmd=[sys.executable, bad_process, sys.argv[1]],
28 | cwd=Path().absolute(),
29 | env=os.environ.copy(),
30 | stdin=StdinSource.API,
31 | run_id="",
32 | )
33 | out_err = (
34 | TextIOWrapper(NamedBytesIO("out"), encoding=locale.getpreferredencoding(False)),
35 | TextIOWrapper(NamedBytesIO("err"), encoding=locale.getpreferredencoding(False)),
36 | )
37 |
38 |
39 | def show_outcome(outcome: Outcome | None) -> None:
40 | if outcome is not None: # pragma: no branch
41 | print(outcome.exit_code) # noqa: T201
42 | print(repr(outcome.out)) # noqa: T201
43 | print(repr(outcome.err)) # noqa: T201
44 | print(outcome.elapsed, end="") # noqa: T201
45 | print("done show outcome", file=sys.stderr) # noqa: T201
46 |
47 |
48 | def handler(s: int, f: FrameType | None) -> None:
49 | logging.info("signal %s at %s", s, f)
50 | global interrupt_done # noqa: PLW0603
51 | if interrupt_done is False: # pragma: no branch
52 | interrupt_done = True
53 | logging.info("interrupt via %s", status)
54 | status.interrupt()
55 | logging.info("interrupt finished via %s", status)
56 |
57 |
58 | interrupt_done = False
59 | signal.signal(signal.SIGINT, handler)
60 | logging.info("PID %d start %r", os.getpid(), request)
61 | tox_env = MagicMock(conf={"suicide_timeout": 0.01, "interrupt_timeout": 0.05, "terminate_timeout": 0.07})
62 | try:
63 | with executor.call(request, show=False, out_err=out_err, env=tox_env) as status:
64 | logging.info("wait on %r", status)
65 | while status.exit_code is None:
66 | status.wait(timeout=0.01) # use wait here with timeout to not block the main thread
67 | logging.info("wait over on %r", status)
68 | show_outcome(status.outcome)
69 | except Exception as exception: # pragma: no cover
70 | logging.exception(exception) # noqa: TRY401 # pragma: no cover
71 | finally:
72 | logging.info("done")
73 | logging.shutdown()
74 |
--------------------------------------------------------------------------------
/tests/execute/local_subprocess/test_execute_util.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | from tox.execute.util import shebang
6 |
7 | if TYPE_CHECKING:
8 | from pathlib import Path
9 |
10 |
11 | def test_shebang_found(tmp_path: Path) -> None:
12 | script_path = tmp_path / "a"
13 | script_path.write_text("#! /bin/python \t-c\t")
14 | assert shebang(str(script_path)) == ["/bin/python", "-c"]
15 |
16 |
17 | def test_shebang_file_missing(tmp_path: Path) -> None:
18 | script_path = tmp_path / "a"
19 | assert shebang(str(script_path)) is None
20 |
21 |
22 | def test_shebang_no_shebang(tmp_path: Path) -> None:
23 | script_path = tmp_path / "a"
24 | script_path.write_bytes(b"magic")
25 | assert shebang(str(script_path)) is None
26 |
27 |
28 | def test_shebang_non_utf8_file(tmp_path: Path) -> None:
29 | script_path, content = tmp_path / "a", b"#!" + bytearray.fromhex("c0")
30 | script_path.write_bytes(content)
31 | assert shebang(str(script_path)) is None
32 |
--------------------------------------------------------------------------------
/tests/execute/local_subprocess/tty_check.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import json
4 | import shutil
5 | import sys
6 |
7 | args = {
8 | "stdout": sys.stdout.isatty(),
9 | "stderr": sys.stderr.isatty(),
10 | "stdin": sys.stdin.isatty(),
11 | "terminal": shutil.get_terminal_size(fallback=(-1, -1)),
12 | }
13 | result = json.dumps(args)
14 | print(result) # noqa: T201
15 |
--------------------------------------------------------------------------------
/tests/execute/test_request.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import os
4 | import sys
5 | from pathlib import Path
6 |
7 | import pytest
8 |
9 | from tox.execute.request import ExecuteRequest, StdinSource
10 |
11 |
12 | def test_execute_request_raise_on_empty_cmd(os_env: dict[str, str]) -> None:
13 | with pytest.raises(ValueError, match="cannot execute an empty command"):
14 | ExecuteRequest(cmd=[], cwd=Path().absolute(), env=os_env, stdin=StdinSource.OFF, run_id="")
15 |
16 |
17 | def test_request_allow_star_is_none() -> None:
18 | request = ExecuteRequest(
19 | cmd=[sys.executable],
20 | cwd=Path.cwd(),
21 | env={"PATH": os.environ["PATH"]},
22 | stdin=StdinSource.OFF,
23 | run_id="run-id",
24 | allow=["*", "magic"],
25 | )
26 | assert request.allow is None
27 |
--------------------------------------------------------------------------------
/tests/execute/test_stream.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from colorama import Fore
4 |
5 | from tox.execute.stream import SyncWrite
6 |
7 |
8 | def test_sync_write_repr() -> None:
9 | sync_write = SyncWrite(name="a", target=None, color=Fore.RED)
10 | assert repr(sync_write) == f"SyncWrite(name='a', target=None, color={Fore.RED!r})"
11 |
12 |
13 | def test_sync_write_decode_surrogate() -> None:
14 | sync_write = SyncWrite(name="a", target=None)
15 | sync_write.handler(b"\xed\n")
16 | assert sync_write.text == "\udced\n"
17 |
--------------------------------------------------------------------------------
/tests/journal/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/tests/journal/__init__.py
--------------------------------------------------------------------------------
/tests/journal/test_main_journal.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import socket
4 | import sys
5 | from typing import Any
6 |
7 | import pytest
8 |
9 | from tox import __version__
10 | from tox.journal.main import Journal
11 |
12 |
13 | @pytest.fixture
14 | def base_info() -> dict[str, Any]:
15 | return {
16 | "reportversion": "1",
17 | "toxversion": __version__,
18 | "platform": sys.platform,
19 | "host": socket.getfqdn(),
20 | }
21 |
22 |
23 | def test_journal_enabled_default(base_info: dict[str, Any]) -> None:
24 | journal = Journal(enabled=True)
25 | assert bool(journal) is True
26 | assert journal.content == base_info
27 |
28 |
29 | def test_journal_disabled_default() -> None:
30 | journal = Journal(enabled=False)
31 | assert bool(journal) is False
32 | assert journal.content == {}
33 |
34 |
35 | def test_env_journal_enabled(base_info: dict[str, Any]) -> None:
36 | journal = Journal(enabled=True)
37 | env = journal.get_env_journal("a")
38 | assert journal.get_env_journal("a") is env
39 | env["demo"] = 1
40 |
41 | assert bool(env) is True
42 | base_info["testenvs"] = {"a": {"demo": 1}}
43 | assert journal.content == base_info
44 |
45 |
46 | def test_env_journal_disabled() -> None:
47 | journal = Journal(enabled=False)
48 | env = journal.get_env_journal("a")
49 | assert bool(env) is False
50 |
51 | env["demo"] = 2
52 | assert journal.content == {"testenvs": {"a": {"demo": 2}}}
53 |
--------------------------------------------------------------------------------
/tests/plugin/conftest.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from pathlib import Path
4 | from typing import Any, Sequence
5 |
6 | HERE = Path(__file__).parent
7 |
8 |
9 | def pytest_collection_modifyitems(items: Sequence[Any]) -> None:
10 | """automatically apply plugin test to all the test in this suite"""
11 | root = str(HERE)
12 | for item in items:
13 | if item.module.__file__.startswith(root):
14 | item.add_marker("plugin_test")
15 |
--------------------------------------------------------------------------------
/tests/plugin/test_inline.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | if TYPE_CHECKING:
6 | from tox.config.cli.parser import ToxParser
7 | from tox.pytest import ToxProjectCreator
8 |
9 |
10 | def test_inline_tox_py(tox_project: ToxProjectCreator) -> None:
11 | def plugin() -> None: # pragma: no cover # the code is copied to a python file
12 | import logging # noqa: PLC0415
13 |
14 | from tox.plugin import impl # noqa: PLC0415
15 |
16 | @impl
17 | def tox_add_option(parser: ToxParser) -> None:
18 | logging.warning("Add magic")
19 | parser.add_argument("--magic", action="store_true")
20 |
21 | project = tox_project({"toxfile.py": plugin})
22 | result = project.run("-h")
23 | result.assert_success()
24 | assert "--magic" in result.out
25 |
--------------------------------------------------------------------------------
/tests/pytest_/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/tests/pytest_/__init__.py
--------------------------------------------------------------------------------
/tests/session/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/tests/session/__init__.py
--------------------------------------------------------------------------------
/tests/session/cmd/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/tests/session/cmd/__init__.py
--------------------------------------------------------------------------------
/tests/session/cmd/run/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/tests/session/cmd/run/__init__.py
--------------------------------------------------------------------------------
/tests/session/cmd/run/test_common.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import os
4 | import re
5 | from argparse import ArgumentError, ArgumentParser, Namespace
6 | from typing import TYPE_CHECKING
7 |
8 | import pytest
9 |
10 | from tox.session.cmd.run.common import InstallPackageAction, SkipMissingInterpreterAction
11 |
12 | if TYPE_CHECKING:
13 | from pathlib import Path
14 |
15 | from tox.pytest import ToxProjectCreator
16 |
17 |
18 | @pytest.mark.parametrize("values", ["config", None, "true", "false"])
19 | def test_skip_missing_interpreter_action_ok(values: str | None) -> None:
20 | args_namespace = Namespace()
21 | SkipMissingInterpreterAction(option_strings=["-i"], dest="into")(ArgumentParser(), args_namespace, values)
22 | expected = "true" if values is None else values
23 | assert args_namespace.into == expected
24 |
25 |
26 | def test_skip_missing_interpreter_action_nok() -> None:
27 | argument_parser = ArgumentParser()
28 | with pytest.raises(ArgumentError, match=r"value must be 'config', 'true', or 'false' \(got 'bad value'\)"):
29 | SkipMissingInterpreterAction(option_strings=["-i"], dest="into")(argument_parser, Namespace(), "bad value")
30 |
31 |
32 | def test_install_pkg_ok(tmp_path: Path) -> None:
33 | argument_parser = ArgumentParser()
34 | path = tmp_path / "a"
35 | path.write_text("")
36 | namespace = Namespace()
37 | InstallPackageAction(option_strings=["--install-pkg"], dest="into")(argument_parser, namespace, str(path))
38 | assert namespace.into == path
39 |
40 |
41 | def test_install_pkg_does_not_exist(tmp_path: Path) -> None:
42 | argument_parser = ArgumentParser()
43 | path = str(tmp_path / "a")
44 | with pytest.raises(ArgumentError, match=re.escape(f"argument --install-pkg: {path} does not exist")):
45 | InstallPackageAction(option_strings=["--install-pkg"], dest="into")(argument_parser, Namespace(), path)
46 |
47 |
48 | def test_install_pkg_not_file(tmp_path: Path) -> None:
49 | argument_parser = ArgumentParser()
50 | path = str(tmp_path)
51 | with pytest.raises(ArgumentError, match=re.escape(f"argument --install-pkg: {path} is not a file")):
52 | InstallPackageAction(option_strings=["--install-pkg"], dest="into")(argument_parser, Namespace(), path)
53 |
54 |
55 | def test_install_pkg_empty() -> None:
56 | argument_parser = ArgumentParser()
57 | with pytest.raises(ArgumentError, match=re.escape("argument --install-pkg: cannot be empty")):
58 | InstallPackageAction(option_strings=["--install-pkg"], dest="into")(argument_parser, Namespace(), "")
59 |
60 |
61 | def test_empty_report(tox_project: ToxProjectCreator) -> None:
62 | proj = tox_project({"tox.ini": ""})
63 | outcome = proj.run("exec", "-qq", "--", "python", "-c", "print('foo')")
64 |
65 | outcome.assert_success()
66 | outcome.assert_out_err(f"foo{os.linesep}", "")
67 |
--------------------------------------------------------------------------------
/tests/session/cmd/test_depends.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 | from textwrap import dedent
5 | from typing import TYPE_CHECKING, Callable
6 |
7 | if TYPE_CHECKING:
8 | from tox.pytest import ToxProjectCreator
9 |
10 |
11 | def test_depends_wheel(tox_project: ToxProjectCreator, patch_prev_py: Callable[[bool], tuple[str, str]]) -> None:
12 | prev_ver, impl = patch_prev_py(True) # has previous python
13 | ver = sys.version_info[0:2]
14 | py = f"py{''.join(str(i) for i in ver)}"
15 | prev_py = f"py{prev_ver}"
16 | ini = f"""
17 | [tox]
18 | env_list = py,{py},{prev_py},py31,cov2,cov
19 | [testenv]
20 | package = wheel
21 | [testenv:cov]
22 | depends = py,{py},{prev_py},py31
23 | skip_install = true
24 | [testenv:cov2]
25 | depends = cov
26 | skip_install = true
27 | """
28 | project = tox_project({"tox.ini": ini, "pyproject.toml": ""})
29 | outcome = project.run("de")
30 | outcome.assert_success()
31 |
32 | expected = f"""
33 | Execution order: py, {py}, {prev_py}, py31, cov, cov2
34 | ALL
35 | py ~ .pkg
36 | {py} ~ .pkg
37 | {prev_py} ~ .pkg | .pkg-{impl}{prev_ver}
38 | py31 ~ .pkg
39 | cov2
40 | cov
41 | py ~ .pkg
42 | {py} ~ .pkg
43 | {prev_py} ~ .pkg | .pkg-{impl}{prev_ver}
44 | py31 ~ .pkg
45 | cov
46 | py ~ .pkg
47 | {py} ~ .pkg
48 | {prev_py} ~ .pkg | .pkg-{impl}{prev_ver}
49 | py31 ~ .pkg
50 | """
51 | assert outcome.out == dedent(expected).lstrip()
52 |
53 |
54 | def test_depends_sdist(tox_project: ToxProjectCreator, patch_prev_py: Callable[[bool], tuple[str, str]]) -> None:
55 | prev_ver, _impl = patch_prev_py(True) # has previous python
56 | ver = sys.version_info[0:2]
57 | py = f"py{''.join(str(i) for i in ver)}"
58 | prev_py = f"py{prev_ver}"
59 | ini = f"""
60 | [tox]
61 | env_list = py,{py},{prev_py},py31,cov2,cov
62 | [testenv]
63 | package = sdist
64 | [testenv:cov]
65 | depends = py,{py},{prev_py},py31
66 | skip_install = true
67 | [testenv:cov2]
68 | depends = cov
69 | skip_install = true
70 | """
71 | project = tox_project({"tox.ini": ini, "pyproject.toml": ""})
72 | outcome = project.run("de")
73 | outcome.assert_success()
74 |
75 | expected = f"""
76 | Execution order: py, {py}, {prev_py}, py31, cov, cov2
77 | ALL
78 | py ~ .pkg
79 | {py} ~ .pkg
80 | {prev_py} ~ .pkg
81 | py31 ~ .pkg
82 | cov2
83 | cov
84 | py ~ .pkg
85 | {py} ~ .pkg
86 | {prev_py} ~ .pkg
87 | py31 ~ .pkg
88 | cov
89 | py ~ .pkg
90 | {py} ~ .pkg
91 | {prev_py} ~ .pkg
92 | py31 ~ .pkg
93 | """
94 | assert outcome.out == dedent(expected).lstrip()
95 |
96 |
97 | def test_depends_help(tox_project: ToxProjectCreator) -> None:
98 | outcome = tox_project({"tox.ini": ""}).run("de", "-h")
99 | outcome.assert_success()
100 |
--------------------------------------------------------------------------------
/tests/session/cmd/test_devenv.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | import pytest
6 |
7 | if TYPE_CHECKING:
8 | from tox.pytest import ToxProjectCreator
9 |
10 |
11 | def test_devenv_fail_multiple_target(tox_project: ToxProjectCreator) -> None:
12 | outcome = tox_project({"tox.ini": "[tox]\nenv_list=a,b"}).run("d", "-e", "a,b")
13 | outcome.assert_failed()
14 | msg = "ROOT: HandledError| exactly one target environment allowed in devenv mode but found a, b\n"
15 | outcome.assert_out_err(msg, "")
16 |
17 |
18 | @pytest.mark.integration
19 | def test_devenv_ok(tox_project: ToxProjectCreator, enable_pip_pypi_access: str | None) -> None: # noqa: ARG001
20 | content = {
21 | "setup.py": "from setuptools import setup\nsetup(name='demo', version='1.0')",
22 | "tox.ini": "[tox]\nenv_list = py\n[testenv]\nusedevelop = True",
23 | }
24 | project = tox_project(content)
25 | outcome = project.run("d", "-e", "py")
26 |
27 | outcome.assert_success()
28 | assert (project.path / "venv").exists()
29 | assert f"created development environment under {project.path / 'venv'}" in outcome.out
30 |
31 |
32 | def test_devenv_help(tox_project: ToxProjectCreator) -> None:
33 | outcome = tox_project({"tox.ini": ""}).run("d", "-h")
34 | outcome.assert_success()
35 |
--------------------------------------------------------------------------------
/tests/session/cmd/test_exec_.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 | from typing import TYPE_CHECKING
5 |
6 | import pytest
7 |
8 | if TYPE_CHECKING:
9 | from tox.pytest import ToxProjectCreator
10 |
11 |
12 | @pytest.mark.parametrize("trail", [[], ["--"]], ids=["no_posargs", "empty_posargs"])
13 | def test_exec_fail_no_posargs(tox_project: ToxProjectCreator, trail: list[str]) -> None:
14 | outcome = tox_project({"tox.ini": ""}).run("e", "-e", "py39", *trail)
15 | outcome.assert_failed()
16 | msg = "ROOT: HandledError| You must specify a command as positional arguments, use -- \n"
17 | outcome.assert_out_err(msg, "")
18 |
19 |
20 | def test_exec_fail_multiple_target(tox_project: ToxProjectCreator) -> None:
21 | outcome = tox_project({"tox.ini": ""}).run("e", "-e", "py39,py38", "--", "py")
22 | outcome.assert_failed()
23 | msg = "ROOT: HandledError| exactly one target environment allowed in exec mode but found py39, py38\n"
24 | outcome.assert_out_err(msg, "")
25 |
26 |
27 | @pytest.mark.parametrize("exit_code", [1, 0])
28 | def test_exec(tox_project: ToxProjectCreator, exit_code: int) -> None:
29 | prj = tox_project({"tox.ini": "[testenv]\npackage=skip"})
30 | py_cmd = f"import sys; print(sys.version); raise SystemExit({exit_code})"
31 | outcome = prj.run("e", "-e", "py", "--", "python", "-c", py_cmd)
32 | if exit_code:
33 | outcome.assert_failed()
34 | else:
35 | outcome.assert_success()
36 | assert sys.version in outcome.out
37 |
38 |
39 | def test_exec_help(tox_project: ToxProjectCreator) -> None:
40 | outcome = tox_project({"tox.ini": ""}).run("e", "-h")
41 | outcome.assert_success()
42 |
--------------------------------------------------------------------------------
/tests/session/cmd/test_list_envs.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | import pytest
6 |
7 | if TYPE_CHECKING:
8 | from tox.pytest import ToxProject, ToxProjectCreator
9 |
10 |
11 | @pytest.fixture
12 | def project(tox_project: ToxProjectCreator) -> ToxProject:
13 | ini = """
14 | [tox]
15 | env_list=py32,py31,py
16 | [testenv]
17 | package = wheel
18 | wheel_build_env = pkg
19 | description = with {basepython}
20 | deps = pypy:
21 | [testenv:py]
22 | basepython=py32,py31
23 | [testenv:fix]
24 | description = fix it
25 | [testenv:pkg]
26 | """
27 | return tox_project({"tox.ini": ini})
28 |
29 |
30 | def test_list_env(project: ToxProject) -> None:
31 | outcome = project.run("l")
32 |
33 | outcome.assert_success()
34 | expected = """
35 | default environments:
36 | py32 -> with py32
37 | py31 -> with py31
38 | py -> with py32 py31
39 |
40 | additional environments:
41 | fix -> fix it
42 | pypy -> with pypy
43 | """
44 | outcome.assert_out_err(expected, "")
45 |
46 |
47 | def test_list_env_default(project: ToxProject) -> None:
48 | outcome = project.run("l", "-d")
49 |
50 | outcome.assert_success()
51 | expected = """
52 | default environments:
53 | py32 -> with py32
54 | py31 -> with py31
55 | py -> with py32 py31
56 | """
57 | outcome.assert_out_err(expected, "")
58 |
59 |
60 | def test_list_env_quiet(project: ToxProject) -> None:
61 | outcome = project.run("l", "--no-desc")
62 |
63 | outcome.assert_success()
64 | expected = """
65 | py32
66 | py31
67 | py
68 | fix
69 | pypy
70 | """
71 | outcome.assert_out_err(expected, "")
72 |
73 |
74 | def test_list_env_quiet_default(project: ToxProject) -> None:
75 | outcome = project.run("l", "--no-desc", "-d")
76 |
77 | outcome.assert_success()
78 | expected = """
79 | py32
80 | py31
81 | py
82 | """
83 | outcome.assert_out_err(expected, "")
84 |
85 |
86 | def test_list_env_package_env_before_run(tox_project: ToxProjectCreator) -> None:
87 | ini = """
88 | [testenv:pkg]
89 | [testenv:run]
90 | package = wheel
91 | wheel_build_env = pkg
92 | """
93 | project = tox_project({"tox.ini": ini})
94 | outcome = project.run("l")
95 |
96 | outcome.assert_success()
97 | expected = """
98 | default environments:
99 | py -> [no description]
100 |
101 | additional environments:
102 | run -> [no description]
103 | """
104 | outcome.assert_out_err(expected, "")
105 |
106 |
107 | def test_list_env_package_self(tox_project: ToxProjectCreator) -> None:
108 | ini = """
109 | [tox]
110 | env_list = pkg
111 | [testenv:pkg]
112 | package = wheel
113 | wheel_build_env = pkg
114 | """
115 | project = tox_project({"tox.ini": ini})
116 | outcome = project.run("l")
117 |
118 | outcome.assert_failed()
119 | assert outcome.out.splitlines() == ["ROOT: HandledError| pkg cannot self-package"]
120 |
121 |
122 | def test_list_envs_help(tox_project: ToxProjectCreator) -> None:
123 | outcome = tox_project({"tox.ini": ""}).run("l", "-h")
124 | outcome.assert_success()
125 |
--------------------------------------------------------------------------------
/tests/session/cmd/test_quickstart.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 | from textwrap import dedent
5 | from typing import TYPE_CHECKING
6 |
7 | from packaging.version import Version
8 |
9 | from tox.version import version as __version__
10 |
11 | if TYPE_CHECKING:
12 | from tox.pytest import ToxProjectCreator
13 |
14 |
15 | def test_quickstart_ok(tox_project: ToxProjectCreator) -> None:
16 | project = tox_project({})
17 | tox_ini = project.path / "demo" / "tox.ini"
18 | assert not tox_ini.exists()
19 |
20 | outcome = project.run("q", str(tox_ini.parent))
21 | outcome.assert_success()
22 |
23 | assert tox_ini.exists()
24 | found = tox_ini.read_text()
25 |
26 | version = str(Version(__version__.split("+")[0]))
27 | text = f"""
28 | [tox]
29 | env_list =
30 | py{"".join(str(i) for i in sys.version_info[0:2])}
31 | minversion = {version}
32 |
33 | [testenv]
34 | description = run the tests with pytest
35 | package = wheel
36 | wheel_build_env = .pkg
37 | deps =
38 | pytest>=6
39 | commands =
40 | pytest {{tty:--color=yes}} {{posargs}}
41 | """
42 | content = dedent(text).lstrip()
43 | assert found == content
44 |
45 |
46 | def test_quickstart_refuse(tox_project: ToxProjectCreator) -> None:
47 | project = tox_project({"tox.ini": ""})
48 | outcome = project.run("q", str(project.path))
49 | outcome.assert_failed(code=1)
50 | assert "tox.ini already exist, refusing to overwrite" in outcome.out
51 |
52 |
53 | def test_quickstart_help(tox_project: ToxProjectCreator) -> None:
54 | outcome = tox_project({"tox.ini": ""}).run("q", "-h")
55 | outcome.assert_success()
56 |
57 |
58 | def test_quickstart_no_args(tox_project: ToxProjectCreator) -> None:
59 | outcome = tox_project({}).run("q")
60 | outcome.assert_success()
61 |
--------------------------------------------------------------------------------
/tests/session/cmd/test_schema.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import json
4 | from typing import TYPE_CHECKING
5 |
6 | if TYPE_CHECKING:
7 | from pathlib import Path
8 |
9 | from tox.pytest import MonkeyPatch, ToxProjectCreator
10 |
11 |
12 | def test_show_schema_empty_dir(tox_project: ToxProjectCreator, monkeypatch: MonkeyPatch, tmp_path: Path) -> None:
13 | monkeypatch.chdir(tmp_path)
14 |
15 | project = tox_project({})
16 | result = project.run("-qq", "schema")
17 | schema = json.loads(result.out)
18 | assert "properties" in schema
19 | assert "tox_root" in schema["properties"]
20 |
--------------------------------------------------------------------------------
/tests/session/cmd/test_state.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | if TYPE_CHECKING:
6 | from tox.pytest import ToxProjectCreator
7 |
8 |
9 | def test_env_already_packaging(tox_project: ToxProjectCreator) -> None:
10 | proj = tox_project(
11 | {
12 | "tox.ini": "[testenv]\npackage=wheel",
13 | "pyproject.toml": '[build-system]\nrequires=[]\nbuild-backend="build"',
14 | },
15 | )
16 | result = proj.run("r", "-e", "py,.pkg")
17 | result.assert_failed(code=-2)
18 | assert "cannot run packaging environment(s) .pkg" in result.out, result.out
19 |
20 |
21 | def test_env_run_cannot_be_packaging_too(tox_project: ToxProjectCreator) -> None:
22 | proj = tox_project({"tox.ini": "[testenv]\npackage=wheel\npackage_env=py", "pyproject.toml": ""})
23 | result = proj.run("r", "-e", "py")
24 | result.assert_failed(code=-2)
25 | assert " py cannot self-package" in result.out, result.out
26 |
--------------------------------------------------------------------------------
/tests/session/test_session_common.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import pytest
4 |
5 | from tox.session.env_select import CliEnv
6 |
7 |
8 | @pytest.mark.parametrize(
9 | ("val", "exp"),
10 | [
11 | (CliEnv(["a", "b"]), "CliEnv('a,b')"),
12 | (CliEnv(["ALL", "b"]), "CliEnv('ALL')"),
13 | (CliEnv([]), "CliEnv()"),
14 | (CliEnv(), "CliEnv()"),
15 | ],
16 | )
17 | def test_cli_env_repr(val: CliEnv, exp: str) -> None:
18 | assert repr(val) == exp
19 |
20 |
21 | @pytest.mark.parametrize(
22 | ("val", "exp"),
23 | [
24 | (CliEnv(["a", "b"]), "a,b"),
25 | (CliEnv(["ALL", "b"]), "ALL"),
26 | (CliEnv([]), ""),
27 | (CliEnv(), ""),
28 | ],
29 | )
30 | def test_cli_env_str(val: CliEnv, exp: str) -> None:
31 | assert str(val) == exp
32 |
--------------------------------------------------------------------------------
/tests/test_call_modes.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import subprocess
4 | import sys
5 | from pathlib import Path
6 | from typing import TYPE_CHECKING
7 |
8 | if TYPE_CHECKING:
9 | from tox.pytest import ToxProject
10 |
11 |
12 | def test_call_as_module(empty_project: ToxProject) -> None: # noqa: ARG001
13 | subprocess.check_output([sys.executable, "-m", "tox", "-h"])
14 |
15 |
16 | def test_call_as_exe(empty_project: ToxProject) -> None: # noqa: ARG001
17 | subprocess.check_output([str(Path(sys.executable).parent / "tox"), "-h"])
18 |
--------------------------------------------------------------------------------
/tests/test_report.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import logging
4 | import os
5 | from typing import TYPE_CHECKING
6 |
7 | import pytest
8 | from colorama import Style, deinit
9 |
10 | from tox.report import setup_report
11 |
12 | if TYPE_CHECKING:
13 | from pytest_mock import MockerFixture
14 |
15 | from tox.pytest import CaptureFixture
16 |
17 |
18 | @pytest.mark.parametrize("color", [True, False], ids=["on", "off"])
19 | @pytest.mark.parametrize("verbosity", range(7))
20 | def test_setup_report(mocker: MockerFixture, capsys: CaptureFixture, verbosity: int, color: bool) -> None:
21 | color_init = mocker.patch("tox.report.init")
22 |
23 | setup_report(verbosity=verbosity, is_colored=color)
24 | try:
25 | logging.critical("critical")
26 | logging.error("error")
27 | # special warning line that should be auto-colored
28 | logging.warning("%s%s> %s", "warning", "foo", "bar")
29 | logging.info("info")
30 | logging.debug("debug")
31 | logging.log(logging.NOTSET, "not-set") # this should not be logged
32 | disabled = "distlib.util", "filelock"
33 | for name in disabled:
34 | logger = logging.getLogger(name)
35 | logger.warning("%s-warn", name)
36 | logger.info("%s-info", name)
37 | logger.debug("%s-debug", name)
38 | logger.log(logging.NOTSET, "%s-notset", name)
39 | finally:
40 | deinit()
41 |
42 | assert color_init.call_count == (1 if color else 0)
43 |
44 | msg_count = min(verbosity + 1, 5)
45 | is_debug_or_more = verbosity >= 4
46 | if is_debug_or_more:
47 | msg_count += 1 # we log at debug level setting up the logger
48 |
49 | out, err = capsys.readouterr()
50 | assert not err
51 | assert out
52 | assert "filelock" not in out
53 | assert "distlib.util" not in out
54 | lines = out.splitlines()
55 | assert len(lines) == msg_count, out
56 |
57 | if is_debug_or_more and lines: # assert we start with relative created, contain path
58 | line = lines[0]
59 | int(line.split(" ")[1]) # first element is an int number
60 | assert f"[tox{os.sep}report.py" in line # relative file location
61 |
62 | if color:
63 | assert f"{Style.RESET_ALL}" in out
64 | # check that our Warning line using special format was colored
65 | expected_warning_text = "W\x1b[0m\x1b[36m warning\x1b[22mfoo\x1b[2m>\x1b[0m bar\x1b[0m\x1b[2m"
66 | else:
67 | assert f"{Style.RESET_ALL}" not in out
68 | expected_warning_text = "warningfoo> bar"
69 | if verbosity >= 4: # where warnings are logged
70 | assert expected_warning_text in lines[3]
71 |
--------------------------------------------------------------------------------
/tests/test_version.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from types import SimpleNamespace
4 | from typing import TYPE_CHECKING
5 |
6 | from tox import __version__
7 | from tox.plugin.manager import MANAGER
8 |
9 | if TYPE_CHECKING:
10 | from pytest_mock import MockFixture
11 |
12 | from tox.pytest import ToxProjectCreator
13 |
14 |
15 | def test_version() -> None:
16 | assert __version__
17 |
18 |
19 | def test_version_without_plugin(tox_project: ToxProjectCreator) -> None:
20 | outcome = tox_project({"tox.ini": ""}).run("--version")
21 | outcome.assert_success()
22 | assert __version__ in outcome.out
23 | assert "plugin" not in outcome.out
24 |
25 |
26 | def test_version_with_plugin(tox_project: ToxProjectCreator, mocker: MockFixture) -> None:
27 | dist = [
28 | (
29 | mocker.create_autospec("types.ModuleType", __file__="B-path", tox_append_version_info=lambda: "magic"),
30 | SimpleNamespace(project_name="B", version="1.0"),
31 | ),
32 | (
33 | mocker.create_autospec("types.ModuleType", __file__="A-path"),
34 | SimpleNamespace(project_name="A", version="2.0"),
35 | ),
36 | ]
37 | mocker.patch.object(MANAGER.manager, "list_plugin_distinfo", return_value=dist)
38 |
39 | outcome = tox_project({"tox.ini": ""}).run("--version")
40 |
41 | outcome.assert_success()
42 | assert not outcome.err
43 | lines = outcome.out.splitlines()
44 | assert lines[0].startswith(__version__)
45 |
46 | assert lines[1:] == [
47 | "registered plugins:",
48 | " B-1.0 at B-path magic",
49 | " A-2.0 at A-path",
50 | ]
51 |
--------------------------------------------------------------------------------
/tests/tox_env/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/tests/tox_env/__init__.py
--------------------------------------------------------------------------------
/tests/tox_env/python/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/tests/tox_env/python/__init__.py
--------------------------------------------------------------------------------
/tests/tox_env/python/pip/test_req_file.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from argparse import Namespace
4 | from typing import TYPE_CHECKING
5 |
6 | import pytest
7 |
8 | from tox.tox_env.python.pip.req_file import PythonDeps
9 |
10 | if TYPE_CHECKING:
11 | from pathlib import Path
12 |
13 |
14 | @pytest.mark.parametrize("legacy_flag", ["-r", "-c"])
15 | def test_legacy_requirement_file(tmp_path: Path, legacy_flag: str) -> None:
16 | python_deps = PythonDeps(f"{legacy_flag}a.txt", tmp_path)
17 | (tmp_path / "a.txt").write_text("b")
18 | assert python_deps.as_root_args == [legacy_flag, "a.txt"]
19 | assert vars(python_deps.options) == {}
20 | assert [str(i) for i in python_deps.requirements] == ["b" if legacy_flag == "-r" else "-c b"]
21 |
22 |
23 | def test_deps_with_hash(tmp_path: Path) -> None:
24 | """deps with --hash should raise an exception."""
25 | python_deps = PythonDeps(
26 | raw="foo==1 --hash sha256:97a702083b0d906517b79672d8501eee470d60ae55df0fa9d4cfba56c7f65a82",
27 | root=tmp_path,
28 | )
29 | with pytest.raises(ValueError, match="Cannot use --hash in deps list"):
30 | _ = python_deps.requirements
31 |
32 |
33 | def test_deps_with_requirements_with_hash(tmp_path: Path) -> None:
34 | """deps can point to a requirements file that has --hash."""
35 | exp_hash = "sha256:97a702083b0d906517b79672d8501eee470d60ae55df0fa9d4cfba56c7f65a82"
36 | requirements = tmp_path / "requirements.txt"
37 | requirements.write_text(f"foo==1 --hash {exp_hash}")
38 | python_deps = PythonDeps(raw="-r requirements.txt", root=tmp_path)
39 | assert len(python_deps.requirements) == 1
40 | parsed_req = python_deps.requirements[0]
41 | assert str(parsed_req.requirement) == "foo==1"
42 | assert parsed_req.options == {"hash": [exp_hash]}
43 | assert parsed_req.from_file == str(requirements)
44 |
45 |
46 | def test_deps_with_no_deps(tmp_path: Path) -> None:
47 | (tmp_path / "r.txt").write_text("urrlib3")
48 | python_deps = PythonDeps(raw="-rr.txt\n--no-deps", root=tmp_path)
49 |
50 | assert len(python_deps.requirements) == 1
51 | parsed_req = python_deps.requirements[0]
52 | assert str(parsed_req.requirement) == "urrlib3"
53 |
54 | assert python_deps.options.no_deps is True
55 | assert python_deps.as_root_args == ["-r", "r.txt", "--no-deps"]
56 |
57 |
58 | def test_req_with_no_deps(tmp_path: Path) -> None:
59 | (tmp_path / "r.txt").write_text("--no-deps")
60 | python_deps = PythonDeps(raw="-rr.txt", root=tmp_path)
61 | with pytest.raises(ValueError, match="unrecognized arguments: --no-deps"):
62 | python_deps.requirements # noqa: B018
63 |
64 |
65 | def test_opt_only_req_file(tmp_path: Path) -> None:
66 | (tmp_path / "r.txt").write_text("--use-feature fast-deps")
67 | python_deps = PythonDeps(raw="-rr.txt", root=tmp_path)
68 | assert not python_deps.requirements
69 | assert python_deps.options == Namespace(features_enabled=["fast-deps"])
70 |
71 |
72 | def test_req_iadd(tmp_path: Path) -> None:
73 | a = PythonDeps(raw="foo", root=tmp_path)
74 | b = PythonDeps(raw="bar", root=tmp_path)
75 | a += b
76 | assert a.lines() == ["foo", "bar"]
77 |
--------------------------------------------------------------------------------
/tests/tox_env/python/test-pkg/pyproject.toml:
--------------------------------------------------------------------------------
1 | # empty
2 |
--------------------------------------------------------------------------------
/tests/tox_env/python/virtual_env/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/tests/tox_env/python/virtual_env/__init__.py
--------------------------------------------------------------------------------
/tests/tox_env/python/virtual_env/package/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/tests/tox_env/python/virtual_env/package/__init__.py
--------------------------------------------------------------------------------
/tests/tox_env/python/virtual_env/package/conftest.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 | from textwrap import dedent
5 | from typing import TYPE_CHECKING
6 |
7 | import pytest
8 |
9 | if TYPE_CHECKING:
10 | from pathlib import Path
11 |
12 |
13 | @pytest.fixture(scope="session")
14 | def pkg_with_extras_project(tmp_path_factory: pytest.TempPathFactory) -> Path:
15 | py_ver = ".".join(str(i) for i in sys.version_info[0:2])
16 | setup_cfg = f"""
17 | [metadata]
18 | name = demo
19 | version = 1.0.0
20 | [options]
21 | packages = find:
22 | install_requires =
23 | platformdirs>=2.1
24 | colorama>=0.4.3
25 |
26 | [options.extras_require]
27 | testing =
28 | covdefaults>=1.2; python_version == '2.7' or python_version == '{py_ver}'
29 | pytest>=5.4.1; python_version == '{py_ver}'
30 | docs =
31 | sphinx>=3
32 | sphinx-rtd-theme>=0.4.3,<1
33 | format =
34 | black>=3
35 | flake8
36 | """
37 | tmp_path = tmp_path_factory.mktemp("prj")
38 | (tmp_path / "setup.cfg").write_text(dedent(setup_cfg))
39 | (tmp_path / "setup.py").write_text("from setuptools import setup; setup()")
40 | toml = '[build-system]\nrequires=["setuptools", "wheel"]\nbuild-backend = "setuptools.build_meta"'
41 | (tmp_path / "pyproject.toml").write_text(toml)
42 | return tmp_path
43 |
--------------------------------------------------------------------------------
/tests/tox_env/python/virtual_env/test_setuptools.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import os
4 | import sys
5 | from typing import TYPE_CHECKING, cast
6 |
7 | import pytest
8 |
9 | from tox.tox_env.python.package import WheelPackage
10 | from tox.tox_env.python.virtual_env.package.pyproject import Pep517VirtualEnvPackager
11 |
12 | if TYPE_CHECKING:
13 | from pathlib import Path
14 |
15 | from tox.pytest import ToxProjectCreator
16 | from tox.tox_env.runner import RunToxEnv
17 |
18 |
19 | @pytest.mark.integration
20 | def test_setuptools_package(
21 | tox_project: ToxProjectCreator,
22 | demo_pkg_setuptools: Path,
23 | enable_pip_pypi_access: str | None, # noqa: ARG001
24 | ) -> None:
25 | tox_ini = """
26 | [testenv]
27 | package = wheel
28 | commands_pre = python -c 'import sys; print("start", sys.executable)'
29 | commands = python -c 'from demo_pkg_setuptools import do; do()'
30 | commands_post = python -c 'import sys; print("end", sys.executable)'
31 | """
32 | project = tox_project({"tox.ini": tox_ini}, base=demo_pkg_setuptools)
33 |
34 | outcome = project.run("r", "-e", "py")
35 |
36 | outcome.assert_success()
37 | assert f"\ngreetings from demo_pkg_setuptools{os.linesep}" in outcome.out
38 | tox_env = cast("RunToxEnv", outcome.state.envs["py"])
39 |
40 | (package_env,) = list(tox_env.package_envs)
41 | assert isinstance(package_env, Pep517VirtualEnvPackager)
42 | packages = package_env.perform_packaging(tox_env.conf)
43 | assert len(packages) == 1
44 | package = packages[0]
45 | assert isinstance(package, WheelPackage)
46 | assert str(package) == str(package.path)
47 | assert package.path.name == f"demo_pkg_setuptools-1.2.3-py{sys.version_info.major}-none-any.whl"
48 |
49 | result = outcome.out.split("\n")
50 | py_messages = [i for i in result if "py: " in i]
51 | assert len(py_messages) == 5, "\n".join(py_messages) # 1 install wheel + 3 command + 1 final report
52 |
53 | package_messages = [i for i in result if ".pkg: " in i]
54 | # 1 optional hooks + 1 install requires + 1 build meta + 1 build isolated
55 | assert len(package_messages) == 4, "\n".join(package_messages)
56 |
--------------------------------------------------------------------------------
/tests/tox_env/test_api.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | if TYPE_CHECKING:
6 | from pathlib import Path
7 |
8 | from tox.pytest import ToxProjectCreator
9 |
10 |
11 | def test_ensure_temp_dir_exists(tox_project: ToxProjectCreator) -> None:
12 | ini = "[testenv]\ncommands=python -c 'import os; os.path.exists(r\"{temp_dir}\")'"
13 | project = tox_project({"tox.ini": ini})
14 | result = project.run()
15 | result.assert_success()
16 |
17 |
18 | def test_dont_cleanup_temp_dir(tox_project: ToxProjectCreator, tmp_path: Path) -> None:
19 | (tmp_path / "foo" / "bar").mkdir(parents=True)
20 | project = tox_project({"tox.ini": "[tox]\ntemp_dir=foo"})
21 | result = project.run()
22 | result.assert_success()
23 | assert (tmp_path / "foo" / "bar").exists()
24 |
25 |
26 | def test_setenv_section_substitution(tox_project: ToxProjectCreator) -> None:
27 | ini = """[variables]
28 | var = VAR = val
29 | [testenv]
30 | setenv = {[variables]var}
31 | commands = python -c 'import os; os.environ["VAR"]'"""
32 | project = tox_project({"tox.ini": ini})
33 | result = project.run()
34 | result.assert_success()
35 |
--------------------------------------------------------------------------------
/tests/tox_env/test_info.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from pathlib import Path
4 |
5 | from tox.tox_env.info import Info
6 |
7 |
8 | def test_info_repr() -> None:
9 | at_loc = Path().absolute()
10 | info_object = Info(at_loc)
11 | assert repr(info_object) == f"Info(path={at_loc / '.tox-info.json'})"
12 |
--------------------------------------------------------------------------------
/tests/tox_env/test_register.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import pytest
4 |
5 | from tox.tox_env.python.virtual_env.runner import VirtualEnvRunner
6 | from tox.tox_env.register import ToxEnvRegister
7 |
8 |
9 | def test_register_set_new_default_no_register() -> None:
10 | register = ToxEnvRegister()
11 | with pytest.raises(ValueError, match="run env must be registered before setting it as default"):
12 | register.default_env_runner = "new-env"
13 |
14 |
15 | def test_register_set_new_default_with_register() -> None:
16 | class B(VirtualEnvRunner):
17 | @staticmethod
18 | def id() -> str:
19 | return "B"
20 |
21 | register = ToxEnvRegister()
22 | register.add_run_env(VirtualEnvRunner)
23 | assert register.default_env_runner == VirtualEnvRunner.id()
24 | register.add_run_env(B)
25 | assert register.default_env_runner == VirtualEnvRunner.id()
26 | register.default_env_runner = B.id()
27 | assert register.default_env_runner == "B"
28 |
--------------------------------------------------------------------------------
/tests/tox_env/test_tox_env_runner.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | if TYPE_CHECKING:
6 | from pathlib import Path
7 |
8 | from tox.pytest import ToxProjectCreator
9 |
10 |
11 | def test_package_only(
12 | tox_project: ToxProjectCreator,
13 | demo_pkg_inline: Path,
14 | ) -> None:
15 | ini = "[testenv]\ncommands = python -c 'print('foo')'"
16 | proj = tox_project(
17 | {"tox.ini": ini, "pyproject.toml": (demo_pkg_inline / "pyproject.toml").read_text()},
18 | base=demo_pkg_inline,
19 | )
20 | execute_calls = proj.patch_execute(lambda r: 0 if "install" in r.run_id else None)
21 | result = proj.run("r", "--sdistonly")
22 | result.assert_success()
23 |
24 | expected_calls = [
25 | (".pkg", "_optional_hooks"),
26 | (".pkg", "get_requires_for_build_sdist"),
27 | (".pkg", "get_requires_for_build_wheel"),
28 | (".pkg", "build_wheel"),
29 | (".pkg", "build_sdist"),
30 | (".pkg", "_exit"),
31 | ]
32 | found_calls = [(i[0][0].conf.name, i[0][3].run_id) for i in execute_calls.call_args_list]
33 | assert found_calls == expected_calls
34 |
--------------------------------------------------------------------------------
/tests/type_check/add_config_container_factory.py:
--------------------------------------------------------------------------------
1 | """check that factory for a container works"""
2 |
3 | from __future__ import annotations
4 |
5 | from typing import List
6 |
7 | from tox.config.sets import ConfigSet
8 |
9 |
10 | class EnvDockerConfigSet(ConfigSet):
11 | def register_config(self) -> None:
12 | def factory(container_name: object) -> str:
13 | raise NotImplementedError
14 |
15 | self.add_config(
16 | keys=["k"],
17 | of_type=List[str],
18 | default=[],
19 | desc="desc",
20 | factory=factory,
21 | )
22 |
--------------------------------------------------------------------------------
/tests/util/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tox-dev/tox/3ad56f04fcf923649e1ce2eb40bb32778a6659c3/tests/util/__init__.py
--------------------------------------------------------------------------------
/tests/util/test_ci.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import operator
4 |
5 | import pytest
6 |
7 | from tox.util.ci import _ENV_VARS, is_ci # noqa: PLC2701
8 |
9 |
10 | @pytest.mark.parametrize(
11 | "env_var",
12 | {
13 | "CI": None, # generic flag
14 | "TF_BUILD": "true", # Azure Pipelines
15 | "bamboo.buildKey": None, # Bamboo
16 | "BUILDKITE": "true", # Buildkite
17 | "CIRCLECI": "true", # Circle CI
18 | "CIRRUS_CI": "true", # Cirrus CI
19 | "CODEBUILD_BUILD_ID": None, # CodeBuild
20 | "GITHUB_ACTIONS": "true", # GitHub Actions
21 | "GITLAB_CI": None, # GitLab CI
22 | "HEROKU_TEST_RUN_ID": None, # Heroku CI
23 | "BUILD_ID": None, # Hudson
24 | "TEAMCITY_VERSION": None, # TeamCity
25 | "TRAVIS": "true", # Travis CI
26 | }.items(),
27 | ids=operator.itemgetter(0),
28 | )
29 | def test_is_ci(env_var: tuple[str, str | None], monkeypatch: pytest.MonkeyPatch) -> None:
30 | for var in _ENV_VARS:
31 | monkeypatch.delenv(var, raising=False)
32 | monkeypatch.setenv(env_var[0], env_var[1] or "")
33 | assert is_ci()
34 |
35 |
36 | @pytest.mark.parametrize(
37 | "env_var",
38 | {
39 | "TF_BUILD": "", # Azure Pipelines
40 | "BUILDKITE": "", # Buildkite
41 | "CIRCLECI": "", # Circle CI
42 | "CIRRUS_CI": "", # Cirrus CI
43 | "GITHUB_ACTIONS": "", # GitHub Actions
44 | "TRAVIS": "", # Travis CI
45 | }.items(),
46 | ids=operator.itemgetter(0),
47 | )
48 | def test_is_ci_bad_set(env_var: tuple[str, str], monkeypatch: pytest.MonkeyPatch) -> None:
49 | for var in _ENV_VARS:
50 | monkeypatch.delenv(var, raising=False)
51 | monkeypatch.setenv(env_var[0], env_var[1])
52 | assert not is_ci()
53 |
54 |
55 | def test_is_ci_not(monkeypatch: pytest.MonkeyPatch) -> None:
56 | for var in _ENV_VARS:
57 | monkeypatch.delenv(var, raising=False)
58 | assert not is_ci()
59 |
60 |
61 | def test_is_ci_not_teamcity_local(monkeypatch: pytest.MonkeyPatch) -> None:
62 | # pycharm sets this
63 | for var in _ENV_VARS:
64 | monkeypatch.delenv(var, raising=False)
65 |
66 | monkeypatch.setenv("TEAMCITY_VERSION", "LOCAL")
67 | assert not is_ci()
68 |
--------------------------------------------------------------------------------
/tests/util/test_cpu.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import multiprocessing
4 | from typing import TYPE_CHECKING
5 |
6 | from tox.util.cpu import auto_detect_cpus
7 |
8 | if TYPE_CHECKING:
9 | from pytest_mock import MockerFixture
10 |
11 |
12 | def test_auto_detect_cpus() -> None:
13 | num_cpus_actual = multiprocessing.cpu_count()
14 | assert auto_detect_cpus() == num_cpus_actual
15 |
16 |
17 | def test_auto_detect_cpus_returns_one_when_cpu_count_throws(mocker: MockerFixture) -> None:
18 | mocker.patch.object(multiprocessing, "cpu_count", side_effect=NotImplementedError)
19 | assert auto_detect_cpus() == 1
20 |
--------------------------------------------------------------------------------
/tests/util/test_graph.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from collections import OrderedDict
4 |
5 | import pytest
6 |
7 | from tox.util.graph import stable_topological_sort
8 |
9 |
10 | def test_topological_order_empty() -> None:
11 | graph: dict[str, set[str]] = OrderedDict()
12 | result = stable_topological_sort(graph)
13 | assert result == []
14 |
15 |
16 | def test_topological_order_specified_only() -> None:
17 | graph: dict[str, set[str]] = OrderedDict()
18 | graph["A"] = {"B", "C"}
19 | result = stable_topological_sort(graph)
20 | assert result == ["A"]
21 |
22 |
23 | def test_topological_order() -> None:
24 | graph: dict[str, set[str]] = OrderedDict()
25 | graph["A"] = {"B", "C"}
26 | graph["B"] = set()
27 | graph["C"] = set()
28 | result = stable_topological_sort(graph)
29 | assert result == ["B", "C", "A"]
30 |
31 |
32 | def test_topological_order_cycle() -> None:
33 | graph: dict[str, set[str]] = OrderedDict()
34 | graph["A"] = {"B", "C"}
35 | graph["B"] = {"A"}
36 | with pytest.raises(ValueError, match=r"^A \| B$"):
37 | stable_topological_sort(graph)
38 |
39 |
40 | def test_topological_complex() -> None:
41 | graph: dict[str, set[str]] = OrderedDict()
42 | graph["A"] = {"B", "C"}
43 | graph["B"] = {"C", "D"}
44 | graph["C"] = {"D"}
45 | graph["D"] = set()
46 | result = stable_topological_sort(graph)
47 | assert result == ["D", "C", "B", "A"]
48 |
49 |
50 | def test_two_sub_graph() -> None:
51 | graph: dict[str, set[str]] = OrderedDict()
52 | graph["F"] = set()
53 | graph["E"] = set()
54 | graph["D"] = {"E", "F"}
55 | graph["A"] = {"B", "C"}
56 | graph["B"] = set()
57 | graph["C"] = set()
58 |
59 | result = stable_topological_sort(graph)
60 | assert result == ["F", "E", "D", "B", "C", "A"]
61 |
62 |
63 | def test_two_sub_graph_circle() -> None:
64 | graph: dict[str, set[str]] = OrderedDict()
65 | graph["F"] = set()
66 | graph["E"] = set()
67 | graph["D"] = {"E", "F"}
68 | graph["A"] = {"B", "C"}
69 | graph["B"] = {"A"}
70 | graph["C"] = set()
71 | with pytest.raises(ValueError, match=r"^A \| B$"):
72 | stable_topological_sort(graph)
73 |
--------------------------------------------------------------------------------
/tests/util/test_path.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | from tox.util.path import ensure_empty_dir
6 |
7 | if TYPE_CHECKING:
8 | from pathlib import Path
9 |
10 |
11 | def test_ensure_empty_dir_file(tmp_path: Path) -> None:
12 | dest = tmp_path / "a"
13 | dest.write_text("")
14 | ensure_empty_dir(dest)
15 | assert dest.is_dir()
16 | assert not list(dest.iterdir())
17 |
--------------------------------------------------------------------------------