Source code for odoo_tools.api.environment

import os
import toml
import json
import logging
from pathlib import Path
from contextlib import contextmanager
from configparser import NoSectionError, NoOptionError

from ..exceptions import OdooNotInstalled
from ..compat import module_path
from ..modules.search import find_addons_paths
from ..utils import (
    filter_excluded_paths,
    to_path_list,
    convert_env_value,
    ConfigParser
)
from ..utilities.config import get_env_params, parse_value, get_defaults
from ..utilities.loaders import FileLoader
from ..configuration.misc import (
    get_resource
)

from .context import Context
from .management import ManagementApi
from .modules import ModuleApi
from .services import ServiceApi


_logger = logging.getLogger(__name__)


[docs]class Environment(object): """ Odoo Environment object. The odoo environment object is a container that store all the information required to prepare an environment for odoo. It can be used to browse the available modules in the configured odoo environment. Or to setup environment variables for the running odoo instance. It can also be used to configure the odoo.cfg file based on the current environment. Attributes: context (Context): The context to use env (Environment): The environment variables container requirement_file_path (Path): The path where to store the requirements file when merging pip requirements. """ def __init__( self, context=None, ): """ Initialize an environment. Parameters: context (Context): The context to use. env (Environment): The environment to use. strict_mode (bool): If the environment is strict. """ if context is None: context = Context.from_env() self.context = context self.modules = ModuleApi(self) self.manage = ManagementApi(self) self.services = ServiceApi(self) self._config = ConfigParser() self._nested = False self._read_config = False self.loader = FileLoader() self._prepare_parser() def _prepare_parser(self): self.loader.parsers['.toml'] = lambda data: toml.loads(data) self.loader.parsers['.json'] = lambda data: json.loads(data)
[docs] def path(self): """ Returns the path where odoo is installed and can be imported. The base addons are installed in the folder `addons` relative to this path. """ if self.context.odoo_base_path: return Path(self.context.odoo_base_path) try: path = module_path("odoo") except Exception: try: path = module_path("openerp") except Exception: raise OdooNotInstalled("Cannot find odoo base path") return path
[docs] def check_odoo(self): try: self.path() except Exception: raise OdooNotInstalled( "Cannot use this api without odoo being installed" )
[docs] def set_config(self, key, value, section='options'): """ Set a configuration settings in the currently open configuration file. Example: .. code:: python env.set_config('server_wide_modules', 'web,base') env.set_config('max_handlers', '3', 'custom_section') Parameters: key (str): The name of the config to set. value (str): The value to store in the config section (str): The section to use in the ConfigParser object. By default, it uses the ``'options'`` section. This is the default section used by odoo. But the section can be set to something different to define additional sections for use. """ with self.config(): self._config.set(section, key, value)
[docs] def get_config(self, key, section='options'): """ Get a configuration settings in the currently open configuration file. """ try: with self.config(readonly=True): return parse_value(self._config.get(section, key)) except NoOptionError: return None except NoSectionError: return None
[docs] @contextmanager def config(self, readonly=False): """ A context manager that can be used to read/write configuration for odoo. The context manager saves the configuration file when it is closed. .. code:: python with env.config(): env.set_config('server_wide_modules', 'web,base') """ config_path = Path(self.context.odoo_rc) nested = self._nested is_top = False if not nested: self._nested = True is_top = True if not nested and config_path.exists() and not self._read_config: try: self._read_config = True self._config.read(str(config_path)) except Exception: _logger.info("Couldn't read ODOO_RC file.", exc_info=True) if not self._config._defaults and self.odoo_version(): try: params_by_name = get_env_params(self, self.odoo_version()) except OdooNotInstalled: params_by_name = {} self._config._defaults = get_defaults(params_by_name) try: yield self._config except Exception: raise finally: if is_top: self._nested = False if not nested and not readonly: try: with config_path.open('w') as out: defaults = self._config._defaults self._config._defaults = {} self._config.write(out) self._config._defaults = defaults except Exception: _logger.error("Couldn't write config ", exc_info=True)
[docs] def addons_paths(self): """ Returns the addons path configured for this environment. For example, it would find the path addons in the odoo installed folder. And in custom paths defined to search for addons. If an addons path had modules in ``/a/b``, ``/a/d/e/f`` and ``/a/b/c``. It would return the following list of addons paths. .. code:: python ['/a/b', '/a/b/c', '/a/d/e/f'] Odoo doesn't load folders recursively so if you have modules within folder that also contains modules. The addons_paths have to be defined. With this, you can store your modules in to ``'/addons'`` and only define as custom_paths ``'/addons'``. If there are modules in ``'/addons/**'``. Those will be returned by ``addons_paths()``. .. code:: python addons_paths = ",".join(env.addons_paths()) with env.config(): env.set_config("addons_path", addons_paths) Returns: paths (List<Path>): The list of paths containing installable addons. """ try: with self.config(readonly=True) as config: paths = config.get('options', 'addons_path') config_paths = set( Path(path.strip()) for path in paths.split(',') if path.strip() ) if len(config_paths) > 0 and not self.context.force_addons_lookup: return config_paths except Exception: config_paths = set() base_addons_paths = config_paths try: base_addons = self.path() / "addons" if not self.context.exclude_odoo: base_addons_paths.add(base_addons) else: self.context.excluded_paths.add(base_addons) except OdooNotInstalled: pass base_addons_paths |= self.context.custom_paths orig_valid_paths = find_addons_paths( base_addons_paths, options=self.context ) if self.context.excluded_paths: excluded_paths = to_path_list(self.context.excluded_paths) valid_paths = filter_excluded_paths( orig_valid_paths, excluded_paths ) else: valid_paths = orig_valid_paths return valid_paths
[docs] def odoo_config(self): """ Returns odoo config regardless of being in openerp/odoo """ try: from odoo.tools import config as odoo_config return odoo_config except ImportError: try: from openerp.tools import config as odoo_config return odoo_config except (ImportError, ModuleNotFoundError): raise OdooNotInstalled( "Cannot use config without odoo being installed" )
[docs] def odoo_options(self): env_options = self.env_options() options = {} with self.config(readonly=True) as config: for section, values in config._sections.items(): sections_vals = options.setdefault(section, {}) for key, value in values.items(): sections_vals[key] = value if 'options' in options: options['options'].update(env_options) else: options['options'] = env_options return options
[docs] def env_options(self): """ Load environment variable options. """ try: params_by_name = get_env_params(self, self.odoo_version()) except OdooNotInstalled: params_by_name = {} configs = {} for key, value in os.environ.items(): if key in params_by_name: option = params_by_name[key] config_name = option[0] converted_value = convert_env_value(key, value) configs[config_name] = converted_value return configs
[docs] def sync_options(self): """ Sync options to odoo configmanager Loads the config file and loads the environment variables options. Then set the options into `odoo.tools.config` in the options and misc parameters. """ config = self.odoo_config() odoo_options = self.odoo_options() opts = odoo_options.pop('options') config.options.update(opts) for section, values in odoo_options.items(): sec = config.misc.setdefault(section, {}) for key, value in values.items(): sec[key] = parse_value(value)
[docs] def requirement_files(self, lookup_requirements=False): found_files = set() for cur_path in self.addons_paths(): if lookup_requirements: found_files |= set(cur_path.glob("**/requirements.txt")) else: requirement_file = cur_path / 'requirements.txt' if requirement_file.exists(): found_files.add(requirement_file) return found_files
def _default_package_map(self): version = self.odoo_version() if not version: return {} file_path = "packages/map-{version}.toml".format( version=version ) resource_path = get_resource('odoo_tools', file_path) if not resource_path.exists(): return {} with resource_path.open('r') as fin: data = toml.load(fin) return data
[docs] def package_map(self): """ Returns a package map. The package map is used to map some module name to python package names. By default, in newer versions of odoo, it will check for the package name. But unfortunately some modules will still have the module name defined in their python external dependencies. When requirements are built from the odoo modules available in the odoo environment. It will wrongly attempt to install let say the module "ldap". ldap is the name of the module that can be imported, but the package that needs to be installed is python-ldap. Such map could look like this: .. code-block:: python {'ldap': 'python-ldap'} Then when the packages required are found, they can be mapped to those package names to find the exact python package name. Mapping a name to an empty string would remove the name from the requirements. This can be useful to remove package defined in the external dependencies that aren't actual packages but builtin dependencies that would be already part of python itself. .. code-block:: python {'asyncio': ''} The behaviour could be also useful when you want to install an alternative to let say the barcode module. Returns: dict: Key, Value of mapped module/package name. """ base_map = self._default_package_map() package_map_file = self.context.package_map_file if not package_map_file: return base_map package_map_path = Path(package_map_file) if not package_map_path.exists(): return base_map content = package_map_path.open('r').read() package_map = toml.loads(content) new_vals = { key.lower(): value.lower() for key, value in package_map.items() } base_map.update(new_vals) return base_map
[docs] def odoo_version(self): """ Returns the odoo version. In case odoo isn't installed, it will fallback to the context variable `odoo_version` which can be set through environment variable ODOO_VERSION. Returns: int|None: The major version number or None """ try: from odoo.release import version_info return version_info[0] except ImportError: try: return int(float(self.context.odoo_version)) except Exception: return None