Skip to content

Commit

Permalink
Implement new wiz.res() API (#41)
Browse files Browse the repository at this point in the history
* Remove plum as dependency

* Implement new `wiz.res()` API (see #33)

* Fix errors in error messages

* Use consistent comment format in error messages

* Add docstrings to `wiz.res()`

* Add missing `int` type for uncertainties

* Add PrintableResult to res() method signature

* Rename `uncert` to `uncerts` (plural)
  • Loading branch information
Splines authored Sep 6, 2024
1 parent cc6bf5b commit e8d3467
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 155 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"texttt",
"TLDR",
"uncert",
"uncerts",
"usepackage"
]
}
1 change: 0 additions & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ verify_ssl = true
name = "pypi"

[packages]
plum-dispatch = "~=2.3"

[dev-packages]
pylint = "~=3.0"
Expand Down
54 changes: 2 additions & 52 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,6 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = [
"plum-dispatch ~= 2.3"
]

[project.urls]
Homepage = "https://resultwizard.github.io/ResultWizard/"
Expand Down
9 changes: 7 additions & 2 deletions src/api/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def check_if_number_string(value: str) -> None:
try:
float(value)
except ValueError as exc:
raise ValueError(error_messages.STRING_MUST_BE_NUMBER.format(value)) from exc
raise ValueError(error_messages.STRING_MUST_BE_NUMBER.format(value=value)) from exc


def parse_name(name: str) -> str:
Expand Down Expand Up @@ -204,7 +204,12 @@ def _parse_uncertainty_value(value: Union[float, int, str, Decimal]) -> Value:
"""Parses the value of an uncertainty."""

if isinstance(value, str):
check_if_number_string(value)
try:
check_if_number_string(value)
except Exception as exc:
msg = error_messages.STRING_MUST_BE_NUMBER.format(value=value)
msg += ". " + error_messages.UNIT_NOT_PASSED_AS_KEYWORD_ARGUMENT
raise ValueError(msg) from exc
return_value = parse_exact_value(value)
else:
return_value = Value(Decimal(value))
Expand Down
97 changes: 28 additions & 69 deletions src/api/res.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from decimal import Decimal
from typing import Union, List, Tuple
from plum import dispatch, overload

from api.printable_result import PrintableResult
from api import parsers
Expand All @@ -16,79 +15,35 @@
import api.config as c # pylint: disable=wrong-import-position,ungrouped-imports


@overload
# pylint: disable-next=too-many-arguments, too-many-locals
def res(
name: str,
value: Union[float, int, str, Decimal],
unit: str = "",
sigfigs: Union[int, None] = None,
decimal_places: Union[int, None] = None,
) -> PrintableResult:
return res(name, value, [], unit, sigfigs, decimal_places)


