Repository navigation
Raise UnitError when passing unit=None in .to() for objects with unit, avoiding silent no-op - #3945
Conversation
|
I think I was misled by both the title and lack of a test: Your intention is that |
There was a problem hiding this comment.
Having looked closer (and corrected my earlier comment): this does not enable conversion to/from None -- that is still rejected in C++ (Unit conversion to / from None is not permitted). It only removes the silent no-op for unit=None, which is exactly what #2468 asks for as the alternative. So this enforces that decision rather than overturning it. Agreed on the intent.
One more: to(unit=None, dtype=...) previously meant "dtype only" and now raises. Minor, but it is an API break and might warrant a release note.
| class _NoUnitProvided: | ||
| """Sentinel type indicating that no unit argument was passed.""" | ||
|
|
||
|
|
||
| _no_unit_provided = _NoUnitProvided() |
There was a problem hiding this comment.
The sentinel is defined twice, here and in data_group.py, presumably because operations imports data_group and not the other way round. Please define it once, in a module both can import -- two independent sentinel objects for the same concept is a trap waiting for the first is comparison across module boundaries. Whether that single definition also needs to be publicly exported is the question in my comment on the signature below.
| var: Variable, | ||
| *, | ||
| unit: _cpp.Unit | str | None = None, | ||
| unit: _cpp.Unit | str | None | _NoUnitProvided = _no_unit_provided, |
There was a problem hiding this comment.
This puts a private type into the public signature and the rendered docs. It also hands the original problem straight to every wrapper of to: downstream code that forwards unit now cannot spell "not provided" without reaching for a private name. DataGroup.to below already has to work around it, and every ess-* wrapper will end up copying that. If we take the sentinel, I think it has to be public and documented.
There was a problem hiding this comment.
This is not a real problem. We don't define a global concept for "no argument provided" that is re-used across the project.
This is just a local type to replace None as the default argument. I think sharing that across modules is messy and confusing.
Every ess-* module will not have to copy that because it's not a public interface. to() is not meant to be called without arguments.
| if isinstance(unit, _NoUnitProvided) and dtype is None: | ||
| raise ValueError("Must provide dtype or unit or both") | ||
|
|
||
| if isinstance(unit, _NoUnitProvided): | ||
| return var.astype(dtype, copy=copy) |
There was a problem hiding this comment.
isinstance here, is not _no_unit_provided in data_group.py. Use the identity check in both -- that is the point of a sentinel instance, and it keeps the class an implementation detail.
| kwargs = {'dtype': dtype, 'copy': copy} | ||
| if unit is not _no_unit_provided: | ||
| kwargs['unit'] = unit | ||
| return self.apply(operator.methodcaller('to', **kwargs)) |
There was a problem hiding this comment.
This kwargs dance is the cost of the sentinel leaking into the signature, made visible. Fine here, but it is the pattern every caller that forwards unit will now have to reproduce -- see my comment on the signature in operations.py.
| def test_to_with_unit_none() -> None: | ||
| data = sc.array(dims=["x"], values=[1, 2, 3], dtype="int64", unit=None) | ||
|
|
||
| assert sc.identical(data.to(unit=None), data) |
There was a problem hiding this comment.
All the new tests cover None -> None, which was already a no-op before this PR (silently, for the wrong reason). The behaviour that actually changes is untested: sc.scalar(1, unit='m').to(unit=None) goes from a silent no-op to UnitError. That is the whole point of the change and the only thing that can regress -- please add it here and for DataArray/DataGroup.
There was a problem hiding this comment.
This is a misunderstanding. None -> None was not a no-op before this PR, it was a ValueError that is the issue that the PR addresses.
There was a problem hiding this comment.
The example should have been:
var.to(unit=None, dtype='int64')which used to be a silent no-op (aside from the dtype conversion). But the new behavior is that this raises, right?
There was a problem hiding this comment.
I don't see why you think there's something wrong with the example, there seems to be a misunderstanding, let's chat in person.
var.to(unit=None, dtype='int64')
if var has dtype int64 then the above used to be a no-op, but now it will raise and say you can't convert the unit to None.
So this change is breaking.
var.to(dtype='int64')
Will do the same thing before or after this change regardless of the unit of var.
There was a problem hiding this comment.
I don't see why you think there's something wrong with the example
You pointed out (correctly) "None -> None was not a no-op before this PR, it was a ValueError that is the issue that the PR addresses.", I looked at my example, "sc.scalar(1, unit='m').to(unit=None) goes from a silent no-op to UnitError." and realized that it indeed does not make sense as written there, i.e., was "wrong" for the behavior-change test that my original comment requested — the behavior change only shows in the presence of a dtype` arg, which my modified code example exercises.
if var has dtype int64 then the above used to be a no-op, but now it will raise and say you can't convert the unit to None.
Yes, that is what I am saying. Where are the tests for that?
There was a problem hiding this comment.
What is var here? I can't answer without knowing.
There was a problem hiding this comment.
var = sc.scalar(1.0, unit='m')
with pytest.raises(sc.UnitError):
sc.to_unit(var, unit=None, dtype='float32')There was a problem hiding this comment.
That test raises TypeError: to_unit() got an unexpected keyword argument 'dtype'
There was a problem hiding this comment.
Typo, I meant to, not to_unit.
There was a problem hiding this comment.
The test has been added 👍
Yes that should raise with a |
UnitError when passing unit=None in .to()
|
I'm not sure the new title is more accurate than before 🤔 We don't raise unconditionally if the unit argument is |
UnitError when passing unit=None in .to()UnitError when passing unit=None in .to() for objects with unit, avoiding silent no-op
Noneis a valid.unitvalue and we might want to do things like:But if
a.unitisNonethen.to()will assume the method was called without any argument, and it will raise.To fix that, make the default
unitargument of.to()be a sentinel so that we can clearly distinguish the case when no argument was provided.