Skip to content

Commit 5af75af

Browse files
committed
Initial implementation of selectlib
0 parents  commit 5af75af

File tree

8 files changed

+427
-0
lines changed

8 files changed

+427
-0
lines changed

.github/workflows/release.yml

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*' # Trigger on version tags such as v1.0.0
7+
8+
jobs:
9+
build_wheels:
10+
name: Build wheels on ${{ matrix.os }}
11+
runs-on: ${{ matrix.os }}
12+
strategy:
13+
matrix:
14+
os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-13, macos-latest]
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Set up Python
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: 3.x
23+
24+
- name: Install build dependencies
25+
run: |
26+
python -m pip install --upgrade pip
27+
pip install cibuildwheel==2.*
28+
29+
- name: Build wheels with cibuildwheel
30+
env:
31+
CIBW_TEST_COMMAND: python -m unittest discover -v -t {package}
32+
run: cibuildwheel --output-dir wheelhouse .
33+
34+
- name: Upload built wheels
35+
uses: actions/upload-artifact@v4
36+
with:
37+
name: wheels-${{ matrix.os }}
38+
path: wheelhouse/*.whl
39+
40+
publish:
41+
# Wait for all platforms to build, then publish wheels and a source distribution.
42+
needs: build_wheels
43+
runs-on: ubuntu-latest
44+
45+
steps:
46+
- uses: actions/checkout@v4
47+
48+
- name: Download wheels (Ubuntu)
49+
uses: actions/download-artifact@v4
50+
with:
51+
name: wheels-ubuntu-latest
52+
path: wheels
53+
54+
- name: Download wheels (macOS)
55+
uses: actions/download-artifact@v4
56+
with:
57+
name: wheels-macos-latest
58+
path: wheels
59+
60+
- name: Download wheels (Windows)
61+
uses: actions/download-artifact@v4
62+
with:
63+
name: wheels-windows-latest
64+
path: wheels
65+
66+
- name: Build source distribution
67+
run: |
68+
python -m pip install --upgrade pip setuptools wheel build twine==6.0.* pkginfo
69+
python -m build --sdist --outdir sdist
70+
71+
- name: Publish wheels and source distribution to PyPI
72+
run: |
73+
twine upload wheels/*.whl sdist/*.tar.gz
74+
env:
75+
TWINE_USERNAME: __token__
76+
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}

.github/workflows/test.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
tox:
11+
strategy:
12+
matrix:
13+
include:
14+
- env: py38
15+
python-version: 3.8
16+
- env: py39
17+
python-version: 3.9
18+
- env: py310
19+
python-version: '3.10'
20+
- env: py311
21+
python-version: 3.11
22+
- env: py312
23+
python-version: 3.12
24+
- env: py313
25+
python-version: 3.13
26+
- env: lint
27+
python-version: 3.13
28+
runs-on: ubuntu-latest
29+
30+
steps:
31+
- uses: actions/checkout@v3
32+
33+
- name: Set up Python ${{ matrix.python-version }}
34+
uses: actions/setup-python@v4
35+
with:
36+
python-version: ${{ matrix.python-version }}
37+
38+
- name: Upgrade pip and install tox
39+
run: |
40+
python -m pip install --upgrade pip
41+
pip install tox
42+
43+
- name: Run tox for environment ${{ matrix.env }}
44+
run: tox -e ${{ matrix.env }}

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
build/
2+
env3/
3+
*.egg-info/
4+
5+
*.so
6+
*.pyc

LICENSE

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Copyright 2025 Grant Jenks
2+
3+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
4+
this file except in compliance with the License. You may obtain a copy of the
5+
License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software distributed
10+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
11+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
12+
specific language governing permissions and limitations under the License.

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include README.md LICENSE

selectlib.c

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
/* selectlib.c */
2+
3+
#include <Python.h>
4+
#include <listobject.h>
5+
#include <stdlib.h>
6+
#include <time.h>
7+
8+
#ifndef PY_SSIZE_T_CLEAN
9+
#define PY_SSIZE_T_CLEAN
10+
#endif
11+
12+
/*
13+
Helper function that compares two PyObject*s using the < operator.
14+
Returns 1 if a < b, 0 if not, or -1 if an error occurred.
15+
*/
16+
static int
17+
less_than(PyObject *a, PyObject *b)
18+
{
19+
int cmp = PyObject_RichCompareBool(a, b, Py_LT);
20+
return cmp;
21+
}
22+
23+
/*
24+
Swap the elements at indices i and j in the Python list.
25+
If keys is not NULL, also swap the corresponding keys.
26+
This function directly manipulates the list internals.
27+
*/
28+
static void
29+
swap_items(PyObject *list, Py_ssize_t i, Py_ssize_t j, PyObject **keys)
30+
{
31+
/* Cast to PyListObject to access the internal array */
32+
PyListObject *lst = (PyListObject *)list;
33+
PyObject *temp = lst->ob_item[i];
34+
lst->ob_item[i] = lst->ob_item[j];
35+
lst->ob_item[j] = temp;
36+
37+
if (keys != NULL) {
38+
PyObject *temp_key = keys[i];
39+
keys[i] = keys[j];
40+
keys[j] = temp_key;
41+
}
42+
}
43+
44+
/*
45+
Partition the subarray [left, right] around a pivot.
46+
The pivot is initially at pivot_index. After partitioning,
47+
the pivot is placed at new_pivot_index (returned via pointer).
48+
Returns 0 on success or -1 if an error occurred.
49+
*/
50+
static int
51+
partition(PyObject *list, PyObject **keys, Py_ssize_t left, Py_ssize_t right,
52+
Py_ssize_t pivot_index, Py_ssize_t *new_pivot_index)
53+
{
54+
/* Move pivot to the end */
55+
swap_items(list, pivot_index, right, keys);
56+
57+
PyObject *pivot_val;
58+
if (keys != NULL)
59+
pivot_val = keys[right];
60+
else
61+
pivot_val = PyList_GET_ITEM(list, right);
62+
63+
Py_ssize_t store_index = left;
64+
for (Py_ssize_t i = left; i < right; i++) {
65+
PyObject *current;
66+
if (keys != NULL)
67+
current = keys[i];
68+
else
69+
current = PyList_GET_ITEM(list, i);
70+
71+
int cmp = less_than(current, pivot_val);
72+
if (cmp < 0)
73+
return -1;
74+
if (cmp) {
75+
swap_items(list, i, store_index, keys);
76+
store_index++;
77+
}
78+
}
79+
swap_items(list, store_index, right, keys);
80+
*new_pivot_index = store_index;
81+
return 0;
82+
}
83+
84+
/*
85+
In-place quickselect algorithm on the list.
86+
It partitions the list (and the keys array if provided) so that
87+
the element at index k is in its final sorted position.
88+
Operates on indices in [left, right].
89+
Returns 0 on success or -1 on error.
90+
*/
91+
static int
92+
quickselect_inplace(PyObject *list, PyObject **keys,
93+
Py_ssize_t left, Py_ssize_t right, Py_ssize_t k)
94+
{
95+
/* Seed the random number generator once (if needed) */
96+
static int seeded = 0;
97+
if (!seeded) {
98+
srand((unsigned)time(NULL));
99+
seeded = 1;
100+
}
101+
102+
while (left < right) {
103+
/* Choose a random pivot_index between left and right (inclusive) */
104+
Py_ssize_t pivot_index = left + rand() % (right - left + 1);
105+
Py_ssize_t pos;
106+
if (partition(list, keys, left, right, pivot_index, &pos) < 0)
107+
return -1;
108+
if (pos == k)
109+
return 0;
110+
else if (k < pos)
111+
right = pos - 1;
112+
else
113+
left = pos + 1;
114+
}
115+
return 0;
116+
}
117+
118+
/*
119+
quickselect(values: list[Any], index: int, key=None) -> None
120+
121+
Partition the list in-place such that the element in the specified
122+
index is the one that would be there in a sorted list. An optional
123+
key function may be provided to extract a comparison key from each element.
124+
*/
125+
static PyObject *
126+
selectlib_quickselect(PyObject *self, PyObject *args, PyObject *kwargs)
127+
{
128+
static char *kwlist[] = {"values", "index", "key", NULL};
129+
PyObject *values;
130+
Py_ssize_t target_index;
131+
PyObject *key = Py_None;
132+
133+
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "On|O:quickselect",
134+
kwlist, &values, &target_index, &key))
135+
return NULL;
136+
137+
if (!PyList_Check(values)) {
138+
PyErr_SetString(PyExc_TypeError, "values must be a list");
139+
return NULL;
140+
}
141+
142+
Py_ssize_t n = PyList_Size(values);
143+
if (target_index < 0 || target_index >= n) {
144+
PyErr_SetString(PyExc_IndexError, "index out of range");
145+
return NULL;
146+
}
147+
148+
int use_key = 0;
149+
if (key != Py_None) {
150+
if (!PyCallable_Check(key)) {
151+
PyErr_SetString(PyExc_TypeError, "key must be callable");
152+
return NULL;
153+
}
154+
use_key = 1;
155+
}
156+
157+
PyObject **keys_arr = NULL;
158+
if (use_key) {
159+
keys_arr = PyMem_New(PyObject *, n);
160+
if (keys_arr == NULL) {
161+
PyErr_NoMemory();
162+
return NULL;
163+
}
164+
for (Py_ssize_t i = 0; i < n; i++) {
165+
PyObject *item = PyList_GET_ITEM(values, i);
166+
PyObject *key_val = PyObject_CallFunctionObjArgs(key, item, NULL);
167+
if (key_val == NULL) {
168+
for (Py_ssize_t j = 0; j < i; j++)
169+
Py_DECREF(keys_arr[j]);
170+
PyMem_Free(keys_arr);
171+
return NULL;
172+
}
173+
keys_arr[i] = key_val;
174+
}
175+
}
176+
177+
if (n > 0) {
178+
if (quickselect_inplace(values, keys_arr, 0, n - 1, target_index) < 0) {
179+
if (use_key) {
180+
for (Py_ssize_t i = 0; i < n; i++)
181+
Py_DECREF(keys_arr[i]);
182+
PyMem_Free(keys_arr);
183+
}
184+
return NULL;
185+
}
186+
}
187+
188+
if (use_key) {
189+
for (Py_ssize_t i = 0; i < n; i++)
190+
Py_DECREF(keys_arr[i]);
191+
PyMem_Free(keys_arr);
192+
}
193+
194+
Py_RETURN_NONE;
195+
}
196+
197+
static PyMethodDef selectlib_methods[] = {
198+
{"quickselect", (PyCFunction)selectlib_quickselect,
199+
METH_VARARGS | METH_KEYWORDS,
200+
"quickselect(values: list[Any], index: int, key=None) -> None\n\n"
201+
"Partition the list in-place so that the element at the given index is in its "
202+
"final sorted position."},
203+
{NULL, NULL, 0, NULL}
204+
};
205+
206+
static struct PyModuleDef selectlibmodule = {
207+
PyModuleDef_HEAD_INIT,
208+
"selectlib",
209+
"Module that implements the quickselect algorithm.",
210+
-1,
211+
selectlib_methods,
212+
};
213+
214+
PyMODINIT_FUNC
215+
PyInit_selectlib(void)
216+
{
217+
return PyModule_Create(&selectlibmodule);
218+
}

0 commit comments

Comments
 (0)