def test_partial_ordering(self): """Always respect explicit order and prefer built-in ordering. Undeclared built-in sections will be inserted into explicit order according to default built-in ordering. The about section should come after custom sections unless explicitly ordered.""" plugin = module.VimPlugin('myplugin') main_module = module.Module('myplugin', plugin) intro = Block(vimdoc.SECTION) intro.Local(name='Introduction', id='intro') # Configure explicit order. intro.Global(order=['custom1', 'intro', 'custom2']) commands = Block(vimdoc.SECTION) commands.Local(name='Commands', id='commands') about = Block(vimdoc.SECTION) about.Local(name='About', id='about') custom1 = Block(vimdoc.SECTION) custom1.Local(name='Custom1', id='custom1') custom2 = Block(vimdoc.SECTION) custom2.Local(name='Custom2', id='custom2') # Merge in arbitrary order. for section in [commands, custom2, about, intro, custom1]: main_module.Merge(section) main_module.Close() self.assertEqual([custom1, intro, commands, custom2, about], list(main_module.Chunks()))
def test_duplicate_section(self): plugin = module.VimPlugin('myplugin') main_module = module.Module('myplugin', plugin) intro = Block(vimdoc.SECTION) intro.Local(name='Introduction', id='intro') main_module.Merge(intro) intro2 = Block(vimdoc.SECTION) intro2.Local(name='Intro', id='intro') with self.assertRaises(error.DuplicateSection) as cm: main_module.Merge(intro2) self.assertEqual(('Duplicate section intro defined.', ), cm.exception.args)
def test_missing_parent(self): """Parent sections should exist.""" plugin = module.VimPlugin('myplugin') main_module = module.Module('myplugin', plugin) first = Block(vimdoc.SECTION) first.Local(name='Section 1', id='first') second = Block(vimdoc.SECTION) second.Local(name='Section 2', id='second', parent_id='missing') main_module.Merge(first) main_module.Merge(second) with self.assertRaises(error.NoSuchParentSection) as cm: main_module.Close() expected = ('Section Section 2 has non-existent parent missing. ' 'Try setting the id of the parent section explicitly.') self.assertEqual((expected, ), cm.exception.args)
def test_default_section_ordering(self): """Sections should be ordered according to documented built-in ordering.""" plugin = module.VimPlugin('myplugin') main_module = module.Module('myplugin', plugin) intro = Block(vimdoc.SECTION) intro.Local(name='Introduction', id='intro') commands = Block(vimdoc.SECTION) commands.Local(name='Commands', id='commands') about = Block(vimdoc.SECTION) about.Local(name='About', id='about') # Merge in arbitrary order. main_module.Merge(commands) main_module.Merge(about) main_module.Merge(intro) main_module.Close() self.assertEqual([intro, commands, about], list(main_module.Chunks()))
def test_ordered_child(self): """Child sections should not be included in @order.""" plugin = module.VimPlugin('myplugin') main_module = module.Module('myplugin', plugin) first = Block(vimdoc.SECTION) first.Local(name='Section 1', id='first') second = Block(vimdoc.SECTION) second.Local(name='Section 2', id='second', parent_id='first') first.Global(order=['first', 'second']) main_module.Merge(first) main_module.Merge(second) with self.assertRaises(error.OrderedChildSections) as cm: main_module.Close() self.assertEqual( ("Child section second included in ordering ['first', 'second'].", ), cm.exception.args)
def test_section(self): plugin = module.VimPlugin('myplugin') main_module = module.Module('myplugin', plugin) intro = Block(vimdoc.SECTION) intro.Local(name='Introduction', id='intro') main_module.Merge(intro) main_module.Close() self.assertEqual([intro], list(main_module.Chunks()))
def test_manual_section_ordering(self): """Sections should be ordered according to explicitly configured order.""" plugin = module.VimPlugin('myplugin') main_module = module.Module('myplugin', plugin) intro = Block(vimdoc.SECTION) intro.Local(name='Introduction', id='intro') # Configure explicit order. intro.Global(order=['commands', 'about', 'intro']) commands = Block(vimdoc.SECTION) commands.Local(name='Commands', id='commands') about = Block(vimdoc.SECTION) about.Local(name='About', id='about') # Merge in arbitrary order. main_module.Merge(commands) main_module.Merge(about) main_module.Merge(intro) main_module.Close() self.assertEqual([commands, about, intro], list(main_module.Chunks()))
def _AddMaktabaFlagHelp(self): """If any maktaba flags were documented, add a default configuration section to explain how to use them. """ if self.GetCollection(vimdoc.FLAG): block = Block(vimdoc.SECTION, is_default=True) block.Local(id='config', name='Configuration') block.AddLine( 'This plugin uses maktaba flags for configuration. Install Glaive' ' (https://github.com/google/glaive) and use the @command(Glaive)' ' command to configure them.') self.Merge(block)
def Close(self): """Closes the module. All default sections that have not been overridden will be created. """ if self.GetCollection( vimdoc.FUNCTION) and 'functions' not in self.sections: functions = Block(vimdoc.SECTION) functions.Local(id='functions', name='Functions') self.Merge(functions) if (self.GetCollection(vimdoc.EXCEPTION) and 'exceptions' not in self.sections): exceptions = Block(vimdoc.SECTION) exceptions.Local(id='exceptions', name='Exceptions') self.Merge(exceptions) if self.GetCollection( vimdoc.COMMAND) and 'commands' not in self.sections: commands = Block(vimdoc.SECTION) commands.Local(id='commands', name='Commands') self.Merge(commands) if self.GetCollection( vimdoc.DICTIONARY) and 'dicts' not in self.sections: dicts = Block(vimdoc.SECTION) dicts.Local(id='dicts', name='Dictionaries') self.Merge(dicts) if self.GetCollection(vimdoc.FLAG): # If any maktaba flags were documented, add a default configuration # section to explain how to use them. config = Block(vimdoc.SECTION, is_default=True) config.Local(id='config', name='Configuration') config.AddLine( 'This plugin uses maktaba flags for configuration. Install Glaive' ' (https://github.com/google/glaive) and use the @command(Glaive)' ' command to configure them.') self.Merge(config) if ((self.GetCollection(vimdoc.FLAG) or self.GetCollection(vimdoc.SETTING)) and 'config' not in self.sections): config = Block(vimdoc.SECTION) config.Local(id='config', name='Configuration') self.Merge(config) for backmatter in self.backmatters: if backmatter not in self.sections: raise error.NoSuchSection(backmatter) # Use explicit order as partial ordering and merge with default section # ordering. All custom sections must be ordered explicitly. self.order = self._GetSectionOrder(self.order, self.sections) known = set(self.sections) neglected = sorted(known.difference(self.order)) if neglected: raise error.NeglectedSections(neglected, self.order) # Sections are now in order. for key in self.order: if key in self.sections: # Move to end. self.sections[key] = self.sections.pop(key)
def LookupTag(self, typ, name): """Returns the tag name for the given type and name.""" # Support both @command(Name) and @command(:Name). if typ == vimdoc.COMMAND: fullname = name.lstrip(':') elif typ == vimdoc.SETTING: scope_match = regex.setting_scope.match(name) fullname = scope_match and name or 'g:' + name else: fullname = name block = None if typ in self.collections: collection = self.collections[typ] candidates = [x for x in collection if x.FullName() == fullname] if len(candidates) > 1: raise KeyError('Found multiple %ss named %s' % (typ, name)) if candidates: block = candidates[0] if block is None: # Create a dummy block to get default tag. block = Block(typ) block.Local(name=fullname) return block.TagName()
def test_child_sections(self): """Sections should be ordered after their parents.""" plugin = module.VimPlugin('myplugin') main_module = module.Module('myplugin', plugin) first = Block(vimdoc.SECTION) first.Local(name='Section 1', id='first') # Configure explicit order. first.Global(order=['first', 'second', 'third']) second = Block(vimdoc.SECTION) second.Local(name='Section 2', id='second') third = Block(vimdoc.SECTION) third.Local(name='Section 3', id='third') child11 = Block(vimdoc.SECTION) child11.Local(name='child11', id='child11', parent_id='first') child12 = Block(vimdoc.SECTION) child12.Local(name='child12', id='child12', parent_id='first') child21 = Block(vimdoc.SECTION) child21.Local(name='child21', id='child21', parent_id='second') # Merge in arbitrary order. for m in [second, child12, third, child11, first, child21]: main_module.Merge(m) main_module.Close() self.assertEqual([first, child11, child12, second, child21, third], list(main_module.Chunks()))
def Modules(directory): """Creates modules from a plugin directory. Note that there can be many, if a plugin has standalone parts that merit their own helpfiles. Args: directory: The plugin directory. Yields: Module objects as necessary. """ directory = directory.rstrip(os.path.sep) addon_info = None # Check for module metadata in addon-info.json (if it exists). addon_info_path = os.path.join(directory, 'addon-info.json') if os.path.isfile(addon_info_path): try: with open(addon_info_path, 'r') as addon_info_file: addon_info = json.loads(addon_info_file.read()) except (IOError, ValueError) as e: warnings.warn( 'Failed to read file {}. Error was: {}'.format( addon_info_path, e), error.InvalidAddonInfo) plugin_name = None # Use plugin name from addon-info.json if available. Fall back to dir name. addon_info = addon_info or {} plugin_name = addon_info.get('name', os.path.basename(os.path.abspath(directory))) plugin = VimPlugin(plugin_name) # Set module metadata from addon-info.json. if addon_info is not None: # Valid addon-info.json. Apply addon metadata. if 'author' in addon_info: plugin.author = addon_info['author'] if 'description' in addon_info: plugin.tagline = addon_info['description'] # Crawl plugin dir and collect parsed blocks for each file path. paths_and_blocks = [] standalone_paths = [] autoloaddir = os.path.join(directory, 'autoload') for (root, dirs, files) in os.walk(directory): # Visit files in a stable order, since the ordering of e.g. the Maktaba # flags below depends upon the order that we visit the files. dirs.sort() files.sort() # Prune non-standard top-level dirs like 'test'. if root == directory: dirs[:] = [x for x in dirs if x in DOC_SUBDIRS + ['after']] if root == os.path.join(directory, 'after'): dirs[:] = [x for x in dirs if x in DOC_SUBDIRS] for f in files: filename = os.path.join(root, f) if os.path.splitext(filename)[1] == '.vim': relative_path = os.path.relpath(filename, directory) with open(filename) as filehandle: lines = list(filehandle) blocks = list(parser.ParseBlocks(lines, filename)) # Define implicit maktaba flags for files that call # maktaba#plugin#Enter. These flags have to be special-cased here # because there aren't necessarily associated doc comment blocks and # the name is computed from the file name. if (not relative_path.startswith('autoload' + os.path.sep) and relative_path != os.path.join( 'instant', 'flags.vim')): if ContainsMaktabaPluginEnterCall(lines): flagpath = relative_path if flagpath.startswith('after' + os.path.sep): flagpath = os.path.relpath(flagpath, 'after') flagblock = Block(vimdoc.FLAG, is_default=True) name_parts = os.path.splitext(flagpath)[0].split( os.path.sep) flagname = name_parts.pop(0) flagname += ''.join('[' + p + ']' for p in name_parts) flagblock.Local(name=flagname) flagblock.AddLine( 'Configures whether {} should be loaded.'. format(relative_path)) default = 0 if flagname == 'plugin[mappings]' else 1 # Use unbulleted list to make sure it's on its own line. Use # backtick to avoid helpfile syntax highlighting. flagblock.AddLine( ' - Default: {} `'.format(default)) blocks.append(flagblock) paths_and_blocks.append((relative_path, blocks)) if filename.startswith(autoloaddir): if blocks and blocks[0].globals.get('standalone'): standalone_paths.append(relative_path) docdir = os.path.join(directory, 'doc') if not os.path.isdir(docdir): os.mkdir(docdir) modules = [] main_module = Module(plugin_name, plugin) for (path, blocks) in paths_and_blocks: # Skip standalone paths. if GetMatchingStandalonePath(path, standalone_paths) is not None: continue namespace = None if path.startswith('autoload' + os.path.sep): namespace = GetAutoloadNamespace(os.path.relpath(path, 'autoload')) for block in blocks: main_module.Merge(block, namespace=namespace) modules.append(main_module) # Process standalone modules. standalone_modules = {} for (path, blocks) in paths_and_blocks: standalone_path = GetMatchingStandalonePath(path, standalone_paths) # Skip all but standalone paths. if standalone_path is None: continue assert path.startswith('autoload' + os.path.sep) namespace = GetAutoloadNamespace(os.path.relpath(path, 'autoload')) standalone_module = standalone_modules.get(standalone_path) # Initialize module if this is the first file processed from it. if standalone_module is None: standalone_module = Module(namespace.rstrip('#'), plugin) standalone_modules[standalone_path] = standalone_module modules.append(standalone_module) for block in blocks: standalone_module.Merge(block, namespace=namespace) for module in modules: module.Close() yield module
def Close(self): """Closes the module. All default sections that have not been overridden will be created. """ # ---------------------------------------------------------- # Add default sections. # type : (id, name) default_sections = [ (vimdoc.FUNCTION, 'functions', 'Functions'), (vimdoc.EXCEPTION, 'exceptions', 'Exceptions'), (vimdoc.COMMAND, 'commands', 'Commands'), (vimdoc.DICTIONARY, 'dicts', 'Dictionaries'), (vimdoc.FLAG, 'config', 'Configuration'), (vimdoc.SETTING, 'config', 'Configuration'), ] for (typ, id, name) in default_sections: if typ == vimdoc.FLAG: self._AddMaktabaFlagHelp() if self.GetCollection(typ) and (id not in self.sections): # Create the section if it does not exist. block = Block(vimdoc.SECTION) block.Local(id=id, name=name) self.Merge(block) # ---------------------------------------------------------- # Add backmatter. for backmatter in self.backmatters: if backmatter not in self.sections: raise error.NoSuchSection(backmatter) # ---------------------------------------------------------- # Expand child sections and order the section list for output. # Use explicit order as partial ordering and merge with default section # ordering. All custom sections must be ordered explicitly. self.order = self._GetSectionOrder(self.order, self.sections) # Child section collection to_delete = [] for key in self.sections: section = self.sections[key] parent_id = section.locals.get('parent_id', None) if parent_id: if parent_id not in self.sections: raise error.NoSuchParentSection( section.locals['name'], parent_id) parent = self.sections[parent_id] parent.locals.setdefault('children', []).append(section) to_delete.append(key) for key in to_delete: self.sections.pop(key) # Check that all top-level sections are included in ordering. known = set(self.sections) neglected = sorted(known.difference(self.order)) if neglected: raise error.NeglectedSections(neglected, self.order) # Reinsert top-level sections in the correct order, expanding the tree of # child sections along the way so we have a linear list of sections to pass # to the output functions. # Helper function to recursively add children to self.sections. # We add a 'level' variable to locals so that WriteTableOfContents can keep # track of the nesting. def _AddChildSections(section): section.locals.setdefault('level', 0) if 'children' in section.locals: sort_key = lambda s: s.locals['name'] for child in sorted(section.locals['children'], key=sort_key): child.locals['level'] = section.locals['level'] + 1 self.sections[child.locals['id']] = child _AddChildSections(child) # Insert sections according to the @order directive for key in self.order: if key in self.sections: section = self.sections.pop(key) if 'parent_id' in section.locals and section.locals['parent_id']: raise error.OrderedChildSections(section.locals['id'], self.order) # Move to end. self.sections[key] = section _AddChildSections(section)