@overload
def res(
name: str,
value: Union[float, int, str, Decimal],
uncert: Union[
uncerts: Union[
float,
int,
str,
Decimal,
Tuple[Union[float, int, str, Decimal], str],
List[Union[float, int, str, Decimal, Tuple[Union[float, int, str, Decimal], str]]],
None,
] = None,
sigfigs: Union[int, None] = None,
decimal_places: Union[int, None] = None,
) -> PrintableResult:
return res(name, value, uncert, "", sigfigs, decimal_places)


@overload
def res(
name: str,
value: Union[float, int, str, Decimal],
sigfigs: Union[int, None] = None,
decimal_places: Union[int, None] = None,
) -> PrintableResult:
return res(name, value, [], "", sigfigs, decimal_places)


@overload
# pylint: disable=too-many-arguments
def res(
name: str,
value: Union[float, int, str, Decimal],
sys: Union[float, Decimal],
stat: Union[float, Decimal],
unit: str = "",
sys: Union[float, int, str, Decimal, None] = None,
stat: Union[float, int, str, Decimal, None] = None,
sigfigs: Union[int, None] = None,
decimal_places: Union[int, None] = None,
) -> PrintableResult:
return res(name, value, [(sys, "sys"), (stat, "stat")], unit, sigfigs, decimal_places)

"""
Declares your result. Give it a name and a value. You may also optionally provide
uncertainties (via `uncert` or `sys`/`stat`) and a unit in `siunitx` format.
@overload
# pylint: disable=too-many-arguments
def res(
name: str,
value: Union[float, int, str, Decimal],
uncert: Union[
float,
str,
Decimal,
Tuple[Union[float, int, str, Decimal], str],
List[Union[float, int, str, Decimal, Tuple[Union[float, int, str, Decimal], str]]],
None,
] = None,
unit: str = "",
sigfigs: Union[int, None] = None,
decimal_places: Union[int, None] = None,
) -> PrintableResult:
if uncert is None:
uncert = []
You may additionally specify the number of significant figures or decimal places
to round this specific result to, irrespective of your global configuration.
TODO: provide a link to the docs for more information and examples.
"""
# Verify user input
if sigfigs is not None and decimal_places is not None:
raise ValueError(error_messages.SIGFIGS_AND_DECIMAL_PLACES_AT_SAME_TIME)

Expand All @@ -98,10 +53,24 @@ def res(
if decimal_places is not None and isinstance(value, str):
raise ValueError(error_messages.DECIMAL_PLACES_AND_EXACT_VALUE_AT_SAME_TIME)

sys_or_stat_specified = sys is not None or stat is not None
if uncerts is not None and sys_or_stat_specified:
raise ValueError(error_messages.UNCERT_AND_SYS_STAT_AT_SAME_TIME)

if sys_or_stat_specified:
uncerts = []
if sys is not None:
uncerts.append((sys, "sys"))
if stat is not None:
uncerts.append((stat, "stat"))

if uncerts is None:
uncerts = []

# Parse user input
name_res = parsers.parse_name(name)
value_res = parsers.parse_value(value)
uncertainties_res = parsers.parse_uncertainties(uncert)
uncertainties_res = parsers.parse_uncertainties(uncerts)
unit_res = parsers.parse_unit(unit)
sigfigs_res = parsers.parse_sigfigs(sigfigs)
decimal_places_res = parsers.parse_decimal_places(decimal_places)
Expand All @@ -124,13 +93,3 @@ def res(
_export(immediate_export_path, print_completed=False)

return printable_result


# Hack for method "overloading" in Python
# see https://beartype.github.io/plum/integration.html
# This is a good writeup: https://stackoverflow.com/a/29091980/
@dispatch
def res(*args, **kwargs) -> object: # pylint: disable=unused-argument
# This method only scans for all `overload`-decorated methods
# and properly adds them as Plum methods.
pass
17 changes: 12 additions & 5 deletions src/application/error_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,15 @@
DECIMAL_PLACES_AND_EXACT_VALUE_AT_SAME_TIME = (
"You can't set decimal places and supply an exact value. Please do one or the other."
)
UNCERT_AND_SYS_STAT_AT_SAME_TIME = (
"You can't set uncertainties and systematic/statistical uncertainties at the same time. "
"Please provide either the `uncert` param or the `sys`/`stat` params."
)

# Parser error messages (generic)
STRING_MUST_BE_NUMBER = "String value must be a valid number, not {value}"
FIELD_MUST_BE_STRING = "{field} must be a string, not {type}}"
FIELD_MUST_BE_INT = "{field} must be an int, not {type}}"
FIELD_MUST_BE_STRING = "{field} must be a string, not {type}"
FIELD_MUST_BE_INT = "{field} must be an int, not {type}"
FIELD_MUST_NOT_BE_EMPTY = "{field} must not be empty"
FIELD_MUST_BE_POSITIVE = "{field} must be positive"
FIELD_MUST_BE_NON_NEGATIVE = "{field} must be non-negative"
Expand All @@ -35,23 +39,26 @@
UNCERTAINTIES_MUST_BE_TUPLES_OR = (
"Each uncertainty must be a tuple or a float/int/Decimal/str, not {type}"
)
UNIT_NOT_PASSED_AS_KEYWORD_ARGUMENT = (
"Could it be the case you provided a unit but forgot `unit=` in front of it?"
)

# Helpers:
# Helpers
PRECISION_TOO_LOW = (
"Your precision is set too low to be able to process the given value without any loss of "
"precision. Set a higher precision via: `wiz.config_init (precision=<a-high-enough-number>)`."
)
NUMBER_TO_WORD_TOO_HIGH = "For variable names, only use numbers between 0 and 999. Got {number}."

# Runtime errors:
# Runtime errors
SHORT_RESULT_IS_NONE = "Short result is None, but there should be at least two uncertainties."
INTERNAL_ROUNDER_HIERARCHY_ERROR = "Internal rounder hierarchy error. Please report this bug."
INTERNAL_MIN_EXPONENT_ERROR = "Internal min_exponent not set error. Please report this bug."
ROUND_TO_NEGATIVE_DECIMAL_PLACES = (
"Internal rounding to negative decimal places. Please report this bug."
)

# Warnings:
# Warnings
INVALID_CHARS_IGNORED = "Invalid characters in name were ignored: {chars}"
NUM_OF_DECIMAL_PLACES_TOO_LOW = (
"Warning: At least one of the specified values is out of range of the specified "
Expand Down
46 changes: 23 additions & 23 deletions tests/playground.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,37 +32,35 @@
# wiz.res("", 42.0).print()
# -> Error: "name must not be empty"

wiz.res("a911", 1.05, r"\mm\s\per\N\kg")
wiz.res("a911", 1.05, unit=r"\mm\s\per\N\kg")
# wiz.res("a911", "1.052", 0.25, r"\mm\s\per\N\kg")

wiz.res("1 b", 1.0, 0.01, r"\per\mm\cubed")
wiz.res("1 b", 1.0, 0.01, unit=r"\per\mm\cubed")

# wiz.config(decimal_places=-1, sigfigs_fallback=3)

wiz.res("c big", 1.0, (0.01, "systematic"), r"\mm")
wiz.res("d", 1.0e10, [(0.01e10, "systematic"), (0.0294999e10, "stat")], r"\mm\per\second\squared")
wiz.res("e", "1.0", r"\mm")
wiz.res("d", 1.0e10, [(0.01e10, "sysyeah"), (0.0294999e10, "statyeah")], r"\mm\per\second^2")
# wiz.res("e", "1.0", r"\mm") # -> except error message that maybe we have forgotten to put `unit=`

wiz.res("f", "1.0e1", 25e-1)
wiz.res("g", 42)
wiz.res("h", 42, 13.0, 24.0)
wiz.res("h&", 42, 13.0, 24.0)
wiz.res("i", Decimal("42.0e-30"), Decimal("0.1e-31"), r"\m")
wiz.res("i", Decimal("42.0e-30"), Decimal("0.1e-31"), Decimal("0.05e-31"), r"\m\per\s\squared")
wiz.res("j", 0.009, None, None, 2)
# wiz.res("k", 1.55, 0.0, r"\tesla") # -> uncertainty must be positive

# wiz.res("k", 3, 1, r"\tesla") # -> plum: Could not be resolved
# TODO: Find out if one can adjust the plum.resolver.NotFoundLookupError such that
# we can give better hints, e.g. "you cannot pass in value and uncertainty as integers"

# wiz.res("g", 1.0, sys=0.01, stat=0.02, unit=r"\mm").print()
# g: (1.0 ± 0.01 sys ± 0.02 stat) \mm
# TODO: Why does this not work?
# -> This fix might help: https://github.com/beartype/plum/issues/40#issuecomment-1836613508

# The following wont' work as we can't have positional arguments (here: unit)
# after keyword arguments (here: uncert)
# wiz.res("d", 1.0, uncert=[(0.01, "systematic"), (0.02, "stat")], r"\mm").print()
wiz.res("h", 42, sys=13.0, stat=24.0)
wiz.res("h&", 42, sys=13.0, stat=24.0)

wiz.res("i", Decimal("42.0e-30"), Decimal("0.1e-31"), unit=r"\m")
wiz.res(
"i",
Decimal("42.0e-30"),
sys=Decimal("0.1e-31"),
stat=Decimal("0.05e-31"),
unit=r"\m\per\s\squared",
)
wiz.res("j", 0.009, None, "", 2) # really bad, but this is valid
# wiz.res("k", 1.55, 0.0, unit=r"\tesla") # -> uncertainty must be positive
wiz.res("k", 3, 1, r"\tesla") # integers work as well, yeah
wiz.res("l", 1.0, sys=0.01, stat=0.02, unit=r"\mm").print()
wiz.res("m", 1.0, uncerts=[(0.01, "systematic"), (0.02, "stat")], unit=r"\mm").print()

# wiz.table(
# "name",
Expand All @@ -75,6 +73,8 @@
# horizontal = True,
# )

wiz.res("Tour Eiffel Height", "330.3141516", "0.5", r"\m")
wiz.res("g Another Test", 9.81, 0.78, unit=r"\m/\s^2")

#############################
# Export
Expand Down

0 comments on commit e8d3467

Please sign in to comment.