Skip to content

Commit

Permalink
[DEV] Cookiecutter Python Package v1.4.1
Browse files Browse the repository at this point in the history
  • Loading branch information
boromir674 committed Jun 7, 2022
2 parents 6400565 + 0ea8ca7 commit e64728a
Show file tree
Hide file tree
Showing 16 changed files with 223 additions and 126 deletions.
7 changes: 3 additions & 4 deletions .prospector.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ ignore-paths:
ignore-patterns:
- (^|/)skip(this)?(/|$)
- src/cookiecutter_python/{{ cookiecutter.project_slug }}/tests/smoke_test.py
- src/cookiecutter_python/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.pkg_name }}/__main__.py
- src/cookiecutter_python/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.pkg_name }}/cli.py
autodetect: false
max-line-length: 95

Expand Down Expand Up @@ -50,10 +52,7 @@ bandit:
frosted:
run: false

pep8:
run: false

pep257:
pycodestyle:
run: false

mypy:
Expand Down
8 changes: 5 additions & 3 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ disable=print-statement,
unnecessary-lambda,
inconsistent-return-statements,
redundant-keyword-arg,
not-callable
not-callable,
unsubscriptable-object,
protected-access

# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
Expand Down Expand Up @@ -225,9 +227,9 @@ notes=FIXME,
contextmanager-decorators=contextlib.contextmanager

# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# system, and so shouldn't trigger E1101 (no-member) when accessed. Python regular
# expressions are accepted.
generated-members=_transform|self\.objects|self\._observers|subclasses
generated-members=_transform|self\.objects|self\._observers|subclasses|InteractiveDialog\.create

# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@
Changelog
=========

1.4.1 (2022-06-07)
==================

Changes
^^^^^^^

refactor
""""""""
- decouple dialog creation

