Skip to content

Commit

Permalink
drop py37 support, remove dependencies related to typing backward-com…
Browse files Browse the repository at this point in the history
…patibility
  • Loading branch information
jdidion committed Feb 22, 2024
1 parent 94b3696 commit 6326d84
Show file tree
Hide file tree
Showing 13 changed files with 523 additions and 676 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
PYTHON_VERSION: ["3.7", "3.8", "3.9", "3.10"]
PYTHON_VERSION: ["3.8", "3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v2

Expand Down
2 changes: 1 addition & 1 deletion fgpyo/fasta/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
'AAAAAAAAAANNN'
"""

import textwrap
from pathlib import Path
from typing import TYPE_CHECKING
Expand All @@ -48,7 +49,6 @@ def samtools_dict(*args: Any) -> None:
def samtools_faidx(*args: Any) -> None:
pass


else:
from pysam import dict as samtools_dict
from pysam import faidx as samtools_faidx
Expand Down
1 change: 1 addition & 0 deletions fgpyo/fastx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
seq2: GGGG, seq2: TTTT
"""

from contextlib import AbstractContextManager
from pathlib import Path
from types import TracebackType
Expand Down
1 change: 1 addition & 0 deletions fgpyo/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"10"
"""

import gzip
import io
import os
Expand Down
1 change: 1 addition & 0 deletions fgpyo/read_structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
correspond to the given read segment
"""

import enum
from typing import Iterable
from typing import Iterator
Expand Down
1 change: 1 addition & 0 deletions fgpyo/sam/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- :class:`~fgpyo.sam.builder.SamBuilder` -- A builder class that allows the accumulation
of alignment records and access as a list and writing to file.
"""

from array import array
from pathlib import Path
from random import Random
Expand Down
91 changes: 57 additions & 34 deletions fgpyo/util/inspect.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,25 @@
import functools
import sys
import typing
from enum import Enum
from functools import partial
from pathlib import PurePath
from typing import Any
from typing import Callable
from typing import Dict
from typing import Iterable
from typing import List
from typing import Literal
from typing import Optional
from typing import Tuple
from typing import Type
from typing import Union

if sys.version_info >= (3, 8):
from typing import Literal
else:
from typing_extensions import Literal
if sys.version_info >= (3, 12):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias

import functools
from enum import Enum
from functools import partial
from pathlib import PurePath
from typing import Callable
from typing import Optional

import attr

import fgpyo.util.types as types
Expand Down Expand Up @@ -60,18 +57,24 @@ def split_at_given_level(
decrease_in_depth += high_level_split.count(char)
outer_depth_of_split += increase_in_depth - decrease_in_depth

assert outer_depth_of_split >= 0, "Unpaired depth character! Likely incorrect output"
assert (
outer_depth_of_split >= 0
), "Unpaired depth character! Likely incorrect output"

current_outer_splits.append(high_level_split)
if outer_depth_of_split == 0:
out_vals.append(split_delim.join(current_outer_splits))
current_outer_splits = []
assert outer_depth_of_split == 0, "Unpaired depth character! Likely incorrect output!"
assert (
outer_depth_of_split == 0
), "Unpaired depth character! Likely incorrect output!"
return out_vals


def _get_parser(
cls: Type, type_: TypeAlias, parsers: Optional[Dict[type, Callable[[str], Any]]] = None
cls: Type,
type_: TypeAlias,
parsers: Optional[Dict[type, Callable[[str], Any]]] = None,
) -> partial:
"""Attempts to find a parser for a provided type.
Expand Down Expand Up @@ -109,8 +112,8 @@ def get_parser() -> partial:
raise ValueError("Unable to parse set (try typing.Set[type])")
elif type_ == dict:
raise ValueError("Unable to parse dict (try typing.Mapping[type])")
elif types.get_origin_type(type_) == list:
subtypes = types.get_arg_types(type_)
elif typing.get_origin(type_) == list:
subtypes = typing.get_args(type_)

assert (
len(subtypes) == 1
Expand All @@ -130,8 +133,8 @@ def get_parser() -> partial:
]
)
)
elif types.get_origin_type(type_) == set:
subtypes = types.get_arg_types(type_)
elif typing.get_origin(type_) == set:
subtypes = typing.get_args(type_)
assert (
len(subtypes) == 1
), "Sets are allowed only one subtype per PEP specification!"
Expand All @@ -146,18 +149,20 @@ def get_parser() -> partial:
if s == "{}"
else [
subtype_parser(item)
for item in set(split_at_given_level(s[1:-1], split_delim=","))
for item in set(
split_at_given_level(s[1:-1], split_delim=",")
)
]
)
)
elif types.get_origin_type(type_) == tuple:
elif typing.get_origin(type_) == tuple:
subtype_parsers = [
_get_parser(
cls,
subtype,
parsers,
)
for subtype in types.get_arg_types(type_)
for subtype in typing.get_args(type_)
]

