-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathyoudao_dict.py
executable file
·1802 lines (1489 loc) · 60.3 KB
/
youdao_dict.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from bs4 import BeautifulSoup
import bs4
import requests
import sys
import os
import json
import stat
import inspect
from random import randint
import glob
# This enables us to display unicode characters correctly
import locale
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
def dbg_printf(format, *args):
"""
C-Style function that writes debugging output to the terminal. If debug flag is off
this function does not print anything
:param format: The format string
:param args: Arguments
:return: None
"""
global debug_flag
# Do not print anything annoying
if debug_flag is False:
return
frame = inspect.currentframe()
prev_frame = frame.f_back
code = prev_frame.f_code
prev_name = code.co_name
# Make it more human readable by replacing the name with
# an easy to understand one
if prev_name == "<module>":
prev_name = "[Top Level Module]"
else:
prev_name += "()"
# Write the prologue of debugging information
sys.stderr.write("%-28s: " % (prev_name,))
format = format % tuple(args)
sys.stderr.write(format)
# So we do not need to worry about new lines
sys.stderr.write('\n')
return
def get_webpage(word):
"""
This function returns a string which is the webpage of a given word
Returns None if the return code is not HTTP status 200 which means an error
happened
:return: str/None
"""
url = "http://dict.youdao.com/search?le=eng&q=%s&keyfrom=dict2.index" % (word, )
r = requests.get(url)
if r.status_code != 200:
dbg_printf("Error executing HTTP request to %s; return code %d",
url,
r.status_code)
return None
return r.text
def parse_webpage(s):
"""
Given the text of the webpage, parse it as an instance of a beautiful soup,
using the default parser and encoding
:param s: The text of the webpage
:return: beautiful soup object
"""
return BeautifulSoup(s, 'html.parser')
def get_alternatives(tree):
"""
This function returns alternative words if the collins div tag is not found
in the returned tree object.
If alternative (suggested) word is also not found then this function returns
None. This behavior is consistent with the main parsing function, and the
caller should not print anything
:param tree: The tree object
:return: None
"""
# This is a list of typos
p_typo_list = tree.select("p.typo-rel")
# If we did not find any then return None
if len(p_typo_list) == 0:
return None
# We push all alternative words into this list
l = []
ret = {"alternatives": l}
index = -1
# For each suggestion in the list push it into the dict's list
for p_typo in p_typo_list:
index += 1
a = p_typo.find("a")
if a is None:
dbg_printf("The <a> tag is not found in <p> typo-rel (index = %d)",
index)
continue
l.append(a.text)
return ret
def get_collins_dict(tree):
"""
This function returns results by collins dictionary, given the beautiful soup
tree object
The return value is defined as a list of the following dict structure:
{
"word": "The word"
"phonetic": "The pronunciation"
"frequency": 4, // This is always a number between 1 and 5, or -1 to indicate unknown
"meanings": [
{
"category": "n. v. adj. adv. , etc.",
"text": "Meaning of the word",
"examples": [
{
"text": "Text of the example, with <b></b> being the keyword"
"translation": "Translation of the text"
},
]
}, ...
],
"word-group": [
{
"text": "Word group text",
"meaning": "The meaning of the word group"
}, ...
],
}
Each element in the list is regarded as a distinct meaning of the word, and is independent of
each other.
However, if the word is not found, and there are suggestions, then the return
value is defined as below:
{
"alternatives": ["word1", "word2", ...]
}
The caller should check whether key "alternatives" is present, and if it is then it
implies that the word being queried is not found, and instead we return suggestions
that are similar to the word.
If alternatives are also not found then we return None
:param tree: The beautiful soup tree
:return: A list of the dict as specified as above, or None if fails
"""
collins_result = tree.find(id="collinsResult")
if isinstance(collins_result, bs4.element.Tag) is False:
dbg_printf("Did not find id='collinsResult'")
# This could also return None if alternatives are also not found
return get_alternatives(tree)
elif collins_result.name != "div":
dbg_printf("id='collinsResult' is not a div tag")
# It is a valid tag, but the name is not div
# This should be strange, though
return None
# This list contains all meanings of the word, each with a pronunciation
top_level_list = collins_result.select("div.wt-container")
ret_list = []
# We set this to be the first word
actual_key = None
# This is the index for <div>.wt
wt_index = -1
for sub_tree in top_level_list:
wt_index += 1
# We append this into a list
ret = {}
# This <h4> contains the main word, the pronunciation
h4 = sub_tree.find("h4")
if h4 is None:
dbg_printf("Did not find <h4> (wt_index = %d)", wt_index)
return None
span_list = h4.find_all("span")
if len(span_list) < 1:
dbg_printf("Did not find <span> under <h4> (wt_index = %d)", wt_index)
return None
# This is the word we are looking for
ret["word"] = span_list[0].text
# This contains the phonetic
em = h4.find("em")
if em is None:
# If we did not find <em> then there is no pronunciation
# and we just set it as empty string
ret["phonetic"] = ""
else:
# Save the phonetic (note: this is not ASCII)
ret["phonetic"] = em.text
# Initialize the meanings list
ret["meanings"] = []
# Get the frequency span; If no such element just set it to -1
# which means the freq is invalid
freq_span = h4.select("span.star")
if len(freq_span) == 0:
ret["frequency"] = -1
else:
freq = -1
star_attr = freq_span[0].attrs["class"]
if "star1" in star_attr:
freq = 1
elif "star2" in star_attr:
freq = 2
elif "star3" in star_attr:
freq = 3
elif "star4" in star_attr:
freq = 4
elif "star5" in star_attr:
freq = 5
ret["frequency"] = freq
# This is all meanings
li_list = sub_tree.find_all("li")
li_count = -1
for li in li_list:
li_count += 1
# Just add stuff into this dict object
d = {}
# find main div and example div list
main_div_list = li.select("div.collinsMajorTrans")
if len(main_div_list) == 0:
continue
else:
main_div = main_div_list[0]
example_div_list = li.select("div.exampleLists")
# Then find the <p> in the first div, which contains word category and
# the meaning of the word
p = main_div.find("p")
if p is None:
dbg_printf("Did not find the <p> under main <div> (index = %d)",
li_count)
return None
span = p.find("span")
# This is possible if this entry is simply a redirection
if span is None:
# if there is an <a> in the <span> then it is a redirection
a = p.find("a")
if a is not None:
# Make it invisible
d["category"] = "REDIRECTION"
else:
d["category"] = "UNKNOWN"
meaning = ""
for content in p.contents:
if isinstance(content, bs4.element.Tag) is True:
if content.name == "a":
meaning += ("<green>" + " ".join(content.text.split()) + "</green>")
elif content.name == "b":
meaning += ("<red>" + " ".join(content.text.split()) + "</red>")
else:
meaning += " ".join(content.text.split())
else:
meaning += " ".join(content.split())
meaning += " "
d["text"] = meaning
d["examples"] = []
ret["meanings"].append(d)
continue
# Save the category as category attribute
d["category"] = span.text
# Then for all text and child nodes in p, find the span
# and then add all strings together after it
meaning = ""
for content in p.contents:
if isinstance(content, bs4.element.Tag) is True and \
content.name == "span" and \
content.text == span.text:
continue
# for keywords in the article we manually surround them with
# <b></b> tags
if isinstance(content, bs4.element.Tag) is True and \
content.name == "b":
content = "<red>" + " ".join(content.text.split()) + "</red>"
elif isinstance(content, bs4.element.Tag):
content = " ".join(content.text.split())
else:
content = " ".join(content.split())
if len(content) == 0:
continue
# Then use space to separate the contents
meaning += (content + " ")
# If we did not find anything then return
if len(meaning) == 0:
dbg_printf("Did not find the meaning of the word in the <p> under main div (index = %d)",
li_count)
return None
# Save the meaning of the word as the text
d["text"] = meaning
# Push examples into this list
l = []
d["examples"] = l
# These are all examples
for div in example_div_list:
# Text is the first p and translation is the second p
# We do not care the remaining <p>, but if there are less than
# two then we simply return
p_list = div.find_all("p")
if len(p_list) < 2:
return None
l.append({
"text": p_list[0].text.strip(),
"translation": p_list[1].text.strip(),
})
# Append the dict here such that if we continue before here
# the changes will not be committed
ret["meanings"].append(d)
#
# Then start to extract word groups
#
word_group_list = []
ret["word-group"] = word_group_list
word_group_div_list = tree.select("#wordGroup")
if len(word_group_div_list) == 0:
dbg_printf("Did not find word group; return empty word group")
else:
word_group_div = word_group_div_list[0]
# This is a list of <p> tags that contains the word group
word_group_p_list = word_group_div.select("p.wordGroup")
word_group_index = -1
for word_group_p in word_group_p_list:
word_group_index += 1
# Search for the <a> tag that contains the text of the word group
a_list = word_group_p.select("a.search-js")
if len(a_list) == 0:
dbg_printf("Did not find word group text (index = %d)",
word_group_index)
continue
text = a_list[0].text
meaning = ""
for content in word_group_p.contents:
if isinstance(content, bs4.element.Tag) is True:
continue
meaning += (" ".join(content.split()) + " ")
meaning = meaning.strip()
if len(meaning) == 0:
dbg_printf("Did not find word group meaning (index = %d)",
word_group_index)
continue
# Finally add an element into the word group list
word_group_list.append(
{"text": text,
"meaning": meaning}
)
# Set the actual key on the web page
if actual_key is None:
actual_key = ret["word"]
ret_list.append(ret)
# Last check to avoid adding a None key into the cache
if actual_key is None:
dbg_printf("Did not find actual key")
return None
# Add the word to the cache
add_to_cache(actual_key, ret_list)
return ret_list
def get_cache_file_list(path):
"""
This function counts the number of json files under a given directory.
To save an system call the path is given as the argument
If the passed path is not a valid directory then the result is undefined
:return: int, as the number of json files
"""
return glob.glob(os.path.join(path, "*.json"))
# The name of the directory under the file directory as the word cache
CACHE_DIRECTORY = "cache"
# The max number of entries we allow for the cache
# When we add to the cache we check this first, and if the actual number of
# json files is greater than this we randomly delete files from the cache
# If this is set to -1 then there is no limit
# If this is set to 0 then cache is disabled
CACHE_MAX_ENTRY = 10
def trim_cache(cache_dir, limit):
"""
Randomly remove cache content under given path until the number of file equals
or is less than the given limit
:param cache_dir: Under which we store cached file
:param limit: The maximum number of entries allowed for the cache
:return: int, the number of files we actually deleted
"""
cache_file_list = get_cache_file_list(cache_dir)
current_cache_size = len(cache_file_list)
deleted_count = 0
if current_cache_size >= limit:
# This is the number of files we need to delete
delta = current_cache_size - limit
# Then do a permutation of the list and pick the first
# "deleted_count" elements to delete
for i in range(0, delta):
exchange_index = randint(0, current_cache_size - 1)
# Then exchange the elements
t = cache_file_list[exchange_index]
cache_file_list[exchange_index] = cache_file_list[i]
cache_file_list[i] = t
for i in range(0, delta):
try:
os.unlink(cache_file_list[i])
except OSError:
# Offset the += 1 later
deleted_count -= 1
deleted_count += 1
return deleted_count
def add_to_cache(word, d):
"""
This function adds a word and its associated dictionary object into the local cache
If this word is queried in the future, it will be served from the cache
If the word is already in the cache we just ignore it
:param word: The word queried
:param d: The dictionary object returned by the parser
:return: None
"""
# If cache is disabled then return directly
if CACHE_MAX_ENTRY == 0:
return
# Also take care of this
if no_add_flag is True:
dbg_printf("no_add_flag is on; do not add to cache")
return
# This is the directory of the current file
file_dir = get_file_dir()
cache_dir = os.path.join(file_dir, CACHE_DIRECTORY)
# If the cache directory has not yet been created then just create it
if os.path.isdir(cache_dir) is False:
os.mkdir(cache_dir)
# Then before we check for the existence of the file, we
# check whether the cache directory is full, and if it is
# randomly choose one and then remove it
# -1 means there is no limit
if CACHE_MAX_ENTRY != -1:
# Since we will add a new entry after this, so the actual limit
# is 1 less than the defined constant
ret = trim_cache(cache_dir, CACHE_MAX_ENTRY - 1)
if ret > 0:
dbg_printf("Deleted %d file(s) from the cache", ret)
# This is the word file
word_file = os.path.join(cache_dir, "%s.json" % (word, ))
# If the file exists then warning
if os.path.isfile(word_file) is True:
dbg_printf("Overwriting cache file for word: %s", word)
fp = open(word_file, "w")
json.dump(d, fp)
fp.close()
return
def check_in_cache(word):
"""
Check whether a word exists in the cache, and if it does then we load from the cache
directly and then display. If not in the cache return None
:param word: The word to be queried
:return: dict/None
"""
# This is the directory of the current file
file_dir = get_file_dir()
cache_dir = os.path.join(file_dir, CACHE_DIRECTORY)
# If the cache directory has not yet been created then just create it
if os.path.isdir(cache_dir) is False:
dbg_printf("Cache directory not valid: %s", cache_dir)
return None
# This is the word file
word_file = os.path.join(cache_dir, "%s.json" % (word, ))
# If the file exists then ignore it
if os.path.isfile(word_file) is False:
dbg_printf("File %s is not valid cached word file", word_file)
return None
fp = open(word_file, "r")
# If we could not decode the json object just remove the invalid
# file and return None
try:
d = json.load(fp)
except ValueError:
print("Invalid JSON object: remove %s" % (word_file, ))
os.unlink(word_file)
fp.close()
return None
fp.close()
return d
RED_TEXT_START = "\033[1;31m"
RED_TEXT_END = "\033[0m"
GREEN_TEXT_START = "\033[1;32m"
GREEN_TEXT_END = "\033[0m"
YELLOW_TEXT_START = "\033[1;33m"
YELLOW_TEXT_END = "\033[0m"
def print_red(text, output_device):
"""
Prints the given text in read fore color
:param text: The text to be printed
:param output_device: Object that supports write() method
:return: None
"""
output_device.write(RED_TEXT_START)
output_device.write(text)
output_device.write(RED_TEXT_END)
return
def print_yellow(text, output_device):
"""
Prints the text in yellow foreground color
:param text: The text to be printed
:param output_device: Object that supports write() method
:return: None
"""
output_device.write(YELLOW_TEXT_START)
output_device.write(text)
output_device.write(YELLOW_TEXT_END)
return
def process_color(s):
"""
Replace color marks in a string with actual color control characters defined
by the terminal
:param s: The input string
:return: str
"""
s = s.replace("<red>", RED_TEXT_START)
s = s.replace("</red>", RED_TEXT_END)
s = s.replace("<green>", GREEN_TEXT_START)
s = s.replace("</green>", GREEN_TEXT_END)
return s
def collins_pretty_print(dict_list, output_device=sys.stdout):
"""
Prints a dict object in pretty form. The input dict object may
be None, in which case we skip printing
:param dict_list: A list of dict objects returned from the parser
Or a dict object implying the alternative words
:param output_device: An output object that supports write() method
for printing or aggregating values
:return: None
"""
global verbose_flag
global m5_flag
if dict_list is None:
return
# If it is the alternative list instead of a word dict list then we
# print out alternatives
if isinstance(dict_list, dict) and \
"alternatives" in dict_list:
output_device.write("The word is not found, but there are are few alternatives: \n")
# Print out each word
for word in dict_list["alternatives"]:
output_device.write(" " + word + "\n")
return
for d in dict_list:
print_red(d["word"], output_device)
output_device.write(" ")
# Write the frequency if it has one
freq = d["frequency"]
if freq != -1:
output_device.write("[%s] " % ("*" * freq, ))
output_device.write(d["phonetic"])
output_device.write("\n")
counter = 1
for meaning in d["meanings"]:
if m5_flag is True and counter == 6:
return
output_device.write("%d. (%s) " % (counter, meaning["category"]))
counter += 1
text = process_color(meaning["text"])
output_device.write(text)
output_device.write("\n")
if verbose_flag is True:
for example in meaning["examples"]:
output_device.write(" - ")
output_device.write(example["text"])
output_device.write("\n")
output_device.write(" ")
output_device.write(example["translation"])
output_device.write("\n")
# If we also print word group then print it
if word_group_flag is True:
output_device.write("\n")
for word_group in d["word-group"]:
print_yellow(word_group["text"], output_device)
output_device.write(" " + word_group["meaning"] + "\n")
return
def get_file_dir():
"""
Returns the directory of the current python file
:return: str
"""
return os.path.dirname(os.path.abspath(__file__))
# This is the file we keep under the same directory as the file
# to record the path that the utility has been installed
PATH_FILE_NAME = "INSTALL_PATH"
def get_path_file_path():
"""
This function opens the path file path (the file that records the installation path)
:return: str
"""
# First check whether the file already exists as a flag to
# indicate previous installation
current_path = get_file_dir()
# This is the abs path for the flag
path_file_path = os.path.join(current_path, PATH_FILE_NAME)
return path_file_path
DEFAULT_INSTALL_DIR = "/usr/local/bin"
INSTALL_FILE_NAME = "define"
def install():
"""
Installs a shortcut as "define" command for the current user. This function
will write a file under the directory of this script for delete to work.
:return: int; 0 if success; Otherwise fail
"""
# This is the path in which the installation information is stored
path_file_path = get_path_file_path()
# Check if it is a directory then something is wrong and installation
# could not proceed
if os.path.isdir(path_file_path) is True:
print("Path %s could not be a directory - installation fail" %
(path_file_path, ))
return 1
elif os.path.isfile(path_file_path) is True:
# Otherwise just read the file and check whether the path
# is still valid
fp = open(path_file_path, "r")
line = fp.read()
fp.close()
# If there is a previous installation then prompt the user to
# uninstall it first
if os.path.isfile(line) is True:
print("Found a previous installation in %s. Please run --uninstall first" %
(line, ))
else:
print("Found an invalid installation in %s. Please manually delete it first" %
(line, ))
return 1
# If there are extra arguments then we use the one after --install command
# as the path to which we install
if len(sys.argv) > 2:
# Need to expand the user and variables like a shell
install_dir = os.path.expandvars(os.path.expanduser(sys.argv[2]))
else:
install_dir = DEFAULT_INSTALL_DIR
# Then make it an absolute path
install_dir = os.path.abspath(install_dir)
# Check whether we have permission to this directory
if os.access(install_dir, os.W_OK) is False:
print("Access to \"%s\" denied. Please try sudo" %
(install_dir, ))
return 1
if os.path.isdir(install_dir) is False:
print("Install path %s is invalid. Please choose a valid one" %
(install_dir, ))
return 1
# Join these two as the path of the file we write into
install_file_path = os.path.join(install_dir, INSTALL_FILE_NAME)
# Check whether there is already an utility with the same name
# Since we already checked installation of this utility before
# then this should be a name conflict rather than another
# installation
if os.path.isfile(install_file_path) is True:
print("There is already a \"define\" at location %s; please check whether it is a name conflict" %
(install_file_path, ))
return 1
# Get the absolute path of this file and write a bash script
current_file = os.path.abspath(__file__)
fp = open(install_file_path, "w")
fp.write("#!/bin/bash\n")
fp.write("python %s $@" % (current_file, ))
fp.close()
# Also usable by other users
os.chmod(install_file_path, stat.S_IRWXO | stat.S_IRWXG | stat.S_IRWXU)
fp = open(path_file_path, "w")
fp.write(install_file_path)
fp.close()
print("Install successful")
return 0
def uninstall():
"""
This function uninstalls the "define" utility. We use the path file to
find the location we have previously installed the utility
:return: int
"""
# This is the path to the path file
path_file_path = get_path_file_path()
if os.path.isdir(path_file_path) is True:
print("Path %s could not be a directory - please manually delete it" %
(path_file_path, ))
return 1
elif os.path.isfile(path_file_path) is False:
print("Did not find a previous installation. To install please run with --install option")
return 1
# Open the file, read the first line and delete the utility
fp = open(path_file_path, "r")
line = fp.read()
fp.close()
# If the path recorded in the file is invalid then prompt the user to manually
# remove it
if os.path.isfile(line) is False:
print("Found an invalid installation in %s. Please manually delete %s" %
(line, INSTALL_FILE_NAME, ))
return 1
# This is the installation directory
install_dir = os.path.dirname(line)
# Then check for permissions of the containing directory
if os.access(install_dir, os.W_OK) is False:
print("Access to \"%s\" denied. Please try sudo" %
(install_dir, ))
return 1
# Try to remove the file and catch any exception thrown
# by the routine
current_file = None
try:
current_file = line
os.unlink(current_file)
current_file = path_file_path
os.unlink(current_file)
except OSError:
print("Fail to remove file: %s" %
(current_file, ))
return 1
print("Uninstall successful")
return 0
def cmd_trim_cache():
"""
This function processes the command line argument --trim-cache, and internally
calls trim_cache() to randomly delete entries from the cache
:return: None
"""
if len(sys.argv) == 2:
limit = 0
else:
try:
# Use the third argument as the limit and try to convert it
# from a string to integer
limit = int(sys.argv[2])
except ValueError:
print("Invalid limit \"%s\". Please specify the correct limit." %
(sys.argv[2], ))
return 1
if limit < 0:
print("Invalid limit: %d. Please specify a positive integer" %
(limit, ))
return 1
ret = trim_cache(os.path.join(get_file_dir(), CACHE_DIRECTORY), limit)
print("Deleted %d entry/-ies" % (ret, ))
return 0
def cmd_ls_cache():
"""
This function prints cache entries. One word per line
:return: None
"""
cache_file_list = get_cache_file_list(os.path.join(get_file_dir(), CACHE_DIRECTORY))
for name in cache_file_list:
base_name = os.path.splitext(os.path.basename(name))[0]
print(base_name)
return 0
def cmd_ls_define():
"""
This function prints the current directory we install the utility
If the utility is not installed or if the installation is invalid nothing is printed.
:return: None
"""
path_file_path = get_path_file_path()
if os.path.isfile(path_file_path) is False:
return 1
# This is the location where we install the utility
fp = open(path_file_path, "r")
line = fp.read()
fp.close()
# If the installation is invalid also return
if os.path.isfile(line) is False:
return 1
print(line)
return 0
#####################################################################
# The following implements interactive mode
#####################################################################
class InterfaceError(BaseException):
"""
This is the error thrown for any unhandled error within the interface
"""
def __init__(self, message):
"""
Initialize the exception object
:param message: A string message
"""
self.message = message
return
def __str__(self):
"""
Extracts the string message of the exception
:return: str
"""
return self.message
__repr__ = __str__
def interactive_mode():
"""
This function initializes interactive mode and functions as the dispatching
engine for the event driven curses library
It only exists when the mode is exited
:return: None
"""
# Import it here to avoid extra overhead even if we do not use interactive mode
import curses
class TextArea:
"""
This class defines a text area that could:
(1) Hold multi-line text
(2) Adjust the range of rows displayed according to the actual dimension
of the text area (vertical)
(3) Automatic wrap-back of rows if text area row length is less than the
actual line length (horizontal)
"""
def __init__(self, context, row_num, col_num, start_row, start_col):
"""
Initialize the dimension of the text area. We need its size and absolute position
on the screen, which should be within the screen
:param context: The context object
:param row_num: The height of the control
:param col_num: The width of the control
:param start_row: Absolute row of the text area on the screen
:param start_col: Absolute column of the text area on the screen
"""
# Check rows and cols to make sure it is inside the screen
if row_num + start_row >= context.get_screen_row_num():
raise InterfaceError("TextArea rows out of screen")
elif col_num + start_col >= context.get_screen_col_num():
raise InterfaceError("TextArea columns out of screen")
self.context = context
self.row_num = row_num
self.col_num = col_num
self.start_row = start_row
self.start_col = start_col
# This is a list of pages that can be displayed at a time
# Its content is another list which contains lines (i.e. the line list)
self.page_list = [[]]
return
@staticmethod
def get_char_width(ch):
"""
This function returns the width of a character, supporting unicode.
Our rule is that, for characters within 0 - 255 we always treat it as