Skip to content

Commit

Permalink
Merge pull request #4 from matyaskuti/main
Browse files Browse the repository at this point in the history
Generic updates and improvements
  • Loading branch information
jeohist authored Oct 24, 2023
2 parents 1a094f4 + 589f733 commit 928b36d
Show file tree
Hide file tree
Showing 21 changed files with 133 additions and 72 deletions.
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
34 changes: 34 additions & 0 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Lint and test
on:
- push
permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- "3.10"
- "3.11"
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Pre-install dependencies
run: |
make install_lint_requirements
make install_test_requirements
- name: Lint
run: |
make lint
- name: Test
run: |
make test
18 changes: 0 additions & 18 deletions .gitlab-ci.yml

This file was deleted.

6 changes: 6 additions & 0 deletions .isort.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[settings]
ensure_newline_before_comments=True
include_trailing_comma=True
line_length=79
multi_line_output=3
split_on_trailing_comma=True
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ lint: install_lint_requirements
flake8 tests
black --line-length=79 --check --diff tests
pylint tests
isort --check-only tests setup.py
mypy tests

.PHONY: install_test_requirements
Expand All @@ -30,6 +31,7 @@ clean:
.PHONY: format
format:
black --line-length=79 tests
isort tests setup.py

.PHONY: install
install:
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# python-project-template

![Test status](https://github.com/matyaskuti/python-project-template/actions/workflows/python-app.yml/badge.svg
)

A [`cookiecutter`](https://github.com/audreyr/cookiecutter) based Python
project template.

Expand Down Expand Up @@ -160,9 +163,7 @@ Below are the main `make` targets and the tools used within:
customizable; the generated project contains a minimal, but decent
`pylintrc` configuration file; its usage is optional, can be decided upon
project generation, however highly recommended and turned on by default
* `mypy` - type checker, the de facto standard at the moment; its usage is
optional, can be decided upon project generation, however highly
recommended and turned on by default
* `mypy` - type checker, the de facto standard at the moment
* `format` - to easily comply with the above standards at the push of a button
* `black` - because of the reasons mentioned above
* `test` - to verify functionality at the smallest level of granularity (unit)
Expand All @@ -185,7 +186,7 @@ are called automatically in their related main target._
### Future extension

New linters can be easily added by extending the `Makefile`, potentially made
optional (just as with `pylint` or `mypy`).
optional (just as with `pylint`).

Currently in the created project there is only one `test` target which is
intendet to be used to run a set of automated tests in the "commit phase".
Expand Down
3 changes: 1 addition & 2 deletions cookiecutter.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,5 @@
"short_description": "Python project",
"author_name": "Mendix Cloud Value Added Services Team",
"author_email": "dis_valueaddedservices@mendix.com",
"use_pylint": "y",
"use_mypy": "y"
"use_pylint": "y"
}
1 change: 0 additions & 1 deletion hooks/post_gen_project.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import os


PROJECT_DIRECTORY = os.path.realpath(os.path.curdir)


Expand Down
1 change: 0 additions & 1 deletion hooks/pre_gen_project.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import re
import sys


MODULE_REGEX = r"^[_a-zA-Z][_a-zA-Z0-9]+$"
PACKAGE_NAME = "{{ cookiecutter.package_name }}"

Expand Down
10 changes: 10 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[mypy]
strict = True
pretty = True

warn_redundant_casts = True
warn_unused_ignores = True
warn_no_return = True

[mypy-pytest_cookies.*]
ignore_missing_imports = True
2 changes: 1 addition & 1 deletion pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,32}|(__.*__))$
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$

[MESSAGES CONTROL]
disable = missing-docstring, locally-disabled, too-few-public-methods, fixme
disable = missing-docstring, locally-disabled, too-few-public-methods, fixme
msg-template = {module}:{line} [{msg_id}: {symbol}] - {msg}

[REPORTS]
Expand Down
10 changes: 6 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from setuptools import setup


