Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/metric with dataclass or attr #80

Merged
merged 1 commit into from
Apr 8, 2024

Conversation

TedBrookings
Copy link
Contributor

Solving the issue was not particularly easy, but the real battle is with mypy. Reviewers should be aware that both dataclasses and attr modules have weird enough issues with type hints that they basically lie; and mypy has special code to figure out what's going on. Thus I had to make use of # type: ignore in multiple locations in actual code, and had to completely exclude test_metric.py from even being checked.

Also in an offline conversation with @tfenne I dropped changing attr from a package requirement to a dev/test requirement, because several attr-decorated classes in the main fgpyo codebase use the slots and kw_only keywords, which are only supported in dataclasses for python versions >= 3.10.

@TedBrookings TedBrookings force-pushed the feature/metric-with-dataclass-or-attr branch 2 times, most recently from a5bec02 to a1df504 Compare November 27, 2023 23:22
Copy link

codecov bot commented Nov 27, 2023

Codecov Report

Attention: Patch coverage is 85.85859% with 14 lines in your changes are missing coverage. Please review.

Project coverage is 87.80%. Comparing base (305530c) to head (10e4689).

❗ Current head 10e4689 differs from pull request most recent head cf2a4dd. Consider uploading reports for the commit cf2a4dd to get more accurate results

Files Patch % Lines
fgpyo/util/inspect.py 81.08% 12 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #80      +/-   ##
==========================================
- Coverage   94.00%   87.80%   -6.21%     
==========================================
  Files          33       16      -17     
  Lines        3403     1738    -1665     
  Branches      694      370     -324     
==========================================
- Hits         3199     1526    -1673     
- Misses        135      144       +9     
+ Partials       69       68       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@TedBrookings
Copy link
Contributor Author

I should add a few tests to cover a handful of helper functions I created in util.inspect. That should likely bring codecov back up to target (or close to it)

@TedBrookings TedBrookings force-pushed the feature/metric-with-dataclass-or-attr branch 2 times, most recently from 569af29 to dbfb9b8 Compare November 28, 2023 17:23
@TedBrookings
Copy link
Contributor Author

I tried to address codecov. There were three categories of issue

  1. Lines that should be covered by tests. I added tests to cover them.
  2. Lines that probably shouldn't be covered. e.g. try/catch blocks for ImportError when attr is missing. I could drop these since the issue goal is no longer to remove attr. It feels like a plausible someday goal, and adds relatively little cost. I'm inclined to leave those lines unchecked, but am happy to remove them if that's the preference.
  3. Lines that are in test files! Those should not be checked at all. I added a codecov.yml file to the project to exclude them.

The final math makes codecov unhappy. I'm inclined to leave things as they stand.

Copy link
Member

@clintval clintval left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only documentation requested in places where I couldn't quite understand what some of the variables were necessary for unless I scanned elsewhere in the code. I'm assuming attrs is used elsewhere in fgpyo so we can't make it an install optional can we?


@length.validator
def _validate_length(self, attribute: Any, value: int) -> None:
def __post_init__(self) -> None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codecov says this branch is uncovered which makes me think attrs is not calling __post_init__ after initialization. Could you add a unit test to ensure this isn't now dead code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was a typo, should have been __attrs_post_init__. Good catch, thanks! I added a test.

As for your top-level question, attrs is used extensively throughout fgpyo. The original mandate was to replace attrs usages with dataclasses, but as I mentioned in the top comment, this can't be done until we drop support for python < 3.10.

fgpyo/util/metric.py Outdated Show resolved Hide resolved
fgpyo/util/metric.py Outdated Show resolved Hide resolved
fgpyo/util/inspect.py Show resolved Hide resolved
Comment on lines 53 to 65
def get_attr_fields(cls: type) -> Tuple[dataclasses.Field, ...]: # type: ignore
return ()

