Removed units functionality and Pint dependency

Closes #2400, unblocks #2320
This commit is contained in:
Simon Willison 2024-08-20 19:03:33 -07:00 committed by GitHub
commit 39dfc7d7d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 14 additions and 182 deletions

View file

@ -37,7 +37,6 @@ from jinja2.exceptions import TemplateNotFound
from .events import Event from .events import Event
from .views import Context from .views import Context
from .views.base import ureg
from .views.database import database_download, DatabaseView, TableCreateView, QueryView from .views.database import database_download, DatabaseView, TableCreateView, QueryView
from .views.index import IndexView from .views.index import IndexView
from .views.special import ( from .views.special import (

View file

@ -368,12 +368,8 @@ class Filters:
) )
_filters_by_key = {f.key: f for f in _filters} _filters_by_key = {f.key: f for f in _filters}
def __init__(self, pairs, units=None, ureg=None): def __init__(self, pairs):
if units is None:
units = {}
self.pairs = pairs self.pairs = pairs
self.units = units
self.ureg = ureg
def lookups(self): def lookups(self):
"""Yields (lookup, display, no_argument) pairs""" """Yields (lookup, display, no_argument) pairs"""
@ -413,20 +409,6 @@ class Filters:
def has_selections(self): def has_selections(self):
return bool(self.pairs) return bool(self.pairs)
def convert_unit(self, column, value):
"""If the user has provided a unit in the query, convert it into the column unit, if present."""
if column not in self.units:
return value
# Try to interpret the value as a unit
value = self.ureg(value)
if isinstance(value, numbers.Number):
# It's just a bare number, assume it's the column unit
return value
column_unit = self.ureg(self.units[column])
return value.to(column_unit).magnitude
def build_where_clauses(self, table): def build_where_clauses(self, table):
sql_bits = [] sql_bits = []
params = {} params = {}
@ -434,9 +416,7 @@ class Filters:
for column, lookup, value in self.selections(): for column, lookup, value in self.selections():
filter = self._filters_by_key.get(lookup, None) filter = self._filters_by_key.get(lookup, None)
if filter: if filter:
sql_bit, param = filter.where_clause( sql_bit, param = filter.where_clause(table, column, value, i)
table, column, self.convert_unit(column, value), i
)
sql_bits.append(sql_bit) sql_bits.append(sql_bit)
if param is not None: if param is not None:
if not isinstance(param, list): if not isinstance(param, list):

View file

@ -1368,7 +1368,6 @@ _table_config_keys = (
"fts_table", "fts_table",
"fts_pk", "fts_pk",
"searchmode", "searchmode",
"units",
) )

View file

