"""
base development constants and helpers
======================================
this module portion in the aedev namespace provides the following base constants, types and helper functions, used for
development operations (DevOps) and tools:
* :data:`APP_PRJ` – gui application project type.
* :data:`DJANGO_PRJ` – django web application project type.
* :data:`MODULE_PRJ` – standalone module project type.
* :data:`NO_PRJ` – marker for invalid or no project (type) detected.
* :data:`PACKAGE_PRJ` – python package project type.
* :data:`PARENT_PRJ` – pseudo-type for parent-directory initialization.
* :data:`PLAYGROUND_PRJ` – playground or scratch project type.
* :data:`ROOT_PRJ` – namespace root project type.
* :data:`ANY_PRJ_TYPE` – tuple of all real project types.
* :data:`ALL_PRJ_TYPES` – tuple of all project types, including :data:`NO_PRJ` and :data:`PARENT_PRJ`.
- :data:`COMMIT_MSG_FILE_NAME`: the default filename for commit messages.
- :data:`DEF_MAIN_BRANCH`: the name of the default/main branch.
- :data:`PIP_CMD`: the pip command.
- :data:`PIP_INSTALL_CMD`: the `pip install` command.
- :data:`PYPI_ROOT_URL`: the production PyPI URL.
- :data:`PYPI_ROOT_URL_TEST`: the test PyPI URL.
- :data:`PROJECT_VERSION_SEP`: the separator used in Pip requirements files to ty/fix a project to a version.
- :data:`VERSION_QUOTE`: the quote character used for the `__version__` variable value (typically `'`).
- :data:`VERSION_PREFIX`: the search string used to find the `__version__` variable (e.g., `__version__ = '`).
- :data:`VERSION_MATCHER`: a pre-compiled regular expression to detect and manage version numbers conforming
to :pep:`pep396 <396>` and python's `distutils` standards.
- :type:`TemplateProjectType`: project template register info (project_templates item type).
- :type:`TemplateProjectsType`: project_templates var type (added by pjm/project_manager).
- :type:`CachedTemplates`: cache mapping of registered project templates.
- :func:`code_file_title`: determines the docstring title of a Python code file.
- :func:`code_file_version`: reads the version number of a Python code file from the `__version__` module variable.
- :func:`code_version`: determines a version number from a specified content string (e.g., file content) using a
configurable prefix and suffix.
- :func:`get_pypi_versions`: determines all available release versions of a package on PyPI.
- :func:`project_name_version`: determine package name and version in the specified list of package/version strings.
"""
import json
import re
from typing import Iterable, Optional, Union
from urllib import request, error
from packaging.version import Version
from ae.base import env_str, norm_name, read_file # type: ignore
__version__ = '0.3.7'
APP_PRJ = 'app' #: gui application project
DJANGO_PRJ = 'django' #: django website project
MODULE_PRJ = 'module' #: module portion/project
NO_PRJ = '' #: no project detected
PACKAGE_PRJ = 'package' #: package portion/project
PARENT_PRJ = 'projects-parent-dir' #: pseudo project type for new project started in parent-dir
PLAYGROUND_PRJ = 'playground' #: playground project
ROOT_PRJ = 'namespace-root' #: namespace root project
ANY_PRJ_TYPE = (APP_PRJ, DJANGO_PRJ, MODULE_PRJ, PACKAGE_PRJ, PLAYGROUND_PRJ, ROOT_PRJ)
""" tuple of real project types (not including the pseudo-project-types: no-/incomplete-project and parent-folder) """
ALL_PRJ_TYPES = ANY_PRJ_TYPE + (NO_PRJ, PARENT_PRJ) #: all project types (including no/parent project)
COMMIT_MSG_FILE_NAME = '.commit_msg.txt' #: name of the file containing the commit message
DEF_MAIN_BRANCH = 'develop' #: main/develop/default branch name
PIP_CMD = "pip" #: pip command using python venvs, especially on Windows
PIP_INSTALL_CMD = f"{PIP_CMD} install" #: pip install command
PROJECT_VERSION_SEP = '==' #: separates package name and version in pip req files
PYPI_ROOT_URL = "https://pypi.org" #: PyPI cheeseshop production domain with service
PYPI_ROOT_URL_TEST = "https://test.pypi.org" #: PyPI cheeseshop test domain with service
TEST_PROJECTS_PARENT_FOLDER = 'TsT' #: integration/unit tests projects local machine parent folder
TEST_PROJECTS_NAMESPACE = 'aetst' #: integration/unit tests namespace
TEST_PROJECTS_REMOTE = 'gitlab.com' #: git remote domain of integration/unit tests projects
VERSION_QUOTE = "'" #: quote character of the __version__ number variable value
VERSION_PREFIX = "__version__ = " + VERSION_QUOTE #: search string to find the __version__ variable
VERSION_MATCHER = re.compile("^" + VERSION_PREFIX + r"(\d+)[.](\d+)[.](\d+)[a-z\d]*" + VERSION_QUOTE, re.MULTILINE)
""" pre-compiled regular expression to detect and change/bump the app/portion file version numbers of a version string.
The version number format has to be :pep:`conform to PEP396 <396>` and the sub-part to `Pythons distutils
<https://docs.python.org/3/distutils/setupscript.html#additional-meta-data>`__ (trailing version information indicating
sub-releases, are either “a1,a2,…,aN” (for alpha releases), “b1,b2,…,bN” (for beta releases) or “pr1,pr2,…,prN” (for
pre-releases). Note that distutils got deprecated in Python 3.12 (see package :mod:`packaging.version` as replacement)).
"""
TemplateProjectType = dict[str, str] #: project template register info (project_templates item type)
TemplateProjectsType = list[TemplateProjectType] #: project_templates var type (added by pjm/project_manager)
CachedTemplates = dict[str, TemplateProjectType] #: cache mapping of registered project templates
[docs]
def code_file_title(file_name: str) -> str:
""" determine docstring title of a Python code file.
:param file_name: name (and optional path) of module/script file to read the docstring title number from.
:return: docstring title string or empty string if file|docstring-title not found.
"""
title = ""
try:
lines = read_file(file_name).splitlines()
for idx, line in enumerate(lines):
if line.startswith('"""'):
title = (line[3:].strip() or lines[idx + 1].strip()).strip('"').strip()
break
except (FileNotFoundError, IndexError, OSError):
pass
return title
[docs]
def code_file_version(file_name: str) -> str:
""" read version of Python code file - from __version__ module variable initialization.
:param file_name: name (and optional path) of module/script file to read the version number from.
:return: version number string or empty string if file or version-in-file not found.
"""
try:
content = read_file(file_name)
return code_version(content)
except (FileNotFoundError, OSError, TypeError, ValueError):
return ""
[docs]
def code_version(content: Union[str, bytes], prefix: str = "^" + VERSION_PREFIX, suffix: str = VERSION_QUOTE) -> str:
""" determine a version number from the specified content string.
:param content: content of type str or bytes to be searched for the definition/declaration of a version.
:param prefix: version string prefix (directly before the veesion number).
:param suffix: version string suffix char/string (directly after the version number string).
passing an empty is not allowed, and will always return an empty string.
:return: version number string or empty string if version could not be not found in content.
"""
try:
pattern = prefix + "([^" + suffix + "]*)" + suffix
if isinstance(content, bytes):
content = content.decode('utf-8')
version_match = re.search(pattern, content, re.M)
return version_match.group(1) if version_match else ""
except (TypeError, UnicodeDecodeError, ValueError, Exception): # pylint: disable=broad-exception-caught
return ""
[docs]
def get_pypi_versions(pip_name: str, pypi_test: Optional[bool] = None) -> list[str]:
""" determine all the available release versions of a package hosted at the PyPI 'Cheese Shop'.
:param pip_name: pip|package|project name to get release versions from.
:param pypi_test: pass True to use the test version of PyPI (at test.pypi.org). if not specified or None
then the test version of PyPI will be used if :paramref:`~get_pypi_versions.pip_name`
starts with the projects namespace (:data:`~aedev.base.TEST_PROJECTS_NAMESPACE),
used for the pjm integration tests.
:return: list of released versions (the latest last) or
on error a list with a single empty string item.
.. note:: if the OS environment variable ``PIP_INDEX_URL`` is set, then its value is used instead of ``pypi.org``.
"""
if pypi_test is None:
pypi_test = pip_name.startswith(TEST_PROJECTS_NAMESPACE) # no path to check for TEST_PROJECTS_PARENT_FOLDER
pypi_root_url = PYPI_ROOT_URL_TEST if pypi_test else (env_str("PIP_INDEX_URL") or "").rstrip('/') or PYPI_ROOT_URL
try:
with request.urlopen(f"{pypi_root_url}/pypi/{pip_name}/json", timeout=12) as response:
if response.status == 200:
data = json.load(response) # faster than: json.loads(response.read().decode('utf-8'))
versions = list(data.get('releases', {}).keys())
if versions:
versions.sort(key=Version)
return versions
except (KeyError, ValueError, error.HTTPError, error.URLError, Exception): # pylint: disable=broad-exception-caught
pass # Exception is catching also json.JSONDecodeError
return [""] # ignore error on invalid pip_name/page-not-found/never released to PyPi
[docs]
def project_name_version(imp_or_pkg_name: str, packages_versions: Iterable[str]) -> tuple[str, str]:
""" determine package name and version in the specified list of package/version strings.
:param imp_or_pkg_name: import or package name to search.
:param packages_versions: an Iterable with project package name and optional version strings, like specified in
requirements.txt files (format: <project_name>[<PROJECT_VERSION_SEP><project_version>]).
:return: tuple of package name and version number. the package name is an empty string if it
is not in :paramref:`~project_version.packages_versions`. the version number is an
empty string if no package version is specified in
:paramref:`~project_version.packages_versions`.
"""
project_name = norm_name(imp_or_pkg_name)
for imp_or_pkg_name_and_ver in packages_versions:
imp_or_pkg_name, *ver = imp_or_pkg_name_and_ver.split(PROJECT_VERSION_SEP)
prj_name = norm_name(imp_or_pkg_name)
if prj_name == project_name:
return project_name, ver[0] if ver else ""
return "", ""