Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 9 additions & 13 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,11 @@ jobs:

steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: astral-sh/setup-uv@v5
with:
python-version-file: .python-version
allow-prereleases: true
cache: pip
- run: pip install tox-uv
- run: tox -e typing
enable-cache: true
- run: uv run --no-dev --group typing mypy
- run: uv run --no-dev --group typing nbqa mypy --ignore-missing-imports .

run-tests:

Expand All @@ -43,12 +41,10 @@ jobs:

steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
allow-prereleases: true
- run: pip install tox-uv
enable-cache: true

- if: matrix.os == 'ubuntu-latest'
run: |
Expand All @@ -59,7 +55,7 @@ jobs:

- name: Run unit tests and doctests.
shell: bash -l {0}
run: tox -e test -- -m "unit or (not integration and not end_to_end)" --cov=src --cov=tests --cov-report=xml -n auto
run: uv run --group test pytest --nbmake -m "unit or (not integration and not end_to_end)" --cov=src --cov=tests --cov-report=xml -n auto

- name: Upload unit test coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v5
Expand All @@ -68,7 +64,7 @@ jobs:

- name: Run integration tests.
shell: bash -l {0}
run: tox -e test -- -m integration --cov=src --cov=tests --cov-report=xml -n auto
run: uv run --group test pytest --nbmake -m integration --cov=src --cov=tests --cov-report=xml -n auto

- name: Upload integration test coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v5
Expand All @@ -77,7 +73,7 @@ jobs:

- name: Run end-to-end tests.
shell: bash -l {0}
run: tox -e test -- -m end_to_end --cov=src --cov=tests --cov-report=xml -n auto
run: uv run --group test pytest --nbmake -m end_to_end --cov=src --cov=tests --cov-report=xml -n auto

- name: Upload end_to_end test coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v5
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,3 @@ tests/test_jupyter/*.txt
.ruff_cache
.venv
docs/jupyter_execute
uv.lock
17 changes: 9 additions & 8 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
version: 2

build:
os: ubuntu-22.04
os: ubuntu-24.04
tools:
python: "3.12"
jobs:
create_environment:
- asdf plugin add uv
- asdf install uv latest
- asdf global uv latest
- UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH uv sync --group docs
install:
- "true"

sphinx:
configuration: docs/source/conf.py
fail_on_warning: true

python:
install:
- method: pip
path: .
extra_requirements:
- docs
8 changes: 3 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,11 @@ text = "MIT"
name = "Tobias Raabe"
email = "raabe@posteo.de"

[project.optional-dependencies]
[dependency-groups]
dev = ["pygraphviz>=1.11;platform_system=='Linux'"]
docs = [
"furo",
"ipython",
"ipython>=8.13.2",
"matplotlib",
"myst-parser",
"myst-nb",
Expand Down Expand Up @@ -85,9 +86,6 @@ Tracker = "https://github.com/pytask-dev/pytask/issues"
[project.scripts]
pytask = "pytask:cli"

[tool.uv]
dev-dependencies = ["tox-uv>=1.7.0", "pygraphviz;platform_system=='Linux'"]

[build-system]
requires = ["hatchling", "hatch_vcs"]
build-backend = "hatchling.build"
Expand Down
2 changes: 1 addition & 1 deletion src/_pytask/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ def build( # noqa: C901, PLR0912, PLR0913
session.exit_code = ExitCode.FAILED

except Exception: # noqa: BLE001
console.print(Traceback(sys.exc_info()))
console.print(Traceback(sys.exc_info(), show_locals=True))
session.exit_code = ExitCode.FAILED

session.hook.pytask_unconfigure(session=session)
Expand Down
17 changes: 13 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,20 @@ class Result(NamedTuple):

def run_in_subprocess(cmd: tuple[str, ...], cwd: Path | None = None) -> Result:
"""Run a command in a subprocess and return the output."""
result = subprocess.run(cmd, cwd=cwd, check=False, capture_output=True)
kwargs = (
{
"env": os.environ | {"PYTHONIOENCODING": "utf-8", "TERM": "unknown"},
"encoding": "utf-8",
}
if sys.platform == "win32"
else {}
)

result = subprocess.run(
cmd, cwd=cwd, check=False, capture_output=True, text=True, **kwargs
)
return Result(
exit_code=result.returncode,
stdout=result.stdout.decode("utf-8", "replace").replace("\r\n", "\n"),
stderr=result.stderr.decode("utf-8", "replace").replace("\r\n", "\n"),
exit_code=result.returncode, stdout=result.stdout, stderr=result.stderr
)


Expand Down
5 changes: 5 additions & 0 deletions tests/test_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,11 @@ def test_many(self, capfd): # noqa: ARG002

@pytest.mark.unit
class TestStdCaptureFDinvalidFD:
@pytest.mark.skipif(
sys.platform == "darwin" and sys.version_info[:2] == (3, 9),
reason="Causes following tests to fail and kills the pytest session with exit "
"code 137.",
)
def test_stdcapture_fd_invalid_fd(self, tmp_path, runner):
source = """
import os
Expand Down
64 changes: 20 additions & 44 deletions tests/test_clean.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,20 @@
from _pytask.git import init_repo
from pytask import ExitCode
from pytask import cli
from pytask import storage
from tests.conftest import enter_directory

_PROJECT_TASK = """
import pytask
from pathlib import Path

def task_write_text(path = Path("in.txt"), produces = Path("out.txt")):
produces.write_text("a")
"""


_PROJECT_TASK_NEW_INTERFACE = """
import pytask
from pathlib import Path

def task_write_text(path=Path("in.txt"), produces=Path("out.txt")):
produces.write_text("a")
"""


@pytest.fixture(params=[_PROJECT_TASK, _PROJECT_TASK_NEW_INTERFACE])
def project(request, tmp_path):
@pytest.fixture
def project(tmp_path):
"""Create a sample project to be cleaned."""
tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(request.param))
content = """
import pytask
from pathlib import Path

def task_write_text(path=Path("in.txt"), produces=Path("out.txt")):
produces.write_text("a")
"""
tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(content))
tmp_path.joinpath("in.txt").touch()

tmp_path.joinpath("to_be_deleted_file_1.txt").touch()
Expand All @@ -42,28 +31,17 @@ def project(request, tmp_path):
return tmp_path


_GIT_PROJECT_TASK = """
import pytask
from pathlib import Path

def task_write_text(path = Path("in_tracked.txt"), produces = Path("out.txt")):
produces.write_text("a")
"""


_GIT_PROJECT_TASK_NEW_INTERFACE = """
import pytask
from pathlib import Path

def task_write_text(path=Path("in_tracked.txt"), produces=Path("out.txt")):
produces.write_text("a")
"""


@pytest.fixture(params=[_GIT_PROJECT_TASK, _GIT_PROJECT_TASK_NEW_INTERFACE])
def git_project(request, tmp_path):
@pytest.fixture
def git_project(tmp_path):
"""Create a sample project to be cleaned."""
tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(request.param))
content = """
import pytask
from pathlib import Path

def task_write_text(path=Path("in_tracked.txt"), produces=Path("out.txt")):
produces.write_text("a")
"""
tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(content))
tmp_path.joinpath("in_tracked.txt").touch()
tmp_path.joinpath("tracked.txt").touch()

Expand All @@ -83,11 +61,9 @@ def test_clean_database_ignored(project, runner):
with enter_directory(project):
result = runner.invoke(cli, ["build"])
assert result.exit_code == ExitCode.OK
storage.create()
result = runner.invoke(cli, ["clean"])
assert result.exit_code == ExitCode.OK

assert result.exit_code == ExitCode.OK
text_without_linebreaks = result.output.replace("\n", "")
assert "to_be_deleted_file_1.txt" in text_without_linebreaks
assert "to_be_deleted_file_2.txt" in text_without_linebreaks
Expand Down
2 changes: 1 addition & 1 deletion tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def test_paths_are_relative_to_configuration_file(tmp_path):
session = build(paths=[Path("src")])
"""
tmp_path.joinpath("script.py").write_text(textwrap.dedent(source))
result = run_in_subprocess(("python", "script.py"), cwd=tmp_path)
result = run_in_subprocess(("uv", "run", "python", "script.py"), cwd=tmp_path)
assert result.exit_code == ExitCode.OK
assert "1 Succeeded" in result.stdout

Expand Down
21 changes: 12 additions & 9 deletions tests/test_execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@
from pytask import build
from pytask import cli
from tests.conftest import enter_directory
from tests.conftest import run_in_subprocess


@pytest.mark.xfail(sys.platform == "win32", reason="See #293.")
@pytest.mark.end_to_end
def test_python_m_pytask(tmp_path):
tmp_path.joinpath("task_module.py").write_text("def task_example(): pass")
subprocess.run(["python", "-m", "pytask", tmp_path.as_posix()], check=False)
result = run_in_subprocess(
("uv", "run", "python", "-m", "pytask", tmp_path.as_posix())
)
assert result.exit_code == ExitCode.OK


@pytest.mark.end_to_end
Expand Down Expand Up @@ -657,10 +660,10 @@ def task2() -> None: pass
sys.exit(session2.exit_code)
"""
tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source))
result = subprocess.run(
("python", tmp_path.joinpath("task_module.py").as_posix()), check=False
result = run_in_subprocess(
("uv", "run", "python", tmp_path.joinpath("task_module.py").as_posix())
)
assert result.returncode == ExitCode.OK
assert result.exit_code == ExitCode.OK


@pytest.mark.end_to_end
Expand Down Expand Up @@ -765,14 +768,14 @@ def task_example() -> Annotated[str, Path("file.txt")]:
"""
tmp_path.joinpath("task_example.py").write_text(textwrap.dedent(source))

result = subprocess.run(("pytask"), cwd=tmp_path) # noqa: PLW1510
assert result.returncode == ExitCode.OK
result = run_in_subprocess(("pytask",), cwd=tmp_path)
assert result.exit_code == ExitCode.OK

hashes = json.loads(tmp_path.joinpath(".pytask", "file_hashes.json").read_text())
assert len(hashes) == 2

result = subprocess.run(("pytask"), cwd=tmp_path) # noqa: PLW1510
assert result.returncode == ExitCode.OK
result = run_in_subprocess(("pytask",), cwd=tmp_path)
assert result.exit_code == ExitCode.OK

hashes_ = json.loads(tmp_path.joinpath(".pytask", "file_hashes.json").read_text())
assert hashes == hashes_
Expand Down
Loading