~amirouche/lexode

a50bf4c970349be99a3a34e539913f2990e4f8c4 — Amirouche 6 months ago bf2c6f0
rework...

It used to not work at all; also added support for float using double
encoding, and nested tuple support, with one test that wants to be
comprehensive that pass green in sourcehut build service with pypy
3.7, 3.8 and stable cpython 3.7+
9 files changed, 449 insertions(+), 113 deletions(-)

A .build.yml
M README.md
A ci.sh
M lexode.py
A poetry.lock
M pyproject.toml
A rc.sh
A tests.py
A venv
A .build.yml => .build.yml +64 -0
@@ 0,0 1,64 @@
image: ubuntu/lts
packages:
  # Dependencies to compile cpython
  - build-essential
  - curl
  - libreadline-dev
  - libbz2-dev
  - libffi-dev
  - liblzma-dev
  - libncursesw5-dev
  - libsqlite3-dev
  - libssl-dev
  - libxml2-dev
  - libxmlsec1-dev
  - make
  - wget
  - xz-utils
  - zlib1g-dev

secrets:
  - 933b7505-48cb-4741-9040-d4f69811f77f

sources:
  - https://git.sr.ht/~amirouche/lexode

tasks:

  - pypy-37: |
      wget -q https://downloads.python.org/pypy/pypy3.7-v7.3.7-linux64.tar.bz2
      tar xf pypy3.7-v7.3.7-linux64.tar.bz2
      cd lexode
      PATH=$(pwd)/../pypy3.7-v7.3.7-linux64/bin/:$PATH ./ci.sh 3.7
      rm -rf .venv 

  - pypy-38: |
      wget -q https://downloads.python.org/pypy/pypy3.8-v7.3.7-linux64.tar.bz2
      tar xf pypy3.8-v7.3.7-linux64.tar.bz2
      cd lexode
      PATH=$(pwd)/../pypy3.8-v7.3.7-linux64/bin/:$PATH ./ci.sh 3.8
      rm -rf .venv 

  - cpython-37: |
      git clone --depth=1 --branch 3.7 https://github.com/python/cpython && cd cpython && ./configure --prefix=$HOME/.local/ && make -j$(nproc) > /dev/null && make install > /dev/null && cd ../ && rm -rf  cpython
      cd lexode
      PATH=$HOME/.local/bin:$PATH ./ci.sh 3.7
      rm -rf .venv

  - cpython-38: |
      git clone --depth=1 --branch 3.8 https://github.com/python/cpython && cd cpython && ./configure --prefix=$HOME/.local/ > /dev/null && make -j$(nproc) > /dev/null && make install > /dev/null && cd .. && rm -rf cpython
      cd lexode
      PATH=$HOME/.local/bin:$PATH ./ci.sh 3.8
      rm -rf .venv $HOME/.local/bin

  - cpython-39: |
      git clone --depth=1 --branch 3.9 https://github.com/python/cpython && cd cpython && ./configure --prefix=$HOME/.local/ > /dev/null&& make -j$(nproc) > /dev/null && make install > /dev/null && cd .. && rm -rf cpython
      cd lexode
      PATH=$HOME/.local/bin:$PATH ./ci.sh 3.9
      rm -rf .venv $HOME/.local/bin

  - cpython-310: |
      git clone --depth=1 --branch 3.10 https://github.com/python/cpython && cd cpython && ./configure --prefix=$HOME/.local/ > /dev/null && make -j$(nproc) > /dev/null && make install > /dev/null && cd .. && rm -rf cpython
      cd lexode
      PATH=$HOME/.local/bin:$PATH ./ci.sh 3.10
      rm -rf .venv $HOME/.local/bin

M README.md => README.md +2 -0
@@ 1,5 1,7 @@
# python-lexode

**beta**

lexicographic packing and unpacking functions for ordered key-value stores

