Skip to content

Incorporate downstream CEGID changes. #202

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

Open
wants to merge 36 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4e3a018
Version pin adjustment.
Nov 24, 2022
0d68aa6
Pin greater than.
Nov 29, 2022
fc6f09c
Adapt (older extensions) to modern dispatch protocol.
Dec 6, 2022
9fffbad
Minimum Python version bump.
Dec 13, 2022
28da0ef
collections->typing module movement.
Dec 13, 2022
e33ce1e
Protect ContextGroup against "protected" access, better handle defaul…
Dec 15, 2022
fa7ab39
Cleanup.
Jan 12, 2023
ce3bb46
Correct units.
Feb 14, 2023
d4fe108
My kingdom for an identity check against None, not a truthy one.
Jun 1, 2023
3ac86b9
Copyright test bump (wat) and actually testing ContextGroup as intended.
Jun 1, 2023
02ac01a
Actually working tests.
Jun 1, 2023
0366da9
Add MockRequest and validate testing helpers.
amcgregor Aug 10, 2023
f449f5d
More explicitly log uncaught exceptions as errors, explicitly includi…
amcgregor Aug 22, 2023
d8dbbad
Desperation.
amcgregor Aug 24, 2023
e1eed88
Add Python 3 type hinting annotation helpers and status code-based in…
amcgregor Sep 5, 2023
c5013ee
Docstrings, and easier customization of normalization.
amcgregor Sep 5, 2023
8d4fec9
Additional annotations.
amcgregor Sep 5, 2023
d5a79c2
Explicit typechecking and shuffling of type definitions.
amcgregor Sep 5, 2023
8fa041b
Proxy through __contains__ checks.
amcgregor Sep 5, 2023
d0ea9c5
Additional armour against empty handlers.
amcgregor Sep 5, 2023
2477f65
Utilize "maintenance mode" only if a handler for 503 has been defined.
amcgregor Sep 5, 2023
1afbabe
Even more armour!
amcgregor Sep 5, 2023
ca41a88
Cleanup.
amcgregor Sep 5, 2023
c8c042a
Impossible word wrapping, mapping length proxy.
amcgregor Sep 5, 2023
a67ddd3
Initial WIP tests for status code handlers.
amcgregor Sep 5, 2023
d49ce5c
Test both registered cases.
amcgregor Sep 5, 2023
218c998
Expand StatusLike to encompass strings.
amcgregor Sep 7, 2023
dc44942
Explicitly warn if maintenance mode is enabled w/o maintenance handler.
amcgregor Sep 7, 2023
8674319
Calculate maintenance status once, not twice.
amcgregor Sep 7, 2023
cd682da
Add note about WSGIWriter return…
amcgregor Sep 7, 2023
7eb27be
Additional comments, incorporate function as classmethod.
amcgregor Sep 7, 2023
fe71834
Add Python 3 asynchronous execution support extension.
amcgregor Nov 2, 2023
0bacd0b
Populate application-scope local context in async thread, better hand…
amcgregor Nov 2, 2023
554ac89
Initial import of WebCore TestingContext fixture.
amcgregor Feb 6, 2024
7da0a78
Test fixture refinement.
amcgregor Feb 6, 2024
9008a10
Merge branch 'develop' into cegid
amcgregor Jan 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions test/test_core/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,24 @@ def test_context_group_initial_arguments():


def test_context_group_default():
inner = ContextGroup()
inner = Context()
group = ContextGroup(inner)

thing = group.foo = Thing()
thing = Thing()

# Propagation from inner to group:
inner.foo = thing
assert inner.foo is thing
assert group.foo is thing
del group.foo

assert 'foo' not in inner, list(inner)
assert 'foo' not in group, group.foo


del inner.foo
assert 'foo' not in inner
assert 'foo' not in group

# Propagation from group to default inner:
group.bar = thing
assert inner.bar is thing

del group.bar
assert 'bar' not in inner
assert 'bar' not in group

2 changes: 1 addition & 1 deletion test/test_extension/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def test_response(self):
assert self.do(response_endpoint).text == "Yo."

def test_file(self):
assert '2016' in self.do(binary_file_endpoint).text
assert '2006' in self.do(binary_file_endpoint).text

def test_generator(self):
assert 'foobar' in self.do(generator_endpoint).text
Expand Down
40 changes: 40 additions & 0 deletions test/test_extension/test_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import pytest
from webob import Request
from webob.exc import HTTPNotFound