def tuple_parse(tuple_string: str) -> Tuple[Any, ...]:
Expand All @@ -172,16 +177,18 @@ def tuple_parse(tuple_string: str) -> Tuple[Any, ...]:
if len(tuple_string) == 0:
return ()
else:
val_strings = split_at_given_level(tuple_string, split_delim=",")
val_strings = split_at_given_level(
tuple_string, split_delim=","
)
return tuple(
parser(val_str)
for parser, val_str in zip(subtype_parsers, val_strings)
)

return functools.partial(tuple_parse)

elif types.get_origin_type(type_) == dict:
subtypes = types.get_arg_types(type_)
elif typing.get_origin(type_) == dict:
subtypes = typing.get_args(type_)
assert (
len(subtypes) == 2
), "Dict object must have exactly 2 subtypes per PEP specification!"
Expand All @@ -208,10 +215,14 @@ def dict_parse(dict_string: str) -> Dict[Any, Any]:
if len(dict_string) == 0:
return {}
else:
outer_splits = split_at_given_level(dict_string, split_delim=",")
outer_splits = split_at_given_level(
dict_string, split_delim=","
)
out_dict = {}
for outer_split in outer_splits:
inner_splits = split_at_given_level(outer_split, split_delim=";")
inner_splits = split_at_given_level(
outer_split, split_delim=";"
)
assert (
len(inner_splits) % 2 == 0
), "Inner splits of dict didn't have matched key val pairs"
Expand All @@ -228,15 +239,21 @@ def dict_parse(dict_string: str) -> Dict[Any, Any]:
return functools.partial(type_)
elif isinstance(type_, type(type(None))):
return functools.partial(types.none_parser)
elif types.get_origin_type(type_) is Union:
elif typing.get_origin(type_) is Union:
return types.make_union_parser(
union=type_,
parsers=[_get_parser(cls, arg, parsers) for arg in types.get_arg_types(type_)],
parsers=[
_get_parser(cls, arg, parsers)
for arg in typing.get_args(type_)
],
)
elif types.get_origin_type(type_) is Literal: # Py>=3.7.
elif typing.get_origin(type_) is Literal: # Py>=3.7.
return types.make_literal_parser(
type_,
[_get_parser(cls, type(arg), parsers) for arg in types.get_arg_types(type_)],
[
_get_parser(cls, type(arg), parsers)
for arg in typing.get_args(type_)
],
)
else:
raise ParserNotFoundException(
Expand All @@ -255,7 +272,9 @@ def dict_parse(dict_string: str) -> Dict[Any, Any]:


def attr_from(
cls: Type, kwargs: Dict[str, str], parsers: Optional[Dict[type, Callable[[str], Any]]] = None
cls: Type,
kwargs: Dict[str, str],
parsers: Optional[Dict[type, Callable[[str], Any]]] = None,
) -> Any:
"""Builds an attr class from key-word arguments
Expand Down Expand Up @@ -289,7 +308,11 @@ def attr_from(
# try setting by casting
# Note that while bools *can* be cast from string, all non-empty strings evaluate to
# True, because python, so we need to check for that explicitly
if not set_value and attribute.type is not None and not attribute.type == bool:
if (
not set_value
and attribute.type is not None
and not attribute.type == bool
):
try:
return_value = attribute.type(str_value)
set_value = True
Expand All @@ -316,8 +339,8 @@ def attr_from(

def attribute_is_optional(attribute: attr.Attribute) -> bool:
"""Returns True if the attribute is optional, False otherwise"""
return types.get_origin_type(attribute.type) is Union and isinstance(
None, types.get_arg_types(attribute.type)
return typing.get_origin(attribute.type) is Union and isinstance(
None, typing.get_args(attribute.type)
)


Expand Down
8 changes: 1 addition & 7 deletions fgpyo/util/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,13 @@
"""

import logging
import sys

if sys.version_info >= (3, 8):
from typing import Literal
else:
from typing_extensions import Literal

import socket
from contextlib import AbstractContextManager
from logging import Logger
from threading import RLock
from typing import Any
from typing import Callable
from typing import Literal
from typing import Optional
from typing import Union

Expand Down
56 changes: 15 additions & 41 deletions fgpyo/util/types.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,15 @@
import collections
import inspect
import sys
import typing
from enum import Enum
from functools import partial
from typing import Callable
from typing import Iterable
from typing import Literal
from typing import Type
from typing import TypeVar
from typing import Union

# `get_origin_type` is a method that gets the outer type (ex list in a List[str])
# `get_arg_types` is a method that gets the inner type (ex str in a List[str])
if sys.version_info >= (3, 8):
from typing import Literal

get_origin_type = typing.get_origin
get_arg_types = typing.get_args
else:
import typing_inspect
from typing_extensions import Literal

def get_origin_type(tp: Type) -> Type:
"""Returns the outer type of a Typing object (ex list in a List[T])"""

if type(tp) is type(Literal): # Py<=3.6.
return Literal
origin = typing_inspect.get_origin(tp)
return {
typing.List: list,
typing.Iterable: collections.abc.Iterable,
typing.Sequence: collections.abc.Sequence,
typing.Tuple: tuple,
typing.Set: set,
typing.Mapping: dict,
typing.Dict: dict,
}.get(origin, origin)

def get_arg_types(tp: Type) -> Type:
"""Gets the inner types of a Typing object (ex T in a List[T])"""

if type(tp) is type(Literal): # Py<=3.6.
return tp.__values__
return typing_inspect.get_args(tp, evaluate=True) # evaluate=True default on Py>=3.7.


UnionType = TypeVar("UnionType", bound="Union")
EnumType = TypeVar("EnumType", bound="Enum")
Expand Down Expand Up @@ -109,7 +75,7 @@ def is_constructible_from_str(type_: type) -> bool:

def _is_optional(type_: type) -> bool:
"""Returns true if type_ is optional"""
return get_origin_type(type_) is Union and type(None) in get_arg_types(type_)
return typing.get_origin(type_) is Union and type(None) in typing.get_args(type_)


def _make_union_parser_worker(
Expand Down Expand Up @@ -137,28 +103,32 @@ def _make_union_parser_worker(
raise ValueError(f"{value} could not be parsed as any of {union}")


def make_union_parser(union: Type[UnionType], parsers: Iterable[Callable[[str], type]]) -> partial:
def make_union_parser(
union: Type[UnionType], parsers: Iterable[Callable[[str], type]]
) -> partial:
"""Generates a parser function for a union type object and set of parsers for the possible
parsers to that union type object
"""
return partial(_make_union_parser_worker, union, parsers)


def _make_literal_parser_worker(
literal: Type[LiteralType], parsers: Iterable[Callable[[str], LiteralType]], value: str
literal: Type[LiteralType],
parsers: Iterable[Callable[[str], LiteralType]],
value: str,
) -> LiteralType:
"""Worker function behind literal parsing. Iterates through possible literals and
returns the value produced by the first literal that matches expectation.
Otherwise raises an error if none work"""
for arg, p in zip(get_arg_types(literal), parsers):
for arg, p in zip(typing.get_args(literal), parsers):
try:
if p(value) == arg:
return arg
except ValueError:
pass
raise InspectException(
"invalid choice: {!r} (choose from {})".format(
value, ", ".join(map(repr, map(str, get_arg_types(literal))))
value, ", ".join(map(repr, map(str, typing.get_args(literal))))
)
)

Expand All @@ -174,7 +144,11 @@ def make_literal_parser(

def is_list_like(type_: type) -> bool:
"""Returns true if the value is a list or list like object"""
return get_origin_type(type_) in [list, collections.abc.Iterable, collections.abc.Sequence]
return typing.get_origin(type_) in [
list,
collections.abc.Iterable,
collections.abc.Sequence,
]


def none_parser(value: str) -> Literal[None]:
Expand Down
Loading

0 comments on commit 6326d84

Please sign in to comment.