Skip to content

v0.1.2

Latest

Choose a tag to compare

@github-actions github-actions released this 17 Oct 05:30

Added

  • max_workers for recursive copy(), rm() and PathSyncer. copy() (and
    copy_into()), rm() and PathSyncer(...) take max_workers=None. None, the
    default, is one thread, exactly as before. An int of 2 or more hands the files
    and links of each directory to a pool of at most that many threads, which
    helps a backend that pays a round trip per entry. It applies to a class that sets
    the new class attribute _threadsafe = True (LocalPath and FTPPath do); any other
    class, including every class of another distribution until it sets the attribute,
    runs serially whatever was asked. Callbacks (progress, on_error, ignore_error,
    the sync hook, follow policies) run one at a time but on pool threads, in no
    fixed order, and the error that ends the operation is the first to happen. A value
    below 1 is ValueError, one that is not an int is TypeError. PathContract
    has a supports_concurrent_use switch for the matching test.
  • AsyncPath and AsyncFile: a path for use from an event loop. AsyncPath(path)
    wraps any Path; the pure members (name, parent, /, with_*(), match(), ...)
    are plain and give AsyncPath, every method that does I/O is a coroutine of the same
    name and arguments, iterdir(), glob(), rglob() and walk() return async iterators,
    and open() returns an object that is awaited or used with async with and gives an
    AsyncFile (read, readline, write, seek, ..., aclose()). They are exported
    from pathapi and pathapi.path. Each call runs the wrapped path's sync method on a
    worker thread (asyncio.to_thread()), so every backend can be used from a loop and the
    loop is not blocked; cancelling an await abandons the wait and the worker finishes the
    call. Recursive copy(), rm() and move() are one such call, so max_workers= is the
    sync fan-out. supports() and capabilities() of an AsyncPath answer for the wrapped
    path. A subclass gets its own class for every path derived from it, built by the
    classmethod AsyncPath._adopt(path) (cls(path)), which a subclass with another
    constructor overrides.
  • AsyncHTTPClient: the HTTP client for an event loop. pathapi.http.AsyncHTTPClient
    is the sibling of HTTPClient: the same constructor arguments, redirect and
    Authorization rules, timeout, deadline, reply limit and errors, with request()
    and redirect() as coroutines, aclose() and async with. It returns an
    AsyncHTTPResponse (await read(), iter_bytes() as an async iterator, await aclose()). It sends through an async transport, an object with async send() and
    async aclose(). The default one, ThreadedTransport, runs the synchronous transport
    PATHAPI_HTTP_TRANSPORT names on worker threads, so every synchronous transport works
    from a loop; a native one is declared in the entry-point group
    pathapi.http_async_transports and chosen by PATHAPI_HTTP_ASYNC_TRANSPORT (same
    rules and messages as the synchronous registry; async_transport_names(),
    get_async_transport() and default_async_transport()).
  • The async half of the transport suite. python -m pathapi.http.testing --http-async-transports NAME holds an async transport to the promises of the
    synchronous suite (test_async_transport.py, test_async_client.py). Without the
    option the async tests run over builtin when --http-transports is left out too,
    and are not collected when it names a transport: a distribution that runs
    --http-transports NAME gets the tests it got before.
  • pathapi.testing.AsyncPathContract. The pytest contract of AsyncPath over a
    backend: it takes the root fixture of PathContract and checks each coroutine
    against the sync path's result, with asyncio.run() and no extra test dependency. Its
    class attribute async_path_class (default AsyncPath) names the class under test, so a
    subclass of AsyncPath is tested with the paths it derives.

Changed

  • An unknown HTTP transport name is told where to look, not which package to
    install.
    get_transport() and PATHAPI_HTTP_TRANSPORT raise ValueError for a
    name no installed distribution declares in the group pathapi.http_transports,
    naming the group and suggesting pathapi-NAME as a name to look for. The error is
    a ValueError that is also an ImportError, so code that caught either still
    does. requests is no longer special: transport_names() lists it only when an
    installed distribution declares it. The refusal of a proxy that is not http://
    points at a transport of that group that supports it. Install the distribution
    that provides the transport.

Fixed

  • A nan or infinite HTTP timeout is refused with ValueError. timeout=float("nan")
    passed the check, and the built-in transport then failed in socket.settimeout() after
    it had opened the connection, which leaked the socket. Pass a positive finite number of
    seconds, or None.
  • move(overwrite=True) no longer destroys the old target when the move fails.
    An existing target was removed before the source was renamed or copied over it, so
    a failing rename or copy lost the target. The target now stays until the source
    has taken its place: a local file is replaced atomically, rename() is tried
    over a file elsewhere, a backend that refuses has the target renamed aside and put
    back on failure, and a copy and delete finishes the new file or tree beside the
    target first. A directory target is swapped in by rename where its class has one.
    If you list the directory while a move runs, you may see a hidden
    .NAME.<hex>.pathapi-tmp sibling.
  • copy(follow_symlinks=False, overwrite=True) of a link no longer destroys the old
    target when the new link cannot be made.
    The existing target was unlinked before
    symlink_to(), so a refused link lost it. Where the target's class can rename, the
    link is now made in a hidden sibling and renamed over the target; a class that
    cannot rename still unlinks first. A target class that cannot make links now raises
    NotImplementedError before anything is removed.

Full Changelog: v0.1.1...v0.1.2