Skip to content

Commit 10e4689

Browse files
committed
Respond to review comments:
* Improve codecov * Update mypy type ignore statements * Fix readthedocs * Fix merge conflicts * Update poetry then fix black issues * Added some tests test * made a few variables and functions private
1 parent ff97fd5 commit 10e4689

File tree

7 files changed

+137
-70
lines changed

7 files changed

+137
-70
lines changed

codecov.yml

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ignore:
2+
- "**/test_*.py" # don't compute coverage of tests

fgpyo/sam/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ class CigarElement:
398398
length: int
399399
operator: CigarOp
400400

401-
def __post_init__(self) -> None:
401+
def __attrs_post_init__(self) -> None:
402402
"""Validates the length attribute is greater than zero."""
403403
if self.length <= 0:
404404
raise ValueError(f"Cigar element must have a length > 0, found {self.length}")

fgpyo/sam/tests/test_sam.py

+6
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,12 @@ def test_cigar_element_length_on(
195195
assert element.length_on_target == length_on_target
196196

197197

198+
@pytest.mark.parametrize("character", ["M", "I", "D", "S"])
199+
def test_invalid_cigar_element(character: str) -> None:
200+
with pytest.raises(ValueError):
201+
CigarElement(-1, operator=CigarOp.from_character(character))
202+
203+
198204
@pytest.mark.parametrize(
199205
"cigartuples,cigarstring",
200206
[

fgpyo/util/inspect.py

+65-48
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,17 @@
1-
import functools
21
import sys
32
import typing
4-
from enum import Enum
5-
from functools import partial
6-
from pathlib import PurePath
73
from typing import Any
8-
from typing import Callable
94
from typing import Dict
105
from typing import Iterable
116
from typing import List
127
from typing import Literal
13-
from typing import Optional
8+
from typing import Mapping
9+
from typing import Protocol
1410
from typing import Tuple
1511
from typing import Type
1612
from typing import Union
17-
from typing import Literal
18-
from typing import Protocol
19-
if sys.version_info >= (3, 12):
13+
14+
if sys.version_info >= (3, 10):
2015
from typing import TypeAlias
2116
else:
2217
from typing_extensions import TypeAlias
@@ -45,20 +40,28 @@
4540
from attr import fields_dict as get_attr_fields_dict
4641

4742
Attribute = attr.Attribute
48-
43+
# dataclasses and attr have internal tokens for missing values, join into a set so that we can
44+
# check if a value is missing without knowing the type of backing class
4945
MISSING = {DATACLASSES_MISSING, ATTR_NOTHING}
5046
except ImportError:
5147
_use_attr = False
5248
attr = None
5349
ATTR_NOTHING = None
54-
Attribute = TypeVar("Attribute", bound=object) # type: ignore
50+
Attribute = TypeVar("Attribute", bound=object) # type: ignore[misc, assignment]
51+
52+
# define empty placeholders for getting attr fields as a tuple or dict. They will never be
53+
# called because the import failed; but they're here to ensure that the function is defined in
54+
# sections of code that don't know if the import was successful or not.
5555

56-
def get_attr_fields(cls: type) -> Tuple[dataclasses.Field, ...]: # type: ignore
56+
def get_attr_fields(cls: type) -> Tuple[dataclasses.Field, ...]: # type: ignore[misc]
57+
"""Get tuple of fields for attr class. attrs isn't imported so return empty tuple."""
5758
return ()
5859

59-
def get_attr_fields_dict(cls: type) -> Dict[str, dataclasses.Field]: # type: ignore
60+
def get_attr_fields_dict(cls: type) -> Dict[str, dataclasses.Field]: # type: ignore[misc]
61+
"""Get dict of name->field for attr class. attrs isn't imported so return empty dict."""
6062
return {}
6163

64+
# for consistency with successful import of attr, create a set for missing values
6265
MISSING = {DATACLASSES_MISSING}
6366

6467
if TYPE_CHECKING:
@@ -73,22 +76,33 @@ class DataclassesProtocol(Protocol):
7376
from attr import AttrsInstance
7477
else:
7578

76-
class AttrsInstance(Protocol): # type: ignore
79+
class AttrsInstance(Protocol): # type: ignore[no-redef]
7780
__attrs_attrs__: Dict[str, Any]
7881

7982

80-
def is_attr_class(cls: type) -> bool: # type: ignore
83+
def is_attr_class(cls: type) -> bool: # type: ignore[arg-type]
84+
"""Return True if the class is an attr class, and False otherwise"""
8185
return hasattr(cls, "__attrs_attrs__")
8286

8387

84-
MISSING_OR_NONE = {*MISSING, None}
85-
DataclassesOrAttrClass = Union[DataclassesProtocol, AttrsInstance]
86-
FieldType: TypeAlias = Union[dataclasses.Field, attr.Attribute]
88+
_MISSING_OR_NONE = {*MISSING, None}
89+
"""Set of values that are considered missing or None for dataclasses or attr classes"""
90+
_DataclassesOrAttrClass: TypeAlias = Union[DataclassesProtocol, AttrsInstance]
91+
"""
92+
TypeAlias for dataclasses or attr classes. Mostly nonsense because they are not true types, they
93+
are traits, but there is no python trait-tester.
94+
"""
95+
FieldType: TypeAlias = Union[dataclasses.Field, Attribute]
96+
"""
97+
TypeAlias for dataclass Fields or attrs Attributes. It will correspond to the correct type for the
98+
corresponding _DataclassesOrAttrClass
99+
"""
87100

88101

89-
def get_dataclasses_fields_dict(
102+
def _get_dataclasses_fields_dict(
90103
class_or_instance: Union[DataclassesProtocol, Type[DataclassesProtocol]],
91104
) -> Dict[str, dataclasses.Field]:
105+
"""Get a dict from field name to Field for a dataclass class or instance."""
92106
return {field.name: field for field in get_dataclasses_fields(class_or_instance)}
93107

94108

@@ -372,46 +386,49 @@ def get_parser() -> partial:
372386
return parser
373387

374388

375-
def get_fields_dict(cls: Type[DataclassesOrAttrClass]) -> Dict[str, FieldType]:
376-
"""
377-
Get the fields dict from either a dataclasses or attr dataclass.
389+
def get_fields_dict(
390+
cls: Union[_DataclassesOrAttrClass, Type[_DataclassesOrAttrClass]]
391+
) -> Mapping[str, FieldType]:
392+
"""Get the fields dict from either a dataclasses or attr dataclass (or instance)"""
393+
if is_dataclasses_class(cls):
394+
return _get_dataclasses_fields_dict(cls) # type: ignore[arg-type]
395+
elif is_attr_class(cls): # type: ignore[arg-type]
396+
return get_attr_fields_dict(cls) # type: ignore[arg-type]
397+
else:
398+
raise TypeError("cls must a dataclasses or attr class")
378399

379-
Combine results in case someone chooses to mix them through inheritance.
380-
"""
381-
if not (is_dataclasses_class(cls) or is_attr_class(cls)):
382-
raise ValueError("cls must a dataclasses or attr class")
383-
return {
384-
**(get_dataclasses_fields_dict(cls) if is_dataclasses_class(cls) else {}),
385-
**(get_attr_fields_dict(cls) if is_attr_class(cls) else {}), # type: ignore
386-
}
387-
388-
389-
def get_fields(cls: Type[DataclassesOrAttrClass]) -> Tuple[FieldType, ...]:
390-
if not (is_dataclasses_class(cls) or is_attr_class(cls)):
391-
raise ValueError("cls must a dataclasses or attr class")
392-
return (get_dataclasses_fields(cls) if is_dataclasses_class(cls) else ()) + (
393-
get_attr_fields(cls) if is_attr_class(cls) else () # type: ignore
394-
)
400+
401+
def get_fields(
402+
cls: Union[_DataclassesOrAttrClass, Type[_DataclassesOrAttrClass]]
403+
) -> Tuple[FieldType, ...]:
404+
"""Get the fields tuple from either a dataclasses or attr dataclass (or instance)"""
405+
if is_dataclasses_class(cls):
406+
return get_dataclasses_fields(cls) # type: ignore[arg-type]
407+
elif is_attr_class(cls): # type: ignore[arg-type]
408+
return get_attr_fields(cls) # type: ignore[arg-type]
409+
else:
410+
raise TypeError("cls must a dataclasses or attr class")
395411

396412

397-
AttrFromType = TypeVar("AttrFromType")
413+
_AttrFromType = TypeVar("_AttrFromType")
414+
"""TypeVar to allow attr_from to be used with either an attr class or a dataclasses class"""
398415

399416

400417
def attr_from(
401-
cls: Type[AttrFromType],
418+
cls: Type[_AttrFromType],
402419
kwargs: Dict[str, str],
403420
parsers: Optional[Dict[type, Callable[[str], Any]]] = None,
404-
) -> AttrFromType:
405-
"""Builds an attr class from key-word arguments
421+
) -> _AttrFromType:
422+
"""Builds an attr or dataclasses class from key-word arguments
406423
407424
Args:
408-
cls: the attr class to be built
425+
cls: the attr or dataclasses class to be built
409426
kwargs: a dictionary of keyword arguments
410427
parsers: a dictionary of parser functions to apply to specific types
411428
412429
"""
413430
return_values: Dict[str, Any] = {}
414-
for attribute in get_fields(cls): # type: ignore
431+
for attribute in get_fields(cls): # type: ignore[arg-type]
415432
return_value: Any
416433
if attribute.name in kwargs:
417434
str_value: str = kwargs[attribute.name]
@@ -447,7 +464,7 @@ def attr_from(
447464
set_value
448465
), f"Do not know how to convert string to {attribute.type} for value: {str_value}"
449466
else: # no value, check for a default
450-
assert attribute.default is not None or attribute_is_optional(
467+
assert attribute.default is not None or _attribute_is_optional(
451468
attribute
452469
), f"No value given and no default for attribute `{attribute.name}`"
453470
return_value = attribute.default
@@ -460,13 +477,13 @@ def attr_from(
460477
return cls(**return_values)
461478

462479

463-
def attribute_is_optional(attribute: FieldType) -> bool:
480+
def _attribute_is_optional(attribute: FieldType) -> bool:
464481
"""Returns True if the attribute is optional, False otherwise"""
465482
return typing.get_origin(attribute.type) is Union and isinstance(
466483
None, typing.get_args(attribute.type)
467484
)
468485

469486

470-
def attribute_has_default(attribute: FieldType) -> bool:
487+
def _attribute_has_default(attribute: FieldType) -> bool:
471488
"""Returns True if the attribute has a default value, False otherwise"""
472-
return attribute.default not in MISSING_OR_NONE or attribute_is_optional(attribute)
489+
return attribute.default not in _MISSING_OR_NONE or _attribute_is_optional(attribute)

fgpyo/util/metric.py

+11-7
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
1111
The :class:`~fgpyo.util.metric.Metric` class makes it easy to read, write, and store one or metrics
1212
of the same type, all the while preserving types for each value in a metric. It is an abstract
13-
base class decorated by `dataclassees <https://docs.python.org/3/library/dataclasses.html>`_, or
14-
`attr <https://www.attrs.org/en/stable/examples.html>`_, with attributes storing one or more typed
15-
values.
13+
base class decorated by `@dataclass <https://docs.python.org/3/library/dataclasses.html>`_, or
14+
`@attr.s <https://www.attrs.org/en/stable/examples.html>`_, with attributes storing one or more
15+
typed values. If using multiple layers of inheritance, keep in mind that it's not possible to mix
16+
these dataclass utils, e.g. a dataclasses class derived from an attr class will not appropriately
17+
initialize the values of the attr superclass.
1618
1719
Examples
1820
~~~~~~~~
@@ -29,6 +31,7 @@
2931
... age: int
3032
3133
or using attr:
34+
3235
.. code-block:: python
3336
3437
>>> from fgpyo.util.metric import Metric
@@ -112,6 +115,7 @@
112115
["first last", "42"]
113116
114117
"""
118+
115119
from abc import ABC
116120
from enum import Enum
117121
from pathlib import Path
@@ -142,7 +146,7 @@ class Metric(ABC, Generic[MetricType]):
142146

143147
def values(self) -> Iterator[Any]:
144148
"""An iterator over attribute values in the same order as the header."""
145-
for field in inspect.get_fields(self.__class__): # type: ignore
149+
for field in inspect.get_fields(self.__class__): # type: ignore[arg-type]
146150
yield getattr(self, field.name)
147151

148152
def formatted_values(self) -> List[str]:
@@ -178,15 +182,15 @@ def read(cls, path: Path, ignore_extra_fields: bool = True) -> Iterator[Any]:
178182
missing_from_class = file_fields.difference(class_fields)
179183
missing_from_file = class_fields.difference(file_fields)
180184

181-
field_name_to_attribute = inspect.get_fields_dict(cls) # type: ignore
185+
field_name_to_attribute = inspect.get_fields_dict(cls) # type: ignore[arg-type]
182186

183187
# ignore class fields that are missing from the file (via header) if they're optional
184188
# or have a default
185189
if len(missing_from_file) > 0:
186190
fields_with_defaults = [
187191
field
188192
for field in missing_from_file
189-
if inspect.attribute_has_default(field_name_to_attribute[field])
193+
if inspect._attribute_has_default(field_name_to_attribute[field])
190194
]
191195
# remove optional class fields from the fields
192196
missing_from_file = missing_from_file.difference(fields_with_defaults)
@@ -258,7 +262,7 @@ def write(cls, path: Path, *values: MetricType) -> None:
258262
@classmethod
259263
def header(cls) -> List[str]:
260264
"""The list of header values for the metric."""
261-
return [a.name for a in inspect.get_fields(cls)] # type: ignore
265+
return [a.name for a in inspect.get_fields(cls)] # type: ignore[arg-type]
262266

263267
@classmethod
264268
def format_value(cls, value: Any) -> str:

0 commit comments

Comments
 (0)