source: orange-bioinformatics/_bioinformatics/obiGeneAtlas.py @ 1694:a240c6650b8e

Revision 1694:a240c6650b8e, 23.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 22 months ago (diff)

BUGFIX. Fixed cache version invalidation.

Must close the shelve before reopening using 'create new' flag. Otherwise
a DBPremissionError is raised on windows.

Line 
1"""
2========================================
3Gene Expression Atlas (``obiGeneAtlas``)
4========================================
5
6Interface to Gene Expression Atlas.
7
8`Gene Expression Atlas <http://www.ebi.ac.uk/gxa/>`_ is a curated subset of
9gene expression experiments in Array Express Archive.
10
11.. autofunction:: gene_expression_atlas
12
13.. autofunction:: default_gene_matcher
14
15.. autofunction:: to_taxid
16
17"""
18
19from __future__ import absolute_import
20
21import os, shelve, sys
22from collections import defaultdict, namedtuple
23from contextlib import closing
24
25from Orange.orng import orngServerFiles
26from Orange.utils import serverfiles
27
28from . import obiGene
29
30GeneResults = namedtuple("GeneResults", "id name synonyms expressions")
31ExpressionResults = namedtuple("ExpressionResults", "ef efv up down experiments")
32ExperimentExpression = namedtuple("ExperimentExpression", "accession expression pvalue")
33
34##
35GeneAtlasResult = GeneResults
36AtlasExpressions = ExpressionResults
37AtlasExperiment = ExperimentExpression
38##
39
40CACHE_VERSION = 1
41
42
43def _cache(name="AtlasGeneResult.shelve"):
44    """ Return a open cache instance (a shelve object).
45    """
46    if not os.path.exists(orngServerFiles.localpath("GeneAtlas")):
47        try:
48            os.makedirs(orngServerFiles.localpath("GeneAtlas"))
49        except OSError:
50            pass
51    cache = shelve.open(orngServerFiles.localpath("GeneAtlas", name))
52    if cache.get(name + "__CACHE_VERSION__", None) == CACHE_VERSION:
53        return cache
54    else:
55        cache.close()
56        cache = shelve.open(orngServerFiles.localpath("GeneAtlas", name), "n")
57        cache[name + "__CACHE_VERSION__"] = CACHE_VERSION
58        return cache
59
60
61SLEEP_TIME_MULTIPLIER = 3.0
62
63def gene_expression_atlas(genes, progress_callback=None):
64    """ Return GeneResults instances for genes (genes must be valid ensembl ids).
65    """
66    import time
67    genes = list(genes)
68    result_dict = {}
69    genes_not_cached = []
70    # See which genes are already cached
71    with closing(_cache()) as cache:
72        for gene in genes:
73            if str(gene) in cache:
74                result_dict[gene] = cache[str(gene)]
75            else:
76                genes_not_cached.append(gene)
77   
78    batch_size = 10
79    start = 0
80    res = []
81    while start < len(genes_not_cached):
82        batch = genes_not_cached[start: start + batch_size]
83        start += batch_size
84        start_time = time.time()
85        batch_res = batch_gene_atlas_expression(batch)
86        # Cache the new results.
87        # TODO: handle genes without any results.
88        genes_with_no_results = set(batch) - set(r.id for r in batch_res) 
89        with closing(_cache()) as cache:
90            for atlas_res in batch_res:
91                cache[str(atlas_res.id)] = atlas_res
92                result_dict[atlas_res.id] = atlas_res
93            for g in genes_with_no_results:
94                cache[str(g)] = None
95        res.extend(batch_res)
96        # Sleep
97        if start % (batch_size * 10) == 0:
98            # every 10 batches wait one minute before continuing.
99            time.sleep(60)
100        else:
101            time.sleep(min(20.0, SLEEP_TIME_MULTIPLIER*(time.time() - start_time)))
102           
103        if progress_callback:
104            progress_callback(100.0 * start / len(genes_not_cached))
105   
106    return [result_dict.get(g, None) for g in genes]
107
108   
109def batch_gene_atlas_expression(genes):
110    cond = GenePropertyCondition("Ensgene", "Is", genes)
111    res = run_query(cond, format="json")
112    results = res["results"]
113    results_genes = []
114    for one_result in results:
115        gene = one_result["gene"]
116        id = gene["id"]
117        name = gene["name"]
118        synonyms = gene.get("synonyms", [])
119        expressions = one_result["expressions"]
120        result_expressions = []
121        for expression in expressions:
122            ef = expression["ef"]
123            efv = expression["efv"]
124            up = expression["upExperiments"]
125            down = expression["downExperiments"]
126            experiments = expression["experiments"]
127            result_experiment = []
128            for exp in experiments:
129                if "accession" in exp:
130                    exp_accession = exp["accession"]
131                elif "experimentAccession" in exp:
132                    exp_accession = exp["experimentAccession"]
133                else:
134                    raise KeyError()
135                if "expression" in exp:
136                    updown = exp["expression"]
137                elif "updn" in exp:
138                    updown = exp["updn"]
139                else:
140                    raise KeyError
141                pval = exp["pvalue"]
142                result_experiment.append(ExperimentExpression(exp_accession, updown, pval))
143            result_expressions.append(ExpressionResults(ef, efv, up, down, result_experiment))
144        results_genes.append(GeneResults(id, name, synonyms, result_expressions))
145    return results_genes
146
147
148def default_gene_matcher(organism):
149    """ Return a default gene matcher for organism
150    (targeting Ensembl gene ids).
151   
152    """
153    taxid = to_taxid(organism)
154    matcher = obiGene.matcher([obiGene.GMEnsembl(taxid),
155                               obiGene.GMNCBI(taxid)])
156    matcher.set_targets(obiGene.EnsembleGeneInfo(taxid).keys())
157    return matcher
158
159from Orange.utils import lru_cache
160
161@lru_cache(maxsize=3)
162def _cached_default_gene_matcher(organism): 
163    return default_gene_matcher(organism)
164   
165
166def get_atlas_summary(genes, organism, gene_matcher=None,
167                      progress_callback=None):
168    """ Return 3 dictionaries containing a summary of atlas information
169    about three experimental factors:
170   
171        - Organism Part (OP)
172        - Disease State (DS)
173        - Cell type (CT)
174   
175    Each dictionary contains query genes as keys. Values are dictionaries
176    mapping factor values to a 2-tuple containig the count of up regulated
177    and down regulated experiments.
178   
179    Example ::
180   
181        >>> get_atlas_summary(["RUNX1"], "Homo sapiens")
182        ({u'RUNX1': ...
183       
184    """
185    if gene_matcher is None:
186        gene_matcher = _cached_default_gene_matcher(organism)
187       
188    matched, unmatched = [], []
189    for gene, match in zip(genes, map(gene_matcher.umatch, genes)):
190        if match:
191            matched.append(match)
192        else:
193            unmatched.append(gene)
194    if unmatched:
195        import warnings
196        warnings.warn("Unmatched genes " + "," .join(["%r" % g for g in unmatched]))
197   
198    results = gene_expression_atlas(matched, progress_callback=progress_callback)
199   
200    def collect_ef_summary(result, ef, summary):
201        for exp in result.expressions:
202            if exp.ef == ef:
203                if any([exp.up, exp.down]):
204                    summary[result.name][exp.efv] = (exp.up, exp.down)
205       
206           
207    op, ds, ct = defaultdict(dict), defaultdict(dict), defaultdict(dict)
208    for res in results:
209        if res:
210            collect_ef_summary(res, "organism_part", op)
211            collect_ef_summary(res, "disease_state", ds)
212            collect_ef_summary(res, "cell_type", ct)
213       
214    return dict(op), dict(ds), dict(ct)
215
216def drop_none(iter):
217    """ Drop all ``None`` from the iterator.
218    """
219    for e in iter:
220        if e is not None:
221            yield e
222           
223def construct_atlas_gene_sets(genes, organism, factors=["organism_part",
224                                    "disease_state", "cell_type"],
225                              max_pvalue=1e-5):
226    """ Construct gene sets for atlas experimental factor values in
227    ``factors``.
228    """
229    results = gene_expression_atlas(genes)
230    sets = defaultdict(list)
231   
232    for res in drop_none(results):
233        for exp in res.expressions:
234            if exp.ef not in factors:
235                continue
236            diff_exp = [e for e in exp.experiments \
237                        if e.pvalue <= max_pvalue]
238            if diff_exp:
239                sets[exp.ef, exp.efv].append(res.id)
240
241    organism = "+".join(organism.lower().split())
242    from .obiGeneSets import GeneSets, GeneSet
243   
244    def display_string(name):
245        return name.capitalize().replace("_", " ")
246   
247    gene_sets = []
248    for (ef, efv), genes in sets.items():
249        ef_display = display_string(ef)
250        gs = GeneSet(genes, "Diff. expressed in %s=%s." % (ef_display, efv), id=ef + ":" + efv,
251                     description="Diff. expressed in %s=%s" % (ef_display, efv),
252                     link="http://www.ebi.ac.uk/gxa/qrs?specie_0={organism}&gprop_0=&gnot_0=&gval_0=&fact_1=&fexp_1=UPDOWN&fmex_1=&fval_1=%22{efv}%22+&view=hm".format( \
253                            organism=organism, efv="+".join(efv.lower().split())),
254                     hierarchy=("Tissues", ef_display))
255        gene_sets.append(gs)
256    return GeneSets(gene_sets)
257
258
259# Mapping for common taxids from obiTaxonomy
260TAXID_TO_ORG = {"": "Anopheles gambiae",
261                "3702": "Arabidopsis thaliana",
262                "9913": "Bos taurus",
263                "6239": "Caenorhabditis elegans",
264                "7955": "Danio rerio",
265                "7227": "Drosophila melanogaster",
266                "": "Epstein barr virus",
267                "": "Gallus gallus",
268                "9606": "Homo sapiens",
269                "": "Human cytomegalovirus",
270                "": "Kaposi sarcoma-associated herpesvirus",
271                "10090": "Mus musculus",
272                "10116": "Rattus norvegicus",
273                "4932": "Saccharomyces cerevisiae",
274                "4896": "Schizosaccharomyces pombe",
275                "8355": "Xenopus laevis"
276     }
277
278def to_taxid(name):
279    dd = dict((v, k) for k, v in TAXID_TO_ORG.items())
280    if name in dd:
281        return dd[name]
282    else:
283        from . import obiTaxonomy as tax
284        ids = tax.to_taxid(name, mapTo=TAXID_TO_ORG.keys())
285        if ids:
286            return ids.pop()
287        else:
288            raise ValueError("Unknown organism.")
289
290
291__doc__ += """\
292Low level REST query interface
293------------------------------
294
295Use `query_atlas_simple` for simple querys.
296
297Example (query human genes for experiments in which they are up regulated) ::
298
299    >>> run_simple_query(genes=["SORL1", "PSIP1", "CDKN1C"], regulation="up", organism="Homo sapiens")
300    {u'...
301   
302Or use the `AtlasCondition` subclasses in this module to construct a more
303advanced query and use the `run_query` function.
304
305Example (query human genes annotated to the GO term 'transporter activity'
306that are up regulated in the liver in at least three experiments) ::
307
308    >>> go_cond = GenePropertyCondition("Goterm", "Is", "transporter activity")
309    >>> liver_cond = ExperimentalFactorCondition("Organism_part", "up", 3, "liver")
310    >>> org_cond = OrganismCondition("Homo sapiens")
311    >>> cond_list = ConditionList([go_cond, liver_cond, org_cond])
312    >>> run_query(cond_list)
313    {u'...
314
315"""
316
317import urllib2
318from  StringIO import StringIO
319import json
320from xml.etree.ElementTree import ElementTree
321
322parse_json = json.load
323
324
325def parse_xml(stream):
326    """ Parse an xml stream into an instance of xml.etree.ElementTree.ElementTree.
327    """
328    return ElementTree(file=stream)
329
330
331class GeneExpressionAtlasConenction(object):
332    """ A connection to Gene Expression Atlas database.
333    """
334    DEFAULT_ADDRESS = "http://www.ebi.ac.uk:80/gxa/"
335    DEFAULT_CACHE = orngServerFiles.localpath("GeneAtlas", "GeneAtlasConnectionCache.shelve")
336    def __init__(self, address=None, timeout=30, cache=None):
337        """ Initialize the conenction.
338       
339        :param address: Address of the server.
340        :param timeout: Socket timeout.
341        :param cache : A dict like object to use as a cache.
342       
343        """
344        self.address = address if address is not None else self.DEFAULT_ADDRESS
345        self.timeout = timeout
346        self.cache = cache if cache is not None else self.DEFAULT_CACHE
347   
348    def query(self, condition, format="json", start=None, rows=None, indent=False):
349        url = self.address + "api/vx?" + condition.rest()
350        if start is not None and rows is not None:
351            url += "&start={0}&rows={1}".format(start, rows)
352        url += "&format={0}".format(format)
353        if indent:
354            url += "&indent"
355#        print url
356        if self.cache is not None:
357            return self._query_cached(url, format)
358        else:
359            return urllib2.urlopen(url)
360        return response
361   
362    def _query_cached(self, url, format):
363        if self.cache is not None:
364            with self.open_cache() as cache:
365                cached = url in cache
366           
367            if not cached:
368                response = urllib2.urlopen(url)
369                contents = response.read()
370                # Test if the contents is a valid json or xml string (some
371                # times the stream just stops in the middle, so this makes
372                # sure we don't cache an invalid response
373                # TODO: what about errors (e.g. 'cannot handle the
374                # query in a timely fashion'
375                if format == "json":
376                    parse_json(StringIO(contents))
377                else:
378                    parse_xml(StringIO(contents))
379                   
380                with self.open_cache() as cache:
381                    cache[url] = contents
382            else:
383                with self.open_cache() as cache:
384                    contents = cache[url]
385            return StringIO(contents)
386        else:
387            return urllib2.urlopen(url)
388       
389    def open_cache(self):
390        if isinstance(self.cache, basestring):
391            return closing(shelve.open(self.cache))
392        elif hasattr(self.cache, "close"):
393            return closing(self.cache)
394        elif self.cache is None:
395            return fake_closing({})
396        else:
397            return fake_closing(self.cache)
398       
399       
400from contextlib import contextmanager
401@contextmanager
402def fake_closing(obj):
403    yield obj
404   
405   
406# Names of all Gene Property filter names
407GENE_FILTERS = \
408    ["Name", # Gene name
409     "Goterm", #Gene Ontology Term
410     "Interproterm", #InterPro Term
411     "Disease", #Gene-Disease Assocation
412     "Keyword", #Gene Keyword
413     "Protein", #Protein
414
415     "Dbxref", #Other Database Cross-Refs
416     "Embl", #EMBL-Bank ID
417     "Ensfamily", #Ensembl Family
418     "Ensgene", #Ensembl Gene ID
419
420     "Ensprotein", #Ensembl Protein ID
421     "Enstranscript", #Ensembl Transcript ID
422     "Goid", #Gene Ontology ID
423     "Image", #IMAGE ID
424     "Interproid", #InterPro ID
425     "Locuslink", #Entrez Gene ID
426
427     "Omimid", #OMIM ID
428     "Orf", #ORF
429     "Refseq", #RefSeq ID
430     "Unigene", #UniGene ID
431     "Uniprot", #UniProt Accession
432
433     "Hmdb", #HMDB ID
434     "Chebi", #ChEBI ID
435     "Cas", #CAS
436     "Uniprotmetenz", #Uniprotmetenz
437     "Gene", #Gene Name or Identifier
438     "Synonym", #Gene Synonym
439     ]
440   
441# Valid Gene Property filter qualifiers
442GENE_FILTER_QUALIFIERS =\
443    ["Is",
444     "IsNot"
445     ]
446
447# Organisms in the Atlas
448ATLAS_ORGANISMS = \
449    ["Anopheles gambiae",
450     "Arabidopsis thaliana",
451     "Bos taurus",
452     "Caenorhabditis elegans",
453     "Danio rerio",
454     "Drosophila melanogaster",
455     "Epstein barr virus",
456     "Gallus gallus",
457     "Homo sapiens",
458     "Human cytomegalovirus",
459     "Kaposi sarcoma-associated herpesvirus",
460     "Mus musculus",
461     "Rattus norvegicus",
462     "Saccharomyces cerevisiae",
463     "Schizosaccharomyces pombe",
464#     "Unknown",
465     "Xenopus laevis"
466     ]
467   
468def ef_ontology():
469    """ Return the `EF <http://www.ebi.ac.uk/efo/>`_ (Experimental Factor) ontology
470    """
471    from . import obiOntology
472    from . import orngServerFiles
473    # Should this be in the OBOFoundry (Ontology) domain
474    try:
475        file = open(orngServerFiles.localpath_download("ArrayExpress", "efo.obo"), "rb")
476    except urllib2.HTTPError:
477        file = urllib2.urlopen("http://efo.svn.sourceforge.net/svnroot/efo/trunk/src/efoinobo/efo.obo")
478    return obiOntology.OBOOntology(file)
479
480
481class Condition(object):
482    """ Base class for Gene Expression Atlas query condition
483    """
484    def validate(self):
485        """ Validate condition in a subclass.
486        """
487        raise NotImplementedError
488   
489    def rest(self):
490        """ Return a REST query part in a subclass.
491        """
492        raise NotImplementedError
493   
494   
495class ConditionList(list, Condition):
496    """ A list of AtlasCondition instances.
497    """ 
498    def validate(self):
499        for item in self:
500            item.validate()
501       
502    def rest(self):
503        return "&".join(cond.rest() for cond in self)
504
505
506class GenePropertyCondition(Condition):
507    """ An atlas gene filter condition.
508   
509    :param property: Property of the gene. If None or "" all properties
510        will be searched.
511    :param qualifier: Qualifier can be 'Is' or 'IsNot'
512    :param value: The value to search for.
513   
514    Example ::
515   
516        >>> # Condition on a gene name
517        >>> condition = GenePropertyCondition("Name", "Is", "AS3MT")
518        >>> # Condition on genes from a GO Term
519        >>> condition = GenePropertyCondition("Goterm", "Is", "p53 binding")
520        >>> # Condition on disease association
521        >>> condition = GenePropertyCondition("Disease", "Is", "cancer")
522       
523    """
524    def __init__(self, property, qualifier, value):
525        self.property = property or ""
526        self.qualifier = qualifier
527        if isinstance(value, basestring):
528            self.value = value.replace(" ", "+")
529        elif isinstance(value, list):
530            self.value = "+".join(value)
531        else:
532            raise ValueError(value)
533       
534        self.validate()
535       
536    def validate(self):
537        assert(self.property in GENE_FILTERS + [""])
538        assert(self.qualifier in GENE_FILTER_QUALIFIERS + [""])
539       
540    def rest(self):
541        return "gene{property}{qualifier}={value}".format(**self.__dict__)
542       
543       
544class ExperimentalFactorCondition(Condition):
545    """ An atlas experimental factor filter condition.
546   
547    :param factor: EFO experiamntal factor
548    :param regulation: "up", "down", "updown", "any" or "none"
549    :param n: Minimum number of of experimants with this condition
550    :param value: Experimantal factor value
551   
552    Example ::
553   
554        >>> # Any genes up regulated in at least 3 experiments involving cancer.
555        >>> condition = ExperimentalFactorCondition("", "up", 3, "cancer")
556        >>> # Only genes which are up/down regulated in the heart in at least one experiment.
557        >>> condition = ExperimentalFactorCondition("Organism_part", "updown", 1, "heart")
558       
559    """
560    def __init__(self, factor, regulation, n, value):
561        self.factor = factor
562        self.regulation = regulation
563        self.n = n
564        self.value = value
565        self.validate()
566       
567    def validate(self):
568        # TODO: validate the factor and value
569#        assert(self.factor in ef_ontology())
570        assert(self.regulation in ["up", "down", "updown"])
571       
572    def rest(self):
573        return "{regulation}{n}In{factor}={value}".format(**self.__dict__)
574       
575       
576class OrganismCondition(Condition):
577    """ Condition on organism.
578    """
579    def __init__(self, organism):
580        self.organism = organism
581        self.validate()
582       
583    def validate(self):
584        assert(self.organism in ATLAS_ORGANISMS)
585       
586    def rest(self):
587        return "species={0}".format(self.organism.replace(" ", "+").lower())
588       
589       
590class ExperimentCondition(Condition):
591    """ Condition on experiement
592   
593    :param property: Property of the experiment. If None or "" all properties
594        will be searched.
595    :param qualifier: Qualifier can be 'Has' or 'HasNot'
596    :param value: The value to search for.
597   
598    Example ::
599   
600        >>> # Condition on a experiemnt acession
601        >>> condition = ExperimentCondition("", "", "E-GEOD-24283")
602        >>> # Condition on experiments involving lung
603        >>> condition = ExperimentCondition("Organism_part", "Has", "lung")
604       
605    """
606    EXPERIMENT_FILTER_QUALIFIERS = [
607                "Has",
608                "HasNot"]
609   
610    def __init__(self, property, qualifier, value):
611        self.property = property
612        self.qualifier = qualifier
613        if isinstance(value, basestring):
614            self.value = value.replace(" ", "+")
615        elif isinstance(value, list):
616            self.value = "+".join(value)
617        else:
618            raise ValueError(value)
619       
620        self.validate()
621       
622    def validate(self):
623        # TODO: check to EFO factors
624#        assert(self.property in EXPERIMENT_FILTERS + [""])
625        assert(self.qualifier in self.EXPERIMENT_FILTER_QUALIFIERS + [""])
626       
627    def rest(self):
628        return "experiment{property}{qualifier}={value}".format(**self.__dict__)
629       
630       
631class GeneExpressionAtlasError(Exception):
632    """ An error response from the Atlas server.
633    """
634    pass
635   
636   
637def __check_atlas_error_json(response):
638    if "error" in response:
639        raise GeneExpressionAtlasError(response["error"])
640    return response
641 
642     
643def __check_atlas_error_xml(response):
644    error = response.find("error")
645    if error is not None:
646        raise GeneExpressionAtlasError(error.text)
647    return response
648   
649       
650def run_simple_query(genes=None, regulation=None, organism=None,
651                     condition=None, format="json", start=None,
652                     rows=None):
653    """ A simple Gene Atlas query.
654   
655    :param genes: A list of gene names to search for.
656    :param regulation: Search for experiments in which `genes` are "up",
657        "down", "updown" or "none" regulated. If None all experiments
658        are searched.
659    :param organism: Search experiments for organism. If None all experiments
660        are searched.
661    :param condition: An EFO factor value (e.g. "brain")
662   
663    Example ::
664       
665        >>> run_simple_query(genes=['Pou5f1', 'Dppa3'], organism="Mus musculus")
666        {u'...
667       
668        >>> run_simple_query(genes=['Pou5f1', 'Dppa3'], regulation="up", organism="Mus musculus")
669        {u'...
670       
671        >>> run_simple_query(genes=['Pou5f1', 'Dppa3'], regulation="up", condition="liver", organism="Mus musculus")
672        {u'...
673       
674    """
675    conditions = ConditionList()
676    if genes:
677        conditions.append(GenePropertyCondition("Gene", "Is", genes))
678    if regulation or condition:
679        regulation = "any" if regulation is None else regulation
680        condition = "" if condition is None else condition
681        conditions.append(ExperimentalFactorCondition("", regulation, 1, condition))
682    if organism:
683        conditions.append(OrganismCondition(organism))
684       
685    connection = GeneExpressionAtlasConenction()
686    results = connection.query(conditions, format=format, start=start,
687                               rows=rows)
688    if format == "json":
689        return parse_json(results)
690    else:
691        return parse_xml(results)
692
693"""\
694.. todo:: can this be implemented query_atlas(organism="...", Locuslink="...", Chebi="...", up3InCompound="..." downInEFO="...")
695      Need a full list of accepted factors
696"""
697
698def run_query(condition, format="json", start=None, rows=None, indent=False, connection=None):
699    """ Query Atlas based on a `condition` (instance of :class:`Condition`)
700   
701    Example ::
702       
703        >>> condition1 = GenePropertyCondition("Goterm", "Is", "p53 binding")
704        >>> condition2 = ExperimentalFactorCondition("Organism_part", "up", 3, "heart")
705        >>> condition = ConditionList([condition1, condition2])
706        >>> run_query(condition)
707        {u'...
708       
709    """
710    if connection is None:
711        connection = GeneExpressionAtlasConenction()
712    results = connection.query(condition, format=format, start=start,
713                               rows=rows, indent=indent)
714    if format == "json":
715        response = parse_json(results)
716        return __check_atlas_error_json(response)
717    else:
718        response = parse_xml(results)
719        return __check_atlas_error_xml(response)
720   
721def test():
722    from pprint import pprint   
723    pprint(get_atlas_summary(['Pou5f1', 'Dppa3'], 'Mus musculus'))
724       
725    pprint(get_atlas_summary(['PDLIM5', 'FGFR2' ], 'Homo sapiens'))
726    import doctest 
727    doctest.testmod(optionflags=doctest.ELLIPSIS)
728   
729if __name__ == "__main__":
730    test()
Note: See TracBrowser for help on using the repository browser.