Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement new wiz.res() API #41

Merged
merged 9 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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