Skip to content

Commit 6b03744

Browse files
committed
Precommit hooks, list_banned_words, more tests
1 parent 19375cb commit 6b03744

File tree

8 files changed

+210
-57
lines changed

8 files changed

+210
-57
lines changed

.pre-commit-config.yaml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
repos:
2+
- repo: https://github.com/pre-commit/pre-commit-hooks
3+
rev: v4.4.0
4+
hooks:
5+
- id: check-added-large-files
6+
- id: check-ast
7+
- id: check-case-conflict
8+
- id: check-docstring-first
9+
- id: check-executables-have-shebangs
10+
- id: check-shebang-scripts-are-executable
11+
- id: check-merge-conflict
12+
- id: check-symlinks
13+
- id: check-toml
14+
- id: check-yaml
15+
- id: debug-statements
16+
- id: destroyed-symlinks
17+
- id: detect-private-key
18+
- id: end-of-file-fixer
19+
- id: forbid-submodules
20+
- id: mixed-line-ending
21+
- id: name-tests-test
22+
args:
23+
- --pytest-test-first
24+
- id: trailing-whitespace
25+
- repo: https://github.com/pre-commit/pygrep-hooks
26+
rev: v1.10.0
27+
hooks:
28+
- id: python-check-blanket-noqa
29+
- id: python-check-blanket-type-ignore
30+
- id: python-check-mock-methods
31+
- id: python-no-eval
32+
- id: python-no-log-warn
33+
- id: python-use-type-annotations
34+
- id: text-unicode-replacement-char
35+
- repo: local
36+
hooks:
37+
- id: flakes
38+
name: flake8
39+
entry: flake8
40+
language: python
41+
types: [python]
42+
pass_filenames: false
43+
args:
44+
- --count
45+
- --show-source
46+
- --statistics
47+
- --max-line-length=120
48+
- --per-file-ignores=src/nexus/Freqlog/backends/__init__.py:F401
49+
- --exclude=venv
50+
additional_dependencies: [flake8]
51+
- id: pytest
52+
name: pytest
53+
entry: pytest
54+
language: python
55+
types: [python]
56+
pass_filenames: false
57+
stages:
58+
- push
59+
args:
60+
- --cov=nexus
61+
- --cov-report=html
62+
additional_dependencies: [., pytest, pytest-cov]

dist.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,7 @@
5858
os.system("cp README.md LICENSE dist")
5959

6060
print("Done! Built executable is in dist/")
61+
62+
# Setup git hooks
63+
print("Setting up git hooks...")
64+
os.system("pre-commit install")

src/nexus/Freqlog/Definitions.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from datetime import datetime, timedelta
22
from enum import Enum
3+
from typing import Any, Self
34

45
from pynput.keyboard import Key
56