HERE = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(HERE, "README.md")) as fobj:
README = fobj.read()
Expand All @@ -14,17 +13,20 @@
description="Template to generate Python projects with cookiecutter",
long_description=README,
classifiers=[
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Framework :: Pytest",
"Typing :: Typed",
],
author="Mendix Cloud Value Added Services Team",
author_email="dis_valueaddedservices@mendix.com",
packages=[],
install_requires=["cookiecutter>=1.4<2"],
install_requires=["cookiecutter>2.1.1,<3"],
extras_require={
"lint": [
"flake8<7",
"black<24",
"pylint<3",
"pylint<4",
"mypy<2"
],
"test": [
Expand Down
53 changes: 28 additions & 25 deletions tests/test_project_creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@
project generator wizard.
"""
import glob
from collections.abc import Sequence

from pytest_cookies.plugin import Cookies, Result

from .util import (
check_output_in_result_dir,
generate_temporary_project,
inside_directory_of,
)


DEFAULT_PROJECT_NAME = "pymx"
EXPECTED_PROJECT_FILES = (
DEFAULT_PROJECT_NAME,
Expand All @@ -25,33 +27,37 @@
"setup.py",
"tests",
"pylintrc",
".isort.cfg",
"mypy.ini",
)


def assert_successful_creation(result):
def assert_successful_creation(result: Result) -> None:
assert result.project.isdir()
assert result.exit_code == 0
assert result.exception is None


def assert_expected_files_exist(result, files=()):
def assert_expected_files_exist(result: Result, files: Sequence[str]) -> None:
created_files = [f.basename for f in result.project.listdir()]
for fname in files:
assert fname in created_files


def test_default_project_creation(cookies):
def test_default_project_creation(cookies: Cookies) -> None:
with generate_temporary_project(cookies) as result:
assert_successful_creation(result)
assert_expected_files_exist(result, files=EXPECTED_PROJECT_FILES)


def test_project_creation_with_invalid_name_fails(cookies):
def test_project_creation_with_invalid_name_fails(cookies: Cookies) -> None:
result = cookies.bake(extra_context={"package_name": "Foo-Bar"})
assert result.exit_code != 0


def assert_expected_files_do_not_exist(result, files=()):
def assert_expected_files_do_not_exist(
result: Result, files: Sequence[str]
) -> None:
with inside_directory_of(result):
for cleaned_up in files:
for filename in glob.glob("./**", recursive=True):
Expand All @@ -66,7 +72,7 @@ def assert_expected_files_do_not_exist(result, files=()):
NO_PLINT = {"use_pylint": "n"}


def test_project_creation_without_pylint(cookies):
def test_project_creation_without_pylint(cookies: Cookies) -> None:
with generate_temporary_project(cookies, extra_context=NO_PLINT) as result:
assert_successful_creation(result)
assert_expected_files_exist(
Expand All @@ -75,7 +81,10 @@ def test_project_creation_without_pylint(cookies):
assert_expected_files_do_not_exist(result, files=("pylintrc",))


def assert_expected_lines_are_in_output(expected_lines, output):
def assert_expected_lines_are_in_output(
expected_lines: Sequence[str],
output: str,
) -> None:
for line in expected_lines:
assert line in output

Expand All @@ -84,48 +93,41 @@ def assert_expected_lines_are_in_output(expected_lines, output):
BLACK_OUTPUT = f"black --line-length=79 --check --diff {FILES_TO_CHECK_FORMAT}"
PYLINT_OUTPUT_1 = f"pylint {DEFAULT_PROJECT_NAME} tests"
PYLINT_OUTPUT_2 = "Your code has been rated at 10.00/10"
MYPY_OUTPUT = f"mypy --ignore-missing-imports {DEFAULT_PROJECT_NAME}"
ISORT_OUTPUT = f"isort --check-only {DEFAULT_PROJECT_NAME} tests setup.py"
MYPY_OUTPUT = f"mypy --ignore-missing-imports {DEFAULT_PROJECT_NAME} tests"
EXPECTED_LINT_OUTPUT = (
"pip3 install -e .[lint]",
f"flake8 {DEFAULT_PROJECT_NAME} tests",
"files would be left unchanged",
BLACK_OUTPUT,
PYLINT_OUTPUT_1,
PYLINT_OUTPUT_2,
ISORT_OUTPUT,
MYPY_OUTPUT,
)


def test_linting(cookies):
def test_linting(cookies: Cookies) -> None:
with generate_temporary_project(cookies) as result:
output = check_output_in_result_dir("make lint", result)
assert_expected_lines_are_in_output(EXPECTED_LINT_OUTPUT, output)


def test_linting_without_pylint(cookies):
def test_linting_without_pylint(cookies: Cookies) -> None:
with generate_temporary_project(cookies, extra_context=NO_PLINT) as result:
output = check_output_in_result_dir("make lint", result)
assert PYLINT_OUTPUT_1 not in output
assert PYLINT_OUTPUT_2 not in output


NO_MYPY = {"use_mypy": "n"}


def test_linting_without_mypy(cookies):
with generate_temporary_project(cookies, extra_context=NO_MYPY) as result:
output = check_output_in_result_dir("make lint", result)
assert MYPY_OUTPUT not in output


EXPECTED_TEST_OUTPUT = (
"pip3 install -e .[test]",
"test session starts",
"files skipped due to complete coverage.",
)


def test_test_run(cookies):
def test_test_run(cookies: Cookies) -> None:
with generate_temporary_project(cookies) as result:
output = check_output_in_result_dir("make test", result)
assert_expected_lines_are_in_output(EXPECTED_TEST_OUTPUT, output)
Expand All @@ -147,7 +149,7 @@ def test_test_run(cookies):
)


def test_cleaning(cookies):
def test_cleaning(cookies: Cookies) -> None:
with generate_temporary_project(cookies) as result:
check_output_in_result_dir("make lint", result)
check_output_in_result_dir("make test", result)
Expand All @@ -159,18 +161,19 @@ def test_cleaning(cookies):
)


def test_clean_can_be_executed_in_empty_project_dir(cookies):
def test_clean_can_be_executed_in_empty_project_dir(cookies: Cookies) -> None:
with generate_temporary_project(cookies) as result:
check_output_in_result_dir("make clean", result)


EXPECTED_FORMAT_OUTPUT = (
f"black --line-length=79 {DEFAULT_PROJECT_NAME} tests setup.py",
"files left unchanged",
f"isort {DEFAULT_PROJECT_NAME} tests setup.py",
)


def test_formatting(cookies):
def test_formatting(cookies: Cookies) -> None:
with generate_temporary_project(cookies) as result:
output = check_output_in_result_dir("make format", result)
assert_expected_lines_are_in_output(EXPECTED_FORMAT_OUTPUT, output)
Expand All @@ -179,7 +182,7 @@ def test_formatting(cookies):
EXPECTED_BUILD_PATTERNS = ("./dist/*.tar.gz", "./dist/*.whl")


def test_build(cookies):
def test_build(cookies: Cookies) -> None:
with generate_temporary_project(cookies) as result:
check_output_in_result_dir("make build", result)
with inside_directory_of(result):
Expand Down
12 changes: 7 additions & 5 deletions tests/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import shlex
import shutil
import subprocess
from typing import Any, Iterator, Mapping
from typing import Any, Iterator

from pytest_cookies.plugin import Cookies, Result


@contextlib.contextmanager
def generate_temporary_project(
cookies: Any, **kwargs: Mapping[Any, Any]
) -> Iterator[Any]:
cookies: Cookies, **kwargs: dict[Any, Any]
) -> Iterator[Cookies]:
try:
result = cookies.bake(**kwargs)
yield result
Expand All @@ -18,13 +20,13 @@ def generate_temporary_project(


@contextlib.contextmanager
def inside_directory_of(result: Any) -> Iterator[None]:
def inside_directory_of(result: Result) -> Iterator[None]:
old_dir = result.project.chdir()
yield
os.chdir(old_dir)


def check_output_in_result_dir(command: str, result: Any) -> str:
def check_output_in_result_dir(command: str, result: Result) -> str:
"""Run the given command in the directory of `result`.
The `command` parameter should be a string, while `result` is the
Expand Down
Loading

0 comments on commit 928b36d

Please sign in to comment.