pax_global_header00006660000000000000000000000064142400074150014507gustar00rootroot0000000000000052 comment=e717a1983b58dcba644153a542dbf8514425a39b plover_stroke-1.1.0/000077500000000000000000000000001424000741500144045ustar00rootroot00000000000000plover_stroke-1.1.0/.github/000077500000000000000000000000001424000741500157445ustar00rootroot00000000000000plover_stroke-1.1.0/.github/workflows/000077500000000000000000000000001424000741500200015ustar00rootroot00000000000000plover_stroke-1.1.0/.github/workflows/wheels.yml000066400000000000000000000041631424000741500220170ustar00rootroot00000000000000name: Build on: [push, pull_request] defaults: run: shell: bash --noprofile --norc -xeo pipefail {0} jobs: build_wheel: name: ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [macos-latest, ubuntu-latest, windows-latest] steps: - name: Checkout uses: actions/checkout@v2 with: submodules: true - name: Setup Python uses: actions/setup-python@v2 with: python-version: "3.x" - name: Install cibuildwheel run: python -m pip install -U cibuildwheel - name: Build wheels env: CIBW_BUILD: "cp3?-* cp31?-*" CIBW_SKIP: "*-manylinux_i686 *-win32" CIBW_BUILD_VERBOSITY: "1" CIBW_ENVIRONMENT_LINUX: "CFLAGS=-g0 LDFLAGS=-Wl,-strip-debug" CIBW_MANYLINUX_X86_64_IMAGE: "manylinux2014" CIBW_BEFORE_TEST: "pip install pytest" CIBW_TEST_COMMAND: "pytest {project}/test" run: python -m cibuildwheel --output-dir wheelhouse - name: Upload artifacts uses: actions/upload-artifact@v2 with: name: Wheels (${{ runner.os }}) path: wheelhouse/* release: name: Release runs-on: ubuntu-latest needs: [build_wheel] if: startsWith(github.ref, 'refs/tags/') steps: - name: Checkout uses: actions/checkout@v2 - name: Setup Python uses: actions/setup-python@v2 with: python-version: "3.x" - name: Install dependencies run: python -m pip install -U setuptools twine - name: Build source distribution run: python setup.py sdist - name: Download artifacts uses: actions/download-artifact@v2 with: path: wheelhouse - name: Publish PyPI release env: TWINE_NON_INTERACTIVE: 1 TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} # Optional: twine will fallback to default if empty. TWINE_REPOSITORY_URL: ${{ secrets.PYPI_URL }} run: python -m twine upload dist/* wheelhouse/*/* plover_stroke-1.1.0/.gitignore000066400000000000000000000000431424000741500163710ustar00rootroot00000000000000/*.egg-info/ /.tox/ /build/ /dist/ plover_stroke-1.1.0/MANIFEST.in000066400000000000000000000000471424000741500161430ustar00rootroot00000000000000include pyproject.toml include tox.ini plover_stroke-1.1.0/README.md000066400000000000000000000035401424000741500156650ustar00rootroot00000000000000# Plover Stroke Helper class for working with steno strokes. Usage: ``` python # Setup: from plover_stroke import BaseStroke class Stroke(BaseStroke): pass Stroke.setup( # System keys. ''' # S- T- K- P- W- H- R- A- O- * -E -U -F -R -P -B -L -G -T -S -D -Z '''.split(), # Implicit hyphen keys (optional, automatically # deduced from system keys if not passed). 'A- O- * -E -U'.split(), # Number bar key and numbers keys (optional). '#', { 'S-': '1-', 'T-': '2-', 'P-': '3-', 'H-': '4-', 'A-': '5-', 'O-': '0-', '-F': '-6', '-P': '-7', '-L': '-8', '-T': '-9', }) # Creating strokes: Stroke(56) # => KPW Stroke(('-F', 'S-', '-S', 'A-', '*')) # => SA*FS Stroke('R-') # => R Stroke('L-') # => invalid, raise a ValueError # Methods: s = Stroke('STK') s.keys() # => ('S-', 'T-', 'K-') s.is_number() # => False int(s) # => 14 s == 0b00000000000000000001110 # => True # Strokes can be compared: sorted(map(Stroke, 'AOE ST-PB *Z # R-R'.split())) # => [#, ST-PB, R-R, AOE, *Z] ``` ## Release history ### 1.1.0 * add `feral_number_key` support: when set to `True`, allow the number key letter anywhere when parsing steno (e.g. `18#`, `#18`, and `1#8` are all valid and equivalent, as per the RTF/CRE spec). ### 1.0.1 * fix exception in case of invalid keys mask ### 1.0.0 * drop `Stroke.xrange` and `Stroke.xsuffixes` methods * `Stroke.keys()` now return a tuple * fix corner case when parsing steno (`RR` -> `R-R`) * fix RTFCRE when numbers are involved (align with Plover's behavior) * fix implicit hyphen handling when numbers are involved * renamed `Stroke.isnumber` to `Stroke.is_number` * speed up implementation through a C extension ### 0.4.0 * fix stroke comparison ### 0.3.3 * fix `python_requires` package metadata ### 0.3.2 * first public release plover_stroke-1.1.0/_plover_stroke.c000066400000000000000000001302511424000741500176070ustar00rootroot00000000000000#define PY_SSIZE_T_CLEAN #include #include // Py_UNREACHABLE is only available starting with Python 3.7. #ifdef Py_UNREACHABLE # define UNREACHABLE() Py_UNREACHABLE() #elif defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 5)) # define UNREACHABLE() __builtin_unreachable() #elif defined(__clang__) || defined(__INTEL_COMPILER) # define UNREACHABLE() __builtin_unreachable() #elif defined(_MSC_VER) # define UNREACHABLE() __assume(0) #else # define UNREACHABLE() Py_FatalError("Unreachable C code path reached") #endif #define MAX_KEYS 63 #define MAX_STENO (MAX_KEYS + 1) // All keys + one hyphen. typedef uint64_t stroke_uint_t; typedef int64_t stroke_int_t; #define INVALID_STROKE ((stroke_uint_t)-1) #if ULONG_MAX == ((1ULL << 64) - 1) # define PyLong_AsStrokeUint PyLong_AsUnsignedLong # define PyLong_FromStrokeUint PyLong_FromUnsignedLong # define PyLong_FromStrokeInt PyLong_FromLong # define T_STROKE_UINT T_ULONG # define STROKE_UINT_FMT "%#lx" # define STROKE_1 1UL #elif ULLONG_MAX == ((1ULL << 64) - 1) # define PyLong_AsStrokeUint PyLong_AsUnsignedLongLong # define PyLong_FromStrokeUint PyLong_FromUnsignedLongLong # define PyLong_FromStrokeInt PyLong_FromLongLong # define T_STROKE_UINT T_ULONGLONG # define STROKE_UINT_FMT "%#llx" # define STROKE_1 1ULL #else # error no suitable 64bits type! #endif typedef enum { KEY_SIDE_NONE, KEY_SIDE_LEFT, KEY_SIDE_RIGHT, } key_side_t; typedef enum { CMP_OP_CMP, CMP_OP_EQ, CMP_OP_NE, CMP_OP_GE, CMP_OP_GT, CMP_OP_LE, CMP_OP_LT, } cmp_op_t; typedef struct { unsigned num_keys; key_side_t key_side[MAX_KEYS]; Py_UCS4 key_letter[MAX_KEYS]; Py_UCS4 key_number[MAX_KEYS]; Py_UCS4 feral_number_key_letter; stroke_uint_t implicit_hyphen_mask; stroke_uint_t number_key_mask; stroke_uint_t numbers_mask; unsigned right_keys_index; } stroke_helper_t; typedef struct { PyObject_HEAD stroke_helper_t helper; } StrokeHelper; static stroke_uint_t lsb(stroke_uint_t x) { return x & (stroke_uint_t)-(stroke_int_t)x; } static stroke_uint_t msb(stroke_uint_t x) { x |= (x >> 1); x |= (x >> 2); x |= (x >> 4); x |= (x >> 8); x |= (x >> 16); x |= (x >> 32); return x & ~(x >> 1); } static unsigned popcount(stroke_uint_t x) { // 0x5555555555555555: 0101... // 0x3333333333333333: 00110011.. // 0x0f0f0f0f0f0f0f0f: 4 zeros, 4 ones ... // Put count of each 2 bits into those 2 bits. x -= (x >> 1) & 0x5555555555555555; // Put count of each 4 bits into those 4 bits. x = (x & 0x3333333333333333) + ((x >> 2) & 0x3333333333333333); // Put count of each 8 bits into those 8 bits. x = (x + (x >> 4)) & 0x0f0f0f0f0f0f0f0f; // Put count of each 16 bits into their lowest 8 bits. x += x >> 8; // Put count of each 32 bits into their lowest 8 bits. x += x >> 16; // Put count of each 64 bits into their lowest 8 bits. x += x >> 32; return x & 0x7f; } static Py_UCS4 key_to_letter(PyObject *key, key_side_t *side) { int kind; const void *data; Py_UCS4 letter1; Py_UCS4 letter2; if (PyUnicode_READY(key)) return 0; kind = PyUnicode_KIND(key); data = PyUnicode_DATA(key); switch (PyUnicode_GET_LENGTH(key)) { case 1: letter1 = PyUnicode_READ(kind, data, 0); if (letter1 == '-') break; *side = KEY_SIDE_NONE; return letter1; case 2: letter1 = PyUnicode_READ(kind, data, 0); letter2 = PyUnicode_READ(kind, data, 1); if (letter1 == '-') { if (letter2 == '-') break; *side = KEY_SIDE_RIGHT; return letter2; } if (letter2 != '-') break; *side = KEY_SIDE_LEFT; return letter1; default: break; } PyErr_Format(PyExc_ValueError, "invalid key: %R", key); return 0; } static stroke_uint_t stroke_from_ucs4(const stroke_helper_t *helper, const Py_UCS4 *stroke_ucs4, Py_ssize_t stroke_len) { stroke_uint_t mask; Py_UCS4 letter; int key_index; unsigned stroke_index; const Py_UCS4 *possible_letters; int implicit_number_key; mask = 0; key_index = -1; implicit_number_key = 0; for (stroke_index = 0; stroke_index < stroke_len; ++stroke_index) { letter = stroke_ucs4[stroke_index]; if (letter == helper->feral_number_key_letter) { if ((mask & helper->number_key_mask)) return INVALID_STROKE; mask |= helper->number_key_mask; continue; } if (letter == '-') { if (key_index > (int)helper->right_keys_index) return INVALID_STROKE; key_index = helper->right_keys_index - 1; continue; } if ('0' <= letter && letter <= '9') { implicit_number_key = 1; possible_letters = helper->key_number; } else { possible_letters = helper->key_letter; } do { if (++key_index == (int)helper->num_keys) return INVALID_STROKE; } while (letter != possible_letters[key_index]); mask |= STROKE_1 << key_index; } if (implicit_number_key) mask |= helper->number_key_mask; return mask; } static stroke_uint_t stroke_from_int(const stroke_helper_t *helper, PyObject *integer) { stroke_uint_t mask = PyLong_AsStrokeUint(integer); if ((mask >> helper->num_keys)) { char error[40]; snprintf(error, sizeof (error), "invalid keys mask: "STROKE_UINT_FMT, mask); PyErr_SetString(PyExc_ValueError, error); return INVALID_STROKE; } return mask; } static stroke_uint_t stroke_from_keys(const stroke_helper_t *helper, PyObject *keys_sequence) \ { stroke_uint_t mask; PyObject *key; Py_UCS4 key_letter; key_side_t key_side; const Py_UCS4 *possible_letters; unsigned k, k_end; mask = 0; for (Py_ssize_t num_keys = PySequence_Fast_GET_SIZE(keys_sequence); num_keys--; ) { key = PySequence_Fast_GET_ITEM(keys_sequence, num_keys); if (!PyUnicode_Check(key)) { PyErr_Format(PyExc_ValueError, "invalid `keys`; key %u is not a string: %R", num_keys, key); return INVALID_STROKE; } key_letter = key_to_letter(key, &key_side); if (!key_letter) { PyErr_Format(PyExc_ValueError, "invalid `keys`; key %u is not valid: %R", num_keys, key); return INVALID_STROKE; } if ('0' <= key_letter && key_letter <= '9') { mask |= helper->number_key_mask; possible_letters = helper->key_number; } else { possible_letters = helper->key_letter; } switch (key_side) { case KEY_SIDE_NONE: k = 0; k_end = helper->num_keys; break; case KEY_SIDE_LEFT: k = 0; k_end = helper->right_keys_index; break; case KEY_SIDE_RIGHT: k = helper->right_keys_index; k_end = helper->num_keys; break; default: UNREACHABLE(); } for (;;) { if (key_letter == possible_letters[k] && key_side == helper->key_side[k]) { mask |= STROKE_1 << k; break; } if (++k == k_end) { PyErr_Format(PyExc_ValueError, "invalid key: %R", key); return INVALID_STROKE; } } } return mask; } static stroke_uint_t stroke_from_steno(const stroke_helper_t *helper, PyObject *steno) { Py_ssize_t steno_len; Py_UCS4 steno_ucs4[MAX_STENO]; stroke_uint_t mask; if (PyUnicode_READY(steno)) return INVALID_STROKE; steno_len = PyUnicode_GET_LENGTH(steno); if (steno_len > MAX_STENO) goto invalid; if (NULL == PyUnicode_AsUCS4(steno, steno_ucs4, MAX_STENO, 0)) return INVALID_STROKE; mask = stroke_from_ucs4(helper, steno_ucs4, steno_len); if (mask == INVALID_STROKE) goto invalid; return mask; invalid: PyErr_Format(PyExc_ValueError, "invalid steno: %R", steno); return INVALID_STROKE; } static stroke_uint_t stroke_from_any(const stroke_helper_t *helper, PyObject *obj) { if (PyLong_Check(obj)) return stroke_from_int(helper, obj); if (PyUnicode_Check(obj)) return stroke_from_steno(helper, obj); obj = PySequence_Fast(obj, "expected a list or tuple"); if (obj != NULL) return stroke_from_keys(helper, obj); PyErr_Format(PyExc_TypeError, "expected an integer (mask of keys), " "sequence of keys, or a string (steno), " "got: %R", obj); return INVALID_STROKE; } static int stroke_has_digit(const stroke_helper_t *helper, stroke_uint_t mask) { return (mask & helper->number_key_mask) && (mask & helper->numbers_mask); } static int stroke_is_number(const stroke_helper_t *helper, stroke_uint_t mask) { // Must have the number key, at least one digit, and no other non-digit key. return (mask & helper->number_key_mask) && mask > helper->number_key_mask && mask == (mask & (helper->number_key_mask | helper->numbers_mask)); } static int unpack_2_strokes(const stroke_helper_t *helper, PyObject *args, const char *fn_name, stroke_uint_t *first_stroke, stroke_uint_t *second_stroke) { PyObject *s1, *s2; if (!PyArg_UnpackTuple(args, fn_name, 2, 2, &s1, &s2)) return 0; *first_stroke = stroke_from_any(helper, s1); if (*first_stroke == INVALID_STROKE) return 0; *second_stroke = stroke_from_any(helper, s2); if (*second_stroke == INVALID_STROKE) return 0; return 1; } static PyObject *stroke_to_keys(const stroke_helper_t *helper, stroke_uint_t mask) { PyObject *stroke_keys[MAX_KEYS]; unsigned stroke_index; unsigned key_index; PyObject *keys_tuple; Py_UCS4 key_ucs4[2]; unsigned key_ucs4_len; PyObject *key; for (stroke_index = key_index = 0; mask; ++key_index, mask >>= 1) { if ((mask & 1)) { switch (helper->key_side[key_index]) { case KEY_SIDE_NONE: key_ucs4[0] = helper->key_letter[key_index]; key_ucs4_len = 1; break; case KEY_SIDE_LEFT: key_ucs4[0] = helper->key_letter[key_index]; key_ucs4[1] = '-'; key_ucs4_len = 2; break; case KEY_SIDE_RIGHT: key_ucs4[0] = '-'; key_ucs4[1] = helper->key_letter[key_index]; key_ucs4_len = 2; break; default: UNREACHABLE(); } key = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, key_ucs4, key_ucs4_len); if (key == NULL) goto error; stroke_keys[stroke_index++] = key; } } keys_tuple = PyTuple_New(stroke_index); if (keys_tuple == NULL) goto error; while (stroke_index--) PyTuple_SET_ITEM(keys_tuple, stroke_index, stroke_keys[stroke_index]); return keys_tuple; error: while (stroke_index--) Py_DECREF(stroke_keys[stroke_index]); return NULL; } static PyObject *stroke_to_str(const stroke_helper_t *helper, stroke_uint_t mask) { const Py_UCS4 *letters; unsigned key_index; unsigned hyphen_index; unsigned stroke_index; Py_UCS4 stroke[MAX_STENO]; if (stroke_has_digit(helper, mask)) { mask &= ~helper->number_key_mask; letters = helper->key_number; } else { letters = helper->key_letter; } if ((mask & helper->implicit_hyphen_mask)) hyphen_index = MAX_KEYS; else hyphen_index = helper->right_keys_index; for (stroke_index = key_index = 0; mask; ++key_index, mask >>= 1) { if ((mask & 1)) { if (key_index >= hyphen_index) { stroke[stroke_index++] = '-'; hyphen_index = MAX_KEYS; } stroke[stroke_index++] = letters[key_index]; } } return PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, stroke, stroke_index); } unsigned stroke_to_sort_key(const stroke_helper_t *helper, stroke_uint_t mask, char *sort_key) { unsigned key_num; unsigned sort_key_index; for (key_num = sort_key_index = 0; mask; mask >>= 1) { ++key_num; if ((mask & 1)) sort_key[sort_key_index++] = key_num; } return sort_key_index; } static PyObject *stroke_cmp(const stroke_helper_t *helper, PyObject *args, const char *fn_name, cmp_op_t op) { stroke_uint_t si1, si2, lsb1, lsb2, m; stroke_int_t c; int b; if (!unpack_2_strokes(helper, args, fn_name, &si1, &si2)) return NULL; c = 0; while (si1 != si2) { lsb1 = lsb(si1); lsb2 = lsb(si2); c = lsb1 - lsb2; if (c) break; m = ~lsb1; si1 &= m; si2 &= m; } switch (op) { case CMP_OP_CMP: return PyLong_FromStrokeInt(c); case CMP_OP_EQ: b = c == 0; break; case CMP_OP_NE: b = c != 0; break; case CMP_OP_GE: b = c >= 0; break; case CMP_OP_GT: b = c > 0; break; case CMP_OP_LE: b = c <= 0; break; case CMP_OP_LT: b = c < 0; break; default: UNREACHABLE(); } if (b) Py_RETURN_TRUE; Py_RETURN_FALSE; } static PyObject *normalize_stroke_ucs4(const stroke_helper_t *helper, const Py_UCS4 *stroke_ucs4, Py_ssize_t stroke_len) { stroke_uint_t mask; mask = stroke_from_ucs4(helper, stroke_ucs4, stroke_len); if (mask == INVALID_STROKE) return NULL; return stroke_to_str(helper, mask); } static PyObject *key_str(const stroke_helper_t *helper, unsigned key_index, int number) { Py_UCS4 key_ucs4[2]; unsigned key_ucs4_len; key_ucs4[0] = (number ? helper->key_number : helper->key_letter)[key_index]; switch (helper->key_side[key_index]) { case KEY_SIDE_NONE: key_ucs4_len = 1; break; case KEY_SIDE_LEFT: key_ucs4[1] = '-'; key_ucs4_len = 2; break; case KEY_SIDE_RIGHT: key_ucs4[1] = key_ucs4[0]; key_ucs4[0] = '-'; key_ucs4_len = 2; break; default: UNREACHABLE(); } return PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, key_ucs4, key_ucs4_len); } static PyObject *StrokeHelper_setup(StrokeHelper *self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = {"keys", "implicit_hyphen_keys", "number_key", "numbers", "feral_number_key", NULL}; PyObject *implicit_hyphen_keys = Py_None; PyObject *number_key = Py_None; PyObject *numbers = Py_None; int feral_number_key = 0; PyObject *keys_sequence; Py_ssize_t num_keys; stroke_uint_t unique_letters_mask; Py_UCS4 number_key_letter; PyObject *key; Py_UCS4 key_letter; stroke_uint_t key_mask; key_side_t key_side; stroke_helper_t helper; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|OOOp", kwlist, &keys_sequence, &implicit_hyphen_keys, &number_key, &numbers, &feral_number_key)) return NULL; keys_sequence = PySequence_Fast(keys_sequence, "expected `keys` to be a list or tuple"); if (keys_sequence == NULL) return NULL; num_keys = PySequence_Fast_GET_SIZE(keys_sequence); if (num_keys == 0 || num_keys > MAX_KEYS) { PyErr_SetString(PyExc_ValueError, "unsupported number of keys"); return NULL; } if (number_key == Py_None) { if (numbers != Py_None) { PyErr_SetString(PyExc_TypeError, "expected `numbers` to be None (since `number_key` is None)"); return NULL; } if (feral_number_key) { PyErr_SetString(PyExc_TypeError, "expected `feral_number_key` to be False (since `number_key` is None)"); return NULL; } number_key_letter = 0; numbers = NULL; } else { if (!PyUnicode_Check(number_key)) { PyErr_SetString(PyExc_TypeError, "expected `number_key` to be a string"); return NULL; } number_key_letter = key_to_letter(number_key, &key_side); if (!number_key_letter) { PyErr_SetString(PyExc_ValueError, "invalid `number_key`"); return NULL; } if (!PyDict_Check(numbers)) { PyErr_SetString(PyExc_TypeError, "expected `numbers` to be a dictionary"); return NULL; } } if (implicit_hyphen_keys != Py_None && !PySet_Check(implicit_hyphen_keys)) { PyErr_SetString(PyExc_TypeError, "expected `implicit_hyphen_keys` to be a set"); return NULL; } helper.num_keys = (unsigned)num_keys; helper.right_keys_index = helper.num_keys; helper.feral_number_key_letter = 0; helper.implicit_hyphen_mask = 0; helper.number_key_mask = 0; helper.numbers_mask = 0; for (unsigned k = 0; k < helper.num_keys; ++k) { key = PySequence_Fast_GET_ITEM(keys_sequence, k); if (!PyUnicode_Check(key)) { PyErr_Format(PyExc_ValueError, "invalid `keys`; key %u is not a string: %R", k, key); return NULL; } key_letter = key_to_letter(key, &key_side); if (!key_letter) { PyErr_Format(PyExc_ValueError, "invalid `keys`; key %u is not valid: %R", k, key); return NULL; } key_mask = STROKE_1 << k; switch (key_side) { case KEY_SIDE_NONE: break; case KEY_SIDE_LEFT: if (helper.right_keys_index != helper.num_keys) { PyErr_Format(PyExc_ValueError, "invalid `keys`; left-key on the right-hand side: %R", key); return NULL; } break; case KEY_SIDE_RIGHT: if (helper.right_keys_index == helper.num_keys) helper.right_keys_index = k; break; default: UNREACHABLE(); } if (key_letter == number_key_letter) helper.number_key_mask = key_mask; if (implicit_hyphen_keys != Py_None && PySet_Contains(implicit_hyphen_keys, key)) helper.implicit_hyphen_mask |= key_mask; helper.key_side[k] = key_side; helper.key_letter[k] = key_letter; if (number_key_letter) { number_key = PyDict_GetItem(numbers, key); if (number_key != NULL) { key_letter = key_to_letter(number_key, &key_side); if (!key_letter) { PyErr_Format(PyExc_ValueError, "invalid `numbers`; entry for %R is not valid: %R", key, number_key); return NULL; } helper.numbers_mask |= key_mask; } } helper.key_number[k] = key_letter; } if (number_key_letter) { if (!helper.number_key_mask) { PyErr_SetString(PyExc_ValueError, "invalid `number_key`"); return NULL; } if (popcount(helper.numbers_mask) != 10) { PyErr_SetString(PyExc_ValueError, "invalid `numbers`"); return NULL; } } // Find out unique letters. unique_letters_mask = 0; { unsigned k, l; for (k = 0; k < helper.num_keys; ++k) { for (l = 0; l < helper.num_keys; ++l) if (l != k && helper.key_letter[l] == helper.key_letter[k]) break; if (l == helper.num_keys) unique_letters_mask |= (STROKE_1 << k); } } if (implicit_hyphen_keys != Py_None) { if ((Py_ssize_t)popcount(helper.implicit_hyphen_mask) != PySet_GET_SIZE(implicit_hyphen_keys)) { PyErr_SetString(PyExc_ValueError, "invalid `implicit_hyphen_keys`: not all keys accounted for"); return NULL; } // Implicit hyphen keys must be a continuous block. if (helper.implicit_hyphen_mask != (// Mask of all bits <= msb. ((msb(helper.implicit_hyphen_mask) << 1) - 1) & // Mask of all bits >= lsb. ~(lsb(helper.implicit_hyphen_mask) - 1))) { PyErr_SetString(PyExc_ValueError, "invalid `implicit_hyphen_keys`: not a continuous block"); return NULL; } if ((helper.implicit_hyphen_mask & unique_letters_mask) != helper.implicit_hyphen_mask) { PyErr_SetString(PyExc_ValueError, "invalid `implicit_hyphen_keys`: some letters are not unique"); return NULL; } } else { unsigned k, l; for (k = helper.right_keys_index; k && (unique_letters_mask & (STROKE_1 << --k)); ) ; for (l = helper.right_keys_index; l < helper.num_keys && (unique_letters_mask & (STROKE_1 << l)); ++l) ; helper.implicit_hyphen_mask = unique_letters_mask & ~((STROKE_1 << k) - 1) & ((STROKE_1 << l) - 1); } if (feral_number_key) { if (helper.number_key_mask & helper.implicit_hyphen_mask) { PyErr_SetString(PyExc_ValueError, "invalid `number_key`: cannot be both feral and an implicit hyphen key"); return NULL; } helper.feral_number_key_letter = number_key_letter; } self->helper = helper; Py_RETURN_NONE; } #define STROKE_CMP_FN(FnName, Op) \ static PyObject *StrokeHelper_##FnName(const StrokeHelper *self, PyObject *args) \ { \ return stroke_cmp(&self->helper, args, #FnName, Op); \ } STROKE_CMP_FN(stroke_cmp, CMP_OP_CMP); STROKE_CMP_FN(stroke_eq, CMP_OP_EQ); STROKE_CMP_FN(stroke_ne, CMP_OP_NE); STROKE_CMP_FN(stroke_ge, CMP_OP_GE); STROKE_CMP_FN(stroke_gt, CMP_OP_GT); STROKE_CMP_FN(stroke_le, CMP_OP_LE); STROKE_CMP_FN(stroke_lt, CMP_OP_LT); #undef STROKE_CMP_FN static PyObject *StrokeHelper_stroke_in(const StrokeHelper *self, PyObject *args) { stroke_uint_t mask1, mask2; if (!unpack_2_strokes(&self->helper, args, "stroke_in", &mask1, &mask2)) return NULL; if ((mask1 & mask2) == mask1) Py_RETURN_TRUE; Py_RETURN_FALSE; } static PyObject *StrokeHelper_stroke_or(const StrokeHelper *self, PyObject *args) { stroke_uint_t mask1, mask2; if (!unpack_2_strokes(&self->helper, args, "stroke_or", &mask1, &mask2)) return NULL; return PyLong_FromStrokeUint(mask1 | mask2); } static PyObject *StrokeHelper_stroke_and(const StrokeHelper *self, PyObject *args) { stroke_uint_t mask1, mask2; if (!unpack_2_strokes(&self->helper, args, "stroke_and", &mask1, &mask2)) return NULL; return PyLong_FromStrokeUint(mask1 & mask2); } static PyObject *StrokeHelper_stroke_add(const StrokeHelper *self, PyObject *args) { stroke_uint_t mask1, mask2; if (!unpack_2_strokes(&self->helper, args, "stroke_add", &mask1, &mask2)) return NULL; return PyLong_FromStrokeUint(mask1 | mask2); } static PyObject *StrokeHelper_stroke_sub(const StrokeHelper *self, PyObject *args) { stroke_uint_t mask1, mask2; if (!unpack_2_strokes(&self->helper, args, "stroke_sub", &mask1, &mask2)) return NULL; return PyLong_FromStrokeUint(mask1 & ~mask2); } static PyObject *StrokeHelper_stroke_is_prefix(const StrokeHelper *self, PyObject *args) { stroke_uint_t mask1, mask2; if (!unpack_2_strokes(&self->helper, args, "stroke_is_prefix", &mask1, &mask2)) return NULL; if (msb(mask1) < lsb(mask2)) Py_RETURN_TRUE; Py_RETURN_FALSE; } static PyObject *StrokeHelper_stroke_is_suffix(const StrokeHelper *self, PyObject *args) { stroke_uint_t mask1, mask2; if (!unpack_2_strokes(&self->helper, args, "stroke_is_suffix", &mask1, &mask2)) return NULL; if (lsb(mask1) > msb(mask2)) Py_RETURN_TRUE; Py_RETURN_FALSE; } static PyObject *StrokeHelper_normalize_stroke(const StrokeHelper *self, PyObject *stroke) { Py_ssize_t stroke_len; Py_UCS4 stroke_ucs4[MAX_STENO]; PyObject *normalized_stroke; if (!PyUnicode_Check(stroke)) { PyErr_SetString(PyExc_TypeError, "expected a string"); return NULL; } if (PyUnicode_READY(stroke)) return NULL; stroke_len = PyUnicode_GET_LENGTH(stroke); if (!stroke_len || stroke_len > MAX_STENO) goto invalid; if (NULL == PyUnicode_AsUCS4(stroke, stroke_ucs4, MAX_STENO, 0)) return NULL; normalized_stroke = normalize_stroke_ucs4(&self->helper, stroke_ucs4, stroke_len); if (normalized_stroke == NULL) goto invalid; return normalized_stroke; invalid: PyErr_Format(PyExc_ValueError, "invalid stroke: %R", stroke); return NULL; } static PyObject *StrokeHelper_normalize_steno(const StrokeHelper *self, PyObject *steno) { int steno_kind; const void *steno_data; Py_ssize_t steno_len; Py_ssize_t steno_index; Py_UCS4 stroke_ucs4[MAX_STENO + 1]; // Account for '/'. Py_ssize_t stroke_len; PyObject *stroke; Py_ssize_t max_strokes; PyObject **strokes_list; Py_ssize_t num_strokes; PyObject *result; strokes_list = NULL; result = NULL; if (!PyUnicode_Check(steno)) { PyErr_SetString(PyExc_TypeError, "expected a string"); goto end; } if (PyUnicode_READY(steno)) goto end; steno_len = PyUnicode_GET_LENGTH(steno); if (!steno_len) { result = PyTuple_New(0); goto end; } max_strokes = steno_len / 2 + 1; strokes_list = PyMem_Malloc(max_strokes * sizeof (*strokes_list)); if (strokes_list == NULL) { PyErr_NoMemory(); goto end; } steno_kind = PyUnicode_KIND(steno); steno_data = PyUnicode_DATA(steno); num_strokes = 0; steno_index = 0; stroke_len = 0; while (1) { stroke_ucs4[stroke_len] = PyUnicode_READ(steno_kind, steno_data, steno_index); if (stroke_ucs4[stroke_len] == '/') { // No trailing '/' allowed. if (++steno_index == steno_len) goto invalid; if (!stroke_len) { // Allow one '/' at the start. if (num_strokes) goto invalid; stroke = PyUnicode_New(0, 0); if (stroke == NULL) goto error; strokes_list[num_strokes++] = stroke; continue; } } else if (++stroke_len > MAX_STENO) goto invalid; else if (++steno_index < steno_len) continue; stroke = normalize_stroke_ucs4(&self->helper, stroke_ucs4, stroke_len); if (stroke == NULL) goto invalid; assert(num_strokes < max_strokes); strokes_list[num_strokes++] = stroke; if (steno_index == steno_len) break; stroke_len = 0; } result = PyTuple_New(num_strokes); if (result == NULL) goto error; while (num_strokes--) PyTuple_SET_ITEM(result, num_strokes, strokes_list[num_strokes]); goto end; invalid: PyErr_Format(PyExc_ValueError, "invalid steno: %R", steno); error: while (num_strokes--) Py_XDECREF(strokes_list[num_strokes]); end: PyMem_Free(strokes_list); return result; } static PyObject *StrokeHelper_steno_to_sort_key(const StrokeHelper *self, PyObject *steno) { int steno_kind; const void *steno_data; Py_ssize_t steno_len; Py_ssize_t steno_index; Py_UCS4 stroke_ucs4[MAX_STENO + 1]; // Account for '/'. Py_ssize_t stroke_len; stroke_uint_t mask; char *sort_key; Py_ssize_t sort_key_index; Py_ssize_t sort_key_max_len; PyObject *result; sort_key = NULL; result = NULL; if (!PyUnicode_Check(steno)) { PyErr_SetString(PyExc_TypeError, "expected a string"); goto end; } if (PyUnicode_READY(steno)) goto end; steno_len = PyUnicode_GET_LENGTH(steno); if (!steno_len) goto invalid; // Note: account for possible extra hyphens. sort_key_max_len = steno_len * 2; sort_key = PyMem_Malloc(sort_key_max_len * sizeof (*sort_key)); if (sort_key == NULL) { PyErr_NoMemory(); goto end; } steno_kind = PyUnicode_KIND(steno); steno_data = PyUnicode_DATA(steno); sort_key_index = 0; steno_index = 0; stroke_len = 0; while (1) { stroke_ucs4[stroke_len] = PyUnicode_READ(steno_kind, steno_data, steno_index); if (stroke_ucs4[stroke_len] == '/') { // No trailing '/' allowed. if (++steno_index == steno_len) goto invalid; if (!stroke_len) { // Allow one '/' at the start. if (sort_key_index) goto invalid; sort_key[sort_key_index++] = 0; continue; } } else if (++stroke_len > MAX_STENO) goto invalid; else if (++steno_index < steno_len) continue; mask = stroke_from_ucs4(&self->helper, stroke_ucs4, stroke_len); if (mask == INVALID_STROKE) goto invalid; sort_key_index += stroke_to_sort_key(&self->helper, mask, &sort_key[sort_key_index]); if (steno_index == steno_len) break; sort_key[sort_key_index++] = 0; stroke_len = 0; } assert(sort_key_index <= sort_key_max_len); result = PyBytes_FromStringAndSize(sort_key, sort_key_index); goto end; invalid: PyErr_Format(PyExc_ValueError, "invalid steno: %R", steno); end: PyMem_Free(sort_key); return result; } static PyObject *StrokeHelper_stroke_from_any(const StrokeHelper *self, PyObject *obj) { stroke_uint_t mask; mask = stroke_from_any(&self->helper, obj); if (mask == INVALID_STROKE) return NULL; return PyLong_FromStrokeUint(mask); } static PyObject *StrokeHelper_stroke_from_int(const StrokeHelper *self, PyObject *integer) { stroke_uint_t mask; mask = stroke_from_int(&self->helper, integer); if (mask == INVALID_STROKE) return NULL; return PyLong_FromStrokeUint(mask); } static PyObject *StrokeHelper_stroke_from_keys(const StrokeHelper *self, PyObject *keys_sequence) { stroke_uint_t mask; keys_sequence = PySequence_Fast(keys_sequence, "expected a list or tuple"); if (keys_sequence == NULL) return NULL; mask = stroke_from_keys(&self->helper, keys_sequence); if (mask == INVALID_STROKE) return NULL; return PyLong_FromStrokeUint(mask); } static PyObject *StrokeHelper_stroke_from_steno(const StrokeHelper *self, PyObject *steno) { stroke_uint_t mask; if (!PyUnicode_Check(steno)) { PyErr_SetString(PyExc_TypeError, "expected a string"); return NULL; } mask = stroke_from_steno(&self->helper, steno); if (mask == INVALID_STROKE) return NULL; return PyLong_FromStrokeUint(mask); } static PyObject *StrokeHelper_stroke_to_keys(const StrokeHelper *self, PyObject *stroke) { stroke_uint_t mask; mask = stroke_from_any(&self->helper, stroke); if (mask == INVALID_STROKE) return NULL; return stroke_to_keys(&self->helper, mask); } static PyObject *StrokeHelper_stroke_first_key(const StrokeHelper *self, PyObject *stroke) { stroke_uint_t mask; unsigned first_key; mask = stroke_from_any(&self->helper, stroke); if (mask == INVALID_STROKE) return NULL; if (!mask) { PyErr_SetString(PyExc_ValueError, "empty stroke"); return NULL; } first_key = popcount(lsb(mask) - 1); return key_str(&self->helper, first_key, 0); } static PyObject *StrokeHelper_stroke_last_key(const StrokeHelper *self, PyObject *stroke) { stroke_uint_t mask; unsigned last_key; mask = stroke_from_any(&self->helper, stroke); if (mask == INVALID_STROKE) return NULL; if (!mask) { PyErr_SetString(PyExc_ValueError, "empty stroke"); return NULL; } last_key = popcount(msb(mask) - 1); return key_str(&self->helper, last_key, 0); } static PyObject *StrokeHelper_stroke_invert(const StrokeHelper *self, PyObject *stroke) { stroke_uint_t mask; mask = stroke_from_any(&self->helper, stroke); if (mask == INVALID_STROKE) return NULL; mask = ~mask & ((STROKE_1 << self->helper.num_keys) - 1); return PyLong_FromStrokeUint(mask); } static PyObject *StrokeHelper_stroke_len(const StrokeHelper *self, PyObject *stroke) { stroke_uint_t mask; mask = stroke_from_any(&self->helper, stroke); if (mask == INVALID_STROKE) return NULL; return PyLong_FromStrokeInt(popcount(mask)); } static PyObject *StrokeHelper_stroke_has_digit(const StrokeHelper *self, PyObject *stroke) { stroke_uint_t mask; mask = stroke_from_any(&self->helper, stroke); if (mask == INVALID_STROKE) return NULL; if (stroke_has_digit(&self->helper, mask)) Py_RETURN_TRUE; Py_RETURN_FALSE; } static PyObject *StrokeHelper_stroke_is_number(const StrokeHelper *self, PyObject *stroke) { stroke_uint_t mask; mask = stroke_from_any(&self->helper, stroke); if (mask == INVALID_STROKE) return NULL; if (stroke_is_number(&self->helper, mask)) Py_RETURN_TRUE; Py_RETURN_FALSE; } static PyObject *StrokeHelper_stroke_to_steno(const StrokeHelper *self, PyObject *stroke) { stroke_uint_t mask; mask = stroke_from_any(&self->helper, stroke); if (mask == INVALID_STROKE) return NULL; return stroke_to_str(&self->helper, mask); } static PyObject *StrokeHelper_stroke_to_sort_key(const StrokeHelper *self, PyObject *stroke) { char sort_key[MAX_KEYS]; unsigned sort_key_len; stroke_uint_t mask; mask = stroke_from_any(&self->helper, stroke); if (mask == INVALID_STROKE) return NULL; sort_key_len = stroke_to_sort_key(&self->helper, mask, sort_key); return PyBytes_FromStringAndSize(sort_key, sort_key_len); } static PyObject *StrokeHelper_get_keys(const StrokeHelper *self, void *Py_UNUSED(closure)) { PyObject *keys_tuple; PyObject *key; keys_tuple = PyTuple_New(self->helper.num_keys); if (keys_tuple == NULL) return NULL; for (unsigned k = 0; k < self->helper.num_keys; ++k) { key = key_str(&self->helper, k, 0); if (key == NULL) { Py_DECREF(keys_tuple); return NULL; } PyTuple_SET_ITEM(keys_tuple, k, key); } return keys_tuple; } static PyObject *StrokeHelper_get_implicit_hyphen_keys(const StrokeHelper *self, void *Py_UNUSED(closure)) { PyObject *implicit_hyphen_keys; PyObject *key; implicit_hyphen_keys = PySet_New(NULL); if (implicit_hyphen_keys == NULL) return NULL; for (unsigned k = 0; k < self->helper.num_keys; ++k) { if (!(self->helper.implicit_hyphen_mask & (1 << k))) continue; key = key_str(&self->helper, k, 0); if (key == NULL || PySet_Add(implicit_hyphen_keys, key)) { Py_DECREF(implicit_hyphen_keys); Py_XDECREF(key); return NULL; } } return implicit_hyphen_keys; } static PyObject *StrokeHelper_get_number_key(const StrokeHelper *self, void *Py_UNUSED(closure)) { if (!self->helper.number_key_mask) Py_RETURN_NONE; return StrokeHelper_stroke_to_steno(self, PyLong_FromStrokeUint(self->helper.number_key_mask)); } static PyObject *StrokeHelper_get_numbers(const StrokeHelper *self, void *Py_UNUSED(closure)) { PyObject *numbers; PyObject *key; PyObject *key_number; if (!self->helper.number_key_mask) Py_RETURN_NONE; numbers = PyDict_New(); if (numbers == NULL) return NULL; for (unsigned k = 0; k < self->helper.num_keys; ++k) { if (self->helper.key_letter[k] == self->helper.key_number[k]) continue; key = key_str(&self->helper, k, 0); key_number = key_str(&self->helper, k, 1); if (key == NULL || key_number == NULL || PyDict_SetItem(numbers, key, key_number)) { Py_DECREF(numbers); Py_XDECREF(key_number); Py_XDECREF(key); return NULL; } } return numbers; } static PyObject *StrokeHelper_get_feral_number_key(const StrokeHelper *self, void *Py_UNUSED(closure)) { return PyBool_FromLong(self->helper.feral_number_key_letter != 0); } static PyObject *StrokeHelper_get_key_letter(const StrokeHelper *self, void *Py_UNUSED(closure)) { return PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, self->helper.key_letter, self->helper.num_keys); } static PyObject *StrokeHelper_get_key_number(const StrokeHelper *self, void *Py_UNUSED(closure)) { return PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, self->helper.key_number, self->helper.num_keys); } static PyObject *StrokeHelper_get_feral_number_key_letter(const StrokeHelper *self, void *Py_UNUSED(closure)) { if (self->helper.feral_number_key_letter == 0) Py_RETURN_NONE; return PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, &self->helper.feral_number_key_letter, 1); } static PyGetSetDef StrokeHelper_getset[] = { // For getting back the arguments passed to setup. {"keys", (getter)StrokeHelper_get_keys, NULL, "List of supported keys.", NULL}, {"implicit_hyphen_keys", (getter)StrokeHelper_get_implicit_hyphen_keys, NULL, "Set of implicit hyphen keys.", NULL}, {"number_key", (getter)StrokeHelper_get_number_key, NULL, "Number key.", NULL}, {"numbers", (getter)StrokeHelper_get_numbers, NULL, "Mapping of key to number.", NULL}, {"feral_number_key", (getter)StrokeHelper_get_feral_number_key, NULL, "Is the number key feral?", NULL}, // Other derived fields. {"key_letter", (getter)StrokeHelper_get_key_letter, NULL, "Letters for the supported keys.", NULL}, {"key_number", (getter)StrokeHelper_get_key_number, NULL, "Numbers for the supported keys.", NULL}, {"feral_number_key_letter", (getter)StrokeHelper_get_feral_number_key_letter, NULL, "Letter for the feral number key.", NULL}, {NULL} }; static PyMemberDef StrokeHelper_members[] = { {"num_keys" , T_UINT , offsetof(StrokeHelper, helper.num_keys) , READONLY, "Number of keys."}, {"implicit_hyphen_mask", T_STROKE_UINT, offsetof(StrokeHelper, helper.implicit_hyphen_mask), READONLY, "Implicit hyphen mask."}, {"number_key_mask" , T_STROKE_UINT, offsetof(StrokeHelper, helper.number_key_mask) , READONLY, "Number key mask."}, {"numbers_mask" , T_STROKE_UINT, offsetof(StrokeHelper, helper.numbers_mask) , READONLY, "Numbers mask."}, {"right_keys_index" , T_UINT , offsetof(StrokeHelper, helper.right_keys_index) , READONLY, "Right keys index."}, {NULL} }; static PyMethodDef StrokeHelper_methods[] = { {"setup" , (PyCFunction)StrokeHelper_setup , METH_VARARGS | METH_KEYWORDS, "Setup."}, // Steno. {"normalize_stroke" , (PyCFunction)StrokeHelper_normalize_stroke , METH_O, "Normalize stroke."}, {"normalize_steno" , (PyCFunction)StrokeHelper_normalize_steno , METH_O, "Normalize steno."}, {"steno_to_sort_key" , (PyCFunction)StrokeHelper_steno_to_sort_key , METH_O, "Convert steno to a binary sort key."}, // Stroke: new. {"stroke_from_any" , (PyCFunction)StrokeHelper_stroke_from_any , METH_O, "Convert an integer (keys mask), string (steno), or sequence of keys to a stroke."}, {"stroke_from_int" , (PyCFunction)StrokeHelper_stroke_from_int , METH_O, "Convert an integer (keys mask) to a stroke."}, {"stroke_from_keys" , (PyCFunction)StrokeHelper_stroke_from_keys , METH_O, "Convert keys to a stroke."}, {"stroke_from_steno" , (PyCFunction)StrokeHelper_stroke_from_steno , METH_O, "Convert steno to a stroke."}, // Stroke: methods. {"stroke_first_key" , (PyCFunction)StrokeHelper_stroke_first_key , METH_O, "Return the stroke first key."}, {"stroke_last_key" , (PyCFunction)StrokeHelper_stroke_last_key , METH_O, "Return the stroke last key."}, {"stroke_invert" , (PyCFunction)StrokeHelper_stroke_invert , METH_O, "Invert stroke."}, {"stroke_len" , (PyCFunction)StrokeHelper_stroke_len , METH_O, "Return the stroke number of keys."}, {"stroke_has_digit" , (PyCFunction)StrokeHelper_stroke_has_digit , METH_O, "Return True if the stroke contains one or more digits."}, {"stroke_is_number" , (PyCFunction)StrokeHelper_stroke_is_number , METH_O, "Return True if the stroke is a number."}, // Stroke: ops. {"stroke_cmp" , (PyCFunction)StrokeHelper_stroke_cmp , METH_VARARGS, "Compare strokes."}, {"stroke_eq" , (PyCFunction)StrokeHelper_stroke_eq , METH_VARARGS, "Compare strokes: `s1 == s2`."}, {"stroke_ne" , (PyCFunction)StrokeHelper_stroke_ne , METH_VARARGS, "Compare strokes: `s1 != s2`."}, {"stroke_ge" , (PyCFunction)StrokeHelper_stroke_ge , METH_VARARGS, "Compare strokes: `s1 >= s2`."}, {"stroke_gt" , (PyCFunction)StrokeHelper_stroke_gt , METH_VARARGS, "Compare strokes: `s1 > s2`."}, {"stroke_le" , (PyCFunction)StrokeHelper_stroke_le , METH_VARARGS, "Compare strokes: `s1 <= s2`."}, {"stroke_lt" , (PyCFunction)StrokeHelper_stroke_lt , METH_VARARGS, "Compare strokes: `s1 < s2`."}, {"stroke_in" , (PyCFunction)StrokeHelper_stroke_in , METH_VARARGS, "`s1 in s2."}, {"stroke_or" , (PyCFunction)StrokeHelper_stroke_or , METH_VARARGS, "`s1 | s2."}, {"stroke_and" , (PyCFunction)StrokeHelper_stroke_and , METH_VARARGS, "`s1 & s2."}, {"stroke_add" , (PyCFunction)StrokeHelper_stroke_add , METH_VARARGS, "`s1 + s2."}, {"stroke_sub" , (PyCFunction)StrokeHelper_stroke_sub , METH_VARARGS, "`s1 - s2."}, {"stroke_is_prefix" , (PyCFunction)StrokeHelper_stroke_is_prefix , METH_VARARGS, "Check if `s1` is a prefix of `s2`."}, {"stroke_is_suffix" , (PyCFunction)StrokeHelper_stroke_is_suffix , METH_VARARGS, "Check if `s1` is a suffix of `s2`."}, // Stroke: convert. {"stroke_to_keys" , (PyCFunction)StrokeHelper_stroke_to_keys , METH_O, "Convert stroke to a tuple of keys."}, {"stroke_to_steno" , (PyCFunction)StrokeHelper_stroke_to_steno , METH_O, "Convert stroke to steno."}, {"stroke_to_sort_key", (PyCFunction)StrokeHelper_stroke_to_sort_key, METH_O, "Convert stroke to a binary sort key."}, {NULL} }; static PyTypeObject StrokeHelperType = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "stroke_helper.StrokeHelper", .tp_basicsize = sizeof (StrokeHelper), .tp_itemsize = 0, .tp_flags = Py_TPFLAGS_DEFAULT, .tp_new = PyType_GenericNew, .tp_methods = StrokeHelper_methods, .tp_members = StrokeHelper_members, .tp_getset = StrokeHelper_getset, }; static struct PyModuleDef module = { PyModuleDef_HEAD_INIT, .m_name = "_plover_stroke", .m_size = -1, }; PyMODINIT_FUNC PyInit__plover_stroke(void) { PyObject *m; if (PyType_Ready(&StrokeHelperType) < 0) return NULL; m = PyModule_Create(&module); if (m == NULL) return NULL; Py_INCREF(&StrokeHelperType); if (PyModule_AddObject(m, "StrokeHelper", (PyObject *)&StrokeHelperType) < 0) { Py_DECREF(&StrokeHelperType); Py_DECREF(m); return NULL; } return m; } plover_stroke-1.1.0/plover_stroke.py000066400000000000000000000062411424000741500176570ustar00rootroot00000000000000from _plover_stroke import StrokeHelper class BaseStroke(int): _helper = None @classmethod def setup(cls, keys, implicit_hyphen_keys=None, number_key=None, numbers=None, feral_number_key=False): cls._helper = StrokeHelper() if number_key is None: assert numbers is None else: assert numbers is not None if implicit_hyphen_keys is not None and not isinstance(implicit_hyphen_keys, set): implicit_hyphen_keys = set(implicit_hyphen_keys) cls._helper.setup(keys, implicit_hyphen_keys=implicit_hyphen_keys, number_key=number_key, numbers=numbers, feral_number_key=feral_number_key) @classmethod def from_steno(cls, steno): return int.__new__(cls, cls._helper.stroke_from_steno(steno)) @classmethod def from_keys(cls, keys): return int.__new__(cls, cls._helper.stroke_from_keys(keys)) @classmethod def from_integer(cls, integer): return int.__new__(cls, cls._helper.stroke_from_int(integer)) def __new__(cls, value): return int.__new__(cls, cls._helper.stroke_from_any(value)) def __hash__(self): return int(self) def __eq__(self, other): return self._helper.stroke_eq(self, other) def __ge__(self, other): return self._helper.stroke_ge(self, other) def __gt__(self, other): return self._helper.stroke_gt(self, other) def __le__(self, other): return self._helper.stroke_le(self, other) def __lt__(self, other): return self._helper.stroke_lt(self, other) def __ne__(self, other): return self._helper.stroke_ne(self, other) def __contains__(self, other): return self._helper.stroke_in(other, self) def __invert__(self): return self.from_integer(self._helper.stroke_invert(self)) def __or__(self, other): return self.from_integer(self._helper.stroke_or(self, other)) def __and__(self, other): return self.from_integer(self._helper.stroke_and(self, other)) def __add__(self, other): return self.from_integer(self._helper.stroke_or(self, other)) def __sub__(self, other): return self.from_integer(self._helper.stroke_sub(self, other)) def __len__(self): return self._helper.stroke_len(self) def __iter__(self): return iter(self._helper.stroke_to_keys(self)) def __repr__(self): return self._helper.stroke_to_steno(self) def __str__(self): return self._helper.stroke_to_steno(self) def first(self): return self._helper.stroke_first_key(self) def last(self): return self._helper.stroke_last_key(self) def keys(self): return self._helper.stroke_to_keys(self) def has_digit(self): return self._helper.stroke_has_digit(self) def is_number(self): return self._helper.stroke_is_number(self) def is_prefix(self, other): return self._helper.stroke_is_prefix(self, other) def is_suffix(self, other): return self._helper.stroke_is_suffix(self, other) # Prevent use of 'from stroke import *'. __all__ = () plover_stroke-1.1.0/pyproject.toml000066400000000000000000000000721424000741500173170ustar00rootroot00000000000000[build-system] requires = ["setuptools>=34.4.0", "wheel"] plover_stroke-1.1.0/setup.cfg000066400000000000000000000021701424000741500162250ustar00rootroot00000000000000[metadata] name = plover_stroke version = 1.1.0 description = Stroke handling helper library for Plover long_description = file: README.md long_description_content_type = text/markdown author = Benoit Pierre author_email = benoit.pierre@gmail.com license = GNU General Public License v2 or later (GPLv2+) url = https://github.com/benoit-pierre/plover_stroke project_urls = Source Code = https://github.com/benoit-pierre/plover_stroke Issue Tracker = https://github.com/benoit-pierre/plover_stroke/issues classifiers = Development Status :: 4 - Beta Intended Audience :: End Users/Desktop License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+) Operating System :: OS Independent Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 keywords = plover [options] zip_safe = True python_requires = >=3.6 py_modules = plover_stroke [options.extras_require] test = pytest>=3.0.1 [tool:pytest] addopts = -ra # vim: commentstring=#\ %s list plover_stroke-1.1.0/setup.py000077500000000000000000000002711424000741500161210ustar00rootroot00000000000000#!/usr/bin/env python3 from setuptools import Extension, setup setup( ext_modules=[ Extension('_plover_stroke', sources=['_plover_stroke.c']), ], ) plover_stroke-1.1.0/test/000077500000000000000000000000001424000741500153635ustar00rootroot00000000000000plover_stroke-1.1.0/test/test_stroke.py000066400000000000000000000546101424000741500203110ustar00rootroot00000000000000import functools import inspect import operator import re import pytest from plover_stroke import BaseStroke @pytest.fixture def stroke_class(): class Stroke(BaseStroke): pass return Stroke @pytest.fixture def english_stroke_class(stroke_class): stroke_class.setup( ''' # S- T- K- P- W- H- R- A- O- * -E -U -F -R -P -B -L -G -T -S -D -Z '''.split(), 'A- O- * -E -U'.split(), '#', { 'S-': '1-', 'T-': '2-', 'P-': '3-', 'H-': '4-', 'A-': '5-', 'O-': '0-', '-F': '-6', '-P': '-7', '-L': '-8', '-T': '-9', }, True, ) return stroke_class def test_setup_minimal(stroke_class): keys = ''' # S- T- K- P- W- H- R- A- O- * -E -U -F -R -P -B -L -G -T -S -D -Z '''.split() stroke_class.setup(keys) helper = stroke_class._helper assert helper.num_keys == len(keys) assert helper.keys == tuple(keys) assert helper.implicit_hyphen_keys == set('A- O- * -E -U -F'.split()) assert helper.number_key == None assert helper.numbers == None assert helper.feral_number_key == False assert helper.feral_number_key_letter == None assert helper.key_letter == ''.join(keys).replace('-', '') assert helper.key_number == ''.join(keys).replace('-', '') assert helper.implicit_hyphen_mask == 0b00000000011111100000000 assert helper.number_key_mask == 0b00000000000000000000000 assert helper.numbers_mask == 0b00000000000000000000000 assert helper.right_keys_index == keys.index('-E') def test_setup_explicit(stroke_class): keys = ''' # S- T- K- P- W- H- R- A- O- * -E -U -F -R -P -B -L -G -T -S -D -Z '''.split() implicit_hyphen_keys = 'A- O- * -E -U'.split() stroke_class.setup(keys, implicit_hyphen_keys) helper = stroke_class._helper assert helper.num_keys == len(keys) assert helper.keys == tuple(keys) assert helper.implicit_hyphen_keys == set(implicit_hyphen_keys) assert helper.number_key == None assert helper.numbers == None assert helper.feral_number_key == False assert helper.feral_number_key_letter == None assert helper.key_letter == ''.join(keys).replace('-', '') assert helper.key_number == ''.join(keys).replace('-', '') assert helper.implicit_hyphen_mask == 0b00000000001111100000000 assert helper.number_key_mask == 0b00000000000000000000000 assert helper.numbers_mask == 0b00000000000000000000000 assert helper.right_keys_index == keys.index('-E') def test_setup_explicit_with_numbers(stroke_class): keys = ''' # S- T- K- P- W- H- R- A- O- * -E -U -F -R -P -B -L -G -T -S -D -Z '''.split() implicit_hyphen_keys = 'A- O- * -E -U'.split() number_key = '#' numbers = { 'S-': '1-', 'T-': '2-', 'P-': '3-', 'H-': '4-', 'A-': '5-', 'O-': '0-', '-F': '-6', '-P': '-7', '-L': '-8', '-T': '-9', } stroke_class.setup(keys, implicit_hyphen_keys, number_key, numbers) helper = stroke_class._helper assert helper.num_keys == len(keys) assert helper.keys == tuple(keys) assert helper.implicit_hyphen_keys == set(implicit_hyphen_keys) assert helper.number_key == number_key assert helper.numbers == numbers assert helper.feral_number_key == False assert helper.feral_number_key_letter == None assert helper.key_letter == ''.join(keys).replace('-', '') assert helper.key_number == ''.join(numbers.get(k, k) for k in keys).replace('-', '') assert helper.implicit_hyphen_mask == 0b00000000001111100000000 assert helper.number_key_mask == 0b00000000000000000000001 assert helper.numbers_mask == 0b00010101010001101010110 assert helper.right_keys_index == keys.index('-E') def test_setup_explicit_with_feral_number_key(stroke_class): keys = ''' # S- T- K- P- W- H- R- A- O- * -E -U -F -R -P -B -L -G -T -S -D -Z '''.split() implicit_hyphen_keys = 'A- O- * -E -U'.split() number_key = '#' numbers = { 'S-': '1-', 'T-': '2-', 'P-': '3-', 'H-': '4-', 'A-': '5-', 'O-': '0-', '-F': '-6', '-P': '-7', '-L': '-8', '-T': '-9', } stroke_class.setup(keys, implicit_hyphen_keys, number_key, numbers, True) helper = stroke_class._helper assert helper.num_keys == len(keys) assert helper.keys == tuple(keys) assert helper.implicit_hyphen_keys == set(implicit_hyphen_keys) assert helper.number_key == number_key assert helper.numbers == numbers assert helper.feral_number_key == True assert helper.feral_number_key_letter == '#' assert helper.key_letter == ''.join(keys).replace('-', '') assert helper.key_number == ''.join(numbers.get(k, k) for k in keys).replace('-', '') assert helper.implicit_hyphen_mask == 0b00000000001111100000000 assert helper.number_key_mask == 0b00000000000000000000001 assert helper.numbers_mask == 0b00010101010001101010110 assert helper.right_keys_index == keys.index('-E') IMPLICIT_HYPHENS_DETECTION_TESTS = ( (''' # S- T- K- P- W- H- R- A- O- * -E -U -F -R -P -B -L -G -T -S -D -Z ''', 'A- O- * -E -U -F' ), (''' # A- O- * ''', '# A- O- *' ), (''' -F -R -P -B -L -G -T -S -D -Z ''', '-F -R -P -B -L -G -T -S -D -Z' ), (''' # S- P- C- T- H- V- R- I- A- -E -O -c -s -t -h -p -r * -i -e -a -o ''', ''' # S- P- C- T- H- V- R- I- A- -E -O -c -s -t -h -p -r * -i -e -a -o ''' ), ) @pytest.mark.parametrize('keys, implicit_hyphen_keys', IMPLICIT_HYPHENS_DETECTION_TESTS) def test_setup_implicit_hyphens_detection(stroke_class, keys, implicit_hyphen_keys): keys = keys.split() implicit_hyphen_keys = implicit_hyphen_keys.split() implicit_hyphen_mask = functools.reduce(operator.or_, ( 1 << keys.index(k) for k in implicit_hyphen_keys ), 0) stroke_class.setup(keys) helper = stroke_class._helper assert helper.keys == tuple(keys) assert helper.implicit_hyphen_mask == implicit_hyphen_mask INVALID_PARAMS_TESTS = ( (''' # S- T- K- P- W- H- R- A- O- * -E -U -F -R -P -B -L -G -T -S -D -Z '''.split(), dict(number_key='V-', numbers={ 'S-': '1-', 'T-': '2-', 'P-': '3-', 'H-': '4-', 'A-': '5-', 'O-': '0-', '-F': '-6', '-P': '-7', '-L': '-8', '-T': '-9', }), ValueError, "invalid `number_key`" ), (''' # S- T- K- P- W- H- R- A- O- * -E -U -F -R -P -B -L -G -T -S -D -Z '''.split(), dict(number_key='#', numbers={ 'S-': '1-', 'T-': '2-', '-F': '-6', '-P': '-7', '-L': '-8', '-T': '-9', }), ValueError, "invalid `numbers`" ), (''' # S- T- K- P- W- H- R- A- O- * -E -U -F -R -P -B -L -G -T -S -D -Z '''.split(), dict(implicit_hyphen_keys='A- O- -E -U'.split()), ValueError, "invalid `implicit_hyphen_keys`: not a continuous block" ), (''' # S- T- K- P- W- H- R- A- O- * -E -U -F -R -P -B -L -G -T -S -D -Z '''.split(), dict(implicit_hyphen_keys='A- O- -E -U -V'.split()), ValueError, "invalid `implicit_hyphen_keys`: not all keys accounted for" ), (''' # S- T- K- P- W- H- R- A- O- * -E -U -F -R -P -B -L -G -T -S -D -Z '''.split(), dict(implicit_hyphen_keys='R- A- O- * -E -U'.split()), ValueError, "invalid `implicit_hyphen_keys`: some letters are not unique" ), (''' # S- T- K- P- W- H- R- A- O- * -E -U -F -R -P -B -L -G -T -S -D -Z '''.split(), dict(feral_number_key=True), TypeError, "expected `feral_number_key` to be False (since `number_key` is None)" ), (''' S- T- K- P- W- H- R- A- O- * # -E -U -F -R -P -B -L -G -T -S -D -Z '''.split(), dict(number_key='#', numbers={ 'S-': '1-', 'T-': '2-', 'P-': '3-', 'H-': '4-', 'A-': '5-', 'O-': '0-', '-F': '-6', '-P': '-7', '-L': '-8', '-T': '-9', }, feral_number_key=True), ValueError, "invalid `number_key`: cannot be both feral and an implicit hyphen key" ), (''' S- T- K- P- W- H- R- A- O- * # -E -U -F -R -P -B -L -G -T -S -D -Z '''.split(), dict(implicit_hyphen_keys='A- O- * # -E -U -F'.split(), number_key='#', numbers={ 'S-': '1-', 'T-': '2-', 'P-': '3-', 'H-': '4-', 'A-': '5-', 'O-': '0-', '-F': '-6', '-P': '-7', '-L': '-8', '-T': '-9', }, feral_number_key=True), ValueError, "invalid `number_key`: cannot be both feral and an implicit hyphen key" ), ) @pytest.mark.parametrize('keys, kwargs, exception, match', INVALID_PARAMS_TESTS) def test_setup_invalid_params(stroke_class, keys, kwargs, exception, match): with pytest.raises(exception, match='^' + re.escape(match) + '$'): stroke_class.setup(keys, **kwargs) NEW_TESTS = ( ( '#', '#-', '#', '#', 0b00000000000000000000001, False, False, ), ( '# -Z', '#Z', '# -Z', '#-Z', 0b10000000000000000000001, False, False, ), ( 'T- -B -P S-', 'ST-PB', 'S- T- -P -B', 'ST-PB', 0b00000011000000000000110, False, False, ), ( 'O- -E A-', 'AO-E', 'A- O- -E', 'AOE', 0b00000000000101100000000, False, False, ), ( '-Z *', '*-Z', '* -Z', '*Z', 0b10000000000010000000000, False, False, ), ( '-R R-', 'RR', 'R- -R', 'R-R', 0b00000000100000010000000, False, False, ), ( 'S- -P O- # T-', '#STO-P', '# S- T- O- -P', '1207', 0b00000001000001000000111, True, True, ), ( '1- 2- 0- -7', '#1207', '# S- T- O- -P', '1207', 0b00000001000001000000111, True, True, ), ( '-L -F', 'FL', '-F -L', '-FL', 0b00000100010000000000000, False, False, ), ( '1- 2- -E -7', '#12E7', '# S- T- -E -P', '12E7', 0b00000001000100000000111, True, False, ), ( ''' # S- 1- T- 2- K- P- 3- W- H- 4- R- A- 5- O- 0- * -E -U -6 -F -R -7 -P -B -8 -L -G -9 -T -S -D -Z ''', '#STKPWHRAO*-EUFRPBLGTSDZ', '# S- T- K- P- W- H- R- A- O- * -E -U -F -R -P -B -L -G -T -S -D -Z', '12K3W4R50*EU6R7B8G9SDZ', 0b11111111111111111111111, True, False, ), ( '1- 2- -E -7', '12E7#', '# S- T- -E -P', '12E7', 0b00000001000100000000111, True, False, ), ( '1- 2- 0- -7', '12#07', '# S- T- O- -P', '1207', 0b00000001000001000000111, True, True, ), ) @pytest.mark.parametrize('in_keys, in_rtfcre, keys, rtfcre, value, has_digit, is_number', NEW_TESTS) def test_new(english_stroke_class, in_keys, in_rtfcre, keys, rtfcre, value, has_digit, is_number): in_keys = in_keys.split() keys = keys.split() for init_arg in (in_keys, in_rtfcre, keys, rtfcre, value): s = english_stroke_class(init_arg) assert int(s) == value assert hash(s) == int(s) assert list(s) == keys assert s.keys() == tuple(keys) assert len(s) == len(keys) assert str(s) == rtfcre assert s.first() == keys[0] assert s.last() == keys[-1] assert s.has_digit() == has_digit assert s.is_number() == is_number def test_empty_stroke(english_stroke_class): empty_stroke = english_stroke_class(0) assert int(empty_stroke) == 0 assert str(empty_stroke) == '' assert empty_stroke == english_stroke_class('') assert empty_stroke == english_stroke_class([]) assert empty_stroke == english_stroke_class(()) assert not empty_stroke.has_digit() assert not empty_stroke.is_number() with pytest.raises(ValueError): empty_stroke.first() with pytest.raises(ValueError): empty_stroke.last() AFFIX_TESTS = ( ('#', 'prefix', 'ST', True), ('#', 'suffix', 'ST', False), ('ST', 'suffix', '#', True), ('ST', 'prefix', '#', False), ('ST', 'suffix', 'T', False), ('ST', 'prefix', 'T', False), ('T', 'suffix', 'ST', False), ('T', 'prefix', 'ST', False), ) @pytest.mark.parametrize('s1, op, s2, expected', AFFIX_TESTS) def test_affix(english_stroke_class, s1, op, s2, expected): op = operator.methodcaller('is_' + op, s2) assert op(english_stroke_class(s1)) == expected CONTAIN_TESTS = ( ('#', '19', True), ('E', 'TEFT', True), ('1', '#START', True), ('S', 'TEFT', False), ('TEFT', 'E', False), ) @pytest.mark.parametrize('s1, s2, expected', CONTAIN_TESTS) def test_contain(english_stroke_class, s1, s2, expected): assert (s1 in english_stroke_class(s2)) == expected INVERT_TESTS = ( (0, '#STKPWHRAO*EUFRPBLGTSDZ'), ('AOEU', '#STKPWHR*FRPBLGTSDZ'), (0b00010001010101100000001, 0b11101110101010011111110), ) @pytest.mark.parametrize('s1, s2', INVERT_TESTS) def test_invert(english_stroke_class, s1, s2): assert ~english_stroke_class(s1) == s2 assert ~english_stroke_class(s2) == s1 HASH_TESTS = ( ('#', 0b00000000000000000000001), ('ST', 0b00000000000000000000110), ('STK', 0b00000000000000000001110), ('*', 0b00000000000010000000000), ('-PB', 0b00000011000000000000000), ('AOE', 0b00000000000101100000000), ('R-R', 0b00000000100000010000000), ('R-F', 0b00000000010000010000000), ('APBD', 0b01000011000000100000000), ) @pytest.mark.parametrize('steno, hash_value', HASH_TESTS) def test_hash(english_stroke_class, steno, hash_value): assert hash(english_stroke_class(steno)) == hash_value OP_TESTS = ( ('#', '|', 'ST', '12'), ('12', '&', '#ST', '12'), ('12', '-', '#', 'ST'), ('PL', '+', '#', '38'), ) @pytest.mark.parametrize('s1, op, s2, expected', OP_TESTS) def test_op(english_stroke_class, s1, op, s2, expected): op = { '|': operator.or_, '&': operator.and_, '+': operator.add, '-': operator.sub, }[op] assert op(english_stroke_class(s1), s2) == expected CMP_OP = { '<': operator.lt, '<=': operator.le, '==': operator.eq, '!=': operator.ne, '>=': operator.ge, '>': operator.gt, } CMP_TESTS = ( ('#', '<', 'ST'), ('T', '>', 'ST'), ('PH', '>', 'TH'), ('SH', '>', 'STH'), ('ST', '<=', 'STK'), ('STK', '<=', 'STK'), ('STK', '==', 'STK'), ('*', '!=', 'R-R'), ('-PB', '>', 'AOE'), ('R-R', '>=', 'R-F'), ('APBD', '>=', 'APBD'), ('ST-TS', '<', 'ST-TZ'), ('ST-TSZ', '<', 'ST-TZ'), ('#STKPWHRAO*-EUFRPBLGTSDZ', '==', '12K3W4R50*EU6R7B8G9SDZ'), ) @pytest.mark.parametrize('steno, op, other_steno', CMP_TESTS) def test_cmp(english_stroke_class, steno, op, other_steno): op = CMP_OP[op] assert op(english_stroke_class(steno), other_steno) @pytest.mark.parametrize('steno, op, other_steno', CMP_TESTS) def test_cmp_sort_key(english_stroke_class, steno, op, other_steno): op = CMP_OP[op] stroke_to_sort_key = english_stroke_class._helper.stroke_to_sort_key steno_sort_key = stroke_to_sort_key(steno) other_sort_key = stroke_to_sort_key(other_steno) assert op(steno_sort_key, other_sort_key) def test_sort(english_stroke_class): unsorted_strokes = [ english_stroke_class(s) for s in ''' AOE ST-PB *Z # R-R '''.split() ] sorted_strokes = [ english_stroke_class(s) for s in ''' # ST-PB R-R AOE *Z '''.split() ] assert list(sorted(unsorted_strokes)) == sorted_strokes def test_sort_key(english_stroke_class): stroke_to_sort_key = english_stroke_class._helper.stroke_to_sort_key unsorted_strokes = ''' AOE ST-PB *Z # R-R '''.split() sorted_strokes = ''' # ST-PB R-R AOE *Z '''.split() assert sorted(unsorted_strokes, key=stroke_to_sort_key) == sorted_strokes def test_no_numbers_system(): class Stroke(BaseStroke): pass Stroke.setup(( '#', 'S-', 'T-', 'K-', 'P-', 'W-', 'H-', 'R-', 'A-', 'O-', '*', '-E', '-U', '-F', '-R', '-P', '-B', '-L', '-G', '-T', '-S', '-D', '-Z', ), ('A-', 'O-', '*', '-E', '-U') ) s1 = Stroke(23) COMMON_NORMALIZE_STENO_TESTS = ( ('#STKPWHRAO*-EUFRPBLGTSDZ/12K3W4R50*-EU6R7B8G9SDZ', ('12K3W4R50*EU6R7B8G9SDZ', '12K3W4R50*EU6R7B8G9SDZ')), ('S', ('S',)), ('S-', ('S',)), ('-S', ('-S',)), ('ES', ('ES',)), ('-ES', ('ES',)), ('TW-EPBL', ('TWEPBL',)), ('TWEPBL', ('TWEPBL',)), ('RR', ('R-R',)), ('19', ('1-9',)), ('14', ('14',)), ('146', ('14-6',)), ('67', ('-67',)), ('120-7', ('1207',)), ('6', ('-6',)), ('9', ('-9',)), ('5', ('5',)), ('0', ('0',)), ('456', ('456',)), ('46', ('4-6',)), ('4*6', ('4*6',)), ('456', ('456',)), ('S46', ('14-6',)), ('T-EFT/-G', ('TEFT', '-G')), ('T-EFT/G', ('TEFT', '-G')), ('/PRE', ('', 'PRE')), ('S--T', ('S-T',)), ('U/E/Z/D', ('U', 'E', '-Z', '-D')), ('F/G/Z/D', ('-F', '-G', '-Z', '-D')), # Number key. ('#', ('#',)), ('#S', ('1',)), ('#A', ('5',)), ('#0', ('0',)), ('#6', ('-6',)), # Implicit hyphens. ('SA-', ('SA',)), ('SA-R', ('SAR',)), ('O', ('O',)), ('O-', ('O',)), ('S*-R', ('S*R',)), # Invalid. ('S' * 65, ValueError), ('TEFT//-G', ValueError), ('//TEFT/', ValueError), ('TEFT/', ValueError), ('SRALD/invalid', ValueError), ('SRALD//invalid', ValueError), ('S-*R', ValueError), ('-O-', ValueError), ('-O', ValueError), ('#S#46', ValueError), ('##', ValueError), ) NORMALIZE_STENO_TESTS = COMMON_NORMALIZE_STENO_TESTS + ( ('S#', ('1',)), ('A#', ('5',)), ('0#', ('0',)), ('2#', ('2',)), ('6#', ('-6',)), ('45#6', ('456',)), ('4#6', ('4-6',)), ('4#*6', ('4*6',)), ('456#', ('456',)), ('S#46', ('14-6',)), ) @pytest.mark.parametrize('steno, expected', NORMALIZE_STENO_TESTS) def test_normalize_steno(english_stroke_class, steno, expected): normalize_steno = english_stroke_class._helper.normalize_steno if inspect.isclass(expected): with pytest.raises(expected): normalize_steno(steno) return assert normalize_steno(steno) == expected NO_FREESTYLE_NORMALIZE_STENO_TESTS = COMMON_NORMALIZE_STENO_TESTS + ( ('S#', ValueError), ('A#', ValueError), ('0#', ValueError), ('2#', ValueError), ('6#', ValueError), ('45#6', ValueError), ('4#6', ValueError), ('4#*6', ValueError), ('456#', ValueError), ('S#46', ValueError), ) @pytest.mark.parametrize('steno, expected', NO_FREESTYLE_NORMALIZE_STENO_TESTS) def test_normalize_steno_no_feral_number_key(stroke_class, steno, expected): stroke_class.setup( ''' # S- T- K- P- W- H- R- A- O- * -E -U -F -R -P -B -L -G -T -S -D -Z '''.split(), 'A- O- * -E -U'.split(), '#', { 'S-': '1-', 'T-': '2-', 'P-': '3-', 'H-': '4-', 'A-': '5-', 'O-': '0-', '-F': '-6', '-P': '-7', '-L': '-8', '-T': '-9', }, False, ) normalize_steno = stroke_class._helper.normalize_steno if inspect.isclass(expected): with pytest.raises(expected): normalize_steno(steno) return assert normalize_steno(steno) == expected STENO_SORT_KEY_TESTS = ( ('12', b'\x01\x02\x03'), ('/12', b'\x00\x01\x02\x03'), ('TEFT', b'\x03\x0c\x0e\x14'), ('/TEFT', b'\x00\x03\x0c\x0e\x14'), ('12/TEFT', b'\x01\x02\x03\x00\x03\x0c\x0e\x14'), ('TEFT/12', b'\x03\x0c\x0e\x14\x00\x01\x02\x03'), ) @pytest.mark.parametrize('steno, sort_key', STENO_SORT_KEY_TESTS) def test_steno_sort_key_1(english_stroke_class, steno, sort_key): normalize_steno = english_stroke_class._helper.normalize_steno steno_to_sort_key = english_stroke_class._helper.steno_to_sort_key assert steno_to_sort_key(steno) == sort_key def test_steno_sort_key_2(english_stroke_class): normalize_steno = english_stroke_class._helper.normalize_steno steno_to_sort_key = english_stroke_class._helper.steno_to_sort_key steno_list = [] for steno, expected in NORMALIZE_STENO_TESTS: if inspect.isclass(expected): with pytest.raises(expected): steno_to_sort_key(steno) continue steno_list.append('/'.join(normalize_steno(steno))) sorted_with_stroke_sort = [ '/'.join(map(str, strokes)) for strokes in sorted( tuple(map(english_stroke_class, steno.split('/'))) for steno in steno_list ) ] assert sorted(steno_list, key=steno_to_sort_key) == sorted_with_stroke_sort plover_stroke-1.1.0/tox.ini000066400000000000000000000006131424000741500157170ustar00rootroot00000000000000[tox] envlist = test [testenv] usedevelop = true extras = test commands = python setup.py build_ext -i pytest {posargs} [testenv:packaging] skip_install = true deps = build check-manifest readme-renderer[md] twine allowlist_externals = rm commands = rm -rf build dist python -m build --sdist --wheel . twine check --strict dist/* check-manifest -v # vim: commentstring=#\ %s list