diff --git a/NEWS.rst b/NEWS.rst index 2a99fb809c685eb438448b434979af9b121e03ef_TkVXUy5yc3Q=..606a967c39b15e0a3d167619fb472e374ba176f9_TkVXUy5yc3Q= 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -15,6 +15,8 @@ Use the presence of a Dockerfile as a sign that the current directory is a super project. Avoids starting to create files by error. +Add new command and flags to run pylint on some modules. + 14.0.0 ------ diff --git a/README.rst b/README.rst index 2a99fb809c685eb438448b434979af9b121e03ef_UkVBRE1FLnJzdA==..606a967c39b15e0a3d167619fb472e374ba176f9_UkVBRE1FLnJzdA== 100644 --- a/README.rst +++ b/README.rst @@ -97,6 +97,13 @@ This is part of the docker section. +docker_pylint +------------- + +Run dockerized pylint on provided module name. Pylint must be present in the project image. + +This is part of the docker section. + conf2reST.py ------------ diff --git a/odoo_scripts/config.py b/odoo_scripts/config.py index 2a99fb809c685eb438448b434979af9b121e03ef_b2Rvb19zY3JpcHRzL2NvbmZpZy5weQ==..606a967c39b15e0a3d167619fb472e374ba176f9_b2Rvb19zY3JpcHRzL2NvbmZpZy5weQ== 100644 --- a/odoo_scripts/config.py +++ b/odoo_scripts/config.py @@ -28,4 +28,5 @@ blacklist: str = None, read: str = None, parent_configuration=None, + registry: str = None, ): @@ -31,4 +32,8 @@ ): + """ + :param registry: use the provided registry instead of the one in the + configuration file + """ self._expanded_configuration = dict() self.path = path setup_path = "setup.cfg" @@ -176,6 +181,15 @@ self.registry = section.get("registry", "registry.xcg.io") project_path = os.path.realpath(".") self.image = section.get("image", os.path.basename(project_path)) + self.repository = f"{self.registry}/{self.image}" + """Registry and image name combination, read from the configuration file""" + self.local_tag = "dev" + """Tag used for local operations""" + # Does not use repository as it is not meant to be pushed + self.local_image = self.image + self.local_image_name = f"{self.local_image}:{self.local_tag}" + """Full image name used for local operations (locally built, used for test and + running)""" read_expanded("odoo_type", "odoo7") if not hasattr(self, "odoo_type"): # if path is set, odoo_type might not be in the values to read diff --git a/odoo_scripts/do_tests.py b/odoo_scripts/do_tests.py index 2a99fb809c685eb438448b434979af9b121e03ef_b2Rvb19zY3JpcHRzL2RvX3Rlc3RzLnB5..606a967c39b15e0a3d167619fb472e374ba176f9_b2Rvb19zY3JpcHRzL2RvX3Rlc3RzLnB5 100755 --- a/odoo_scripts/do_tests.py +++ b/odoo_scripts/do_tests.py @@ -13,6 +13,7 @@ from odoo_scripts.docker_postgresql import docker_run_postgresql from .config import Config +from .docker_build import add_build_options, get_build_options from .docker_dev_start import main as docker_dev_start_main from .docker_flake8 import apply_flake8, parser_add_flake8_group from .docker_isort import apply_isort, parser_add_isort_group @@ -21,5 +22,5 @@ _logger = logging.getLogger(__name__) -__version__ = "3.0.0" +__version__ = "3.0.1" __date__ = "2018-04-13" @@ -25,5 +26,5 @@ __date__ = "2018-04-13" -__updated__ = "2021-10-21" +__updated__ = "2022-01-27" def main(argv=None): # IGNORE:C0111 @@ -144,6 +145,7 @@ ) parser_add_flake8_group(parser) parser_add_isort_group(parser) + add_build_options(parser) # TODO options # - db host/uri (include socket) # - db user for creation/remove @@ -165,8 +167,6 @@ config = Config() - registry = config.registry - project = config.image languages = config.load_language postgresql_version = config.postgresql_version odoo_type = config.odoo_type @@ -170,8 +170,7 @@ languages = config.load_language postgresql_version = config.postgresql_version odoo_type = config.odoo_type - # TODO factorize with docker_dev_start - image = "%s/%s:latest" % (registry, project) + image = config.local_image_name _logger.debug("Docker image: %s", image) extensions = config.pg_extensions @@ -245,6 +244,8 @@ if nmspc.LOAD_LANGUAGE: args.append("--load-language") args.append(nmspc.LOAD_LANGUAGE) + # also pass build options + args.extend(get_build_options(nmspc)) if recreate_db: if start_postgresql: container, stop_method, socket_path = docker_run_postgresql( diff --git a/odoo_scripts/docker_build.py b/odoo_scripts/docker_build.py index 2a99fb809c685eb438448b434979af9b121e03ef_b2Rvb19zY3JpcHRzL2RvY2tlcl9idWlsZC5weQ==..606a967c39b15e0a3d167619fb472e374ba176f9_b2Rvb19zY3JpcHRzL2RvY2tlcl9idWlsZC5weQ== 100644 --- a/odoo_scripts/docker_build.py +++ b/odoo_scripts/docker_build.py @@ -2,6 +2,7 @@ # vim: set shiftwidth=4 softtabstop=4: """Script to locally build a docker image """ +import argparse import datetime import json import logging @@ -18,5 +19,5 @@ _logger = logging.getLogger(__name__) -__version__ = "1.1.0" +__version__ = "1.2.0" __date__ = "2018-04-04" @@ -22,5 +23,25 @@ __date__ = "2018-04-04" -__updated__ = "2021-12-13" +__updated__ = "2022-01-27" + + +def add_build_options(parser: argparse.ArgumentParser): + """Add build options to parser""" + parser.add_argument( + "--build-arg", + help="build arg for the image, formatted like FOO=BAR [default: %(default)s]", + default=None, + nargs="*", + ) + + +def get_build_options(namespace: argparse.Namespace): + """Return build options usable in :func:`build_local_image`.""" + result = [] + if namespace.build_arg: + for arg in namespace.build_arg: + result.append("--build-arg") + result.append(arg) + return result def __parser(): @@ -34,7 +55,7 @@ program_license = """%s Created by Vincent Hatakeyama on %s. - Copyright 2018, 2019, 2020, 2021 XCG Consulting. All rights reserved. + Copyright 2018-2022 XCG Consulting. All rights reserved. Licensed under the MIT License @@ -62,6 +83,7 @@ # TODO add a way to store configuration options in a project file # Argument parsing parser = __parser() + add_build_options(parser) # TODO add tag option, and maybe force tag option parser.add_argument( "--ensureconf", @@ -77,13 +99,7 @@ action="store_true", ) parser.add_argument( - "--build-arg", - help="build arg for the image, formatted like FOO=BAR [default: %(default)s]", - default=None, - nargs="*", - ) - parser.add_argument( "--no-pull", help="indicate to docker to not pull the base image [default: %(default)s]", action="store_true", ) @@ -86,7 +102,14 @@ "--no-pull", help="indicate to docker to not pull the base image [default: %(default)s]", action="store_true", ) + parser.add_argument( + "--local", + help="Use local image name", + action="store_true", + dest="local", + default=None, + ) # TODO (maybe) add argument for other build arg nmspc = parser.parse_args(argv) @@ -101,6 +124,4 @@ return 1 c = Config() - registry = c.registry - project = c.image odoo_type = c.odoo_type @@ -106,5 +127,5 @@ odoo_type = c.odoo_type - repository = f"{registry}/{project}" + repository = c.local_image_name if nmspc.local else c.repository _logger.debug("Docker repository: %s", repository) # TODO ensureconf if ensureconf: @@ -124,7 +145,7 @@ # used to retag tags = [] - buildargs = dict() + buildargs = {} if os.path.exists(".hg"): tags = check_output(["hg", "identify", "--tags"]).decode() buildargs["VERSION"] = tags @@ -212,5 +233,27 @@ return 0 +def build_local_image(build_options, force: bool = False) -> int: + """Build the local image if necessary + :param force: force build if True + :return: 0 if success, docker failed error code otherwise + """ + c = Config() + image = c.local_image_name + + image_list = DockerClient.client.images.list(name=image) + if not image_list: + _logger.info("Image %s does not exist", image) + else: + _logger.info("Image %s exists", image) + if not image_list or force: + _logger.info("Building image %s", image) + arguments = ["--dev", "--local"] + build_options + failed = main(arguments) + if failed: + return failed + return 0 + + if __name__ == "__main__": sys.exit(main(sys.argv[1:])) diff --git a/odoo_scripts/docker_client.py b/odoo_scripts/docker_client.py index 2a99fb809c685eb438448b434979af9b121e03ef_b2Rvb19zY3JpcHRzL2RvY2tlcl9jbGllbnQucHk=..606a967c39b15e0a3d167619fb472e374ba176f9_b2Rvb19zY3JpcHRzL2RvY2tlcl9jbGllbnQucHk= 100644 --- a/odoo_scripts/docker_client.py +++ b/odoo_scripts/docker_client.py @@ -1,2 +1,3 @@ +import atexit import logging import os @@ -1,3 +2,4 @@ import logging import os +import re import tarfile @@ -3,5 +5,5 @@ import tarfile -from typing import Dict, List +from typing import Any, Dict, List, Optional, Tuple import docker import dockerpty @@ -5,6 +7,7 @@ import docker import dockerpty +from docker.types import Mount _logger = logging.getLogger(__name__) if docker.__version__ < "3.4.0": @@ -8,9 +11,9 @@ _logger = logging.getLogger(__name__) if docker.__version__ < "3.4.0": - _logger.warning("Unexepected python docker version: %s", docker.__version__) + _logger.warning("Unexpected python docker version: %s", docker.__version__) # TODO detect that user is member of docker group class DockerClient: @@ -12,11 +15,13 @@ # TODO detect that user is member of docker group class DockerClient: + """Provide additional abstraction and extra methods of the docker client API""" + client = docker.from_env() @classmethod def create_volume( cls, volume_name: str, extra_labels: Dict[str, str] = None ) -> docker.models.volumes.Volume: @@ -17,11 +22,10 @@ client = docker.from_env() @classmethod def create_volume( cls, volume_name: str, extra_labels: Dict[str, str] = None ) -> docker.models.volumes.Volume: - """Return the volume passed in parameter, creating it if it does not - exists. + """Return the volume passed in parameter, creating it if it does not exist. :param volume_name: name of the volume to create :param extra_labels: extra labels to put on the volume (only on creation) @@ -77,3 +81,104 @@ result = run_result.exit_code print(run_result.output.decode()) return result + + @classmethod + def run( + cls, + repository: str, + tag: str, + run_kwargs: Dict[str, Any], + pull: bool = True, + target_source_dict: Optional[Dict[str, str]] = None, + ): + """Run a container with the provided args""" + replaces = ( + [(re.compile(key), value) for key, value in target_source_dict.items()] + if target_source_dict + else [] + ) + + image = f"{repository}:{tag}" + + if pull: + _logger.debug("Pulling %s", image) + # TODO find a better way to avoid error when pulling without a connection + try: + cls.client.images.pull(repository, tag) + except docker.errors.APIError: + _logger.warning( + "No connection to server, using existing version, if any" + ) + + container = None + + # XXX When pressing ctrl-c during the container creating, the container + # is not removed as there is no return of the value of the Container + # object. There is several ways to do that: + # - give the container a unique name we know, and use that to clean + # - maybe use some kind of time frame to identify the container + # - warn the user of a potential container leak by using signals + + def remove(): + """Remove the container""" + if container: + _logger.debug("Removing container %s", container.name) + container.remove() + + def stop_remove(): + """Stop then remove the container.""" + if container: + if container.status == "running": + _logger.debug("Stopping container %s", container.name) + container.stop() + _logger.debug("Waiting for container %s", container.name) + container.wait() + remove() + + atexit.register(stop_remove) + + # split create and start so in case of SIGTERM, container has fewer chances + # of being None + kwargs = run_kwargs.copy() + kwargs["detach"] = True + _logger.debug("Create container with image %s", image) + container = cls.client.containers.create(image, **kwargs) + _logger.debug("Created container %s", container.name) + _logger.debug("Starting container %s", container.name) + container.start() + atexit.unregister(stop_remove) + atexit.register(remove) + + for log in container.logs(stream=True, follow=True): + log = log.decode().rstrip() + # apply all the replacement of the path inside the image to the real path + for pattern, value in replaces: + log = pattern.sub(value, log) + # print to play nicely with pycharm that will linkify the result + print(log) + + _logger.debug("Waiting for container %s", container.name) + result = container.wait() + + if result.get("Error", None): + _logger.warning(result.decode()) + _logger.debug("Removing container %s", container.name) + container.remove(v=True) + atexit.unregister(remove) + return result["StatusCode"] + + +def modules_mount( + modules, project_path, target_path: str +) -> Tuple[List[Mount], Dict[str, str]]: + """Return a list of Mount for the given modules""" + mount_list: List[Mount] = [] + mount_dict: Dict[str, str] = {} + + for module in modules: + full_target_path = os.path.join(target_path, os.path.basename(module)) + full_project_path = os.path.realpath(os.path.join(project_path, module)) + mount_list.append(Mount(full_target_path, full_project_path, "bind")) + mount_dict[full_target_path] = full_project_path + + return mount_list, mount_dict diff --git a/odoo_scripts/docker_dev_start.py b/odoo_scripts/docker_dev_start.py index 2a99fb809c685eb438448b434979af9b121e03ef_b2Rvb19zY3JpcHRzL2RvY2tlcl9kZXZfc3RhcnQucHk=..606a967c39b15e0a3d167619fb472e374ba176f9_b2Rvb19zY3JpcHRzL2RvY2tlcl9kZXZfc3RhcnQucHk= 100755 --- a/odoo_scripts/docker_dev_start.py +++ b/odoo_scripts/docker_dev_start.py @@ -15,10 +15,10 @@ from docker.types import Mount from psycopg2 import OperationalError, connect -from . import docker_build -from .config import Config -from .docker_client import DockerClient +from .config import ODOO_15, Config +from .docker_build import add_build_options, build_local_image, get_build_options +from .docker_client import DockerClient, modules_mount from .docker_flake8 import apply_flake8, parser_add_flake8_group from .docker_isort import apply_isort, parser_add_isort_group from .docker_postgresql import POSTGRES_PASSWORD, docker_run_postgresql from .docker_py3o import start_py3o @@ -21,10 +21,11 @@ from .docker_flake8 import apply_flake8, parser_add_flake8_group from .docker_isort import apply_isort, parser_add_isort_group from .docker_postgresql import POSTGRES_PASSWORD, docker_run_postgresql from .docker_py3o import start_py3o +from .docker_pylint import apply_pylint, parser_add_pylint_group from .parsing import apply, basic_parser # TODO auto create list of module _logger = logging.getLogger(__name__) @@ -25,8 +26,8 @@ from .parsing import apply, basic_parser # TODO auto create list of module _logger = logging.getLogger(__name__) -__version__ = "3.2.0" +__version__ = "3.3.0" __date__ = "2017-08-11" @@ -32,5 +33,5 @@ __date__ = "2017-08-11" -__updated__ = "2022-01-17" +__updated__ = "2022-01-27" def main(argv=None): # IGNORE:C0111 @@ -157,6 +158,8 @@ ) parser_add_flake8_group(parser) parser_add_isort_group(parser) + parser_add_pylint_group(parser) + add_build_options(parser) parser.add_argument( "--odoo-help", help="Pass --help to odoo binary [default: %(default)s]", @@ -306,9 +309,6 @@ c = Config() - modules = c.modules - _logger.debug("addon modules: %s", ",".join(modules)) - if not db_user and c.db_user is not None: db_user = c.db_user if not db_password and c.db_password is not None: @@ -316,6 +316,4 @@ if c.load_language is not None: load_language = c.load_language - registry = c.registry - project = c.image odoo_type = c.odoo_type @@ -321,5 +319,5 @@ odoo_type = c.odoo_type - image = "%s/%s:latest" % (registry, project) + image = c.local_image_name postgresql_version = c.postgresql_version module_list = c.module_list if nmspc.start_py3o: @@ -332,15 +330,8 @@ _logger.debug("Docker image: %s", image) # detect if docker image already exists client = DockerClient.client - image_list = client.images.list(name=image) - if not image_list: - _logger.info("Image %s does not exist", image) - else: - _logger.info("Image %s exists", image) - if (not image_list and not nmspc.no_build) or nmspc.force_build: - _logger.info("Building image %s", image) - arguments = ["--dev"] - failed = docker_build.main(arguments) + if not nmspc.no_build: + failed = build_local_image(get_build_options(nmspc), nmspc.force_build) if failed: return failed @@ -355,8 +346,8 @@ } if not use_host_network: options["ports"][8069] = 8069 - mounts: List[Mount] = list() + mounts: List[Mount] = [] # TODO handle other version of odoo (base image need to be changed too) coverage = (nmspc.test or nmspc.test_default) and odoo_type in ( "odoo11", "odoo13", @@ -359,8 +350,8 @@ # TODO handle other version of odoo (base image need to be changed too) coverage = (nmspc.test or nmspc.test_default) and odoo_type in ( "odoo11", "odoo13", - "odoo15", + ODOO_15, ) arg = ["coverage-start" if coverage else "start"] if db_password: @@ -457,7 +448,7 @@ _logger.info("Local configuration file found: %s", local_conf_path) odoo_conf_file = ( "/etc/odoo/odoo.conf" - if odoo_type in ("odoo11", "odoo12", "odoo13", "odoo15") + if odoo_type in ("odoo11", "odoo12", "odoo13", ODOO_15) else "/opt/odoo/etc/odoo.conf" ) mounts.append( @@ -668,8 +659,8 @@ connection.commit() connection.close() else: - _logger.fatal("Cannot connect to database with user %s", user) - _logger.fatal(exception) + _logger.critical("Cannot connect to database with user %s", user) + _logger.critical(exception) _logger.info("You can add the --create-user argument to create it") return 16 @@ -751,4 +742,6 @@ return 16 # volume magic + modules = c.modules + _logger.debug("addon modules: %s", ",".join(modules)) target_module_directory = "/mnt/addons" @@ -754,12 +747,5 @@ target_module_directory = "/mnt/addons" - for module in modules: - mounts.append( - Mount( - "%s/%s" % (target_module_directory, os.path.basename(module)), - os.path.realpath(os.path.join(project_path, module)), - "bind", - ) - ) + mounts.extend(modules_mount(modules, project_path, target_module_directory)[0]) all_addons_dir = [target_module_directory] if odoo_type in ("odoo7", "odoo8"): all_addons_dir.append("/opt/odoo/sources/odoo/addons") @@ -769,6 +755,7 @@ apply_flake8(nmspc, odoo_type) apply_isort(nmspc, odoo_type) + apply_pylint(nmspc) # developer mode options if dev: @@ -772,7 +759,7 @@ # developer mode options if dev: - if odoo_type in ("odoo10", "odoo11", "odoo13", "odoo15"): + if odoo_type in ("odoo10", "odoo11", "odoo13", ODOO_15): # automatically add reload if not already asks for if not any(opt in dev_opts for opt in ("all", "reload")): dev_opts.append("reload") @@ -795,7 +782,7 @@ # bind Odoo sources if odoo_sources: _logger.info("Binding Odoo sources at %s", odoo_sources) - if odoo_type in ("odoo10", "odoo11", "odoo13", "odoo15"): + if odoo_type in ("odoo10", "odoo11", "odoo13", ODOO_15): # Image 11 uses python 3.5 if based on xenial, or 3.6 for bionic # 13 uses bionic if odoo_type == "odoo10": @@ -804,7 +791,7 @@ pythons = ("python3.5", "python3.6") elif odoo_type == "odoo13": pythons = ("python3.8",) - elif odoo_type == "odoo15": + elif odoo_type == ODOO_15: pythons = ("python3.9",) else: pythons = ("python3.6",) diff --git a/odoo_scripts/docker_flake8.py b/odoo_scripts/docker_flake8.py index 2a99fb809c685eb438448b434979af9b121e03ef_b2Rvb19zY3JpcHRzL2RvY2tlcl9mbGFrZTgucHk=..606a967c39b15e0a3d167619fb472e374ba176f9_b2Rvb19zY3JpcHRzL2RvY2tlcl9mbGFrZTgucHk= 100644 --- a/odoo_scripts/docker_flake8.py +++ b/odoo_scripts/docker_flake8.py @@ -3,7 +3,6 @@ """Run flake8 in a docker """ import argparse -import atexit import logging import os import sys @@ -12,5 +11,5 @@ from .docker_client import DockerClient from .parsing import apply, basic_parser -__version__ = "1.1.0" +__version__ = "1.2.0" __date__ = "2020-09-10" @@ -16,5 +15,5 @@ __date__ = "2020-09-10" -__updated__ = "2022-01-25" +__updated__ = "2022-01-27" _logger = logging.getLogger(__name__) @@ -30,7 +29,7 @@ program_license = """%s Created by Vincent Hatakeyama on %s. - Copyright 2020 XCG Consulting. All rights reserved. + Copyright 2020, 2022 XCG Consulting. All rights reserved. Licensed under the MIT License @@ -84,8 +83,6 @@ def flake8(odoo_type: str, pull: bool = True): """Run flake8""" - client = DockerClient.client - # determine image to use based on odoo version repository = "quay.orus.io/odoo/odoo" if odoo_type in ("odoo7", "odoo8", "odoo9", "odoo10"): @@ -96,5 +93,4 @@ else: repository = "xcgd/flake8" tag = "3" - image = f"{repository}:{tag}" @@ -100,35 +96,15 @@ - if pull: - _logger.info("Pulling flake8 (%s)", image) - client.images.pull(repository, tag) - - _logger.info("Running flake8 (%s)", image) - container = None - - # XXX When pressing ctrl-c during the container creating, the container - # is not removed as there is no return of the value of the Container - # object. There is several ways to do that: - # - give the container a unique name we know, and use that to clean - # - maybe use some kind of time frame to identify the container - # - warn the user of a potential container leak by using signals - - def stop_remove(): - """Stop then remove the container.""" - if container: - if container.status == "running": - container.stop() - container.wait() - container.remove() - - atexit.register(stop_remove) - - # split create and start so in case of SIGTERM, container has fewer chances - # of being None - container = client.containers.create( - image, - entrypoint="flake8", - command=["."], - volumes={os.environ["PWD"]: {"bind": "/mnt", "mode": "ro"}}, - working_dir="/mnt", - detach=True, + _logger.info("Running flake8") + pwd = os.environ["PWD"] + return DockerClient.run( + repository, + tag, + { + "entrypoint": "flake8", + "command": ["/mnt"], + "volumes": {pwd: {"bind": "/mnt", "mode": "ro"}}, + "working_dir": "/mnt", + }, + pull, + {"/mnt": pwd}, ) @@ -134,14 +110,4 @@ ) - container.start() - result = container.wait() - atexit.unregister(stop_remove) - atexit.register(container.remove) - print(container.logs().decode().rstrip()) - if result.get("Error", None): - _logger.warning(result.decode()) - container.remove(v=True) - atexit.unregister(container.remove) - return result["StatusCode"] if __name__ == "__main__": diff --git a/odoo_scripts/docker_isort.py b/odoo_scripts/docker_isort.py index 2a99fb809c685eb438448b434979af9b121e03ef_b2Rvb19zY3JpcHRzL2RvY2tlcl9pc29ydC5weQ==..606a967c39b15e0a3d167619fb472e374ba176f9_b2Rvb19zY3JpcHRzL2RvY2tlcl9pc29ydC5weQ== 100644 --- a/odoo_scripts/docker_isort.py +++ b/odoo_scripts/docker_isort.py @@ -3,8 +3,7 @@ """Run isort in a docker """ import argparse -import atexit import logging import os import sys @@ -7,10 +6,8 @@ import logging import os import sys -import docker - from .config import ODOO_15, Config from .docker_client import DockerClient from .parsing import apply, basic_parser @@ -13,6 +10,6 @@ from .config import ODOO_15, Config from .docker_client import DockerClient from .parsing import apply, basic_parser -__version__ = "2.0.0" +__version__ = "2.1.0" __date__ = "2020-09-10" @@ -18,5 +15,5 @@ __date__ = "2020-09-10" -__updated__ = "2022-01-25" +__updated__ = "2022-01-27" _logger = logging.getLogger(__name__) _ISORT_DEST = "isort" @@ -101,8 +98,6 @@ def isort(mode: str, odoo_type: str, pull: bool = True): """Run isort""" - client = DockerClient.client - if mode == CHECK_MODE: command = ["--check"] if mode == DIFF_MODE: @@ -119,5 +114,4 @@ else: repository = "xcgd/isort" tag = "odoo" - image = f"{repository}:{tag}" @@ -123,43 +117,18 @@ - if pull: - _logger.info("Pulling isort (%s)", image) - # TODO find a better way to avoid error when pulling without a - # connection - try: - client.images.pull(repository=repository, tag=tag) - except docker.errors.APIError: - _logger.warning("No connection to server, using existing version, if any") - - _logger.info("Running isort for Odoo (%s)", image) - container = None - - # XXX When pressing ctrl-c during the container creating, the container - # is not removed as there is no return of the value of the Container - # object. There is several ways to do that: - # - give the container a unique name we know, and use that to clean - # - maybe use some kind of time frame to identify the container - # - warn the user of a potential container leak by using signals - - def stop_remove(): - """Stop then remove the container.""" - if container: - if container.status == "running": - container.stop() - container.wait() - container.remove() - - atexit.register(stop_remove) - - # split create and start so in case of SIGTERM, container has fewer chances - # of being None - container = client.containers.create( - image, - entrypoint="isort", - user="root", - command=command, - volumes={ - os.environ["PWD"]: { - "bind": "/mnt", - "mode": "ro" if mode in (CHECK_MODE, DIFF_MODE) else "rw", - } + _logger.info("Running isort") + pwd = os.environ["PWD"] + return DockerClient.run( + repository, + tag, + { + "entrypoint": "isort", + "user": "root", + "command": command, + "volumes": { + pwd: { + "bind": "/mnt", + "mode": "ro" if mode in (CHECK_MODE, DIFF_MODE) else "rw", + } + }, + "working_dir": "/mnt", }, @@ -165,4 +134,4 @@ }, - working_dir="/mnt", - detach=True, + pull, + {"/mnt": pwd}, ) @@ -168,14 +137,4 @@ ) - container.start() - result = container.wait() - atexit.unregister(stop_remove) - atexit.register(container.remove) - print(container.logs().decode().rstrip()) - if result.get("Error", None): - _logger.warning(result.decode()) - container.remove(v=True) - atexit.unregister(container.remove) - return result["StatusCode"] if __name__ == "__main__": diff --git a/odoo_scripts/docker_flake8.py b/odoo_scripts/docker_pylint.py similarity index 27% copy from odoo_scripts/docker_flake8.py copy to odoo_scripts/docker_pylint.py index 2a99fb809c685eb438448b434979af9b121e03ef_b2Rvb19zY3JpcHRzL2RvY2tlcl9mbGFrZTgucHk=..606a967c39b15e0a3d167619fb472e374ba176f9_b2Rvb19zY3JpcHRzL2RvY2tlcl9weWxpbnQucHk= 100644 --- a/odoo_scripts/docker_flake8.py +++ b/odoo_scripts/docker_pylint.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 # vim: set shiftwidth=4 softtabstop=4: -"""Run flake8 in a docker +"""Run pylint in a docker """ import argparse @@ -4,6 +4,5 @@ """ import argparse -import atexit import logging import os import sys @@ -7,5 +6,8 @@ import logging import os import sys +from typing import Dict, List + +from docker.types import Mount from .config import ODOO_15, Config @@ -10,5 +12,6 @@ from .config import ODOO_15, Config -from .docker_client import DockerClient +from .docker_build import add_build_options, build_local_image, get_build_options +from .docker_client import DockerClient, modules_mount from .parsing import apply, basic_parser @@ -13,8 +16,8 @@ from .parsing import apply, basic_parser -__version__ = "1.1.0" -__date__ = "2020-09-10" -__updated__ = "2022-01-25" +__version__ = "1.0.0" +__date__ = "2022-01-26" +__updated__ = "2022-01-27" _logger = logging.getLogger(__name__) @@ -18,6 +21,9 @@ _logger = logging.getLogger(__name__) +_PYLINT_DEST = "pylint" +"""Name of destination variable used for pylint in parsing""" + def __parser(): program_version = __version__ @@ -30,7 +36,7 @@ program_license = """%s Created by Vincent Hatakeyama on %s. - Copyright 2020 XCG Consulting. All rights reserved. + Copyright 2022 XCG Consulting. All rights reserved. Licensed under the MIT License @@ -46,23 +52,15 @@ return parser -def parser_add_flake8_group(parser: argparse.ArgumentParser): - """Add flake8 option (--flake8/--no-flake8) to the given parser. - Defaults to not running flake8.""" - flake8_group = parser.add_mutually_exclusive_group() - flake8_group.add_argument( - "--flake8", - help="Run flake8", - action="store_true", - dest="flake8", - default=False, - ) - flake8_group.add_argument( - "--no-flake8", - help="Do not run flake8 [default]", - action="store_false", - dest="flake8", - default=False, +def parser_add_pylint_group(parser: argparse.ArgumentParser): + """Add pylint option to the given parser. + Defaults to not running pylint.""" + parser.add_argument( + "--pylint", + help="Run pylint on given python module (for example odoo.addons.base)", + dest=_PYLINT_DEST, + default=[], + nargs="+", ) @@ -66,12 +64,13 @@ ) -def apply_flake8(namespace: argparse.Namespace, odoo_type: str): - """Run flake8 if the option was set.""" - if namespace.flake8: - flake8(odoo_type) +def apply_pylint(namespace: argparse.Namespace): + """Run pylint if the option was set.""" + if _PYLINT_DEST in namespace and getattr(namespace, _PYLINT_DEST): + return pylint(getattr(namespace, _PYLINT_DEST), get_build_options(namespace)) + return 0 def main(argv=None): """Copy modules for a build, callable version that parses arguments""" parser = __parser() @@ -73,7 +72,9 @@ def main(argv=None): """Copy modules for a build, callable version that parses arguments""" parser = __parser() + parser_add_pylint_group(parser) + add_build_options(parser) nmspc = parser.parse_args(argv) apply(nmspc) @@ -78,7 +79,5 @@ nmspc = parser.parse_args(argv) apply(nmspc) - c = Config() - odoo_type = c.odoo_type - return flake8(odoo_type) + return apply_pylint(nmspc) @@ -83,6 +82,6 @@ -def flake8(odoo_type: str, pull: bool = True): - """Run flake8""" - client = DockerClient.client +def pylint(modules, build_options): + """Run pylint""" + project_path = os.path.realpath(".") @@ -88,13 +87,4 @@ - # determine image to use based on odoo version - repository = "quay.orus.io/odoo/odoo" - if odoo_type in ("odoo7", "odoo8", "odoo9", "odoo10"): - repository = "xcgd/flake8" - tag = "2" - elif odoo_type == ODOO_15: - tag = "15.0" - else: - repository = "xcgd/flake8" - tag = "3" - image = f"{repository}:{tag}" + # always use project image + c = Config() @@ -100,8 +90,3 @@ - if pull: - _logger.info("Pulling flake8 (%s)", image) - client.images.pull(repository, tag) - - _logger.info("Running flake8 (%s)", image) - container = None + odoo_type = c.odoo_type @@ -107,8 +92,5 @@ - # XXX When pressing ctrl-c during the container creating, the container - # is not removed as there is no return of the value of the Container - # object. There is several ways to do that: - # - give the container a unique name we know, and use that to clean - # - maybe use some kind of time frame to identify the container - # - warn the user of a potential container leak by using signals + mounts: List[Mount] = [] + environment = {} + target_source_dict: Dict[str, str] = {} @@ -114,11 +96,24 @@ - def stop_remove(): - """Stop then remove the container.""" - if container: - if container.status == "running": - container.stop() - container.wait() - container.remove() - - atexit.register(stop_remove) + # odoo configuration is not read so put the modules in the path + if odoo_type in ("odoo10", "odoo11", "odoo13", ODOO_15): + if odoo_type == "odoo10": + pythons = ("python2.7",) + elif odoo_type == "odoo11": + pythons = ("python3.5", "python3.6") + elif odoo_type == "odoo13": + pythons = ("python3.8",) + elif odoo_type == ODOO_15: + pythons = ("python3.9",) + for python in pythons: + result = modules_mount( + c.modules, + project_path, + f"/usr/local/lib/{python}/dist-packages/odoo/addons", + ) + mounts.extend(result[0]) + target_source_dict.update(result[1]) + elif odoo_type in ("odoo7", "odoo8"): + mounts, target_source_dict = modules_mount( + c.modules, project_path, "/opt/odoo/sources/odoo/addons" + ) @@ -124,11 +119,25 @@ - # split create and start so in case of SIGTERM, container has fewer chances - # of being None - container = client.containers.create( - image, - entrypoint="flake8", - command=["."], - volumes={os.environ["PWD"]: {"bind": "/mnt", "mode": "ro"}}, - working_dir="/mnt", - detach=True, + # Add a volume to store cache + cache_volume_name = "pylint-cache" + DockerClient.create_volume(cache_volume_name) + cache_target_path = "/var/cache/pylint" + mounts.append(Mount(cache_target_path, cache_volume_name)) + environment["PYLINTHOME"] = cache_target_path + + build_local_image(build_options) + + _logger.info("Running pylint") + return DockerClient.run( + c.local_image, + c.local_tag, + { + "entrypoint": "pylint", + "command": modules + ["--load-plugins=pylint_odoo"], + # needed to write in the cache + "user": "root", + "mounts": mounts, + "environment": environment, + }, + False, + target_source_dict, ) @@ -134,14 +143,4 @@ ) - container.start() - result = container.wait() - atexit.unregister(stop_remove) - atexit.register(container.remove) - print(container.logs().decode().rstrip()) - if result.get("Error", None): - _logger.warning(result.decode()) - container.remove(v=True) - atexit.unregister(container.remove) - return result["StatusCode"] if __name__ == "__main__": diff --git a/setup.py b/setup.py index 2a99fb809c685eb438448b434979af9b121e03ef_c2V0dXAucHk=..606a967c39b15e0a3d167619fb472e374ba176f9_c2V0dXAucHk= 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,7 @@ "docker_flake8=odoo_scripts.docker_flake8:main [docker]", "docker_isort=odoo_scripts.docker_isort:main [docker]", "docker_pg=odoo_scripts.docker_postgresql:main [docker]", + "docker_pylint=odoo_scripts.docker_pylint:main [docker]", "conf2reST=odoo_scripts.conf2reST:main [conf2reST]", "list_modules=odoo_scripts.list_modules:main", "update_duplicate_sources="