nltk 3.2__zip → 3.3__zip

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (406) hide show
  1. {nltk-3.2 → nltk-3.3}/INSTALL.txt +6 -6
  2. {nltk-3.2 → nltk-3.3}/LICENSE.txt +13 -13
  3. {nltk-3.2 → nltk-3.3}/MANIFEST.in +6 -6
  4. {nltk-3.2/nltk.egg-info → nltk-3.3}/PKG-INFO +13 -5
  5. nltk-3.2/README.txt → nltk-3.3/README.md +49 -48
  6. nltk-3.3/nltk/VERSION +1 -0
  7. {nltk-3.2 → nltk-3.3}/nltk/__init__.py +185 -185
  8. {nltk-3.2 → nltk-3.3}/nltk/app/__init__.py +52 -53
  9. {nltk-3.2 → nltk-3.3}/nltk/app/chartparser_app.py +2276 -2276
  10. {nltk-3.2 → nltk-3.3}/nltk/app/chunkparser_app.py +1262 -1264
  11. {nltk-3.2 → nltk-3.3}/nltk/app/collocations_app.py +347 -350
  12. {nltk-3.2 → nltk-3.3}/nltk/app/concordance_app.py +568 -570
  13. {nltk-3.2 → nltk-3.3}/nltk/app/nemo_app.py +156 -155
  14. {nltk-3.2 → nltk-3.3}/nltk/app/rdparser_app.py +892 -894
  15. {nltk-3.2 → nltk-3.3}/nltk/app/srparser_app.py +809 -809
  16. {nltk-3.2 → nltk-3.3}/nltk/app/wordfreq_app.py +32 -32
  17. {nltk-3.2 → nltk-3.3}/nltk/app/wordnet_app.py +968 -968
  18. {nltk-3.2 → nltk-3.3}/nltk/book.py +91 -90
  19. {nltk-3.2 → nltk-3.3}/nltk/ccg/__init__.py +22 -22
  20. {nltk-3.2 → nltk-3.3}/nltk/ccg/api.py +359 -335
  21. {nltk-3.2 → nltk-3.3}/nltk/ccg/chart.py +394 -361
  22. {nltk-3.2 → nltk-3.3}/nltk/ccg/combinator.py +343 -312
  23. {nltk-3.2 → nltk-3.3}/nltk/ccg/lexicon.py +328 -290
  24. nltk-3.3/nltk/ccg/logic.py +46 -0
  25. {nltk-3.2 → nltk-3.3}/nltk/chat/__init__.py +49 -49
  26. {nltk-3.2 → nltk-3.3}/nltk/chat/eliza.py +244 -244
  27. {nltk-3.2 → nltk-3.3}/nltk/chat/iesha.py +140 -140
  28. {nltk-3.2 → nltk-3.3}/nltk/chat/rude.py +92 -92
  29. {nltk-3.2 → nltk-3.3}/nltk/chat/suntsu.py +117 -117
  30. {nltk-3.2 → nltk-3.3}/nltk/chat/util.py +122 -120
  31. {nltk-3.2 → nltk-3.3}/nltk/chat/zen.py +282 -282
  32. {nltk-3.2 → nltk-3.3}/nltk/chunk/__init__.py +190 -190
  33. {nltk-3.2 → nltk-3.3}/nltk/chunk/api.py +51 -51
  34. {nltk-3.2 → nltk-3.3}/nltk/chunk/named_entity.py +331 -331
  35. {nltk-3.2 → nltk-3.3}/nltk/chunk/regexp.py +1390 -1384
  36. {nltk-3.2 → nltk-3.3}/nltk/chunk/util.py +600 -600
  37. {nltk-3.2 → nltk-3.3}/nltk/classify/__init__.py +98 -98
  38. {nltk-3.2 → nltk-3.3}/nltk/classify/api.py +193 -193
  39. {nltk-3.2 → nltk-3.3}/nltk/classify/decisiontree.py +295 -295
  40. {nltk-3.2 → nltk-3.3}/nltk/classify/maxent.py +1510 -1492
  41. {nltk-3.2 → nltk-3.3}/nltk/classify/megam.py +179 -178
  42. {nltk-3.2 → nltk-3.3}/nltk/classify/naivebayes.py +245 -242
  43. {nltk-3.2 → nltk-3.3}/nltk/classify/positivenaivebayes.py +170 -170
  44. {nltk-3.2 → nltk-3.3}/nltk/classify/rte_classify.py +160 -191
  45. {nltk-3.2 → nltk-3.3}/nltk/classify/scikitlearn.py +153 -151
  46. {nltk-3.2 → nltk-3.3}/nltk/classify/senna.py +183 -181
  47. {nltk-3.2 → nltk-3.3}/nltk/classify/svm.py +15 -15
  48. {nltk-3.2 → nltk-3.3}/nltk/classify/tadm.py +113 -112
  49. {nltk-3.2 → nltk-3.3}/nltk/classify/textcat.py +193 -193
  50. {nltk-3.2 → nltk-3.3}/nltk/classify/util.py +324 -312
  51. {nltk-3.2 → nltk-3.3}/nltk/classify/weka.py +346 -345
  52. {nltk-3.2 → nltk-3.3}/nltk/cluster/__init__.py +86 -86
  53. {nltk-3.2 → nltk-3.3}/nltk/cluster/api.py +74 -70
  54. {nltk-3.2 → nltk-3.3}/nltk/cluster/em.py +247 -247
  55. {nltk-3.2 → nltk-3.3}/nltk/cluster/gaac.py +168 -168
  56. {nltk-3.2 → nltk-3.3}/nltk/cluster/kmeans.py +221 -221
  57. {nltk-3.2 → nltk-3.3}/nltk/cluster/util.py +304 -289
  58. nltk-3.3/nltk/collections.py +640 -0
  59. {nltk-3.2 → nltk-3.3}/nltk/collocations.py +388 -388
  60. nltk-3.3/nltk/compat.py +365 -0
  61. {nltk-3.2 → nltk-3.3}/nltk/corpus/__init__.py +341 -320
  62. {nltk-3.2 → nltk-3.3}/nltk/corpus/europarl_raw.py +44 -44
  63. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/__init__.py +147 -145
  64. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/aligned.py +115 -114
  65. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/api.py +448 -446
  66. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/bnc.py +252 -252
  67. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/bracket_parse.py +227 -227
  68. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/categorized_sents.py +179 -178
  69. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/chasen.py +140 -139
  70. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/childes.py +522 -494
  71. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/chunked.py +212 -212
  72. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/cmudict.py +97 -95
  73. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/comparative_sents.py +280 -278
  74. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/conll.py +528 -521
  75. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/crubadan.py +116 -116
  76. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/dependency.py +101 -101
  77. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/framenet.py +3059 -2093
  78. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/ieer.py +112 -111
  79. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/indian.py +85 -86
  80. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/ipipan.py +332 -331
  81. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/knbc.py +187 -187
  82. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/lin.py +156 -156
  83. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/mte.py +311 -310
  84. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/nkjp.py +429 -428
  85. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/nombank.py +421 -421
  86. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/nps_chat.py +73 -73
  87. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/opinion_lexicon.py +115 -115
  88. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/panlex_lite.py +165 -165
  89. nltk-3.3/nltk/corpus/reader/pl196x.py +292 -0
  90. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/plaintext.py +232 -233
  91. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/ppattach.py +96 -95
  92. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/propbank.py +479 -481
  93. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/pros_cons.py +128 -126
  94. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/reviews.py +330 -328
  95. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/rte.py +144 -146
  96. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/semcor.py +256 -256
  97. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/senseval.py +203 -201
  98. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/sentiwordnet.py +137 -137
  99. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/sinica_treebank.py +75 -75
  100. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/string_category.py +60 -59
  101. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/switchboard.py +119 -119
  102. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/tagged.py +295 -294
  103. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/timit.py +452 -450
  104. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/toolbox.py +68 -68
  105. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/twitter.py +155 -157
  106. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/udhr.py +80 -80
  107. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/util.py +804 -800
  108. nltk-3.3/nltk/corpus/reader/verbnet.py +595 -0
  109. nltk-3.3/nltk/corpus/reader/wordlist.py +134 -0
  110. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/wordnet.py +2108 -2036
  111. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/xmldocs.py +392 -387
  112. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/ycoe.py +243 -242
  113. {nltk-3.2 → nltk-3.3}/nltk/corpus/util.py +144 -127
  114. {nltk-3.2 → nltk-3.3}/nltk/data.py +1487 -1451
  115. {nltk-3.2 → nltk-3.3}/nltk/decorators.py +219 -215
  116. {nltk-3.2 → nltk-3.3}/nltk/downloader.py +2278 -2274
  117. {nltk-3.2 → nltk-3.3}/nltk/draw/__init__.py +27 -27
  118. {nltk-3.2 → nltk-3.3}/nltk/draw/cfg.py +774 -777
  119. {nltk-3.2 → nltk-3.3}/nltk/draw/dispersion.py +58 -58
  120. {nltk-3.2 → nltk-3.3}/nltk/draw/table.py +1098 -1098
  121. {nltk-3.2 → nltk-3.3}/nltk/draw/tree.py +963 -966
  122. {nltk-3.2 → nltk-3.3}/nltk/draw/util.py +2356 -2356
  123. {nltk-3.2 → nltk-3.3}/nltk/featstruct.py +2617 -2500
  124. {nltk-3.2 → nltk-3.3}/nltk/grammar.py +1559 -1516
  125. {nltk-3.2 → nltk-3.3}/nltk/help.py +56 -56
  126. {nltk-3.2 → nltk-3.3}/nltk/inference/__init__.py +20 -20
  127. {nltk-3.2 → nltk-3.3}/nltk/inference/api.py +597 -589
  128. {nltk-3.2 → nltk-3.3}/nltk/inference/discourse.py +612 -608
  129. {nltk-3.2 → nltk-3.3}/nltk/inference/mace.py +311 -311
  130. {nltk-3.2 → nltk-3.3}/nltk/inference/nonmonotonic.py +509 -509
  131. {nltk-3.2 → nltk-3.3}/nltk/inference/prover9.py +431 -431
  132. {nltk-3.2 → nltk-3.3}/nltk/inference/resolution.py +687 -687
  133. {nltk-3.2 → nltk-3.3}/nltk/inference/tableau.py +607 -607
  134. {nltk-3.2 → nltk-3.3}/nltk/internals.py +982 -982
  135. {nltk-3.2 → nltk-3.3}/nltk/jsontags.py +63 -63
  136. {nltk-3.2 → nltk-3.3}/nltk/lazyimport.py +142 -142
  137. {nltk-3.2 → nltk-3.3}/nltk/metrics/__init__.py +30 -29
  138. {nltk-3.2 → nltk-3.3}/nltk/metrics/agreement.py +421 -401
  139. nltk-3.3/nltk/metrics/aline.py +607 -0
  140. {nltk-3.2 → nltk-3.3}/nltk/metrics/association.py +414 -410
  141. {nltk-3.2 → nltk-3.3}/nltk/metrics/confusionmatrix.py +206 -206
  142. {nltk-3.2 → nltk-3.3}/nltk/metrics/distance.py +207 -202
  143. {nltk-3.2 → nltk-3.3}/nltk/metrics/paice.py +381 -381
  144. {nltk-3.2 → nltk-3.3}/nltk/metrics/scores.py +228 -228
  145. {nltk-3.2 → nltk-3.3}/nltk/metrics/segmentation.py +231 -232
  146. {nltk-3.2 → nltk-3.3}/nltk/metrics/spearman.py +69 -69
  147. {nltk-3.2 → nltk-3.3}/nltk/misc/__init__.py +11 -11
  148. {nltk-3.2 → nltk-3.3}/nltk/misc/babelfish.py +10 -10
  149. {nltk-3.2 → nltk-3.3}/nltk/misc/chomsky.py +135 -133
  150. {nltk-3.2 → nltk-3.3}/nltk/misc/minimalset.py +83 -83
  151. {nltk-3.2 → nltk-3.3}/nltk/misc/sort.py +157 -157
  152. {nltk-3.2 → nltk-3.3}/nltk/misc/wordfinder.py +129 -129
  153. {nltk-3.2 → nltk-3.3}/nltk/parse/__init__.py +81 -80
  154. {nltk-3.2 → nltk-3.3}/nltk/parse/api.py +66 -66
  155. {nltk-3.2 → nltk-3.3}/nltk/parse/bllip.py +282 -282
  156. {nltk-3.2 → nltk-3.3}/nltk/parse/chart.py +1682 -1681
  157. nltk-3.3/nltk/parse/corenlp.py +780 -0
  158. {nltk-3.2 → nltk-3.3}/nltk/parse/dependencygraph.py +760 -758
  159. {nltk-3.2 → nltk-3.3}/nltk/parse/earleychart.py +452 -451
  160. {nltk-3.2 → nltk-3.3}/nltk/parse/evaluate.py +129 -129
  161. {nltk-3.2 → nltk-3.3}/nltk/parse/featurechart.py +580 -578
  162. {nltk-3.2 → nltk-3.3}/nltk/parse/generate.py +87 -76
  163. {nltk-3.2 → nltk-3.3}/nltk/parse/malt.py +356 -356
  164. {nltk-3.2 → nltk-3.3}/nltk/parse/nonprojectivedependencyparser.py +783 -783
  165. {nltk-3.2 → nltk-3.3}/nltk/parse/pchart.py +522 -522
  166. {nltk-3.2 → nltk-3.3}/nltk/parse/projectivedependencyparser.py +579 -578
  167. {nltk-3.2 → nltk-3.3}/nltk/parse/recursivedescent.py +655 -656
  168. {nltk-3.2 → nltk-3.3}/nltk/parse/shiftreduce.py +458 -459
  169. {nltk-3.2 → nltk-3.3}/nltk/parse/stanford.py +431 -406
  170. {nltk-3.2 → nltk-3.3}/nltk/parse/transitionparser.py +774 -772
  171. {nltk-3.2 → nltk-3.3}/nltk/parse/util.py +231 -231
  172. {nltk-3.2 → nltk-3.3}/nltk/parse/viterbi.py +401 -401
  173. {nltk-3.2 → nltk-3.3}/nltk/probability.py +2410 -2355
  174. {nltk-3.2 → nltk-3.3}/nltk/sem/__init__.py +61 -61
  175. {nltk-3.2 → nltk-3.3}/nltk/sem/boxer.py +1261 -1261
  176. {nltk-3.2 → nltk-3.3}/nltk/sem/chat80.py +782 -782
  177. {nltk-3.2 → nltk-3.3}/nltk/sem/cooper_storage.py +118 -118
  178. {nltk-3.2 → nltk-3.3}/nltk/sem/drt.py +1258 -1257
  179. {nltk-3.2 → nltk-3.3}/nltk/sem/drt_glue_demo.py +485 -489
  180. {nltk-3.2 → nltk-3.3}/nltk/sem/evaluate.py +790 -784
  181. {nltk-3.2 → nltk-3.3}/nltk/sem/glue.py +696 -694
  182. {nltk-3.2 → nltk-3.3}/nltk/sem/hole.py +386 -384
  183. {nltk-3.2 → nltk-3.3}/nltk/sem/lfg.py +210 -208
  184. {nltk-3.2 → nltk-3.3}/nltk/sem/linearlogic.py +451 -449
  185. {nltk-3.2 → nltk-3.3}/nltk/sem/logic.py +1910 -1910
  186. {nltk-3.2 → nltk-3.3}/nltk/sem/relextract.py +476 -483
  187. {nltk-3.2 → nltk-3.3}/nltk/sem/skolemize.py +101 -101
  188. {nltk-3.2 → nltk-3.3}/nltk/sem/util.py +249 -249
  189. {nltk-3.2 → nltk-3.3}/nltk/sentiment/__init__.py +14 -14
  190. {nltk-3.2 → nltk-3.3}/nltk/sentiment/sentiment_analyzer.py +229 -229
  191. {nltk-3.2 → nltk-3.3}/nltk/sentiment/util.py +762 -752
  192. {nltk-3.2 → nltk-3.3}/nltk/sentiment/vader.py +443 -445
  193. {nltk-3.2 → nltk-3.3}/nltk/stem/__init__.py +31 -33
  194. {nltk-3.2 → nltk-3.3}/nltk/stem/api.py +28 -25
  195. nltk-3.3/nltk/stem/arlstem.py +369 -0
  196. {nltk-3.2 → nltk-3.3}/nltk/stem/isri.py +345 -345
  197. {nltk-3.2 → nltk-3.3}/nltk/stem/lancaster.py +337 -310
  198. nltk-3.3/nltk/stem/porter.py +710 -0
  199. {nltk-3.2 → nltk-3.3}/nltk/stem/regexp.py +61 -61
  200. {nltk-3.2 → nltk-3.3}/nltk/stem/rslp.py +142 -142
  201. {nltk-3.2 → nltk-3.3}/nltk/stem/snowball.py +4236 -3712
  202. {nltk-3.2 → nltk-3.3}/nltk/stem/util.py +22 -12
  203. {nltk-3.2 → nltk-3.3}/nltk/stem/wordnet.py +51 -51
  204. {nltk-3.2 → nltk-3.3}/nltk/tag/__init__.py +152 -127
  205. {nltk-3.2 → nltk-3.3}/nltk/tag/api.py +86 -81
  206. {nltk-3.2 → nltk-3.3}/nltk/tag/brill.py +424 -426
  207. {nltk-3.2 → nltk-3.3}/nltk/tag/brill_trainer.py +608 -608
  208. {nltk-3.2 → nltk-3.3}/nltk/tag/crf.py +204 -200
  209. {nltk-3.2 → nltk-3.3}/nltk/tag/hmm.py +1277 -1277
  210. {nltk-3.2 → nltk-3.3}/nltk/tag/hunpos.py +132 -132
  211. {nltk-3.2 → nltk-3.3}/nltk/tag/mapping.py +101 -101
  212. {nltk-3.2 → nltk-3.3}/nltk/tag/perceptron.py +329 -314
  213. {nltk-3.2 → nltk-3.3}/nltk/tag/senna.py +147 -145
  214. {nltk-3.2 → nltk-3.3}/nltk/tag/sequential.py +740 -736
  215. {nltk-3.2 → nltk-3.3}/nltk/tag/stanford.py +219 -196
  216. {nltk-3.2 → nltk-3.3}/nltk/tag/tnt.py +595 -604
  217. {nltk-3.2 → nltk-3.3}/nltk/tag/util.py +72 -72
  218. {nltk-3.2 → nltk-3.3}/nltk/tbl/__init__.py +28 -28
  219. {nltk-3.2 → nltk-3.3}/nltk/tbl/api.py +1 -1
  220. {nltk-3.2 → nltk-3.3}/nltk/tbl/demo.py +366 -366
  221. {nltk-3.2 → nltk-3.3}/nltk/tbl/erroranalysis.py +39 -39
  222. {nltk-3.2 → nltk-3.3}/nltk/tbl/feature.py +259 -261
  223. {nltk-3.2 → nltk-3.3}/nltk/tbl/rule.py +321 -321
  224. {nltk-3.2 → nltk-3.3}/nltk/tbl/template.py +303 -308
  225. {nltk-3.2 → nltk-3.3}/nltk/test/__init__.py +18 -18
  226. {nltk-3.2 → nltk-3.3}/nltk/test/all.py +24 -23
  227. {nltk-3.2 → nltk-3.3}/nltk/test/bleu.doctest +14 -14
  228. {nltk-3.2 → nltk-3.3}/nltk/test/bnc.doctest +60 -60
  229. {nltk-3.2 → nltk-3.3}/nltk/test/ccg.doctest +376 -395
  230. nltk-3.3/nltk/test/ccg_semantics.doctest +553 -0
  231. {nltk-3.2 → nltk-3.3}/nltk/test/chat80.doctest +234 -234
  232. {nltk-3.2 → nltk-3.3}/nltk/test/childes.doctest +184 -184
  233. {nltk-3.2 → nltk-3.3}/nltk/test/childes_fixt.py +14 -13
  234. {nltk-3.2 → nltk-3.3}/nltk/test/chunk.doctest +373 -373
  235. {nltk-3.2 → nltk-3.3}/nltk/test/classify.doctest +185 -185
  236. {nltk-3.2 → nltk-3.3}/nltk/test/classify_fixt.py +11 -10
  237. nltk-3.3/nltk/test/collections.doctest +20 -0
  238. {nltk-3.2 → nltk-3.3}/nltk/test/collocations.doctest +276 -276
  239. {nltk-3.2 → nltk-3.3}/nltk/test/compat.doctest +134 -138
  240. {nltk-3.2 → nltk-3.3}/nltk/test/compat_fixt.py +9 -8
  241. nltk-3.3/nltk/test/concordance.doctest +55 -0
  242. {nltk-3.2 → nltk-3.3}/nltk/test/corpus.doctest +2200 -2212
  243. {nltk-3.2 → nltk-3.3}/nltk/test/corpus_fixt.py +3 -3
  244. {nltk-3.2 → nltk-3.3}/nltk/test/crubadan.doctest +65 -65
  245. {nltk-3.2 → nltk-3.3}/nltk/test/data.doctest +379 -379
  246. {nltk-3.2 → nltk-3.3}/nltk/test/dependency.doctest +241 -241
  247. {nltk-3.2 → nltk-3.3}/nltk/test/discourse.doctest +546 -546
  248. {nltk-3.2 → nltk-3.3}/nltk/test/discourse_fixt.py +14 -13
  249. {nltk-3.2 → nltk-3.3}/nltk/test/doctest_nose_plugin.py +159 -156
  250. {nltk-3.2 → nltk-3.3}/nltk/test/drt.doctest +517 -517
  251. {nltk-3.2 → nltk-3.3}/nltk/test/featgram.doctest +607 -607
  252. {nltk-3.2 → nltk-3.3}/nltk/test/featstruct.doctest +1229 -1230
  253. {nltk-3.2 → nltk-3.3}/nltk/test/framenet.doctest +288 -239
  254. {nltk-3.2 → nltk-3.3}/nltk/test/generate.doctest +67 -67
  255. {nltk-3.2 → nltk-3.3}/nltk/test/gensim.doctest +140 -141
  256. {nltk-3.2 → nltk-3.3}/nltk/test/gensim_fixt.py +10 -9
  257. {nltk-3.2 → nltk-3.3}/nltk/test/gluesemantics.doctest +384 -386
  258. {nltk-3.2 → nltk-3.3}/nltk/test/gluesemantics_malt.doctest +68 -68
  259. {nltk-3.2 → nltk-3.3}/nltk/test/gluesemantics_malt_fixt.py +12 -11
  260. {nltk-3.2 → nltk-3.3}/nltk/test/grammar.doctest +48 -48
  261. {nltk-3.2 → nltk-3.3}/nltk/test/grammartestsuites.doctest +109 -109
  262. {nltk-3.2 → nltk-3.3}/nltk/test/index.doctest +100 -100
  263. {nltk-3.2 → nltk-3.3}/nltk/test/inference.doctest +534 -534
  264. {nltk-3.2 → nltk-3.3}/nltk/test/inference_fixt.py +12 -11
  265. {nltk-3.2 → nltk-3.3}/nltk/test/internals.doctest +140 -140
  266. {nltk-3.2 → nltk-3.3}/nltk/test/japanese.doctest +48 -48
  267. {nltk-3.2 → nltk-3.3}/nltk/test/logic.doctest +1098 -1098
  268. {nltk-3.2 → nltk-3.3}/nltk/test/metrics.doctest +270 -270
  269. {nltk-3.2 → nltk-3.3}/nltk/test/misc.doctest +118 -118
  270. {nltk-3.2 → nltk-3.3}/nltk/test/nonmonotonic.doctest +286 -286
  271. {nltk-3.2 → nltk-3.3}/nltk/test/nonmonotonic_fixt.py +12 -11
  272. {nltk-3.2 → nltk-3.3}/nltk/test/paice.doctest +35 -35
  273. {nltk-3.2 → nltk-3.3}/nltk/test/parse.doctest +884 -884
  274. {nltk-3.2 → nltk-3.3}/nltk/test/portuguese_en.doctest +565 -565
  275. {nltk-3.2 → nltk-3.3}/nltk/test/portuguese_en_fixt.py +16 -16
  276. {nltk-3.2 → nltk-3.3}/nltk/test/probability.doctest +304 -304
  277. {nltk-3.2 → nltk-3.3}/nltk/test/probability_fixt.py +13 -12
  278. {nltk-3.2 → nltk-3.3}/nltk/test/propbank.doctest +176 -176
  279. {nltk-3.2 → nltk-3.3}/nltk/test/relextract.doctest +263 -263
  280. {nltk-3.2 → nltk-3.3}/nltk/test/resolution.doctest +221 -221
  281. {nltk-3.2 → nltk-3.3}/nltk/test/runtests.py +86 -64
  282. {nltk-3.2 → nltk-3.3}/nltk/test/segmentation_fixt.py +10 -10
  283. {nltk-3.2 → nltk-3.3}/nltk/test/semantics.doctest +665 -665
  284. {nltk-3.2 → nltk-3.3}/nltk/test/semantics_fixt.py +7 -7
  285. {nltk-3.2 → nltk-3.3}/nltk/test/sentiment.doctest +233 -233
  286. {nltk-3.2 → nltk-3.3}/nltk/test/sentiwordnet.doctest +41 -41
  287. {nltk-3.2 → nltk-3.3}/nltk/test/simple.doctest +85 -85
  288. {nltk-3.2 → nltk-3.3}/nltk/test/stem.doctest +78 -78
  289. {nltk-3.2 → nltk-3.3}/nltk/test/tag.doctest +33 -33
  290. {nltk-3.2 → nltk-3.3}/nltk/test/tokenize.doctest +273 -166
  291. {nltk-3.2 → nltk-3.3}/nltk/test/toolbox.doctest +307 -307
  292. {nltk-3.2 → nltk-3.3}/nltk/test/translate.doctest +242 -242
  293. {nltk-3.2 → nltk-3.3}/nltk/test/translate_fixt.py +3 -3
  294. {nltk-3.2 → nltk-3.3}/nltk/test/tree.doctest +1101 -1101
  295. {nltk-3.2 → nltk-3.3}/nltk/test/treeprettyprinter.doctest +127 -127
  296. {nltk-3.2 → nltk-3.3}/nltk/test/treetransforms.doctest +156 -156
  297. nltk-3.3/nltk/test/unit/test_2x_compat.py +63 -0
  298. nltk-3.3/nltk/test/unit/test_aline.py +52 -0
  299. nltk-3.3/nltk/test/unit/test_chunk.py +56 -0
  300. {nltk-3.2 → nltk-3.3}/nltk/test/unit/test_classify.py +55 -53
  301. {nltk-3.2 → nltk-3.3}/nltk/test/unit/test_collocations.py +88 -79
  302. nltk-3.3/nltk/test/unit/test_concordance.py +98 -0
  303. nltk-3.3/nltk/test/unit/test_corenlp.py +1000 -0
  304. {nltk-3.2 → nltk-3.3}/nltk/test/unit/test_corpora.py +200 -183
  305. {nltk-3.2 → nltk-3.3}/nltk/test/unit/test_corpus_views.py +45 -45
  306. {nltk-3.2 → nltk-3.3}/nltk/test/unit/test_hmm.py +88 -88
  307. {nltk-3.2 → nltk-3.3}/nltk/test/unit/test_json2csv_corpus.py +188 -187
  308. {nltk-3.2 → nltk-3.3}/nltk/test/unit/test_naivebayes.py +26 -26
  309. nltk-3.3/nltk/test/unit/test_rte_classify.py +86 -0
  310. {nltk-3.2 → nltk-3.3}/nltk/test/unit/test_seekable_unicode_stream_reader.py +124 -124
  311. nltk-3.3/nltk/test/unit/test_senna.py +74 -0
  312. nltk-3.3/nltk/test/unit/test_stem.py +121 -0
  313. {nltk-3.2 → nltk-3.3}/nltk/test/unit/test_tag.py +20 -20
  314. {nltk-3.2 → nltk-3.3}/nltk/test/unit/test_tgrep.py +628 -626
  315. nltk-3.3/nltk/test/unit/test_tokenize.py +168 -0
  316. {nltk-3.2 → nltk-3.3}/nltk/test/unit/test_twitter_auth.py +185 -185
  317. nltk-3.3/nltk/test/unit/test_wordnet.py +134 -0
  318. nltk-3.3/nltk/test/unit/translate/test_bleu.py +244 -0
  319. nltk-3.3/nltk/test/unit/translate/test_gdfa.py +57 -0
  320. {nltk-3.2 → nltk-3.3}/nltk/test/unit/translate/test_ibm1.py +75 -75
  321. {nltk-3.2 → nltk-3.3}/nltk/test/unit/translate/test_ibm2.py +87 -87
  322. {nltk-3.2 → nltk-3.3}/nltk/test/unit/translate/test_ibm3.py +105 -105
  323. {nltk-3.2 → nltk-3.3}/nltk/test/unit/translate/test_ibm4.py +126 -126
  324. {nltk-3.2 → nltk-3.3}/nltk/test/unit/translate/test_ibm5.py +166 -166
  325. {nltk-3.2 → nltk-3.3}/nltk/test/unit/translate/test_ibm_model.py +270 -270
  326. nltk-3.3/nltk/test/unit/translate/test_nist.py +37 -0
  327. {nltk-3.2 → nltk-3.3}/nltk/test/unit/translate/test_stack_decoder.py +300 -300
  328. {nltk-3.2 → nltk-3.3}/nltk/test/unit/utils.py +41 -41
  329. {nltk-3.2 → nltk-3.3}/nltk/test/util.doctest +48 -48
  330. {nltk-3.2 → nltk-3.3}/nltk/test/wordnet.doctest +595 -581
  331. {nltk-3.2 → nltk-3.3}/nltk/test/wordnet_fixt.py +7 -6
  332. {nltk-3.2 → nltk-3.3}/nltk/test/wordnet_lch.doctest +53 -53
  333. {nltk-3.2 → nltk-3.3}/nltk/test/wsd.doctest +68 -68
  334. {nltk-3.2 → nltk-3.3}/nltk/text.py +663 -610
  335. {nltk-3.2 → nltk-3.3}/nltk/tgrep.py +938 -937
  336. {nltk-3.2 → nltk-3.3}/nltk/tokenize/__init__.py +130 -107
  337. {nltk-3.2 → nltk-3.3}/nltk/tokenize/api.py +77 -78
  338. {nltk-3.2 → nltk-3.3}/nltk/tokenize/casual.py +343 -342
  339. {nltk-3.2 → nltk-3.3}/nltk/tokenize/mwe.py +120 -120
  340. nltk-3.3/nltk/tokenize/nist.py +173 -0
  341. {nltk-3.2 → nltk-3.3}/nltk/tokenize/punkt.py +1620 -1601
  342. {nltk-3.2 → nltk-3.3}/nltk/tokenize/regexp.py +208 -202
  343. nltk-3.3/nltk/tokenize/repp.py +151 -0
  344. {nltk-3.2 → nltk-3.3}/nltk/tokenize/sexpr.py +140 -142
  345. {nltk-3.2 → nltk-3.3}/nltk/tokenize/simple.py +138 -136
  346. {nltk-3.2 → nltk-3.3}/nltk/tokenize/stanford.py +117 -113
  347. nltk-3.3/nltk/tokenize/stanford_segmenter.py +246 -0
  348. {nltk-3.2 → nltk-3.3}/nltk/tokenize/texttiling.py +458 -460
  349. nltk-3.3/nltk/tokenize/toktok.py +155 -0
  350. nltk-3.3/nltk/tokenize/treebank.py +363 -0
  351. nltk-3.3/nltk/tokenize/util.py +261 -0
  352. {nltk-3.2 → nltk-3.3}/nltk/toolbox.py +499 -497
  353. {nltk-3.2 → nltk-3.3}/nltk/translate/__init__.py +24 -24
  354. {nltk-3.2 → nltk-3.3}/nltk/translate/api.py +331 -321
  355. {nltk-3.2 → nltk-3.3}/nltk/translate/bleu_score.py +615 -556
  356. nltk-3.3/nltk/translate/chrf_score.py +137 -0
  357. {nltk-3.2 → nltk-3.3}/nltk/translate/gale_church.py +248 -235
  358. {nltk-3.2 → nltk-3.3}/nltk/translate/gdfa.py +135 -131
  359. nltk-3.3/nltk/translate/gleu_score.py +193 -0
  360. {nltk-3.2 → nltk-3.3}/nltk/translate/ibm1.py +250 -241
  361. {nltk-3.2 → nltk-3.3}/nltk/translate/ibm2.py +308 -308
  362. {nltk-3.2 → nltk-3.3}/nltk/translate/ibm3.py +337 -337
  363. {nltk-3.2 → nltk-3.3}/nltk/translate/ibm4.py +467 -467
  364. {nltk-3.2 → nltk-3.3}/nltk/translate/ibm5.py +639 -639
  365. {nltk-3.2 → nltk-3.3}/nltk/translate/ibm_model.py +536 -533
  366. {nltk-3.2 → nltk-3.3}/nltk/translate/metrics.py +40 -40
  367. nltk-3.3/nltk/translate/nist_score.py +178 -0
  368. {nltk-3.2 → nltk-3.3}/nltk/translate/phrase_based.py +175 -175
  369. {nltk-3.2 → nltk-3.3}/nltk/translate/ribes_score.py +325 -325
  370. {nltk-3.2 → nltk-3.3}/nltk/translate/stack_decoder.py +499 -495
  371. {nltk-3.2 → nltk-3.3}/nltk/tree.py +1605 -1603
  372. {nltk-3.2 → nltk-3.3}/nltk/treeprettyprinter.py +564 -564
  373. {nltk-3.2 → nltk-3.3}/nltk/treetransforms.py +309 -309
  374. {nltk-3.2 → nltk-3.3}/nltk/twitter/__init__.py +28 -28
  375. {nltk-3.2 → nltk-3.3}/nltk/twitter/api.py +141 -136
  376. {nltk-3.2 → nltk-3.3}/nltk/twitter/common.py +264 -258
  377. {nltk-3.2 → nltk-3.3}/nltk/twitter/twitter_demo.py +288 -288
  378. {nltk-3.2 → nltk-3.3}/nltk/twitter/twitterclient.py +530 -530
  379. {nltk-3.2 → nltk-3.3}/nltk/twitter/util.py +147 -146
  380. nltk-3.3/nltk/util.py +751 -0
  381. {nltk-3.2 → nltk-3.3}/nltk/wsd.py +53 -53
  382. {nltk-3.2 → nltk-3.3/nltk.egg-info}/PKG-INFO +13 -5
  383. {nltk-3.2 → nltk-3.3}/nltk.egg-info/SOURCES.txt +26 -3
  384. nltk-3.3/nltk.egg-info/requires.txt +31 -0
  385. {nltk-3.2 → nltk-3.3}/setup.cfg +0 -1
  386. {nltk-3.2 → nltk-3.3}/setup.py +102 -75
  387. nltk-3.2/nltk/VERSION +0 -1
  388. nltk-3.2/nltk/compat.py +0 -680
  389. nltk-3.2/nltk/corpus/reader/pl196x.py +0 -278
  390. nltk-3.2/nltk/corpus/reader/verbnet.py +0 -408
  391. nltk-3.2/nltk/corpus/reader/wordlist.py +0 -37
  392. nltk-3.2/nltk/sentiment/vader_lexicon.txt +0 -7517
  393. nltk-3.2/nltk/six.py +0 -868
  394. nltk-3.2/nltk/stem/porter.py +0 -692
  395. nltk-3.2/nltk/test/unit/test_2x_compat.py +0 -26
  396. nltk-3.2/nltk/test/unit/test_stem.py +0 -36
  397. nltk-3.2/nltk/test/unit/test_tokenize.py +0 -23
  398. nltk-3.2/nltk/test/unit/translate/test_bleu.py +0 -84
  399. nltk-3.2/nltk/tokenize/treebank.py +0 -124
  400. nltk-3.2/nltk/tokenize/util.py +0 -90
  401. nltk-3.2/nltk/util.py +0 -1364
  402. {nltk-3.2 → nltk-3.3}/nltk/test/unit/__init__.py +0 -0
  403. {nltk-3.2 → nltk-3.3}/nltk/test/unit/translate/__init__.py +0 -0
  404. {nltk-3.2 → nltk-3.3}/nltk.egg-info/dependency_links.txt +0 -0
  405. {nltk-3.2 → nltk-3.3}/nltk.egg-info/not-zip-safe +0 -0
  406. {nltk-3.2 → nltk-3.3}/nltk.egg-info/top_level.txt +0 -0
