Skip to content

Commit

Permalink
fix(config): set commit parser opt defaults based on parser choice (#782
Browse files Browse the repository at this point in the history
)
  • Loading branch information
codejedi365 committed Feb 6, 2024
1 parent 279b680 commit 9c594fb
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 45 deletions.
74 changes: 54 additions & 20 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -207,27 +207,61 @@ or transformation.
For more information, see :ref:`commit-parsing-parser-options`.
The default values are the defaults for :ref:`commit-parser-angular`
**Default:**
.. code-block:: toml
The default value for this setting depends on what you specify as
:ref:`commit_parser <config-commit-parser>`. The table below outlines
the expections from ``commit_parser`` value to default options value.
================== == =================================
``commit_parser`` Default ``commit_parser_options``
================== == =================================
``"angular"`` -> .. code-block:: toml
[tool.semantic_release.commit_parser_options]
allowed_types = [
"build", "chore", "ci", "docs", "feat", "fix",
"perf", "style", "refactor", "test"
]
minor_types = ["feat"]
patch_types = ["fix", "perf"]
``"emoji"`` -> .. code-block:: toml
[tool.semantic_release.commit_parser_options]
major_tags = [":boom:"]
minor_tags = [
":sparkles:", ":children_crossing:", ":lipstick:",
":iphone:", ":egg:", ":chart_with_upwards_trend:"
]
patch_tags = [
":ambulance:", ":lock:", ":bug:", ":zap:", ":goal_net:",
":alien:", ":wheelchair:", ":speech_balloon:", ":mag:",
":apple:", ":penguin:", ":checkered_flag:", ":robot:",
":green_apple:"
]
``"scipy"`` -> .. code-block:: toml
[tool.semantic_release.commit_parser_options]
allowed_tags = [
"API", "DEP", "ENH", "REV", "BUG", "MAINT", "BENCH",
"BLD", "DEV", "DOC", "STY", "TST", "REL", "FEAT", "TEST",
]
major_tags = ["API",]
minor_tags = ["DEP", "DEV", "ENH", "REV", "FEAT"]
patch_tags = ["BLD", "BUG", "MAINT"]
``"tag"`` -> .. code-block:: toml
[tool.semantic_release.commit_parser_options]
minor_tag = ":sparkles:"
patch_tag = ":nut_and_bolt:"
``"module:class"`` -> ``**module:class.parser_options()``
================== == =================================
**Default:** ``ParserOptions { ... }``, where ``...`` depends on
:ref:`commit_parser <config-commit-parser>` as indicated above.
[tool.semantic_release.commit_parser_options]
allowed_tags = [
"build",
"chore",
"ci",
"docs",
"feat",
"fix",
"perf",
"style",
"refactor",
"test",
]
minor_tags = ["feat"]
patch_tags = ["fix", "perf"]
.. _config-logging-use-named-masks:
Expand Down
59 changes: 38 additions & 21 deletions semantic_release/cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@
import logging
import os
import re
from dataclasses import dataclass
from collections.abc import Mapping
from dataclasses import dataclass, is_dataclass
from enum import Enum
from pathlib import Path
from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type, Union

from git import Actor
from git.repo.base import Repo
from jinja2 import Environment
from pydantic import BaseModel, model_validator
from typing_extensions import Literal
from pydantic import BaseModel, Field, RootModel, ValidationError, model_validator
from typing_extensions import Annotated, Literal

from semantic_release import hvcs
from semantic_release.changelog import environment
Expand All @@ -38,6 +39,7 @@
)

log = logging.getLogger(__name__)
NonEmptyString = Annotated[str, Field(..., min_length=1)]


class HvcsClient(str, Enum):
Expand All @@ -46,7 +48,7 @@ class HvcsClient(str, Enum):
GITEA = "gitea"


