Skip to content

Commit

Permalink
Fix mypy errors
Browse files Browse the repository at this point in the history
  • Loading branch information
OmeGak committed Sep 19, 2023
1 parent 0b2b646 commit d026bcf
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 64 deletions.
56 changes: 25 additions & 31 deletions src/unbeheader/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,29 @@

import re

# Dictionary listing the files for which to change the header.
# The key is the extension of the file (without the dot) and the value is another
# dictionary containing two keys:
# - 'regex' : A regular expression matching comments in the given file type
# - 'comments': A dictionary with the comment characters to add to the header.
# There must be a `comment_start` inserted before the header,
# `comment_middle` inserted at the beginning of each line except the
# first and last one, and `comment_end` inserted at the end of the
# header.
SUPPORTED_FILES = {
'py': {
'regex': re.compile(r'((^#|[\r\n]#).*)*'),
'comments': {'comment_start': '#', 'comment_middle': '#', 'comment_end': ''}},
'wsgi': {
'regex': re.compile(r'((^#|[\r\n]#).*)*'),
'comments': {'comment_start': '#', 'comment_middle': '#', 'comment_end': ''}},
'js': {
'regex': re.compile(r'/\*(.|[\r\n])*?\*/|((^//|[\r\n]//).*)*'),
'comments': {'comment_start': '//', 'comment_middle': '//', 'comment_end': ''}},
'jsx': {
'regex': re.compile(r'/\*(.|[\r\n])*?\*/|((^//|[\r\n]//).*)*'),
'comments': {'comment_start': '//', 'comment_middle': '//', 'comment_end': ''}},
'css': {
'regex': re.compile(r'/\*(.|[\r\n])*?\*/'),
'comments': {'comment_start': '/*', 'comment_middle': ' *', 'comment_end': ' */'}},
'scss': {
'regex': re.compile(r'/\*(.|[\r\n])*?\*/|((^//|[\r\n]//).*)*'),
'comments': {'comment_start': '//', 'comment_middle': '//', 'comment_end': ''}},
'sh': {
'regex': re.compile(r'((^#|[\r\n]#).*)*'),
'comments': {'comment_start': '#', 'comment_middle': '#', 'comment_end': ''}},
from .typing import CommentSkeleton
from .typing import SupportedFileType

SUPPORTED_FILE_TYPES: dict[str, SupportedFileType] = {
'py': SupportedFileType(
re.compile(r'((^#|[\r\n]#).*)*'),
CommentSkeleton(comment_start='#', comment_middle='#', comment_end='')),
'wsgi': SupportedFileType(
re.compile(r'((^#|[\r\n]#).*)*'),
CommentSkeleton(comment_start='#', comment_middle='#', comment_end='')),
'js': SupportedFileType(
re.compile(r'/\*(.|[\r\n])*?\*/|((^//|[\r\n]//).*)*'),
CommentSkeleton(comment_start='//', comment_middle='//', comment_end='')),
'jsx': SupportedFileType(
re.compile(r'/\*(.|[\r\n])*?\*/|((^//|[\r\n]//).*)*'),
CommentSkeleton(comment_start='//', comment_middle='//', comment_end='')),
'css': SupportedFileType(
re.compile(r'/\*(.|[\r\n])*?\*/'),
CommentSkeleton(comment_start='/*', comment_middle=' *', comment_end=' */')),
'scss': SupportedFileType(
re.compile(r'/\*(.|[\r\n])*?\*/|((^//|[\r\n]//).*)*'),
CommentSkeleton(comment_start='//', comment_middle='//', comment_end='')),
'sh': SupportedFileType(
re.compile(r'((^#|[\r\n]#).*)*'),
CommentSkeleton(comment_start='#', comment_middle='#', comment_end='')),
}
22 changes: 11 additions & 11 deletions src/unbeheader/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,21 @@
import click
from click import UsageError

from . import SUPPORTED_FILES
from . import SUPPORTED_FILE_TYPES
from .headers import update_header
from .util import cformat
from .util import is_excluded

USAGE = '''
Updates all the headers in the supported files ({supported_files}).
Updates all the headers in the supported files ({supported_file_types}).
By default, all the files tracked by git in the current repository are updated
to the current year.
You can specify a year to update to as well as a file or directory.
This will update all the supported files in the scope including those not tracked
by git. If the directory does not contain any supported files (or if the file
specified is not supported) nothing will be updated.
'''.format(supported_files=', '.join(SUPPORTED_FILES)).strip()
'''.format(supported_file_types=', '.join(SUPPORTED_FILE_TYPES)).strip()


@click.command(help=USAGE)
Expand All @@ -33,12 +33,12 @@
@click.option('--year', '-y', type=click.IntRange(min=1000), default=date.today().year, metavar='YEAR',
help='Indicate the target year')
@click.option('--path', '-p', type=click.Path(exists=True), help='Restrict updates to a specific file or directory')
def main(check: bool, year: int, path: str):
path = Path(path).resolve() if path else None
if path and path.is_dir():
error = _run_on_directory(path, year, check)
elif path and path.is_file():
error = _run_on_file(path, year, check)
def main(check: bool, year: int, path: str) -> None:
_path = Path(path).resolve() if path else None
if _path and _path.is_dir():
error = _run_on_directory(_path, year, check)
elif _path and _path.is_file():
error = _run_on_file(_path, year, check)
else:
error = _run_on_repo(year, check)
if not error:
Expand Down Expand Up @@ -89,8 +89,8 @@ def _run_on_repo(year: int, check: bool) -> bool:
git_file_paths |= set(subprocess.check_output(cmd + untracked_flags, text=True).splitlines())
# Exclude deleted files
git_file_paths -= set(subprocess.check_output(cmd + deleted_flags, text=True).splitlines())
for file_path in git_file_paths:
file_path = Path(file_path).absolute()
for file_path_str in git_file_paths:
file_path = Path(file_path_str).absolute()
if not is_excluded(file_path.parent, Path.cwd()):
if update_header(file_path, year, check):
error = True
Expand Down
10 changes: 6 additions & 4 deletions src/unbeheader/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@
import click
import yaml

from .typing import ConfigDict

# The substring which must be part of a comment block in order for the comment to be updated by the header
DEFAULT_SUBSTRING = 'This file is part of'

# The name of the files containing header configuration
CONFIG_FILE_NAME = '.header.yaml'


def get_config(path: Path, end_year: int) -> dict:
def get_config(path: Path, end_year: int) -> ConfigDict:
"""Get configuration from headers files."""
config = _load_config(path)
_validate_config(config)
Expand All @@ -24,8 +26,8 @@ def get_config(path: Path, end_year: int) -> dict:
return config


def _load_config(path: Path) -> dict:
config = {}
def _load_config(path: Path) -> ConfigDict:
config: ConfigDict = {}
found = False
for dir_path in _walk_to_root(path):
check_path = dir_path / CONFIG_FILE_NAME
Expand All @@ -40,7 +42,7 @@ def _load_config(path: Path) -> dict:
return config


def _validate_config(config: dict):
def _validate_config(config: ConfigDict) -> None:
valid_keys = {'owner', 'start_year', 'substring', 'template'}
mandatory_keys = {'owner', 'template'}
config_keys = set(config)
Expand Down
21 changes: 13 additions & 8 deletions src/unbeheader/headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,28 @@

import click

from . import SUPPORTED_FILES
from . import SUPPORTED_FILE_TYPES
from .config import get_config
from .typing import CommentSkeleton
from .typing import ConfigDict
from .util import cformat


def update_header(file_path: Path, year: int, check: bool = False) -> bool:
"""Update the header of a file."""
config = get_config(file_path, year)
ext = file_path.suffix[1:]
if ext not in SUPPORTED_FILES or not file_path.is_file():
if ext not in SUPPORTED_FILE_TYPES or not file_path.is_file():
return False
if file_path.name.startswith('.'):
return False
return _do_update_header(file_path, config, SUPPORTED_FILES[ext]['regex'], SUPPORTED_FILES[ext]['comments'], check)
return _do_update_header(
file_path, config, SUPPORTED_FILE_TYPES[ext].regex, SUPPORTED_FILE_TYPES[ext].comments, check
)


def _do_update_header(file_path: Path, config: dict, regex: Pattern[str], comments: dict, check: bool) -> bool:
def _do_update_header(file_path: Path, config: ConfigDict, regex: Pattern[str], comments: CommentSkeleton,
check: bool) -> bool:
found = False
content = orig_content = file_path.read_text()
# Do nothing for empty files
Expand All @@ -44,12 +49,12 @@ def _do_update_header(file_path: Path, config: dict, regex: Pattern[str], commen
# file is otherwise empty, we do not want a header in there
content = ''
else:
content = content[:match.start()] + _generate_header(comments | config) + match_end
content = content[:match.start()] + _generate_header(comments._asdict() | config) + match_end
# Strip leading empty characters
content = content.lstrip()
# Add the header if it was not found
if not found:
content = _generate_header(comments | config) + '\n' + content
content = _generate_header(comments._asdict() | config) + '\n' + content
# Readd the shebang line if it was there
if shebang_line:
content = shebang_line + '\n' + content
Expand All @@ -64,7 +69,7 @@ def _do_update_header(file_path: Path, config: dict, regex: Pattern[str], commen
return True


def _generate_header(data: dict) -> str:
def _generate_header(data: ConfigDict) -> str:
if 'start_year' not in data:
data['start_year'] = data['end_year']
if data['start_year'] == data['end_year']:
Expand All @@ -80,7 +85,7 @@ def _generate_header(data: dict) -> str:
return f'{comment}\n'


def _print_results(file_path: Path, found: bool, check: bool):
def _print_results(file_path: Path, found: bool, check: bool) -> None:
ci = os.environ.get('CI') == '1'
if found:
check_msg = 'Incorrect header in {}' if ci else 'Incorrect header in %{white!}{}'
Expand Down
27 changes: 27 additions & 0 deletions src/unbeheader/typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# This file is part of Unbeheader.
# Copyright (C) CERN & UNCONVENTIONAL

from pathlib import Path
from re import Pattern
from typing import Any
from typing import NamedTuple
from typing import TypeAlias

ConfigDict: TypeAlias = dict[str, Any]
PathCache: TypeAlias = dict[Path, bool]


class CommentSkeleton(NamedTuple):
# The string that indicates the start of a comment
comment_start: str
# The string that indicates the continuation of a comment
comment_middle: str
# The string that indicates the end of a comment
comment_end: str


class SupportedFileType(NamedTuple):
# A regular expression matching header comments
regex: Pattern[str]
# A dictionary defining the skeleton of comments
comments: CommentSkeleton
7 changes: 5 additions & 2 deletions src/unbeheader/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@

import re
from pathlib import Path
from re import Match

from colorclass import Color

from .typing import PathCache

# The name of the files that exclude the directory from header updates
EXCLUDE_FILE_NAME = '.no-header'

Expand All @@ -15,7 +18,7 @@ def cformat(string: str) -> Color:
Bold foreground can be achieved by suffixing the color with a '!'.
"""
def repl(m):
def repl(m: Match[str]) -> Color:
bg = bold = ''
if m.group('fg_bold'):
bold = '{b}'
Expand All @@ -32,7 +35,7 @@ def repl(m):
return Color(string)


def is_excluded(path: Path, root_path: Path = None, cache: dict = None) -> bool:
def is_excluded(path: Path, root_path: Path | None = None, cache: PathCache | None = None) -> bool:
""""Whether the path is excluded by a .no-headers file.
The .no-headers file is searched for in the path and all parents up to the root.
Expand Down
6 changes: 6 additions & 0 deletions stubs/colorclass/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# This file is part of Unbeheader.
# Copyright (C) CERN & UNCONVENTIONAL

from typing import TypeAlias

Color: TypeAlias = str
16 changes: 8 additions & 8 deletions tests/test_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import pytest
from colorclass import Color

from unbeheader import SUPPORTED_FILES
from unbeheader import SUPPORTED_FILE_TYPES
from unbeheader.config import DEFAULT_SUBSTRING
from unbeheader.headers import _do_update_header
from unbeheader.headers import _generate_header
Expand Down Expand Up @@ -49,8 +49,8 @@ def create_py_file(file_content):
@pytest.fixture
def py_files_settings():
return {
'regex': SUPPORTED_FILES['py']['regex'],
'comments': SUPPORTED_FILES['py']['comments']
'regex': SUPPORTED_FILE_TYPES['py'].regex,
'comments': SUPPORTED_FILE_TYPES['py'].comments
}


Expand All @@ -65,7 +65,7 @@ def test_update_header(_do_update_header, get_config, create_py_file):
file_ext = file_path.suffix[1:]
update_header(file_path, year, check)
_do_update_header.assert_called_once_with(
file_path, config, SUPPORTED_FILES[file_ext]['regex'], SUPPORTED_FILES[file_ext]['comments'], check
file_path, config, SUPPORTED_FILE_TYPES[file_ext].regex, SUPPORTED_FILE_TYPES[file_ext].comments, check
)


Expand All @@ -84,7 +84,7 @@ def test_update_header_for_unsupported_file(_do_update_header, get_config, tmp_p
year = date.today().year
file_path = tmp_path / 'manuscript.txt'
file_path.touch()
assert 'txt' not in SUPPORTED_FILES
assert 'txt' not in SUPPORTED_FILE_TYPES
assert update_header(file_path, year) is False
assert _do_update_header.call_count == 0

Expand Down Expand Up @@ -294,15 +294,15 @@ def test_do_update_header_for_empty_file(create_py_file, py_files_settings):
'''),
))
def test_generate_header(extension, expected, config):
data = SUPPORTED_FILES[extension]['comments'] | config
data = SUPPORTED_FILE_TYPES[extension].comments._asdict() | config
header = _generate_header(data)
assert header == dedent(expected).lstrip()


def test_generate_header_for_different_end_year(config):
end_year = date.today().year
config['end_year'] = end_year
data = SUPPORTED_FILES['py']['comments'] | config
data = SUPPORTED_FILE_TYPES['py'].comments._asdict() | config
header = _generate_header(data)
assert header == dedent(f'''
# This file is part of Thelema.
Expand All @@ -314,7 +314,7 @@ def test_generate_header_for_different_end_year(config):
'{root}', '{template}', '{substring}'
))
def test_generate_header_for_invalid_placeholder(template, config):
data = SUPPORTED_FILES['py']['comments'] | config
data = SUPPORTED_FILE_TYPES['py'].comments._asdict() | config
data['template'] = template
with pytest.raises(SystemExit) as exc:
_generate_header(data)
Expand Down

0 comments on commit d026bcf

Please sign in to comment.