From cba78110b570e19e7fdc2753e2e1a39979012bb7 Mon Sep 17 00:00:00 2001 From: Ryan Soley Date: Mon, 12 Aug 2024 12:48:22 -0400 Subject: [PATCH 1/4] override domain project init --- rubicon_ml/domain/project.py | 80 ++++++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 9 deletions(-) diff --git a/rubicon_ml/domain/project.py b/rubicon_ml/domain/project.py index ff159590..cecd0a35 100644 --- a/rubicon_ml/domain/project.py +++ b/rubicon_ml/domain/project.py @@ -1,18 +1,80 @@ -from __future__ import annotations +import datetime +import logging +import uuid +from dataclasses import dataclass +from typing import TYPE_CHECKING, Optional -from dataclasses import dataclass, field -from datetime import datetime -from typing import Optional +if TYPE_CHECKING: + from rubicon_ml.domain.utils import TrainingMetadata -from rubicon_ml.domain.utils import TrainingMetadata, uuid +LOGGER = logging.getLogger() -@dataclass +@dataclass(init=False) class Project: + """A domain-level project. + + Parameters + ---------- + name : str + The project's name. + created_at : datetime, optional + The date and time the project was created. Defaults to `None` and uses + `datetime.datetime.now` to generate a UTC timestamp. `created_at` should be + left as `None` to allow for automatic generation. + description : str, optional + A description of the project. Defaults to `None`. + github_url : str, optional + The URL of the GitHub repository associated with this project. Defaults to + `None`. + id : str, optional + The project's unique identifier. Defaults to `None` and uses `uuid.uuid4` + to generate a unique ID. `id` should be left `None` to allow for automatic + generation. + training_metadata : rubicon_ml.domain.utils.TrainingMetadata, optional + Additional metadata pertaining to any data this project was trained on. + Defaults to `None`. + """ + name: str - id: str = field(default_factory=uuid.uuid4) + created_at: Optional[datetime] = None description: Optional[str] = None github_url: Optional[str] = None - training_metadata: Optional[TrainingMetadata] = None - created_at: datetime = field(default_factory=datetime.utcnow) + id: Optional[str] = None + training_metadata: Optional["TrainingMetadata"] = None + + def __init__( + self, + name: str, + created_at: Optional[datetime] = None, + description: Optional[str] = None, + github_url: Optional[str] = None, + id: Optional[str] = None, + training_metadata: Optional["TrainingMetadata"] = None, + **kwargs, + ): + """Initialize this domain project.""" + + self.name = name + + self.created_at = created_at + self.description = description + self.github_url = github_url + self.id = id + self.training_metadata = training_metadata + + if self.created_at is None: + try: # `datetime.UTC` added & `datetime.utcnow` deprecated in Python 3.11 + self.created_at = datetime.datetime.now(datetime.UTC) + except AttributeError: + self.created_at = datetime.datetime.utcnow() + + if self.id is None: + self.id = str(uuid.uuid4()) + + if kwargs: # replaces `dataclass` behavior of erroring on unexpected kwargs + LOGGER.warn( + f"{self.__class__.__name__}.__init__() got an unexpected keyword " + f"argument(s): `{'`, `'.join([key for key in kwargs])}`" + ) From 207625854b9af9d70c34a04a112655d9f1a24f96 Mon Sep 17 00:00:00 2001 From: Ryan Soley Date: Mon, 12 Aug 2024 12:56:49 -0400 Subject: [PATCH 2/4] add domain extra kwarg test --- tests/unit/domain/test_domain.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 tests/unit/domain/test_domain.py diff --git a/tests/unit/domain/test_domain.py b/tests/unit/domain/test_domain.py new file mode 100644 index 00000000..ed442695 --- /dev/null +++ b/tests/unit/domain/test_domain.py @@ -0,0 +1,16 @@ +import pytest + +from rubicon_ml.domain.project import Project + + +@pytest.mark.parametrize( + ["domain_cls", "required_kwargs"], + [(Project, {"name": "test_domain_extra_kwargs"})], +) +def test_domain_extra_kwargs(domain_cls, required_kwargs): + domain = domain_cls(extra="extra", **required_kwargs) + + assert "extra" not in domain.__dict__ + + for key, value in required_kwargs.items(): + assert getattr(domain, key) == value From 6ca8c77b33bd5c42cc2e1295309dbe307b04f5b4 Mon Sep 17 00:00:00 2001 From: Ryan Soley Date: Mon, 12 Aug 2024 13:38:21 -0400 Subject: [PATCH 3/4] parametrize test and test warning --- rubicon_ml/domain/project.py | 2 +- tests/unit/domain/test_domain.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/rubicon_ml/domain/project.py b/rubicon_ml/domain/project.py index cecd0a35..528aa0f8 100644 --- a/rubicon_ml/domain/project.py +++ b/rubicon_ml/domain/project.py @@ -74,7 +74,7 @@ def __init__( self.id = str(uuid.uuid4()) if kwargs: # replaces `dataclass` behavior of erroring on unexpected kwargs - LOGGER.warn( + LOGGER.warning( f"{self.__class__.__name__}.__init__() got an unexpected keyword " f"argument(s): `{'`, `'.join([key for key in kwargs])}`" ) diff --git a/tests/unit/domain/test_domain.py b/tests/unit/domain/test_domain.py index ed442695..ba3a086f 100644 --- a/tests/unit/domain/test_domain.py +++ b/tests/unit/domain/test_domain.py @@ -1,3 +1,5 @@ +from unittest import mock + import pytest from rubicon_ml.domain.project import Project @@ -8,9 +10,15 @@ [(Project, {"name": "test_domain_extra_kwargs"})], ) def test_domain_extra_kwargs(domain_cls, required_kwargs): - domain = domain_cls(extra="extra", **required_kwargs) + with mock.patch( + f"rubicon_ml.domain.{domain_cls.__name__.lower()}.LOGGER.warning" + ) as logger_warning: + domain = domain_cls(extra="extra", **required_kwargs) - assert "extra" not in domain.__dict__ + logger_warning.assert_called_once_with( + f"{domain_cls.__name__}.__init__() got an unexpected keyword argument(s): `extra`", + ) + assert "extra" not in domain.__dict__ for key, value in required_kwargs.items(): assert getattr(domain, key) == value From af28ecc3da9d5a813a8ea2be887a501893abc4e1 Mon Sep 17 00:00:00 2001 From: Ryan Soley Date: Mon, 12 Aug 2024 14:12:41 -0400 Subject: [PATCH 4/4] clean up --- rubicon_ml/domain/project.py | 6 +++--- tests/unit/domain/test_domain.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rubicon_ml/domain/project.py b/rubicon_ml/domain/project.py index 528aa0f8..570742f9 100644 --- a/rubicon_ml/domain/project.py +++ b/rubicon_ml/domain/project.py @@ -29,7 +29,7 @@ class Project: `None`. id : str, optional The project's unique identifier. Defaults to `None` and uses `uuid.uuid4` - to generate a unique ID. `id` should be left `None` to allow for automatic + to generate a unique ID. `id` should be left as `None` to allow for automatic generation. training_metadata : rubicon_ml.domain.utils.TrainingMetadata, optional Additional metadata pertaining to any data this project was trained on. @@ -38,7 +38,7 @@ class Project: name: str - created_at: Optional[datetime] = None + created_at: Optional[datetime.datetime] = None description: Optional[str] = None github_url: Optional[str] = None id: Optional[str] = None @@ -47,7 +47,7 @@ class Project: def __init__( self, name: str, - created_at: Optional[datetime] = None, + created_at: Optional[datetime.datetime] = None, description: Optional[str] = None, github_url: Optional[str] = None, id: Optional[str] = None, diff --git a/tests/unit/domain/test_domain.py b/tests/unit/domain/test_domain.py index ba3a086f..2b4bdec6 100644 --- a/tests/unit/domain/test_domain.py +++ b/tests/unit/domain/test_domain.py @@ -12,10 +12,10 @@ def test_domain_extra_kwargs(domain_cls, required_kwargs): with mock.patch( f"rubicon_ml.domain.{domain_cls.__name__.lower()}.LOGGER.warning" - ) as logger_warning: + ) as mock_logger_warning: domain = domain_cls(extra="extra", **required_kwargs) - logger_warning.assert_called_once_with( + mock_logger_warning.assert_called_once_with( f"{domain_cls.__name__}.__init__() got an unexpected keyword argument(s): `extra`", )