@@ -43,7 +44,7 @@ def __init__(self, word: str, frequency: int, last_used: datetime, average_speed
4344
self.last_used = last_used
4445
self.average_speed = average_speed
4546

46-
def __or__(self, other):
47+
def __or__(self, other: Any) -> Self:
4748
"""Merge two WordMetadata objects"""
4849
if other is not None and not isinstance(other, WordMetadata):
4950
raise TypeError(f"unsupported operand type(s) for |: '{type(self).__name__}' and '{type(other).__name__}'")
@@ -91,7 +92,7 @@ class ChordMetadataAttr(Enum):
9192
last_used = "lastused"
9293

9394

94-
class Banlist:
95+
class BanlistEntry:
9596
"""Banlist entry"""
9697

9798
def __init__(self, word: str, date_added: datetime) -> None:

src/nexus/Freqlog/Freqlog.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from pynput import keyboard as kbd, mouse
66

77
from .backends import Backend, SQLiteBackend
8-
from .Definitions import ActionType, Banlist, BanlistAttr, CaseSensitivity, ChordMetadata, ChordMetadataAttr, \
8+
from .Definitions import ActionType, BanlistAttr, BanlistEntry, CaseSensitivity, ChordMetadata, ChordMetadataAttr, \
99
Defaults, WordMetadata, WordMetadataAttr
1010

1111

@@ -115,7 +115,7 @@ def _log_and_reset_word(min_length: int = 1) -> None:
115115
if action == ActionType.PRESS and key in self.modifier_keys:
116116
active_modifier_keys.add(key)
117117
elif action == ActionType.RELEASE:
118-
active_modifier_keys.remove(key)
118+
active_modifier_keys.discard(key)
119119

120120
# On backspace, remove last char from word if word is not empty
121121
if key == kbd.Key.backspace and word:
@@ -186,10 +186,10 @@ def check_banned(self, word: str, case: CaseSensitivity) -> bool:
186186
logging.info(f"Checking if {word} is banned, case {case}")
187187
return self.backend.check_banned(word, case)
188188

189-
def ban_word(self, word: str, case: CaseSensitivity) -> None:
189+
def ban_word(self, word: str, case: CaseSensitivity, time: datetime) -> None:
190190
"""Ban a word from being logged"""
191-
logging.info(f"Banning {word}, case {case}")
192-
self.backend.ban_word(word, case)
191+
logging.info(f"Banning {word}, case {case} - {time}")
192+
self.backend.ban_word(word, case, time)
193193
logging.warning(f"Banned {word}, case {case}")
194194

195195
def unban_word(self, word: str, case: CaseSensitivity) -> None:
@@ -222,12 +222,14 @@ def list_chords(self, limit: int, sort_by: ChordMetadataAttr,
222222
logging.info(f"Listing chords, limit {limit}, sort_by {sort_by}, reverse {reverse}, case {case}")
223223
return self.backend.list_chords(limit, sort_by, reverse, case)
224224

225-
def list_banned_words(self, limit: int, sort_by: BanlistAttr, reverse: bool) -> list[Banlist]:
225+
def list_banned_words(self, limit: int, sort_by: BanlistAttr, reverse: bool) \
226+
-> tuple[list[BanlistEntry], list[BanlistEntry]]:
226227
"""
227228
List banned words
228229
:param limit: Maximum number of banned words to return
229230
:param sort_by: Attribute to sort by: word
230231
:param reverse: Reverse sort order
232+
:returns: Tuple of (banned words with case, banned words without case)
231233
"""
232234
logging.info(f"Listing banned words, limit {limit}, sort_by {sort_by}, reverse {reverse}")
233235
return self.backend.list_banned_words(limit, sort_by, reverse)

src/nexus/Freqlog/backends/Backend.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from abc import ABC, abstractmethod
22
from datetime import datetime
33

4-
from nexus.Freqlog.Definitions import Banlist, BanlistAttr, CaseSensitivity, ChordMetadata, ChordMetadataAttr, \
4+
from nexus.Freqlog.Definitions import BanlistAttr, BanlistEntry, CaseSensitivity, ChordMetadata, ChordMetadataAttr, \
55
WordMetadata, WordMetadataAttr
66

77

@@ -40,7 +40,7 @@ def check_banned(self, word: str, case: CaseSensitivity) -> bool:
4040
# TODO: Support banning chords
4141

4242
@abstractmethod
43-
def ban_word(self, word: str, case: CaseSensitivity) -> None:
43+
def ban_word(self, word: str, case: CaseSensitivity, time: datetime) -> None:
4444
"""Delete a word entry and add it to the ban list"""
4545

4646
@abstractmethod
@@ -70,13 +70,15 @@ def list_chords(self, limit: int, sort_by: ChordMetadataAttr,
7070
"""
7171

7272
@abstractmethod
73-
def list_banned_words(self, limit: int, sort_by: BanlistAttr, reverse: bool) -> list[Banlist]:
73+
def list_banned_words(self, limit: int, sort_by: BanlistAttr, reverse: bool) \
74+
-> tuple[list[BanlistEntry], list[BanlistEntry]]:
7475
"""
7576
List banned words
7677
:param limit: Maximum number of banned words to return
7778
:param sort_by: Attribute to sort by: word
7879
:param reverse: Reverse sort order
80+
:returns: Tuple of (banned words with case, banned words without case)
7981
"""
8082

81-
def close(self):
83+
def close(self) -> None:
8284
"""Close the backend"""

src/nexus/Freqlog/backends/SQLite/SQLiteBackend.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from nexus import __version__
55
from nexus.Freqlog.backends.Backend import Backend
6-
from nexus.Freqlog.Definitions import Banlist, BanlistAttr, CaseSensitivity, ChordMetadata, ChordMetadataAttr, \
6+
from nexus.Freqlog.Definitions import BanlistAttr, BanlistEntry, CaseSensitivity, ChordMetadata, ChordMetadataAttr, \
77
WordMetadata, WordMetadataAttr
88

99
# WARNING: Loaded into SQL query, do not use unsanitized user input
@@ -35,8 +35,9 @@ def __init__(self, db_path: str) -> None:
3535
# Banlist table
3636
self._execute(
3737
"CREATE TABLE IF NOT EXISTS banlist (word TEXT PRIMARY KEY, dateadded timestamp NOT NULL) WITHOUT ROWID")
38-
self._execute("CREATE INDEX IF NOT EXISTS banlist_lower ON banlist(word COLLATE NOCASE)")
39-
self._execute("CREATE UNIQUE INDEX IF NOT EXISTS banlist_dateadded ON banlist(dateadded)")
38+
self._execute("CREATE INDEX IF NOT EXISTS banlist_dateadded ON banlist(dateadded)")
39+
self._execute("CREATE TABLE IF NOT EXISTS banlist_lower (word TEXT PRIMARY KEY COLLATE NOCASE,"
40+
"dateadded timestamp NOT NULL) WITHOUT ROWID")
4041

4142
def _execute(self, query: str, params=None) -> None:
4243
if params:
@@ -83,9 +84,11 @@ def get_word_metadata(self, word: str, case: CaseSensitivity) -> WordMetadata |
8384
res_u = self._fetchone(f"{SQL_SELECT_STAR_FROM_FREQLOG} WHERE word=?", (word_u,))
8485
res_l = self._fetchone(f"{SQL_SELECT_STAR_FROM_FREQLOG} WHERE word=?", (word_l,))
8586
word_meta_u = WordMetadata(word, res_u[1], datetime.fromtimestamp(res_u[2]),
86-
timedelta(seconds=res_u[3]))
87+
timedelta(seconds=res_u[3])) if res_u else None
8788
word_meta_l = WordMetadata(word, res_l[1], datetime.fromtimestamp(res_l[2]),
88-
timedelta(seconds=res_l[3]))
89+
timedelta(seconds=res_l[3])) if res_l else None
90+
if not word_meta_u:
91+
return word_meta_l
8992
return word_meta_u | word_meta_l
9093
case CaseSensitivity.SENSITIVE:
9194
res = self._fetchone(f"{SQL_SELECT_STAR_FROM_FREQLOG} WHERE word=?", (word,))
@@ -134,30 +137,32 @@ def check_banned(self, word: str, case: CaseSensitivity) -> bool:
134137
res = self._fetchone("SELECT word FROM banlist WHERE word=?", (word,))
135138
return res is not None
136139

137-
def ban_word(self, word: str, case: CaseSensitivity) -> None:
140+
def ban_word(self, word: str, case: CaseSensitivity, time: datetime) -> None:
138141
"""Delete a word entry and add it to the ban list"""
139142
match case:
140143
case CaseSensitivity.INSENSITIVE:
141144
word = word.lower()
142145
self._execute("DELETE FROM freqlog WHERE word = ? COLLATE NOCASE", (word,))
143-
self._execute("INSERT INTO banlist VALUES (?)", (word,))
146+
self._execute("INSERT INTO banlist VALUES (?, ?)", (word, time.timestamp()))
147+
self._execute("INSERT INTO banlist_lower VALUES (?, ?)", (word, time.timestamp()))
144148
case CaseSensitivity.FIRST_CHAR:
145149
word_u = word[0].upper() + word[1:]
146150
word_l = word[0].lower() + word[1:]
147151
self._execute("DELETE FROM freqlog WHERE word=?", (word_u,))
148152
self._execute("DELETE FROM freqlog WHERE word=?", (word_l,))
149-
self._execute("INSERT INTO banlist VALUES (?)", (word_u,))
150-
self._execute("INSERT INTO banlist VALUES (?)", (word_l,))
153+
self._execute("INSERT INTO banlist VALUES (?, ?)", (word_u, time.timestamp()))
154+
self._execute("INSERT INTO banlist VALUES (?, ?)", (word_l, time.timestamp()))
151155
case CaseSensitivity.SENSITIVE:
152156
self._execute("DELETE FROM freqlog WHERE word=?", (word,))
153-
self._execute("INSERT INTO banlist VALUES (?)", (word,))
157+
self._execute("INSERT INTO banlist VALUES (?, ?)", (word, time.timestamp()))
154158

155159
def unban_word(self, word: str, case: CaseSensitivity) -> None:
156160
"""Remove a word from the ban list"""
157161
match case:
158162
case CaseSensitivity.INSENSITIVE:
159163
word = word.lower()
160164
self._execute("DELETE FROM banlist WHERE word = ? COLLATE NOCASE", (word,))
165+
self._execute("DELETE FROM banlist_lower WHERE word = ? COLLATE NOCASE", (word,))
161166
case CaseSensitivity.FIRST_CHAR:
162167
word_u = word[0].upper() + word[1:]
163168
word_l = word[0].lower() + word[1:]
@@ -208,16 +213,27 @@ def list_chords(self, limit: int, sort_by: ChordMetadataAttr,
208213
"""
209214
raise NotImplementedError # TODO: implement
210215

211-
def list_banned_words(self, limit: int, sort_by: BanlistAttr, reverse: bool) -> list[Banlist]:
216+
def list_banned_words(self, limit: int, sort_by: BanlistAttr, reverse: bool) \
217+
-> tuple[list[BanlistEntry], list[BanlistEntry]]:
212218
"""
213219
List banned words
214220
:param limit: Maximum number of banned words to return
215221
:param sort_by: Attribute to sort by: word
216222
:param reverse: Reverse sort order
223+
:returns: Tuple of (banned words with case, banned words without case)
217224
"""
218-
raise NotImplementedError # TODO: implement
219-
220-
def close(self):
225+
if reverse:
226+
sql_sort_limit = f"{sort_by.value} DESC"
227+
else:
228+
sql_sort_limit = sort_by.value
229+
if limit > 0:
230+
sql_sort_limit += f" LIMIT {limit}"
231+
res = self._fetchall(f"SELECT * FROM banlist ORDER BY {sql_sort_limit}")
232+
res_lower = self._fetchall(f"SELECT * FROM banlist_lower ORDER BY {sql_sort_limit}")
233+
return [BanlistEntry(row[0], datetime.fromtimestamp(row[1])) for row in res], \
234+
[BanlistEntry(row[0], datetime.fromtimestamp(row[1])) for row in res_lower]
235+
236+
def close(self) -> None:
221237
"""Close the database connection"""
222238
self.cursor.close()
223239
self.conn.close()

src/nexus/__main__.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -236,13 +236,17 @@ def main():
236236
reverse=(args.order == Order.DESCENDING)):
237237
print(chord)
238238
case "banlist": # get banned words
239-
res = freqlog.list_banned_words(args.num, BanlistAttr[args.sort_by], args.order == Order.DESCENDING)
240-
if len(res) == 0:
239+
res, res1 = freqlog.list_banned_words(args.num, BanlistAttr[args.sort_by],
240+
args.order == Order.DESCENDING)
241+
if len(res) == 0 and len(res1) == 0:
241242
print("No banned words")
242243
else:
243-
for word in sorted(res, key=lambda x: getattr(x, args.sort_by),
244-
reverse=(args.order == Order.DESCENDING)):
245-
print(word)
244+
for entry in res1:
245+
entry.word += "*"
246+
print("Banned words (* denotes case-insensitive entries):")
247+
for entry in sorted(res + res1, key=lambda x: getattr(x, args.sort_by),
248+
reverse=(args.order == Order.DESCENDING)):
249+
print(entry)
246250
except NotImplementedError:
247251
print(f"Error: The '{args.command}' command has not been implemented yet")
248252
exit_code = 100

0 commit comments

Comments
 (0)