Skip to content

Commit 4b913ef

Browse files
prokazovprokazov-rediszalmanechayim
authored
Add pack_command to support writing via hiredis-py (#147)
Co-authored-by: Sergey Prokazov <[email protected]> Co-authored-by: zalmane <[email protected]> Co-authored-by: Chayim <[email protected]>
1 parent 1b2e6fc commit 4b913ef

File tree

9 files changed

+287
-50
lines changed

9 files changed

+287
-50
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
MANIFEST
55
.venv
66
**/*.so
7+
hiredis.egg-info

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
* Implement pack_command that serializes redis-py command to the RESP bytes object.
2+
13
### 2.1.1 (2023-10-01)
24

35
* Restores publishing of source distribution (#139)

hiredis/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
from .hiredis import Reader, HiredisError, ProtocolError, ReplyError
1+
from .hiredis import Reader, HiredisError, pack_command, ProtocolError, ReplyError
22
from .version import __version__
33

44
__all__ = [
5-
"Reader", "HiredisError", "ProtocolError", "ReplyError",
5+
"Reader",
6+
"HiredisError",
7+
"pack_command",
8+
"ProtocolError",
9+
"ReplyError",
610
"__version__"]

hiredis/hiredis.pyi

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1-
from typing import Any, Callable, Optional, Union
1+
from typing import Any, Callable, Optional, Union, Tuple
2+
3+
4+
class HiredisError(Exception):
5+
...
6+
7+
8+
class ProtocolError(HiredisError):
9+
...
10+
11+
12+
class ReplyError(HiredisError):
13+
...
214

3-
class HiredisError(Exception): ...
4-
class ProtocolError(HiredisError): ...
5-
class ReplyError(HiredisError): ...
615

716
class Reader:
817
def __init__(
@@ -13,6 +22,7 @@ class Reader:
1322
errors: Optional[str] = ...,
1423
notEnoughData: Any = ...,
1524
) -> None: ...
25+
1626
def feed(
1727
self, __buf: Union[str, bytes], __off: int = ..., __len: int = ...
1828
) -> None: ...
@@ -21,6 +31,10 @@ class Reader:
2131
def getmaxbuf(self) -> int: ...
2232
def len(self) -> int: ...
2333
def has_data(self) -> bool: ...
34+
2435
def set_encoding(
2536
self, encoding: Optional[str] = ..., errors: Optional[str] = ...
2637
) -> None: ...
38+
39+
40+
def pack_command(cmd: Tuple[str | int | float | bytes | memoryview]): ...

setup.py

100755100644
Lines changed: 76 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,89 @@
11
#!/usr/bin/env python
22

33
try:
4-
from setuptools import setup, Extension
4+
from setuptools import setup, Extension
55
except ImportError:
6-
from distutils.core import setup, Extension
7-
import sys, importlib, os, glob, io
6+
from distutils.core import setup, Extension
7+
import importlib
8+
import glob
9+
import io
10+
import sys
11+
812

913
def version():
10-
loader = importlib.machinery.SourceFileLoader("hiredis.version", "hiredis/version.py")
11-
module = loader.load_module()
12-
return module.__version__
14+
loader = importlib.machinery.SourceFileLoader(
15+
"hiredis.version", "hiredis/version.py")
16+
module = loader.load_module()
17+
return module.__version__
18+
19+
20+
def get_sources():
21+
hiredis_sources = ("alloc", "async", "hiredis", "net", "read", "sds", "sockcompat")
22+
return sorted(glob.glob("src/*.c") + ["vendor/hiredis/%s.c" % src for src in hiredis_sources])
23+
24+
25+
def get_linker_args():
26+
if 'win32' in sys.platform or 'darwin' in sys.platform:
27+
return []
28+
else:
29+
return ["-Wl,-Bsymbolic",]
30+
31+
32+
def get_compiler_args():
33+
if 'win32' in sys.platform:
34+
return []
35+
else:
36+
return ["-std=c99",]
37+
38+
39+
def get_libraries():
40+
if 'win32' in sys.platform:
41+
return ["ws2_32",]
42+
else:
43+
return []
44+
1345

1446
ext = Extension("hiredis.hiredis",
15-
sources=sorted(glob.glob("src/*.c") +
16-
["vendor/hiredis/%s.c" % src for src in ("alloc", "read", "sds")]),
17-
extra_compile_args=["-std=c99"],
18-
include_dirs=["vendor"])
47+
sources=get_sources(),
48+
extra_compile_args=get_compiler_args(),
49+
extra_link_args=get_linker_args(),
50+
libraries=get_libraries(),
51+
include_dirs=["vendor"])
1952

2053
setup(
21-
name="hiredis",
22-
version=version(),
23-
description="Python wrapper for hiredis",
24-
long_description=io.open('README.md', 'rt', encoding='utf-8').read(),
25-
long_description_content_type='text/markdown',
26-
url="https://github.com/redis/hiredis-py",
27-
author="Jan-Erik Rediger, Pieter Noordhuis",
28-
29-
keywords=["Redis"],
30-
license="BSD",
31-
packages=["hiredis"],
32-
package_data={"hiredis": ["hiredis.pyi", "py.typed"]},
33-
ext_modules=[ext],
34-
python_requires=">=3.7",
35-
project_urls={
54+
name="hiredis",
55+
version=version(),
56+
description="Python wrapper for hiredis",
57+
long_description=io.open('README.md', 'rt', encoding='utf-8').read(),
58+
long_description_content_type='text/markdown',
59+
url="https://github.com/redis/hiredis-py",
60+
author="Jan-Erik Rediger, Pieter Noordhuis",
61+
62+
keywords=["Redis"],
63+
license="BSD",
64+
packages=["hiredis"],
65+
package_data={"hiredis": ["hiredis.pyi", "py.typed"]},
66+
ext_modules=[ext],
67+
python_requires=">=3.7",
68+
project_urls={
3669
"Changes": "https://github.com/redis/hiredis-py/releases",
3770
"Issue tracker": "https://github.com/redis/hiredis-py/issues",
38-
},
39-
classifiers=[
40-
'Development Status :: 5 - Production/Stable',
41-
'Intended Audience :: Developers',
42-
'License :: OSI Approved :: BSD License',
43-
'Operating System :: MacOS',
44-
'Operating System :: POSIX',
45-
'Programming Language :: C',
46-
'Programming Language :: Python :: 3',
47-
'Programming Language :: Python :: 3 :: Only',
48-
'Programming Language :: Python :: 3.7',
49-
'Programming Language :: Python :: 3.8',
50-
'Programming Language :: Python :: 3.9',
51-
'Programming Language :: Python :: 3.10',
52-
'Programming Language :: Python :: 3.11',
53-
'Programming Language :: Python :: Implementation :: CPython',
54-
'Topic :: Software Development',
55-
],
71+
},
72+
classifiers=[
73+
'Development Status :: 5 - Production/Stable',
74+
'Intended Audience :: Developers',
75+
'License :: OSI Approved :: BSD License',
76+
'Operating System :: MacOS',
77+
'Operating System :: POSIX',
78+
'Programming Language :: C',
79+
'Programming Language :: Python :: 3',
80+
'Programming Language :: Python :: 3 :: Only',
81+
'Programming Language :: Python :: 3.7',
82+
'Programming Language :: Python :: 3.8',
83+
'Programming Language :: Python :: 3.9',
84+
'Programming Language :: Python :: 3.10',
85+
'Programming Language :: Python :: 3.11',
86+
'Programming Language :: Python :: Implementation :: CPython',
87+
'Topic :: Software Development',
88+
],
5689
)

src/hiredis.c

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "hiredis.h"
22
#include "reader.h"
3+
#include "pack.h"
34

45
static int hiredis_ModuleTraverse(PyObject *m, visitproc visit, void *arg) {
56
Py_VISIT(GET_STATE(m)->HiErr_Base);
@@ -15,12 +16,33 @@ static int hiredis_ModuleClear(PyObject *m) {
1516
return 0;
1617
}
1718

19+
static PyObject*
20+
py_pack_command(PyObject* self, PyObject* cmd)
21+
{
22+
return pack_command(cmd);
23+
}
24+
25+
PyDoc_STRVAR(pack_command_doc, "Pack a series of arguments into the Redis protocol");
26+
27+
PyMethodDef pack_command_method = {
28+
"pack_command", /* The name as a C string. */
29+
(PyCFunction) py_pack_command, /* The C function to invoke. */
30+
METH_O, /* Flags telling Python how to invoke */
31+
pack_command_doc, /* The docstring as a C string. */
32+
};
33+
34+
35+
PyMethodDef methods[] = {
36+
{"pack_command", (PyCFunction) py_pack_command, METH_O, pack_command_doc},
37+
{NULL},
38+
};
39+
1840
static struct PyModuleDef hiredis_ModuleDef = {
1941
PyModuleDef_HEAD_INIT,
2042
MOD_HIREDIS,
2143
NULL,
2244
sizeof(struct hiredis_ModuleState), /* m_size */
23-
NULL, /* m_methods */
45+
methods, /* m_methods */
2446
NULL, /* m_reload */
2547
hiredis_ModuleTraverse, /* m_traverse */
2648
hiredis_ModuleClear, /* m_clear */

src/pack.c

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#include "pack.h"
2+
#include <hiredis/hiredis.h>
3+
#include <hiredis/sdsalloc.h>
4+
5+
PyObject *
6+
pack_command(PyObject *cmd)
7+
{
8+
assert(cmd);
9+
PyObject *result = NULL;
10+
11+
if (cmd == NULL || !PyTuple_Check(cmd))
12+
{
13+
PyErr_SetString(PyExc_TypeError,
14+
"The argument must be a tuple of str, int, float or bytes.");
15+
return NULL;
16+
}
17+
18+
int tokens_number = PyTuple_Size(cmd);
19+
sds *tokens = s_malloc(sizeof(sds) * tokens_number);
20+
if (tokens == NULL)
21+
{
22+
return PyErr_NoMemory();
23+
}
24+
25+
memset(tokens, 0, sizeof(sds) * tokens_number);
26+
27+
size_t *lengths = hi_malloc(sizeof(size_t) * tokens_number);
28+
if (lengths == NULL)
29+
{
30+
sds_free(tokens);
31+
return PyErr_NoMemory();
32+
}
33+
34+
Py_ssize_t len = 0;
35+
36+
for (Py_ssize_t i = 0; i < PyTuple_Size(cmd); i++)
37+
{
38+
PyObject *item = PyTuple_GetItem(cmd, i);
39+
40+
if (PyBytes_Check(item))
41+
{
42+
char *bytes = NULL;
43+
Py_buffer buffer;
44+
PyObject_GetBuffer(item, &buffer, PyBUF_SIMPLE);
45+
PyBytes_AsStringAndSize(item, &bytes, &len);
46+
tokens[i] = sdsempty();
47+
tokens[i] = sdscpylen(tokens[i], bytes, len);
48+
lengths[i] = buffer.len;
49+
PyBuffer_Release(&buffer);
50+
}
51+
else if (PyUnicode_Check(item))
52+
{
53+
const char *bytes = PyUnicode_AsUTF8AndSize(item, &len);
54+
if (bytes == NULL)
55+
{
56+
// PyUnicode_AsUTF8AndSize sets an exception.
57+
goto cleanup;
58+
}
59+
60+
tokens[i] = sdsnewlen(bytes, len);
61+
lengths[i] = len;
62+
}
63+
else if (PyMemoryView_Check(item))
64+
{
65+
Py_buffer *p_buf = PyMemoryView_GET_BUFFER(item);
66+
tokens[i] = sdsnewlen(p_buf->buf, p_buf->len);
67+
lengths[i] = p_buf->len;
68+
}
69+
else
70+
{
71+
if (PyLong_CheckExact(item) || PyFloat_Check(item))
72+
{
73+
PyObject *repr = PyObject_Repr(item);
74+
const char *bytes = PyUnicode_AsUTF8AndSize(repr, &len);
75+
76+
tokens[i] = sdsnewlen(bytes, len);
77+
lengths[i] = len;
78+
Py_DECREF(repr);
79+
}
80+
else
81+
{
82+
PyErr_SetString(PyExc_TypeError,
83+
"A tuple item must be str, int, float or bytes.");
84+
goto cleanup;
85+
}
86+
}
87+
}
88+
89+
char *resp_bytes = NULL;
90+
91+
len = redisFormatCommandArgv(&resp_bytes, tokens_number, (const char **)tokens, lengths);
92+
93+
if (len == -1)
94+
{
95+
PyErr_SetString(PyExc_RuntimeError,
96+
"Failed to serialize the command.");
97+
goto cleanup;
98+
}
99+
100+
result = PyBytes_FromStringAndSize(resp_bytes, len);
101+
hi_free(resp_bytes);
102+
cleanup:
103+
sdsfreesplitres(tokens, tokens_number);
104+
hi_free(lengths);
105+
return result;
106+
}

src/pack.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#ifndef __PACK_H
2+
#define __PACK_H
3+
4+
#include <Python.h>
5+
6+
extern PyObject* pack_command(PyObject* cmd);
7+
8+
#endif

0 commit comments

Comments
 (0)