diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index e54e855..65bed49 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -12,8 +12,14 @@ concurrency: cancel-in-progress: true jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: pre-commit/action@v3.0.0 unit_test_suite: name: Unit tests on ${{ matrix.os }} with Python ${{ matrix.python-version }} + needs: [pre-commit] runs-on: ${{ matrix.os }} strategy: fail-fast: false diff --git a/.gitignore b/.gitignore index 7ab3f19..5c9216a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ __pycache__/ .ipynb_checkpoints/ .pytest_cache dist -scratch/ \ No newline at end of file +scratch/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a18cf58 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,33 @@ +# This is the configuration for pre-commit, a local framework for managing pre-commit hooks +# Check out the docs at: https://pre-commit.com/ + +exclude: (\.min\.js$|\.svg$|\.html$) +default_stages: [commit] +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-builtin-literals + - id: check-case-conflict + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-toml + - id: detect-private-key + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.285 + hooks: + - id: ruff + args: [holonote] + files: holonote/ + - repo: https://github.com/hoxbro/clean_notebook + rev: v0.1.10 + hooks: + - id: clean-notebook + - repo: https://github.com/codespell-project/codespell + rev: v2.2.5 + hooks: + - id: codespell + additional_dependencies: + - tomli diff --git a/README.md b/README.md index 9c877bf..d3e4094 100644 --- a/README.md +++ b/README.md @@ -64,4 +64,4 @@ have different types: data, containing information that is not to be visualized by `holonote`. These are defined by the user or application and can have arbitrary types and contents. A complete set of fields together with one -or more regions constitutes an annotation. \ No newline at end of file +or more regions constitutes an annotation. diff --git a/examples/Basics.ipynb b/examples/Basics.ipynb index 0a02cf6..9546819 100644 --- a/examples/Basics.ipynb +++ b/examples/Basics.ipynb @@ -105,7 +105,7 @@ "id": "616fd061-fffd-4f1a-94ce-dc8bc6b9d5bd", "metadata": {}, "source": [ - "**Note**: The tools made available by the region editor are appropriate to both the enable region types as well as the dimensionality of the elemnt (here, a single key dimension along the x-axis)\n" + "**Note**: The tools made available by the region editor are appropriate to both the enable region types as well as the dimensionality of the element (here, a single key dimension along the x-axis)\n" ] }, { @@ -141,7 +141,7 @@ "id": "c0fd9b3f-8827-4e02-bb03-8edfbde88eb1", "metadata": {}, "source": [ - "You can set the range of interest programatically as well:" + "You can set the range of interest programmatically as well:" ] }, { @@ -227,7 +227,7 @@ "id": "18335801-7230-40b9-a5b1-ed4d7050deec", "metadata": {}, "source": [ - "Note that uuid values are randomly generated (by default) which means we do not know what these values will be ahead of time. As a result we need a programatic way to access them. Using the dataframe index directly is awkard, so annotators offer a more natural, interactive way to select annotations - simply click on them in the plot to select them.\n", + "Note that uuid values are randomly generated (by default) which means we do not know what these values will be ahead of time. As a result we need a programmatic way to access them. Using the dataframe index directly is awkward, so annotators offer a more natural, interactive way to select annotations - simply click on them in the plot to select them.\n", "\n", "Click on a range region in the plot above and run the following cell to see it's uuid:" ] @@ -702,7 +702,7 @@ "\n", "### Styling the annotator\n", "\n", - "You can set the style either throught the `_style` keywords in `.overlay`:" + "You can set the style either through the `_style` keywords in `.overlay`:" ] }, { @@ -737,22 +737,9 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.16" + "pygments_lexer": "ipython3" } }, "nbformat": 4, diff --git a/examples/Basics_2D.ipynb b/examples/Basics_2D.ipynb index 1f5cf3d..0f42121 100644 --- a/examples/Basics_2D.ipynb +++ b/examples/Basics_2D.ipynb @@ -144,7 +144,7 @@ "id": "c0fd9b3f-8827-4e02-bb03-8edfbde88eb1", "metadata": {}, "source": [ - "You can set the range of interest programatically as well:" + "You can set the range of interest programmatically as well:" ] }, { @@ -220,7 +220,7 @@ "id": "18335801-7230-40b9-a5b1-ed4d7050deec", "metadata": {}, "source": [ - "Note that uuid values are randomly generated (by default) which means we do not know what these values will be ahead of time. As a result we need a programatic way to access them. Using the dataframe index directly is awkard, so annotators offer a more natural, interactive way to select annotations - simply click on them in the plot to select them.\n", + "Note that uuid values are randomly generated (by default) which means we do not know what these values will be ahead of time. As a result we need a programmatic way to access them. Using the dataframe index directly is awkward, so annotators offer a more natural, interactive way to select annotations - simply click on them in the plot to select them.\n", "\n", "Click on a range region in the plot above and run the following cell to see it's uuid:" ] @@ -678,7 +678,7 @@ "\n", "### Styling the annotator\n", "\n", - "You can set the style either throught the `_style` keywords in `.overlay`:" + "You can set the style either through the `_style` keywords in `.overlay`:" ] }, { @@ -713,22 +713,9 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.16" + "pygments_lexer": "ipython3" } }, "nbformat": 4, diff --git a/holonote/__init__.py b/holonote/__init__.py index fb51d82..b9583f1 100644 --- a/holonote/__init__.py +++ b/holonote/__init__.py @@ -1,2 +1,3 @@ -from . import annotate -from . import editor +from __future__ import annotations + +from . import annotate, editor # noqa: F401 diff --git a/holonote/annotate/__init__.py b/holonote/annotate/__init__.py index 56cff72..d5810e5 100644 --- a/holonote/annotate/__init__.py +++ b/holonote/annotate/__init__.py @@ -1,3 +1,11 @@ -from .annotator import Annotator -from .connector import Connector, SQLiteDB, AutoIncrementKey, UUIDHexStringKey, UUIDBinaryKey +from __future__ import annotations + +from .annotator import Annotator # noqa: F401 +from .connector import ( # noqa: F401 + AutoIncrementKey, + Connector, + SQLiteDB, + UUIDBinaryKey, + UUIDHexStringKey, +) from .table import * diff --git a/holonote/annotate/annotator.py b/holonote/annotate/annotator.py index 192e8fd..c6722de 100644 --- a/holonote/annotate/annotator.py +++ b/holonote/annotate/annotator.py @@ -1,13 +1,14 @@ -import sys -import weakref +from __future__ import annotations + import holoviews as hv import numpy as np import pandas as pd import param from bokeh.models.tools import BoxSelectTool, HoverTool from holoviews.element.selection import Selection1DExpr -from .connector import Connector, SQLiteDB, AnnotationTable +from .connector import Connector, SQLiteDB +from .table import AnnotationTable class Indicator: @@ -17,11 +18,11 @@ class Indicator: """ range_style = dict(color='red', alpha=0.4, apply_ranges=False) - point_style = dict() + point_style = {} indicator_highlight = {'alpha':(0.7,0.2)} edit_range_style = dict(alpha=0.4, line_alpha=1, line_width=1, line_color='black') - edit_point_style = dict() + edit_point_style = {} @classmethod def indicator_style(cls, range_style, point_style, highlighters): @@ -167,13 +168,12 @@ def df(self): region_column_names.extend(point_column_names) view = pd.concat(views) view = view.rename(columns={'_id':field_name}) - column_ordering = [field_name] + region_column_names + fields_columns + column_ordering = [field_name, *region_column_names, *fields_columns] return view[column_ordering].set_index(field_name) def refresh(self, clear=False): "Method to update display state of the annotator and optionally clear stale visual state" - pass def set_annotation_table(self, annotation_table): # FIXME! Won't work anymore, set_connector?? self._region = {} @@ -430,7 +430,7 @@ def clear_indicated_region(self): def selection_element(self): if self.element is None: kdims = list(self.kdim_dtypes.keys()) - kdim_dtype = list(self.kdim_dtypes.values())[0] + kdim_dtype = next(iter(self.kdim_dtypes.values())) return hv.Curve(([kdim_dtype(), kdim_dtype()], [0,1]), kdims=kdims) # Note: Any concrete Selection1dExpr will do... @@ -596,7 +596,7 @@ def _build_hover_tool(self): extra_cols = [(col, '@{%s}' % col.replace(' ','_')) for col in self.annotation_table._field_df.columns] region_tooltips = [] region_formatters = {} - for direction, kdim, dtype in zip(['x','y'], self.kdim_dtypes.keys(), self.kdim_dtypes.values()): + for direction, kdim in zip(['x','y'], self.kdim_dtypes.keys()): if self.kdim_dtypes[kdim] is np.datetime64: region_tooltips.append((f'start {kdim}', f'@{direction}0{{%F}}')) region_tooltips.append((f'end {kdim}', f'@{direction}1{{%F}}')) diff --git a/holonote/annotate/connector.py b/holonote/annotate/connector.py index 3a82f39..3e50cfc 100644 --- a/holonote/annotate/connector.py +++ b/holonote/annotate/connector.py @@ -1,19 +1,18 @@ -import os -import uuid -import sqlite3 +from __future__ import annotations + import datetime as dt +import sqlite3 +import uuid -import param -import pandas as pd import numpy as np +import pandas as pd +import param try: import sqlalchemy -except: +except ModuleNotFoundError: sqlalchemy = None -from .table import AnnotationTable - class PrimaryKey(param.Parameterized): """ @@ -21,12 +20,12 @@ class PrimaryKey(param.Parameterized): HoloViews. The generated key is used to reference annotations until they are - comitted, at which point they may 1) be inserted in the database as + committed, at which point they may 1) be inserted in the database as the primary key value (policy='insert') 2) are checked against the primary key value chosen by the database which is expected to match in most cases. - In real situations where the key is chosen by the databse, the key + In real situations where the key is chosen by the database, the key generated will *not* always match the actual key assigned. The policy parameter decides the resulting behavior in these cases. """ @@ -44,7 +43,7 @@ class PrimaryKey(param.Parameterized): def __call__(self, connector, key_list=None): """ The key list is the current list of index values that are - outstanding (i.e. have not been comitted). + outstanding (i.e. have not been committed). """ raise NotImplementedError @@ -133,7 +132,6 @@ class WidgetKey(PrimaryKey): Placeholder for a concept where the user can insert a primary key value via a widget. """ - pass @@ -186,7 +184,7 @@ def field_value_to_type(cls, value): elif isinstance(value, param.Parameter) and value.default is not None: return type(value.default) else: - raise Exception(f'Connector cannot handle type {str(type(value))}') + raise Exception(f'Connector cannot handle type {type(value)!s}') @classmethod def schema_from_field_values(cls, fields): @@ -231,7 +229,7 @@ def _incompatible_schema_check(self, expected_keys, columns, fields, region_type missing_region_columns = set(expected_keys) - non_field_columns if missing_region_columns: raise Exception(msg_prefix - + f'Missing {repr(region_type)} region columns {missing_region_columns}. ' + + f'Missing {region_type!r} region columns {missing_region_columns}. ' + msg_suffix) @@ -315,7 +313,7 @@ def get_tables(self): def create_table(self, column_schema=None): column_schema = column_schema if column_schema else self.column_schema - column_spec = ',\n'.join(['{name} {spec}'.format(name=name, spec=spec) + column_spec = ',\n'.join([f'{name} {spec}' for name, spec in column_schema.items()]) create_table_sql = f'CREATE TABLE IF NOT EXISTS {self.table_name} (' + column_spec + ');' self.cursor.execute(create_table_sql) @@ -340,7 +338,7 @@ def add_row(self, **fields): columns = columns[1:] placeholders = ', '.join(['?'] * len(field_values)) - self.cursor.execute(f"INSERT INTO {self.table_name} {str(columns)} VALUES({placeholders});", field_values) + self.cursor.execute(f"INSERT INTO {self.table_name} {columns!s} VALUES({placeholders});", field_values) self.primary_key.validate(self.cursor.lastrowid, fields[self.primary_key.field_name]) self.con.commit() @@ -359,7 +357,7 @@ def update_row(self, **updates): # updates as a dictionary OR remove posarg? id_val = updates.pop(self.primary_key.field_name) set_updates = ', '.join('\"' + k + '\"' + " = ?" for k in updates.keys()) query = f"UPDATE {self.table_name} SET " + set_updates + f" WHERE \"{self.primary_key.field_name}\" = ?;" - self.cursor.execute(query, list(updates.values()) + [id_val]) + self.cursor.execute(query, [*updates.values(), id_val]) self.con.commit() def add_schema(self, schema): diff --git a/holonote/annotate/table.py b/holonote/annotate/table.py index dba19e7..fb7d348 100644 --- a/holonote/annotate/table.py +++ b/holonote/annotate/table.py @@ -1,9 +1,11 @@ -import param -import sys +from __future__ import annotations + import weakref import numpy as np import pandas as pd +import param + class AnnotationTable(param.Parameterized): """ @@ -53,7 +55,7 @@ def load(self, connector=None, fields_df=None, primary_key_name=None, fields=Non elif connector: self.load_annotation_table(connector, fields) elif fields_df is None: - fields_df = pd.DataFrame(columns=[primary_key_name] + fields) + fields_df = pd.DataFrame(columns=[primary_key_name, *fields]) fields_df = fields_df.set_index(primary_key_name) self._field_df = fields_df @@ -66,7 +68,7 @@ def register_annotator(self, annotator): # FIXME: Multiple region updates def update_annotation_region(self, index): - region = list(self._annotators.values())[0]._region + region = next(iter(self._annotators.values()))._region if region == {}: print('No new region selected. Skipping') return @@ -364,7 +366,7 @@ def load_annotation_table(self, conn, fields): assert all(el in ['Range', 'Point'] for el in region_types) for region_type in region_types: if len(kdim_dtypes)==1: - kdim = list(kdim_dtypes.keys())[0] + kdim = next(iter(kdim_dtypes.keys())) if region_type == 'Range': expected_keys = [f'start_{kdim}', f'end_{kdim}'] conn._incompatible_schema_check(expected_keys, list(df.columns), fields, region_type) diff --git a/holonote/editor/__init__.py b/holonote/editor/__init__.py index 5b2bf83..eb0ef08 100644 --- a/holonote/editor/__init__.py +++ b/holonote/editor/__init__.py @@ -1 +1,3 @@ -from .editors import PathEditor +from __future__ import annotations + +from .editors import PathEditor # noqa: F401 diff --git a/holonote/editor/editors.py b/holonote/editor/editors.py index c3881cf..bde3bdc 100644 --- a/holonote/editor/editors.py +++ b/holonote/editor/editors.py @@ -1,3 +1,6 @@ # Move https://github.com/holoviz/holoviews/blob/main/holoviews/annotators.py here +from __future__ import annotations + + class PathEditor: pass diff --git a/holonote/tests/test_annotation_table.py b/holonote/tests/test_annotation_table.py index a5ac429..c61e778 100644 --- a/holonote/tests/test_annotation_table.py +++ b/holonote/tests/test_annotation_table.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pandas as pd from holonote.annotate import AnnotationTable diff --git a/holonote/tests/test_annotators_advanced.py b/holonote/tests/test_annotators_advanced.py index 40b73d0..13b1787 100644 --- a/holonote/tests/test_annotators_advanced.py +++ b/holonote/tests/test_annotators_advanced.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import holoviews as hv import numpy as np import pandas as pd diff --git a/holonote/tests/test_annotators_basic.py b/holonote/tests/test_annotators_basic.py index 2338fef..8b3f180 100644 --- a/holonote/tests/test_annotators_basic.py +++ b/holonote/tests/test_annotators_basic.py @@ -1,11 +1,4 @@ -# TODO: - -# * (after refactor) annotators -> annotator, connectors -> connector [ ] - -# TESTS - -# Schema error (needs file or connect in memory??) -# .snapshot() and .revert_to_snapshot() +from __future__ import annotations import uuid @@ -13,6 +6,13 @@ import pandas as pd import pytest +# TODO: + +# * (after refactor) annotators -> annotator, connectors -> connector [ ] +# TESTS +# Schema error (needs file or connect in memory??) +# .snapshot() and .revert_to_snapshot() + class TestBasicRange1DAnnotator: def test_point_insertion_exception(self, annotator_range1d): diff --git a/holonote/tests/test_connectors.py b/holonote/tests/test_connectors.py index 33d1e2d..5047488 100644 --- a/holonote/tests/test_connectors.py +++ b/holonote/tests/test_connectors.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import numpy as np import pandas as pd import pytest diff --git a/pyproject.toml b/pyproject.toml index f373277..65f5faf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,5 +26,65 @@ dependencies = ["pytest"] scripts.run = "python -m pytest holonote/tests" matrix = [{ python = ["3.8", "3.9", "3.10"] }] +[tool.hatch.envs.fmt] +dependencies = ["pre-commit"] +scripts.run = "pre-commit run --all" +scripts.update = "pre-commit autoupdate" + [tool.pytest.ini_options] addopts = "-vv" + +[tool.ruff] +target-version = "py38" + +select = [ + "B", + "E", + "F", + "FLY", + "ICN", + "I", + "NPY", + "PIE", + "PLC", + "PLE", + "PLR", + "PLW", + "RUF", + "UP", + "W", +] + +ignore = [ + "E402", # Module level import not at top of file + "E501", # Line too long + "E701", # Multiple statements on one line + "E712", # Comparison to true should be is + "E731", # Do not assign a lambda expression, use a def + "E741", # Ambiguous variable name + "F405", # From star imports + "PLC1901", # empty string is falsey + "PLE0604", # Invalid object in `__all__`, must contain only strings + "PLE0605", # Invalid format for `__all__` + "PLR091", # Too many arguments/branches/statements + "PLR2004", # Magic value used in comparison + "PLW2901", # `for` loop variable is overwritten + "RUF012", # Mutable class attributes should use `typing.ClassVar` +] + +fix = true +unfixable = [ + "F401", # Unused imports +] + +[tool.ruff.per-file-ignores] +"__init__.py" = ["F403"] + +[tool.ruff.isort] +known-first-party = ["holonote"] +required-imports = ["from __future__ import annotations"] +force-wrap-aliases = true +combine-as-imports = true + +[tool.codespell] +write-changes = true