chore
"""""
- satisfy prospector linter even better


1.4.0 (2022-06-06)
==================

Expand Down
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,9 @@ For more complex use cases, you can modify the Template and also leverage all of

.. Github Releases & Tags
.. |commits_since_specific_tag_on_master| image:: https://img.shields.io/github/commits-since/boromir674/cookiecutter-python-package/v1.4.0/master?color=blue&logo=github
.. |commits_since_specific_tag_on_master| image:: https://img.shields.io/github/commits-since/boromir674/cookiecutter-python-package/v1.4.1/master?color=blue&logo=github
:alt: GitHub commits since tagged version (branch)
:target: https://github.com/boromir674/cookiecutter-python-package/compare/v1.4.0..master
:target: https://github.com/boromir674/cookiecutter-python-package/compare/v1.4.1..master

.. |commits_since_latest_github_release| image:: https://img.shields.io/github/commits-since/boromir674/cookiecutter-python-package/latest?color=blue&logo=semver&sort=semver
:alt: GitHub commits since latest release (by SemVer)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ build-backend = "poetry.core.masonry.api"
## Also renders on pypi as 'subtitle'
[tool.poetry]
name = "cookiecutter_python"
version = "1.4.0"
version = "1.4.1"
description = "Yet another modern Python Package (pypi) with emphasis in CI/CD and automation."
authors = ["Konstantinos Lampridis <[email protected]>"]
maintainers = ["Konstantinos Lampridis <[email protected]>"]
Expand Down
2 changes: 1 addition & 1 deletion src/cookiecutter_python/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.4.0'
__version__ = '1.4.1'
72 changes: 72 additions & 0 deletions src/cookiecutter_python/backend/load_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import io
import json
import logging
import typing as t
from json import JSONDecodeError

import poyo

GivenInterpreters = t.Mapping[str, t.Sequence[str]]

logger = logging.getLogger(__name__)


def load_yaml(config_file) -> t.Mapping:
# TODO use a proxy to load yaml
with io.open(config_file, encoding='utf-8') as file_handle:
try:
yaml_dict = poyo.parse_string(file_handle.read())
except poyo.exceptions.PoyoException as error:
raise InvalidYamlFormatError(
'Unable to parse YAML file {}. Error: {}' ''.format(config_file, error)
) from error
return yaml_dict


def get_interpreters_from_yaml(config_file: str) -> t.Optional[GivenInterpreters]:
"""Parse the 'interpreters' variable out of the user's config yaml file.
Args:
config_file (str): path to the user's config yaml file
Raises:
InvalidYamlFormatError: if yaml parser fails to load the user's config
UserYamlDesignError: if yaml does not contain the 'default_context' key
Returns:
GivenInterpreters: dictionary with intepreters as a sequence of strings,
mapped to the 'supported-interpreters' key
"""
data = load_yaml(config_file)
if 'default_context' not in data:
raise UserYamlDesignError(
"User config (is valid yaml but) does not contain a 'default_context' outer key!"
)
context = data['default_context']
if 'interpreters' not in context:
return None

try:
interpreters_data = json.loads(context['interpreters'])
except JSONDecodeError as error:
logger.warning(
"User's yaml config 'interpreters' value Error: %s",
json.dumps(
{
'error': error,
'message': "Expected json 'parasable' value for the 'interpreters' key",
},
sort_keys=True,
indent=4,
),
)
return None
return interpreters_data


class UserYamlDesignError(Exception):
pass


class InvalidYamlFormatError(Exception):
pass
100 changes: 12 additions & 88 deletions src/cookiecutter_python/backend/main.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import io
import json
import logging
import os
import sys
import typing as t

import poyo
from cookiecutter.exceptions import InvalidConfiguration
from requests.exceptions import ConnectionError, JSONDecodeError
from requests.exceptions import ConnectionError

from cookiecutter_python.backend.check_pypi import check_pypi
from cookiecutter_python.backend.check_pypi_handler import handler
from cookiecutter_python.backend.load_config import get_interpreters_from_yaml
from cookiecutter_python.handle.interpreters_support import handle as get_interpreters

from .cookiecutter_proxy import cookiecutter

Expand All @@ -19,94 +17,28 @@
my_dir = os.path.dirname(os.path.realpath(__file__))


def load_yaml(config_file) -> t.Mapping:
# TODO use a proxy to load yaml
with io.open(config_file, encoding='utf-8') as file_handle:
try:
yaml_dict = poyo.parse_string(file_handle.read())
except poyo.exceptions.PoyoException as error:
raise InvalidConfiguration(
'Unable to parse YAML file {}. Error: {}' ''.format(config_file, error)
) from error
return yaml_dict


GivenInterpreters = t.Mapping[str, t.Sequence[str]]


def supported_interpreters(config_file, no_input) -> t.Optional[GivenInterpreters]:
if not no_input: # interactive
if sys.version_info < (3, 10):
check_box_dialog(config_file=config_file)
# else return None: let generator backend (ie cookiecutter) handle
# receiving the 'supported-interpreters' information from user input
# non-interactive
return check_box_dialog(config_file=config_file)
return None
if config_file:
try:
return get_interpreters_from_yaml(config_file)
except (
InvalidConfiguration,
UserConfigFormatError,
NoInterpretersInUserConfigException,
JSONDecodeError,
):
pass
return get_interpreters_from_yaml(config_file)
return None


def check_box_dialog(config_file=None) -> GivenInterpreters:
from cookiecutter_python.handle.interpreters_support import (
handle as get_interpreters,
)

defaults = None
defaults: t.Optional[t.Sequence[str]] = None
if config_file:
try:
defaults = get_interpreters_from_yaml(config_file)['supported-interpreters']
except (
InvalidConfiguration,
UserConfigFormatError,
NoInterpretersInUserConfigException,
JSONDecodeError,
):
pass
return get_interpreters(choices=defaults)


def get_interpreters_from_yaml(config_file: str) -> GivenInterpreters:
"""Parse the 'interpreters' variable out of the user's config yaml file.
Args:
config_file (str): path to the user's config yaml file
Raises:
InvalidConfiguration: if yaml parser fails to load the user's config
UserConfigFormatError: if yaml doesn't contain the 'default_context' key
NoInterpretersInUserConfigException: if yaml doesn't contain the
'interpreters' key, under the 'default_context' key
JSONDecodeError: if json parser fails to load the 'interpreters' value
Returns:
GivenInterpreters: dictionary with intepreters as a sequence of strings,
mapped to the 'supported-interpreters' key
"""
data = load_yaml(config_file)
if 'default_context' not in data:
raise UserConfigFormatError(
"User config (is valid yaml but) does not contain a 'default_context' outer key!"
)
context = data['default_context']
if 'interpreters' not in context:
raise NoInterpretersInUserConfigException(
"No 'interpreters' key found in user's config (under the 'default_context' key)."
interpreters_data: t.Optional[GivenInterpreters] = get_interpreters_from_yaml(
config_file
)
interpreters_data = json.loads(context['interpreters'])
if 'supported-interpreters' not in interpreters_data:
raise UserConfigFormatError(
"User config (is valid yaml but) does not contain a "
"'supported-interpreters' key in the 'interpreters' key"
)
return {'supported-interpreters': interpreters_data['supported-interpreters']}
if interpreters_data:
defaults = interpreters_data.get('supported-interpreters', None)
return get_interpreters(choices=defaults)


def generate(
Expand Down Expand Up @@ -174,11 +106,3 @@ def generate(

class CheckPypiError(Exception):
pass


class UserConfigFormatError(Exception):
pass


class NoInterpretersInUserConfigException(Exception):
pass
4 changes: 4 additions & 0 deletions src/cookiecutter_python/handle/dialogs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import lib # noqa
from .dialog import InteractiveDialog

__all__ = ['InteractiveDialog']
9 changes: 9 additions & 0 deletions src/cookiecutter_python/handle/dialogs/dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from abc import abstractmethod

from software_patterns import SubclassRegistry


class InteractiveDialog(metaclass=SubclassRegistry):
@abstractmethod
def dialog(self, *args, **kwargs):
raise NotImplementedError
18 changes: 0 additions & 18 deletions src/cookiecutter_python/handle/dialogs/interpreters.py

This file was deleted.

6 changes: 6 additions & 0 deletions src/cookiecutter_python/handle/dialogs/lib/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from cookiecutter_python.utils import load

from ..dialog import InteractiveDialog

# Import all classes subclassing InteractiveDialog
load(InteractiveDialog)
33 changes: 33 additions & 0 deletions src/cookiecutter_python/handle/dialogs/lib/interpreters_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import typing as t

try:
from PyInquirer import prompt
except ImportError:

def prompt(*args, **kwargs):
return {}


from ..dialog import InteractiveDialog


@InteractiveDialog.register_as_subclass('interpreters-checkbox')
class InterpretersCheckbox(InteractiveDialog):
def dialog(self, *args, **kwargs):
return self._dialog(*args, **kwargs)

def _dialog(
self, choices: t.Dict[str, t.Union[str, bool]]
) -> t.Dict[str, t.Sequence[str]]:

return prompt(
[
# Question 1
{
'type': 'checkbox',
'name': 'supported-interpreters',
'message': 'Select the python Interpreters you wish to support',
'choices': choices,
},
]
)
4 changes: 2 additions & 2 deletions src/cookiecutter_python/handle/interpreters_support.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import typing as t

from .dialogs.interpreters import dialog
from .dialogs import InteractiveDialog

INTERPRETERS_ATTR = 'interpreters'

Expand All @@ -27,6 +27,6 @@ def handle(choices: t.Optional[t.Sequence[str]] = None) -> t.Dict[str, t.Sequenc
Returns:
t.Sequence[str]: [description]
"""
return dialog(
return InteractiveDialog.create('interpreters-checkbox')(
[{'name': version, 'checked': True} for version in choices] if choices else CHOICES
)
Loading

0 comments on commit e64728a

Please sign in to comment.