diff --git a/django_readonly_field/apps.py b/django_readonly_field/apps.py index f84b459..ba630ef 100644 --- a/django_readonly_field/apps.py +++ b/django_readonly_field/apps.py @@ -2,11 +2,10 @@ class Readonly(AppConfig): - name = 'django_readonly_field' + name = "django_readonly_field" def ready(self): - from django.db import connections - from django.db import utils + from django.db import connections, utils readonly_compiler_module = "django_readonly_field.compiler" @@ -19,7 +18,7 @@ def ready(self): def custom_load_backend(*args, **kwargs): backend = original_load_backend(*args, **kwargs) - class ReadOnlyBackend(object): + class ReadOnlyBackend: @staticmethod def DatabaseWrapper(*args2, **kwargs2): connection = backend.DatabaseWrapper(*args2, **kwargs2) diff --git a/django_readonly_field/compiler.py b/django_readonly_field/compiler.py index ae97b61..f16df80 100644 --- a/django_readonly_field/compiler.py +++ b/django_readonly_field/compiler.py @@ -1,16 +1,21 @@ -from django.db.models.sql.compiler import SQLCompiler -from django.db.models.sql.compiler import SQLInsertCompiler as BaseSQLInsertCompiler # noqa -from django.db.models.sql.compiler import SQLDeleteCompiler -from django.db.models.sql.compiler import SQLUpdateCompiler as BaseSQLUpdateCompiler # noqa -from django.db.models.sql.compiler import SQLAggregateCompiler +from django.db.models.sql.compiler import ( + SQLAggregateCompiler, + SQLCompiler, + SQLDeleteCompiler, +) +from django.db.models.sql.compiler import ( # noqa + SQLInsertCompiler as BaseSQLInsertCompiler, +) +from django.db.models.sql.compiler import ( # noqa + SQLUpdateCompiler as BaseSQLUpdateCompiler, +) SQLCompiler = SQLCompiler SQLDeleteCompiler = SQLDeleteCompiler SQLAggregateCompiler = SQLAggregateCompiler -class ReadonlySQLCompilerMixin(object): - +class ReadonlySQLCompilerMixin: @property def readonly_field_names(self): try: @@ -21,18 +26,18 @@ def readonly_field_names(self): fields = getattr(readonly_meta, "_cached_readonly", None) if not fields: readonly_meta._cached_readonly = fields = frozenset( - getattr(readonly_meta, "readonly", ())) + getattr(readonly_meta, "readonly", ()) + ) return fields def as_sql(self): readonly_field_names = self.readonly_field_names if readonly_field_names: self.remove_readonly_fields(readonly_field_names) - return super(ReadonlySQLCompilerMixin, self).as_sql() + return super().as_sql() class SQLUpdateCompiler(ReadonlySQLCompilerMixin, BaseSQLUpdateCompiler): - def remove_readonly_fields(self, readonly_field_names): """ Remove the values from the query which correspond to a @@ -42,13 +47,13 @@ def remove_readonly_fields(self, readonly_field_names): # The tuple is (field, model, value) where model if used for FKs. values[:] = ( - (field, _, __) for (field, _, __) in values + (field, _, __) + for (field, _, __) in values if field.name not in readonly_field_names ) class SQLInsertCompiler(ReadonlySQLCompilerMixin, BaseSQLInsertCompiler): - def _exclude_readonly_fields(self, fields, readonly_field_names): for field in fields: if field.name not in readonly_field_names: @@ -62,8 +67,7 @@ def remove_readonly_fields(self, readonly_field_names): fields = self.query.fields try: - fields[:] = self._exclude_readonly_fields( - fields, readonly_field_names) + fields[:] = self._exclude_readonly_fields(fields, readonly_field_names) except AttributeError: # When deserializing, we might get an attribute error because this # list shoud be copied first : @@ -72,5 +76,6 @@ def remove_readonly_fields(self, readonly_field_names): # should never be mutated. If you want to manipulate this list for # your own use, make a copy first." - self.query.fields = list(self._exclude_readonly_fields( - fields, readonly_field_names)) + self.query.fields = list( + self._exclude_readonly_fields(fields, readonly_field_names) + ) diff --git a/docs/conf.py b/docs/conf.py index 22428bd..0f9c04c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # complexity documentation build configuration file, created by # sphinx-quickstart on Tue Jul 9 22:26:36 2013. @@ -11,12 +10,13 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import os +import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) cwd = os.getcwd() parent = os.path.dirname(cwd) @@ -27,27 +27,27 @@ # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'Django Readonly Field' -copyright = u'2016, Joachim Jablon, PeopleDoc' +project = "Django Readonly Field" +copyright = "2016, Joachim Jablon, PeopleDoc" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -60,71 +60,71 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'alabaster' +html_theme = "alabaster" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -133,44 +133,44 @@ # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'django-readonly-fielddoc' +htmlhelp_basename = "django-readonly-fielddoc" diff --git a/tests/readonly_app/migrations/0001_initial.py b/tests/readonly_app/migrations/0001_initial.py index 3a3e514..e1858fa 100644 --- a/tests/readonly_app/migrations/0001_initial.py +++ b/tests/readonly_app/migrations/0001_initial.py @@ -2,34 +2,56 @@ class Migration(migrations.Migration): - - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Car', + name="Car", fields=[ - ('id', models.BigAutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('wheel_number', models.IntegerField()), - ('manufacturer', models.CharField(max_length=100)), + ( + "id", + models.BigAutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("wheel_number", models.IntegerField()), + ("manufacturer", models.CharField(max_length=100)), ], ), migrations.CreateModel( - name='Book', + name="Book", fields=[ - ('id', models.BigAutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('ref', models.IntegerField()), - ('iban', models.CharField(max_length=100)), - ('name', models.CharField(max_length=250)), + ( + "id", + models.BigAutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("ref", models.IntegerField()), + ("iban", models.CharField(max_length=100)), + ("name", models.CharField(max_length=250)), ], ), migrations.CreateModel( - name='Bus', + name="Bus", fields=[ - ('id', models.BigAutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('wheel_number', models.IntegerField()), - ('manufacturer', models.CharField(max_length=100)), + ( + "id", + models.BigAutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("wheel_number", models.IntegerField()), + ("manufacturer", models.CharField(max_length=100)), ], ), ] diff --git a/tests/readonly_app/migrations/0002_sql_default.py b/tests/readonly_app/migrations/0002_sql_default.py index 7728677..f3cb023 100644 --- a/tests/readonly_app/migrations/0002_sql_default.py +++ b/tests/readonly_app/migrations/0002_sql_default.py @@ -2,22 +2,21 @@ class Migration(migrations.Migration): - dependencies = [ - ('readonly_app', '0001_initial'), + ("readonly_app", "0001_initial"), ] operations = [ - migrations.RunSQL([ - '''ALTER TABLE readonly_app_car ''' - '''ALTER COLUMN manufacturer SET DEFAULT 'Renault';''', - - '''ALTER TABLE readonly_app_bus ''' - '''ALTER COLUMN wheel_number SET DEFAULT 22;''' - - '''ALTER TABLE readonly_app_book ''' - '''ALTER COLUMN iban SET DEFAULT '1234-abcd';''', - - '''ALTER TABLE readonly_app_book ''' - '''ALTER COLUMN ref SET DEFAULT 123456789;''']) + migrations.RunSQL( + [ + """ALTER TABLE readonly_app_car """ + """ALTER COLUMN manufacturer SET DEFAULT 'Renault';""", + """ALTER TABLE readonly_app_bus """ + """ALTER COLUMN wheel_number SET DEFAULT 22;""" + """ALTER TABLE readonly_app_book """ + """ALTER COLUMN iban SET DEFAULT '1234-abcd';""", + """ALTER TABLE readonly_app_book """ + """ALTER COLUMN ref SET DEFAULT 123456789;""", + ] + ) ] diff --git a/tests/readonly_app/models.py b/tests/readonly_app/models.py index 1f5bc86..49677e9 100644 --- a/tests/readonly_app/models.py +++ b/tests/readonly_app/models.py @@ -2,13 +2,11 @@ class Car(models.Model): - wheel_number = models.IntegerField() manufacturer = models.CharField(max_length=100) def __str__(self): - return "{car.manufacturer} Car with {car.wheel_number} wheels".format( - car=self) + return "{car.manufacturer} Car with {car.wheel_number} wheels".format(car=self) class ReadonlyMeta: readonly = ["manufacturer"] @@ -18,6 +16,7 @@ class Book(models.Model): """ A completely different model """ + ref = models.IntegerField() iban = models.CharField(max_length=100) name = models.CharField(max_length=250) @@ -39,8 +38,7 @@ class Bus(models.Model): manufacturer = models.CharField(max_length=100) def __str__(self): - return "{car.manufacturer} Bus with {car.wheel_number} wheels".format( - car=self) + return "{car.manufacturer} Bus with {car.wheel_number} wheels".format(car=self) class ReadonlyMeta: readonly = ["wheel_number"] diff --git a/tests/readonly_app/views.py b/tests/readonly_app/views.py index 25ccb40..5caccd0 100644 --- a/tests/readonly_app/views.py +++ b/tests/readonly_app/views.py @@ -10,8 +10,8 @@ def test_view(request): valid = car.manufacturer == "Renault" return http.HttpResponse( - content_type="text/plain", - content="OK" if valid else "Fail") + content_type="text/plain", content="OK" if valid else "Fail" + ) urlpatterns = [path("", test_view)] diff --git a/tests/test_readonly.py b/tests/test_readonly.py index 561522e..f32dbee 100644 --- a/tests/test_readonly.py +++ b/tests/test_readonly.py @@ -1,18 +1,14 @@ -from contextlib import contextmanager import json +from contextlib import contextmanager import requests - -from django.test import TestCase -from django.test import LiveServerTestCase +from django.core import serializers from django.db import connection +from django.test import LiveServerTestCase, TestCase from django.test.utils import CaptureQueriesContext -from django.core import serializers # Create your tests here. -from tests.readonly_app.models import Car -from tests.readonly_app.models import Bus -from tests.readonly_app.models import Book +from tests.readonly_app.models import Book, Bus, Car class ReadonlyFieldTest(TestCase):