Skip to content

Commit

Permalink
Finally some coherence about how to do this
Browse files Browse the repository at this point in the history
More tests needed for repr, pickling, equality when fields are included, hashing, and stability as dict keys when merging dicts.
  • Loading branch information
jace committed Nov 23, 2023
1 parent cb0e816 commit 1b60518
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 100 deletions.
4 changes: 4 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ exclude_lines =

# Ignore stub code
\.\.\.

# Ignore type checking declarations
if TYPE_CHECKING:
if t.TYPE_CHECKING:
2 changes: 1 addition & 1 deletion src/coaster/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from flask.json.provider import DefaultJSONProvider
from flask.sessions import SecureCookieSessionInterface

try: # Flask >= 3.0
try: # Flask >= 3.0 # pragma: no cover
from flask.sansio.app import App as FlaskApp
except ModuleNotFoundError: # Flask < 3.0
from flask import Flask as FlaskApp
Expand Down
2 changes: 1 addition & 1 deletion src/coaster/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from flask import g, request, session
from flask.config import Config

try: # Flask >= 3.0
try: # Flask >= 3.0 # pragma: no cover
from flask.sansio.app import App as FlaskApp
except ModuleNotFoundError:
from flask import Flask as FlaskApp
Expand Down
2 changes: 1 addition & 1 deletion src/coaster/sqlalchemy/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class MyModel(BaseMixin[int], Model): # Integer serial primary key; alt: UUID

from flask import current_app, url_for

try: # Flask >= 3.0
try: # Flask >= 3.0 # pragma: no cover
from flask.sansio.app import App as FlaskApp
except ModuleNotFoundError: # Flask < 3.0
from flask import Flask as FlaskApp
Expand Down
210 changes: 125 additions & 85 deletions src/coaster/utils/classes.py

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/coaster/views/classview.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from flask import abort, g, has_app_context, make_response, redirect, request
from flask.typing import ResponseReturnValue

try: # Flask >= 3.0
try: # Flask >= 3.0 # pragma: no cover
from flask.sansio.app import App as FlaskApp
from flask.sansio.blueprints import Blueprint, BlueprintSetupState
except ModuleNotFoundError: # Flask < 3.0
Expand Down
54 changes: 43 additions & 11 deletions tests/coaster_tests/utils_classes_dataclass_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,25 @@
# pylint: disable=redefined-outer-name,unused-variable

import typing as t
from dataclasses import dataclass
from dataclasses import FrozenInstanceError, dataclass
from enum import Enum

import pytest

from coaster.utils import DataclassFromType


@dataclass(frozen=True)
@dataclass(frozen=True, eq=False)
class StringMetadata(DataclassFromType, str):
description: str
extra: t.Optional[str] = None


@dataclass(frozen=True, eq=True)
class DoubleString(DataclassFromType, str):
second: str


class MetadataEnum(StringMetadata, Enum):
FIRST = "first", "First string"
SECOND = "second", "Second string", "Optional extra"
Expand All @@ -36,11 +41,23 @@ def b2() -> StringMetadata:
return StringMetadata('b', "Also B string", "Extra metadata")


def test_required_base_type() -> None:
with pytest.raises(
TypeError,
match="Subclasses must specify the data type as the second base class",
):
def test_no_init() -> None:
"""DataclassFromType cannot be instantiated."""
with pytest.raises(TypeError, match="cannot be directly instantiated"):
DataclassFromType(0)


def test_first_base() -> None:
"""DataclassFromType must be the first base in a subclass."""
with pytest.raises(TypeError, match="must be the first base"):

class WrongSubclass(str, DataclassFromType):
pass


def test_required_data_type() -> None:
"""Subclasses must have a second base class for the data type."""
with pytest.raises(TypeError, match="second base class"):

class MissingDataType(DataclassFromType):
pass
Expand All @@ -51,6 +68,18 @@ class GivenDataType(DataclassFromType, int):
assert GivenDataType('0') == 0 # Same as int('0') == 0


def test_immutable_data_type() -> None:
"""The data type must be immutable."""

class Immutable(DataclassFromType, tuple):
pass

with pytest.raises(TypeError, match="data type must be immutable"):

class Mutable(DataclassFromType, list):
pass


def test_annotated_str(
a: StringMetadata, b: StringMetadata, b2: StringMetadata
) -> None:
Expand Down Expand Up @@ -83,15 +112,18 @@ def test_dataclass_fields_set(
a: StringMetadata, b: StringMetadata, b2: StringMetadata
) -> None:
"""Confirm dataclass fields have been set."""
assert a.self_ == 'a'
assert a.self == 'a'
assert a.description == "A string"
assert a.extra is None
assert b.self_ == 'b'
assert b.self == 'b'
assert b.description == "B string"
assert b.extra is None
assert b2.self_ == 'b'
assert b2.self == 'b'
assert b2.description == "Also B string"
assert b2.extra == "Extra metadata"
# Confirm self cannot be set
with pytest.raises(FrozenInstanceError):
a.self = 'b' # type: ignore[misc]


def test_dict_keys(a: StringMetadata, b: StringMetadata, b2: StringMetadata):
Expand All @@ -106,7 +138,7 @@ def test_dict_keys(a: StringMetadata, b: StringMetadata, b2: StringMetadata):

def test_metadata_enum() -> None:
assert isinstance(MetadataEnum.FIRST, str)
assert MetadataEnum.FIRST.self_ == "first"
assert MetadataEnum.FIRST.self == "first"
assert MetadataEnum.FIRST == "first"
assert MetadataEnum.SECOND == "second" # type: ignore[unreachable]
assert MetadataEnum['FIRST'] is MetadataEnum.FIRST
Expand Down

0 comments on commit 1b60518

Please sign in to comment.