`anki.collection.Collection::export_anki_package` crashes if given a relative path with one component

(Frustratingly, the forum isn’t allowing me to include links in my post. I’ve attempted to work around that.)

I’m using the anki Python module. I found that calling export_anki_package crashes if I give it a relative path. Here’s a simple repro.

demo.py:

import tempfile
from pathlib import Path

import anki.collection

with tempfile.TemporaryDirectory() as tempdir:
    tempdir = Path(tempdir)
    col = anki.collection.Collection(str(tempdir / "temp.anki2"))
    col.export_anki_package(
        out_path="output.apkg",
        options=anki.collection.ExportAnkiPackageOptions(),
        limit=None,
    )
$ python demo.py
Traceback (most recent call last):
  [... some noise elided here ...]
  File "/nix/store/1w7swjzmjp7171yspih1q07a9vgacjzb-dev-env/lib/python3.14/site-packages/anki/_backend_generated.py", line 2037, in export_anki_package
    raw_bytes = self._run_command(37, 4, message.SerializeToString())
  File "/nix/store/1w7swjzmjp7171yspih1q07a9vgacjzb-dev-env/lib/python3.14/site-packages/anki/_backend.py", line 171, in _run_command
    raise backend_exception_to_pylib(err)
anki.errors.BackendIOError: Failed to open '': No such file or directory (os error 2)

I dug into this:

  • apkg::Collection::export_apkg calls atomic_rename [0] with target = "output.apkg"
  • atomic_rename attempts to open target’s parent [1], but the parent of a relative path with one component is "". From [2]:

    This means it returns Some("") for relative paths with one component.

The fix seems simple enough: change atomic_rename to call std::path::absolute on the given target before accessing its parent. I wouldn’t be surprised if this fixes similar bugs in other places that call atomic_rename. Happy to send in a PR implementing this if it’s acceptable.

[0]: https://github.com/ankitects/anki/blob/25.09.2/rslib/src/import_export/package/apkg/export.rs#L64
[1]: https://github.com/ankitects/anki/blob/25.09.2/rslib/io/src/lib.rs#L265-L266
[2]: https://doc.rust-lang.org/std/path/struct.Path.html#method.parent

I suggest limiting this to only the export methods. atomic_rename is also used when importing media files, so I think it’s better to avoid an extra system call (on Windows) for each file there if we can: absolute in std::path - Rust

I suggest limiting this to only the export methods.

I can do that, but I still suggest that we make some change to atomic_rename to handle this more gracefully. The current behavior is kind of confusing, and took me a bit of strace-ing to figure out. A few ideas:

  • If target is not absolute, raise an error (or call absolute).
  • If target.parent() == "", raise an error (or call absolute).

Of these 4 options, I like these 2 best:

  1. Reject non absolute paths: if target is not absolute, raise an error.
  2. if target.parent() == "", call absolute

I slightly prefer 2), as it removes the footgun entirely. 1) seems pretty reasonable as well, though. WDYT?