diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2ffdef4..bce634c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,22 +27,18 @@ jobs: fail-fast: false matrix: os: ['ubuntu-latest', 'macos-latest', 'windows-latest'] - python-version: ['3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v4 - uses: r-lib/actions/setup-tinytex@v2 if: runner.os != 'Windows' - - uses: conda-incubator/setup-miniconda@v2 + - uses: actions/setup-python@v4 with: - auto-update-conda: false python-version: ${{ matrix.python-version }} - channels: conda-forge,nodefaults - miniforge-variant: Mambaforge - - - name: Install core dependencies. - shell: bash -l {0} - run: mamba install -c conda-forge tox-conda coverage + cache: pip + allow-prereleases: true + - run: pip install tox # Unit, integration, and end-to-end tests. @@ -51,7 +47,7 @@ jobs: run: tox -e pytest -- -m "unit or (not integration and not end_to_end)" --cov=./ --cov-report=xml -n auto - name: Upload coverage report for unit tests and doctests. - if: runner.os == 'Linux' && matrix.python-version == '3.8' + if: runner.os == 'Linux' && matrix.python-version == '3.10' shell: bash -l {0} run: bash <(curl -s https://codecov.io/bash) -F unit -c @@ -60,6 +56,6 @@ jobs: run: tox -e pytest -- -m end_to_end --cov=./ --cov-report=xml -n auto - name: Upload coverage reports of end-to-end tests. - if: runner.os == 'Linux' && matrix.python-version == '3.8' + if: runner.os == 'Linux' && matrix.python-version == '3.10' shell: bash -l {0} run: bash <(curl -s https://codecov.io/bash) -F end_to_end -c diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1072259..cc6405b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -70,14 +70,10 @@ repos: rev: 'v1.5.1' hooks: - id: mypy - args: [ - --no-strict-optional, - --ignore-missing-imports, - ] additional_dependencies: [ attrs>=21.3.0, click, - pytask, + pytask>=0.4.0, types-PyYAML, types-setuptools ] diff --git a/CHANGES.md b/CHANGES.md index c87f960..76d53af 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,10 @@ This is a record of all past pytask-latex releases and what went into them in re chronological order. Releases follow [semantic versioning](https://semver.org/) and all releases are available on [Anaconda.org](https://anaconda.org/conda-forge/pytask-latex). +## 0.4.0 - 2023-10-07 + +- {pull}`60` updates the package to use pytask v0.4.0. + ## 0.3.0 - 2022-12-xx - {pull}`49` removes support for INI configurations. diff --git a/README.md b/README.md index 5affd71..82b7bbf 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,11 @@ popular LaTeX distributions, like [TeX Live](https://www.tug.org/texlive/), Compiling your PDF can be as simple as writing the following task. ```python +from pathlib import Path import pytask -@pytask.mark.latex(script="document.tex", document="document.pdf") +@pytask.mark.latex(script=Path("document.tex"), document=Path("document.pdf")) def task_compile_latex_document(): pass ``` @@ -64,11 +65,24 @@ task module to the LaTeX file and the compiled document. ### Dependencies and Products -Dependencies and products can be added as with a normal pytask task using the -`@pytask.mark.depends_on` and `@pytask.mark.produces` decorators. which is explained in -this +Dependencies and products can be added as usual. Read this [tutorial](https://pytask-dev.readthedocs.io/en/stable/tutorials/defining_dependencies_products.html). +For example, with the `@pytask.task` decorator. (The choice of the kwarg name, here +`path`, is arbitrary.) + +```python +import pytask +from pytask import task +from pathlib import Path + + +@task(kwargs={"path": Path("path_to_another_dependency.tex")}) +@pytask.mark.latex(script=Path("document.tex"), document=Path("document.pdf")) +def task_compile_latex_document(): + pass +``` + ### Customizing the compilation pytask-latex uses latexmk by default to compile the document because it handles most @@ -77,8 +91,8 @@ decorator. ```python @pytask.mark.latex( - script="document.tex", - document="document.pdf", + script=Path("document.tex"), + document=Path("document.pdf"), compilation_steps="latexmk", ) def task_compile_latex_document(): @@ -95,8 +109,8 @@ from pytask_latex import compilation_steps as cs @pytask.mark.latex( - script="document.tex", - document="document.pdf", + script=Path("document.tex"), + document=Path("document.pdf"), compilation_steps=cs.latexmk( options=("--pdf", "--interaction=nonstopmode", "--synctex=1", "--cd") ), @@ -113,8 +127,8 @@ an example for generating a `.dvi`. ```python @pytask.mark.latex( - script="document.tex", - document="document.pdf", + script=Path("document.tex"), + document=Path("document.pdf"), compilation_steps=cs.latexmk( options=("--dvi", "--interaction=nonstopmode", "--synctex=1", "--cd") ), @@ -157,14 +171,15 @@ The following task compiles two latex documents. for i in range(2): @pytask.mark.task - @pytask.mark.latex(script=f"document_{i}.tex", document=f"document_{i}.pdf") + @pytask.mark.latex( + script=Path(f"document_{i}.tex"), document=Path(f"document_{i}.pdf") + ) def task_compile_latex_document(): pass ``` If you want to compile the same document with different command line options, you have -to include the latex decorator in the parametrization just like with -`@pytask.mark.depends_on` and `@pytask.mark.produces`. Pass a dictionary for possible +to include the latex decorator in the parametrization. Pass a dictionary for possible compilation steps and their options. ```python @@ -172,8 +187,8 @@ for format_ in ("pdf", "dvi"): @pytask.mark.task @pytask.mark.latex( - script="document.tex", - document=f"document.{format_}", + script=Path("document.tex"), + document=Path(f"document.{format_}"), compilation_steps=cs.latexmk( (f"--{format_}", "--interaction=nonstopmode", "--synctex=1", "--cd") ), diff --git a/environment.yml b/environment.yml index d747ffd..0802715 100644 --- a/environment.yml +++ b/environment.yml @@ -1,6 +1,8 @@ name: pytask-latex channels: + - conda-forge/label/pytask_rc + - conda-forge/label/pytask_parallel_rc - conda-forge - nodefaults @@ -11,10 +13,9 @@ dependencies: - toml # Package dependencies - - pytask >= 0.3 - - pytask-parallel >= 0.3 + - pytask >= 0.4.0 + - pytask-parallel >= 0.4.0 - latex-dependency-scanner >=0.1.1 - - pybaum >=0.1.1 # Misc - black diff --git a/pyproject.toml b/pyproject.toml index ec866d8..9975ad2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,7 @@ convention = "numpy" [tool.pytest.ini_options] # Do not add src since it messes with the loading of pytask-parallel as a plugin. -testpaths = ["test"] +testpaths = ["tests"] markers = [ "wip: Tests that are work-in-progress.", "unit: Flag for unit tests which target mainly a single function.", diff --git a/setup.cfg b/setup.cfg index 56aadba..70ad8eb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,10 +24,9 @@ project_urls = [options] packages = find: install_requires = - click latex-dependency-scanner>=0.1.1 - pybaum>=0.1.1 - pytask>=0.3 + pluggy>=1.0.0 + pytask>=0.4.0 python_requires = >=3.8 include_package_data = True package_dir = =src diff --git a/src/pytask_latex/collect.py b/src/pytask_latex/collect.py index 3819a18..303658f 100644 --- a/src/pytask_latex/collect.py +++ b/src/pytask_latex/collect.py @@ -1,28 +1,33 @@ """Collect tasks.""" from __future__ import annotations -import functools +import warnings from pathlib import Path from subprocess import CalledProcessError -from types import FunctionType from typing import Any from typing import Callable from typing import Sequence import latex_dependency_scanner as lds -from pybaum.tree_util import tree_map -from pytask import depends_on -from pytask import FilePathNode from pytask import has_mark from pytask import hookimpl +from pytask import is_task_function from pytask import Mark from pytask import MetaNode +from pytask import NodeInfo from pytask import NodeNotCollectedError -from pytask import parse_nodes -from pytask import produces +from pytask import parse_dependencies_from_task_function +from pytask import parse_products_from_task_function +from pytask import PathNode +from pytask import PPathNode +from pytask import PTask +from pytask import PTaskWithPath from pytask import remove_marks from pytask import Session from pytask import Task +from pytask import TaskWithoutPath +from pytask.tree_util import tree_leaves +from pytask.tree_util import tree_map from pytask_latex import compilation_steps as cs from pytask_latex.utils import to_list @@ -56,9 +61,10 @@ def latex( def compile_latex_document( - compilation_steps: list[Callable[..., Any]], - path_to_tex: Path, - path_to_document: Path, + _compilation_steps: list[Callable[..., Any]], + _path_to_tex: Path, + _path_to_document: Path, + **kwargs: Any, # noqa: ARG001 ) -> None: """Compile a LaTeX document iterating over compilations steps. @@ -66,26 +72,26 @@ def compile_latex_document( """ try: - for step in compilation_steps: - step(path_to_tex=path_to_tex, path_to_document=path_to_document) + for step in _compilation_steps: + step(path_to_tex=_path_to_tex, path_to_document=_path_to_document) except CalledProcessError as e: raise RuntimeError(f"Compilation step {step.__name__} failed.") from e @hookimpl def pytask_collect_task( - session: Session, path: Path, name: str, obj: Any -) -> Task | None: + session: Session, path: Path | None, name: str, obj: Any +) -> PTask | None: """Perform some checks.""" __tracebackhide__ = True if ( (name.startswith("task_") or has_mark(obj, "task")) - and callable(obj) + and is_task_function(obj) and has_mark(obj, "latex") ): + # Parse the @pytask.mark.latex decorator. obj, marks = remove_marks(obj, "latex") - if len(marks) > 1: raise ValueError( f"Task {name!r} has multiple @pytask.mark.latex marks, but only one is " @@ -93,67 +99,112 @@ def pytask_collect_task( ) latex_mark = marks[0] script, document, compilation_steps = latex(**latex_mark.kwargs) - parsed_compilation_steps = _parse_compilation_steps(compilation_steps) obj.pytask_meta.markers.append(latex_mark) - dependencies = parse_nodes(session, path, name, obj, depends_on) - products = parse_nodes(session, path, name, obj, produces) + # Collect the nodes in @pytask.mark.latex and validate them. + path_nodes = Path.cwd() if path is None else path.parent - markers = obj.pytask_meta.markers if hasattr(obj, "pytask_meta") else [] - kwargs = obj.pytask_meta.kwargs if hasattr(obj, "pytask_meta") else {} - - task = Task( - base_name=name, - path=path, - function=_copy_func(compile_latex_document), # type: ignore[arg-type] - depends_on=dependencies, - produces=products, - markers=markers, - kwargs=kwargs, - ) + if isinstance(script, str): + warnings.warn( + "Passing a string for the latex parameter 'script' is deprecated. " + "Please, use a pathlib.Path instead.", + stacklevel=1, + ) + script = Path(script) script_node = session.hook.pytask_collect_node( - session=session, path=path, node=script + session=session, + path=path_nodes, + node_info=NodeInfo( + arg_name="script", path=(), value=script, task_path=path, task_name=name + ), ) + + if isinstance(document, str): + warnings.warn( + "Passing a string for the latex parameter 'document' is deprecated. " + "Please, use a pathlib.Path instead.", + stacklevel=1, + ) + document = Path(document) + document_node = session.hook.pytask_collect_node( - session=session, path=path, node=document + session=session, + path=path_nodes, + node_info=NodeInfo( + arg_name="document", + path=(), + value=document, + task_path=path, + task_name=name, + ), ) if not ( - isinstance(script_node, FilePathNode) and script_node.value.suffix == ".tex" + isinstance(script_node, PathNode) and script_node.path.suffix == ".tex" ): raise ValueError( "The 'script' keyword of the @pytask.mark.latex decorator must point " - "to LaTeX file with the .tex suffix." + f"to LaTeX file with the .tex suffix, but it is {script_node}." ) if not ( - isinstance(document_node, FilePathNode) - and document_node.value.suffix in (".pdf", ".ps", ".dvi") + isinstance(document_node, PathNode) + and document_node.path.suffix in (".pdf", ".ps", ".dvi") ): raise ValueError( "The 'document' keyword of the @pytask.mark.latex decorator must point " "to a .pdf, .ps or .dvi file." ) - if isinstance(task.depends_on, dict): - task.depends_on["__script"] = script_node - else: - task.depends_on = {0: task.depends_on, "__script": script_node} - if isinstance(task.produces, dict): - task.produces["__document"] = document_node - else: - task.produces = {0: task.produces, "__document": document_node} + compilation_steps_node = session.hook.pytask_collect_node( + session=session, + path=path_nodes, + node_info=NodeInfo( + arg_name="_compilation_steps", + path=(), + value=parsed_compilation_steps, + task_path=path, + task_name=name, + ), + ) - task.function = functools.partial( - task.function, - compilation_steps=parsed_compilation_steps, - path_to_tex=script_node.path, - path_to_document=document_node.path, + # Parse other dependencies and products. + dependencies = parse_dependencies_from_task_function( + session, path, name, path_nodes, obj + ) + products = parse_products_from_task_function( + session, path, name, path_nodes, obj ) + # Add script and document + dependencies["_path_to_tex"] = script_node + dependencies["_compilation_steps"] = compilation_steps_node + products["_path_to_document"] = document_node + + markers = obj.pytask_meta.markers if hasattr(obj, "pytask_meta") else [] + + task: PTask + if path is None: + task = TaskWithoutPath( + name=name, + function=compile_latex_document, + depends_on=dependencies, + produces=products, + markers=markers, + ) + else: + task = Task( + base_name=name, + path=path, + function=compile_latex_document, + depends_on=dependencies, + produces=products, + markers=markers, + ) + if session.config["infer_latex_dependencies"]: task = _add_latex_dependencies_retroactively(task, session) @@ -161,7 +212,7 @@ def pytask_collect_task( return None -def _add_latex_dependencies_retroactively(task: Task, session: Session) -> Task: +def _add_latex_dependencies_retroactively(task: PTask, session: Session) -> PTask: """Add dependencies from LaTeX document to task. Unfortunately, the dependencies have to be added retroactively, after the task has @@ -182,24 +233,46 @@ def _add_latex_dependencies_retroactively(task: Task, session: Session) -> Task: """ # Scan the LaTeX document for included files. - latex_dependencies = set( - lds.scan(task.depends_on["__script"].path) # type: ignore[attr-defined] - ) + try: + latex_dependencies = set( + lds.scan(task.depends_on["_path_to_tex"].path) # type: ignore[attr-defined] + ) + except Exception: # noqa: BLE001 + warnings.warn( + "pytask-latex failed to scan latex document for dependencies.", stacklevel=1 + ) + latex_dependencies = set() # Remove duplicated dependencies which have already been added by the user and those # which do not exist. existing_paths = { - i.path for i in task.depends_on.values() if isinstance(i, FilePathNode) + i.path for i in tree_leaves(task.depends_on) if isinstance(i, PPathNode) } new_deps = latex_dependencies - existing_paths new_existing_deps = {i for i in new_deps if i.exists()} new_numbered_deps = dict(enumerate(new_existing_deps)) # Collect new dependencies and add them to the task. + task_path = task.path if isinstance(task, PTaskWithPath) else None + path_nodes = task.path.parent if isinstance(task, PTaskWithPath) else Path.cwd() + collected_dependencies = tree_map( - lambda x: _collect_node(session, task.path, task.name, x), new_numbered_deps + lambda x: _collect_node( + session, + path_nodes, + NodeInfo( + arg_name="_scanned_dependencies", + path=(), + value=x, + task_path=task_path, + task_name=task.name, + ), + ), + new_numbered_deps, ) - task.depends_on["__scanned_dependencies"] = collected_dependencies + task.depends_on[ + "_scanned_dependencies" + ] = collected_dependencies # type: ignore[assignment] # Mark the task as being delayed to avoid conflicts with unmatched dependencies. task.markers.append(Mark("try_last", (), {})) @@ -207,52 +280,11 @@ def _add_latex_dependencies_retroactively(task: Task, session: Session) -> Task: return task -def _copy_func(func: FunctionType) -> FunctionType: - """Create a copy of a function. - - Based on https://stackoverflow.com/a/13503277/7523785. - - Example - ------- - >>> def _func(): pass - >>> copied_func = _copy_func(_func) - >>> _func is copied_func - False - - """ - new_func = FunctionType( - func.__code__, - func.__globals__, - name=func.__name__, - argdefs=func.__defaults__, - closure=func.__closure__, - ) - new_func = functools.update_wrapper(new_func, func) - new_func.__kwdefaults__ = func.__kwdefaults__ - return new_func - - def _collect_node( - session: Session, path: Path, name: str, node: str | Path + session: Session, path: Path, node_info: NodeInfo ) -> dict[str, MetaNode]: """Collect nodes for a task. - Parameters - ---------- - session : pytask.Session - The session. - path : Path - The path to the task whose nodes are collected. - name : str - The name of the task. - nodes : Dict[str, Union[str, Path]] - A dictionary of nodes parsed from the ``depends_on`` or ``produces`` markers. - - Returns - ------- - Dict[str, MetaNode] - A dictionary of node names and their paths. - Raises ------ NodeNotCollectedError @@ -260,19 +292,22 @@ def _collect_node( """ collected_node = session.hook.pytask_collect_node( - session=session, path=path, node=node + session=session, path=path, node_info=node_info ) if collected_node is None: raise NodeNotCollectedError( - f"{node!r} cannot be parsed as a dependency or product for task " - f"{name!r} in {path!r}." + f"{node_info.arg_name!r} cannot be parsed as a dependency or product for " + f"task {node_info.task_name!r} in {node_info.task_path!r}." ) return collected_node def _parse_compilation_steps( - compilation_steps: str | Callable[..., Any] | Sequence[str | Callable[..., Any]] + compilation_steps: str + | Callable[..., Any] + | Sequence[str | Callable[..., Any]] + | None ) -> list[Callable[..., Any]]: """Parse compilation steps.""" __tracebackhide__ = True diff --git a/src/pytask_latex/execute.py b/src/pytask_latex/execute.py index 7603bb1..504e2d8 100644 --- a/src/pytask_latex/execute.py +++ b/src/pytask_latex/execute.py @@ -5,11 +5,11 @@ from pytask import has_mark from pytask import hookimpl -from pytask import Task +from pytask import PTask @hookimpl -def pytask_execute_task_setup(task: Task) -> None: +def pytask_execute_task_setup(task: PTask) -> None: """Check that latexmk is found on the PATH if a LaTeX task should be executed.""" if has_mark(task, "latex") and shutil.which("latexmk") is None: raise RuntimeError( diff --git a/src/pytask_latex/parametrize.py b/src/pytask_latex/parametrize.py deleted file mode 100644 index 6bd5266..0000000 --- a/src/pytask_latex/parametrize.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Parametrize tasks.""" -from __future__ import annotations - -from typing import Any - -import pytask -from pytask import hookimpl - - -@hookimpl -def pytask_parametrize_kwarg_to_marker(obj: Any, kwargs: dict[str, Any]) -> None: - """Register kwargs as latex marker.""" - if callable(obj) and "latex" in kwargs: - pytask.mark.latex(**kwargs.pop("latex"))(obj) diff --git a/src/pytask_latex/plugin.py b/src/pytask_latex/plugin.py index 52adddf..65d7287 100644 --- a/src/pytask_latex/plugin.py +++ b/src/pytask_latex/plugin.py @@ -6,7 +6,6 @@ from pytask_latex import collect from pytask_latex import config from pytask_latex import execute -from pytask_latex import parametrize @hookimpl @@ -15,4 +14,3 @@ def pytask_add_hooks(pm: PluginManager) -> None: pm.register(collect) pm.register(config) pm.register(execute) - pm.register(parametrize) diff --git a/tests/conftest.py b/tests/conftest.py index 1ae27ba..b1d8e7a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,9 @@ import os import shutil import sys +from contextlib import contextmanager from pathlib import Path +from typing import Callable import pytest from click.testing import CliRunner @@ -23,6 +25,64 @@ ) +class SysPathsSnapshot: + """A snapshot for sys.path.""" + + def __init__(self) -> None: + self.__saved = list(sys.path), list(sys.meta_path) + + def restore(self) -> None: + sys.path[:], sys.meta_path[:] = self.__saved + + +class SysModulesSnapshot: + """A snapshot for sys.modules.""" + + def __init__(self, preserve: Callable[[str], bool] | None = None) -> None: + self.__preserve = preserve + self.__saved = dict(sys.modules) + + def restore(self) -> None: + if self.__preserve: + self.__saved.update( + (k, m) for k, m in sys.modules.items() if self.__preserve(k) + ) + sys.modules.clear() + sys.modules.update(self.__saved) + + +@contextmanager +def restore_sys_path_and_module_after_test_execution(): + sys_path_snapshot = SysPathsSnapshot() + sys_modules_snapshot = SysModulesSnapshot() + yield + sys_modules_snapshot.restore() + sys_path_snapshot.restore() + + +@pytest.fixture(autouse=True) +def _restore_sys_path_and_module_after_test_execution(): + """Restore sys.path and sys.modules after every test execution. + + This fixture became necessary because most task modules in the tests are named + `task_example`. Since the change in #424, the same module is not reimported which + solves errors with parallelization. At the same time, modules with the same name in + the tests are overshadowing another and letting tests fail. + + The changes to `sys.path` might not be necessary to restore, but we do it anyways. + + """ + with restore_sys_path_and_module_after_test_execution(): + yield + + +class CustomCliRunner(CliRunner): + def invoke(self, *args, **kwargs): + """Restore sys.path and sys.modules after an invocation.""" + with restore_sys_path_and_module_after_test_execution(): + return super().invoke(*args, **kwargs) + + @pytest.fixture() def runner(): - return CliRunner() + return CustomCliRunner() diff --git a/tests/test_config.py b/tests/test_config.py index d7b2ea9..423359c 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,10 +1,10 @@ from __future__ import annotations import pytest -from pytask import main +from pytask import build @pytest.mark.end_to_end() def test_marker_is_configured(tmp_path): - session = main({"paths": tmp_path}) + session = build(paths=tmp_path) assert "latex" in session.config["markers"] diff --git a/tests/test_execute.py b/tests/test_execute.py index 1492bd6..1726429 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -4,9 +4,9 @@ from pathlib import Path import pytest +from pytask import build from pytask import cli from pytask import ExitCode -from pytask import main from pytask import Mark from pytask import Task from pytask_latex.execute import pytask_execute_task_setup @@ -122,9 +122,11 @@ def test_compile_w_bibliography(runner, tmp_path): """Compile a LaTeX document with bibliography.""" task_source = """ import pytask + from pytask import task + from pathlib import Path + @task(kwargs={"path": Path("references.bib")}) @pytask.mark.latex(script="in_w_bib.tex", document="out_w_bib.pdf") - @pytask.mark.depends_on("references.bib") def task_compile_document(): pass """ @@ -150,9 +152,8 @@ def task_compile_document(): } """ tmp_path.joinpath("references.bib").write_text(textwrap.dedent(bib_source)) - - session = runner.invoke(cli, [tmp_path.as_posix()]) - assert session.exit_code == ExitCode.OK + result = runner.invoke(cli, [tmp_path.as_posix()]) + assert result.exit_code == ExitCode.OK @needs_latexmk @@ -182,7 +183,7 @@ def task_compile_document(): "pytask_latex.execute.shutil.which", lambda x: None # noqa: ARG005 ) - session = main({"paths": tmp_path}) + session = build(paths=tmp_path) assert session.exit_code == ExitCode.FAILED assert isinstance(session.execution_reports[0].exc_info[1], RuntimeError) @@ -274,7 +275,7 @@ def task_compile_document(): tmp_path.joinpath("document.tex").write_text(textwrap.dedent(latex_source)) tmp_path.joinpath("in.txt").touch() - session = main({"paths": tmp_path}) + session = build(paths=tmp_path) assert session.exit_code == ExitCode.COLLECTION_FAILED assert isinstance(session.collection_reports[0].exc_info[1], ValueError) @@ -317,7 +318,7 @@ def task_compile_document(): """ tmp_path.joinpath("sub", "resources", "content.tex").write_text(resources) - session = main({"paths": tmp_path}) + session = build(paths=tmp_path) assert session.exit_code == ExitCode.OK assert len(session.tasks) == 1 @@ -352,7 +353,7 @@ def task_compile_document(): """ tmp_path.joinpath("sub", "document.tex").write_text(textwrap.dedent(latex_source)) - session = main({"paths": tmp_path}) + session = build(paths=tmp_path) assert session.exit_code == ExitCode.FAILED assert len(session.tasks) == 1 assert isinstance(session.execution_reports[0].exc_info[1], RuntimeError) @@ -555,3 +556,31 @@ def compile_document(): result = runner.invoke(cli, [tmp_path.as_posix()]) assert result.exit_code == ExitCode.OK + + +@needs_latexmk +@skip_on_github_actions_with_win +@pytest.mark.end_to_end() +def test_use_task_without_path(tmp_path): + task_source = """ + import pytask + from pytask import task + + task_compile_document = pytask.mark.latex( + script="document.tex", document="document.pdf" + )( + task()(lambda *x: None) + ) + """ + tmp_path.joinpath("task_dummy.py").write_text(textwrap.dedent(task_source)) + + latex_source = r""" + \documentclass{report} + \begin{document} + Ein Fuchs muss tun, was ein Fuchs tun muss. Luxus und Ruhm und rulen bis zum + Schluss. + \end{document} + """ + tmp_path.joinpath("document.tex").write_text(textwrap.dedent(latex_source)) + session = build(paths=tmp_path) + assert session.exit_code == ExitCode.OK diff --git a/tests/test_latex_dependency_scanner.py b/tests/test_latex_dependency_scanner.py index c0e292f..5a758a3 100644 --- a/tests/test_latex_dependency_scanner.py +++ b/tests/test_latex_dependency_scanner.py @@ -3,8 +3,8 @@ import textwrap import pytest +from pytask import build from pytask import ExitCode -from pytask import main from tests.conftest import needs_latexmk from tests.conftest import skip_on_github_actions_with_win @@ -37,10 +37,10 @@ def task_compile_document(): f"[tool.pytask.ini_options]\ninfer_latex_dependencies = {infer_dependencies}" ) - session = main({"paths": tmp_path}) + session = build(paths=tmp_path) assert session.exit_code == ExitCode.OK assert len(session.tasks) == 1 if infer_dependencies == "true": - assert len(session.tasks[0].depends_on) == 2 + assert len(session.tasks[0].depends_on) == 3 else: - assert len(session.tasks[0].depends_on) == 1 + assert len(session.tasks[0].depends_on) == 2 diff --git a/tests/test_parallel.py b/tests/test_parallel.py index 51656b5..9ab0bf6 100644 --- a/tests/test_parallel.py +++ b/tests/test_parallel.py @@ -3,7 +3,6 @@ import os import textwrap -import time import pytest from pytask import cli @@ -30,60 +29,6 @@ ) -@xfail_on_remote -@needs_latexmk -@skip_on_github_actions_with_win -@pytest.mark.end_to_end() -def test_parallel_parametrization_over_source_files_w_parametrize(runner, tmp_path): - source = """ - import pytask - - @pytask.mark.parametrize( - "latex", - [ - {"script": "document_1.tex", "document": "document_1.pdf"}, - {"script": "document_2.tex", "document": "document_2.pdf"} - ], - ) - def task_compile_latex_document(): - pass - """ - tmp_path.joinpath("task_dummy.py").write_text(textwrap.dedent(source)) - - latex_source = r""" - \documentclass{report} - \begin{document} - He said yeah. - \end{document} - """ - tmp_path.joinpath("document_1.tex").write_text(textwrap.dedent(latex_source)) - - latex_source = r""" - \documentclass{report} - \begin{document} - You better come out with your hands up. - \end{document} - """ - tmp_path.joinpath("document_2.tex").write_text(textwrap.dedent(latex_source)) - - start = time.time() - result = runner.invoke(cli, [tmp_path.as_posix()]) - - assert result.exit_code == ExitCode.OK - duration_normal = time.time() - start - - for name in ("document_1.pdf", "document_2.pdf"): - tmp_path.joinpath(name).unlink() - - start = time.time() - result = runner.invoke(cli, [tmp_path.as_posix(), "-n", 2]) - - assert result.exit_code == ExitCode.OK - duration_parallel = time.time() - start - - assert duration_parallel < duration_normal - - @xfail_on_remote @needs_latexmk @skip_on_github_actions_with_win @@ -116,82 +61,8 @@ def task_compile_latex_document(): \end{document} """ tmp_path.joinpath("document_2.tex").write_text(textwrap.dedent(latex_source)) - - start = time.time() - result = runner.invoke(cli, [tmp_path.as_posix()]) - - assert result.exit_code == ExitCode.OK - duration_normal = time.time() - start - - for name in ("document_1.pdf", "document_2.pdf"): - tmp_path.joinpath(name).unlink() - - start = time.time() result = runner.invoke(cli, [tmp_path.as_posix(), "-n", 2]) - assert result.exit_code == ExitCode.OK - duration_parallel = time.time() - start - - assert duration_parallel < duration_normal - - -@xfail_on_remote -@needs_latexmk -@skip_on_github_actions_with_win -@pytest.mark.end_to_end() -def test_parallel_parametrization_over_source_file_w_parametrize(runner, tmp_path): - source = """ - import pytask - from pytask_latex import compilation_steps as cs - - @pytask.mark.parametrize( - "latex", - [ - { - "script": "document.tex", - "document": "document.pdf", - "compilation_steps": cs.latexmk( - ("--pdf", "--interaction=nonstopmode", "--synctex=1", "--cd") - ), - }, - { - "script": "document.tex", - "document": "document.dvi", - "compilation_steps": cs.latexmk( - ("--dvi", "--interaction=nonstopmode", "--synctex=1", "--cd") - ), - } - ] - ) - def task_compile_latex_document(): - pass - """ - tmp_path.joinpath("task_dummy.py").write_text(textwrap.dedent(source)) - - latex_source = r""" - \documentclass{report} - \begin{document} - Ma il mio mistero e chiuso in me - \end{document} - """ - tmp_path.joinpath("document.tex").write_text(textwrap.dedent(latex_source)) - - start = time.time() - result = runner.invoke(cli, [tmp_path.as_posix()]) - - assert result.exit_code == ExitCode.OK - duration_normal = time.time() - start - - for name in ("document.pdf", "document.dvi"): - tmp_path.joinpath(name).unlink() - - start = time.time() - result = runner.invoke(cli, [tmp_path.as_posix(), "-n", 2]) - - assert result.exit_code == ExitCode.OK - duration_parallel = time.time() - start - - assert duration_parallel < duration_normal @xfail_on_remote @@ -225,20 +96,5 @@ def task_compile_latex_document(): \end{document} """ tmp_path.joinpath("document.tex").write_text(textwrap.dedent(latex_source)) - - start = time.time() - result = runner.invoke(cli, [tmp_path.as_posix()]) - - assert result.exit_code == ExitCode.OK - duration_normal = time.time() - start - - for name in ("document.pdf", "document.dvi"): - tmp_path.joinpath(name).unlink() - - start = time.time() result = runner.invoke(cli, [tmp_path.as_posix(), "-n", 2]) - assert result.exit_code == ExitCode.OK - duration_parallel = time.time() - start - - assert duration_parallel < duration_normal diff --git a/tests/test_parametrize.py b/tests/test_parametrize.py index 3673407..905b50e 100644 --- a/tests/test_parametrize.py +++ b/tests/test_parametrize.py @@ -3,48 +3,13 @@ import textwrap import pytest +from pytask import build from pytask import ExitCode -from pytask import main from tests.conftest import needs_latexmk from tests.conftest import skip_on_github_actions_with_win -@needs_latexmk -@skip_on_github_actions_with_win -@pytest.mark.end_to_end() -def test_parametrized_compilation_of_latex_documents_w_parametrize(tmp_path): - task_source = """ - import pytask - - @pytask.mark.parametrize("latex", [ - {"script": "document_1.tex", "document": "document_1.pdf"}, - {"script": "document_2.tex", "document": "document_2.pdf"}, - ]) - def task_compile_latex_document(): - pass - """ - tmp_path.joinpath("task_dummy.py").write_text(textwrap.dedent(task_source)) - - for name, content in ( - ("document_1.tex", "Like a worn out recording"), - ("document_2.tex", "Of a favorite song"), - ): - latex_source = rf""" - \documentclass{{report}} - \begin{{document}} - {content} - \end{{document}} - """ - tmp_path.joinpath(name).write_text(textwrap.dedent(latex_source)) - - session = main({"paths": tmp_path}) - - assert session.exit_code == ExitCode.OK - assert tmp_path.joinpath("document_1.pdf").exists() - assert tmp_path.joinpath("document_2.pdf").exists() - - @needs_latexmk @skip_on_github_actions_with_win @pytest.mark.end_to_end() @@ -73,60 +38,13 @@ def task_compile_latex_document(): """ tmp_path.joinpath(name).write_text(textwrap.dedent(latex_source)) - session = main({"paths": tmp_path}) + session = build(paths=tmp_path) assert session.exit_code == ExitCode.OK assert tmp_path.joinpath("document_1.pdf").exists() assert tmp_path.joinpath("document_2.pdf").exists() -@needs_latexmk -@skip_on_github_actions_with_win -@pytest.mark.end_to_end() -def test_parametrizing_latex_options_w_parametrize(tmp_path): - task_source = """ - import pytask - from pytask_latex import compilation_steps as cs - - @pytask.mark.parametrize( - "latex", - [ - { - "script": "document.tex", - "document": "document.pdf", - "compilation_steps": cs.latexmk( - ("--pdf", "--interaction=nonstopmode", "--synctex=1", "--cd") - ), - }, - { - "script": "document.tex", - "document": "document.dvi", - "compilation_steps": cs.latexmk( - ("--dvi", "--interaction=nonstopmode", "--synctex=1", "--cd") - ), - } - ] - ) - def task_compile_latex_document(): - pass - """ - tmp_path.joinpath("task_dummy.py").write_text(textwrap.dedent(task_source)) - - latex_source = r""" - \documentclass{report} - \begin{document} - I can't stop this feeling. Deep inside of me. - \end{document} - """ - tmp_path.joinpath("document.tex").write_text(textwrap.dedent(latex_source)) - - session = main({"paths": tmp_path}) - - assert session.exit_code == ExitCode.OK - assert tmp_path.joinpath("document.pdf").exists() - assert tmp_path.joinpath("document.dvi").exists() - - @needs_latexmk @skip_on_github_actions_with_win @pytest.mark.end_to_end() @@ -158,7 +76,7 @@ def compile_latex_document(): """ tmp_path.joinpath("document.tex").write_text(textwrap.dedent(latex_source)) - session = main({"paths": tmp_path}) + session = build(paths=tmp_path) assert session.exit_code == ExitCode.OK assert tmp_path.joinpath("document.pdf").exists() diff --git a/tox.ini b/tox.ini index 91a551a..0341bd7 100644 --- a/tox.ini +++ b/tox.ini @@ -6,15 +6,15 @@ usedevelop = true passenv = CI [testenv:pytest] -conda_channels = - conda-forge - nodefaults -conda_deps = - pytask >=0.3 - pytask-parallel >=0.3 - latex-dependency-scanner +deps = + # pytest pytest pytest-cov pytest-xdist + + # Package + pytask>=0.4.0 + pytask-parallel>=0.4.0 + latex-dependency-scanner>=0.1.1 commands = pytest {posargs}