Skip to content

Commit

Permalink
[NEW] Supported Interpreters Selection
Browse files Browse the repository at this point in the history
  • Loading branch information
boromir674 committed Jun 1, 2022
2 parents 5c78034 + b168376 commit 8e1dbe9
Show file tree
Hide file tree
Showing 31 changed files with 975 additions and 316 deletions.
1 change: 1 addition & 0 deletions .github/biskotaki.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ default_context:
github_username: boromir674
project_short_description: Project generated from the https://github.com/boromir674/cookiecutter-python-package/tree/master/src/cookiecutter_python cookiecutter
initialize_git_repo: no
interpreters: {"supported-interpreters": ["3.6", "3.7", "3.8", "3.9", "3.10"]}
63 changes: 63 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,69 @@
Changelog
=========

1.3.0 (2022-05-31)
==================

Python Interpreters Support and Test
------------------------------------

This release allows the user to select the Python Interpreters they wish their Project
to support and be tested on.
The generator then creates the Test Matrix in the CI config file, which factors in the
Python Interpreter versions supplied by the user.

Consistent with the currect behaviour of the cli, passing the '--no-input' flag,
instructs the Generator to try find the selected interpreters in a config yaml file,
if given, or else to use the information in the cookiecutter.json.

If the '--no-input' flag is missing, then the user is asked for input, through
their console.
The input is read by supplying an interactive console dialog, which allows the user to
easily select the interpreters they wish to support, by enabling or disabling
'check boxes' through their console.

Development
-----------

All tox environments related to 'Linting' now all do by default a 'check'.
Doing a 'check' means returning a 0 as exit code in case the check is successfull
and it is suitable for local and remote running on a CI server.

The aforementioned environments are 'lint', 'black', 'isort':
- tox -e lint
- tox -e black
- tox -e isort

Optionally, running as below will modify the source code to comply with
each respective 'lint check'.

Running environment 'lint', 'black', 'isort' with 'lint apply' enabled:
- *APPLY_LINT= tox -e lint*
- *APPLY_BLACK= tox -e black*
- *APPLY_ISORT= tox -e isort*

Changes
^^^^^^^

feature
"""""""
- generate the Project's CI Test Workflow with a build matrix based on the user's input python interpreters

test
""""
- verify 'pre gen' script exits with 1 in case module name given is incorrect
- write scenarios with/without 'config file' and with/without given 'interpreters'

development
"""""""""""
- add env for integration testing
- add checks for 'scripts' dir, make 'black', 'isort' cmds only do 'lint-check' by default and add switch to allow doing 'lint-apply'

build
"""""
- add PyInquirer '>= 1.0.3 and < 1.1.0' dependency: required by checkbox dialog


1.2.1 (2022-05-27)
==================

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.2.1/master?color=blue&logo=github
.. |commits_since_specific_tag_on_master| image:: https://img.shields.io/github/commits-since/boromir674/cookiecutter-python-package/v1.3.0/master?color=blue&logo=github
:alt: GitHub commits since tagged version (branch)
:target: https://github.com/boromir674/cookiecutter-python-package/compare/v1.2.1..master
:target: https://github.com/boromir674/cookiecutter-python-package/compare/v1.3.0..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
6 changes: 4 additions & 2 deletions 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.2.1"
version = "1.3.0"
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 Expand Up @@ -95,7 +95,8 @@ click = "^8"
cookiecutter = "^1.7.3"
software-patterns = "^1.2.1"
requests-futures = "^1.0.0"

PyInquirer = "^1.0.3"
prompt-toolkit = "==1.0.14"

# A list of all of the optional dependencies, some of which are included in the
# below `extras`. They can be opted into by apps.
Expand All @@ -106,6 +107,7 @@ pytest-click = { version = "~= 1.1.0", optional = true }
pytest-cov = { version = ">= 2.12", optional = true }
pytest-explicit = { version = "~= 1.0.1", optional = true }
pytest-xdist = { version = ">= 1.34", optional = true }
attrs = { version = "^21.4.0", optional = true }

# Docs: development and build dependencies
sphinx = { version = "~= 4.0", optional = true }
Expand Down
69 changes: 38 additions & 31 deletions scripts/parse_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,11 @@
TOML_FILE = os.path.abspath(os.path.join(my_dir, '..', TOML))

DEMO_SECTION: str = (
"[tool.software-release]\nversion_variable = "
"src/package_name/__init__.py:__version__"
"[tool.software-release]\nversion_variable = " "src/package_name/__init__.py:__version__"
)


def build_client_callback(data: MatchData, factory: ExceptionFactory) -> ClientCallback:

def client_callback(file_path: str, regex: str) -> t.Tuple:
with open(file_path, 'r') as _file:
contents = _file.read()
Expand All @@ -38,38 +36,44 @@ def client_callback(file_path: str, regex: str) -> t.Tuple:
return extracted_tuple
else:
raise factory(file_path, regex, contents)

return client_callback


# PARSERS

software_release_parser = build_client_callback((
'search',
[re.MULTILINE,],
lambda match: (match.group(1), match.group(2))
),
software_release_parser = build_client_callback(
(
'search',
[
re.MULTILINE,
],
lambda match: (match.group(1), match.group(2)),
),
lambda file_path, reg, string: RuntimeError(
"Expected to find the '[tool.software-release]' section, in "
f"the '{file_path}' file, with key "
"'version_variable'.\nFor example:\n"
f"{DEMO_SECTION}\n "
"indicates that the version string should be looked up in "
f"the src/package_name/__init__.py file and specifically "
"a '__version__ = 1.2.3' kind of line is expected to be found."
)
"Expected to find the '[tool.software-release]' section, in "
f"the '{file_path}' file, with key "
"'version_variable'.\nFor example:\n"
f"{DEMO_SECTION}\n "
"indicates that the version string should be looked up in "
f"the src/package_name/__init__.py file and specifically "
"a '__version__ = 1.2.3' kind of line is expected to be found."
),
)


version_file_parser = build_client_callback((
'search',
[re.MULTILINE,],
lambda match: (match.group(1),)
),
version_file_parser = build_client_callback(
(
'search',
[
re.MULTILINE,
],
lambda match: (match.group(1),),
),
lambda file_path, reg, string: AttributeError(
"Could not find a match for regex {regex} when applied to:".format(
regex=reg
) + "\n{content}".format(content=string)
)
"Could not find a match for regex {regex} when applied to:".format(regex=reg)
+ "\n{content}".format(content=string)
),
)


Expand All @@ -84,15 +88,18 @@ def parse_version(software_release_cfg: str) -> str:
"""
header = r'\[tool\.software-release\]'
sep = r'[\w\s=/\.:\d]+' # in some cases accounts for miss-typed characters!
version_specification = \
version_specification = (
r"version_variable[\ \t]*=[\ \t]*['\"]?([\w\.]+(?:/[\w\.]+)*):(\w+)['\"]?"
)
regex = f"{header}{sep}{version_specification}"

