def make_hashed_id(app, need_type, full_title, content, id_length=None): """ Creates an ID based on title or need. Also cares about the correct prefix, which is specified for each need type. :param app: Sphinx application object :param need_type: name of the need directive, e.g. req :param full_title: full title of the need :param content: content of the need :param id_length: maximum length of the generated ID :return: ID as string """ types = app.config.needs_types if id_length is None: id_length = app.config.needs_id_length type_prefix = None for ntype in types: if ntype["directive"] == need_type: type_prefix = ntype["prefix"] break if type_prefix is None: raise NeedsInvalidException( 'Given need_type {} is unknown. File {}'.format( need_type, app.env.docname)) hashable_content = full_title or '\n'.join(content) return "%s%s" % (type_prefix, hashlib.sha1( hashable_content.encode("UTF-8")).hexdigest().upper()[:id_length])
def _merge_global_options(app, needs_info, global_options): """Add all global defined options to needs_info""" if global_options is None: return for key, value in global_options.items(): # If key already exists in needs_info, this global_option got overwritten manually in current need if key in needs_info and needs_info[key]: continue if isinstance(value, tuple): values = [value] elif isinstance(value, list): values = value else: needs_info[key] = value continue for single_value in values: if len(single_value) < 2 or len(single_value) > 3: raise NeedsInvalidException( f"global option tuple has wrong amount of parameters: {key}" ) if filter_single_need(app, needs_info, single_value[1]): # Set value, if filter has matched needs_info[key] = single_value[0] elif len(single_value) == 3 and (key not in needs_info.keys() or len(str(needs_info[key])) > 0): # Otherwise set default, but only if no value was set before or value is "" and a default is defined needs_info[key] = single_value[2] else: # If not value was set until now, we have to set an empty value, so that we are sure that each need # has at least the key. if key not in needs_info.keys(): needs_info[key] = ""
def _get_full_title(self): """ Determines the title for the need in order of precedence: directive argument, first sentence of requirement (if `:title_from_content:` was set, and '' if no title is to be derived.""" if len(self.arguments) > 0: # a title was passed if "title_from_content" in self.options: self.log.warning( 'Needs: need "{}" has :title_from_content: set, ' "but a title was provided. (see file {})".format( self.arguments[0], self.docname)) return self.arguments[0] elif self.title_from_content: first_sentence = re.split(r"[.\n]", "\n".join(self.content))[0] if not first_sentence: raise NeedsInvalidException(":title_from_content: set, but " "no content provided. " "(Line {} of file {}".format( self.lineno, self.docname)) return first_sentence else: return ""
def run(self): env = self.state.document.settings.env if not hasattr(env, "need_all_needtables"): env.need_all_needtables = {} # be sure, global var is available. If not, create it if not hasattr(env, "needs_all_needs"): env.needs_all_needs = {} targetid = "needtable-{docname}-{id}".format( docname=env.docname, id=env.new_serialno("needtable")) targetnode = nodes.target("", "", ids=[targetid]) columns = str(self.options.get("columns", "")) if len(columns) == 0: columns = env.app.config.needs_table_columns if isinstance(columns, str): columns = [col.strip() for col in re.split(";|,", columns)] columns = [get_title(col) for col in columns] colwidths = str(self.options.get("colwidths", "")) colwidths_list = [] if colwidths: colwidths_list = [ int(width.strip()) for width in re.split(";|,", colwidths) ] if len(columns) != len(colwidths_list): raise NeedsInvalidException( f"Amount of elements in colwidths and columns do not match: " f"colwidths: {len(colwidths_list)} and columns: {len(columns)}" ) classes = get_option_list(self.options, "class") style = self.options.get("style", "").upper() style_row = self.options.get("style_row", "") style_col = self.options.get("style_col", "") sort = self.options.get("sort", "id_complete") title = None if self.arguments: title = self.arguments[0] # Add the need and all needed information env.need_all_needtables[targetid] = { "docname": env.docname, "lineno": self.lineno, "target_node": targetnode, "caption": title, "classes": classes, "columns": columns, "colwidths": colwidths_list, "style": style, "style_row": style_row, "style_col": style_col, "sort": sort, # As the following options are flags, the content is None, if set. # If not set, the options.get() method returns False "show_filters": self.options.get("show_filters", False) is None, "show_parts": self.options.get("show_parts", False) is None, "env": env, } env.need_all_needtables[targetid].update( self.collect_filter_attributes()) return [targetnode] + [Needtable("")]
def add_need(app, state, docname, lineno, need_type, title, id=None, content="", status=None, tags=None, links_string=None, hide=False, hide_tags=False, hide_status=False, collapse=None, style=None, layout=None, template=None, pre_template=None, post_template=None, **kwargs): """ Creates a new need and returns its node. ``add_need`` allows to create needs programmatically and use its returned node to be integrated in any docutils based structure. ``kwags`` can contain options defined in ``needs_extra_options`` and ``needs_extra_links``. If an entry is found in ``kwags``, which *is not* specified in the configuration or registered e.g. via ``add_extra_option``, an exception is raised. **Usage**: Normally needs get created during handling of a specialised directive. So this pseudo-code shows how to use ``add_need`` inside such a directive. .. code-block:: python from docutils.parsers.rst import Directive from sphinxcontrib.needs.api import add_need class MyDirective(Directive) # configs and init routine def run(): main_section = [] docname = self.state.document.settings.env.docname # All needed sphinx-internal information we can take from our current directive class. # e..g app, state, lineno main_section += add_need(self.env.app, self.state, docname, self.lineno, need_type="req", title="my title", id="ID_001" content=self.content) # Feel free to add custom stuff to main_section like sections, text, ... return main_section :param app: Sphinx application object. :param state: Current state object. :param docname: documentation name. :param lineno: line number. :param need_type: Name of the need type to create. :param title: String as title. :param id: ID as string. If not given, a id will get generated. :param content: Content as single string. :param status: Status as string. :param tags: Tags as single string. :param links_string: Links as single string. :param hide: boolean value. :param hide_tags: boolean value. (Not used with Sphinx-Needs >0.5.0) :param hide_status: boolean value. (Not used with Sphinx-Needs >0.5.0) :param collapse: boolean value. :param style: String value of class attribute of node. :param layout: String value of layout definition to use :param template: Template name to use for the content of this need :param pre_template: Template name to use for content added before need :param post_template: Template name to use for the content added after need :return: node """ ############################################################################################# # Get environment ############################################################################################# env = app.env types = env.app.config.needs_types type_name = "" type_prefix = "" type_color = "" type_style = "" found = False for ntype in types: if ntype["directive"] == need_type: type_name = ntype["title"] type_prefix = ntype["prefix"] type_color = ntype["color"] type_style = ntype["style"] found = True break if not found: # This should never happen. But it may happen, if Sphinx is called multiples times # inside one ongoing python process. # In this case the configuration from a prior sphinx run may be active, which has registered a directive, # which is reused inside a current document, but no type was defined for the current run... # Yeah, this really has happened... return [nodes.Text('', '')] # Get the id or generate a random string/hash string, which is hopefully unique # TODO: Check, if id was already given. If True, recalculate id # id = self.options.get("id", ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for # _ in range(5))) if id is None and env.app.config.needs_id_required: raise NeedsNoIdException( "An id is missing for this need and must be set, because 'needs_id_required' " "is set to True in conf.py. Need '{}' in {} ({})".format( title, docname, lineno)) if id is None: need_id = make_hashed_id(app, need_type, title, content) else: need_id = id if env.app.config.needs_id_regex and not re.match( env.app.config.needs_id_regex, need_id): raise NeedsInvalidException( "Given ID '{id}' does not match configured regex '{regex}'".format( id=need_id, regex=env.app.config.needs_id_regex)) #print ("create need: ", need_id, id) # Calculate target id, to be able to set a link back target_node = nodes.target('', '', ids=[need_id]) #print (target_node) # Removed 0.5.0 # if collapse is None: # collapse = getattr(env.app.config, "needs_collapse_details", True) # Handle status # Check if status is in needs_statuses. If not raise an error. if env.app.config.needs_statuses: if status not in [ stat["name"] for stat in env.app.config.needs_statuses ]: raise NeedsStatusNotAllowed( "Status {0} of need id {1} is not allowed " "by config value 'needs_statuses'.".format(status, need_id)) if tags is None: tags = [] if len(tags) > 0: tags = [tag.strip() for tag in re.split(";|,", tags)] for i in range(len(tags)): if len(tags[i]) == 0 or tags[i].isspace(): del (tags[i]) logger.warning( 'Scruffy tag definition found in need {}. ' 'Defined tag contains spaces only.'.format(need_id)) # Check if tag is in needs_tags. If not raise an error. if env.app.config.needs_tags: for tag in tags: if tag not in [ tag["name"] for tag in env.app.config.needs_tags ]: raise NeedsTagNotAllowed( "Tag {0} of need id {1} is not allowed " "by config value 'needs_tags'.".format(tag, need_id)) # This may have cut also dynamic function strings, as they can contain , as well. # So let put them together again # ToDo: There may be a smart regex for the splitting. This would avoid this mess of code... tags = _fix_list_dyn_func(tags) ############################################################################################# # Add need to global need list ############################################################################################# # be sure, global var is available. If not, create it if not hasattr(env, 'needs_all_needs'): env.needs_all_needs = {} if need_id in env.needs_all_needs.keys(): if id is not None: raise NeedsDuplicatedId( "A need with ID {} already exists! " "This is not allowed. Document {}[{}] Title: {}.".format( need_id, docname, lineno, title)) else: # this is a generated ID raise NeedsDuplicatedId( "Needs could not generate a unique ID for a need with " "the title '{}' because another need had the same title. " "Either supply IDs for the requirements or ensure the " "titles are different. NOTE: If title is being generated " "from the content, then ensure the first sentence of the " "requirements are different.".format(' '.join(title))) # Trim title if it is too long max_length = getattr(env.app.config, 'needs_max_title_length', 30) if max_length == -1 or len(title) <= max_length: trimmed_title = title elif max_length <= 3: trimmed_title = title[:max_length] else: trimmed_title = title[:max_length - 3] + '...' # Add the need and all needed information needs_info = { 'docname': docname, 'lineno': lineno, 'target_node': target_node, 'content_node': None, # gets set after rst parsing 'type': need_type, 'type_name': type_name, 'type_prefix': type_prefix, 'type_color': type_color, 'type_style': type_style, 'status': status, 'tags': tags, 'id': need_id, 'title': trimmed_title, 'full_title': title, 'content': content, 'collapse': collapse, 'style': style, 'layout': layout, 'template': template, 'pre_template': pre_template, 'post_template': post_template, 'hide': hide, 'parts': {}, 'is_part': False, 'is_need': True } #print (needs_info) needs_extra_options = env.config.needs_extra_options.keys() _merge_extra_options(needs_info, kwargs, needs_extra_options) needs_global_options = env.config.needs_global_options _merge_global_options(needs_info, needs_global_options) link_names = [x['option'] for x in env.config.needs_extra_links] for keyword in kwargs: if keyword not in needs_extra_options and keyword not in link_names: raise NeedsInvalidOption( 'Unknown Option {}. ' 'Use needs_extra_options or needs_extra_links in conf.py' 'to define this option.'.format(keyword)) # Merge links copy_links = [] for link_type in env.config.needs_extra_links: # Check, if specific link-type got some arguments during method call if link_type['option'] not in list(kwargs.keys( )) and link_type['option'] not in needs_global_options.keys(): # if not we set no links, but entry in needS_info must be there links = [] elif link_type['option'] in needs_global_options.keys() and \ (link_type['option'] not in list(kwargs.keys()) or len(str( kwargs[link_type['option']])) == 0): # If it is in global option, value got already set during prior handling of them links_string = needs_info[link_type['option']] links = _read_in_links(links_string) else: # if it is set in kwargs, take this value and maybe override set value from global_options links_string = kwargs[link_type['option']] links = _read_in_links(links_string) needs_info[link_type["option"]] = links needs_info['{}_back'.format(link_type["option"])] = set() if 'copy' not in link_type.keys(): link_type['copy'] = False if link_type['copy'] and link_type['option'] != 'links': copy_links += links # Save extra links for main-links needs_info['links'] += copy_links # Set copied links to main-links env.needs_all_needs[need_id] = needs_info # Template builds ############################## # template if needs_info['template'] is not None and len(needs_info['template']) > 0: new_content = _prepare_template(app, needs_info, 'template') # Overwrite current content content = new_content needs_info['content'] = new_content else: new_content = None # pre_template if needs_info['pre_template'] is not None and len( needs_info['pre_template']) > 0: pre_content = _prepare_template(app, needs_info, 'pre_template') needs_info['pre_content'] = pre_content else: pre_content = None # post_template if needs_info['post_template'] is not None and len( needs_info['post_template']) > 0: post_content = _prepare_template(app, needs_info, 'post_template') needs_info['post_content'] = post_content else: post_content = None if needs_info['hide']: return [target_node] # Adding of basic Need node. ############################ # Title and meta data information gets added alter during event handling via process_need_nodes() # We just add a basic need node and render the rst-based content, because this can not be done later. # style_classes = ['need', type_name, 'need-{}'.format(type_name.lower())] # Used < 0.4.4 style_classes = ['need', 'need-{}'.format(need_type.lower())] if style is not None and style != '': style_classes.append(style) node_need = sphinxcontrib.needs.directives.need.Need('', classes=style_classes, ids=[need_id]) # Render rst-based content and add it to the need-node node_need_content = _render_template(content, docname, lineno, state) need_parts = find_parts(node_need_content) update_need_with_parts(env, needs_info, need_parts) node_need += node_need_content.children needs_info['content_node'] = node_need #print("target_node: ", target_node) #print("node_need: ", node_need) #needs_info['content_node']['id'] = need_id return_nodes = [target_node] + [node_need] if pre_content is not None: node_need_pre_content = _render_template(pre_content, docname, lineno, state) pre_container = nodes.container() pre_container += node_need_pre_content.children return_nodes = [pre_container] + return_nodes if post_content is not None: node_need_post_content = _render_template(post_content, docname, lineno, state) post_container = nodes.container() post_container += node_need_post_content.children return_nodes = return_nodes + [post_container] return return_nodes
def add_need( app, state, docname, lineno, need_type, title, id=None, content="", status=None, tags=None, links_string=None, hide=False, hide_tags=False, hide_status=False, collapse=None, style=None, layout=None, template=None, pre_template=None, post_template=None, is_external=False, external_url=None, external_css="external_link", **kwargs, ): """ Creates a new need and returns its node. ``add_need`` allows to create needs programmatically and use its returned node to be integrated in any docutils based structure. ``kwags`` can contain options defined in ``needs_extra_options`` and ``needs_extra_links``. If an entry is found in ``kwags``, which *is not* specified in the configuration or registered e.g. via ``add_extra_option``, an exception is raised. If ``is_external`` is set to ``True``, no node will be created. Instead the need is referencing an external url. Used mostly for :ref:`needs_external_needs` to integrate and reference needs from external documentation. **Usage**: Normally needs get created during handling of a specialised directive. So this pseudo-code shows how to use ``add_need`` inside such a directive. .. code-block:: python from docutils.parsers.rst import Directive from sphinxcontrib.needs.api import add_need class MyDirective(Directive) # configs and init routine def run(): main_section = [] docname = self.state.document.settings.env.docname # All needed sphinx-internal information we can take from our current directive class. # e..g app, state, lineno main_section += add_need(self.env.app, self.state, docname, self.lineno, need_type="req", title="my title", id="ID_001" content=self.content) # Feel free to add custom stuff to main_section like sections, text, ... return main_section :param app: Sphinx application object. :param state: Current state object. :param docname: documentation name. :param lineno: line number. :param need_type: Name of the need type to create. :param title: String as title. :param id: ID as string. If not given, a id will get generated. :param content: Content as single string. :param status: Status as string. :param tags: Tags as single string. :param links_string: Links as single string. :param hide: boolean value. :param hide_tags: boolean value. (Not used with Sphinx-Needs >0.5.0) :param hide_status: boolean value. (Not used with Sphinx-Needs >0.5.0) :param collapse: boolean value. :param style: String value of class attribute of node. :param layout: String value of layout definition to use :param template: Template name to use for the content of this need :param pre_template: Template name to use for content added before need :param post_template: Template name to use for the content added after need :param is_external: Is true, no node is created and need is referencing external url :param external_url: URL as string, which is used as target if ``is_external`` is ``True`` :param external_css: CSS class name as string, which is set for the <a> tag. :return: node """ ############################################################################################# # Get environment ############################################################################################# env = app.env types = env.app.config.needs_types type_name = "" type_prefix = "" type_color = "" type_style = "" found = False for ntype in types: if ntype["directive"] == need_type: type_name = ntype["title"] type_prefix = ntype["prefix"] type_color = ntype[ "color"] or "#000000" # if no color set up user in config type_style = ntype[ "style"] or "node" # if no style set up user in config found = True break if not found: # This should never happen. But it may happen, if Sphinx is called multiples times # inside one ongoing python process. # In this case the configuration from a prior sphinx run may be active, which has registered a directive, # which is reused inside a current document, but no type was defined for the current run... # Yeah, this really has happened... return [nodes.Text("", "")] # Get the id or generate a random string/hash string, which is hopefully unique # TODO: Check, if id was already given. If True, recalculate id # id = self.options.get("id", ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for # _ in range(5))) if id is None and env.app.config.needs_id_required: raise NeedsNoIdException( "An id is missing for this need and must be set, because 'needs_id_required' " "is set to True in conf.py. Need '{}' in {} ({})".format( title, docname, lineno)) if id is None: need_id = make_hashed_id(app, need_type, title, content) else: need_id = id if env.app.config.needs_id_regex and not re.match( env.app.config.needs_id_regex, need_id): raise NeedsInvalidException( "Given ID '{id}' does not match configured regex '{regex}'".format( id=need_id, regex=env.app.config.needs_id_regex)) # Calculate target id, to be able to set a link back if is_external: target_node = None external_url = external_url else: target_node = nodes.target("", "", ids=[need_id], refid=need_id) external_url = None # Handle status # Check if status is in needs_statuses. If not raise an error. if env.app.config.needs_statuses and status not in [ stat["name"] for stat in env.app.config.needs_statuses ]: raise NeedsStatusNotAllowed( f"Status {status} of need id {need_id} is not allowed " "by config value 'needs_statuses'.") if tags is None: tags = [] if len(tags) > 0: # tags should be a string, but it can also be already a list,which can be used. if isinstance(tags, str): tags = [tag.strip() for tag in re.split(";|,", tags)] new_tags = [] # Shall contain only valid tags for i in range(len(tags)): if len(tags[i]) == 0 or tags[i].isspace(): logger.warning( f"Scruffy tag definition found in need {need_id}. " "Defined tag contains spaces only.") else: new_tags.append(tags[i]) tags = new_tags # Check if tag is in needs_tags. If not raise an error. if env.app.config.needs_tags: for tag in tags: if tag not in [ tag["name"] for tag in env.app.config.needs_tags ]: raise NeedsTagNotAllowed( f"Tag {tag} of need id {need_id} is not allowed " "by config value 'needs_tags'.") # This may have cut also dynamic function strings, as they can contain , as well. # So let put them together again # ToDo: There may be a smart regex for the splitting. This would avoid this mess of code... tags = _fix_list_dyn_func(tags) ############################################################################################# # Add need to global need list ############################################################################################# # be sure, global var is available. If not, create it if not hasattr(env, "needs_all_needs"): env.needs_all_needs = {} if need_id in env.needs_all_needs: if id: raise NeedsDuplicatedId( f"A need with ID {need_id} already exists! " f"This is not allowed. Document {docname}[{lineno}] Title: {title}." ) else: # this is a generated ID raise NeedsDuplicatedId( "Needs could not generate a unique ID for a need with " "the title '{}' because another need had the same title. " "Either supply IDs for the requirements or ensure the " "titles are different. NOTE: If title is being generated " "from the content, then ensure the first sentence of the " "requirements are different.".format(" ".join(title))) # Trim title if it is too long max_length = env.app.config.needs_max_title_length if max_length == -1 or len(title) <= max_length: trimmed_title = title elif max_length <= 3: trimmed_title = title[:max_length] else: trimmed_title = title[:max_length - 3] + "..." # Add the need and all needed information needs_info = { "docname": docname, "lineno": lineno, "target_node": target_node, "external_url": external_url, "content_node": None, # gets set after rst parsing "type": need_type, "type_name": type_name, "type_prefix": type_prefix, "type_color": type_color, "type_style": type_style, "status": status, "tags": tags, "id": need_id, "title": trimmed_title, "full_title": title, "content": content, "collapse": collapse, "style": style, "layout": layout, "template": template, "pre_template": pre_template, "post_template": post_template, "hide": hide, "parts": {}, "is_part": False, "is_need": True, "parent_need": None, "is_external": is_external or False, "external_css": external_css or "external_link", "is_modified": False, # needed by needextend "modifications": 0, # needed by needextend } # needs_extra_options = env.config.needs_extra_options.keys() needs_extra_option_names = NEEDS_CONFIG.get("extra_options").keys() _merge_extra_options(needs_info, kwargs, needs_extra_option_names) needs_global_options = env.config.needs_global_options _merge_global_options(app, needs_info, needs_global_options) link_names = [x["option"] for x in env.config.needs_extra_links] for keyword in kwargs: if keyword not in needs_extra_option_names and keyword not in link_names: raise NeedsInvalidOption( "Unknown Option {}. " "Use needs_extra_options or needs_extra_links in conf.py" "to define this option.".format(keyword)) # Merge links copy_links = [] for link_type in env.config.needs_extra_links: # Check, if specific link-type got some arguments during method call if link_type["option"] not in kwargs and link_type[ "option"] not in needs_global_options: # if not we set no links, but entry in needS_info must be there links = [] elif link_type["option"] in needs_global_options and ( link_type["option"] not in kwargs or len(str(kwargs[link_type["option"]])) == 0): # If it is in global option, value got already set during prior handling of them links_string = needs_info[link_type["option"]] links = _read_in_links(links_string) else: # if it is set in kwargs, take this value and maybe override set value from global_options links_string = kwargs[link_type["option"]] links = _read_in_links(links_string) needs_info[link_type["option"]] = links needs_info["{}_back".format(link_type["option"])] = [] if "copy" not in link_type: link_type["copy"] = False if link_type["copy"] and link_type["option"] != "links": copy_links += links # Save extra links for main-links needs_info["links"] += copy_links # Set copied links to main-links env.needs_all_needs[need_id] = needs_info # Template builds ############################## # template if needs_info["template"]: new_content = _prepare_template(app, needs_info, "template") # Overwrite current content content = new_content needs_info["content"] = new_content # pre_template if needs_info["pre_template"]: pre_content = _prepare_template(app, needs_info, "pre_template") needs_info["pre_content"] = pre_content else: pre_content = None # post_template if needs_info["post_template"]: post_content = _prepare_template(app, needs_info, "post_template") needs_info["post_content"] = post_content else: post_content = None if needs_info["is_external"]: return [] if needs_info["hide"]: return [target_node] # Adding of basic Need node. ############################ # Title and meta data information gets added alter during event handling via process_need_nodes() # We just add a basic need node and render the rst-based content, because this can not be done later. # style_classes = ['need', type_name, 'need-{}'.format(type_name.lower())] # Used < 0.4.4 style_classes = ["need", f"need-{need_type.lower()}"] if style: style_classes.append(style) node_need = Need("", classes=style_classes, ids=[need_id], refid=need_id) # Render rst-based content and add it to the need-node node_need_content = _render_template(content, docname, lineno, state) need_parts = find_parts(node_need_content) update_need_with_parts(env, needs_info, need_parts) node_need += node_need_content.children needs_info["content_node"] = node_need return_nodes = [target_node] + [node_need] if pre_content: node_need_pre_content = _render_template(pre_content, docname, lineno, state) pre_container = nodes.container() pre_container += node_need_pre_content.children return_nodes = node_need_pre_content.children + return_nodes if post_content: node_need_post_content = _render_template(post_content, docname, lineno, state) post_container = nodes.container() post_container += node_need_post_content.children return_nodes = return_nodes + node_need_post_content.children return return_nodes