diff --git a/src/gitmojis/__init__.py b/src/gitmojis/__init__.py index e69de29..faf89ac 100644 --- a/src/gitmojis/__init__.py +++ b/src/gitmojis/__init__.py @@ -0,0 +1,6 @@ +from .model import Gitmoji, Guide + +__all__ = [ + "Gitmoji", + "Guide", +] diff --git a/src/gitmojis/model.py b/src/gitmojis/model.py new file mode 100644 index 0000000..2cf5117 --- /dev/null +++ b/src/gitmojis/model.py @@ -0,0 +1,53 @@ +from collections import UserList +from dataclasses import dataclass +from typing import Iterable, Literal + + +@dataclass(frozen=True, kw_only=True) +class Gitmoji: + """Represents a single Gitmoji and its data. + + The data model is adapted from the schema of the original Gitmoji project. + + Attributes: + emoji: The emoji symbol representing the Gitmoji. + entity: The HTML entity corresponding to the Gitmoji. + code: The Markdown code of the Gitmoji's emoji. + description: A brief description of changes introduced by commits and pull + requests marked by the Gitmoji. + name: The user-defined name or identifier of the Gitmoji. + semver: The Semantic Versioning level affected by the commits or pull requests + marked by the emoji associated with the Gitmoji, if specified. May be `None` + or one of the following: `"major"`, `"minor"`, `"patch"`. + """ + + emoji: str + entity: str + code: str + description: str + name: str + semver: Literal["major", "minor", "patch"] | None + + +class Guide(UserList[Gitmoji]): + """Represents a list of (a "guide" through) `Gitmoji` objects. + + This class is used to create a collection of `Gitmoji` objects, providing a simple + framework for accessing various Gitmojis and their data. + """ + + def __init__(self, *, gitmojis: Iterable[Gitmoji] | None = None) -> None: + """Construct a new `Guide` object. + + Args: + gitmojis: An optional iterable of `Gitmoji` objects used to create + the guide. If `None`, the guide is initialized as empty. Note + that the data must be passed as a keyword argument, in contrast + to the implementation provided by the base class. + """ + super().__init__(gitmojis) + + @property + def gitmojis(self) -> list[Gitmoji]: + """Return the guide's data with a semantically meaningful attribute name.""" + return self.data diff --git a/tests/test_gitmoji.py b/tests/test_gitmoji.py new file mode 100644 index 0000000..be89ce9 --- /dev/null +++ b/tests/test_gitmoji.py @@ -0,0 +1,36 @@ +from dataclasses import FrozenInstanceError, fields + +import pytest + +from gitmojis.model import Gitmoji + + +@pytest.fixture() +def gitmoji_json(): + return { + "emoji": "🐛", + "entity": "🐛", + "code": ":bug:", + "description": "Fix a bug.", + "name": "bug", + "semver": "patch", + } + + +@pytest.fixture() +def gitmoji(gitmoji_json): + return Gitmoji(**gitmoji_json) + + +def test_gitmoji_init_populates_fields_from_kwargs(gitmoji_json, gitmoji): + for key, value in gitmoji_json.items(): + assert getattr(gitmoji, key) == value + + +@pytest.mark.parametrize( + "field_name", + [field.name for field in fields(Gitmoji)], +) +def test_gitmoji_is_immutable(gitmoji, field_name): + with pytest.raises(FrozenInstanceError): + setattr(gitmoji, field_name, getattr(gitmoji, field_name)) diff --git a/tests/test_guide.py b/tests/test_guide.py new file mode 100644 index 0000000..e51fe95 --- /dev/null +++ b/tests/test_guide.py @@ -0,0 +1,62 @@ +import pytest + +from gitmojis.model import Gitmoji, Guide + + +@pytest.fixture() +def gitmojis_json(): + return [ + { + "emoji": "💥", + "entity": "💥", + "code": ":boom:", + "description": "Introduce breaking changes.", + "name": "boom", + "semver": "major", + }, + { + "emoji": "✨", + "entity": "✨", + "code": ":sparkles:", + "description": "Introduce new features.", + "name": "sparkles", + "semver": "minor", + }, + { + "emoji": "🐛", + "entity": "🐛", + "code": ":bug:", + "description": "Fix a bug.", + "name": "bug", + "semver": "patch", + }, + { + "emoji": "📝", + "entity": "📝", + "code": ":memo:", + "description": "Add or update documentation.", + "name": "memo", + "semver": None, + }, + ] + + +@pytest.fixture() +def guide(gitmojis_json): + return Guide(gitmojis=[Gitmoji(**gitmoji_json) for gitmoji_json in gitmojis_json]) + + +def test_guide_init_raises_if_called_with_positional_argument(): + with pytest.raises(TypeError): + Guide([]) + + +def test_guide_init_succeeds_if_called_with_keyword_argument(): + try: + Guide(gitmojis=[]) + except TypeError: + pytest.fail() + + +def test_guide_gitmojis_returns_data(guide): + assert guide.gitmojis == guide.data