file_name_with_version, version_variable_name = \
software_release_parser(software_release_cfg, regex)
file_name_with_version, version_variable_name = software_release_parser(
software_release_cfg, regex
)

file_with_version_string = \
os.path.abspath(os.path.join(my_dir, '../', file_name_with_version))
file_with_version_string = os.path.abspath(
os.path.join(my_dir, '../', file_name_with_version)
)

if not os.path.isfile(file_with_version_string):
raise FileNotFoundError(
Expand All @@ -104,7 +111,7 @@ def parse_version(software_release_cfg: str) -> str:
)

reg = f'^{version_variable_name}' + r'\s*=\s*[\'\"]([^\'\"]*)[\'\"]'
version, = version_file_parser(file_with_version_string, reg)
(version,) = version_file_parser(file_with_version_string, reg)
return version


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.2.1'
__version__ = '1.3.0'
25 changes: 14 additions & 11 deletions src/cookiecutter_python/backend/cookiecutter_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@

__all__ = ['cookiecutter']

# This sets the root logger to write to stdout (your console).
# Your script/app needs to call this somewhere at least once.
logging.basicConfig()

logger = logging.getLogger(__name__)

my_dir = os.path.dirname(os.path.realpath(__file__))
Expand All @@ -19,27 +23,26 @@ class CookiecutterSubject(ProxySubject[str]):


class CookiecutterProxy(Proxy[str]):
"""Proxy to cookiecutter: 'from cookiecutter.main import cookiecutter'."""

def request(self, *args, **kwargs) -> str:
"""[summary]
Returns:
str: [description]
"""
logger.info(
'Cookiecutter invocation: %s',
logger.debug(
'Cookiecutter Proxy Request: %s',
json.dumps(
{
'args': '[{arg_values}]'.format(
arg_values=', '.join([f"'{str(x)}'" for x in args])
),
'kwargs': '{{{key_value_pairs}}}'.format(
key_value_pairs=json.dumps({k: str(v) for k, v in kwargs.items()})
),
}
'keyword_args': {k: str(v) for k, v in kwargs.items()},
'positional_args': [str(arg_value) for arg_value in args],
},
indent=2,
sort_keys=True,
),
)
output_dir: str = super().request(*args, **kwargs)
return output_dir
return super().request(*args, **kwargs)


# Singleton and Adapter of Cookiecutter Proxy
Expand Down
52 changes: 52 additions & 0 deletions src/cookiecutter_python/backend/interpreters_support.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import typing as t

InterpretersSequence = t.Sequence[str]


# TODO Improvement: use an Enum
# SUPPORTED = {
# 'py35',
# 'py36',
# 'py37',
# 'py38',
# 'py39',
# 'py310',
# 'py311',
# }

SUPPORTED = {
'3.5',
'3.6',
'3.7',
'3.8',
'3.9',
'3.10',
'3.11',
}


def verify_input_interpreters(interpreters: InterpretersSequence) -> None:
user_interpreters_set = set(interpreters)
if len(user_interpreters_set) != len(interpreters):
raise InvalidInterpretersError("Found duplicate interpreters!")

if not user_interpreters_set.issubset(SUPPORTED):
# not all user requested interpreters are included in the supported ones
raise InvalidInterpretersError(
"Unsupported interpreter given Error!\n"
+ "Given interpreters: [{given}]\n".format(given=', '.join(interpreters))
+ "Supported interpreters: [{supported}]\n".format(supported=', '.join(SUPPORTED))
+ "Unsupported interpreters: [{unsupported}]".format(
unsupported=', '.join(iter(unsupported_interpreters(interpreters)))
)
)


def unsupported_interpreters(interpreters: InterpretersSequence) -> t.Iterator[str]:
for interpreter in interpreters:
if interpreter not in SUPPORTED:
yield interpreter


class InvalidInterpretersError(Exception):
pass
Loading

0 comments on commit 8e1dbe9

Please sign in to comment.