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

Behavior of custom formatter #2085

Open
lendres opened this issue Dec 4, 2024 · 7 comments
Open

Behavior of custom formatter #2085

lendres opened this issue Dec 4, 2024 · 7 comments

Comments

@lendres
Copy link

lendres commented Dec 4, 2024

Description

I'm trying to understand how to write a custom formatter using pint 0.24. I am trying to format the units portion of a quantity. There is something about the behavior I do not understand. I wrote a test that passes every to the super class (DefaultFormatter) but the results are different than without the custom formatter. Am I missing something?

Input

import pandas as pd
import pint
from pint.delegates.formatter.plain import DefaultFormatter
import pint_pandas

ureg = pint.UnitRegistry()
ureg.formatter.default_format = "P#~"
pint_pandas.PintType.ureg = ureg

class CustomFormatter(DefaultFormatter):
    default_format = ""
    def format_unit(self, unit, uspec = "", sort_func = None, **babel_kwds) -> str:
        return super().format_unit(unit, uspec, sort_func, **babel_kwds)

acceleration = 1.0 * ureg.meter / ureg.second
series = pd.Series([3, 6])
pintSeries = pd.Series(series, dtype="pint[meter]")

print("Default formatter:", acceleration)
print("Default formatter:", pintSeries.pint.units)

ureg.formatter = CustomFormatter()
ureg.formatter._registry = ureg
ureg.formatter.default_format = "P#~"

print("\nCustom formatter:", acceleration)
print("Custom formatter:", pintSeries.pint.units)

Output

Default formatter: 1.0 m/s
Default formatter: m

Custom formatter: 1.0 m / s
Custom formatter: meter

Expected Behavior

Since the CustomFormatter just passes everything to the super class, I would expect the results to be the same as the default formatter. Am I misunderstanding how this is intended to work?

@andrewgsavage
Copy link
Collaborator

could you try your formatter on standard pint quuantitys or units, without importing pint-pandas? I think your issue may be related to setting the ureg for pint-pandas

@lendres
Copy link
Author

lendres commented Dec 20, 2024

I have removed the pint-pandas and I am seeing the same result.

Input

import pint
from pint.delegates.formatter.plain import DefaultFormatter

ureg = pint.UnitRegistry()
ureg.formatter.default_format = "P#~"

class CustomFormatter(DefaultFormatter):
    default_format = ""
    def format_unit(self, unit, uspec = "", sort_func = None, **babel_kwds) -> str:
        return super().format_unit(unit, uspec, sort_func, **babel_kwds)

acceleration = 1.0 * ureg.meter / ureg.second

print("Default formatter:", acceleration)
print("Default formatter:", acceleration.units)

ureg.formatter = CustomFormatter()
ureg.formatter._registry = ureg
ureg.formatter.default_format = "P#~"

print("\nCustom formatter:", acceleration)
print("Custom formatter:", acceleration.units)

Output

Default formatter: 1.0 m/s
Default formatter: m/s

Custom formatter: 1.0 m / s
Custom formatter: meter / second

@lendres
Copy link
Author

lendres commented Dec 20, 2024

It seems that the difference is coming from what is considered the "default" formatter. The initial formatter is .formatter.Formatter. If that is changed to .formatter.plain.DefaultFormatter then it explains the results above (spaces around the forward slash).

Input

import pint
from pint.delegates.formatter.plain import DefaultFormatter

ureg = pint.UnitRegistry()
ureg.formatter.default_format = "P#~"

acceleration = 1.0 * ureg.meter / ureg.second

print("Type:", type(ureg.formatter))
print("Default formatter:", acceleration)
print("Default formatter:", acceleration.units)

ureg.formatter = DefaultFormatter()
ureg.formatter._registry = ureg
ureg.formatter.default_format = "P#~"

print("\nType:", type(ureg.formatter))
print("Custom formatter:", acceleration)
print("Custom formatter:", acceleration.units)

Output

Type: <class 'pint.delegates.formatter.Formatter'>
Default formatter: 1.0 m/s
Default formatter: m/s

Type: <class 'pint.delegates.formatter.plain.DefaultFormatter'>
Custom formatter: 1.0 m / s
Custom formatter: meter / second