![Funny bunny in front of brick of wall](https://images.unsplash.com/photo-1584759985030-352927f481a3?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9)

A ci.sh => ci.sh +29 -0
@@ 0,0 1,29 @@
#!/bin/bash

if [ "$1" = "" ]; then
    echo "Please provide a the python major.minor version as first argument."
    exit 1
fi;

set -xe

PYTHON_MAJOR_MINOR=$1

rm -rf .venv

echo "exit()" | ./venv python$PYTHON_MAJOR_MINOR
./venv pip install --quiet poetry
# CPython 3.9+ requires to install cython prior to poetry install
# (possibly because there is no binary wheel for that version in pypi
# as of yet)
./venv poetry install --no-ansi --quiet
./venv pytest tests.py

# Publish if there is a tag on the current commit

# XXX: use set +x to avoid to leak the pypi secret token inside ~/.pypi-token!
set +x
git tag -l --points-at $(git show -q --format=%H) | grep v && ./venv poetry config http-basic.pypi __token__ $(cat ~/.pypi-token) || true
set -x
git tag -l --points-at $(git show -q --format=%H) | grep v && ./venv poetry build --format wheel || true
git tag -l --points-at $(git show -q --format=%H) | grep v && ./venv poetry publish || true

M lexode.py => lexode.py +56 -113
@@ 1,7 1,7 @@
# This source file was part of the FoundationDB open source project
#
# Copyright 2013-2018 Apple Inc. and the FoundationDB project authors
# Copyright 2018 Amirouche Boubekki <amirouche.boubekki@gmail.com>
# Copyright 2018-2022 Amirouche Boubekki <amirouche@hyper.dev>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.


@@ 17,47 17,23 @@
#
import struct

from uuid import UUID
from bisect import bisect_left


_size_limits = tuple((1 << (i * 8)) - 1 for i in range(9))

int2byte = struct.Struct(">B").pack

# Define type codes:
BYTES_CODE = 0x01
DOUBLE_CODE = 0x21
FALSE_CODE = 0x26
INT_ZERO_CODE = 0x14
NEG_INT_START = 0x0B
NESTED_CODE = 0x05
DOUBLE_CODE = 0x09
FALSE_CODE = 0x02
INTEGER_NEGATIVE_CODE = 0x04
INTEGER_POSITIVE_CODE = 0x06
INTEGER_ZERO = 0x05
NESTED_CODE = 0x08
NULL_CODE = 0x00
POS_INT_END = 0x1D
STRING_CODE = 0x02
TRUE_CODE = 0x27
UUID_CODE = 0x30


# Reserved: Codes 0x03, 0x04, 0x23, and 0x24 are reserved for historical reasons.

def _reduce_children(child_values):
    version_pos = -1
    len_so_far = 0
    bytes_list = []
    for child_bytes, child_pos in child_values:
        if child_pos >= 0:
            if version_pos >= 0:
                raise ValueError("Multiple incomplete versionstamps included in tuple")
            version_pos = len_so_far + child_pos
        len_so_far += len(child_bytes)
        bytes_list.append(child_bytes)
    return bytes_list, version_pos
STRING_CODE = 0x07
TRUE_CODE = 0x03


def next_prefix(key):
    key = key.rstrip(b"\xff")
    return key[:-1] + int2byte(ord(key[-1:]) + 1)
INTEGER_MAX = struct.unpack('>Q', b'\xff' * 8)[0]


def _find_terminator(v, pos):


@@ 66,7 42,7 @@ def _find_terminator(v, pos):
        pos = v.find(b"\x00", pos)
        if pos < 0:
            return len(v)
        if pos + 1 == len(v) or v[pos + 1 : pos + 2] != b"\xff":
        if pos + 1 == len(v) or v[pos + 1:pos + 2] != b"\xff":
            return pos
        pos += 2



@@ 75,11 51,11 @@ def _find_terminator(v, pos):
# If decoding and sign bit is 0 (negative), flip all of the bits. Otherwise, just flip sign.
def _float_adjust(v, encode):
    if encode and v[0] & 0x80 != 0x00:
        return b"".join((int2byte(x ^ 0xFF) for x in v))
        return bytes(x ^ 0xFF for x in v)
    elif not encode and v[0] & 0x80 != 0x80:
        return b"".join((int2byte(x ^ 0xFF) for x in v))
        return bytes(x ^ 0xFF for x in v)
    else:
        return int2byte(v[0] ^ 0x80) + v[1:]
        return bytes((v[0] ^ 0x80,)) + v[1:]


def _decode(v, pos):


@@ 92,43 68,25 @@ def _decode(v, pos):
    elif code == STRING_CODE:
        end = _find_terminator(v, pos + 1)
        return v[pos + 1 : end].replace(b"\x00\xFF", b"\x00").decode("utf-8"), end + 1
    elif code >= INT_ZERO_CODE and code < POS_INT_END:
        n = code - 20
        end = pos + 1 + n
        return struct.unpack(">Q", b"\x00" * (8 - n) + v[pos + 1 : end])[0], end
    elif code > NEG_INT_START and code < INT_ZERO_CODE:
        n = 20 - code
        end = pos + 1 + n
        return (
            struct.unpack(">Q", b"\x00" * (8 - n) + v[pos + 1 : end])[0]
            - _size_limits[n],
            end,
        )
    elif code == POS_INT_END:  # 0x1d; Positive 9-255 byte integer
        length = v[pos + 1]
        val = 0
        for i in range(length):
            val = val << 8
            val += v[pos + 2 + i]
        return val, pos + 2 + length
    elif code == NEG_INT_START:  # 0x0b; Negative 9-255 byte integer
        length = v[pos + 1] ^ 0xFF
        val = 0
        for i in range(length):
            val = val << 8
            val += v[pos + 2 + i]
        return val - (1 << (length * 8)) + 1, pos + 2 + length
    elif code == INTEGER_ZERO:
        return 0, pos + 1
    elif code == INTEGER_NEGATIVE_CODE:
        end = pos + 1 + 8
        value = struct.unpack(">Q", v[pos + 1 : end])[0] - INTEGER_MAX
        return value, end
    elif code == INTEGER_POSITIVE_CODE:
        end = pos + 1 + 8
        value = struct.unpack(">Q", v[pos + 1 : end])[0]
        return value, end
    elif code == FALSE_CODE:
        return False, pos + 1
    elif code == TRUE_CODE:
        return True, pos + 1
    elif code == DOUBLE_CODE:
        return (
            struct.unpack(">d", _float_adjust(v[pos + 1 : pos + 9], False))[0],
            pos + 9,
        )
    elif code == UUID_CODE:
        return UUID(bytes=v[pos + 1 : pos + 17]), pos + 17
    elif code == FALSE_CODE:
        return False, pos + 1
    elif code == TRUE_CODE:
        return True, pos + 1
    elif code == NESTED_CODE:
        ret = []
        end_pos = pos + 1


@@ 144,62 102,45 @@ def _decode(v, pos):
                ret.append(val)
        return tuple(ret), end_pos + 1
    else:
        raise ValueError("Unknown data type in DB: " + repr(v))
        raise ValueError("Unknown data type from database: " + repr(v))


def _encode(value):
    # returns [code][data] (code != 0xFF)
    # encoded values are self-terminating
    # sorting need to work too!
def _encode(value, nested=False):
    if value is None:
        return int2byte(NULL_CODE)
        if nested:
            return bytes((NULL_CODE, 0xFF))
        else:
            return bytes((NULL_CODE,))
    elif isinstance(value, bool):
        if value:
            return int2byte(TRUE_CODE)
            return bytes((TRUE_CODE,))
        else:
            return int2byte(FALSE_CODE)
            return bytes((FALSE_CODE,))
    elif isinstance(value, bytes):
        return int2byte(BYTES_CODE) + value.replace(b"\x00", b"\x00\xFF") + b"\x00"
        return bytes((BYTES_CODE,)) + value.replace(b"\x00", b"\x00\xFF") + b"\x00"
    elif isinstance(value, str):
        return (
            int2byte(STRING_CODE)
            bytes((STRING_CODE,))
            + value.encode("utf-8").replace(b"\x00", b"\x00\xFF")
            + b"\x00"
        )
    elif value == 0:
        return bytes((INTEGER_ZERO,))
    elif isinstance(value, int):
        if value == 0:
            return int2byte(INT_ZERO_CODE)
        elif value > 0:
            if value >= _size_limits[-1]:
                length = (value.bit_length() + 7) // 8
                data = [int2byte(POS_INT_END), int2byte(length)]
                for i in range(length - 1, -1, -1):
                    data.append(int2byte((value >> (8 * i)) & 0xFF))
                return b"".join(data)

            n = bisect_left(_size_limits, value)
            return int2byte(INT_ZERO_CODE + n) + struct.pack(">Q", value)[-n:]
        if value > 0:
            out = bytes((INTEGER_POSITIVE_CODE,)) + struct.pack('>Q', value)
            return out
        else:
            if -value >= _size_limits[-1]:
                length = (value.bit_length() + 7) // 8
                value += (1 << (length * 8)) - 1
                data = [int2byte(NEG_INT_START), int2byte(length ^ 0xFF)]
                for i in range(length - 1, -1, -1):
                    data.append(int2byte((value >> (8 * i)) & 0xFF))
                return b"".join(data)

            n = bisect_left(_size_limits, -value)
            maxv = _size_limits[n]
            return int2byte(INT_ZERO_CODE - n) + struct.pack(">Q", maxv + value)[-n:]
            value = INTEGER_MAX + value
            out = bytes((INTEGER_NEGATIVE_CODE,)) + struct.pack('>Q', value)
            return out
    elif isinstance(value, float):
        return int2byte(DOUBLE_CODE) + _float_adjust(struct.pack(">d", value), True)
    elif isinstance(value, UUID):
        return int2byte(UUID_CODE) + value.bytes
    elif isinstance(value, tuple) or isinstance(value, list):
        child_bytes, _ = _reduce_children(map(lambda x: _encode(x, True), value))
        return b''.join([bytes([NESTED_CODE])] + child_bytes + [bytes([0x00])])
        return bytes((DOUBLE_CODE,)) + _float_adjust(struct.pack(">d", value), True)
    elif isinstance(value, (tuple, list)):
        child_bytes = list(map(lambda x: _encode(x, True), value))
        return b''.join([bytes((NESTED_CODE,))] + child_bytes + [bytes((0x00,))])
    else:
        raise ValueError("Unsupported data type: " + str(type(value)))
        raise ValueError("Unsupported data type: {}".format(type(value)))


def pack(t):


@@ 209,10 150,12 @@ def pack(t):
def unpack(key):
    pos = 0
    res = []
    if not isinstance(key, bytes):
        # it's ffi.buffer
        key = b"".join((x for x in key))
    while pos < len(key):
        r, pos = _decode(key, pos)
        res.append(r)
    return tuple(res)


def next_prefix(x):
    x = x.rstrip(b"\xff")
    return x[:-1] + bytes((x[-1] + 1,))

A poetry.lock => poetry.lock +208 -0
@@ 0,0 1,208 @@
[[package]]
name = "atomicwrites"
version = "1.4.0"
description = "Atomic file writes."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"

[[package]]
name = "attrs"
version = "21.4.0"
description = "Classes Without Boilerplate"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"

[package.extras]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]

