mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
279 lines
7.8 KiB
Python
279 lines
7.8 KiB
Python
"""
|
|
pint.delegates.txt_defparser.plain
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Definitions for parsing:
|
|
- Equality
|
|
- CommentDefinition
|
|
- PrefixDefinition
|
|
- UnitDefinition
|
|
- DimensionDefinition
|
|
- DerivedDimensionDefinition
|
|
- AliasDefinition
|
|
|
|
Notices that some of the checks are done within the
|
|
format agnostic parent definition class.
|
|
|
|
See each one for a slighly longer description of the
|
|
syntax.
|
|
|
|
:copyright: 2022 by Pint Authors, see AUTHORS for more details.
|
|
:license: BSD, see LICENSE for more details.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
import flexparser as fp
|
|
|
|
from ...converters import Converter
|
|
from ...facets.plain import definitions
|
|
from ...util import UnitsContainer
|
|
from ..base_defparser import ParserConfig, PintParsedStatement
|
|
from . import common
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Equality(PintParsedStatement, definitions.Equality):
|
|
"""An equality statement contains a left and right hand separated
|
|
|
|
lhs and rhs should be space stripped.
|
|
"""
|
|
|
|
@classmethod
|
|
def from_string(cls, s: str) -> fp.NullableParsedResult[Equality]:
|
|
if "=" not in s:
|
|
return None
|
|
parts = [p.strip() for p in s.split("=")]
|
|
if len(parts) != 2:
|
|
return common.DefinitionSyntaxError(
|
|
f"Exactly two terms expected, not {len(parts)} (`{s}`)"
|
|
)
|
|
return cls(*parts)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CommentDefinition(PintParsedStatement, definitions.CommentDefinition):
|
|
"""Comments start with a # character.
|
|
|
|
# This is a comment.
|
|
## This is also a comment.
|
|
|
|
Captured value does not include the leading # character and space stripped.
|
|
"""
|
|
|
|
@classmethod
|
|
def from_string(cls, s: str) -> fp.NullableParsedResult[CommentDefinition]:
|
|
if not s.startswith("#"):
|
|
return None
|
|
return cls(s[1:].strip())
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PrefixDefinition(PintParsedStatement, definitions.PrefixDefinition):
|
|
"""Definition of a prefix::
|
|
|
|
<prefix>- = <value> [= <symbol>] [= <alias>] [ = <alias> ] [...]
|
|
|
|
Example::
|
|
|
|
deca- = 1e+1 = da- = deka-
|
|
"""
|
|
|
|
@classmethod
|
|
def from_string_and_config(
|
|
cls, s: str, config: ParserConfig
|
|
) -> fp.NullableParsedResult[PrefixDefinition]:
|
|
if "=" not in s:
|
|
return None
|
|
|
|
name, value, *aliases = s.split("=")
|
|
|
|
name = name.strip()
|
|
if not name.endswith("-"):
|
|
return None
|
|
|
|
name = name.rstrip("-")
|
|
aliases = tuple(alias.strip().rstrip("-") for alias in aliases)
|
|
|
|
defined_symbol = None
|
|
if aliases:
|
|
if aliases[0] == "_":
|
|
aliases = aliases[1:]
|
|
else:
|
|
defined_symbol, *aliases = aliases
|
|
|
|
aliases = tuple(alias for alias in aliases if alias not in ("", "_"))
|
|
|
|
try:
|
|
value = config.to_number(value)
|
|
except definitions.NotNumeric as ex:
|
|
return common.DefinitionSyntaxError(
|
|
f"Prefix definition ('{name}') must contain only numbers, not {ex.value}"
|
|
)
|
|
|
|
try:
|
|
return cls(name, value, defined_symbol, aliases)
|
|
except Exception as exc:
|
|
return common.DefinitionSyntaxError(str(exc))
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class UnitDefinition(PintParsedStatement, definitions.UnitDefinition):
|
|
"""Definition of a unit::
|
|
|
|
<canonical name> = <relation to another unit or dimension> [= <symbol>] [= <alias>] [ = <alias> ] [...]
|
|
|
|
Example::
|
|
|
|
millennium = 1e3 * year = _ = millennia
|
|
|
|
Parameters
|
|
----------
|
|
reference : UnitsContainer
|
|
Reference units.
|
|
is_base : bool
|
|
Indicates if it is a base unit.
|
|
|
|
"""
|
|
|
|
@classmethod
|
|
def from_string_and_config(
|
|
cls, s: str, config: ParserConfig
|
|
) -> fp.NullableParsedResult[UnitDefinition]:
|
|
if "=" not in s:
|
|
return None
|
|
|
|
name, value, *aliases = (p.strip() for p in s.split("="))
|
|
|
|
defined_symbol = None
|
|
if aliases:
|
|
if aliases[0] == "_":
|
|
aliases = aliases[1:]
|
|
else:
|
|
defined_symbol, *aliases = aliases
|
|
|
|
aliases = tuple(alias for alias in aliases if alias not in ("", "_"))
|
|
|
|
if ";" in value:
|
|
[converter, modifiers] = value.split(";", 1)
|
|
|
|
try:
|
|
modifiers = {
|
|
key.strip(): config.to_number(value)
|
|
for key, value in (part.split(":") for part in modifiers.split(";"))
|
|
}
|
|
except definitions.NotNumeric as ex:
|
|
return common.DefinitionSyntaxError(
|
|
f"Unit definition ('{name}') must contain only numbers in modifier, not {ex.value}"
|
|
)
|
|
|
|
else:
|
|
converter = value
|
|
modifiers = {}
|
|
|
|
converter = config.to_scaled_units_container(converter)
|
|
|
|
try:
|
|
reference = UnitsContainer(converter)
|
|
# reference = converter.to_units_container()
|
|
except common.DefinitionSyntaxError as ex:
|
|
return common.DefinitionSyntaxError(f"While defining {name}: {ex}")
|
|
|
|
try:
|
|
converter = Converter.from_arguments(scale=converter.scale, **modifiers)
|
|
except Exception as ex:
|
|
return common.DefinitionSyntaxError(
|
|
f"Unable to assign a converter to the unit {ex}"
|
|
)
|
|
|
|
try:
|
|
return cls(name, defined_symbol, tuple(aliases), converter, reference)
|
|
except Exception as ex:
|
|
return common.DefinitionSyntaxError(str(ex))
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DimensionDefinition(PintParsedStatement, definitions.DimensionDefinition):
|
|
"""Definition of a root dimension::
|
|
|
|
[dimension name]
|
|
|
|
Example::
|
|
|
|
[volume]
|
|
"""
|
|
|
|
@classmethod
|
|
def from_string(cls, s: str) -> fp.NullableParsedResult[DimensionDefinition]:
|
|
s = s.strip()
|
|
|
|
if not (s.startswith("[") and "=" not in s):
|
|
return None
|
|
|
|
return cls(s)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DerivedDimensionDefinition(
|
|
PintParsedStatement, definitions.DerivedDimensionDefinition
|
|
):
|
|
"""Definition of a derived dimension::
|
|
|
|
[dimension name] = <relation to other dimensions>
|
|
|
|
Example::
|
|
|
|
[density] = [mass] / [volume]
|
|
"""
|
|
|
|
@classmethod
|
|
def from_string_and_config(
|
|
cls, s: str, config: ParserConfig
|
|
) -> fp.NullableParsedResult[DerivedDimensionDefinition]:
|
|
if not (s.startswith("[") and "=" in s):
|
|
return None
|
|
|
|
name, value, *aliases = s.split("=")
|
|
|
|
if aliases:
|
|
return common.DefinitionSyntaxError(
|
|
"Derived dimensions cannot have aliases."
|
|
)
|
|
|
|
try:
|
|
reference = config.to_dimension_container(value)
|
|
except common.DefinitionSyntaxError as exc:
|
|
return common.DefinitionSyntaxError(
|
|
f"In {name} derived dimensions must only be referenced "
|
|
f"to dimensions. {exc}"
|
|
)
|
|
|
|
try:
|
|
return cls(name.strip(), reference)
|
|
except Exception as exc:
|
|
return common.DefinitionSyntaxError(str(exc))
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AliasDefinition(PintParsedStatement, definitions.AliasDefinition):
|
|
"""Additional alias(es) for an already existing unit::
|
|
|
|
@alias <canonical name or previous alias> = <alias> [ = <alias> ] [...]
|
|
|
|
Example::
|
|
|
|
@alias meter = my_meter
|
|
"""
|
|
|
|
@classmethod
|
|
def from_string(cls, s: str) -> fp.NullableParsedResult[AliasDefinition]:
|
|
if not s.startswith("@alias "):
|
|
return None
|
|
name, *aliases = s[len("@alias ") :].split("=")
|
|
|
|
try:
|
|
return cls(name.strip(), tuple(alias.strip() for alias in aliases))
|
|
except Exception as exc:
|
|
return common.DefinitionSyntaxError(str(exc))
|