Skip to content

Commit

Permalink
move to github actions and change linters
Browse files Browse the repository at this point in the history
  • Loading branch information
isik-kaplan committed Oct 16, 2024
1 parent 8c39c62 commit 60fe7bd
Show file tree
Hide file tree
Showing 16 changed files with 198 additions and 217 deletions.
48 changes: 48 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Django Tests

on:
push:
branches:
- master
pull_request:
branches:
- master

jobs:
test:
strategy:
matrix:
python-version: ['3.5', '3.6', '3.7', '3.8', '3.9', '3.10', '3.11', '3.12']
django-version: ['2.0', '2.1', '2.2', '3.0', '3.1', '3.2', '4.0', '4.1', '4.2', '5.0', '5.1']
os-version: [ubuntu-18.04, ubuntu-22.04]

exclude:
- python-version: '3.5'
os-version: ubuntu-22.04
- python-version: '3.6'
os-version: ubuntu-22.04
- python-version: '3.7'
os-version: ubuntu-22.04
- python-version: '3.8'
os-version: ubuntu-22.04
- python-version: '3.9'
os-version: ubuntu-22.04

runs-on: ${{ matrix.os-version }}

steps:
- uses: actions/checkout@v3

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools packaging
python -m pip install Django==${{ matrix.django-version }} pytest pytest-django
- name: Run Django tests
run: |
python -m pytest
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,3 @@ dmypy.json

# Pyre type checker
.pyre/

20 changes: 20 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: debug-statements
- id: end-of-file-fixer
- id: trailing-whitespace
- id: detect-private-key

- repo: https://github.com/psf/black
rev: 24.4.2
hooks:
- id: black
args: [--line-length=120]

- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
args: [--profile=black, -m=3, -l=120]
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.
56 changes: 28 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[![Build Status](https://app.travis-ci.com/isik-kaplan/django-http-exceptions.svg?token=GLRwtEHQ7cKeAZcq6ZpN&branch=master)](https://app.travis-ci.com/isik-kaplan/django-http-exceptions)
[![codecov](https://codecov.io/gh/isik-kaplan/django-http-exceptions/branch/master/graph/badge.svg)](https://codecov.io/gh/isik-kaplan/django-http-exceptions)
[![codecov](https://codecov.io/gh/isik-kaplan/django-http-exceptions/branch/master/graph/badge.svg)](https://codecov.io/gh/isik-kaplan/django-http-exceptions)
[![Python 3.5+](https://img.shields.io/badge/python-3.5+-brightgreen.svg)](#)
[![Django 2.0+](https://img.shields.io/badge/django-2.0+-brightgreen.svg)](#)
[![PyPI - License](https://img.shields.io/pypi/l/django-http-exceptions.svg)](https://pypi.org/project/django-http-exceptions/)
Expand All @@ -14,7 +14,7 @@ It is raisable exceptions for your django views.

## What is it good for?

It makes this
It makes this

````py
def some_function():
Expand All @@ -34,11 +34,11 @@ def some_function():
raise HTTPExceptions.FORBIDDEN # HTTPExceptions.from_status(403)

def view(request):
return some_function()
return some_function()

````

meaning that is saves you from boilerplate code.
meaning that is saves you from boilerplate code.

It also allows you to hook default views to **all possible http response codes**, meaning that you can use more than the 5-6 django provided error handlers.

Expand All @@ -61,66 +61,66 @@ And that is it, you are ready to raise your http exceptions.



## What else?
## What else?


#### `HTTPExceptions`
Base class that provides all the exceptions to be raised.


#### `HTTPExceptions.from_status(status)`
In case you don't want to write
`HTTPExceptions.REQUEST_HEADER_FIELDS_TOO_LARGE`
You can just write
#### `HTTPExceptions.from_status(status)`
In case you don't want to write
`HTTPExceptions.REQUEST_HEADER_FIELDS_TOO_LARGE`
You can just write
`HTTPExceptions.from_status(431)`


#### `HTTPExceptions.BASE_EXCEPTON`
#### `HTTPExceptions.BASE_EXCEPTON`
The base exception for all http exception

#### `HTTPExceptions.register_base_exception(exception)`
Given that `exception` is a class that inherits from `HTTPException` you can customize the exceptions.
Keep in mind that `HTTPException` is an `Exception` subclass itself.


#### `HTTPExceptions.BASE_EXCEPTION.with_response(response)`
#### `HTTPExceptions.BASE_EXCEPTION.with_response(response)`
This is the method for raising exceptions with a response. You can put any response in this method while raising your
error.

Let's say you have a view named `index`, then this example would return what `index` function would return, but with
status code `410`
status code `410`
`HTTPExceptions.GONE.with_response(index(request))`


#### `HTTPExceptions.BASE_EXCEPTION.with_content(content)`
#### `HTTPExceptions.BASE_EXCEPTION.with_content(content)`
This method allow to raise an **HTTPException** with a custom message (can be either `str` or `bytes`).

For instance, `HTTPExceptions.NOT_FOUND.with_content("The user named 'username' could not be found")`
would return something equivalent to `HttpResponse("The user named 'username' could not be found", status=404)`.

#### `HTTPExceptions.BASE_EXCEPTION.with_json(json_data)`
This method allow to raise an **HTTPException** with a custom json response,
This method allow to raise an **HTTPException** with a custom json response,
`json_data` can be anything that `JsonResponse` accepts.

#### `HTTPExceptions.BASE_EXCEPTION.register_default_view(view)`
#### `HTTPExceptions.BASE_EXCEPTION.register_default_view(view)`
`view` is a function that takes only one argument, `request` when you register a default view to an error class with
`HTTPExceptions.NOT_FOUND.register_defaul_view(view)` when `HTTPExceptions.GONE` is raised it returns the view function,
but again, with `404` status code. If the error has been raised with `.with_response`, that is used instead.
`HTTPExceptions.NOT_FOUND.register_defaul_view(view)` when `HTTPExceptions.GONE` is raised it returns the view function,
but again, with `404` status code. If the error has been raised with `.with_response`, that is used instead.


#### `get_current_request`

This function gets you the current request anywhere in your django application, making it easier for your dynamic error
This function gets you the current request anywhere in your django application, making it easier for your dynamic error
responses to be created, like in the `HTTPExceptions.GONE.with_response(index(request))` example.
#### `ExceptionHandlerMiddleware`


#### `ExceptionHandlerMiddleware`

Just there for to exception handling to work.
#### `ThreadLocalRequestMiddleware`


#### `ThreadLocalRequestMiddleware`

Just there for to `get_current_request` to work.


Expand All @@ -134,7 +134,7 @@ class Subscribe(TemplateView):
template = SUBSCRIBE_TEMPLATE
````


## Avaliable Exceptions
```py
HTTPExceptions.CONTINUE # HTTPExceptions.from_status(100)
Expand Down
13 changes: 6 additions & 7 deletions django_http_exceptions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from ._exceptions import * # Not adding this to __all__, there are 57 variables here
from .exceptions import HTTPExceptions
from ._exceptions import * # Not adding this to __all__, there are 57 variables here
from .middleware import (get_current_request, ExceptionHandlerMiddleware,
ThreadLocalRequestMiddleware)
from .middleware import ExceptionHandlerMiddleware, ThreadLocalRequestMiddleware, get_current_request

__all__ = [
'HTTPExceptions',
'ExceptionHandlerMiddleware',
'ThreadLocalRequestMiddleware',
'get_current_request',
"HTTPExceptions",
"ExceptionHandlerMiddleware",
"ThreadLocalRequestMiddleware",
"get_current_request",
]
65 changes: 25 additions & 40 deletions django_http_exceptions/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,19 @@

def _is_dunder(name):
"""Returns True if a __dunder__ name, False otherwise."""
return (name[:2] == name[-2:] == '__' and
name[2:3] != '_' and
name[-3:-2] != '_' and
len(name) > 4)
return name[:2] == name[-2:] == "__" and name[2:3] != "_" and name[-3:-2] != "_" and len(name) > 4


class transform(type):
"""A metaclass to help automatically apply a function to all 3rd party members of a class
@DynamicAttrs."""
"""A metaclass to help automatically apply a function to all 3rd party members of a class"""

def __setattr__(self, key, value):
return super().__setattr__(key, self.__transform__(value))

@staticmethod
def __raise_on_new(klass):
"""We don't want our transform types to be initilizable, they are only there to group
together similiar items."""

def __new__(cls, *a, **kw):
raise TypeError('{} can not be initilazed.'.format(klass))
raise TypeError("{} can not be initilazed.".format(klass))

return __new__

Expand All @@ -38,22 +30,15 @@ def __noop_checks(key, value, classdict):
return True

def __new__(mcs, cls, bases, classdict):
_transform = classdict.get('__transform__', mcs.__noop_transform)
_checks = classdict.get('__checks__', mcs.__noop_checks)
return type.__new__(
mcs,
cls,
bases,
{
**{k: _transform(k, v, classdict) if _checks(k, v, classdict) else v
for k, v in classdict.items()},
'__new__': mcs.__raise_on_new(cls)
}
)
c = classdict
_transform = c.get("__transform__", mcs.__noop_transform)
_checks = c.get("__checks__", mcs.__noop_checks)
transformed_classdict = {k: _transform(k, v, c) if _checks(k, v, c) else v for k, v in c.items()}
new_classdict = {**transformed_classdict, "__new__": mcs.__raise_on_new(cls)}
return type.__new__(mcs, cls, bases, new_classdict)


class HTTPException(Exception):
"""@DynamicAttrs."""
_error_handlers = []

@classmethod
Expand Down Expand Up @@ -90,7 +75,7 @@ def remove_error_handler(cls, handler):

@classmethod
def _has_default_view(cls):
return hasattr(cls, '_default_view')
return hasattr(cls, "_default_view")

@classmethod
def _get_default_view_response(cls, request):
Expand All @@ -104,31 +89,31 @@ def __call__(self, *args):


class HTTPExceptions(metaclass=transform):
"""@DynamicAttrs.""" # PycharmDisableInspection
BASE_EXCEPTION = HTTPException
encapsulated = ['exceptions', 'register_base_exception']
encapsulated = ["exceptions", "register_base_exception"]
exceptions = []

def __transform__(key, value, classdict):
base_exception = classdict.get('BASE_EXCEPTION') or HTTPException
base_exception = classdict.get("BASE_EXCEPTION") or HTTPException
if not issubclass(base_exception, HTTPException):
raise TypeError('BASE_EXCEPTION must be a subclass of HTTPException.')
raise TypeError("BASE_EXCEPTION must be a subclass of HTTPException.")
return type(
value.name,
(base_exception,),
{'__module__': 'HTTPExceptions', 'status': value.value,
'description': value.description}
{"__module__": "HTTPExceptions", "status": value.value, "description": value.description},
)

def __checks__(key, value, classdict):
return all((
not _is_dunder(key),
not callable(value),
not isinstance(value, Exception),
not isinstance(value, classmethod),
key != 'encapsulated',
key not in classdict.get('encapsulated', []),
))
return all(
(
not _is_dunder(key),
not callable(value),
not isinstance(value, Exception),
not isinstance(value, classmethod),
key != "encapsulated",
key not in classdict.get("encapsulated", []),
)
)

@classmethod
def from_status(cls, code):
Expand All @@ -139,7 +124,7 @@ def from_status(cls, code):
def register_base_exception(cls, new_exception):
for exception in cls.exceptions:
if not issubclass(new_exception, HTTPException):
raise TypeError('New exception must be a subclass of HTTPException.')
raise TypeError("New exception must be a subclass of HTTPException.")
getattr(cls, exception).__bases__ = (new_exception,)

# Add all possible HTTP status codes from http.HTTPStatus
Expand Down
9 changes: 4 additions & 5 deletions django_http_exceptions/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def process_exception(request, exc):
if isinstance(exc, HTTPExceptions.BASE_EXCEPTION):
for handler in exc._error_handlers:
handler(request, exc)
response = getattr(exc, 'response', None)
response = getattr(exc, "response", None)
if not response and exc._has_default_view():
response = exc._get_default_view_response(request)
if not response:
Expand All @@ -24,22 +24,21 @@ def process_exception(request, exc):


def get_current_request():
return getattr(_thread_locals, 'request', None)
return getattr(_thread_locals, "request", None)


class ThreadLocalRequestMiddleware(MiddlewareMixin):

@staticmethod
def process_request(request):
_thread_locals.request = request

@staticmethod
def process_response(request, response):
if hasattr(_thread_locals, 'request'):
if hasattr(_thread_locals, "request"):
del _thread_locals.request
return response

@staticmethod
def process_exception(request, exception):
if hasattr(_thread_locals, 'request'):
if hasattr(_thread_locals, "request"):
del _thread_locals.request
2 changes: 1 addition & 1 deletion django_http_exceptions/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def decorator(view):


def _errorify_class(cls, error):
return method_decorator(functools.partial(_errorify_function, error=error), name='dispatch')(cls)
return method_decorator(functools.partial(_errorify_function, error=error), name="dispatch")(cls)


def _errorify_function(f, error):
Expand Down
Loading

0 comments on commit 60fe7bd

Please sign in to comment.