def get_attr_fields_dict(cls: type) -> Dict[str, dataclasses.Field]: # type: ignore
return {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstrings on all public functions please.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added them. In this case these functions will never be called, and are basically there for static checking. I added an additional comment explaining this too.

Comment on lines 332 to 337
def get_fields(cls: Type[DataclassesOrAttrClass]) -> Tuple[FieldType, ...]:
if not (is_dataclasses_class(cls) or is_attr_class(cls)):
raise TypeError("cls must a dataclasses or attr class")
return (get_dataclasses_fields(cls) if is_dataclasses_class(cls) else ()) + (
get_attr_fields(cls) if is_attr_class(cls) else () # type: ignore
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this return a set so no one relies on the order for anything meaningful?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out that you can't mix attr and dataclasses classes (the constructors are not smart enough to pear into the superclass and set the fields, so you can't even initialize such a hybrid), so I refactored this to simply return one or the other or raise ValueError.

fgpyo/util/inspect.py Outdated Show resolved Hide resolved
fgpyo/util/metric.py Outdated Show resolved Hide resolved
fgpyo/util/metric.py Show resolved Hide resolved
fgpyo/util/tests/test_inspect.py Show resolved Hide resolved
@clintval clintval assigned TedBrookings and unassigned clintval Feb 29, 2024
@TedBrookings TedBrookings force-pushed the feature/metric-with-dataclass-or-attr branch 2 times, most recently from 0fa5e0d to d77edd7 Compare March 4, 2024 17:50
Copy link
Member

@clintval clintval left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a masterclass in Python type trickery.

Thanks for your hard and careful work!

My only lingering concern is exposing more inspect code to the public API and then having users accidentally (or purposefully) import some of the code when we probably don't want that to happen. How do you feel about that? Do we have any easy options since the inspect namespace is already "public"?

fgpyo/util/inspect.py Outdated Show resolved Hide resolved

def is_attr_class(cls: type) -> bool: # type: ignore[arg-type]
"""Return True if the class is an attr class, and False otherwise"""
return hasattr(cls, "__attrs_attrs__")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this unit tested?

Comment on lines 88 to 98
MISSING_OR_NONE = {*MISSING, None}
DataclassesOrAttrClass = Union[DataclassesProtocol, AttrsInstance]
FieldType: TypeAlias = Union[dataclasses.Field, Attribute]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would still document these. It makes sense what they are, but a reader will wonder what they are meant to be used for. Document like this so Sphinx picks it up:

AttrFromType = TypeVar("AttrFromType")
"""TypeVar to allow attr_from to be used with either an attr class or a dataclasses class."""

@@ -1,16 +1,12 @@
import functools
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's too late to propose making inspect.py "private" (e.g. _inspect.py) but I'm wondering if you think any of the public API is too tempting or dangerous for users to accidentally import and use and rely on. If yes, then we could go and prefix lots of what you added with _ to hide more things.

@TedBrookings TedBrookings force-pushed the feature/metric-with-dataclass-or-attr branch from 3bfbdca to 10e4689 Compare March 15, 2024 17:27
@TedBrookings TedBrookings requested a review from nh13 March 18, 2024 13:33
@TedBrookings TedBrookings assigned nh13 and unassigned TedBrookings Mar 18, 2024
@TedBrookings
Copy link
Contributor Author

@nh13 I think I've responded to all the substantial issues. @clintval has requested that you give a thumbs-up before I merge.

Copy link
Member

@nh13 nh13 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve based on conversation to add a little more docs to the test metric module and perhaps a type hint or two in inspect, but don't spend more than 15 minutes, and if it doesn't work it doesn't work.

* Update util.metric and related util.inspect modules to work with
  dataclasses or attr
* Update test_metric to test both dataclasses and attr classes

Closes #45
@TedBrookings TedBrookings force-pushed the feature/metric-with-dataclass-or-attr branch from 88be57f to cf2a4dd Compare April 8, 2024 16:53
@TedBrookings TedBrookings merged commit f6f00df into main Apr 8, 2024
6 checks passed
@TedBrookings TedBrookings deleted the feature/metric-with-dataclass-or-attr branch April 8, 2024 16:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants