From 51cc817f8e2bab01ee028fbc1a0029a3f314d576 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 4 Jul 2022 09:59:01 +0200 Subject: Merging upstream version 1.12.0. Signed-off-by: Daniel Baumann --- .bumpversion.cfg | 2 +- .github/workflows/test.yaml | 2 +- CHANGELOG.md | 29 +++++- iredis/__init__.py | 2 +- iredis/client.py | 4 +- iredis/completers.py | 39 ++++++-- iredis/data/command_syntax.csv | 29 ++++-- iredis/redis_grammar.py | 107 +++++++++++++++------ iredis/utils.py | 6 +- pyproject.toml | 6 +- tests/cli_tests/test_command_input.py | 1 + tests/conftest.py | 17 ++++ tests/unittests/command_parse/test_base_token.py | 24 +++++ tests/unittests/command_parse/test_connection.py | 25 ++++- .../unittests/command_parse/test_generic_parse.py | 39 ++++++++ tests/unittests/command_parse/test_geo.py | 36 ++++--- tests/unittests/command_parse/test_list_parse.py | 35 +++++++ tests/unittests/command_parse/test_server.py | 71 ++++++++++++++ tests/unittests/test_client.py | 47 ++++++++- tests/unittests/test_completers.py | 93 +++++++++++++++++- 20 files changed, 535 insertions(+), 79 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 9c11de2..1207eca 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.11.1 +current_version = 1.12.0 commit = True tag = True diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 88e3ee4..722c808 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -49,7 +49,7 @@ jobs: REDIS_VERSION: ${{ matrix.redis }} run: | . venv/bin/activate - pytest || cat cli_test.log + pytest lint: name: flake8 & black runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 8db7aea..1420608 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,25 @@ -## 1.11.1 +## 1.12 + +- Feature: `CLIENT KILL` now support `LADDR` argument. +- Feature: `CLIENT LIST` now support `ID` argument. +- Feature: `CLIENT PAUSE` support options and added `CLIENT UNPAUSE` command. +- Feature: `CLIENT TRACKING` support multiple prefixes. +- Feature: support new command: `CLIENT TRACKINGINFO`. +- Feature: support new command: `COPY`. +- Feature: support new command: `EVAL_RO` and `EVALSHA_RO`. +- Feature: support new command: `EXPIRETIME`. +- Feature: support new command: `FAILOVER`. +- Feature: support new command: `GEOSEARCH`. +- Feature: support new command: `GEOSEARCHRESTORE`. +- Feature: support new command: `GETDEL`. +- Feature: support new command: `GETEX`. +- Feature: `FLUSHDB` and `FLUSHALL` supports `SYNC` option. +- Feature: `GEOADD` supports `CH XX NX` options. +- Feature: Timestamp Completers are now support completion for timestamp fields and milliseconds timestamp fields. +- Deprecate: `GEORADIUS` is deprecated, no auto-complete for this command anymore. +- Deprecate: `GEORADIUSBYMEMBER` is deprecated, no auto-complete for this command anymore. + +### 1.11.1 - Bugfix: Switch `distutils.version` to `packaging.version` to fix the version parse for windows. (new dependency: pypi's python-packaging. @@ -16,6 +37,12 @@ command like `BLPOP`. - Test: IRedis now tested under ubuntu-latest (before is ubuntu-16.04) - Dependency: Support Python 3.10 now, thanks to [tssujt]. +- Add new command group: `bitmap`. +- Support new command in Redis: + - `ACL GETUSER` + - `ACL HELP` + - `BLMOVE` + - `CLIENT INFO` ### 1.9.4 diff --git a/iredis/__init__.py b/iredis/__init__.py index c3fa782..b518f6e 100644 --- a/iredis/__init__.py +++ b/iredis/__init__.py @@ -1 +1 @@ -__version__ = "1.11.1" +__version__ = "1.12.0" diff --git a/iredis/client.py b/iredis/client.py index 29045da..1b1bf38 100644 --- a/iredis/client.py +++ b/iredis/client.py @@ -186,9 +186,7 @@ class Client: def get_server_info(self): # safe to decode Redis's INFO response info_resp = nativestr(self.execute("INFO")) - version = re.findall(r"^redis_version:([\d\.]+)\r\n", info_resp, re.MULTILINE)[ - 0 - ] + version = re.findall(r"redis_version:(.+)\r\n", info_resp)[0] logger.debug(f"[Redis Version] {version}") config.version = version diff --git a/iredis/completers.py b/iredis/completers.py index f5d922e..e3ebb5a 100644 --- a/iredis/completers.py +++ b/iredis/completers.py @@ -79,6 +79,15 @@ class TimestampCompleter(Completer): The timezone is read from system. """ + def __init__(self, is_milliseconds, future_time, *args, **kwargs): + if is_milliseconds: + self.factor = 1000 + else: + self.factor = 1 + + self.future_time = future_time + super().__init__(*args, **kwargs) + when_lower_than = { "year": 20, "month": 12, @@ -96,10 +105,17 @@ class TimestampCompleter(Completer): now = pendulum.now() for unit, minimum in self.when_lower_than.items(): if current <= minimum: - dt = now.subtract(**{f"{unit}s": current}) - meta = f"{text} {unit}{'s' if current > 1 else ''} ago ({dt.format('YYYY-MM-DD HH:mm:ss')})" + + if self.future_time: + dt = now.add(**{f"{unit}s": current}) + offset_text = "later" + else: + dt = now.subtract(**{f"{unit}s": current}) + offset_text = "ago" + + meta = f"{text} {unit}{'s' if current > 1 else ''} {offset_text} ({dt.format('YYYY-MM-DD HH:mm:ss')})" yield Completion( - str(dt.int_timestamp * 1000), + str(dt.int_timestamp * self.factor), start_position=-len(document.text_before_cursor), display_meta=meta, ) @@ -111,7 +127,7 @@ class TimestampCompleter(Completer): except Exception: return yield Completion( - str(dt.int_timestamp * 1000), + str(dt.int_timestamp * self.factor), start_position=-len(document.text_before_cursor), display_meta=str(dt), ) @@ -296,7 +312,16 @@ class IRedisCompleter(Completer): config.completer_max, [] ) categoryname_completer = MostRecentlyUsedFirstWordCompleter(100, []) - timestamp_completer = TimestampCompleter() + + timestamp_ms_ago_completer = TimestampCompleter( + is_milliseconds=True, future_time=False + ) + timestamp_ms_after_completer = TimestampCompleter( + is_milliseconds=True, future_time=True + ) + timestamp_after_completer = TimestampCompleter( + is_milliseconds=False, future_time=True + ) integer_type_completer = IntegerTypeCompleter() completer_mapping.update( @@ -317,7 +342,9 @@ class IRedisCompleter(Completer): # stream groups "group": group_completer, # stream id - "stream_id": timestamp_completer, + "stream_id": timestamp_ms_ago_completer, + "timestampms": timestamp_ms_after_completer, + "timestamp": timestamp_after_completer, "inttype": integer_type_completer, "categoryname": categoryname_completer, "username": username_completer, diff --git a/iredis/data/command_syntax.csv b/iredis/data/command_syntax.csv index 4036ff0..477c15c 100644 --- a/iredis/data/command_syntax.csv +++ b/iredis/data/command_syntax.csv @@ -32,19 +32,24 @@ connection,SELECT,command_index,render_simple_string connection,CLIENT CACHING,command_yes,render_simple_string connection,CLIENT GETREDIR,command,render_int connection,CLIENT TRACKING,command_client_tracking,render_simple_string -connection,CLIENT LIST,command_type_conntype_x,render_bulk_string_decode +connection,CLIENT TRACKINGINFO,command,render_list +connection,CLIENT LIST,command_client_list,render_bulk_string_decode connection,CLIENT GETNAME,command,render_bulk_string connection,CLIENT ID,command,render_int +connection,CLIENT INFO,command,render_bulk_string_decode connection,CLIENT KILL,command_clientkill,render_string_or_int -connection,CLIENT PAUSE,command_timeout,render_simple_string +connection,CLIENT PAUSE,command_pause,render_simple_string +connection,CLIENT UNPAUSE,command,render_simple_string connection,CLIENT REPLY,command_switch,render_simple_string connection,CLIENT SETNAME,command_value,render_simple_string connection,CLIENT UNBLOCK,command_clientid_errorx,render_int +generic,COPY,command_copy,render_int generic,DEL,command_keys,render_int generic,DUMP,command_key,render_bulk_string generic,EXISTS,command_keys,render_int generic,EXPIRE,command_key_second,render_int generic,EXPIREAT,command_key_timestamp,render_int +generic,EXPIRETIME,command_key,render_int generic,KEYS,command_pattern,command_keys generic,MIGRATE,command_migrate,render_simple_string generic,MOVE,command_key_index,render_int @@ -68,8 +73,10 @@ geo,GEOADD,command_key_longitude_latitude_members,render_int geo,GEODIST,command_geodist,render_bulk_string geo,GEOHASH,command_key_members,render_list geo,GEOPOS,command_key_members,render_list -geo,GEORADIUS,command_radius,render_list_or_string -geo,GEORADIUSBYMEMBER,command_georadiusbymember,render_list_or_string +geo,GEORADIUS,command_any,render_list_or_string +geo,GEORADIUSBYMEMBER,command_any,render_list_or_string +geo,GEOSEARCH,command_key_any,render_list +geo,GEOSEARCHSTORE,command_key_key_any,render_list hash,HDEL,command_key_fields,render_int hash,HEXISTS,command_key_field,render_int hash,HGET,command_key_field,render_bulk_string @@ -88,6 +95,7 @@ hash,HVALS,command_key,render_list hyperloglog,PFADD,command_key_values,render_int hyperloglog,PFCOUNT,command_keys,render_int hyperloglog,PFMERGE,command_newkey_keys,render_simple_string +list,BLMOVE,command_key_key_lr_lr_timeout, render_bulk_string list,BLPOP,command_keys_timeout,render_list_or_string list,BRPOP,command_keys_timeout,render_list_or_string list,BRPOPLPUSH,command_key_newkey_timeout,render_bulk_string @@ -113,7 +121,9 @@ pubsub,PUNSUBSCRIBE,command_channels,render_subscribe pubsub,SUBSCRIBE,command_channels,render_subscribe pubsub,UNSUBSCRIBE,command_channels,render_subscribe scripting,EVAL,command_lua_any,render_list_or_string +scripting,EVAL_RO,command_lua_any,render_list_or_string scripting,EVALSHA,command_any,render_list_or_string +scripting,EVALSHA_RO,command_any,render_list_or_string scripting,SCRIPT DEBUG,command_scriptdebug,render_simple_string scripting,SCRIPT EXISTS,command_any,render_list scripting,SCRIPT FLUSH,command,render_simple_string @@ -145,6 +155,7 @@ server,CONFIG SET,command_parameter_value,render_simple_string server,DBSIZE,command,render_int server,DEBUG OBJECT,command_key,render_simple_string server,DEBUG SEGFAULT,command,render_simple_string +server,FAILOVER,command_failover,render_simple_string server,FLUSHALL,command_asyncx,render_simple_string server,FLUSHDB,command_asyncx,render_simple_string server,INFO,command_sectionx,render_bulk_string_decode @@ -229,14 +240,16 @@ stream,XREADGROUP,command_xreadgroup,render_list stream,XREVRANGE,command_key_start_end_countx,render_list stream,XTRIM,command_key_maxlen,render_int string,APPEND,command_key_value,render_int -string,BITCOUNT,command_key_start_end_x,render_int -string,BITFIELD,command_bitfield,render_list -string,BITOP,command_operation_key_keys,render_int -string,BITPOS,command_key_bit_start_end,render_int +bitmap,BITCOUNT,command_key_start_end_x,render_int +bitmap,BITFIELD,command_bitfield,render_list +bitmap,BITOP,command_operation_key_keys,render_int +bitmap,BITPOS,command_key_bit_start_end,render_int string,DECR,command_key,render_int string,DECRBY,command_key_delta,render_int string,GET,command_key,render_bulk_string +string,GETEX,command_key_expire,render_bulk_string string,GETBIT,command_key_offset,render_int +string,GETDEL,command_key,render_bulk_string string,GETRANGE,command_key_start_end,render_bulk_string string,GETSET,command_key_value,render_bulk_string string,INCR,command_key,render_int diff --git a/iredis/redis_grammar.py b/iredis/redis_grammar.py index 666c0da..4e38540 100644 --- a/iredis/redis_grammar.py +++ b/iredis/redis_grammar.py @@ -18,6 +18,8 @@ CONST = { "withscores": "WITHSCORES", "limit": "LIMIT", "expiration": "EX PX", + "exat_const": "EXAT", + "pxat_const": "PXAT", "condition": "NX XX", "keepttl": "KEEPTTL", "operation": "AND OR XOR NOT", @@ -32,7 +34,7 @@ CONST = { "type": "string list set zset hash stream", "position_choice": "BEFORE AFTER", "error": "TIMEOUT ERROR", - "async": "ASYNC", + "async": "ASYNC SYNC", "conntype": "NORMAL MASTER REPLICA PUBSUB", "samples": "SAMPLES", "slotsubcmd": "IMPORTING MIGRATING NODE STABLE", @@ -45,6 +47,7 @@ CONST = { "on_off": "ON OFF", "const_id": "ID", "addr": "ADDR", + "laddr": "LADDR", "skipme": "SKIPME", "yes": "YES NO", "migratechoice": "COPY REPLACE", @@ -130,6 +133,13 @@ CONST = { "withmatchlen_const": "WITHMATCHLEN", "strings_const": "STRINGS", "rank_const": "RANK", + "lr_const": "LEFT RIGHT", + "pause_type": "WRITE ALL", + "db_const": "DB", + "replace_const": "REPLACE", + "to_const": "TO", + "timeout_const": "TIMEOUT", + "abort_const": "ABORT", } @@ -151,14 +161,16 @@ VALID_NODE = r"\w+" NUM = r"\d+" NNUM = r"-?\+?\(?\[?(\d+|inf)" # number cloud be negative _FLOAT = r"-?(\d|\.|e)+" +DOUBLE = r"\d*(\.\d+)?" LEXNUM = r"(\[\w+)|(\(\w+)|(\+)|(-)" SLOT = rf"(?P{VALID_SLOT})" SLOTS = rf"(?P{VALID_SLOT}(\s+{VALID_SLOT})*)" NODE = rf"(?P{VALID_NODE})" KEY = rf"(?P{VALID_TOKEN})" -PREFIX = rf"(?P{VALID_TOKEN})" KEYS = rf"(?P{VALID_TOKEN}(\s+{VALID_TOKEN})*)" +PREFIX = rf"(?P{VALID_TOKEN})" +PREFIXES = rf"(?P{VALID_TOKEN}(\s+{VALID_TOKEN})*?)" DESTINATION = rf"(?P{VALID_TOKEN})" NEWKEY = rf"(?P{VALID_TOKEN})" VALUE = rf"(?P{VALID_TOKEN})" @@ -205,13 +217,15 @@ PASSWORD = rf"(?P{VALID_TOKEN})" REPLICATIONID = rf"(?P{VALID_TOKEN})" INDEX = r"(?P(1[0-5]|\d))" CLIENTID = rf"(?P{NUM})" +CLIENTIDS = rf"(?P{NUM}(\s+{NUM})*)" + SECOND = rf"(?P{NUM})" -TIMESTAMP = rf"(?P{NUM})" +TIMESTAMP = r"(?P[T\d:>+*\-\$]+)" # TODO test lexer & completer for multi spaces in command # For now, redis command can have one space at most -COMMAND = "(\s* (?P[\w -]+))" +COMMAND = r"(\s* (?P[\w -]+))" MILLISECOND = rf"(?P{NUM})" -TIMESTAMPMS = rf"(?P{NUM})" +TIMESTAMPMS = r"(?P[T\d:>+*\-\$]+)" ANY = r"(?P.*)" # TODO deleted START = rf"(?P{NNUM})" END = rf"(?P{NNUM})" @@ -221,15 +235,24 @@ END = rf"(?P{NNUM})" # https://redis.io/topics/streams-intro#special-ids-in-the-streams-api # stream id, DO NOT use r"" here, or the \+ will be two string # NOTE: if miss the outer (), multi IDS won't work. -STREAM_ID = "(?P[T\d:>+*\-\$]+)" +STREAM_ID = r"(?P[T\d:>+*\-\$]+)" DELTA = rf"(?P{NNUM})" OFFSET = rf"(?P{NUM})" # string offset, can't be negative -SHARP_OFFSET = f"(?P\#?{NUM})" # for bitfield command +SHARP_OFFSET = rf"(?P\#?{NUM})" # for bitfield command +MIN = rf"(?P{NNUM})" +MAX = rf"(?P{NNUM})" +POSITION = rf"(?P{NNUM})" +SCORE = rf"(?P{_FLOAT})" +LEXMIN = rf"(?P{LEXNUM})" +LEXMAX = rf"(?P{LEXNUM})" +WEIGHTS = rf"(?P{_FLOAT}(\s+{_FLOAT})*)" +IP_PORT = rf"(?P{IP}:{PORT})" +HOST = rf"(?P{VALID_TOKEN})" MIN = rf"(?P{NNUM})" MAX = rf"(?P{NNUM})" POSITION = rf"(?P{NNUM})" -TIMEOUT = rf"(?P{NUM})" +TIMEOUT = rf"(?P{DOUBLE})" SCORE = rf"(?P{_FLOAT})" LEXMIN = rf"(?P{LEXNUM})" LEXMAX = rf"(?P{LEXNUM})" @@ -267,6 +290,7 @@ ON_OFF = rf"(?P{c('on_off')})" CONST_ID = rf"(?P{c('const_id')})" CONST_USER = rf"(?P{c('const_user')})" ADDR = rf"(?P{c('addr')})" +LADDR = rf"(?P{c('laddr')})" SKIPME = rf"(?P{c('skipme')})" YES = rf"(?P{c('yes')})" MIGRATECHOICE = rf"(?P{c('migratechoice')})" @@ -329,6 +353,16 @@ WITHMATCHLEN_CONST = rf"(?P{c('withmatchlen_const')})" STRINGS_CONST = rf"(?P{c('strings_const')})" RANK_CONST = rf"(?P{c('rank_const')})" +LR_CONST = rf"(?P{c('lr_const')})" +PAUSE_TYPE = rf"(?P{c('pause_type')})" +DB_CONST = rf"(?P{c('db_const')})" +REPLACE_CONST = rf"(?P{c('replace_const')})" +TO_CONST = rf"(?P{c('to_const')})" +TIMEOUT_CONST = rf"(?P{c('timeout_const')})" +ABORT_CONST = rf"(?P{c('abort_const')})" +PXAT_CONST = rf"(?P{c('pxat_const')})" +EXAT_CONST = rf"(?P{c('exat_const')})" + command_grammar = compile(COMMAND) # Here are the core grammars, those are tokens after ``command``. @@ -339,14 +373,6 @@ command_grammar = compile(COMMAND) GRAMMAR = { "command_key": rf"\s+ {KEY} \s*", "command_pattern": rf"\s+ {PATTERN} \s*", - "command_georadiusbymember": rf""" - \s+ {KEY} \s+ {MEMBER} - \s+ {FLOAT} \s+ {DISTUNIT} - (\s+ {GEOCHOICE})* - (\s+ {COUNT_CONST} \s+ {COUNT})? - (\s+ {ORDER})? - (\s+ {CONST_STORE} \s+ {KEY})? - (\s+ {CONST_STOREDIST} \s+ {KEY})? \s*""", "command_command": rf"\s+ {COMMAND} \s*", "command_slots": rf"\s+ {SLOTS} \s*", "command_node": rf"\s+ {NODE} \s*", @@ -370,8 +396,12 @@ GRAMMAR = { "command_messagex": rf"(\s+{MESSAGE})? \s*", "command_index": rf"\s+ {INDEX} \s*", "command_index_index": rf"\s+ {INDEX} \s+ {INDEX} \s*", - "command_type_conntype_x": rf""" - (\s+ {TYPE_CONST} \s+ {CONNTYPE})? \s*""", + "command_client_list": rf""" + ( + (\s+ {TYPE_CONST} \s+ {CONNTYPE})| + (\s+ {CONST_ID} \s+ {CLIENTIDS}) + )* + \s*""", "command_clientid_errorx": rf"\s+ {CLIENTID} (\s+ {ERROR})? \s*", "command_keys": rf"\s+ {KEYS} \s*", "command_key_value": rf"\s+ {KEY} \s+ {VALUE} \s*", @@ -388,7 +418,7 @@ GRAMMAR = { "command_key_newkey_timeout": rf"\s+ {KEY} \s+ {NEWKEY} \s+ {TIMEOUT} \s*", "command_keys_timeout": rf"\s+ {KEYS} \s+ {TIMEOUT} \s*", "command_count_timeout": rf"\s+ {COUNT} \s+ {TIMEOUT} \s*", - "command_timeout": rf"\s+ {TIMEOUT} \s*", + "command_pause": rf"\s+ {TIMEOUT} (\s+ {PAUSE_TYPE})? \s*", "command_key_positionchoice_pivot_value": rf""" \s+ {KEY} \s+ {POSITION_CHOICE} \s+ {VALUE} \s+ {VALUE} \s*""", "command_pass": rf"\s+ {ANY} \s*", @@ -420,10 +450,16 @@ GRAMMAR = { "command_key_members": rf"\s+ {KEY} \s+ {MEMBERS} \s*", "command_geodist": rf"\s+ {KEY} \s+ {MEMBER} \s+ {MEMBER} (\s+ {DISTUNIT})? \s*", "command_key_longitude_latitude_members": rf""" - \s+ {KEY} (\s+ {LONGITUDE} \s+ {LATITUDE} \s {MEMBER})+ \s*""", + \s+ {KEY} + (\s+ {CONDITION})? + (\s+ {CHANGED})? + (\s+ {LONGITUDE} \s+ {LATITUDE} \s {MEMBER})+ + \s*""", "command_destination_keys": rf"\s+ {DESTINATION} \s+ {KEYS} \s*", "command_object_key": rf"\s+ {OBJECT} \s+ {KEY} \s*", "command_key_member": rf"\s+ {KEY} \s+ {MEMBER} \s*", + "command_key_any": rf"\s+ {KEY} \s+ {ANY} \s*", + "command_key_key_any": rf"\s+ {KEY} \s+ {KEY} \s+ {ANY} \s*", "command_key_newkey_member": rf"\s+ {KEY} \s+ {NEWKEY} \s+ {MEMBER} \s*", "command_key_count_x": rf"\s+ {KEY} (\s+ {COUNT})? \s*", "command_key_min_max": rf"\s+ {KEY} \s+ {MIN} \s+ {MAX} \s*", @@ -459,6 +495,7 @@ GRAMMAR = { ( (\s+ {IP_PORT})| (\s+ {ADDR} \s+ {IP_PORT})| + (\s+ {LADDR} \s+ {IP_PORT})| (\s+ {CONST_ID} \s+ {CLIENTID})| (\s+ {TYPE_CONST} \s+ {CONNTYPE})| (\s+ {CONST_USER} \s+ {USERNAME})| @@ -474,12 +511,6 @@ GRAMMAR = { )? (\s+ {CONST_KEYS} \s+ {KEYS})? \s*""", - "command_radius": rf"""\s+ {KEY} - \s+ {LONGITUDE} \s+ {LATITUDE} \s+ {FLOAT} \s+ {DISTUNIT} - (\s+ {GEOCHOICE})* (\s+ {COUNT_CONST} \s+ {COUNT})? - (\s+ {ORDER})? - (\s+ {CONST_STORE} \s+ {KEY})? - (\s+ {CONST_STOREDIST} \s+ {KEY})? \s*""", "command_restore": rf"""\s+ {KEY} \s+ {TIMEOUT} \s+ {VALUE} (\s+ {SUBRESTORE} \s+ {SECOND})? \s*""", "command_pubsubcmd_channels": rf"\s+ {PUBSUBCMD} (\s+ {CHANNEL})+ \s*", @@ -573,7 +604,7 @@ GRAMMAR = { \s+ {ON_OFF} ( (\s+ {REDIRECT_CONST} \s+ {CLIENTID})| - (\s+ {PREFIX_CONST} \s+ {PREFIX})| + (\s+ {PREFIX_CONST} \s+ {PREFIXES})| (\s+ {BCAST_CONST})| (\s+ {OPTIN_CONST})| (\s+ {OPTOUT_CONST})| @@ -607,6 +638,28 @@ GRAMMAR = { (\s+ {MAXLEN} \s+ {LEN}) )* \s*""", + "command_key_key_lr_lr_timeout": rf""" + \s+ {KEY} \s+ {KEY} + \s+ {LR_CONST} \s+ {LR_CONST} + \s+ {TIMEOUT} \s*""", + "command_copy": rf""" + \s+ {KEY} \s+ {KEY} + (\s+ {DB_CONST} \s+ {INDEX})? + (\s+ {REPLACE_CONST})? + \s*""", + "command_failover": rf""" + (\s+ {TO_CONST} \s+ {HOST} \s+ {PORT} (\s+ {FORCE})? )? + (\s+ {ABORT_CONST})? + (\s+ {TIMEOUT_CONST} \s+ {MILLISECOND})? + \s*""", + "command_key_expire": rf""" + \s+ {KEY} + ( + (\s+ {EXPIRATION} \s+ {MILLISECOND})| + (\s+ {PXAT_CONST} \s+ {TIMESTAMPMS})| + (\s+ {EXAT_CONST} \s+ {TIMESTAMP}) + )? + \s*""", } pipeline = r"(?P\|.*)?" diff --git a/iredis/utils.py b/iredis/utils.py index 7cfae5a..593241b 100644 --- a/iredis/utils.py +++ b/iredis/utils.py @@ -14,7 +14,7 @@ logger = logging.getLogger(__name__) _last_timer = time.time() _timer_counter = 0 -sperator = re.compile(r"\s") +separator = re.compile(r"\s") logger.debug(f"[timer] start on {_last_timer}") @@ -73,8 +73,8 @@ def strip_quote_args(s): word.append(char) # not in quote else: - # sperator - if sperator.match(char): + # separator + if separator.match(char): if word: yield "".join(word) word = [] diff --git a/pyproject.toml b/pyproject.toml index e69dfc9..10b3a1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "iredis" -version = "1.11.1" +version = "1.12.0" description = "Terminal client for Redis with auto-completion and syntax highlighting." authors = ["laixintao "] readme = 'README.md' @@ -52,5 +52,5 @@ pexpect = "^4.7" iredis = 'iredis.entry:main' [build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/cli_tests/test_command_input.py b/tests/cli_tests/test_command_input.py index f70ee3c..75917cb 100644 --- a/tests/cli_tests/test_command_input.py +++ b/tests/cli_tests/test_command_input.py @@ -61,6 +61,7 @@ def test_hello_command_is_not_supported(cli): cli.expect("IRedis currently not support RESP3") +@pytest.mark.xfail(reason="unstable, maybe due to github action's signal handling") def test_abort_reading_connection(cli): cli.sendline("blpop mylist 30") cli.send(chr(3)) diff --git a/tests/conftest.py b/tests/conftest.py index 4d7d643..b70bf95 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ import os +import re import tempfile from textwrap import dedent @@ -17,6 +18,22 @@ TIMEOUT = 2 HISTORY_FILE = ".iredis_history" +@pytest.fixture +def token_should_match(): + def match_func(token, tomatch): + assert re.fullmatch(token, tomatch) is not None + + return match_func + + +@pytest.fixture +def token_should_not_match(): + def match_func(token, tomatch): + assert re.fullmatch(token, tomatch) is None + + return match_func + + @pytest.fixture def judge_command(): def judge_command_func(input_text, expect): diff --git a/tests/unittests/command_parse/test_base_token.py b/tests/unittests/command_parse/test_base_token.py index b04722e..545bc92 100644 --- a/tests/unittests/command_parse/test_base_token.py +++ b/tests/unittests/command_parse/test_base_token.py @@ -54,3 +54,27 @@ def test_command_with_key_in_quotes(judge_command): judge_command( 'cluster keyslot "mykey "', {"command": "cluster keyslot", "key": '"mykey "'} ) + + +def test_timeout(token_should_match, token_should_not_match): + from iredis.redis_grammar import TIMEOUT + + token_should_match(TIMEOUT, "1.1") + token_should_match(TIMEOUT, "1.0") + token_should_match(TIMEOUT, ".1") + token_should_match(TIMEOUT, "123123.1123") + token_should_not_match(TIMEOUT, "1.") + token_should_not_match(TIMEOUT, ".") + token_should_not_match(TIMEOUT, ".a") + + +def test_lr_const(token_should_match, token_should_not_match): + from iredis.redis_grammar import LR_CONST + + token_should_match(LR_CONST, "left") + token_should_match(LR_CONST, "right") + token_should_match(LR_CONST, "LEFT") + token_should_match(LR_CONST, "RIGHT") + token_should_not_match(LR_CONST, "righ") + token_should_not_match(LR_CONST, "ab") + token_should_not_match(LR_CONST, "123") diff --git a/tests/unittests/command_parse/test_connection.py b/tests/unittests/command_parse/test_connection.py index 8952e53..2a1de26 100644 --- a/tests/unittests/command_parse/test_connection.py +++ b/tests/unittests/command_parse/test_connection.py @@ -71,7 +71,7 @@ def test_client_tracking(judge_command): "command": "CLIENT TRACKING", "on_off": "ON", "prefix_const": "PREFIX", - "prefix": "foo", + "prefixes": "foo", }, ) judge_command( @@ -80,7 +80,7 @@ def test_client_tracking(judge_command): "command": "CLIENT TRACKING", "on_off": "ON", "prefix_const": "PREFIX", - "prefix": "foo", + "prefixes": "foo", }, ) judge_command( @@ -89,9 +89,28 @@ def test_client_tracking(judge_command): "command": "CLIENT TRACKING", "on_off": "ON", "prefix_const": "PREFIX", - "prefix": "foo", + "prefixes": "foo", "bcast_const": "BCAST", "noloop_const": "NOLOOP", "optin_const": "OPTIN", }, ) + judge_command( + "CLIENT TRACKING ON PREFIX foo bar ok BCAST NOLOOP OPTIN", + { + "command": "CLIENT TRACKING", + "on_off": "ON", + "prefix_const": "PREFIX", + "prefixes": "foo bar ok", + "bcast_const": "BCAST", + "noloop_const": "NOLOOP", + "optin_const": "OPTIN", + }, + ) + + +def test_client_pause(judge_command): + judge_command( + "CLIENT PAUSE 20 WRITE", + {"command": "CLIENT PAUSE", "timeout": "20", "pause_type": "WRITE"}, + ) diff --git a/tests/unittests/command_parse/test_generic_parse.py b/tests/unittests/command_parse/test_generic_parse.py index f41d887..6c4b0f3 100644 --- a/tests/unittests/command_parse/test_generic_parse.py +++ b/tests/unittests/command_parse/test_generic_parse.py @@ -177,3 +177,42 @@ def test_restore(judge_command): "value": '"\n\x17\x17\x00\x00\x00\x12\x00\x00\x00\x03\x00\x00\xc0\x01\x00\x04\xc0\x02\x00\x04\xc0\x03\x00\xff\x04\x00u#<\xc0;.\xe9\xdd"', # noqa }, ) + + +def test_copy(judge_command): + judge_command( + "COPY foo bar DB 3 REPLACE", + { + "command": "COPY", + "key": ["foo", "bar"], + "db_const": "DB", + "index": "3", + "replace_const": "REPLACE", + }, + ) + judge_command( + "COPY foo bar REPLACE", + {"command": "COPY", "key": ["foo", "bar"], "replace_const": "REPLACE"}, + ) + judge_command("COPY foo bar", {"command": "COPY", "key": ["foo", "bar"]}) + + +def test_getex(judge_command): + judge_command("GETEX foo", {"command": "GETEX", "key": "foo"}) + judge_command( + "GETEX bar ex 5", + {"command": "GETEX", "key": "bar", "expiration": "ex", "millisecond": "5"}, + ) + judge_command( + "GETEX bar px 5", + {"command": "GETEX", "key": "bar", "expiration": "px", "millisecond": "5"}, + ) + judge_command( + "GETEX bar pxat 5", + {"command": "GETEX", "key": "bar", "pxat_const": "pxat", "timestampms": "5"}, + ) + judge_command( + "GETEX bar exat 5", + {"command": "GETEX", "key": "bar", "exat_const": "exat", "timestamp": "5"}, + ) + judge_command("GETEX bar ex 5 exat 5", None) diff --git a/tests/unittests/command_parse/test_geo.py b/tests/unittests/command_parse/test_geo.py index 8a8d4c7..8502696 100644 --- a/tests/unittests/command_parse/test_geo.py +++ b/tests/unittests/command_parse/test_geo.py @@ -9,31 +9,37 @@ def test_geoadd(judge_command): "member": '"Catania"', }, ) + judge_command( + 'GEOADD Sicily NX CH 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"', + { + "command": "GEOADD", + "condition": "NX", + "changed": "CH", + "key": "Sicily", + "longitude": "15.087269", + "latitude": "37.502669", + "member": '"Catania"', + }, + ) -def test_georadiusbymember(judge_command): +def test_geosearch(judge_command): judge_command( - "GEORADIUSBYMEMBER Sicily Agrigento 100 km", + "GEOSEARCH Sicily FROMLONLAT 15 37 BYBOX 400 400 km ASC WITHCOORD WITHDIST", { - "command": "GEORADIUSBYMEMBER", + "command": "GEOSEARCH", "key": "Sicily", - "member": "Agrigento", - "float": "100", - "distunit": "km", + "any": "FROMLONLAT 15 37 BYBOX 400 400 km ASC WITHCOORD WITHDIST", }, ) -def test_georadius(judge_command): +def test_geosearchstore(judge_command): judge_command( - "GEORADIUS Sicily 15 37 200 km WITHDIST WITHCOORD ", + "GEOSEARCHSTORE key2 Sicily FROMLONLAT 15 37 BYBOX 400 400 km ASC COUNT 3 STOREDIST", { - "command": "GEORADIUS", - "key": "Sicily", - "longitude": "15", - "latitude": "37", - "float": "200", - "distunit": "km", - "geochoice": "WITHCOORD", + "command": "GEOSEARCHSTORE", + "key": ["Sicily", "key2"], + "any": "FROMLONLAT 15 37 BYBOX 400 400 km ASC COUNT 3 STOREDIST", }, ) diff --git a/tests/unittests/command_parse/test_list_parse.py b/tests/unittests/command_parse/test_list_parse.py index e60d7a2..69a294e 100644 --- a/tests/unittests/command_parse/test_list_parse.py +++ b/tests/unittests/command_parse/test_list_parse.py @@ -41,6 +41,18 @@ def test_brpoplpush(judge_command): judge_command("BRPOPLPUSH list1 list2 -1", None) +def test_brpoplpush_with_double_timeout(judge_command): + judge_command( + "BRPOPLPUSH list1 list2 10.0", + {"command": "BRPOPLPUSH", "key": "list1", "newkey": "list2", "timeout": "10.0"}, + ) + judge_command( + "BRPOPLPUSH list1 list2 .2", + {"command": "BRPOPLPUSH", "key": "list1", "newkey": "list2", "timeout": ".2"}, + ) + judge_command("BRPOPLPUSH list1 list2 12.", None) + + def test_linsert(judge_command): judge_command( 'LINSERT mylist BEFORE "World" "There"', @@ -106,3 +118,26 @@ def test_lpos(judge_command): "rank": "-1", }, ) + + +def test_blmove(judge_command): + judge_command( + "blmove list1 list2 left right 1.2", + { + "command": "blmove", + "key": ["list1", "list2"], + "lr_const": ["left", "right"], + "timeout": "1.2", + }, + ) + judge_command( + "blmove list1 list2 right right .2", + { + "command": "blmove", + "key": ["list1", "list2"], + "lr_const": ["right", "right"], + "timeout": ".2", + }, + ) + judge_command("blmove list1 list2 right right", None) + judge_command("blmove list1 right right 1", None) diff --git a/tests/unittests/command_parse/test_server.py b/tests/unittests/command_parse/test_server.py index 5129e5f..5ecccbf 100644 --- a/tests/unittests/command_parse/test_server.py +++ b/tests/unittests/command_parse/test_server.py @@ -31,6 +31,20 @@ def test_client_list(judge_command): {"command": "client list", "type_const": "TYPE", "conntype": "REPLICA"}, ) + judge_command( + "client list TYPE REPLICA id 1 2 3", + { + "command": "client list", + "type_const": "TYPE", + "conntype": "REPLICA", + "clientids": "1 2 3", + }, + ) + judge_command( + "client list ID 1 2 3", + {"command": "client list", "clientids": "1 2 3"}, + ) + def test_configset(judge_command): judge_command( @@ -71,6 +85,18 @@ def test_client_kill(judge_command): "CLIENT KILL 127.0.0.1:12345 ", {"command": "CLIENT KILL", "ip_port": "127.0.0.1:12345"}, ) + judge_command( + "CLIENT KILL ADDR 127.0.0.1:12345 ", + {"command": "CLIENT KILL", "ip_port": "127.0.0.1:12345", "addr": "ADDR"}, + ) + judge_command( + "CLIENT KILL LADDR 127.0.0.1:12345 ", + {"command": "CLIENT KILL", "ip_port": "127.0.0.1:12345", "laddr": "LADDR"}, + ) + judge_command( + "CLIENT KILL USER myuser", + {"command": "CLIENT KILL", "const_user": "USER", "username": "myuser"}, + ) judge_command( "CLIENT KILL id 123455 type pubsub skipme no", { @@ -199,3 +225,48 @@ def test_acl_setuser(judge_command): def test_acl_getuser(judge_command): judge_command("acl getuser alan", {"command": "acl getuser", "username": "alan"}) judge_command("acl getuser", None) + + +def test_failover(judge_command): + judge_command( + "failover to 10.0.0.5 7379 abort timeout 101", + { + "command": "failover", + "to_const": "to", + "host": "10.0.0.5", + "port": "7379", + "abort_const": "abort", + "timeout_const": "timeout", + "millisecond": "101", + }, + ) + judge_command( + "failover abort timeout 101", + { + "command": "failover", + "abort_const": "abort", + "timeout_const": "timeout", + "millisecond": "101", + }, + ) + judge_command( + "failover timeout 101", + { + "command": "failover", + "timeout_const": "timeout", + "millisecond": "101", + }, + ) + judge_command( + "failover to 10.0.0.5 7379 force abort timeout 101", + { + "command": "failover", + "to_const": "to", + "force": "force", + "host": "10.0.0.5", + "port": "7379", + "abort_const": "abort", + "timeout_const": "timeout", + "millisecond": "101", + }, + ) diff --git a/tests/unittests/test_client.py b/tests/unittests/test_client.py index f27587b..5c6628f 100644 --- a/tests/unittests/test_client.py +++ b/tests/unittests/test_client.py @@ -33,7 +33,7 @@ def test_send_command(_input, command_name, expect_args): client = Client("127.0.0.1", "6379", None) client.execute = MagicMock() next(client.send_command(_input, None)) - args, kwargs = client.execute.call_args + args, _ = client.execute.call_args assert args == (command_name, *expect_args) @@ -511,7 +511,7 @@ def test_reissue_command_on_redis_cluster_with_password_in_dsn( assert call_args[0].password == "bar" -def test_version_parse(iredis_client): +def test_version_parse_for_auth(iredis_client): """ fix: https://github.com/laixintao/iredis/issues/418 """ @@ -521,3 +521,46 @@ def test_version_parse(iredis_client): assert command2syntax["AUTH"] == "command_password" iredis_client.auth_compat("5.0.14.1") assert command2syntax["AUTH"] == "command_password" + + +@pytest.mark.parametrize( + "info, version", + [ + ( + ( + "# Server\r\nredis_version:df--128-NOTFOUND\r\n" + "redis_mode:standalone\r\narch_bits:64" + ), + "df--128-NOTFOUND", + ), + ( + ( + "# Server\r\nredis_version:6.2.5\r\n" + "redis_git_sha1:00000000\r\n" + "redis_git_dirty:0\r\n" + "redis_build_id:915e5480613bc9b6\r\n" + "redis_mode:standalone " + ), + "6.2.5", + ), + ( + ( + "# Server\r\nredis_version:5.0.14.1\r\n" + "redis_git_sha1:00000000\r\nredis_git_dirty:0\r\n" + "redis_build_id:915e5480613bc9b6\r\n" + "redis_mode:standalone " + ), + "5.0.14.1", + ), + ], +) +def test_version_path(info, version): + with patch("iredis.client.config") as mock_config: + mock_config.no_info = True + mock_config.pager = "less" + mock_config.version = "5.0.0" + with patch("iredis.client.Client.execute") as mock_execute: + mock_execute.return_value = info + client = Client("127.0.0.1", "6379", None) + client.get_server_info() + assert mock_config.version == version diff --git a/tests/unittests/test_completers.py b/tests/unittests/test_completers.py index d0344fd..5441b0e 100644 --- a/tests/unittests/test_completers.py +++ b/tests/unittests/test_completers.py @@ -183,7 +183,7 @@ def test_group_completer(): @patch("iredis.completers.pendulum.now") def test_timestamp_completer_humanize_time_completion(fake_now): fake_now.return_value = pendulum.from_timestamp(1578487013) - c = TimestampCompleter() + c = TimestampCompleter(is_milliseconds=True, future_time=False) fake_document = MagicMock() fake_document.text = fake_document.text_before_cursor = "30" @@ -260,8 +260,87 @@ def test_timestamp_completer_humanize_time_completion(fake_now): ] +@patch("iredis.completers.pendulum.now") +def test_timestamp_completer_humanize_time_completion_seconds(fake_now): + fake_now.return_value = pendulum.from_timestamp(1578487013) + c = TimestampCompleter(is_milliseconds=False, future_time=False) + + fake_document = MagicMock() + fake_document.text = fake_document.text_before_cursor = "30" + completions = list(c.get_completions(fake_document, None)) + + assert completions == [ + Completion( + text="1575895013", + start_position=-2, + display=FormattedText([("", "1575895013")]), + display_meta="30 days ago (2019-12-09 12:36:53)", + ), + Completion( + text="1578379013", + start_position=-2, + display=FormattedText([("", "1578379013")]), + display_meta="30 hours ago (2020-01-07 06:36:53)", + ), + Completion( + text="1578485213", + start_position=-2, + display=FormattedText([("", "1578485213")]), + display_meta="30 minutes ago (2020-01-08 12:06:53)", + ), + Completion( + text="1578486983", + start_position=-2, + display=FormattedText([("", "1578486983")]), + display_meta="30 seconds ago (2020-01-08 12:36:23)", + ), + ] + + +@patch("iredis.completers.pendulum.now") +def test_timestamp_completer_humanize_time_completion_seconds_future_time(fake_now): + fake_now.return_value = pendulum.from_timestamp(1578487013) + c = TimestampCompleter(is_milliseconds=False, future_time=True) + + fake_document = MagicMock() + fake_document.text = fake_document.text_before_cursor = "30" + completions = list(c.get_completions(fake_document, None)) + + print(completions) + for c in completions: + print(c.text) + print(c.display) + print(c.display_meta) + assert completions == [ + Completion( + text="1578487043", + start_position=-2, + display=FormattedText([("", "1578487043")]), + display_meta="30 seconds later (2020-01-08 12:37:23)", + ), + Completion( + text="1578488813", + start_position=-2, + display=FormattedText([("", "1578488813")]), + display_meta="30 minutes later (2020-01-08 13:06:53)", + ), + Completion( + text="1578595013", + start_position=-2, + display=FormattedText([("", "1578595013")]), + display_meta="30 hours later (2020-01-09 18:36:53)", + ), + Completion( + text="1581079013", + start_position=-2, + display=FormattedText([("", "1581079013")]), + display_meta="30 days later (2020-02-07 12:36:53)", + ), + ] + + def test_timestamp_completer_datetime_format_time_completion(): - c = TimestampCompleter() + c = TimestampCompleter(is_milliseconds=True, future_time=False) fake_document = MagicMock() fake_document.text = fake_document.text_before_cursor = "2020-02-07" completions = list(c.get_completions(fake_document, None)) @@ -299,14 +378,18 @@ def test_completion_casing(): completion.text for completion in c.get_completions(fake_document, None) ] == [ "get", + "getex", "getset", + "getdel", "getbit", "geopos", "geoadd", "geohash", "geodist", "getrange", + "geosearch", "georadius", + "geosearchstore", "georadiusbymember", ] @@ -314,19 +397,19 @@ def test_completion_casing(): fake_document.text = fake_document.text_before_cursor = "GET" assert [ completion.text for completion in c.get_completions(fake_document, None) - ] == ["GET", "GETSET", "GETBIT", "GETRANGE"] + ] == ["GET", "GETEX", "GETSET", "GETDEL", "GETBIT", "GETRANGE"] c = IRedisCompleter(completion_casing="upper") fake_document.text = fake_document.text_before_cursor = "get" assert [ completion.text for completion in c.get_completions(fake_document, None) - ] == ["GET", "GETSET", "GETBIT", "GETRANGE"] + ] == ["GET", "GETEX", "GETSET", "GETDEL", "GETBIT", "GETRANGE"] c = IRedisCompleter(completion_casing="lower") fake_document.text = fake_document.text_before_cursor = "GET" assert [ completion.text for completion in c.get_completions(fake_document, None) - ] == ["get", "getset", "getbit", "getrange"] + ] == ["get", "getex", "getset", "getdel", "getbit", "getrange"] def test_username_completer(): -- cgit v1.2.3