from web.core import Application
from web.core.testing import MockRequest
from web.core.ext.handler import StatusHandlers


def mock_endpoint(context, status, state=None):
context.response.status_int = int(status)
mock_endpoint.state = state
return state


@pytest.fixture
def app():
return Application(mock_endpoint, [StatusHandlers({
HTTPNotFound: '/404/notfound',
503: '/503/maintenance',
})])


def test_notfound(app):
with MockRequest('/404') as request:
response = request.send(app)

assert response.text == 'notfound'
assert response.status_int == 404
assert mock_endpoint.state == 'notfound'


def test_maintenance(app):
with MockRequest('/404') as request:
response = request.send(app)

assert response.text == 'notfound'
assert response.status_int == 404
assert mock_endpoint.state == 'notfound'

4 changes: 3 additions & 1 deletion web/core/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,9 @@ def application(self, environ, start_response):
try:
result = self._execute_endpoint(context, handler, signals) # Process the endpoint.
except Exception as e:
log.exception("Caught exception attempting to execute the endpoint.")
log.error("Caught exception attempting to execute the endpoint.", exc_info=True)
raise

result = HTTPInternalServerError(str(e) if __debug__ else "Please see the logs.")

if 'debugger' in context.extension.feature:
Expand Down
2 changes: 1 addition & 1 deletion web/core/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ class ContextGroup(Context):
default = None

def __init__(self, default=None, **kw):
if default:
if default is not None:
self.__dict__['default'] = default # Avoid attribute assignment protocol for this one.

for name in kw:
Expand Down
18 changes: 3 additions & 15 deletions web/core/dispatch.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,11 @@
# encoding: utf-8

# ## Imports

from __future__ import unicode_literals

from collections import deque
from inspect import isclass
from marrow.package.host import PluginManager


# ## Module Globals

# A standard logger object.
log = __import__('logging').getLogger(__name__)
log = __import__('logging').getLogger(__name__) # A standard logger object.


# ## Dispatch Plugin Manager

class WebDispatchers(PluginManager):
"""WebCore dispatch protocol adapter.

Expand All @@ -43,7 +32,7 @@ def __init__(self, ctx):
an attribute of the current Application or Request context: `context.dispatch`
"""

super(WebDispatchers, self).__init__('web.dispatch')
super().__init__('web.dispatch')

def __call__(self, context, handler, path):
"""Having been bound to an appropriate context, find a handler for the request path.
Expand Down Expand Up @@ -85,8 +74,7 @@ def __call__(self, context, handler, path):

if is_endpoint and not callable(handler) and hasattr(handler, '__dispatch__'):
crumb = crumb.replace(endpoint=False)

#__import__('wdb').set_trace()

# DO NOT add production logging statements (ones not wrapped in `if __debug__`) to this callback!
for ext in callbacks: ext(context, str(crumb.path) if crumb.path else None, crumb.handler, crumb.endpoint)

Expand Down
110 changes: 110 additions & 0 deletions web/core/testing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from warnings import warn
from typing import Any, Optional, Mapping

from uri import URI, Bucket
from web.auth import authenticate, deauthenticate
from web.core import local
from webob import Request, Response
from webob.exc import HTTPException, HTTPOk


''' WIP
from pytest import fixture


@fixture(scope='session', autouse=True)
def testing_context(application):
"""Provide and manage the TestingContext Pytest must execute within.

Your own test suite must define a fixture named `application`, which will be utilized by this fixture to populate
the context and signal any configured extensions.
"""

original = local.context
signals = original.extension.signal

# Prepare a TestingContext using a blank request.
request = Request.blank('/')
context = app.RequestContext(environ=request.environ)._promote('TestingContext')

# notify suite began

yield context

# notify suite finished

for ext in signals.stop: ext(original)

local.context = original


@fixture(scope='function', autouse=True)
def test_context(testing_context):
for ext in signals.pre: ext(testing_context)
for ext in signals.test: ext(testing_context)

yield context

for ext in signals.after: ext(testing_context)
for ext in signals.done: ext(testing_context)
'''


class MockRequest:
"""Prepare a new, mocked request.

Must be passed the current context and the path to an endpoint to execute.

The `verb` defaults to "get". Any `data` passed in will be encoded as the request body. Additional keyword
arguments are treated as query string parameters. These will be added to the existing set, they will not override
existing values present in the base endpoint URI.
"""

__slots__ = ('url', 'verb', 'data', 'user')

uri: URI
verb: str
data: Optional[Mapping]
user: Any

def __init__(self, verb:str='GET', path:str='', data:Optional[Mapping]=None, user:Optional[str]=None, /, **kw):
"""Construct the components necessary to request a given endpoint directly from a WSGI application.

