Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ cyvcf.egg-info
dist
cyvcf/parser.c
cyvcf/parser.so
cyvcf/parser.*.so
.history
107 changes: 66 additions & 41 deletions cyvcf/parser.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import csv
import gzip
import sys
import itertools
import operator
import codecs
file=open
PY3 = sys.version_info > (3,)

from . import utils

Expand Down Expand Up @@ -34,9 +38,9 @@ HET = 1
HOM_ALT = 3
UNKNOWN = 2

cdef _Call _parse_sample(char *sample, list samp_fmt,
cdef _Call _parse_sample(str sample, list samp_fmt,
list samp_fmt_types, list samp_fmt_nums,
char *name, Record rec):
str name, Record rec):

cdef dict sampdict = {x: None for x in samp_fmt}
cdef list lvals
Expand Down Expand Up @@ -92,7 +96,7 @@ cdef _Call _parse_sample(char *sample, list samp_fmt,
sampdict[fmt] = vals
return _Call(rec, name, sampdict)

cdef inline list _map(func, list iterable, char *bad='.'):
cdef inline list _map(func, list iterable, str bad='.'):
'''``map``, but make bad values None.'''
return [func(x) if x != bad else None for x in iterable]

Expand Down Expand Up @@ -175,16 +179,17 @@ class _vcf_metadata_parser(object):
cdef class _Call(object):
""" A genotype call, a cell entry in a VCF file"""

cdef public bytes sample #NA12878
cdef bytes gt_nums #'0/1'
cdef public str sample #NA12878
cdef str gt_nums #'0/1'
# use str instead of bytes for python3 compatability
# use bytes instead of char * because of C -> Python string complications
# see: http://docs.cython.org/src/tutorial/strings.html
cdef public Record site #instance of Record
cdef public dict data
cdef public bint called, phased
cdef list alleles

def __cinit__(self, Record site, char *sample, dict data):
def __cinit__(self, Record site, str sample, dict data):
#: The ``Record`` for this ``_Call``
self.site = site
#: The sample name
Expand Down Expand Up @@ -426,7 +431,7 @@ cdef class Record(object):
gt_phred_likelihoods
# use bytes instead of char * because of C -> Python string complications
# see: http://docs.cython.org/src/tutorial/strings.html
cdef readonly bytes CHROM, ID, FORMAT
cdef readonly str CHROM, ID, FORMAT
cdef public REF
cdef readonly object FILTER, QUAL
cdef public int POS, start, end, num_hom_ref, num_het, num_hom_alt, \
Expand All @@ -435,8 +440,8 @@ cdef class Record(object):
cdef public dict _sample_indexes
cdef public bint has_genotypes

def __cinit__(self, char *CHROM, int POS, char *ID,
char *REF, list ALT, object QUAL=None,
def __cinit__(self, str CHROM, int POS, str ID,
str REF, list ALT, object QUAL=None,
object FILTER=None, dict INFO=None, object FORMAT=None,
dict sample_indexes=None, list samples=None,
list gt_bases=None, list gt_types=None,
Expand Down Expand Up @@ -493,6 +498,13 @@ cdef class Record(object):
self.POS == other.POS and
self.REF == other.REF and
self.ALT == other.ALT)
elif op == 3:
return not self.__richcmp__(other, 2)
else:
ops = { 0: operator.lt, 1: operator.le,
2: operator.eq, 3: operator.ne,
4: operator.gt, 5: operator.ge }
return ops[op]( (self.CHROM, self.POS), (other.CHROM, other.POS) )

def __iter__(self):
return iter(self.samples)
Expand Down Expand Up @@ -534,8 +546,8 @@ cdef class Record(object):
self._format_qual() or '.', self.FILTER or '.', self._format_info()])


def __cmp__(self, other):
return cmp( (self.CHROM, self.POS), (other.CHROM, other.POS))
# def __cmp__(self, other):
# return cmp( (self.CHROM, self.POS), (other.CHROM, other.POS))

def add_format(self, fmt):
tmp = self.FORMAT + ':' + fmt
Expand Down Expand Up @@ -660,10 +672,10 @@ cdef class Record(object):
if self.is_snp:
# just one alt allele
alt_allele = self.ALT[0]
if ((self.REF == b'A' and alt_allele == b'G') or
(self.REF == b'G' and alt_allele == b'A') or
(self.REF == b'C' and alt_allele == b'T') or
(self.REF == b'T' and alt_allele == b'C')):
if ((self.REF == 'A' and alt_allele == 'G') or
(self.REF == 'G' and alt_allele == 'A') or
(self.REF == 'C' and alt_allele == 'T') or
(self.REF == 'T' and alt_allele == 'C')):
return True
else: return False
else: return False
Expand Down Expand Up @@ -775,20 +787,20 @@ cdef class Record(object):
cdef class Reader(object):

""" Reader for a VCF v 4.1 file, an iterator returning ``Record objects`` """
cdef bytes _col_defn_line
cdef char _prepend_chr
cdef str _col_defn_line
cdef public object reader
cdef bint compressed, prepend_chr
cdef bint compressed, _prepend_chr
cdef public dict metadata, infos, filters, formats,
cdef readonly dict _sample_indexes
cdef list _header_lines, samp_data
cdef public list samples
cdef object _tabix
cdef public object filename
cdef public str filename
cdef int num_samples
cdef str encoding

