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

route_prefix doesn't allow the prefix pattern to match without a trailing slash #406

Closed
mmerickel opened this issue Jan 18, 2012 · 36 comments

Comments

@mmerickel
Copy link
Member

The issue here is an ambiguity in handling route_prefix. In general, things work correctly:

def routes(config):
    config.add_route('add_user', '/add')

config.include(routes, route_prefix='/users')

# resulting pattern: '/users/add'

The ambiguity arises when the included function attempts to add a route using the pattern '/' or '' (an empty string). Pyramid elects to treat the prefix as a container.

def routes(config):
    config.add_route('users', '/')

config.include(routes, route_prefix='/users')

# resulting pattern: '/users/'

Remember that in Pyramid's config.add_route there is no difference between prepending a '/' or leaving no prefix. Thus, the two route definitions below result in the same url being matched and generated.

config.add_route('r1', '/route')
config.add_route('r2', 'route')

There are 4 possible use cases to account for when looking at the issue.

Case 1

def routes(config):
    config.add_route('route', '')

config.include(routes, route_prefix='/prefix')

Case 2

def routes(config):
    config.add_route('route', '/')

config.include(routes, route_prefix='/prefix')

Case 3

def routes(config):
    config.add_route('route', '')

config.include(routes, route_prefix='/prefix/')

Case 4

def routes(config):
    config.add_route('route', '/')

config.include(routes, route_prefix='/prefix/')

Workaround

Pyramid currently treats the route prefix as a container, thus the resulting route will always be appended with a '/'. This may not be ideal, but it at least allows for a workaround where the user can add their own route (at the level of the include):

def routes(config):
    config.add_route('add', '/add')

config.include(routes, route_prefix='/users')
config.add_route('users', '/users')

If there is a clear way to handle each use-case, and document it as such, then it may be possible to change this behavior but to me it's hard to expect a user to "do the right thing" with case 2 due to the way pyramid currently handles add_route patterns with and without a prefixed slash.

@simonyarde
Copy link

Thanks Michael for responding to my question on pylons-discuss here.

My impression from the docs was that Pyramid considers empty routes to be "bad" and wants to correct your mistake - I don't see anything wrong with that assertion (which benefits from decisiveness) provided we can overrule:

# users.__init__.py

def includeme(config):
    config.add_route('users_list', '', allow_empty_pattern=true)
    config.add_route('users_item', '/{id}')

# __init__.py

config.include('my_app.users', route_prefix='/my_users')

# patterns:
# /my_users
# /my_users/1

The semantics of allow_empty_pattern might not be ideal for Pyramid, but the general approach would solve a problem without breaking anyone's code. If someone sets this option and their routes break then it should be clear to them what has gone wrong.

If we can do something like the above then the includeme style of configuration becomes genuinely useful in keeping related route configuration in one place, thus keeping the main __init__.py really clean.

Furthermore, if someone wanted to make a particular 'modules' routes available from the web-root (for whatever reason), this would be expected:

# __init__.py

config.include('my_app.users', route_prefix='/')

# patterns:
# /
# /1

@mmerickel
Copy link
Member Author

Avoiding the real issue, I'll just say that your last example already works.

@simonyarde
Copy link

Quite so!

@simonyarde
Copy link

I'm may well not be understanding the issue as well as Michael, but I might well consider patching something like this:

# pyramid/config/routes.py

class RoutesConfiguratorMixin(object): 
    @action_method 
    def add_route(self, 
              name, 
              # ..
              allow_empty_pattern=False
              ):

        # ..

        if self.route_prefix:

            route_prefix_join = '/'

            if allow_empty_pattern and pattern == '' and self.route_prefix != '/':
                route_prefix_join = ''

            pattern = self.route_prefix.rstrip('/') + route_prefix_join + pattern.lstrip('/')

Checking that the path genuinely is empty when we've flagged the allow_empty_pattern option on should circumvent inadvertent setting the option and then actually providing a pattern, which presumably would be the main concern re breaking paths.

@mcdonc
Copy link
Member

mcdonc commented Jan 18, 2012

Instead of doing anything more complicated, I'd probably suggest that you use http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/narr/urldispatch.html#redirecting-to-slash-appended-routes

@mmerickel
Copy link
Member Author

