diff --git a/src/genomicranges/GenomicRanges.py b/src/genomicranges/GenomicRanges.py index 70c4d4a..c69cccb 100644 --- a/src/genomicranges/GenomicRanges.py +++ b/src/genomicranges/GenomicRanges.py @@ -2236,3 +2236,12 @@ def concat(self, *granges: "GenomicRanges") -> "GenomicRanges": new_data[col].extend([None] * len(self)) return GenomicRanges(new_data, column_names=all_unique_columns) + + @classmethod + def empty(cls): + """Create an zero-length `GenomicRanges` object. + + Returns: + same type as caller, in this case a `GenomicRanges`. + """ + return cls(number_of_rows=0) diff --git a/src/genomicranges/GenomicRangesList.py b/src/genomicranges/GenomicRangesList.py index 1d9895e..5a0d737 100644 --- a/src/genomicranges/GenomicRangesList.py +++ b/src/genomicranges/GenomicRangesList.py @@ -57,8 +57,8 @@ class GenomicRangesList: def __init__( self, - ranges: List[GenomicRanges] = [], - number_of_ranges: Optional[int] = None, + ranges: Union[GenomicRanges, List[GenomicRanges]], + range_lengths: Optional[List[int]] = None, names: Optional[List[str]] = None, mcols: BiocOrPandasFrame = None, metadata: Optional[dict] = None, @@ -66,10 +66,11 @@ def __init__( """Initialize a `GenomicRangesList` object. Args: - ranges (List[GenomicRanges], optional): List of genomic elements. + ranges (GenomicRangesList, optional): List of genomic elements. All elements in this list must be :py:class:`genomicranges.GenomicRanges.GenomicRanges` - class. Defaults to []. - number_of_ranges (Optional[int], optional): Number of genomic elements. Defaults to None. + class. + range_lengths (Optional[List[int]], optional): Number of ranges within each genomic element. + Defaults to None. names (Optional[List[str]], optional): Names of the genomic elements. The length of this must match the number of genomic elements in ``ranges``. Defaults to None. @@ -77,28 +78,27 @@ def __init__( metadata (Optional[Dict], optional): Additional metadata. Defaults to None. """ self._validate(ranges) - _data = {"ranges": ranges} + self._data = {"ranges": ranges} + + if range_lengths is None: + range_lengths = [len(x) for x in ranges] - if number_of_ranges is None: - number_of_ranges = len(ranges) + self._range_lengths = range_lengths if mcols is None: - mcols = BiocFrame(number_of_rows=number_of_ranges) + mcols = BiocFrame(number_of_rows=len(range_lengths)) - _data["mcols"] = mcols - - self._frame = BiocFrame( - _data, - number_of_rows=number_of_ranges, - row_names=names, - ) + self._data["mcols"] = mcols + self._names = names self._metadata = {} if metadata is None else metadata - def _validate(self, ranges: List[GenomicRanges]): - if not is_list_of_type(ranges, GenomicRanges): + def _validate(self, ranges: Union[GenomicRanges, List[GenomicRanges]]): + if not ( + isinstance(ranges, GenomicRanges) or is_list_of_type(ranges, GenomicRanges) + ): raise TypeError( - "All genomic elements in `ranges` must be of type `GenomicRanges`." + "`ranges` must be either a `GenomicRanges` or a list of `GenomicRanges`." ) @property @@ -127,7 +127,7 @@ def ranges(self) -> Dict[str, List[str]]: Dict[str, List[str]]: A list with the same length as keys in the object, each element in the list contains another list of ranges names. """ - return self._frame.column("ranges") + return self._data["ranges"] @property def groups(self) -> Optional[list]: @@ -138,7 +138,7 @@ def groups(self) -> Optional[list]: :py:attr:`genomicranges.GenomicRanges.GenomicRangesList.ranges`, with each element specifying the name of the element. None if names are not provided. """ - return self._frame.row_names + return self._names @property def names(self) -> Optional[list]: @@ -158,8 +158,8 @@ def mcols(self) -> Optional[BiocOrPandasFrame]: Returns: (BiocOrPandasFrame, optional): Metadata frame or None if there is no element level metadata. """ - if "mcols" in self._frame.column_names: - return self._frame.column("mcols") + if "mcols" in self._data: + return self._data["mcols"] return None @@ -361,4 +361,15 @@ def __len__(self) -> int: Returns: int: number of genomic elements. """ - return len(self._frame) + return len(self._range_lengths) + + @classmethod + def empty(cls, n: int): + """Create an ``n``-length `GenomicRangesList` object. + + Returns: + same type as caller, in this case a `GenomicRangesList`. + """ + _range_lengths = [0] * n + + return cls(ranges=GenomicRanges.empty(), range_lengths=_range_lengths) diff --git a/tests/test_gr_initialize.py b/tests/test_gr_initialize.py index 75ad81a..be58344 100644 --- a/tests/test_gr_initialize.py +++ b/tests/test_gr_initialize.py @@ -85,3 +85,13 @@ def test_gr_empty(): gr = GenomicRanges(number_of_rows=100) assert gr is not None + assert isinstance(gr, GenomicRanges) + assert len(gr) == 100 + assert gr.shape == (100, 0) + + gre = GenomicRanges.empty() + + assert gre is not None + assert isinstance(gre, GenomicRanges) + assert len(gre) == 0 + assert gre.shape == (0, 0) diff --git a/tests/test_grl_methods.py b/tests/test_grl_methods.py index d8b95b5..8dcc539 100644 --- a/tests/test_grl_methods.py +++ b/tests/test_grl_methods.py @@ -37,9 +37,10 @@ def test_is_empty_False(): def test_is_empty_True(): - grl = GenomicRangesList() + grl = GenomicRangesList(GenomicRanges.empty(), range_lengths=[0] * 10) assert grl.is_empty() == True + assert len(grl) == 10 def test_nrows():