Repository: mwlon/pcodec (language: python)

Issue:
Format Python files using a standard tool
The Python files in the repo are not manually formatted. To ensure consistent styling we should use a common formatter such as `black` or `yapf` and define any custom style settings in `pyproject.toml`.


Discussion / hints:
@mwlon, do you have any preference the formatting tool apart from preferring 2-space indents? Personally, I've been happy with Black but it's strictly 4-space indents. Here are four of the popular formatters:
- [autopep8](https://github.com/hhatto/autopep8) - autopep8 automatically formats Python code to conform to the [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guide. It uses the [pycodestyle](https://github.com/PyCQA/pycodestyle) utility to determine what parts of the code needs to be formatted. autopep8 is capable of fixing most of the formatting [issues](https://pycodestyle.readthedocs.org/en/latest/intro.html#error-codes) that can be reported by pycodestyle.
- [Black](https://black.readthedocs.io/en/stable/index.html) - By using Black, you agree to cede control over minutiae of hand-formatting. In return, Black gives you speed, determinism, and freedom from pycodestyle nagging about formatting. You will save time and mental energy for more important matters.
- [yapf](https://github.com/google/yapf) - YAPF is a Python formatter based on [clang-format](https://clang.llvm.org/docs/ClangFormat.html) (developed by Daniel Jasper). In essence, the algorithm takes the code and calculates the best formatting that conforms to the configured style. It takes away a lot of the drudgery of maintaining your code.
- [Ruff](https://docs.astral.sh/ruff/) - The Ruff formatter is an extremely fast Python code formatter designed as a drop-in replacement for [Black](https://pypi.org/project/black/), available as part of the ruff CLI via ruff format.

If you want 2-space indents I'd probably consider yapf or Ruff. Otherwise, I'd probably stick with Black.
I've actually heard good things about Ruff. Would you be on board with it?
Sure. Once the #203 is merged we can do a formatting PR with Ruff. 
I personally use flake8 in most of my stuff, and personally prefer 4-space indents lol.  I'll be good with whatever you decide tho
Yeah a couple people have expressed preference for that, and it's the default, so let's just use the default formatting. Though customizability and speed are pros regardless, so let's go with ruff.

Fix patch:
diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml
index 8b0cb977..ee427adb 100644
--- a/.github/workflows/python.yml
+++ b/.github/workflows/python.yml
@@ -11,6 +11,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - uses: actions/checkout@v3
+    - uses: jpetrucciani/ruff-check@main
     - uses: actions/setup-python@v4
       with:
         python-version: '3.11'
diff --git a/pco_cli/generate_randoms.py b/pco_cli/generate_randoms.py
index 89bdfb90..c4eefc1b 100644
--- a/pco_cli/generate_randoms.py
+++ b/pco_cli/generate_randoms.py
@@ -2,297 +2,353 @@
 # pip requirement: numpy
 
 import argparse
-import numpy as np
-from datetime import datetime
 import os
+from datetime import datetime
 from pathlib import Path
 
+import numpy as np
+
 n = 1_000_000
-max_n = 2 ** 24 - 1
+max_n = 2**24 - 1
+
 
 def write_generic(strs, arr, full_name, base_dir):
-  print(f'writing {full_name}...')
-  joined = '\n'.join(strs)
-  with open(base_dir / 'txt' / f'{full_name}.txt', 'w') as f:
-    f.write(joined)
-  with open(base_dir / 'binary' / f'{full_name}.bin', 'wb') as f:
-    f.write(arr.tobytes())
+    print(f"writing {full_name}...")
+    joined = "\n".join(strs)
+    with open(base_dir / "txt" / f"{full_name}.txt", "w") as f:
+        f.write(joined)
+    with open(base_dir / "binary" / f"{full_name}.bin", "wb") as f:
+        f.write(arr.tobytes())
+
 
 WRITERS = {}
+
+
 def writer(f):
-  name = f.__name__
-  assert name.startswith('write_')
-  dtype = name.removeprefix('write_')
-  WRITERS[dtype] = f
-  return f
+    name = f.__name__
+    assert name.startswith("write_")
+    dtype = name.removeprefix("write_")
+    WRITERS[dtype] = f
+    return f
+
 
 def write_dispatch(dtype, arr, name, base_dir):
-  WRITERS[dtype](arr, name, base_dir)
+    WRITERS[dtype](arr, name, base_dir)
+
 
 @writer
 def write_i32(arr, name, base_dir):
-  if arr.dtype != np.int32:
-    arr = np.floor(arr).astype(np.int32)
-  strs = [str(x) for x in arr]
-  full_name = f'i32_{name}'
-  write_generic(strs, arr, full_name, base_dir)
+    if arr.dtype != np.int32:
+        arr = np.floor(arr).astype(np.int32)
+    strs = [str(x) for x in arr]
+    full_name = f"i32_{name}"
+    write_generic(strs, arr, full_name, base_dir)
+
 
 @writer
 def write_u32(arr, name, base_dir):
-  if arr.dtype != np.uint32:
-    arr = np.floor(arr).astype(np.uint32)
-  strs = [str(x) for x in arr]
-  full_name = f'u32_{name}'
-  write_generic(strs, arr, full_name, base_dir)
+    if arr.dtype != np.uint32:
+        arr = np.floor(arr).astype(np.uint32)
+    strs = [str(x) for x in arr]
+    full_name = f"u32_{name}"
+    write_generic(strs, arr, full_name, base_dir)
+
 
 @writer
 def write_i64(arr, name, base_dir):
-  if arr.dtype != np.int64:
-    arr = np.floor(arr).astype(np.int64)
-  strs = [str(x) for x in arr]
-  full_name = f'i64_{name}'
-  write_generic(strs, arr, full_name, base_dir)
+    if arr.dtype != np.int64:
+        arr = np.floor(arr).astype(np.int64)
+    strs = [str(x) for x in arr]
+    full_name = f"i64_{name}"
+    write_generic(strs, arr, full_name, base_dir)
+
 
 @writer
 def write_f16(arr, name, base_dir):
-  arr = arr.astype(np.float16)
-  strs = [str(x) for x in arr]
-  full_name = f'f16_{name}'
-  write_generic(strs, arr, full_name, base_dir)
+    arr = arr.astype(np.float16)
+    strs = [str(x) for x in arr]
+    full_name = f"f16_{name}"
+    write_generic(strs, arr, full_name, base_dir)
+
 
 @writer
 def write_f32(arr, name, base_dir):
-  arr = arr.astype(np.float32)
-  strs = [str(x) for x in arr]
-  full_name = f'f32_{name}'
-  write_generic(strs, arr, full_name, base_dir)
+    arr = arr.astype(np.float32)
+    strs = [str(x) for x in arr]
+    full_name = f"f32_{name}"
+    write_generic(strs, arr, full_name, base_dir)
+
 
 @writer
 def write_f64(arr, name, base_dir):
-  arr = arr.astype(np.float64)
-  strs = [str(x) for x in arr]
-  full_name = f'f64_{name}'
-  write_generic(strs, arr, full_name, base_dir)
+    arr = arr.astype(np.float64)
+    strs = [str(x) for x in arr]
+    full_name = f"f64_{name}"
+    write_generic(strs, arr, full_name, base_dir)
+
 
 @writer
 def write_timestamp_micros(arr, name, base_dir):
-  if arr.dtype != np.int64:
-    arr = np.floor(arr).astype(np.int64)
-  ts = [datetime.utcfromtimestamp(x / 10 ** 6) for x in arr]
-  strs = [x.strftime('%Y-%m-%dT%H:%M:%S:%fZ') for x in ts]
-  full_name = f'micros_{name}'
-  write_generic(strs, arr, full_name, base_dir)
+    if arr.dtype != np.int64:
+        arr = np.floor(arr).astype(np.int64)
+    ts = [datetime.utcfromtimestamp(x / 10**6) for x in arr]
+    strs = [x.strftime("%Y-%m-%dT%H:%M:%S:%fZ") for x in ts]
+    full_name = f"micros_{name}"
+    write_generic(strs, arr, full_name, base_dir)
 
 
 DATA_GENS = {}
+
+
 def datagen(*dtypes):
-  def decorate(f):
-    name = f.__name__
-    DATA_GENS[name] = (f, dtypes)
-    return f
-  return decorate
+    def decorate(f):
+        name = f.__name__
+        DATA_GENS[name] = (f, dtypes)
+        return f
+
+    return decorate
+
 
-@datagen('i64')
+@datagen("i64")
 def geo():
-  return np.random.geometric(p=0.001, size=n)
+    return np.random.geometric(p=0.001, size=n)
+
 
 def fixed_median_lomax(a, median):
-  unscaled_median = 2 ** (1 / a) - 1
-  return np.random.pareto(a=a, size=n) / unscaled_median * median
+    unscaled_median = 2 ** (1 / a) - 1
+    return np.random.pareto(a=a, size=n) / unscaled_median * median
+
 
-@datagen('i32', 'u32', 'i64')
+@datagen("i32", "u32", "i64")
 def lomax05():
-  return fixed_median_lomax(0.5, 1000)
+    return fixed_median_lomax(0.5, 1000)
+
 
-@datagen('i64')
+@datagen("i64")
 def uniform():
-  return np.random.randint(-2**63, 2**63, size=max_n)[:n]
+    return np.random.randint(-(2**63), 2**63, size=max_n)[:n]
+
+
 # disable the following by default because it's kinda a waste of disk:
 # @datagen('i64')
 # def uniform_xl():
 #   return np.random.randint(-2**63, 2**63, size=max_n)
 
-@datagen('i64')
+
+@datagen("i64")
 def constant():
-  return np.repeat(77777, n)
+    return np.repeat(77777, n)
+
 
-@datagen('i64')
+@datagen("i64")
 def sparse():
-  return np.random.binomial(1, p=0.01, size=n)
+    return np.random.binomial(1, p=0.01, size=n)
+
 
 money = {}
+
+
 def gen_money_once():
-  if 'dollars' in money:
-    return
-  dollars = np.floor(fixed_median_lomax(1.5, 5)).astype(np.int64)
-  cents = np.random.randint(0, 100, size=n)
-  p = np.random.uniform(size=n)
-  cents[p < 0.9] = 99
-  cents[p < 0.75] = 98
-  cents[p < 0.6] = 95
-  cents[p < 0.45] = 75
-  cents[p < 0.4] = 50
-  cents[p < 0.25] = 25
-  cents[p < 0.15] = 0
-  total_cents = dollars * 100 + cents
-
-  money['dollars'] = dollars
-  money['cents'] = cents
-  money['total_cents'] = total_cents
-
-
-@datagen('i64')
+    if "dollars" in money:
+        return
+    dollars = np.floor(fixed_median_lomax(1.5, 5)).astype(np.int64)
+    cents = np.random.randint(0, 100, size=n)
+    p = np.random.uniform(size=n)
+    cents[p < 0.9] = 99
+    cents[p < 0.75] = 98
+    cents[p < 0.6] = 95
+    cents[p < 0.45] = 75
+    cents[p < 0.4] = 50
+    cents[p < 0.25] = 25
+    cents[p < 0.15] = 0
+    total_cents = dollars * 100 + cents
+
+    money["dollars"] = dollars
+    money["cents"] = cents
+    money["total_cents"] = total_cents
+
+
+@datagen("i64")
 def dollars():
-  gen_money_once()
-  return money['dollars']
+    gen_money_once()
+    return money["dollars"]
 
-@datagen('i64')
+
+@datagen("i64")
 def cents():
-  gen_money_once()
-  return money['cents']
+    gen_money_once()
+    return money["cents"]
+
 
-@datagen('i64')
+@datagen("i64")
 def total_cents():
-  gen_money_once()
-  return money['total_cents']
+    gen_money_once()
+    return money["total_cents"]
 
-@datagen('i64', 'f64')
+
+@datagen("i64", "f64")
 def slow_cosine():
-  amplitude = 100000
-  periods = 103 # something prime
-  period = n / periods
-  return 100_000 * np.cos(np.arange(n) * 2 * np.pi / period)
+    periods = 103  # something prime
+    period = n / periods
+    return 100_000 * np.cos(np.arange(n) * 2 * np.pi / period)
+
 
 # Including lower precisions mostly just to test performance bottlenecks
-@datagen('f64', 'f32', 'f16')
+@datagen("f64", "f32", "f16")
 def normal():
-  return np.random.normal(size=n)
+    return np.random.normal(size=n)
 
-@datagen('f32')
+
+@datagen("f32")
 def log_normal():
-  return np.exp(normal())
+    return np.exp(normal())
+
 
-@datagen('f32')
+@datagen("f32")
 def csum():
-  return np.cumsum(log_normal() - np.exp(0.5))
+    return np.cumsum(log_normal() - np.exp(0.5))
+
 
 # timestamps increasing 1s at a time on average from 2022-01-01T00:00:00 with
 # 1s random jitter
-@datagen('timestamp_micros')
+@datagen("timestamp_micros")
 def near_linear():
-  return 10**6 * (1640995200 + np.arange(n) + np.random.normal(size=n))
+    return 10**6 * (1640995200 + np.arange(n) + np.random.normal(size=n))
+
 
 # millisecond timestamps compressed as microseconds
-@datagen('timestamp_micros')
+@datagen("timestamp_micros")
 def millis():
-  return 10 ** 3 * (1640995200000  + np.random.randint(0, 10 ** 9, size=n))
+    return 10**3 * (1640995200000 + np.random.randint(0, 10**9, size=n))
+
 
 # integers compressed as floats
-@datagen('f64')
+@datagen("f64")
 def integers():
-  return np.random.randint(0, 2 ** 30, size=n)
+    return np.random.randint(0, 2**30, size=n)
+
 
 # `float32`s compressed as `float64`s
-@datagen('f64')
+@datagen("f64")
 def quantized_normal():
     return np.random.normal(size=n).astype(np.float32).astype(np.float64)
 
+
 # decimal floats
-@datagen('f64')
+@datagen("f64")
 def decimal():
-  return np.random.randint(1000, 10000, size=n) / 100
+    return np.random.randint(1000, 10000, size=n) / 100
 
-@datagen('f64')
+
+@datagen("f64")
 def radians():
-  return (np.arange(n) + 10) * np.pi
+    return (np.arange(n) + 10) * np.pi
+
 
 # 10 interleaved 0th order sequences with different scales
-@datagen('i64')
+@datagen("i64")
 def interl0():
-  bases = 10 ** np.arange(10)
-  interleaved = bases[None, :] + np.random.normal(scale=22, size=[n // 10, 10])
-  return interleaved.reshape(-1)
+    bases = 10 ** np.arange(10)
+    interleaved = bases[None, :] + np.random.normal(scale=22, size=[n // 10, 10])
+    return interleaved.reshape(-1)
+
 
 # 10 interleaved 1st order sequences with different scales
 def interl1_helper():
-  deltas = np.random.randint(-10, 10, size=[n // 10, 10])
-  bases = 10 ** np.arange(10)
-  return bases[None, :] + np.cumsum(deltas, axis=0)
+    deltas = np.random.randint(-10, 10, size=[n // 10, 10])
+    bases = 10 ** np.arange(10)
+    return bases[None, :] + np.cumsum(deltas, axis=0)
 
-@datagen('i64')
+
+@datagen("i64")
 def interl1():
-  interleaved = interl1_helper()
-  return interleaved.reshape(-1)
+    interleaved = interl1_helper()
+    return interleaved.reshape(-1)
+
 
 # the same as interleaved, but shuffled within each group of 10
-@datagen('i64')
+@datagen("i64")
 def interl_scrambl1():
-  interleaved = interl1_helper()
-  idxs = np.random.rand(*interleaved.shape).argsort(axis=1)
-  interleaved_scrambled = np.take_along_axis(interleaved, idxs, axis=1)
-  return interleaved_scrambled.reshape(-1)
+    interleaved = interl1_helper()
+    idxs = np.random.rand(*interleaved.shape).argsort(axis=1)
+    interleaved_scrambled = np.take_along_axis(interleaved, idxs, axis=1)
+    return interleaved_scrambled.reshape(-1)
+
 
 # a sequence whose variance gradually increases
-@datagen('i64')
+@datagen("i64")
 def dist_shift():
-  log_std = np.linspace(-1.5, 25, n)
-  return 0.5 + np.exp(log_std) * np.random.normal(size=n)
+    log_std = np.linspace(-1.5, 25, n)
+    return 0.5 + np.exp(log_std) * np.random.normal(size=n)
+
 
 # a diabolically hard sequence
 # * float decimals plus small jitter
 # * numerous interleaved subsequences with occasionally missing elements
 # * each subsequence benefits from delta
 # * distribution shift on delta size
-@datagen('f64')
+@datagen("f64")
 def diablo():
-  subseq_idx = 0
-  n_subseqs = 77
-  subseq_vals = np.random.randint(1E4, 1E5, size=n_subseqs)
-  the_data = []
-  log_delta_scale = 0.0
-  frequency = 1E-4
-  add_scale = np.sqrt(np.exp(2 * frequency) - 1)
-  mult = np.exp(-frequency)
-  while len(the_data) < n:
-    the_data.extend(subseq_vals[np.random.uniform(size=n_subseqs) > 0.9])
-    log_delta_scale += np.random.normal() * add_scale
-    log_delta_scale *= mult
-    delta_scale = 3 * np.exp(log_delta_scale)
-    subseq_vals += np.random.uniform(-delta_scale, delta_scale, size=n_subseqs).astype(int)
-  the_data = np.array(the_data[:n]).astype(np.float64)
-  the_data /= 100.0
-  machine_eps = 1.0E-52
-  the_data *= np.random.uniform(1 - 2 * machine_eps, 1 + 3 * machine_eps, size=n)
-  return the_data
+    n_subseqs = 77
+    subseq_vals = np.random.randint(1e4, 1e5, size=n_subseqs)
+    the_data = []
+    log_delta_scale = 0.0
+    frequency = 1e-4
+    add_scale = np.sqrt(np.exp(2 * frequency) - 1)
+    mult = np.exp(-frequency)
+    while len(the_data) < n:
+        the_data.extend(subseq_vals[np.random.uniform(size=n_subseqs) > 0.9])
+        log_delta_scale += np.random.normal() * add_scale
+        log_delta_scale *= mult
+        delta_scale = 3 * np.exp(log_delta_scale)
+        subseq_vals += np.random.uniform(
+            -delta_scale, delta_scale, size=n_subseqs
+        ).astype(int)
+    the_data = np.array(the_data[:n]).astype(np.float64)
+    the_data /= 100.0
+    machine_eps = 1.0e-52
+    the_data *= np.random.uniform(1 - 2 * machine_eps, 1 + 3 * machine_eps, size=n)
+    return the_data
+
 
 def uniquify_preserving_order(xs):
-  return list(dict.fromkeys(xs))
-
-
-if __name__ == '__main__':
-  parser = argparse.ArgumentParser()
-  parser.add_argument('--base_dir', type=Path, default=Path('data'),
-                      help='Directory in which to write the output data')
-  parser.add_argument('datasets', type=str, nargs='*',
-                      help=('Datasets to generate. If not provided, all'
-                            ' datasets are generated. Available datasets are:'
-                            f' {", ".join(DATA_GENS)}'))
-  args = parser.parse_args()
-
-  os.makedirs(args.base_dir / 'txt', exist_ok=True)
-  os.makedirs(args.base_dir / 'binary', exist_ok=True)
-
-  if not args.datasets:
-    datasets = list(DATA_GENS)
-  else:
-    datasets = uniquify_preserving_order(args.datasets)
+    return list(dict.fromkeys(xs))
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "--base_dir",
+        type=Path,
+        default=Path("data"),
+        help="Directory in which to write the output data",
+    )
+    parser.add_argument(
+        "datasets",
+        type=str,
+        nargs="*",
+        help=(
+            'Datasets to generate. If not provided, all'
+            ' datasets are generated. Available datasets are:'
+            f' {", ".join(DATA_GENS)}'
+        ),
+    )
+    args = parser.parse_args()
+
+    os.makedirs(args.base_dir / "txt", exist_ok=True)
+    os.makedirs(args.base_dir / "binary", exist_ok=True)
+
+    if not args.datasets:
+        datasets = list(DATA_GENS)
+    else:
+        datasets = uniquify_preserving_order(args.datasets)
+        for name in datasets:
+            if name not in DATA_GENS:
+                raise NotImplementedError(f"Unrecognized dataset name: {name}")
+
     for name in datasets:
-      if name not in DATA_GENS:
-        raise NotImplementedError(f'Unrecognized dataset name: {name}')
-
-  for name in datasets:
-    (f, dtypes) = DATA_GENS[name]
-    np.random.seed(0)
-    data = f()
-    for dtype in dtypes:
-      write_dispatch(dtype, data, name, args.base_dir)
+        (f, dtypes) = DATA_GENS[name]
+        np.random.seed(0)
+        data = f()
+        for dtype in dtypes:
+            write_dispatch(dtype, data, name, args.base_dir)


Test patch:
diff --git a/pco_python/test/test_standalone.py b/pco_python/test/test_standalone.py
index a60e7d21..647d1a8b 100644
--- a/pco_python/test/test_standalone.py
+++ b/pco_python/test/test_standalone.py
@@ -1,13 +1,20 @@
 import numpy as np
-from pcodec import ChunkConfig, FloatMultSpec, FloatQuantSpec, IntMultSpec, PagingSpec, standalone
 import pytest
+from pcodec import (
+    ChunkConfig,
+    FloatMultSpec,
+    FloatQuantSpec,
+    IntMultSpec,
+    PagingSpec,
+    standalone,
+)
 
 np.random.seed(12345)
 
 all_shapes = (
-  [],
-  [900],
-  [9, 100],
+    [],
+    [900],
+    [9, 100],
 )
 
 all_dtypes = ("f2", "f4", "f8", "i2", "i4", "i8", "u2", "u4", "u8")
@@ -36,143 +43,150 @@
 @pytest.mark.parametrize("shape", all_shapes)
 @pytest.mark.parametrize("dtype", all_dtypes)
 def test_round_trip_decompress_into(shape, dtype):
-  data = np.random.uniform(0, 1000, size=shape).astype(dtype)
-  compressed = standalone.simple_compress(data, ChunkConfig())
+    data = np.random.uniform(0, 1000, size=shape).astype(dtype)
+    compressed = standalone.simple_compress(data, ChunkConfig())
 
-  # decompress exactly
-  out = np.empty_like(data)
-  progress = standalone.simple_decompress_into(compressed, out)
-  np.testing.assert_array_equal(data, out)
-  assert progress.n_processed == data.size
-  assert progress.finished
+    # decompress exactly
+    out = np.empty_like(data)
+    progress = standalone.simple_decompress_into(compressed, out)
+    np.testing.assert_array_equal(data, out)
+    assert progress.n_processed == data.size
+    assert progress.finished
 
 
 @pytest.mark.parametrize("shape", all_shapes)
 @pytest.mark.parametrize("dtype", all_dtypes)
 def test_round_trip_simple_decompress(shape, dtype):
-  data = np.random.uniform(0, 1000, size=shape).astype(dtype)
-  compressed = standalone.simple_compress(data, ChunkConfig(paging_spec=PagingSpec.equal_pages_up_to(300)))
-  out = standalone.simple_decompress(compressed)
-  # data are decompressed into a 1D array; ensure it can be reshaped to the original shape
-  out.shape = shape
-  np.testing.assert_array_equal(data, out)
+    data = np.random.uniform(0, 1000, size=shape).astype(dtype)
+    compressed = standalone.simple_compress(
+        data, ChunkConfig(paging_spec=PagingSpec.equal_pages_up_to(300))
+    )
+    out = standalone.simple_decompress(compressed)
+    # data are decompressed into a 1D array; ensure it can be reshaped to the original shape
+    out.shape = shape
+    np.testing.assert_array_equal(data, out)
 
 
 def test_inexact_decompression():
-  data = np.random.uniform(size=300)
-  compressed = standalone.simple_compress(data, ChunkConfig())
-
-  # decompress partially
-  out = np.zeros(3)
-  progress = standalone.simple_decompress_into(compressed, out)
-  np.testing.assert_array_equal(out, data[:3])
-  assert progress.n_processed == 3
-  assert not progress.finished
-
-  # decompress with room to spare
-  out = np.zeros(600)
-  progress = standalone.simple_decompress_into(compressed, out)
-  np.testing.assert_array_equal(out[:300], data)
-  np.testing.assert_array_equal(out[300:], np.zeros(300))
-  assert progress.n_processed == 300
-  assert progress.finished
+    data = np.random.uniform(size=300)
+    compressed = standalone.simple_compress(data, ChunkConfig())
+
+    # decompress partially
+    out = np.zeros(3)
+    progress = standalone.simple_decompress_into(compressed, out)
+    np.testing.assert_array_equal(out, data[:3])
+    assert progress.n_processed == 3
+    assert not progress.finished
+
+    # decompress with room to spare
+    out = np.zeros(600)
+    progress = standalone.simple_decompress_into(compressed, out)
+    np.testing.assert_array_equal(out[:300], data)
+    np.testing.assert_array_equal(out[300:], np.zeros(300))
+    assert progress.n_processed == 300
+    assert progress.finished
+
 
 def test_simple_decompress_into_errors():
-  """Test possible error states for standalone.simple_decompress_into"""
-  data = np.random.uniform(size=100).astype(np.float32)
-  compressed = standalone.simple_compress(data, ChunkConfig())
+    """Test possible error states for standalone.simple_decompress_into"""
+    data = np.random.uniform(size=100).astype(np.float32)
+    compressed = standalone.simple_compress(data, ChunkConfig())
 
-  out = np.zeros(100).astype(np.float64)
-  with pytest.raises(RuntimeError, match="data type byte does not match"):
-    standalone.simple_decompress_into(compressed, out)
+    out = np.zeros(100).astype(np.float64)
+    with pytest.raises(RuntimeError, match="data type byte does not match"):
+        standalone.simple_decompress_into(compressed, out)
 
 
 def test_simple_decompress_errors():
-  """Test possible error states for standalone.simple_decompress"""
-  data = np.random.uniform(size=100).astype(np.float32)
-  compressed = bytearray(standalone.simple_compress(data, ChunkConfig()))
+    """Test possible error states for standalone.simple_decompress"""
+    data = np.random.uniform(size=100).astype(np.float32)
+    compressed = bytearray(standalone.simple_compress(data, ChunkConfig()))
 
-  truncated = compressed[:8]
-  with pytest.raises(RuntimeError, match="empty bytes"):
-      standalone.simple_decompress(bytes(truncated))
+    truncated = compressed[:8]
+    with pytest.raises(RuntimeError, match="empty bytes"):
+        standalone.simple_decompress(bytes(truncated))
 
-  # corrupt the data with unknown dtype byte
-  # (is this safe to hard code? could the length of the header change in future version?)
-  compressed[8] = 99
-  with pytest.raises(RuntimeError, match="unrecognized dtype byte"):
-      standalone.simple_decompress(bytes(compressed))
+    # corrupt the data with unknown dtype byte
+    # (is this safe to hard code? could the length of the header change in future version?)
+    compressed[8] = 99
+    with pytest.raises(RuntimeError, match="unrecognized dtype byte"):
+        standalone.simple_decompress(bytes(compressed))
 
-  # this happens if the user passed in a file with no chunks.
-  compressed[8] = 0
-  assert standalone.simple_decompress(bytes(compressed)) is None
+    # this happens if the user passed in a file with no chunks.
+    compressed[8] = 0
+    assert standalone.simple_decompress(bytes(compressed)) is None
 
 
 def test_compression_options():
-  data = np.random.normal(size=100).astype(np.float32)
-  default_size = len(standalone.simple_compress(data, ChunkConfig()))
-
-  # this is mostly just to check that there is no error, but these settings
-  # should give worse compression than the defaults
-  assert (len(
-      standalone.simple_compress(
-          data,
-          ChunkConfig(
-              compression_level=0,
-              delta_encoding_order=1,
-              int_mult_spec=IntMultSpec.disabled(),
-              float_mult_spec=FloatMultSpec.disabled(),
-              float_quant_spec=FloatQuantSpec.disabled(),
-              paging_spec=PagingSpec.equal_pages_up_to(77),
-          ),
-      )) > default_size)
+    data = np.random.normal(size=100).astype(np.float32)
+    default_size = len(standalone.simple_compress(data, ChunkConfig()))
+
+    # this is mostly just to check that there is no error, but these settings
+    # should give worse compression than the defaults
+    assert (
+        len(
+            standalone.simple_compress(
+                data,
+                ChunkConfig(
+                    compression_level=0,
+                    delta_encoding_order=1,
+                    int_mult_spec=IntMultSpec.disabled(),
+                    float_mult_spec=FloatMultSpec.disabled(),
+                    float_quant_spec=FloatQuantSpec.disabled(),
+                    paging_spec=PagingSpec.equal_pages_up_to(77),
+                ),
+            )
+        )
+        > default_size
+    )
 
 
 @pytest.mark.parametrize("int_mult_spec", all_int_mult_specs)
 def test_compression_int_mult_spec_options(int_mult_spec):
-  data = (np.random.normal(size=100) * 1000).astype(np.int32)
+    data = (np.random.normal(size=100) * 1000).astype(np.int32)
 
-  # check for errors
-  compressed = standalone.simple_compress(
-    data,
-    ChunkConfig(int_mult_spec=int_mult_spec),
-  )
+    # check for errors
+    compressed = standalone.simple_compress(
+        data,
+        ChunkConfig(int_mult_spec=int_mult_spec),
+    )
 
-  out = standalone.simple_decompress(compressed)
+    out = standalone.simple_decompress(compressed)
 
-  # check that the decompressed data is correct
-  np.testing.assert_array_equal(data, out)
+    # check that the decompressed data is correct
+    np.testing.assert_array_equal(data, out)
 
 
 @pytest.mark.parametrize("float_mult_spec", all_float_mult_specs)
 def test_compression_float_mult_spec_options(float_mult_spec):
-  data = (np.random.normal(size=100) * 1000).astype(np.int32) * np.pi
+    data = (np.random.normal(size=100) * 1000).astype(np.int32) * np.pi
 
-  # check for errors
-  compressed = standalone.simple_compress(
-    data,
-    ChunkConfig(float_mult_spec=float_mult_spec),
-  )
+    # check for errors
+    compressed = standalone.simple_compress(
+        data,
+        ChunkConfig(float_mult_spec=float_mult_spec),
+    )
 
-  out = standalone.simple_decompress(compressed)
+    out = standalone.simple_decompress(compressed)
 
-  # check that the decompressed data is correct
-  np.testing.assert_array_equal(data, out)
+    # check that the decompressed data is correct
+    np.testing.assert_array_equal(data, out)
 
 
 @pytest.mark.parametrize("float_quant_spec", all_float_quant_specs)
 def test_compression_float_quant_spec_options(float_quant_spec):
-  data = np.random.normal(size=100).astype(np.float32).astype(np.float64)
+    data = np.random.normal(size=100).astype(np.float32).astype(np.float64)
 
-  # check for errors
-  compressed = standalone.simple_compress(
-    data,
-    ChunkConfig(
-      float_mult_spec=FloatMultSpec.disabled(),
-      float_quant_spec=float_quant_spec,
-    ),
-  )
+    # check for errors
+    compressed = standalone.simple_compress(
+        data,
+        ChunkConfig(
+            float_mult_spec=FloatMultSpec.disabled(),
+            float_quant_spec=float_quant_spec,
+        ),
+    )
 
-  out = standalone.simple_decompress(compressed)
+    out = standalone.simple_decompress(compressed)
 
-  # check that the decompressed data is correct
-  np.testing.assert_array_equal(data, out)
+    # check that the decompressed data is correct
+    np.testing.assert_array_equal(data, out)
diff --git a/pco_python/test/test_wrapped.py b/pco_python/test/test_wrapped.py
index 9e1a2520..c870424c 100644
--- a/pco_python/test/test_wrapped.py
+++ b/pco_python/test/test_wrapped.py
@@ -1,49 +1,50 @@
 import numpy as np
+import pytest
 from pcodec import ChunkConfig, PagingSpec
 from pcodec.wrapped import FileCompressor, FileDecompressor
-import pytest
 
 np.random.seed(12345)
-all_dtypes = ('f2', 'f4', 'f8', 'i2', 'i4', 'i8', 'u2', 'u4', 'u8')
+all_dtypes = ("f2", "f4", "f8", "i2", "i4", "i8", "u2", "u4", "u8")
+
 
 @pytest.mark.parametrize("dtype", all_dtypes)
 def test_compress(dtype):
-  data = np.random.uniform(0, 1000, size=[10]).astype(dtype)
-  pco_dtype = dtype[0].upper() + str(int(dtype[1]) * 8)
-  page_sizes = [6, 4] # so there are 2 pages
+    data = np.random.uniform(0, 1000, size=[10]).astype(dtype)
+    pco_dtype = dtype[0].upper() + str(int(dtype[1]) * 8)
+    page_sizes = [6, 4]  # so there are 2 pages
 
-  # compress
-  fc = FileCompressor()
-  header = fc.write_header()
-  cc = fc.chunk_compressor(
-    data,
-    ChunkConfig(paging_spec=PagingSpec.exact_page_sizes(page_sizes)),
-  )
-  assert cc.n_per_page() == page_sizes
-  chunk_meta = cc.write_chunk_meta()
-  page0 = cc.write_page(0)
-  page1 = cc.write_page(1)
-  with pytest.raises(RuntimeError, match="page idx exceeds num pages"):
-    cc.write_page(2)
+    # compress
+    fc = FileCompressor()
+    header = fc.write_header()
+    cc = fc.chunk_compressor(
+        data,
+        ChunkConfig(paging_spec=PagingSpec.exact_page_sizes(page_sizes)),
+    )
+    assert cc.n_per_page() == page_sizes
+    chunk_meta = cc.write_chunk_meta()
+    page0 = cc.write_page(0)
+    page1 = cc.write_page(1)
+    with pytest.raises(RuntimeError, match="page idx exceeds num pages"):
+        cc.write_page(2)
 
-  # decompress
-  fd, n_bytes_read = FileDecompressor.from_header(header)
-  assert n_bytes_read == len(header)
-  # check that undershooting is fine
-  _, n_bytes_read = FileDecompressor.from_header(header + b'foo')
-  assert n_bytes_read == len(header)
-  cd, n_bytes_read = fd.read_chunk_meta(chunk_meta, pco_dtype)
-  assert n_bytes_read == len(chunk_meta)
+    # decompress
+    fd, n_bytes_read = FileDecompressor.from_header(header)
+    assert n_bytes_read == len(header)
+    # check that undershooting is fine
+    _, n_bytes_read = FileDecompressor.from_header(header + b"foo")
+    assert n_bytes_read == len(header)
+    cd, n_bytes_read = fd.read_chunk_meta(chunk_meta, pco_dtype)
+    assert n_bytes_read == len(chunk_meta)
 
-  # page 1, which has elements 6-10
-  dst1 = np.zeros(100).astype(dtype)
-  progress, n_bytes_read = cd.read_page_into(page1, 4, dst1)
-  np.testing.assert_array_equal(dst1[4:], np.zeros(96))
-  np.testing.assert_array_equal(dst1[:4], data[6:])
-  assert n_bytes_read == len(page1)
+    # page 1, which has elements 6-10
+    dst1 = np.zeros(100).astype(dtype)
+    progress, n_bytes_read = cd.read_page_into(page1, 4, dst1)
+    np.testing.assert_array_equal(dst1[4:], np.zeros(96))
+    np.testing.assert_array_equal(dst1[:4], data[6:])
+    assert n_bytes_read == len(page1)
 
-  # page 0, which has elements 0-6
-  dst0 = np.zeros(6).astype(dtype)
-  progress, n_bytes_read = cd.read_page_into(page0, 6, dst0)
-  np.testing.assert_array_equal(dst0, data[:6])
-  assert n_bytes_read == len(page0)
+    # page 0, which has elements 0-6
+    dst0 = np.zeros(6).astype(dtype)
+    progress, n_bytes_read = cd.read_page_into(page0, 6, dst0)
+    np.testing.assert_array_equal(dst0, data[:6])
+    assert n_bytes_read == len(page0)