From your solution we have the following results (assuming the empty string cases have allow_empty_pattern=True:

Case 1 - /prefix
Case 2 - /prefix/ (this doesn't make sense)
Case 3 - /prefix (this doesn't make sense)
Case 4 - /prefix/

I appreciate the effort, but we've thought about this and a flag on add_route breaks the idea of route_prefix entirely because you then have to think about the calls to add_route within the included function. The goal of route_prefix was that you could take routes in any function and mount them at a prefix without those routes being "configured to support mounting".

I think the AppendSlashNotFoundFactory is probably the right way to go here as well.

@mmerickel
Copy link
Member Author

Where in the docs it "sound like empty routes are bad", out of curiosity? They've always been treated as the equivalent of a route with a '/' prefix.

@mcdonc
Copy link
Member

mcdonc commented Jan 21, 2012

I'm closing this, because I think we've reached an impasse. If I'm doing this in error, please reopen.

@mcdonc mcdonc closed this as completed Jan 21, 2012
@simonyarde
Copy link

@mcdonc

I don't think there is an impasse. Sorry for slow reply - it's not an indication that I don't see a way to resolve this. In fact I just wanted to make sure I had properly considered all the information before responding further. If you can re-open I will post a snippet with my suggested design and documentation/rationale.

@mmerickel:

  • Re my previous solution and your Case 2 (''/prefix" + "/" = "/prefix/") I would suggest this is the logical concatenation and a valid route.
  • Re my previous solution and your Case 3 ("/prefix/" + "" = "/prefix") - I agree my solution doesn't make sense (I feel that it should be "/prefix/")

For now here is my revised code and tests - I will follow with the documentation/rationale.

def route_prefix(
              route_prefix='',
              pattern='',
              ):

    """ Proposed re-design for route-prefix re Issue #406 """

    if route_prefix and pattern:
        pattern = route_prefix.rstrip('/') + '/' + pattern.lstrip('/')
    else:
        pattern = route_prefix + pattern

    return pattern


# Test

#        (test #,    prefix,       pattern,  goal,      )
cases = (
         (u'1.1',    u'/p',        u'',      u'/p',     ),
         (u'1.2',    u'/p',        u'1',     u'/p/1',   ),
         (u'1.3',    u'/p',        u'/',     u'/p/',    ),
         (u'1.4',    u'/p',        u'/1',    u'/p/1',   ),
         (u'1.5',    u'/p/',       u'' ,     u'/p/',    ),
         (u'1.6',    u'/p/',       u'1',     u'/p/1',   ),
         (u'1.7',    u'/p/',       u'/',     u'/p/',    ),
         (u'1.8',    u'/p/',       u'/1',    u'/p/1',   ),

         (u'2.1',    u'p',         u'',      u'p',      ),
         (u'2.2',    u'p',         u'1',     u'p/1',    ),
         (u'2.3',    u'p',         u'/',     u'p/',     ),
         (u'2.4',    u'p',         u'/1',    u'p/1',    ),
         (u'2.5',    u'p/',        u'' ,     u'p/',     ),
         (u'2.6',    u'p/',        u'1',     u'p/1',    ),
         (u'2.7',    u'p/',        u'/',     u'p/',     ),
         (u'2.8',    u'p/',        u'/1',    u'p/1',    ),

         (u'3.1',    u'',          u'',      u'',       ),
         (u'3.2',    u'',          u'1',     u'1',      ),
         (u'3.3',    u'',          u'/',     u'/',      ),
         (u'3.4',    u'',          u'/1',    u'/1',     ),

         (u'4.1',    u'/',         u'',      u'/',      ),
         (u'4.2',    u'/',         u'1',     u'/1',     ),
         (u'4.3',    u'/',         u'/',     u'/',      ),
         (u'4.4',    u'/',         u'/1',    u'/1',     ),
        )


if __name__ == '__main__':

    colw = 8

    print
    print "ROUTE PREFIX TEST"
    print
    print "%s %s %s %s %s %s" % ('Test'.ljust(colw),
                                 'Prefix'.ljust(colw),
                                 'Pattern'.ljust(colw),
                                 'Result'.ljust(colw),
                                 'Goal'.ljust(colw),
                                 'Success'.ljust(colw),
                                 )
    for c in cases:

        output = route_prefix(route_prefix=c[1],pattern=c[2])

        success = 'ok' if (output == c[3]) else 'FAIL'

        print "%s %s %s %s %s %s" % (c[0].ljust(colw), 
                                     c[1].ljust(colw), 
                                     c[2].ljust(colw), 
                                     output.ljust(colw),
                                     c[3].ljust(colw),
                                     success.ljust(colw),
                                     )

@simonyarde
Copy link

The below function is how I arrived at my proposed solution (above), I believe this is the easiest way for me to communicate my reasoning.

My criticism of Pyramid's current route-prefix design is that:

  • it relies on implicit route configuration ("/prefix" + "" = "/prefix/");
  • it is at odds with non-prefixed route-patterns, which my be "/" or empty path, because it makes supplying an empty pattern impossible;
  • it breaches TIMTOWTDI design defence because it forces developers who don't want to use slash-appended-routes to use workarounds and excludes the nicer project design features - @mmerickel this is why I was joking that "empty routes are bad" :)

If my suggestion were to be adopted, projects affected would be those relying on the current implicit configuration - that is, using empty routes with route-prefixes and expecting a slash ("/prefix" + "" = "/prefix/". The solution is pretty painless, either:

a.) make empty patterns explicit (either empty or "/")

b.) change the route-prefix to "/prefix/" which explicitly configures any mounted routes with the current "implicit behaviour" ("/prefix/" + "" = "/prefix/").

From this point on, I believe the route-prefix and pattern relationship would be completely consistent and 100% explicitly configurable.

def route_prefix_logic(
              route_prefix='',
              pattern=None,
              ):

    """ Logic/rationale of proposed re-design for route-prefix re Issue 
    #406, excluding simplification through coercion """

    if route_prefix.endswith('/') and pattern.startswith('/'):
        # explicit slash to right and left of join
        # - act to prevent a double-slash at the join
        pattern = route_prefix.rstrip('/') + pattern

    elif route_prefix.endswith('/') or pattern.startswith('/'):
        # explicit slash to right or left of the join (but not both)
        # - the natural join is ok
        pattern = route_prefix + pattern

    else:
        # no slashes specified, but there are only two possible scenarios
        if route_prefix and pattern:
            # act to prevent a join without a slash on either side 
            pattern = route_prefix + '/' + pattern
        else:
            # the route is the route_prefix or the pattern 
            pattern = route_prefix + pattern

    return pattern

@mcdonc mcdonc reopened this Jan 22, 2012
@mcdonc
Copy link
Member

mcdonc commented Jan 22, 2012

A link to the original issue which added route prefixes for reference: #222

@mcdonc
Copy link
Member

mcdonc commented Jan 22, 2012

it is at odds with non-prefixed route-patterns, which my be "/" or empty path, because it makes supplying an empty pattern impossible;

I'll note that it's just as impossible even without any prefix involved to make a sane distinction between an empty route pattern and a single '/'. This is because user agents are required to send '/' as the path even when a user explictly leaves off the trailing slash from a root URL. As a result, users currently don't expect that visiting "http://localhost" will behave any differently from "http://localhost/".

The rationale for this is present in the HTTP 1.0 spec in http://www.w3.org/Protocols/HTTP/1.0/spec.html#Request-Line :

The most common form of Request-URI is that used to identify a resource on an origin server or gateway. In
this case, only the absolute path of the URI is transmitted (see Section 3.2.1, abs_path). For example, a client
wishing to retrieve the resource above directly from the origin server would create a TCP connection to port 80
of the host "www.w3.org" and send the line:

  GET /pub/WWW/TheProject.html HTTP/1.0

followed by the remainder of the Full-Request. Note that the absolute path cannot be empty; if none is present
in the original URI, it must be given as "/" (the server root).

When a broken user agent does not send a "/" as the path and sends an empty string instead (e.g. "GET HTTP/1.0") CGI and WSGI servers tend to convert the empty path to "/" as an implicit fix. Eg. https://github.com/Pylons/waitress/blob/master/waitress/task.py#L449 . Every CGI and WSGI server has similar logic in it.

@simonyarde
Copy link

Thinking a little further on my approach, I think I'm trying to say that:

the remit of route-prefix should be limited to forming a successful join of a prefix and a pattern; that is to say, one in which the right-side of the prefix and the left-side of the pattern make sense.

I'll note that it's just as impossible even without any prefix involved to make a sane distinction between an empty route pattern and a single '/'.

I think I understand - although empty route and '/' are equivalent from a match-perspective (and docs have always made this very clear), I believe 'empty patterns' are essential from a configuration perspective.

By allowing an empty route in configuration you achieve 100% explicitly configurable routes where the right-edge of prefix and left-edge of pattern are a means of supplying configuration intentions, and meet @mmerickel's comment to me that route_prefix must be able to mount any path without preparatory configuration. Older code may be mounted and 'work' without modification. One imagines module developers would be inclined to make their module-root '' (empty) to allow maximum flexibility in integration ("/mounted-module"), but they could just as well assert that their module-root always be "/mounted-module/" by making their module-root "/".

@mcdonc
Copy link
Member

mcdonc commented Jan 22, 2012

While I'm explicitly not passing judgment on the utility of the proposal, in general, for any change to be seriously considered, the benefit of the change is going to need to greatly outweigh the potential misery for existing users caused by a backwards incompatibility.

The change you're proposing would break any application which currently uses an include with a route_prefix where the set of routes being included contains a route with a pattern of '/' or ''.

In the meantime, the current behavior is indeed logically consistent (the empty route pattern at the root represents '/', a prefixed-and-included empty route pattern represents '/prefix/'). Additionally, a mechanism exists for resolving an included empty route pattern (AppendSlashNotFoundView).

So I think this is eventually going to boil down to a judgment between the benefits of retaining bw compat and the benefits of making the change. We're (for better or worse) very conservative when it comes to breaking bw compatibility, so at the risk of irritating you, such a change is very unlikely unless it can be shown to be in some way logically inconsistent.

@mcdonc
Copy link
Member

mcdonc commented Jan 22, 2012

Correction:

The change you're proposing would break any application which currently uses an include with a route_prefix where the set of routes being included contains a route with a pattern of ''. The pattern '/' would work.

@mmerickel
Copy link
Member Author

Pyramid makes no distinction between '' and '/' routes. This precedent needs to remain.

The goal of config.include is to allow routes to be mountable at any location. This works universally if we treat the route_prefix as a container appended with a slash. Things to consider are a) how difficult is it to use? and b) is this backward incompatible?

The question is how will config.include handle routes that have been added via config.add_route('' or '/')?

My proposed solution is to make the behavior only dependent on route_prefix. If it ends in a slash, then the route will end in a slash. If it doesn't, then the resulting route will not end in a slash.

Case 1 - (route_prefix='/prefix', pattern='') -> result='/prefix'
Case 2 - (route_prefix='/prefix', pattern='/') -> result='/prefix'
Case 3 - (route_prefix='/prefix/', pattern='') -> result='/prefix/'
Case 4 - (route_prefix='/prefix/', pattern='/') -> result='/prefix/'

  1. Maintains the precedent that '' == '/' in Pyramid.
  2. Predictable behavior.
  3. Controllable by the user doing the include, not the author of the included callable.
  4. Breaks existing URLs generated via route_prefix='/prefix' and pattern='' or '/'.
  5. Breaks any callable that is doing relative includes in their HTML/CSS for dependent resources, but only for the case where route_prefix='/prefix' and pattern='' or '/'.
    • e.g. For a view attached to '', with HTML in the template like <a href="index.html">Home</a> - The URL will be generated relative to the parent, ignoring the route_prefix, likely resulting in an incorrect URL.

For the case of view-relative includes, it seems to me that there is no good solution via route_prefix except the current behavior of treating an include as a container. Anything else would require the addon to either stop using view-relative includes, or document that it must be included with a route_prefix that ends in a slash.

Alternatively, this approach gives the user more control over the URLs supported by their application.

In general, the behavior most people probably want in their application is that of the AppendSlashNotFoundFactory as it will handle any style URL that a request may want, and resources will be included properly. The inverse RemoveSlashNotFoundFactory may sound great, but will require the user to think more carefully before using any view-relative includes in their application or in addons they are including.

Input would be great on this issue, because to me the solution is not obvious given Pyramid's history and the fact that no one can agree on the format for URLs on the internet in general.

@mcdonc
Copy link
Member

mcdonc commented Jan 23, 2012

Wrt to @mmerickel proposal, here are my thoughts...

I'll likely be eventually irritated when I need to remember to append a slash to a prefix when mounting an arbitrary third-party app that has a root view which depends on view-relative images/css. With the status quo, I don't need to remember anything, because the system makes the choice for me, and there's one unambiguous way it works. When I use e.g. rsync, or other systems where trailing slashes have meaning, I can never remember when appending a slash is necessary without reading the docs, and I usually succumb to that particular gotcha at least once before reading the docs.

I think it's highly likely that people are going to want both '/prefix/' and '/prefix' to work. Adding the proposed feature is going to make it slightly harder to document how to get that behavior, because it will depend on the prefix they're using to include the external routes. Currently it's pretty easy; we just tell them to use AppendSlashNotFoundView. And if AppendSlashNotFoundView is undesirable, it's not strictly necessary to have one. Something like this would work too:

config.add_route('redir_to_prefix', '/prefix')

@view_config(route_name='redir_to_prefix')
def redir(request):
    return pyramid.httpexceptions.HTTPFound(location='/prefix/')

Those things said, I'm OK with the proposal @mmerickel mentions above if he believes it's the Right Thing and the requisite documentation is added, even if it would entail a minor bw incompatibility. I'd also be quite happy to have the status quo.

@simonyarde
Copy link

I've put my efforts into a pull-request rather than try to describe further.

#414

@mmerickel I think your requirements are contained within what I'm proposing here and hope I have understood you correctly. I think module authors should be considering that the empty-route gives the user doing the include the most flexibility in how they mount it, but that the author can be opinionated if there is some reason for doing so. Likewise, with my solution above a user can overrule the author and use a slash-prepended prefix to insist that an empty-route becomes slash-appended. I think the system works well as it is with the exception of the issue with prefix mounting and empty pattern being ambiguous, so just solve the ambiguity with an explicit configuration setting.

@mcdonc it seems to me it's a matter of opinion what is more logical, from either the HTTP spec or the configuration utility perspective. As @mmerickel says "no one can agree on a format anyway". I'm proposing to solve the problem comprehensively by doing what Pyramid does best - let people choose, and ship the best default as and when the web evolves. I believe the above solution creates more flexibility in this edge-case and doesn't break bw compatibility re:

The change you're proposing would break any application which currently uses an include with a route_prefix where the set of routes being included contains a route with a pattern of ''. The pattern '/' would work.

@mcdonc not irritated in the slightest! And delighted to be having a conversation about this. I really need this flexibility for my project, and our marketing has very strong opinions about slash-appended paths. Personally I think it creates a dissonance between what a user sees in literature and what they see in the browser; we want to publish /mygreatproduct and not have it redirected. I think it will be like http:// when Berners-Lee said, "did we really need to design it like that!?" (or something along those lines), i.e. it is not elegant.

I'm not specifically hassling for you guys to accept this, but I will be using this patch (or something very like it) and am very pleased to contribute - even if my code helps by showing you what you don't want! :) Naturally, I would much prefer just to enable pyramid.allow_empty_pattern in my project.

