-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
Implement dynamic prefix for subapps #2756
base: master
Are you sure you want to change the base?
Conversation
My implementation approach:
|
Codecov Report
@@ Coverage Diff @@
## master #2756 +/- ##
==========================================
- Coverage 97.97% 97.88% -0.09%
==========================================
Files 39 39
Lines 7464 7528 +64
Branches 1310 1322 +12
==========================================
+ Hits 7313 7369 +56
- Misses 48 52 +4
- Partials 103 107 +4
Continue to review full report at Codecov.
|
To allow access to all overriden variables, we could use |
Okay, I implemented using ChainMap. 😉 |
* Utilize aiohttp's nested application architecture to modularize. - Requires aio-libs/aiohttp#2756 to be merged. * Each extension module must provide: - "create_app()" module-level function. It should return a pair of an aiohttp.web.Application instance and a list of global middlewares. If the app instance has "prefix" property key, this will be used as the URL prefix: "/v{version:\d+}/PREFIX". If not set, the module name will be used instead. > The request.app in global middleware will be the root app instead of the extension app. To pass the extension app to a global middleware, you should wrap it with "web.middleware(functools.partial(mwfunc, app))" in the "create_app()" function. See example: ai/backend/gateway/ratelimit.py * Each extension module have access to several global objects such as AgentRegistry and the database connection pool. The exact list of available "public APIs" is at "server_main()" function of ai/backend/gateway/server.py: property set from app to subapp. * Now admin, auth, etcd, events, kernel, server, and vfolder became "intrinsic" extension modules that are loaded always and automatically. - You can add arbitrary app modules which are importable Python package module names specified in the BACKEND_EXTENSIONS environment variable as comma-separated strings.
@@ -1003,7 +1005,7 @@ def test_url_for_in_resource_route(router): | |||
|
|||
def test_subapp_get_info(app, loop): | |||
subapp = web.Application() | |||
resource = subapp.add_subapp('/pre', subapp) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch!
@achimnol thanks for PR. I see the solution:
Right now I don't care about performance, clean API is the first target. Later we can add Cythonized version of What do you think about? |
|
To change Also I still don't get the benefit of having multiple per-app-level I think it is sufficient to have nested maps via ChainMap just like |
I've convinced. Let's use chained map approach. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for delay and thank you for patience.
I have some comments.
Let's discuss my proposals.
@@ -172,12 +172,13 @@ def url_for(self, *args, **kwargs): | |||
return await self._expect_handler(request) | |||
|
|||
|
|||
class UrlMappingMatchInfo(dict, AbstractMatchInfo): | |||
class UrlMappingMatchInfo(AbstractMatchInfo, Mapping): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like the fact that UrlMappingMatchInfo
is immutable now.
aiohttp/web_urldispatcher.py
Outdated
|
||
def __init__(self, match_dict, route): | ||
super().__init__(match_dict) | ||
super().__init__() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
super()
call is not needed, Python machinery will work fine without it.
aiohttp/web_urldispatcher.py
Outdated
return tuple(reversed(self._variables.maps)) | ||
|
||
def add_variables(self, match_dict): | ||
self._variables.maps.append(match_dict) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suspect you should insert a new dict in front of maps -- like we do it for add_app()
.
Moreover maybe merging add_app()
and add_variables()
functionality makes sense.
What do you think about _add_app(app, match_dict)
method as replacement for add_app
.
Adding underscore emphasizes that the method is private. add_app()
was never a part of documented public API, I think the change is safe in backward compatibility perspective.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ChainMap
resolves from the beginning of its maps
. The point here is that add_variables()
is called after resolving the subapp's routes, which means that subapp's match variables are added to the chain map first. Since we want to give priority to the innermost subapp's variables in the merged view of this chain map, this append
is natural.
I think merging this method into add_app(app, match_dict)
is a good idea. 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm.... merging into a single method causes a lot of problem:
- When instantiating
UrlMappingMatchInfo
we already havematch_dict
(so the chain map begins with a single layer) but_app
is None and_apps
is an empty tuple. This makes the number of nested maps and apps inconsistent. - Changing
UrlMappingMatchInfo
's constructor to accept an explicitapp
argument requiresRoute
andResource
classes to keep track of their ownerapp
becauseresolve()
method should be able to indicate the exactapp
instance to create the match info object.request.app
is a self null reference there. add_app()
is called at many other places such astest_utils.py
andweb_app.py
wherematch_dict
is out-of-scope.
The root cause of this mess is that app
and match_dict
are added to UrlMappingMatchInfo
at different places and timings. 😕
aiohttp/web_urldispatcher.py
Outdated
def __repr__(self): | ||
return "<MatchInfo {}: {}>".format(super().__repr__(), self._route) | ||
return "<MatchInfo {}: {}>".format(dict(self).__repr__(), self._route) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
repr(self._variables)
instead of repr(dict(self))
is better I believe. There is no reason to hide chained variables.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was to make existing test cases that compares the repr
result with a constant string to pass. Then I will change those test cases to match with ChainMap
's repr
result.
aiohttp/web_urldispatcher.py
Outdated
@@ -230,8 +231,24 @@ def set_current_app(self, app): | |||
def freeze(self): | |||
self._frozen = True | |||
|
|||
@property | |||
def variable_maps(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe let's use just maps
name?
maps
is shorted, in documentation we could refer to ChainMap.maps
. maps
is mentally more familiar to people if they know about ChainMap
already.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also related to the fact that resolution order of the chain map and its build order are reversed. The use of reversed()
here is to be consistent with the order of the return value of apps
property.
Also I wanted to make it read-only since ChainMap
's maps
property is mutable -- so I've used a different name because they are not the same. Still, I'd like to change the name to maps
if you still want it. :) If so, I will update the docs as well.
docs/web_reference.rst
Outdated
@@ -2181,8 +2189,8 @@ In general the result may be any object derived from | |||
|
|||
.. class:: UrlMappingMatchInfo | |||
|
|||
Inherited from :class:`dict` and :class:`AbstractMatchInfo`. Dict | |||
items are filled by matching info and is :term:`resource`\-specific. | |||
Inherited from :class:`collections.ChainMap` and :class:`AbstractMatchInfo`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The doc is not relevant anymore, Mapping
should replace ChainMap
.
Also .variable_maps
or .maps
attribute should be documented.
aiohttp/web_urldispatcher.py
Outdated
def add_map(self, match_dict): | ||
if self._frozen: | ||
raise RuntimeError("Cannot change maps stack after .freeze() call") | ||
self._variables.maps.append(match_dict) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's consider the following:
async def handler(request):
return web.Response(text=request.math_info['name'])
subapp = web.Application()
subapp.add_routes([web.get('/{name}', handler)])
app = web.Application()
app.add_subapp('/{name}', subapp)
What name
should be returned by the handler?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, request.match_info['name']
in the handler will return the matched value of the innermost {name}
definition: the route one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perfect!
aiohttp/web_urldispatcher.py
Outdated
self._app = app | ||
self._prefix = prefix | ||
|
||
def url_for(self, *args, **kwargs): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see more complex problem.
.url_for()
for all resources from sub-application are broken for dynamic prefixed resource.
Sorry, but the problem is blocker for merging.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's what I mentioned in the gitter channel.
I think rewriting url_for()
to have a recursive architecture so that route resources of different types in different subapp levels could be chained requires a lot of work, so I'd like to suggest it as a separate PR.
Maybe we could mark this PR as "provisional" if merged before it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm currently very busy due to my business work -- once I get some time after a few weeks, I'll give a look on the extension plan for url_for()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is that without url_for
fix not only url_for()
for subapp itself doesn't work but all url_for()
calls for subapp resources are broken.
That's why I think everything should be done in single PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I agree with you if possible it's better to make the changes in a single PR.
While I have to be a little bit away from this PR right now, other people's contributions to this PR are welcome!
4d33cba
to
50ac11d
Compare
Implemented support for
@asvetlov What do you think? |
Ah, since dynamic prefix is not a released API yet, I think it is safe to make the proposed name-and-predixed-keys scheme optional. |
aiohttp/web_urldispatcher.py
Outdated
|
||
def get_info(self): | ||
return {'app': self._app, | ||
'prefix': self._prefix} | ||
|
||
async def resolve(self, request): | ||
if not request.url.raw_path.startswith(self._prefix): | ||
if not request.url.path.startswith(self._prefix): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not raw_path
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if the prefix includes arbitrary unicode characters?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mixing PCT encoded and decoded data can make a mess easy.
I believe we should follow the strategy:
- Convert all user provided path, prefix etc into normalized (PCT-encoded actually) form by using
yarl
. Already normalized paths passed as-is by yarl. - Normalize given HTTP path (we do it).
- Match normalized path to already pre-normalized patterns.
- Return
request.match_info['name']
in PCT-decoded form because users most likely want getting이름
instead of%EC%9D%B4%EB%A6%84
If aiohttp code violates the rule somewhere -- it should be fixed.
Do you agree?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I agree to keep the rule consistent. Let me inspect and fix up this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think then PlainResource.__init__()
also need this, like PrefixResource
:
self._path = URL.build(path=path).raw_path
Now if we have a nested dynamic subapp chain: root_app = web.Application()
outer_app = web.Application()
inner_app = web.Application()
resource = inner_app.router.add_resource('/{var}')
route = resource.add_route('GET', ...)
outer_app.add_subapp('/{var}', inner_app, name='b')
root_app.add_subapp('/{var}', outer_app, name='a') Then {
'a.var': 'abc',
'b.var': 'def',
'var': 'ghi',
}
'/123/456/789' When {
'var': 'ghi'
} and '/xxx/xxx/xxx' No more ChainMap! |
ed5565e
to
3aa48f7
Compare
Rebased against latest master. |
I think it's ready to get reviewed. :) |
Let me meditate for a while. If you have ideas how to make it steady -- please share your vision. I understand your striving to make subapp prefix flexible but my guts feel that we should be very careful with chosen design. Otherwise fitting a opened can of worms will be painful. I suggest excluding the PR from aiohttp 3.1 but working hard to publish it as part of aiohttp 3.2
P.S. |
I see. You want more isolation between apps -- and I agree with that. I'm fine to go back to the first ChainMap design for To allow use Still we have two issues:
@asvetlov What do you think? |
More thoughts: |
More more thoughts: From the perspective of inner apps, it's safe to assume that the aiohttp's web framework will take care of filling outer prefix variables with the handler-local match_info context. From the perspective of outer apps, they need to explicitly give the value of prefix variables when calling |
Maybe name prefixes are not bad but I need a time to experiment with.
|
* This behavior should be consistent with the constructor.
* So let it add an empty dict when nesting.
* Let UrlMappingMatchInfo inherit collections.abc.Mapping instead of ChainMap directly. - Make the order of inheritance consistent with UrlDispatcher. * Make the order of the nested dictionaries returned by "variable_maps" consistent with the order of the nested apps returned by "apps".
* Kept add_map() (formerly add_variables) and add_app() methods sepearte since they are often called in disjoint contexts where the other is out-of-scope. * Renamed variable_maps to maps. * Updated the docs related to add_subapp(), MatchInfo, and DynamicSubAppResource.
* Changed add_prefix() to accept resource instances instead of prefix strings. * url_for() now recursively call the parent's url_for() and prefix the current url with it. For this, url_for() with PrefixedSubAppResoucre/DynamicSubAppResource are no longer errors. * Changed PrefixedSubAppResource.resolve() to also clone the request with the stripped URL like DymaicSubAppResource.resolve(). * This implementation does not break any existing code. :) * TODO: I will add some test cases and extra checks to the code.
* Fixed some unimplemented/mistaken parts of the previous commit * Reverted back to use 'raw_path' where it had been used before as it still works as desired.
* All paths (even regexs against them) stored in resources are already encoded. They are only decoded when passed back to the user. * PlainResource was missing this explicit encoding when initialized.
d3e8e46
to
245be63
Compare
What do these changes do?
This allows use of variables (regular expressions) in the subapp prefixes.
Are there changes in behavior for the user?
Existing codes are not affected.
Related issue number
I am making this PR after discussion with @asvetlov in gitter.
Checklist
CONTRIBUTORS.txt
CHANGES
folder<issue_id>.<type>
for example (588.bugfix)issue_id
change it to the pr id after creating the pr.feature
: Signifying a new feature..bugfix
: Signifying a bug fix..doc
: Signifying a documentation improvement..removal
: Signifying a deprecation or removal of public API..misc
: A ticket has been closed, but it is not of interest to users.