def __init__(self, fsock=None, filename=None,
bint compressed=False, bint prepend_chr=False):
def __init__(self, fsock=None, str filename=None,
bint compressed=False, bint prepend_chr=False, encoding='ascii'):
""" Create a new Reader for a VCF file.

You must specify a filename. Gzipped streams
Expand All @@ -797,13 +809,14 @@ cdef class Reader(object):
"""
super(VCFReader, self).__init__()

self.encoding = encoding
if not (fsock or filename):
raise Exception('You must provide at least fsock or filename')

if filename:
self.filename = filename
if fsock is None:
self.reader = file(filename)
self.reader = file(filename, 'rb')

if fsock:
self.reader = fsock
Expand All @@ -814,6 +827,8 @@ cdef class Reader(object):

if compressed or (filename and filename.endswith('.gz')):
self.reader = gzip.GzipFile(fileobj=self.reader)
if PY3:
self.reader = codecs.getreader(self.encoding)(self.reader)

#: metadata fields from header
self.metadata = {}
Expand Down Expand Up @@ -857,7 +872,10 @@ cdef class Reader(object):

parser = _vcf_metadata_parser()

line = self.reader.next()
line = next(self.reader)
if PY3 and type(line) is bytes:
self.reader = codecs.getreader(self.encoding)(self.reader)
line = line.decode(self.encoding)
while line.startswith('##'):
self._header_lines.append(line)
line = line.rstrip('\n')
Expand All @@ -878,7 +896,7 @@ cdef class Reader(object):
key, val = parser.read_meta(line.strip())
self.metadata[key] = val

line = self.reader.next()
line = next(self.reader)

if line.startswith('#'): # the column def'n line - REQ'D
self._col_defn_line = line
Expand All @@ -888,10 +906,10 @@ cdef class Reader(object):
self.num_samples = len(self.samples)
self._sample_indexes = dict([(x,i) for (i,x) in enumerate(self.samples)])
else:
sys.exit("Expected column definition line beginning with #. Not found - exiting.")
sys.exit("Expected column definition line beginning with #. Not found - exiting.")


cdef list _map(Reader self, func, iterable, char *bad='.'):
cdef list _map(Reader self, func, iterable, str bad='.'):
'''``map``, but make bad values None.'''
return [func(x) if x != bad else None for x in iterable]

Expand All @@ -909,7 +927,7 @@ cdef class Reader(object):

cdef int i = 0
cdef int n = len(entries)
cdef char *entry_type
cdef str entry_type
cdef list entry
# for entry in entries:
for i in xrange(n):
Expand All @@ -926,18 +944,18 @@ cdef class Reader(object):
else:
entry_type = 'String'

if entry_type == b'Integer':
if entry_type == 'Integer':
vals = entry[1].split(',')
try:
val = _map(int, vals)
except ValueError:
val = _map(float, vals)
elif entry_type == b'Float':
elif entry_type == 'Float':
vals = entry[1].split(',')
val = _map(float, vals)
elif entry_type == b'Flag':
elif entry_type == 'Flag':
val = True
elif entry_type == b'String':
elif entry_type == 'String':
if len(entry) > 1:
val = entry[1]
else:
Expand All @@ -959,7 +977,7 @@ cdef class Reader(object):
return retdict


def _parse_samples(self, Record rec, list samples, char *samp_fmt_s):
def _parse_samples(self, Record rec, list samples, str samp_fmt_s):
'''Parse a sample entry according to the format specified in the FORMAT
column.'''
cdef list samp_fmt = samp_fmt_s.split(':')
Expand All @@ -968,7 +986,7 @@ cdef class Reader(object):
cdef list samp_fmt_nums = [None] * n

cdef int i = 0
cdef char *fmt
cdef str fmt
# for fmt in samp_fmt:
for i in xrange(n):
fmt = samp_fmt[i]
Expand Down Expand Up @@ -1045,28 +1063,29 @@ cdef class Reader(object):

def __next__(self):
'''Return the next record in the file.'''
line = self.reader.next().rstrip()
line = next(self.reader).rstrip()
return self.parse(line)

def parse(self, line):
'''Return the next record in the file.'''
cdef list row = line.split('\t')

#CHROM
cdef bytes chrom = row[0]
cdef str chrom = row[0]
if self._prepend_chr:
chrom = 'chr' + str(chrom)
tmp = 'chr' + str(chrom)
chrom = tmp
# POS
cdef int pos = int(row[1])
# ID
cdef bytes id = row[2]
cdef str id = row[2]
#REF
cdef bytes ref = row[3]
cdef str ref = row[3]
#ALT
cdef list alt = self._map(str, row[4].split(','))
#QUAL
cdef object qual
if row[5] == b'.':
if row[5] == '.':
qual = None
else:
qual = float(row[5])
Expand All @@ -1077,7 +1096,7 @@ cdef class Reader(object):
#INFO
cdef dict info = self._parse_info(row[7])
#FORMAT
cdef bytes fmt
cdef str fmt
try:
fmt = row[8]
except IndexError:
Expand Down Expand Up @@ -1113,13 +1132,19 @@ cdef class Reader(object):
if end is None:
self.reader = self._tabix.fetch(chrom, start, start+1)
try:
return self.next()
return next(self)
except StopIteration:
return None

self.reader = self._tabix.fetch(chrom, start, end)
return self

def close(self):
try:
self.reader.close()
finally:
return


class Writer(object):
""" VCF Writer """
Expand Down
Loading