Source code for candyshop.environment

# -*- coding: utf-8 -*-
#
# Please refer to AUTHORS.rst for a complete list of Copyright holders.
# Copyright (C) 2016-2022, Candyshop Developers.

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""
``candyshop.environment`` is a module for creating Odoo environments.

This module implements an abstraction layer to create an environment where
bundles can be consulted for different reports.
"""

import os
import sys
import shutil
import tempfile

from sh import git

from .bundle import Bundle

DEFAULT_URL = 'https://github.com/odoo/odoo'
DEFAULT_BRANCH = '15.0'


[docs]class Environment(object): """ An Environment is a virtual space where you can enclose bundles. Think about it as an invisible container where you can put bundles to study its relationships; for example, listing all modules and see which ones have missing dependencies (that are not within the environment). """
[docs] def __init__(self, init=True, init_from=None, url=DEFAULT_URL, branch=DEFAULT_BRANCH): """ Initialize the ``Environment`` instance. :param init: (boolean) specifies if the environment should be initialized, that is, if an Odoo repo should be cloned and the native addons added as bundles. Default: True. :param init_from: (string) a path pointing to an Odoo codebase. If present, the Odoo codebase will be taken from this folder instead of cloning from start. :param url: (string) an URL pointing to a git repository. This URL is used to clone the Odoo Codebase if ``init`` is ``True`` and ``init_from`` is ``None``. :param branch: (string) the branch used to clone ``url``. :return: an ``Environment`` instance. .. versionadded:: 0.1.0 """ #: Attribute ``Environment.bundles`` (list): A list of ``Bundle`` #: instances representing the bundles contained in this environment. self.bundles = [] #: Attribute ``Environment.path`` (string): A path pointing to #: the temporary directory where odoo and OCA dependencies will be #: cloned. self.path = tempfile.mkdtemp() if init: self.__initialize_odoo(url, branch, init_from)
def __initialize_odoo(self, url=DEFAULT_URL, branch=DEFAULT_BRANCH, init_from=None): """ Private method to clone an Odoo codebase inside the Environment path. This method clones an odoo codebase specified by ``url`` and ``branch`` so that bundles can have the native odoo bundles to compare with. Without this method, native modules (base, board, etc) would appear as missing dependencies. .. versionadded:: 0.1.0 """ if init_from: if not os.path.isdir(init_from): raise Exception('init_from directory "{0}" doesn\'t' ' exist'.format(init_from)) odoo_dir = os.path.abspath(init_from) else: odoo_dir = os.path.join(self.path, 'odoo') if not os.path.isdir(odoo_dir): self.__git_clone(url, branch, odoo_dir) self.addbundles([ os.path.join(odoo_dir, 'addons'), os.path.join(odoo_dir, 'odoo', 'addons') ]) @staticmethod def __git_clone(url, branch, path): """ Private method to clone a git repository. This method clones a git repository specified by ``url`` and ``branch`` to a folder ``path``. The ``--depth=1`` option is passed to the command to avoid cloning full history. .. versionadded:: 0.1.0 """ try: git.clone(url, path, quiet=True, depth=1, branch=branch) except BaseException: print('There was a problem cloning {0}.'.format(url)) raise def __clone_deptree(self): """ Private method that clones the dependency tree of existing bundles. It reads the oca_dependencies attribute of each bundle, clones each one (if any) and then adds them as bundles (which invokes this method again to satisfy dependencies in the new bundles). .. versionadded:: 0.1.0 """ for bundle in self.bundles: for name, url, branch in bundle.oca_dependencies: bundle_dir = os.path.join(self.path, name) if os.path.isdir(bundle_dir): continue self.__git_clone(url=url, branch=branch, path=bundle_dir) self.addbundles([bundle_dir]) def __deps_notin_e(self, deps=None): """ Private method that informs about missing modules in the environment. :param deps: (list) a list of module names to check. :return: (generator) a generator that produces an iterable of module names that are not present in the environment. .. versionadded:: 0.1.0 """ deps = deps or [] for dep in deps: if dep not in self.get_modules_slug_list(): yield dep
[docs] def addbundles(self, locations=None, exclude_tests=True): """ Public method that inserts bundles inside the environment. This method register a list of bundles and builds the dependency tree of each bundle recursively by calling ``__clone_deptree()``. :param locations: (list) a list of strings containing relative or absolute paths to directories containig bundles. :param exclude_tests: (boolean) if ``True``, will exclude modules inside ``tests`` directories. .. versionadded:: 0.1.0 """ locations = locations or [] for location in locations: location = os.path.abspath(location) if location in self.get_bundle_path_list() or \ not os.path.isdir(location): continue try: self.bundles.append(Bundle(location, exclude_tests)) except BaseException: print(('There was a problem inserting the bundle' ' located at {0}').format(location)) raise else: self.__clone_deptree()
[docs] def destroy(self): """ Public method to destroy an ``Environment`` instance. This method empties the bundle list and deletes the environment path, including all previously clones bundles. .. versionadded:: 0.1.0 """ self.bundles = [] shutil.rmtree(self.path)
[docs] def reset(self): """ Public method to destroy and initialize the ``Environment`` instance. This method calls ``self.destroy()`` on the instance and then re-initializes the Odoo environment. """ self.destroy() self.__initialize_odoo()
[docs] def get_bundle_path_list(self): """ Public method that informs about bundle paths. :return: (generator) a generator that produces an iterable of paths pointing to the bundles registered so far. .. versionadded:: 0.1.0 """ for bundle in self.bundles: yield bundle.path
[docs] def get_modules_list(self): """ Public method that informs about modules instances. :return: (generator) a generator that produces an iterable of ``Module`` instances of all the modules present within the environment. .. versionadded:: 0.1.0 """ for bundle in self.bundles: for module in bundle.modules: yield module
[docs] def get_modules_slug_list(self): """ Public method that informs about module names. :return: (generator) a generator that produces an iterable of strings containing the names of all the modules present within the environment. .. versionadded:: 0.1.0 """ for bundle in self.bundles: for module in bundle.modules: yield module.properties.slug
[docs] def get_notmet_dependencies(self): """ Public method that informs about missing dependencies in modules. :return: (generator) a generator that produces an iterable of dictionaries containing references to each bundle that have unmet dependencies within a module. The output is something similar tho this:: [ {'bundle-name': { 'module_name': ['missing_module_a', 'missing_module_b'] } } ] .. versionadded:: 0.1.0 """ for module in self.get_modules_list(): if hasattr(module.properties, 'depends'): deplist = list(self.__deps_notin_e(module.properties.depends)) if not deplist: continue yield {module.bundle.name: {module.properties.slug: deplist}}
[docs] def get_notmet_record_ids(self): """ Public method that informs about missing dependencies in XML files. :return: (generator) a generator that produces an iterable of dictionaries containing references to each bundle that have unmet dependencies refereced inside XML record ids. The output is something similar to this:: [ {'bundle-name': { 'module_name/path/file.xml': ['missing_module_a', 'missing_module_b'] } } ] .. versionadded:: 0.1.0 """ for module in self.get_modules_list(): for data in module.get_record_ids_module_references(): for xml, refs in data.items(): deplist = list(self.__deps_notin_e(refs)) if not deplist: continue relxml = os.path.join(module.properties.slug, xml) yield {module.bundle.name: {relxml: deplist}}
[docs] def get_notmet_dependencies_report(self): """ Public method that reports missing dependencies in modules. :return: (string) a report of human readable output for the ``get_notmet_dependencies()`` method. .. versionadded:: 0.1.0 """ report = list(self.get_notmet_dependencies()) if report: print('The following module dependencies are not found' ' in the environment:') for item in report: bundle, data = list(item.items())[0] module, depends = list(data.items())[0] print('') print(' Bundle: {0}'.format(bundle)) print(' Module: {0}'.format(module)) print(' Missing dependencies:') for dep in depends: print(' - {0}'.format(dep)) print('') sys.exit(1) else: print('All dependencies are satisfied in the environment.')
[docs] def get_notmet_record_ids_report(self): """ Public method that reports missing dependencies in XML files. :return: (string) a report of human readable output for the ``get_notmet_record_ids()`` method. .. versionadded:: 0.1.0 """ report = list(self.get_notmet_record_ids()) if report: print('The following record ids are not found in the environment:') for item in report: bundle, data = list(item.items())[0] xmlfile, depends = list(data.items())[0] print('') print(' Bundle: {0}'.format(bundle)) print(' XML file: {0}'.format(xmlfile)) print(' Missing references:') for dep in depends: print(' - {0}'.format(dep)) print('') sys.exit(1) else: print('All references are present in the environment.')