Repository: jazzband/django-simple-menu (language: python)

Issue:
Rename the Python module?
IMO, the current name of the Python module (`menu`) is too broad. I can imagine people using it for their own Python modules or Django apps (think of a restaurant website with an app for menus).

I propose renaming the Python module to `simple_menu`, similarly to how many other Django packages do it.

EDIT: of course, we will need to bump the major version, but we do it anyway with the v2.0.0 release


Discussion / hints:
Hi, I would be a proponent of this, indeed the name is too generic otherwise.

I'd propose to _also_ allow using the include in the old way, for the time being, with a deprecation warning.

Fix patch:
diff --git a/.coveragerc b/.coveragerc
index 57be0ea..08a9b69 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,6 +1,6 @@
 [run]
-source = menu
+source = simple_menu
 branch = 1
 
 [report]
-omit = *migrations*,.tox/*,setup.py,*settings.py
+omit = *migrations*,.tox/*,setup.py,*settings.py,menu/*
diff --git a/MANIFEST.in b/MANIFEST.in
index 5c58b24..e2e22f5 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -3,6 +3,8 @@ include LICENSE
 include README.rst
 include CHANGES
 include AUTHORS
+recursive-include simple_menu/templates *
+recursive-include simple_menu/templatetags *
 recursive-include menu/templates *
 recursive-include menu/templatetags *
 recursive-include docs *
diff --git a/README.rst b/README.rst
index 2bd00e7..f052c08 100644
--- a/README.rst
+++ b/README.rst
@@ -47,18 +47,18 @@ Quickstart
 
 1. Install the ``django-simple-menu`` package.
 
-2. Add ``menu`` to your ``INSTALLED_APPS``.
+2. Add ``simple_menu`` to your ``INSTALLED_APPS``.
 
    - please ensure that you have ``django.template.context_processors.request``
      listed under ``TEMPLATES[...]["OPTIONS"]["context_processors"]``.
 
 3. Create ``menus.py`` inside each app you want to create a menu for and define
    said menus using the ``Menu`` and ``MenuItem`` classes you can import from
-   the ``menu`` namespace.
+   the ``simple_menu`` package.
 
-4. In your templates, load the template tags (``{% load menu %}``) and call
-   ``{% generate_menu %}`` inside a block. Your context will be populated with
-   a new object named ``menus``.  You can now iterate over it to render your
+4. In your templates, load the template tags (``{% load simple_menu %}``) and
+   call ``{% generate_menu %}`` inside a block. Your context will be populated
+   with a new object named ``menus``. You can now iterate over it to render your
    menus.
 
 To quickly see ``django-simple-menu`` in action, check out the
diff --git a/docs/conf.py b/docs/conf.py
index fda6573..90cc91e 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -12,7 +12,7 @@
 
 import sys, os
 
-from menu import __version__
+from simple_menu import __version__
 
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
diff --git a/docs/installation.rst b/docs/installation.rst
index 2aaca39..a5813f3 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -5,7 +5,7 @@ Installing django-simple-menu
 
     pip install django-simple-menu
 
-#. Add ``menu`` to your ``INSTALLED_APPS`` list in your settings
+#. Add ``simple_menu`` to your ``INSTALLED_APPS`` list in your settings
 
 #. ``django-simple-menu`` requires that the ``request`` object be available in
    the context when you call the ``{% generate_menu %}`` template tag. This
diff --git a/docs/usage.rst b/docs/usage.rst
index 1bc0dc7..72566ec 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -9,17 +9,11 @@ access from within your templates. This way you can have a menu for your main
 navigation, another menu for logged in users, another menu for anonymous users,
 etc.
 
-.. note::
-
-    Since we use the ``menu`` namespace you will get ``ImportError`` if you
-    have any files named ``menu.py``, ensure you use a plural version:
-    ``menus.py``
-
 To define your menus you need to create a file named ``menus.py`` inside of the
 app that you wish to hook menus up to. In the ``menus.py`` file you should
-import the ``Menu`` and ``MenuItem`` classes from the ``menu`` package::
+import the ``Menu`` and ``MenuItem`` classes from the ``simple_menu`` package::
 
-    from menu import Menu, MenuItem
+    from simple_menu import Menu, MenuItem
 
 The ``Menu`` class exposes a class method named ``add_item`` that accepts two
 arguments; the menu name you want to add to, and the ``MenuItem`` you're going
@@ -44,7 +38,8 @@ available in your templates. For example adding ``separator=True`` could be
 used to add separators to menus ``{% if item.separator %}<li
 class="divider"></li>{% endif %}``
 
-For the full list of ``MenuItem`` options see the `menu __init__.py source file`_.
+For the full list of ``MenuItem`` options see the
+`simple_menu __init__.py source file`_.
 
 Usage Example
 -------------
@@ -92,7 +87,7 @@ Once you have your menus defined you need to incorporate them into your
 templates. This is done through the ``generate_menu`` template tag::
 
     {% extends "base.html" %}
-    {% load menu %}
+    {% load simple_menu %}
 
     {% block content %}
     {% generate_menu %}
@@ -117,7 +112,7 @@ the following items will be set in the context for you.
 
 
 See the bootstrap-navbar.html file in the templates dir of the source code for
-an example that renders menus for the `Twitter Bootstrap Navbar Component`_.
+an example that renders menus for the `Bootstrap Navbar Component`_.
 You can use it like::
 
     {% with menu=menus.main %}{% include "bootstrap-navbar.html" %}{% endwith %}
@@ -137,7 +132,7 @@ This assumes you have a ``utils`` package.
 
     from django.core.urlresolvers import resolve
 
-    from menu import MenuItem
+    from simple_menu import MenuItem
 
 
     class ViewMenuItem(MenuItem):
@@ -158,7 +153,7 @@ This assumes you have a ``utils`` package.
 
      from utils.menus import ViewMenuItem
 
-     from menu import Menu, MenuItem
+     from simple_menu import Menu, MenuItem
 
      from django.core.urlresolvers import reverse
 
@@ -175,5 +170,5 @@ This assumes you have a ``utils`` package.
                                     children=reports_children))
 
 
-.. _menu __init__.py source file: https://github.com/jazzband/django-simple-menu/blob/master/menu/__init__.py
-.. _Twitter Bootstrap Navbar Component: http://twitter.github.com/bootstrap/components.html#navbar
+.. _simple_menu __init__.py source file: https://github.com/jazzband/django-simple-menu/blob/master/simple_menu/__init__.py
+.. _Bootstrap Navbar Component: https://getbootstrap.com/docs/5.1/components/navbar/
diff --git a/menu/__init__.py b/menu/__init__.py
index c419206..26e356e 100644
--- a/menu/__init__.py
+++ b/menu/__init__.py
@@ -1,10 +1,2 @@
-from pkg_resources import get_distribution, DistributionNotFound
-from .menu import Menu, MenuItem
-
-try:
-    __version__ = get_distribution("django-simple-menu").version
-except DistributionNotFound:
-    # package is not installed
-    __version__ = None
-
-__url__ = 'https://github.com/jazzband/django-simple-menu'
+from menu.menu import Menu, MenuItem
+from simple_menu import __version__, __url__
diff --git a/menu/menu.py b/menu/menu.py
index 3968dcd..eabc3c2 100644
--- a/menu/menu.py
+++ b/menu/menu.py
@@ -1,252 +1,10 @@
-import copy
-import re
+import warnings
 
-from django.apps import apps
-from django.conf import settings
-from django.utils.text import slugify
+from simple_menu.menu import *
 
-
-class Menu:
-    """
-    Menu is a class that generates menus
-
-    It allows for multiple named menus, which can be accessed in your templates
-    using the generate_menu template tag.
-
-    Menus are loaded from the INSTALLED_APPS, inside a file named menus.py.
-    This file should import the Menu & MenuItem classes and then call add_item:
-
-        Menu.add_item("main", MenuItem("My item",
-                                       reverse("myapp.views.myview"),
-                                       weight=10))
-
-    Note: You cannot have the same URL in a MenuItem in different
-    Menus, but it is not enforced. If you do submenus will not work
-    properly.
-    """
-    items = {}
-    sorted = {}
-    loaded = False
-
-    @classmethod
-    def add_item(c, name, item):
-        """
-        add_item adds MenuItems to the menu identified by 'name'
-        """
-        if isinstance(item, MenuItem):
-            if name not in c.items:
-                c.items[name] = []
-            c.items[name].append(item)
-            c.sorted[name] = False
-
-    @classmethod
-    def load_menus(c):
-        """
-        load_menus loops through INSTALLED_APPS and loads the menu.py
-        files from them.
-        """
-
-        # we don't need to do this more than once
-        if c.loaded:
-            return
-
-        # Fetch all installed app names
-        app_names = [
-            app_config.name
-            for app_config in apps.get_app_configs()
-        ]
-
-        # loop through our INSTALLED_APPS
-        for app in app_names:
-            # skip any django apps
-            if app.startswith("django."):
-                continue
-
-            menu_module = '%s.menus' % app
-            try:
-                __import__(menu_module, fromlist=["menu", ])
-            except ImportError:
-                pass
-
-        c.loaded = True
-
-    @classmethod
-    def sort_menus(c):
-        """
-        sort_menus goes through the items and sorts them based on
-        their weight
-        """
-        for name in c.items:
-            if not c.sorted[name]:
-                c.items[name].sort(key=lambda x: x.weight)
-                c.sorted[name] = True
-
-    @classmethod
-    def process(c, request, name=None):
-        """
-        process uses the current request to determine which menus
-        should be visible, which are selected, etc.
-        """
-        # make sure we're loaded & sorted
-        c.load_menus()
-        c.sort_menus()
-
-        if name is None:
-            # special case, process all menus
-            items = {}
-            for name in c.items:
-                items[name] = c.process(request, name)
-            return items
-
-        if name not in c.items:
-            return []
-
-        items = copy.deepcopy(c.items[name])
-        curitem = None
-        for item in items:
-            item.process(request)
-            if item.visible:
-                item.selected = False
-                if item.match_url(request):
-                    if curitem is None or len(curitem.url) < len(item.url):
-                        curitem = item
-
-        if curitem is not None:
-            curitem.selected = True
-
-        # return only visible items
-        visible = [
-            item
-            for item in items
-            if item.visible
-        ]
-
-        # determine if we should apply 'selected' to parents when one of their
-        # children is the 'selected' menu
-        if getattr(settings, 'MENU_SELECT_PARENTS', False):
-            def is_child_selected(item):
-                for child in item.children:
-                    if child.selected or is_child_selected(child):
-                        return True
-
-            for item in visible:
-                if is_child_selected(item):
-                    item.selected = True
-
-        return visible
-
-
-class MenuItem:
-    """
-    MenuItem represents an item in a menu, possibly one that has a sub-menu (children).
-    """
-
-    def __init__(self, title, url, children=[], weight=1, check=None,
-                 visible=True, slug=None, exact_url=False, **kwargs):
-        """
-        MenuItem constructor
-
-        title       either a string or a callable to be used for the title
-        url         the url of the item
-        children    an array of MenuItems that are sub menus to this item
-                    this can also be a callable that generates an array
-        weight      used to sort adjacent MenuItems
-        check       a callable to determine if this item is visible
-        slug        used to generate id's in the HTML, auto generated from
-                    the title if left as None
-        exact_url   normally we check if the url matches the request prefix
-                    this requires an exact match if set
-
-        All other keyword arguments passed into the MenuItem constructor are
-        assigned to the MenuItem object as attributes so that they may be used
-        in your templates. This allows you to attach arbitrary data and use it
-        in which ever way suits your menus the best.
-        """
-
-        self.url = url
-        self.title = title
-        self.visible = visible
-        self.children = children
-        self.weight = weight
-        self.check_func = check
-        self.slug = slug
-        self.exact_url = exact_url
-        self.selected = False
-        self.parent = None
-
-        # merge our kwargs into our self
-        for k in kwargs:
-            setattr(self, k, kwargs[k])
-
-    def check(self, request):
-        """
-        Evaluate if we should be visible for this request
-        """
-        if callable(self.check_func):
-            self.visible = self.check_func(request)
-
-    def process(self, request):
-        """
-        process determines if this item should visible, if its selected, etc...
-        """
-        # if we're not visible we return since we don't need to do anymore processing
-        self.check(request)
-        if not self.visible:
-            return
-
-        # evaluate our title
-        if callable(self.title):
-            self.title = self.title(request)
-
-        # if no title is set turn it into a slug
-        if self.slug is None:
-            # in python3 we don't need to convert to unicode, in python2 slugify
-            # requires a unicode string
-            self.slug = slugify(self.title)
-
-        # evaluate children
-        if callable(self.children):
-            children = list(self.children(request))
-        else:
-            children = list(self.children)
-
-        for child in children:
-            child.parent = self
-            child.process(request)
-
-        self.children = [
-            child
-            for child in children
-            if child.visible
-        ]
-        self.children.sort(key=lambda child: child.weight)
-
-        # if we have no children and MENU_HIDE_EMPTY then we are not visible and should return
-        hide_empty = getattr(settings, 'MENU_HIDE_EMPTY', False)
-        if hide_empty and len(self.children) == 0:
-            self.visible = False
-            return
-
-        # find out if one of our children is selected, and mark it as such
-        curitem = None
-        for item in self.children:
-            item.selected = False
-
-            if item.match_url(request):
-                if curitem is None or len(curitem.url) < len(item.url):
-                    curitem = item
-
-        if curitem is not None:
-            curitem.selected = True
-
-    def match_url(self, request):
-        """
-        match url determines if this is selected
-        """
-        matched = False
-        if self.exact_url:
-            if re.match(f"{self.url}$", request.path):
-                matched = True
-        elif re.match("%s" % self.url, request.path):
-            matched = True
-        return matched
+warnings.warn(
+	"Importing from 'menu' will be removed in django-simple-menu v2.3 and/or "
+	"v3.0. Use imports from 'simple_menu' package instead.",
+	DeprecationWarning,
+	stacklevel=2
+)
diff --git a/menu/templates/bootstrap-navbar-submenu.html b/menu/templates/bootstrap-navbar-submenu.html
deleted file mode 100644
index cd33482..0000000
--- a/menu/templates/bootstrap-navbar-submenu.html
+++ /dev/null
@@ -1,14 +0,0 @@
-<ul class="dropdown-menu">
-    {% for item in items %}
-        {% if item.visible %}
-            <li class="{% if item.selected %} active{% endif %}{% if item.children %} dropdown-submenu{% endif %}">
-                <a href="{{ item.url }}">{{ item.title }}</a>
-                {% if item.children %}
-                    {% with items=item.children template_name='bootstrap-navbar-submenu.html' %}
-                       {% include template_name %}
-                    {% endwith %}
-                {% endif %}
-            </li>
-        {% endif %}
-    {% endfor %}
-</ul>
diff --git a/menu/templates/bootstrap-navbar-submenu.html b/menu/templates/bootstrap-navbar-submenu.html
new file mode 120000
index 0000000..8e6561a
--- /dev/null
+++ b/menu/templates/bootstrap-navbar-submenu.html
@@ -0,0 +1,1 @@
+../../simple_menu/templates/bootstrap-navbar-submenu.html
\ No newline at end of file
diff --git a/menu/templates/bootstrap-navbar.html b/menu/templates/bootstrap-navbar.html
deleted file mode 100644
index e40af91..0000000
--- a/menu/templates/bootstrap-navbar.html
+++ /dev/null
@@ -1,22 +0,0 @@
-{% for item in menu %}
-    {% if item.visible %}
-        {% if item.separator %}
-            <li class="divider-vertical"></li>
-        {% endif %}
-
-        <li class="{% if item.selected %} active{% endif %}{% if item.children %} dropdown{% endif %}" id="top-menu-{{ item.slug }}">
-            <a href="{{ item.url }}"{% if item.children %} class="dropdown-toggle" data-hover="dropdown" data-toggle="dropdown"{% endif %}>
-                {% if item.icon %}
-                    <i class="icon-{{ item.icon }} icon-white"></i>&nbsp;
-                {% endif %}
-                <span>{{ item.title }}</span>
-                {% if item.children %}
-                    &nbsp;<b class="caret"></b>
-                {% endif %}
-            </a>
-            {% if item.children %}
-                {% include 'bootstrap-navbar-submenu.html' with items=item.children %}
-            {% endif %}
-        </li>
-    {% endif %}
-{% endfor %}
diff --git a/menu/templates/bootstrap-navbar.html b/menu/templates/bootstrap-navbar.html
new file mode 120000
index 0000000..d9a97f2
--- /dev/null
+++ b/menu/templates/bootstrap-navbar.html
@@ -0,0 +1,1 @@
+../../simple_menu/templates/bootstrap-navbar.html
\ No newline at end of file
diff --git a/menu/templatetags/menu.py b/menu/templatetags/menu.py
index 598c5ae..9642389 100644
--- a/menu/templatetags/menu.py
+++ b/menu/templatetags/menu.py
@@ -1,66 +1,1 @@
-from django import template
-from .. import Menu
-
-register = template.Library()
-
-class MenuNode(template.Node):
-    def __init__(self):
-        pass
-
-    def render(self, context):
-        # if a 500 error happens we get an empty context, in which case
-        # we should just return to let the 500.html template render
-        if 'request' not in context:
-            return '<!-- menu failed to render due to missing request in the context -->'
-
-        menus = Menu.process(context['request'])
-
-        # recursively find the selected item
-        def find_selected(menu):
-            process = []
-            for item in menu:
-                if item.selected:
-                    return item
-                if len(item.children) > 0:
-                    process.append(item)
-            for item in process:
-                r = find_selected(item.children)
-                if r is not None:
-                    return r
-            return None
-
-        selected_menu = None
-        for name in menus:
-            found_menu = find_selected(menus[name])
-            if found_menu:
-                if selected_menu is None or len(selected_menu.url) < len(found_menu.url):
-                    # since our call to Menu.process above allows for menus
-                    # in multiple menus we reset the selected attr to False
-                    # if we find a more specific match
-                    if selected_menu is not None:
-                        selected_menu.selected = False
-
-                    selected_menu = found_menu
-
-        # now for the submenu
-        submenu = []
-        has_submenu = False
-        if selected_menu is not None:
-            if selected_menu.parent is not None:
-                submenu = selected_menu.parent.children
-                has_submenu = True
-            elif len(selected_menu.children) > 0:
-                submenu = selected_menu.children
-                has_submenu = True
-
-        # set the items in our context
-        context['menus'] = menus
-        context['selected_menu'] = selected_menu
-        context['submenu'] = submenu
-        context['has_submenu'] = has_submenu
-
-        return ''
-
-def generate_menu(parser, token):
-    return MenuNode()
-register.tag('generate_menu', generate_menu)
+from simple_menu.templatetags.menu import *
diff --git a/setup.cfg b/setup.cfg
index efbea96..c6afd99 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -24,7 +24,9 @@ classifiers =
     Programming Language :: Python :: 3.9
 
 [options]
-packages = menu
+packages =
+    simple_menu
+    menu
 include_package_data = True
 python_requires = >=3.6,<4.0
 install_requires =
diff --git a/simple_menu/__init__.py b/simple_menu/__init__.py
new file mode 100644
index 0000000..c419206
--- /dev/null
+++ b/simple_menu/__init__.py
@@ -0,0 +1,10 @@
+from pkg_resources import get_distribution, DistributionNotFound
+from .menu import Menu, MenuItem
+
+try:
+    __version__ = get_distribution("django-simple-menu").version
+except DistributionNotFound:
+    # package is not installed
+    __version__ = None
+
+__url__ = 'https://github.com/jazzband/django-simple-menu'
diff --git a/simple_menu/menu.py b/simple_menu/menu.py
new file mode 100644
index 0000000..3968dcd
--- /dev/null
+++ b/simple_menu/menu.py
@@ -0,0 +1,252 @@
+import copy
+import re
+
+from django.apps import apps
+from django.conf import settings
+from django.utils.text import slugify
+
+
+class Menu:
+    """
+    Menu is a class that generates menus
+
+    It allows for multiple named menus, which can be accessed in your templates
+    using the generate_menu template tag.
+
+    Menus are loaded from the INSTALLED_APPS, inside a file named menus.py.
+    This file should import the Menu & MenuItem classes and then call add_item:
+
+        Menu.add_item("main", MenuItem("My item",
+                                       reverse("myapp.views.myview"),
+                                       weight=10))
+
+    Note: You cannot have the same URL in a MenuItem in different
+    Menus, but it is not enforced. If you do submenus will not work
+    properly.
+    """
+    items = {}
+    sorted = {}
+    loaded = False
+
+    @classmethod
+    def add_item(c, name, item):
+        """
+        add_item adds MenuItems to the menu identified by 'name'
+        """
+        if isinstance(item, MenuItem):
+            if name not in c.items:
+                c.items[name] = []
+            c.items[name].append(item)
+            c.sorted[name] = False
+
+    @classmethod
+    def load_menus(c):
+        """
+        load_menus loops through INSTALLED_APPS and loads the menu.py
+        files from them.
+        """
+
+        # we don't need to do this more than once
+        if c.loaded:
+            return
+
+        # Fetch all installed app names
+        app_names = [
+            app_config.name
+            for app_config in apps.get_app_configs()
+        ]
+
+        # loop through our INSTALLED_APPS
+        for app in app_names:
+            # skip any django apps
+            if app.startswith("django."):
+                continue
+
+            menu_module = '%s.menus' % app
+            try:
+                __import__(menu_module, fromlist=["menu", ])
+            except ImportError:
+                pass
+
+        c.loaded = True
+
+    @classmethod
+    def sort_menus(c):
+        """
+        sort_menus goes through the items and sorts them based on
+        their weight
+        """
+        for name in c.items:
+            if not c.sorted[name]:
+                c.items[name].sort(key=lambda x: x.weight)
+                c.sorted[name] = True
+
+    @classmethod
+    def process(c, request, name=None):
+        """
+        process uses the current request to determine which menus
+        should be visible, which are selected, etc.
+        """
+        # make sure we're loaded & sorted
+        c.load_menus()
+        c.sort_menus()
+
+        if name is None:
+            # special case, process all menus
+            items = {}
+            for name in c.items:
+                items[name] = c.process(request, name)
+            return items
+
+        if name not in c.items:
+            return []
+
+        items = copy.deepcopy(c.items[name])
+        curitem = None
+        for item in items:
+            item.process(request)
+            if item.visible:
+                item.selected = False
+                if item.match_url(request):
+                    if curitem is None or len(curitem.url) < len(item.url):
+                        curitem = item
+
+        if curitem is not None:
+            curitem.selected = True
+
+        # return only visible items
+        visible = [
+            item
+            for item in items
+            if item.visible
+        ]
+
+        # determine if we should apply 'selected' to parents when one of their
+        # children is the 'selected' menu
+        if getattr(settings, 'MENU_SELECT_PARENTS', False):
+            def is_child_selected(item):
+                for child in item.children:
+                    if child.selected or is_child_selected(child):
+                        return True
+
+            for item in visible:
+                if is_child_selected(item):
+                    item.selected = True
+
+        return visible
+
+
+class MenuItem:
+    """
+    MenuItem represents an item in a menu, possibly one that has a sub-menu (children).
+    """
+
+    def __init__(self, title, url, children=[], weight=1, check=None,
+                 visible=True, slug=None, exact_url=False, **kwargs):
+        """
+        MenuItem constructor
+
+        title       either a string or a callable to be used for the title
+        url         the url of the item
+        children    an array of MenuItems that are sub menus to this item
+                    this can also be a callable that generates an array
+        weight      used to sort adjacent MenuItems
+        check       a callable to determine if this item is visible
+        slug        used to generate id's in the HTML, auto generated from
+                    the title if left as None
+        exact_url   normally we check if the url matches the request prefix
+                    this requires an exact match if set
+
+        All other keyword arguments passed into the MenuItem constructor are
+        assigned to the MenuItem object as attributes so that they may be used
+        in your templates. This allows you to attach arbitrary data and use it
+        in which ever way suits your menus the best.
+        """
+
+        self.url = url
+        self.title = title
+        self.visible = visible
+        self.children = children
+        self.weight = weight
+        self.check_func = check
+        self.slug = slug
+        self.exact_url = exact_url
+        self.selected = False
+        self.parent = None
+
+        # merge our kwargs into our self
+        for k in kwargs:
+            setattr(self, k, kwargs[k])
+
+    def check(self, request):
+        """
+        Evaluate if we should be visible for this request
+        """
+        if callable(self.check_func):
+            self.visible = self.check_func(request)
+
+    def process(self, request):
+        """
+        process determines if this item should visible, if its selected, etc...
+        """
+        # if we're not visible we return since we don't need to do anymore processing
+        self.check(request)
+        if not self.visible:
+            return
+
+        # evaluate our title
+        if callable(self.title):
+            self.title = self.title(request)
+
+        # if no title is set turn it into a slug
+        if self.slug is None:
+            # in python3 we don't need to convert to unicode, in python2 slugify
+            # requires a unicode string
+            self.slug = slugify(self.title)
+
+        # evaluate children
+        if callable(self.children):
+            children = list(self.children(request))
+        else:
+            children = list(self.children)
+
+        for child in children:
+            child.parent = self
+            child.process(request)
+
+        self.children = [
+            child
+            for child in children
+            if child.visible
+        ]
+        self.children.sort(key=lambda child: child.weight)
+
+        # if we have no children and MENU_HIDE_EMPTY then we are not visible and should return
+        hide_empty = getattr(settings, 'MENU_HIDE_EMPTY', False)
+        if hide_empty and len(self.children) == 0:
+            self.visible = False
+            return
+
+        # find out if one of our children is selected, and mark it as such
+        curitem = None
+        for item in self.children:
+            item.selected = False
+
+            if item.match_url(request):
+                if curitem is None or len(curitem.url) < len(item.url):
+                    curitem = item
+
+        if curitem is not None:
+            curitem.selected = True
+
+    def match_url(self, request):
+        """
+        match url determines if this is selected
+        """
+        matched = False
+        if self.exact_url:
+            if re.match(f"{self.url}$", request.path):
+                matched = True
+        elif re.match("%s" % self.url, request.path):
+            matched = True
+        return matched
diff --git a/simple_menu/templates/bootstrap-navbar-submenu.html b/simple_menu/templates/bootstrap-navbar-submenu.html
new file mode 100644
index 0000000..cd33482
--- /dev/null
+++ b/simple_menu/templates/bootstrap-navbar-submenu.html
@@ -0,0 +1,14 @@
+<ul class="dropdown-menu">
+    {% for item in items %}
+        {% if item.visible %}
+            <li class="{% if item.selected %} active{% endif %}{% if item.children %} dropdown-submenu{% endif %}">
+                <a href="{{ item.url }}">{{ item.title }}</a>
+                {% if item.children %}
+                    {% with items=item.children template_name='bootstrap-navbar-submenu.html' %}
+                       {% include template_name %}
+                    {% endwith %}
+                {% endif %}
+            </li>
+        {% endif %}
+    {% endfor %}
+</ul>
diff --git a/simple_menu/templates/bootstrap-navbar.html b/simple_menu/templates/bootstrap-navbar.html
new file mode 100644
index 0000000..e40af91
--- /dev/null
+++ b/simple_menu/templates/bootstrap-navbar.html
@@ -0,0 +1,22 @@
+{% for item in menu %}
+    {% if item.visible %}
+        {% if item.separator %}
+            <li class="divider-vertical"></li>
+        {% endif %}
+
+        <li class="{% if item.selected %} active{% endif %}{% if item.children %} dropdown{% endif %}" id="top-menu-{{ item.slug }}">
+            <a href="{{ item.url }}"{% if item.children %} class="dropdown-toggle" data-hover="dropdown" data-toggle="dropdown"{% endif %}>
+                {% if item.icon %}
+                    <i class="icon-{{ item.icon }} icon-white"></i>&nbsp;
+                {% endif %}
+                <span>{{ item.title }}</span>
+                {% if item.children %}
+                    &nbsp;<b class="caret"></b>
+                {% endif %}
+            </a>
+            {% if item.children %}
+                {% include 'bootstrap-navbar-submenu.html' with items=item.children %}
+            {% endif %}
+        </li>
+    {% endif %}
+{% endfor %}
diff --git a/menu/templatetags/__init__.py b/simple_menu/templatetags/__init__.py
similarity index 100%
rename from menu/templatetags/__init__.py
rename to simple_menu/templatetags/__init__.py
diff --git a/simple_menu/templatetags/menu.py b/simple_menu/templatetags/menu.py
new file mode 100644
index 0000000..cfccb1e
--- /dev/null
+++ b/simple_menu/templatetags/menu.py
@@ -0,0 +1,10 @@
+import warnings
+
+from simple_menu.templatetags.simple_menu import *
+
+warnings.warn(
+	"The 'menu' template library will be removed in django-simple-menu v2.3 "
+	"and/or v3.0. Use '{% load simple_menu %}' instead.",
+	DeprecationWarning,
+	stacklevel=2
+)
diff --git a/simple_menu/templatetags/simple_menu.py b/simple_menu/templatetags/simple_menu.py
new file mode 100644
index 0000000..598c5ae
--- /dev/null
+++ b/simple_menu/templatetags/simple_menu.py
@@ -0,0 +1,66 @@
+from django import template
+from .. import Menu
+
+register = template.Library()
+
+class MenuNode(template.Node):
+    def __init__(self):
+        pass
+
+    def render(self, context):
+        # if a 500 error happens we get an empty context, in which case
+        # we should just return to let the 500.html template render
+        if 'request' not in context:
+            return '<!-- menu failed to render due to missing request in the context -->'
+
+        menus = Menu.process(context['request'])
+
+        # recursively find the selected item
+        def find_selected(menu):
+            process = []
+            for item in menu:
+                if item.selected:
+                    return item
+                if len(item.children) > 0:
+                    process.append(item)
+            for item in process:
+                r = find_selected(item.children)
+                if r is not None:
+                    return r
+            return None
+
+        selected_menu = None
+        for name in menus:
+            found_menu = find_selected(menus[name])
+            if found_menu:
+                if selected_menu is None or len(selected_menu.url) < len(found_menu.url):
+                    # since our call to Menu.process above allows for menus
+                    # in multiple menus we reset the selected attr to False
+                    # if we find a more specific match
+                    if selected_menu is not None:
+                        selected_menu.selected = False
+
+                    selected_menu = found_menu
+
+        # now for the submenu
+        submenu = []
+        has_submenu = False
+        if selected_menu is not None:
+            if selected_menu.parent is not None:
+                submenu = selected_menu.parent.children
+                has_submenu = True
+            elif len(selected_menu.children) > 0:
+                submenu = selected_menu.children
+                has_submenu = True
+
+        # set the items in our context
+        context['menus'] = menus
+        context['selected_menu'] = selected_menu
+        context['submenu'] = submenu
+        context['has_submenu'] = has_submenu
+
+        return ''
+
+def generate_menu(parser, token):
+    return MenuNode()
+register.tag('generate_menu', generate_menu)
diff --git a/tox.ini b/tox.ini
index a073957..a0e5be1 100644
--- a/tox.ini
+++ b/tox.ini
@@ -28,8 +28,8 @@ deps =
     dj31: Django>=3.1,<3.2
     djmain: https://github.com/django/django/tarball/main
 setenv =
-    DJANGO_SETTINGS_MODULE=menu.test_settings
+    DJANGO_SETTINGS_MODULE=simple_menu.test_settings
 commands =
-    coverage run {envbindir}/django-admin test -v2 {posargs:menu}
+    coverage run {envbindir}/django-admin test -v2 {posargs:simple_menu}
     coverage report
     coverage xml


Test patch:
diff --git a/menu/test_settings.py b/simple_menu/test_settings.py
similarity index 96%
rename from menu/test_settings.py
rename to simple_menu/test_settings.py
index 0b6f310..5a02cee 100644
--- a/menu/test_settings.py
+++ b/simple_menu/test_settings.py
@@ -9,7 +9,7 @@
     }
 }
 
-INSTALLED_APPS = ["menu"]
+INSTALLED_APPS = ["simple_menu"]
 
 STATIC_URL = "/static/"
 STATIC_ROOT = os.path.join(TEST_DIR, "static")
diff --git a/menu/tests/__init__.py b/simple_menu/tests/__init__.py
similarity index 100%
rename from menu/tests/__init__.py
rename to simple_menu/tests/__init__.py
diff --git a/menu/tests/test_menu.py b/simple_menu/tests/test_menu.py
similarity index 99%
rename from menu/tests/test_menu.py
rename to simple_menu/tests/test_menu.py
index 29413fe..14aa6f6 100644
--- a/menu/tests/test_menu.py
+++ b/simple_menu/tests/test_menu.py
@@ -7,7 +7,7 @@
 from django.test import TestCase
 from django.test.client import RequestFactory
 
-from menu import Menu, MenuItem
+from simple_menu import Menu, MenuItem
 
 # XXX TODO: test MENU_HIDE_EMPTY