_known_commit_parsers = {
_known_commit_parsers: Dict[str, type[CommitParser]] = {
"angular": AngularCommitParser,
"emoji": EmojiCommitParser,
"scipy": ScipyCommitParser,
Expand Down Expand Up @@ -136,24 +138,9 @@ class RawConfig(BaseModel):
env="GIT_COMMIT_AUTHOR", default=DEFAULT_COMMIT_AUTHOR
)
commit_message: str = COMMIT_MESSAGE
commit_parser: str = "angular"
commit_parser: NonEmptyString = "angular"
# It's up to the parser_options() method to validate these
commit_parser_options: Dict[str, Any] = {
"allowed_tags": [
"build",
"chore",
"ci",
"docs",
"feat",
"fix",
"perf",
"style",
"refactor",
"test",
],
"minor_tags": ["feat"],
"patch_tags": ["fix", "perf"],
}
commit_parser_options: Dict[str, Any] = {}
logging_use_named_masks: bool = False
major_on_zero: bool = True
remote: RemoteConfig = RemoteConfig()
Expand All @@ -162,6 +149,36 @@ class RawConfig(BaseModel):
version_toml: Optional[Tuple[str, ...]] = None
version_variables: Optional[Tuple[str, ...]] = None

@model_validator(mode="after")
def set_default_opts(self) -> RawConfig:
# Set the default parser options for the given commit parser when no user input is given
if not self.commit_parser_options and self.commit_parser:
parser_opts_type = None
# If the commit parser is a known one, pull the default options object from it
if self.commit_parser in _known_commit_parsers:
parser_opts_type = _known_commit_parsers[self.commit_parser].parser_options
else:
# if its a custom parser, try to import it and pull the default options object type
custom_class = dynamic_import(self.commit_parser)
if hasattr(custom_class, "parser_options"):
parser_opts_type = custom_class.parser_options

# from either the custom opts class or the known parser opts class, create an instance
if callable(parser_opts_type):
opts_obj = parser_opts_type()
# if the opts object is a dataclass, wrap it in a RootModel so it can be transformed to a Mapping
opts_obj = opts_obj if not is_dataclass(opts_obj) else RootModel(opts_obj)
# Must be a mapping, so if it's a BaseModel, dump the model to a dict
self.commit_parser_options = (
opts_obj.model_dump()
if isinstance(opts_obj, (BaseModel, RootModel))
else opts_obj
)
if not isinstance(self.commit_parser_options, Mapping):
raise ValidationError(f"Invalid parser options: {opts_obj}. Must be a mapping.")

return self


@dataclass
class GlobalCommandLineOptions:
Expand Down
2 changes: 1 addition & 1 deletion semantic_release/commit_parser/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from git.objects.commit import Commit


class ParserOptions:
class ParserOptions(dict):
"""
ParserOptions should accept the keyword arguments they are interested in
from configuration and process them as desired, ultimately creating attributes
Expand Down
59 changes: 57 additions & 2 deletions tests/unit/semantic_release/cli/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import pytest
import tomlkit
from pydantic import ValidationError
from pydantic import RootModel, ValidationError

from semantic_release.cli.config import (
EnvConfigVar,
Expand All @@ -14,7 +14,14 @@
RawConfig,
RuntimeContext,
)
from semantic_release.commit_parser.angular import AngularParserOptions
from semantic_release.commit_parser.emoji import EmojiParserOptions
from semantic_release.commit_parser.scipy import ScipyParserOptions
from semantic_release.commit_parser.tag import TagParserOptions
from semantic_release.const import DEFAULT_COMMIT_AUTHOR
from semantic_release.enums import LevelBump

from tests.util import CustomParserOpts

if TYPE_CHECKING:
from typing import Any
Expand All @@ -27,9 +34,10 @@
({"type": HvcsClient.GITLAB.value}, EnvConfigVar(env="GITLAB_TOKEN")),
({"type": HvcsClient.GITEA.value}, EnvConfigVar(env="GITEA_TOKEN")),
({}, EnvConfigVar(env="GH_TOKEN")), # default not provided -> means Github
({"type": HvcsClient.GITHUB.value, "token": {'env': "CUSTOM_TOKEN"}}, EnvConfigVar(env="CUSTOM_TOKEN")),
],
)
def test_load_hvcs_default_token(remote_config: dict[str, Any], expected_token):
def test_load_hvcs_default_token(remote_config: dict[str, Any], expected_token: EnvConfigVar):
raw_config = RawConfig.model_validate(
{
"remote": remote_config,
Expand All @@ -49,6 +57,53 @@ def test_invalid_hvcs_type(remote_config: dict[str, Any]):
assert "remote.type" in str(excinfo.value)


@pytest.mark.parametrize(
"commit_parser, expected_parser_opts",
[
(None, RootModel(AngularParserOptions()).model_dump()), # default not provided -> means angular
("angular", RootModel(AngularParserOptions()).model_dump()),
("emoji", RootModel(EmojiParserOptions()).model_dump()),
("scipy", RootModel(ScipyParserOptions()).model_dump()),
("tag", RootModel(TagParserOptions()).model_dump()),
("tests.util:CustomParserWithNoOpts", {}),
("tests.util:CustomParserWithOpts", RootModel(CustomParserOpts()).model_dump()),
],
)
def test_load_default_parser_opts(commit_parser: str | None, expected_parser_opts: dict[str, Any]):
raw_config = RawConfig.model_validate(
# Since TOML does not support NoneTypes, we need to not include the key
{ "commit_parser": commit_parser } if commit_parser else {}
)
assert expected_parser_opts == raw_config.commit_parser_options


def test_load_user_defined_parser_opts():
user_defined_opts = {
"allowed_tags": ["foo", "bar", "baz"],
"minor_tags": ["bar"],
"patch_tags": ["baz"],
"default_bump_level": LevelBump.PATCH.value,
}
raw_config = RawConfig.model_validate(
{
"commit_parser": "angular",
"commit_parser_options": user_defined_opts,
}
)
assert user_defined_opts == raw_config.commit_parser_options


@pytest.mark.parametrize("commit_parser", [""])
def test_invalid_commit_parser_value(commit_parser: str):
with pytest.raises(ValidationError) as excinfo:
RawConfig.model_validate(
{
"commit_parser": commit_parser,
}
)
assert "commit_parser" in str(excinfo.value)


def test_default_toml_config_valid(example_project):
default_config_file = example_project / "default.toml"
default_config_file.write_text(
Expand Down
19 changes: 18 additions & 1 deletion tests/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@
import string
from contextlib import contextmanager
from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING, Any, Iterable, TypeVar
from typing import TYPE_CHECKING, Any, Iterable, Tuple, TypeVar

from pydantic.dataclasses import dataclass

from semantic_release.changelog.context import make_changelog_context
from semantic_release.changelog.release_history import ReleaseHistory
from semantic_release.cli.commands import main
from semantic_release.commit_parser._base import CommitParser, ParserOptions
from semantic_release.commit_parser.token import ParseResult

if TYPE_CHECKING:
import filecmp
Expand Down Expand Up @@ -136,3 +140,16 @@ def __getattr__(self, name: str) -> Any:
for name, method in mocked_methods.items():
setattr(MockGitCommandWrapperType, f"mocked_{name}", method)
return MockGitCommandWrapperType


class CustomParserWithNoOpts(CommitParser[ParseResult, ParserOptions]):
parser_options = ParserOptions


@dataclass
class CustomParserOpts(ParserOptions):
allowed_tags: Tuple[str, ...] = ("new", "custom") # noqa: UP006


class CustomParserWithOpts(CommitParser[ParseResult, CustomParserOpts]):
parser_options = CustomParserOpts

0 comments on commit 9c594fb

Please sign in to comment.