@@ -1,2274 +1,2278 @@
1
- # Natural Language Toolkit: Corpus & Model Downloader
2
- #
3
- # Copyright (C) 2001-2015 NLTK Project
4
- # Author: Edward Loper <edloper@gmail.com>
5
- # URL: <http://nltk.org/>
6
- # For license information, see LICENSE.TXT
7
-
8
- """
9
- The NLTK corpus and module downloader. This module defines several
10
- interfaces which can be used to download corpora, models, and other
11
- data packages that can be used with NLTK.
12
-
13
- Downloading Packages
14
- ====================
15
- If called with no arguments, ``download()`` will display an interactive
16
- interface which can be used to download and install new packages.
17
- If Tkinter is available, then a graphical interface will be shown,
18
- otherwise a simple text interface will be provided.
19
-
20
- Individual packages can be downloaded by calling the ``download()``
21
- function with a single argument, giving the package identifier for the
22
- package that should be downloaded:
23
-
24
- >>> download('treebank') # doctest: +SKIP
25
- [nltk_data] Downloading package 'treebank'...
26
- [nltk_data] Unzipping corpora/treebank.zip.
27
-
28
- NLTK also provides a number of \"package collections\", consisting of
29
- a group of related packages. To download all packages in a
30
- colleciton, simply call ``download()`` with the collection's
31
- identifier:
32
-
33
- >>> download('all-corpora') # doctest: +SKIP
34
- [nltk_data] Downloading package 'abc'...
35
- [nltk_data] Unzipping corpora/abc.zip.
36
- [nltk_data] Downloading package 'alpino'...
37
- [nltk_data] Unzipping corpora/alpino.zip.
38
- ...
39
- [nltk_data] Downloading package 'words'...
40
- [nltk_data] Unzipping corpora/words.zip.
41
-
42
- Download Directory
43
- ==================
44
- By default, packages are installed in either a system-wide directory
45
- (if Python has sufficient access to write to it); or in the current
46
- user's home directory. However, the ``download_dir`` argument may be
47
- used to specify a different installation target, if desired.
48
-
49
- See ``Downloader.default_download_dir()`` for more a detailed
50
- description of how the default download directory is chosen.
51
-
52
- NLTK Download Server
53
- ====================
54
- Before downloading any packages, the corpus and module downloader
55
- contacts the NLTK download server, to retrieve an index file
56
- describing the available packages. By default, this index file is
57
- loaded from ``https://raw.githubusercontent.com/nltk/nltk_data/gh-pages/index.xml``.
58
- If necessary, it is possible to create a new ``Downloader`` object,
59
- specifying a different URL for the package index file.
60
-
61
- Usage::
62
-
63
- python nltk/downloader.py [-d DATADIR] [-q] [-f] [-k] PACKAGE_IDS
64
-
65
- or::
66
-
67
- python -m nltk.downloader [-d DATADIR] [-q] [-f] [-k] PACKAGE_IDS
68
- """
69
- #----------------------------------------------------------------------
70
- from __future__ import print_function, division, unicode_literals
71
-
72
- """
73
-
74
- 0 1 2 3
75
- [label][----][label][----]
76
- [column ][column ]
77
-
78
- Notes
79
- =====
80
- Handling data files.. Some questions:
81
-
82
- * Should the data files be kept zipped or unzipped? I say zipped.
83
-
84
- * Should the data files be kept in svn at all? Advantages: history;
85
- automatic version numbers; 'svn up' could be used rather than the
86
- downloader to update the corpora. Disadvantages: they're big,
87
- which makes working from svn a bit of a pain. And we're planning
88
- to potentially make them much bigger. I don't think we want
89
- people to have to download 400MB corpora just to use nltk from svn.
90
-
91
- * Compromise: keep the data files in trunk/data rather than in
92
- trunk/nltk. That way you can check them out in svn if you want
93
- to; but you don't need to, and you can use the downloader instead.
94
-
95
- * Also: keep models in mind. When we change the code, we'd
96
- potentially like the models to get updated. This could require a
97
- little thought.
98
-
99
- * So.. let's assume we have a trunk/data directory, containing a bunch
100
- of packages. The packages should be kept as zip files, because we
101
- really shouldn't be editing them much (well -- we may edit models
102
- more, but they tend to be binary-ish files anyway, where diffs
103
- aren't that helpful). So we'll have trunk/data, with a bunch of
104
- files like abc.zip and treebank.zip and propbank.zip. For each
105
- package we could also have eg treebank.xml and propbank.xml,
106
- describing the contents of the package (name, copyright, license,
107
- etc). Collections would also have .xml files. Finally, we would
108
- pull all these together to form a single index.xml file. Some
109
- directory structure wouldn't hurt. So how about::
110
-
111
- /trunk/data/ ....................... root of data svn
112
- index.xml ........................ main index file
113
- src/ ............................. python scripts
114
- packages/ ........................ dir for packages
115
- corpora/ ....................... zip & xml files for corpora
116
- grammars/ ...................... zip & xml files for grammars
117
- taggers/ ....................... zip & xml files for taggers
118
- tokenizers/ .................... zip & xml files for tokenizers
119
- etc.
120
- collections/ ..................... xml files for collections
121
-
122
- Where the root (/trunk/data) would contain a makefile; and src/
123
- would contain a script to update the info.xml file. It could also
124
- contain scripts to rebuild some of the various model files. The
125
- script that builds index.xml should probably check that each zip
126
- file expands entirely into a single subdir, whose name matches the
127
- package's uid.
128
-
129
- Changes I need to make:
130
- - in index: change "size" to "filesize" or "compressed-size"
131
- - in index: add "unzipped-size"
132
- - when checking status: check both compressed & uncompressed size.
133
- uncompressed size is important to make sure we detect a problem
134
- if something got partially unzipped. define new status values
135
- to differentiate stale vs corrupt vs corruptly-uncompressed??
136
- (we shouldn't need to re-download the file if the zip file is ok
137
- but it didn't get uncompressed fully.)
138
- - add other fields to the index: author, license, copyright, contact,
139
- etc.
140
-
141
- the current grammars/ package would become a single new package (eg
142
- toy-grammars or book-grammars).
143
-
144
- xml file should have:
145
- - authorship info
146
- - license info
147
- - copyright info
148
- - contact info
149
- - info about what type of data/annotation it contains?
150
- - recommended corpus reader?
151
-
152
- collections can contain other collections. they can also contain
153
- multiple package types (corpora & models). Have a single 'basics'
154
- package that includes everything we talk about in the book?
155
-
156
- n.b.: there will have to be a fallback to the punkt tokenizer, in case
157
- they didn't download that model.
158
-
159
- default: unzip or not?
160
-
161
- """
162
- import time, os, zipfile, sys, textwrap, threading, itertools
163
- from hashlib import md5
164
-
165
- try:
166
- TKINTER = True
167
- from tkinter import (Tk, Frame, Label, Entry, Button, Canvas, Menu, IntVar,
168
- TclError)
169
- from tkinter.messagebox import showerror
170
- from nltk.draw.table import Table
171
- from nltk.draw.util import ShowText
172
- except:
173
- TKINTER = False
174
- TclError = ValueError
175
-
176
- from xml.etree import ElementTree
177
- import nltk
178
- from nltk import compat
179
- #urllib2 = nltk.internals.import_from_stdlib('urllib2')
180
-
181
-
182
- ######################################################################
183
- # Directory entry objects (from the data server's index file)
184
- ######################################################################
185
-
186
- @compat.python_2_unicode_compatible
187
- class Package(object):
188
- """
189
- A directory entry for a downloadable package. These entries are
190
- extracted from the XML index file that is downloaded by
191
- ``Downloader``. Each package consists of a single file; but if
192
- that file is a zip file, then it can be automatically decompressed
193
- when the package is installed.
194
- """
195
- def __init__(self, id, url, name=None, subdir='',
196
- size=None, unzipped_size=None,
197
- checksum=None, svn_revision=None,
198
- copyright='Unknown', contact='Unknown',
199
- license='Unknown', author='Unknown',
200
- unzip=True,
201
- **kw):
202
- self.id = id
203
- """A unique identifier for this package."""
204
-
205
- self.name = name or id
206
- """A string name for this package."""
207
-
208
- self.subdir = subdir
209
- """The subdirectory where this package should be installed.
210
- E.g., ``'corpora'`` or ``'taggers'``."""
211
-
212
- self.url = url
213
- """A URL that can be used to download this package's file."""
214
-
215
- self.size = int(size)
216
- """The filesize (in bytes) of the package file."""
217
-
218
- self.unzipped_size = int(unzipped_size)
219
- """The total filesize of the files contained in the package's
220
- zipfile."""
221
-
222
- self.checksum = checksum
223
- """The MD-5 checksum of the package file."""
224
-
225
- self.svn_revision = svn_revision
226
- """A subversion revision number for this package."""
227
-
228
- self.copyright = copyright
229
- """Copyright holder for this package."""
230
-
231
- self.contact = contact
232
- """Name & email of the person who should be contacted with
233
- questions about this package."""
234
-
235
- self.license = license
236
- """License information for this package."""
237
-
238
- self.author = author
239
- """Author of this package."""
240
-
241
- ext = os.path.splitext(url.split('/')[-1])[1]
242
- self.filename = os.path.join(subdir, id+ext)
243
- """The filename that should be used for this package's file. It
244
- is formed by joining ``self.subdir`` with ``self.id``, and
245
- using the same extension as ``url``."""
246
-
247
- self.unzip = bool(int(unzip)) # '0' or '1'
248
- """A flag indicating whether this corpus should be unzipped by
249
- default."""
250
-
251
- # Include any other attributes provided by the XML file.
252
- self.__dict__.update(kw)
253
-
254
- @staticmethod
255
- def fromxml(xml):
256
- if isinstance(xml, compat.string_types):
257
- xml = ElementTree.parse(xml)
258
- for key in xml.attrib:
259
- xml.attrib[key] = compat.text_type(xml.attrib[key])
260
- return Package(**xml.attrib)
261
-
262
- def __lt__(self, other):
263
- return self.id < other.id
264
-
265
- def __repr__(self):
266
- return '<Package %s>' % self.id
267
-
268
- @compat.python_2_unicode_compatible
269
- class Collection(object):
270
- """
271
- A directory entry for a collection of downloadable packages.
272
- These entries are extracted from the XML index file that is
273
- downloaded by ``Downloader``.
274
- """
275
- def __init__(self, id, children, name=None, **kw):
276
- self.id = id
277
- """A unique identifier for this collection."""
278
-
279
- self.name = name or id
280
- """A string name for this collection."""
281
-
282
- self.children = children
283
- """A list of the ``Collections`` or ``Packages`` directly
284
- contained by this collection."""
285
-
286
- self.packages = None
287
- """A list of ``Packages`` contained by this collection or any
288
- collections it recursively contains."""
289
-
290
- # Include any other attributes provided by the XML file.
291
- self.__dict__.update(kw)
292
-
293
- @staticmethod
294
- def fromxml(xml):
295
- if isinstance(xml, compat.string_types):
296
- xml = ElementTree.parse(xml)
297
- for key in xml.attrib:
298
- xml.attrib[key] = compat.text_type(xml.attrib[key])
299
- children = [child.get('ref') for child in xml.findall('item')]
300
- return Collection(children=children, **xml.attrib)
301
-
302
- def __lt__(self, other):
303
- return self.id < other.id
304
-
305
- def __repr__(self):
306
- return '<Collection %s>' % self.id
307
-
308
- ######################################################################
309
- # Message Passing Objects
310
- ######################################################################
311
-
312
- class DownloaderMessage(object):
313
- """A status message object, used by ``incr_download`` to
314
- communicate its progress."""
315
- class StartCollectionMessage(DownloaderMessage):
316
- """Data server has started working on a collection of packages."""
317
- def __init__(self, collection): self.collection = collection
318
- class FinishCollectionMessage(DownloaderMessage):
319
- """Data server has finished working on a collection of packages."""
320
- def __init__(self, collection): self.collection = collection
321
- class StartPackageMessage(DownloaderMessage):
322
- """Data server has started working on a package."""
323
- def __init__(self, package): self.package = package
324
- class FinishPackageMessage(DownloaderMessage):
325
- """Data server has finished working on a package."""
326
- def __init__(self, package): self.package = package
327
- class StartDownloadMessage(DownloaderMessage):
328
- """Data server has started downloading a package."""
329
- def __init__(self, package): self.package = package
330
- class FinishDownloadMessage(DownloaderMessage):
331
- """Data server has finished downloading a package."""
332
- def __init__(self, package): self.package = package
333
- class StartUnzipMessage(DownloaderMessage):
334
- """Data server has started unzipping a package."""
335
- def __init__(self, package): self.package = package
336
- class FinishUnzipMessage(DownloaderMessage):
337
- """Data server has finished unzipping a package."""
338
- def __init__(self, package): self.package = package
339
- class UpToDateMessage(DownloaderMessage):
340
- """The package download file is already up-to-date"""
341
- def __init__(self, package): self.package = package
342
- class StaleMessage(DownloaderMessage):
343
- """The package download file is out-of-date or corrupt"""
344
- def __init__(self, package): self.package = package
345
- class ErrorMessage(DownloaderMessage):
346
- """Data server encountered an error"""
347
- def __init__(self, package, message):
348
- self.package = package
349
- if isinstance(message, Exception):
350
- self.message = str(message)
351
- else:
352
- self.message = message
353
-
354
- class ProgressMessage(DownloaderMessage):
355
- """Indicates how much progress the data server has made"""
356
- def __init__(self, progress): self.progress = progress
357
- class SelectDownloadDirMessage(DownloaderMessage):
358
- """Indicates what download directory the data server is using"""
359
- def __init__(self, download_dir): self.download_dir = download_dir
360
-
361
- ######################################################################
362
- # NLTK Data Server
363
- ######################################################################
364
-
365
- class Downloader(object):
366
- """
367
- A class used to access the NLTK data server, which can be used to
368
- download corpora and other data packages.
369
- """
370
-
371
- #/////////////////////////////////////////////////////////////////
372
- # Configuration
373
- #/////////////////////////////////////////////////////////////////
374
-
375
- INDEX_TIMEOUT = 60*60 # 1 hour
376
- """The amount of time after which the cached copy of the data
377
- server index will be considered 'stale,' and will be
378
- re-downloaded."""
379
-
380
- DEFAULT_URL = 'https://raw.githubusercontent.com/nltk/nltk_data/gh-pages/index.xml'
381
- """The default URL for the NLTK data server's index. An
382
- alternative URL can be specified when creating a new
383
- ``Downloader`` object."""
384
-
385
- #/////////////////////////////////////////////////////////////////
386
- # Status Constants
387
- #/////////////////////////////////////////////////////////////////
388
-
389
- INSTALLED = 'installed'
390
- """A status string indicating that a package or collection is
391
- installed and up-to-date."""
392
- NOT_INSTALLED = 'not installed'
393
- """A status string indicating that a package or collection is
394
- not installed."""
395
- STALE = 'out of date'
396
- """A status string indicating that a package or collection is
397
- corrupt or out-of-date."""
398
- PARTIAL = 'partial'
399
- """A status string indicating that a collection is partially
400
- installed (i.e., only some of its packages are installed.)"""
401
-
402
- #/////////////////////////////////////////////////////////////////
403
- # Cosntructor
404
- #/////////////////////////////////////////////////////////////////
405
-
406
- def __init__(self, server_index_url=None, download_dir=None):
407
- self._url = server_index_url or self.DEFAULT_URL
408
- """The URL for the data server's index file."""
409
-
410
- self._collections = {}
411
- """Dictionary from collection identifier to ``Collection``"""
412
-
413
- self._packages = {}
414
- """Dictionary from package identifier to ``Package``"""
415
-
416
- self._download_dir = download_dir
417
- """The default directory to which packages will be downloaded."""
418
-
419
- self._index = None
420
- """The XML index file downloaded from the data server"""
421
-
422
- self._index_timestamp = None
423
- """Time at which ``self._index`` was downloaded. If it is more
424
- than ``INDEX_TIMEOUT`` seconds old, it will be re-downloaded."""
425
-
426
- self._status_cache = {}
427
- """Dictionary from package/collection identifier to status
428
- string (``INSTALLED``, ``NOT_INSTALLED``, ``STALE``, or
429
- ``PARTIAL``). Cache is used for packages only, not
430
- collections."""
431
-
432
- self._errors = None
433
- """Flag for telling if all packages got successfully downloaded or not."""
434
-
435
- # decide where we're going to save things to.
436
- if self._download_dir is None:
437
- self._download_dir = self.default_download_dir()
438
-
439
- #/////////////////////////////////////////////////////////////////
440
- # Information
441
- #/////////////////////////////////////////////////////////////////
442
-
443
- def list(self, download_dir=None, show_packages=True,
444
- show_collections=True, header=True, more_prompt=False,
445
- skip_installed=False):
446
- lines = 0 # for more_prompt
447
- if download_dir is None:
448
- download_dir = self._download_dir
449
- print('Using default data directory (%s)' % download_dir)
450
- if header:
451
- print('='*(26+len(self._url)))
452
- print(' Data server index for <%s>' % self._url)
453
- print('='*(26+len(self._url)))
454
- lines += 3 # for more_prompt
455
- stale = partial = False
456
-
457
- categories = []
458
- if show_packages: categories.append('packages')
459
- if show_collections: categories.append('collections')
460
- for category in categories:
461
- print('%s:' % category.capitalize())
462
- lines += 1 # for more_prompt
463
- for info in sorted(getattr(self, category)(), key=str):
464
- status = self.status(info, download_dir)
465
- if status == self.INSTALLED and skip_installed: continue
466
- if status == self.STALE: stale = True
467
- if status == self.PARTIAL: partial = True
468
- prefix = {self.INSTALLED:'*', self.STALE:'-',
469
- self.PARTIAL:'P', self.NOT_INSTALLED: ' '}[status]
470
- name = textwrap.fill('-'*27 + (info.name or info.id),
471
- 75, subsequent_indent=27*' ')[27:]
472
- print(' [%s] %s %s' % (prefix, info.id.ljust(20, '.'), name))
473
- lines += len(name.split('\n')) # for more_prompt
474
- if more_prompt and lines > 20:
475
- user_input = compat.raw_input("Hit Enter to continue: ")
476
- if (user_input.lower() in ('x', 'q')): return
477
- lines = 0
478
- print()
479
- msg = '([*] marks installed packages'
480
- if stale: msg += '; [-] marks out-of-date or corrupt packages'
481
- if partial: msg += '; [P] marks partially installed collections'
482
- print(textwrap.fill(msg+')', subsequent_indent=' ', width=76))
483
-
484
- def packages(self):
485
- self._update_index()
486
- return self._packages.values()
487
-
488
- def corpora(self):
489
- self._update_index()
490
- return [pkg for (id,pkg) in self._packages.items()
491
- if pkg.subdir == 'corpora']
492
-
493
- def models(self):
494
- self._update_index()
495
- return [pkg for (id,pkg) in self._packages.items()
496
- if pkg.subdir != 'corpora']
497
-
498
- def collections(self):
499
- self._update_index()
500
- return self._collections.values()
501
-
502
- #/////////////////////////////////////////////////////////////////
503
- # Downloading
504
- #/////////////////////////////////////////////////////////////////
505
-
506
- def _info_or_id(self, info_or_id):
507
- if isinstance(info_or_id, compat.string_types):
508
- return self.info(info_or_id)
509
- else:
510
- return info_or_id
511
-
512
- # [xx] When during downloading is it 'safe' to abort? Only unsafe
513
- # time is *during* an unzip -- we don't want to leave a
514
- # partially-unzipped corpus in place because we wouldn't notice
515
- # it. But if we had the exact total size of the unzipped corpus,
516
- # then that would be fine. Then we could abort anytime we want!
517
- # So this is really what we should do. That way the threaded
518
- # downloader in the gui can just kill the download thread anytime
519
- # it wants.
520
-
521
- def incr_download(self, info_or_id, download_dir=None, force=False):
522
- # If they didn't specify a download_dir, then use the default one.
523
- if download_dir is None:
524
- download_dir = self._download_dir
525
- yield SelectDownloadDirMessage(download_dir)
526
-
527
- # If they gave us a list of ids, then download each one.
528
- if isinstance(info_or_id, (list,tuple)):
529
- for msg in self._download_list(info_or_id, download_dir, force):
530
- yield msg
531
- return
532
-
533
- # Look up the requested collection or package.
534
- try: info = self._info_or_id(info_or_id)
535
- except (IOError, ValueError) as e:
536
- yield ErrorMessage(None, 'Error loading %s: %s' %
537
- (info_or_id, e))
538
- return
539
-
540
- # Handle collections.
541
- if isinstance(info, Collection):
542
- yield StartCollectionMessage(info)
543
- for msg in self.incr_download(info.children, download_dir, force):
544
- yield msg
545
- yield FinishCollectionMessage(info)
546
-
547
- # Handle Packages (delegate to a helper function).
548
- else:
549
- for msg in self._download_package(info, download_dir, force):
550
- yield msg
551
-
552
- def _num_packages(self, item):
553
- if isinstance(item, Package): return 1
554
- else: return len(item.packages)
555
-
556
- def _download_list(self, items, download_dir, force):
557
- # Look up the requested items.
558
- for i in range(len(items)):
559
- try: items[i] = self._info_or_id(items[i])
560
- except (IOError, ValueError) as e:
561
- yield ErrorMessage(items[i], e)
562
- return
563
-
564
- # Download each item, re-scaling their progress.
565
- num_packages = sum(self._num_packages(item) for item in items)
566
- progress = 0
567
- for i, item in enumerate(items):
568
- if isinstance(item, Package):
569
- delta = 1./num_packages
570
- else:
571
- delta = len(item.packages)/num_packages
572
- for msg in self.incr_download(item, download_dir, force):
573
- if isinstance(msg, ProgressMessage):
574
- yield ProgressMessage(progress + msg.progress*delta)
575
- else:
576
- yield msg
577
-
578
- progress += 100*delta
579
-
580
- def _download_package(self, info, download_dir, force):
581
- yield StartPackageMessage(info)
582
- yield ProgressMessage(0)
583
-
584
- # Do we already have the current version?
585
- status = self.status(info, download_dir)
586
- if not force and status == self.INSTALLED:
587
- yield UpToDateMessage(info)
588
- yield ProgressMessage(100)
589
- yield FinishPackageMessage(info)
590
- return
591
-
592
- # Remove the package from our status cache
593
- self._status_cache.pop(info.id, None)
594
-
595
- # Check for (and remove) any old/stale version.
596
- filepath = os.path.join(download_dir, info.filename)
597
- if os.path.exists(filepath):
598
- if status == self.STALE:
599
- yield StaleMessage(info)
600
- os.remove(filepath)
601
-
602
- # Ensure the download_dir exists
603
- if not os.path.exists(download_dir):
604
- os.mkdir(download_dir)
605
- if not os.path.exists(os.path.join(download_dir, info.subdir)):
606
- os.mkdir(os.path.join(download_dir, info.subdir))
607
-
608
- # Download the file. This will raise an IOError if the url
609
- # is not found.
610
- yield StartDownloadMessage(info)
611
- yield ProgressMessage(5)
612
- try:
613
- infile = compat.urlopen(info.url)
614
- with open(filepath, 'wb') as outfile:
615
- #print info.size
616
- num_blocks = max(1, info.size/(1024*16))
617
- for block in itertools.count():
618
- s = infile.read(1024*16) # 16k blocks.
619
- outfile.write(s)
620
- if not s: break
621
- if block % 2 == 0: # how often?
622
- yield ProgressMessage(min(80, 5+75*(block/num_blocks)))
623
- infile.close()
624
- except IOError as e:
625
- yield ErrorMessage(info, 'Error downloading %r from <%s>:'
626
- '\n %s' % (info.id, info.url, e))
627
- return
628
- yield FinishDownloadMessage(info)
629
- yield ProgressMessage(80)
630
-
631
- # If it's a zipfile, uncompress it.
632
- if info.filename.endswith('.zip'):
633
- zipdir = os.path.join(download_dir, info.subdir)
634
- # Unzip if we're unzipping by default; *or* if it's already
635
- # been unzipped (presumably a previous version).
636
- if info.unzip or os.path.exists(os.path.join(zipdir, info.id)):
637
- yield StartUnzipMessage(info)
638
- for msg in _unzip_iter(filepath, zipdir, verbose=False):
639
- # Somewhat of a hack, but we need a proper package reference
640
- msg.package = info
641
- yield msg
642
- yield FinishUnzipMessage(info)
643
-
644
- yield FinishPackageMessage(info)
645
-
646
- def download(self, info_or_id=None, download_dir=None, quiet=False,
647
- force=False, prefix='[nltk_data] ', halt_on_error=True,
648
- raise_on_error=False):
649
- # If no info or id is given, then use the interactive shell.
650
- if info_or_id is None:
651
- # [xx] hmm -- changing self._download_dir here seems like
652
- # the wrong thing to do. Maybe the _interactive_download
653
- # function should make a new copy of self to use?
654
- if download_dir is not None: self._download_dir = download_dir
655
- self._interactive_download()
656
- return True
657
-
658
- else:
659
- # Define a helper function for displaying output:
660
- def show(s, prefix2=''):
661
- print(textwrap.fill(s, initial_indent=prefix+prefix2,
662
- subsequent_indent=prefix+prefix2+' '*4))
663
-
664
- for msg in self.incr_download(info_or_id, download_dir, force):
665
- # Error messages
666
- if isinstance(msg, ErrorMessage):
667
- show(msg.message)
668
- if raise_on_error:
669
- raise ValueError(msg.message)
670
- if halt_on_error:
671
- return False
672
- self._errors = True
673
- if not quiet:
674
- print("Error installing package. Retry? [n/y/e]")
675
- choice = compat.raw_input().strip()
676
- if choice in ['y', 'Y']:
677
- if not self.download(msg.package.id, download_dir,
678
- quiet, force, prefix,
679
- halt_on_error, raise_on_error):
680
- return False
681
- elif choice in ['e', 'E']:
682
- return False
683
-
684
- # All other messages
685
- if not quiet:
686
- # Collection downloading messages:
687
- if isinstance(msg, StartCollectionMessage):
688
- show('Downloading collection %r' % msg.collection.id)
689
- prefix += ' | '
690
- print(prefix)
691
- elif isinstance(msg, FinishCollectionMessage):
692
- print(prefix)
693
- prefix = prefix[:-4]
694
- if self._errors:
695
- show('Downloaded collection %r with errors' %
696
- msg.collection.id)
697
- else:
698
- show('Done downloading collection %s' %
699
- msg.collection.id)
700
-
701
- # Package downloading messages:
702
- elif isinstance(msg, StartPackageMessage):
703
- show('Downloading package %s to %s...' %
704
- (msg.package.id, download_dir))
705
- elif isinstance(msg, UpToDateMessage):
706
- show('Package %s is already up-to-date!' %
707
- msg.package.id, ' ')
708
- #elif isinstance(msg, StaleMessage):
709
- # show('Package %s is out-of-date or corrupt' %
710
- # msg.package.id, ' ')
711
- elif isinstance(msg, StartUnzipMessage):
712
- show('Unzipping %s.' % msg.package.filename, ' ')
713
-
714
- # Data directory message:
715
- elif isinstance(msg, SelectDownloadDirMessage):
716
- download_dir = msg.download_dir
717
- return True
718
-
719
- def is_stale(self, info_or_id, download_dir=None):
720
- return self.status(info_or_id, download_dir) == self.STALE
721
-
722
- def is_installed(self, info_or_id, download_dir=None):
723
- return self.status(info_or_id, download_dir) == self.INSTALLED
724
-
725
- def clear_status_cache(self, id=None):
726
- if id is None:
727
- self._status_cache.clear()
728
- else:
729
- self._status_cache.pop(id, None)
730
-
731
- def status(self, info_or_id, download_dir=None):
732
- """
733
- Return a constant describing the status of the given package
734
- or collection. Status can be one of ``INSTALLED``,
735
- ``NOT_INSTALLED``, ``STALE``, or ``PARTIAL``.
736
- """
737
- if download_dir is None: download_dir = self._download_dir
738
- info = self._info_or_id(info_or_id)
739
-
740
- # Handle collections:
741
- if isinstance(info, Collection):
742
- pkg_status = [self.status(pkg.id) for pkg in info.packages]
743
- if self.STALE in pkg_status:
744
- return self.STALE
745
- elif self.PARTIAL in pkg_status:
746
- return self.PARTIAL
747
- elif (self.INSTALLED in pkg_status and
748
- self.NOT_INSTALLED in pkg_status):
749
- return self.PARTIAL
750
- elif self.NOT_INSTALLED in pkg_status:
751
- return self.NOT_INSTALLED
752
- else:
753
- return self.INSTALLED
754
-
755
- # Handle packages:
756
- else:
757
- filepath = os.path.join(download_dir, info.filename)
758
- if download_dir != self._download_dir:
759
- status = self._pkg_status(info, filepath)
760
- else:
761
- if info.id not in self._status_cache:
762
- self._status_cache[info.id] = self._pkg_status(info,
763
- filepath)
764
- return self._status_cache[info.id]
765
-
766
- def _pkg_status(self, info, filepath):
767
- if not os.path.exists(filepath):
768
- return self.NOT_INSTALLED
769
-
770
- # Check if the file has the correct size.
771
- try: filestat = os.stat(filepath)
772
- except OSError: return self.NOT_INSTALLED
773
- if filestat.st_size != int(info.size):
774
- return self.STALE
775
-
776
- # Check if the file's checksum matches
777
- if md5_hexdigest(filepath) != info.checksum:
778
- return self.STALE
779
-
780
- # If it's a zipfile, and it's been at least partially
781
- # unzipped, then check if it's been fully unzipped.
782
- if filepath.endswith('.zip'):
783
- unzipdir = filepath[:-4]
784
- if not os.path.exists(unzipdir):
785
- return self.INSTALLED # but not unzipped -- ok!
786
- if not os.path.isdir(unzipdir):
787
- return self.STALE
788
-
789
- unzipped_size = sum(os.stat(os.path.join(d, f)).st_size
790
- for d, _, files in os.walk(unzipdir)
791
- for f in files)
792
- if unzipped_size != info.unzipped_size:
793
- return self.STALE
794
-
795
- # Otherwise, everything looks good.
796
- return self.INSTALLED
797
-
798
- def update(self, quiet=False, prefix='[nltk_data] '):
799
- """
800
- Re-download any packages whose status is STALE.
801
- """
802
- self.clear_status_cache()
803
- for pkg in self.packages():
804
- if self.status(pkg) == self.STALE:
805
- self.download(pkg, quiet=quiet, prefix=prefix)
806
-
807
- #/////////////////////////////////////////////////////////////////
808
- # Index
809
- #/////////////////////////////////////////////////////////////////
810
-
811
- def _update_index(self, url=None):
812
- """A helper function that ensures that self._index is
813
- up-to-date. If the index is older than self.INDEX_TIMEOUT,
814
- then download it again."""
815
- # Check if the index is aleady up-to-date. If so, do nothing.
816
- if not (self._index is None or url is not None or
817
- time.time()-self._index_timestamp > self.INDEX_TIMEOUT):
818
- return
819
-
820
- # If a URL was specified, then update our URL.
821
- self._url = url or self._url
822
-
823
- # Download the index file.
824
- self._index = nltk.internals.ElementWrapper(
825
- ElementTree.parse(compat.urlopen(self._url)).getroot())
826
- self._index_timestamp = time.time()
827
-
828
- # Build a dictionary of packages.
829
- packages = [Package.fromxml(p) for p in
830
- self._index.findall('packages/package')]
831
- self._packages = dict((p.id, p) for p in packages)
832
-
833
- # Build a dictionary of collections.
834
- collections = [Collection.fromxml(c) for c in
835
- self._index.findall('collections/collection')]
836
- self._collections = dict((c.id, c) for c in collections)
837
-
838
- # Replace identifiers with actual children in collection.children.
839
- for collection in self._collections.values():
840
- for i, child_id in enumerate(collection.children):
841
- if child_id in self._packages:
842
- collection.children[i] = self._packages[child_id]
843
- elif child_id in self._collections:
844
- collection.children[i] = self._collections[child_id]
845
- else:
846
- print('removing collection member with no package: {}'.format(child_id))
847
- del collection.children[i]
848
-
849
- # Fill in collection.packages for each collection.
850
- for collection in self._collections.values():
851
- packages = {}
852
- queue = [collection]
853
- for child in queue:
854
- if isinstance(child, Collection):
855
- queue.extend(child.children)
856
- else:
857
- packages[child.id] = child
858
- collection.packages = packages.values()
859
-
860
- # Flush the status cache
861
- self._status_cache.clear()
862
-
863
- def index(self):
864
- """
865
- Return the XML index describing the packages available from
866
- the data server. If necessary, this index will be downloaded
867
- from the data server.
868
- """
869
- self._update_index()
870
- return self._index
871
-
872
- def info(self, id):
873
- """Return the ``Package`` or ``Collection`` record for the
874
- given item."""
875
- self._update_index()
876
- if id in self._packages: return self._packages[id]
877
- if id in self._collections: return self._collections[id]
878
- raise ValueError('Package %r not found in index' % id)
879
-
880
- def xmlinfo(self, id):
881
- """Return the XML info record for the given item"""
882
- self._update_index()
883
- for package in self._index.findall('packages/package'):
884
- if package.get('id') == id:
885
- return package
886
- for collection in self._index.findall('collections/collection'):
887
- if collection.get('id') == id:
888
- return collection
889
- raise ValueError('Package %r not found in index' % id)
890
-
891
- #/////////////////////////////////////////////////////////////////
892
- # URL & Data Directory
893
- #/////////////////////////////////////////////////////////////////
894
-
895
- def _get_url(self):
896
- """The URL for the data server's index file."""
897
- return self._url
898
- def _set_url(self, url):
899
- """
900
- Set a new URL for the data server. If we're unable to contact
901
- the given url, then the original url is kept.
902
- """
903
- original_url = self._url
904
- try:
905
- self._update_index(url)
906
- except:
907
- self._url = original_url
908
- raise
909
- url = property(_get_url, _set_url)
910
-
911
- def default_download_dir(self):
912
- """
913
- Return the directory to which packages will be downloaded by
914
- default. This value can be overridden using the constructor,
915
- or on a case-by-case basis using the ``download_dir`` argument when
916
- calling ``download()``.
917
-
918
- On Windows, the default download directory is
919
- ``PYTHONHOME/lib/nltk``, where *PYTHONHOME* is the
920
- directory containing Python, e.g. ``C:\\Python25``.
921
-
922
- On all other platforms, the default directory is the first of
923
- the following which exists or which can be created with write
924
- permission: ``/usr/share/nltk_data``, ``/usr/local/share/nltk_data``,
925
- ``/usr/lib/nltk_data``, ``/usr/local/lib/nltk_data``, ``~/nltk_data``.
926
- """
927
- # Check if we are on GAE where we cannot write into filesystem.
928
- if 'APPENGINE_RUNTIME' in os.environ:
929
- return
930
-
931
- # Check if we have sufficient permissions to install in a
932
- # variety of system-wide locations.
933
- for nltkdir in nltk.data.path:
934
- if (os.path.exists(nltkdir) and
935
- nltk.internals.is_writable(nltkdir)):
936
- return nltkdir
937
-
938
- # On Windows, use %APPDATA%
939
- if sys.platform == 'win32' and 'APPDATA' in os.environ:
940
- homedir = os.environ['APPDATA']
941
-
942
- # Otherwise, install in the user's home directory.
943
- else:
944
- homedir = os.path.expanduser('~/')
945
- if homedir == '~/':
946
- raise ValueError("Could not find a default download directory")
947
-
948
- # append "nltk_data" to the home directory
949
- return os.path.join(homedir, 'nltk_data')
950
-
951
- def _get_download_dir(self):
952
- """
953
- The default directory to which packages will be downloaded.
954
- This defaults to the value returned by ``default_download_dir()``.
955
- To override this default on a case-by-case basis, use the
956
- ``download_dir`` argument when calling ``download()``.
957
- """
958
- return self._download_dir
959
- def _set_download_dir(self, download_dir):
960
- self._download_dir = download_dir
961
- # Clear the status cache.
962
- self._status_cache.clear()
963
- download_dir = property(_get_download_dir, _set_download_dir)
964
-
965
- #/////////////////////////////////////////////////////////////////
966
- # Interactive Shell
967
- #/////////////////////////////////////////////////////////////////
968
-
969
- def _interactive_download(self):
970
- # Try the GUI first; if that doesn't work, try the simple
971
- # interactive shell.
972
- if TKINTER:
973
- try:
974
- DownloaderGUI(self).mainloop()
975
- except TclError:
976
- DownloaderShell(self).run()
977
- else:
978
- DownloaderShell(self).run()
979
-
980
- class DownloaderShell(object):
981
- def __init__(self, dataserver):
982
- self._ds = dataserver
983
-
984
- def _simple_interactive_menu(self, *options):
985
- print('-'*75)
986
- spc = (68 - sum(len(o) for o in options))//(len(options)-1)*' '
987
- print(' ' + spc.join(options))
988
- #w = 76/len(options)
989
- #fmt = ' ' + ('%-'+str(w)+'s')*(len(options)-1) + '%s'
990
- #print fmt % options
991
- print('-'*75)
992
-
993
- def run(self):
994
- print('NLTK Downloader')
995
- while True:
996
- self._simple_interactive_menu(
997
- 'd) Download', 'l) List', ' u) Update', 'c) Config', 'h) Help', 'q) Quit')
998
- user_input = compat.raw_input('Downloader> ').strip()
999
- if not user_input: print(); continue
1000
- command = user_input.lower().split()[0]
1001
- args = user_input.split()[1:]
1002
- try:
1003
- if command == 'l':
1004
- print()
1005
- self._ds.list(self._ds.download_dir, header=False,
1006
- more_prompt=True)
1007
- elif command == 'h':
1008
- self._simple_interactive_help()
1009
- elif command == 'c':
1010
- self._simple_interactive_config()
1011
- elif command in ('q', 'x'):
1012
- return
1013
- elif command == 'd':
1014
- self._simple_interactive_download(args)
1015
- elif command == 'u':
1016
- self._simple_interactive_update()
1017
- else:
1018
- print('Command %r unrecognized' % user_input)
1019
- except compat.HTTPError as e:
1020
- print('Error reading from server: %s'%e)
1021
- except compat.URLError as e:
1022
- print('Error connecting to server: %s'%e.reason)
1023
- # try checking if user_input is a package name, &
1024
- # downloading it?
1025
- print()
1026
-
1027
- def _simple_interactive_download(self, args):
1028
- if args:
1029
- for arg in args:
1030
- try: self._ds.download(arg, prefix=' ')
1031
- except (IOError, ValueError) as e: print(e)
1032
- else:
1033
- while True:
1034
- print()
1035
- print('Download which package (l=list; x=cancel)?')
1036
- user_input = compat.raw_input(' Identifier> ')
1037
- if user_input.lower()=='l':
1038
- self._ds.list(self._ds.download_dir, header=False,
1039
- more_prompt=True, skip_installed=True)
1040
- continue
1041
- elif user_input.lower() in ('x', 'q', ''):
1042
- return
1043
- elif user_input:
1044
- for id in user_input.split():
1045
- try: self._ds.download(id, prefix=' ')
1046
- except (IOError, ValueError) as e: print(e)
1047
- break
1048
-
1049
- def _simple_interactive_update(self):
1050
- while True:
1051
- stale_packages = []
1052
- stale = partial = False
1053
- for info in sorted(getattr(self._ds, 'packages')(), key=str):
1054
- if self._ds.status(info) == self._ds.STALE:
1055
- stale_packages.append((info.id, info.name))
1056
-
1057
- print()
1058
- if stale_packages:
1059
- print('Will update following packages (o=ok; x=cancel)')
1060
- for pid, pname in stale_packages:
1061
- name = textwrap.fill('-'*27 + (pname),
1062
- 75, subsequent_indent=27*' ')[27:]
1063
- print(' [ ] %s %s' % (pid.ljust(20, '.'), name))
1064
- print()
1065
-
1066
- user_input = compat.raw_input(' Identifier> ')
1067
- if user_input.lower()=='o':
1068
- for pid, pname in stale_packages:
1069
- try: self._ds.download(pid, prefix=' ')
1070
- except (IOError, ValueError) as e: print(e)
1071
- break
1072
- elif user_input.lower() in ('x', 'q', ''):
1073
- return
1074
- else:
1075
- print('Nothing to update.')
1076
- return
1077
-
1078
- def _simple_interactive_help(self):
1079
- print()
1080
- print('Commands:')
1081
- print(' d) Download a package or collection u) Update out of date packages')
1082
- print(' l) List packages & collections h) Help')
1083
- print(' c) View & Modify Configuration q) Quit')
1084
-
1085
- def _show_config(self):
1086
- print()
1087
- print('Data Server:')
1088
- print(' - URL: <%s>' % self._ds.url)
1089
- print((' - %d Package Collections Available' %
1090
- len(self._ds.collections())))
1091
- print((' - %d Individual Packages Available' %
1092
- len(self._ds.packages())))
1093
- print()
1094
- print('Local Machine:')
1095
- print(' - Data directory: %s' % self._ds.download_dir)
1096
-
1097
- def _simple_interactive_config(self):
1098
- self._show_config()
1099
- while True:
1100
- print()
1101
- self._simple_interactive_menu(
1102
- 's) Show Config', 'u) Set Server URL',
1103
- 'd) Set Data Dir', 'm) Main Menu')
1104
- user_input = compat.raw_input('Config> ').strip().lower()
1105
- if user_input == 's':
1106
- self._show_config()
1107
- elif user_input == 'd':
1108
- new_dl_dir = compat.raw_input(' New Directory> ').strip()
1109
- if new_dl_dir in ('', 'x', 'q', 'X', 'Q'):
1110
- print(' Cancelled!')
1111
- elif os.path.isdir(new_dl_dir):
1112
- self._ds.download_dir = new_dl_dir
1113
- else:
1114
- print(('Directory %r not found! Create it first.' %
1115
- new_dl_dir))
1116
- elif user_input == 'u':
1117
- new_url = compat.raw_input(' New URL> ').strip()
1118
- if new_url in ('', 'x', 'q', 'X', 'Q'):
1119
- print(' Cancelled!')
1120
- else:
1121
- if not new_url.startswith(('http://', 'https://')):
1122
- new_url = 'http://'+new_url
1123
- try: self._ds.url = new_url
1124
- except Exception as e:
1125
- print('Error reading <%r>:\n %s' % (new_url, e))
1126
- elif user_input == 'm':
1127
- break
1128
-
1129
- class DownloaderGUI(object):
1130
- """
1131
- Graphical interface for downloading packages from the NLTK data
1132
- server.
1133
- """
1134
-
1135
- #/////////////////////////////////////////////////////////////////
1136
- # Column Configuration
1137
- #/////////////////////////////////////////////////////////////////
1138
-
1139
- COLUMNS = ['', 'Identifier', 'Name', 'Size', 'Status',
1140
- 'Unzipped Size',
1141
- 'Copyright', 'Contact', 'License', 'Author',
1142
- 'Subdir', 'Checksum']
1143
- """A list of the names of columns. This controls the order in
1144
- which the columns will appear. If this is edited, then
1145
- ``_package_to_columns()`` may need to be edited to match."""
1146
-
1147
- COLUMN_WEIGHTS = {'': 0, 'Name': 5, 'Size': 0, 'Status': 0}
1148
- """A dictionary specifying how columns should be resized when the
1149
- table is resized. Columns with weight 0 will not be resized at
1150
- all; and columns with high weight will be resized more.
1151
- Default weight (for columns not explicitly listed) is 1."""
1152
-
1153
- COLUMN_WIDTHS = {'':1, 'Identifier':20, 'Name':45,
1154
- 'Size': 10, 'Unzipped Size': 10,
1155
- 'Status': 12}
1156
- """A dictionary specifying how wide each column should be, in
1157
- characters. The default width (for columns not explicitly
1158
- listed) is specified by ``DEFAULT_COLUMN_WIDTH``."""
1159
-
1160
- DEFAULT_COLUMN_WIDTH = 30
1161
- """The default width for columns that are not explicitly listed
1162
- in ``COLUMN_WIDTHS``."""
1163
-
1164
- INITIAL_COLUMNS = ['', 'Identifier', 'Name', 'Size', 'Status']
1165
- """The set of columns that should be displayed by default."""
1166
-
1167
- # Perform a few import-time sanity checks to make sure that the
1168
- # column configuration variables are defined consistently:
1169
- for c in COLUMN_WEIGHTS: assert c in COLUMNS
1170
- for c in COLUMN_WIDTHS: assert c in COLUMNS
1171
- for c in INITIAL_COLUMNS: assert c in COLUMNS
1172
-
1173
- #/////////////////////////////////////////////////////////////////
1174
- # Color Configuration
1175
- #/////////////////////////////////////////////////////////////////
1176
-
1177
- _BACKDROP_COLOR = ('#000', '#ccc')
1178
-
1179
- _ROW_COLOR = {Downloader.INSTALLED: ('#afa', '#080'),
1180
- Downloader.PARTIAL: ('#ffa', '#880'),
1181
- Downloader.STALE: ('#faa', '#800'),
1182
- Downloader.NOT_INSTALLED: ('#fff', '#888')}
1183
-
1184
- _MARK_COLOR = ('#000', '#ccc')
1185
-
1186
- #_FRONT_TAB_COLOR = ('#ccf', '#008')
1187
- #_BACK_TAB_COLOR = ('#88a', '#448')
1188
- _FRONT_TAB_COLOR = ('#fff', '#45c')
1189
- _BACK_TAB_COLOR = ('#aaa', '#67a')
1190
-
1191
- _PROGRESS_COLOR = ('#f00', '#aaa')
1192
-
1193
- _TAB_FONT = 'helvetica -16 bold'
1194
-
1195
- #/////////////////////////////////////////////////////////////////
1196
- # Constructor
1197
- #/////////////////////////////////////////////////////////////////
1198
-
1199
- def __init__(self, dataserver, use_threads=True):
1200
- self._ds = dataserver
1201
- self._use_threads = use_threads
1202
-
1203
- # For the threaded downloader:
1204
- self._download_lock = threading.Lock()
1205
- self._download_msg_queue = []
1206
- self._download_abort_queue = []
1207
- self._downloading = False
1208
-
1209
- # For tkinter after callbacks:
1210
- self._afterid = {}
1211
-
1212
- # A message log.
1213
- self._log_messages = []
1214
- self._log_indent = 0
1215
- self._log('NLTK Downloader Started!')
1216
-
1217
- # Create the main window.
1218
- top = self.top = Tk()
1219
- top.geometry('+50+50')
1220
- top.title('NLTK Downloader')
1221
- top.configure(background=self._BACKDROP_COLOR[1])
1222
-
1223
- # Set up some bindings now, in case anything goes wrong.
1224
- top.bind('<Control-q>', self.destroy)
1225
- top.bind('<Control-x>', self.destroy)
1226
- self._destroyed = False
1227
-
1228
- self._column_vars = {}
1229
-
1230
- # Initialize the GUI.
1231
- self._init_widgets()
1232
- self._init_menu()
1233
- try:
1234
- self._fill_table()
1235
- except compat.HTTPError as e:
1236
- showerror('Error reading from server', e)
1237
- except compat.URLError as e:
1238
- showerror('Error connecting to server', e.reason)
1239
-
1240
- self._show_info()
1241
- self._select_columns()
1242
- self._table.select(0)
1243
-
1244
- # Make sure we get notified when we're destroyed, so we can
1245
- # cancel any download in progress.
1246
- self._table.bind('<Destroy>', self._destroy)
1247
-
1248
- def _log(self, msg):
1249
- self._log_messages.append('%s %s%s' % (time.ctime(),
1250
- ' | '*self._log_indent, msg))
1251
-
1252
- #/////////////////////////////////////////////////////////////////
1253
- # Internals
1254
- #/////////////////////////////////////////////////////////////////
1255
-
1256
- def _init_widgets(self):
1257
- # Create the top-level frame structures
1258
- f1 = Frame(self.top, relief='raised', border=2, padx=8, pady=0)
1259
- f1.pack(sid='top', expand=True, fill='both')
1260
- f1.grid_rowconfigure(2, weight=1)
1261
- f1.grid_columnconfigure(0, weight=1)
1262
- Frame(f1, height=8).grid(column=0, row=0) # spacer
1263
- tabframe = Frame(f1)
1264
- tabframe.grid(column=0, row=1, sticky='news')
1265
- tableframe = Frame(f1)
1266
- tableframe.grid(column=0, row=2, sticky='news')
1267
- buttonframe = Frame(f1)
1268
- buttonframe.grid(column=0, row=3, sticky='news')
1269
- Frame(f1, height=8).grid(column=0, row=4) # spacer
1270
- infoframe = Frame(f1)
1271
- infoframe.grid(column=0, row=5, sticky='news')
1272
- Frame(f1, height=8).grid(column=0, row=6) # spacer
1273
- progressframe = Frame(self.top, padx=3, pady=3,
1274
- background=self._BACKDROP_COLOR[1])
1275
- progressframe.pack(side='bottom', fill='x')
1276
- self.top['border'] = 0
1277
- self.top['highlightthickness'] = 0
1278
-
1279
- # Create the tabs
1280
- self._tab_names = ['Collections', 'Corpora',
1281
- 'Models', 'All Packages',]
1282
- self._tabs = {}
1283
- for i, tab in enumerate(self._tab_names):
1284
- label = Label(tabframe, text=tab, font=self._TAB_FONT)
1285
- label.pack(side='left', padx=((i+1)%2)*10)
1286
- label.bind('<Button-1>', self._select_tab)
1287
- self._tabs[tab.lower()] = label
1288
-
1289
- # Create the table.
1290
- column_weights = [self.COLUMN_WEIGHTS.get(column, 1)
1291
- for column in self.COLUMNS]
1292
- self._table = Table(tableframe, self.COLUMNS,
1293
- column_weights=column_weights,
1294
- highlightthickness=0, listbox_height=16,
1295
- reprfunc=self._table_reprfunc)
1296
- self._table.columnconfig(0, foreground=self._MARK_COLOR[0]) # marked
1297
- for i, column in enumerate(self.COLUMNS):
1298
- width = self.COLUMN_WIDTHS.get(column, self.DEFAULT_COLUMN_WIDTH)
1299
- self._table.columnconfig(i, width=width)
1300
- self._table.pack(expand=True, fill='both')
1301
- self._table.focus()
1302
- self._table.bind_to_listboxes('<Double-Button-1>',
1303
- self._download)
1304
- self._table.bind('<space>', self._table_mark)
1305
- self._table.bind('<Return>', self._download)
1306
- self._table.bind('<Left>', self._prev_tab)
1307
- self._table.bind('<Right>', self._next_tab)
1308
- self._table.bind('<Control-a>', self._mark_all)
1309
-
1310
- # Create entry boxes for URL & download_dir
1311
- infoframe.grid_columnconfigure(1, weight=1)
1312
-
1313
- info = [('url', 'Server Index:', self._set_url),
1314
- ('download_dir','Download Directory:',self._set_download_dir)]
1315
- self._info = {}
1316
- for (i, (key, label, callback)) in enumerate(info):
1317
- Label(infoframe, text=label).grid(column=0, row=i, sticky='e')
1318
- entry = Entry(infoframe, font='courier', relief='groove',
1319
- disabledforeground='black')
1320
- self._info[key] = (entry, callback)
1321
- entry.bind('<Return>', self._info_save)
1322
- entry.bind('<Button-1>', lambda e,key=key: self._info_edit(key))
1323
- entry.grid(column=1, row=i, sticky='ew')
1324
-
1325
- # If the user edits url or download_dir, and then clicks outside
1326
- # the entry box, then save their results.
1327
- self.top.bind('<Button-1>', self._info_save)
1328
-
1329
- # Create Download & Refresh buttons.
1330
- self._download_button = Button(
1331
- buttonframe, text='Download', command=self._download, width=8)
1332
- self._download_button.pack(side='left')
1333
- self._refresh_button = Button(
1334
- buttonframe, text='Refresh', command=self._refresh, width=8)
1335
- self._refresh_button.pack(side='right')
1336
-
1337
- # Create Progress bar
1338
- self._progresslabel = Label(progressframe, text='',
1339
- foreground=self._BACKDROP_COLOR[0],
1340
- background=self._BACKDROP_COLOR[1])
1341
- self._progressbar = Canvas(progressframe, width=200, height=16,
1342
- background=self._PROGRESS_COLOR[1],
1343
- relief='sunken', border=1)
1344
- self._init_progressbar()
1345
- self._progressbar.pack(side='right')
1346
- self._progresslabel.pack(side='left')
1347
-
1348
- def _init_menu(self):
1349
- menubar = Menu(self.top)
1350
-
1351
- filemenu = Menu(menubar, tearoff=0)
1352
- filemenu.add_command(label='Download', underline=0,
1353
- command=self._download, accelerator='Return')
1354
- filemenu.add_separator()
1355
- filemenu.add_command(label='Change Server Index', underline=7,
1356
- command=lambda: self._info_edit('url'))
1357
- filemenu.add_command(label='Change Download Directory', underline=0,
1358
- command=lambda: self._info_edit('download_dir'))
1359
- filemenu.add_separator()
1360
- filemenu.add_command(label='Show Log', underline=5,
1361
- command=self._show_log)
1362
- filemenu.add_separator()
1363
- filemenu.add_command(label='Exit', underline=1,
1364
- command=self.destroy, accelerator='Ctrl-x')
1365
- menubar.add_cascade(label='File', underline=0, menu=filemenu)
1366
-
1367
- # Create a menu to control which columns of the table are
1368
- # shown. n.b.: we never hide the first two columns (mark and
1369
- # identifier).
1370
- viewmenu = Menu(menubar, tearoff=0)
1371
- for column in self._table.column_names[2:]:
1372
- var = IntVar(self.top)
1373
- assert column not in self._column_vars
1374
- self._column_vars[column] = var
1375
- if column in self.INITIAL_COLUMNS: var.set(1)
1376
- viewmenu.add_checkbutton(label=column, underline=0, variable=var,
1377
- command=self._select_columns)
1378
- menubar.add_cascade(label='View', underline=0, menu=viewmenu)
1379
-
1380
- # Create a sort menu
1381
- # [xx] this should be selectbuttons; and it should include
1382
- # reversed sorts as options.
1383
- sortmenu = Menu(menubar, tearoff=0)
1384
- for column in self._table.column_names[1:]:
1385
- sortmenu.add_command(label='Sort by %s' % column,
1386
- command=(lambda c=column:
1387
- self._table.sort_by(c, 'ascending')))
1388
- sortmenu.add_separator()
1389
- #sortmenu.add_command(label='Descending Sort:')
1390
- for column in self._table.column_names[1:]:
1391
- sortmenu.add_command(label='Reverse sort by %s' % column,
1392
- command=(lambda c=column:
1393
- self._table.sort_by(c, 'descending')))
1394
- menubar.add_cascade(label='Sort', underline=0, menu=sortmenu)
1395
-
1396
- helpmenu = Menu(menubar, tearoff=0)
1397
- helpmenu.add_command(label='About', underline=0,
1398
- command=self.about)
1399
- helpmenu.add_command(label='Instructions', underline=0,
1400
- command=self.help, accelerator='F1')
1401
- menubar.add_cascade(label='Help', underline=0, menu=helpmenu)
1402
- self.top.bind('<F1>', self.help)
1403
-
1404
- self.top.config(menu=menubar)
1405
-
1406
- def _select_columns(self):
1407
- for (column, var) in self._column_vars.items():
1408
- if var.get():
1409
- self._table.show_column(column)
1410
- else:
1411
- self._table.hide_column(column)
1412
-
1413
- def _refresh(self):
1414
- self._ds.clear_status_cache()
1415
- try:
1416
- self._fill_table()
1417
- except compat.HTTPError as e:
1418
- showerror('Error reading from server', e)
1419
- except compat.URLError as e:
1420
- showerror('Error connecting to server', e.reason)
1421
- self._table.select(0)
1422
-
1423
- def _info_edit(self, info_key):
1424
- self._info_save() # just in case.
1425
- (entry, callback) = self._info[info_key]
1426
- entry['state'] = 'normal'
1427
- entry['relief'] = 'sunken'
1428
- entry.focus()
1429
-
1430
- def _info_save(self, e=None):
1431
- focus = self._table
1432
- for entry, callback in self._info.values():
1433
- if entry['state'] == 'disabled': continue
1434
- if e is not None and e.widget is entry and e.keysym != 'Return':
1435
- focus = entry
1436
- else:
1437
- entry['state'] = 'disabled'
1438
- entry['relief'] = 'groove'
1439
- callback(entry.get())
1440
- focus.focus()
1441
-
1442
- def _table_reprfunc(self, row, col, val):
1443
- if self._table.column_names[col].endswith('Size'):
1444
- if isinstance(val, compat.string_types): return ' %s' % val
1445
- elif val < 1024**2: return ' %.1f KB' % (val/1024.**1)
1446
- elif val < 1024**3: return ' %.1f MB' % (val/1024.**2)
1447
- else: return ' %.1f GB' % (val/1024.**3)
1448
-
1449
- if col in (0, ''): return str(val)
1450
- else: return ' %s' % val
1451
-
1452
- def _set_url(self, url):
1453
- if url == self._ds.url: return
1454
- try:
1455
- self._ds.url = url
1456
- self._fill_table()
1457
- except IOError as e:
1458
- showerror('Error Setting Server Index', str(e))
1459
- self._show_info()
1460
-
1461
-
1462
- def _set_download_dir(self, download_dir):
1463
- if self._ds.download_dir == download_dir: return
1464
- # check if the dir exists, and if not, ask if we should create it?
1465
-
1466
- # Clear our status cache, & re-check what's installed
1467
- self._ds.download_dir = download_dir
1468
- try:
1469
- self._fill_table()
1470
- except compat.HTTPError as e:
1471
- showerror('Error reading from server', e)
1472
- except compat.URLError as e:
1473
- showerror('Error connecting to server', e.reason)
1474
- self._show_info()
1475
-
1476
- def _show_info(self):
1477
- print('showing info', self._ds.url)
1478
- for entry,cb in self._info.values():
1479
- entry['state'] = 'normal'
1480
- entry.delete(0, 'end')
1481
- self._info['url'][0].insert(0, self._ds.url)
1482
- self._info['download_dir'][0].insert(0, self._ds.download_dir)
1483
- for entry,cb in self._info.values():
1484
- entry['state'] = 'disabled'
1485
-
1486
- def _prev_tab(self, *e):
1487
- for i, tab in enumerate(self._tab_names):
1488
- if tab.lower() == self._tab and i > 0:
1489
- self._tab = self._tab_names[i-1].lower()
1490
- try:
1491
- return self._fill_table()
1492
- except compat.HTTPError as e:
1493
- showerror('Error reading from server', e)
1494
- except compat.URLError as e:
1495
- showerror('Error connecting to server', e.reason)
1496
-
1497
- def _next_tab(self, *e):
1498
- for i, tab in enumerate(self._tab_names):
1499
- if tab.lower() == self._tab and i < (len(self._tabs)-1):
1500
- self._tab = self._tab_names[i+1].lower()
1501
- try:
1502
- return self._fill_table()
1503
- except compat.HTTPError as e:
1504
- showerror('Error reading from server', e)
1505
- except compat.URLError as e:
1506
- showerror('Error connecting to server', e.reason)
1507
-
1508
- def _select_tab(self, event):
1509
- self._tab = event.widget['text'].lower()
1510
- try:
1511
- self._fill_table()
1512
- except compat.HTTPError as e:
1513
- showerror('Error reading from server', e)
1514
- except compat.URLError as e:
1515
- showerror('Error connecting to server', e.reason)
1516
-
1517
- _tab = 'collections'
1518
- #_tab = 'corpora'
1519
- _rows = None
1520
- def _fill_table(self):
1521
- selected_row = self._table.selected_row()
1522
- self._table.clear()
1523
- if self._tab == 'all packages':
1524
- items = self._ds.packages()
1525
- elif self._tab == 'corpora':
1526
- items = self._ds.corpora()
1527
- elif self._tab == 'models':
1528
- items = self._ds.models()
1529
- elif self._tab == 'collections':
1530
- items = self._ds.collections()
1531
- else:
1532
- assert 0, 'bad tab value %r' % self._tab
1533
- rows = [self._package_to_columns(item) for item in items]
1534
- self._table.extend(rows)
1535
-
1536
- # Highlight the active tab.
1537
- for tab, label in self._tabs.items():
1538
- if tab == self._tab:
1539
- label.configure(foreground=self._FRONT_TAB_COLOR[0],
1540
- background=self._FRONT_TAB_COLOR[1])
1541
- else:
1542
- label.configure(foreground=self._BACK_TAB_COLOR[0],
1543
- background=self._BACK_TAB_COLOR[1])
1544
-
1545
- self._table.sort_by('Identifier', order='ascending')
1546
- self._color_table()
1547
- self._table.select(selected_row)
1548
-
1549
- # This is a hack, because the scrollbar isn't updating its
1550
- # position right -- I'm not sure what the underlying cause is
1551
- # though. (This is on OS X w/ python 2.5) The length of
1552
- # delay that's necessary seems to depend on how fast the
1553
- # comptuer is. :-/
1554
- self.top.after(150, self._table._scrollbar.set,
1555
- *self._table._mlb.yview())
1556
- self.top.after(300, self._table._scrollbar.set,
1557
- *self._table._mlb.yview())
1558
-
1559
- def _update_table_status(self):
1560
- for row_num in range(len(self._table)):
1561
- status = self._ds.status(self._table[row_num, 'Identifier'])
1562
- self._table[row_num, 'Status'] = status
1563
- self._color_table()
1564
-
1565
- def _download(self, *e):
1566
- # If we're using threads, then delegate to the threaded
1567
- # downloader instead.
1568
- if self._use_threads:
1569
- return self._download_threaded(*e)
1570
-
1571
- marked = [self._table[row, 'Identifier']
1572
- for row in range(len(self._table))
1573
- if self._table[row, 0] != '']
1574
- selection = self._table.selected_row()
1575
- if not marked and selection is not None:
1576
- marked = [self._table[selection, 'Identifier']]
1577
-
1578
- download_iter = self._ds.incr_download(marked, self._ds.download_dir)
1579
- self._log_indent = 0
1580
- self._download_cb(download_iter, marked)
1581
-
1582
- _DL_DELAY=10
1583
- def _download_cb(self, download_iter, ids):
1584
- try: msg = next(download_iter)
1585
- except StopIteration:
1586
- #self._fill_table(sort=False)
1587
- self._update_table_status()
1588
- afterid = self.top.after(10, self._show_progress, 0)
1589
- self._afterid['_download_cb'] = afterid
1590
- return
1591
-
1592
- def show(s):
1593
- self._progresslabel['text'] = s
1594
- self._log(s)
1595
- if isinstance(msg, ProgressMessage):
1596
- self._show_progress(msg.progress)
1597
- elif isinstance(msg, ErrorMessage):
1598
- show(msg.message)
1599
- if msg.package is not None:
1600
- self._select(msg.package.id)
1601
- self._show_progress(None)
1602
- return # halt progress.
1603
- elif isinstance(msg, StartCollectionMessage):
1604
- show('Downloading collection %s' % msg.collection.id)
1605
- self._log_indent += 1
1606
- elif isinstance(msg, StartPackageMessage):
1607
- show('Downloading package %s' % msg.package.id)
1608
- elif isinstance(msg, UpToDateMessage):
1609
- show('Package %s is up-to-date!' % msg.package.id)
1610
- #elif isinstance(msg, StaleMessage):
1611
- # show('Package %s is out-of-date or corrupt' % msg.package.id)
1612
- elif isinstance(msg, FinishDownloadMessage):
1613
- show('Finished downloading %r.' % msg.package.id)
1614
- elif isinstance(msg, StartUnzipMessage):
1615
- show('Unzipping %s' % msg.package.filename)
1616
- elif isinstance(msg, FinishCollectionMessage):
1617
- self._log_indent -= 1
1618
- show('Finished downloading collection %r.' % msg.collection.id)
1619
- self._clear_mark(msg.collection.id)
1620
- elif isinstance(msg, FinishPackageMessage):
1621
- self._clear_mark(msg.package.id)
1622
- afterid = self.top.after(self._DL_DELAY, self._download_cb,
1623
- download_iter, ids)
1624
- self._afterid['_download_cb'] = afterid
1625
-
1626
- def _select(self, id):
1627
- for row in range(len(self._table)):
1628
- if self._table[row, 'Identifier'] == id:
1629
- self._table.select(row)
1630
- return
1631
-
1632
- def _color_table(self):
1633
- # Color rows according to status.
1634
- for row in range(len(self._table)):
1635
- bg, sbg = self._ROW_COLOR[self._table[row, 'Status']]
1636
- fg, sfg = ('black', 'white')
1637
- self._table.rowconfig(row, foreground=fg, selectforeground=sfg,
1638
- background=bg, selectbackground=sbg)
1639
- # Color the marked column
1640
- self._table.itemconfigure(row, 0,
1641
- foreground=self._MARK_COLOR[0],
1642
- background=self._MARK_COLOR[1])
1643
-
1644
-
1645
- def _clear_mark(self, id):
1646
- for row in range(len(self._table)):
1647
- if self._table[row, 'Identifier'] == id:
1648
- self._table[row, 0] = ''
1649
-
1650
- def _mark_all(self, *e):
1651
- for row in range(len(self._table)):
1652
- self._table[row,0] = 'X'
1653
-
1654
- def _table_mark(self, *e):
1655
- selection = self._table.selected_row()
1656
- if selection >= 0:
1657
- if self._table[selection][0] != '':
1658
- self._table[selection,0] = ''
1659
- else:
1660
- self._table[selection,0] = 'X'
1661
- self._table.select(delta=1)
1662
-
1663
- def _show_log(self):
1664
- text = '\n'.join(self._log_messages)
1665
- ShowText(self.top, 'NLTK Downloader Log', text)
1666
-
1667
- def _package_to_columns(self, pkg):
1668
- """
1669
- Given a package, return a list of values describing that
1670
- package, one for each column in ``self.COLUMNS``.
1671
- """
1672
- row = []
1673
- for column_index, column_name in enumerate(self.COLUMNS):
1674
- if column_index == 0: # Mark:
1675
- row.append('')
1676
- elif column_name == 'Identifier':
1677
- row.append(pkg.id)
1678
- elif column_name == 'Status':
1679
- row.append(self._ds.status(pkg))
1680
- else:
1681
- attr = column_name.lower().replace(' ', '_')
1682
- row.append(getattr(pkg, attr, 'n/a'))
1683
- return row
1684
-
1685
- #/////////////////////////////////////////////////////////////////
1686
- # External Interface
1687
- #/////////////////////////////////////////////////////////////////
1688
-
1689
- def destroy(self, *e):
1690
- if self._destroyed: return
1691
- self.top.destroy()
1692
- self._destroyed = True
1693
-
1694
- def _destroy(self, *e):
1695
- if self.top is not None:
1696
- for afterid in self._afterid.values():
1697
- self.top.after_cancel(afterid)
1698
-
1699
- # Abort any download in progress.
1700
- if self._downloading and self._use_threads:
1701
- self._abort_download()
1702
-
1703
- # Make sure the garbage collector destroys these now;
1704
- # otherwise, they may get destroyed when we're not in the main
1705
- # thread, which would make Tkinter unhappy.
1706
- self._column_vars.clear()
1707
-
1708
- def mainloop(self, *args, **kwargs):
1709
- self.top.mainloop(*args, **kwargs)
1710
-
1711
- #/////////////////////////////////////////////////////////////////
1712
- # HELP
1713
- #/////////////////////////////////////////////////////////////////
1714
-
1715
- HELP = textwrap.dedent("""\
1716
- This tool can be used to download a variety of corpora and models
1717
- that can be used with NLTK. Each corpus or model is distributed
1718
- in a single zip file, known as a \"package file.\" You can
1719
- download packages individually, or you can download pre-defined
1720
- collections of packages.
1721
-
1722
- When you download a package, it will be saved to the \"download
1723
- directory.\" A default download directory is chosen when you run
1724
-
1725
- the downloader; but you may also select a different download
1726
- directory. On Windows, the default download directory is
1727
-
1728
-
1729
- \"package.\"
1730
-
1731
- The NLTK downloader can be used to download a variety of corpora,
1732
- models, and other data packages.
1733
-
1734
- Keyboard shortcuts::
1735
- [return]\t Download
1736
- [up]\t Select previous package
1737
- [down]\t Select next package
1738
- [left]\t Select previous tab
1739
- [right]\t Select next tab
1740
- """)
1741
-
1742
- def help(self, *e):
1743
- # The default font's not very legible; try using 'fixed' instead.
1744
- try:
1745
- ShowText(self.top, 'Help: NLTK Dowloader',
1746
- self.HELP.strip(), width=75, font='fixed')
1747
- except:
1748
- ShowText(self.top, 'Help: NLTK Downloader',
1749
- self.HELP.strip(), width=75)
1750
-
1751
- def about(self, *e):
1752
- ABOUT = ("NLTK Downloader\n"+
1753
- "Written by Edward Loper")
1754
- TITLE = 'About: NLTK Downloader'
1755
- try:
1756
- from tkMessageBox import Message
1757
- Message(message=ABOUT, title=TITLE).show()
1758
- except ImportError:
1759
- try:
1760
- from tkinter.messagebox import Message
1761
- Message(message=ABOUT, title=TITLE).show()
1762
- except ImportError:
1763
- ShowText(self.top, TITLE, ABOUT)
1764
-
1765
- #/////////////////////////////////////////////////////////////////
1766
- # Progress Bar
1767
- #/////////////////////////////////////////////////////////////////
1768
-
1769
- _gradient_width = 5
1770
- def _init_progressbar(self):
1771
- c = self._progressbar
1772
- width, height = int(c['width']), int(c['height'])
1773
- for i in range(0, (int(c['width'])*2)//self._gradient_width):
1774
- c.create_line(i*self._gradient_width+20, -20,
1775
- i*self._gradient_width-height-20, height+20,
1776
- width=self._gradient_width,
1777
- fill='#%02x0000' % (80 + abs(i%6-3)*12))
1778
- c.addtag_all('gradient')
1779
- c.itemconfig('gradient', state='hidden')
1780
-
1781
- # This is used to display progress
1782
- c.addtag_withtag('redbox', c.create_rectangle(
1783
- 0, 0, 0, 0, fill=self._PROGRESS_COLOR[0]))
1784
-
1785
- def _show_progress(self, percent):
1786
- c = self._progressbar
1787
- if percent is None:
1788
- c.coords('redbox', 0, 0, 0, 0)
1789
- c.itemconfig('gradient', state='hidden')
1790
- else:
1791
- width, height = int(c['width']), int(c['height'])
1792
- x = percent * int(width) // 100 + 1
1793
- c.coords('redbox', 0, 0, x, height+1)
1794
-
1795
- def _progress_alive(self):
1796
- c = self._progressbar
1797
- if not self._downloading:
1798
- c.itemconfig('gradient', state='hidden')
1799
- else:
1800
- c.itemconfig('gradient', state='normal')
1801
- x1, y1, x2, y2 = c.bbox('gradient')
1802
- if x1 <= -100:
1803
- c.move('gradient', (self._gradient_width*6)-4, 0)
1804
- else:
1805
- c.move('gradient', -4, 0)
1806
- afterid = self.top.after(200, self._progress_alive)
1807
- self._afterid['_progress_alive'] = afterid
1808
-
1809
- #/////////////////////////////////////////////////////////////////
1810
- # Threaded downloader
1811
- #/////////////////////////////////////////////////////////////////
1812
-
1813
- def _download_threaded(self, *e):
1814
- # If the user tries to start a new download while we're already
1815
- # downloading something, then abort the current download instead.
1816
- if self._downloading:
1817
- self._abort_download()
1818
- return
1819
-
1820
- # Change the 'download' button to an 'abort' button.
1821
- self._download_button['text'] = 'Cancel'
1822
-
1823
- marked = [self._table[row, 'Identifier']
1824
- for row in range(len(self._table))
1825
- if self._table[row, 0] != '']
1826
- selection = self._table.selected_row()
1827
- if not marked and selection is not None:
1828
- marked = [self._table[selection, 'Identifier']]
1829
-
1830
- # Create a new data server object for the download operation,
1831
- # just in case the user modifies our data server during the
1832
- # download (e.g., clicking 'refresh' or editing the index url).
1833
- ds = Downloader(self._ds.url, self._ds.download_dir)
1834
-
1835
- # Start downloading in a separate thread.
1836
- assert self._download_msg_queue == []
1837
- assert self._download_abort_queue == []
1838
- self._DownloadThread(ds, marked, self._download_lock,
1839
- self._download_msg_queue,
1840
- self._download_abort_queue).start()
1841
-
1842
- # Monitor the download message queue & display its progress.
1843
- self._log_indent = 0
1844
- self._downloading = True
1845
- self._monitor_message_queue()
1846
-
1847
- # Display an indication that we're still alive and well by
1848
- # cycling the progress bar.
1849
- self._progress_alive()
1850
-
1851
- def _abort_download(self):
1852
- if self._downloading:
1853
- self._download_lock.acquire()
1854
- self._download_abort_queue.append('abort')
1855
- self._download_lock.release()
1856
-
1857
- class _DownloadThread(threading.Thread):
1858
- def __init__(self, data_server, items, lock, message_queue, abort):
1859
- self.data_server = data_server
1860
- self.items = items
1861
- self.lock = lock
1862
- self.message_queue = message_queue
1863
- self.abort = abort
1864
- threading.Thread.__init__(self)
1865
-
1866
- def run (self):
1867
- for msg in self.data_server.incr_download(self.items):
1868
- self.lock.acquire()
1869
- self.message_queue.append(msg)
1870
- # Check if we've been told to kill ourselves:
1871
- if self.abort:
1872
- self.message_queue.append('aborted')
1873
- self.lock.release()
1874
- return
1875
- self.lock.release()
1876
- self.lock.acquire()
1877
- self.message_queue.append('finished')
1878
- self.lock.release()
1879
-
1880
- _MONITOR_QUEUE_DELAY=100
1881
- def _monitor_message_queue(self):
1882
- def show(s):
1883
- self._progresslabel['text'] = s
1884
- self._log(s)
1885
-
1886
- # Try to acquire the lock; if it's busy, then just try again later.
1887
- if not self._download_lock.acquire():
1888
- return
1889
- for msg in self._download_msg_queue:
1890
-
1891
- # Done downloading?
1892
- if msg == 'finished' or msg == 'aborted':
1893
- #self._fill_table(sort=False)
1894
- self._update_table_status()
1895
- self._downloading = False
1896
- self._download_button['text'] = 'Download'
1897
- del self._download_msg_queue[:]
1898
- del self._download_abort_queue[:]
1899
- self._download_lock.release()
1900
- if msg == 'aborted':
1901
- show('Download aborted!')
1902
- self._show_progress(None)
1903
- else:
1904
- afterid = self.top.after(100, self._show_progress, None)
1905
- self._afterid['_monitor_message_queue'] = afterid
1906
- return
1907
-
1908
- # All other messages
1909
- elif isinstance(msg, ProgressMessage):
1910
- self._show_progress(msg.progress)
1911
- elif isinstance(msg, ErrorMessage):
1912
- show(msg.message)
1913
- if msg.package is not None:
1914
- self._select(msg.package.id)
1915
- self._show_progress(None)
1916
- self._downloading = False
1917
- return # halt progress.
1918
- elif isinstance(msg, StartCollectionMessage):
1919
- show('Downloading collection %r' % msg.collection.id)
1920
- self._log_indent += 1
1921
- elif isinstance(msg, StartPackageMessage):
1922
- self._ds.clear_status_cache(msg.package.id)
1923
- show('Downloading package %r' % msg.package.id)
1924
- elif isinstance(msg, UpToDateMessage):
1925
- show('Package %s is up-to-date!' % msg.package.id)
1926
- #elif isinstance(msg, StaleMessage):
1927
- # show('Package %s is out-of-date or corrupt; updating it' %
1928
- # msg.package.id)
1929
- elif isinstance(msg, FinishDownloadMessage):
1930
- show('Finished downloading %r.' % msg.package.id)
1931
- elif isinstance(msg, StartUnzipMessage):
1932
- show('Unzipping %s' % msg.package.filename)
1933
- elif isinstance(msg, FinishUnzipMessage):
1934
- show('Finished installing %s' % msg.package.id)
1935
- elif isinstance(msg, FinishCollectionMessage):
1936
- self._log_indent -= 1
1937
- show('Finished downloading collection %r.' % msg.collection.id)
1938
- self._clear_mark(msg.collection.id)
1939
- elif isinstance(msg, FinishPackageMessage):
1940
- self._update_table_status()
1941
- self._clear_mark(msg.package.id)
1942
-
1943
- # Let the user know when we're aborting a download (but
1944
- # waiting for a good point to abort it, so we don't end up
1945
- # with a partially unzipped package or anything like that).
1946
- if self._download_abort_queue:
1947
- self._progresslabel['text'] = 'Aborting download...'
1948
-
1949
- # Clear the message queue and then release the lock
1950
- del self._download_msg_queue[:]
1951
- self._download_lock.release()
1952
-
1953
- # Check the queue again after MONITOR_QUEUE_DELAY msec.
1954
- afterid = self.top.after(self._MONITOR_QUEUE_DELAY,
1955
- self._monitor_message_queue)
1956
- self._afterid['_monitor_message_queue'] = afterid
1957
-
1958
- ######################################################################
1959
- # Helper Functions
1960
- ######################################################################
1961
- # [xx] It may make sense to move these to nltk.internals.
1962
-
1963
- def md5_hexdigest(file):
1964
- """
1965
- Calculate and return the MD5 checksum for a given file.
1966
- ``file`` may either be a filename or an open stream.
1967
- """
1968
- if isinstance(file, compat.string_types):
1969
- with open(file, 'rb') as infile:
1970
- return _md5_hexdigest(infile)
1971
- return _md5_hexdigest(file)
1972
-
1973
- def _md5_hexdigest(fp):
1974
- md5_digest = md5()
1975
- while True:
1976
- block = fp.read(1024*16) # 16k blocks
1977
- if not block: break
1978
- md5_digest.update(block)
1979
- return md5_digest.hexdigest()
1980
-
1981
-
1982
- # change this to periodically yield progress messages?
1983
- # [xx] get rid of topdir parameter -- we should be checking
1984
- # this when we build the index, anyway.
1985
- def unzip(filename, root, verbose=True):
1986
- """
1987
- Extract the contents of the zip file ``filename`` into the
1988
- directory ``root``.
1989
- """
1990
- for message in _unzip_iter(filename, root, verbose):
1991
- if isinstance(message, ErrorMessage):
1992
- raise Exception(message)
1993
-
1994
- def _unzip_iter(filename, root, verbose=True):
1995
- if verbose:
1996
- sys.stdout.write('Unzipping %s' % os.path.split(filename)[1])
1997
- sys.stdout.flush()
1998
-
1999
- try: zf = zipfile.ZipFile(filename)
2000
- except zipfile.error as e:
2001
- yield ErrorMessage(filename, 'Error with downloaded zip file')
2002
- return
2003
- except Exception as e:
2004
- yield ErrorMessage(filename, e)
2005
- return
2006
-
2007
- # Get lists of directories & files
2008
- namelist = zf.namelist()
2009
- dirlist = set()
2010
- for x in namelist:
2011
- if x.endswith('/'):
2012
- dirlist.add(x)
2013
- else:
2014
- dirlist.add(x.rsplit('/',1)[0] + '/')
2015
- filelist = [x for x in namelist if not x.endswith('/')]
2016
-
2017
- # Create the target directory if it doesn't exist
2018
- if not os.path.exists(root):
2019
- os.mkdir(root)
2020
-
2021
- # Create the directory structure
2022
- for dirname in sorted(dirlist):
2023
- pieces = dirname[:-1].split('/')
2024
- for i in range(len(pieces)):
2025
- dirpath = os.path.join(root, *pieces[:i+1])
2026
- if not os.path.exists(dirpath):
2027
- os.mkdir(dirpath)
2028
-
2029
- # Extract files.
2030
- for i, filename in enumerate(filelist):
2031
- filepath = os.path.join(root, *filename.split('/'))
2032
-
2033
- with open(filepath, 'wb') as outfile:
2034
- try:
2035
- contents = zf.read(filename)
2036
- except Exception as e:
2037
- yield ErrorMessage(filename, e)
2038
- return
2039
- outfile.write(contents)
2040
-
2041
- if verbose and (i*10/len(filelist) > (i-1)*10/len(filelist)):
2042
- sys.stdout.write('.')
2043
- sys.stdout.flush()
2044
- if verbose:
2045
- print()
2046
-
2047
- ######################################################################
2048
- # Index Builder
2049
- ######################################################################
2050
- # This may move to a different file sometime.
2051
- import subprocess, zipfile
2052
-
2053
- def build_index(root, base_url):
2054
- """
2055
- Create a new data.xml index file, by combining the xml description
2056
- files for various packages and collections. ``root`` should be the
2057
- path to a directory containing the package xml and zip files; and
2058
- the collection xml files. The ``root`` directory is expected to
2059
- have the following subdirectories::
2060
-
2061
- root/
2062
- packages/ .................. subdirectory for packages
2063
- corpora/ ................. zip & xml files for corpora
2064
- grammars/ ................ zip & xml files for grammars
2065
- taggers/ ................. zip & xml files for taggers
2066
- tokenizers/ .............. zip & xml files for tokenizers
2067
- etc.
2068
- collections/ ............... xml files for collections
2069
-
2070
- For each package, there should be two files: ``package.zip``
2071
- (where *package* is the package name)
2072
- which contains the package itself as a compressed zip file; and
2073
- ``package.xml``, which is an xml description of the package. The
2074
- zipfile ``package.zip`` should expand to a single subdirectory
2075
- named ``package/``. The base filename ``package`` must match
2076
- the identifier given in the package's xml file.
2077
-
2078
- For each collection, there should be a single file ``collection.zip``
2079
- describing the collection, where *collection* is the name of the collection.
2080
-
2081
- All identifiers (for both packages and collections) must be unique.
2082
- """
2083
- # Find all packages.
2084
- packages = []
2085
- for pkg_xml, zf, subdir in _find_packages(os.path.join(root, 'packages')):
2086
- zipstat = os.stat(zf.filename)
2087
- url = '%s/%s/%s' % (base_url, subdir, os.path.split(zf.filename)[1])
2088
- unzipped_size = sum(zf_info.file_size for zf_info in zf.infolist())
2089
-
2090
- # Fill in several fields of the package xml with calculated values.
2091
- pkg_xml.set('unzipped_size', '%s' % unzipped_size)
2092
- pkg_xml.set('size', '%s' % zipstat.st_size)
2093
- pkg_xml.set('checksum', '%s' % md5_hexdigest(zf.filename))
2094
- pkg_xml.set('subdir', subdir)
2095
- #pkg_xml.set('svn_revision', _svn_revision(zf.filename))
2096
- if not pkg_xml.get('url'):
2097
- pkg_xml.set('url', url)
2098
-
2099
- # Record the package.
2100
- packages.append(pkg_xml)
2101
-
2102
- # Find all collections
2103
- collections = list(_find_collections(os.path.join(root, 'collections')))
2104
-
2105
- # Check that all UIDs are unique
2106
- uids = set()
2107
- for item in packages+collections:
2108
- if item.get('id') in uids:
2109
- raise ValueError('Duplicate UID: %s' % item.get('id'))
2110
- uids.add(item.get('id'))
2111
-
2112
- # Put it all together
2113
- top_elt = ElementTree.Element('nltk_data')
2114
- top_elt.append(ElementTree.Element('packages'))
2115
- for package in packages: top_elt[0].append(package)
2116
- top_elt.append(ElementTree.Element('collections'))
2117
- for collection in collections: top_elt[1].append(collection)
2118
-
2119
- _indent_xml(top_elt)
2120
- return top_elt
2121
-
2122
- def _indent_xml(xml, prefix=''):
2123
- """
2124
- Helper for ``build_index()``: Given an XML ``ElementTree``, modify it
2125
- (and its descendents) ``text`` and ``tail`` attributes to generate
2126
- an indented tree, where each nested element is indented by 2
2127
- spaces with respect to its parent.
2128
- """
2129
- if len(xml) > 0:
2130
- xml.text = (xml.text or '').strip() + '\n' + prefix + ' '
2131
- for child in xml:
2132
- _indent_xml(child, prefix+' ')
2133
- for child in xml[:-1]:
2134
- child.tail = (child.tail or '').strip() + '\n' + prefix + ' '
2135
- xml[-1].tail = (xml[-1].tail or '').strip() + '\n' + prefix
2136
-
2137
- def _check_package(pkg_xml, zipfilename, zf):
2138
- """
2139
- Helper for ``build_index()``: Perform some checks to make sure that
2140
- the given package is consistent.
2141
- """
2142
- # The filename must patch the id given in the XML file.
2143
- uid = os.path.splitext(os.path.split(zipfilename)[1])[0]
2144
- if pkg_xml.get('id') != uid:
2145
- raise ValueError('package identifier mismatch (%s vs %s)' %
2146
- (pkg_xml.get('id'), uid))
2147
-
2148
- # Zip file must expand to a subdir whose name matches uid.
2149
- if sum( (name!=uid and not name.startswith(uid+'/'))
2150
- for name in zf.namelist() ):
2151
- raise ValueError('Zipfile %s.zip does not expand to a single '
2152
- 'subdirectory %s/' % (uid, uid))
2153
-
2154
- # update for git?
2155
- def _svn_revision(filename):
2156
- """
2157
- Helper for ``build_index()``: Calculate the subversion revision
2158
- number for a given file (by using ``subprocess`` to run ``svn``).
2159
- """
2160
- p = subprocess.Popen(['svn', 'status', '-v', filename],
2161
- stdout=subprocess.PIPE,
2162
- stderr=subprocess.PIPE)
2163
- (stdout, stderr) = p.communicate()
2164
- if p.returncode != 0 or stderr or not stdout:
2165
- raise ValueError('Error determining svn_revision for %s: %s' %
2166
- (os.path.split(filename)[1], textwrap.fill(stderr)))
2167
- return stdout.split()[2]
2168
-
2169
- def _find_collections(root):
2170
- """
2171
- Helper for ``build_index()``: Yield a list of ElementTree.Element
2172
- objects, each holding the xml for a single package collection.
2173
- """
2174
- packages = []
2175
- for dirname, subdirs, files in os.walk(root):
2176
- for filename in files:
2177
- if filename.endswith('.xml'):
2178
- xmlfile = os.path.join(dirname, filename)
2179
- yield ElementTree.parse(xmlfile).getroot()
2180
-
2181
- def _find_packages(root):
2182
- """
2183
- Helper for ``build_index()``: Yield a list of tuples
2184
- ``(pkg_xml, zf, subdir)``, where:
2185
- - ``pkg_xml`` is an ``ElementTree.Element`` holding the xml for a
2186
- package
2187
- - ``zf`` is a ``zipfile.ZipFile`` for the package's contents.
2188
- - ``subdir`` is the subdirectory (relative to ``root``) where
2189
- the package was found (e.g. 'corpora' or 'grammars').
2190
- """
2191
- from nltk.corpus.reader.util import _path_from
2192
- # Find all packages.
2193
- packages = []
2194
- for dirname, subdirs, files in os.walk(root):
2195
- relpath = '/'.join(_path_from(root, dirname))
2196
- for filename in files:
2197
- if filename.endswith('.xml'):
2198
- xmlfilename = os.path.join(dirname, filename)
2199
- zipfilename = xmlfilename[:-4]+'.zip'
2200
- try: zf = zipfile.ZipFile(zipfilename)
2201
- except Exception as e:
2202
- raise ValueError('Error reading file %r!\n%s' %
2203
- (zipfilename, e))
2204
- try: pkg_xml = ElementTree.parse(xmlfilename).getroot()
2205
- except Exception as e:
2206
- raise ValueError('Error reading file %r!\n%s' %
2207
- (xmlfilename, e))
2208
-
2209
- # Check that the UID matches the filename
2210
- uid = os.path.split(xmlfilename[:-4])[1]
2211
- if pkg_xml.get('id') != uid:
2212
- raise ValueError('package identifier mismatch (%s '
2213
- 'vs %s)' % (pkg_xml.get('id'), uid))
2214
-
2215
- # Check that the zipfile expands to a subdir whose
2216
- # name matches the uid.
2217
- if sum( (name!=uid and not name.startswith(uid+'/'))
2218
- for name in zf.namelist() ):
2219
- raise ValueError('Zipfile %s.zip does not expand to a '
2220
- 'single subdirectory %s/' % (uid, uid))
2221
-
2222
- yield pkg_xml, zf, relpath
2223
- # Don't recurse into svn subdirectories:
2224
- try: subdirs.remove('.svn')
2225
- except ValueError: pass
2226
-
2227
- ######################################################################
2228
- # Main:
2229
- ######################################################################
2230
-
2231
- # There should be a command-line interface
2232
-
2233
- # Aliases
2234
- _downloader = Downloader()
2235
- download = _downloader.download
2236
-
2237
- def download_shell():
2238
- DownloaderShell(_downloader).run()
2239
-
2240
- def download_gui():
2241
- DownloaderGUI(_downloader).mainloop()
2242
-
2243
- def update():
2244
- _downloader.update()
2245
-
2246
- if __name__ == '__main__':
2247
- from optparse import OptionParser
2248
- parser = OptionParser()
2249
- parser.add_option("-d", "--dir", dest="dir",
2250
- help="download package to directory DIR", metavar="DIR")
2251
- parser.add_option("-q", "--quiet", dest="quiet", action="store_true",
2252
- default=False, help="work quietly")
2253
- parser.add_option("-f", "--force", dest="force", action="store_true",
2254
- default=False, help="download even if already installed")
2255
- parser.add_option("-e", "--exit-on-error", dest="halt_on_error", action="store_true",
2256
- default=False, help="exit if an error occurs")
2257
- parser.add_option("-u", "--url", dest="server_index_url",
2258
- default=None, help="download server index url")
2259
-
2260
- (options, args) = parser.parse_args()
2261
-
2262
- downloader = Downloader(server_index_url = options.server_index_url)
2263
-
2264
- if args:
2265
- for pkg_id in args:
2266
- rv = downloader.download(info_or_id=pkg_id, download_dir=options.dir,
2267
- quiet=options.quiet, force=options.force,
2268
- halt_on_error=options.halt_on_error)
2269
- if rv==False and options.halt_on_error:
2270
- break
2271
- else:
2272
- downloader.download(download_dir=options.dir,
2273
- quiet=options.quiet, force=options.force,
2274
- halt_on_error=options.halt_on_error)
1
+ # Natural Language Toolkit: Corpus & Model Downloader
2
+ #
3
+ # Copyright (C) 2001-2018 NLTK Project
4
+ # Author: Edward Loper <edloper@gmail.com>
5
+ # URL: <http://nltk.org/>
6
+ # For license information, see LICENSE.TXT
7
+
8
+ """
9
+ The NLTK corpus and module downloader. This module defines several
10
+ interfaces which can be used to download corpora, models, and other
11
+ data packages that can be used with NLTK.
12
+
13
+ Downloading Packages
14
+ ====================
15
+ If called with no arguments, ``download()`` will display an interactive
16
+ interface which can be used to download and install new packages.
17
+ If Tkinter is available, then a graphical interface will be shown,
18
+ otherwise a simple text interface will be provided.
19
+
20
+ Individual packages can be downloaded by calling the ``download()``
21
+ function with a single argument, giving the package identifier for the
22
+ package that should be downloaded:
23
+
24
+ >>> download('treebank') # doctest: +SKIP
25
+ [nltk_data] Downloading package 'treebank'...
26
+ [nltk_data] Unzipping corpora/treebank.zip.
27
+
28
+ NLTK also provides a number of \"package collections\", consisting of
29
+ a group of related packages. To download all packages in a
30
+ colleciton, simply call ``download()`` with the collection's
31
+ identifier:
32
+
33
+ >>> download('all-corpora') # doctest: +SKIP
34
+ [nltk_data] Downloading package 'abc'...
35
+ [nltk_data] Unzipping corpora/abc.zip.
36
+ [nltk_data] Downloading package 'alpino'...
37
+ [nltk_data] Unzipping corpora/alpino.zip.
38
+ ...
39
+ [nltk_data] Downloading package 'words'...
40
+ [nltk_data] Unzipping corpora/words.zip.
41
+
42
+ Download Directory
43
+ ==================
44
+ By default, packages are installed in either a system-wide directory
45
+ (if Python has sufficient access to write to it); or in the current
46
+ user's home directory. However, the ``download_dir`` argument may be
47
+ used to specify a different installation target, if desired.
48
+
49
+ See ``Downloader.default_download_dir()`` for more a detailed
50
+ description of how the default download directory is chosen.
51
+
52
+ NLTK Download Server
53
+ ====================
54
+ Before downloading any packages, the corpus and module downloader
55
+ contacts the NLTK download server, to retrieve an index file
56
+ describing the available packages. By default, this index file is
57
+ loaded from ``https://raw.githubusercontent.com/nltk/nltk_data/gh-pages/index.xml``.
58
+ If necessary, it is possible to create a new ``Downloader`` object,
59
+ specifying a different URL for the package index file.
60
+
61
+ Usage::
62
+
63
+ python nltk/downloader.py [-d DATADIR] [-q] [-f] [-k] PACKAGE_IDS
64
+
65
+ or::
66
+
67
+ python -m nltk.downloader [-d DATADIR] [-q] [-f] [-k] PACKAGE_IDS
68
+ """
69
+ #----------------------------------------------------------------------
70
+ from __future__ import print_function, division, unicode_literals
71
+
72
+ """
73
+
74
+ 0 1 2 3
75
+ [label][----][label][----]
76
+ [column ][column ]
77
+
78
+ Notes
79
+ =====
80
+ Handling data files.. Some questions:
81
+
82
+ * Should the data files be kept zipped or unzipped? I say zipped.
83
+
84
+ * Should the data files be kept in svn at all? Advantages: history;
85
+ automatic version numbers; 'svn up' could be used rather than the
86
+ downloader to update the corpora. Disadvantages: they're big,
87
+ which makes working from svn a bit of a pain. And we're planning
88
+ to potentially make them much bigger. I don't think we want
89
+ people to have to download 400MB corpora just to use nltk from svn.
90
+
91
+ * Compromise: keep the data files in trunk/data rather than in
92
+ trunk/nltk. That way you can check them out in svn if you want
93
+ to; but you don't need to, and you can use the downloader instead.
94
+
95
+ * Also: keep models in mind. When we change the code, we'd
96
+ potentially like the models to get updated. This could require a
97
+ little thought.
98
+
99
+ * So.. let's assume we have a trunk/data directory, containing a bunch
100
+ of packages. The packages should be kept as zip files, because we
101
+ really shouldn't be editing them much (well -- we may edit models
102
+ more, but they tend to be binary-ish files anyway, where diffs
103
+ aren't that helpful). So we'll have trunk/data, with a bunch of
104
+ files like abc.zip and treebank.zip and propbank.zip. For each
105
+ package we could also have eg treebank.xml and propbank.xml,
106
+ describing the contents of the package (name, copyright, license,
107
+ etc). Collections would also have .xml files. Finally, we would
108
+ pull all these together to form a single index.xml file. Some
109
+ directory structure wouldn't hurt. So how about::
110
+
111
+ /trunk/data/ ....................... root of data svn
112
+ index.xml ........................ main index file
113
+ src/ ............................. python scripts
114
+ packages/ ........................ dir for packages
115
+ corpora/ ....................... zip & xml files for corpora
116
+ grammars/ ...................... zip & xml files for grammars
117
+ taggers/ ....................... zip & xml files for taggers
118
+ tokenizers/ .................... zip & xml files for tokenizers
119
+ etc.
120
+ collections/ ..................... xml files for collections
121
+
122
+ Where the root (/trunk/data) would contain a makefile; and src/
123
+ would contain a script to update the info.xml file. It could also
124
+ contain scripts to rebuild some of the various model files. The
125
+ script that builds index.xml should probably check that each zip
126
+ file expands entirely into a single subdir, whose name matches the
127
+ package's uid.
128
+
129
+ Changes I need to make:
130
+ - in index: change "size" to "filesize" or "compressed-size"
131
+ - in index: add "unzipped-size"
132
+ - when checking status: check both compressed & uncompressed size.
133
+ uncompressed size is important to make sure we detect a problem
134
+ if something got partially unzipped. define new status values
135
+ to differentiate stale vs corrupt vs corruptly-uncompressed??
136
+ (we shouldn't need to re-download the file if the zip file is ok
137
+ but it didn't get uncompressed fully.)
138
+ - add other fields to the index: author, license, copyright, contact,
139
+ etc.
140
+
141
+ the current grammars/ package would become a single new package (eg
142
+ toy-grammars or book-grammars).
143
+
144
+ xml file should have:
145
+ - authorship info
146
+ - license info
147
+ - copyright info
148
+ - contact info
149
+ - info about what type of data/annotation it contains?
150
+ - recommended corpus reader?
151
+
152
+ collections can contain other collections. they can also contain
153
+ multiple package types (corpora & models). Have a single 'basics'
154
+ package that includes everything we talk about in the book?
155
+
156
+ n.b.: there will have to be a fallback to the punkt tokenizer, in case
157
+ they didn't download that model.
158
+
159
+ default: unzip or not?
160
+
161
+ """
162
+ import time, os, zipfile, sys, textwrap, threading, itertools, shutil
163
+ from hashlib import md5
164
+
165
+ try:
166
+ TKINTER = True
167
+ from six.moves.tkinter import (Tk, Frame, Label, Entry, Button, Canvas,
168
+ Menu, IntVar, TclError)
169
+ from six.moves.tkinter_messagebox import showerror
170
+ from nltk.draw.table import Table
171
+ from nltk.draw.util import ShowText
172
+ except:
173
+ TKINTER = False
174
+ TclError = ValueError
175
+
176
+ from xml.etree import ElementTree
177
+
178
+ from six import string_types, text_type
179
+ from six.moves import input
180
+ from six.moves.urllib.request import urlopen
181
+ from six.moves.urllib.error import HTTPError, URLError
182
+
183
+ import nltk
184
+ from nltk.compat import python_2_unicode_compatible
185
+ #urllib2 = nltk.internals.import_from_stdlib('urllib2')
186
+
187
+
188
+ ######################################################################
189
+ # Directory entry objects (from the data server's index file)
190
+ ######################################################################
191
+
192
+ @python_2_unicode_compatible
193
+ class Package(object):
194
+ """
195
+ A directory entry for a downloadable package. These entries are
196
+ extracted from the XML index file that is downloaded by
197
+ ``Downloader``. Each package consists of a single file; but if
198
+ that file is a zip file, then it can be automatically decompressed
199
+ when the package is installed.
200
+ """
201
+ def __init__(self, id, url, name=None, subdir='',
202
+ size=None, unzipped_size=None,
203
+ checksum=None, svn_revision=None,
204
+ copyright='Unknown', contact='Unknown',
205
+ license='Unknown', author='Unknown',
206
+ unzip=True,
207
+ **kw):
208
+ self.id = id
209
+ """A unique identifier for this package."""
210
+
211
+ self.name = name or id
212
+ """A string name for this package."""
213
+
214
+ self.subdir = subdir
215
+ """The subdirectory where this package should be installed.
216
+ E.g., ``'corpora'`` or ``'taggers'``."""
217
+
218
+ self.url = url
219
+ """A URL that can be used to download this package's file."""
220
+
221
+ self.size = int(size)
222
+ """The filesize (in bytes) of the package file."""
223
+
224
+ self.unzipped_size = int(unzipped_size)
225
+ """The total filesize of the files contained in the package's
226
+ zipfile."""
227
+
228
+ self.checksum = checksum
229
+ """The MD-5 checksum of the package file."""
230
+
231
+ self.svn_revision = svn_revision
232
+ """A subversion revision number for this package."""
233
+
234
+ self.copyright = copyright
235
+ """Copyright holder for this package."""
236
+
237
+ self.contact = contact
238
+ """Name & email of the person who should be contacted with
239
+ questions about this package."""
240
+
241
+ self.license = license
242
+ """License information for this package."""
243
+
244
+ self.author = author
245
+ """Author of this package."""
246
+
247
+ ext = os.path.splitext(url.split('/')[-1])[1]
248
+ self.filename = os.path.join(subdir, id+ext)
249
+ """The filename that should be used for this package's file. It
250
+ is formed by joining ``self.subdir`` with ``self.id``, and
251
+ using the same extension as ``url``."""
252
+
253
+ self.unzip = bool(int(unzip)) # '0' or '1'
254
+ """A flag indicating whether this corpus should be unzipped by
255
+ default."""
256
+
257
+ # Include any other attributes provided by the XML file.
258
+ self.__dict__.update(kw)
259
+
260
+ @staticmethod
261
+ def fromxml(xml):
262
+ if isinstance(xml, string_types):
263
+ xml = ElementTree.parse(xml)
264
+ for key in xml.attrib:
265
+ xml.attrib[key] = text_type(xml.attrib[key])
266
+ return Package(**xml.attrib)
267
+
268
+ def __lt__(self, other):
269
+ return self.id < other.id
270
+
271
+ def __repr__(self):
272
+ return '<Package %s>' % self.id
273
+
274
+ @python_2_unicode_compatible
275
+ class Collection(object):
276
+ """
277
+ A directory entry for a collection of downloadable packages.
278
+ These entries are extracted from the XML index file that is
279
+ downloaded by ``Downloader``.
280
+ """
281
+ def __init__(self, id, children, name=None, **kw):
282
+ self.id = id
283
+ """A unique identifier for this collection."""
284
+
285
+ self.name = name or id
286
+ """A string name for this collection."""
287
+
288
+ self.children = children
289
+ """A list of the ``Collections`` or ``Packages`` directly
290
+ contained by this collection."""
291
+
292
+ self.packages = None
293
+ """A list of ``Packages`` contained by this collection or any
294
+ collections it recursively contains."""
295
+
296
+ # Include any other attributes provided by the XML file.
297
+ self.__dict__.update(kw)
298
+
299
+ @staticmethod
300
+ def fromxml(xml):
301
+ if isinstance(xml, string_types):
302
+ xml = ElementTree.parse(xml)
303
+ for key in xml.attrib:
304
+ xml.attrib[key] = text_type(xml.attrib[key])
305
+ children = [child.get('ref') for child in xml.findall('item')]
306
+ return Collection(children=children, **xml.attrib)
307
+
308
+ def __lt__(self, other):
309
+ return self.id < other.id
310
+
311
+ def __repr__(self):
312
+ return '<Collection %s>' % self.id
313
+
314
+ ######################################################################
315
+ # Message Passing Objects
316
+ ######################################################################
317
+
318
+ class DownloaderMessage(object):
319
+ """A status message object, used by ``incr_download`` to
320
+ communicate its progress."""
321
+ class StartCollectionMessage(DownloaderMessage):
322
+ """Data server has started working on a collection of packages."""
323
+ def __init__(self, collection): self.collection = collection
324
+ class FinishCollectionMessage(DownloaderMessage):
325
+ """Data server has finished working on a collection of packages."""
326
+ def __init__(self, collection): self.collection = collection
327
+ class StartPackageMessage(DownloaderMessage):
328
+ """Data server has started working on a package."""
329
+ def __init__(self, package): self.package = package
330
+ class FinishPackageMessage(DownloaderMessage):
331
+ """Data server has finished working on a package."""
332
+ def __init__(self, package): self.package = package
333
+ class StartDownloadMessage(DownloaderMessage):
334
+ """Data server has started downloading a package."""
335
+ def __init__(self, package): self.package = package
336
+ class FinishDownloadMessage(DownloaderMessage):
337
+ """Data server has finished downloading a package."""
338
+ def __init__(self, package): self.package = package
339
+ class StartUnzipMessage(DownloaderMessage):
340
+ """Data server has started unzipping a package."""
341
+ def __init__(self, package): self.package = package
342
+ class FinishUnzipMessage(DownloaderMessage):
343
+ """Data server has finished unzipping a package."""
344
+ def __init__(self, package): self.package = package
345
+ class UpToDateMessage(DownloaderMessage):
346
+ """The package download file is already up-to-date"""
347
+ def __init__(self, package): self.package = package
348
+ class StaleMessage(DownloaderMessage):
349
+ """The package download file is out-of-date or corrupt"""
350
+ def __init__(self, package): self.package = package
351
+ class ErrorMessage(DownloaderMessage):
352
+ """Data server encountered an error"""
353
+ def __init__(self, package, message):
354
+ self.package = package
355
+ if isinstance(message, Exception):
356
+ self.message = str(message)
357
+ else:
358
+ self.message = message
359
+
360
+ class ProgressMessage(DownloaderMessage):
361
+ """Indicates how much progress the data server has made"""
362
+ def __init__(self, progress): self.progress = progress
363
+ class SelectDownloadDirMessage(DownloaderMessage):
364
+ """Indicates what download directory the data server is using"""
365
+ def __init__(self, download_dir): self.download_dir = download_dir
366
+
367
+ ######################################################################
368
+ # NLTK Data Server
369
+ ######################################################################
370
+
371
+ class Downloader(object):
372
+ """
373
+ A class used to access the NLTK data server, which can be used to
374
+ download corpora and other data packages.
375
+ """
376
+
377
+ #/////////////////////////////////////////////////////////////////
378
+ # Configuration
379
+ #/////////////////////////////////////////////////////////////////
380
+
381
+ INDEX_TIMEOUT = 60*60 # 1 hour
382
+ """The amount of time after which the cached copy of the data
383
+ server index will be considered 'stale,' and will be
384
+ re-downloaded."""
385
+
386
+ DEFAULT_URL = 'https://raw.githubusercontent.com/nltk/nltk_data/gh-pages/index.xml'
387
+ """The default URL for the NLTK data server's index. An
388
+ alternative URL can be specified when creating a new
389
+ ``Downloader`` object."""
390
+
391
+ #/////////////////////////////////////////////////////////////////
392
+ # Status Constants
393
+ #/////////////////////////////////////////////////////////////////
394
+
395
+ INSTALLED = 'installed'
396
+ """A status string indicating that a package or collection is
397
+ installed and up-to-date."""
398
+ NOT_INSTALLED = 'not installed'
399
+ """A status string indicating that a package or collection is
400
+ not installed."""
401
+ STALE = 'out of date'
402
+ """A status string indicating that a package or collection is
403
+ corrupt or out-of-date."""
404
+ PARTIAL = 'partial'
405
+ """A status string indicating that a collection is partially
406
+ installed (i.e., only some of its packages are installed.)"""
407
+
408
+ #/////////////////////////////////////////////////////////////////
409
+ # Cosntructor
410
+ #/////////////////////////////////////////////////////////////////
411
+
412
+ def __init__(self, server_index_url=None, download_dir=None):
413
+ self._url = server_index_url or self.DEFAULT_URL
414
+ """The URL for the data server's index file."""
415
+
416
+ self._collections = {}
417
+ """Dictionary from collection identifier to ``Collection``"""
418
+
419
+ self._packages = {}
420
+ """Dictionary from package identifier to ``Package``"""
421
+
422
+ self._download_dir = download_dir
423
+ """The default directory to which packages will be downloaded."""
424
+
425
+ self._index = None
426
+ """The XML index file downloaded from the data server"""
427
+
428
+ self._index_timestamp = None
429
+ """Time at which ``self._index`` was downloaded. If it is more
430
+ than ``INDEX_TIMEOUT`` seconds old, it will be re-downloaded."""
431
+
432
+ self._status_cache = {}
433
+ """Dictionary from package/collection identifier to status
434
+ string (``INSTALLED``, ``NOT_INSTALLED``, ``STALE``, or
435
+ ``PARTIAL``). Cache is used for packages only, not
436
+ collections."""
437
+
438
+ self._errors = None
439
+ """Flag for telling if all packages got successfully downloaded or not."""
440
+
441
+ # decide where we're going to save things to.
442
+ if self._download_dir is None:
443
+ self._download_dir = self.default_download_dir()
444
+
445
+ #/////////////////////////////////////////////////////////////////
446
+ # Information
447
+ #/////////////////////////////////////////////////////////////////
448
+
449
+ def list(self, download_dir=None, show_packages=True,
450
+ show_collections=True, header=True, more_prompt=False,
451
+ skip_installed=False):
452
+ lines = 0 # for more_prompt
453
+ if download_dir is None:
454
+ download_dir = self._download_dir
455
+ print('Using default data directory (%s)' % download_dir)
456
+ if header:
457
+ print('='*(26+len(self._url)))
458
+ print(' Data server index for <%s>' % self._url)
459
+ print('='*(26+len(self._url)))
460
+ lines += 3 # for more_prompt
461
+ stale = partial = False
462
+
463
+ categories = []
464
+ if show_packages: categories.append('packages')
465
+ if show_collections: categories.append('collections')
466
+ for category in categories:
467
+ print('%s:' % category.capitalize())
468
+ lines += 1 # for more_prompt
469
+ for info in sorted(getattr(self, category)(), key=str):
470
+ status = self.status(info, download_dir)
471
+ if status == self.INSTALLED and skip_installed: continue
472
+ if status == self.STALE: stale = True
473
+ if status == self.PARTIAL: partial = True
474
+ prefix = {self.INSTALLED:'*', self.STALE:'-',
475
+ self.PARTIAL:'P', self.NOT_INSTALLED: ' '}[status]
476
+ name = textwrap.fill('-'*27 + (info.name or info.id),
477
+ 75, subsequent_indent=27*' ')[27:]
478
+ print(' [%s] %s %s' % (prefix, info.id.ljust(20, '.'), name))
479
+ lines += len(name.split('\n')) # for more_prompt
480
+ if more_prompt and lines > 20:
481
+ user_input = input("Hit Enter to continue: ")
482
+ if (user_input.lower() in ('x', 'q')): return
483
+ lines = 0
484
+ print()
485
+ msg = '([*] marks installed packages'
486
+ if stale: msg += '; [-] marks out-of-date or corrupt packages'
487
+ if partial: msg += '; [P] marks partially installed collections'
488
+ print(textwrap.fill(msg+')', subsequent_indent=' ', width=76))
489
+
490
+ def packages(self):
491
+ self._update_index()
492
+ return self._packages.values()
493
+
494
+ def corpora(self):
495
+ self._update_index()
496
+ return [pkg for (id,pkg) in self._packages.items()
497
+ if pkg.subdir == 'corpora']
498
+
499
+ def models(self):
500
+ self._update_index()
501
+ return [pkg for (id,pkg) in self._packages.items()
502
+ if pkg.subdir != 'corpora']
503
+
504
+ def collections(self):
505
+ self._update_index()
506
+ return self._collections.values()
507
+
508
+ #/////////////////////////////////////////////////////////////////
509
+ # Downloading
510
+ #/////////////////////////////////////////////////////////////////
511
+
512
+ def _info_or_id(self, info_or_id):
513
+ if isinstance(info_or_id, string_types):
514
+ return self.info(info_or_id)
515
+ else:
516
+ return info_or_id
517
+
518
+ # [xx] When during downloading is it 'safe' to abort? Only unsafe
519
+ # time is *during* an unzip -- we don't want to leave a
520
+ # partially-unzipped corpus in place because we wouldn't notice
521
+ # it. But if we had the exact total size of the unzipped corpus,
522
+ # then that would be fine. Then we could abort anytime we want!
523
+ # So this is really what we should do. That way the threaded
524
+ # downloader in the gui can just kill the download thread anytime
525
+ # it wants.
526
+
527
+ def incr_download(self, info_or_id, download_dir=None, force=False):
528
+ # If they didn't specify a download_dir, then use the default one.
529
+ if download_dir is None:
530
+ download_dir = self._download_dir
531
+ yield SelectDownloadDirMessage(download_dir)
532
+
533
+ # If they gave us a list of ids, then download each one.
534
+ if isinstance(info_or_id, (list,tuple)):
535
+ for msg in self._download_list(info_or_id, download_dir, force):
536
+ yield msg
537
+ return
538
+
539
+ # Look up the requested collection or package.
540
+ try: info = self._info_or_id(info_or_id)
541
+ except (IOError, ValueError) as e:
542
+ yield ErrorMessage(None, 'Error loading %s: %s' %
543
+ (info_or_id, e))
544
+ return
545
+
546
+ # Handle collections.
547
+ if isinstance(info, Collection):
548
+ yield StartCollectionMessage(info)
549
+ for msg in self.incr_download(info.children, download_dir, force):
550
+ yield msg
551
+ yield FinishCollectionMessage(info)
552
+
553
+ # Handle Packages (delegate to a helper function).
554
+ else:
555
+ for msg in self._download_package(info, download_dir, force):
556
+ yield msg
557
+
558
+ def _num_packages(self, item):
559
+ if isinstance(item, Package): return 1
560
+ else: return len(item.packages)
561
+
562
+ def _download_list(self, items, download_dir, force):
563
+ # Look up the requested items.
564
+ for i in range(len(items)):
565
+ try: items[i] = self._info_or_id(items[i])
566
+ except (IOError, ValueError) as e:
567
+ yield ErrorMessage(items[i], e)
568
+ return
569
+
570
+ # Download each item, re-scaling their progress.
571
+ num_packages = sum(self._num_packages(item) for item in items)
572
+ progress = 0
573
+ for i, item in enumerate(items):
574
+ if isinstance(item, Package):
575
+ delta = 1./num_packages
576
+ else:
577
+ delta = len(item.packages)/num_packages
578
+ for msg in self.incr_download(item, download_dir, force):
579
+ if isinstance(msg, ProgressMessage):
580
+ yield ProgressMessage(progress + msg.progress*delta)
581
+ else:
582
+ yield msg
583
+
584
+ progress += 100*delta
585
+
586
+ def _download_package(self, info, download_dir, force):
587
+ yield StartPackageMessage(info)
588
+ yield ProgressMessage(0)
589
+
590
+ # Do we already have the current version?
591
+ status = self.status(info, download_dir)
592
+ if not force and status == self.INSTALLED:
593
+ yield UpToDateMessage(info)
594
+ yield ProgressMessage(100)
595
+ yield FinishPackageMessage(info)
596
+ return
597
+
598
+ # Remove the package from our status cache
599
+ self._status_cache.pop(info.id, None)
600
+
601
+ # Check for (and remove) any old/stale version.
602
+ filepath = os.path.join(download_dir, info.filename)
603
+ if os.path.exists(filepath):
604
+ if status == self.STALE:
605
+ yield StaleMessage(info)
606
+ os.remove(filepath)
607
+
608
+ # Ensure the download_dir exists
609
+ if not os.path.exists(download_dir):
610
+ os.mkdir(download_dir)
611
+ if not os.path.exists(os.path.join(download_dir, info.subdir)):
612
+ os.mkdir(os.path.join(download_dir, info.subdir))
613
+
614
+ # Download the file. This will raise an IOError if the url
615
+ # is not found.
616
+ yield StartDownloadMessage(info)
617
+ yield ProgressMessage(5)
618
+ try:
619
+ infile = urlopen(info.url)
620
+ with open(filepath, 'wb') as outfile:
621
+ #print info.size
622
+ num_blocks = max(1, info.size/(1024*16))
623
+ for block in itertools.count():
624
+ s = infile.read(1024*16) # 16k blocks.
625
+ outfile.write(s)
626
+ if not s: break
627
+ if block % 2 == 0: # how often?
628
+ yield ProgressMessage(min(80, 5+75*(block/num_blocks)))
629
+ infile.close()
630
+ except IOError as e:
631
+ yield ErrorMessage(info, 'Error downloading %r from <%s>:'
632
+ '\n %s' % (info.id, info.url, e))
633
+ return
634
+ yield FinishDownloadMessage(info)
635
+ yield ProgressMessage(80)
636
+
637
+ # If it's a zipfile, uncompress it.
638
+ if info.filename.endswith('.zip'):
639
+ zipdir = os.path.join(download_dir, info.subdir)
640
+ # Unzip if we're unzipping by default; *or* if it's already
641
+ # been unzipped (presumably a previous version).
642
+ if info.unzip or os.path.exists(os.path.join(zipdir, info.id)):
643
+ yield StartUnzipMessage(info)
644
+ for msg in _unzip_iter(filepath, zipdir, verbose=False):
645
+ # Somewhat of a hack, but we need a proper package reference
646
+ msg.package = info
647
+ yield msg
648
+ yield FinishUnzipMessage(info)
649
+
650
+ yield FinishPackageMessage(info)
651
+
652
+ def download(self, info_or_id=None, download_dir=None, quiet=False,
653
+ force=False, prefix='[nltk_data] ', halt_on_error=True,
654
+ raise_on_error=False):
655
+ # If no info or id is given, then use the interactive shell.
656
+ if info_or_id is None:
657
+ # [xx] hmm -- changing self._download_dir here seems like
658
+ # the wrong thing to do. Maybe the _interactive_download
659
+ # function should make a new copy of self to use?
660
+ if download_dir is not None: self._download_dir = download_dir
661
+ self._interactive_download()
662
+ return True
663
+
664
+ else:
665
+ # Define a helper function for displaying output:
666
+ def show(s, prefix2=''):
667
+ print(textwrap.fill(s, initial_indent=prefix+prefix2,
668
+ subsequent_indent=prefix+prefix2+' '*4))
669
+
670
+ for msg in self.incr_download(info_or_id, download_dir, force):
671
+ # Error messages
672
+ if isinstance(msg, ErrorMessage):
673
+ show(msg.message)
674
+ if raise_on_error:
675
+ raise ValueError(msg.message)
676
+ if halt_on_error:
677
+ return False
678
+ self._errors = True
679
+ if not quiet:
680
+ print("Error installing package. Retry? [n/y/e]")
681
+ choice = input().strip()
682
+ if choice in ['y', 'Y']:
683
+ if not self.download(msg.package.id, download_dir,
684
+ quiet, force, prefix,
685
+ halt_on_error, raise_on_error):
686
+ return False
687
+ elif choice in ['e', 'E']:
688
+ return False
689
+
690
+ # All other messages
691
+ if not quiet:
692
+ # Collection downloading messages:
693
+ if isinstance(msg, StartCollectionMessage):
694
+ show('Downloading collection %r' % msg.collection.id)
695
+ prefix += ' | '
696
+ print(prefix)
697
+ elif isinstance(msg, FinishCollectionMessage):
698
+ print(prefix)
699
+ prefix = prefix[:-4]
700
+ if self._errors:
701
+ show('Downloaded collection %r with errors' %
702
+ msg.collection.id)
703
+ else:
704
+ show('Done downloading collection %s' %
705
+ msg.collection.id)
706
+
707
+ # Package downloading messages:
708
+ elif isinstance(msg, StartPackageMessage):
709
+ show('Downloading package %s to %s...' %
710
+ (msg.package.id, download_dir))
711
+ elif isinstance(msg, UpToDateMessage):
712
+ show('Package %s is already up-to-date!' %
713
+ msg.package.id, ' ')
714
+ #elif isinstance(msg, StaleMessage):
715
+ # show('Package %s is out-of-date or corrupt' %
716
+ # msg.package.id, ' ')
717
+ elif isinstance(msg, StartUnzipMessage):
718
+ show('Unzipping %s.' % msg.package.filename, ' ')
719
+
720
+ # Data directory message:
721
+ elif isinstance(msg, SelectDownloadDirMessage):
722
+ download_dir = msg.download_dir
723
+ return True
724
+
725
+ def is_stale(self, info_or_id, download_dir=None):
726
+ return self.status(info_or_id, download_dir) == self.STALE
727
+
728
+ def is_installed(self, info_or_id, download_dir=None):
729
+ return self.status(info_or_id, download_dir) == self.INSTALLED
730
+
731
+ def clear_status_cache(self, id=None):
732
+ if id is None:
733
+ self._status_cache.clear()
734
+ else:
735
+ self._status_cache.pop(id, None)
736
+
737
+ def status(self, info_or_id, download_dir=None):
738
+ """
739
+ Return a constant describing the status of the given package
740
+ or collection. Status can be one of ``INSTALLED``,
741
+ ``NOT_INSTALLED``, ``STALE``, or ``PARTIAL``.
742
+ """
743
+ if download_dir is None: download_dir = self._download_dir
744
+ info = self._info_or_id(info_or_id)
745
+
746
+ # Handle collections:
747
+ if isinstance(info, Collection):
748
+ pkg_status = [self.status(pkg.id) for pkg in info.packages]
749
+ if self.STALE in pkg_status:
750
+ return self.STALE
751
+ elif self.PARTIAL in pkg_status:
752
+ return self.PARTIAL
753
+ elif (self.INSTALLED in pkg_status and
754
+ self.NOT_INSTALLED in pkg_status):
755
+ return self.PARTIAL
756
+ elif self.NOT_INSTALLED in pkg_status:
757
+ return self.NOT_INSTALLED
758
+ else:
759
+ return self.INSTALLED
760
+
761
+ # Handle packages:
762
+ else:
763
+ filepath = os.path.join(download_dir, info.filename)
764
+ if download_dir != self._download_dir:
765
+ return self._pkg_status(info, filepath)
766
+ else:
767
+ if info.id not in self._status_cache:
768
+ self._status_cache[info.id] = self._pkg_status(info,
769
+ filepath)
770
+ return self._status_cache[info.id]
771
+
772
+ def _pkg_status(self, info, filepath):
773
+ if not os.path.exists(filepath):
774
+ return self.NOT_INSTALLED
775
+
776
+ # Check if the file has the correct size.
777
+ try: filestat = os.stat(filepath)
778
+ except OSError: return self.NOT_INSTALLED
779
+ if filestat.st_size != int(info.size):
780
+ return self.STALE
781
+
782
+ # Check if the file's checksum matches
783
+ if md5_hexdigest(filepath) != info.checksum:
784
+ return self.STALE
785
+
786
+ # If it's a zipfile, and it's been at least partially
787
+ # unzipped, then check if it's been fully unzipped.
788
+ if filepath.endswith('.zip'):
789
+ unzipdir = filepath[:-4]
790
+ if not os.path.exists(unzipdir):
791
+ return self.INSTALLED # but not unzipped -- ok!
792
+ if not os.path.isdir(unzipdir):
793
+ return self.STALE
794
+
795
+ unzipped_size = sum(os.stat(os.path.join(d, f)).st_size
796
+ for d, _, files in os.walk(unzipdir)
797
+ for f in files)
798
+ if unzipped_size != info.unzipped_size:
799
+ return self.STALE
800
+
801
+ # Otherwise, everything looks good.
802
+ return self.INSTALLED
803
+
804
+ def update(self, quiet=False, prefix='[nltk_data] '):
805
+ """
806
+ Re-download any packages whose status is STALE.
807
+ """
808
+ self.clear_status_cache()
809
+ for pkg in self.packages():
810
+ if self.status(pkg) == self.STALE:
811
+ self.download(pkg, quiet=quiet, prefix=prefix)
812
+
813
+ #/////////////////////////////////////////////////////////////////
814
+ # Index
815
+ #/////////////////////////////////////////////////////////////////
816
+
817
+ def _update_index(self, url=None):
818
+ """A helper function that ensures that self._index is
819
+ up-to-date. If the index is older than self.INDEX_TIMEOUT,
820
+ then download it again."""
821
+ # Check if the index is aleady up-to-date. If so, do nothing.
822
+ if not (self._index is None or url is not None or
823
+ time.time()-self._index_timestamp > self.INDEX_TIMEOUT):
824
+ return
825
+
826
+ # If a URL was specified, then update our URL.
827
+ self._url = url or self._url
828
+
829
+ # Download the index file.
830
+ self._index = nltk.internals.ElementWrapper(
831
+ ElementTree.parse(urlopen(self._url)).getroot())
832
+ self._index_timestamp = time.time()
833
+
834
+ # Build a dictionary of packages.
835
+ packages = [Package.fromxml(p) for p in
836
+ self._index.findall('packages/package')]
837
+ self._packages = dict((p.id, p) for p in packages)
838
+
839
+ # Build a dictionary of collections.
840
+ collections = [Collection.fromxml(c) for c in
841
+ self._index.findall('collections/collection')]
842
+ self._collections = dict((c.id, c) for c in collections)
843
+
844
+ # Replace identifiers with actual children in collection.children.
845
+ for collection in self._collections.values():
846
+ for i, child_id in enumerate(collection.children):
847
+ if child_id in self._packages:
848
+ collection.children[i] = self._packages[child_id]
849
+ elif child_id in self._collections:
850
+ collection.children[i] = self._collections[child_id]
851
+ else:
852
+ print('removing collection member with no package: {}'.format(child_id))
853
+ del collection.children[i]
854
+
855
+ # Fill in collection.packages for each collection.
856
+ for collection in self._collections.values():
857
+ packages = {}
858
+ queue = [collection]
859
+ for child in queue:
860
+ if isinstance(child, Collection):
861
+ queue.extend(child.children)
862
+ elif isinstance(child, Package):
863
+ packages[child.id] = child
864
+ else:
865
+ pass
866
+ collection.packages = packages.values()
867
+
868
+ # Flush the status cache
869
+ self._status_cache.clear()
870
+
871
+ def index(self):
872
+ """
873
+ Return the XML index describing the packages available from
874
+ the data server. If necessary, this index will be downloaded
875
+ from the data server.
876
+ """
877
+ self._update_index()
878
+ return self._index
879
+
880
+ def info(self, id):
881
+ """Return the ``Package`` or ``Collection`` record for the
882
+ given item."""
883
+ self._update_index()
884
+ if id in self._packages: return self._packages[id]
885
+ if id in self._collections: return self._collections[id]
886
+ raise ValueError('Package %r not found in index' % id)
887
+
888
+ def xmlinfo(self, id):
889
+ """Return the XML info record for the given item"""
890
+ self._update_index()
891
+ for package in self._index.findall('packages/package'):
892
+ if package.get('id') == id:
893
+ return package
894
+ for collection in self._index.findall('collections/collection'):
895
+ if collection.get('id') == id:
896
+ return collection
897
+ raise ValueError('Package %r not found in index' % id)
898
+
899
+ #/////////////////////////////////////////////////////////////////
900
+ # URL & Data Directory
901
+ #/////////////////////////////////////////////////////////////////
902
+
903
+ def _get_url(self):
904
+ """The URL for the data server's index file."""
905
+ return self._url
906
+ def _set_url(self, url):
907
+ """
908
+ Set a new URL for the data server. If we're unable to contact
909
+ the given url, then the original url is kept.
910
+ """
911
+ original_url = self._url
912
+ try:
913
+ self._update_index(url)
914
+ except:
915
+ self._url = original_url
916
+ raise
917
+ url = property(_get_url, _set_url)
918
+
919
+ def default_download_dir(self):
920
+ """
921
+ Return the directory to which packages will be downloaded by
922
+ default. This value can be overridden using the constructor,
923
+ or on a case-by-case basis using the ``download_dir`` argument when
924
+ calling ``download()``.
925
+
926
+ On Windows, the default download directory is
927
+ ``PYTHONHOME/lib/nltk``, where *PYTHONHOME* is the
928
+ directory containing Python, e.g. ``C:\\Python25``.
929
+
930
+ On all other platforms, the default directory is the first of
931
+ the following which exists or which can be created with write
932
+ permission: ``/usr/share/nltk_data``, ``/usr/local/share/nltk_data``,
933
+ ``/usr/lib/nltk_data``, ``/usr/local/lib/nltk_data``, ``~/nltk_data``.
934
+ """
935
+ # Check if we are on GAE where we cannot write into filesystem.
936
+ if 'APPENGINE_RUNTIME' in os.environ:
937
+ return
938
+
939
+ # Check if we have sufficient permissions to install in a
940
+ # variety of system-wide locations.
941
+ for nltkdir in nltk.data.path:
942
+ if (os.path.exists(nltkdir) and
943
+ nltk.internals.is_writable(nltkdir)):
944
+ return nltkdir
945
+
946
+ # On Windows, use %APPDATA%
947
+ if sys.platform == 'win32' and 'APPDATA' in os.environ:
948
+ homedir = os.environ['APPDATA']
949
+
950
+ # Otherwise, install in the user's home directory.
951
+ else:
952
+ homedir = os.path.expanduser('~/')
953
+ if homedir == '~/':
954
+ raise ValueError("Could not find a default download directory")
955
+
956
+ # append "nltk_data" to the home directory
957
+ return os.path.join(homedir, 'nltk_data')
958
+
959
+ def _get_download_dir(self):
960
+ """
961
+ The default directory to which packages will be downloaded.
962
+ This defaults to the value returned by ``default_download_dir()``.
963
+ To override this default on a case-by-case basis, use the
964
+ ``download_dir`` argument when calling ``download()``.
965
+ """
966
+ return self._download_dir
967
+ def _set_download_dir(self, download_dir):
968
+ self._download_dir = download_dir
969
+ # Clear the status cache.
970
+ self._status_cache.clear()
971
+ download_dir = property(_get_download_dir, _set_download_dir)
972
+
973
+ #/////////////////////////////////////////////////////////////////
974
+ # Interactive Shell
975
+ #/////////////////////////////////////////////////////////////////
976
+
977
+ def _interactive_download(self):
978
+ # Try the GUI first; if that doesn't work, try the simple
979
+ # interactive shell.
980
+ if TKINTER:
981
+ try:
982
+ DownloaderGUI(self).mainloop()
983
+ except TclError:
984
+ DownloaderShell(self).run()
985
+ else:
986
+ DownloaderShell(self).run()
987
+
988
+ class DownloaderShell(object):
989
+ def __init__(self, dataserver):
990
+ self._ds = dataserver
991
+
992
+ def _simple_interactive_menu(self, *options):
993
+ print('-'*75)
994
+ spc = (68 - sum(len(o) for o in options))//(len(options)-1)*' '
995
+ print(' ' + spc.join(options))
996
+ #w = 76/len(options)
997
+ #fmt = ' ' + ('%-'+str(w)+'s')*(len(options)-1) + '%s'
998
+ #print fmt % options
999
+ print('-'*75)
1000
+
1001
+ def run(self):
1002
+ print('NLTK Downloader')
1003
+ while True:
1004
+ self._simple_interactive_menu(
1005
+ 'd) Download', 'l) List', ' u) Update', 'c) Config', 'h) Help', 'q) Quit')
1006
+ user_input = input('Downloader> ').strip()
1007
+ if not user_input: print(); continue
1008
+ command = user_input.lower().split()[0]
1009
+ args = user_input.split()[1:]
1010
+ try:
1011
+ if command == 'l':
1012
+ print()
1013
+ self._ds.list(self._ds.download_dir, header=False,
1014
+ more_prompt=True)
1015
+ elif command == 'h':
1016
+ self._simple_interactive_help()
1017
+ elif command == 'c':
1018
+ self._simple_interactive_config()
1019
+ elif command in ('q', 'x'):
1020
+ return
1021
+ elif command == 'd':
1022
+ self._simple_interactive_download(args)
1023
+ elif command == 'u':
1024
+ self._simple_interactive_update()
1025
+ else:
1026
+ print('Command %r unrecognized' % user_input)
1027
+ except HTTPError as e:
1028
+ print('Error reading from server: %s'%e)
1029
+ except URLError as e:
1030
+ print('Error connecting to server: %s'%e.reason)
1031
+ # try checking if user_input is a package name, &
1032
+ # downloading it?
1033
+ print()
1034
+
1035
+ def _simple_interactive_download(self, args):
1036
+ if args:
1037
+ for arg in args:
1038
+ try: self._ds.download(arg, prefix=' ')
1039
+ except (IOError, ValueError) as e: print(e)
1040
+ else:
1041
+ while True:
1042
+ print()
1043
+ print('Download which package (l=list; x=cancel)?')
1044
+ user_input = input(' Identifier> ')
1045
+ if user_input.lower()=='l':
1046
+ self._ds.list(self._ds.download_dir, header=False,
1047
+ more_prompt=True, skip_installed=True)
1048
+ continue
1049
+ elif user_input.lower() in ('x', 'q', ''):
1050
+ return
1051
+ elif user_input:
1052
+ for id in user_input.split():
1053
+ try: self._ds.download(id, prefix=' ')
1054
+ except (IOError, ValueError) as e: print(e)
1055
+ break
1056
+
1057
+ def _simple_interactive_update(self):
1058
+ while True:
1059
+ stale_packages = []
1060
+ stale = partial = False
1061
+ for info in sorted(getattr(self._ds, 'packages')(), key=str):
1062
+ if self._ds.status(info) == self._ds.STALE:
1063
+ stale_packages.append((info.id, info.name))
1064
+
1065
+ print()
1066
+ if stale_packages:
1067
+ print('Will update following packages (o=ok; x=cancel)')
1068
+ for pid, pname in stale_packages:
1069
+ name = textwrap.fill('-'*27 + (pname),
1070
+ 75, subsequent_indent=27*' ')[27:]
1071
+ print(' [ ] %s %s' % (pid.ljust(20, '.'), name))
1072
+ print()
1073
+
1074
+ user_input = input(' Identifier> ')
1075
+ if user_input.lower()=='o':
1076
+ for pid, pname in stale_packages:
1077
+ try: self._ds.download(pid, prefix=' ')
1078
+ except (IOError, ValueError) as e: print(e)
1079
+ break
1080
+ elif user_input.lower() in ('x', 'q', ''):
1081
+ return
1082
+ else:
1083
+ print('Nothing to update.')
1084
+ return
1085
+
1086
+ def _simple_interactive_help(self):
1087
+ print()
1088
+ print('Commands:')
1089
+ print(' d) Download a package or collection u) Update out of date packages')
1090
+ print(' l) List packages & collections h) Help')
1091
+ print(' c) View & Modify Configuration q) Quit')
1092
+
1093
+ def _show_config(self):
1094
+ print()
1095
+ print('Data Server:')
1096
+ print(' - URL: <%s>' % self._ds.url)
1097
+ print((' - %d Package Collections Available' %
1098
+ len(self._ds.collections())))
1099
+ print((' - %d Individual Packages Available' %
1100
+ len(self._ds.packages())))
1101
+ print()
1102
+ print('Local Machine:')
1103
+ print(' - Data directory: %s' % self._ds.download_dir)
1104
+
1105
+ def _simple_interactive_config(self):
1106
+ self._show_config()
1107
+ while True:
1108
+ print()
1109
+ self._simple_interactive_menu(
1110
+ 's) Show Config', 'u) Set Server URL',
1111
+ 'd) Set Data Dir', 'm) Main Menu')
1112
+ user_input = input('Config> ').strip().lower()
1113
+ if user_input == 's':
1114
+ self._show_config()
1115
+ elif user_input == 'd':
1116
+ new_dl_dir = input(' New Directory> ').strip()
1117
+ if new_dl_dir in ('', 'x', 'q', 'X', 'Q'):
1118
+ print(' Cancelled!')
1119
+ elif os.path.isdir(new_dl_dir):
1120
+ self._ds.download_dir = new_dl_dir
1121
+ else:
1122
+ print(('Directory %r not found! Create it first.' %
1123
+ new_dl_dir))
1124
+ elif user_input == 'u':
1125
+ new_url = input(' New URL> ').strip()
1126
+ if new_url in ('', 'x', 'q', 'X', 'Q'):
1127
+ print(' Cancelled!')
1128
+ else:
1129
+ if not new_url.startswith(('http://', 'https://')):
1130
+ new_url = 'http://'+new_url
1131
+ try: self._ds.url = new_url
1132
+ except Exception as e:
1133
+ print('Error reading <%r>:\n %s' % (new_url, e))
1134
+ elif user_input == 'm':
1135
+ break
1136
+
1137
+ class DownloaderGUI(object):
1138
+ """
1139
+ Graphical interface for downloading packages from the NLTK data
1140
+ server.
1141
+ """
1142
+
1143
+ #/////////////////////////////////////////////////////////////////
1144
+ # Column Configuration
1145
+ #/////////////////////////////////////////////////////////////////
1146
+
1147
+ COLUMNS = ['', 'Identifier', 'Name', 'Size', 'Status',
1148
+ 'Unzipped Size',
1149
+ 'Copyright', 'Contact', 'License', 'Author',
1150
+ 'Subdir', 'Checksum']
1151
+ """A list of the names of columns. This controls the order in
1152
+ which the columns will appear. If this is edited, then
1153
+ ``_package_to_columns()`` may need to be edited to match."""
1154
+
1155
+ COLUMN_WEIGHTS = {'': 0, 'Name': 5, 'Size': 0, 'Status': 0}
1156
+ """A dictionary specifying how columns should be resized when the
1157
+ table is resized. Columns with weight 0 will not be resized at
1158
+ all; and columns with high weight will be resized more.
1159
+ Default weight (for columns not explicitly listed) is 1."""
1160
+
1161
+ COLUMN_WIDTHS = {'':1, 'Identifier':20, 'Name':45,
1162
+ 'Size': 10, 'Unzipped Size': 10,
1163
+ 'Status': 12}
1164
+ """A dictionary specifying how wide each column should be, in
1165
+ characters. The default width (for columns not explicitly
1166
+ listed) is specified by ``DEFAULT_COLUMN_WIDTH``."""
1167
+
1168
+ DEFAULT_COLUMN_WIDTH = 30
1169
+ """The default width for columns that are not explicitly listed
1170
+ in ``COLUMN_WIDTHS``."""
1171
+
1172
+ INITIAL_COLUMNS = ['', 'Identifier', 'Name', 'Size', 'Status']
1173
+ """The set of columns that should be displayed by default."""
1174
+
1175
+ # Perform a few import-time sanity checks to make sure that the
1176
+ # column configuration variables are defined consistently:
1177
+ for c in COLUMN_WEIGHTS: assert c in COLUMNS
1178
+ for c in COLUMN_WIDTHS: assert c in COLUMNS
1179
+ for c in INITIAL_COLUMNS: assert c in COLUMNS
1180
+
1181
+ #/////////////////////////////////////////////////////////////////
1182
+ # Color Configuration
1183
+ #/////////////////////////////////////////////////////////////////
1184
+
1185
+ _BACKDROP_COLOR = ('#000', '#ccc')
1186
+
1187
+ _ROW_COLOR = {Downloader.INSTALLED: ('#afa', '#080'),
1188
+ Downloader.PARTIAL: ('#ffa', '#880'),
1189
+ Downloader.STALE: ('#faa', '#800'),
1190
+ Downloader.NOT_INSTALLED: ('#fff', '#888')}
1191
+
1192
+ _MARK_COLOR = ('#000', '#ccc')
1193
+
1194
+ #_FRONT_TAB_COLOR = ('#ccf', '#008')
1195
+ #_BACK_TAB_COLOR = ('#88a', '#448')
1196
+ _FRONT_TAB_COLOR = ('#fff', '#45c')
1197
+ _BACK_TAB_COLOR = ('#aaa', '#67a')
1198
+
1199
+ _PROGRESS_COLOR = ('#f00', '#aaa')
1200
+
1201
+ _TAB_FONT = 'helvetica -16 bold'
1202
+
1203
+ #/////////////////////////////////////////////////////////////////
1204
+ # Constructor
1205
+ #/////////////////////////////////////////////////////////////////
1206
+
1207
+ def __init__(self, dataserver, use_threads=True):
1208
+ self._ds = dataserver
1209
+ self._use_threads = use_threads
1210
+
1211
+ # For the threaded downloader:
1212
+ self._download_lock = threading.Lock()
1213
+ self._download_msg_queue = []
1214
+ self._download_abort_queue = []
1215
+ self._downloading = False
1216
+
1217
+ # For tkinter after callbacks:
1218
+ self._afterid = {}
1219
+
1220
+ # A message log.
1221
+ self._log_messages = []
1222
+ self._log_indent = 0
1223
+ self._log('NLTK Downloader Started!')
1224
+
1225
+ # Create the main window.
1226
+ top = self.top = Tk()
1227
+ top.geometry('+50+50')
1228
+ top.title('NLTK Downloader')
1229
+ top.configure(background=self._BACKDROP_COLOR[1])
1230
+
1231
+ # Set up some bindings now, in case anything goes wrong.
1232
+ top.bind('<Control-q>', self.destroy)
1233
+ top.bind('<Control-x>', self.destroy)
1234
+ self._destroyed = False
1235
+
1236
+ self._column_vars = {}
1237
+
1238
+ # Initialize the GUI.
1239
+ self._init_widgets()
1240
+ self._init_menu()
1241
+ try:
1242
+ self._fill_table()
1243
+ except HTTPError as e:
1244
+ showerror('Error reading from server', e)
1245
+ except URLError as e:
1246
+ showerror('Error connecting to server', e.reason)
1247
+
1248
+ self._show_info()
1249
+ self._select_columns()
1250
+ self._table.select(0)
1251
+
1252
+ # Make sure we get notified when we're destroyed, so we can
1253
+ # cancel any download in progress.
1254
+ self._table.bind('<Destroy>', self._destroy)
1255
+
1256
+ def _log(self, msg):
1257
+ self._log_messages.append('%s %s%s' % (time.ctime(),
1258
+ ' | '*self._log_indent, msg))
1259
+
1260
+ #/////////////////////////////////////////////////////////////////
1261
+ # Internals
1262
+ #/////////////////////////////////////////////////////////////////
1263
+
1264
+ def _init_widgets(self):
1265
+ # Create the top-level frame structures
1266
+ f1 = Frame(self.top, relief='raised', border=2, padx=8, pady=0)
1267
+ f1.pack(sid='top', expand=True, fill='both')
1268
+ f1.grid_rowconfigure(2, weight=1)
1269
+ f1.grid_columnconfigure(0, weight=1)
1270
+ Frame(f1, height=8).grid(column=0, row=0) # spacer
1271
+ tabframe = Frame(f1)
1272
+ tabframe.grid(column=0, row=1, sticky='news')
1273
+ tableframe = Frame(f1)
1274
+ tableframe.grid(column=0, row=2, sticky='news')
1275
+ buttonframe = Frame(f1)
1276
+ buttonframe.grid(column=0, row=3, sticky='news')
1277
+ Frame(f1, height=8).grid(column=0, row=4) # spacer
1278
+ infoframe = Frame(f1)
1279
+ infoframe.grid(column=0, row=5, sticky='news')
1280
+ Frame(f1, height=8).grid(column=0, row=6) # spacer
1281
+ progressframe = Frame(self.top, padx=3, pady=3,
1282
+ background=self._BACKDROP_COLOR[1])
1283
+ progressframe.pack(side='bottom', fill='x')
1284
+ self.top['border'] = 0
1285
+ self.top['highlightthickness'] = 0
1286
+
1287
+ # Create the tabs
1288
+ self._tab_names = ['Collections', 'Corpora',
1289
+ 'Models', 'All Packages',]
1290
+ self._tabs = {}
1291
+ for i, tab in enumerate(self._tab_names):
1292
+ label = Label(tabframe, text=tab, font=self._TAB_FONT)
1293
+ label.pack(side='left', padx=((i+1)%2)*10)
1294
+ label.bind('<Button-1>', self._select_tab)
1295
+ self._tabs[tab.lower()] = label
1296
+
1297
+ # Create the table.
1298
+ column_weights = [self.COLUMN_WEIGHTS.get(column, 1)
1299
+ for column in self.COLUMNS]
1300
+ self._table = Table(tableframe, self.COLUMNS,
1301
+ column_weights=column_weights,
1302
+ highlightthickness=0, listbox_height=16,
1303
+ reprfunc=self._table_reprfunc)
1304
+ self._table.columnconfig(0, foreground=self._MARK_COLOR[0]) # marked
1305
+ for i, column in enumerate(self.COLUMNS):
1306
+ width = self.COLUMN_WIDTHS.get(column, self.DEFAULT_COLUMN_WIDTH)
1307
+ self._table.columnconfig(i, width=width)
1308
+ self._table.pack(expand=True, fill='both')
1309
+ self._table.focus()
1310
+ self._table.bind_to_listboxes('<Double-Button-1>',
1311
+ self._download)
1312
+ self._table.bind('<space>', self._table_mark)
1313
+ self._table.bind('<Return>', self._download)
1314
+ self._table.bind('<Left>', self._prev_tab)
1315
+ self._table.bind('<Right>', self._next_tab)
1316
+ self._table.bind('<Control-a>', self._mark_all)
1317
+
1318
+ # Create entry boxes for URL & download_dir
1319
+ infoframe.grid_columnconfigure(1, weight=1)
1320
+
1321
+ info = [('url', 'Server Index:', self._set_url),
1322
+ ('download_dir','Download Directory:',self._set_download_dir)]
1323
+ self._info = {}
1324
+ for (i, (key, label, callback)) in enumerate(info):
1325
+ Label(infoframe, text=label).grid(column=0, row=i, sticky='e')
1326
+ entry = Entry(infoframe, font='courier', relief='groove',
1327
+ disabledforeground='black')
1328
+ self._info[key] = (entry, callback)
1329
+ entry.bind('<Return>', self._info_save)
1330
+ entry.bind('<Button-1>', lambda e,key=key: self._info_edit(key))
1331
+ entry.grid(column=1, row=i, sticky='ew')
1332
+
1333
+ # If the user edits url or download_dir, and then clicks outside
1334
+ # the entry box, then save their results.
1335
+ self.top.bind('<Button-1>', self._info_save)
1336
+
1337
+ # Create Download & Refresh buttons.
1338
+ self._download_button = Button(
1339
+ buttonframe, text='Download', command=self._download, width=8)
1340
+ self._download_button.pack(side='left')
1341
+ self._refresh_button = Button(
1342
+ buttonframe, text='Refresh', command=self._refresh, width=8)
1343
+ self._refresh_button.pack(side='right')
1344
+
1345
+ # Create Progress bar
1346
+ self._progresslabel = Label(progressframe, text='',
1347
+ foreground=self._BACKDROP_COLOR[0],
1348
+ background=self._BACKDROP_COLOR[1])
1349
+ self._progressbar = Canvas(progressframe, width=200, height=16,
1350
+ background=self._PROGRESS_COLOR[1],
1351
+ relief='sunken', border=1)
1352
+ self._init_progressbar()
1353
+ self._progressbar.pack(side='right')
1354
+ self._progresslabel.pack(side='left')
1355
+
1356
+ def _init_menu(self):
1357
+ menubar = Menu(self.top)
1358
+
1359
+ filemenu = Menu(menubar, tearoff=0)
1360
+ filemenu.add_command(label='Download', underline=0,
1361
+ command=self._download, accelerator='Return')
1362
+ filemenu.add_separator()
1363
+ filemenu.add_command(label='Change Server Index', underline=7,
1364
+ command=lambda: self._info_edit('url'))
1365
+ filemenu.add_command(label='Change Download Directory', underline=0,
1366
+ command=lambda: self._info_edit('download_dir'))
1367
+ filemenu.add_separator()
1368
+ filemenu.add_command(label='Show Log', underline=5,
1369
+ command=self._show_log)
1370
+ filemenu.add_separator()
1371
+ filemenu.add_command(label='Exit', underline=1,
1372
+ command=self.destroy, accelerator='Ctrl-x')
1373
+ menubar.add_cascade(label='File', underline=0, menu=filemenu)
1374
+
1375
+ # Create a menu to control which columns of the table are
1376
+ # shown. n.b.: we never hide the first two columns (mark and
1377
+ # identifier).
1378
+ viewmenu = Menu(menubar, tearoff=0)
1379
+ for column in self._table.column_names[2:]:
1380
+ var = IntVar(self.top)
1381
+ assert column not in self._column_vars
1382
+ self._column_vars[column] = var
1383
+ if column in self.INITIAL_COLUMNS: var.set(1)
1384
+ viewmenu.add_checkbutton(label=column, underline=0, variable=var,
1385
+ command=self._select_columns)
1386
+ menubar.add_cascade(label='View', underline=0, menu=viewmenu)
1387
+
1388
+ # Create a sort menu
1389
+ # [xx] this should be selectbuttons; and it should include
1390
+ # reversed sorts as options.
1391
+ sortmenu = Menu(menubar, tearoff=0)
1392
+ for column in self._table.column_names[1:]:
1393
+ sortmenu.add_command(label='Sort by %s' % column,
1394
+ command=(lambda c=column:
1395
+ self._table.sort_by(c, 'ascending')))
1396
+ sortmenu.add_separator()
1397
+ #sortmenu.add_command(label='Descending Sort:')
1398
+ for column in self._table.column_names[1:]:
1399
+ sortmenu.add_command(label='Reverse sort by %s' % column,
1400
+ command=(lambda c=column:
1401
+ self._table.sort_by(c, 'descending')))
1402
+ menubar.add_cascade(label='Sort', underline=0, menu=sortmenu)
1403
+
1404
+ helpmenu = Menu(menubar, tearoff=0)
1405
+ helpmenu.add_command(label='About', underline=0,
1406
+ command=self.about)
1407
+ helpmenu.add_command(label='Instructions', underline=0,
1408
+ command=self.help, accelerator='F1')
1409
+ menubar.add_cascade(label='Help', underline=0, menu=helpmenu)
1410
+ self.top.bind('<F1>', self.help)
1411
+
1412
+ self.top.config(menu=menubar)
1413
+
1414
+ def _select_columns(self):
1415
+ for (column, var) in self._column_vars.items():
1416
+ if var.get():
1417
+ self._table.show_column(column)
1418
+ else:
1419
+ self._table.hide_column(column)
1420
+
1421
+ def _refresh(self):
1422
+ self._ds.clear_status_cache()
1423
+ try:
1424
+ self._fill_table()
1425
+ except HTTPError as e:
1426
+ showerror('Error reading from server', e)
1427
+ except URLError as e:
1428
+ showerror('Error connecting to server', e.reason)
1429
+ self._table.select(0)
1430
+
1431
+ def _info_edit(self, info_key):
1432
+ self._info_save() # just in case.
1433
+ (entry, callback) = self._info[info_key]
1434
+ entry['state'] = 'normal'
1435
+ entry['relief'] = 'sunken'
1436
+ entry.focus()
1437
+
1438
+ def _info_save(self, e=None):
1439
+ focus = self._table
1440
+ for entry, callback in self._info.values():
1441
+ if entry['state'] == 'disabled': continue
1442
+ if e is not None and e.widget is entry and e.keysym != 'Return':
1443
+ focus = entry
1444
+ else:
1445
+ entry['state'] = 'disabled'
1446
+ entry['relief'] = 'groove'
1447
+ callback(entry.get())
1448
+ focus.focus()
1449
+
1450
+ def _table_reprfunc(self, row, col, val):
1451
+ if self._table.column_names[col].endswith('Size'):
1452
+ if isinstance(val, string_types): return ' %s' % val
1453
+ elif val < 1024**2: return ' %.1f KB' % (val/1024.**1)
1454
+ elif val < 1024**3: return ' %.1f MB' % (val/1024.**2)
1455
+ else: return ' %.1f GB' % (val/1024.**3)
1456
+
1457
+ if col in (0, ''): return str(val)
1458
+ else: return ' %s' % val
1459
+
1460
+ def _set_url(self, url):
1461
+ if url == self._ds.url: return
1462
+ try:
1463
+ self._ds.url = url
1464
+ self._fill_table()
1465
+ except IOError as e:
1466
+ showerror('Error Setting Server Index', str(e))
1467
+ self._show_info()
1468
+
1469
+
1470
+ def _set_download_dir(self, download_dir):
1471
+ if self._ds.download_dir == download_dir: return
1472
+ # check if the dir exists, and if not, ask if we should create it?
1473
+
1474
+ # Clear our status cache, & re-check what's installed
1475
+ self._ds.download_dir = download_dir
1476
+ try:
1477
+ self._fill_table()
1478
+ except HTTPError as e:
1479
+ showerror('Error reading from server', e)
1480
+ except URLError as e:
1481
+ showerror('Error connecting to server', e.reason)
1482
+ self._show_info()
1483
+
1484
+ def _show_info(self):
1485
+ print('showing info', self._ds.url)
1486
+ for entry,cb in self._info.values():
1487
+ entry['state'] = 'normal'
1488
+ entry.delete(0, 'end')
1489
+ self._info['url'][0].insert(0, self._ds.url)
1490
+ self._info['download_dir'][0].insert(0, self._ds.download_dir)
1491
+ for entry,cb in self._info.values():
1492
+ entry['state'] = 'disabled'
1493
+
1494
+ def _prev_tab(self, *e):
1495
+ for i, tab in enumerate(self._tab_names):
1496
+ if tab.lower() == self._tab and i > 0:
1497
+ self._tab = self._tab_names[i-1].lower()
1498
+ try:
1499
+ return self._fill_table()
1500
+ except HTTPError as e:
1501
+ showerror('Error reading from server', e)
1502
+ except URLError as e:
1503
+ showerror('Error connecting to server', e.reason)
1504
+
1505
+ def _next_tab(self, *e):
1506
+ for i, tab in enumerate(self._tab_names):
1507
+ if tab.lower() == self._tab and i < (len(self._tabs)-1):
1508
+ self._tab = self._tab_names[i+1].lower()
1509
+ try:
1510
+ return self._fill_table()
1511
+ except HTTPError as e:
1512
+ showerror('Error reading from server', e)
1513
+ except URLError as e:
1514
+ showerror('Error connecting to server', e.reason)
1515
+
1516
+ def _select_tab(self, event):
1517
+ self._tab = event.widget['text'].lower()
1518
+ try:
1519
+ self._fill_table()
1520
+ except HTTPError as e:
1521
+ showerror('Error reading from server', e)
1522
+ except URLError as e:
1523
+ showerror('Error connecting to server', e.reason)
1524
+
1525
+ _tab = 'collections'
1526
+ #_tab = 'corpora'
1527
+ _rows = None
1528
+ def _fill_table(self):
1529
+ selected_row = self._table.selected_row()
1530
+ self._table.clear()
1531
+ if self._tab == 'all packages':
1532
+ items = self._ds.packages()
1533
+ elif self._tab == 'corpora':
1534
+ items = self._ds.corpora()
1535
+ elif self._tab == 'models':
1536
+ items = self._ds.models()
1537
+ elif self._tab == 'collections':
1538
+ items = self._ds.collections()
1539
+ else:
1540
+ assert 0, 'bad tab value %r' % self._tab
1541
+ rows = [self._package_to_columns(item) for item in items]
1542
+ self._table.extend(rows)
1543
+
1544
+ # Highlight the active tab.
1545
+ for tab, label in self._tabs.items():
1546
+ if tab == self._tab:
1547
+ label.configure(foreground=self._FRONT_TAB_COLOR[0],
1548
+ background=self._FRONT_TAB_COLOR[1])
1549
+ else:
1550
+ label.configure(foreground=self._BACK_TAB_COLOR[0],
1551
+ background=self._BACK_TAB_COLOR[1])
1552
+
1553
+ self._table.sort_by('Identifier', order='ascending')
1554
+ self._color_table()
1555
+ self._table.select(selected_row)
1556
+
1557
+ # This is a hack, because the scrollbar isn't updating its
1558
+ # position right -- I'm not sure what the underlying cause is
1559
+ # though. (This is on OS X w/ python 2.5) The length of
1560
+ # delay that's necessary seems to depend on how fast the
1561
+ # comptuer is. :-/
1562
+ self.top.after(150, self._table._scrollbar.set,
1563
+ *self._table._mlb.yview())
1564
+ self.top.after(300, self._table._scrollbar.set,
1565
+ *self._table._mlb.yview())
1566
+
1567
+ def _update_table_status(self):
1568
+ for row_num in range(len(self._table)):
1569
+ status = self._ds.status(self._table[row_num, 'Identifier'])
1570
+ self._table[row_num, 'Status'] = status
1571
+ self._color_table()
1572
+
1573
+ def _download(self, *e):
1574
+ # If we're using threads, then delegate to the threaded
1575
+ # downloader instead.
1576
+ if self._use_threads:
1577
+ return self._download_threaded(*e)
1578
+
1579
+ marked = [self._table[row, 'Identifier']
1580
+ for row in range(len(self._table))
1581
+ if self._table[row, 0] != '']
1582
+ selection = self._table.selected_row()
1583
+ if not marked and selection is not None:
1584
+ marked = [self._table[selection, 'Identifier']]
1585
+
1586
+ download_iter = self._ds.incr_download(marked, self._ds.download_dir)
1587
+ self._log_indent = 0
1588
+ self._download_cb(download_iter, marked)
1589
+
1590
+ _DL_DELAY=10
1591
+ def _download_cb(self, download_iter, ids):
1592
+ try: msg = next(download_iter)
1593
+ except StopIteration:
1594
+ #self._fill_table(sort=False)
1595
+ self._update_table_status()
1596
+ afterid = self.top.after(10, self._show_progress, 0)
1597
+ self._afterid['_download_cb'] = afterid
1598
+ return
1599
+
1600
+ def show(s):
1601
+ self._progresslabel['text'] = s
1602
+ self._log(s)
1603
+ if isinstance(msg, ProgressMessage):
1604
+ self._show_progress(msg.progress)
1605
+ elif isinstance(msg, ErrorMessage):
1606
+ show(msg.message)
1607
+ if msg.package is not None:
1608
+ self._select(msg.package.id)
1609
+ self._show_progress(None)
1610
+ return # halt progress.
1611
+ elif isinstance(msg, StartCollectionMessage):
1612
+ show('Downloading collection %s' % msg.collection.id)
1613
+ self._log_indent += 1
1614
+ elif isinstance(msg, StartPackageMessage):
1615
+ show('Downloading package %s' % msg.package.id)
1616
+ elif isinstance(msg, UpToDateMessage):
1617
+ show('Package %s is up-to-date!' % msg.package.id)
1618
+ #elif isinstance(msg, StaleMessage):
1619
+ # show('Package %s is out-of-date or corrupt' % msg.package.id)
1620
+ elif isinstance(msg, FinishDownloadMessage):
1621
+ show('Finished downloading %r.' % msg.package.id)
1622
+ elif isinstance(msg, StartUnzipMessage):
1623
+ show('Unzipping %s' % msg.package.filename)
1624
+ elif isinstance(msg, FinishCollectionMessage):
1625
+ self._log_indent -= 1
1626
+ show('Finished downloading collection %r.' % msg.collection.id)
1627
+ self._clear_mark(msg.collection.id)
1628
+ elif isinstance(msg, FinishPackageMessage):
1629
+ self._clear_mark(msg.package.id)
1630
+ afterid = self.top.after(self._DL_DELAY, self._download_cb,
1631
+ download_iter, ids)
1632
+ self._afterid['_download_cb'] = afterid
1633
+
1634
+ def _select(self, id):
1635
+ for row in range(len(self._table)):
1636
+ if self._table[row, 'Identifier'] == id:
1637
+ self._table.select(row)
1638
+ return
1639
+
1640
+ def _color_table(self):
1641
+ # Color rows according to status.
1642
+ for row in range(len(self._table)):
1643
+ bg, sbg = self._ROW_COLOR[self._table[row, 'Status']]
1644
+ fg, sfg = ('black', 'white')
1645
+ self._table.rowconfig(row, foreground=fg, selectforeground=sfg,
1646
+ background=bg, selectbackground=sbg)
1647
+ # Color the marked column
1648
+ self._table.itemconfigure(row, 0,
1649
+ foreground=self._MARK_COLOR[0],
1650
+ background=self._MARK_COLOR[1])
1651
+
1652
+
1653
+ def _clear_mark(self, id):
1654
+ for row in range(len(self._table)):
1655
+ if self._table[row, 'Identifier'] == id:
1656
+ self._table[row, 0] = ''
1657
+
1658
+ def _mark_all(self, *e):
1659
+ for row in range(len(self._table)):
1660
+ self._table[row,0] = 'X'
1661
+
1662
+ def _table_mark(self, *e):
1663
+ selection = self._table.selected_row()
1664
+ if selection >= 0:
1665
+ if self._table[selection][0] != '':
1666
+ self._table[selection,0] = ''
1667
+ else:
1668
+ self._table[selection,0] = 'X'
1669
+ self._table.select(delta=1)
1670
+
1671
+ def _show_log(self):
1672
+ text = '\n'.join(self._log_messages)
1673
+ ShowText(self.top, 'NLTK Downloader Log', text)
1674
+
1675
+ def _package_to_columns(self, pkg):
1676
+ """
1677
+ Given a package, return a list of values describing that
1678
+ package, one for each column in ``self.COLUMNS``.
1679
+ """
1680
+ row = []
1681
+ for column_index, column_name in enumerate(self.COLUMNS):
1682
+ if column_index == 0: # Mark:
1683
+ row.append('')
1684
+ elif column_name == 'Identifier':
1685
+ row.append(pkg.id)
1686
+ elif column_name == 'Status':
1687
+ row.append(self._ds.status(pkg))
1688
+ else:
1689
+ attr = column_name.lower().replace(' ', '_')
1690
+ row.append(getattr(pkg, attr, 'n/a'))
1691
+ return row
1692
+
1693
+ #/////////////////////////////////////////////////////////////////
1694
+ # External Interface
1695
+ #/////////////////////////////////////////////////////////////////
1696
+
1697
+ def destroy(self, *e):
1698
+ if self._destroyed: return
1699
+ self.top.destroy()
1700
+ self._destroyed = True
1701
+
1702
+ def _destroy(self, *e):
1703
+ if self.top is not None:
1704
+ for afterid in self._afterid.values():
1705
+ self.top.after_cancel(afterid)
1706
+
1707
+ # Abort any download in progress.
1708
+ if self._downloading and self._use_threads:
1709
+ self._abort_download()
1710
+
1711
+ # Make sure the garbage collector destroys these now;
1712
+ # otherwise, they may get destroyed when we're not in the main
1713
+ # thread, which would make Tkinter unhappy.
1714
+ self._column_vars.clear()
1715
+
1716
+ def mainloop(self, *args, **kwargs):
1717
+ self.top.mainloop(*args, **kwargs)
1718
+
1719
+ #/////////////////////////////////////////////////////////////////
1720
+ # HELP
1721
+ #/////////////////////////////////////////////////////////////////
1722
+
1723
+ HELP = textwrap.dedent("""\
1724
+ This tool can be used to download a variety of corpora and models
1725
+ that can be used with NLTK. Each corpus or model is distributed
1726
+ in a single zip file, known as a \"package file.\" You can
1727
+ download packages individually, or you can download pre-defined
1728
+ collections of packages.
1729
+
1730
+ When you download a package, it will be saved to the \"download
1731
+ directory.\" A default download directory is chosen when you run
1732
+
1733
+ the downloader; but you may also select a different download
1734
+ directory. On Windows, the default download directory is
1735
+
1736
+
1737
+ \"package.\"
1738
+
1739
+ The NLTK downloader can be used to download a variety of corpora,
1740
+ models, and other data packages.
1741
+
1742
+ Keyboard shortcuts::
1743
+ [return]\t Download
1744
+ [up]\t Select previous package
1745
+ [down]\t Select next package
1746
+ [left]\t Select previous tab
1747
+ [right]\t Select next tab
1748
+ """)
1749
+
1750
+ def help(self, *e):
1751
+ # The default font's not very legible; try using 'fixed' instead.
1752
+ try:
1753
+ ShowText(self.top, 'Help: NLTK Dowloader',
1754
+ self.HELP.strip(), width=75, font='fixed')
1755
+ except:
1756
+ ShowText(self.top, 'Help: NLTK Downloader',
1757
+ self.HELP.strip(), width=75)
1758
+
1759
+ def about(self, *e):
1760
+ ABOUT = ("NLTK Downloader\n"+
1761
+ "Written by Edward Loper")
1762
+ TITLE = 'About: NLTK Downloader'
1763
+ try:
1764
+ from six.moves.tkinter_messagebox import Message
1765
+ Message(message=ABOUT, title=TITLE).show()
1766
+ except ImportError:
1767
+ ShowText(self.top, TITLE, ABOUT)
1768
+
1769
+ #/////////////////////////////////////////////////////////////////
1770
+ # Progress Bar
1771
+ #/////////////////////////////////////////////////////////////////
1772
+
1773
+ _gradient_width = 5
1774
+ def _init_progressbar(self):
1775
+ c = self._progressbar
1776
+ width, height = int(c['width']), int(c['height'])
1777
+ for i in range(0, (int(c['width'])*2)//self._gradient_width):
1778
+ c.create_line(i*self._gradient_width+20, -20,
1779
+ i*self._gradient_width-height-20, height+20,
1780
+ width=self._gradient_width,
1781
+ fill='#%02x0000' % (80 + abs(i%6-3)*12))
1782
+ c.addtag_all('gradient')
1783
+ c.itemconfig('gradient', state='hidden')
1784
+
1785
+ # This is used to display progress
1786
+ c.addtag_withtag('redbox', c.create_rectangle(
1787
+ 0, 0, 0, 0, fill=self._PROGRESS_COLOR[0]))
1788
+
1789
+ def _show_progress(self, percent):
1790
+ c = self._progressbar
1791
+ if percent is None:
1792
+ c.coords('redbox', 0, 0, 0, 0)
1793
+ c.itemconfig('gradient', state='hidden')
1794
+ else:
1795
+ width, height = int(c['width']), int(c['height'])
1796
+ x = percent * int(width) // 100 + 1
1797
+ c.coords('redbox', 0, 0, x, height+1)
1798
+
1799
+ def _progress_alive(self):
1800
+ c = self._progressbar
1801
+ if not self._downloading:
1802
+ c.itemconfig('gradient', state='hidden')
1803
+ else:
1804
+ c.itemconfig('gradient', state='normal')
1805
+ x1, y1, x2, y2 = c.bbox('gradient')
1806
+ if x1 <= -100:
1807
+ c.move('gradient', (self._gradient_width*6)-4, 0)
1808
+ else:
1809
+ c.move('gradient', -4, 0)
1810
+ afterid = self.top.after(200, self._progress_alive)
1811
+ self._afterid['_progress_alive'] = afterid
1812
+
1813
+ #/////////////////////////////////////////////////////////////////
1814
+ # Threaded downloader
1815
+ #/////////////////////////////////////////////////////////////////
1816
+
1817
+ def _download_threaded(self, *e):
1818
+ # If the user tries to start a new download while we're already
1819
+ # downloading something, then abort the current download instead.
1820
+ if self._downloading:
1821
+ self._abort_download()
1822
+ return
1823
+
1824
+ # Change the 'download' button to an 'abort' button.
1825
+ self._download_button['text'] = 'Cancel'
1826
+
1827
+ marked = [self._table[row, 'Identifier']
1828
+ for row in range(len(self._table))
1829
+ if self._table[row, 0] != '']
1830
+ selection = self._table.selected_row()
1831
+ if not marked and selection is not None:
1832
+ marked = [self._table[selection, 'Identifier']]
1833
+
1834
+ # Create a new data server object for the download operation,
1835
+ # just in case the user modifies our data server during the
1836
+ # download (e.g., clicking 'refresh' or editing the index url).
1837
+ ds = Downloader(self._ds.url, self._ds.download_dir)
1838
+
1839
+ # Start downloading in a separate thread.
1840
+ assert self._download_msg_queue == []
1841
+ assert self._download_abort_queue == []
1842
+ self._DownloadThread(ds, marked, self._download_lock,
1843
+ self._download_msg_queue,
1844
+ self._download_abort_queue).start()
1845
+
1846
+ # Monitor the download message queue & display its progress.
1847
+ self._log_indent = 0
1848
+ self._downloading = True
1849
+ self._monitor_message_queue()
1850
+
1851
+ # Display an indication that we're still alive and well by
1852
+ # cycling the progress bar.
1853
+ self._progress_alive()
1854
+
1855
+ def _abort_download(self):
1856
+ if self._downloading:
1857
+ self._download_lock.acquire()
1858
+ self._download_abort_queue.append('abort')
1859
+ self._download_lock.release()
1860
+
1861
+ class _DownloadThread(threading.Thread):
1862
+ def __init__(self, data_server, items, lock, message_queue, abort):
1863
+ self.data_server = data_server
1864
+ self.items = items
1865
+ self.lock = lock
1866
+ self.message_queue = message_queue
1867
+ self.abort = abort
1868
+ threading.Thread.__init__(self)
1869
+
1870
+ def run (self):
1871
+ for msg in self.data_server.incr_download(self.items):
1872
+ self.lock.acquire()
1873
+ self.message_queue.append(msg)
1874
+ # Check if we've been told to kill ourselves:
1875
+ if self.abort:
1876
+ self.message_queue.append('aborted')
1877
+ self.lock.release()
1878
+ return
1879
+ self.lock.release()
1880
+ self.lock.acquire()
1881
+ self.message_queue.append('finished')
1882
+ self.lock.release()
1883
+
1884
+ _MONITOR_QUEUE_DELAY=100
1885
+ def _monitor_message_queue(self):
1886
+ def show(s):
1887
+ self._progresslabel['text'] = s
1888
+ self._log(s)
1889
+
1890
+ # Try to acquire the lock; if it's busy, then just try again later.
1891
+ if not self._download_lock.acquire():
1892
+ return
1893
+ for msg in self._download_msg_queue:
1894
+
1895
+ # Done downloading?
1896
+ if msg == 'finished' or msg == 'aborted':
1897
+ #self._fill_table(sort=False)
1898
+ self._update_table_status()
1899
+ self._downloading = False
1900
+ self._download_button['text'] = 'Download'
1901
+ del self._download_msg_queue[:]
1902
+ del self._download_abort_queue[:]
1903
+ self._download_lock.release()
1904
+ if msg == 'aborted':
1905
+ show('Download aborted!')
1906
+ self._show_progress(None)
1907
+ else:
1908
+ afterid = self.top.after(100, self._show_progress, None)
1909
+ self._afterid['_monitor_message_queue'] = afterid
1910
+ return
1911
+
1912
+ # All other messages
1913
+ elif isinstance(msg, ProgressMessage):
1914
+ self._show_progress(msg.progress)
1915
+ elif isinstance(msg, ErrorMessage):
1916
+ show(msg.message)
1917
+ if msg.package is not None:
1918
+ self._select(msg.package.id)
1919
+ self._show_progress(None)
1920
+ self._downloading = False
1921
+ return # halt progress.
1922
+ elif isinstance(msg, StartCollectionMessage):
1923
+ show('Downloading collection %r' % msg.collection.id)
1924
+ self._log_indent += 1
1925
+ elif isinstance(msg, StartPackageMessage):
1926
+ self._ds.clear_status_cache(msg.package.id)
1927
+ show('Downloading package %r' % msg.package.id)
1928
+ elif isinstance(msg, UpToDateMessage):
1929
+ show('Package %s is up-to-date!' % msg.package.id)
1930
+ #elif isinstance(msg, StaleMessage):
1931
+ # show('Package %s is out-of-date or corrupt; updating it' %
1932
+ # msg.package.id)
1933
+ elif isinstance(msg, FinishDownloadMessage):
1934
+ show('Finished downloading %r.' % msg.package.id)
1935
+ elif isinstance(msg, StartUnzipMessage):
1936
+ show('Unzipping %s' % msg.package.filename)
1937
+ elif isinstance(msg, FinishUnzipMessage):
1938
+ show('Finished installing %s' % msg.package.id)
1939
+ elif isinstance(msg, FinishCollectionMessage):
1940
+ self._log_indent -= 1
1941
+ show('Finished downloading collection %r.' % msg.collection.id)
1942
+ self._clear_mark(msg.collection.id)
1943
+ elif isinstance(msg, FinishPackageMessage):
1944
+ self._update_table_status()
1945
+ self._clear_mark(msg.package.id)
1946
+
1947
+ # Let the user know when we're aborting a download (but
1948
+ # waiting for a good point to abort it, so we don't end up
1949
+ # with a partially unzipped package or anything like that).
1950
+ if self._download_abort_queue:
1951
+ self._progresslabel['text'] = 'Aborting download...'
1952
+
1953
+ # Clear the message queue and then release the lock
1954
+ del self._download_msg_queue[:]
1955
+ self._download_lock.release()
1956
+
1957
+ # Check the queue again after MONITOR_QUEUE_DELAY msec.
1958
+ afterid = self.top.after(self._MONITOR_QUEUE_DELAY,
1959
+ self._monitor_message_queue)
1960
+ self._afterid['_monitor_message_queue'] = afterid
1961
+
1962
+ ######################################################################
1963
+ # Helper Functions
1964
+ ######################################################################
1965
+ # [xx] It may make sense to move these to nltk.internals.
1966
+
1967
+ def md5_hexdigest(file):
1968
+ """
1969
+ Calculate and return the MD5 checksum for a given file.
1970
+ ``file`` may either be a filename or an open stream.
1971
+ """
1972
+ if isinstance(file, string_types):
1973
+ with open(file, 'rb') as infile:
1974
+ return _md5_hexdigest(infile)
1975
+ return _md5_hexdigest(file)
1976
+
1977
+ def _md5_hexdigest(fp):
1978
+ md5_digest = md5()
1979
+ while True:
1980
+ block = fp.read(1024*16) # 16k blocks
1981
+ if not block: break
1982
+ md5_digest.update(block)
1983
+ return md5_digest.hexdigest()
1984
+
1985
+
1986
+ # change this to periodically yield progress messages?
1987
+ # [xx] get rid of topdir parameter -- we should be checking
1988
+ # this when we build the index, anyway.
1989
+ def unzip(filename, root, verbose=True):
1990
+ """
1991
+ Extract the contents of the zip file ``filename`` into the
1992
+ directory ``root``.
1993
+ """
1994
+ for message in _unzip_iter(filename, root, verbose):
1995
+ if isinstance(message, ErrorMessage):
1996
+ raise Exception(message)
1997
+
1998
+ def _unzip_iter(filename, root, verbose=True):
1999
+ if verbose:
2000
+ sys.stdout.write('Unzipping %s' % os.path.split(filename)[1])
2001
+ sys.stdout.flush()
2002
+
2003
+ try: zf = zipfile.ZipFile(filename)
2004
+ except zipfile.error as e:
2005
+ yield ErrorMessage(filename, 'Error with downloaded zip file')
2006
+ return
2007
+ except Exception as e:
2008
+ yield ErrorMessage(filename, e)
2009
+ return
2010
+
2011
+ # Get lists of directories & files
2012
+ namelist = zf.namelist()
2013
+ dirlist = set()
2014
+ for x in namelist:
2015
+ if x.endswith('/'):
2016
+ dirlist.add(x)
2017
+ else:
2018
+ dirlist.add(x.rsplit('/',1)[0] + '/')
2019
+ filelist = [x for x in namelist if not x.endswith('/')]
2020
+
2021
+ # Create the target directory if it doesn't exist
2022
+ if not os.path.exists(root):
2023
+ os.mkdir(root)
2024
+
2025
+ # Create the directory structure
2026
+ for dirname in sorted(dirlist):
2027
+ pieces = dirname[:-1].split('/')
2028
+ for i in range(len(pieces)):
2029
+ dirpath = os.path.join(root, *pieces[:i+1])
2030
+ if not os.path.exists(dirpath):
2031
+ os.mkdir(dirpath)
2032
+
2033
+ # Extract files.
2034
+ for i, filename in enumerate(filelist):
2035
+ filepath = os.path.join(root, *filename.split('/'))
2036
+
2037
+ try:
2038
+ with open(filepath, 'wb') as dstfile, zf.open(filename) as srcfile:
2039
+ shutil.copyfileobj(srcfile, dstfile)
2040
+ except Exception as e:
2041
+ yield ErrorMessage(filename, e)
2042
+ return
2043
+
2044
+ if verbose and (i*10/len(filelist) > (i-1)*10/len(filelist)):
2045
+ sys.stdout.write('.')
2046
+ sys.stdout.flush()
2047
+ if verbose:
2048
+ print()
2049
+
2050
+ ######################################################################
2051
+ # Index Builder
2052
+ ######################################################################
2053
+ # This may move to a different file sometime.
2054
+ import subprocess, zipfile
2055
+
2056
+ def build_index(root, base_url):
2057
+ """
2058
+ Create a new data.xml index file, by combining the xml description
2059
+ files for various packages and collections. ``root`` should be the
2060
+ path to a directory containing the package xml and zip files; and
2061
+ the collection xml files. The ``root`` directory is expected to
2062
+ have the following subdirectories::
2063
+
2064
+ root/
2065
+ packages/ .................. subdirectory for packages
2066
+ corpora/ ................. zip & xml files for corpora
2067
+ grammars/ ................ zip & xml files for grammars
2068
+ taggers/ ................. zip & xml files for taggers
2069
+ tokenizers/ .............. zip & xml files for tokenizers
2070
+ etc.
2071
+ collections/ ............... xml files for collections
2072
+
2073
+ For each package, there should be two files: ``package.zip``
2074
+ (where *package* is the package name)
2075
+ which contains the package itself as a compressed zip file; and
2076
+ ``package.xml``, which is an xml description of the package. The
2077
+ zipfile ``package.zip`` should expand to a single subdirectory
2078
+ named ``package/``. The base filename ``package`` must match
2079
+ the identifier given in the package's xml file.
2080
+
2081
+ For each collection, there should be a single file ``collection.zip``
2082
+ describing the collection, where *collection* is the name of the collection.
2083
+
2084
+ All identifiers (for both packages and collections) must be unique.
2085
+ """
2086
+ # Find all packages.
2087
+ packages = []
2088
+ for pkg_xml, zf, subdir in _find_packages(os.path.join(root, 'packages')):
2089
+ zipstat = os.stat(zf.filename)
2090
+ url = '%s/%s/%s' % (base_url, subdir, os.path.split(zf.filename)[1])
2091
+ unzipped_size = sum(zf_info.file_size for zf_info in zf.infolist())
2092
+
2093
+ # Fill in several fields of the package xml with calculated values.
2094
+ pkg_xml.set('unzipped_size', '%s' % unzipped_size)
2095
+ pkg_xml.set('size', '%s' % zipstat.st_size)
2096
+ pkg_xml.set('checksum', '%s' % md5_hexdigest(zf.filename))
2097
+ pkg_xml.set('subdir', subdir)
2098
+ #pkg_xml.set('svn_revision', _svn_revision(zf.filename))
2099
+ if not pkg_xml.get('url'):
2100
+ pkg_xml.set('url', url)
2101
+
2102
+ # Record the package.
2103
+ packages.append(pkg_xml)
2104
+
2105
+ # Find all collections
2106
+ collections = list(_find_collections(os.path.join(root, 'collections')))
2107
+
2108
+ # Check that all UIDs are unique
2109
+ uids = set()
2110
+ for item in packages+collections:
2111
+ if item.get('id') in uids:
2112
+ raise ValueError('Duplicate UID: %s' % item.get('id'))
2113
+ uids.add(item.get('id'))
2114
+
2115
+ # Put it all together
2116
+ top_elt = ElementTree.Element('nltk_data')
2117
+ top_elt.append(ElementTree.Element('packages'))
2118
+ for package in packages: top_elt[0].append(package)
2119
+ top_elt.append(ElementTree.Element('collections'))
2120
+ for collection in collections: top_elt[1].append(collection)
2121
+
2122
+ _indent_xml(top_elt)
2123
+ return top_elt
2124
+
2125
+ def _indent_xml(xml, prefix=''):
2126
+ """
2127
+ Helper for ``build_index()``: Given an XML ``ElementTree``, modify it
2128
+ (and its descendents) ``text`` and ``tail`` attributes to generate
2129
+ an indented tree, where each nested element is indented by 2
2130
+ spaces with respect to its parent.
2131
+ """
2132
+ if len(xml) > 0:
2133
+ xml.text = (xml.text or '').strip() + '\n' + prefix + ' '
2134
+ for child in xml:
2135
+ _indent_xml(child, prefix+' ')
2136
+ for child in xml[:-1]:
2137
+ child.tail = (child.tail or '').strip() + '\n' + prefix + ' '
2138
+ xml[-1].tail = (xml[-1].tail or '').strip() + '\n' + prefix
2139
+
2140
+ def _check_package(pkg_xml, zipfilename, zf):
2141
+ """
2142
+ Helper for ``build_index()``: Perform some checks to make sure that
2143
+ the given package is consistent.
2144
+ """
2145
+ # The filename must patch the id given in the XML file.
2146
+ uid = os.path.splitext(os.path.split(zipfilename)[1])[0]
2147
+ if pkg_xml.get('id') != uid:
2148
+ raise ValueError('package identifier mismatch (%s vs %s)' %
2149
+ (pkg_xml.get('id'), uid))
2150
+
2151
+ # Zip file must expand to a subdir whose name matches uid.
2152
+ if sum( (name!=uid and not name.startswith(uid+'/'))
2153
+ for name in zf.namelist() ):
2154
+ raise ValueError('Zipfile %s.zip does not expand to a single '
2155
+ 'subdirectory %s/' % (uid, uid))
2156
+
2157
+ # update for git?
2158
+ def _svn_revision(filename):
2159
+ """
2160
+ Helper for ``build_index()``: Calculate the subversion revision
2161
+ number for a given file (by using ``subprocess`` to run ``svn``).
2162
+ """
2163
+ p = subprocess.Popen(['svn', 'status', '-v', filename],
2164
+ stdout=subprocess.PIPE,
2165
+ stderr=subprocess.PIPE)
2166
+ (stdout, stderr) = p.communicate()
2167
+ if p.returncode != 0 or stderr or not stdout:
2168
+ raise ValueError('Error determining svn_revision for %s: %s' %
2169
+ (os.path.split(filename)[1], textwrap.fill(stderr)))
2170
+ return stdout.split()[2]
2171
+
2172
+ def _find_collections(root):
2173
+ """
2174
+ Helper for ``build_index()``: Yield a list of ElementTree.Element
2175
+ objects, each holding the xml for a single package collection.
2176
+ """
2177
+ packages = []
2178
+ for dirname, subdirs, files in os.walk(root):
2179
+ for filename in files:
2180
+ if filename.endswith('.xml'):
2181
+ xmlfile = os.path.join(dirname, filename)
2182
+ yield ElementTree.parse(xmlfile).getroot()
2183
+
2184
+ def _find_packages(root):
2185
+ """
2186
+ Helper for ``build_index()``: Yield a list of tuples
2187
+ ``(pkg_xml, zf, subdir)``, where:
2188
+ - ``pkg_xml`` is an ``ElementTree.Element`` holding the xml for a
2189
+ package
2190
+ - ``zf`` is a ``zipfile.ZipFile`` for the package's contents.
2191
+ - ``subdir`` is the subdirectory (relative to ``root``) where
2192
+ the package was found (e.g. 'corpora' or 'grammars').
2193
+ """
2194
+ from nltk.corpus.reader.util import _path_from
2195
+ # Find all packages.
2196
+ packages = []
2197
+ for dirname, subdirs, files in os.walk(root):
2198
+ relpath = '/'.join(_path_from(root, dirname))
2199
+ for filename in files:
2200
+ if filename.endswith('.xml'):
2201
+ xmlfilename = os.path.join(dirname, filename)
2202
+ zipfilename = xmlfilename[:-4]+'.zip'
2203
+ try: zf = zipfile.ZipFile(zipfilename)
2204
+ except Exception as e:
2205
+ raise ValueError('Error reading file %r!\n%s' %
2206
+ (zipfilename, e))
2207
+ try: pkg_xml = ElementTree.parse(xmlfilename).getroot()
2208
+ except Exception as e:
2209
+ raise ValueError('Error reading file %r!\n%s' %
2210
+ (xmlfilename, e))
2211
+
2212
+ # Check that the UID matches the filename
2213
+ uid = os.path.split(xmlfilename[:-4])[1]
2214
+ if pkg_xml.get('id') != uid:
2215
+ raise ValueError('package identifier mismatch (%s '
2216
+ 'vs %s)' % (pkg_xml.get('id'), uid))
2217
+
2218
+ # Check that the zipfile expands to a subdir whose
2219
+ # name matches the uid.
2220
+ if sum( (name!=uid and not name.startswith(uid+'/'))
2221
+ for name in zf.namelist() ):
2222
+ raise ValueError('Zipfile %s.zip does not expand to a '
2223
+ 'single subdirectory %s/' % (uid, uid))
2224
+
2225
+ yield pkg_xml, zf, relpath
2226
+ # Don't recurse into svn subdirectories:
2227
+ try: subdirs.remove('.svn')
2228
+ except ValueError: pass
2229
+
2230
+ ######################################################################
2231
+ # Main:
2232
+ ######################################################################
2233
+
2234
+ # There should be a command-line interface
2235
+
2236
+ # Aliases
2237
+ _downloader = Downloader()
2238
+ download = _downloader.download
2239
+
2240
+ def download_shell():
2241
+ DownloaderShell(_downloader).run()
2242
+
2243
+ def download_gui():
2244
+ DownloaderGUI(_downloader).mainloop()
2245
+
2246
+ def update():
2247
+ _downloader.update()
2248
+
2249
+ if __name__ == '__main__':
2250
+ from optparse import OptionParser
2251
+ parser = OptionParser()
2252
+ parser.add_option("-d", "--dir", dest="dir",
2253
+ help="download package to directory DIR", metavar="DIR")
2254
+ parser.add_option("-q", "--quiet", dest="quiet", action="store_true",
2255
+ default=False, help="work quietly")
2256
+ parser.add_option("-f", "--force", dest="force", action="store_true",
2257
+ default=False, help="download even if already installed")
2258
+ parser.add_option("-e", "--exit-on-error", dest="halt_on_error", action="store_true",
2259
+ default=False, help="exit if an error occurs")
2260
+ parser.add_option("-u", "--url", dest="server_index_url",
2261
+ default=os.environ.get('NLTK_DOWNLOAD_URL'),
2262
+ help="download server index url")
2263
+
2264
+ (options, args) = parser.parse_args()
2265
+
2266
+ downloader = Downloader(server_index_url = options.server_index_url)
2267
+
2268
+ if args:
2269
+ for pkg_id in args:
2270
+ rv = downloader.download(info_or_id=pkg_id, download_dir=options.dir,
2271
+ quiet=options.quiet, force=options.force,
2272
+ halt_on_error=options.halt_on_error)
2273
+ if rv==False and options.halt_on_error:
2274
+ break
2275
+ else:
2276
+ downloader.download(download_dir=options.dir,
2277
+ quiet=options.quiet, force=options.force,
2278
+ halt_on_error=options.halt_on_error)