Skip to content

Path normalization not working for Paths generated by callable functions (Windows) #3830

@Eric-Nitschke

Description

@Eric-Nitschke

Snakemake version
snakemake-minimal 9.13.4 pyhdfd78af_1 bioconda

Describe the bug
The issue occurs when running snakemake rules that fulfill the following requirements (such as in PyPSA-Eur #1675 and the corresponding discussion here: open-energy-transition/pypsa-eur#70) on Windows:

  • a rule requires an input which is determined by a callable, such as a lambda function or unpack(). Called rule1 from now on.
  • the output of the rule that generates this input is not dependend on a callable. Instead the output is specified in the rule file via a pathlib.Path. Called rule2 from now on.

In io._init.py class _IOFile(str, AnnotatedStringInterface): inputs and outputs that are already a pathlib.Path are converted to a posix style string:

    def __new__(
        cls,
        file: Union[str, Path, "AnnotatedString", Callable],
        rule: Optional["snakemake.rules.Rule"],
    ):
        is_annotated = isinstance(file, AnnotatedString)
        is_callable = (
            isfunction(file) or ismethod(file) or (is_annotated and bool(file.callable))
        )
        if isinstance(file, Path):
            file = str(file.as_posix())

Callables are flagged, but not yet dealt with. Thus rule2 now "knows" that it has a posix style output, regardless of the OS.

The callables are evaluated in rules.py by expand_input() and _apply_wildcards(), which handle the outputs using concretize_iofile(). There pathlib.Path outputs from the callable are directly converted to strings, without converting them to posix style paths:

def concretize_iofile(f, wildcards, from_callable, incomplete):
            if from_callable is not None:
                if isinstance(f, Path):
                    f = str(f)
                iofile = IOFile(f, rule=self).apply_wildcards(wildcards)

As a result, rule1 is now looking for a Windows style input while rule2 only provides a posix style output.

This might be related to #3637.

Fix
This can be fixed through a minor adjustment to concretize_iofile():

def concretize_iofile(f, wildcards, from_callable, incomplete):
            if from_callable is not None:
                if isinstance(f, Path):
                    f = str(f.as_posix())
                iofile = IOFile(f, rule=self).apply_wildcards(wildcards)

I can create a PR for this.

Logs

Missing input files for rule add_electricity:
    output: resources/Versioning/Test01/networks/base_s_5_elec.nc
    wildcards: clusters=5
    affected files:
        data\costs\primary\v0.13.3\costs_2050.csv

Minimal example
Snakefile:

from pathlib import Path

# rule1
rule add_electricity:
    input:
        tech_costs=lambda w: Path("test/test") / "costs_2030.csv",

# rule2
rule retrieve_cost_data:
    output:
        costs=Path("test/test") / "costs_{year}.csv",

Command that will fail on Windows:

snakemake -c1 add_electricity -n

Snakefile that will also fail:

from pathlib import Path

def input_add_electricity(w):
    return {"tech_costs": Path("test/test") / "costs_2030.csv"}

# rule1
rule add_electricity:
    input:
        unpack(input_add_electricity),

# rule2
rule retrieve_cost_data:
    output:
        costs=Path("test/test") / "costs_{year}.csv",

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions