prompt: You will be provided with a partial code base and an issue statement explaining a problem to resolve.
  <issue>
  💫 Lemmatizer should apply rules on OOV words
  @juanmirocks points out in #327 that the lemmatizer fails on OOV words:
  
  ```python
  
  >>> nlp.vocab.morphology.lemmatizer(u'endosomes', 'noun', morphology={'number': 'plur'})set([u'endosomes'])
  >>> nlp.vocab.morphology.lemmatizer(u'chromosomes', 'noun', morphology={'number': 'plur'})
  set([u'chromosome'])
  ```
  
  Suggested patch to lemmatizer.py
  
  ```python
  
      oov_forms = []
      for old, new in rules:
          if string.endswith(old):
              form = string[:len(string) - len(old)] + new
              if form in index or not form.isalpha():
                  forms.append(form)
              else:
                  oov_forms.append(form)
      if not forms:
          forms.extend(oov_forms)
  ```
  
  
  ## Your Environment
  <!-- Include details of your environment -->
  * Operating System: 
  * Python Version Used: 
  * spaCy Version Used: 
  * Environment Information: 
  
  
  </issue>
  <code>
  [start of spacy/lemmatizer.py]
  1 from __future__ import unicode_literals, print_function
  2 import codecs
  3 import pathlib
  4 
  5 import ujson as json
  6 
  7 from .symbols import POS, NOUN, VERB, ADJ, PUNCT
  8 
  9 
  10 class Lemmatizer(object):
  11     @classmethod
  12     def load(cls, path, rules=None):
  13         index = {}
  14         exc = {}
  15         for pos in ['adj', 'noun', 'verb']:
  16             pos_index_path = path / 'wordnet' / 'index.{pos}'.format(pos=pos)
  17             if pos_index_path.exists():
  18                 with pos_index_path.open() as file_:
  19                     index[pos] = read_index(file_)
  20             else:
  21                 index[pos] = set()
  22             pos_exc_path = path / 'wordnet' / '{pos}.exc'.format(pos=pos)
  23             if pos_exc_path.exists():
  24                 with pos_exc_path.open() as file_:
  25                     exc[pos] = read_exc(file_)
  26             else:
  27                 exc[pos] = {}
  28         if rules is None and (path / 'vocab' / 'lemma_rules.json').exists():
  29             with (path / 'vocab' / 'lemma_rules.json').open('r', encoding='utf8') as file_:
  30                 rules = json.load(file_)
  31         elif rules is None:
  32             rules = {}
  33         return cls(index, exc, rules)
  34 
  35     def __init__(self, index, exceptions, rules):
  36         self.index = index
  37         self.exc = exceptions
  38         self.rules = rules
  39 
  40     def __call__(self, string, univ_pos, morphology=None):
  41         if univ_pos == NOUN:
  42             univ_pos = 'noun'
  43         elif univ_pos == VERB:
  44             univ_pos = 'verb'
  45         elif univ_pos == ADJ:
  46             univ_pos = 'adj'
  47         elif univ_pos == PUNCT:
  48             univ_pos = 'punct'
  49         # See Issue #435 for example of where this logic is requied.
  50         if self.is_base_form(univ_pos, morphology):
  51             return set([string.lower()])
  52         lemmas = lemmatize(string, self.index.get(univ_pos, {}),
  53                            self.exc.get(univ_pos, {}),
  54                            self.rules.get(univ_pos, []))
  55         return lemmas
  56 
  57     def is_base_form(self, univ_pos, morphology=None):
  58         '''Check whether we're dealing with an uninflected paradigm, so we can
  59         avoid lemmatization entirely.'''
  60         morphology = {} if morphology is None else morphology
  61         others = [key for key in morphology if key not in (POS, 'number', 'pos', 'verbform')]
  62         if univ_pos == 'noun' and morphology.get('number') == 'sing' and not others:
  63             return True
  64         elif univ_pos == 'verb' and morphology.get('verbform') == 'inf' and not others:
  65             return True
  66         else:
  67             return False
  68 
  69     def noun(self, string, morphology=None):
  70         return self(string, 'noun', morphology)
  71 
  72     def verb(self, string, morphology=None):
  73         return self(string, 'verb', morphology)
  74 
  75     def adj(self, string, morphology=None):
  76         return self(string, 'adj', morphology)
  77 
  78     def punct(self, string, morphology=None):
  79         return self(string, 'punct', morphology)
  80 
  81 
  82 def lemmatize(string, index, exceptions, rules):
  83     string = string.lower()
  84     forms = []
  85     # TODO: Is this correct? See discussion in Issue #435.
  86     #if string in index:
  87     #    forms.append(string)
  88     forms.extend(exceptions.get(string, []))
  89     for old, new in rules:
  90         if string.endswith(old):
  91             form = string[:len(string) - len(old)] + new
  92             if form in index or not form.isalpha():
  93                 forms.append(form)
  94     if not forms:
  95         forms.append(string)
  96     return set(forms)
  97 
  98 
  99 def read_index(fileobj):
  100     index = set()
  101     for line in fileobj:
  102         if line.startswith(' '):
  103             continue
  104         pieces = line.split()
  105         word = pieces[0]
  106         if word.count('_') == 0:
  107             index.add(word)
  108     return index
  109 
  110 
  111 def read_exc(fileobj):
  112     exceptions = {}
  113     for line in fileobj:
  114         if line.startswith(' '):
  115             continue
  116         pieces = line.split()
  117         exceptions[pieces[0]] = tuple(pieces[1:])
  118     return exceptions
  119
  [end of spacy/lemmatizer.py]
  </code>
  
  I need you to solve this issue by generating a single patch file that I can apply directly to this repository using git apply. Please respond with a single patch file in the following format.
  <patch>
  diff --git a/file.py b/file.py
  --- a/file.py
  +++ b/file.py
  @@ -1,27 +1,35 @@
   def euclidean(a, b):
  -    while b:
  -        a, b = b, a % b
  -    return a
  +    if b == 0:
  +        return a
  +    return euclidean(b, a % b)
   
   
   def bresenham(x0, y0, x1, y1):
       points = []
       dx = abs(x1 - x0)
       dy = abs(y1 - y0)
  -    sx = 1 if x0 < x1 else -1
  -    sy = 1 if y0 < y1 else -1
  -    err = dx - dy
  +    x, y = x0, y0
  +    sx = -1 if x0 > x1 else 1
  +    sy = -1 if y0 > y1 else 1
   
  -    while True:
  -        points.append((x0, y0))
  -        if x0 == x1 and y0 == y1:
  -            break
  -        e2 = 2 * err
  -        if e2 > -dy:
  -            err -= dy
  -            x0 += sx
  -        if e2 < dx:
  -            err += dx
  -            y0 += sy
  +    if dx > dy:
  +        err = dx / 2.0
  +        while x != x1:
  +            points.append((x, y))
  +            err -= dy
  +            if err < 0:
  +                y += sy
  +                err += dx
  +            x += sx
  +    else:
  +        err = dy / 2.0
  +        while y != y1:
  +            points.append((x, y))
  +            err -= dx
  +            if err < 0:
  +                x += sx
  +                err += dy
  +            y += sy
  +
  +    points.append((x, y))
       return points
  
  </patch>
golden_diff: diff --git a/spacy/lemmatizer.py b/spacy/lemmatizer.py
  --- a/spacy/lemmatizer.py
  +++ b/spacy/lemmatizer.py
  @@ -86,13 +86,16 @@
       #if string in index:
       #    forms.append(string)
       forms.extend(exceptions.get(string, []))
  +    oov_forms = []
       for old, new in rules:
           if string.endswith(old):
               form = string[:len(string) - len(old)] + new
               if form in index or not form.isalpha():
                   forms.append(form)
  +            else:
  +                oov_forms.append(form)
       if not forms:
  -        forms.append(string)
  +        forms.extend(oov_forms)
       return set(forms)
  
verification_info: {"golden_diff": "diff --git a/spacy/lemmatizer.py b/spacy/lemmatizer.py\n--- a/spacy/lemmatizer.py\n+++ b/spacy/lemmatizer.py\n@@ -86,13 +86,16 @@\n     #if string in index:\n     #    forms.append(string)\n     forms.extend(exceptions.get(string, []))\n+    oov_forms = []\n     for old, new in rules:\n         if string.endswith(old):\n             form = string[:len(string) - len(old)] + new\n             if form in index or not form.isalpha():\n                 forms.append(form)\n+            else:\n+                oov_forms.append(form)\n     if not forms:\n-        forms.append(string)\n+        forms.extend(oov_forms)\n     return set(forms)\n", "issue": "\ud83d\udcab Lemmatizer should apply rules on OOV words\n@juanmirocks points out in #327 that the lemmatizer fails on OOV words:\r\n\r\n```python\r\n\r\n>>> nlp.vocab.morphology.lemmatizer(u'endosomes', 'noun', morphology={'number': 'plur'})set([u'endosomes'])\r\n>>> nlp.vocab.morphology.lemmatizer(u'chromosomes', 'noun', morphology={'number': 'plur'})\r\nset([u'chromosome'])\r\n```\r\n\r\nSuggested patch to lemmatizer.py\r\n\r\n```python\r\n\r\n    oov_forms = []\r\n    for old, new in rules:\r\n        if string.endswith(old):\r\n            form = string[:len(string) - len(old)] + new\r\n            if form in index or not form.isalpha():\r\n                forms.append(form)\r\n            else:\r\n                oov_forms.append(form)\r\n    if not forms:\r\n        forms.extend(oov_forms)\r\n```\r\n\r\n\r\n## Your Environment\r\n<!-- Include details of your environment -->\r\n* Operating System: \r\n* Python Version Used: \r\n* spaCy Version Used: \r\n* Environment Information: \r\n\n", "before_files": [{"content": "from __future__ import unicode_literals, print_function\nimport codecs\nimport pathlib\n\nimport ujson as json\n\nfrom .symbols import POS, NOUN, VERB, ADJ, PUNCT\n\n\nclass Lemmatizer(object):\n    @classmethod\n    def load(cls, path, rules=None):\n        index = {}\n        exc = {}\n        for pos in ['adj', 'noun', 'verb']:\n            pos_index_path = path / 'wordnet' / 'index.{pos}'.format(pos=pos)\n            if pos_index_path.exists():\n                with pos_index_path.open() as file_:\n                    index[pos] = read_index(file_)\n            else:\n                index[pos] = set()\n            pos_exc_path = path / 'wordnet' / '{pos}.exc'.format(pos=pos)\n            if pos_exc_path.exists():\n                with pos_exc_path.open() as file_:\n                    exc[pos] = read_exc(file_)\n            else:\n                exc[pos] = {}\n        if rules is None and (path / 'vocab' / 'lemma_rules.json').exists():\n            with (path / 'vocab' / 'lemma_rules.json').open('r', encoding='utf8') as file_:\n                rules = json.load(file_)\n        elif rules is None:\n            rules = {}\n        return cls(index, exc, rules)\n\n    def __init__(self, index, exceptions, rules):\n        self.index = index\n        self.exc = exceptions\n        self.rules = rules\n\n    def __call__(self, string, univ_pos, morphology=None):\n        if univ_pos == NOUN:\n            univ_pos = 'noun'\n        elif univ_pos == VERB:\n            univ_pos = 'verb'\n        elif univ_pos == ADJ:\n            univ_pos = 'adj'\n        elif univ_pos == PUNCT:\n            univ_pos = 'punct'\n        # See Issue #435 for example of where this logic is requied.\n        if self.is_base_form(univ_pos, morphology):\n            return set([string.lower()])\n        lemmas = lemmatize(string, self.index.get(univ_pos, {}),\n                           self.exc.get(univ_pos, {}),\n                           self.rules.get(univ_pos, []))\n        return lemmas\n\n    def is_base_form(self, univ_pos, morphology=None):\n        '''Check whether we're dealing with an uninflected paradigm, so we can\n        avoid lemmatization entirely.'''\n        morphology = {} if morphology is None else morphology\n        others = [key for key in morphology if key not in (POS, 'number', 'pos', 'verbform')]\n        if univ_pos == 'noun' and morphology.get('number') == 'sing' and not others:\n            return True\n        elif univ_pos == 'verb' and morphology.get('verbform') == 'inf' and not others:\n            return True\n        else:\n            return False\n\n    def noun(self, string, morphology=None):\n        return self(string, 'noun', morphology)\n\n    def verb(self, string, morphology=None):\n        return self(string, 'verb', morphology)\n\n    def adj(self, string, morphology=None):\n        return self(string, 'adj', morphology)\n\n    def punct(self, string, morphology=None):\n        return self(string, 'punct', morphology)\n\n\ndef lemmatize(string, index, exceptions, rules):\n    string = string.lower()\n    forms = []\n    # TODO: Is this correct? See discussion in Issue #435.\n    #if string in index:\n    #    forms.append(string)\n    forms.extend(exceptions.get(string, []))\n    for old, new in rules:\n        if string.endswith(old):\n            form = string[:len(string) - len(old)] + new\n            if form in index or not form.isalpha():\n                forms.append(form)\n    if not forms:\n        forms.append(string)\n    return set(forms)\n\n\ndef read_index(fileobj):\n    index = set()\n    for line in fileobj:\n        if line.startswith(' '):\n            continue\n        pieces = line.split()\n        word = pieces[0]\n        if word.count('_') == 0:\n            index.add(word)\n    return index\n\n\ndef read_exc(fileobj):\n    exceptions = {}\n    for line in fileobj:\n        if line.startswith(' '):\n            continue\n        pieces = line.split()\n        exceptions[pieces[0]] = tuple(pieces[1:])\n    return exceptions\n", "path": "spacy/lemmatizer.py"}]}