From 6739dc64f49f681ceda9f762c3509e5aeb95553f Mon Sep 17 00:00:00 2001 From: bw2 Date: Mon, 22 May 2017 23:59:58 -0400 Subject: [PATCH 01/19] initial commit --- README.md | 2 - obo_parser.py | 387 +++++++++++++++++++++++++++++++++ tests/__init__.py | 0 tests/data/hpo_subset.obo | 198 +++++++++++++++++ tests/test_other_functions.py | 118 ++++++++++ tests/test_parser_functions.py | 42 ++++ 6 files changed, 745 insertions(+), 2 deletions(-) delete mode 100644 README.md create mode 100644 obo_parser.py create mode 100644 tests/__init__.py create mode 100644 tests/data/hpo_subset.obo create mode 100644 tests/test_other_functions.py create mode 100644 tests/test_parser_functions.py diff --git a/README.md b/README.md deleted file mode 100644 index 9348dd0..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# obo_parser -Parses .obo files such the one containing the Human Phenotype Ontology (HPO) and converts them to a .tsv table which is easier to work with. diff --git a/obo_parser.py b/obo_parser.py new file mode 100644 index 0000000..4212ad6 --- /dev/null +++ b/obo_parser.py @@ -0,0 +1,387 @@ +""" +This module provides utility functions for parsing data in the .obo (Open Biomedical Ontologies) +format and writing it out as a .tsv table for easier analysis. + +The .obo format spec can be found here: +http://owlcollab.github.io/oboformat/doc/GO.format.obo-1_2.html +""" + +import argparse +import collections +import contextlib +import logging +import os +import re +import sys +import tqdm +import urllib + +logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s') +logger = logging.getLogger(__name__) + +# regex used to parse records +TAG_AND_VALUE_REGEX = "(?P[^:]+):(?P[^!]+)" + +# column groups and re-mappings +ONLY_ONE_ALLOWED_PER_STANZA = set(["id", "name", "def", "comment"]) +EXCLUDE_FROM_TSV = set(["consider", "replaced_by", "property_value", "is_obsolete", "is_anonymous"]) +RENAME_COLUMNS = { + 'is_a': 'parent_ids', + 'def': 'definition', +} + + +def convert_obo_to_tsv(input_path, output_path="-", root_id=None, add_category_column=False): + """Main entry point for parsing an .obo file and converting it to a .tsv table. + + Args: + input_path (str): .obo file url or local file path + output_path (str): path where to write the .tsv file. Defaults to "-" which is standard out. + root_id (str): If specified, ignore ontology terms that are not either descendants of the + given id or have this id themselves. For example, 'HP:0000118'. + add_category_column (bool): Whether to add a 'category' column to the output .tsv file + which lists each term's top-level category. A top-level category is a term that's a + direct child of the ontology's root term. + """ + + if output_path is None: + output_path = os.path.basename(input_path).replace(".obo", "") + ".tsv" + + # read in data + logger.info("Parsing %s", input_path) + with _open_input_stream(input_path) as input_stream: + obo_records_dict = parse_obo_format(input_stream) + + # find root term + if root_id is None: + root_id = _compute_root_id(obo_records_dict) + + _confirm_id_is_valid(root_id, obo_records_dict, label="root_id") + + # add 'category' columns to records + if add_category_column: + compute_category_column(obo_records_dict, root_id=root_id) + + # print stats and output .tsv + print_stats(obo_records_dict, input_path) + + if output_path == "-": + write_tsv(obo_records_dict, output_stream=sys.stdout, root_id=root_id) + else: + with open(output_path, "w") as output_stream: + write_tsv(obo_records_dict, output_stream, root_id=root_id) + + logger.info("Done") + + +def parse_obo_format(lines): + """Parses .obo-formatted text. + + Args: + lines (iter): Iterator over lines of text in .obo format. + Returns: + dict: .obo records, keyed by term id. Each record is a dictionary where the keys are tags + such as "id", "name", "is_a", and values are strings (for tags that can only occur once + - such as "id"), or lists (for tags that can appear multiple times per stanza - such as + "xref") + """ + + obo_records_dict = collections.OrderedDict() + current_stanza_type = None + current_record = None + all_tags = set() + + if logger.isEnabledFor(logging.INFO): + lines = tqdm.tqdm(lines, unit=" lines") + + for line in lines: + if line.startswith("["): + current_stanza_type = line.strip("[]\n") + continue + + # skip header lines and stanzas that aren't "Terms" + if current_stanza_type != "Term": + continue + + # remove new-line character and any comments + line = line.rstrip('\n').split("!")[0] + if len(line) == 0: + continue + + match = re.match(TAG_AND_VALUE_REGEX, line) + if not match: + raise ValueError("Unexpected line format: %s" % str(line)) + + tag = match.group("tag") + value = match.group("value").strip() + + if tag == "id": + current_record = collections.defaultdict(list) + obo_records_dict[value] = current_record + + all_tags.add(tag) + if tag in ONLY_ONE_ALLOWED_PER_STANZA: + if tag in current_record: + raise ValueError("More than one '%s' found in %s stanza: %s" % ( + tag, current_stanza_type, ", ".join([current_record[tag], value]))) + + current_record[tag] = value + else: + current_record[tag].append(value) + + # add a 'children' key and list of child ids to all records that have children + _compute_children_column(obo_records_dict) + + return obo_records_dict + + +def print_stats(obo_records_dict, input_path): + """Print various summary stats about the given .obo records. + + Args: + obo_records_dict (dict): data structure returned by parse_obo_format(..) + input_path (str): source path of .obo data. + """ + + if not logger.isEnabledFor(logging.INFO): + return + + tag_counter = collections.defaultdict(int) + value_counter = collections.defaultdict(int) + for term_id, record in obo_records_dict.items(): + for tag, value in record.items(): + tag_counter[tag] += 1 + if isinstance(value, list): + value_counter[tag] += len(value) + + logger.info("Parsed %s terms from %s", len(obo_records_dict), input_path) + total_records = len(obo_records_dict) + for tag, records_with_tag in sorted(tag_counter.items(), key=lambda t: t[1], reverse=True): + percent_with_tag = 100*records_with_tag/float(total_records) if total_records > 0 else 0 + + message = "%(records_with_tag)s out of %(total_records)s (%(percent_with_tag)0.1f%%) " \ + "records have a %(tag)s tag" + if tag in value_counter: + values_per_record = value_counter[tag] / float(records_with_tag) + message += ", and have, on average, %(values_per_record)0.1f values per record." + logger.info(message % locals()) + + +def _compute_root_id(obo_records_dict): + """Finds the top-level term in the heirarchy. + NOTE: this implementation assumes the ontology has a single root term, and doesn't have cycles. + """ + + if not obo_records_dict: + return None + + # start with a random id and walk up the heirarchy to find a term that doesn't have a parent + term_id = obo_records_dict.iterkeys().next() + while True: + parent_ids = obo_records_dict[term_id].get("is_a") + if parent_ids is None or len(parent_ids) == 0: + return term_id + + _confirm_id_is_valid(parent_ids[0], obo_records_dict, label="%s's parent id" % term_id) + + term_id = parent_ids[0] + + +def get_substree(obo_records_dict, root_id, skip_record=None): + """Generates .obo records that are either descendants of the given root_id or the root record + itself. + + Args: + obo_records_dict (dict): data structure returned by parse_obo_format(..) + root_id (str): Only ontology terms that are either descendants of the + given id or have this id themselves are returned. For example, 'HP:0000118'. + skip_record (function): A function which takes a record and returns True if the record (and + it's descendants) should be skipped. + Yields: + dict: .obo records + """ + + _confirm_id_is_valid(root_id, obo_records_dict, label='root_id') + + ids_to_process = collections.deque([root_id]) + processed_ids = set() + while ids_to_process: + next_id = ids_to_process.popleft() + record = obo_records_dict[next_id] + if next_id in processed_ids or (skip_record is not None and skip_record(record)): + continue + + yield record + + processed_ids.add(next_id) + child_ids = record.get('children', []) + ids_to_process.extend(child_ids) + + +def _compute_children_column(obo_records_dict): + """For each record that has child terms, compute a list of child term ids and store it in the + record under a new 'children' attribute. + """ + + for term_id, current_record in obo_records_dict.items(): + if "is_a" not in current_record: + continue + + for parent_id in current_record["is_a"]: + if parent_id not in obo_records_dict: + logger.warn("%s has a parent id %s which is not in the ontology" % ( + term_id, parent_id)) + continue + + parent_record = obo_records_dict[parent_id] + if 'children' not in parent_record: + parent_record['children'] = [] + + parent_record['children'].append(term_id) + + +def compute_category_column( + obo_records_dict, + root_id, + add_category_id_column=True, + add_category_name_column=True): + """Adds a "category_id" and/or "category_name" column to each record that's a descendant of the + root term. + + Args: + obo_records_dict (dict): data structure returned by parse_obo_format(..) + root_id (str): Only ontology terms that are either descendants of the + given id or have this id themselves are returned. For example, 'HP:0000118'. + add_category_id_column (bool): Whether to add a "category_id" to each record. + add_category_name_column (bool): Whether to add a "category_name" to each record. + """ + + _confirm_id_is_valid(root_id, obo_records_dict, label='root_id') + + root_record = obo_records_dict[root_id] + root_child_ids = root_record.get('children', []) + + if not root_child_ids: + logger.warn("root term has no child terms") + return + + for category_id in root_child_ids: + category_name = obo_records_dict[category_id].get("name", "") + + def is_category_already_assigned(record): + return 'category_id' in record or 'category_name' in record + + category_subtree = get_substree( + obo_records_dict, + root_id=category_id, + skip_record=is_category_already_assigned + ) + + for record in category_subtree: + if add_category_id_column: + record['category_id'] = category_id + if add_category_name_column: + record['category_name'] = category_name + + +def _open_input_stream(path): + """Returns an open stream for iterating over lines in the given path. + + Args: + path (str): url or local file path + Return: + iter: iterator over file handle + """ + if not isinstance(path, (str, unicode)): + raise ValueError("Unexpected path type: %s" % str(path)) + + is_url = path.startswith("http") + if is_url: + line_iterator = contextlib.closing(urllib.urlopen(path)) + else: + if not os.path.isfile(path): + raise ValueError("File not found: %s" % path) + + line_iterator = open(path) + + return line_iterator + + +def _compute_tsv_header(obo_records): + """Compute .tsv file header as a list of strings containing all tags in the given obo_records + + Args: + obo_records (iter): iterator over .obo records + """ + all_tags = set() + for record in obo_records: + for tag in record.keys(): + all_tags.add(tag) + + header = ['id', 'name'] + other_columns = sorted(list(all_tags - set(EXCLUDE_FROM_TSV) - set(header))) + header.extend(other_columns) + + return header + + +def write_tsv(obo_records_dict, output_stream, root_id=None, separator=", "): + """Write obo_records_dict to the given output_stream. + + Args: + obo_records_dict (dict): data structure returned by parse_obo_format(..) + output_stream (file): output stream where to write the .tsv file + root_id (str): Only ontology terms that are either descendants of the + given id or have this id themselves are returned. For example, 'HP:0000118'. + separator (str): separator for concatenating multiple values in a single column + """ + + header = _compute_tsv_header(obo_records_dict.itervalues()) + output_stream.write("\t".join([RENAME_COLUMNS.get(column, column) for column in header])) + output_stream.write("\n") + for record in get_substree(obo_records_dict, root_id): + row = [] + for tag in header: + value = record.get(tag) + if value is None: + row.append("") + elif isinstance(value, list): + row.append(separator.join(map(str, value))) + else: + row.append(str(value)) + output_stream.write("\t".join(row)) + output_stream.write("\n") + + +def _confirm_id_is_valid(term_id, obo_records_dict, label="id"): + """Raises an exception if the given term id doesn't exist in the given obo_records_dict.""" + + if term_id not in obo_records_dict: + raise ValueError("%s '%s' not found in ontology" % (label, term_id)) + + +if __name__ == "__main__": + p = argparse.ArgumentParser(description="Parse an .obo file and write out a .tsv table") + p.add_argument("-o", "--output-path", help="output .tsv file path. Defaults to standard out.", + default="-") + p.add_argument("-r", "--root-id", help="If specified, ignore ontology terms that are not " + "either descendants of the given id or have this id themselves. For example: 'HP:0000118'.") + p.add_argument("-c", "--add-category-column", action="store_true", help="add a 'category' " + "column to the output .tsv file which lists each term's top-level category. A top-level " + "category is a term that's a direct child of the ontology's root term.") + p.add_argument("input_path", help=".obo file url or local file path. For example: " + "http://purl.obolibrary.org/obo/hp.obo") + p.add_argument("-v", "--verbose", action="store_true", help="Print stats and other info") + args = p.parse_args() + + if args.verbose: + logger.setLevel(logging.INFO) + else: + logger.setLevel(logging.WARN) + + convert_obo_to_tsv( + args.input_path, + output_path=args.output_path, + root_id=args.root_id, + add_category_column=args.add_category_column, + ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/data/hpo_subset.obo b/tests/data/hpo_subset.obo new file mode 100644 index 0000000..6d43733 --- /dev/null +++ b/tests/data/hpo_subset.obo @@ -0,0 +1,198 @@ +format-version: 1.2 +data-version: releases/2017-04-13 +saved-by: Peter Robinson, Sebastian Koehler, Sandra Doelken, Chris Mungall, Melissa Haendel, Nicole Vasilevsky, Monarch Initiative, et al. +subsetdef: hposlim_core "Core clinical terminology" +subsetdef: secondary_consequence "Consequence of a disorder in another organ system." +synonymtypedef: HP:0045076 "UK spelling" +synonymtypedef: HP:0045077 "abbreviation" +synonymtypedef: HP:0045078 "plural form" +synonymtypedef: layperson "layperson term" +default-namespace: human_phenotype +ontology: hp +property_value: http://purl.org/dc/elements/1.1/contributor "Chris Mungall" xsd:string +property_value: http://purl.org/dc/elements/1.1/contributor "Courtney Hum" xsd:string +property_value: http://purl.org/dc/elements/1.1/contributor "Joie Davis" xsd:string +property_value: http://purl.org/dc/elements/1.1/contributor "Mark Engelstad" xsd:string +property_value: http://purl.org/dc/elements/1.1/contributor "Melissa Haendel" xsd:string +property_value: http://purl.org/dc/elements/1.1/contributor "Nicole Vasilevsky" xsd:string +property_value: http://purl.org/dc/elements/1.1/contributor "Sandra Doelken" xsd:string +property_value: http://purl.org/dc/elements/1.1/creator "Peter N Robinson" xsd:string +property_value: http://purl.org/dc/elements/1.1/creator "Sebastian Koehler" xsd:string +property_value: http://purl.org/dc/elements/1.1/creator "The Human Phenotype Ontology Consortium" xsd:string +property_value: http://purl.org/dc/elements/1.1/creator "The Monarch Initiative" xsd:string +property_value: http://purl.org/dc/elements/1.1/license "see http://www.human-phenotype-ontology.org" xsd:string +property_value: http://purl.org/dc/elements/1.1/rights "Peter Robinson, Sebastian Koehler, The Human Phenotype Ontology Consortium, and The Monarch Initiative" xsd:string +property_value: http://purl.org/dc/elements/1.1/subject "Phenotypic abnormalities encountered in human disease" xsd:string +owl-axioms: Prefix(owl:=)\nPrefix(rdf:=)\nPrefix(xml:=)\nPrefix(xsd:=)\nPrefix(rdfs:=)\n\n\nOntology(\nAnnotationAssertion( \"\")\nAnnotationAssertion( \"\")\nAnnotationAssertion( \"\")\nAnnotationAssertion( \"\")\nAnnotationAssertion(rdfs:comment \"\")\nAnnotationAssertion( \"\"^^xsd:string)\n) +logical-definition-view-relation: has_part + +[Term] +id: HP:0000001 +name: All +comment: Root of all terms in the Human Phenotype Ontology. +xref: UMLS:C0444868 + +[Term] +id: HP:0000118 +name: Phenotypic abnormality +def: "A phenotypic abnormality." [HPO:probinson] +comment: This is the root of the phenotypic abnormality subontology of the HPO. +synonym: "Organ abnormality" EXACT [] +xref: UMLS:C4021819 +is_a: HP:0000001 ! All + +[Term] +id: HP:0012374 +name: Abnormality of the globe +def: "An anomaly of the eyeball." [HPO:probinson] +comment: This term is used to separate anomalies of the eye proper from the ocular adnexa such as the eyelid and the tear glands. +subset: hposlim_core +xref: UMLS:C4022923 +is_a: HP:0012372 ! Abnormal eye morphology +created_by: peter +creation_date: 2013-10-13T03:50:35Z + +[Term] +id: HP:0012372 +name: Abnormal eye morphology +def: "A structural anomaly of the eye." [HPO:probinson] +synonym: "Abnormal eye morphology" EXACT layperson [] +synonym: "Abnormally shaped eye" EXACT layperson [orcid.org/0000-0001-5208-3432] +xref: UMLS:C4022925 +is_a: HP:0000478 ! Abnormality of the eye +created_by: peter +creation_date: 2013-10-13T03:44:43Z + +[Term] +id: HP:0004329 +name: Abnormality of the posterior segment of the globe +comment: The posterior segment comprises the anterior hyaloid membrane and all of the optical structures behind it: the vitreous humor, retina, choroid, and optic nerve. +synonym: "Abnormality of the posterior segment of the eye" EXACT [] +synonym: "Abnormality of the posterior segment of the eyeball" EXACT [] +xref: UMLS:C4025354 +is_a: HP:0012374 ! Abnormality of the globe +created_by: peter +creation_date: 2008-02-27T04:25:00Z + +[Term] +id: HP:0001098 +name: Abnormality of the fundus +xref: UMLS:C4025804 +is_a: HP:0004329 ! Abnormality of the posterior segment of the globe + +[Term] +id: HP:0000478 +name: Abnormality of the eye +def: "Any abnormality of the eye, including location, spacing, and intraocular abnormalities." [HPO:probinson] +subset: hposlim_core +synonym: "Abnormal eye" EXACT layperson [HPO:skoehler] +synonym: "Abnormality of the eye" EXACT layperson [] +synonym: "Eye disease" RELATED layperson [] +xref: MSH:D005124 +xref: MSH:D005128 +xref: SNOMEDCT_US:19416009 +xref: SNOMEDCT_US:371405004 +xref: SNOMEDCT_US:371409005 +xref: UMLS:C0015393 +xref: UMLS:C0015397 +is_a: HP:0000118 ! Phenotypic abnormality + +[Term] +id: HP:0000479 +name: Abnormality of the retina +def: "An abnormality of the retina." [HPO:probinson] +subset: hposlim_core +synonym: "Abnormal retina" EXACT [HPO:skoehler] +synonym: "Anomaly of the retina" EXACT [] +synonym: "Retinal disease" RELATED [] +xref: MSH:D012164 +xref: SNOMEDCT_US:29555009 +xref: UMLS:C0035300 +xref: UMLS:C0035309 +is_a: HP:0001098 ! Abnormality of the fundus + +[Term] +id: HP:0000480 +name: Retinal coloboma +def: "A notch or cleft of the retina." [HPO:probinson] +subset: hposlim_core +xref: SNOMEDCT_US:39302008 +xref: UMLS:C0240896 +is_a: HP:0000479 ! Abnormality of the retina +is_a: HP:0000589 ! Coloboma + +[Term] +id: HP:0000589 +name: Coloboma +alt_id: HP:0007767 +alt_id: HP:0007995 +def: "A developmental defect characterized by a cleft of some portion of the eye or ocular adnexa." [HPO:probinson] +synonym: "Ocular coloboma" EXACT [] +synonym: "Ocular colobomas" EXACT [] +xref: MSH:D003103 +xref: SNOMEDCT_US:92828000 +xref: SNOMEDCT_US:93390002 +xref: UMLS:C0009363 +is_a: HP:0000315 ! Abnormality of the orbital region + +[Term] +id: HP:0000315 +name: Abnormality of the orbital region +alt_id: HP:0000284 +synonym: "Abnormality of the eye region" EXACT layperson [orcid.org/0000-0001-5889-4463] +synonym: "Abnormality of the region around the eyes" EXACT layperson [orcid.org/0000-0001-5889-4463] +synonym: "Anomaly of the orbital region of the face" NARROW [orcid.org/0000-0001-5889-4463] +synonym: "Deformity of the orbital region of the face" NARROW [orcid.org/0000-0001-5889-4463] +synonym: "Malformation of the orbital region of the face" NARROW [orcid.org/0000-0001-5889-4463] +xref: UMLS:C4025863 +is_a: HP:0000271 ! Abnormality of the face + +[Term] +id: HP:0000271 +name: Abnormality of the face +def: "An abnormality of the face." [HPO:probinson] +subset: hposlim_core +synonym: "Abnormal face" EXACT layperson [HPO:skoehler] +synonym: "Abnormality of the countenance" BROAD [orcid.org/0000-0001-5889-4463] +synonym: "Abnormality of the face" EXACT layperson [] +synonym: "Abnormality of the physiognomy" BROAD [orcid.org/0000-0001-5889-4463] +synonym: "Abnormality of the visage" BROAD [orcid.org/0000-0001-5889-4463] +synonym: "Anomaly of face" RELATED [orcid.org/0000-0001-5889-4463] +synonym: "Anomaly of the face" RELATED [orcid.org/0000-0001-5889-4463] +synonym: "Disorder of face" BROAD layperson [orcid.org/0000-0001-5889-4463] +synonym: "Disorder of the face" NARROW layperson [orcid.org/0000-0001-5889-4463] +synonym: "Facial abnormality" EXACT layperson [orcid.org/0000-0001-5889-4463] +synonym: "Facial anomaly" RELATED [orcid.org/0000-0001-5889-4463] +xref: SNOMEDCT_US:118930001 +xref: SNOMEDCT_US:32003007 +xref: SNOMEDCT_US:398206004 +xref: SNOMEDCT_US:398302004 +xref: UMLS:C0266617 +xref: UMLS:C1290857 +xref: UMLS:C4025871 +is_a: HP:0000234 ! Abnormality of the head + +[Term] +id: HP:0000234 +name: Abnormality of the head +def: "An abnormality of the head." [HPO:probinson] +synonym: "Abnormal head" EXACT layperson [HPO:skoehler] +synonym: "Abnormality of the head" EXACT layperson [] +synonym: "Head abnormality" EXACT layperson [] +xref: UMLS:C4021812 +is_a: HP:0000152 ! Abnormality of head or neck + +[Term] +id: HP:0000152 +name: Abnormality of head or neck +def: "An abnormality of head and neck." [HPO:probinson] +synonym: "Abnormality of head or neck" EXACT layperson [] +synonym: "Head and neck abnormality" EXACT layperson [] +xref: UMLS:C4021817 +is_a: HP:0000118 ! Phenotypic abnormality + +[Term] +id: HP:0007808 +name: Bilateral retinal coloboma +xref: UMLS:C4024797 +is_a: HP:0000480 ! Retinal coloboma diff --git a/tests/test_other_functions.py b/tests/test_other_functions.py new file mode 100644 index 0000000..e8cd323 --- /dev/null +++ b/tests/test_other_functions.py @@ -0,0 +1,118 @@ +import logging +import StringIO +import os +import unittest + + + +from obo_parser import _open_input_stream, parse_obo_format, _compute_tsv_header, \ + compute_category_column, _compute_root_id, get_substree, \ + _confirm_id_is_valid, print_stats, write_tsv, logger + +OBO_FILE_PATH = os.path.join(os.path.dirname(__file__), "data/hpo_subset.obo") + + +class ParserTests(unittest.TestCase): + + def setUp(self): + with _open_input_stream(OBO_FILE_PATH) as input_stream: + self.obo_records_dict = parse_obo_format(input_stream) + + def test_compute_root_id(self): + self.obo_records_dict + + def test_compute_tsv_header(self): + self.assertListEqual(_compute_tsv_header([]), ['id', 'name']) + + self.assertListEqual(_compute_tsv_header(self.obo_records_dict.itervalues()), [ + 'id', 'name', 'alt_id', 'comment', 'created_by', 'creation_date', 'def', 'is_a', + 'subset', 'synonym', 'xref' + ]) + + def test_compute_children_column(self): + self.assertTrue('children' in self.obo_records_dict['HP:0000118']) + self.assertTrue('children' in self.obo_records_dict['HP:0000480']) + self.assertTrue('children' in self.obo_records_dict['HP:0000479']) + + self.assertListEqual( + self.obo_records_dict['HP:0000118']['children'], + ['HP:0000478', 'HP:0000152'] + ) + + def test_compute_category_column(self): + compute_category_column(self.obo_records_dict, root_id='HP:0000118') + + self.assertTrue('category_id' not in self.obo_records_dict['HP:0000118']) + self.assertTrue('category_id' in self.obo_records_dict['HP:0000480']) + self.assertTrue('category_id' in self.obo_records_dict['HP:0000479']) + self.assertTrue('category_id' in self.obo_records_dict['HP:0007808']) + + self.assertEqual('HP:0000478', self.obo_records_dict['HP:0000480']['category_id']) + self.assertEqual('HP:0000478', self.obo_records_dict['HP:0007808']['category_id']) + self.assertEqual('HP:0000478', self.obo_records_dict['HP:0007808']['category_id']) + self.assertEqual('HP:0000478', self.obo_records_dict['HP:0000478']['category_id']) + self.assertEqual('HP:0000478', self.obo_records_dict['HP:0000479']['category_id']) + + self.assertEqual('HP:0000152', self.obo_records_dict['HP:0000152']['category_id']) + self.assertEqual('HP:0000152', self.obo_records_dict['HP:0000234']['category_id']) + self.assertEqual('HP:0000152', self.obo_records_dict['HP:0000271']['category_id']) + + def test_compute_tsv_header(self): + self.assertListEqual(_compute_tsv_header([]), ['id', 'name']) + self.assertListEqual(_compute_tsv_header(self.obo_records_dict.itervalues()), [ + 'id', 'name', 'alt_id', 'children', + 'comment', 'created_by', 'creation_date', 'def', 'is_a', 'subset', 'synonym', 'xref' + ]) + + compute_category_column(self.obo_records_dict, root_id='HP:0000118') + + self.assertListEqual(_compute_tsv_header(self.obo_records_dict.itervalues()), [ + 'id', 'name', 'alt_id', 'category_id', 'category_name', 'children', + 'comment', 'created_by', 'creation_date', 'def', 'is_a', 'subset', 'synonym', 'xref' + ]) + + def test_print_stats(self): + # just test that the code runs without crashing + logger.setLevel(logging.INFO) + + print_stats({}, 'input_path.obo') + + print_stats(self.obo_records_dict, 'input_path.obo') + + def test_get_subtree(self): + subtree = { + record['id']: record for record in get_substree(self.obo_records_dict, 'HP:0000118') + } + self.assertFalse('HP:000001' in subtree) + self.assertTrue('HP:0000118' in subtree) + self.assertTrue('HP:0000118' in subtree) + + def test_compute_root_id(self): + root_id = _compute_root_id(self.obo_records_dict) + self.assertEqual('HP:0000001', root_id) + + for root_id in ['HP:0000118', 'HP:0000479']: + subtree = { + record['id']: record for record in get_substree(self.obo_records_dict, root_id) + } + + # unlink subtree from parent tree + subtree[root_id]['is_a'] = None + + computed_root_id = _compute_root_id(subtree) + self.assertEqual(root_id, computed_root_id) + + def test_confirm_id_is_valid(self): + self.assertRaises(lambda: _confirm_id_is_valid('HP:000ABC', self.obo_records_dict)) + + def test_write_tsv(self): + output_stream = StringIO.StringIO() + + write_tsv(self.obo_records_dict, output_stream, root_id="HP:0000480") + + lines = output_stream.getvalue().rstrip('\n').split('\n') + self.assertEqual(3, len(lines)) + self.assertEqual(lines[0], "id name alt_id children comment created_by creation_date definition parent_ids subset synonym xref") + self.assertEqual(lines[1], 'HP:0000480 Retinal coloboma HP:0007808 "A notch or cleft of the retina." [HPO:probinson] HP:0000479, HP:0000589 hposlim_core SNOMEDCT_US:39302008, UMLS:C0240896') + self.assertEqual(lines[2], "HP:0007808 Bilateral retinal coloboma HP:0000480 UMLS:C4024797") + diff --git a/tests/test_parser_functions.py b/tests/test_parser_functions.py new file mode 100644 index 0000000..b8769cf --- /dev/null +++ b/tests/test_parser_functions.py @@ -0,0 +1,42 @@ +import os +import unittest + +from obo_parser import _open_input_stream, parse_obo_format + +OBO_FILE_PATH = os.path.join(os.path.dirname(__file__), "data/hpo_subset.obo") + + +class ParserTests(unittest.TestCase): + + def test_open_input_stream(self): + self.assertRaises(ValueError, lambda: _open_input_stream(None)) + self.assertRaises(ValueError, lambda: _open_input_stream("dir/missing_file.obo")) + + with _open_input_stream(OBO_FILE_PATH) as input_stream: + content = input_stream.read() + file_size = len(content) + self.assertGreater(file_size, 2000) + + def test_parse_obo_format(self): + with _open_input_stream(OBO_FILE_PATH) as input_stream: + obo_records_dict = parse_obo_format(input_stream) + + self.assertListEqual(list(obo_records_dict.keys()), [ + 'HP:0000001', 'HP:0000118', 'HP:0012374', 'HP:0012372', 'HP:0004329', + 'HP:0001098', 'HP:0000478', 'HP:0000479', 'HP:0000480', 'HP:0000589', + 'HP:0000315', 'HP:0000271', 'HP:0000234', 'HP:0000152', 'HP:0007808', + ]) + + self.assertEqual(obo_records_dict['HP:0000480'].get('name'), "Retinal coloboma") + self.assertEqual(obo_records_dict['HP:0000118'].get('name'), "Phenotypic abnormality") + self.assertEqual(obo_records_dict['HP:0000234'].get('name'), "Abnormality of the head") + self.assertEqual(obo_records_dict['HP:0000152'].get('name'), "Abnormality of head or neck") + self.assertEqual(obo_records_dict['HP:0007808'].get('name'), "Bilateral retinal coloboma") + + self.assertEqual( + obo_records_dict['HP:0000480'].get('def'), + '"A notch or cleft of the retina." [HPO:probinson]') + + self.assertListEqual( + obo_records_dict['HP:0000480'].get('is_a'), + ['HP:0000479', 'HP:0000589']) From 3d81a08026af107a9e481f5f7a6cf0842c14b62f Mon Sep 17 00:00:00 2001 From: bw2 Date: Tue, 23 May 2017 06:53:31 -0400 Subject: [PATCH 02/19] removed unused var and unnecessary call to .keys() --- obo_parser.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/obo_parser.py b/obo_parser.py index 4212ad6..7db9d9c 100644 --- a/obo_parser.py +++ b/obo_parser.py @@ -89,8 +89,6 @@ def parse_obo_format(lines): obo_records_dict = collections.OrderedDict() current_stanza_type = None current_record = None - all_tags = set() - if logger.isEnabledFor(logging.INFO): lines = tqdm.tqdm(lines, unit=" lines") @@ -119,7 +117,6 @@ def parse_obo_format(lines): current_record = collections.defaultdict(list) obo_records_dict[value] = current_record - all_tags.add(tag) if tag in ONLY_ONE_ALLOWED_PER_STANZA: if tag in current_record: raise ValueError("More than one '%s' found in %s stanza: %s" % ( @@ -315,7 +312,7 @@ def _compute_tsv_header(obo_records): """ all_tags = set() for record in obo_records: - for tag in record.keys(): + for tag in record: all_tags.add(tag) header = ['id', 'name'] From 74f4ad2a58a29980d831ff8c864a3ca414d45dec Mon Sep 17 00:00:00 2001 From: bw2 Date: Tue, 23 May 2017 06:55:18 -0400 Subject: [PATCH 03/19] files for turning this into a PyPI package --- .gitignore | 105 +++-------------------------------------------- .travis.yaml | 11 +++++ README.rst | 6 +++ __init__.py | 0 requirements.txt | 1 + setup.py | 67 ++++++++++++++++++++++++++++++ 6 files changed, 90 insertions(+), 100 deletions(-) create mode 100644 .travis.yaml create mode 100644 README.rst create mode 100644 __init__.py create mode 100644 requirements.txt create mode 100644 setup.py diff --git a/.gitignore b/.gitignore index 7bbc71c..0338b58 100644 --- a/.gitignore +++ b/.gitignore @@ -1,101 +1,6 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class +.idea +*.pyc +*.egg-info -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# dotenv -.env - -# virtualenv -.venv -venv/ -ENV/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ +htmlcov +.coverage \ No newline at end of file diff --git a/.travis.yaml b/.travis.yaml new file mode 100644 index 0000000..085e045 --- /dev/null +++ b/.travis.yaml @@ -0,0 +1,11 @@ +language: python +python: + - "2.7" + - "3.2" + - "3.3" + - "3.4" + - "3.5" + - "3.6" +install: "pip install -r requirements.txt" +# command to run tests +script: python setup.py test diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..032241f --- /dev/null +++ b/README.rst @@ -0,0 +1,6 @@ +Parses .obo files such the one containing the Human Phenotype Ontology (HPO) and converts them to a .tsv table which is easier to work with. + + + + + diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d167fc6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +tqdm==4.11.2 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..017ba74 --- /dev/null +++ b/setup.py @@ -0,0 +1,67 @@ +import glob +import logging +import os +import sys + + +try: + from setuptools import setup +except ImportError: + print("WARNING: setuptools not installed. Will try using distutils instead..") + from distutils.core import setup + + +command = sys.argv[-1] +if command == 'publish': + os.system('python setup.py sdist upload') + sys.exit() +elif command == "coverage": + try: + import coverage + except: + sys.exit("coverage.py not installed (pip install --user coverage)") + setup_py_path = os.path.abspath(__file__) + os.system('coverage run -m unittest discover') + os.system('coverage html') + os.system('open htmlcov/index.html') + print("Done computing coverage") + sys.exit() + +long_description = '' +if command not in ['test', 'coverage']: + long_description = open('README.rst').read() + +setup( + name='obo_parser', + version="0.9", + description='.obo format parser', + long_description=long_description, + author='Ben Weisburd', + author_email='weisburd@broadinstitute.org', + url='https://github.com/macarthur-lab/igv_utils', + py_modules=['igv_api'], + include_package_data=True, + zip_safe=False, + install_requires=[ + 'tqdm', + ], + license="MIT", + keywords='obo, bioinformatics, parser', + classifiers=[ + 'Development Status :: 4 - Beta', + 'License :: OSI Approved :: MIT License', + 'Natural Language :: English', + "Programming Language :: Python :: 2", + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + ], + + test_suite='tests', +) From 04445f79a786c73f4c39047556d63cc3c7604b6f Mon Sep 17 00:00:00 2001 From: bw2 Date: Tue, 23 May 2017 06:59:38 -0400 Subject: [PATCH 04/19] Add build status --- README.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 032241f..ba34e4a 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,4 @@ Parses .obo files such the one containing the Human Phenotype Ontology (HPO) and converts them to a .tsv table which is easier to work with. - - - - +.. image:: https://travis-ci.org/macarthur-lab/obo_parser.svg?branch=master + :target: https://travis-ci.org/macarthur-lab/obo_parser From 6285c843913d7befe237c6da5269ac1c5927e993 Mon Sep 17 00:00:00 2001 From: bw2 Date: Tue, 23 May 2017 07:03:04 -0400 Subject: [PATCH 05/19] renamed to match TravisCI spec --- .travis.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e95b9a9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: python +python: + - "2.7" + - "3.2" + - "3.3" + - "3.4" + - "3.5" + - "3.6" +install: "pip install -r requirements.txt" + +# command to run tests +script: python setup.py test From bfa332f064469316634a52a64d8943198746a4f5 Mon Sep 17 00:00:00 2001 From: bw2 Date: Tue, 23 May 2017 07:03:18 -0400 Subject: [PATCH 06/19] renamed to match TravisCI spec --- .travis.yaml | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 .travis.yaml diff --git a/.travis.yaml b/.travis.yaml deleted file mode 100644 index 085e045..0000000 --- a/.travis.yaml +++ /dev/null @@ -1,11 +0,0 @@ -language: python -python: - - "2.7" - - "3.2" - - "3.3" - - "3.4" - - "3.5" - - "3.6" -install: "pip install -r requirements.txt" -# command to run tests -script: python setup.py test From aac2546e2e6f1aed50606a52643aa504f0d034b0 Mon Sep 17 00:00:00 2001 From: bw2 Date: Tue, 23 May 2017 07:22:38 -0400 Subject: [PATCH 07/19] changes to make code work on python3 as well as python2 --- obo_parser.py | 9 ++++++--- tests/test_other_functions.py | 18 ++++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/obo_parser.py b/obo_parser.py index 7db9d9c..f372d71 100644 --- a/obo_parser.py +++ b/obo_parser.py @@ -16,6 +16,9 @@ import tqdm import urllib +from builtins import dict +from builtins import str + logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s') logger = logging.getLogger(__name__) @@ -173,7 +176,7 @@ def _compute_root_id(obo_records_dict): return None # start with a random id and walk up the heirarchy to find a term that doesn't have a parent - term_id = obo_records_dict.iterkeys().next() + term_id = next(iter(obo_records_dict)) while True: parent_ids = obo_records_dict[term_id].get("is_a") if parent_ids is None or len(parent_ids) == 0: @@ -289,7 +292,7 @@ def _open_input_stream(path): Return: iter: iterator over file handle """ - if not isinstance(path, (str, unicode)): + if not isinstance(path, str): raise ValueError("Unexpected path type: %s" % str(path)) is_url = path.startswith("http") @@ -333,7 +336,7 @@ def write_tsv(obo_records_dict, output_stream, root_id=None, separator=", "): separator (str): separator for concatenating multiple values in a single column """ - header = _compute_tsv_header(obo_records_dict.itervalues()) + header = _compute_tsv_header(obo_records_dict.values()) output_stream.write("\t".join([RENAME_COLUMNS.get(column, column) for column in header])) output_stream.write("\n") for record in get_substree(obo_records_dict, root_id): diff --git a/tests/test_other_functions.py b/tests/test_other_functions.py index e8cd323..9947528 100644 --- a/tests/test_other_functions.py +++ b/tests/test_other_functions.py @@ -1,8 +1,14 @@ import logging -import StringIO import os +import sys import unittest +from builtins import dict + +if sys.version_info > (3, 0): + from io import StringIO +else: + from StringIO import StringIO from obo_parser import _open_input_stream, parse_obo_format, _compute_tsv_header, \ @@ -24,7 +30,7 @@ def test_compute_root_id(self): def test_compute_tsv_header(self): self.assertListEqual(_compute_tsv_header([]), ['id', 'name']) - self.assertListEqual(_compute_tsv_header(self.obo_records_dict.itervalues()), [ + self.assertListEqual(_compute_tsv_header(self.obo_records_dict.values()), [ 'id', 'name', 'alt_id', 'comment', 'created_by', 'creation_date', 'def', 'is_a', 'subset', 'synonym', 'xref' ]) @@ -59,14 +65,14 @@ def test_compute_category_column(self): def test_compute_tsv_header(self): self.assertListEqual(_compute_tsv_header([]), ['id', 'name']) - self.assertListEqual(_compute_tsv_header(self.obo_records_dict.itervalues()), [ + self.assertListEqual(_compute_tsv_header(self.obo_records_dict.values()), [ 'id', 'name', 'alt_id', 'children', 'comment', 'created_by', 'creation_date', 'def', 'is_a', 'subset', 'synonym', 'xref' ]) compute_category_column(self.obo_records_dict, root_id='HP:0000118') - self.assertListEqual(_compute_tsv_header(self.obo_records_dict.itervalues()), [ + self.assertListEqual(_compute_tsv_header(self.obo_records_dict.values()), [ 'id', 'name', 'alt_id', 'category_id', 'category_name', 'children', 'comment', 'created_by', 'creation_date', 'def', 'is_a', 'subset', 'synonym', 'xref' ]) @@ -103,10 +109,10 @@ def test_compute_root_id(self): self.assertEqual(root_id, computed_root_id) def test_confirm_id_is_valid(self): - self.assertRaises(lambda: _confirm_id_is_valid('HP:000ABC', self.obo_records_dict)) + self.assertRaises(ValueError, lambda: _confirm_id_is_valid('HP:000ABC', self.obo_records_dict)) def test_write_tsv(self): - output_stream = StringIO.StringIO() + output_stream = StringIO() write_tsv(self.obo_records_dict, output_stream, root_id="HP:0000480") From a5f6ea9324c4fa6b0dd28c536be2844e32e24cb9 Mon Sep 17 00:00:00 2001 From: bw2 Date: Tue, 23 May 2017 07:33:20 -0400 Subject: [PATCH 08/19] added pypy to tested version --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index e95b9a9..a8f58aa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: python + python: - "2.7" - "3.2" @@ -6,6 +7,10 @@ python: - "3.4" - "3.5" - "3.6" + - "pypy" # PyPy2 2.5.0 + - "pypy3" # Pypy3 2.4.0 + - "pypy-5.3.1" + install: "pip install -r requirements.txt" # command to run tests From 153ec5dd6220e308da56d73eae92c646df2909d7 Mon Sep 17 00:00:00 2001 From: bw2 Date: Tue, 23 May 2017 07:33:38 -0400 Subject: [PATCH 09/19] fixed type-checking for string/unicode --- obo_parser.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/obo_parser.py b/obo_parser.py index f372d71..f8649b4 100644 --- a/obo_parser.py +++ b/obo_parser.py @@ -17,7 +17,8 @@ import urllib from builtins import dict -from builtins import str +if sys.version_info > (3, 0): + basestring = str logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s') logger = logging.getLogger(__name__) @@ -292,8 +293,8 @@ def _open_input_stream(path): Return: iter: iterator over file handle """ - if not isinstance(path, str): - raise ValueError("Unexpected path type: %s" % str(path)) + if not isinstance(path, basestring): + raise ValueError("Unexpected path type: %s" % type(path)) is_url = path.startswith("http") if is_url: From be2769d230271b550031a2ebbf0e8c9e0ed93f5d Mon Sep 17 00:00:00 2001 From: bw2 Date: Tue, 23 May 2017 07:36:49 -0400 Subject: [PATCH 10/19] fixed git path --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 017ba74..7f25548 100644 --- a/setup.py +++ b/setup.py @@ -38,8 +38,8 @@ long_description=long_description, author='Ben Weisburd', author_email='weisburd@broadinstitute.org', - url='https://github.com/macarthur-lab/igv_utils', - py_modules=['igv_api'], + url='https://github.com/macarthur-lab/obo_parser', + py_modules=['obo_parser'], include_package_data=True, zip_safe=False, install_requires=[ From c20221abeb434a8f7ea04a43168a7f4e0fd04ede Mon Sep 17 00:00:00 2001 From: bw2 Date: Tue, 23 May 2017 07:42:59 -0400 Subject: [PATCH 11/19] removed unused imports --- setup.py | 2 -- tests/test_other_functions.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/setup.py b/setup.py index 7f25548..0f8909f 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,3 @@ -import glob -import logging import os import sys diff --git a/tests/test_other_functions.py b/tests/test_other_functions.py index 9947528..e0cfab7 100644 --- a/tests/test_other_functions.py +++ b/tests/test_other_functions.py @@ -3,8 +3,6 @@ import sys import unittest -from builtins import dict - if sys.version_info > (3, 0): from io import StringIO else: From 6a479c743140c39a753bccf484ca6174d3110e66 Mon Sep 17 00:00:00 2001 From: bw2 Date: Tue, 23 May 2017 07:47:23 -0400 Subject: [PATCH 12/19] switching test-running command --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a8f58aa..7727735 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,4 +14,5 @@ python: install: "pip install -r requirements.txt" # command to run tests -script: python setup.py test +script: python -m unittest discover + From bdc7df066d5f982f2364025ff1b0db295625ea56 Mon Sep 17 00:00:00 2001 From: bw2 Date: Tue, 23 May 2017 07:52:58 -0400 Subject: [PATCH 13/19] added future as a dependency to allow 'from builtins import dict' --- requirements.txt | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index d167fc6..8ba39ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ +future tqdm==4.11.2 diff --git a/setup.py b/setup.py index 0f8909f..a1d4b24 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,7 @@ zip_safe=False, install_requires=[ 'tqdm', + 'future', ], license="MIT", keywords='obo, bioinformatics, parser', From 3b29bfee01f8fb77aa8e3e9c271b3dddddb6ec15 Mon Sep 17 00:00:00 2001 From: bw2 Date: Tue, 23 May 2017 09:55:36 -0400 Subject: [PATCH 14/19] fixed typo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index ba34e4a..920767c 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -Parses .obo files such the one containing the Human Phenotype Ontology (HPO) and converts them to a .tsv table which is easier to work with. +Parses ontologies in .obo format (such as the Human Phenotype Ontology) and converts them to a .tsv table. .. image:: https://travis-ci.org/macarthur-lab/obo_parser.svg?branch=master :target: https://travis-ci.org/macarthur-lab/obo_parser From c3dab720506144004afe0bf31729f69e58d3a3c3 Mon Sep 17 00:00:00 2001 From: bw2 Date: Tue, 23 May 2017 10:19:19 -0400 Subject: [PATCH 15/19] added install, test, run sections --- README.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.rst b/README.rst index 920767c..164b559 100644 --- a/README.rst +++ b/README.rst @@ -2,3 +2,26 @@ Parses ontologies in .obo format (such as the Human Phenotype Ontology) and conv .. image:: https://travis-ci.org/macarthur-lab/obo_parser.svg?branch=master :target: https://travis-ci.org/macarthur-lab/obo_parser + + +**Install** + +.. code:: bash + + git clone https://github.com/macarthur-lab/obo_parser.git + +**Test** + +.. code:: bash + + python -m unittest discover + +**Run** + +Examples: + +.. code:: bash + + python obo_parser.py --help + python obo_parser.py -r HP:0000118 http://purl.obolibrary.org/obo/hp.obo -o hpo.tsv + python obo_parser.py -c -r HP:0000118 http://purl.obolibrary.org/obo/hp.obo | cut -f 1,2,4,5,10,11 > hp.tsv From 11409a125c4ff43a8e64853b5ae062d81638b838 Mon Sep 17 00:00:00 2001 From: bw2 Date: Wed, 24 May 2017 05:20:15 -0400 Subject: [PATCH 16/19] get rid of "-" for specifying stdout --- obo_parser.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/obo_parser.py b/obo_parser.py index f8649b4..737b199 100644 --- a/obo_parser.py +++ b/obo_parser.py @@ -35,7 +35,7 @@ } -def convert_obo_to_tsv(input_path, output_path="-", root_id=None, add_category_column=False): +def convert_obo_to_tsv(input_path, output_path=None, root_id=None, add_category_column=False): """Main entry point for parsing an .obo file and converting it to a .tsv table. Args: @@ -48,9 +48,6 @@ def convert_obo_to_tsv(input_path, output_path="-", root_id=None, add_category_c direct child of the ontology's root term. """ - if output_path is None: - output_path = os.path.basename(input_path).replace(".obo", "") + ".tsv" - # read in data logger.info("Parsing %s", input_path) with _open_input_stream(input_path) as input_stream: @@ -69,7 +66,7 @@ def convert_obo_to_tsv(input_path, output_path="-", root_id=None, add_category_c # print stats and output .tsv print_stats(obo_records_dict, input_path) - if output_path == "-": + if output_path is None: write_tsv(obo_records_dict, output_stream=sys.stdout, root_id=root_id) else: with open(output_path, "w") as output_stream: @@ -363,8 +360,7 @@ def _confirm_id_is_valid(term_id, obo_records_dict, label="id"): if __name__ == "__main__": p = argparse.ArgumentParser(description="Parse an .obo file and write out a .tsv table") - p.add_argument("-o", "--output-path", help="output .tsv file path. Defaults to standard out.", - default="-") + p.add_argument("-o", "--output-path", help="output .tsv file path. Defaults to standard out.") p.add_argument("-r", "--root-id", help="If specified, ignore ontology terms that are not " "either descendants of the given id or have this id themselves. For example: 'HP:0000118'.") p.add_argument("-c", "--add-category-column", action="store_true", help="add a 'category' " From 5902962b0c1e03e4c3e972ead9484a8c7ba1b339 Mon Sep 17 00:00:00 2001 From: bw2 Date: Wed, 24 May 2017 05:24:00 -0400 Subject: [PATCH 17/19] use >= instead of > for python 3.0 version check --- obo_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obo_parser.py b/obo_parser.py index 737b199..5402908 100644 --- a/obo_parser.py +++ b/obo_parser.py @@ -17,7 +17,7 @@ import urllib from builtins import dict -if sys.version_info > (3, 0): +if sys.version_info >= (3, 0): basestring = str logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s') From 36455945573f438255c9c90280a996e71f9ffd91 Mon Sep 17 00:00:00 2001 From: bw2 Date: Wed, 24 May 2017 05:24:21 -0400 Subject: [PATCH 18/19] use >= instead of > for python 3.0 version check --- tests/test_other_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_other_functions.py b/tests/test_other_functions.py index e0cfab7..6a8eb54 100644 --- a/tests/test_other_functions.py +++ b/tests/test_other_functions.py @@ -3,7 +3,7 @@ import sys import unittest -if sys.version_info > (3, 0): +if sys.version_info >= (3, 0): from io import StringIO else: from StringIO import StringIO From 98fc5ece61ee091d4996fb71aa5d1aaba6532420 Mon Sep 17 00:00:00 2001 From: bw2 Date: Wed, 24 May 2017 05:29:11 -0400 Subject: [PATCH 19/19] rstrip('\n') => strip() --- obo_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obo_parser.py b/obo_parser.py index 5402908..fe1a2b6 100644 --- a/obo_parser.py +++ b/obo_parser.py @@ -103,7 +103,7 @@ def parse_obo_format(lines): continue # remove new-line character and any comments - line = line.rstrip('\n').split("!")[0] + line = line.strip().split("!")[0] if len(line) == 0: continue