Repository: nvim-lua/plenary.nvim
Pull request #167 (2021-06-07 04:21:50)

[BLOCKER] Fixed raw body passing for the cURL wrapper
Hey all!

at first: let me thank you for this awesome library!

I found a bug while trying to pass a string as body to the cURL wrapper.

As we can see in [this line](https://github.com/nvim-lua/plenary.nvim/blame/22127c47454e3b09be38b3a4bbe3edf42e7b3fc7/lua/plenary/curl.lua#L142) the argument for passing extra command line arguments to curl was named `raw_args` and [the option](https://github.com/nvim-lua/plenary.nvim/blame/22127c47454e3b09be38b3a4bbe3edf42e7b3fc7/lua/plenary/curl.lua#L137) for setting the raw body was named `raw`.

As we can see in [this commit](https://github.com/nvim-lua/plenary.nvim/blob/08c0eabcb1/lua/plenary/curl.lua#L171) by @tami5 it changed. The name for the option for passing extra arguments to cURL was renamed to `raw`!

Source: https://github.com/nvim-lua/plenary.nvim/commit/08c0eabcb1fdcc5b72f60c3a328ae8eeb7ad374e#diff-ee99434c11a1e82e1ffd7308db38479b4648e17027be731e1ef592a055df0c89R171

For backwards compatibility I renamed the internal option name to `raw_body` and adjusted the call to the `parse.raw_body` function.

YannickFricke: cc @tjdevries 
tjdevries: @tami5 do you think you could take a look at this? I didn't review this code much -- but I can if you don't have time.

Patch:
diff --git a/README.md b/README.md
index 81ad0ee..1e4ee71 100644
--- a/README.md
+++ b/README.md
@@ -206,6 +206,23 @@ Window helper functions to wrap some of the more difficult cases. Particularly f
 
 Status: WIP
 
+### plenary.collections
+
+Contains pure lua implementations for various standard collections.
+
+```lua
+local List = require 'plenary.collections.py_list'
+
+local myList = List { 9, 14, 32, 5 }
+
+for i, v in myList:iter() do
+    print(i, v)
+end
+
+```
+
+Status: WIP
+
 ### Troubleshooting
 
 If you're having trouble / things are hanging / other problems:
diff --git a/data/plenary/filetypes/builtin.lua b/data/plenary/filetypes/builtin.lua
index 600a6f5..9947aba 100644
--- a/data/plenary/filetypes/builtin.lua
+++ b/data/plenary/filetypes/builtin.lua
@@ -3,6 +3,7 @@ local shebang_fts = {
   ['sh'] = 'sh',
   ['bash'] = 'sh',
   ['zsh'] = 'zsh',
+  ['fish'] = 'fish',
   ['python'] = 'python',
   ['python2'] = 'python',
   ['python3'] = 'python',
@@ -32,6 +33,7 @@ return {
     ['coffee'] = 'coffee',
     ['_coffee'] = 'coffee',
     ['nix'] = 'nix',
+    ['erb'] = 'eruby',
   },
   file_name = {
     ['cakefile'] = 'coffee',
diff --git a/lua/plenary/async/api.lua b/lua/plenary/async/api.lua
new file mode 100644
index 0000000..4a7e84a
--- /dev/null
+++ b/lua/plenary/async/api.lua
@@ -0,0 +1,13 @@
+local a = require('plenary.async.async')
+local util = require('plenary.async.util')
+
+return setmetatable({}, {__index = function(t, k)
+  return function(...)
+    -- if we are in a fast event await the scheduler
+    if vim.in_fast_event() then
+      util.scheduler()
+    end
+
+    vim.api[k](...)
+  end
+end})
diff --git a/lua/plenary/async/async.lua b/lua/plenary/async/async.lua
new file mode 100644
index 0000000..490dfad
--- /dev/null
+++ b/lua/plenary/async/async.lua
@@ -0,0 +1,112 @@
+local co = coroutine
+local vararg = require('plenary.vararg')
+local errors = require('plenary.errors')
+local traceback_error = errors.traceback_error
+local f = require('plenary.functional')
+
+local M = {}
+
+---because we can't store varargs
+local function callback_or_next(step, thread, callback, ...)
+  local stat = f.first(...)
+
+  if not stat then
+    error(string.format("The coroutine failed with this message: %s", f.second(...)))
+  end
+
+  if co.status(thread) == "dead" then
+    if callback == nil then return end
+    callback(select(2, ...))
+  else
+    local returned_function = f.second(...)
+    assert(type(returned_function) == "function", "type error :: expected func")
+    local stat, msg = pcall(returned_function, vararg.rotate(step, select(3, ...)))
+    if not stat then
+      error(('Failed to call leaf async function: %s'):format(msg))
+    end
+  end
+end
+
+---Executes a future with a callback when it is done
+---@param async_function Future: the future to execute
+---@param callback function: the callback to call when done
+local execute = function(async_function, callback)
+  assert(type(async_function) == "function", "type error :: expected func")
+
+  local thread = co.create(async_function)
+
+  local step
+  step = function(...)
+    callback_or_next(step, thread, callback, co.resume(thread, ...))
+  end
+
+  step()
+end
+
+local add_leaf_function
+do
+  ---A table to store all leaf async functions
+  _PlenaryLeafTable = setmetatable({}, {
+    __mode = "k",
+  })
+
+  add_leaf_function = function(async_func)
+    assert(_PlenaryLeafTable[async_func] == nil, "Async function should not already be in the table")
+    _PlenaryLeafTable[async_func] = true
+  end
+
+  function M.is_leaf_function(async_func)
+    return _PlenaryLeafTable[async_func] ~= nil
+  end
+end
+
+---Creates an async function with a callback style function.
+---@param func function: A callback style function to be converted. The last argument must be the callback.
+---@param argc number: The number of arguments of func. Must be included.
+---@return function: Returns an async function
+M.wrap = function(func, argc)
+  if type(func) ~= "function" then
+    traceback_error("type error :: expected func, got " .. type(func))
+  end
+
+  if type(argc) ~= "number" then
+    traceback_error("type error :: expected number, got " .. type(argc))
+  end
+
+  local function leaf(...)
+    local nargs = select('#', ...)
+
+    if not (nargs == argc - 1 or nargs == argc) then
+      print(('Expected %s or %s number of arguments, got %s'):format(argc - 1, argc, nargs))
+    end
+
+    if nargs == argc then
+      return func(...)
+    else
+      return co.yield(func, ...)
+    end
+  end
+
+  add_leaf_function(leaf)
+
+  return leaf
+end
+
+---Use this to either run a future concurrently and then do something else
+---or use it to run a future with a callback in a non async context
+---@param async_function function
+---@param callback function
+M.run = function(async_function, callback)
+  if M.is_leaf_function(async_function) then
+    async_function(callback)
+  else
+    execute(async_function, callback)
+  end
+end
+
+---this needs to be fixed
+M.void = function(async_fun)
+  return co.wrap(async_fun)
+end
+
+return M
diff --git a/lua/plenary/async/control.lua b/lua/plenary/async/control.lua
new file mode 100644
index 0000000..094cc22
--- /dev/null
+++ b/lua/plenary/async/control.lua
@@ -0,0 +1,214 @@
+local a = require('plenary.async.async')
+local Deque = require('plenary.async.structs').Deque
+local tbl = require('plenary.tbl')
+
+local M = {}
+
+local Condvar = {}
+Condvar.__index = Condvar
+
+---@class Condvar
+---@return Condvar
+function Condvar.new()
+  return setmetatable({handles = {}}, Condvar)
+end
+
+---`blocks` the thread until a notification is received
+Condvar.wait = a.wrap(function(self, callback)
+  -- not calling the callback will block the coroutine
+  table.insert(self.handles, callback)
+end, 2)
+
+---notify everyone that is waiting on this Condvar
+function Condvar:notify_all()
+  for i, callback in ipairs(self.handles) do
+    callback()
+    self.handles[i] = nil
+  end
+end
+
+---notify randomly one person that is waiting on this Condvar
+function Condvar:notify_one()
+  if #self.handles == 0 then return end
+
+  local idx = math.random(#self.handles)
+  self.handles[idx]()
+  table.remove(self.handles, idx)
+end
+
+M.Condvar = Condvar
+
+local Semaphore = {}
+Semaphore.__index = Semaphore
+
+---@class Semaphore
+---@param initial_permits number: the number of permits that it can give out
+---@return Semaphore
+function Semaphore.new(initial_permits) 
+  vim.validate {
+    initial_permits = {
+      initial_permits,
+      function(n) return n > 0 end,
+      'number greater than 0'
+    }
+  }
+
+  return setmetatable({permits = initial_permits, handles = {}}, Semaphore)
+end
+
+---async function, blocks until a permit can be acquired
+---example:
+---local semaphore = Semaphore.new(1024)
+---local permit = await(semaphore:acquire())
+---permit:forget()
+---when a permit can be acquired returns it
+---call permit:forget() to forget the permit
+Semaphore.acquire = a.wrap(function(self, callback)
+  if self.permits > 0 then
+    self.permits = self.permits - 1
+  else
+    table.insert(self.handles, callback)
+    return
+  end
+
+  local permit = {}
+
+  permit.forget = function(self_permit)
+    self.permits = self.permits + 1
+
+    if self.permits > 0 and #self.handles > 0 then
+      self.permits = self.permits - 1
+      local callback = table.remove(self.handles)
+      callback(self_permit)
+    end
+  end
+
+  callback(permit)
+end, 2)
+
+M.Semaphore = Semaphore
+
+M.channel = {}
+
+---Creates a oneshot channel
+---returns a sender and receiver function
+---the sender is not async while the receiver is
+---@return function, function
+M.channel.oneshot = function()
+  local val = nil
+  local saved_callback = nil
+  local sent = false
+  local received = false
+  local is_single = false
+
+  --- sender is not async
+  --- sends a value which can be nil
+  local sender = function(...)
+    assert(not sent, 'Oneshot channel can only send once')
+    sent = true
+
+    if saved_callback ~= nil then
+      saved_callback(...)
+      return
+    end
+
+    -- optimise for when there is only one or zero argument, no need to pack
+    local nargs = select('#', ...)
+    if nargs == 1 or nargs == 0 then
+      val = ...
+      is_single = true
+    else
+      val = tbl.pack(...)
+    end
+  end
+
+  --- receiver is async
+  --- blocks until a value is received
+  local receiver = a.wrap(function(callback)
+    assert(not received, 'Oneshot channel can only receive one value!')
+
+    if sent then
+      received = true
+      if is_single then
+        return callback(val)
+      else
+        return callback(tbl.unpack(val))
+      end
+    else
+      saved_callback = callback
+    end
+  end, 1)
+
+  return sender, receiver
+end
+
+---A counter channel.
+---Basically a channel that you want to use only to notify and not to send any actual values.
+---@return function: sender
+---@return function: receiver
+M.channel.counter = function()
+  local counter = 0
+  local condvar = Condvar.new()
+
+  local Sender = {}
+
+  function Sender:send()
+    counter = counter + 1
+    condvar:notify_all()
+  end
+
+  local Receiver = {}
+
+  Receiver.recv = function()
+    if counter == 0 then
+      await(condvar:wait())
+    end
+    counter = counter - 1
+  end
+
+  Receiver.last = function()
+    if counter == 0 then
+      await(condvar:wait())
+    end
+    counter = 0
+  end
+
+  return Sender, Receiver
+end
+
+---A multiple producer single consumer channel
+---@return table
+---@return table
+M.channel.mpsc = function()
+  local deque = Deque.new()
+  local condvar = Condvar.new()
+
+  local Sender = {}
+
+  function Sender.send(...)
+    deque:pushleft({...})
+    condvar:notify_all()
+  end
+
+  local Receiver = {}
+
+  Receiver.recv = function()
+    if deque:is_empty() then
+      condvar:wait()
+    end
+    return unpack(deque:popright())
+  end
+
+  Receiver.last = function()
+    if deque:is_empty() then
+      condvar:wait()
+    end
+    local val = deque:popright()
+    deque:clear()
+    return unpack(val)
+  end
+
+  return Sender, Receiver
+end
+
+return M
diff --git a/lua/plenary/async/init.lua b/lua/plenary/async/init.lua
new file mode 100644
index 0000000..93a08a0
--- /dev/null
+++ b/lua/plenary/async/init.lua
@@ -0,0 +1,42 @@
+---@brief [[
+--- NOTE: This API is still under construction.
+---         It may change in the future :)
+---@brief ]]
+
+local exports = require('plenary.async.async')
+exports.uv = require('plenary.async.uv_async')
+exports.util = require('plenary.async.util')
+exports.lsp = require('plenary.async.lsp')
+exports.api = require('plenary.async.api')
+exports.tests = require('plenary.async.tests')
+exports.control = require('plenary.async.control')
+
+exports.tests.add_globals = function()
+  a = exports
+  async = exports.async
+  await = exports.await
+  await_all = exports.await_all
+
+  -- must prefix with a or stack overflow, plenary.test harness already added it
+  a.describe = exports.tests.describe
+  -- must prefix with a or stack overflow
+  a.it = exports.tests.it
+end
+
+exports.tests.add_to_env = function()
+  local env = getfenv(2)
+
+  env.a = exports
+  env.async = exports.async
+  env.await = exports.await
+  env.await_all = exports.await_all
+
+  -- must prefix with a or stack overflow, plenary.test harness already added it
+  env.a.describe = exports.tests.describe
+  -- must prefix with a or stack overflow
+  env.a.it = exports.tests.it
+
+  setfenv(2, env)
+end
+
+return exports
diff --git a/lua/plenary/async/lsp.lua b/lua/plenary/async/lsp.lua
new file mode 100644
index 0000000..c0271cd
--- /dev/null
+++ b/lua/plenary/async/lsp.lua
@@ -0,0 +1,12 @@
+local a = require('plenary.async.async')
+
+local M = {}
+
+---This will be deprecated because the callback can be called multiple times.
+---This will give a coroutine error because the coroutine will be resumed multiple times.
+---Please use buf_request_all instead.
+M.buf_request = a.wrap(vim.lsp.buf_request, 4)
+
+M.buf_request_all = a.wrap(vim.lsp.buf_request_all, 4)
+
+return M
diff --git a/lua/plenary/async/structs.lua b/lua/plenary/async/structs.lua
new file mode 100644
index 0000000..a5c2f97
--- /dev/null
+++ b/lua/plenary/async/structs.lua
@@ -0,0 +1,112 @@
+local M = {}
+
+Deque = {}
+Deque.__index = Deque
+
+---@class Deque
+---A double ended queue
+---
+---@return Deque
+function Deque.new()
+  -- the indexes are created with an offset so that the indices are consequtive
+  -- otherwise, when both pushleft and pushright are used, the indices will have a 1 length hole in the middle
+  return setmetatable({first = 0, last = -1}, Deque)
+end
+
+---push to the left of the deque
+---@param value any
+function Deque:pushleft(value)
+  local first = self.first - 1
+  self.first = first
+  self[first] = value
+end
+
+---push to the right of the deque
+---@param value any
+function Deque:pushright(value)
+  local last = self.last + 1
+  self.last = last
+  self[last] = value
+end
+
+---pop from the left of the deque
+---@return any
+function Deque:popleft()
+  local first = self.first
+  if first > self.last then return nil end
+  local value = self[first]
+  self[first] = nil        -- to allow garbage collection
+  self.first = first + 1
+  return value
+end
+
+---pops from the right of the deque
+---@return any
+function Deque:popright()
+  local last = self.last
+  if self.first > last then return nil end
+  local value = self[last]
+  self[last] = nil         -- to allow garbage collection
+  self.last = last - 1
+  return value
+end
+
+---checks if the deque is empty
+---@return boolean
+function Deque:is_empty()
+  return self:len() == 0
+end
+
+---returns the number of elements of the deque
+---@return number
+function Deque:len()
+  return self.last - self.first + 1
+end
+
+---returns and iterator of the indices and values starting from the left
+---@return function
+function Deque:ipairs_left()
+  local i = self.first
+
+  return function()
+    local res = self[i]
+    local idx = i
+
+    if res then
+      i = i + 1
+
+      return idx, res
+    end
+  end
+end
+
+---returns and iterator of the indices and values starting from the right
+---@return function
+function Deque:ipairs_right()
+  local i = self.last
+
+  return function()
+    local res = self[i]
+    local idx = i
+
+    if res then
+      i = i - 1 -- advance the iterator before we return
+
+      return idx, res
+    end
+  end
+end
+
+---removes all values from the deque
+---@return nil
+function Deque:clear()
+  for i, _ in self:ipairs_left() do
+    self[i] = nil
+  end
+  self.first = 0
+  self.last = -1
+end
+
+M.Deque = Deque
+
+return M
diff --git a/lua/plenary/async/util.lua b/lua/plenary/async/util.lua
new file mode 100644
index 0000000..684e0c1
--- /dev/null
+++ b/lua/plenary/async/util.lua
@@ -0,0 +1,130 @@
+local a = require('plenary.async.async')
+local await = a.await
+local async = a.async
+local vararg = require('plenary.vararg')
+local uv = vim.loop
+-- local control = a.control
+local control = require('plenary.async.control')
+local channel = control.channel
+
+local M = {}
+
+local defer_swapped = function(timeout, callback)
+  vim.defer_fn(callback, timeout)
+end
+
+---Sleep for milliseconds
+---@param ms number
+M.sleep = a.wrap(defer_swapped, 2)
+
+---This will COMPLETELY block neovim
+---please just use a.run unless you have a very special usecase
+---for example, in plenary test_harness you must use this
+---@param async_function Future
+---@param timeout number: Stop blocking if the timeout was surpassed. Default 2000.
+M.block_on = function(async_function, timeout)
+  async_function = M.protected(async_function)
+
+  local stat, ret
+
+  a.run(async_function, function(_stat, ...)
+    stat = _stat
+    ret = {...}
+  end)
+
+  local function check()
+    if stat == false then
+      error("Blocking on future failed " .. unpack(ret))
+    end
+    return stat == true
+  end
+
+  if not vim.wait(timeout or 2000, check, 20, false) then
+    error("Blocking on future timed out or was interrupted")
+  end
+
+  return unpack(ret)
+end
+
+M.will_block = function(async_func)
+  return function()
+    M.block_on(async_func)
+  end
+end
+
+M.join = function(async_fns)
+  local len = #async_fns
+  local results = {}
+  local done = 0
+
+  local tx, rx = channel.oneshot()
+
+  for i, async_fn in ipairs(async_fns) do
+    assert(type(async_fn) == "function", "type error :: future must be function")
+
+    local cb = function(...)
+      results[i] = {...}
+      done = done + 1
+      if done == len then
+        tx()
+      end
+    end
+
+    a.run(async_fn, cb)
+  end
+
+  rx()
+
+  return results
+end
+
+---Returns a future that when run will select the first async_function that finishes
+---@param async_funs table: The async_function that you want to select
+---@return ...
+M.run_first = a.wrap(function(async_funs, step)
+  local ran = false
+
+  for _, future in ipairs(async_funs) do
+    assert(type(future) == "function", "type error :: future must be function")
+
+    local callback = function(...)
+      if not ran then
+        ran = true
+        step(...)
+      end
+    end
+
+    future(callback)
+  end
+end, 2)
+
+M.run_all = function(async_fns, callback)
+  a.run(function()
+    M.join(async_fns)
+  end, callback)
+end
+
+function M.apcall(async_fn, ...)
+  if a.is_leaf_function(async_fn) then
+    local tx, rx = channel.oneshot()
+    local stat, ret = pcall(async_fn, vararg.rotate(tx, ...))
+    if not stat then
+      return stat, ret
+    else
+      return stat, rx()
+    end
+  else
+    return pcall(async_fn, ...)
+  end
+end
+
+function M.protected(async_fn)
+  return function()
+    return M.apcall(async_fn)
+  end
+end
+
+---An async function that when called will yield to the neovim scheduler to be able to call the api.
+M.scheduler = a.wrap(vim.schedule, 1)
+
+return M
diff --git a/lua/plenary/async/uv_async.lua b/lua/plenary/async/uv_async.lua
new file mode 100644
index 0000000..66f3b2c
--- /dev/null
+++ b/lua/plenary/async/uv_async.lua
@@ -0,0 +1,82 @@
+local a = require('plenary.async.async')
+local uv = vim.loop
+
+local M = {}
+
+local function add(name, argc)
+  local success, ret = pcall(a.wrap, uv[name], argc)
+
+  if not success then
+    error("Failed to add function with name " .. name)
+  end
+
+  M[name] = ret
+end
+
+add('close', 4) -- close a handle
+
+-- filesystem operations
+add('fs_open', 4)
+add('fs_read', 4)
+add('fs_close', 2)
+add('fs_unlink', 2)
+add('fs_write', 4)
+add('fs_mkdir', 3)
+add('fs_mkdtemp', 2)
+-- 'fs_mkstemp',
+add('fs_rmdir', 2)
+add('fs_scandir', 2)
+add('fs_stat', 2)
+add('fs_fstat', 2)
+add('fs_lstat', 2)
+add('fs_rename', 3)
+add('fs_fsync', 2)
+add('fs_fdatasync', 2)
+add('fs_ftruncate', 3)
+add('fs_sendfile', 5)
+add('fs_access', 3)
+add('fs_chmod', 3)
+add('fs_fchmod', 3)
+add('fs_utime', 4)
+add('fs_futime', 4)
+-- 'fs_lutime',
+add('fs_link', 3)
+add('fs_symlink', 4)
+add('fs_readlink', 2)
+add('fs_realpath', 2)
+add('fs_chown', 4)
+add('fs_fchown', 4)
+-- 'fs_lchown',
+add('fs_copyfile', 4)
+-- add('fs_opendir', 3) -- TODO: fix this one
+add('fs_readdir', 2)
+add('fs_closedir', 2)
+-- 'fs_statfs',
+
+-- stream
+add('shutdown', 2)
+add('listen', 3)
+-- add('read_start', 2) -- do not do this one, the callback is made multiple times
+add('write', 3)
+add('write2', 4)
+add('shutdown', 2)
+
+-- tcp
+add('tcp_connect', 4)
+-- 'tcp_close_reset',
+
+-- pipe
+add('pipe_connect', 3)
+
+-- udp
+add('udp_send', 5)
+add('udp_recv_start', 2)
+
+-- fs event (wip make into async await event)
+-- fs poll event (wip make into async await event)
+
+-- dns
+add('getaddrinfo', 4)
+add('getnameinfo', 2)
+
+return M
diff --git a/lua/plenary/collections/py_list.lua b/lua/plenary/collections/py_list.lua
new file mode 100644
index 0000000..06d9bec
--- /dev/null
+++ b/lua/plenary/collections/py_list.lua
@@ -0,0 +1,363 @@
+---@brief [[
+--- This module implements python-like lists. It can be used like so:
+--- <pre>
+---     local List = require 'plenary.collections.py_list'
+---     local l = List{3, 20, 44}
+---     print(l)  -- [3, 20, 44]
+--- </pre>
+---@brief ]]
+local List = {}
+
+---@class List @The base class for all list objects
+
+---List constructor. Can be used in higher order functions
+---@param  tbl table: A list-like table containing the initial elements of the list
+---@return List: A new list object
+function List.new(tbl)
+  if type(tbl) == 'table' then
+    local len = #tbl
+    local obj = setmetatable(tbl, List)
+    obj._len = len
+    return obj
+  end
+  error 'List constructor must be called with table argument'
+end
+
+--- Checks whether the argument is a List object
+--- @param tbl table: The object to test
+--- @return boolean: Whether tbl is an instance of List
+function List.is_list(tbl)
+  local meta = getmetatable(tbl) or {}
+  return meta == List
+end
+
+function List:__index(key)
+  if self ~= List then
+    local field = List[key]
+    if field then return field end
+  end
+end
+
+-- TODO: Similar to python, use [...] if the table references itself --
+function List:__tostring()
+  local elements = self:join ', '
+  return '[' .. elements .. ']'
+end
+
+function List:__eq(other)
+  if #self ~= #other then return false end
+  for i = 1, #self do if self[i] ~= other[i] then return false end end
+  return true
+end
+
+function List:__mul(other)
+  local result = List.new {}
+  for i = 1, other do result[i] = self end
+  return result
+end
+
+function List:__len()
+  return self._len
+end
+
+function List:__concat(other)
+  return self:concat(other)
+end
+
+--- Pushes the element to the end of the list
+--- @param other any: The object to append
+--- @see List.pop
+function List:push(other)
+  self[#self + 1] = other
+  self._len = self._len + 1
+end
+
+--- Pops the last element off the list and returns it
+--- @return any: The (previously) last element from the list
+--- @see List.push
+function List:pop()
+  local result = table.remove(self)
+  self._len = self._len - 1
+  return result
+end
+
+--- Inserts other into the specified idx
+--- @param idx number: The index that other will be inserted to
+--- @param other any: The element to insert
+--- @see List.remove
+function List:insert(idx, other)
+  table.insert(self, idx, other)
+  self._len = self._len + 1
+end
+
+--- Removes the element at index idx and returns it
+--- @param idx number: The index of the element to remove
+--- @return any: The element previously at index idx
+--- @see List.insert
+function List:remove(idx)
+  self._len = self._len - 1
+  return table.remove(self, idx)
+end
+
+--- Can be used to compare elements with any list-like table. It only checks for
+--- shallow equality
+--- @param other any: The element to test for
+--- @return boolean: True if other is a list object and all it's elements are equal
+--- @see List.deep_equal
+function List:equal(other)
+  return self:__eq(other)
+end
+
+--- Checks for deep equality between lists. This uses vim.deep_equal for testing
+--- @param other any: The element to test for
+--- @return boolean: True if all elements and their children are equal
+--- @see List.equal
+--- @see vim.deep_equal
+function List:deep_equal(other)
+  return vim.deep_equal(self, other)
+end
+
+--- Returns a copy of the list with elements between a and b, inclusive
+--- <pre>
+---     local list = List{1, 2, 3, 4}
+---     local slice = list:slice(2, 3)
+---     print(slice) -- [2, 3]
+--- </pre>
+--- @param a number: The low end of the slice
+--- @param b number: The high end of the slice
+--- @return List: A list with elements between a and b
+function List:slice(a, b)
+  return List.new(vim.list_slice(self, a, b))
+end
+
+--- Similar to slice, but with every element. It only makes a shallow copy
+--- @return List: A slice from 1 to #self, i.e., a complete copy of the list
+--- @see List.deep_copy
+function List:copy()
+  return self:slice(1, #self)
+end
+
+--- Similar to copy, but makes a deep copy instead
+--- @return List: A deep copy of the object
+--- @see List.copy
+--- @see vim.deep_copy
+function List:deep_copy()
+  return vim.deep_copy(self)
+end
+
+--- Reverses the list in place. If you don't want this, you could do something
+--- like this
+--- <pre>
+---     local list = List{1, 2, 3, 4}
+---     local reversed = list:copy():reverse()
+--- </pre>
+--- @return List: The list itself, so you can chain method calls
+--- @see List.copy
+--- @see List.deep_copy
+function List:reverse()
+  local n = #self
+  local i = 1
+  while i < n do
+    self[i], self[n] = self[n], self[i]
+    i = i + 1
+    n = n - 1
+  end
+  return self
+end
+
+--- Concatenates the elements whithin the list separated by the given string
+--- <pre>
+---     local list = List{1, 2, 3, 4}
+---     print(list:join('-'))  -- 1-2-3-4
+--- </pre>
+--- @param sep string: The separator to place between the elements. Default ''
+--- @return string: The elements in the list separated by sep
+function List:join(sep)
+  sep = sep or ''
+  local result = ''
+  for i, v in self:iter() do
+    result = result .. tostring(v)
+    if i ~= #self then result = result .. sep end
+  end
+  return result
+end
+
+--- Returns a list with the elements of self concatenated with those in the
+--- given arguments
+--- @vararg table|List: The sequences to concatenate to this one
+--- @return List
+function List:concat(...)
+  local result = self:copy()
+  local others = {...}
+  for _, other in ipairs(others) do
+    for _, v in ipairs(other) do result:push(v) end
+  end
+  return result
+end
+
+--- Moves the elements between from and from+len in self, to positions between
+--- to and to+len in other, like so
+--- <pre>
+---     other[to], other[to+1]... other[to+len] = self[from], self[from+1]... self[from+len]
+--- </pre>
+--- @param from number: The first index of the origin slice
+--- @param len number: The length of the slices
+--- @param to number: The first index of the destination slice
+--- @param other table|List: The destination list. Defaults to self
+--- @see table.move
+function List:move(from, len, to, other)
+  return table.move(self, from, len, to, other)
+end
+
+--- Packs the given elements into a list. Similar to lua 5.3's table.pack
+--- @vararg any: The elements to pack
+--- @return List: a list containing all the given elements
+--- @see table.pack
+function List.pack(...)
+  return List.new {...}
+end
+
+--- Unpacks the elements from this list and returns them
+--- @return ...any: All the elements from self[1] to self[#self]
+function List:unpack()
+  return unpack(self, 1, #self)
+end
+
+-- Iterator stuff
+
+local Iter = require 'plenary.iterators'
+
+local itermetatable = getmetatable(Iter:wrap())
+
+local function forward_list_gen(param, state)
+  state = state + 1
+  local v = param[state]
+  if v ~= nil then return state, v end
+end
+
+local function backward_list_gen(param, state)
+  state = state - 1
+  local v = param[state]
+  if v ~= nil then return state, v end
+end
+
+--- Run the given predicate through all the elements pointed by this iterator,
+--- and classify them into two lists. The first one holds the elements for which
+--- predicate returned a truthy value, and the second holds the rest. For
+--- example:
+---
+--- <pre>
+---     local list = List{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
+---     local evens, odds = list:iter():partition(function(e)
+---         return e % 2 == 0
+---     end)
+---     print(evens, odds)
+--- </pre>
+---
+--- Would print
+---
+--- <pre>
+---     [0, 2, 4, 6, 8] [1, 3, 5, 7, 9]
+--- </pre>
+---@param predicate function: The predicate to classify the elements
+---@return List,List
+local function partition(self, predicate)
+  local list1, list2 = List.new {}, List.new {}
+  for _, v in self do
+    if predicate(v) then
+      list1:push(v)
+    else
+      list2:push(v)
+    end
+  end
+  return list1, list2
+end
+
+local function wrap_iter(f, l, n)
+  local iter = Iter.wrap(f, l, n)
+  iter.partition = partition
+  return iter
+end
+
+--- Counts the occurrences of e inside the list
+--- @param e any: The element to test for
+--- @return number: The number of occurrences of e
+function List:count(e)
+  local count = 0
+  for _, v in self:iter() do if e == v then count = count + 1 end end
+  return count
+end
+
+--- Appends the elements in the given iterator to the list
+--- @param other table: An iterator object
+function List:extend(other)
+  if type(other) == 'table' and getmetatable(other) == itermetatable then
+    for _, v in other do self:push(v) end
+  else
+    error 'Argument must be an iterator'
+  end
+end
+
+--- Checks whether there is an occurence of the given element in the list
+--- @param e any: The object to test for
+--- @return boolean: True if e is present
+function List:contains(e)
+  for _, v in self:iter() do if v == e then return true end end
+  return false
+end
+
+--- Creates an iterator for the list. For example:
+--- <pre>
+---     local list = List{8, 4, 7, 9}
+---     for i, v in list:iter() do
+---         print(i, v)
+---     end
+--- </pre>
+--- Would print:
+--- <pre>
+---     1    8
+---     2    4
+---     3    7
+---     4    9
+--- </pre>
+--- @return table: An iterator object
+function List:iter()
+  return wrap_iter(forward_list_gen, self, 0)
+end
+
+--- Creates a reverse iterator for the list. For example:
+--- <pre>
+---     local list = List{8, 4, 7, 9}
+---     for i, v in list:riter() do
+---         print(i, v)
+---     end
+--- </pre>
+--- Would print:
+--- <pre>
+---     4    9
+---     3    7
+---     2    4
+---     1    8
+--- </pre>
+--- @return table: An iterator object
+function List:riter()
+  return wrap_iter(backward_list_gen, self, #self + 1)
+end
+
+-- Miscellaneous
+
+--- Create a list from the elements pointed at by the given iterator.
+--- @param iter table: An iterator object
+--- @return List
+function List.from_iter(iter)
+  local result = List.new {}
+  for _, v in iter do result:push(v) end
+  return result
+end
+
+return setmetatable({}, {
+  __call = function(_, tbl)
+    return List.new(tbl)
+  end,
+  __index = List,
+})
diff --git a/lua/plenary/curl.lua b/lua/plenary/curl.lua
index ed8bbec..563c82c 100644
--- a/lua/plenary/curl.lua
+++ b/lua/plenary/curl.lua
@@ -154,7 +154,7 @@ parse.request = function(opts)
     elseif P.is_file(P.new(b)) then
       opts.in_file = b
     elseif type(b) == "string" then
-      opts.raw = b
+      opts.raw_body = b
     end
   end
   return vim.tbl_flatten({
@@ -163,7 +163,7 @@ parse.request = function(opts)
     parse.method(opts.method),
     parse.headers(opts.headers),
     parse.accept_header(opts.accept),
-    parse.raw_body(opts.raw),
+    parse.raw_body(opts.raw_body),
     parse.data_body(opts.data),
     parse.form(opts.form),
     parse.file(opts.in_file),
diff --git a/lua/plenary/fun.lua b/lua/plenary/fun.lua
new file mode 100644
index 0000000..2f46677
--- /dev/null
+++ b/lua/plenary/fun.lua
@@ -0,0 +1,37 @@
+local tbl = require('plenary.tbl')
+
+local M = {}
+
+function M.bind(fn, ...)
+  if select('#', ...) == 1 then
+    local arg = ...
+    return function(...)
+      fn(arg, ...)
+    end
+  end
+
+  local args = tbl.pack(...)
+  return function(...)
+    fn(tbl.unpack(args), ...)
+  end
+end
+
+function M.arify(fn, argc)
+  return function(...)
+    if select('#', ...) ~= argc then
+      error(("Expected %s number of arguments"):format(argc))
+    end
+
+    fn(...)
+  end
+end
+
+function M.create_wrapper(map)
+  return function(to_wrap)
+    return function(...)
+      return map(to_wrap(...))
+    end
+  end
+end
+
+return M
diff --git a/lua/plenary/path.lua b/lua/plenary/path.lua
index a7b0802..cf3d907 100644
--- a/lua/plenary/path.lua
+++ b/lua/plenary/path.lua
@@ -277,17 +277,15 @@ end
 function Path:make_relative(cwd)
   self.filename = clean(self.filename)
   cwd = clean(F.if_nil(cwd, self._cwd, cwd))
+  if self.filename == cwd then
+    self.filename = "."
+  else
+    if cwd:sub(#cwd, #cwd) ~= path.sep then
+      cwd = cwd .. path.sep
+    end
 
-  if self.filename:sub(1, #cwd) == cwd then
-    if #self.filename == #cwd then
-      self.filename = "."
-    else
-      -- skip path separator, unless cwd is root
-      local offset = 2
-      if cwd:sub(-1) == path.sep then
-        offset = 1
-      end
-      self.filename = self.filename:sub(#cwd + offset, -1)
+    if self.filename:sub(1, #cwd) == cwd then
+      self.filename = self.filename:sub(#cwd + 1, -1)
     end
   end
 
diff --git a/lua/plenary/vararg/init.lua b/lua/plenary/vararg/init.lua
new file mode 100644
index 0000000..d60cbd8
--- /dev/null
+++ b/lua/plenary/vararg/init.lua
@@ -0,0 +1,3 @@
+return {
+  rotate = require('plenary.vararg.rotate'),
+}
diff --git a/lua/plenary/vararg/rotate.lua b/lua/plenary/vararg/rotate.lua
new file mode 100644
index 0000000..d840128
--- /dev/null
+++ b/lua/plenary/vararg/rotate.lua
@@ -0,0 +1,75 @@
+---@brief [[
+---Do not edit this file, it was generated!
+---Provides a function to rotate a lua vararg
+---@brief ]]
+local tbl = require('plenary.tbl')
+
+local rotate_lookup = {}
+
+rotate_lookup[0] = function() return A0 end
+
+rotate_lookup[1] = function(A0) return A0 end
+
+rotate_lookup[2] = function(A0, A1) return A1, A0 end
+
+rotate_lookup[3] = function(A0, A1, A2) return A1, A2, A0 end
+
+rotate_lookup[4] = function(A0, A1, A2, A3) return A1, A2, A3, A0 end
+
+rotate_lookup[5] = function(A0, A1, A2, A3, A4) return A1, A2, A3, A4, A0 end
+
+rotate_lookup[6] = function(A0, A1, A2, A3, A4, A5) return A1, A2, A3, A4, A5,
+                                                           A0 end
+
+rotate_lookup[7] = function(A0, A1, A2, A3, A4, A5, A6)
+    return A1, A2, A3, A4, A5, A6, A0
+end
+
+rotate_lookup[8] = function(A0, A1, A2, A3, A4, A5, A6, A7)
+    return A1, A2, A3, A4, A5, A6, A7, A0
+end
+
+rotate_lookup[9] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8)
+    return A1, A2, A3, A4, A5, A6, A7, A8, A0
+end
+
+rotate_lookup[10] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9)
+    return A1, A2, A3, A4, A5, A6, A7, A8, A9, A0
+end
+
+rotate_lookup[11] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10)
+    return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A0
+end
+
+rotate_lookup[12] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11)
+    return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A0
+end
+
+rotate_lookup[13] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11,
+                             A12)
+    return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A0
+end
+
+rotate_lookup[14] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11,
+                             A12, A13)
+    return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A0
+end
+
+rotate_lookup[15] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11,
+                             A12, A13, A14)
+    return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A0
+end
+
+local function rotate_n(first, ...)
+    local args = tbl.pack(...)
+    args[#args + 1] = first
+    return tbl.unpack(args)
+end
+
+local function rotate(...)
+    local nargs = select('#', ...)
+
+    return (rotate_lookup[nargs] or rotate_n)(...)
+end
+
+return rotate
diff --git a/scratch/async.lua b/scratch/async.lua
deleted file mode 100644
index 38c7881..0000000
--- a/scratch/async.lua
+++ /dev/null
@@ -1,5 +0,0 @@
-local a = require('plenary.async_lib')
-local async = a.async
-
-async(nil)
--- a.wrap(function() end, nil)
diff --git a/scratch/rotate_bench.lua b/scratch/rotate_bench.lua
new file mode 100644
index 0000000..2b6a9d3
--- /dev/null
+++ b/scratch/rotate_bench.lua
@@ -0,0 +1,21 @@
+local tbl = require('plenary/tbl')
+
+local vararg = require('plenary/vararg')
+local rotate = vararg.rotate
+local bench = require('plenary.profile').benchmark
+
+local function rotate_n(first, ...)
+  local args = tbl.pack(...)
+  args[#args+1] = first
+  return tbl.unpack(args)
+end
+
+local num = 2e7 -- 2e4
+
+print('rotate: ', bench(num, function()
+  local a, b, c, d, e, f, g = rotate(1, 2, 3, 4, 5, 6)
+end))
+
+print('rotate_n: ', bench(num, function()
+  local a, b, c, d, e, f, g = rotate_n(1, 2, 3, 4, 5, 6)
+end))
diff --git a/scripts/update_vararg.py b/scripts/update_vararg.py
new file mode 100755
index 0000000..68f6733
--- /dev/null
+++ b/scripts/update_vararg.py
@@ -0,0 +1,15 @@
+#!/usr/bin/env python3
+import os
+import subprocess
+
+def generate_file(name, outpath, **kwargs):
+    from jinja2 import Environment, FileSystemLoader
+    env = Environment(loader=FileSystemLoader('./vararg'))
+    template = env.get_template(name)
+    path = os.path.join(outpath, name)
+    with open(path, 'w') as fp:
+        fp.write(template.render(kwargs))
+    subprocess.run(["lua-format", "-i", path])
+
+if __name__ == '__main__':
+    generate_file('rotate.lua', '../lua/plenary/vararg', amount=16)
diff --git a/scripts/vararg/rotate.lua b/scripts/vararg/rotate.lua
new file mode 100644
index 0000000..43066a9
--- /dev/null
+++ b/scripts/vararg/rotate.lua
@@ -0,0 +1,28 @@
+---@brief [[
+---Do not edit this file, it was generated!
+---Provides a function to rotate a lua vararg
+---@brief ]]
+
+local tbl = require('plenary.tbl')
+
+local rotate_lookup = {}
+
+{% for n in range(0, amount) %}
+  rotate_lookup[{{n}}] = function ({% for n in range(n) %} A{{n}} {{ ", " if not loop.last else "" }} {% endfor %})
+    return {% for n in range(1, n) %} A{{n}}, {% endfor %} A0
+  end
+{% endfor %}
+
+local function rotate_n(first, ...)
+  local args = tbl.pack(...)
+  args[#args+1] = first
+  return tbl.unpack(args)
+end
+
+local function rotate(...)
+  local nargs = select('#', ...)
+
+  return (rotate_lookup[nargs] or rotate_n)(...)
+end
+
+return rotate


Test patch:
diff --git a/lua/plenary/async/tests.lua b/lua/plenary/async/tests.lua
new file mode 100644
index 0000000..26654c6
--- /dev/null
+++ b/lua/plenary/async/tests.lua
@@ -0,0 +1,14 @@
+local a = require('plenary.async.async')
+local util = require('plenary.async.util')
+
+local M = {}
+
+M.describe = function(s, async_func)
+  describe(s, async_func)
+end
+
+M.it = function(s, async_func)
+  it(s, util.will_block(async_func))
+end
+
+return M
diff --git a/lua/plenary/test_harness.lua b/lua/plenary/test_harness.lua
index 35caf63..87aeeb4 100644
--- a/lua/plenary/test_harness.lua
+++ b/lua/plenary/test_harness.lua
@@ -75,7 +75,9 @@ function harness.test_directory(directory, opts)
         string.format('lua require("plenary.busted").run("%s")', p:absolute())
       }
 
-      if opts.minimal_init ~= nil then
+      if opts.minimal ~= nil then
+        table.insert(args, '--noplugin')
+      elseif opts.minimal_init ~= nil then
         table.insert(args, '--noplugin')
 
         table.insert(args, '-u')
diff --git a/tests/plenary/async_lib/channel_spec.lua b/tests/plenary/async/channel_spec.lua
similarity index 57%
rename from tests/plenary/async_lib/channel_spec.lua
rename to tests/plenary/async/channel_spec.lua
index f7da479..ee12a4e 100644
--- a/tests/plenary/async_lib/channel_spec.lua
+++ b/tests/plenary/async/channel_spec.lua
@@ -1,19 +1,21 @@
-require('plenary.async_lib').tests.add_to_env()
-local channel = a.util.channel
+require('plenary.async').tests.add_to_env()
+local channel = a.control.channel
 local eq = assert.are.same
-local protected = a.util.protected
+local apcall = a.util.apcall
 
-a.describe('channel', function()
-  a.describe('oneshot', function()
+describe('channel', function()
+  describe('oneshot', function()
     a.it('should work when rx is used first', function()
       local tx, rx = channel.oneshot()
 
-      a.run(a.future(function()
-        local got = await(rx())
+      a.run(function()
+        local got = rx()
+
         eq("sent value", got)
-      end))
+      end)
 
       tx("sent value")
+
     end)
 
     a.it('should work when tx is used first', function()
@@ -21,39 +23,52 @@ a.describe('channel', function()
 
       tx("sent value")
 
-      a.run(a.future(function()
-        local got = await(rx())
-        eq("sent value", got)
-      end))
+      local got = rx()
+
+      eq("sent value", got)
     end)
 
     a.it('should work with multiple returns', function()
       local tx, rx = channel.oneshot()
 
-      a.run(a.future(function()
-        local got, got2 = await(rx())
+      a.run(function()
+        local got, got2 = rx()
         eq("sent value", got)
         eq("another sent value", got2)
-      end))
+      end)
 
       tx("sent value", "another sent value")
     end)
 
+    a.it('should work when sending a falsey value', function ()
+      local tx, rx = channel.oneshot()
+
+      tx(false)
+
+      local res = rx()
+      eq(res, false)
+
+      local stat, ret = apcall(rx)
+      eq(stat, false)
+      local stat, ret = apcall(rx)
+      eq(stat, false)
+    end)
+
     a.it('should work when sending a nil value', function ()
       local tx, rx = channel.oneshot()
 
       tx(nil)
 
-      local res = await(rx())
+      local res = rx()
       eq(res, nil)
 
-      local stat, ret = await(protected(rx()))
+      local stat, ret = apcall(rx)
       eq(stat, false)
-      local stat, ret = await(protected(rx()))
+      local stat, ret = apcall(rx)
       eq(stat, false)
     end)
 
-    a.it('should block sending mulitple times', function()
+    a.it('should error when sending mulitple times', function()
       local tx, rx = channel.oneshot()
 
       tx()
@@ -61,16 +76,16 @@ a.describe('channel', function()
       eq(stat, false)
     end)
 
-    a.it('should block receiving multiple times', function ()
+    a.it('should block receiving multiple times', function()
       local tx, rx = channel.oneshot()
-      tx()
-      await(rx())
-      local stat = await(protected(rx()))
+      tx(true)
+      rx()
+      local stat = apcall(rx)
       eq(stat, false)
     end)
   end)
 
-  a.describe('counter', function()
+  describe('counter', function()
     a.it('should work', function()
       local tx, rx = channel.counter()
 
@@ -80,15 +95,13 @@ a.describe('channel', function()
 
       local counter = 0
 
-      local recv_stuff = async(function()
+      a.run(function()
         for i = 1, 3 do
-          await(rx.recv())
+          rx.recv()
           counter = counter + 1
         end
       end)
 
-      a.run(recv_stuff())
-
       eq(counter, 3)
     end)
 
@@ -101,15 +114,11 @@ a.describe('channel', function()
 
       local counter = 0
 
-      local recv_stuff = async(function()
-        for i = 1, 3 do
-          await(rx.last())
-          counter = counter + 1
-        end
+      a.run(function()
+        rx.last()
+        counter = counter + 1
       end)
 
-      a.run(recv_stuff())
-
       eq(counter, 1)
     end)
   end)
diff --git a/tests/plenary/async_lib/condvar_spec.lua b/tests/plenary/async/condvar_spec.lua
similarity index 56%
rename from tests/plenary/async_lib/condvar_spec.lua
rename to tests/plenary/async/condvar_spec.lua
index 1793c29..afe2f55 100644
--- a/tests/plenary/async_lib/condvar_spec.lua
+++ b/tests/plenary/async/condvar_spec.lua
@@ -1,20 +1,19 @@
-require('plenary.async_lib').tests.add_to_env()
-local Condvar = a.util.Condvar
+require('plenary.async').tests.add_to_env()
+local Condvar = a.control.Condvar
 local eq = assert.are.same
+local join, run_all = a.util.join, a.util.run_all
 
-a.describe('condvar', function()
+describe('condvar', function()
   a.it('should allow blocking', function()
     local var = false
 
     local condvar = Condvar.new()
 
-    local blocking = async(function()
-      await(condvar:wait())
+    a.run(function()
+      condvar:wait()
       var = true
     end)
 
-    a.run(blocking())
-
     eq(var, false)
 
     condvar:notify_one()
@@ -27,22 +26,24 @@ a.describe('condvar', function()
 
     local condvar = Condvar.new()
 
-    local first = async(function()
-      await(condvar:wait())
+    local first = function()
+      condvar:wait()
       counter = counter + 1
-    end)
+    end
 
-    local second = async(function()
-      await(condvar:wait())
+    local second = function()
+      condvar:wait()
       counter = counter + 1
-    end)
+    end
 
-    local third = async(function()
-      await(condvar:wait())
+    local third = function()
+      condvar:wait()
       counter = counter + 1
-    end)
+    end
 
-    a.run_all { first(), second(), third() }
+    a.run(function()
+      join { first, second, third }
+    end)
 
     eq(0, counter)
 
@@ -64,22 +65,22 @@ a.describe('condvar', function()
 
     local condvar = Condvar.new()
 
-    local first = async(function()
-      await(condvar:wait())
+    local first = function()
+      condvar:wait()
       counter = counter + 1
-    end)
+    end
 
-    local second = async(function()
-      await(condvar:wait())
+    local second = function()
+      condvar:wait()
       counter = counter + 1
-    end)
+    end
 
-    local third = async(function()
-      await(condvar:wait())
+    local third = function()
+      condvar:wait()
       counter = counter + 1
-    end)
+    end
 
-    a.run_all { first(), second(), third() }
+    run_all { first, second, third }
 
     eq(0, counter)
 
@@ -101,22 +102,22 @@ a.describe('condvar', function()
 
     local condvar = Condvar.new()
 
-    local first = async(function()
-      await(condvar:wait())
+    local first = function()
+      condvar:wait()
       counter = counter + 1
-    end)
+    end
 
-    local second = async(function()
-      await(condvar:wait())
+    local second = function()
+      condvar:wait()
       counter = counter + 1
-    end)
+    end
 
-    local third = async(function()
-      await(condvar:wait())
+    local third = function()
+      condvar:wait()
       counter = counter + 1
-    end)
+    end
 
-    a.run_all { first(), second(), third() }
+    run_all { first, second, third }
 
     eq(0, counter)
 
diff --git a/tests/plenary/async_lib/deque_spec.lua b/tests/plenary/async/deque_spec.lua
similarity index 95%
rename from tests/plenary/async_lib/deque_spec.lua
rename to tests/plenary/async/deque_spec.lua
index ae14f83..3143037 100644
--- a/tests/plenary/async_lib/deque_spec.lua
+++ b/tests/plenary/async/deque_spec.lua
@@ -1,4 +1,4 @@
-local Deque = require('plenary.async_lib.structs').Deque
+local Deque = require('plenary.async.structs').Deque
 local eq = assert.are.same
 
 -- just a helper to create the test deque
diff --git a/tests/plenary/async_lib/read_file_bench.lua b/tests/plenary/async/read_file_bench.lua
similarity index 98%
rename from tests/plenary/async_lib/read_file_bench.lua
rename to tests/plenary/async/read_file_bench.lua
index c4b0d37..a976c73 100644
--- a/tests/plenary/async_lib/read_file_bench.lua
+++ b/tests/plenary/async/read_file_bench.lua
@@ -1,5 +1,5 @@
 -- do not run this for now, the asset was removed
-local a = require('plenary.async_lib')
+local a = require('plenary.async')
 local async = a.async
 local await = a.await
 local await_all = a.await_all
diff --git a/tests/plenary/async_lib/semaphore_spec.lua b/tests/plenary/async/semaphore_spec.lua
similarity index 54%
rename from tests/plenary/async_lib/semaphore_spec.lua
rename to tests/plenary/async/semaphore_spec.lua
index f3df6a4..3c23031 100644
--- a/tests/plenary/async_lib/semaphore_spec.lua
+++ b/tests/plenary/async/semaphore_spec.lua
@@ -1,9 +1,9 @@
-require('plenary.async_lib').tests.add_to_env()
-local Semaphore = a.util.Semaphore
+require('plenary.async').tests.add_to_env()
+local Semaphore = a.control.Semaphore
 
 local eq = assert.are.same
 
-a.describe('semaphore', function()
+describe('semaphore', function()
   a.it('should validate arguments', function()
     local status = pcall(Semaphore.new, -1)
     eq(status, false)
@@ -13,44 +13,44 @@ a.describe('semaphore', function()
   end)
 
   a.it('should acquire a permit if available', function ()
-    local sem = a.util.Semaphore.new(1)
-    local permit = await(sem:acquire())
+    local sem = Semaphore.new(1)
+    local permit = sem:acquire()
     assert(permit ~= nil)
   end)
 
   a.it('should block if no permit is available', function ()
-    local sem = a.util.Semaphore.new(1)
-    await(sem:acquire())
-    
+    local sem = Semaphore.new(1)
+    sem:acquire()
+
     local completed = false
-    local blocking = async(function ()
-      await(sem:acquire())
+    local blocking = function ()
+      sem:acquire()
       completed = true
-    end)
-    a.run(blocking())
+    end
+    a.run(blocking)
 
     eq(completed, false)
   end)
 
   a.it('should give another permit when an acquired permit is released', function ()
-    local sem = a.util.Semaphore.new(1)
-    local permit = await(sem:acquire())
+    local sem = Semaphore.new(1)
+    local permit = sem:acquire()
     permit:forget()
-    local next_permit = await(sem:acquire())
+    local next_permit = sem:acquire()
     assert(next_permit ~= nil)
   end)
 
   a.it('should permit the next waiting client when a permit is released', function () 
-    local sem = a.util.Semaphore.new(1)
-    local permit = await(sem:acquire())
+    local sem = Semaphore.new(1)
+    local permit = sem:acquire()
 
     local completed = false
-    local blocking = async(function ()
-      await(sem:acquire())
+    local blocking = function ()
+      sem:acquire()
       completed = true
-    end)
+    end
 
-    a.run(blocking())
+    a.run(blocking)
     permit:forget()
 
     eq(completed, true)
diff --git a/tests/plenary/async/util_spec.lua b/tests/plenary/async/util_spec.lua
new file mode 100644
index 0000000..b0b0da0
--- /dev/null
+++ b/tests/plenary/async/util_spec.lua
@@ -0,0 +1,50 @@
+require('plenary.async').tests.add_to_env()
+local block_on = a.util.block_on
+local eq = assert.are.same
+local id = a.util.id
+
+describe('async await util', function()
+  describe('block_on', function()
+    a.it('should block_on', function()
+      local fn = function()
+        a.util.sleep(100)
+        return 'hello'
+      end
+
+      local res = fn()
+      eq(res, 'hello')
+    end)
+
+    a.it('should work even when failing', function ()
+      local nonleaf = function()
+        eq(true, false)
+      end
+
+      local stat = pcall(block_on, nonleaf)
+      eq(stat, false)
+    end)
+  end)
+
+  describe('protect', function()
+    a.it('should be able to protect a non-leaf future', function()
+      local nonleaf = function()
+        error("This should error")
+        return 'return'
+      end
+
+      local stat, ret = pcall(nonleaf)
+      eq(false, stat)
+      assert(ret:match("This should error"))
+    end)
+
+    a.it('should be able to protect a non-leaf future that doesnt fail', function()
+      local nonleaf = function()
+        return 'didnt fail'
+      end
+
+      local stat, ret = pcall(nonleaf)
+      eq(stat, true)
+      eq(ret, 'didnt fail')
+    end)
+  end)
+end)
diff --git a/tests/plenary/async_lib/util_spec.lua b/tests/plenary/async_lib/util_spec.lua
deleted file mode 100644
index ccbc1b6..0000000
--- a/tests/plenary/async_lib/util_spec.lua
+++ /dev/null
@@ -1,104 +0,0 @@
-require('plenary.async_lib').tests.add_to_env()
-local block_on = a.util.block_on
-local eq = assert.are.same
-local id = a.util.id
-
-a.describe('async await util', function()
-  a.describe('block_on', function()
-    a.it('should block_on', function()
-      local fn = async(function()
-        await(a.util.sleep(100))
-        return 'hello'
-      end)
-
-      local res = block_on(fn())
-      eq(res, 'hello')
-    end)
-
-    a.it('should work even when failing', function ()
-      local nonleaf = async(function()
-        eq(true, false)
-      end)
-
-      local stat = pcall(block_on, nonleaf())
-      eq(stat, false)
-    end)
-  end)
-
-  a.describe('protect', function()
-    a.it('should be able to protect a non-leaf future', function()
-      local nonleaf = async(function()
-        error("This should error")
-        return 'return'
-      end)
-
-      local stat, ret = await(a.util.protected_non_leaf(nonleaf()))
-      eq(false, stat)
-      assert(ret:match("This should error"))
-    end)
-
-    a.it('should be able to protect a non-leaf future that doesnt fail', function()
-      local nonleaf = async(function()
-        return 'didnt fail'
-      end)
-
-      local stat, ret = await(a.util.protected_non_leaf(nonleaf()))
-      eq(stat, true)
-      eq(ret, 'didnt fail')
-    end)
-
-    a.it('should be able to protect a leaf future', function()
-      local leaf = a.wrap(function(callback)
-        error("This should error")
-        callback()
-      end, 1)
-
-      local stat, ret = await(a.util.protected(leaf()))
-      eq(stat, false)
-      assert(ret:match("This should error") ~= nil)
-    end)
-
-    a.it('should be able to protect a leaf future that doesnt fail', function()
-      local fn = a.wrap(function(callback)
-        callback('didnt fail')
-      end, 1)
-
-      local stat, ret = await(a.util.protected(fn()))
-      eq(stat, true)
-      eq(ret, 'didnt fail')
-    end)
-  end)
-
-  a.describe('timeout', function()
-    a.it('should block one and work', function()
-      local timed_out = await(a.util.timeout(a.util.sleep(1000), 500))
-
-      print('timed out 2:', timed_out)
-
-      assert(timed_out == true)
-    end)
-
-    a.it('should work when timeout is longer', function ()
-      local timed_out = await(a.util.timeout(a.util.sleep(100), 1000))
-
-      print('timed out 1:', timed_out)
-
-      assert(timed_out == false)
-    end)
-  end)
-
-  a.it('id should work', function()
-    eq(await(id(1, 2, 3, 4, 5)), 1, 2, 3, 4, 5)
-  end)
-
-  a.it('yield_now should work', function ()
-    local yield_now = a.util.yield_now
-    yield_now()
-    yield_now()
-    yield_now()
-    yield_now()
-    yield_now()
-    yield_now()
-    yield_now()
-  end)
-end)
diff --git a/tests/plenary/curl_spec.lua b/tests/plenary/curl_spec.lua
index c19923b..786a723 100644
--- a/tests/plenary/curl_spec.lua
+++ b/tests/plenary/curl_spec.lua
@@ -151,6 +151,22 @@ describe('CURL Wrapper:', function()
       eq(json, vim.fn.json_decode(res).json)
     end)
 
+
+    it("should not include the body twice", function ()
+      local json = { title = "New", name = "YORK" }
+      local body = vim.fn.json_encode(json)
+      local res = curl.post("https://postman-echo.com/post", {
+        body = body,
+        headers = {
+          content_type = "application/json"
+        },
+        dry_run = true,
+      })
+      local joined_response = table.concat(res, " ")
+      local first_index = joined_response:find(body)
+
+      eq(nil, joined_response:find(body, first_index + 1))
+    end)
   end)
   describe("PUT", function() --------------------------------------------------
 
diff --git a/tests/plenary/path_spec.lua b/tests/plenary/path_spec.lua
index 571d89f..063ae90 100644
--- a/tests/plenary/path_spec.lua
+++ b/tests/plenary/path_spec.lua
@@ -154,6 +154,20 @@ describe('Path', function()
       local relative = Path:new(p.filename):make_relative(p.filename)
       assert.are.same(relative, ".")
     end)
+
+    it('should not truncate if path separator is not present after cwd', function()
+      local cwd = "tmp" .. path.sep .. "foo"
+      local p = Path:new { 'tmp', 'foo_bar', 'fileb.lua'}
+      local relative = Path:new(p.filename):make_relative(cwd)
+      assert.are.same(p.filename, relative)
+    end)
+
+    it('should not truncate if path separator is not present after cwd and cwd ends in path sep', function()
+      local cwd = "tmp" .. path.sep .. "foo" .. path.sep
+      local p = Path:new { 'tmp', 'foo_bar', 'fileb.lua'}
+      local relative = Path:new(p.filename):make_relative(cwd)
+      assert.are.same(p.filename, relative)
+    end)
   end)
 
   describe(':normalize', function()
diff --git a/tests/plenary/py_list_spec.lua b/tests/plenary/py_list_spec.lua
new file mode 100644
index 0000000..c90485c
--- /dev/null
+++ b/tests/plenary/py_list_spec.lua
@@ -0,0 +1,61 @@
+local List = require('plenary.collections.py_list')
+local Iter = require('plenary.iterators')
+
+describe('List', function()
+  it('should detect whether a value is an instance of List', function()
+    local l = List {1, 2}
+    local n = 42
+    assert(List.is_list(l))
+    assert(not List.is_list(n))
+  end)
+  it('should be equal if all elements are equal', function()
+    local l1 = List {1, 2, 3}
+    local l2 = List {1, 2, 3}
+    local l3 = List {4, 5, 6}
+    assert.are.equal(l1, l2)
+    assert.are_not.equal(l1, l3)
+  end)
+  it('can be concatenated to other list-like tables', function()
+    local l1 = List {1, 2, 3} .. {4}
+    assert.are.equal(l1, List {1, 2, 3, 4})
+  end)
+  it('can create a copy of itself with equal elements', function()
+    local l1 = List {1, 2, 3}
+    local l2 = l1:copy()
+    assert.are.equal(l1, l2)
+  end)
+  it('can create a slice between two indices', function()
+    local l1 = List {1, 2, 3, 4}
+    local l2 = l1:slice(2, 4)
+    assert.are.equal(l2, List {2, 3, 4})
+  end)
+  it('can reverse itself in place', function()
+    local l = List {1, 2, 3, 4}
+    l:reverse()
+    assert.are.equal(l, List {4, 3, 2, 1})
+  end)
+  it('can push elements to itself', function()
+    local l = List {1, 2, 3}
+    l:push(4)
+    assert.are.equal(l, List {1, 2, 3, 4})
+  end)
+  it('can pop the n-th element from itself (last one by default)', function()
+    local l = List {1, 2, 3, 4}
+    local n = l:pop()
+    assert.are.equal(l, List {1, 2, 3})
+    assert.are.equal(n, 4)
+  end)
+  it('can create a list from an iterable', function()
+    local l = List.from_iter(Iter.range(5, 10))
+    assert.are.equal(l, List {5, 6, 7, 8, 9, 10})
+  end)
+  it('can be partitioned based on a predicate', function()
+    local l = List.from_iter(Iter.range(1, 10))
+    local evens, odds = l:iter():partition(
+                            function(e)
+          return e % 2 == 0
+        end)
+    assert.are.equal(evens, List {2, 4, 6, 8, 10})
+    assert.are.equal(odds, List {1, 3, 5, 7, 9})
+  end)
+end)
diff --git a/tests/plenary/rotate_spec.lua b/tests/plenary/rotate_spec.lua
new file mode 100644
index 0000000..44c2f9e
--- /dev/null
+++ b/tests/plenary/rotate_spec.lua
@@ -0,0 +1,18 @@
+local rotate = require('plenary.vararg').rotate
+
+local eq = vim.deep_equal
+
+describe('rotate', function ()
+  it('should rotate varargs', function ()
+    eq({rotate(1, 2, 3)}, {2, 3, 1})
+    eq({rotate(1, 2, 3, 4, 5, 6, 7, 8, 9)}, {2, 3, 4, 5, 6, 7, 8, 9, 1})
+  end)
+
+  it('should rotate none', function ()
+    eq({rotate()}, {})
+  end)
+
+  it('should rotate one', function ()
+    eq({rotate(1)}, {1})
+  end)
+end)