Skip to content

Commit

Permalink
feat: update the typeline library (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
clintval authored Nov 13, 2024
1 parent 633956d commit 99cd8fe
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 59 deletions.
93 changes: 51 additions & 42 deletions bedspec/_reader.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
import json
from dataclasses import asdict as as_dict
from io import TextIOWrapper
from pathlib import Path
from types import NoneType
from types import UnionType
from typing import Any
from typing import cast
from typing import get_args
from typing import get_origin

from typeline import TsvStructReader
from typeline import TsvRecordReader
from typing_extensions import Self
from typing_extensions import override

from bedspec._bedspec import COMMENT_PREFIXES
from bedspec._bedspec import MISSING_FIELD
from bedspec._bedspec import BedColor
from bedspec._bedspec import BedStrand
from bedspec._bedspec import BedType


class BedReader(TsvStructReader[BedType]):
class BedReader(TsvRecordReader[BedType]):
"""A reader of BED records."""

@override
Expand All @@ -28,47 +26,49 @@ def __init__(
handle: TextIOWrapper,
record_type: type[BedType],
/,
has_header: bool = False,
header: bool = False,
comment_prefixes: set[str] = COMMENT_PREFIXES,
):
"""Initialize the BED reader."""
super().__init__(handle, record_type, has_header=has_header)
"""Instantiate a new BED reader.
@property
@override
def comment_prefixes(self) -> set[str]:
return {"#", "browser", "track"}

@staticmethod
def _build_union(*types: type) -> type | UnionType:
"""Build a singular type or a union type if multiple types are provided."""
if len(types) == 1:
return types[0]
union: UnionType | type = types[0]
for t in types[1:]:
union |= t
return cast(UnionType, union)
Args:
handle: a file-like object to read delimited data from.
record_type: the type of BED record we will be writing.
header: whether we expect the first line to be a header or not.
comment_prefixes: skip lines that have any of these string prefixes.
"""
super().__init__(handle, record_type, header=header, comment_prefixes=comment_prefixes)

@override
def _decode(self, field_type: type[Any] | str | Any, item: Any) -> Any:
"""A callback for overriding the decoding of builtin types and custom types."""
def _decode(self, field_type: type[Any] | str | Any, item: str) -> str:
"""A callback for overriding the string formatting of builtin and custom types."""
if field_type is BedStrand:
return f'"{item}"'
elif field_type is BedColor:
color = BedColor.from_string(item)
return f'{{"r":{color.r},"g":{color.g},"b":{color.b}}}'

is_union: bool = isinstance(field_type, UnionType)
type_args: tuple[type, ...] = get_args(field_type)
is_optional: bool = is_union and NoneType in type_args

if is_optional:
if item == MISSING_FIELD:
return "null"
elif type_args.count(BedStrand) == 1:
return f'"{item}"'
elif type_args.count(BedColor) == 1:
if item == "0":
return "null"
else:
color = BedColor.from_string(item)
return f'{{"r":{color.r},"g":{color.g},"b":{color.b}}}'

type_origin: type | None = get_origin(field_type)
is_union: bool = isinstance(field_type, UnionType)

if item == MISSING_FIELD and NoneType in type_args:
return None
elif field_type is BedColor or BedColor in type_args:
if item == "0":
return None
return json.dumps(as_dict(BedColor.from_string(cast(str, item)))) # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]
elif field_type is BedStrand or BedStrand in type_args:
return f'"{item}"'
elif type_origin in (frozenset, list, tuple, set):
stripped: str = item.rstrip(",")
return f"[{stripped}]"
elif is_union and len(type_args) >= 2 and NoneType in type_args:
other_types: set[type] = set(type_args) - {NoneType}
return self._decode(self._build_union(*other_types), item)
if type_origin in (frozenset, list, set, tuple):
return f"[{item.rstrip(',')}]"

return super()._decode(field_type, item=item)

@classmethod
Expand All @@ -78,8 +78,17 @@ def from_path(
path: Path | str,
record_type: type[BedType],
/,
has_header: bool = False,
header: bool = False,
comment_prefixes: set[str] = COMMENT_PREFIXES,
) -> Self:
"""Construct a BED reader from a file path."""
reader = cls(Path(path).open("r"), record_type, has_header=has_header)
"""Construct a BED reader from a file path.
Args:
path: the path to the file to read delimited data from.
record_type: the type of the object we will be writing.
header: whether we expect the first line to be a header or not.
comment_prefixes: skip lines that have any of these string prefixes.
"""
handle = Path(path).open("r")
reader = cls(handle, record_type, header=header, comment_prefixes=comment_prefixes)
return reader
10 changes: 5 additions & 5 deletions bedspec/_writer.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
from typing import Any

from typeline import TsvStructWriter
from typeline import TsvRecordWriter
from typing_extensions import override

from bedspec._bedspec import COMMENT_PREFIXES
from bedspec._bedspec import BedColor
from bedspec._bedspec import BedType


class BedWriter(TsvStructWriter[BedType]):
"""A writer of BED records."""
class BedWriter(TsvRecordWriter[BedType]):
"""A writer for writing dataclasses into BED text data."""

@override
def _encode(self, item: Any) -> Any:
"""A callback for overriding the encoding of builtin types and custom types."""
if item is None:
return "."
if isinstance(item, (list, set, tuple)):
elif isinstance(item, (frozenset, list, set, tuple)):
return ",".join(map(str, item)) # pyright: ignore[reportUnknownArgumentType]
if isinstance(item, BedColor):
elif isinstance(item, BedColor):
return str(item)
return super()._encode(item=item)

Expand Down
20 changes: 10 additions & 10 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ packages = [{ include = "bedspec" }, { include = "cgranges" }]

[tool.poetry.dependencies]
python = "^3.10.0,<4.0.0"
typeline = "^0.4"
typeline = "^0.6"
typing-extensions = "^4.12"

[tool.poetry.dev-dependencies]
Expand Down Expand Up @@ -177,7 +177,7 @@ ignore = [
unfixable = ["B"]

[tool.ruff.lint.mccabe]
max-complexity = 10
max-complexity = 13

[tool.ruff.lint.isort]
force-single-line = true
Expand Down

0 comments on commit 99cd8fe

Please sign in to comment.