diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4ee08d36..45fcabef 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,39 +1,21 @@ repos: - - repo: https://github.com/psf/black - rev: 24.10.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.0 hooks: - - id: black - language_version: python3.11 - - - repo: https://github.com/asottile/pyupgrade - rev: v3.19.0 - hooks: - - id: pyupgrade - args: [--py39-plus] + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format + types_or: [ python, pyi ] - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks rev: v2.14.0 hooks: - id: pretty-format-toml args: [--autofix] - - repo: https://github.com/pycqa/isort - rev: 5.13.2 - hooks: - - id: isort - - - repo: https://github.com/PyCQA/flake8 - rev: 7.1.1 - hooks: - - id: flake8 - repo: https://github.com/adamchainz/django-upgrade rev: "1.22.1" hooks: - id: django-upgrade args: [--target-version, "4.2"] - - repo: https://github.com/asottile/pyupgrade - rev: v3.19.0 - hooks: - - id: pyupgrade - args: [--py39-plus] diff --git a/django_tables2/columns/base.py b/django_tables2/columns/base.py index d8298fb3..7f8631a8 100644 --- a/django_tables2/columns/base.py +++ b/django_tables2/columns/base.py @@ -62,15 +62,15 @@ def column_for_field(self, field, **kwargs): class LinkTransform: - """Object used to generate attributes for the ``-tag to wrap the cell content in.""" - viewname = None accessor = None attrs = None def __init__(self, url=None, accessor=None, attrs=None, reverse_args=None): """ - arguments: + Object used to generate attributes for the ``-tag to wrap the cell content in. + + Arguments: url (callable): If supplied, the result of this callable will be used as ``href`` attribute. accessor (Accessor): if supplied, the accessor will be used to decide on which object ``get_absolute_url()`` is called. @@ -120,9 +120,7 @@ def compose_url(self, **kwargs): return context.get_absolute_url() def call_reverse(self, record): - """ - Prepares the arguments to reverse() for this record and calls reverse() - """ + """Prepare the arguments to reverse() for this record and calls reverse().""" def resolve_if_accessor(val): return val.resolve(record) if isinstance(val, Accessor) else val @@ -393,7 +391,7 @@ def order(self, queryset, is_descending): table or by subclassing `.Column`; but only overrides if second element in return tuple is True. - returns: + Returns: Tuple (QuerySet, boolean) """ return (queryset, False) @@ -404,7 +402,9 @@ def from_field(cls, field, **kwargs): Return a specialized column for the model field or `None`. Arguments: - field (Model Field instance): the field that needs a suitable column + field (Model Field instance): the field that needs a suitable column. + **kwargs: passed on to the column. + Returns: `.Column` object or `None` @@ -429,7 +429,7 @@ class BoundColumn: In practice, this means that a `.BoundColumn` knows the *"variable name"* given to the `.Column` when it was declared on the `.Table`. - arguments: + Arguments: table (`~.Table`): The table in which this column exists column (`~.Column`): The type of column name (str): The variable name of the column used when defining the @@ -465,7 +465,6 @@ def attrs(self): templates easier. ``tf`` is not actually a HTML tag, but this key name will be used for attributes for column's footer, if the column has one. """ - # prepare kwargs for computed_values() kwargs = {"table": self._table, "bound_column": self} # BoundRow.items() sets current_record and current_value when iterating over @@ -503,25 +502,19 @@ def attrs(self): return attrs def _get_cell_class(self, attrs): - """ - Return a set of the classes from the class key in ``attrs``. - """ + """Return a set of the classes from the class key in ``attrs``.""" classes = attrs.get("class", None) classes = set() if classes is None else set(classes.split(" ")) return self._table.get_column_class_names(classes, self) def get_td_class(self, td_attrs): - """ - Returns the HTML class attribute for a data cell in this column - """ + """Return the HTML class attribute for a data cell in this column.""" classes = sorted(self._get_cell_class(td_attrs)) return None if len(classes) == 0 else " ".join(classes) def get_th_class(self, th_attrs): - """ - Returns the HTML class attribute for a header cell in this column - """ + """Return the HTML class attribute for a header cell in this column.""" classes = self._get_cell_class(th_attrs) # add classes for ordering @@ -539,7 +532,7 @@ def get_th_class(self, th_attrs): @property def default(self): - """Returns the default value for this column.""" + """Return the default value for this column.""" value = self.column.default if value is None: value = self._table.default @@ -698,7 +691,7 @@ def visible(self): @property def localize(self): - """Return `True`, `False` or `None` as described in ``Column.localize``""" + """Return `True`, `False` or `None` as described in ``Column.localize``.""" return self.column.localize @@ -742,10 +735,7 @@ def names(self): return list(self.iternames()) def iterall(self): - """ - Return an iterator that exposes all `.BoundColumn` objects, - regardless of visibility or sortability. - """ + """Return an iterator that exposes all `.BoundColumn` objects, regardless of visibility or sortability.""" return (column for name, column in self.iteritems()) def all(self): @@ -759,7 +749,6 @@ def iteritems(self): consideration all of the ordering and filtering modifiers that a table supports (e.g. `~Table.Meta.exclude` and `~Table.Meta.sequence`). """ - for name in self._table.sequence: if name not in self._table.exclude: yield (name, self.columns[name]) @@ -769,7 +758,7 @@ def items(self): def iterorderable(self): """ - Same as `BoundColumns.all` but only returns orderable columns. + `BoundColumns.all` filtered for whether they can be ordered. This is useful in templates, where iterating over the full set and checking ``{% if column.ordarable %}`` can be problematic in @@ -780,7 +769,7 @@ def iterorderable(self): def itervisible(self): """ - Same as `.iterorderable` but only returns visible `.BoundColumn` objects. + Return `.iterorderable` filtered by visibility. This is geared towards table rendering. """ @@ -805,7 +794,7 @@ def show(self, name): self.columns[name].column.visible = True def __iter__(self): - """Convenience API, alias of `.itervisible`.""" + """Alias of `.itervisible` (for convenience).""" return self.itervisible() def __contains__(self, item): diff --git a/django_tables2/columns/booleancolumn.py b/django_tables2/columns/booleancolumn.py index d3bcbb28..93cee5e0 100644 --- a/django_tables2/columns/booleancolumn.py +++ b/django_tables2/columns/booleancolumn.py @@ -52,9 +52,7 @@ def render(self, value, record, bound_column): return format_html("{}", AttributeDict(attrs).as_html(), escape(text)) def value(self, record, value, bound_column): - """ - Returns the content for a specific cell similarly to `.render` however without any html content. - """ + """Return the content for a specific cell similarly to `.render` however without any html content.""" value = self._get_bool_value(record, value, bound_column) return str(value) diff --git a/django_tables2/columns/checkboxcolumn.py b/django_tables2/columns/checkboxcolumn.py index caecb7f9..767b9037 100644 --- a/django_tables2/columns/checkboxcolumn.py +++ b/django_tables2/columns/checkboxcolumn.py @@ -71,9 +71,7 @@ def render(self, value, bound_column, record): return mark_safe(f"") def is_checked(self, value, record): - """ - Determine if the checkbox should be checked - """ + """Determine if the checkbox should be checked.""" if self.checked is None: return False if self.checked is True: diff --git a/django_tables2/columns/datecolumn.py b/django_tables2/columns/datecolumn.py index ef500115..d9942ba6 100644 --- a/django_tables2/columns/datecolumn.py +++ b/django_tables2/columns/datecolumn.py @@ -19,7 +19,7 @@ class DateColumn(TemplateColumn): def __init__(self, format=None, short=True, *args, **kwargs): if format is None: format = "SHORT_DATE_FORMAT" if short else "DATE_FORMAT" - template = '{{ value|date:"%s"|default:default }}' % format + template = '{{ value|date:"%s"|default:default }}' % format # noqa: UP031 super().__init__(template_code=template, *args, **kwargs) @classmethod diff --git a/django_tables2/columns/datetimecolumn.py b/django_tables2/columns/datetimecolumn.py index 097735e5..2602c951 100644 --- a/django_tables2/columns/datetimecolumn.py +++ b/django_tables2/columns/datetimecolumn.py @@ -19,7 +19,7 @@ class DateTimeColumn(TemplateColumn): def __init__(self, format=None, short=True, *args, **kwargs): if format is None: format = "SHORT_DATETIME_FORMAT" if short else "DATETIME_FORMAT" - template = '{{ value|date:"%s"|default:default }}' % format + template = '{{ value|date:"%s"|default:default }}' % format # noqa: UP031 super().__init__(template_code=template, *args, **kwargs) @classmethod diff --git a/django_tables2/columns/linkcolumn.py b/django_tables2/columns/linkcolumn.py index 54685def..4f627f65 100644 --- a/django_tables2/columns/linkcolumn.py +++ b/django_tables2/columns/linkcolumn.py @@ -25,10 +25,7 @@ def text_value(self, record, value): return self.text(record) if callable(self.text) else self.text def value(self, record, value): - """ - Returns the content for a specific cell similarly to `.render` however - without any html content. - """ + """Return the content for a specific cell similarly to `.render` without any HTML content.""" return self.text_value(record, value) def render(self, record, value): @@ -38,7 +35,7 @@ def render(self, record, value): @library.register class LinkColumn(BaseLinkColumn): """ - Renders a normal value as an internal hyperlink to another page. + Render a normal value as an internal hyperlink to another page. .. note :: diff --git a/django_tables2/columns/manytomanycolumn.py b/django_tables2/columns/manytomanycolumn.py index ba9c0240..825d6912 100644 --- a/django_tables2/columns/manytomanycolumn.py +++ b/django_tables2/columns/manytomanycolumn.py @@ -9,7 +9,7 @@ @library.register class ManyToManyColumn(Column): """ - Display the list of objects from a `ManyRelatedManager` + Display the list of objects from a `ManyRelatedManager`. Ordering is disabled for this column. @@ -72,16 +72,11 @@ def __init__( self.linkify_item = LinkTransform(attrs=self.attrs.get("a", {}), **link_kwargs) def transform(self, obj): - """ - Transform is applied to each item of the list of objects from the ManyToMany relation. - """ + """Apply to each item of the list of objects from the ManyToMany relation.""" return force_str(obj) def filter(self, qs): - """ - Filter is called on the ManyRelatedManager to allow ordering, filtering or limiting - on the set of related objects. - """ + """Call on the ManyRelatedManager to allow ordering, filtering or limiting on the set of related objects.""" return qs.all() def render(self, value): diff --git a/django_tables2/columns/templatecolumn.py b/django_tables2/columns/templatecolumn.py index ddddc24a..d5984b0b 100644 --- a/django_tables2/columns/templatecolumn.py +++ b/django_tables2/columns/templatecolumn.py @@ -8,8 +8,7 @@ @library.register class TemplateColumn(Column): """ - A subclass of `.Column` that renders some template code to use as - the cell value. + A subclass of `.Column` that renders some template code to use as the cell value. Arguments: template_code (str): template code to render @@ -69,8 +68,9 @@ def render(self, record, table, value, bound_column, **kwargs): def value(self, **kwargs): """ - The value returned from a call to `value()` on a `TemplateColumn` is - the rendered template with `django.utils.html.strip_tags` applied. + Non-HTML value returned from a call to `value()` on a `TemplateColumn`. + + By default this is the rendered template with `django.utils.html.strip_tags` applied. Leading and trailing whitespace is stripped. """ html = super().value(**kwargs) diff --git a/django_tables2/columns/timecolumn.py b/django_tables2/columns/timecolumn.py index b4564fc2..c3e4aff6 100644 --- a/django_tables2/columns/timecolumn.py +++ b/django_tables2/columns/timecolumn.py @@ -17,7 +17,7 @@ class TimeColumn(TemplateColumn): def __init__(self, format=None, *args, **kwargs): if format is None: format = "TIME_FORMAT" - template = '{{ value|date:"%s"|default:default }}' % format + template = '{{ value|date:"%s"|default:default }}' % format # noqa: UP031 super().__init__(template_code=template, *args, **kwargs) @classmethod diff --git a/django_tables2/data.py b/django_tables2/data.py index c4900c63..a28ba2d9 100644 --- a/django_tables2/data.py +++ b/django_tables2/data.py @@ -6,33 +6,29 @@ class TableData: - """ - Base class for table data containers. - """ + """Base class for table data containers.""" def __init__(self, data): self.data = data def __getitem__(self, key): - """ - Slicing returns a new `.TableData` instance, indexing returns a single record. - """ + """Slicing returns a new `.TableData` instance, indexing returns a single record.""" return self.data[key] def __iter__(self): """ - for ... in ... default to using this. There's a bug in Django 1.3 - with indexing into QuerySets, so this side-steps that problem (as well + Iterate over the table data (for ... in ... defaults to using this). + + There's a bug in Django 1.3 with indexing into QuerySets, so this side-steps that problem (as well as just being a better way to iterate). """ return iter(self.data) def set_table(self, table): """ - `Table.__init__` calls this method to inject an instance of itself into the - `TableData` instance. - Good place to do additional checks if Table and TableData instance will work - together properly. + `Table.__init__` calls this method to inject an instance of itself into the `TableData` instance. + + Good place to do additional checks if Table and TableData instance will work together properly. """ self.table = table @@ -70,24 +66,22 @@ def from_data(data): class TableListData(TableData): """ - Table data container for a list of dicts, for example:: + Table data container for a list of dicts. - [ - {'name': 'John', 'age': 20}, - {'name': 'Brian', 'age': 25} - ] + For example:: + [ + {'name': 'John', 'age': 20}, + {'name': 'Brian', 'age': 25} + ] .. note:: - Other structures might have worked in the past, but are not explicitly - supported or tested. + Other structures might have worked in the past, but are not explicitly supported or tested. """ @staticmethod def validate(data): - """ - Validates `data` for use in this container - """ + """Validate `data` for use in this container.""" return hasattr(data, "__iter__") or ( hasattr(data, "__len__") and hasattr(data, "__getitem__") ) @@ -105,8 +99,7 @@ def verbose_name_plural(self): def order_by(self, aliases): """ - Order the data based on order by aliases (prefixed column names) in the - table. + Order the data based on order by aliases (prefixed column names) in the table. Arguments: aliases (`~.utils.OrderByTuple`): optionally prefixed names of @@ -129,15 +122,11 @@ def order_by(self, aliases): class TableQuerysetData(TableData): - """ - Table data container for a queryset. - """ + """Table data container for a QuerySet.""" @staticmethod def validate(data): - """ - Validates `data` for use in this container - """ + """Validate `data` for use in this container.""" return ( hasattr(data, "count") and callable(data.count) @@ -146,7 +135,7 @@ def validate(data): ) def __len__(self): - """Cached data length""" + """Length of the data (cached).""" if not hasattr(self, "_length") or self._length is None: if hasattr(self.table, "paginator"): # for paginated tables, use QuerySet.count() as we are interested in total number of records. @@ -167,8 +156,7 @@ def set_table(self, table): @property def ordering(self): """ - Returns the list of order by aliases that are enforcing ordering on the - data. + Return the list of order by aliases that are enforcing ordering on the data. If the data is unordered, an empty sequence is returned. If the ordering can not be determined, `None` is returned. @@ -176,7 +164,6 @@ def ordering(self): This works by inspecting the actual underlying data. As such it's only supported for querysets. """ - aliases = {} for bound_column in self.table.columns: aliases[bound_column.order_by_alias] = bound_column.order_by @@ -187,8 +174,7 @@ def ordering(self): def order_by(self, aliases): """ - Order the data based on order by aliases (prefixed column names) in the - table. + Order the data based on order by aliases (prefixed column names) in the table. Arguments: aliases (`~.utils.OrderByTuple`): optionally prefixed names of @@ -226,7 +212,7 @@ def order_by(self, aliases): @cached_property def verbose_name(self): """ - The full (singular) name for the data. + Full (singular) name for the data. Model's `~django.db.Model.Meta.verbose_name` is honored. """ @@ -235,7 +221,7 @@ def verbose_name(self): @cached_property def verbose_name_plural(self): """ - The full (plural) name for the data. + Full (plural) name for the data. Model's `~django.db.Model.Meta.verbose_name` is honored. """ diff --git a/django_tables2/export/export.py b/django_tables2/export/export.py index 49bda338..6e2204fe 100644 --- a/django_tables2/export/export.py +++ b/django_tables2/export/export.py @@ -72,26 +72,20 @@ def default_dataset_title(): @classmethod def is_valid_format(self, export_format): - """ - Returns true if `export_format` is one of the supported export formats - """ + """Return True if `export_format` is one of the supported export formats.""" return export_format is not None and export_format in TableExport.FORMATS.keys() def content_type(self): - """ - Returns the content type for the current export format - """ + """Return the content type for the current export format.""" return self.FORMATS[self.format] def export(self): - """ - Returns the string/bytes for the current export format - """ + """Return the string/bytes for the current export format.""" return self.dataset.export(self.format) def response(self, filename=None): """ - Builds and returns a `HttpResponse` containing the exported data + Build and return a `HttpResponse` containing the exported data. Arguments: filename (str): if not `None`, the filename is attached to the diff --git a/django_tables2/rows.py b/django_tables2/rows.py index de5d0c1c..9e2684a0 100644 --- a/django_tables2/rows.py +++ b/django_tables2/rows.py @@ -7,9 +7,7 @@ class CellAccessor: - """ - Allows accessing cell contents on a row object (see `BoundRow`) - """ + """Access cell contents on a row object (see `BoundRow`).""" def __init__(self, row): self.row = row @@ -173,11 +171,7 @@ def _get_and_render_with(self, bound_column, render_func, default): return render_func(bound_column, value) def _optional_cell_arguments(self, bound_column, value): - """ - Defines the arguments that will optionally be passed while calling the - cell's rendering or value getter if that function has one of these as a - keyword argument. - """ + """Arguments that will optionally be passed while rendering cells.""" return { "value": value, "record": self.record, @@ -188,10 +182,7 @@ def _optional_cell_arguments(self, bound_column, value): } def get_cell(self, name): - """ - Returns the final rendered html for a cell in the row, given the name - of a column. - """ + """Return the final rendered html for a cell in the row, given the name of a column.""" bound_column = self.table.columns[name] return self._get_and_render_with( @@ -199,40 +190,31 @@ def get_cell(self, name): ) def _call_render(self, bound_column, value=None): - """ - Call the column's render method with appropriate kwargs - """ + """Call the column's render method with appropriate kwargs.""" render_kwargs = self._optional_cell_arguments(bound_column, value) content = call_with_appropriate(bound_column.render, render_kwargs) return bound_column.link(content, **render_kwargs) if bound_column.link else content def get_cell_value(self, name): - """ - Returns the final rendered value (excluding any html) for a cell in the - row, given the name of a column. - """ + """Return the final rendered value (excluding any html) for a cell in the row, given the name of a column.""" return self._get_and_render_with( self.table.columns[name], render_func=self._call_value, default=None ) def _call_value(self, bound_column, value=None): - """ - Call the column's value method with appropriate kwargs - """ + """Call the column's value method with appropriate kwargs.""" return call_with_appropriate( bound_column.value, self._optional_cell_arguments(bound_column, value) ) def __contains__(self, item): - """ - Check by both row object and column name. - """ + """Check by both row object and column name.""" return item in (self.table.columns if isinstance(item, str) else self) def items(self): """ - Returns iterator yielding ``(bound_column, cell)`` pairs. + Return an iterator yielding ``(bound_column, cell)`` pairs. *cell* is ``row[name]`` -- the rendered unicode value that should be ``rendered within ````. @@ -247,14 +229,13 @@ def items(self): class BoundPinnedRow(BoundRow): - """ - Represents a *pinned* row in a table. - """ + """A *pinned* row in a table.""" @property def attrs(self): """ Return the attributes for a certain pinned row. + Add CSS classes `pinned-row` and `odd` or `even` to `class` attribute. Return: @@ -328,10 +309,7 @@ def __len__(self): return length def __getitem__(self, key): - """ - Slicing returns a new `~.BoundRows` instance, indexing returns a single - `~.BoundRow` instance. - """ + """Return a new `~.BoundRows` instance for a slice, a single `~.BoundRow` instance for an index.""" if isinstance(key, slice): return BoundRows(data=self.data[key], table=self.table, pinned_data=self.pinned_data) else: diff --git a/django_tables2/tables.py b/django_tables2/tables.py index 10dea6e6..8336439f 100644 --- a/django_tables2/tables.py +++ b/django_tables2/tables.py @@ -154,9 +154,7 @@ def __init__(self, options, class_name): self.unlocalize = getattr(options, "unlocalize", ()) def _check_types(self, options, class_name): - """ - Check class Meta attributes to prevent common mistakes. - """ + """Check class Meta attributes to prevent common mistakes.""" if options is None: return @@ -372,9 +370,9 @@ def __init__( def get_top_pinned_data(self): """ Return data for top pinned rows containing data for each row. + Iterable type like: QuerySet, list of dicts, list of objects. - Having a non-zero number of pinned rows - will not result in an empty result set message being rendered, + Having a non-zero number of pinned rows will not result in an empty result set message being rendered, even if there are no regular data rows Returns: @@ -396,9 +394,9 @@ def get_top_pinned_data(self): def get_bottom_pinned_data(self): """ Return data for bottom pinned rows containing data for each row. + Iterable type like: QuerySet, list of dicts, list of objects. - Having a non-zero number of pinned rows - will not result in an empty result set message being rendered, + Having a non-zero number of pinned rows will not result in an empty result set message being rendered, even if there are no regular data rows Returns: @@ -419,7 +417,7 @@ def get_bottom_pinned_data(self): def before_render(self, request): """ - A way to hook into the moment just before rendering the template. + Perform an action just before rendering the template. Can be used to hide a column. @@ -443,9 +441,7 @@ def before_render(self, request): return def as_html(self, request): - """ - Render the table to an HTML table, adding `request` to the context. - """ + """Render the table to an HTML table, adding `request` to the context.""" # reset counter for new rendering self._counter = count() template = get_template(self.template_name) @@ -457,18 +453,15 @@ def as_html(self, request): def as_values(self, exclude_columns=None): """ - Return a row iterator of the data which would be shown in the table where - the first row is the table headers. + Return a row iterator of the data which would be shown in the table where the first row is the table headers. - arguments: + Arguments: exclude_columns (iterable): columns to exclude in the data iterator. - This can be used to output the table data as CSV, excel, for example using the - `~.export.ExportMixin`. + This can be used to output the table data as CSV, excel, for example using the `~.export.ExportMixin`. - If a column is defined using a :ref:`table.render_FOO`, the returned value from - that method is used. If you want to differentiate between the rendered cell - and a value, use a `value_Foo`-method:: + If a column is defined using a :ref:`table.render_FOO`, the returned value from that method is used. + If you want to differentiate between the rendered cell and a value, use a `value_Foo`-method:: class Table(tables.Table): name = tables.Column() @@ -501,10 +494,7 @@ def value_name(self, value): ] def has_footer(self): - """ - Returns True if any of the columns define a ``_footer`` attribute or a - ``render_footer()`` method - """ + """Return True if any of the columns define a ``_footer`` attribute or a ``render_footer()`` method.""" return self.show_footer and any(column.has_footer() for column in self.columns) @property @@ -561,8 +551,7 @@ def page_field(self, value): def paginate(self, paginator_class=Paginator, per_page=None, page=1, *args, **kwargs): """ - Paginates the table using a paginator and creates a ``page`` property - containing information for the current page. + Paginate the table using a paginator and creates a `page` property containing information for the current page. Arguments: paginator_class (`~django.core.paginator.Paginator`): A paginator class to @@ -570,14 +559,12 @@ def paginate(self, paginator_class=Paginator, per_page=None, page=1, *args, **kw per_page (int): Number of records to display on each page. page (int): Page to display. + *args: passed on to the paginator. + **kwargs: passed on to the paginator. - Extra arguments are passed to the paginator. - - Pagination exceptions (`~django.core.paginator.EmptyPage` and - `~django.core.paginator.PageNotAnInteger`) may be raised from this - method and should be handled by the caller. + Pagination exceptions (`~django.core.paginator.EmptyPage` and `~django.core.paginator.PageNotAnInteger`) + may be raised from this method and should be handled by the caller. """ - per_page = per_page or self._meta.per_page self.paginator = paginator_class(self.rows, per_page, *args, **kwargs) self.page = self.paginator.page(page) @@ -649,21 +636,17 @@ def template_name(self, value): @property def paginated_rows(self): - """ - Return the rows for the current page if the table is paginated, else all rows. - """ + """Return the rows for the current page if the table is paginated, else all rows.""" if hasattr(self, "page"): return self.page.object_list return self.rows def get_column_class_names(self, classes_set, bound_column): """ - Returns a set of HTML class names for cells (both ``td`` and ``th``) of a - **bound column** in this table. - By default this returns the column class names defined in the table's - attributes. - This method can be overridden to change the default behavior, for - example to simply `return classes_set`. + Return a set of HTML class names for cells (both ``td`` and ``th``) of a **bound column** in this table. + + By default this returns the column class names defined in the table's attributes. + This method can be overridden to change the default behavior, for example to simply `return classes_set`. Arguments: classes_set(set of string): a set of class names to be added @@ -695,8 +678,10 @@ def get_column_class_names(self, classes_set, bound_column): def table_factory(model, table=Table, fields=None, exclude=None, localize=None): """ - Return Table class for given `model`, equivalent to defining a custom table class:: + Return Table class for given `model`, equivalent to defining a custom table class. + + Example:: class MyTable(tables.Table): class Meta: model = model diff --git a/django_tables2/templatetags/django_tables2.py b/django_tables2/templatetags/django_tables2.py index cfcf5276..8439432c 100644 --- a/django_tables2/templatetags/django_tables2.py +++ b/django_tables2/templatetags/django_tables2.py @@ -26,8 +26,7 @@ def token_kwargs(bits, parser): """ - Based on Django's `~django.template.defaulttags.token_kwargs`, but with a - few changes: + Based on Django's `~django.template.defaulttags.token_kwargs`, but with a few changes. - No legacy mode. - Both keys and values are compiled as a filter @@ -81,9 +80,9 @@ def render(self, context): @register.tag def querystring(parser, token): """ - Creates a URL (containing only the query string [including "?"]) derived - from the current URL's query string, by updating it with the provided - keyword arguments. + Create an URL (containing only the query string [including "?"]) derivedfrom the current URL's query string. + + By updating it with the provided keyword arguments. Example (imagine URL is ``/abc/?gender=male&name=Brad``):: @@ -119,9 +118,12 @@ def querystring(parser, token): class RenderTableNode(Node): """ - parameters: + Node to render a table. + + Parameters: table (~.Table): the table to render template (str or list): Name[s] of template to render + """ def __init__(self, table, template_name=None): @@ -213,8 +215,7 @@ class Meta: @register.simple_tag(takes_context=True) def export_url(context, export_format, export_trigger_param=None): """ - Returns an export URL for the given file `export_format`, preserving current - query string parameters. + Return an export URL for the given file `export_format`, preserving current query string parameters. Example for a page requested with querystring ``?q=blue``:: @@ -224,7 +225,6 @@ def export_url(context, export_format, export_trigger_param=None): ?q=blue&_export=csv """ - if export_trigger_param is None and "view" in context: export_trigger_param = getattr(context["view"], "export_trigger_param", None) @@ -238,7 +238,8 @@ def export_url(context, export_format, export_trigger_param=None): @register.filter def table_page_range(page, paginator): """ - Given an page and paginator, return a list of max 10 (by default) page numbers: + Given an page and paginator, return a list of max 10 (by default) page numbers. + - always containing the first, last and current page. - containing one or two '...' to skip ranges between first/last and current. @@ -247,7 +248,6 @@ def table_page_range(page, paginator): {{ p }} {% endfor %} """ - page_range = getattr(settings, "DJANGO_TABLES2_PAGE_RANGE", 10) num_pages = paginator.num_pages diff --git a/django_tables2/utils.py b/django_tables2/utils.py index 33efabc9..c15aa53a 100644 --- a/django_tables2/utils.py +++ b/django_tables2/utils.py @@ -11,7 +11,7 @@ class Sequence(list): """ - Represents a column sequence, e.g. ``('first_name', '...', 'last_name')`` + A column sequence, e.g. ``('first_name', '...', 'last_name')``. This is used to represent `.Table.Meta.sequence` or the `.Table` constructors's *sequence* keyword argument. @@ -24,15 +24,15 @@ class Sequence(list): def expand(self, columns): """ - Expands the ``'...'`` item in the sequence into the appropriate column - names that should be placed there. + Expand the ``'...'`` item in the sequence into the appropriate column names that should be placed there. - arguments: + Arguments: columns (list): list of column names. - returns: + + Returns: The current instance. - raises: + Raises: `ValueError` if the sequence is invalid for the columns. """ ellipses = self.count("...") @@ -83,11 +83,9 @@ def __new__(cls, value): @property def bare(self): """ - Returns: - `.OrderBy`: the bare form. + Return the bare form, without the direction prefix. - The *bare form* is the non-prefixed form. Typically the bare form is - just the ascending form. + The *bare form* is the non-prefixed form. Typically the bare form is just the ascending form. Example: ``age`` is the bare form of ``-age`` @@ -97,7 +95,7 @@ def bare(self): @property def opposite(self): """ - Provides the opposite of the current sorting direction. + Provide the opposite of the current sorting direction. Returns: `.OrderBy`: object with an opposite sort influence. @@ -113,29 +111,22 @@ def opposite(self): @property def is_descending(self): - """ - Returns `True` if this object induces *descending* ordering. - """ + """Return `True` if this object induces *descending* ordering.""" return self.startswith("-") @property def is_ascending(self): - """ - Returns `True` if this object induces *ascending* ordering. - """ + """Return `True` if this object induces *ascending* ordering.""" return not self.is_descending def for_queryset(self): - """ - Returns the current instance usable in Django QuerySet's order_by - arguments. - """ + """Return the current instance usable in Django QuerySet's order_by arguments.""" return self.replace(Accessor.LEGACY_SEPARATOR, OrderBy.QUERYSET_SEPARATOR) class OrderByTuple(tuple): """ - Stores ordering as (as `.OrderBy` objects). + Store ordering as (as `.OrderBy` objects). The `~.Table.order_by` property is always converted to an `.OrderByTuple` object. This class is essentially just a `tuple` with some useful extras. @@ -189,8 +180,7 @@ def __contains__(self, name): def __getitem__(self, index): """ - Allows an `.OrderBy` object to be extracted via named or integer - based indexing. + Extract an `.OrderBy` item by index. When using named based indexing, it's fine to used a prefixed named:: @@ -264,9 +254,7 @@ def __lt__(self, other): return Comparator def get(self, key, fallback): - """ - Identical to `__getitem__`, but supports fallback value. - """ + """Identical to `__getitem__`, but supports fallback value.""" try: return self[key] except (KeyError, IndexError): @@ -275,7 +263,7 @@ def get(self, key, fallback): @property def opposite(self): """ - Return version with each `.OrderBy` prefix toggled:: + Return version with each `.OrderBy` prefix toggled. >>> order_by = OrderByTuple(('name', '-age')) >>> order_by.opposite @@ -417,9 +405,7 @@ def bits(self): return self.split(self.SEPARATOR) def get_field(self, model): - """ - Return the django model field for model in context, following relations. - """ + """Return the django model field for model in context, following relations.""" if not hasattr(model, "_meta"): return @@ -438,7 +424,8 @@ def get_field(self, model): def penultimate(self, context, quiet=True): """ - Split the accessor on the right-most separator ('__'), return a tuple with: + Split the accessor on the right-most separator ('__'), return a tuple with. + - the resolved left part. - the remainder @@ -495,7 +482,7 @@ def as_html(self): def segment(sequence, aliases): """ - Translates a flat sequence of items into a set of prefixed aliases. + Translate a flat sequence of items into a set of prefixed aliases. This allows the value set by `.QuerySet.order_by` to be translated into a list of columns that would have the same result. These are called @@ -532,14 +519,13 @@ def segment(sequence, aliases): def signature(fn): """ - Returns: - tuple: Returns a (arguments, kwarg_name)-tuple: - - the arguments (positional or keyword) - - the name of the ** kwarg catch all. + Return an (arguments, kwargs)-tuple. + + - the arguments (positional or keyword) + - the name of the ** kwarg catch all. The self-argument for methods is always removed. """ - signature = inspect.signature(fn) args = [] @@ -557,10 +543,9 @@ def signature(fn): def call_with_appropriate(fn, kwargs): """ - Calls the function ``fn`` with the keyword arguments from ``kwargs`` it expects + Call the function ``fn`` with the keyword arguments from ``kwargs`` it expects. - If the kwargs argument is defined, pass all arguments, else provide exactly - the arguments wanted. + If the kwargs argument is defined, pass all arguments, else provide exactly the arguments wanted. If one of the arguments of ``fn`` are not contained in kwargs, ``fn`` will not be called and ``None`` will be returned. @@ -579,7 +564,7 @@ def call_with_appropriate(fn, kwargs): def computed_values(d, kwargs=None): """ - Returns a new `dict` that has callable values replaced with the return values. + Return a new `dict` that has callable values replaced with the return values. Example:: diff --git a/django_tables2/views.py b/django_tables2/views.py index b972c0a0..0a6ccab3 100644 --- a/django_tables2/views.py +++ b/django_tables2/views.py @@ -9,25 +9,22 @@ class TableMixinBase: - """ - Base mixin for the Single- and MultiTable class based views. - """ + """Base mixin for the Single- and MultiTable class based views.""" context_table_name = "table" table_pagination = None def get_context_table_name(self, table): - """ - Get the name to use for the table's template variable. - """ + """Return the name to use for the table's template variable.""" return self.context_table_name def get_table_pagination(self, table): """ - Return pagination options passed to `.RequestConfig`: - - True for standard pagination (default), - - False for no pagination, - - a dictionary for custom pagination. + Return pagination options passed to `.RequestConfig`. + + - True for standard pagination (default), + - False for no pagination, + - a dictionary for custom pagination. `ListView`s pagination attributes are taken into account, if `table_pagination` does not define the corresponding value. @@ -63,7 +60,7 @@ def get_table_pagination(self, table): def get_paginate_by(self, table_data) -> Optional[int]: """ - Determines the number of items per page, or ``None`` for no pagination. + Determine the number of items per page, or ``None`` for no pagination. Args: table_data: The table's data. @@ -102,9 +99,7 @@ class SingleTableMixin(TableMixinBase): table_data = None def get_table_class(self): - """ - Return the class to use for the table. - """ + """Return the class to use for the table.""" if self.table_class: return self.table_class if self.model: @@ -125,9 +120,7 @@ def get_table(self, **kwargs): ) def get_table_data(self): - """ - Return the table data that should be used to populate the rows. - """ + """Return the table data that should be used to populate the rows.""" if self.table_data is not None: return self.table_data elif hasattr(self, "object_list"): @@ -153,10 +146,7 @@ def get_table_kwargs(self): return {} def get_context_data(self, **kwargs: Any) -> dict[str, Any]: - """ - Overridden version of `.TemplateResponseMixin` to inject the table into - the template's context. - """ + """Overridden version of `.TemplateResponseMixin` to inject the table into the template's context.""" context = super().get_context_data(**kwargs) table = self.get_table(**self.get_table_kwargs()) context[self.get_context_table_name(table)] = table @@ -205,9 +195,7 @@ class MultiTableMixin(TableMixinBase): context_table_name = "tables" def get_tables(self): - """ - Return an array of table instances containing data. - """ + """Return an array of table instances containing data.""" if self.tables is None: view_name = type(self).__name__ raise ImproperlyConfigured(f"No tables were specified. Define {view_name}.tables") @@ -222,9 +210,7 @@ def get_tables(self): return list(Table(data[i]) for i, Table in enumerate(self.tables)) def get_tables_data(self): - """ - Return an array of table_data that should be used to populate each table - """ + """Return an array of table_data that should be used to populate each table.""" return self.tables_data def get_context_data(self, **kwargs: Any) -> dict[str, Any]: diff --git a/example/app/migrations/0001_initial.py b/example/app/migrations/0001_initial.py index 0dc7318a..e2a06d7b 100644 --- a/example/app/migrations/0001_initial.py +++ b/example/app/migrations/0001_initial.py @@ -1,7 +1,7 @@ # Generated by Django 1.11.5 on 2017-09-22 13:23 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/example/app/migrations/0002_auto_20180416_0959.py b/example/app/migrations/0002_auto_20180416_0959.py index 4662bdb4..fb791cf1 100644 --- a/example/app/migrations/0002_auto_20180416_0959.py +++ b/example/app/migrations/0002_auto_20180416_0959.py @@ -1,7 +1,7 @@ # Generated by Django 2.0.1 on 2018-04-16 09:59 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/example/app/models.py b/example/app/models.py index b1e2c93a..4ace6b54 100644 --- a/example/app/models.py +++ b/example/app/models.py @@ -11,9 +11,7 @@ def __str__(self): class Country(models.Model): - """ - Represents a geographical Country - """ + """Represents a geographical Country.""" name = models.CharField(max_length=100) population = models.PositiveIntegerField(verbose_name=_("population")) diff --git a/example/app/views.py b/example/app/views.py index 0bdcb6c9..28847b72 100644 --- a/example/app/views.py +++ b/example/app/views.py @@ -117,8 +117,7 @@ def checkbox(request): def template_example(request, version): - """Demonstrate the use of the bootstrap template""" - + """Demonstrate the use of the bootstrap template.""" versions = { "bootstrap3": (BootstrapTable, "bootstrap_template.html"), "bootstrap4": (Bootstrap4Table, "bootstrap4_template.html"), diff --git a/pyproject.toml b/pyproject.toml index b4f2d03c..2a2c860a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,9 +44,6 @@ Documentation = "https://django-tables2.readthedocs.io/en/latest/" Homepage = "https://github.com/jieter/django-tables2/" Readme = "https://github.com/jieter/django-tables2/blob/master/README.md" -[tool.black] -line-length = 100 - [tool.hatch.build.targets.sdist] exclude = ["docs"] @@ -56,5 +53,46 @@ packages = ["django_tables2"] [tool.hatch.version] path = "django_tables2/__init__.py" +[tool.ruff] +line-length = 100 +target-version = "py39" + +[tool.ruff.lint] +fixable = [ + # "D", + "D200", + "D202", + "D401", + "D400", + "D415", + "E", + "F", + "I", + "UP" +] +ignore = [ + "D1", # Missing docstring error codes (because not every function and class has a docstring) + "D203", # 1 blank line required before class docstring (conflicts with D211 and should be disabled, see https://github.com/PyCQA/pydocstyle/pull/91) + "D205", + "D406", # Section name should end with a newline + "D407", # Missing dashed underline after section + "D413", # Missing blank line after last section ... + "D212", # Multi-line docstring summary should start at the first line + "E501" # Line too long +] +select = [ + "D", # pydocstyle + "E", # pycodestyle + "F", # flake8 + "I", # isort + "UP" # pyupgrade +] +unfixable = [ + "F8" # names in flake8, such as defined but unused variables +] + +[tool.ruff.lint.per-file-ignores] +"*/migrations/*" = ["D417", "E501"] + [tool.setuptools.dynamic] version = {attr = "django_tables2.__version__"} diff --git a/tests/app/migrations/0001_initial.py b/tests/app/migrations/0001_initial.py index 3bebbd39..f8466de6 100644 --- a/tests/app/migrations/0001_initial.py +++ b/tests/app/migrations/0001_initial.py @@ -1,7 +1,7 @@ # Generated by Django 4.0a1 on 2021-09-22 18:51 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/tests/app/views.py b/tests/app/views.py index 2e39d97e..5cb57800 100644 --- a/tests/app/views.py +++ b/tests/app/views.py @@ -5,12 +5,12 @@ def person(request, pk): - """A really simple view to provide an endpoint for the 'person' URL.""" + """Endpoint for the 'person' URL.""" person = get_object_or_404(Person, pk=pk) return HttpResponse(f"Person: {person}") def occupation(request, pk): - """Another really simple view to provide an endpoint for the 'occupation' URL.""" + """Endpoint for the 'occupation' URL.""" occupation = get_object_or_404(Occupation, pk=pk) return HttpResponse(f"Occupation: {occupation}") diff --git a/tests/columns/test_booleancolumn.py b/tests/columns/test_booleancolumn.py index a61c9a94..792f031b 100644 --- a/tests/columns/test_booleancolumn.py +++ b/tests/columns/test_booleancolumn.py @@ -1,6 +1,3 @@ -from unittest import skipIf - -from django import VERSION as django_version from django.db import models from django.test import TestCase @@ -26,11 +23,8 @@ class Meta: self.assertEqual(type(column), tables.BooleanColumn) self.assertEqual(column.empty_values, ()) - @skipIf(django_version < (2, 1, 0), "Feature added in django 2.1") def test_should_use_nullability_for_booloanfield(self): - """ - Django 2.1 supports null=(True|False) for BooleanField. - """ + """Django 2.1 supports null=(True|False) for BooleanField.""" class BoolModel2(models.Model): field = models.BooleanField(null=True) @@ -99,6 +93,8 @@ class Table(tables.Table): def test_boolean_field_choices_with_real_model_instances(self): """ + Verify that choices are used if defined. + If a booleanField has choices defined, the value argument passed to BooleanColumn.render() is the rendered value, not a bool. """ @@ -119,7 +115,7 @@ class Meta: self.assertEqual(table.rows[1].get_cell("field"), '') def test_boolean_field_choices_spanning_relations(self): - "The inverse lookup voor boolean choices should also work on related models" + """The inverse lookup voor boolean choices should also work on related models.""" class Table(tables.Table): boolean = tables.BooleanColumn(accessor="occupation__boolean_with_choices") @@ -141,7 +137,7 @@ class Meta: self.assertEqual(table.rows[1].get_cell("boolean"), '') def test_boolean_should_not_prevent_rendering_of_other_columns(self): - """Test for issue 360""" + """Test that other columns should render even if the boolean column receives a non-boolean value (#360).""" class Table(tables.Table): boolean = tables.BooleanColumn(yesno="waar,onwaar") @@ -150,8 +146,8 @@ class Meta: model = Occupation fields = ("boolean", "name") - Occupation.objects.create(name="Waar", boolean=True), - Occupation.objects.create(name="Onwaar", boolean=False), + Occupation.objects.create(name="Waar", boolean=True) + Occupation.objects.create(name="Onwaar", boolean=False) Occupation.objects.create(name="Onduidelijk") html = Table(Occupation.objects.all()).as_html(build_request()) diff --git a/tests/columns/test_datecolumn.py b/tests/columns/test_datecolumn.py index 73ff95d1..5d0121c3 100644 --- a/tests/columns/test_datecolumn.py +++ b/tests/columns/test_datecolumn.py @@ -12,6 +12,8 @@ def isoformat_link(value): class DateColumnTest(SimpleTestCase): """ + Date formatting test case. + Format string: https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date D -- Day of the week, textual, 3 letters -- 'Fri' b -- Month, textual, 3 letters, lowercase -- 'jan' diff --git a/tests/columns/test_datetimecolumn.py b/tests/columns/test_datetimecolumn.py index 7539a9b3..52a1cad0 100644 --- a/tests/columns/test_datetimecolumn.py +++ b/tests/columns/test_datetimecolumn.py @@ -14,6 +14,8 @@ def isoformat_link(value): class DateTimeColumnTest(SimpleTestCase): """ + Date time formatting test case. + Format string: https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date D -- Day of the week, textual, 3 letters -- 'Fri' b -- Month, textual, 3 letters, lowercase -- 'jan' diff --git a/tests/columns/test_filecolumn.py b/tests/columns/test_filecolumn.py index 278dd3da..f7d8b6be 100644 --- a/tests/columns/test_filecolumn.py +++ b/tests/columns/test_filecolumn.py @@ -12,7 +12,7 @@ def storage(): - """Provide a storage that exposes the test templates""" + """Provide a storage that exposes the test templates.""" root = os.path.join(os.path.dirname(__file__), "..", "app", "templates") return FileSystemStorage(location=root, base_url="/baseurl/") diff --git a/tests/columns/test_general.py b/tests/columns/test_general.py index 4cd44225..6944c0c7 100644 --- a/tests/columns/test_general.py +++ b/tests/columns/test_general.py @@ -144,10 +144,7 @@ class SimpleTable(tables.Table): self.assertFalse(table.columns["name"].is_ordered) def test_translation(self): - """ - Tests different types of values for the ``verbose_name`` property of a - column. - """ + """Tests different types of values for the ``verbose_name`` property of a column.""" class TranslationTable(tables.Table): text = tables.Column(verbose_name=gettext_lazy("Text")) @@ -156,9 +153,7 @@ class TranslationTable(tables.Table): self.assertEqual(table.columns["text"].header, "Text") def test_sequence(self): - """ - Ensures that the sequence of columns is configurable. - """ + """Ensure that the sequence of columns is configurable.""" class TestTable(tables.Table): a = tables.Column() @@ -280,18 +275,20 @@ class SimpleTable(tables.Table): table = SimpleTable([{"a": "value"}]) root = parse(table.as_html(request)) + # return classes of an element as a set - classes = lambda x: set(x.attrib.get("class", "").split()) - self.assertIn("orderable", classes(root.findall(".//thead/tr/th")[0])) - self.assertNotIn("orderable", classes(root.findall(".//thead/tr/th")[1])) + def get_classes(element) -> set[str]: + return set(element.attrib.get("class", "").split()) + + self.assertIn("orderable", get_classes(root.findall(".//thead/tr/th")[0])) + self.assertNotIn("orderable", get_classes(root.findall(".//thead/tr/th")[1])) # Now try with an ordered table table = SimpleTable([], order_by="a") root = parse(table.as_html(request)) - # return classes of an element as a set - self.assertIn("orderable", classes(root.findall(".//thead/tr/th")[0])) - self.assertIn("asc", classes(root.findall(".//thead/tr/th")[0])) - self.assertNotIn("orderable", classes(root.findall(".//thead/tr/th")[1])) + self.assertIn("orderable", get_classes(root.findall(".//thead/tr/th")[0])) + self.assertIn("asc", get_classes(root.findall(".//thead/tr/th")[0])) + self.assertNotIn("orderable", get_classes(root.findall(".//thead/tr/th")[1])) def test_empty_values_triggers_default(self): class Table(tables.Table): @@ -324,9 +321,7 @@ class Table(tables.Table): row[table] def test_related_fields_get_correct_type(self): - """ - Types of related fields should also lead to the correct type of column. - """ + """Types of related fields should also lead to the correct type of column.""" class PersonTable(tables.Table): class Meta: @@ -358,10 +353,9 @@ class Meta: class ColumnInheritanceTest(TestCase): def test_column_params_should_be_preserved_under_inheritance(self): """ - Github issue #337 + Column parameters should be preserved under inheritance (#337). - Columns explicitly defined on MyTable get overridden by columns implicitly - defined on it's child. + Columns explicitly defined on MyTable get overridden by columns implicitly defined on it's child. If the column is not redefined, the explicit definition of MyTable is used, preserving the specialized verbose_name defined on it. """ @@ -394,9 +388,7 @@ class Meta(MyTable.Meta): def test_explicit_column_can_be_overridden_by_other_explicit_column(self): class MyTableC(MyTable): - """ - If we define a new explict item1 column, that one should be used. - """ + """If we define a new explicit item1 column, that one should be used.""" item1 = tables.Column(verbose_name="New nice column name") @@ -407,10 +399,7 @@ class MyTableC(MyTable): self.assertEqual(tableC.columns["item1"].verbose_name, "New nice column name") def test_override_column_class_names(self): - """ - We control the output of CSS class names for a column by overriding - get_column_class_names - """ + """We control the output of CSS class names for a column by overriding get_column_class_names().""" class MyTable(tables.Table): population = tables.Column(verbose_name="Population") @@ -436,7 +425,7 @@ def setUp(self): Person.objects.create(first_name="Sjon", last_name="Jansen") def test_computable_td_attrs(self): - """Computable attrs for columns, using table argument""" + """Computable attrs for columns, using table argument.""" class Table(tables.Table): person = tables.Column(attrs={"cell": {"data-length": lambda table: len(table.data)}}) @@ -453,7 +442,7 @@ class Table(tables.Table): self.assertIn('', html) def test_computable_td_attrs_defined_in_column_class_attribute(self): - """Computable attrs for columns, using custom Column""" + """Computable attrs for columns, using custom Column.""" class MyColumn(tables.Column): attrs = {"td": {"data-test": lambda table: len(table.data)}} @@ -469,7 +458,7 @@ class Table(tables.Table): self.assertEqual(root.findall(".//tbody/tr/td")[1].attrib, {"data-test": "2"}) def test_computable_td_attrs_defined_in_column_class_attribute_record(self): - """Computable attrs for columns, using custom column""" + """Computable attrs for columns, using custom column.""" class PersonColumn(tables.Column): attrs = { @@ -495,10 +484,7 @@ class Table(tables.Table): ) def test_computable_column_td_attrs_record_header(self): - """ - Computable attrs for columns, using custom column with a callable containing - a catch-all argument. - """ + """Computable attrs for columns, using custom column with a callable containing a catch-all argument.""" def data_first_name(**kwargs): record = kwargs.get("record", None) diff --git a/tests/columns/test_linkcolumn.py b/tests/columns/test_linkcolumn.py index 01d3da83..123095d9 100644 --- a/tests/columns/test_linkcolumn.py +++ b/tests/columns/test_linkcolumn.py @@ -12,7 +12,7 @@ class LinkColumnTest(TestCase): def test_unicode(self): - """Test LinkColumn for unicode values + headings""" + """Test LinkColumn for unicode values + headings.""" class UnicodeTable(tables.Table): first_name = tables.LinkColumn("person", args=[A("pk")]) @@ -77,7 +77,7 @@ class PersonTable(tables.Table): self.assertIn("—", html) def test_linkcolumn_non_field_based(self): - """Test for issue 257, non-field based columns""" + """Test for issue 257, non-field based columns.""" class Table(tables.Table): first_name = tables.Column() @@ -141,8 +141,7 @@ class TestTable(tables.Table): self.assertEqual(table.rows[0].get_cell("col"), table.rows[0].get_cell("col_linkify")) def test_td_attrs_should_be_supported(self): - """LinkColumn should support both and attrs""" - + """LinkColumn should support both and attrs.""" person = Person.objects.create(first_name="Bob", last_name="Builder") class Table(tables.Table): diff --git a/tests/columns/test_manytomanycolumn.py b/tests/columns/test_manytomanycolumn.py index d45a8f76..b842228c 100644 --- a/tests/columns/test_manytomanycolumn.py +++ b/tests/columns/test_manytomanycolumn.py @@ -38,7 +38,10 @@ def setUpTestData(cls): def test_from_model(self): """ - Automatically uses the ManyToManyColumn for a ManyToManyField, and calls the + Test if ManyToManyColumn is used for ManyToManyFields. + + + Should automatically use ManyToManyColumn for a ManyToManyField, and calls the Models's `__str__` method to transform the model instance to string. """ @@ -73,9 +76,7 @@ class Table(tables.Table): self.assertIn(str(friend), friends) def test_linkify_item_different_model(self): - """ - Make sure the correct get_absolute_url() is used to linkify the items. - """ + """Make sure the correct get_absolute_url() is used to linkify the items.""" class GroupTable(tables.Table): name = tables.Column(linkify=True) diff --git a/tests/columns/test_templatecolumn.py b/tests/columns/test_templatecolumn.py index 643ab3cb..eef1678d 100644 --- a/tests/columns/test_templatecolumn.py +++ b/tests/columns/test_templatecolumn.py @@ -90,9 +90,7 @@ class Table(tables.Table): col = tables.TemplateColumn() def test_should_support_value_with_curly_braces(self): - """ - https://github.com/bradleyayers/django-tables2/issues/441 - """ + """Test that TemplateColumn can handle values with curly braces (#441).""" class Table(tables.Table): track = tables.TemplateColumn("track: {{ value }}") diff --git a/tests/columns/test_timecolumn.py b/tests/columns/test_timecolumn.py index d444ab4c..e4e09f7c 100644 --- a/tests/columns/test_timecolumn.py +++ b/tests/columns/test_timecolumn.py @@ -7,11 +7,6 @@ class TimeColumnTest(SimpleTestCase): - """ - Format string for TimeColumn: - https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date - """ - def test_should_handle_explicit_format(self): class TestTable(tables.Table): time = tables.TimeColumn(format="H:i:s") diff --git a/tests/test_config.py b/tests/test_config.py index 7209dc93..c9c4305a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -66,7 +66,6 @@ def test_silent_empty_page_error(self): def test_passing_request_to_constructor(self): """Table constructor should call RequestConfig if a request is passed.""" - request = build_request("/?page=1&sort=abc") class SimpleTable(Table): diff --git a/tests/test_core.py b/tests/test_core.py index c0cb83d2..9660a557 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -81,7 +81,7 @@ class MayorlessCityTable(CityTable): def test_metaclass_inheritance(self): class Tweaker(type): - """Adds an attribute "tweaked" to all classes""" + """Add an attribute "tweaked" to all classes.""" def __new__(cls, name, bases, attrs): attrs["tweaked"] = True diff --git a/tests/test_export.py b/tests/test_export.py index 5b4d9f0d..7424e0fa 100644 --- a/tests/test_export.py +++ b/tests/test_export.py @@ -59,9 +59,7 @@ class View(ExportMixin, tables.SingleTableView): @skipIf(TableExport is None, "Tablib is required to run the export tests") class TableExportTest(TestCase): - """ - github issue #474: null/None values in exports - """ + """Test for null/None values in exports (#474).""" def test_None_values(self): table = Table( @@ -265,7 +263,6 @@ def test_should_work_with_foreign_keys(self): def test_datetime_xls(self): """Verify datatime objects can be exported to xls.""" - utc = pytz.timezone("UTC") class Table(tables.Table): @@ -298,7 +295,6 @@ def get_queryset(self): def test_export_invisible_columns(self): """Verify columns with visible=False *do* get exported.""" - DATA = [{"name": "Bess W. Fletcher", "website": "teammonka.com"}] class Table(tables.Table): diff --git a/tests/test_extra_columns.py b/tests/test_extra_columns.py index 015c285d..fa4202a0 100644 --- a/tests/test_extra_columns.py +++ b/tests/test_extra_columns.py @@ -21,11 +21,11 @@ class DynamicColumnsTest(TestCase): def test_dynamically_adding_columns(self): """ - When adding columns to self.base_columns, they were actually added to - the class attribute `Table.base_columns`, and not to the instance - attribute, `table.base_columns` + Test adding columns dynamically (#403). - issue #403 + + When adding columns to self.base_columns, they were actually added to the class attribute `Table.base_columns`, + and not to the instance attribute, `table.base_columns`. """ class MyTable(tables.Table): @@ -162,12 +162,7 @@ def before_render(self, request): self.assertRegex(html, re_Country) def test_sequence_and_extra_columns(self): - """ - https://github.com/jieter/django-tables2/issues/486 - - The exact moment the '...' is expanded is crucial here. - """ - + """The exact moment the '...' is expanded is crucial here (#486).""" add_occupation_column = True class MyTable(tables.Table): @@ -194,9 +189,7 @@ def __init__(self, data, *args, **kwargs): self.assertEqual([c.name for c in table.columns], ["first_name", "friends"]) def test_change_attributes(self): - """ - https://github.com/jieter/django-tables2/issues/574 - """ + """Test attributes of columns can be changed #574).""" class Table(tables.Table): mycolumn = tables.Column(orderable=False) diff --git a/tests/test_footer.py b/tests/test_footer.py index d2ea1ac2..f5e58078 100644 --- a/tests/test_footer.py +++ b/tests/test_footer.py @@ -39,10 +39,7 @@ class Table(tables.Table): self.assertEqual(columns[2].text, "18833000") def test_footer_disable_on_table(self): - """ - Showing the footer can be disabled using show_footer argument to the Table - constructor - """ + """Showing the footer can be disabled using show_footer argument to the Table constructor.""" class Table(tables.Table): name = tables.Column() diff --git a/tests/test_models.py b/tests/test_models.py index d670c1be..e485b154 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -96,10 +96,7 @@ class Meta: self.assertEqual(table.rows[0].get_cell("first_name"), "Bradley") def test_Meta_option_model_table(self): - """ - The ``model`` option on a table causes the table to dynamically add columns - based on the fields. - """ + """The `model` option on a table causes the table to dynamically add columns based on the fields.""" class OccupationTable(tables.Table): class Meta: @@ -168,10 +165,7 @@ class SimpleTable(tables.Table): self.assertEqual(parse(html).findall(".//thead/tr/th/a")[0].attrib, {"href": "?sort=-name"}) def test_default_order(self): - """ - If orderable=False, do not sort queryset. - https://github.com/bradleyayers/django-tables2/issues/204 - """ + """If orderable=False, do not sort queryset (#204).""" class PersonTable(tables.Table): first_name = tables.Column() @@ -509,9 +503,7 @@ class PersonTable(tables.Table): self.assertEqual(Person.objects.all().count(), 10) def test_model__str__calls(self): - """ - Model.__str__ should not be called when not necessary. - """ + """Model.__str__ should not be called when not necessary.""" calls = defaultdict(int) def counting__str__(self): diff --git a/tests/test_ordering.py b/tests/test_ordering.py index 57e10190..1b848e31 100644 --- a/tests/test_ordering.py +++ b/tests/test_ordering.py @@ -162,12 +162,13 @@ class PersonTable(tables.Table): def test_ordering_by_custom_field(self): """ + Test ordering by a custom field. + When defining a custom field in a table, as name=tables.Column() with methods to render and order render_name and order_name, sorting by this column causes an error if the custom field is not in last position. https://github.com/jieter/django-tables2/issues/413 """ - Person.objects.create(first_name="Alice", last_name="Beta") Person.objects.create(first_name="Bob", last_name="Alpha") diff --git a/tests/test_paginators.py b/tests/test_paginators.py index 89177f3d..cfd1120c 100644 --- a/tests/test_paginators.py +++ b/tests/test_paginators.py @@ -72,9 +72,7 @@ def test_lookahead(self): self.assertEqual(paginator.num_pages, 100) def test_number_is_none(self): - """ - When number=None is supplied, the paginator should serve its first page. - """ + """When number=None is supplied, the paginator should serve its first page.""" objects = list(range(1, 1000)) paginator = LazyPaginator(objects, 10, look_ahead=3) self.assertEqual(paginator.page(None).object_list, list(range(1, 11))) diff --git a/tests/test_pinned_rows.py b/tests/test_pinned_rows.py index 0dbab3ba..26d7eafc 100644 --- a/tests/test_pinned_rows.py +++ b/tests/test_pinned_rows.py @@ -51,9 +51,7 @@ def test_bound_rows_with_pinned_data(self): self.assertNotIn("gamma", row) def test_as_html(self): - """ - Ensure that html render correctly. - """ + """Ensure that as_html() renders correctly.""" request = build_request("/") table = SimpleTable([{"name": "Grzegorz", "age": 30, "occupation": "programmer"}]) root = parse(table.as_html(request)) @@ -94,9 +92,7 @@ def test_as_html(self): self.assertEqual(td[2].text, "130") def test_pinned_row_attrs(self): - """ - Testing attrs for pinned rows. - """ + """Testing attrs for pinned rows.""" pinned_row_attrs = {"class": "super-mega-row", "data-foo": "bar"} request = build_request("/") @@ -109,9 +105,7 @@ def test_pinned_row_attrs(self): self.assertIn("data-foo", html) def test_ordering(self): - """ - Change sorting should not change ordering pinned rows. - """ + """Change sorting should not change ordering pinned rows.""" request = build_request("/") records = [ {"name": "Alex", "age": 42, "occupation": "programmer"}, @@ -139,6 +133,7 @@ def test_ordering(self): def test_bound_rows_getitem(self): """ Testing BoundRows.__getitem__() method. + Checking the return class for simple value and for slice. Ensure that inside of BoundRows pinned rows are included in length. """ @@ -155,10 +150,7 @@ def test_bound_rows_getitem(self): self.assertEqual(len(table.rows[:]), 6) def test_uniterable_pinned_data(self): - """ - Ensure that, when data for pinned rows are not iterable, - the ValueError exception will be raised. - """ + """Ensure that, when data for pinned rows are not iterable, the ValueError exception will be raised.""" class FooTable(tables.Table): col = tables.Column() diff --git a/tests/test_rows.py b/tests/test_rows.py index 62e2c6aa..2af993ea 100644 --- a/tests/test_rows.py +++ b/tests/test_rows.py @@ -76,10 +76,7 @@ class SimpleTable(tables.Table): row.cells["gamma"] def test_row_attrs(self): - """ - If a callable returns an empty string, do not add a space to the CSS class - attribute. (#416) - """ + """If a callable returns an empty string, do not add a space to the CSS class attribute (#416).""" counter = count() class Table(tables.Table): @@ -128,9 +125,7 @@ class Meta: self.assertEqual(row.get_cell("a"), "valA") def test_even_odd_css_class(self): - """ - Test for BoundRow.get_even_odd_css_class() method - """ + """Test for BoundRow.get_even_odd_css_class() method.""" class SimpleTable(tables.Table): foo = tables.Column() diff --git a/tests/test_tabledata.py b/tests/test_tabledata.py index 74dd7a82..a2974311 100644 --- a/tests/test_tabledata.py +++ b/tests/test_tabledata.py @@ -82,10 +82,7 @@ def test_TableListData_basic_list(self): self.assertEqual(data.verbose_name_plural, "items") def test_TableListData_with_verbose_name(self): - """ - TableListData uses the attributes on the listlike object to generate - it's verbose_name. - """ + """TableListData uses the attributes on the list-like object to generate it's verbose_name.""" class listlike(list): verbose_name = "unit" @@ -101,7 +98,7 @@ class listlike(list): class TableQuerysetDataTest(TestCase): def test_custom_TableData(self): - """If TableQuerysetData._length is set, no count() query will be performed""" + """If TableQuerysetData._length is set, no count() query will be performed.""" for i in range(11): Person.objects.create(first_name=f"first {i}") diff --git a/tests/test_templates.py b/tests/test_templates.py index 99da44a7..de7191bc 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -1,6 +1,7 @@ from django.template import Context, Template from django.test import SimpleTestCase, TestCase, override_settings -from django.utils.translation import gettext_lazy, override as translation_override +from django.utils.translation import gettext_lazy +from django.utils.translation import override as translation_override from lxml import etree import django_tables2 as tables @@ -116,7 +117,8 @@ class Meta: def test_render_table_db_queries(self): """ - Paginated tables should result in two queries: + Paginated tables should result in two queries. + - one query for pagination: .count() - one query for records on the current page: .all()[start:end] """ @@ -147,9 +149,7 @@ class TemplateLocalizeTest(TestCase): expected_results = {None: "1234.5", False: "1234.5", True: "1 234,5"} # non-breaking space def assert_cond_localized_table(self, localizeit=None, expected=None): - """ - helper function for defining Table class conditionally - """ + """Conditionally define a table class.""" class TestTable(tables.Table): name = tables.Column(verbose_name="my column", localize=localizeit) diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py index 7700dd12..a149f180 100644 --- a/tests/test_templatetags.py +++ b/tests/test_templatetags.py @@ -38,7 +38,8 @@ def test_basic(self): def test_does_not_mutate_context(self): """ - Make sure the tag does not change the context of the template the tag is called from + Make sure the tag does not change the context of the template the tag is called from. + https://github.com/jieter/django-tables2/issues/547 """ @@ -98,7 +99,7 @@ def test_no_data_with_empty_text(self): @override_settings(DEBUG=True) def test_missing_variable(self): - """Variable that doesn't exist (issue #8)""" + """Variable that doesn't exist (issue #8).""" template = Template("{% load django_tables2 %}{% render_table this_doesnt_exist %}") with self.assertRaisesMessage(ValueError, "Expected table or queryset, not str"): template.render(Context()) diff --git a/tests/test_views.py b/tests/test_views.py index 85de7e8c..8c6c609d 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -243,7 +243,8 @@ def get_table_pagination(self, table): def test_using_get_queryset(self): """ - Should not raise a value-error for a View using View.get_queryset() + Should not raise a value-error for a View using View.get_queryset(). + (test for reverting regressing in #423) """ Person.objects.create(first_name="Anton", last_name="Sam") @@ -295,7 +296,7 @@ class Table(tables.SingleTableView): class SingleTableMixinTest(TestCase): def test_with_non_paginated_view(self): """ - SingleTableMixin should not assume it is mixed with a ListView + SingleTableMixin should not assume it is mixed with a ListView. Github issue #326 """ @@ -313,10 +314,7 @@ class View(tables.SingleTableMixin, TemplateView): View.as_view()(build_request()) def test_should_paginate_by_default(self): - """ - When mixing SingleTableMixin with FilterView, the table should paginate by default - """ - + """When mixing SingleTableMixin with FilterView, the table should paginate by default.""" total_records = 60 for i in range(1, total_records + 1): Region.objects.create(name=f"region {i:02d} / {total_records}") diff --git a/tests/utils.py b/tests/utils.py index 595fbc8a..eb0c56b7 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -11,10 +11,8 @@ def parse(html): return lxml.html.fromstring(html) -def attrs(xml): - """ - Helper function that returns a dict of XML attributes, given an element. - """ +def attrs(xml: str) -> dict: + """Return a dict of XML attributes, given an element.""" return lxml.html.fromstring(xml).attrib diff --git a/tox.ini b/tox.ini index b49a9113..f226806e 100644 --- a/tox.ini +++ b/tox.ini @@ -43,30 +43,3 @@ commands = make spelling deps = -r{toxinidir}/docs/requirements.txt - -[testenv:flake8] -basepython = python3.11 -deps = flake8==3.7.9 -commands = flake8 - -[flake8] -ignore = E731,W503,E203 -exclude = .git,__pycache__,.tox,example/app/migrations -max-line-length = 120 - -[testenv:isort] -basepython = python3.11 -deps = - -r requirements/common.pip - isort==5.6.4 -commands = isort --diff --check django_tables2 test - -[isort] -multi_line_output = 3 -include_trailing_comma = True -force_grid_wrap = 0 -combine_as_imports = True -line_length = 100 -skip = migrations -known_third_party=django,django_filters,pytest,fudge,lxml,pytz -known_first_party=django_tables2