[[package]]
name = "colorama"
version = "0.4.4"
description = "Cross-platform colored terminal text."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"

[[package]]
name = "importlib-metadata"
version = "4.10.1"
description = "Read metadata from Python packages"
category = "dev"
optional = false
python-versions = ">=3.7"

[package.dependencies]
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
zipp = ">=0.5"

[package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
perf = ["ipython"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]

[[package]]
name = "iniconfig"
version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = "*"

[[package]]
name = "packaging"
version = "21.3"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.6"

[package.dependencies]
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"

[[package]]
name = "pluggy"
version = "1.0.0"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=3.6"

[package.dependencies]
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}

[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]

[[package]]
name = "py"
version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"

[[package]]
name = "pyparsing"
version = "3.0.7"
description = "Python parsing module"
category = "dev"
optional = false
python-versions = ">=3.6"

[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]

[[package]]
name = "pytest"
version = "6.2.5"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.6"

[package.dependencies]
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
py = ">=1.8.2"
toml = "*"

[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]

[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"

[[package]]
name = "typing-extensions"
version = "4.0.1"
description = "Backported and Experimental Type Hints for Python 3.6+"
category = "dev"
optional = false
python-versions = ">=3.6"

[[package]]
name = "zipp"
version = "3.7.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "dev"
optional = false
python-versions = ">=3.7"

[package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]

[metadata]
lock-version = "1.1"
python-versions = "^3.7"
content-hash = "342c2663c6d73b208986a6367a7e67ee4664aee54bc5a0ffb009b6d0bfa75989"

[metadata.files]
atomicwrites = [
    {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
    {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
]
attrs = [
    {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
    {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
]
colorama = [
    {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
    {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
importlib-metadata = [
    {file = "importlib_metadata-4.10.1-py3-none-any.whl", hash = "sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6"},
    {file = "importlib_metadata-4.10.1.tar.gz", hash = "sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e"},
]
iniconfig = [
    {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
    {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
packaging = [
    {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
    {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
pluggy = [
    {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
    {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
py = [
    {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
    {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
]
pyparsing = [
    {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"},
    {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"},
]
pytest = [
    {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
    {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
]
toml = [
    {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
    {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
typing-extensions = [
    {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"},
    {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"},
]
zipp = [
    {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"},
    {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"},
]

M pyproject.toml => pyproject.toml +1 -0
@@ 10,6 10,7 @@ readme = "README.md"
python = "^3.7"

[tool.poetry.dev-dependencies]
pytest = "^6.*"

[build-system]
requires = ["poetry-core>=1.0.0"]

A rc.sh => rc.sh +28 -0
@@ 0,0 1,28 @@
MYVENV=1

HISTCONTROL=ignoreboth

# append to the history file, don't overwrite it
shopt -s histappend

# for setting history length see HISTSIZE and HISTFILESIZE in bash(1)
HISTSIZE=
HISTFILESIZE=

test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)"

alias ls='ls --color=auto'

alias grep='grep --color=auto'
alias fgrep='fgrep --color=auto'
alias egrep='egrep --color=auto'

alias ll='ls -alF'
alias la='ls -A'
alias l='ls -CF'

alias e="emacs -nw"

export EDITOR=emacs
export PATH=$HOME/.local/bin:$PATH
export PS1="$(basename $(pwd)) % "

A tests.py => tests.py +16 -0
@@ 0,0 1,16 @@
import lexode




def test_nominal():
    expected = [
        None, True, False,
        -2**64 + 1, -1234567890, -42, -3.14, 0, 3.14, 42, 1234567890, 2**64 - 1,
        "hello world",
        b"C0FF33B4D",
    ]
    expected.append(tuple(expected))
    expected = tuple(expected)

    assert lexode.unpack(lexode.pack(expected)) == expected

A venv => venv +45 -0
@@ 0,0 1,45 @@
#!/bin/bash

#
# tl;dr: use or create a virtual environment.
#
# If there is no directory called .venv, create a virtual
# environment called .venv, install dependencies from requirements.txt
# and spawn a new shell inside that venv. The Python version used to
# create the venv can be passed as first argument.
#
# Otherwise spawn a new shell for the virtualenv .venv, possibly run
# the provided command: ./venv ipython
#
# TODO: investigate https://github.com/berdario/pew
#

set -e

if test -d .venv; then
    if [[ -z "$MYVENV" ]]; then
        if [[ -z "$@" ]]; then
            bash --rcfile rc.sh --rcfile .venv/bin/activate
        else
            source .venv/bin/activate
            exec "$@"
        fi
    else
        echo "You are already in a virtual environment!"
        exec "$@"
    fi
else
    set -x

    PYTHON=$1

    if [ "$PYTHON" = "" ]; then
        PYTHON="python3"
    fi;

    $PYTHON -m venv .venv
    curl -sSL https://bootstrap.pypa.io/get-pip.py -o get-pip.py
    ./venv python get-pip.py
    rm -r get-pip.py
    ./venv "$@"
fi