diff --git a/.pylintrc b/.pylintrc index 7fd3c57..51f5f37 100644 --- a/.pylintrc +++ b/.pylintrc @@ -37,4 +37,4 @@ disable=bad-inline-option, [IMPORTS] -ignored-modules=lxml,odoo,pytest +ignored-modules=lxml,odoo,pytest,radon,odoo_analyse diff --git a/README.md b/README.md index 9d1b761..7957792 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,8 @@ Or if you want to output it to `stdout`: `--test-filter` .. Include module starting with `test_` +`--estimate-state` .. Estimate the state of a module (installed/uninstalled) only using dependencies and auto_install flags. + `--state-filter installed` .. Only modules with a specific state. This connects to a database to determine the state of a module. The connection information are extracted from a configuration file or using the database parameters `--full-graph` .. If set all the above filters are only used for the starting nodes and not for the base modules @@ -77,6 +79,22 @@ These options can be used to extract instance specific information about modules `--db-password` .. If specified a password prompt will ask for the password to connect to the database +### Skipping + +These options control what is analysed and can result in a faster analyse. + +`--skip-all` .. All of the below + +`--skip-assets` .. Skip analysing assets (JS, CSS, Web Templates, ...) + +`--skip-data` .. Skip analysing CSV and XML data + +`--skip-language` .. Skip analysing the lines of code + +`--skip-python` .. Skip analysing python files (controllers, models, ...) + +`--skip-readme` .. Skip reading the readme files + ## Examples ### Usage as library @@ -112,4 +130,7 @@ $ odoo_analyse -l /path/to/cache.json --modules 'sale_*' --show-dependency --ful # Connect to the database from the odoo.cfg and create the dependency graph of all installed modules $ odoo_analyse -l /path/to/cache.json -c /path/to/odoo.cfg --state-filter installed --show-dependency + +# Estimate the state using the module dependencies and auto_install flags based on the `modules` given via stdin. Output the analysed installed modules to `analyse.json`. +$ cat modules.txt | odoo_analyse -p odoo --modules - --analyse analyse.json --estimate-state --state-filter installed ``` diff --git a/src/odoo_analyse/__init__.py b/src/odoo_analyse/__init__.py index 496187c..071b5fc 100644 --- a/src/odoo_analyse/__init__.py +++ b/src/odoo_analyse/__init__.py @@ -19,4 +19,4 @@ "geometric_mean", ] -VERSION = "1.5.0" +VERSION = "1.6.0" diff --git a/src/odoo_analyse/__main__.py b/src/odoo_analyse/__main__.py index dd0bf9c..a8ed971 100644 --- a/src/odoo_analyse/__main__.py +++ b/src/odoo_analyse/__main__.py @@ -6,10 +6,12 @@ import glob import logging import os +import shutil import sys from getpass import getpass from .odoo import Odoo +from .utils import folder_blacklist try: import graphviz @@ -26,199 +28,238 @@ _logger = logging.getLogger(__name__) +Extensions = { + "2to3": "Automatic porting needs 2to3 installed", + "cloc": "Language analyse needs cloc", + "eslintcc": "eslintcc not found. Skipping complexity for js", +} + + def ensure_module(name, module): """Exit if module isn't installed""" if module is None: - print("Python module %s isn't installed" % name) + print(f"Python module {name} isn't installed") sys.exit(1) -def parse_args(): - parser = argparse.ArgumentParser( - description=( - "Default color encoding for the module graph:\n" - " * Green: Migrated module where all dependencies are migrated\n" - " * Orange: Migrated module where not all dependencies are migrated\n" - " * Blue: Not migrated module where all dependencies are migrated\n" - " * Red: Edges belonging to a loop\n" - "\n" - "Default color encoding for the model/view graph:\n" - " * Blue: Module without dependencies\n" - " * Red: Edges belonging to a loop\n" - ), - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - - group = parser.add_argument_group("Loading/Saving") - group.add_argument( +def parser_load_save(parser): + parser.add_argument( "-c", "--config", default=False, help="Specify an odoo configuration file to load modules", ) - group.add_argument( + parser.add_argument( "-p", "--path", default=[], action="append", help="Specify a path to search for odoo modules", ) - group.add_argument( + parser.add_argument( "-l", "--load", default=[], action="append", help="Load from a json file. To read the data from the stdin you can use `-`", ) - group.add_argument( + parser.add_argument( "-s", "--save", default=False, help="Save to a json file. To write the data to the stdout you can use `-`", ) - group = parser.add_argument_group("Filters") - group.add_argument( + +def parser_analyse(parser): + parser.add_argument( + "--skip-assets", + action="store_true", + help="Skip analysing assets", + ) + parser.add_argument( + "--skip-data", + action="store_true", + help="Skip analysing data/views", + ) + parser.add_argument( + "--skip-language", + action="store_true", + help="Skip analysing the language", + ) + parser.add_argument( + "--skip-python", + action="store_true", + help="Skip analysing the language", + ) + parser.add_argument( + "--skip-readme", + action="store_true", + help="Skip analysing the readme", + ) + parser.add_argument( + "--skip-all", + action="store_true", + help="Only analyse the absolute minimum", + ) + + +def parser_filters(parser): + parser.add_argument( "--path-filter", default="*", help="Filter out modules which paths aren't matching the glob. " "Separate multiple filters by comma", ) - group.add_argument( + parser.add_argument( "--models", default="*", help="Filter out models which names aren't matching the glob. " "Separate multiple filters by comma", ) - group.add_argument( + parser.add_argument( "--modules", default="*", help="Filter out modules which names aren't matching the glob. " - "Separate multiple filters by comma", + "Separate multiple filters by comma. Use `-` load load from stdin", ) - group.add_argument( + parser.add_argument( "--views", default="*", help="Filter out views which names aren't matching the glob. " "Separate multiple filters by comma", ) - group.add_argument( - "--test-filter", + parser.add_argument( + "--no-test-filter", action="store_true", default=False, help="Include testing modules starting with test_", ) - group.add_argument( + parser.add_argument( + "--estimate-state", + action="store_true", + default=False, + help="Estimate the module state by the module list", + ) + parser.add_argument( "--state-filter", default=False, help="Filter modules by their state in a database. The connection information " "can be used for a configuration file or directly passed.", ) - group = parser.add_argument_group("Database") - group.add_argument("--db_host", default=None, help="The database host") - group.add_argument("--db_port", default=None, type=int, help="The database port") - group.add_argument("--db_user", default=None, help="The database user") - group.add_argument( + +def parser_database(parser): + parser.add_argument("--db_host", default=None, help="The database host") + parser.add_argument("--db_port", default=None, type=int, help="The database port") + parser.add_argument("--db_user", default=None, help="The database user") + parser.add_argument( "--db_password", default=False, action="store_true", help="Ask for the database password", ) - group.add_argument("--db_name", default=None, help="The name of the database") + parser.add_argument("--db_name", default=None, help="The name of the database") - group = parser.add_argument_group( - "Module graphs", - "Generate a module dependency graph using the following options to " - "Specify the visible dependencies", - ) - group.add_argument( + +def parser_module_graph(parser): + parser.add_argument( "--show-dependency", action="store_true", default=False, help="Show the module dependency of the manifest", ) - group.add_argument( + parser.add_argument( "--show-import", action="store_true", default=False, help="Show python imports between modules", ) - group.add_argument( + parser.add_argument( "--show-reference", action="store_true", default=False, help="Show xml references between modules", ) - group.add_argument( + parser.add_argument( "--migration", default=False, help="Color the migration status in the module graph. " "Must be a glob which matches all migrated versions", ) + parser.add_argument( + "--odoo-version", + default=None, + help="The Odoo version which will be used to extend module versions with " + "only `major.minor.patch` format", + ) + - group = parser.add_argument_group("Model graph") - group.add_argument( +def parser_model_graph(parser): + parser.add_argument( "--model-graph", action="store_true", default=False, help="Show the dependency graph of the models", ) - group.add_argument( + parser.add_argument( "--no-model-inherit", action="store_true", default=False, help="Don't use inherit in the dependency graph of the models", ) - group.add_argument( + parser.add_argument( "--no-model-inherits", action="store_true", default=False, help="Don't use inherits in the dependency graph of the models", ) - group = parser.add_argument_group("View graph") - group.add_argument( + +def parser_view_graph(parser): + parser.add_argument( "--view-graph", action="store_true", default=False, help="Show the dependency graph of the views", ) - group.add_argument( + parser.add_argument( "--no-view-inherit", action="store_true", default=False, help="Don't use inherit in the dependency graph of the views", ) - group.add_argument( + parser.add_argument( "--no-view-call", action="store_true", default=False, help="Don't use t-calls in the dependency graph of the views", ) - group = parser.add_argument_group("Stucture graph") - group.add_argument( + +def parser_structure_graph(parser): + parser.add_argument( "--structure-graph", action="store_true", default=False, help="Show the structure of the modules", ) - group = parser.add_argument_group("Misc") - group.add_argument( + +def parser_misc(parser): + parser.add_argument( "--analyse", default="", help="Analyse the modules and store it in the given file. " "To output to the stdout you can use `-`", ) - group.add_argument( + parser.add_argument( "--analyse-output", default="json", choices=("csv", "json"), help="The format the analyse will use. Default is %(default)s", ) - group.add_argument( + parser.add_argument( "-i", "--interactive", default=False, @@ -226,30 +267,65 @@ def parse_args(): help="Enter the interactive mode", ) - group = parser.add_argument_group("Options") - group.add_argument( + +def parser_options(parser): + parser.add_argument( "--verbose", default=False, action="store_true", help="Be verbose", ) - group.add_argument( + parser.add_argument( "--full-graph", action="store_true", default=False, help="Show the full graph and only use the filters for the starting nodes", ) if graphviz is not None: - group.add_argument( + parser.add_argument( "--renderer", default="dot", help=f"Specify the rendering engine. {graphviz.ENGINES}", ) + +def parse_args(): + parser = argparse.ArgumentParser( + description=( + "Default color encoding for the module graph:\n" + " * Green: Migrated module where all dependencies are migrated\n" + " * Orange: Migrated module where not all dependencies are migrated\n" + " * Blue: Not migrated module where all dependencies are migrated\n" + " * Red: Edges belonging to a loop\n" + "\n" + "Default color encoding for the model/view graph:\n" + " * Blue: Module without dependencies\n" + " * Red: Edges belonging to a loop\n" + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + parser_load_save(parser.add_argument_group("Loading/Saving")) + parser_analyse(parser.add_argument_group("Analyse Options")) + parser_filters(parser.add_argument_group("Filters")) + parser_database(parser.add_argument_group("Database")) + parser_module_graph( + parser.add_argument_group( + "Module graphs", + "Generate a module dependency graph using the following options to " + "Specify the visible dependencies", + ) + ) + parser_model_graph(parser.add_argument_group("Model graph")) + parser_view_graph(parser.add_argument_group("View graph")) + parser_structure_graph(parser.add_argument_group("Stucture graph")) + parser_misc(parser.add_argument_group("Misc")) + parser_options(parser.add_argument_group("Options")) + return parser.parse_args() -def main(): +def main(): # noqa: C901 # pylint: disable=R0915 args = parse_args() handler = logging.StreamHandler(sys.stderr) @@ -261,6 +337,14 @@ def main(): if args.verbose: logger.setLevel(logging.DEBUG) + # Check addons + for extension, warning in Extensions.items(): + if shutil.which(extension) is None: + _logger.warning(warning) + + # Blacklist setup folders + folder_blacklist({"setup"}) + # Load modules if args.config and not args.load: odoo = Odoo.from_config(args.config) @@ -270,13 +354,32 @@ def main(): for load in args.load: odoo.load_json(load) + cfg = { + "skip_assets": args.skip_assets or args.skip_all, + "skip_data": args.skip_data or args.skip_all, + "skip_language": args.skip_language or args.skip_all, + "skip_python": args.skip_python or args.skip_all, + "skip_readme": args.skip_readme or args.skip_all, + } + for p in args.path: - odoo.load_path(glob.glob(os.path.abspath(os.path.expanduser(p)))) + odoo.load_path(glob.glob(os.path.abspath(os.path.expanduser(p))), **cfg) if args.interactive: odoo.interactive() sys.exit() + if args.modules == "-" and args.load == "-": + raise ValueError("Only `--load` or `--modules` can be `-` but not both") + + if args.modules == "-": + modules = ",".join(filter(None, (x.strip() for x in sys.stdin.readlines()))) + else: + modules = args.modules + + if args.estimate_state: + odoo.estimate_state(modules) + # Save the modules if args.save: odoo.save_json(args.save) @@ -289,21 +392,23 @@ def main(): odoo.set_opt("odoo.engine", args.renderer) # Apply the filters - if not args.test_filter: + if not args.no_test_filter: odoo.test_filter() odoo.path_filter(args.path_filter) if args.state_filter: - ensure_module("psycopg2", psycopg2) - odoo.state_filter( - args.config, - state=args.state_filter, - host=args.db_host, - database=args.db_name, - user=args.db_user, - port=args.db_port, - password=getpass() if args.db_password else None, - ) + if not args.estimate_state: + ensure_module("psycopg2", psycopg2) + odoo.load_state_from_database( + args.config, + host=args.db_host, + database=args.db_name, + user=args.db_user, + port=args.db_port, + password=getpass() if args.db_password else None, + ) + + odoo.state_filter(args.state_filter) if args.analyse: odoo.analyse(args.analyse, out_format=args.analyse_output) @@ -312,17 +417,18 @@ def main(): if args.show_dependency or args.show_import or args.show_reference: ensure_module("graphviz", graphviz) odoo.show_module_graph( - args.modules, - args.migration, - args.show_dependency, - args.show_import, - args.show_reference, + modules, + version=args.migration, + depends=args.show_dependency, + imports=args.show_import, + refers=args.show_reference, + odoo_version=args.odoo_version, ) # Render the structure graph if args.structure_graph: ensure_module("graphviz", graphviz) - odoo.show_structure_graph(args.modules, args.models, args.views) + odoo.show_structure_graph(modules, args.models, args.views) # Render the model graph if args.model_graph: diff --git a/src/odoo_analyse/js_module.py b/src/odoo_analyse/js_module.py index d9a754f..0caf070 100644 --- a/src/odoo_analyse/js_module.py +++ b/src/odoo_analyse/js_module.py @@ -74,9 +74,9 @@ def url_to_module_path(url): if url.endswith(".js"): url = url[:-3] if match["type"] == "src": - return "@%s%s" % (match["module"], url) + return f"@{match['module']}{url}" - return "@%s/../tests%s" % (match["module"], url) + return f"@{match['module']}/../tests{url}" return url @@ -94,8 +94,8 @@ def __init__(self, name, alias=None, complexity=None, default=True, requires=Non def __repr__(self): name = self.name if self.alias: - name = "%s/%s" % (name, self.alias) - return "" % name + name = f"{name}/{self.alias}" + return f"" def copy(self): return JSModule( @@ -140,24 +140,30 @@ def from_file(cls, path, file): # Old odoo.define format defines = ODOO_DEFINE_RE.findall(content) if defines: - if len(defines) > 1: - _logger.warning("Multiple odoo.define in single JS %s", name) - - define = defines[0][1] - requires = [x[1] for x in REQUIRE_RE.findall(content)] - return cls(name, alias=define, complexity=complexity, requires=requires) + result = {} + for define in defines: + requires = [x[1] for x in REQUIRE_RE.findall(content)] + result[name] = cls( + name, + alias=define[1], + complexity=complexity, + requires=requires, + ) + return result # Newer odoo-module format module = ODOO_MODULE_RE.findall(content) if module: imports = [x[-1] for x in IMPORT_BASIC_RE.findall(content)] requires = [x[1] for x in REQUIRE_RE.findall(content)] - return cls( - name, - alias=module[0][2], - complexity=complexity, - default=not module[0][4], - requires=imports + requires, - ) - - return cls(name, complexity=complexity) + return { + name: cls( + name, + alias=module[0][2], + complexity=complexity, + default=not module[0][4], + requires=imports + requires, + ) + } + + return {name: cls(name, complexity=complexity)} diff --git a/src/odoo_analyse/module.py b/src/odoo_analyse/module.py index 7c72a70..3a9ae24 100644 --- a/src/odoo_analyse/module.py +++ b/src/odoo_analyse/module.py @@ -40,6 +40,8 @@ def __init__(self, path): self.path = path # Technical name of the module self.name = path.rstrip("/").split("/")[-1] + # Odoo state of the module + self.state = None # Manifest of the module self.manifest = {} # Models defined in the module @@ -292,11 +294,11 @@ def _parse_js(self, path, pattern): if not file.endswith(".js"): continue - module = JSModule.from_file(file, pattern) - if not module: + modules = JSModule.from_file(file, pattern) + if not modules: return - self.js_modules[module.name] = module + self.js_modules.update(modules) def _parse_assets(self, parent_path): for files in self.manifest.get("assets", {}).values(): @@ -379,6 +381,7 @@ def _parse_readme(self, path): def to_json(self): return { "path": self.path, + "state": self.state, "name": self.name, "duration": self.duration, "manifest": self.manifest, @@ -426,7 +429,7 @@ def from_json(cls, data): return module @classmethod - def from_path(cls, path): + def from_path(cls, path, **config): # noqa: C901 parent_path = str(Path(path).parent.absolute()) files_list = [] analyse_start = time.time() @@ -452,10 +455,11 @@ def from_path(cls, path): # Found the init script if f == "__init__.py": found_init = True - module._parse_python(path, f) + if not config.get("skip_python"): + module._parse_python(path, f) # Found the readme - elif is_readme(f): + elif is_readme(f) and not config.get("skip_readme"): module._parse_readme(path + f) filepath = os.path.join(path, f) @@ -465,17 +469,20 @@ def from_path(cls, path): if not found_init: return None - module.analyse_language() + if not config.get("skip_language"): + module.analyse_language() - module._parse_assets(parent_path) + if not config.get("skip_assets"): + module._parse_assets(parent_path) - for file in module.files: - file_path = os.path.join(path, file) - files_list.append(file_path) - if file.endswith(".xml"): - module._parse_xml(file_path, parent_path) - elif file.endswith(".csv"): - module._parse_csv(file_path) + if not config.get("skip_data"): + for file in module.files: + file_path = os.path.join(path, file) + files_list.append(file_path) + if file.endswith(".xml"): + module._parse_xml(file_path, parent_path) + elif file.endswith(".csv"): + module._parse_csv(file_path) module.analyse_hash(files_list) @@ -487,7 +494,7 @@ def from_path(cls, path): return module @classmethod - def find_modules_iter(cls, paths, depth=None): + def find_modules_iter(cls, paths, depth=None, **config): result = {} if isinstance(paths, str): paths = [paths] @@ -502,7 +509,7 @@ def find_modules_iter(cls, paths, depth=None): continue try: - module = cls.from_path(path) + module = cls.from_path(path, **config) except Exception as e: _logger.exception(e) continue @@ -520,5 +527,5 @@ def find_modules_iter(cls, paths, depth=None): paths.extend((p, d + 1) for p in sub_paths if os.path.isdir(p)) @classmethod - def find_modules(cls, paths, depth=None): - return dict(cls.find_modules_iter(paths, depth)) + def find_modules(cls, paths, depth=None, **config): + return dict(cls.find_modules_iter(paths, depth, **config)) diff --git a/src/odoo_analyse/odoo.py b/src/odoo_analyse/odoo.py index 780440b..f9c1c24 100644 --- a/src/odoo_analyse/odoo.py +++ b/src/odoo_analyse/odoo.py @@ -26,6 +26,16 @@ _logger = logging.getLogger(__name__) +def extend_version(version, odoo_version): + if not isinstance(version, str) or not odoo_version: + return version + + if "." not in odoo_version: + odoo_version += ".0" + + return f"{odoo_version}.{version}" if version.count(".") < 3 else version + + def match(s, patterns): return any(fnmatch(s, x) for x in patterns.split(",")) @@ -79,8 +89,7 @@ def load_config(self, file): for section_name, section in cp.items(): self.config[section_name] = dict(section) for option_name, value in section.items(): - key = "%s.%s" % (section_name, option_name) - self.config[key] = value + self.config[f"{section_name}.{option_name}"] = value def set_opt(self, option, value): self.config[option] = value @@ -134,10 +143,33 @@ def name_filter(self, pattern): name: module for name, module in self.items() if match(name, pattern) } - def state_filter(self, config_path=None, state="installed", **kwargs): - """Filter the modules by their states in a database""" - + def state_filter(self, state="installed"): _logger.debug("Applying filter: state [%s]", state) + self.modules = { + name: module for name, module in self.items() if module.state == state + } + + def estimate_state(self, modules): + installed = {name for name in self.full if match(name, modules)} + + graph = {} + for name, module in self.full.items(): + if name not in graph: + graph[name] = set() + graph[name].update(module.depends) + + for dep in module.depends: + if dep not in graph: + graph[dep] = set() + + downstream, upstream = self._resolve_dependencies(graph, installed) + installed = downstream | upstream + + for name, module in self.full.items(): + module.state = ["uninstalled", "installed"][name in installed] + + def load_state_from_database(self, config_path=None, **kwargs): + """Filter the modules by their states in a database""" def adapt(val): if val.lower() in ("false", "none", ""): @@ -181,11 +213,10 @@ def adapt(val): # Connect to the database and fetch the modules in the given state with connect(**args) as db: cr = db.cursor() - cr.execute("SELECT name FROM ir_module_module WHERE state = %s", (state,)) - names = {row[0] for row in cr.fetchall()} - - # Apply the filter - self.modules = {name: module for name, module in self.items() if name in names} + cr.execute("SELECT name, state FROM ir_module_module") + for name, state in cr.fetchall(): + if name in self.full: + self.full[name].state = state def load(self, config_path): _logger.debug("Reading odoo configuration file") @@ -259,6 +290,7 @@ def analyse(self, file_path, out_format="json"): "category": module.category, "version": module.version, "status": list(module.status), + "state": module.state or "", } if missing: _logger.error("Missing dependency: %s -> %s", name, missing) @@ -299,16 +331,16 @@ def _analyse_out_json(self, data, file_path): # pylint: disable=R0201 """Output the analyse result as JSON""" # Write to a file or stdout if file_path == "-": - json.dump(data, sys.stdout, indent=2) + print(json.dumps(data, indent=2)) else: with open(file_path, "w+", encoding="utf-8") as fp: json.dump(data, fp, indent=2) - def load_path(self, paths, depth=None): + def load_path(self, paths, depth=None, **config): if isinstance(paths, str): paths = [paths] - result = Module.find_modules(paths, depth=depth) + result = Module.find_modules(paths, depth=depth, **config) self.full.update(result.copy()) self.modules.update(result.copy()) @@ -346,6 +378,37 @@ def _find_edges_in_loop(self, graph): # pylint: disable=R0201 return {(a, b) for a, bs in graph.items() for b in bs} + def _resolve_dependencies(self, graph, nodes): + visible = set(nodes) + nodes = list(nodes) + highlight = set() + + visited = set() + while nodes: + current = nodes.pop() + if current in visited: + continue + visited.add(current) + + depends = graph[current] + visible.update(depends) + nodes.extend(depends) + + # Extend the auto install + current, previous = len(visible), None + while current != previous: + for name, module in self.full.items(): + if ( + module.auto_install + and module.depends.issubset(visible) + and name not in visible + ): + visible.add(name) + highlight.add(name) + current, previous = len(visible), current + + return visible, highlight + def _show_graph( self, graph, @@ -377,30 +440,7 @@ def _show_graph( # Show all dependency ignoring the filters highlight = set() if self.opt("odoo.show_full_dependency") or self.show_full_dependency: - nodes = list(visible) - visited = set() - while nodes: - current = nodes.pop() - if current in visited: - continue - visited.add(current) - - depends = graph[current] - visible.update(depends) - nodes.extend(depends) - - # Extend the auto install - current, previous = len(visible), None - while current != previous: - for name, module in self.full.items(): - if ( - module.auto_install - and module.depends.issubset(visible) - and name not in visible - ): - visible.add(name) - highlight.add(name) - current, previous = len(visible), current + visible, highlight = self._resolve_dependencies(graph, visible) if not visible: return @@ -487,7 +527,7 @@ def render_view(module_id, view_name, _view): self._show_output(output, filename=filename or "structure.gv") - def _build_module_graph(self, depends, imports, refers): + def _build_module_graph(self, depends=True, imports=False, refers=False): graph = {} for name, module in self.items(): graph[name] = set() @@ -508,6 +548,7 @@ def show_module_graph( imports=False, refers=False, filename=None, + odoo_version=None, ): # Build the dependency graph graph = self._build_module_graph(depends, imports, refers) @@ -516,7 +557,12 @@ def show_module_graph( loop_edges = self._find_edges_in_loop(graph) # Evaluate the migration state if possible if version: - migrated = {name for name, module in self.items()} + migrated = { + name + for name, module in self.items() + if match(extend_version(module.version, odoo_version), version) + } + fully_migrated = { name for name, mod in self.items() diff --git a/src/odoo_analyse/utils.py b/src/odoo_analyse/utils.py index 635aa14..a52c319 100644 --- a/src/odoo_analyse/utils.py +++ b/src/odoo_analyse/utils.py @@ -94,7 +94,6 @@ def try_automatic_port(filepath): """Tries to port a python 2 script to python 3 using 2to3""" cmd = shutil.which("2to3") if cmd is None: - _logger.warning("Automatic porting needs 2to3 installed") return False with subprocess.Popen( @@ -110,10 +109,9 @@ def analyse_language(path): """Analyse the languages of a directory""" cmd = shutil.which("cloc") if cmd is None: - _logger.warning("Language analyse needs cloc") return {} - output, error = call([cmd, path, "--json"]) + output, error = call([cmd, path, "--json", "--strip-str-comments"]) if error: _logger.warning(error) @@ -164,7 +162,6 @@ def eslint_complexity(js_file): """Return the JS complexity using eslintcc""" cmd = shutil.which("eslintcc") if not cmd: - _logger.warning(f"eslintcc not found. Skipping complexity for js {js_file}") return None output, _ = call([cmd, "-a", "-f=json", js_file]) diff --git a/tests/test_odoo.py b/tests/test_odoo.py index 6707a31..071a691 100644 --- a/tests/test_odoo.py +++ b/tests/test_odoo.py @@ -97,14 +97,14 @@ def test_odoo_filters(odoo): odoo.modules["abc"] = odoo.modules["def"] = module cr = db.__enter__.return_value = MagicMock() cur = cr.cursor.return_value = MagicMock() - cur.fetchall.return_value = [("def",)] - odoo.state_filter() + cur.fetchall.return_value = [("def", "installed")] + odoo.load_state_from_database() mock.assert_called_once() cr.cursor.assert_called_once() cur.execute.assert_called_once() - assert len(odoo) == 1 + odoo.state_filter() def test_odoo_run_graph(odoo):