@lendres
Copy link
Author

lendres commented Dec 20, 2024

It seems like pint.delegates.formatter.plain.DefaultFormatter is not respecting the "P" option in the ureg.formatter.default_format string.

@andrewgsavage
Copy link
Collaborator

So if you use pint.delegates.formatter.Formatter do you get the behaviour you're expecting?

@lendres
Copy link
Author

lendres commented Dec 20, 2024

I checked that. Deriving directly from pint.delegates.formatter.Formatter seems to throw an error. Was it intended to allow deriving from this class?

Input

import pint
from pint.delegates.formatter import Formatter

ureg = pint.UnitRegistry()
ureg.formatter.default_format = "P#~"

class CustomFormatter(Formatter):
    default_format = ""
    def format_unit(self, unit, uspec = "", sort_func = None, **babel_kwds) -> str:
        return super().format_unit(unit, uspec, sort_func, **babel_kwds)

acceleration = 1.0 * ureg.meter / ureg.second

print("Default formatter:", acceleration)
print("Default formatter:", acceleration.units)


ureg.formatter = CustomFormatter()
ureg.formatter._registry = ureg
ureg.formatter.default_format = "P#~"

print("\nCustom formatter:", acceleration)
print("Custom formatter:", acceleration.units)

Output

Default formatter: 1.0 m/s
Default formatter: m/s

Custom formatter: Traceback (most recent call last):

  File ~\AppData\Local\Programs\Python\envs\ddosi312\Lib\site-packages\spyder_kernels\customize\utils.py:209 in exec_encapsulate_locals
    exec_fun(compile(code_ast, filename, "exec"), globals)

  File c:\programming\test projects\pint-formatter\pint-custom-formatter.py:22
    print("\nCustom formatter:", acceleration)

  File ~\AppData\Local\Programs\Python\envs\ddosi312\Lib\site-packages\pint\facets\plain\quantity.py:270 in __str__
    return self._REGISTRY.formatter.format_quantity(self)

  File ~\AppData\Local\Programs\Python\envs\ddosi312\Lib\site-packages\pint\delegates\formatter\full.py:169 in format_quantity
    return self.get_formatter(spec).format_quantity(

  File ~\AppData\Local\Programs\Python\envs\ddosi312\Lib\site-packages\pint\delegates\formatter\plain.py:354 in format_quantity
    qspec, registry.formatter.default_format, registry.separate_format_defaults

AttributeError: 'NoneType' object has no attribute 'formatter'

@andrewgsavage
Copy link
Collaborator

I've looked into this more. pint.delegates.formatter.Formatter holds the different formatters and uses the letter in the format string, eg ureg.formatter.default_format = "P#~" to pick which formatter to use, in the case of P it uses PrettyFormatter. So shouldn't be inherited from. If you're inheriting fromDefaultFormatter you should compare against ureg.formatter.default_format = "D#~"

The behaviour of printing the units is different as DefaultFormatter.format_unit isn't using the default_format, whereas DefaultFormatter.format_quantity does use it.
Adding the line uspec = uspec or self.default_format gives the same behaviour:

import pint
from pint.delegates.formatter.plain import DefaultFormatter

ureg = pint.UnitRegistry()
ureg.formatter.default_format = "D#~"

class CustomFormatter(DefaultFormatter):
    default_format = ""
    def format_unit(self, unit, uspec = "", sort_func = None, **babel_kwds) -> str:
        uspec = uspec or self.default_format
        return super().format_unit(unit, uspec, sort_func, **babel_kwds)

acceleration = 1.0 * ureg.meter / ureg.second

print("Default formatter:", acceleration)
print("Default formatter:", acceleration.units)

ureg.formatter = CustomFormatter()
ureg.formatter._registry = ureg
ureg.formatter.default_format = "D#~"

print("\nCustom formatter:", acceleration)
print("Custom formatter:", acceleration.units)
Default formatter: 1.0 m / s
Default formatter: m / s

Custom formatter: 1.0 m / s
Custom formatter: m / s

I think it would be worth adding uspec = uspec or self.default_format to DefaultFormatter, PrettyFormatter etc so the behaviour is consistent when users try to inherit them.
Same goes for magnitude and measurement specs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants