from __future__ import annotations from argparse import SUPPRESS from collections import namedtuple from contextlib import contextmanager from docutils import nodes as n from docutils.parsers.rst.directives import unchanged_required from sphinx.util.docutils import SphinxDirective from sphinxarg.parser import parse_parser from virtualenv.run.plugin.base import ComponentBuilder TableRow = namedtuple("TableRow", ["names", "default", "choices", "help"]) TextAsDefault = namedtuple("TextAsDefault", ["text"]) CUSTOM = { "discovery": ComponentBuilder.entry_points_for("virtualenv.discovery"), "creator": ComponentBuilder.entry_points_for("virtualenv.create"), "seeder": ComponentBuilder.entry_points_for("virtualenv.seed"), "activators": ComponentBuilder.entry_points_for("virtualenv.activate"), } class CliTable(SphinxDirective): name = "table_cli" option_spec = {"module": unchanged_required, "func": unchanged_required} def run(self): module_name, attr_name = self.options["module"], self.options["func"] parser_creator = getattr(__import__(module_name, fromlist=[attr_name]), attr_name) core_result = parse_parser(parser_creator()) core_result["action_groups"] = [i for i in core_result["action_groups"] if i["title"] not in CUSTOM] content = [] for i in core_result["action_groups"]: content.append(self._build_table(i["options"], i["title"], i["description"])) for key, name_to_class in CUSTOM.items(): section = n.section("", ids=[f"section-{key}"]) title = n.title("", key) section += title self.state.document.note_implicit_target(title) content.append(section) results = {} for name, class_n in name_to_class.items(): with self._run_parser(class_n, key, name): cmd = [f"--{key}", name] parser_result = parse_parser(parser_creator(cmd)) opt_group = next(i["options"] for i in parser_result["action_groups"] if i["title"] == key) results[name] = opt_group core_names = set.intersection(*[{tuple(i["name"]) for i in v} for v in results.values()]) if core_names: rows = [i for i in next(iter(results.values())) if tuple(i["name"]) in core_names] content.append( self._build_table(rows, title="core", description=f"options shared across all {key}"), ) for name, group in results.items(): rows = [i for i in group if tuple(i["name"]) not in core_names] if rows: content.append( self._build_table(rows, title=name, description=f"options specific to {key} {name}"), ) return content @contextmanager def _run_parser(self, class_n, key, name): test_name = {"creator": "can_create", "activators": "supports"} func_name = test_name.get(key) try: if func_name is not None: prev = getattr(class_n, func_name) def a(*args, **kwargs): prev(*args, **kwargs) if key == "activators": return True elif key == "creator": if name == "venv": from virtualenv.create.via_global_ref.venv import ( ViaGlobalRefMeta, ) meta = ViaGlobalRefMeta() meta.symlink_error = None return meta from virtualenv.create.via_global_ref.builtin.via_global_self_do import ( BuiltinViaGlobalRefMeta, ) meta = BuiltinViaGlobalRefMeta() meta.symlink_error = None return meta raise RuntimeError setattr(class_n, func_name, a) yield finally: if func_name is not None: # noinspection PyUnboundLocalVariable setattr(class_n, func_name, prev) def _build_table(self, options, title, description): table = n.table() table["classes"] += ["colwidths-auto"] options_group = n.tgroup(cols=3) table += options_group for _ in range(3): options_group += n.colspec() body = self._make_table_body(self.build_rows(options), title, description) options_group += body return table plugins = { "creator": "virtualenv.create", "seed": "virtualenv.seed", "activators": "virtualenv.activate", "discovery": "virtualenv.discovery", } @staticmethod def build_rows(options): result = [] for option in options: names = option["name"] default = option["default"] if default is not None: if isinstance(default, str) and default and default[0] == default[-1] and default[0] == '"': default = default[1:-1] if default == SUPPRESS: default = None choices = option.get("choices") key = names[0].strip("-") if key in CliTable.plugins: choices = list(ComponentBuilder.entry_points_for(CliTable.plugins[key]).keys()) help_text = option["help"] row = TableRow(names, default, choices, help_text) result.append(row) return result def _make_table_body(self, rows, title, description): t_body = n.tbody() header_row = n.paragraph() header_row += n.strong(text=title) if description: header_row += n.Text(" ⇒ ") header_row += n.Text(description) t_body += n.row("", n.entry("", header_row, morecols=2)) for row in rows: name_list = self._get_targeted_names(row) default = CliTable._get_default(row) help_text = CliTable._get_help_text(row) row_node = n.row("", n.entry("", name_list), n.entry("", default), n.entry("", help_text)) t_body += row_node return t_body def _get_targeted_names(self, row): names = [name.lstrip("-") for name in row.names] target = n.target("", "", ids=names, names=names) self.register_target_option(target) first = True for name, orig in zip(names, row.names): if first: first = False else: target += n.Text(", ") self_ref = n.reference(refid=name) self_ref += n.literal(text=orig) target += self_ref para = n.paragraph(text="") para += target return para @staticmethod def _get_help_text(row): name = row.names[0] if name in ("--creator",): content = row.help[: row.help.index("(") - 1] else: content = row.help if name in ("--setuptools", "--pip", "--wheel"): text = row.help at = text.index(" bundle ") help_body = n.paragraph("") help_body += n.Text(text[: at + 1]) help_body += n.literal(text="bundle") help_body += n.Text(text[at + 7 :]) else: help_body = n.paragraph("", "", n.Text(content)) if row.choices is not None: help_body += n.Text("; choice of: ") first = True for choice in row.choices: if first: first = False else: help_body += n.Text(", ") help_body += n.literal(text=choice) return help_body @staticmethod def _get_default(row): default = row.default name = row.names[0] if name == "-p": default_body = n.Text("the python executable virtualenv is installed into") elif name == "--app-data": default_body = n.Text("platform specific application data folder") elif name == "--activators": default_body = n.Text("comma separated list of activators supported") elif name == "--creator": default_body = n.paragraph("") default_body += n.literal(text="builtin") default_body += n.Text(" if exist, else ") default_body += n.literal(text="venv") else: if default is None: default_body = n.paragraph("", text="") else: default_body = n.literal(text=default if isinstance(default, str) else str(default)) return default_body def register_target_option(self, target) -> None: domain = self.env.get_domain("std") self.state.document.note_explicit_target(target) for key in target["ids"]: domain.add_program_option(None, key, self.env.docname, key) def literal_data(rawtext, app, type, slug, options): # noqa: U100 """Create a link to a BitBucket resource.""" of_class = type.split(".") data = getattr(__import__(".".join(of_class[:-1]), fromlist=[of_class[-1]]), of_class[-1]) return [n.literal("", text=",".join(data))], [] __all__ = ( "CliTable", "literal_data", )