Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document wild card filters with named groups, possible feature improvement #1468

Open
oz123 opened this issue Nov 12, 2024 · 1 comment
Open

Comments

@oz123
Copy link
Contributor

oz123 commented Nov 12, 2024

I wanted to use wild card pattern with named groups, this was surprisingly easy, even thought the example in the docs is very terse.

As an example, consider the following:

from pypiserver.bottle import Bottle, run

def project_version_filter(config):
    pattern = r"(?P<project>.*)-(?P<version>.*)-(?P<constraints>.*-.*-.*)\.whl\.metadata$"

    def to_python(match):
        """
        >>> "bottle-0.13.2-py2.py3-none-any.whl.metadata".split("-",2)
        ['bottle', '0.13.2', 'py2.py3-none-any.whl.metadata']
        """
        return match.split("-", 2)
    
    def to_url(value):
        return f"{value['project']}-{value['version']}-{value['constraints']}.whl.metadata"

    return pattern, to_python, to_url

app = Bottle()
app.router.add_filter('project_version', project_version_filter)

@app.route("/packages/<pkg:project_version>")
def serve_metadata(pkg, project, version, constraints):
    return locals() 


if __name__ == '__main__':
    run(app, reload=True, debug=True)

This works suprisingly well. However, there is small annoyance in the API. I must also pass the filter key.

def serve_metadata(pkg, project, version, constraints)

I patched bottle in the following way to allow a slightly more comfortable API which does not require passing the key,

e.g. the filter now injects only the groups to the function:

@app.route("/packages/<pkg:project_version>")
def serve_metadata(project, version, constraints):
    return locals() 

The complete app:

app = Bottle()
app.config["skip_filter_key"] = True
app.router.add_filter('project_version', project_version_filter)

@app.route("/packages/<pkg:project_version>")
def serve_metadata(project, version, constraints):
    return locals() 

The patch:

diff --git a/bottle.py b/bottle.py
index 8348da6..d9aaab9 100644
--- a/pypiserver/bottle.py
+++ b/pypiserver/bottle.py
@@ -359,7 +359,7 @@ class Router(object):
         if offset <= len(rule) or prefix:
             yield prefix + rule[offset:], None, None
 
-    def add(self, rule, method, target, name=None):
+    def add(self, rule, method, target, name=None, skip_filter_key=False):
         """ Add a new rule or replace the target for an existing rule. """
         anons = 0  # Number of anonymous wildcards found
         keys = []  # Names of keys
@@ -377,6 +377,9 @@ class Router(object):
                     pattern += '(?:%s)' % mask
                     key = 'anon%d' % anons
                     anons += 1
+                if skip_filter_key:
+                    pattern += '(%s)' % mask
+                    keys.append(key)
                 else:
                     pattern += '(?P<%s>%s)' % (key, mask)
                     keys.append(key)
@@ -400,8 +403,7 @@ class Router(object):
         except re.error as e:
             raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, e))
 
-        if filters:
-
+        if filters and not skip_filter_key:
             def getargs(path):
                 url_args = re_match(path).groupdict()
                 for name, wildcard_filter in filters:
@@ -506,7 +508,7 @@ class Route(object):
                  skiplist=None, **config):
         #: The application this route is installed to.
         self.app = app
-        #: The path-rule string (e.g. ``/wiki/<page>``).
+        #: The path-rule st/sring (e.g. ``/wiki/<page>``).
         self.rule = rule
         #: The HTTP method as a string (e.g. ``GET``).
         self.method = method
@@ -868,7 +870,11 @@ class Bottle(object):
         """ Add a route object, but do not change the :data:`Route.app`
             attribute."""
         self.routes.append(route)
-        self.router.add(route.rule, route.method, route, name=route.name)
+        self.router.add(route.rule,
+                        route.method,
+                        route,
+                        name=route.name,
+                        skip_filter_key=self.config.get("skip_filter_key"))
         if DEBUG: route.prepare()
 
     def route(self,

Would you be interested in merging this?

@oz123
Copy link
Contributor Author

oz123 commented Nov 12, 2024

One more note:

I only went down this path, because I could only do this in the route's regex:

@app.route("/packages/<pkg_version:re:(.*-.*)-.*-.*-.*\.whl\.metadata>")
def serve_metadata(pkg_version):
    pkg, version, _ = pkg_version.split("-", 2)
    return locals() 

Unfortunately, we can't use named groupes as with:

pattern ="(?P<project>.*)-(?P<version>.*)-(?P<constraints>.*-.*-.*)\.whl\.metadata$"
@app.route(f"/packages/<pkg_version:re:{pattern}")
def serve_metadata(pkg_version):
    pkg, version, _ = pkg_version.split("-", 2)
    return locals() 

as this would raise an exception:

Traceback (most recent call last):
  File "/home/oznt/Software/pypiserver/test.py", line 22, in <module>
    @app.route(f"/packages/<pkg_version:re:{pattern}")
     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/oznt/Software/pypiserver/pypiserver/bottle.py", line 925, in decorator
    self.add_route(route)
  File "/home/oznt/Software/pypiserver/pypiserver/bottle.py", line 873, in add_route
    self.router.add(route.rule,
  File "/home/oznt/Software/pypiserver/pypiserver/bottle.py", line 404, in add
    raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, e))
pypiserver.bottle.RouteSyntaxError: Could not add Route: /packages/<pkg_version:re:(?P<project>.*)-(?P<version>.*)-(?P<constraints>.*-.*-.*)\.whl\.metadata$ (bad character in group name 'project)\\.\\*\\)\\-\\(\\?P(?P<version' at position 32)

My point is that it's worth documenting, even if only as an issue for the benefit of others.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant