diff --git a/src/rda_python_miscs/pg_rst.py b/src/rda_python_miscs/pg_rst.py index 715b256..4ac759a 100755 --- a/src/rda_python_miscs/pg_rst.py +++ b/src/rda_python_miscs/pg_rst.py @@ -19,7 +19,6 @@ import argparse import importlib from os import path as op -from rda_python_common.pg_LOG import PgLOG from rda_python_common.pg_file import PgFile from rda_python_common.pg_util import PgUtil @@ -169,8 +168,10 @@ def parse_docs(self, docname): Lines beginning with ``#`` are treated as comments and skipped. Inline trailing comments are also stripped. Angle-bracketed uppercase tokens - (e.g. ````) are temporarily escaped to ``<FILENAME>`` - so they are not misidentified as option markers later in processing. + (e.g. ````) are temporarily escaped to ``<FILENAME>`` + so they are not misidentified as option markers (``<:>``, ``<=>``, + ````) later in processing. They are unescaped back to ```` + in :meth:`replace_option_link` before appearing in RST output. Args: docname (str): Short document name used to locate ``/.usg``. @@ -179,48 +180,47 @@ def parse_docs(self, docname): self.pglog("Parsing info for Document '{}'".format(docname), self.LOGWRN) section = self.init_section('0', "Preface") option = example = None - fh = open(docfile, 'r') - line = fh.readline() - while line: - if re.match(r'\s*#', line): - line = fh.readline() - continue # skip comment lines - ms = re.match(r'^(.*\S)\s+#', line) - if ms: - line = ms.group(1) # remove comments - else: - line = line.rstrip() # remove trailing white spaces - - # Temporarily escape tokens so they are not confused - # with special markers like <:>, <=>, used in option parsing. - while True: - ms = re.search(r'(<([A-Z/\-\.]+)>)', line) + with open(docfile, 'r') as fh: + line = fh.readline() + while line: + if re.match(r'\s*#', line): + line = fh.readline() + continue # skip comment lines + ms = re.match(r'^(.*\S)\s+#', line) if ms: - line = line.replace(ms.group(1), "<{}>".format(ms.group(2))) + line = ms.group(1) # remove comments else: - break - ms = re.match(r'^([\d\.]+)\s+(.+)$', line) - if ms: # start new section - section = self.record_section(section, option, example, ms.group(1), ms.group(2)) - option = example = None - else: - ms = re.match(r'^ -([A-Z]{2}) or -\w+(.*)$', line) - if ms: # found new option - option = self.record_option(section, option, example, ms.group(1), ms.group(2)) - example = None - elif option: - ms = re.match(r'^ For( | another )example, (.*)$', line) - if ms: # found example - example = self.record_example(option, example, ms.group(2)) - elif example: - example['desc'] += line + "\n" + line = line.rstrip() # remove trailing white spaces + + # Temporarily escape tokens so they are not confused + # with special markers like <:>, <=>, used in option parsing. + while True: + ms = re.search(r'(<([A-Z/\-\.]+)>)', line) + if ms: + line = line.replace(ms.group(1), "<{}>".format(ms.group(2))) else: - option['desc'] += line + "\n" + break + ms = re.match(r'^([\d\.]+)\s+(.+)$', line) + if ms: # start new section + section = self.record_section(section, option, example, ms.group(1), ms.group(2)) + option = example = None else: - section['desc'] += line + "\n" + ms = re.match(r'^ -([A-Z]{2}) or -\w+(.*)$', line) + if ms: # found new option + option = self.record_option(section, option, example, ms.group(1), ms.group(2)) + example = None + elif option: + ms = re.match(r'^ For( | another )example, (.*)$', line) + if ms: # found example + example = self.record_example(option, example, ms.group(2)) + elif example: + example['desc'] += line + "\n" + else: + option['desc'] += line + "\n" + else: + section['desc'] += line + "\n" - line = fh.readline() - fh.close() + line = fh.readline() self.record_section(section, option, example) @@ -323,15 +323,14 @@ def init_section(self, secid, title): dict: New section dict with keys ``secid``, ``title``, ``desc``, ``level``, and ``opts``. """ + level = secid.count('.') + 1 section = { 'secid' : secid, 'title' : title, 'desc' : "", - 'level' : 0, + 'level' : level, 'opts' : [] } - level = len(re.split(r'\.', secid)) - section['level'] = level if level == 1: if re.match(r'^ACTION', section['title']): self.SECIDS['Action'] = secid @@ -397,9 +396,7 @@ def init_example(self, opt, desc): Returns: dict: New example dict with keys ``opt``, ``title``, and ``desc``. """ - example = {'opt' : opt, 'title' : "", 'desc' : desc.title() + "\n"} - - return example + return {'opt' : opt, 'title' : "", 'desc' : desc.title() + "\n"} # # write the entry file: index.rst @@ -471,32 +468,28 @@ def template_to_rst(self, template, hash, extra=None): if extra is None: extra = "" rstfile = "{}/{}{}.rst".format(self.DOCS['DOCDIR'], template, extra) - tf = open(tempfile, 'r') - rf = open(rstfile, 'w') - idx = 0 - line = tf.readline() - while line: - idx += 1 - if re.match(r'\s*#', line): - line = tf.readline() - continue # skip comment lines - ms = re.match(r'^(.*\S)\s+#', line) - if ms: - line = ms.group(1) # remove comments - else: - line = line.rstrip() # remove trailing white spaces - - matches = re.findall(r'__([A-Z]+)__', line) - if matches: - for key in matches: - if key not in hash: self.pglog("{}: not defined at {}({}) {}".format(key, line, idx, tempfile), self.LGWNEX) - if not hash[key]: self.pglog(key + ": empty content", self.LGWNEX) - line = line.replace("__{}__".format(key), hash[key]) - rf.write(line + "\n") + with open(tempfile, 'r') as tf, open(rstfile, 'w') as rf: + idx = 0 line = tf.readline() - - tf.close() - rf.close() + while line: + idx += 1 + if re.match(r'\s*#', line): + line = tf.readline() + continue # skip comment lines + ms = re.match(r'^(.*\S)\s+#', line) + if ms: + line = ms.group(1) # remove comments + else: + line = line.rstrip() # remove trailing white spaces + + matches = re.findall(r'__([A-Z]+)__', line) + if matches: + for key in matches: + if key not in hash: self.pglog("{}: not defined at {}({}) {}".format(key, line, idx, tempfile), self.LGWNEX) + if not hash[key]: self.pglog(key + ": empty content", self.LGWNEX) + line = line.replace("__{}__".format(key), hash[key]) + rf.write(line + "\n") + line = tf.readline() self.pglog("{}{}.rst created from {}.rst.temp".format(template, extra, template), self.LOGWRN) # @@ -685,12 +678,10 @@ def replace_option_link(self, line, csecid, ptype=None, dtype=None): elif ptype == 2: opts = re.findall(r'(-\(*)([a-zA-Z]{2,})(\W|$)', line) ms = re.match(r'^\s*%s(\s+[\w\.]+\s+|\s+)([a-zA-Z]{2})(\s)' % self.DOCS['DOCNAM'], line) - # list.insert() returns None; prepend the match groups explicitly. if ms: opts = [ms.groups()] + opts else: opts = re.findall(r'(^-\(*|\W-\(*)([a-zA-Z]{2,})(\W|$)', line) - if opts is None: opts = [] for optary in opts: opt = self.get_short_option(optary[1]) pre = optary[0] @@ -712,7 +703,6 @@ def replace_option_link(self, line, csecid, ptype=None, dtype=None): after = ')' replace = pre + opt + after - if re.search(r'', after): after = after.replace(r'', '<!>') link = "{}`{} <{}>`_{}".format(pre, opt, link, after) line = line.replace(replace, link) @@ -753,6 +743,10 @@ def replace_option_link(self, line, csecid, ptype=None, dtype=None): link = self.Q1 + opt + self.Q2 line = line.replace(replace, link) + # Unescape tokens that were temporarily escaped during + # parsing to avoid confusion with option markers (<:>, <=>, ). + line = line.replace('<', '<').replace('>', '>') + return line # @@ -781,7 +775,7 @@ def create_description(self, desc, secid, dtype): ptype = 0 # paragraph type: 0 - normal, 1 - table, 2 = synopsis content = '' cnt = 0 - alllines = re.split(r'\n', desc) + alllines = desc.split('\n') lines = [] for line in alllines: if re.match(r'^\s*\S', line): @@ -1078,7 +1072,7 @@ def get_short_option(self, p): for opt in self.ALIAS: for alias in self.ALIAS[opt]: - if re.match(r'^{}$'.format(alias), p, re.I): return opt + if re.match(r'^{}$'.format(alias), p, re.I): return opt self.pglog("{} - unknown option for {}".format(p, self.DOCS['DOCNAM']), self.LGWNEX) @@ -1120,84 +1114,82 @@ def get_section(self, secid): self.pglog("Unknown Section ID {}".format(secid), self.LGWNEX) + def load_opts_alias(self, docname): + """Import ``rda_python_.`` and return its ``(OPTS, ALIAS, origin)`` triple. -# --------------------------------------------------------------------------- -# Command-line entry point -# --------------------------------------------------------------------------- - -def _load_opts_alias(docname): - """Import ``rda_python_.`` and return its ``(OPTS, ALIAS, origin)`` triple. + Resolution order for OPTS / ALIAS: - Resolution order for OPTS / ALIAS: + 1. Module-level ``OPTS`` / ``ALIAS`` attributes. + 2. The first class *defined in that module* that carries both ``OPTS`` + and ``ALIAS`` as class-level attributes. - 1. Module-level ``OPTS`` / ``ALIAS`` attributes. - 2. The first class *defined in that module* that carries both ``OPTS`` - and ``ALIAS`` as class-level attributes. + ``ALIAS`` is optional; an empty dict is returned when not found. - ``ALIAS`` is optional; an empty dict is returned when not found. + The ``origin`` value is the absolute path of the directory that contains + ``.py`` (i.e. ``rda_python_/``), derived from + ``mod.__file__``. It is intended to be assigned to + ``PgRST.DOCS['ORIGIN']`` so that :meth:`PgRST.parse_docs` looks for the + ``.usg`` source file in the same location as the document module. - The ``origin`` value is the absolute path of the directory that contains - ``.py`` (i.e. ``rda_python_/``), derived from - ``mod.__file__``. It is intended to be assigned to - ``PgRST.DOCS['ORIGIN']`` so that :meth:`PgRST.parse_docs` looks for the - ``.usg`` source file in the same location as the document module. + Args: + docname (str): Short document name used to build the module path + ``rda_python_.``. - Args: - docname (str): Short document name used to build the module path - ``rda_python_.``. + Returns: + tuple[dict, dict, str]: ``(OPTS, ALIAS, origin)`` where *origin* is + the absolute directory path of the imported module file. - Returns: - tuple[dict, dict, str]: ``(OPTS, ALIAS, origin)`` where *origin* is - the absolute directory path of the imported module file. + Raises: + SystemExit: via :func:`PgLOG.pglog` (``LGWNEX``) if the module + cannot be imported or ``OPTS`` cannot be found. + """ + modname = "rda_python_{}.{}".format(docname, docname) + try: + mod = importlib.import_module(modname) + except ImportError as exc: + self.pglog( + "Cannot import module '{}': {}".format(modname, exc), + self.LGWNEX, + ) + + # Derive ORIGIN from the module's own file path. + origin = op.dirname(op.abspath(mod.__file__)) + + # 1. Try module-level attributes first. + opts = getattr(mod, 'OPTS', None) + alias = getattr(mod, 'ALIAS', None) + + # 2. Fall back to the first class in the module that owns both. + if opts is None or alias is None: + for _, obj in inspect.getmembers(mod, inspect.isclass): + if obj.__module__ == modname: + cls_opts = getattr(obj, 'OPTS', None) + cls_alias = getattr(obj, 'ALIAS', None) + if cls_opts is not None: + if opts is None: opts = cls_opts + if alias is None: alias = cls_alias + break + + if opts is None: + self.pglog( + "Module '{}' does not define OPTS (checked module level and " + "all classes defined in the module)".format(modname), + self.LGWNEX, + ) + + # ALIAS is optional; default to empty dict. + if alias is None: + alias = {} + + return opts, alias, origin - Raises: - SystemExit: via :func:`PgLOG.pglog` (``LGWNEX``) if the module - cannot be imported or ``OPTS`` cannot be found. - """ - modname = "rda_python_{}.{}".format(docname, docname) - try: - mod = importlib.import_module(modname) - except ImportError as exc: - PgLOG.pglog( - "Cannot import module '{}': {}".format(modname, exc), - PgLOG.LGWNEX, - ) - - # Derive ORIGIN from the module's own file path. - origin = op.dirname(op.abspath(mod.__file__)) - - # 1. Try module-level attributes first. - opts = getattr(mod, 'OPTS', None) - alias = getattr(mod, 'ALIAS', None) - - # 2. Fall back to the first class in the module that owns both. - if opts is None or alias is None: - for _, obj in inspect.getmembers(mod, inspect.isclass): - if obj.__module__ == modname: - cls_opts = getattr(obj, 'OPTS', None) - cls_alias = getattr(obj, 'ALIAS', None) - if cls_opts is not None: - if opts is None: opts = cls_opts - if alias is None: alias = cls_alias - break - - if opts is None: - PgLOG.pglog( - "Module '{}' does not define OPTS (checked module level and " - "all classes defined in the module)".format(modname), - PgLOG.LGWNEX, - ) - - # ALIAS is optional; default to empty dict. - if alias is None: - alias = {} - - return opts, alias, origin +# --------------------------------------------------------------------------- +# Command-line entry point +# --------------------------------------------------------------------------- def main(): """Entry point for command-line usage of pg_rst.py.""" - import argparse parser = argparse.ArgumentParser( description=( "Convert a .usg help document to reStructuredText (.rst) using RST templates. " @@ -1229,8 +1221,8 @@ def main(): ) args = parser.parse_args() - opts, alias, origin = _load_opts_alias(args.docname) pg = PgRST() + opts, alias, origin = pg.load_opts_alias(args.docname) pg.DOCS['ORIGIN'] = origin if args.docdir is not None: pg.DOCS['DOCDIR'] = args.docdir