Form-encoded body data is specified by way of the `data` argument; keyword arguments are added to the endpoint
URI as query string arguments. Active user mocking exposes a `web.account` WSGI environment variable.
"""

self.uri = URI("http://localhost/") / path
self.verb = verb
self.data = data
self.user = user

for k, v in kw.items():
self.uri.query.buckets.append(Bucket(k ,v))

def __enter__(self):
"""Populate and hand back a populated mock WebOb Request object, ready to be invoked."""
req = Request.blank(str(self.uri), POST=self.data, environ={'REQUEST_METHOD': self.verb})
if self.user: req.environ['web.account'] = self.user
return req

@classmethod
def send(MockRequest, app:WSGI, verb:str='GET', path:str='', data:Optional[Mapping]=None, user:Optional[str]=None,
expect:Optional[HTTPException]=None) -> Response:
"""Populate, then invoke a populated WebOb Request instance against the given application.

This returns the resulting WebOb Response instance.
"""

if expect is None: expect = HTTPOk
location = None

with MockRequest(verb, path, user=user, **((data or {}) if verb == 'GET' else {'data': data})) as request:
response = request.send(app)

if isinstance(expect, tuple): expect, location = expect
assert response.status_int == expect.code
if location: assert response.location == 'http://localhost:8080' + location

return response

81 changes: 81 additions & 0 deletions web/core/typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Typing helpers."""

from logging import Logger
from pathlib import Path, PurePosixPath
from types import ModuleType
from typing import Any, Callable, ClassVar, Dict, Generator, Iterable, List, Mapping, Optional, Set, Tuple, Union, \
Text, Type, Pattern, MutableSet

from typeguard import check_argument_types
from uri import URI
from webob import Request, Response

from ..dispatch.core import Crumb
from .context import Context # Make abstract? :'(


# Core application configuration components.

AccelRedirectSourcePrefix = Union[str, Path]
AccelRedirectSourceTarget = PathLike = Union[str, PurePosixPath, URI]
AccelRedirect = Optional[Tuple[AccelRedirectSourcePrefix, AccelRedirectSourceTarget]]


# Types for WebCore extension component parts.

Tags = Set[str] # Extension feature and dependency tags.
PositionalArgs = List[Any] # Positional arguments to the endpoint callable.
KeywordArgs = Dict[str, Any] # Keyword arguments to the endpoint callable.
Environment = Dict[str, Any] # An interactive shell REPL environment.
StatusLike = Union[str, int, Response, HTTPError] # Objects usable as an HTTP status.


# Types for WSGI component parts.

# Passed to the WSGI application.
WSGIEnvironment = Dict[Text, Any]

# Passed to start_response.
WSGIStatus = str
WSGIHeaders = List[Tuple[str, str]]
WSGIException = Optional[Tuple[Any, Any, Any]]

# Returned by start_response.
WSGIWriter = Callable[[bytes], None]

WSGIResponse = Union[
Generator[bytes, None, None],
Iterable[bytes]
]

# Types for core WSGI protocol components.

# Passed to the WSGI application.
WSGIStartResponse = Callable[[WSGIStatus, WSGIHeaders, WSGIException], WSGIWriter]

# WSGI application object itself.
WSGI = Callable[[WSGIEnvironment, WSGIStartResponse], WSGIResponse]


# Types relating to specific forms of callback utilized by the framework.

# The `serve` web application/server bridge API interface.
HostBind = str
PortBind = int
DomainBind = Optional[Union[str,Path]]
WebServer = Callable[..., None] # [WSGI, HostBind, PortBind, ...]

# Endpoint return value handlers.
View = Callable[[Context,Any],bool]

# Serialization extension related typing.
SerializationTypes = Iterable[type]
Serializer = Callable[[Any], str]
Deserializer = Callable[[str], Any]


# Specific utility forms.

PatternString = Union[str, Pattern]
PatternStrings = Iterable[PatternString]

2 changes: 1 addition & 1 deletion web/ext/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def after(self, context, exc=None):
context.response.headers[self.header] = delta

if self.log:
self.log("Response generated in " + delta + " seconds.", extra=dict(
self.log("Response generated in " + delta + " milliseconds.", extra=dict(
duration = duration,
request = id(context)
))
Expand Down
Loading