@mcdonc
Copy link
Member

mcdonc commented Jan 23, 2012

@simonyarde

I'm -1 on any extra knobs (a global "allow_empty_pattern" setting or an "allow_empty_pattern" arg to include). Raydeo's proposal of making the slash at the end of a prefix meaningful makes more sense to me, although with the caveats I mentioned in my last comment (particularly, I'm not enthused by the idea of needing to explain to folks how both to append a slash and how to remove a slash depending on their prefix, it'll get awful confusing).

I recognize that there's an aesthetic value to non-slash-appended routes, and that you're trying to make use of a route prefix for code factoring. At the same time, non-slash-appended URLs aren't universally sensible in the context of the original use case for route_prefix, which was to allow people to mount arbitrary third-party applications at arbitrary prefixes. Root views of such apps will always work at slash-appended URLs but they might or might not work at non-slash-appended URLs. For better or worse, I lean towards making the default something that has the highest likelihood of working independent of aesthetics.

At the risk of stating the obvious, you don't have to use include() here at all if you're not concerned about composing multiple apps together (you don't need the conflict resolution behavior of include, presumably). Something like this would be just as sensible in that context:

def add_more_routes(config, p):
    prefix = p + '/sub'
    config.add_route('sub', prefix)

def add_routes(config, p):
    config.add_route('root', p)
    config.add_route('another', p+'/another')
    add_more_routes(config, p)

add_routes(config, '')

@simonyarde
Copy link

Thanks Chris.

I am seeking to make good-use of code factoring, but I was also hoping to present a highly logical approach to joining routes, which Pyramid currently does aside from #406.

To me, "/prefix" + '' = /prefix/ is like 2 + 2 = 5, and I wonder if most users would probably see it this way and find any discussion of containers very confusing.

At the risk of beating you over the head with it, I'll just add the my personal note that I chose Pyramid having matched your design defence against our requirements, particularly:

It is “Pyramidic” to compose multiple external sources into the same configuration using include(). Any number of includes can be done to compose an application; includes can even be done from within other includes. Any directive can be used within an include that can be used outside of one (such as add_view(), etc).

Sure, I know I can fudge-it but I want to be 'Pyramidic' and architect a pretty and logical application that is easy to understand for our team. I feel the current state of affairs excludes my approach, and I was hoping for:

Pyramid is, for better or worse, a “TIMTOWTDI” system.

I worry that if Pyramid starts excluding certain approaches on something as fundamental as route-design, what other features will be closed to me in future?

Re your comment @mcdonc:

the original use case for route_prefix, which was to allow people to mount arbitrary third-party applications at arbitrary prefixes

I was working on the basis of your design defence:

Truly pluggable applications need to be created at a much higher level than a web framework, as no web framework can offer enough constraints to really make them work out of the box.

I don't think I understand why the discussion is about how 'third-party-apps' can be mounted and 'just work' when Pyramid is not designed to be pluggable with third-party modules, since this is seen to hamstring other frameworks. This requirement seems like it is causing a headache over what to do, rather than letting us decide how we want to architect our applications. Just advise module developers on how to provide maximum flexibility, and provided route_url is used in templates nothing will break.

If my input is still welcome, I'll gladly try to follow along or re-think my solution re the -1 for a knob, otherwise I look forward to seeing what you come up with :)

@mcdonc
Copy link
Member

mcdonc commented Jan 23, 2012

I don't think I understand why the discussion is about how 'third-party-apps' can be mounted and 'just work'
when Pyramid is not designed to be pluggable with third-party modules, since this is seen to hamstring other
frameworks.

Fair point. "apps" is probably the wrong word here, sorry and I'm lazy to use it, particularly given the design defense stuff. Pyramid's isn't a platform for integrating "reusable apps". But Pyramid does allow a single app to be composed from multiple configuration sources, where some of those sources were not necessarily written by the person doing the composing. In particular, it should be possible for someone to write an includeme that provides routes and views that are consumable by a third party. And, in fact, if the includeme has views that work at the Pyramid root, if he doesn't use absolute literal URLs in the views, ideally, the original author shouldn't really need to think much about whether it will be included by somebody. It should just work when it's included, and currently does.

This requirement seems like it is causing a headache over what to do, rather than letting us decide
how we want to architect our applications. Just advise module developers on how to provide maximum flexibility,
and provided route_url is used in templates nothing will break.

We're really talking about a very small subset of includeables that will break. Currently it's possible for a consumer to pull an arbitrary set of routes into a Pyramid application's configuration as long as those routes are factored out into an includeme-style include function as long as views attached to the routes use 1) completely relative URLs (urls without any leading slash) 2) absolute qualified URLs generated via route_url or 3) absolute nonqualified urls generated via route_path. However, when the root view is served without a slash appended, URLs it generates via #1 will not work.

I'm nominally OK with nixing #1 as something that people who create includeables are allowed to do, although it makes the chances that using an include with a route pattern will work against arbitrary includemes slightly less likely. I'm not really OK with making it a global flag, because I don't think it's necessary if we do what @mmerickel has suggested and just make the slash at the end of the route prefix meaningful. Even though this is a backwards incompatibility, I think it's more sensible. My remaining concern is being able to provide enough documentation to someone whom:

  • Uses include(route_prefix='/prefix')
  • Wants to serve the root view of the included set of views as both '/prefix' and as '/prefix/'.

I'm loath to add a RemoveSlashNotFoundView to do this, in particular, but given that it's bound to come up, it needs to be addressed.

@simonyarde
Copy link

I'm not really OK with making it a global flag, because I don't think it's necessary if we do what @mmerickel has suggested and just make the slash at the end of the route prefix meaningful.

Got it. @mmerickel's proposal is very appealing, and since route-prefixes are the point at which a user has control (but having no other usage) they would be the natural configuration mechanism.

The code I was presenting was coming from the perspective that you'd probably prefer the module's authors to have the ultimate say (to be strongly opinionated in enforcing the appended-slash, OR allow the prefix-right-edge to decide). @mmerickel sorry for banging the stick the other way, I misunderstood that it was commonly preferred to let the user/implementer have ultimate control of how empty-routes should be handled, and your approach solves my particular issue in any case.

@mmerickel
Copy link
Member Author

I'm going to implement this before the next alpha release. We'll see how it pans out. It's just odd to me to allow the includeme to have control over every route that it defines except one. For example the includeme may add '/add/', and '/', and all of a sudden there is '/users/add/' and '/users'. The allow_empty_pattern originally proposed does solve this by allowing the person implementing the route to specify whether it must have a slash, just in case it uses view-relative includes or something, but I'm just not sure it feels right.

@simonyarde
Copy link

@mmerickel

Adding to your proposal - If you make route_prefix the configuration mechanism, and it becomes a Pyramid policy that module authors do not have ultimate control over their route-style (great), then it would make sense to have route_suffix as well, thus completing the configuration mechanism.

route_suffix would generally be used to supply '/' or '' or None, and would give implementers control over whether to have slash-appended-routes or non-slash-appended-routes. It also provides for suffixing in general, perhaps thinking of flexibility for designs as yet unknown, since there's no reason to be opinionated about what can be suffixed.

    # configure '/users/' + '' = '/users/' and slash-appended-route-patterns
    include('module', route_prefix='/users/', route_suffix='/')

    # configure '/users' + '/' = '/users' and non-slash-appended-route-patterns
    include('module', route_prefix'/users', route_suffix='')

    # do whatever module author wants for slash-appending
    include('module', route_prefix='/users', route_suffix=None)

@mcdonc

  • Wants to serve the root view of the included set of views as both '/prefix' and as '/prefix/'.

Do you mean possibility of configuring both routes within the module code? Or that they should both serve the same view? (bad www? although github does it)

@simonyarde
Copy link

@mcdonc

  • Wants to serve the root view of the included set of views as both '/prefix' and as '/prefix/'.

Actually.. does this comes under "there is no difference between '' and '/'" from the module authors perspective because they have no control over how their patterns will be joined in the implementation?

@mmerickel
Copy link
Member Author

I think we can eliminate route_suffix from the mix when you consider that you can include from within an include. It'd not be obvious what to do when I config.include(callable, route_suffix='/') and within callable the code config.include(.., route_suffix='') happens. The way Pyramid's conflict resolution works you'd want the user to win ('/'), but it could break an assumption that callable could have been expecting due to its own use of route_suffix.

@mmerickel
Copy link
Member Author

Personally I think this whole ticket is just a shortcoming of the design where '' == '/' in Pyramid. If this were not the case we would just strip and ignore the '/' from the end of route_prefix and the whole logic would be dictated by what was supplied to add_route.

@simonyarde
Copy link

The way Pyramid's conflict resolution works you'd want the user to win ('/')

I came to that conclusion, because if the implementer is going to control the slash on empty routes they should really control the slash on non-empty routes too. I've put my thinking into a pull re

https://github.com/simonyarde/pyramid/compare/master...route-prefix-issue-406

but it could break an assumption that callable could have been expecting due to its own use of route_suffix.

The direction of the solution seems to be that callables cannot assume anything about how they will be mounted, that is if the design goal is met where any route can be mounted, and any level of nesting.

What I was aiming for was to address that a when a third-party module is used on its own (top-level) then its internal prefix and suffix config works as normal, but when a module becomes a sub-module its prefix and suffix are ignored (in order to create joins) and the top-level controls empty-route and slash-appending. This assumes of course the route_suffix option is supplied; if it's empty then the sub-modules internal config wins.

@simonyarde
Copy link

Please see alternative approach passing route_prefixes as lists instead of strings, I think it's neater and you can centralise all the route-pattern string creation. Route-suffixes make no difference to the slash-style, because the top-level include wins (user implementation level).

https://github.com/Pylons/pyramid/pull/414/files

@mmerickel
Copy link
Member Author

Regarding this issue, it's going to get tabled for now. The modification of route_prefix to support mutating a route that was defined elsewhere was hinting at something, and the route_suffix feature that has been suggested makes it clear that the whole idea of mutating routes defined by "other people" is a whole new feature that needs to be more properly discussed API-wise if it's to be included. Pyramid's current implementation of route_prefix, while arguably limiting, is consistent and predictable. I've also looked at other frameworks that implement a similar feature and they also do it this way (automatic / appended), requiring you to use the AppendSlashNotFoundFactory to get the actual URL that you want or defining a separate route that does the redirect.

Anyway, if you feel strongly enough about this I think it should be rephrased in terms of modifying route patterns in addons, which I will argue is out-of-scope for Pyramid.

@mcdonc
Copy link
Member

mcdonc commented Feb 22, 2012

So here's the plan wrt to this issue:

  • We're going to leave the behavior of route_prefix as-is for 1.3. This means that without any help from external bits,
    users will be obligated to contact the root route of applications included with a route prefix using a slash-appended
    URL.
  • When folks want an included root route to match without a trailing slash, we're going to suggest one of two things:
    an AppendSlashNotFoundView or use of something along the lines of
    http://pypi.python.org/pypi/pyramid_rewrite/0.2

If it turns out there's enough problems with this decision to indicate a change needs to be made, we'll reconsider this in 1.4.

In the meantime, I'm going to make AppendSlashNotFoundView a little less crappy to actually use.

@bismigalis
Copy link

Reread the thread 3 times. Now understand.
There is two group of peoples

  1. Those who likes urls with slash at the end
  2. Those who likes urls without slash at the end(I'm in this group)
    Both groups must be satisfied. Pyramid is not opinionated, what is at the end in add_route, that is match.
    Problem arise when pattern='' or pattern='/'. Because of this code:
if self.route_prefix:
    pattern = self.route_prefix.rstrip('/') + '/' + pattern.lstrip('/')

Right now, In all 4 cases from first post, end result is the same 'some_prefix/'

  1. First group is satisfied, b/c end slash
  2. Second group unsatisfied

After this patch 0169301:

  1. First group is unsatisfied
  2. Second group satisfied, b/c there is no end slash

@mcdonc suggested using append_slash option for people from second group. But this option is convenient for people from first group, b/c their patterns contains slash at the end. And they most likely will use this option for convenience for their users.

For people from 1 group this option needed only in connection with this #406 issue.

So may be it is wise to switch to second behavior, so both group will be satisfied.

@sontek
Copy link
Member

sontek commented Jun 22, 2015

@mmerickel When you going to fix this so that #819 will just work? =)

@digitalresistor
Copy link
Member

I have a feeling that this new bug report is related: #2143

@jvanasco
Copy link
Contributor

jvanasco commented Apr 7, 2016

Regarding the 2.0 (#2362) possibility:

I think @mmerickel's original notion of making things dependent on the existence of a trailing slash in the config.include is the correct option, but that discussion is 4 years old and so even more backwards compatible now.

I think think the simplest option is to addd a kwarg to config_include that could disable the trailing slash on indexes (but defaults to keeping it for backwards compatibility).

  • The AppendSlashNotFoundFactory is not a good solution, because it applies globally - not just on a particular route/component.
  • The pyramid_rewrite is an external library/not-core and is not guaranteed to work across versions.

Keep in mind that different plugins will be built and want to be implemented differently. One plugin might not even have a '' route, others might. Some people might not even be using the route_prefix functionality to build plugins, but just to customize how their app mounts a particular section.

The best workaround I've found in the meantime is to explicitly declare whatever the "index" route is named to be just the prefix:

def includeme(config):
    route_prefix = config.registry.settings.get("admin_prefix")
    config.include(_admin_views, route_prefix=route_prefix)
    config.add_route('admin-index', route_prefix)

def _admin_views(config):
    config.add_route('admin-index', '')

That requires no 3rd party module, and does not introduce a global not-found behavior.

An implementation detail of routes that I noticed (but haven't had time to look into), is that this method will always result in the config.add_route('admin', route_prefix) to be the working route regardless of order (either before or after the include).

@mmerickel
Copy link
Member Author

fixed via #3420

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

Successfully merging a pull request may close this issue.

7 participants