@ -8,8 +8,6 @@ import urllib
from markupsafe import escape from markupsafe import escape
import pint
from datasette.database import QueryInterrupted from datasette.database import QueryInterrupted
from datasette.utils.asgi import Request from datasette.utils.asgi import Request
from datasette.utils import ( from datasette.utils import (
@ -32,8 +30,6 @@ from datasette.utils.asgi import (
BadRequest, BadRequest,
) )
ureg = pint.UnitRegistry()
class DatasetteError(Exception): class DatasetteError(Exception):
def __init__( def __init__(

View file

@ -103,7 +103,6 @@ class RowView(DataView):
"columns": columns, "columns": columns,
"primary_keys": resolved.pks, "primary_keys": resolved.pks,
"primary_key_values": pk_values, "primary_key_values": pk_values,
"units": (await self.ds.table_config(database, table)).get("units", {}),
} }
if "foreign_key_tables" in (request.args.get("_extras") or "").split(","): if "foreign_key_tables" in (request.args.get("_extras") or "").split(","):

View file

@ -43,7 +43,7 @@ from datasette.utils import (
from datasette.utils.asgi import BadRequest, Forbidden, NotFound, Response from datasette.utils.asgi import BadRequest, Forbidden, NotFound, Response
from datasette.filters import Filters from datasette.filters import Filters
import sqlite_utils import sqlite_utils
from .base import BaseView, DatasetteError, ureg, _error, stream_csv from .base import BaseView, DatasetteError, _error, stream_csv
from .database import QueryView from .database import QueryView
LINK_WITH_LABEL = ( LINK_WITH_LABEL = (
@ -292,14 +292,6 @@ async def display_columns_and_rows(
), ),
) )
) )
elif column in table_config.get("units", {}) and value != "":
# Interpret units using pint
value = value * ureg(table_config["units"][column])
# Pint uses floating point which sometimes introduces errors in the compact
# representation, which we have to round off to avoid ugliness. In the vast
# majority of cases this rounding will be inconsequential. I hope.
value = round(value.to_compact(), 6)
display_value = markupsafe.Markup(f"{value:~P}".replace(" ", " "))
else: else:
display_value = str(value) display_value = str(value)
if truncate_cells and len(display_value) > truncate_cells: if truncate_cells and len(display_value) > truncate_cells:
@ -1017,7 +1009,6 @@ async def table_view_data(
nofacet = True nofacet = True
table_metadata = await datasette.table_config(database_name, table_name) table_metadata = await datasette.table_config(database_name, table_name)
units = table_metadata.get("units", {})
# Arguments that start with _ and don't contain a __ are # Arguments that start with _ and don't contain a __ are
# special - things like ?_search= - and should not be # special - things like ?_search= - and should not be
@ -1029,7 +1020,7 @@ async def table_view_data(
filter_args.append((key, v)) filter_args.append((key, v))
# Build where clauses from query string arguments # Build where clauses from query string arguments
filters = Filters(sorted(filter_args), units, ureg) filters = Filters(sorted(filter_args))
where_clauses, params = filters.build_where_clauses(table_name) where_clauses, params = filters.build_where_clauses(table_name)
# Execute filters_from_request plugin hooks - including the default # Execute filters_from_request plugin hooks - including the default

View file

@ -205,100 +205,6 @@ These will be displayed at the top of the table page, and will also show in the
You can see an example of how these look at `latest.datasette.io/fixtures/roadside_attractions <https://latest.datasette.io/fixtures/roadside_attractions>`__. You can see an example of how these look at `latest.datasette.io/fixtures/roadside_attractions <https://latest.datasette.io/fixtures/roadside_attractions>`__.
Specifying units for a column
-----------------------------
Datasette supports attaching units to a column, which will be used when displaying
values from that column. SI prefixes will be used where appropriate.
Column units are configured in the metadata like so:
.. [[[cog
metadata_example(cog, {
"databases": {
"database1": {
"tables": {
"example_table": {
"units": {
"column1": "metres",
"column2": "Hz"
}
}
}
}
}
})
.. ]]]
.. tab:: metadata.yaml
.. code-block:: yaml
databases:
database1:
tables:
example_table:
units:
column1: metres
column2: Hz
.. tab:: metadata.json
.. code-block:: json
{
"databases": {
"database1": {
"tables": {
"example_table": {
"units": {
"column1": "metres",
"column2": "Hz"
}
}
}
}
}
}
.. [[[end]]]
Units are interpreted using Pint_, and you can see the full list of available units in
Pint's `unit registry`_. You can also add `custom units`_ to the metadata, which will be
registered with Pint:
.. [[[cog
metadata_example(cog, {
"custom_units": [
"decibel = [] = dB"
]
})
.. ]]]
.. tab:: metadata.yaml
.. code-block:: yaml
custom_units:
- decibel = [] = dB
.. tab:: metadata.json
.. code-block:: json
{
"custom_units": [
"decibel = [] = dB"
]
}
.. [[[end]]]
.. _Pint: https://pint.readthedocs.io/
.. _unit registry: https://github.com/hgrecco/pint/blob/master/pint/default_en.txt
.. _custom units: http://pint.readthedocs.io/en/latest/defining.html
.. _metadata_default_sort: .. _metadata_default_sort:
Setting a default sort order Setting a default sort order

View file

@ -50,7 +50,6 @@ setup(
"httpx>=0.20", "httpx>=0.20",
'importlib_resources>=1.3.1; python_version < "3.9"', 'importlib_resources>=1.3.1; python_version < "3.9"',
'importlib_metadata>=4.6; python_version < "3.10"', 'importlib_metadata>=4.6; python_version < "3.10"',
"pint>=0.9",
"pluggy>=1.0", "pluggy>=1.0",
"uvicorn>=0.11", "uvicorn>=0.11",
"aiofiles>=0.4", "aiofiles>=0.4",

View file

@ -379,7 +379,6 @@ METADATA = {
], ],
}, },
"no_primary_key": {"sortable_columns": [], "hidden": True}, "no_primary_key": {"sortable_columns": [], "hidden": True},
"units": {"units": {"distance": "m", "frequency": "Hz"}},
"primary_key_multiple_columns_explicit_label": { "primary_key_multiple_columns_explicit_label": {
"label_column": "content2" "label_column": "content2"
}, },
@ -507,16 +506,6 @@ CREATE TABLE "custom_foreign_key_label" (
FOREIGN KEY ("foreign_key_with_custom_label") REFERENCES [primary_key_multiple_columns_explicit_label](id) FOREIGN KEY ("foreign_key_with_custom_label") REFERENCES [primary_key_multiple_columns_explicit_label](id)
); );
CREATE TABLE units (
pk integer primary key,
distance int,
frequency int
);
INSERT INTO units VALUES (1, 1, 100);
INSERT INTO units VALUES (2, 5000, 2500);
INSERT INTO units VALUES (3, 100000, 75000);
CREATE TABLE tags ( CREATE TABLE tags (
tag TEXT PRIMARY KEY tag TEXT PRIMARY KEY
); );

View file

@ -5,18 +5,21 @@ from datasette import tracer
from datasette.utils import path_with_added_args from datasette.utils import path_with_added_args
from datasette.utils.asgi import asgi_send_json, Response from datasette.utils.asgi import asgi_send_json, Response
import base64 import base64
import pint
import json import json
import urllib import urllib.parse
ureg = pint.UnitRegistry()
@hookimpl @hookimpl
def prepare_connection(conn, database, datasette): def prepare_connection(conn, database, datasette):
def convert_units(amount, from_, to_): def convert_units(amount, from_, to_):
"""select convert_units(100, 'm', 'ft');""" """select convert_units(100, 'm', 'ft');"""
return (amount * ureg(from_)).to(to_).to_tuple()[0] # Convert meters to feet
if from_ == "m" and to_ == "ft":
return amount * 3.28084
# Convert feet to meters
if from_ == "ft" and to_ == "m":
return amount / 3.28084
assert False, "Unsupported conversion"
conn.create_function("convert_units", 3, convert_units) conn.create_function("convert_units", 3, convert_units)

View file

@ -528,16 +528,6 @@ async def test_database_page(ds_client):
}, },
"private": False, "private": False,
}, },
{
"name": "units",
"columns": ["pk", "distance", "frequency"],
"primary_keys": ["pk"],
"count": 3,
"hidden": False,
"fts_table": None,
"foreign_keys": {"incoming": [], "outgoing": []},
"private": False,
},
{ {
"name": "no_primary_key", "name": "no_primary_key",
"columns": ["content", "a", "b", "c"], "columns": ["content", "a", "b", "c"],
@ -1133,7 +1123,6 @@ async def test_config_json(config, expected):
], ],
}, },
"no_primary_key": {"sortable_columns": [], "hidden": True}, "no_primary_key": {"sortable_columns": [], "hidden": True},
"units": {"units": {"distance": "m", "frequency": "Hz"}},
"primary_key_multiple_columns_explicit_label": { "primary_key_multiple_columns_explicit_label": {
"label_column": "content2" "label_column": "content2"
}, },
@ -1168,7 +1157,6 @@ async def test_config_json(config, expected):
"text", "text",
] ]
}, },
"units": {"units": {"distance": "m", "frequency": "Hz"}},
# These one get redacted: # These one get redacted:
"no_primary_key": "***", "no_primary_key": "***",
"primary_key_multiple_columns_explicit_label": "***", "primary_key_multiple_columns_explicit_label": "***",

View file

@ -422,7 +422,6 @@ async def test_table_names(db):
"table/with/slashes.csv", "table/with/slashes.csv",
"complex_foreign_keys", "complex_foreign_keys",
"custom_foreign_key_label", "custom_foreign_key_label",
"units",
"tags", "tags",
"searchable", "searchable",
"searchable_tags", "searchable_tags",

View file

@ -424,8 +424,8 @@ def view_names_client(tmp_path_factory):
( (
("/", "index"), ("/", "index"),
("/fixtures", "database"), ("/fixtures", "database"),
("/fixtures/units", "table"), ("/fixtures/facetable", "table"),
("/fixtures/units/1", "row"), ("/fixtures/facetable/1", "row"),
("/-/versions", "json_data"), ("/-/versions", "json_data"),
("/fixtures/-/query?sql=select+1", "database"), ("/fixtures/-/query?sql=select+1", "database"),
), ),

View file

@ -720,22 +720,6 @@ async def test_view(ds_client):
] ]
@pytest.mark.xfail
@pytest.mark.asyncio
async def test_unit_filters(ds_client):
response = await ds_client.get(
"/fixtures/units.json?_shape=arrays&distance__lt=75km&frequency__gt=1kHz"
)
assert response.status_code == 200
data = response.json()
assert data["units"]["distance"] == "m"
assert data["units"]["frequency"] == "Hz"
assert len(data["rows"]) == 1
assert data["rows"][0][0] == 2
def test_page_size_matching_max_returned_rows( def test_page_size_matching_max_returned_rows(
app_client_returned_rows_matches_page_size, app_client_returned_rows_matches_page_size,
): ):