# Part of Odoo. See LICENSE file for full copyright and licensing details. import argparse import os import re import sys from pathlib import Path import jinja2 from . import Command class Scaffold(Command): """ Generates an Odoo module skeleton. """ def run(self, cmdargs): # TODO: bash completion file parser = argparse.ArgumentParser( prog=f'{Path(sys.argv[0]).name} {self.name}', description=self.__doc__, epilog=self.epilog(), ) parser.add_argument( '-t', '--template', type=template, default=template('default'), help="Use a custom module template, can be a template name or the" " path to a module template (default: %(default)s)") parser.add_argument('name', help="Name of the module to create") parser.add_argument( 'dest', default='.', nargs='?', help="Directory to create the module in (default: %(default)s)") if not cmdargs: sys.exit(parser.print_help()) args = parser.parse_args(args=cmdargs) if args.template.id == 'l10n_payroll': name_split = args.name.split('-') params = { 'name': name_split[0], 'code': name_split[1] } else: params = {'name': args.name} args.template.render_to( snake(args.name), directory(args.dest, create=True), params=params, ) def epilog(self): return "Built-in templates available are: %s" % ', '.join( d for d in os.listdir(builtins()) if d != 'base' ) builtins = lambda *args: os.path.join( os.path.abspath(os.path.dirname(__file__)), 'templates', *args) def snake(s): """ snake cases ``s`` :param str s: :return: str """ # insert a space before each uppercase character preceded by a # non-uppercase letter s = re.sub(r'(?<=[^A-Z])\B([A-Z])', r' \1', s) # lowercase everything, split on whitespace and join return '_'.join(s.lower().split()) def pascal(s): return ''.join( ss.capitalize() for ss in re.sub('[_\s]+', ' ', s).split() ) def directory(p, create=False): expanded = os.path.abspath( os.path.expanduser( os.path.expandvars(p))) if create and not os.path.exists(expanded): os.makedirs(expanded) if not os.path.isdir(expanded): die("%s is not a directory" % p) return expanded env = jinja2.Environment() env.filters['snake'] = snake env.filters['pascal'] = pascal class template(object): def __init__(self, identifier): # TODO: archives (zipfile, tarfile) self.id = identifier # is identifier a builtin? self.path = builtins(identifier) if os.path.isdir(self.path): return # is identifier a directory? self.path = identifier if os.path.isdir(self.path): return die("{} is not a valid module template".format(identifier)) def __str__(self): return self.id def files(self): """ Lists the (local) path and content of all files in the template """ for root, _, files in os.walk(self.path): for f in files: path = os.path.join(root, f) yield path, open(path, 'rb').read() def render_to(self, modname, directory, params=None): """ Render this module template to ``dest`` with the provided rendering parameters """ # overwrite with local for path, content in self.files(): path = env.from_string(path).render(params) local = os.path.relpath(path, self.path) # strip .template extension root, ext = os.path.splitext(local) if ext == '.template': local = root if self.id == "l10n_payroll": modname = f"l10n_{params['code']}_hr_payroll" dest = os.path.join(directory, modname, local) destdir = os.path.dirname(dest) if not os.path.exists(destdir): os.makedirs(destdir) with open(dest, 'wb') as f: if ext not in ('.py', '.xml', '.csv', '.js', '.rst', '.html', '.template'): f.write(content) else: env.from_string(content.decode('utf-8'))\ .stream(params or {})\ .dump(f, encoding='utf-8') f.write(b'\n') def die(message, code=1): print(message, file=sys.stderr) sys.exit(code) def warn(message): # ASK: shall we use logger ? print("WARNING:", message)