· 6 years ago · Sep 04, 2019, 07:54 PM
1diff --git a/.github/CONTRIBUTOR_AGREEMENT.md b/.github/CONTRIBUTOR_AGREEMENT.md
2index 919fb81f..f3460306 100644
3--- a/.github/CONTRIBUTOR_AGREEMENT.md
4+++ b/.github/CONTRIBUTOR_AGREEMENT.md
5@@ -87,7 +87,7 @@ U.S. Federal law. Any choice of law rules will not apply.
6 7. Please place an “x” on one of the applicable statement below. Please do NOT
7 mark both statements:
8
9- * [x] I am signing on behalf of myself as an individual and no other person
10+ * [ ] I am signing on behalf of myself as an individual and no other person
11 or entity, including my employer, has or will have rights with respect to my
12 contributions.
13
14@@ -96,11 +96,11 @@ mark both statements:
15
16 ## Contributor Details
17
18-| Field | Entry |
19-|------------------------------- | -------------------- |
20-| Name | Abhinav Sharma |
21-| Company name (if applicable) | Fourtek I.T. Solutions Pvt. Ltd. |
22-| Title or role (if applicable) | Machine Learning Engineer |
23-| Date | 3 Novermber 2017 |
24-| GitHub username | abhi18av |
25-| Website (optional) | https://abhi18av.github.io/ |
26+| Field | Entry |
27+|------------------------------- | -------------------- |
28+| Name | |
29+| Company name (if applicable) | |
30+| Title or role (if applicable) | |
31+| Date | |
32+| GitHub username | |
33+| Website (optional) | |
34diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
35index ec11b78b..3a314384 100644
36--- a/.github/PULL_REQUEST_TEMPLATE.md
37+++ b/.github/PULL_REQUEST_TEMPLATE.md
38@@ -7,7 +7,7 @@ ran. If your test fixes a bug reported in an issue, don't forget to include the
39 issue number. If your PR is still a work in progress, that's totally fine – just
40 include a note to let us know. -->
41
42-### Types of change
43+### Types of changes
44 <!-- What type of change does your PR cover? Is it a bug fix, an enhancement
45 or new feature, or a change to the documentation? -->
46
47diff --git a/.github/contributors/IamJeffG.md b/.github/contributors/IamJeffG.md
48new file mode 100644
49index 00000000..030e711a
50--- /dev/null
51+++ b/.github/contributors/IamJeffG.md
52@@ -0,0 +1,106 @@
53+# spaCy contributor agreement
54+
55+This spaCy Contributor Agreement (**"SCA"**) is based on the
56+[Oracle Contributor Agreement](http://www.oracle.com/technetwork/oca-405177.pdf).
57+The SCA applies to any contribution that you make to any product or project
58+managed by us (the **"project"**), and sets out the intellectual property rights
59+you grant to us in the contributed materials. The term **"us"** shall mean
60+[ExplosionAI UG (haftungsbeschränkt)](https://explosion.ai/legal). The term
61+**"you"** shall mean the person or entity identified below.
62+
63+If you agree to be bound by these terms, fill in the information requested
64+below and include the filled-in version with your first pull request, under the
65+folder [`.github/contributors/`](/.github/contributors/). The name of the file
66+should be your GitHub username, with the extension `.md`. For example, the user
67+example_user would create the file `.github/contributors/example_user.md`.
68+
69+Read this agreement carefully before signing. These terms and conditions
70+constitute a binding legal agreement.
71+
72+## Contributor Agreement
73+
74+1. The term "contribution" or "contributed materials" means any source code,
75+object code, patch, tool, sample, graphic, specification, manual,
76+documentation, or any other material posted or submitted by you to the project.
77+
78+2. With respect to any worldwide copyrights, or copyright applications and
79+registrations, in your contribution:
80+
81+ * you hereby assign to us joint ownership, and to the extent that such
82+ assignment is or becomes invalid, ineffective or unenforceable, you hereby
83+ grant to us a perpetual, irrevocable, non-exclusive, worldwide, no-charge,
84+ royalty-free, unrestricted license to exercise all rights under those
85+ copyrights. This includes, at our option, the right to sublicense these same
86+ rights to third parties through multiple levels of sublicensees or other
87+ licensing arrangements;
88+
89+ * you agree that each of us can do all things in relation to your
90+ contribution as if each of us were the sole owners, and if one of us makes
91+ a derivative work of your contribution, the one who makes the derivative
92+ work (or has it made will be the sole owner of that derivative work;
93+
94+ * you agree that you will not assert any moral rights in your contribution
95+ against us, our licensees or transferees;
96+
97+ * you agree that we may register a copyright in your contribution and
98+ exercise all ownership rights associated with it; and
99+
100+ * you agree that neither of us has any duty to consult with, obtain the
101+ consent of, pay or render an accounting to the other for any use or
102+ distribution of your contribution.
103+
104+3. With respect to any patents you own, or that you can license without payment
105+to any third party, you hereby grant to us a perpetual, irrevocable,
106+non-exclusive, worldwide, no-charge, royalty-free license to:
107+
108+ * make, have made, use, sell, offer to sell, import, and otherwise transfer
109+ your contribution in whole or in part, alone or in combination with or
110+ included in any product, work or materials arising out of the project to
111+ which your contribution was submitted, and
112+
113+ * at our option, to sublicense these same rights to third parties through
114+ multiple levels of sublicensees or other licensing arrangements.
115+
116+4. Except as set out above, you keep all right, title, and interest in your
117+contribution. The rights that you grant to us under these terms are effective
118+on the date you first submitted a contribution to us, even if your submission
119+took place before the date you sign these terms.
120+
121+5. You covenant, represent, warrant and agree that:
122+
123+ * Each contribution that you submit is and shall be an original work of
124+ authorship and you can legally grant the rights set out in this SCA;
125+
126+ * to the best of your knowledge, each contribution will not violate any
127+ third party's copyrights, trademarks, patents, or other intellectual
128+ property rights; and
129+
130+ * each contribution shall be in compliance with U.S. export control laws and
131+ other applicable export and import laws. You agree to notify us if you
132+ become aware of any circumstance which would make any of the foregoing
133+ representations inaccurate in any respect. We may publicly disclose your
134+ participation in the project, including the fact that you have signed the SCA.
135+
136+6. This SCA is governed by the laws of the State of California and applicable
137+U.S. Federal law. Any choice of law rules will not apply.
138+
139+7. Please place an “x” on one of the applicable statement below. Please do NOT
140+mark both statements:
141+
142+ * [ ] I am signing on behalf of myself as an individual and no other person
143+ or entity, including my employer, has or will have rights with respect my
144+ contributions.
145+
146+ * [x] I am signing on behalf of my employer or a legal entity and I have the
147+ actual authority to contractually bind that entity.
148+
149+## Contributor Details
150+
151+| Field | Entry |
152+|------------------------------- | -------------------- |
153+| Name | Jeffrey Gerard |
154+| Company name (if applicable) | cephalo-ai / wellio |
155+| Title or role (if applicable) | Senior Data Scientist|
156+| Date | 11/02/2017 |
157+| GitHub username | IamJeffG |
158+| Website (optional) | |
159diff --git a/.gitignore b/.gitignore
160index 14097dfc..572eea92 100644
161--- a/.gitignore
162+++ b/.gitignore
163@@ -7,6 +7,8 @@ keys/
164 # Website
165 website/www/
166 website/_deploy.sh
167+website/package.json
168+website/announcement.jade
169 website/.gitignore
170
171 # Cython / C extensions
172diff --git a/README.rst b/README.rst
173index 32937839..eb9e1c81 100644
174--- a/README.rst
175+++ b/README.rst
176@@ -14,7 +14,11 @@ integration. It's commercial open-source software, released under the MIT licens
177
178 .. image:: https://img.shields.io/travis/explosion/spaCy/master.svg?style=flat-square
179 :target: https://travis-ci.org/explosion/spaCy
180- :alt: Build Status
181+ :alt: Travis Build Status
182+
183+.. image:: https://img.shields.io/appveyor/ci/explosion/spacy/master.svg?style=flat-square
184+ :target: https://ci.appveyor.com/project/explosion/spacy
185+ :alt: Appveyor Build Status
186
187 .. image:: https://img.shields.io/github/release/explosion/spacy.svg?style=flat-square
188 :target: https://github.com/explosion/spaCy/releases
189@@ -101,7 +105,6 @@ Features
190 ? **For more details, see the** `facts, figures and benchmarks <https://alpha.spacy.io/usage/facts-figures>`_.
191
192 Install spaCy
193-=============
194
195 For detailed installation instructions, see
196 the `documentation <https://alpha.spacy.io/usage>`_.
197diff --git a/examples/dependency_patterns.py b/examples/dependency_patterns.py
198new file mode 100644
199index 00000000..776e045b
200--- /dev/null
201+++ b/examples/dependency_patterns.py
202@@ -0,0 +1,33 @@
203+'''
204+Match a dependency pattern. See https://github.com/explosion/spaCy/pull/1120
205+
206+We start by creating a DependencyTree for the Doc. This class models the document
207+dependency tree. Then we compile the query into a Pattern using the PatternParser.
208+The syntax is quite simple:
209+
210+we define a node named 'fox', that must match in the dep tree a token
211+whose orth_ is 'fox'. an anonymous token whose lemma is 'quick' must have fox
212+as parent, with a dep_ matching the regex am.* another anonymous token whose
213+orth_ matches the regex brown|yellow has fox as parent, with whathever dep_
214+DependencyTree.match returns a list of PatternMatch. Notice that we can assign
215+names to anonymous or defined nodes ([word:fox]=f). We can get the Token mapped
216+to the fox node using match['f'].
217+'''
218+import spacy
219+from spacy.pattern import PatternParser, DependencyTree
220+
221+nlp = spacy.load('en')
222+doc = nlp("The quick brown fox jumped over the lazy dog.")
223+tree = DependencyTree(doc)
224+
225+query = """fox [word:fox]=f
226+ [lemma:quick]=q >/am.*/ fox
227+ [word:/brown|yellow/] > fox"""
228+
229+pattern = PatternParser.parse(query)
230+matches = tree.match(pattern)
231+
232+assert len(matches) == 1
233+match = matches[0]
234+
235+assert match['f'] == doc[3]
236diff --git a/examples/training/train_new_entity_type.py b/examples/training/train_new_entity_type.py
237index 9a150461..30dc1599 100644
238--- a/examples/training/train_new_entity_type.py
239+++ b/examples/training/train_new_entity_type.py
240@@ -24,13 +24,6 @@ For more details, see the documentation:
241 * NER: https://alpha.spacy.io/usage/linguistic-features#named-entities
242
243 Compatible with: spaCy v2.0.0+
244-"""
245-from __future__ import unicode_literals, print_function
246-
247-import plac
248-import random
249-from pathlib import Path
250-import spacy
251
252
253 # new entity label
254@@ -109,6 +102,89 @@ def main(model=None, new_model_name='animal', output_dir=None, n_iter=50):
255 test_text = 'Do you like horses?'
256 doc = nlp(test_text)
257 print("Entities in '%s'" % test_text)
258+=======
259+from spacy.gold import GoldParse
260+from spacy.tagger import Tagger
261+
262+
263+def train_ner(nlp, train_data, output_dir):
264+ # Add new words to vocab
265+ for raw_text, _ in train_data:
266+ doc = nlp.make_doc(raw_text)
267+ for word in doc:
268+ _ = nlp.vocab[word.orth]
269+ random.seed(0)
270+ # You may need to change the learning rate. It's generally difficult to
271+ # guess what rate you should set, especially when you have limited data.
272+ nlp.entity.model.learn_rate = 0.001
273+ for itn in range(1000):
274+ random.shuffle(train_data)
275+ loss = 0.
276+ for raw_text, entity_offsets in train_data:
277+ doc = nlp.make_doc(raw_text)
278+ gold = GoldParse(doc, entities=entity_offsets)
279+ # By default, the GoldParse class assumes that the entities
280+ # described by offset are complete, and all other words should
281+ # have the tag 'O'. You can tell it to make no assumptions
282+ # about the tag of a word by giving it the tag '-'.
283+ # However, this allows a trivial solution to the current
284+ # learning problem: if words are either 'any tag' or 'ANIMAL',
285+ # the model can learn that all words can be tagged 'ANIMAL'.
286+ #for i in range(len(gold.ner)):
287+ #if not gold.ner[i].endswith('ANIMAL'):
288+ # gold.ner[i] = '-'
289+ nlp.tagger(doc)
290+ # As of 1.9, spaCy's parser now lets you supply a dropout probability
291+ # This might help the model generalize better from only a few
292+ # examples.
293+ loss += nlp.entity.update(doc, gold, drop=0.9)
294+ if loss == 0:
295+ break
296+ # This step averages the model's weights. This may or may not be good for
297+ # your situation --- it's empirical.
298+ nlp.end_training()
299+ if output_dir:
300+ if not output_dir.exists():
301+ output_dir.mkdir()
302+ nlp.save_to_directory(output_dir)
303+
304+
305+def main(model_name, output_directory=None):
306+ print("Loading initial model", model_name)
307+ nlp = spacy.load(model_name)
308+ if output_directory is not None:
309+ output_directory = Path(output_directory)
310+
311+ train_data = [
312+ (
313+ "Horses are too tall and they pretend to care about your feelings",
314+ [(0, 6, 'ANIMAL')],
315+ ),
316+ (
317+ "horses are too tall and they pretend to care about your feelings",
318+ [(0, 6, 'ANIMAL')]
319+ ),
320+ (
321+ "horses pretend to care about your feelings",
322+ [(0, 6, 'ANIMAL')]
323+ ),
324+ (
325+ "they pretend to care about your feelings, those horses",
326+ [(48, 54, 'ANIMAL')]
327+ ),
328+ (
329+ "horses?",
330+ [(0, 6, 'ANIMAL')]
331+ )
332+
333+ ]
334+ nlp.entity.add_label('ANIMAL')
335+ train_ner(nlp, train_data, output_directory)
336+
337+ # Test that the entity is recognized
338+ doc = nlp('Do you like horses?')
339+ print("Ents in 'Do you like horses?':")
340+>>>>>>> 841e2225
341 for ent in doc.ents:
342 print(ent.label_, ent.text)
343
344diff --git a/examples/training/train_tagger_standalone_ud.py b/examples/training/train_tagger_standalone_ud.py
345new file mode 100644
346index 00000000..ce1ab50d
347--- /dev/null
348+++ b/examples/training/train_tagger_standalone_ud.py
349@@ -0,0 +1,164 @@
350+'''
351+This example shows training of the POS tagger without the Language class,
352+showing the APIs of the atomic components.
353+
354+This example was adapted from the gist here:
355+
356+https://gist.github.com/kamac/a7bc139f62488839a8118214a4d932f2
357+
358+Issue discussing the gist:
359+
360+https://github.com/explosion/spaCy/issues/1179
361+
362+The example was written for spaCy 1.8.2.
363+'''
364+from __future__ import unicode_literals
365+from __future__ import print_function
366+
367+import plac
368+import codecs
369+import spacy.symbols as symbols
370+import spacy
371+from pathlib import Path
372+
373+from spacy.vocab import Vocab
374+from spacy.tagger import Tagger
375+from spacy.tokens import Doc
376+from spacy.gold import GoldParse
377+from spacy.language import Language
378+from spacy import orth
379+from spacy import attrs
380+
381+import random
382+
383+TAG_MAP = {
384+ 'ADJ': {symbols.POS: symbols.ADJ},
385+ 'ADP': {symbols.POS: symbols.ADP},
386+ 'PUNCT': {symbols.POS: symbols.PUNCT},
387+ 'ADV': {symbols.POS: symbols.ADV},
388+ 'AUX': {symbols.POS: symbols.AUX},
389+ 'SYM': {symbols.POS: symbols.SYM},
390+ 'INTJ': {symbols.POS: symbols.INTJ},
391+ 'CCONJ': {symbols.POS: symbols.CCONJ},
392+ 'X': {symbols.POS: symbols.X},
393+ 'NOUN': {symbols.POS: symbols.NOUN},
394+ 'DET': {symbols.POS: symbols.DET},
395+ 'PROPN': {symbols.POS: symbols.PROPN},
396+ 'NUM': {symbols.POS: symbols.NUM},
397+ 'VERB': {symbols.POS: symbols.VERB},
398+ 'PART': {symbols.POS: symbols.PART},
399+ 'PRON': {symbols.POS: symbols.PRON},
400+ 'SCONJ': {symbols.POS: symbols.SCONJ},
401+}
402+
403+LEX_ATTR_GETTERS = {
404+ attrs.LOWER: lambda string: string.lower(),
405+ attrs.NORM: lambda string: string,
406+ attrs.SHAPE: orth.word_shape,
407+ attrs.PREFIX: lambda string: string[0],
408+ attrs.SUFFIX: lambda string: string[-3:],
409+ attrs.CLUSTER: lambda string: 0,
410+ attrs.IS_ALPHA: orth.is_alpha,
411+ attrs.IS_ASCII: orth.is_ascii,
412+ attrs.IS_DIGIT: lambda string: string.isdigit(),
413+ attrs.IS_LOWER: orth.is_lower,
414+ attrs.IS_PUNCT: orth.is_punct,
415+ attrs.IS_SPACE: lambda string: string.isspace(),
416+ attrs.IS_TITLE: orth.is_title,
417+ attrs.IS_UPPER: orth.is_upper,
418+ attrs.IS_BRACKET: orth.is_bracket,
419+ attrs.IS_QUOTE: orth.is_quote,
420+ attrs.IS_LEFT_PUNCT: orth.is_left_punct,
421+ attrs.IS_RIGHT_PUNCT: orth.is_right_punct,
422+ attrs.LIKE_URL: orth.like_url,
423+ attrs.LIKE_NUM: orth.like_number,
424+ attrs.LIKE_EMAIL: orth.like_email,
425+ attrs.IS_STOP: lambda string: False,
426+ attrs.IS_OOV: lambda string: True
427+}
428+
429+
430+def read_ud_data(path):
431+ data = []
432+ last_number = -1
433+ sentence_words = []
434+ sentence_tags = []
435+ with codecs.open(path, encoding="utf-8") as f:
436+ while True:
437+ line = f.readline()
438+ if not line:
439+ break
440+
441+ if line[0].isdigit():
442+ d = line.split()
443+ if not "-" in d[0]:
444+ number = int(line[0])
445+ if number < last_number:
446+ data.append((sentence_words, sentence_tags),)
447+ sentence_words = []
448+ sentence_tags = []
449+ sentence_words.append(d[2])
450+ sentence_tags.append(d[3])
451+ last_number = number
452+ if len(sentence_words) > 0:
453+ data.append((sentence_words, sentence_tags,))
454+ return data
455+
456+def ensure_dir(path):
457+ if not path.exists():
458+ path.mkdir()
459+
460+
461+def main(train_loc, dev_loc, output_dir=None):
462+ if output_dir is not None:
463+ output_dir = Path(output_dir)
464+ ensure_dir(output_dir)
465+ ensure_dir(output_dir / "pos")
466+ ensure_dir(output_dir / "vocab")
467+
468+ train_data = read_ud_data(train_loc)
469+ vocab = Vocab(tag_map=TAG_MAP, lex_attr_getters=LEX_ATTR_GETTERS)
470+ # Populate vocab
471+ for words, _ in train_data:
472+ for word in words:
473+ _ = vocab[word]
474+
475+ model = spacy.tagger.TaggerModel(spacy.tagger.Tagger.feature_templates)
476+ tagger = Tagger(vocab, model)
477+ print(tagger.tag_names)
478+ for i in range(30):
479+ print("training model (iteration " + str(i) + ")...")
480+ score = 0.
481+ num_samples = 0.
482+ for words, tags in train_data:
483+ doc = Doc(vocab, words=words)
484+ gold = GoldParse(doc, tags=tags)
485+ cost = tagger.update(doc, gold)
486+ for i, word in enumerate(doc):
487+ num_samples += 1
488+ if word.tag_ == tags[i]:
489+ score += 1
490+ print('Train acc', score/num_samples)
491+ random.shuffle(train_data)
492+ tagger.model.end_training()
493+
494+ score = 0.0
495+ test_data = read_ud_data(dev_loc)
496+ num_samples = 0
497+ for words, tags in test_data:
498+ doc = Doc(vocab, words)
499+ tagger(doc)
500+ for i, word in enumerate(doc):
501+ num_samples += 1
502+ if word.tag_ == tags[i]:
503+ score += 1
504+ print("score: " + str(score / num_samples * 100.0))
505+
506+ if output_dir is not None:
507+ tagger.model.dump(str(output_dir / 'pos' / 'model'))
508+ with (output_dir / 'vocab' / 'strings.json').open('w') as file_:
509+ tagger.vocab.strings.dump(file_)
510+
511+
512+if __name__ == '__main__':
513+ plac.call(main)
514diff --git a/requirements.txt b/requirements.txt
515index 01e41c99..84def41c 100644
516--- a/requirements.txt
517+++ b/requirements.txt
518@@ -7,10 +7,11 @@ thinc>=6.10.0,<6.11.0
519 murmurhash>=0.28,<0.29
520 plac<1.0.0,>=0.9.6
521 six
522+html5lib==1.0b8
523 ujson>=1.35
524 dill>=0.2,<0.3
525-requests>=2.13.0,<3.0.0
526-regex==2017.4.5
527+requests>=2.11.0,<3.0.0
528+regex>=2017.4.1,<2017.12.1
529 ftfy>=4.4.2,<5.0.0
530 pytest>=3.0.6,<4.0.0
531 mock>=2.0.0,<3.0.0
532diff --git a/setup.py b/setup.py
533index 727df5e4..ea45bb9e 100755
534--- a/setup.py
535+++ b/setup.py
536@@ -192,6 +192,7 @@ def setup_package():
537 'preshed>=1.0.0,<2.0.0',
538 'thinc>=6.10.0,<6.11.0',
539 'plac<1.0.0,>=0.9.6',
540+ 'pip>=9.0.0,<10.0.0',
541 'six',
542 'pathlib',
543 'ujson>=1.35',
544diff --git a/spacy/lang/fr/stop_words.py b/spacy/lang/fr/stop_words.py
545index d9b82053..71f124d6 100644
546--- a/spacy/lang/fr/stop_words.py
547+++ b/spacy/lang/fr/stop_words.py
548@@ -86,3 +86,28 @@ votre vous vous-mêmes vu vé vôtre vôtres
549
550 zut
551 """.split())
552+
553+
554+
555+# Number words
556+
557+NUM_WORDS = set("""
558+zero un deux trois quatre cinq six sept huit neuf dix
559+onze douze treize quatorze quinze seize dix-sept dix-huit dix-neuf
560+vingt trente quanrante cinquante soixante septante quatre-vingt huitante nonante
561+cent mille mil million milliard billion quadrillion quintillion
562+sextillion septillion octillion nonillion decillion
563+""".split())
564+
565+# Ordinal words
566+
567+ORDINAL_WORDS = set("""
568+premier deuxième second troisième quatrième cinquième sixième septième huitième neuvième dixième
569+onzième douzième treizième quatorzième quinzième seizième dix-septième dix-huitième dix-neufième
570+vingtième trentième quanrantième cinquantième soixantième septantième quatre-vingtième huitantième nonantième
571+centième millième millionnième milliardième billionnième quadrillionnième quintillionnième
572+sextillionnième septillionnième octillionnième nonillionnième decillionnième
573+""".split())
574+
575+
576+
577diff --git a/spacy/lang/nl/stop_words.py b/spacy/lang/nl/stop_words.py
578deleted file mode 100644
579index 22f1d714..00000000
580--- a/spacy/lang/nl/stop_words.py
581+++ /dev/null
582@@ -1,43 +0,0 @@
583-# coding: utf8
584-from __future__ import unicode_literals
585-
586-
587-# Stop words are retrieved from http://www.damienvanholten.com/downloads/dutch-stop-words.txt
588-
589-STOP_WORDS = set("""
590-aan af al alles als altijd andere
591-
592-ben bij
593-
594-daar dan dat de der deze die dit doch doen door dus
595-
596-een eens en er
597-
598-ge geen geweest
599-
600-haar had heb hebben heeft hem het hier hij hoe hun
601-
602-iemand iets ik in is
603-
604-ja je
605-
606-kan kon kunnen
607-
608-maar me meer men met mij mijn moet
609-
610-na naar niet niets nog nu
611-
612-of om omdat ons ook op over
613-
614-reeds
615-
616-te tegen toch toen tot
617-
618-u uit uw
619-
620-van veel voor
621-
622-want waren was wat we wel werd wezen wie wij wil worden
623-
624-zal ze zei zelf zich zij zijn zo zonder zou
625-""".split())
626diff --git a/spacy/pattern/__init__.py b/spacy/pattern/__init__.py
627new file mode 100644
628index 00000000..325ba04e
629--- /dev/null
630+++ b/spacy/pattern/__init__.py
631@@ -0,0 +1,4 @@
632+# coding: utf-8
633+
634+from .pattern import DependencyTree
635+from .parser import PatternParser
636diff --git a/spacy/pattern/parser.py b/spacy/pattern/parser.py
637new file mode 100644
638index 00000000..122d2b8f
639--- /dev/null
640+++ b/spacy/pattern/parser.py
641@@ -0,0 +1,377 @@
642+# coding: utf-8
643+
644+from spacy.compat import intern, queue
645+from spacy.strings import hash_string
646+from operator import itemgetter
647+import re
648+import json
649+
650+from .pattern import DependencyPattern
651+
652+TOKEN_INITIAL = intern('initial')
653+
654+
655+class PatternParser(object):
656+ """Compile a Pattern query into a :class:`Pattern`, that can be used to
657+ match :class:`DependencyTree`s."""
658+ whitespace_re = re.compile(r'\s+', re.U)
659+ newline_re = re.compile(r'(\r\n|\r|\n)')
660+ name_re = re.compile(r'\w+', re.U)
661+
662+ TOKEN_BLOCK_BEGIN = '['
663+ TOKEN_BLOCK_END = ']'
664+ EDGE_BLOCK_BEGIN = '>'
665+ WHITESPACE = ' '
666+
667+ @classmethod
668+ def parse(cls, query):
669+ """Parse the given `query`, and compile it into a :class:`Pattern`."""
670+ pattern = DependencyPattern()
671+
672+ for lineno, token_stream in enumerate(cls.tokenize(query)):
673+ try:
674+ cls._parse_line(token_stream, pattern, lineno+1)
675+ except StopIteration:
676+ raise SyntaxError("A token is missing, please check your "
677+ "query.")
678+
679+ if not pattern.nodes:
680+ return
681+
682+ cls.check_pattern(pattern)
683+ return pattern
684+
685+ @staticmethod
686+ def check_pattern(pattern):
687+ if not pattern.is_connected():
688+ raise ValueError("The pattern tree must be a fully connected "
689+ "graph.")
690+
691+ if pattern.root_node is None:
692+ raise ValueError("The root node of the tree could not be found.")
693+
694+ @classmethod
695+ def _parse_line(cls, stream, pattern, lineno):
696+ while not stream.closed:
697+ token = stream.current
698+
699+ if token.type == 'name':
700+ next_token = stream.look()
701+
702+ if next_token.type == 'node':
703+ cls.parse_node_def(stream, pattern)
704+
705+ elif next_token.type == 'edge':
706+ cls.parse_edge_def(stream, pattern)
707+
708+ else:
709+ raise SyntaxError("line %d: A 'node' or 'edge' token must "
710+ "follow a 'name' token." % lineno)
711+
712+ elif token.type == 'node':
713+ next_token = stream.look()
714+
715+ if next_token.type == 'edge':
716+ cls.parse_edge_def(stream, pattern)
717+ else:
718+ raise SyntaxError("line %d: an 'edge' token is "
719+ "expected." % lineno)
720+
721+ if not stream.closed:
722+ next(stream)
723+
724+ @classmethod
725+ def parse_node_def(cls, stream, pattern):
726+ name_token = stream.current
727+ next(stream)
728+ node_token = stream.current
729+ cls.add_node(node_token, pattern, name_token)
730+
731+ @classmethod
732+ def add_node(cls, node_token, pattern, name_token=None):
733+ token_name = None
734+ if name_token is not None:
735+ token_id = name_token.value
736+ token_name = name_token.value
737+ else:
738+ token_id = node_token.hash()
739+
740+ if token_id in pattern.nodes:
741+ raise SyntaxError("Token with ID '{}' already registered.".format(
742+ token_id))
743+
744+ token_attr = cls.parse_node_attributes(node_token.value)
745+ token_attr['_name'] = token_name
746+ pattern.add_node(token_id, token_attr)
747+
748+ @classmethod
749+ def parse_edge_def(cls, stream, pattern):
750+ token = stream.current
751+
752+ if token.type == 'name':
753+ token_id = token.value
754+ if token_id not in pattern.nodes:
755+ raise SyntaxError("Token '{}' with ID '{}' is not "
756+ "defined.".format(token, token_id))
757+
758+ elif token.type == 'node':
759+ token_id = token.hash()
760+ cls.add_node(token, pattern)
761+
762+ next(stream)
763+ edge_attr = cls.parse_edge_attributes(stream.current.value)
764+ next(stream)
765+
766+ head_token = stream.current
767+ if head_token.type == 'name':
768+ head_token_id = head_token.value
769+ if head_token_id not in pattern.nodes:
770+ raise SyntaxError("Token '{}' with ID '{}' is not "
771+ "defined.".format(head_token, head_token_id))
772+ elif head_token.type == 'node':
773+ head_token_id = head_token.hash()
774+ cls.add_node(head_token, pattern)
775+ else:
776+ raise SyntaxError("A 'node' or 'name' token was expected.")
777+
778+ # inverse the dependency to have an actual tree
779+ pattern.add_edge(head_token_id, token_id, edge_attr)
780+
781+ @classmethod
782+ def parse_node_attributes(cls, string):
783+ string = string[1:] # remove the trailing '['
784+ end_delimiter_idx = string.find(']')
785+
786+ attr_str = string[:end_delimiter_idx]
787+ attr = {}
788+
789+ try:
790+ attr = json.loads(attr_str)
791+ except json.JSONDecodeError:
792+ for pair in attr_str.split(","):
793+ key, value = pair.split(':')
794+ attr[key] = value
795+
796+ for key, value in attr.items():
797+ attr[key] = cls.compile_expression(value)
798+
799+ alias = string[end_delimiter_idx+2:]
800+
801+ if alias:
802+ attr['_alias'] = alias
803+
804+ return attr
805+
806+ @classmethod
807+ def parse_edge_attributes(cls, string):
808+ string = string[1:] # remove the trailing '>'
809+
810+ if not string:
811+ return None
812+
813+ return cls.compile_expression(string)
814+
815+ @staticmethod
816+ def compile_expression(expr):
817+ if expr.startswith('/') and expr.endswith('/'):
818+ string = expr[1:-1]
819+ return re.compile(string, re.U)
820+
821+ return expr
822+
823+ @classmethod
824+ def tokenize(cls, text):
825+ lines = text.splitlines()
826+
827+ for lineno, line in enumerate(lines):
828+ yield TokenStream(cls._tokenize_line(line, lineno+1))
829+
830+ @classmethod
831+ def _tokenize_line(cls, line, lineno):
832+ reader = Reader(line)
833+
834+ while reader.remaining():
835+ char = reader.next()
836+
837+ if char == cls.TOKEN_BLOCK_BEGIN:
838+ token = 'node'
839+ idx = reader.find(cls.TOKEN_BLOCK_END)
840+
841+ if idx == -1:
842+ raise SyntaxError("A token block end ']' was expected.")
843+
844+ idx += 1
845+ if len(reader) > idx and reader[idx] == '=':
846+ # The node has a name
847+ idx = reader.find(cls.WHITESPACE, start=idx)
848+
849+ if idx == -1:
850+ idx = reader.remaining()
851+
852+ elif char == cls.EDGE_BLOCK_BEGIN:
853+ token = 'edge'
854+ idx = reader.find(cls.WHITESPACE)
855+
856+ elif cls.name_re.match(char):
857+ token = 'name'
858+ idx = reader.find(cls.WHITESPACE)
859+
860+ if idx == -1:
861+ whole_name_match = cls.name_re.match(str(reader))
862+ idx = whole_name_match.end()
863+
864+ elif cls.newline_re.match(char) or cls.whitespace_re.match(char):
865+ # skip the whitespace
866+ reader.consume()
867+ continue
868+
869+ else:
870+ raise SyntaxError("Unrecognized token BEGIN char: '{"
871+ "}'".format(char))
872+
873+ if idx == -1:
874+ raise SyntaxError("Ending character of token '{}' not "
875+ "found.".format(token))
876+ value = reader.consume(idx)
877+
878+ yield Token(lineno, token, value)
879+
880+
881+class Reader(object):
882+ """A class used by the :class:`PatternParser` to tokenize the `text`."""
883+ __slots__ = ('text', 'pos')
884+
885+ def __init__(self, text):
886+ self.text = text
887+ self.pos = 0
888+
889+ def find(self, needle, start=0, end=None):
890+ pos = self.pos
891+ start += pos
892+ if end is None:
893+ index = self.text.find(needle, start)
894+ else:
895+ end += pos
896+ index = self.text.find(needle, start, end)
897+ if index != -1:
898+ index -= pos
899+ return index
900+
901+ def consume(self, count=1):
902+ new_pos = self.pos + count
903+ s = self.text[self.pos:new_pos]
904+ self.pos = new_pos
905+ return s
906+
907+ def next(self):
908+ return self.text[self.pos:self.pos + 1]
909+
910+ def remaining(self):
911+ return len(self.text) - self.pos
912+
913+ def __len__(self):
914+ return self.remaining()
915+
916+ def __getitem__(self, key):
917+ if key < 0:
918+ return self.text[key]
919+ else:
920+ return self.text[self.pos + key]
921+
922+ def __str__(self):
923+ return self.text[self.pos:]
924+
925+
926+# The following classes were copied from Jinja2, a BSD-licensed project,
927+# and slightly modified: Token, TokenStreamIterator, TokenStream.
928+
929+class Token(tuple):
930+ """Token class."""
931+ __slots__ = ()
932+ lineno, type, value = (property(itemgetter(x)) for x in range(3))
933+
934+ def __new__(cls, lineno, type, value):
935+ return tuple.__new__(cls, (lineno, intern(str(type)), value))
936+
937+ def hash(self):
938+ string = self.value
939+ return hash_string(string)
940+
941+ def __repr__(self):
942+ return 'Token(%r, %r, %r)' % (
943+ self.lineno,
944+ self.type,
945+ self.value)
946+
947+
948+class TokenStreamIterator(object):
949+ """The iterator for tokenstreams. Iterate over the stream until the
950+ stream is empty.
951+ """
952+
953+ def __init__(self, stream):
954+ self.stream = stream
955+
956+ def __iter__(self):
957+ return self
958+
959+ def __next__(self):
960+ token = self.stream.current
961+ try:
962+ next(self.stream)
963+ except StopIteration:
964+ self.stream.close()
965+ raise StopIteration()
966+
967+ return token
968+
969+
970+class TokenStream(object):
971+ """A token stream is an iterable that yields :class:`Token`s. The
972+ current active token is stored as :attr:`current`.
973+ """
974+
975+ def __init__(self, generator):
976+ self._iter = iter(generator)
977+ self._pushed = queue.deque()
978+ self.closed = False
979+ self.current = Token(1, TOKEN_INITIAL, '')
980+ next(self)
981+
982+ def __iter__(self):
983+ return TokenStreamIterator(self)
984+
985+ def __bool__(self):
986+ return bool(self._pushed)
987+ __nonzero__ = __bool__ # py2
988+
989+ def push(self, token):
990+ """Push a token back to the stream."""
991+ self._pushed.append(token)
992+
993+ def look(self):
994+ """Look at the next token."""
995+ old_token = next(self)
996+ result = self.current
997+ self.push(result)
998+ self.current = old_token
999+ return result
1000+
1001+ def __next__(self):
1002+ """Go one token ahead and return the old one."""
1003+ rv = self.current
1004+ if self._pushed:
1005+ self.current = self._pushed.popleft()
1006+ else:
1007+ if self.closed:
1008+ raise StopIteration("No token left.")
1009+ try:
1010+ self.current = next(self._iter)
1011+ except StopIteration:
1012+ self.close()
1013+ return rv
1014+
1015+ def close(self):
1016+ """Close the stream."""
1017+ self._iter = None
1018+ self.closed = True
1019diff --git a/spacy/pattern/pattern.py b/spacy/pattern/pattern.py
1020new file mode 100644
1021index 00000000..55228306
1022--- /dev/null
1023+++ b/spacy/pattern/pattern.py
1024@@ -0,0 +1,318 @@
1025+# coding: utf-8
1026+
1027+import logging
1028+from collections import defaultdict
1029+
1030+
1031+logger = logging.getLogger(__name__)
1032+
1033+
1034+class Tree(object):
1035+ def __init__(self):
1036+ self.adjacency = defaultdict(dict)
1037+ self.nodes = {}
1038+
1039+ def __getitem__(self, item):
1040+ return self.nodes[item]
1041+
1042+ def add_node(self, node, attr_dict=None):
1043+ attr_dict = attr_dict or {}
1044+ self.nodes[node] = attr_dict
1045+
1046+ def add_edge(self, u, v, dep=None):
1047+ if u not in self.nodes or v not in self.nodes:
1048+ raise ValueError("Each node must be defined before adding an edge.")
1049+
1050+ self.adjacency[u][v] = dep
1051+
1052+ def number_of_nodes(self):
1053+ return len(self)
1054+
1055+ def __len__(self):
1056+ return len(self.nodes)
1057+
1058+ def number_of_edges(self):
1059+ return sum(len(adj_dict) for adj_dict in self.adjacency.values())
1060+
1061+ def edges_iter(self, origin=None, data=True):
1062+ nbunch = (self.adjacency.items() if origin is None
1063+ else [(origin, self.adjacency[origin])])
1064+
1065+ for u, nodes in nbunch:
1066+ for v, dep in nodes.items():
1067+ if data:
1068+ yield (u, v, dep)
1069+ else:
1070+ yield (u, v)
1071+
1072+ def nodes_iter(self):
1073+ for node in self.nodes.keys():
1074+ yield node
1075+
1076+ def is_connected(self):
1077+ if len(self) == 0:
1078+ raise ValueError('Connectivity is undefined for the null graph.')
1079+ return len(set(self._plain_bfs(next(self.nodes_iter()),
1080+ undirected=True))) == len(self)
1081+
1082+ def _plain_bfs(self, source, undirected=False):
1083+ """A fast BFS node generator.
1084+ :param: source: the source node
1085+ """
1086+ seen = set()
1087+ next_level = {source}
1088+ while next_level:
1089+ this_level = next_level
1090+ next_level = set()
1091+ for v in this_level:
1092+ if v not in seen:
1093+ yield v
1094+ seen.add(v)
1095+ next_level.update(self.adjacency[v].keys())
1096+
1097+ if undirected:
1098+ for n, adj in self.adjacency.items():
1099+ if v in adj.keys():
1100+ next_level.add(n)
1101+
1102+
1103+class DependencyPattern(Tree):
1104+ @property
1105+ def root_node(self):
1106+ if self.number_of_nodes() == 1:
1107+ # if the graph has a single node, it is the root
1108+ return next(iter(self.nodes.keys()))
1109+
1110+ if not self.is_connected():
1111+ return None
1112+
1113+ in_node = set()
1114+ out_node = set()
1115+ for u, v in self.edges_iter(data=False):
1116+ in_node.add(v)
1117+ out_node.add(u)
1118+
1119+ try:
1120+ return list(out_node.difference(in_node))[0]
1121+ except IndexError:
1122+ return None
1123+
1124+
1125+class DependencyTree(Tree):
1126+ def __init__(self, doc):
1127+ super(DependencyTree, self).__init__()
1128+
1129+ for token in doc:
1130+ self.nodes[token.i] = token
1131+
1132+ if token.head.i != token.i:
1133+ # inverse the dependency to have an actual tree
1134+ self.adjacency[token.head.i][token.i] = token.dep_
1135+
1136+ def __getitem__(self, item):
1137+ return self.nodes[item]
1138+
1139+ def match_nodes(self, attr_dict, **kwargs):
1140+ results = []
1141+ for token_idx, token in self.nodes.items():
1142+ if match_token(token, attr_dict, **kwargs):
1143+ results.append(token_idx)
1144+
1145+ return results
1146+
1147+ def match(self, pattern):
1148+ """Return a list of matches between the given
1149+ :class:`DependencyPattern` and `self` if any, or None.
1150+
1151+ :param pattern: a :class:`DependencyPattern`
1152+ """
1153+ pattern_root_node = pattern.root_node
1154+ pattern_root_node_attr = pattern[pattern_root_node]
1155+ dep_root_nodes = self.match_nodes(pattern_root_node_attr)
1156+
1157+ if not dep_root_nodes:
1158+ logger.debug("No node matches the pattern root "
1159+ "'{}'".format(pattern_root_node_attr))
1160+
1161+ matches = []
1162+ for candidate_root_node in dep_root_nodes:
1163+ match_list = subtree_in_graph(candidate_root_node, self,
1164+ pattern_root_node, pattern)
1165+ for mapping in match_list:
1166+ match = PatternMatch(mapping, pattern, self)
1167+ matches.append(match)
1168+
1169+ return matches
1170+
1171+
1172+class PatternMatch(object):
1173+ def __init__(self, mapping, pattern, tree):
1174+ for pattern_node_id, tree_node_id in mapping.items():
1175+ mapping[pattern_node_id] = tree[tree_node_id]
1176+ self.mapping = mapping
1177+ self.pattern = pattern
1178+ self.tree = tree
1179+
1180+ self.alias_map = {}
1181+ for pattern_node_id in self.mapping:
1182+ pattern_node = self.pattern[pattern_node_id]
1183+
1184+ alias = pattern_node.get('_alias')
1185+ if alias:
1186+ self.alias_map[alias] = self.mapping[pattern_node_id]
1187+
1188+ def __repr__(self):
1189+ return "<Pattern Match: {} node>".format(len(self.mapping))
1190+
1191+ def __getitem__(self, item):
1192+ return self.alias_map[item]
1193+
1194+
1195+def subtree_in_graph(dep_tree_node, dep_tree, pattern_node, pattern):
1196+ """Return a list of matches of `pattern` as a subtree of `dep_tree`.
1197+ :param dep_tree_node: the token (identified by its index) to start from
1198+ (int)
1199+ :param dep_tree: a :class:`DependencyTree`
1200+ :param pattern_node: the pattern node to start from
1201+ :param pattern: a :class:`DependencyPattern`
1202+ :return: found matches (list)
1203+ """
1204+ results = []
1205+ association_dict = {pattern_node: dep_tree_node}
1206+ _subtree_in_graph(dep_tree_node, dep_tree, pattern_node,
1207+ pattern, results=results,
1208+ association_dict=association_dict)
1209+ results = results or []
1210+ return results
1211+
1212+
1213+def _subtree_in_graph(dep_tree_node, dep_tree, pattern_node, pattern,
1214+ association_dict=None, results=None):
1215+ token = dep_tree[dep_tree_node]
1216+ logger.debug("Starting from token '{}'".format(token.orth_))
1217+
1218+ adjacent_edges = list(pattern.edges_iter(origin=pattern_node))
1219+ if adjacent_edges:
1220+ for (_, adjacent_pattern_node,
1221+ dep) in adjacent_edges:
1222+ adjacent_pattern_node_attr = pattern[adjacent_pattern_node]
1223+ logger.debug("Exploring relation {} -[{}]-> {} from "
1224+ "pattern".format(pattern[pattern_node],
1225+ dep,
1226+ adjacent_pattern_node_attr))
1227+
1228+ adjacent_nodes = find_adjacent_nodes(dep_tree,
1229+ dep_tree_node,
1230+ dep,
1231+ adjacent_pattern_node_attr)
1232+
1233+ if not adjacent_nodes:
1234+ logger.debug("No adjacent nodes in dep_tree satisfying these "
1235+ "conditions.")
1236+ return None
1237+
1238+ for adjacent_node in adjacent_nodes:
1239+ logger.debug("Found adjacent node '{}' in "
1240+ "dep_tree".format(dep_tree[adjacent_node].orth_))
1241+ association_dict[adjacent_pattern_node] = adjacent_node
1242+ recursive_return = _subtree_in_graph(adjacent_node,
1243+ dep_tree,
1244+ adjacent_pattern_node,
1245+ pattern,
1246+ association_dict,
1247+ results=results)
1248+
1249+ if recursive_return is None:
1250+ # No Match
1251+ return None
1252+
1253+ association_dict, results = recursive_return
1254+
1255+ else:
1256+ if len(association_dict) == pattern.number_of_nodes():
1257+ logger.debug("Add to results: {}".format(association_dict))
1258+ results.append(dict(association_dict))
1259+
1260+ else:
1261+ logger.debug("{} nodes in subgraph, only {} "
1262+ "mapped".format(pattern.number_of_nodes(),
1263+ len(association_dict)))
1264+
1265+ logger.debug("Return intermediate: {}".format(association_dict))
1266+ return association_dict, results
1267+
1268+
1269+def find_adjacent_nodes(dep_tree, node, target_dep, node_attributes):
1270+ """Find nodes adjacent to ``node`` that fulfill specified attributes
1271+ values on edge and node.
1272+
1273+ :param dep_tree: a :class:`DependencyTree`
1274+ :param node: initial node to search from
1275+ :param target_dep: edge attributes that must be fulfilled (pair-value)
1276+ :type target_dep: dict
1277+ :param node_attributes: node attributes that must be fulfilled (pair-value)
1278+ :type node_attributes: dict
1279+ :return: adjacent nodes that fulfill the given criteria (list)
1280+ """
1281+ results = []
1282+ for _, adj_node, adj_dep in dep_tree.edges_iter(origin=node):
1283+ adj_token = dep_tree[adj_node]
1284+ if (match_edge(adj_dep, target_dep)
1285+ and match_token(adj_token, node_attributes)):
1286+ results.append(adj_node)
1287+
1288+ return results
1289+
1290+
1291+def match_edge(token_dep, target_dep):
1292+ if target_dep is None:
1293+ return True
1294+
1295+ if hasattr(target_dep, 'match'):
1296+ return target_dep.match(token_dep) is not None
1297+
1298+ if token_dep == target_dep:
1299+ return True
1300+
1301+ return False
1302+
1303+
1304+def match_token(token,
1305+ target_attributes,
1306+ ignore_special_key=True,
1307+ lower=True):
1308+ bind_map = {
1309+ 'word': lambda t: t.orth_,
1310+ 'lemma': lambda t: t.lemma_,
1311+ 'ent': lambda t: t.ent_type_,
1312+ }
1313+
1314+ for target_key, target_value in target_attributes.items():
1315+ is_special_key = target_key[0] == '_'
1316+
1317+ if ignore_special_key and is_special_key:
1318+ continue
1319+
1320+ if lower and hasattr(target_value, 'lower'):
1321+ target_value = target_value.lower()
1322+
1323+ if target_key in bind_map:
1324+ token_attr = bind_map[target_key](token)
1325+
1326+ if lower:
1327+ token_attr = token_attr.lower()
1328+
1329+ if hasattr(target_value, 'match'): # if it is a compiled regex
1330+ if target_value.match(token_attr) is None:
1331+ break
1332+ else:
1333+ if not token_attr == target_value:
1334+ break
1335+
1336+ else:
1337+ raise ValueError("Unknown key: '{}'".format(target_key))
1338+
1339+ else: # the loop was not broken
1340+ return True
1341+
1342+ return False
1343diff --git a/spacy/ru/__init__.py b/spacy/ru/__init__.py
1344new file mode 100644
1345index 00000000..8789cd6e
1346--- /dev/null
1347+++ b/spacy/ru/__init__.py
1348@@ -0,0 +1,78 @@
1349+# encoding: utf8
1350+from __future__ import unicode_literals, print_function
1351+
1352+from ..language import Language
1353+from ..attrs import LANG
1354+from ..tokens import Doc
1355+from .language_data import *
1356+
1357+
1358+class RussianTokenizer(object):
1359+ _morph = None
1360+
1361+ def __init__(self, spacy_tokenizer, cls, nlp=None):
1362+ try:
1363+ from pymorphy2 import MorphAnalyzer
1364+ except ImportError:
1365+ raise ImportError(
1366+ "The Russian tokenizer requires the pymorphy2 library: "
1367+ "try to fix it with "
1368+ "pip install pymorphy2==0.8")
1369+
1370+ RussianTokenizer._morph = RussianTokenizer._create_morph(MorphAnalyzer)
1371+
1372+ self.vocab = nlp.vocab if nlp else cls.create_vocab(nlp)
1373+ self._spacy_tokenizer = spacy_tokenizer
1374+
1375+ def __call__(self, text):
1376+ get_norm = RussianTokenizer._get_norm
1377+ has_space = RussianTokenizer._has_space
1378+
1379+ words_with_space_flags = [(get_norm(token), has_space(token, text))
1380+ for token in self._spacy_tokenizer(text)]
1381+
1382+ words, spaces = map(lambda s: list(s), zip(*words_with_space_flags))
1383+
1384+ return Doc(self.vocab, words, spaces)
1385+
1386+ @staticmethod
1387+ def _get_word(token):
1388+ return token.lemma_ if len(token.lemma_) > 0 else token.text
1389+
1390+ @staticmethod
1391+ def _has_space(token, text):
1392+ pos_after_token = token.idx + len(token.text)
1393+ return pos_after_token < len(text) and text[pos_after_token] == ' '
1394+
1395+ @classmethod
1396+ def _get_norm(cls, token):
1397+ return cls._normalize(cls._get_word(token))
1398+
1399+ @classmethod
1400+ def _normalize(cls, word):
1401+ return cls._morph.parse(word)[0].normal_form
1402+
1403+ @classmethod
1404+ def _create_morph(cls, morph_analyzer_class):
1405+ if not cls._morph:
1406+ cls._morph = morph_analyzer_class()
1407+ return cls._morph
1408+
1409+
1410+class RussianDefaults(Language.Defaults):
1411+ lex_attr_getters = dict(Language.Defaults.lex_attr_getters)
1412+ lex_attr_getters[LANG] = lambda text: 'ru'
1413+
1414+ tokenizer_exceptions = TOKENIZER_EXCEPTIONS
1415+ stop_words = STOP_WORDS
1416+
1417+ @classmethod
1418+ def create_tokenizer(cls, nlp=None):
1419+ tokenizer = super(RussianDefaults, cls).create_tokenizer(nlp)
1420+ return RussianTokenizer(tokenizer, cls, nlp)
1421+
1422+
1423+class Russian(Language):
1424+ lang = 'ru'
1425+
1426+ Defaults = RussianDefaults
1427diff --git a/spacy/ru/language_data.py b/spacy/ru/language_data.py
1428new file mode 100644
1429index 00000000..d33d388f
1430--- /dev/null
1431+++ b/spacy/ru/language_data.py
1432@@ -0,0 +1,18 @@
1433+# encoding: utf8
1434+from __future__ import unicode_literals
1435+
1436+from .. import language_data as base
1437+from ..language_data import update_exc, strings_to_exc
1438+
1439+from .stop_words import STOP_WORDS
1440+from .tokenizer_exceptions import TOKENIZER_EXCEPTIONS
1441+
1442+
1443+STOP_WORDS = set(STOP_WORDS)
1444+TOKENIZER_EXCEPTIONS = dict(TOKENIZER_EXCEPTIONS)
1445+
1446+
1447+update_exc(TOKENIZER_EXCEPTIONS, strings_to_exc(base.EMOTICONS))
1448+
1449+
1450+__all__ = ["STOP_WORDS", "TOKENIZER_EXCEPTIONS"]
1451diff --git a/spacy/ru/stop_words.py b/spacy/ru/stop_words.py
1452new file mode 100644
1453index 00000000..2d89b772
1454--- /dev/null
1455+++ b/spacy/ru/stop_words.py
1456@@ -0,0 +1,54 @@
1457+# encoding: utf8
1458+from __future__ import unicode_literals
1459+
1460+
1461+STOP_WORDS = set("""
1462+а
1463+
1464+будем будет будете будешь буду будут будучи будь будьте бы был была были было
1465+быть
1466+
1467+в вам вами вас весь во вот все всё всего всей всем всём всеми всему всех всею
1468+всея всю вся вы
1469+
1470+да для до
1471+
1472+его едим едят ее её ей ел ела ем ему емъ если ест есть ешь еще ещё ею
1473+
1474+же
1475+
1476+за
1477+
1478+и из или им ими имъ их
1479+
1480+к как кем ко когда кого ком кому комья которая которого которое которой котором
1481+которому которою которую которые который которым которыми которых кто
1482+
1483+меня мне мной мною мог моги могите могла могли могло могу могут мое моё моего
1484+моей моем моём моему моею можем может можете можешь мои мой моим моими моих
1485+мочь мою моя мы
1486+
1487+на нам нами нас наса наш наша наше нашего нашей нашем нашему нашею наши нашим
1488+нашими наших нашу не него нее неё ней нем нём нему нет нею ним ними них но
1489+
1490+о об один одна одни одним одними одних одно одного одной одном одному одною
1491+одну он она оне они оно от
1492+
1493+по при
1494+
1495+с сам сама сами самим самими самих само самого самом самому саму свое своё
1496+своего своей своем своём своему своею свои свой своим своими своих свою своя
1497+себе себя собой собою
1498+
1499+та так такая такие таким такими таких такого такое такой таком такому такою
1500+такую те тебе тебя тем теми тех то тобой тобою того той только том томах тому
1501+тот тою ту ты
1502+
1503+у уже
1504+
1505+чего чем чём чему что чтобы
1506+
1507+эта эти этим этими этих это этого этой этом этому этот этою эту
1508+
1509+я
1510+""".split())
1511diff --git a/spacy/ru/tokenizer_exceptions.py b/spacy/ru/tokenizer_exceptions.py
1512new file mode 100644
1513index 00000000..f444f3df
1514--- /dev/null
1515+++ b/spacy/ru/tokenizer_exceptions.py
1516@@ -0,0 +1,30 @@
1517+# encoding: utf8
1518+from __future__ import unicode_literals
1519+
1520+from ..symbols import *
1521+
1522+
1523+TOKENIZER_EXCEPTIONS = {
1524+ "Пн.": [
1525+ {ORTH: "Пн.", LEMMA: "Понедельник"}
1526+ ],
1527+ "Вт.": [
1528+ {ORTH: "Вт.", LEMMA: "Вторник"}
1529+ ],
1530+ "Ср.": [
1531+ {ORTH: "Ср.", LEMMA: "Среда"}
1532+ ],
1533+ "Чт.": [
1534+ {ORTH: "Чт.", LEMMA: "Четверг"}
1535+ ],
1536+ "Пт.": [
1537+ {ORTH: "Пт.", LEMMA: "Пятница"}
1538+ ],
1539+ "Сб.": [
1540+ {ORTH: "Сб.", LEMMA: "Суббота"}
1541+ ],
1542+ "Вс.": [
1543+ {ORTH: "Вс.", LEMMA: "Воскресенье"}
1544+ ],
1545+}
1546+
1547diff --git a/spacy/tests/conftest.py b/spacy/tests/conftest.py
1548index 2d1b0351..0c210e17 100644
1549--- a/spacy/tests/conftest.py
1550+++ b/spacy/tests/conftest.py
1551@@ -67,6 +67,7 @@ def en_parser(en_vocab):
1552 return nlp.create_pipe('parser')
1553
1554
1555+
1556 @pytest.fixture
1557 def es_tokenizer():
1558 return util.get_lang_class('es').Defaults.create_tokenizer()
1559@@ -97,6 +98,18 @@ def id_tokenizer():
1560 return util.get_lang_class('id').Defaults.create_tokenizer()
1561
1562
1563+@pytest.fixture
1564+def ja_tokenizer():
1565+ pytest.importorskip("MeCab")
1566+ return Japanese.Defaults.create_tokenizer()
1567+
1568+
1569+@pytest.fixture
1570+def japanese():
1571+ pytest.importorskip("MeCab")
1572+ return Japanese()
1573+
1574+
1575 @pytest.fixture
1576 def sv_tokenizer():
1577 return util.get_lang_class('sv').Defaults.create_tokenizer()
1578@@ -117,10 +130,30 @@ def he_tokenizer():
1579 return util.get_lang_class('he').Defaults.create_tokenizer()
1580
1581
1582+
1583 @pytest.fixture
1584 def nb_tokenizer():
1585 return util.get_lang_class('nb').Defaults.create_tokenizer()
1586
1587+
1588+@pytest.fixture
1589+def th_tokenizer():
1590+ pythainlp = pytest.importorskip("pythainlp")
1591+ return Thai.Defaults.create_tokenizer()
1592+
1593+
1594+@pytest.fixture
1595+def ru_tokenizer():
1596+ pytest.importorskip("pymorphy2")
1597+ return Russian.Defaults.create_tokenizer()
1598+
1599+
1600+@pytest.fixture
1601+def russian():
1602+ pytest.importorskip("pymorphy2")
1603+ return Russian()
1604+
1605+
1606 @pytest.fixture
1607 def da_tokenizer():
1608 return util.get_lang_class('da').Defaults.create_tokenizer()
1609@@ -159,11 +192,11 @@ def text_file_b():
1610
1611 def pytest_addoption(parser):
1612 parser.addoption("--models", action="store_true",
1613- help="include tests that require full models")
1614+ help="include tests that require full models")
1615 parser.addoption("--vectors", action="store_true",
1616- help="include word vectors tests")
1617+ help="include word vectors tests")
1618 parser.addoption("--slow", action="store_true",
1619- help="include slow tests")
1620+ help="include slow tests")
1621
1622 for lang in _languages + ['all']:
1623 parser.addoption("--%s" % lang, action="store_true", help="Use %s models" % lang)
1624diff --git a/spacy/tests/ja/__init__.py b/spacy/tests/ja/__init__.py
1625new file mode 100644
1626index 00000000..e69de29b
1627diff --git a/spacy/tests/ja/test_tagger.py b/spacy/tests/ja/test_tagger.py
1628new file mode 100644
1629index 00000000..85f65383
1630--- /dev/null
1631+++ b/spacy/tests/ja/test_tagger.py
1632@@ -0,0 +1,38 @@
1633+# coding: utf-8
1634+from __future__ import unicode_literals
1635+
1636+import pytest
1637+
1638+TAGGER_TESTS = [
1639+ ('あれならそこにあるよ',
1640+ (('代名詞,*,*,*', 'PRON'),
1641+ ('助動詞,*,*,*', 'AUX'),
1642+ ('代名詞,*,*,*', 'PRON'),
1643+ ('助詞,格助詞,*,*', 'ADP'),
1644+ ('動詞,非自立可能,*,*', 'VERB'),
1645+ ('助詞,終助詞,*,*', 'PART'))),
1646+ ('このファイルには小さなテストが入っているよ',
1647+ (('連体詞,*,*,*,DET', 'DET'),
1648+ ('名詞,普通名詞,サ変可能,*', 'NOUN'),
1649+ ('助詞,格助詞,*,*', 'ADP'),
1650+ ('助詞,係助詞,*,*', 'ADP'),
1651+ ('連体詞,*,*,*,ADJ', 'ADJ'),
1652+ ('名詞,普通名詞,サ変可能,*', 'NOUN'),
1653+ ('助詞,格助詞,*,*', 'ADP'),
1654+ ('動詞,一般,*,*', 'VERB'),
1655+ ('助詞,接続助詞,*,*', 'SCONJ'),
1656+ ('動詞,非自立可能,*,*', 'VERB'),
1657+ ('助詞,終助詞,*,*', 'PART'))),
1658+ ('プププランドに行きたい',
1659+ (('名詞,普通名詞,一般,*', 'NOUN'),
1660+ ('助詞,格助詞,*,*', 'ADP'),
1661+ ('動詞,非自立可能,*,*', 'VERB'),
1662+ ('助動詞,*,*,*', 'AUX')))
1663+]
1664+
1665+@pytest.mark.parametrize('text,expected_tags', TAGGER_TESTS)
1666+def test_japanese_tagger(japanese, text, expected_tags):
1667+ tokens = japanese.make_doc(text)
1668+ assert len(tokens) == len(expected_tags)
1669+ for token, res in zip(tokens, expected_tags):
1670+ assert token.tag_ == res[0] and token.pos_ == res[1]
1671diff --git a/spacy/tests/ja/test_tokenizer.py b/spacy/tests/ja/test_tokenizer.py
1672new file mode 100644
1673index 00000000..17411aee
1674--- /dev/null
1675+++ b/spacy/tests/ja/test_tokenizer.py
1676@@ -0,0 +1,17 @@
1677+# coding: utf-8
1678+from __future__ import unicode_literals
1679+
1680+import pytest
1681+
1682+TOKENIZER_TESTS = [
1683+ ("日本語だよ", ['日本', '語', 'だ', 'よ']),
1684+ ("東京タワーの近くに住んでいます。", ['東京', 'タワー', 'の', '近く', 'に', '住ん', 'で', 'い', 'ます', '。']),
1685+ ("吾輩は猫である。", ['吾輩', 'は', '猫', 'で', 'ある', '。']),
1686+ ("月に代わって、お仕置きよ!", ['月', 'に', '代わっ', 'て', '、', 'お', '仕置き', 'よ', '!']),
1687+ ("すもももももももものうち", ['すもも', 'も', 'もも', 'も', 'もも', 'の', 'うち'])
1688+]
1689+
1690+@pytest.mark.parametrize('text,expected_tokens', TOKENIZER_TESTS)
1691+def test_japanese_tokenizer(ja_tokenizer, text, expected_tokens):
1692+ tokens = [token.text for token in ja_tokenizer(text)]
1693+ assert tokens == expected_tokens
1694diff --git a/spacy/tests/pattern/__init__.py b/spacy/tests/pattern/__init__.py
1695new file mode 100644
1696index 00000000..57d631c3
1697--- /dev/null
1698+++ b/spacy/tests/pattern/__init__.py
1699@@ -0,0 +1 @@
1700+# coding: utf-8
1701diff --git a/spacy/tests/pattern/parser.py b/spacy/tests/pattern/parser.py
1702new file mode 100644
1703index 00000000..50dd3ac6
1704--- /dev/null
1705+++ b/spacy/tests/pattern/parser.py
1706@@ -0,0 +1,76 @@
1707+# coding: utf-8
1708+
1709+
1710+import re
1711+from ...pattern.parser import PatternParser
1712+
1713+
1714+class TestPatternParser:
1715+ def test_empty_query(self):
1716+ assert PatternParser.parse('') is None
1717+ assert PatternParser.parse(' ') is None
1718+
1719+ def test_define_node(self):
1720+ query = "fox [lemma:fox,word:fox]=alias"
1721+ pattern = PatternParser.parse(query)
1722+
1723+ assert pattern is not None
1724+ assert pattern.number_of_nodes() == 1
1725+ assert pattern.number_of_edges() == 0
1726+
1727+ assert 'fox' in pattern.nodes
1728+
1729+ attrs = pattern['fox']
1730+ assert attrs.get('lemma') == 'fox'
1731+ assert attrs.get('word') == 'fox'
1732+ assert attrs.get('_name') == 'fox'
1733+ assert attrs.get('_alias') == 'alias'
1734+
1735+ for adj_list in pattern.adjacency.values():
1736+ assert not adj_list
1737+
1738+ def test_define_node_with_regex(self):
1739+ query = "fox [lemma:/fo.*/]"
1740+ pattern = PatternParser.parse(query)
1741+
1742+ attrs = pattern['fox']
1743+ assert attrs.get('lemma') == re.compile(r'fo.*', re.U)
1744+
1745+ def test_define_edge(self):
1746+ query = "[word:quick] >amod [word:fox]"
1747+ pattern = PatternParser.parse(query)
1748+
1749+ assert pattern is not None
1750+ assert pattern.number_of_nodes() == 2
1751+ assert pattern.number_of_edges() == 1
1752+
1753+ quick_id = [node_id for node_id, node_attr in pattern.nodes.items()
1754+ if node_attr['word'] == 'quick'][0]
1755+
1756+ fox_id = [node_id for node_id, node_attr in pattern.nodes.items()
1757+ if node_attr['word'] == 'fox'][0]
1758+
1759+ quick_map = pattern.adjacency[quick_id]
1760+ fox_map = pattern.adjacency[fox_id]
1761+
1762+ assert len(quick_map) == 0
1763+ assert len(fox_map) == 1
1764+
1765+ dep = fox_map[quick_id]
1766+
1767+ assert dep == 'amod'
1768+
1769+ def test_define_edge_with_regex(self):
1770+ query = "[word:quick] >/amod|nsubj/ [word:fox]"
1771+ pattern = PatternParser.parse(query)
1772+
1773+ quick_id = [node_id for node_id, node_attr in pattern.nodes.items()
1774+ if node_attr['word'] == 'quick'][0]
1775+
1776+ fox_id = [node_id for node_id, node_attr in pattern.nodes.items()
1777+ if node_attr['word'] == 'fox'][0]
1778+
1779+ fox_map = pattern.adjacency[fox_id]
1780+ dep = fox_map[quick_id]
1781+
1782+ assert dep == re.compile(r'amod|nsubj', re.U)
1783diff --git a/spacy/tests/pattern/pattern.py b/spacy/tests/pattern/pattern.py
1784new file mode 100644
1785index 00000000..a476f92f
1786--- /dev/null
1787+++ b/spacy/tests/pattern/pattern.py
1788@@ -0,0 +1,61 @@
1789+# coding: utf-8
1790+
1791+from ..util import get_doc
1792+from ...pattern.pattern import Tree, DependencyTree
1793+from ...pattern.parser import PatternParser
1794+
1795+import pytest
1796+
1797+import logging
1798+logger = logging.getLogger()
1799+logger.addHandler(logging.StreamHandler())
1800+logger.setLevel(logging.DEBUG)
1801+
1802+
1803+@pytest.fixture
1804+def doc(en_vocab):
1805+ words = ['I', "'m", 'going', 'to', 'the', 'zoo', 'next', 'week', '.']
1806+ doc = get_doc(en_vocab,
1807+ words=words,
1808+ deps=['nsubj', 'aux', 'ROOT', 'prep', 'det', 'pobj',
1809+ 'amod', 'npadvmod', 'punct'],
1810+ heads=[2, 1, 0, -1, 1, -2, 1, -5, -6])
1811+ return doc
1812+
1813+
1814+class TestTree:
1815+ def test_is_connected(self):
1816+ tree = Tree()
1817+ tree.add_node(1)
1818+ tree.add_node(2)
1819+ tree.add_edge(1, 2)
1820+
1821+ assert tree.is_connected()
1822+
1823+ tree.add_node(3)
1824+ assert not tree.is_connected()
1825+
1826+
1827+class TestDependencyTree:
1828+ def test_from_doc(self, doc):
1829+ dep_tree = DependencyTree(doc)
1830+
1831+ assert len(dep_tree) == len(doc)
1832+ assert dep_tree.is_connected()
1833+ assert dep_tree.number_of_edges() == len(doc) - 1
1834+
1835+ def test_simple_matching(self, doc):
1836+ dep_tree = DependencyTree(doc)
1837+ pattern = PatternParser.parse("""root [word:going]
1838+ to [word:to]
1839+ [word:week]=date > root
1840+ [word:/zoo|park/]=place >pobj to
1841+ to >prep root
1842+ """)
1843+ assert pattern is not None
1844+ matches = dep_tree.match(pattern)
1845+ assert len(matches) == 1
1846+
1847+ match = matches[0]
1848+ assert match['place'] == doc[5]
1849+ assert match['date'] == doc[7]
1850diff --git a/spacy/tests/regression/test_issue1031.py b/spacy/tests/regression/test_issue1031.py
1851new file mode 100644
1852index 00000000..1ac14eb7
1853--- /dev/null
1854+++ b/spacy/tests/regression/test_issue1031.py
1855@@ -0,0 +1,13 @@
1856+from ...vocab import Vocab
1857+
1858+def test_lexeme_text():
1859+ vocab = Vocab()
1860+ lex = vocab[u'the']
1861+ assert lex.text == u'the'
1862+
1863+
1864+def test_lexeme_lex_id():
1865+ vocab = Vocab()
1866+ lex1 = vocab[u'the']
1867+ lex2 = vocab[u'be']
1868+ assert lex1.lex_id != lex2.lex_id
1869diff --git a/spacy/tests/regression/test_issue1061.py b/spacy/tests/regression/test_issue1061.py
1870new file mode 100644
1871index 00000000..821ca2bf
1872--- /dev/null
1873+++ b/spacy/tests/regression/test_issue1061.py
1874@@ -0,0 +1,27 @@
1875+from __future__ import unicode_literals
1876+
1877+from ...symbols import ORTH
1878+
1879+from ...vocab import Vocab
1880+from ...en import English
1881+
1882+
1883+def test_issue1061():
1884+ '''Test special-case works after tokenizing. Was caching problem.'''
1885+ text = 'I like _MATH_ even _MATH_ when _MATH_, except when _MATH_ is _MATH_! but not _MATH_.'
1886+ tokenizer = English.Defaults.create_tokenizer()
1887+ doc = tokenizer(text)
1888+ assert 'MATH' in [w.text for w in doc]
1889+ assert '_MATH_' not in [w.text for w in doc]
1890+
1891+ tokenizer.add_special_case('_MATH_', [{ORTH: '_MATH_'}])
1892+ doc = tokenizer(text)
1893+ assert '_MATH_' in [w.text for w in doc]
1894+ assert 'MATH' not in [w.text for w in doc]
1895+
1896+ # For sanity, check it works when pipeline is clean.
1897+ tokenizer = English.Defaults.create_tokenizer()
1898+ tokenizer.add_special_case('_MATH_', [{ORTH: '_MATH_'}])
1899+ doc = tokenizer(text)
1900+ assert '_MATH_' in [w.text for w in doc]
1901+ assert 'MATH' not in [w.text for w in doc]
1902diff --git a/spacy/tests/regression/test_issue1207.py b/spacy/tests/regression/test_issue1207.py
1903new file mode 100644
1904index 00000000..a71faebc
1905--- /dev/null
1906+++ b/spacy/tests/regression/test_issue1207.py
1907@@ -0,0 +1,25 @@
1908+from __future__ import unicode_literals
1909+from ..util import get_doc
1910+from ...vocab import Vocab
1911+from ...en import English
1912+
1913+
1914+def test_span_noun_chunks():
1915+ vocab = Vocab(lang='en', tag_map=English.Defaults.tag_map)
1916+ words = "Employees are recruiting talented staffers from overseas .".split()
1917+ heads = [1, 1, 0, 1, -2, -1, -5]
1918+ deps = ['nsubj', 'aux', 'ROOT', 'nmod', 'dobj', 'adv', 'pobj']
1919+ tags = ['NNS', 'VBP', 'VBG', 'JJ', 'NNS', 'IN', 'NN', '.']
1920+ doc = get_doc(vocab, words=words, heads=heads, deps=deps, tags=tags)
1921+ doc.is_parsed = True
1922+
1923+ noun_chunks = [np.text for np in doc.noun_chunks]
1924+ assert noun_chunks == ['Employees', 'talented staffers', 'overseas']
1925+
1926+ span = doc[0:4]
1927+ noun_chunks = [np.text for np in span.noun_chunks]
1928+ assert noun_chunks == ['Employees']
1929+
1930+ for sent in doc.sents:
1931+ noun_chunks = [np.text for np in sent.noun_chunks]
1932+ assert noun_chunks == ['Employees', 'talented staffers', 'overseas']
1933diff --git a/spacy/tests/regression/test_issue1281.py b/spacy/tests/regression/test_issue1281.py
1934new file mode 100644
1935index 00000000..17307b1d
1936--- /dev/null
1937+++ b/spacy/tests/regression/test_issue1281.py
1938@@ -0,0 +1,13 @@
1939+# coding: utf8
1940+from __future__ import unicode_literals
1941+
1942+import pytest
1943+
1944+
1945+@pytest.mark.parametrize('text', [
1946+ "She hasn't done the housework.",
1947+ "I haven't done it before.",
1948+ "you daren't do that"])
1949+def test_issue1281(en_tokenizer, text):
1950+ tokens = en_tokenizer(text)
1951+ assert tokens[2].text == "n't"
1952diff --git a/spacy/tests/th/test_tokenizer.py b/spacy/tests/th/test_tokenizer.py
1953new file mode 100644
1954index 00000000..851c6f06
1955--- /dev/null
1956+++ b/spacy/tests/th/test_tokenizer.py
1957@@ -0,0 +1,13 @@
1958+# coding: utf-8
1959+from __future__ import unicode_literals
1960+
1961+import pytest
1962+
1963+TOKENIZER_TESTS = [
1964+ ("คุณรักผมไหม", ['คุณ', 'รัก', 'ผม', 'ไหม'])
1965+]
1966+
1967+@pytest.mark.parametrize('text,expected_tokens', TOKENIZER_TESTS)
1968+def test_thai_tokenizer(th_tokenizer, text, expected_tokens):
1969+ tokens = [token.text for token in th_tokenizer(text)]
1970+ assert tokens == expected_tokens
1971diff --git a/spacy/tests/tokenizer/test_customized_tokenizer.py b/spacy/tests/tokenizer/test_customized_tokenizer.py
1972new file mode 100644
1973index 00000000..855f3386
1974--- /dev/null
1975+++ b/spacy/tests/tokenizer/test_customized_tokenizer.py
1976@@ -0,0 +1,40 @@
1977+# coding: utf-8
1978+from __future__ import unicode_literals
1979+
1980+from ...en import English
1981+from ...tokenizer import Tokenizer
1982+from ... import util
1983+
1984+import pytest
1985+
1986+@pytest.fixture
1987+def tokenizer(en_vocab):
1988+ prefix_re = util.compile_prefix_regex(English.Defaults.prefixes)
1989+ suffix_re = util.compile_suffix_regex(English.Defaults.suffixes)
1990+ custom_infixes = ['\.\.\.+',
1991+ '(?<=[0-9])-(?=[0-9])',
1992+ # '(?<=[0-9]+),(?=[0-9]+)',
1993+ '[0-9]+(,[0-9]+)+',
1994+ u'[\[\]!&:,()\*—–\/-]']
1995+
1996+ infix_re = util.compile_infix_regex(custom_infixes)
1997+ return Tokenizer(en_vocab,
1998+ English.Defaults.tokenizer_exceptions,
1999+ prefix_re.search,
2000+ suffix_re.search,
2001+ infix_re.finditer,
2002+ token_match=None)
2003+
2004+def test_customized_tokenizer_handles_infixes(tokenizer):
2005+ sentence = "The 8 and 10-county definitions are not used for the greater Southern California Megaregion."
2006+ context = [word.text for word in tokenizer(sentence)]
2007+ assert context == [u'The', u'8', u'and', u'10', u'-', u'county', u'definitions', u'are', u'not', u'used',
2008+ u'for',
2009+ u'the', u'greater', u'Southern', u'California', u'Megaregion', u'.']
2010+
2011+ # the trailing '-' may cause Assertion Error
2012+ sentence = "The 8- and 10-county definitions are not used for the greater Southern California Megaregion."
2013+ context = [word.text for word in tokenizer(sentence)]
2014+ assert context == [u'The', u'8', u'-', u'and', u'10', u'-', u'county', u'definitions', u'are', u'not', u'used',
2015+ u'for',
2016+ u'the', u'greater', u'Southern', u'California', u'Megaregion', u'.']
2017diff --git a/spacy/th/__init__.py b/spacy/th/__init__.py
2018new file mode 100644
2019index 00000000..0ed5268c
2020--- /dev/null
2021+++ b/spacy/th/__init__.py
2022@@ -0,0 +1,28 @@
2023+# coding: utf8
2024+from __future__ import unicode_literals
2025+
2026+from .tokenizer_exceptions import TOKENIZER_EXCEPTIONS
2027+from .language_data import *
2028+from ..language import Language, BaseDefaults
2029+from ..attrs import LANG
2030+from ..tokenizer import Tokenizer
2031+from ..tokens import Doc
2032+class ThaiDefaults(BaseDefaults):
2033+ lex_attr_getters = dict(Language.Defaults.lex_attr_getters)
2034+ lex_attr_getters[LANG] = lambda text: 'th'
2035+ tokenizer_exceptions = TOKENIZER_EXCEPTIONS
2036+ tag_map = TAG_MAP
2037+ stop_words = set(STOP_WORDS)
2038+
2039+
2040+class Thai(Language):
2041+ lang = 'th'
2042+ Defaults = ThaiDefaults
2043+ def make_doc(self, text):
2044+ try:
2045+ from pythainlp.tokenize import word_tokenize
2046+ except ImportError:
2047+ raise ImportError("The Thai tokenizer requires the PyThaiNLP library: "
2048+ "https://github.com/wannaphongcom/pythainlp/")
2049+ words = [x for x in list(word_tokenize(text,"newmm"))]
2050+ return Doc(self.vocab, words=words, spaces=[False]*len(words))
2051\ No newline at end of file
2052diff --git a/spacy/th/language_data.py b/spacy/th/language_data.py
2053new file mode 100644
2054index 00000000..03800ba1
2055--- /dev/null
2056+++ b/spacy/th/language_data.py
2057@@ -0,0 +1,25 @@
2058+# encoding: utf8
2059+from __future__ import unicode_literals
2060+
2061+
2062+# import base language data
2063+from .. import language_data as base
2064+
2065+
2066+# import util functions
2067+from ..language_data import update_exc, strings_to_exc
2068+
2069+
2070+# import language-specific data from files
2071+#from .tag_map import TAG_MAP
2072+from .tag_map import TAG_MAP
2073+from .stop_words import STOP_WORDS
2074+from .tokenizer_exceptions import TOKENIZER_EXCEPTIONS
2075+
2076+
2077+TAG_MAP = dict(TAG_MAP)
2078+STOP_WORDS = set(STOP_WORDS)
2079+TOKENIZER_EXCEPTIONS = dict(TOKENIZER_EXCEPTIONS)
2080+
2081+# export __all__ = ["TAG_MAP", "STOP_WORDS"]
2082+__all__ = ["TAG_MAP", "STOP_WORDS","TOKENIZER_EXCEPTIONS"]
2083\ No newline at end of file
2084diff --git a/spacy/th/stop_words.py b/spacy/th/stop_words.py
2085new file mode 100644
2086index 00000000..e13dec98
2087--- /dev/null
2088+++ b/spacy/th/stop_words.py
2089@@ -0,0 +1,62 @@
2090+# encoding: utf8
2091+from __future__ import unicode_literals
2092+
2093+# data from https://github.com/wannaphongcom/pythainlp/blob/dev/pythainlp/corpus/stopwords-th.txt
2094+# stop words as whitespace-separated list
2095+STOP_WORDS = set("""
2096+นี้ นํา นั้น นัก นอกจาก ทุก ที่สุด ที่ ทําให้ ทํา ทาง ทั้งนี้ ดัง ซึ่ง ช่วง จาก จัด จะ คือ ความ ครั้ง คง ขึ้น ของ
2097+ขอ รับ ระหว่าง รวม ยัง มี มาก มา พร้อม พบ ผ่าน ผล บาง น่า เปิดเผย เปิด เนื่องจาก เดียวกัน เดียว เช่น เฉพาะ เข้า ถ้า
2098+ถูก ถึง ต้อง ต่างๆ ต่าง ต่อ ตาม ตั้งแต่ ตั้ง ด้าน ด้วย อีก อาจ ออก อย่าง อะไร อยู่ อยาก หาก หลาย หลังจาก แต่ เอง เห็น
2099+เลย เริ่ม เรา เมื่อ เพื่อ เพราะ เป็นการ เป็น หลัง หรือ หนึ่ง ส่วน ส่ง สุด สําหรับ ว่า ลง ร่วม ราย ขณะ ก่อน ก็ การ กับ กัน
2100+กว่า กล่าว จึง ไว้ ไป ได้ ให้ ใน โดย แห่ง แล้ว และ แรก แบบ ๆ ทั้ง วัน เขา เคย ไม่ อยาก เกิน เกินๆ เกี่ยวกัน เกี่ยวกับ
2101+เกี่ยวข้อง เกี่ยวเนื่อง เกี่ยวๆ เกือบ เกือบจะ เกือบๆ แก แก่ แก้ไข ใกล้ ใกล้ๆ ไกล ไกลๆ ขณะเดียวกัน ขณะใด ขณะใดๆ ขณะที่ ขณะนั้น ขณะนี้ ขณะหนึ่ง ขวาง
2102+ขวางๆ ขั้น ใคร ใคร่ ใคร่จะ ใครๆ ง่าย ง่ายๆ ไง จง จด จน จนกระทั่ง จนกว่า จนขณะนี้ จนตลอด จนถึง จนทั่ว จนบัดนี้ จนเมื่อ จนแม้ จนแม้น
2103+จรด จรดกับ จริง จริงจัง จริงๆ จริงๆจังๆ จวน จวนจะ จวนเจียน จวบ ซึ่งก็ ซึ่งก็คือ ซึ่งกัน ซึ่งกันและกัน ซึ่งได้แก่ ซึ่งๆ ณ ด้วย ด้วยกัน ด้วยเช่นกัน ด้วยที่ ด้วยประการฉะนี้
2104+ด้วยเพราะ ด้วยว่า ด้วยเหตุที่ ด้วยเหตุนั้น ด้วยเหตุนี้ ด้วยเหตุเพราะ ด้วยเหตุว่า ด้วยเหมือนกัน ดั่ง ดังกล่าว ดังกับ ดั่งกับ ดังกับว่า ดั่งกับว่า ดังเก่า
2105+ดั่งเก่า ดังเคย ใดๆ ได้ ได้แก่ ได้แต่ ได้ที่ ได้มา ได้รับ ตน ตนเอง ตนฯ ตรง ตรงๆ ตลอด ตลอดกาล ตลอดกาลนาน ตลอดจน ตลอดถึง ตลอดทั้ง
2106+ตลอดทั่ว ตลอดทั่วถึง ตลอดทั่วทั้ง ตลอดปี ตลอดไป ตลอดมา ตลอดระยะเวลา ตลอดวัน ตลอดเวลา ตลอดศก ต่อ ต่อกัน ถึงแก่ ถึงจะ ถึงบัดนั้น ถึงบัดนี้
2107+ถึงเมื่อ ถึงเมื่อใด ถึงเมื่อไร ถึงแม้ ถึงแม้จะ ถึงแม้ว่า ถึงอย่างไร ถือ ถือว่า ถูกต้อง ถูกๆ เถอะ เถิด ทรง ทว่า ทั้งคน ทั้งตัว ทั้งที ทั้งที่ ทั้งนั้น ทั้งนั้นด้วย ทั้งนั้นเพราะ
2108+นอก นอกจากที่ นอกจากนั้น นอกจากนี้ นอกจากว่า นอกนั้น นอกเหนือ นอกเหนือจาก น้อย น้อยกว่า น้อยๆ นะ น่ะ นักๆ นั่น นั่นไง นั่นเป็น นั่นแหละ
2109+นั่นเอง นั้นๆ นับ นับจากนั้น นับจากนี้ นับตั้งแต่ นับแต่ นับแต่ที่ นับแต่นั้น เป็นต้น เป็นต้นไป เป็นต้นมา เป็นแต่ เป็นแต่เพียง เป็นที เป็นที่ เป็นที่สุด เป็นเพราะ
2110+เป็นเพราะว่า เป็นเพียง เป็นเพียงว่า เป็นเพื่อ เป็นอัน เป็นอันมาก เป็นอันว่า เป็นอันๆ เป็นอาทิ เป็นๆ เปลี่ยน เปลี่ยนแปลง เปิด เปิดเผย ไป่ ผ่าน ผ่านๆ
2111+ผิด ผิดๆ ผู้ เพียงเพื่อ เพียงไร เพียงไหน เพื่อที่ เพื่อที่จะ เพื่อว่า เพื่อให้ ภาค ภาคฯ ภาย ภายใต้ ภายนอก ภายใน ภายภาค ภายภาคหน้า ภายหน้า ภายหลัง
2112+มอง มองว่า มัก มักจะ มัน มันๆ มั้ย มั้ยนะ มั้ยนั่น มั้ยเนี่ย มั้ยล่ะ ยืนนาน ยืนยง ยืนยัน ยืนยาว เยอะ เยอะแยะ เยอะๆ แยะ แยะๆ รวด รวดเร็ว ร่วม รวมกัน ร่วมกัน
2113+รวมด้วย ร่วมด้วย รวมถึง รวมทั้ง ร่วมมือ รวมๆ ระยะ ระยะๆ ระหว่าง รับรอง รึ รึว่า รือ รือว่า สิ้นกาลนาน สืบเนื่อง สุดๆ สู่ สูง สูงกว่า สูงส่ง สูงสุด สูงๆ เสมือนกับ
2114+เสมือนว่า เสร็จ เสร็จกัน เสร็จแล้ว เสร็จสมบูรณ์ เสร็จสิ้น เสีย เสียก่อน เสียจน เสียจนกระทั่ง เสียจนถึง เสียด้วย เสียนั่น เสียนั่นเอง เสียนี่ เสียนี่กระไร เสียยิ่ง
2115+เสียยิ่งนัก เสียแล้ว ใหญ่ๆ ให้ดี ให้แด่ ให้ไป ใหม่ ให้มา ใหม่ๆ ไหน ไหนๆ อดีต อนึ่ง อย่าง อย่างเช่น อย่างดี อย่างเดียว อย่างใด อย่างที่ อย่างน้อย อย่างนั้น
2116+อย่างนี้ อย่างโน้น ก็คือ ก็แค่ ก็จะ ก็ดี ก็ได้ ก็ต่อเมื่อ ก็ตาม ก็ตามแต่ ก็ตามที ก็แล้วแต่ กระทั่ง กระทำ กระนั้น กระผม กลับ กล่าวคือ กลุ่ม กลุ่มก้อน
2117+กลุ่มๆ กว้าง กว้างขวาง กว้างๆ ก่อนหน้า ก่อนหน้านี้ ก่อนๆ กันดีกว่า กันดีไหม กันเถอะ กันนะ กันและกัน กันไหม กันเอง กำลัง กำลังจะ กำหนด กู เก็บ
2118+เกิด เกี่ยวข้อง แก่ แก้ไข ใกล้ ใกล้ๆ ข้า ข้าง ข้างเคียง ข้างต้น ข้างบน ข้างล่าง ข้างๆ ขาด ข้าพเจ้า ข้าฯ เข้าใจ เขียน คงจะ คงอยู่ ครบ ครบครัน ครบถ้วน
2119+ครั้งกระนั้น ครั้งก่อน ครั้งครา ครั้งคราว ครั้งใด ครั้งที่ ครั้งนั้น ครั้งนี้ ครั้งละ ครั้งหนึ่ง ครั้งหลัง ครั้งหลังสุด ครั้งไหน ครั้งๆ ครัน ครับ ครา คราใด คราที่ ครานั้น ครานี้ คราหนึ่ง
2120+คราไหน คราว คราวก่อน คราวใด คราวที่ คราวนั้น คราวนี้ คราวโน้น คราวละ คราวหน้า คราวหนึ่ง คราวหลัง คราวไหน คราวๆ คล้าย คล้ายกัน คล้ายกันกับ
2121+คล้ายกับ คล้ายกับว่า คล้ายว่า ควร ค่อน ค่อนข้าง ค่อนข้างจะ ค่อยไปทาง ค่อนมาทาง ค่อย ค่อยๆ คะ ค่ะ คำ คิด คิดว่า คุณ คุณๆ
2122+เคยๆ แค่ แค่จะ แค่นั้น แค่นี้ แค่เพียง แค่ว่า แค่ไหน ใคร่ ใคร่จะ ง่าย ง่ายๆ จนกว่า จนแม้ จนแม้น จังๆ จวบกับ จวบจน จ้ะ จ๊ะ จะได้ จัง จัดการ จัดงาน จัดแจง
2123+จัดตั้ง จัดทำ จัดหา จัดให้ จับ จ้า จ๋า จากนั้น จากนี้ จากนี้ไป จำ จำเป็น จำพวก จึงจะ จึงเป็น จู่ๆ ฉะนั้น ฉะนี้ ฉัน เฉกเช่น เฉย เฉยๆ ไฉน ช่วงก่อน
2124+ช่วงต่อไป ช่วงถัดไป ช่วงท้าย ช่วงที่ ช่วงนั้น ช่วงนี้ ช่วงระหว่าง ช่วงแรก ช่วงหน้า ช่วงหลัง ช่วงๆ ช่วย ช้า ช้านาน ชาว ช้าๆ เช่นก่อน เช่นกัน เช่นเคย
2125+เช่นดัง เช่นดังก่อน เช่นดังเก่า เช่นดังที่ เช่นดังว่า เช่นเดียวกัน เช่นเดียวกับ เช่นใด เช่นที่ เช่นที่เคย เช่นที่ว่า เช่นนั้น เช่นนั้นเอง เช่นนี้ เช่นเมื่อ เช่นไร เชื่อ
2126+เชื่อถือ เชื่อมั่น เชื่อว่า ใช่ ใช่ไหม ใช้ ซะ ซะก่อน ซะจน ซะจนกระทั่ง ซะจนถึง ซึ่งได้แก่ ด้วยกัน ด้วยเช่นกัน ด้วยที่ ด้วยเพราะ ด้วยว่า ด้วยเหตุที่ ด้วยเหตุนั้น
2127+ด้วยเหตุนี้ ด้วยเหตุเพราะ ด้วยเหตุว่า ด้วยเหมือนกัน ดังกล่าว ดังกับว่า ดั่งกับว่า ดังเก่า ดั่งเก่า ดั่งเคย ต่างก็ ต่างหาก ตามด้วย ตามแต่ ตามที่
2128+ตามๆ เต็มไปด้วย เต็มไปหมด เต็มๆ แต่ก็ แต่ก่อน แต่จะ แต่เดิม แต่ต้อง แต่ถ้า แต่ทว่า แต่ที่ แต่นั้น แต่เพียง แต่เมื่อ แต่ไร แต่ละ แต่ว่า แต่ไหน แต่อย่างใด โต
2129+โตๆ ใต้ ถ้าจะ ถ้าหาก ถึงแก่ ถึงแม้ ถึงแม้จะ ถึงแม้ว่า ถึงอย่างไร ถือว่า ถูกต้อง ทว่า ทั้งนั้นด้วย ทั้งปวง ทั้งเป็น ทั้งมวล ทั้งสิ้น ทั้งหมด ทั้งหลาย ทั้งๆ ทัน
2130+ทันใดนั้น ทันที ทันทีทันใด ทั่ว ทำไม ทำไร ทำให้ ทำๆ ที ที่จริง ที่ซึ่ง ทีเดียว ทีใด ที่ใด ที่ได้ ทีเถอะ ที่แท้ ที่แท้จริง ที่นั้น ที่นี้ ทีไร ทีละ ที่ละ
2131+ที่แล้ว ที่ว่า ที่แห่งนั้น ที่ไหน ทีๆ ที่ๆ ทุกคน ทุกครั้ง ทุกครา ทุกคราว ทุกชิ้น ทุกตัว ทุกทาง ทุกที ทุกที่ ทุกเมื่อ ทุกวัน ทุกวันนี้ ทุกสิ่ง ทุกหน ทุกแห่ง ทุกอย่าง
2132+ทุกอัน ทุกๆ เท่า เท่ากัน เท่ากับ เท่าใด เท่าที่ เท่านั้น เท่านี้ เท่าไร เท่าไหร่ แท้ แท้จริง เธอ นอกจากว่า น้อย น้อยกว่า น้อยๆ น่ะ นั้นไว นับแต่นี้ นาง
2133+นางสาว น่าจะ นาน นานๆ นาย นำ นำพา นำมา นิด นิดหน่อย นิดๆ นี่ นี่ไง นี่นา นี่แน่ะ นี่แหละ นี้แหล่ นี่เอง นี้เอง นู่น นู้น เน้น เนี่ย
2134+เนี่ยเอง ในช่วง ในที่ ในเมื่อ ในระหว่าง บน บอก บอกแล้ว บอกว่า บ่อย บ่อยกว่า บ่อยครั้ง บ่อยๆ บัดดล บัดเดี๋ยวนี้ บัดนั้น บัดนี้ บ้าง บางกว่า
2135+บางขณะ บางครั้ง บางครา บางคราว บางที บางที่ บางแห่ง บางๆ ปฏิบัติ ประกอบ ประการ ประการฉะนี้ ประการใด ประการหนึ่ง ประมาณ ประสบ ปรับ
2136+ปรากฏ ปรากฏว่า ปัจจุบัน ปิด เป็นด้วย เป็นดัง เป็นต้น เป็นแต่ เป็นเพื่อ เป็นอัน เป็นอันมาก เป็นอาทิ ผ่านๆ ผู้ ผู้ใด เผื่อ เผื่อจะ เผื่อที่ เผื่อว่า ฝ่าย
2137+ฝ่ายใด พบว่า พยายาม พร้อมกัน พร้อมกับ พร้อมด้วย พร้อมทั้ง พร้อมที่ พร้อมเพียง พวก พวกกัน พวกกู พวกแก พวกเขา พวกคุณ พวกฉัน พวกท่าน
2138+พวกที่ พวกเธอ พวกนั้น พวกนี้ พวกนู้น พวกโน้น พวกมัน พวกมึง พอ พอกัน พอควร พอจะ พอดี พอตัว พอที พอที่ พอเพียง พอแล้ว พอสม พอสมควร
2139+พอเหมาะ พอๆ พา พึง พึ่ง พื้นๆ พูด เพราะฉะนั้น เพราะว่า เพิ่ง เพิ่งจะ เพิ่ม เพิ่มเติม เพียง เพียงแค่ เพียงใด เพียงแต่ เพียงพอ เพียงเพราะ
2140+เพื่อว่า เพื่อให้ ภายใต้ มองว่า มั๊ย มากกว่า มากมาย มิ มิฉะนั้น มิใช่ มิได้ มีแต่ มึง มุ่ง มุ่งเน้น มุ่งหมาย เมื่อก่อน เมื่อครั้ง เมื่อครั้งก่อน
2141+เมื่อคราวก่อน เมื่อคราวที่ เมื่อคราว เมื่อคืน เมื่อเช้า เมื่อใด เมื่อนั้น เมื่อนี้ เมื่อเย็น เมื่อไร เมื่อวันวาน เมื่อวาน เมื่อไหร่ แม้ แม้กระทั่ง แม้แต่ แม้นว่า แม้ว่า
2142+ไม่ค่อย ไม่ค่อยจะ ไม่ค่อยเป็น ไม่ใช่ ไม่เป็นไร ไม่ว่า ยก ยกให้ ยอม ยอมรับ ย่อม ย่อย ยังคง ยังงั้น ยังงี้ ยังโง้น ยังไง ยังจะ ยังแต่ ยาก
2143+ยาว ยาวนาน ยิ่ง ยิ่งกว่า ยิ่งขึ้น ยิ่งขึ้นไป ยิ่งจน ยิ่งจะ ยิ่งนัก ยิ่งเมื่อ ยิ่งแล้ว ยิ่งใหญ่ ร่วมกัน รวมด้วย ร่วมด้วย รือว่า เร็ว เร็วๆ เราๆ เรียก เรียบ เรื่อย
2144+เรื่อยๆ ไร ล้วน ล้วนจน ล้วนแต่ ละ ล่าสุด เล็ก เล็กน้อย เล็กๆ เล่าว่า แล้วกัน แล้วแต่ แล้วเสร็จ วันใด วันนั้น วันนี้ วันไหน สบาย สมัย สมัยก่อน
2145+สมัยนั้น สมัยนี้ สมัยโน้น ส่วนเกิน ส่วนด้อย ส่วนดี ส่วนใด ส่วนที่ ส่วนน้อย ส่วนนั้น ส่วนมาก ส่วนใหญ่ สั้น สั้นๆ สามารถ สำคัญ สิ่ง
2146+สิ่งใด สิ่งนั้น สิ่งนี้ สิ่งไหน สิ้น เสร็จแล้ว เสียด้วย เสียแล้ว แสดง แสดงว่า หน หนอ หนอย หน่อย หมด หมดกัน หมดสิ้น หรือไง หรือเปล่า หรือไม่ หรือยัง
2147+หรือไร หากแม้ หากแม้น หากแม้นว่า หากว่า หาความ หาใช่ หารือ เหตุ เหตุผล เหตุนั้น เหตุนี้ เหตุไร เห็นแก่ เห็นควร เห็นจะ เห็นว่า เหลือ เหลือเกิน เหล่า
2148+เหล่านั้น เหล่านี้ แห่งใด แห่งนั้น แห่งนี้ แห่งโน้น แห่งไหน แหละ ให้แก่ ใหญ่ ใหญ่โต อย่างเช่น อย่างดี อย่างเดียว อย่างใด อย่างที่ อย่างน้อย อย่างนั้น อย่างนี้
2149+อย่างโน้น อย่างมาก อย่างยิ่ง อย่างไร อย่างไรก็ อย่างไรก็ได้ อย่างไรเสีย อย่างละ อย่างหนึ่ง อย่างไหน อย่างๆ อัน อันจะ อันใด อันได้แก่ อันที่
2150+อันที่จริง อันที่จะ อันเนื่องมาจาก อันละ อันไหน อันๆ อาจจะ อาจเป็น อาจเป็นด้วย อื่น อื่นๆ เอ็ง เอา ฯ ฯล ฯลฯ
2151+""".split())
2152\ No newline at end of file
2153diff --git a/spacy/th/tag_map.py b/spacy/th/tag_map.py
2154new file mode 100644
2155index 00000000..e225f728
2156--- /dev/null
2157+++ b/spacy/th/tag_map.py
2158@@ -0,0 +1,81 @@
2159+# encoding: utf8
2160+# data from Korakot Chaovavanich (https://www.facebook.com/photo.php?fbid=390564854695031&set=p.390564854695031&type=3&permPage=1&ifg=1)
2161+from __future__ import unicode_literals
2162+
2163+from ..symbols import *
2164+
2165+TAG_MAP = {
2166+ #NOUN
2167+ "NOUN": {POS: NOUN},
2168+ "NCMN": {POS: NOUN},
2169+ "NTTL": {POS: NOUN},
2170+ "CNIT": {POS: NOUN},
2171+ "CLTV": {POS: NOUN},
2172+ "CMTR": {POS: NOUN},
2173+ "CFQC": {POS: NOUN},
2174+ "CVBL": {POS: NOUN},
2175+ #PRON
2176+ "PRON": {POS: PRON},
2177+ "NPRP": {POS: PRON},
2178+ # ADJ
2179+ "ADJ": {POS: ADJ},
2180+ "NONM": {POS: ADJ},
2181+ "VATT": {POS: ADJ},
2182+ "DONM": {POS: ADJ},
2183+ # ADV
2184+ "ADV": {POS: ADV},
2185+ "ADVN": {POS: ADV},
2186+ "ADVI": {POS: ADV},
2187+ "ADVP": {POS: ADV},
2188+ "ADVS": {POS: ADV},
2189+ # INT
2190+ "INT": {POS: INTJ},
2191+ # PRON
2192+ "PROPN": {POS: PROPN},
2193+ "PPRS": {POS: PROPN},
2194+ "PDMN": {POS: PROPN},
2195+ "PNTR": {POS: PROPN},
2196+ # DET
2197+ "DET": {POS: DET},
2198+ "DDAN": {POS: DET},
2199+ "DDAC": {POS: DET},
2200+ "DDBQ": {POS: DET},
2201+ "DDAQ": {POS: DET},
2202+ "DIAC": {POS: DET},
2203+ "DIBQ": {POS: DET},
2204+ "DIAQ": {POS: DET},
2205+ "DCNM": {POS: DET},
2206+ # NUM
2207+ "NUM": {POS: NUM},
2208+ "NCNM": {POS: NUM},
2209+ "NLBL": {POS: NUM},
2210+ "DCNM": {POS: NUM},
2211+ # AUX
2212+ "AUX": {POS: AUX},
2213+ "XVBM": {POS: AUX},
2214+ "XVAM": {POS: AUX},
2215+ "XVMM": {POS: AUX},
2216+ "XVBB": {POS: AUX},
2217+ "XVAE": {POS: AUX},
2218+ # ADP
2219+ "ADP": {POS: ADP},
2220+ "RPRE": {POS: ADP},
2221+ # CCONJ
2222+ "CCONJ": {POS: CCONJ},
2223+ "JCRG": {POS: CCONJ},
2224+ # SCONJ
2225+ "SCONJ": {POS: SCONJ},
2226+ "PREL": {POS: SCONJ},
2227+ "JSBR": {POS: SCONJ},
2228+ "JCMP": {POS: SCONJ},
2229+ # PART
2230+ "PART": {POS: PART},
2231+ "FIXN": {POS: PART},
2232+ "FIXV": {POS: PART},
2233+ "EAFF": {POS: PART},
2234+ "AITT": {POS: PART},
2235+ "NEG": {POS: PART},
2236+ # PUNCT
2237+ "PUNCT": {POS: PUNCT},
2238+ "PUNC": {POS: PUNCT}
2239+}
2240\ No newline at end of file
2241diff --git a/spacy/th/tokenizer_exceptions.py b/spacy/th/tokenizer_exceptions.py
2242new file mode 100644
2243index 00000000..0f933f1c
2244--- /dev/null
2245+++ b/spacy/th/tokenizer_exceptions.py
2246@@ -0,0 +1,45 @@
2247+# encoding: utf8
2248+from __future__ import unicode_literals
2249+
2250+from ..symbols import *
2251+from ..language_data import PRON_LEMMA
2252+
2253+
2254+TOKENIZER_EXCEPTIONS = {
2255+ "ม.ค.": [
2256+ {ORTH: "ม.ค.", LEMMA: "มกราคม"}
2257+ ],
2258+ "ก.พ.": [
2259+ {ORTH: "ก.พ.", LEMMA: "กุมภาพันธ์"}
2260+ ],
2261+ "มี.ค.": [
2262+ {ORTH: "มี.ค.", LEMMA: "มีนาคม"}
2263+ ],
2264+ "เม.ย.": [
2265+ {ORTH: "เม.ย.", LEMMA: "เมษายน"}
2266+ ],
2267+ "พ.ค.": [
2268+ {ORTH: "พ.ค.", LEMMA: "พฤษภาคม"}
2269+ ],
2270+ "มิ.ย.": [
2271+ {ORTH: "มิ.ย.", LEMMA: "มิถุนายน"}
2272+ ],
2273+ "ก.ค.": [
2274+ {ORTH: "ก.ค.", LEMMA: "กรกฎาคม"}
2275+ ],
2276+ "ส.ค.": [
2277+ {ORTH: "ส.ค.", LEMMA: "สิงหาคม"}
2278+ ],
2279+ "ก.ย.": [
2280+ {ORTH: "ก.ย.", LEMMA: "กันยายน"}
2281+ ],
2282+ "ต.ค.": [
2283+ {ORTH: "ต.ค.", LEMMA: "ตุลาคม"}
2284+ ],
2285+ "พ.ย.": [
2286+ {ORTH: "พ.ย.", LEMMA: "พฤศจิกายน"}
2287+ ],
2288+ "ธ.ค.": [
2289+ {ORTH: "ธ.ค.", LEMMA: "ธันวาคม"}
2290+ ]
2291+}
2292\ No newline at end of file
2293diff --git a/spacy/tokenizer.pyx b/spacy/tokenizer.pyx
2294index 3996819f..61c4e7ba 100644
2295--- a/spacy/tokenizer.pyx
2296+++ b/spacy/tokenizer.pyx
2297@@ -135,7 +135,13 @@ cdef class Tokenizer:
2298 cdef int _try_cache(self, hash_t key, Doc tokens) except -1:
2299 cached = <_Cached*>self._cache.get(key)
2300 if cached == NULL:
2301- return False
2302+ # See 'flush_cache' below for hand-wringing about
2303+ # how to handle this.
2304+ cached = <_Cached*>self._specials.get(key)
2305+ if cached == NULL:
2306+ return False
2307+ else:
2308+ self._cache.set(key, cached)
2309 cdef int i
2310 if cached.is_lex:
2311 for i in range(cached.length):
2312@@ -338,7 +344,6 @@ cdef class Tokenizer:
2313 cached.data.tokens = self.vocab.make_fused_token(substrings)
2314 key = hash_string(string)
2315 self._specials.set(key, cached)
2316- self._cache.set(key, cached)
2317 self._rules[string] = substrings
2318
2319 def to_disk(self, path, **exclude):
2320diff --git a/spacy/tokens/doc.pyx b/spacy/tokens/doc.pyx
2321index eef25c71..8fb148be 100644
2322--- a/spacy/tokens/doc.pyx
2323+++ b/spacy/tokens/doc.pyx
2324@@ -550,6 +550,7 @@ cdef class Doc:
2325 @cython.boundscheck(False)
2326 cpdef np.ndarray to_array(self, object py_attr_ids):
2327 """Export given token attributes to a numpy `ndarray`.
2328+<<<<<<< HEAD
2329 If `attr_ids` is a sequence of M attributes, the output array will be
2330 of shape `(N, M)`, where N is the length of the `Doc` (in tokens). If
2331 `attr_ids` is a single attribute, the output shape will be (N,). You
2332@@ -566,12 +567,35 @@ cdef class Doc:
2333 >>> doc = nlp(text)
2334 >>> # All strings mapped to integers, for easy export to numpy
2335 >>> np_array = doc.to_array([LOWER, POS, ENT_TYPE, IS_ALPHA])
2336+=======
2337+
2338+ If `attr_ids` is a sequence of M attributes, the output array will
2339+ be of shape `(N, M)`, where N is the length of the `Doc`
2340+ (in tokens). If `attr_ids` is a single attribute, the output shape will
2341+ be (N,). You can specify attributes by integer ID (e.g. spacy.attrs.LEMMA)
2342+ or string name (e.g. 'LEMMA' or 'lemma').
2343+
2344+ Example:
2345+ from spacy import attrs
2346+ doc = nlp(text)
2347+ # All strings mapped to integers, for easy export to numpy
2348+ np_array = doc.to_array([attrs.LOWER, attrs.POS, attrs.ENT_TYPE, attrs.IS_ALPHA])
2349+
2350+ Arguments:
2351+ attr_ids (list[]): A list of attributes (int IDs or string names).
2352+
2353+ Returns:
2354+ feat_array (numpy.ndarray[long, ndim=2]):
2355+ A feature matrix, with one row per word, and one column per attribute
2356+ indicated in the input `attr_ids`.
2357+>>>>>>> 841e2225
2358 """
2359 cdef int i, j
2360 cdef attr_id_t feature
2361 cdef np.ndarray[attr_t, ndim=1] attr_ids
2362 cdef np.ndarray[attr_t, ndim=2] output
2363 # Handle scalar/list inputs of strings/ints for py_attr_ids
2364+<<<<<<< HEAD
2365 if not hasattr(py_attr_ids, '__iter__') \
2366 and not isinstance(py_attr_ids, basestring_):
2367 py_attr_ids = [py_attr_ids]
2368@@ -584,12 +608,25 @@ cdef class Doc:
2369 attr_ids = numpy.asarray(py_attr_ids, dtype=numpy.uint64)
2370 output = numpy.ndarray(shape=(self.length, len(attr_ids)),
2371 dtype=numpy.uint64)
2372+=======
2373+ if not hasattr(py_attr_ids, '__iter__'):
2374+ py_attr_ids = [py_attr_ids]
2375+
2376+ # Allow strings, e.g. 'lemma' or 'LEMMA'
2377+ py_attr_ids = [(IDS[id_.upper()] if hasattr(id_, 'upper') else id_)
2378+ for id_ in py_attr_ids]
2379+ # Make an array from the attributes --- otherwise inner loop would be Python
2380+ # dict iteration.
2381+ attr_ids = numpy.asarray(py_attr_ids, dtype=numpy.int32)
2382+ output = numpy.ndarray(shape=(self.length, len(attr_ids)), dtype=numpy.int32)
2383+>>>>>>> 841e2225
2384 for i in range(self.length):
2385 for j, feature in enumerate(attr_ids):
2386 output[i, j] = get_token_attr(&self.c[i], feature)
2387 # Handle 1d case
2388 return output if len(attr_ids) >= 2 else output.reshape((self.length,))
2389
2390+<<<<<<< HEAD
2391 def count_by(self, attr_id_t attr_id, exclude=None,
2392 PreshCounter counts=None):
2393 """Count the frequencies of a given attribute. Produces a dict of
2394@@ -606,6 +643,30 @@ cdef class Doc:
2395 {12800L: 1, 11880L: 2, 7561L: 1}
2396 >>> tokens.to_array([attrs.ORTH])
2397 array([[11880], [11880], [7561], [12800]])
2398+=======
2399+ def count_by(self, attr_id_t attr_id, exclude=None, PreshCounter counts=None):
2400+ """
2401+ Produce a dict of {attribute (int): count (ints)} frequencies, keyed
2402+ by the values of the given attribute ID.
2403+
2404+ Example:
2405+ from spacy.en import English
2406+ from spacy import attrs
2407+ nlp = English()
2408+ tokens = nlp(u'apple apple orange banana')
2409+ tokens.count_by(attrs.ORTH)
2410+ # {12800L: 1, 11880L: 2, 7561L: 1}
2411+ tokens.to_array([attrs.ORTH])
2412+ # array([[11880],
2413+ # [11880],
2414+ # [ 7561],
2415+ # [12800]])
2416+
2417+ Arguments:
2418+ attr_id
2419+ int
2420+ The attribute ID to key the counts.
2421+>>>>>>> 841e2225
2422 """
2423 cdef int i
2424 cdef attr_t attr
2425@@ -687,6 +748,7 @@ cdef class Doc:
2426 self.is_tagged = bool(TAG in attrs or POS in attrs)
2427 return self
2428
2429+<<<<<<< HEAD
2430 def get_lca_matrix(self):
2431 """Calculates the lowest common ancestor matrix for a given `Doc`.
2432 Returns LCA matrix containing the integer index of the ancestor, or -1
2433@@ -694,6 +756,59 @@ cdef class Doc:
2434 ancestor). Apologies about the recursion, but the impact on
2435 performance is negligible given the natural limitations on the depth
2436 of a typical human sentence.
2437+=======
2438+
2439+ def get_lca_matrix(self):
2440+ '''
2441+ Calculates the lowest common ancestor matrix
2442+ for a given Spacy doc.
2443+ Returns LCA matrix containing the integer index
2444+ of the ancestor, or -1 if no common ancestor is
2445+ found (ex if span excludes a necessary ancestor).
2446+ Apologies about the recursion, but the
2447+ impact on performance is negligible given
2448+ the natural limitations on the depth of a typical human sentence.
2449+ '''
2450+ # Efficiency notes:
2451+ #
2452+ # We can easily improve the performance here by iterating in Cython.
2453+ # To loop over the tokens in Cython, the easiest way is:
2454+ # for token in doc.c[:doc.c.length]:
2455+ # head = token + token.head
2456+ # Both token and head will be TokenC* here. The token.head attribute
2457+ # is an integer offset.
2458+ def __pairwise_lca(token_j, token_k, lca_matrix):
2459+ if lca_matrix[token_j.i][token_k.i] != -2:
2460+ return lca_matrix[token_j.i][token_k.i]
2461+ elif token_j == token_k:
2462+ lca_index = token_j.i
2463+ elif token_k.head == token_j:
2464+ lca_index = token_j.i
2465+ elif token_j.head == token_k:
2466+ lca_index = token_k.i
2467+ elif (token_j.head == token_j) and (token_k.head == token_k):
2468+ lca_index = -1
2469+ else:
2470+ lca_index = __pairwise_lca(token_j.head, token_k.head, lca_matrix)
2471+ lca_matrix[token_j.i][token_k.i] = lca_index
2472+ lca_matrix[token_k.i][token_j.i] = lca_index
2473+
2474+ return lca_index
2475+
2476+ lca_matrix = numpy.empty((len(self), len(self)), dtype=numpy.int32)
2477+ lca_matrix.fill(-2)
2478+ for j in range(len(self)):
2479+ token_j = self[j]
2480+ for k in range(j, len(self)):
2481+ token_k = self[k]
2482+ lca_matrix[j][k] = __pairwise_lca(token_j, token_k, lca_matrix)
2483+ lca_matrix[k][j] = lca_matrix[j][k]
2484+
2485+ return lca_matrix
2486+
2487+
2488+ def to_bytes(self):
2489+>>>>>>> 841e2225
2490 """
2491 # Efficiency notes:
2492 # We can easily improve the performance here by iterating in Cython.
2493diff --git a/spacy/tokens/printers.py b/spacy/tokens/printers.py
2494index 92b2cd84..16216974 100644
2495--- a/spacy/tokens/printers.py
2496+++ b/spacy/tokens/printers.py
2497@@ -6,14 +6,26 @@ from ..symbols import HEAD, TAG, DEP, ENT_IOB, ENT_TYPE
2498
2499
2500 def merge_ents(doc):
2501+<<<<<<< HEAD
2502 """Helper: merge adjacent entities into single tokens; modifies the doc."""
2503+=======
2504+ """
2505+ Helper: merge adjacent entities into single tokens; modifies the doc.
2506+ """
2507+>>>>>>> 841e2225
2508 for ent in doc.ents:
2509 ent.merge(ent.root.tag_, ent.text, ent.label_)
2510 return doc
2511
2512
2513 def format_POS(token, light, flat):
2514+<<<<<<< HEAD
2515 """Helper: form the POS output for a token."""
2516+=======
2517+ """
2518+ Helper: form the POS output for a token.
2519+ """
2520+>>>>>>> 841e2225
2521 subtree = dict([
2522 ("word", token.text),
2523 ("lemma", token.lemma_), # trigger
2524@@ -33,14 +45,28 @@ def format_POS(token, light, flat):
2525
2526
2527 def POS_tree(root, light=False, flat=False):
2528+<<<<<<< HEAD
2529 """Helper: generate a POS tree for a root token. The doc must have
2530 `merge_ents(doc)` ran on it.
2531+=======
2532+ """
2533+ Helper: generate a POS tree for a root token. The doc must have
2534+ merge_ents(doc) ran on it.
2535+>>>>>>> 841e2225
2536 """
2537 subtree = format_POS(root, light=light, flat=flat)
2538 for c in root.children:
2539 subtree["modifiers"].append(POS_tree(c))
2540 return subtree
2541
2542+<<<<<<< HEAD
2543+=======
2544+
2545+def parse_tree(doc, light=False, flat=False):
2546+ """
2547+ Makes a copy of the doc, then construct a syntactic parse tree, similar to
2548+ the one used in displaCy. Generates the POS tree for all sentences in a doc.
2549+>>>>>>> 841e2225
2550
2551 def parse_tree(doc, light=False, flat=False):
2552 """Make a copy of the doc and construct a syntactic parse tree similar to
2553@@ -67,8 +93,14 @@ def parse_tree(doc, light=False, flat=False):
2554 'POS_fine': 'VBD', 'lemma': 'eat'}
2555 """
2556 doc_clone = Doc(doc.vocab, words=[w.text for w in doc])
2557+<<<<<<< HEAD
2558 doc_clone.from_array([HEAD, TAG, DEP, ENT_IOB, ENT_TYPE],
2559 doc.to_array([HEAD, TAG, DEP, ENT_IOB, ENT_TYPE]))
2560+=======
2561+ doc_clone = Doc(doc.vocab, words=[w.text for w in doc])
2562+ doc_clone.from_array([HEAD, TAG, DEP, ENT_IOB, ENT_TYPE],
2563+ doc.to_array([HEAD, TAG, DEP, ENT_IOB, ENT_TYPE]))
2564+>>>>>>> 841e2225
2565 merge_ents(doc_clone) # merge the entities into single tokens first
2566 return [POS_tree(sent.root, light=light, flat=flat)
2567 for sent in doc_clone.sents]
2568diff --git a/spacy/tokens/span.pyx b/spacy/tokens/span.pyx
2569index 4056ef61..954e00c4 100644
2570--- a/spacy/tokens/span.pyx
2571+++ b/spacy/tokens/span.pyx
2572@@ -17,7 +17,10 @@ from ..attrs cimport IS_PUNCT, IS_SPACE
2573 from ..lexeme cimport Lexeme
2574 from ..compat import is_config
2575 from .. import about
2576+<<<<<<< HEAD
2577 from .underscore import Underscore
2578+=======
2579+>>>>>>> 841e2225
2580
2581
2582 cdef class Span:
2583@@ -184,6 +187,7 @@ cdef class Span:
2584 return numpy.dot(self.vector, other.vector) / (self.vector_norm * other.vector_norm)
2585
2586 def get_lca_matrix(self):
2587+<<<<<<< HEAD
2588 """Calculates the lowest common ancestor matrix for a given `Span`.
2589 Returns LCA matrix containing the integer index of the ancestor, or -1
2590 if no common ancestor is found (ex if span excludes a necessary
2591@@ -191,12 +195,29 @@ cdef class Span:
2592 performance is negligible given the natural limitations on the depth
2593 of a typical human sentence.
2594 """
2595+=======
2596+ '''
2597+ Calculates the lowest common ancestor matrix
2598+ for a given Spacy span.
2599+ Returns LCA matrix containing the integer index
2600+ of the ancestor, or -1 if no common ancestor is
2601+ found (ex if span excludes a necessary ancestor).
2602+ Apologies about the recursion, but the
2603+ impact on performance is negligible given
2604+ the natural limitations on the depth of a typical human sentence.
2605+ '''
2606+
2607+>>>>>>> 841e2225
2608 def __pairwise_lca(token_j, token_k, lca_matrix, margins):
2609 offset = margins[0]
2610 token_k_head = token_k.head if token_k.head.i in range(*margins) else token_k
2611 token_j_head = token_j.head if token_j.head.i in range(*margins) else token_j
2612 token_j_i = token_j.i - offset
2613 token_k_i = token_k.i - offset
2614+<<<<<<< HEAD
2615+=======
2616+
2617+>>>>>>> 841e2225
2618 if lca_matrix[token_j_i][token_k_i] != -2:
2619 return lca_matrix[token_j_i][token_k_i]
2620 elif token_j == token_k:
2621@@ -209,19 +230,31 @@ cdef class Span:
2622 lca_index = -1
2623 else:
2624 lca_index = __pairwise_lca(token_j_head, token_k_head, lca_matrix, margins)
2625+<<<<<<< HEAD
2626+ lca_matrix[token_j_i][token_k_i] = lca_index
2627+ lca_matrix[token_k_i][token_j_i] = lca_index
2628+=======
2629+
2630 lca_matrix[token_j_i][token_k_i] = lca_index
2631 lca_matrix[token_k_i][token_j_i] = lca_index
2632+
2633+>>>>>>> 841e2225
2634 return lca_index
2635
2636 lca_matrix = numpy.empty((len(self), len(self)), dtype=numpy.int32)
2637 lca_matrix.fill(-2)
2638 margins = [self.start, self.end]
2639+<<<<<<< HEAD
2640+=======
2641+
2642+>>>>>>> 841e2225
2643 for j in range(len(self)):
2644 token_j = self[j]
2645 for k in range(len(self)):
2646 token_k = self[k]
2647 lca_matrix[j][k] = __pairwise_lca(token_j, token_k, lca_matrix, margins)
2648 lca_matrix[k][j] = lca_matrix[j][k]
2649+<<<<<<< HEAD
2650 return lca_matrix
2651
2652 cpdef np.ndarray to_array(self, object py_attr_ids):
2653@@ -246,6 +279,12 @@ cdef class Span:
2654 for j, feature in enumerate(attr_ids):
2655 output[i-self.start, j] = get_token_attr(&self.doc.c[i], feature)
2656 return output
2657+=======
2658+
2659+ return lca_matrix
2660+
2661+
2662+>>>>>>> 841e2225
2663
2664 cpdef int _recalculate_indices(self) except -1:
2665 if self.end > self.doc.length \
2666@@ -359,6 +398,7 @@ cdef class Span:
2667 if not self.doc.is_parsed:
2668 raise ValueError(
2669 "noun_chunks requires the dependency parse, which "
2670+<<<<<<< HEAD
2671 "requires a statistical model to be installed and loaded. "
2672 "For more info, see the "
2673 "documentation: \n%s\n" % about.__docs_models__)
2674@@ -367,10 +407,18 @@ cdef class Span:
2675 # during the iteration. The tricky thing here is that Span accepts
2676 # its tokenisation changing, so it's okay once we have the Span
2677 # objects. See Issue #375
2678+=======
2679+ "requires data to be installed. For more info, see the "
2680+ "documentation: \n%s\n" % about.__docs_models__)
2681+ # Accumulate the result before beginning to iterate over it. This prevents
2682+ # the tokenisation from being changed out from under us during the iteration.
2683+ # The tricky thing here is that Span accepts its tokenisation changing,
2684+ # so it's okay once we have the Span objects. See Issue #375
2685+>>>>>>> 841e2225
2686 spans = []
2687 cdef attr_t label
2688 for start, end, label in self.doc.noun_chunks_iterator(self):
2689- spans.append(Span(self, start, end, label=label))
2690+ spans.append(Span(self.doc, start, end, label=label))
2691 for span in spans:
2692 yield span
2693
2694diff --git a/spacy/tokens/token.pyx b/spacy/tokens/token.pyx
2695index 6715c509..2b44ac64 100644
2696--- a/spacy/tokens/token.pyx
2697+++ b/spacy/tokens/token.pyx
2698@@ -19,9 +19,13 @@ from ..attrs cimport IS_OOV, IS_TITLE, IS_UPPER, LIKE_URL, LIKE_NUM, LIKE_EMAIL
2699 from ..attrs cimport IS_STOP, ID, ORTH, NORM, LOWER, SHAPE, PREFIX, SUFFIX
2700 from ..attrs cimport LENGTH, CLUSTER, LEMMA, POS, TAG, DEP
2701 from ..compat import is_config
2702+<<<<<<< HEAD
2703 from .. import util
2704 from .. import about
2705 from .underscore import Underscore
2706+=======
2707+from .. import about
2708+>>>>>>> 841e2225
2709
2710
2711 cdef class Token:
2712@@ -305,10 +309,30 @@ cdef class Token:
2713 def __get__(self):
2714 if 'vector' in self.doc.user_token_hooks:
2715 return self.doc.user_token_hooks['vector'](self)
2716+<<<<<<< HEAD
2717 if self.vocab.vectors.size == 0 and self.doc.tensor.size != 0:
2718 return self.doc.tensor[self.i]
2719 else:
2720 return self.vocab.get_vector(self.c.lex.orth)
2721+=======
2722+ cdef int length = self.vocab.vectors_length
2723+ if length == 0:
2724+ raise ValueError(
2725+ "Word vectors set to length 0. This may be because you "
2726+ "don't have a model installed or loaded, or because your "
2727+ "model doesn't include word vectors. For more info, see "
2728+ "the documentation: \n%s\n" % about.__docs_models__)
2729+ vector_view = <float[:length,]>self.c.lex.vector
2730+ return numpy.asarray(vector_view)
2731+
2732+ property repvec:
2733+ def __get__(self):
2734+ raise AttributeError("repvec was renamed to vector in v0.100")
2735+
2736+ property has_repvec:
2737+ def __get__(self):
2738+ raise AttributeError("has_repvec was renamed to has_vector in v0.100")
2739+>>>>>>> 841e2225
2740
2741 property vector_norm:
2742 """The L2 norm of the token's vector representation.
2743diff --git a/website/_harp.json b/website/_harp.json
2744index 8cd9bbbf..9f1ba58d 100644
2745--- a/website/_harp.json
2746+++ b/website/_harp.json
2747@@ -13,7 +13,15 @@
2748 "DEMOS_URL": "https://demos.explosion.ai",
2749 "MODELS_REPO": "explosion/spacy-models",
2750
2751+<<<<<<< HEAD
2752 "SPACY_VERSION": "2.0",
2753+=======
2754+ "SPACY_VERSION": "1.9",
2755+ "LATEST_NEWS": {
2756+ "url": "/docs/usage/models",
2757+ "title": "The first official Spanish model is here!"
2758+ },
2759+>>>>>>> 841e2225
2760
2761 "SOCIAL": {
2762 "twitter": "spacy_io",
2763@@ -68,6 +76,7 @@
2764 { "id": "config", "title": "Configuration", "multiple": true, "options": [
2765 {"id": "venv", "title": "virtualenv", "help": "Use a virtual environment and install spaCy into a user directory" }]
2766 },
2767+<<<<<<< HEAD
2768 { "id": "model", "title": "Models", "multiple": true }
2769 ],
2770
2771@@ -85,6 +94,18 @@
2772 "ALPHA": true,
2773 "V_CSS": "2.0a3",
2774 "V_JS": "2.0a0",
2775+=======
2776+ { "id": "model", "title": "Models", "multiple": true, "options": [
2777+ { "id": "en", "title": "English", "meta": "50MB" },
2778+ { "id": "de", "title": "German", "meta": "645MB" },
2779+ { "id": "fr", "title": "French", "meta": "1.33GB" },
2780+ { "id": "es", "title": "Spanish", "meta": "377MB"}]
2781+ }
2782+ ],
2783+
2784+ "V_CSS": "1.8",
2785+ "V_JS": "1.2",
2786+>>>>>>> 841e2225
2787 "DEFAULT_SYNTAX": "python",
2788 "ANALYTICS": "UA-58931649-1",
2789 "MAILCHIMP": {
2790diff --git a/website/_includes/_mixins.jade b/website/_includes/_mixins.jade
2791index e93fc7af..e19e4762 100644
2792--- a/website/_includes/_mixins.jade
2793+++ b/website/_includes/_mixins.jade
2794@@ -138,6 +138,14 @@ mixin aside-wrapper(label, emoji)
2795 block
2796
2797
2798+//- Help icon with tooltip
2799+ tooltip - [string] Tooltip text
2800+
2801+mixin help(tooltip)
2802+ span(data-tooltip=tooltip)&attributes(attributes)
2803+ +icon("help", 16).i-icon--inline
2804+
2805+
2806 //- Aside for text
2807 label - [string] aside title (optional)
2808
2809diff --git a/website/_includes/_scripts.jade b/website/_includes/_scripts.jade
2810index a70f880a..6412874e 100644
2811--- a/website/_includes/_scripts.jade
2812+++ b/website/_includes/_scripts.jade
2813@@ -1,7 +1,25 @@
2814 //- ? INCLUDES > SCRIPTS
2815
2816+<<<<<<< HEAD
2817 if quickstart
2818 script(src="/assets/js/vendor/quickstart.min.js")
2819+=======
2820+script(src="/assets/js/main.js?v#{V_JS}")
2821+script(src="/assets/js/prism.js")
2822+
2823+if SECTION == "docs"
2824+ if quickstart
2825+ script(src="/assets/js/quickstart.js")
2826+ script var qs = new Quickstart("#qs");
2827+
2828+ script.
2829+ ((window.gitter = {}).chat = {}).options = {
2830+ useStyles: false,
2831+ activationElement: '.js-gitter-button',
2832+ targetElement: '.js-gitter',
2833+ room: '!{SOCIAL.gitter}'
2834+ };
2835+>>>>>>> 841e2225
2836
2837 if IS_PAGE
2838 script(src="/assets/js/vendor/in-view.min.js")
2839diff --git a/website/api/_annotation/_named-entities.jade b/website/api/_annotation/_named-entities.jade
2840index 4cc8a707..14217476 100644
2841--- a/website/api/_annotation/_named-entities.jade
2842+++ b/website/api/_annotation/_named-entities.jade
2843@@ -1,11 +1,15 @@
2844 //- ? DOCS > API > ANNOTATION > NAMED ENTITIES
2845
2846+<<<<<<< HEAD:website/api/_annotation/_named-entities.jade
2847 p
2848 | Models trained on the
2849 | #[+a("https://catalog.ldc.upenn.edu/ldc2013t19") OntoNotes 5] corpus
2850 | support the following entity types:
2851
2852 +table(["Type", "Description"])
2853+=======
2854++table([ "Type", "Description" ])
2855+>>>>>>> 841e2225:website/docs/api/_annotation/_named-entities.jade
2856 +row
2857 +cell #[code PERSON]
2858 +cell People, including fictional.
2859diff --git a/website/assets/css/_base/_utilities.sass b/website/assets/css/_base/_utilities.sass
2860index 9b1c0ced..a9f9d73c 100644
2861--- a/website/assets/css/_base/_utilities.sass
2862+++ b/website/assets/css/_base/_utilities.sass
2863@@ -201,7 +201,11 @@
2864 .u-border-dotted
2865 border-bottom: 1px dotted $color-subtle
2866
2867+<<<<<<< HEAD
2868 @each $name, $color in (theme: $color-theme, dark: $color-dark, subtle: $color-subtle-dark, light: $color-back, red: $color-red, green: $color-green, yellow: $color-yellow)
2869+=======
2870+@each $name, $color in (theme: $color-theme, subtle: $color-subtle-dark, light: $color-back, 'red': $color-red, 'green': $color-green, 'yellow': $color-yellow)
2871+>>>>>>> 841e2225
2872 .u-color-#{$name}
2873 color: $color
2874
2875diff --git a/website/assets/css/_components/_code.sass b/website/assets/css/_components/_code.sass
2876index 0fec230c..379b2773 100644
2877--- a/website/assets/css/_components/_code.sass
2878+++ b/website/assets/css/_components/_code.sass
2879@@ -34,7 +34,11 @@
2880
2881 .c-code-block__content
2882 display: block
2883+<<<<<<< HEAD
2884 font: normal normal 1.1rem/#{1.9} $font-code
2885+=======
2886+ font: normal 600 1.1rem/#{2} $font-code
2887+>>>>>>> 841e2225
2888 padding: 1em 2em
2889
2890 &[data-prompt]:before,
2891diff --git a/website/assets/css/_components/_quickstart.sass b/website/assets/css/_components/_quickstart.sass
2892index d853d756..bef192c7 100644
2893--- a/website/assets/css/_components/_quickstart.sass
2894+++ b/website/assets/css/_components/_quickstart.sass
2895@@ -1,17 +1,25 @@
2896 //- ? CSS > COMPONENTS > QUICKSTART
2897
2898 .c-quickstart
2899+<<<<<<< HEAD
2900 border-radius: $border-radius
2901+=======
2902+ border: 1px solid $color-subtle
2903+ border-radius: 2px
2904+>>>>>>> 841e2225
2905 display: none
2906 background: $color-subtle-light
2907
2908 &:not([style]) + .c-quickstart__info
2909 display: none
2910
2911+<<<<<<< HEAD
2912 .c-code-block
2913 border-top-left-radius: 0
2914 border-top-right-radius: 0
2915
2916+=======
2917+>>>>>>> 841e2225
2918 .c-quickstart__content
2919 padding: 2rem 3rem
2920
2921@@ -52,7 +60,11 @@
2922 vertical-align: middle
2923 margin-right: 1rem
2924 cursor: pointer
2925+<<<<<<< HEAD
2926 border-radius: 2px
2927+=======
2928+ border-radius: 50%
2929+>>>>>>> 841e2225
2930
2931 .c-quickstart__input--check:checked + &:before
2932 background: $color-theme url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4gICAgPHBhdGggZmlsbD0iI2ZmZiIgZD0iTTkgMTYuMTcybDEwLjU5NC0xMC41OTQgMS40MDYgMS40MDYtMTIgMTItNS41NzgtNS41NzggMS40MDYtMS40MDZ6Ii8+PC9zdmc+)
2933@@ -75,6 +87,10 @@
2934 flex: 100%
2935
2936 .c-quickstart__legend
2937+<<<<<<< HEAD
2938+=======
2939+ color: $color-subtle-dark
2940+>>>>>>> 841e2225
2941 margin-right: 2rem
2942 padding-top: 0.75rem
2943 flex: 1 1 35%
2944@@ -86,6 +102,7 @@
2945 &:before
2946 color: $color-theme
2947 margin-right: 1em
2948+<<<<<<< HEAD
2949
2950 &.c-quickstart__line--bash:before
2951 content: "$"
2952@@ -98,3 +115,9 @@
2953
2954 .c-quickstart__code
2955 font-size: 1.4rem
2956+=======
2957+ content: "$"
2958+
2959+.c-quickstart__code
2960+ font-size: 1.6rem
2961+>>>>>>> 841e2225
2962diff --git a/website/assets/css/_components/_tooltips.sass b/website/assets/css/_components/_tooltips.sass
2963index f9284dcd..e195c33f 100644
2964--- a/website/assets/css/_components/_tooltips.sass
2965+++ b/website/assets/css/_components/_tooltips.sass
2966@@ -4,6 +4,7 @@
2967 position: relative
2968
2969 @include breakpoint(min, sm)
2970+<<<<<<< HEAD
2971 &[data-tooltip-style="code"]:before
2972 -webkit-font-smoothing: subpixel-antialiased
2973 -moz-osx-font-smoothing: auto
2974@@ -12,11 +13,14 @@
2975 white-space: nowrap
2976 min-width: auto
2977
2978+=======
2979+>>>>>>> 841e2225
2980 &:before
2981 @include position(absolute, top, left, 125%, 50%)
2982 display: inline-block
2983 content: attr(data-tooltip)
2984 background: $color-front
2985+<<<<<<< HEAD
2986 border-radius: $border-radius
2987 border: 1px solid rgba($color-subtle-dark, 0.5)
2988 color: $color-back
2989@@ -32,6 +36,21 @@
2990 padding: 0.75em 1em 1em
2991 z-index: 200
2992 white-space: pre-wrap
2993+=======
2994+ border-radius: 2px
2995+ color: $color-back
2996+ font-family: inherit
2997+ font-size: 1.3rem
2998+ line-height: 1.25
2999+ opacity: 0
3000+ padding: 0.5em 0.75em
3001+ transform: translateX(-50%) translateY(-2px)
3002+ transition: opacity 0.1s ease-out, transform 0.1s ease-out
3003+ visibility: hidden
3004+ min-width: 200px
3005+ max-width: 300px
3006+ z-index: 200
3007+>>>>>>> 841e2225
3008
3009 &:hover:before
3010 opacity: 1
3011diff --git a/website/assets/css/_variables.sass b/website/assets/css/_variables.sass
3012index fbceb5a6..70ba40f5 100644
3013--- a/website/assets/css/_variables.sass
3014+++ b/website/assets/css/_variables.sass
3015@@ -26,15 +26,25 @@ $font-code: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace
3016
3017 // Colors
3018
3019+<<<<<<< HEAD
3020 $colors: ( blue: #09a3d5, green: #05b083 )
3021+=======
3022+$colors: ( blue: #09a3d5, red: #d9515d )
3023+$colors-light: (blue: #cceaf4, red: #f9d7da)
3024+>>>>>>> 841e2225
3025
3026 $color-back: #fff !default
3027 $color-front: #1a1e23 !default
3028 $color-dark: lighten($color-front, 20) !default
3029
3030 $color-theme: map-get($colors, $theme)
3031+<<<<<<< HEAD
3032 $color-theme-dark: darken(map-get($colors, $theme), 10)
3033 $color-theme-light: rgba($color-theme, 0.05)
3034+=======
3035+$color-theme-dark: darken(map-get($colors, $theme), 5)
3036+$color-theme-light: map-get($colors-light, $theme)
3037+>>>>>>> 841e2225
3038
3039 $color-subtle: #ddd !default
3040 $color-subtle-light: #f6f6f6 !default
3041diff --git a/website/assets/css/style.sass b/website/assets/css/style.sass
3042index 47cf3f1b..5ee9fee8 100644
3043--- a/website/assets/css/style.sass
3044+++ b/website/assets/css/style.sass
3045@@ -33,5 +33,10 @@ $theme: blue !default
3046 @import _components/progress
3047 @import _components/sidebar
3048 @import _components/tables
3049+<<<<<<< HEAD
3050 @import _components/quickstart
3051 @import _components/tooltips
3052+=======
3053+@import _components/tooltips
3054+@import _components/quickstart
3055+>>>>>>> 841e2225
3056diff --git a/website/assets/js/quickstart.js b/website/assets/js/quickstart.js
3057new file mode 100644
3058index 00000000..d062aa91
3059--- /dev/null
3060+++ b/website/assets/js/quickstart.js
3061@@ -0,0 +1,8 @@
3062+/**
3063+ * quickstart.js
3064+ * A micro-form for user-specific installation instructions
3065+ *
3066+ * @author Ines Montani <ines@ines.io>
3067+ * @version 0.0.1
3068+ * @license MIT
3069+ */'use strict';var _createClass=function(){function a(b,c){for(var e,d=0;d<c.length;d++)e=c[d],e.enumerable=e.enumerable||!1,e.configurable=!0,'value'in e&&(e.writable=!0),Object.defineProperty(b,e.key,e)}return function(b,c,d){return c&&a(b.prototype,c),d&&a(b,d),b}}();function _toConsumableArray(a){if(Array.isArray(a)){for(var b=0,c=Array(a.length);b<a.length;b++)c[b]=a[b];return c}return Array.from(a)}function _classCallCheck(a,b){if(!(a instanceof b))throw new TypeError('Cannot call a class as a function')}var Quickstart=function(){function a(){var b=0<arguments.length&&void 0!==arguments[0]?arguments[0]:'#quickstart',d=arguments[1],c=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{};_classCallCheck(this,a),this.container='string'==typeof b?this._$(b):b,this.groups=d,this.pfx=c.prefix||'qs',this.dpfx='data-'+this.pfx,this.init=this.init.bind(this),c.noInit||document.addEventListener('DOMContentLoaded',this.init)}return _createClass(a,[{key:'init',value:function init(){this.updateContainer(),this.container.style.display='block',this.container.classList.add(''+this.pfx);var b=this.groups;b instanceof Array?b.reverse().forEach(this.createGroup.bind(this)):this._$$('['+this.dpfx+'-group]').forEach(this.updateGroup.bind(this))}},{key:'initGroup',value:function initGroup(b,c){b.addEventListener('change',this.update.bind(this)),b.dispatchEvent(new CustomEvent('change',{detail:c}))}},{key:'updateGroup',value:function updateGroup(b){var c=b.getAttribute(this.dpfx+'-group'),d=this.createStyles(c);b.insertBefore(d,b.firstChild),this.initGroup(b,c)}},{key:'update',value:function update(b){var f=this,c=b.detail||b.target.name,d=this._$$('[name='+c+']:checked').map(function(h){return h.value}),e=d.map(function(h){return':not(['+f.dpfx+'-'+c+'="'+h+'"])'}).join(''),g='['+this.dpfx+'-results]>['+this.dpfx+'-'+c+']'+e+' {display: none}';this._$('['+this.dpfx+'-style="'+c+'"]').textContent=g}},{key:'updateContainer',value:function updateContainer(){if(!this._$('['+this.dpfx+'-results]')){var b=this.childNodes(this.container,'pre'),c=b?b[0]:this._c('pre',this.pfx+'-code'),d=this.childNodes(c,'code')||this.childNodes(this.container,'code'),e=d?d[0]:this._c('code',this.pfx+'-results');e.setAttribute(this.dpfx+'-results','');var f=this.childNodes(e,'span')||this.childNodes(c,'span')||this.childNodes(this.container,'span');f&&f.forEach(function(g){return e.appendChild(g)}),c.appendChild(e),this.container.appendChild(c)}}},{key:'createGroup',value:function createGroup(b){var d=this,c=this._c('fieldset',this.pfx+'-group');c.setAttribute(this.dpfx+'-group',b.id),c.innerHTML=this.createStyles(b.id).outerHTML,c.innerHTML+='<legend class="'+this.pfx+'-legend">'+b.title+'</legend>',c.innerHTML+=b.options.map(function(e){var f=b.multiple?'checkbox':'radio';return'<input class="'+d.pfx+'-input '+d.pfx+'-input--'+f+'" type="'+f+'" name="'+b.id+'" id="'+e.id+'" value="'+e.id+'" '+(e.checked?'checked':'')+' /><label class="'+d.pfx+'-label" for="'+e.id+'">'+e.title+'</label>'}).join(''),this.container.insertBefore(c,this.container.firstChild),this.initGroup(c,b.id)}},{key:'createStyles',value:function createStyles(b){var c=this._c('style');return c.setAttribute(this.dpfx+'-style',b),c.textContent='['+this.dpfx+'-results]>['+this.dpfx+'-'+b+'] {display: none}',c}},{key:'childNodes',value:function childNodes(b,c){var d=c.toUpperCase();if(!b.hasChildNodes)return!1;var e=[].concat(_toConsumableArray(b.childNodes)).filter(function(f){return f.nodeName===d});return!!e.length&&e}},{key:'_$',value:function _$(b){return document.querySelector(b)}},{key:'_$$',value:function _$$(b){return[].concat(_toConsumableArray(document.querySelectorAll(b)))}},{key:'_c',value:function _c(b,c){var d=document.createElement(b);return c&&(d.className=c),d}}]),a}();
3070diff --git a/website/index.jade b/website/index.jade
3071index 3e15559a..cbddf870 100644
3072--- a/website/index.jade
3073+++ b/website/index.jade
3074@@ -1,13 +1,9 @@
3075-//- ? LANDING PAGE
3076-
3077-include _includes/_mixins
3078-
3079-+landing-header
3080 h1.c-landing__title.u-heading-0
3081 | Industrial-Strength#[br]
3082 | Natural Language#[br]
3083 | Processing
3084
3085+<<<<<<< HEAD
3086 h2.c-landing__title.o-block.u-heading-3
3087 span.u-text-label.u-text-label--light in Python
3088
3089@@ -50,6 +46,63 @@ include _includes/_mixins
3090
3091 +button("/usage/deep-learning", true, "primary")
3092 | Read more
3093+=======
3094+ h2.c-landing__title.o-block.u-heading-1
3095+ | in Python
3096+
3097+ +landing-badge(gh("spaCy") + "/releases/tag/v2.0.0-alpha", "v2alpha", "Try spaCy v2.0.0 alpha!")
3098+
3099+ +grid.o-content
3100+ +grid-col("third").o-card
3101+ +h(2) Fastest in the world
3102+ p
3103+ | spaCy excels at large-scale information extraction tasks.
3104+ | It's written from the ground up in carefully memory-managed
3105+ | Cython. Independent research has confirmed that spaCy is
3106+ | the fastest in the world. If your application needs to
3107+ | process entire web dumps, spaCy is the library you want to
3108+ | be using.
3109+
3110+ +button("/docs/api", true, "primary")
3111+ | Facts & figures
3112+
3113+ +grid-col("third").o-card
3114+ +h(2) Get things done
3115+ p
3116+ | spaCy is designed to help you do real work — to build real
3117+ | products, or gather real insights. The library respects
3118+ | your time, and tries to avoid wasting it. It's easy to
3119+ | install, and its API is simple and productive. We like to
3120+ | think of spaCy as the Ruby on Rails of Natural Language
3121+ | Processing.
3122+
3123+ +button("/docs/usage", true, "primary")
3124+ | Get started
3125+
3126+ +grid-col("third").o-card
3127+ +h(2) Deep learning
3128+ p
3129+ | spaCy is the best way to prepare text for deep learning.
3130+ | It interoperates seamlessly with
3131+ | #[+a("https://www.tensorflow.org") TensorFlow],
3132+ | #[+a("https://keras.io") Keras],
3133+ | #[+a("http://scikit-learn.org") Scikit-Learn],
3134+ | #[+a("https://radimrehurek.com/gensim") Gensim] and the
3135+ | rest of Python's awesome AI ecosystem. spaCy helps you
3136+ | connect the statistical models trained by these libraries
3137+ | to the rest of your application.
3138+
3139+ +button("/docs/usage/deep-learning", true, "primary")
3140+ | Read more
3141+
3142+.o-inline-list.o-block.u-border-bottom.u-text-small.u-text-center.u-padding-small
3143+ +a(gh("spaCy") + "/releases")
3144+ strong.u-text-label.u-color-subtle #[+icon("code", 18)] Latest release:
3145+ | v#{SPACY_VERSION}
3146+
3147+ if LATEST_NEWS
3148+ +a(LATEST_NEWS.url) #[+tag.o-icon New!] #{LATEST_NEWS.title}
3149+>>>>>>> 841e2225
3150
3151 .o-content
3152 +grid
3153@@ -93,7 +146,14 @@ include _includes/_mixins
3154 +item Convenient string-to-hash mapping
3155 +item Export to numpy data arrays
3156 +item Efficient binary serialization
3157+<<<<<<< HEAD
3158 +item Easy #[strong model packaging] and deployment
3159+=======
3160+ +item Easy #[strong deep learning] integration
3161+ +item
3162+ | Statistical models for #[strong English],
3163+ | #[strong German], #[strong French] and #[strong Spanish]
3164+>>>>>>> 841e2225
3165 +item State-of-the-art speed
3166 +item Robust, rigorously evaluated accuracy