Skip to content

Commit

Permalink
Add validation schema for configuration file
Browse files Browse the repository at this point in the history
Co-Authored-By: Blank Spruce <32396809+BlankSpruce@users.noreply.github.com>
  • Loading branch information
fruzitent and BlankSpruce committed Dec 22, 2023
1 parent 810dd9a commit 196013b
Show file tree
Hide file tree
Showing 9 changed files with 299 additions and 68 deletions.
8 changes: 8 additions & 0 deletions .gersemirc.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/BlankSpruce/gersemi/master/gersemi/configuration.schema.json

color: false
definitions: []
line_length: 80
list_expansion: favour-inlining
quiet: false
unsafe: false
61 changes: 32 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ A formatter to make your CMake code the real treasure.
## Installation

You can install gersemi from PyPI:
```
$ pip3 install gersemi

```shell
pip3 install gersemi
```

## Usage

```
```plain
usage: gersemi [-c] [-i] [--diff] [--default-config] [--version] [-h] [-l INTEGER]
[--unsafe] [-q] [--color] [--definitions src [src ...]]
[--list-expansion {favour-inlining,favour-expansion}]
Expand All @@ -22,49 +23,51 @@ usage: gersemi [-c] [-i] [--diff] [--default-config] [--version] [-h] [-l INTEGE
A formatter to make your CMake code the real treasure.
positional arguments:
src File or directory to format. If only - is provided input is taken
from stdin instead
src File or directory to format. If only `-` is provided, input is
taken from stdin instead.
modes:
-c, --check Check if files require reformatting. Return 0 when there's nothing
to reformat, return 1 when some files would be reformatted
-i, --in-place Format files in-place
--diff Show diff on stdout for each formatted file instead
--default-config Generate default .gersemirc configuration file
-c, --check Check if files require reformatting. Return 0 when there's
nothing to reformat. Return 1 when some files would be
reformatted.
-i, --in-place Format files in-place.
--diff Show diff on stdout for each formatted file instead.
--default-config Generate default .gersemirc configuration file.
--version Show version.
-h, --help Show this help message and exit.
configuration:
By default configuration is loaded from YAML formatted .gersemirc file if it's
available. This file should be placed in one of the common parent directories of source
files. Arguments from command line can be used to override parts of that configuration
or supply them in absence of configuration file.
available. This file should be placed in one of the common parent directories of
source files. Arguments from command line can be used to override parts of that
configuration or supply them in absence of configuration file.
-l INTEGER, --line-length INTEGER
Maximum line length in characters [default: 80]
--unsafe Skip default sanity checks
-q, --quiet Skip printing non-error messages to stderr
--color If --diff is selected showed diff is colorized
Maximum line length in characters. [default: 80]
--unsafe Skip default sanity checks.
-q, --quiet Skip printing non-error messages to stderr.
--color If --diff is selected showed diff is colorized.
--definitions src [src ...]
Files or directories containing custom command definitions
(functions or macros). If only - is provided custom definitions, if
there are any, are taken from stdin instead. Commands from not
deprecated CMake native modules don't have to be provided (check
https://cmake.org/cmake/help/latest/manual/cmake-modules.7.html)
(functions or macros). If only - is provided custom definitions,
if there are any, are taken from stdin instead. Commands from
not deprecated CMake native modules don't have to be provided.
See: https://cmake.org/cmake/help/latest/manual/cmake-modules.7.html
--list-expansion {favour-inlining,favour-expansion}
Switch controls how code is expanded into multiple lines when it's
not possible to keep it formatted in one line. With 'favour-
inlining' (default) the list of entities will be formatted in such
way that sublists might still be formatted into single line as long
as it's possible. With 'favour-expansion' the list of entities will
be formatted in such way that sublists will be completely expanded
once expansion becomes necessary at all.
Switch controls how code is expanded into multiple lines when
it's not possible to keep it formatted in one line. With
"favour-inlining" the list of entities will be formatted in such
way that sublists might still be formatted into single line as
long as it's possible. With "favour-expansion" the list of
entities will be formatted in such way that sublists will be
completely expanded once expansion becomes necessary at all.
[default: favour-inlining]
```

### [pre-commit](https://pre-commit.com/) hook

You can use gersemi with a pre-commit hook by adding the following to `.pre-commit-config.yaml` of your repository:

```yaml
repos:
- repo: https://github.com/BlankSpruce/gersemi
Expand Down
58 changes: 30 additions & 28 deletions gersemi/__main__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import argparse
from dataclasses import fields
import pathlib
import sys
from lark import __version__ as lark_version
from gersemi.configuration import (
make_configuration,
make_default_configuration_file,
Configuration,
ListExpansion,
)
from gersemi.mode import get_mode
from gersemi.return_codes import SUCCESS, FAIL
Expand Down Expand Up @@ -37,28 +39,30 @@ def create_argparser():
"--check",
dest="check_formatting",
action="store_true",
help=f"Check if files require reformatting. "
f"Return {SUCCESS} when there's nothing to reformat, "
f"return {FAIL} when some files would be reformatted",
help=f"""
Check if files require reformatting.
Return {SUCCESS} when there's nothing to reformat.
Return {FAIL} when some files would be reformatted.
""",
)
modes_group.add_argument(
"-i",
"--in-place",
dest="in_place",
action="store_true",
help="Format files in-place",
help="Format files in-place.",
)
modes_group.add_argument(
"--diff",
dest="show_diff",
action="store_true",
help="Show diff on stdout for each formatted file instead",
help="Show diff on stdout for each formatted file instead.",
)
modes_group.add_argument(
"--default-config",
nargs=0,
action=GenerateConfigurationFile,
help="Generate default .gersemirc configuration file",
help="Generate default .gersemirc configuration file.",
)
modes_group.add_argument(
"--version",
Expand All @@ -75,40 +79,39 @@ def create_argparser():
help="Show this help message and exit.",
)

conf_doc: dict[str, str] = {
item.name: item.metadata["description"] for item in fields(Configuration)
}

configuration_group = parser.add_argument_group(
title="configuration",
description="By default configuration is loaded from YAML formatted .gersemirc "
"file if it's available. "
"This file should be placed in one of the common parent directories of source files. "
"Arguments from command line can be used to override parts of that configuration "
"or supply them in absence of configuration file.",
title="configuration", description=Configuration.__doc__
)
configuration_group.add_argument(
"-l",
"--line-length",
metavar="INTEGER",
dest="line_length",
type=int,
help=f"Maximum line length in characters [default: {Configuration().line_length}]",
help=f"{conf_doc['line_length']} [default: {Configuration.line_length}]",
)
configuration_group.add_argument(
"--unsafe",
dest="unsafe",
action="store_true",
help="Skip default sanity checks",
help=conf_doc["unsafe"],
)
configuration_group.add_argument(
"-q",
"--quiet",
dest="quiet",
action="store_true",
help="Skip printing non-error messages to stderr",
help=conf_doc["quiet"],
)
configuration_group.add_argument(
"--color",
dest="color",
action="store_true",
help="If --diff is selected showed diff is colorized",
help=conf_doc["color"],
)
configuration_group.add_argument(
"--definitions",
Expand All @@ -117,30 +120,29 @@ def create_argparser():
default=[],
nargs="+",
type=pathlib.Path,
help="Files or directories containing custom command definitions (functions or macros). "
"If only - is provided custom definitions, if there are any, are taken from stdin instead. "
"Commands from not deprecated CMake native modules don't have to be provided "
"(check https://cmake.org/cmake/help/latest/manual/cmake-modules.7.html)",
help=conf_doc["definitions"],
)
configuration_group.add_argument(
"--list-expansion",
dest="list_expansion",
choices=["favour-inlining", "favour-expansion"],
help="Switch controls how code is expanded into multiple lines when it's not possible "
"to keep it formatted in one line. With 'favour-inlining' (default) the list of entities "
"will be formatted in such way that sublists might still be formatted into single line "
"as long as it's possible. With 'favour-expansion' the list of entities will be formatted "
"in such way that sublists will be completely expanded once expansion becomes necessary "
"at all.",
help=f"""
{conf_doc['list_expansion']}
{ListExpansion.FavourInlining.description}
{ListExpansion.FavourExpansion.description}
[default: {Configuration.list_expansion.value}]
""",
)

parser.add_argument(
dest="sources",
metavar="src",
nargs="*",
type=pathlib.Path,
help="File or directory to format. "
"If only - is provided input is taken from stdin instead",
help="""
File or directory to format.
If only `-` is provided, input is taken from stdin instead.
""",
)

return parser
Expand Down
115 changes: 104 additions & 11 deletions gersemi/configuration.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,115 @@
from contextlib import contextmanager
from dataclasses import asdict, dataclass, fields
from dataclasses import asdict, dataclass, field, fields
from enum import Enum
from hashlib import sha1
from itertools import chain
import os
from pathlib import Path
import textwrap
from typing import Iterable, Optional
import yaml


class ListExpansion(Enum):
FavourInlining = "favour-inlining"
FavourExpansion = "favour-expansion"
def doc(text: str) -> str:
return " ".join(textwrap.dedent(text).splitlines()).strip()


class EnumWithMetadata(Enum):
def __new__(cls, data):
self = object.__new__(cls)
self._value_ = data["value"]
self.description = doc(data["description"])
self.title = data["title"]
return self


class ListExpansion(EnumWithMetadata):
FavourInlining = dict(
value="favour-inlining",
description="""
With "favour-inlining" the list of entities will be formatted in such way that sublists
might still be formatted into single line as long as it's possible.
""",
title="Favour inlining",
)
FavourExpansion = dict(
value="favour-expansion",
description="""
With "favour-expansion" the list of entities will be formatted in such way that sublists
will be completely expanded once expansion becomes necessary at all.
""",
title="Favour expansion",
)


@dataclass
class Configuration:
line_length: int = 80
unsafe: bool = False
quiet: bool = False
color: bool = False
definitions: Iterable[Path] = tuple()
list_expansion: ListExpansion = ListExpansion.FavourInlining
"""
By default configuration is loaded from YAML formatted .gersemirc file if it's available.
This file should be placed in one of the common parent directories of source files.
Arguments from command line can be used to override parts of that configuration or
supply them in absence of configuration file.
"""

line_length: int = field(
default=80,
metadata=dict(
title="Line length",
description="Maximum line length in characters.",
),
)

unsafe: bool = field(
default=False,
metadata=dict(
title="Unsafe",
description="Skip default sanity checks.",
),
)

quiet: bool = field(
default=False,
metadata=dict(
title="Quiet",
description="Skip printing non-error messages to stderr.",
),
)

color: bool = field(
default=False,
metadata=dict(
title="Colorized diff",
description="If --diff is selected showed diff is colorized.",
),
)

definitions: Iterable[Path] = field(
default=tuple(),
metadata=dict(
title="Definitions",
description=doc(
"""
Files or directories containing custom command definitions (functions or macros).
If only - is provided custom definitions, if there are any, are taken from stdin instead.
Commands from not deprecated CMake native modules don't have to be provided.
See: https://cmake.org/cmake/help/latest/manual/cmake-modules.7.html
"""
),
),
)

list_expansion: ListExpansion = field(
default=ListExpansion.FavourInlining,
metadata=dict(
title="List expansion",
description=doc(
"""
Switch controls how code is expanded into multiple lines
when it's not possible to keep it formatted in one line.
"""
),
),
)

def summary(self):
hasher = sha1()
Expand All @@ -33,7 +121,12 @@ def make_default_configuration_file():
default_configuration = Configuration()
d = asdict(default_configuration)
d["list_expansion"] = d["list_expansion"].value
return yaml.safe_dump(d)
result = f"""
# yaml-language-server: $schema=https://raw.githubusercontent.com/BlankSpruce/gersemi/master/gersemi/configuration.schema.json
{yaml.safe_dump(d)}
"""
return result.strip()


def find_common_parent_path(paths: Iterable[Path]) -> Path:
Expand Down
Loading

0 comments on commit 196013b

Please sign in to comment.